Просмотр исходного кода

Merge pull request #7453 from arafubeatbox/feat/114424-116536-track-guest-user-questionnaire-answer-status-with-local-storage

Feat/114424 116536 track guest user questionnaire answer status with local storage
Ryoji Shimizu 3 лет назад
Родитель
Сommit
5190c4edc9

+ 79 - 0
packages/app/src/client/services/guest-questionnaire-answer-status.ts

@@ -0,0 +1,79 @@
+// A service to manage questionnaire answer statuses for guest user.
+// Saves statuses in localStorage.
+
+import { StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
+
+interface GuestQuestionnaireAnswerStatus {
+  status: StatusType
+  updatedDate: string
+}
+
+interface GuestQuestionnaireAnswerStatusStorage {
+  [key: string]: GuestQuestionnaireAnswerStatus
+}
+
+const storageKey = 'guestQuestionnaireAnswerStatuses';
+const DAYS_UNTIL_EXPIRATION = 30;
+
+/**
+ * Get all answer statuses stored in localStorage as GuestQuestionnaireAnswerStatusStorage,
+ * and update outdated information.
+ */
+const getStorage = (): GuestQuestionnaireAnswerStatusStorage | null => {
+  if (typeof window === 'undefined') { return null }
+
+  const currentStorage = localStorage.getItem(storageKey);
+
+  if (currentStorage == null) { return null }
+
+  const storageJson: GuestQuestionnaireAnswerStatusStorage = JSON.parse(currentStorage);
+  // delete status if outdated to prevent localStorage overflow
+  // change skipped to not_answered if different date than when skipped
+  Object.keys(storageJson).forEach((key) => {
+    const answerStatus = storageJson[key];
+    const updatedDate = new Date(answerStatus.updatedDate);
+    const expirationDate = new Date(updatedDate.setDate(updatedDate.getDate() + DAYS_UNTIL_EXPIRATION));
+    if (expirationDate < new Date()) {
+      delete storageJson[key];
+    }
+    else if (answerStatus.status === StatusType.skipped
+          && new Date().toDateString() !== answerStatus.updatedDate) {
+      storageJson[key] = {
+        status: StatusType.not_answered,
+        updatedDate: new Date().toDateString(),
+      };
+    }
+  });
+
+  return storageJson;
+};
+
+/**
+ * Set answer status for questionnaire order in GuestQuestionnaireAnswerStatusStorage,
+ * and save it in localStorage.
+ */
+const setStatus = (questionnaireOrderId: string, status: StatusType): void => {
+  if (typeof window === 'undefined') { return }
+
+  const guestQuestionnaireAnswerStatus: GuestQuestionnaireAnswerStatus = {
+    status,
+    updatedDate: new Date().toDateString(),
+  };
+
+  const storage = getStorage();
+
+  if (storage != null) {
+    storage[questionnaireOrderId] = guestQuestionnaireAnswerStatus;
+    localStorage.setItem(storageKey, JSON.stringify(storage));
+    return;
+  }
+
+  const initialStorage: GuestQuestionnaireAnswerStatusStorage = { [questionnaireOrderId]: guestQuestionnaireAnswerStatus };
+  localStorage.setItem(storageKey, JSON.stringify(initialStorage));
+
+};
+
+export const GuestQuestionnaireAnswerStatusService = {
+  setStatus,
+  getStorage,
+};

+ 22 - 11
packages/app/src/components/Questionnaire/QuestionnaireModal.tsx

@@ -3,9 +3,11 @@ import { useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import { Modal, ModalBody } from 'reactstrap';
 
+import { GuestQuestionnaireAnswerStatusService } from '~/client/services/guest-questionnaire-answer-status';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { IAnswer } from '~/interfaces/questionnaire/answer';
+import { StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
 import { IQuestionnaireOrderHasId } from '~/interfaces/questionnaire/questionnaire-order';
 import { useCurrentUser } from '~/stores/context';
 import { useQuestionnaireModal } from '~/stores/modal';
@@ -36,6 +38,9 @@ const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JS
         questionnaireOrderId: questionnaireOrder._id,
         answers,
       });
+      if (currentUser == null) {
+        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.answered);
+      }
       toastSuccess(
         <>
           <div className="font-weight-bold">{t('questionnaire.thank_you_for_answering')}</div>
@@ -51,7 +56,7 @@ const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JS
       logger.error(e);
       toastError(t('questionnaire.failed_to_send'));
     }
-  }, [questionnaireOrder._id, t]);
+  }, [questionnaireOrder._id, t, currentUser]);
 
   const submitHandler = useCallback(async(event) => {
     event.preventDefault();
@@ -63,8 +68,8 @@ const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JS
 
     sendAnswer(answers);
 
-    const shouldCloseToastor = true;
-    closeQuestionnaireModal(shouldCloseToastor);
+    const shouldCloseToast = true;
+    closeQuestionnaireModal(shouldCloseToast);
   }, [closeQuestionnaireModal, questionnaireOrder.questions, sendAnswer]);
 
   const denyBtnClickHandler = useCallback(async() => {
@@ -72,30 +77,36 @@ const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JS
       apiv3Put('/questionnaire/deny', {
         questionnaireOrderId: questionnaireOrder._id,
       });
+      if (currentUser == null) {
+        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
+      }
       toastSuccess(t('questionnaire.denied'));
     }
     catch (e) {
       logger.error(e);
     }
-    const shouldCloseToastor = true;
-    closeQuestionnaireModal(shouldCloseToastor);
-  }, [closeQuestionnaireModal, questionnaireOrder._id, t]);
+    const shouldCloseToast = true;
+    closeQuestionnaireModal(shouldCloseToast);
+  }, [closeQuestionnaireModal, questionnaireOrder._id, t, currentUser]);
 
   // No showing toasts since not important
-  const closeBtnClickHandler = useCallback(async(shouldCloseToastor: boolean) => {
-    closeQuestionnaireModal(shouldCloseToastor);
+  const closeBtnClickHandler = useCallback(async(shouldCloseToast: boolean) => {
+    closeQuestionnaireModal(shouldCloseToast);
 
     try {
       await apiv3Put('/questionnaire/skip', {
         questionnaireOrderId: questionnaireOrder._id,
       });
+      if (currentUser == null) {
+        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
+      }
     }
     catch (e) {
       logger.error(e);
     }
-  }, [closeQuestionnaireModal, questionnaireOrder._id]);
+  }, [closeQuestionnaireModal, questionnaireOrder._id, currentUser]);
 
-  const closeBtnClickHandlerClosingToastor = useCallback(async() => {
+  const closeBtnClickHandlerClosingToast = useCallback(async() => {
     closeBtnClickHandler(true);
   }, [closeBtnClickHandler]);
 
@@ -104,7 +115,7 @@ const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JS
   return (<Modal
     size="lg"
     isOpen={isOpened}
-    toggle={closeBtnClickHandlerClosingToastor}
+    toggle={closeBtnClickHandlerClosingToast}
     centered
   >
     <form onSubmit={submitHandler}>

+ 20 - 1
packages/app/src/components/Questionnaire/QuestionnaireModalManager.tsx

@@ -1,3 +1,9 @@
+import { useCallback } from 'react';
+
+import { GuestQuestionnaireAnswerStatusService } from '~/client/services/guest-questionnaire-answer-status';
+import { StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
+import { IQuestionnaireOrderHasId } from '~/interfaces/questionnaire/questionnaire-order';
+import { useCurrentUser } from '~/stores/context';
 import { useSWRxQuestionnaireOrders } from '~/stores/questionnaire';
 
 import QuestionnaireModal from './QuestionnaireModal';
@@ -7,6 +13,19 @@ import styles from './QuestionnaireModalManager.module.scss';
 
 const QuestionnaireModalManager = ():JSX.Element => {
   const { data: questionnaireOrders } = useSWRxQuestionnaireOrders();
+  const { data: currentUser } = useCurrentUser();
+
+  const questionnaireOrdersToShow = useCallback((questionnaireOrders: IQuestionnaireOrderHasId[] | undefined) => {
+    const guestQuestionnaireAnswerStorage = GuestQuestionnaireAnswerStatusService.getStorage();
+    if (currentUser != null || guestQuestionnaireAnswerStorage == null) {
+      return questionnaireOrders;
+    }
+
+    return questionnaireOrders?.filter((questionnaireOrder) => {
+      const localAnswerStatus = guestQuestionnaireAnswerStorage[questionnaireOrder._id];
+      return localAnswerStatus == null || localAnswerStatus.status === StatusType.not_answered;
+    });
+  }, [currentUser]);
 
   return <>
     {questionnaireOrders?.map((questionnaireOrder) => {
@@ -15,7 +34,7 @@ const QuestionnaireModalManager = ():JSX.Element => {
         key={questionnaireOrder._id} />;
     })}
     <div className={styles['grw-questionnaire-toasts']}>
-      {questionnaireOrders?.map((questionnaireOrder) => {
+      {questionnaireOrdersToShow(questionnaireOrders)?.map((questionnaireOrder) => {
         return <QuestionnaireToast questionnaireOrder={questionnaireOrder} key={questionnaireOrder._id}/>;
       })}
     </div>

+ 10 - 2
packages/app/src/components/Questionnaire/QuestionnaireToast.tsx

@@ -2,8 +2,10 @@ import { useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
+import { GuestQuestionnaireAnswerStatusService } from '~/client/services/guest-questionnaire-answer-status';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess } from '~/client/util/toastr';
+import { StatusType } from '~/interfaces/questionnaire/questionnaire-answer-status';
 import { IQuestionnaireOrderHasId } from '~/interfaces/questionnaire/questionnaire-order';
 import { useCurrentUser } from '~/stores/context';
 import { useQuestionnaireModal } from '~/stores/modal';
@@ -36,12 +38,15 @@ const QuestionnaireToast = ({ questionnaireOrder }: QuestionnaireToastProps): JS
       await apiv3Put('/questionnaire/deny', {
         questionnaireOrderId: questionnaireOrder._id,
       });
+      if (currentUser == null) {
+        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
+      }
       toastSuccess(t('questionnaire.denied'));
     }
     catch (e) {
       logger.error(e);
     }
-  }, [questionnaireOrder._id, t]);
+  }, [questionnaireOrder._id, t, currentUser]);
 
   // No showing toasts since not important
   const closeBtnClickHandler = useCallback(async() => {
@@ -51,11 +56,14 @@ const QuestionnaireToast = ({ questionnaireOrder }: QuestionnaireToastProps): JS
       await apiv3Put('/questionnaire/skip', {
         questionnaireOrderId: questionnaireOrder._id,
       });
+      if (currentUser == null) {
+        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
+      }
     }
     catch (e) {
       logger.error(e);
     }
-  }, [questionnaireOrder._id]);
+  }, [questionnaireOrder._id, currentUser]);
 
   const questionnaireOrderShortTitle = lang === 'en_US' ? questionnaireOrder.shortTitle.en_US : questionnaireOrder.shortTitle.ja_JP;
 

+ 9 - 9
packages/app/src/stores/modal.tsx

@@ -589,12 +589,12 @@ export const useConflictDiffModal = (): SWRResponse<ConflictDiffModalStatus, Err
 */
 type QuestionnaireModalStatuses = {
   openedQuestionnaireId: string | null,
-  closeToastor?: () => void | Promise<void>,
+  closeToast?: () => void | Promise<void>,
 }
 
 type QuestionnaireModalStatusUtils = {
-  open(string: string, closeToastor: () => void | Promise<void>): Promise<QuestionnaireModalStatuses | undefined>
-  close(shouldCloseToastor?: boolean): Promise<QuestionnaireModalStatuses | undefined>
+  open(string: string, closeToast: () => void | Promise<void>): Promise<QuestionnaireModalStatuses | undefined>
+  close(shouldCloseToast?: boolean): Promise<QuestionnaireModalStatuses | undefined>
 }
 
 export const useQuestionnaireModal = (status?: QuestionnaireModalStatuses): SWRResponse<QuestionnaireModalStatuses, Error> & QuestionnaireModalStatusUtils => {
@@ -603,14 +603,14 @@ export const useQuestionnaireModal = (status?: QuestionnaireModalStatuses): SWRR
 
   return {
     ...swrResponse,
-    open: (questionnaireOrderId: string, closeToastor: () => void | Promise<void>) => swrResponse.mutate({
+    open: (questionnaireOrderId: string, closeToast: () => void | Promise<void>) => swrResponse.mutate({
       openedQuestionnaireId: questionnaireOrderId,
-      closeToastor,
+      closeToast,
     }),
-    close: (shouldCloseToastor?: boolean) => {
-      if (shouldCloseToastor) {
-        swrResponse.data?.closeToastor?.();
-        if (swrResponse.data?.closeToastor === undefined) logger.debug('Tried to run `swrResponse.data?.closeToastor` but it was `undefined`');
+    close: (shouldCloseToast?: boolean) => {
+      if (shouldCloseToast) {
+        swrResponse.data?.closeToast?.();
+        if (swrResponse.data?.closeToast === undefined) logger.debug('Tried to run `swrResponse.data?.closeToast` but it was `undefined`');
       }
 
       return swrResponse.mutate({ openedQuestionnaireId: null });