diff --git a/cypress/integration/dashboard.spec.js b/cypress/integration/dashboard.spec.js new file mode 100644 index 00000000..5acbfb8a --- /dev/null +++ b/cypress/integration/dashboard.spec.js @@ -0,0 +1,346 @@ +import loginVerifiedUser from '../util/loginVerifiedUser'; +import createAskForHelpPosting from '../util/createAskForHelpPosting'; +import createResponseForPosting from '../util/createResponseForPosting'; +import { removeAskForHelpEntryWithoutResponses, removeSolvedAskForHelpEntry } from '../util/removeExistingAskForHelpEntries'; + +const dummyUserMail = 'verified@example.com'; +const dummyUserPw = 'test1234'; + +// test are currently flaky, see https://github.com/kenodressel/quarantine-hero/issues/216 +context.skip('Dashboard', () => { + + beforeEach(() => { + indexedDB.deleteDatabase('firebaseLocalStorageDb'); + cy.visit('localhost:3000'); + loginVerifiedUser(dummyUserMail, dummyUserPw); + }); + + describe('user has no entries', () => { + + beforeEach(() => { + cy.visit('localhost:3000/#/dashboard'); + }); + + it('should show the OPEN tab by default', () => { + cy.get('[data-cy=tabs-open]').click(); + cy.get('[data-cy=tabs-open-content]').should('be.visible'); + }); + + it('should show the SOLVED tab when selecting it', () => { + cy.get('[data-cy=tabs-solved]').click(); + cy.get('[data-cy=tabs-solved-content]').should('be.visible'); + }); + }); + + describe('user has one open entry without any responses', () => { + + it('should 1) show a reassurement popup when clicking on "LÖSCHEN", and 2) cancel when clicking the "ABBRECHEN" button and take the user back to the dashboard', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 1!`); + cy.wait(5000); // wait for data to be created server-side + + // initialize deletion attempt + cy.visit('localhost:3000/#/dashboard'); + cy.get('[data-cy=ask-for-help-entry]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with popup content + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-cancel-positive]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('be.visible'); + + // cancel deletion process + cy.get('[data-cy=btn-popup-cancel-positive]').click(); + + // status should be same as initial status + cy.get('.popup-content').should('not.be.visible'); + cy.get('[data-cy=btn-popup-cancel-positive]').should('not.be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=ask-for-help-entry]').should('be.visible'); + + // remove posting + removeAskForHelpEntryWithoutResponses(); + }); + + it('should 1) show a reassurement popup when clicking on "LÖSCHEN", 2) show a success confirmation when clicking on "ENDGÜLTIG LÖSCHEN" and 3) take the user back to the overview when clicking on "ZURÜCK ZUR ÜBERSICHT"', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 2!`); + cy.wait(5000); // wait for data to be created server-side + + // initialize deletion attempt + cy.visit('localhost:3000/#/dashboard'); + cy.wait(500); + cy.get('[data-cy=ask-for-help-entry]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with popup content + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-cancel-positive]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('be.visible'); + // delete entry + cy.get('[data-cy=btn-popup-delete-terminally]').click(); + cy.wait(500); // wait for popup to render + + // click "ZURÜCK ZUR ÜBERSICHT" in the following success confirmation + cy.get('[data-cy=btn-popup-ask-for-help]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').click(); + + // make sure popup content is not visible + cy.get('.popup-content').should('not.be.visible'); + cy.get('[data-cy=btn-popup-cancel-positive]').should('not.be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('not.be.visible'); + + // make sure entry is removed (at least UI side) + cy.get('[data-cy=btn-entry-solve]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('not.be.visible'); + cy.get('[data-cy=ask-for-help-entry]').should('not.be.visible'); + }); + }); + + describe('user has one open entry with responses', () => { + + it('should 1) show a reassurement popup when clicking on "LÖSCHEN", 2) move the entry to solved if clicking the "HELD*IN GEFUNDEN" button in the popup and 3) take the user back to the overview when clicking on "ZURÜCK ZUR ÜBERSICHT"', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 3!`); + cy.wait(5000); // wait for data to be created server-side + createResponseForPosting(`dashboard.spec.js ${new Date().toUTCString()} I can help you!`, dummyUserMail); + cy.wait(5000); // wait for data to be created server-side + + // initialize deletion attempt + cy.visit('localhost:3000/#/dashboard'); + cy.wait(500); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with "hero found" button popup content + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-anyway]').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').click(); + + // entry should be removed from "OPEN" TAB + cy.get('.popup-content').should('not.be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('not.be.visible'); + cy.get('[data-cy=btn-popup-delete-anyway]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('not.be.visible'); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('not.be.visible'); + + // cleanup: remove posting after reload + cy.reload(); // reload to fetch updated data + removeSolvedAskForHelpEntry(); + }); + + it('should 1) show a reassurement popup when clicking on "LÖSCHEN", 2) delete the entry if clicking the "DELETE ANYWAY button in the popup and 3) take the user back to the overview when clicking on "ZURÜCK ZUR ÜBERSICHT"', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 4!`); + cy.wait(5000); // wait for data to be created server-side + createResponseForPosting(`dashboard.spec.js ${new Date().toUTCString()} I can help you!`, dummyUserMail); + cy.wait(5000); // wait for data to be created server-side + + // initialize deletion attempt + cy.visit('localhost:3000/#/dashboard'); + cy.wait(500); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with popup content + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-anyway]').should('be.visible'); + + // cancel deletion process + cy.get('[data-cy=btn-popup-delete-anyway]').click(); + + // click "ZURÜCK ZUR ÜBERSICHT" in the following popup + cy.get('[data-cy=btn-popup-ask-for-help]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').click(); + cy.wait(5000); // wait for deletion + + // entry should be gone + cy.get('.popup-content').should('not.be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('not.be.visible'); + cy.get('[data-cy=btn-popup-delete-anyway]').should('not.be.visible'); + cy.get('[data-cy=ask-for-help-entry]').should('not.be.visible'); + }); + + it('should 1) show a reassurement popup when clicking on "LÖSCHEN", 2) delete the entry if clicking the "DELETE ANYWAY button in the popup and 3) take the user back to the ask-for-help page when clicking on "NEUE ANFRAGE ERSTELLEN"', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 5!`); + cy.wait(5000); // wait for data to be created server-side + createResponseForPosting(`dashboard.spec.js ${new Date().toUTCString()} I can help you!`, dummyUserMail); + cy.wait(5000); // wait for data to be created server-side + + // initialize deletion attempt + cy.visit('localhost:3000/#/dashboard'); + cy.wait(500); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with popup content + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-anyway]').should('be.visible'); + + // cancel deletion process + cy.get('[data-cy=btn-popup-delete-anyway]').click(); + + // click "ZURÜCK ZUR ÜBERSICHT" in the following popup + cy.get('[data-cy=btn-popup-ask-for-help]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').should('be.visible'); + cy.get('[data-cy=btn-popup-ask-for-help]').click(); + cy.wait(1000); // wait for deletion + + // check redirect was successful + cy.hash().should('equal', '#/ask-for-help'); + }); + + it('should 1) show a reassurement popup when clicking on "HELD*IN GEFUNDEN" and 2) mark an entry as solved when clicking the "HELD*IN GEFUNDEN" button in the popup', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 6!`); + cy.wait(5000); // wait for data to be created server-side + createResponseForPosting(`dashboard.spec.js ${new Date().toUTCString()} I can help you!`, dummyUserMail); + cy.wait(5000); // wait for data to be created server-side + + // mark as solved + cy.visit('localhost:3000/#/dashboard'); + cy.wait(500); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').click(); + // engage with button in popup + cy.get('[data-cy=btn-popup-hero-found]').should('be.visible'); + cy.get('[data-cy=btn-popup-cancel-negative]').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').click(); + cy.wait(5000); // wait for data to be updated server-side + + // entry should not be visible in "OPEN" tab anymore + cy.get('[data-cy=btn-popup-hero-found]').should('not.be.visible'); + cy.get('[data-cy=btn-popup-cancel-negative]').should('not.be.visible'); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('not.be.visible'); + + // navigate to "ABGESCHLOSSEN" TAB + cy.reload(); // reload to fetch updated data + cy.get('[data-cy=tabs-solved]').click(); + cy.get('[data-cy=tabs-solved-content]').should('be.visible'); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + + removeSolvedAskForHelpEntry(); + }); + }); + + describe('user has one solved entry with responses', () => { + + it('should 1) show a reassurement popup when clicking on "LÖSCHEN", and 2) cancel when clicking the "ABBRECHEN" button and take the user back to the overview', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 7!`); + cy.wait(5000); // wait for data to be created server-side + createResponseForPosting(`dashboard.spec.js ${new Date().toUTCString()} I can help you!`, dummyUserMail); + cy.wait(5000); // wait for data to be created server-side + + // mark as solved + cy.visit('localhost:3000/#/dashboard'); + cy.wait(500); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').click(); + + // engage with solve button in popup + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('be.visible'); + cy.get('[data-cy=btn-popup-cancel-negative]').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').click(); + cy.wait(5000); // wait for data to be updated server-side + + // navigate to "ABGESCHLOSSEN" TAB + cy.reload(); // reload to fetch updated data + cy.get('[data-cy=tabs-solved]').click(); + cy.get('[data-cy=tabs-solved-content]').should('be.visible'); + + // get solved entry and initiate deletion + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with cancel button in popup + cy.get('[data-cy=btn-popup-cancel-positive]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('be.visible'); + cy.get('[data-cy=btn-popup-cancel-positive]').click(); + + // should be back on dashboard + cy.get('.popup-content').should('not.be.visible'); + cy.get('[data-cy=btn-popup-cancel-positive]').should('not.be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('not.be.visible'); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + + // cleanup + removeSolvedAskForHelpEntry(); + }); + + it('should 1) show a reassurement popup when clicking on "LÖSCHEN", 2) show a success confirmation when clicking on "ENDGÜLTIG LÖSCHEN" and 3) take the user back to the overview when clicking on "ZURÜCK ZUR ÜBERSICHT"', () => { + // create a new posting + createAskForHelpPosting('68159', `dashboard.spec.js ${new Date().toUTCString()} test case 8!`); + cy.wait(5000); // wait for data to be created server-side + createResponseForPosting(`dashboard.spec.js ${new Date().toUTCString()} I can help you!`, dummyUserMail); + cy.wait(5000); // wait for data to be created server-side + + // mark as solved + cy.visit('localhost:3000/#/dashboard'); + cy.wait(500); + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').click(); + + // engage with solve button in popup + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('be.visible'); + cy.get('[data-cy=btn-popup-cancel-negative]').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').click(); + cy.wait(5000); // wait for data to be updated server-side + + // navigate to "ABGESCHLOSSEN" TAB + cy.reload(); // reload to fetch updated data + cy.get('[data-cy=tabs-solved]').click(); + cy.get('[data-cy=tabs-solved-content]').should('be.visible'); + + // get entry + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with delete terminally button in popup + cy.get('[data-cy=btn-popup-cancel-positive]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').click(); + cy.wait(500); // wait for popup + + // click "ZURÜCK ZUR ÜBERSICHT" in the following popup + cy.get('[data-cy=btn-popup-ask-for-help]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').click(); + cy.wait(1000); // wait for deletion + cy.get('.popup-content').should('not.be.visible'); + }); + }); +}); diff --git a/cypress/util/createAskForHelpPosting.js b/cypress/util/createAskForHelpPosting.js new file mode 100644 index 00000000..e069b400 --- /dev/null +++ b/cypress/util/createAskForHelpPosting.js @@ -0,0 +1,8 @@ +export default function createAskForHelpPosting(postalCode, description) { + cy.visit('localhost:3000/#/ask-for-help'); + cy.get('[data-cy=location-search-input]').type(`${postalCode}{enter}`); + cy.get('[data-cy=autocomplete-suggestion]').first().click(); + cy.get('[data-cy=ask-for-help-text-input]').type(description); + cy.get('[data-cy=ask-for-help-submit]').click(); + cy.get('[data-cy=success-link-to-dashboard]').click(); +} diff --git a/cypress/util/createResponseForPosting.js b/cypress/util/createResponseForPosting.js new file mode 100644 index 00000000..08412611 --- /dev/null +++ b/cypress/util/createResponseForPosting.js @@ -0,0 +1,14 @@ +export default function createResponseForPosting(responseText, email) { + cy.visit('localhost:3000/#/dashboard'); + cy.wait(1000); // wait for data to load + cy.get('[data-cy=ask-for-help-entry]').invoke('attr', 'data-id').as('askForHelpId'); + cy.get('@askForHelpId') + .then((askForHelpId) => { + cy.visit(`localhost:3000/#/offer-help/${askForHelpId}`); + cy.get('[data-cy=offer-help-text-input]').type(`${responseText}{enter}`); + cy.get('[data-cy=mail-input]').type(`${email}`); + cy.get('[data-cy=offer-help-submit]').click(); + cy.get('[data-cy=success-offer-link]').click(); + }); + cy.wait(1000); // wait for data be updated +} diff --git a/cypress/util/loginVerifiedUser.js b/cypress/util/loginVerifiedUser.js new file mode 100644 index 00000000..f6509ea4 --- /dev/null +++ b/cypress/util/loginVerifiedUser.js @@ -0,0 +1,20 @@ +export default function loginVerifiedUser(email, password) { + cy.visit('localhost:3000'); + + // TODO: This yells ANTI-PATTERN. We should find a way to fix this + cy.wait(3000); + cy.get('body').then(($body) => { + if ($body.find('[data-cy=btn-sign-out]').length > 0) { + cy.get('[data-cy=btn-sign-out]').click(); + } + }); + + cy.visit('localhost:3000/#/signin'); + cy.get('form input[type="email"]').type(`${email}{enter}`); + cy.get('form input[type="password"]').type(`${password}{enter}`); + + // TODO: Why do we redirect to ask-for-help here! + cy.hash().should('equal', '#/ask-for-help'); + + cy.visit('localhost:3000'); +} diff --git a/cypress/util/removeExistingAskForHelpEntries.js b/cypress/util/removeExistingAskForHelpEntries.js new file mode 100644 index 00000000..020d3dec --- /dev/null +++ b/cypress/util/removeExistingAskForHelpEntries.js @@ -0,0 +1,63 @@ +export function removeAskForHelpEntryWithoutResponses() { + cy.get('[data-cy=ask-for-help-entry]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('not.be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with popup content + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-cancel-positive]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').click(); + + // click "ZURÜCK ZUR ÜBERSICHT" in the following popup + cy.get('[data-cy=btn-popup-ask-for-help]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').click(); + cy.wait(1000); // wait for deletion + cy.get('.popup-content').should('not.be.visible'); +} + +export function removeAskForHelpEntryWithResponses() { + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + + // engage with popup content + cy.get('.popup-content').should('be.visible'); + cy.get('[data-cy=btn-popup-hero-found]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-anyway]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-anyway]').click(); + + // click "ZURÜCK ZUR ÜBERSICHT" in the following popup + cy.get('[data-cy=btn-popup-ask-for-help]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').click(); + cy.wait(1000); // wait for deletion + cy.get('.popup-content').should('not.be.visible'); +} + +export function removeSolvedAskForHelpEntry() { + cy.get('[data-cy=tabs-solved]').click(); + cy.get('[data-cy=tabs-solved-content]').should('be.visible'); + + // get entry + cy.get('[data-cy=ask-for-help-entry-with-responses]').should('be.visible'); + cy.get('[data-cy=btn-entry-solve]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').should('be.visible'); + cy.get('[data-cy=btn-entry-delete]').first().click(); + // engage popup content + cy.get('[data-cy=btn-popup-cancel-positive]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').should('be.visible'); + cy.get('[data-cy=btn-popup-delete-terminally]').click({ force: true }); + cy.wait(500); // wait for popup + + // click "ZURÜCK ZUR ÜBERSICHT" in the following popup + cy.get('[data-cy=btn-popup-ask-for-help]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').should('be.visible'); + cy.get('[data-cy=btn-popup-back-to-overview]').click(); + cy.wait(1000); // wait for deletion + cy.get('.popup-content').should('not.be.visible'); + cy.wait(2000); +} diff --git a/package.json b/package.json index 50416029..3f5e756c 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,10 @@ "react-scroll-up-button": "^1.6.4", "react-share": "^4.1.0", "react-slider": "^1.0.3", - "tailwindcss": "^1.2.0", + "react-web-tabs": "^1.0.1", + "reactjs-popup": "^1.5.0", "supercluster": "^7.0.0", + "tailwindcss": "^1.2.0", "use-supercluster": "^0.2.6" }, "scripts": { diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 9f8354fa..3d689cd4 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -85,11 +85,16 @@ "needsHelp": "Hilfe benötigt", "yourRequests": "Deine Hilfegesuche", "noRequests": "Du hast noch keine Hilfegesuche eingestellt. Du kannst ein neues Gesuch", + "noResolvedRequests": "Du hast noch keine Held*innen für deine Gesuche gefunden. Du kannst ein neues Gesuch", "create": "erstellen", "yourNotifications": "Deine Benachrichtigungen", "noNotificationsSubscribed": "Du hast noch keine Benachrichtigungen aktiviert. Du kannst neue Benachrichtigungen", "here": "hier", - "register": "registrieren" + "register": "registrieren", + "tabs": { + "open": "OFFEN", + "solved": "ABGESCHLOSSEN" + } }, "privacy": { "title": "Datenschutzerklärung" @@ -269,12 +274,36 @@ "somebodyAt": "Jemand in", "needsHelp": "braucht Hilfe!", "before": "vor", - "showResponses": "Antwort ansehen", - "showResponses_plural": "{{count}} Antworten ansehen", - "hideResponses": "Antwort verbergen", - "hideResponses_plural": "{{count}} Antworten verbergen", + "message": "{{count}} Nachricht", + "message_plural": "{{count}} Nachrichten", "deleteRequestForHelp": "Löschen", - "registrationReason": "den Beitrag zu melden" + "registrationReason": "den Beitrag zu melden", + "heroFound": "Held*in gefunden", + "popup": { + "heroFound": "Ich habe eine Held*in gefunden", + "deleteAnyway": "Trotzdem löschen", + "backToOverview": "Zurück zur Übersicht", + "reassureDeletion": "Diese Anfrage wirklich löschen?", + "cancel": "Abbrechen", + "deleteTerminally": "Endgültig löschen", + "createNewRequest": "Neue Anfrage Erstellen", + "solveReassure": { + "heading": "War Dein Hilfegesuch erfolgreich?", + "firstSentence": "Lass die Community wissen ob Deine Suche erfolgreich war.", + "secondSentence": "Die Anfrage wird in das '$t(views.dashboard.tabs.solved)' Tab bewegt \nund Du bekommst keine weiteren Nachrichten von Helfenden mehr." + }, + "wasYourRequestSuccessful": { + "heading": "War Dein Hilfegesuch erfolgreich?", + "firstSentence": "Lass die Community wissen ob Deine Suche erfolgreich war.", + "secondSentence": "Anstatt Dein Hilfegesuch zu löschen markieren wir es als abgeschlossen.", + "thirdSentence": "Du kannst es natürlich jederzeit trotzdem löschen." + }, + "yourRequestWasDeleted": { + "heading": "Dein Hilfegesuch wurde gelöscht!", + "firstSentence": "Bitte beachte, dass es einen Moment dauern kann, bis der Eintrag gelöscht wird.", + "secondSentence": "Du kannst natürlich jederzeit eine neue Anfrage erstellen \nund eine Held*in um Hilfe bitten." + } + } }, "filteredList": { "list": "LISTE", diff --git a/src/assets/questionmark.svg b/src/assets/questionmark.svg new file mode 100644 index 00000000..af06b2e1 --- /dev/null +++ b/src/assets/questionmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/EntryList.jsx b/src/components/EntryList.jsx index 384eefc4..1d27b12d 100644 --- a/src/components/EntryList.jsx +++ b/src/components/EntryList.jsx @@ -13,6 +13,7 @@ import { getGeodataForString, getLatLng, } from '../services/GeoService'; +import parseDoc from '../util/parseDoc'; export default function EntryList({ pageSize = 0 }) { const { t } = useTranslation(); @@ -76,12 +77,12 @@ export default function EntryList({ pageSize = 0 }) { } }; + const appendDocuments = (documents) => { setLastEntry(documents[documents.length - 1]); - const newEntries = documents.map((doc) => { - const data = doc.data(); - return { ...(data.d || data), id: doc.id }; - }); + const newEntries = documents + .map(parseDoc) + .filter(Boolean); // filter entries that we weren't able to parse and are therefore null setEntries((e) => [...e, ...newEntries]); }; diff --git a/src/components/EntryMap.jsx b/src/components/EntryMap.jsx index 44188794..eae91b63 100644 --- a/src/components/EntryMap.jsx +++ b/src/components/EntryMap.jsx @@ -3,12 +3,12 @@ import GoogleMapReact from 'google-map-react'; import '../styles/Map.css'; import useSupercluster from 'use-supercluster'; import { useTranslation } from 'react-i18next'; -import * as Sentry from '@sentry/browser'; import fb from '../firebase'; import Entry from './entry/Entry'; import NotifyMe from './NotifyMe'; import userIsOnMobile from '../util/userIsOnMobile'; +import parseDoc from '../util/parseDoc'; const DEFAULT_ZOOM_LEVEL = userIsOnMobile() ? 5 : 6; @@ -32,15 +32,6 @@ export default function EntryMap() { const [entries, setEntries] = useState([]); - const parseDoc = (doc) => { - try { - return { ...doc.data().d, id: doc.id }; - } catch (err) { - Sentry.captureException(new Error(`Error parsing ask-for-help ${doc.id}`)); - return null; - } - }; - useEffect(() => { const fetchEntries = async () => { const queryResult = await fb.store.collection('ask-for-help').get(); diff --git a/src/components/LocationInput.jsx b/src/components/LocationInput.jsx index aa7c6afd..d6662dfd 100644 --- a/src/components/LocationInput.jsx +++ b/src/components/LocationInput.jsx @@ -186,6 +186,7 @@ function Autocomplete(props) { { @@ -209,6 +210,7 @@ function Autocomplete(props) { {suggestions.map((s, i) => ( { diff --git a/src/components/Popup.jsx b/src/components/Popup.jsx new file mode 100644 index 00000000..b13992e3 --- /dev/null +++ b/src/components/Popup.jsx @@ -0,0 +1,197 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Popup from 'reactjs-popup'; + +import DoneIcon from '@material-ui/icons/Done'; +import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; + +function getPopupContentComponent(heading, firstButtonComponent, secondButtonComponent, textBody = null) { + const popupContentClasses = 'p-4 bg-kaki font-open-sans flex flex-col justify-center items-center'; + const popupTextClasses = 'mb-3 pt-2 pb-5 min-w-full pl-5 md:pl-8 whitespace-pre-line'; + return () => ( +
+
+
{heading}
+ {textBody} +
+ {firstButtonComponent} + {secondButtonComponent} +
+ ); +} + +function getButtonForPopup(commonButtonClasses, text, onClickFunction, icon, cyIdentifier, disabled = false) { + return () => ( + + ); +} + +export default function PopupOnEntryAction(props) { + const { + responses, + attemptingToDelete, + attemptingToSolve, + deleted, + popupVisible, + setPopupVisible, + setAttemptingToDelete, + handleSolved, + showAsSolved, + handleNewAskForHelp, + cancelDelete, + handleDelete, + backToOverview, + } = props; + + const { t } = useTranslation(); + + const positiveActionButtonClasses = 'bg-secondary text-white hover:opacity-75 rounded mb-2 block min-w-90 btn-common'; + const negativeActionButtonClasses = 'text-primary min-w-90 btn-common-font-normal'; + + const strongerTogetherHashtag = #strongertogether; + const textBodyWasYourRequestSuccessful = ( + <> +

+ {t('components.entry.popup.wasYourRequestSuccessful.firstSentence')} + {strongerTogetherHashtag} +

+

{t('components.entry.popup.wasYourRequestSuccessful.secondSentence')}

+

{t('components.entry.popup.wasYourRequestSuccessful.thirdSentence')}

+ + ); + + const textBodySolveReassure = ( + <> +

+ {t('components.entry.popup.solveReassure.firstSentence')} +

+

{t('components.entry.popup.solveReassure.secondSentence')}

+ + ); + + const textBodyYourRequestWasDeleted = ( + <> +

{t('components.entry.popup.yourRequestWasDeleted.firstSentence')}

+

+ {t('components.entry.popup.yourRequestWasDeleted.secondSentence')} + {strongerTogetherHashtag} +

+ + ); + + const HeroFoundButton = getButtonForPopup( + positiveActionButtonClasses, + t('components.entry.popup.heroFound'), + handleSolved, + , + 'btn-popup-hero-found', + showAsSolved, + ); + + const NewAskForHelpButton = getButtonForPopup( + positiveActionButtonClasses, + t('components.entry.popup.createNewRequest'), + handleNewAskForHelp, + , + 'btn-popup-ask-for-help', + ); + + const CancelButtonPositiveClass = getButtonForPopup( + positiveActionButtonClasses, + t('components.entry.popup.cancel'), + cancelDelete, + null, + 'btn-popup-cancel-positive', + ); + + const CancelButtonNegativeClass = getButtonForPopup( + negativeActionButtonClasses, + t('components.entry.popup.cancel'), + cancelDelete, + null, + 'btn-popup-cancel-negative', + ); + + const DeleteAnywayButton = getButtonForPopup( + negativeActionButtonClasses, + t('components.entry.popup.deleteAnyway'), + handleDelete, + , + 'btn-popup-delete-anyway', + ); + + const DeleteTerminallyButton = getButtonForPopup( + negativeActionButtonClasses, + t('components.entry.popup.deleteTerminally'), + handleDelete, + , + 'btn-popup-delete-terminally', + ); + + const BackToOverviewButton = getButtonForPopup( + negativeActionButtonClasses, + t('components.entry.popup.backToOverview'), + backToOverview, + , + 'btn-popup-back-to-overview', + ); + + const PopupContentDeleteReassure = getPopupContentComponent( + t('components.entry.popup.reassureDeletion'), + , + , + ); + + const PopupContentSolveReassure = getPopupContentComponent( + t('components.entry.popup.solveReassure.heading'), + , + , + textBodySolveReassure, + ); + + const PopupContentSolvedHint = getPopupContentComponent( + t('components.entry.popup.wasYourRequestSuccessful.heading'), + , + , + textBodyWasYourRequestSuccessful, + ); + + const PopupContentDeleteSuccess = getPopupContentComponent( + t('components.entry.popup.yourRequestWasDeleted.heading'), + , + , + textBodyYourRequestWasDeleted, + ); + + let popupContent = <>; + if (attemptingToSolve && !showAsSolved) popupContent = ; + if (attemptingToDelete && (responses === 0 || showAsSolved)) popupContent = ; + if (attemptingToDelete && responses !== 0 && !showAsSolved) popupContent = ; + if (deleted) popupContent = ; + + return ( + { + setAttemptingToDelete(false); + setPopupVisible(false); + }} + // we cannot set this with classes because the popup library has inline style, which would overwrite the width and padding again + contentStyle={ + { + padding: '0', + width: 'auto', + maxWidth: '90%', + minWidth: '30%', + } + } + > + {popupContent} + + ); +} diff --git a/src/components/Responses.jsx b/src/components/Responses.jsx index e36de98f..1740ad40 100644 --- a/src/components/Responses.jsx +++ b/src/components/Responses.jsx @@ -1,13 +1,7 @@ import React, { useEffect, useState } from 'react'; import formatDistance from 'date-fns/formatDistance'; import { de } from 'date-fns/locale'; -import fb from '../firebase'; - -const loadResponses = async (requestForHelpId) => { - const request = fb.store.collection('ask-for-help').doc(requestForHelpId).collection('offer-help').orderBy('timestamp', 'asc'); - const querySnapshot = await request.get(); - return querySnapshot.docs.map((docSnapshot) => ({ ...docSnapshot.data(), id: docSnapshot.id })); -}; +import loadResponses from '../services/loadResponses'; const Response = ({ response }) => { const date = formatDistance(new Date(response.timestamp), Date.now(), { locale: de }); @@ -34,12 +28,12 @@ const Response = ({ response }) => { ); }; -export default function Responses({ id }) { +export default function Responses({ id, collectionName }) { const [responses, setResponses] = useState(undefined); useEffect(() => { - loadResponses(id).then(setResponses); - }, [id]); + loadResponses(id, collectionName).then(setResponses); + }, [id, collectionName]); return (
diff --git a/src/components/entry/Entry.jsx b/src/components/entry/Entry.jsx index 25b92da6..926b636b 100644 --- a/src/components/entry/Entry.jsx +++ b/src/components/entry/Entry.jsx @@ -4,16 +4,25 @@ import formatDistance from 'date-fns/formatDistance'; import { de } from 'date-fns/locale'; import { useTranslation } from 'react-i18next'; import { useAuthState } from 'react-firebase-hooks/auth'; + import DeleteOutlineIcon from '@material-ui/icons/DeleteOutline'; -import ExpandLessIcon from '@material-ui/icons/ExpandLess'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import MailOutlineIcon from '@material-ui/icons/MailOutline'; +import DoneIcon from '@material-ui/icons/Done'; + import fb from '../../firebase'; import Responses from '../Responses'; +import PopupOnEntryAction from '../Popup'; + +import { ReactComponent as QuestionMarkSvg } from '../../assets/questionmark.svg'; +import { ReactComponent as FlagRedSvg } from '../../assets/flag_red.svg'; +import { ReactComponent as FlagOrangeSvg } from '../../assets/flag_orange.svg'; +import { ReactComponent as XSymbolSvg } from '../../assets/x.svg'; import './Entry.css'; export default function Entry(props) { const { showFullText = false, + showAsSolved = false, location = '', id = '', request = '', @@ -32,13 +41,18 @@ export default function Entry(props) { const date = formatDistance(new Date(timestamp), Date.now(), { locale: de }); // @TODO get locale from i18n.language or use i18n for formatting const [responsesVisible, setResponsesVisible] = useState(false); - const [deleted, setDeleted] = useState(''); + const [deleted, setDeleted] = useState(false); + const [solved, setSolved] = useState(false); + const [attemptingToDelete, setAttemptingToDelete] = useState(false); + const [attemptingToSolve, setAttemptingToSolve] = useState(false); const [attemptingToReport, setAttemptingToReport] = useState(false); + const [popupVisible, setPopupVisible] = useState(false); const userIsLoggedIn = !!user && !!user.uid; const userLoggedInAndReportedEntryBefore = userIsLoggedIn && reportedBy.includes(user.uid); const [reported, setReported] = useState(userLoggedInAndReportedEntryBefore); const entryBelongsToCurrentUser = userIsLoggedIn && user.uid === uid; + const collectionName = showAsSolved ? 'solved-posts' : 'ask-for-help'; let textToDisplay; if (showFullText) { @@ -51,12 +65,52 @@ export default function Entry(props) { const handleDelete = async (e) => { e.preventDefault(); - const collectionName = 'ask-for-help'; const doc = await fb.store.collection(collectionName).doc(id).get(); await fb.store.collection('/deleted').doc(id).set({ collectionName, ...doc.data(), }); setDeleted(true); + setAttemptingToDelete(false); + setPopupVisible(true); // trigger the deletion confirmation popup + }; + + const handleSolved = async (e) => { + e.preventDefault(); + const askForHelpDoc = await fb.store.collection('ask-for-help').doc(id).get(); + const data = askForHelpDoc.data(); + await fb.store.collection('solved-posts').doc(id).set(data); + setSolved(true); + setAttemptingToDelete(false); + setPopupVisible(false); + }; + + const handleNewAskForHelp = async (e) => { + e.preventDefault(); + setPopupVisible(false); + return history.push('/ask-for-help'); + }; + + const initializeDelete = async (e) => { + e.preventDefault(); + setAttemptingToDelete(true); + setPopupVisible(true); + }; + + const initializeSolve = async (e) => { + e.preventDefault(); + setAttemptingToSolve(true); + setPopupVisible(true); + }; + + const cancelDelete = async (e) => { + e.preventDefault(); + setAttemptingToDelete(false); + setPopupVisible(false); + }; + + const backToOverview = async (e) => { + e.preventDefault(); + setPopupVisible(false); }; const reportEntry = async (e) => { @@ -64,8 +118,7 @@ export default function Entry(props) { // https://stackoverflow.com/a/53005834/8547462 e.preventDefault(); - const collectionName = 'reported-posts'; - const reportedPostsCollection = fb.store.collection(collectionName); + const reportedPostsCollection = fb.store.collection('reported-posts'); // redirect the user to the login page, as we can only store user ids for logged-in users if (!userIsLoggedIn) { @@ -96,8 +149,27 @@ export default function Entry(props) { numberOfResponsesText = `${responses} ${t('components.entry.repliesReceived')}`; } - if (deleted) { - return null; + const popupOnEntryAction = ( + + ); + + if (deleted || solved) { + // make popup component available to show the success hint, if the entry was previously deleted + return <>{popupOnEntryAction}; } // eslint-disable-next-line no-nested-ternary @@ -115,40 +187,35 @@ export default function Entry(props) { return <>; } - const commonButtonClasses = 'px-6 py-3 uppercase font-open-sans font-bold text-center'; - const expandIconPropsStyle = { - fontSize: '32px', - marginTop: '-4px', - marginBottom: '-4px', - verticalAlign: 'bottom', - }; + const heroFoundButtonClasses = showAsSolved + ? 'bg-secondary text-white btn-common' + : 'bg-tertiary text-secondary hover:bg-secondary hover:text-white btn-common'; return ( -
+
{responses === 0 - ?
{numberOfResponsesText}
- : ( - + ? ( +
+
{numberOfResponsesText}
+
+ ) : ( + <> + + + )} -
); @@ -160,88 +227,93 @@ export default function Entry(props) { }; const requestCard = ( - -
- - {t('components.entry.somebodyAt')} - {' '} - - {location} + <> + 0 ? '-with-responses' : ''}`} + className={`bg-white px-4 py-2 rounded w-full my-3 text-xl block entry relative ${highlightLeft && 'border-l-4 border-secondary'}`} + key={id} + ref={link} + > +
+ + {t('components.entry.somebodyAt')} + {' '} + + {location} + + {' '} + {t('components.entry.needsHelp')} - {' '} - {t('components.entry.needsHelp')} - - - {!entryBelongsToCurrentUser ? ( - - ) : null} - {attemptingToReport - ? ( -
{ e.preventDefault(); + const prevValue = attemptingToReport; setAttemptingToReport((curr) => !curr); - }} - onKeyDown={(e) => { - if (e.keyCode === 13) { - setAttemptingToReport((curr) => !curr); - } + if (!reported && !prevValue) document.body.addEventListener('click', clearReportAttempt); }} > - -
+ {reported ? : null} + {!reported && !attemptingToReport ? : null} + {!reported && attemptingToReport ? : null} + ) : null} + {attemptingToReport + ? ( +
{ + e.preventDefault(); + setAttemptingToReport((curr) => !curr); + }} + onKeyDown={(e) => { + if (e.keyCode === 13) { + setAttemptingToReport((curr) => !curr); + } + }} + > + +
+ ) : null} -
-

{textToDisplay}

-
-
{mayDeleteEntryAndSeeResponses ? '' : numberOfResponsesText}
- - {t('components.entry.before')} - {' '} - {date} - -
- {buttonBar} - +
+

{textToDisplay}

+
+
{mayDeleteEntryAndSeeResponses ? '' : numberOfResponsesText}
+ + {t('components.entry.before')} + {' '} + {date} + +
+ {buttonBar} + + {popupOnEntryAction} + ); return ( <> {requestCard} - {mayDeleteEntryAndSeeResponses - ?
+ {mayDeleteEntryAndSeeResponses && !deleted && responsesVisible + ?
: <>} ); diff --git a/src/index.jsx b/src/index.jsx index b09e7475..8be7a299 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'; // we ignore this check here. // eslint-disable-next-line import/no-unresolved import './styles/tailwind.css'; +import 'react-web-tabs/dist/react-web-tabs.css'; import './styles/index.css'; import './styles/App.css'; diff --git a/src/services/loadResponses.js b/src/services/loadResponses.js new file mode 100644 index 00000000..1e36403a --- /dev/null +++ b/src/services/loadResponses.js @@ -0,0 +1,7 @@ +import fb from '../firebase'; + +export default async function loadResponses(requestForHelpId, collectionName) { + const request = fb.store.collection(collectionName).doc(requestForHelpId).collection('offer-help').orderBy('timestamp', 'asc'); + const querySnapshot = await request.get(); + return querySnapshot.docs.map((docSnapshot) => ({ ...docSnapshot.data(), id: docSnapshot.id })); +} diff --git a/src/styles/App.css b/src/styles/App.css index 88e92ce8..e5a7d066 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -44,11 +44,6 @@ background-color: #87a544; } -.font-open-sans { - font-family: 'Open Sans', sans-serif; - letter-spacing: 0; -} - .angle-cut-background { background: #f9f7f0; position: relative; diff --git a/src/styles/index.css b/src/styles/index.css index 9c1da7e9..86c4c134 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -6,6 +6,12 @@ @tailwind utilities; + +.font-open-sans { + font-family: 'Open Sans', sans-serif; + letter-spacing: 0; +} + .btn-primary { @apply rounded p-4 bg-primary text-secondary w-full inline-block text-center font-bold; } @@ -14,6 +20,24 @@ @apply rounded p-4 bg-secondary text-secondary w-full inline-block text-center font-bold; } +.btn-common { + @apply py-2 uppercase font-open-sans font-bold text-center text-xs; +} + +.btn-common-font-normal { + @apply py-2 uppercase font-open-sans font-normal text-center text-xs; +} + +@screen md { + .btn-common, .btn-common-normal-font { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + padding-right: 1.5rem; + padding-left: 1.5rem; + font-size: .875rem; + } +} + .btn-green { background-color: #87a544; font-family: 'Open Sans Condensed', sans-serif; @@ -204,7 +228,7 @@ 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } - + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } @@ -303,4 +327,34 @@ .btn-white-shadow:hover { box-shadow: 0 0 20px 0px rgba(255, 255, 255, 0.8); -} \ No newline at end of file +} + +/* the following classes are necessary to overwrite the default border of the tab bar */ +.btn-bottom-border-black::after { + border-bottom: 3px solid #222222; +} + +.rwt__tablist:not([aria-orientation="vertical"]) .rwt__tab[aria-selected="true"]:after { + bottom: -1px; + left: 0; + width: 100%; + border-bottom: 3px solid #222222; +} + +.rwt__tablist[aria-orientation="vertical"] .rwt__tab[aria-selected="true"]:after { + right: -1px; + top: 0; + height: 100%; + border-bottom: 3px solid #222222; +} + +@media only screen and (max-width: 834px) { + .rwt__tab { + background: transparent; + border: 0; + font-family: inherit; + font-size: .875rem; + transition: background 0.3s cubic-bezier(0.22, 0.61, 0.36, 1); + padding-left: 1rem; + } +} diff --git a/src/util/parseDoc.js b/src/util/parseDoc.js new file mode 100644 index 00000000..e4354efe --- /dev/null +++ b/src/util/parseDoc.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +export default function parseDoc(doc) { + try { + return { ...doc.data().d, id: doc.id }; + } catch (err) { + Sentry.captureException(new Error(`Error parsing ask-for-help ${doc.id}`)); + return null; + } +} diff --git a/src/views/AskForHelp.jsx b/src/views/AskForHelp.jsx index 08d088cb..bd5e5a7f 100644 --- a/src/views/AskForHelp.jsx +++ b/src/views/AskForHelp.jsx @@ -107,6 +107,7 @@ export default function AskForHelp() {