@constructor-io/constructorio-ui-quizzes 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +3 -1
  2. package/dist/constructorio-ui-quizzes-bundled.js +18 -18
  3. package/lib/cjs/components/CioQuiz/actions.js +11 -1
  4. package/lib/cjs/components/CioQuiz/index.js +15 -103
  5. package/lib/cjs/components/CioQuiz/quizApiReducer.js +32 -0
  6. package/lib/cjs/components/CioQuiz/{reducer.js → quizLocalReducer.js} +2 -2
  7. package/lib/cjs/components/CoverTypeQuestion/CoverTypeQuestion.js +6 -12
  8. package/lib/cjs/components/OpenTextTypeQuestion/OpenTextTypeQuestion.js +12 -18
  9. package/lib/cjs/components/QuizQuestions/index.js +6 -3
  10. package/lib/cjs/components/ResultCard/ResultCard.js +10 -30
  11. package/lib/cjs/components/ResultContainer/ResultContainer.js +8 -31
  12. package/lib/cjs/components/ResultCtaButton/ResultCtaButton.js +7 -26
  13. package/lib/cjs/components/ResultFilters/ResultFilters.js +6 -23
  14. package/lib/cjs/components/Results/Results.js +5 -5
  15. package/lib/cjs/components/SelectTypeQuestion/SelectTypeQuestion.js +16 -23
  16. package/lib/cjs/components/ZeroResults/ZeroResults.js +2 -2
  17. package/lib/cjs/constants.js +5 -3
  18. package/lib/cjs/hooks/useCioClient.js +4 -3
  19. package/lib/cjs/hooks/useConsoleErrors.js +20 -0
  20. package/lib/cjs/hooks/useQuiz.js +48 -0
  21. package/lib/cjs/hooks/useQuizApiState.js +91 -0
  22. package/lib/cjs/hooks/useQuizEvents/index.js +36 -0
  23. package/lib/cjs/hooks/useQuizEvents/useQuizAddToCart.js +20 -0
  24. package/lib/cjs/hooks/useQuizEvents/useQuizBackClick.js +13 -0
  25. package/lib/cjs/hooks/useQuizEvents/useQuizNextClick.js +48 -0
  26. package/lib/cjs/hooks/useQuizEvents/useQuizResultClick.js +19 -0
  27. package/lib/cjs/hooks/useQuizEvents/useQuizResultsLoaded.js +22 -0
  28. package/lib/cjs/hooks/useQuizLocalState.js +20 -0
  29. package/lib/cjs/services/index.js +72 -0
  30. package/lib/cjs/utils.js +42 -21
  31. package/lib/mjs/components/CioQuiz/actions.js +10 -0
  32. package/lib/mjs/components/CioQuiz/index.js +15 -103
  33. package/lib/mjs/components/CioQuiz/quizApiReducer.js +49 -0
  34. package/lib/mjs/components/CioQuiz/{reducer.js → quizLocalReducer.js} +1 -1
  35. package/lib/mjs/components/CoverTypeQuestion/CoverTypeQuestion.js +6 -12
  36. package/lib/mjs/components/OpenTextTypeQuestion/OpenTextTypeQuestion.js +11 -17
  37. package/lib/mjs/components/QuizQuestions/index.js +5 -3
  38. package/lib/mjs/components/ResultCard/ResultCard.js +10 -29
  39. package/lib/mjs/components/ResultContainer/ResultContainer.js +8 -31
  40. package/lib/mjs/components/ResultCtaButton/ResultCtaButton.js +7 -25
  41. package/lib/mjs/components/ResultFilters/ResultFilters.js +5 -23
  42. package/lib/mjs/components/Results/Results.js +3 -3
  43. package/lib/mjs/components/SelectTypeQuestion/SelectTypeQuestion.js +13 -20
  44. package/lib/mjs/components/ZeroResults/ZeroResults.js +2 -2
  45. package/lib/mjs/constants.js +4 -2
  46. package/lib/mjs/hooks/useCioClient.js +4 -3
  47. package/lib/mjs/hooks/useConsoleErrors.js +18 -0
  48. package/lib/mjs/hooks/useQuiz.js +47 -0
  49. package/lib/mjs/hooks/useQuizApiState.js +87 -0
  50. package/lib/mjs/hooks/useQuizEvents/index.js +33 -0
  51. package/lib/mjs/hooks/useQuizEvents/useQuizAddToCart.js +18 -0
  52. package/lib/mjs/hooks/useQuizEvents/useQuizBackClick.js +11 -0
  53. package/lib/mjs/hooks/useQuizEvents/useQuizNextClick.js +45 -0
  54. package/lib/mjs/hooks/useQuizEvents/useQuizResultClick.js +17 -0
  55. package/lib/mjs/hooks/useQuizEvents/useQuizResultsLoaded.js +19 -0
  56. package/lib/mjs/hooks/useQuizLocalState.js +17 -0
  57. package/lib/mjs/services/index.js +60 -0
  58. package/lib/mjs/utils.js +39 -17
  59. package/lib/styles.css +23 -20
  60. package/lib/types/components/CioQuiz/actions.d.ts +20 -0
  61. package/lib/types/components/CioQuiz/context.d.ts +10 -14
  62. package/lib/types/components/CioQuiz/index.d.ts +1 -12
  63. package/lib/types/components/CioQuiz/quizApiReducer.d.ts +14 -0
  64. package/lib/types/components/CioQuiz/{reducer.d.ts → quizLocalReducer.d.ts} +3 -3
  65. package/lib/types/components/QuizQuestions/index.d.ts +1 -4
  66. package/lib/types/components/ResultCard/ResultCard.d.ts +2 -4
  67. package/lib/types/components/ResultContainer/ResultContainer.d.ts +1 -2
  68. package/lib/types/components/ResultCtaButton/ResultCtaButton.d.ts +2 -3
  69. package/lib/types/components/ResultFilters/ResultFilters.d.ts +2 -5
  70. package/lib/types/components/Results/Results.d.ts +1 -7
  71. package/lib/types/components/ZeroResults/ZeroResults.d.ts +1 -1
  72. package/lib/types/constants.d.ts +2 -1
  73. package/lib/types/hooks/useCioClient.d.ts +1 -1
  74. package/lib/types/hooks/useConsoleErrors.d.ts +3 -0
  75. package/lib/types/hooks/useQuiz.d.ts +3 -0
  76. package/lib/types/hooks/useQuizApiState.d.ts +10 -0
  77. package/lib/types/hooks/useQuizEvents/index.d.ts +15 -0
  78. package/lib/types/hooks/useQuizEvents/useQuizAddToCart.d.ts +5 -0
  79. package/lib/types/hooks/useQuizEvents/useQuizBackClick.d.ts +4 -0
  80. package/lib/types/hooks/useQuizEvents/useQuizNextClick.d.ts +5 -0
  81. package/lib/types/hooks/useQuizEvents/useQuizResultClick.d.ts +5 -0
  82. package/lib/types/hooks/useQuizEvents/useQuizResultsLoaded.d.ts +5 -0
  83. package/lib/types/hooks/useQuizLocalState.d.ts +6 -0
  84. package/lib/types/services/index.d.ts +8 -0
  85. package/lib/types/types.d.ts +63 -0
  86. package/lib/types/utils.d.ts +12 -7
  87. package/package.json +12 -12
@@ -6,27 +6,29 @@ import { renderImages } from '../../utils';
6
6
  import { QuestionTypes } from '../CioQuiz/actions';
7
7
  import ControlBar from '../ControlBar/ControlBar';
8
8
  function SelectTypeQuestion() {
9
- const { questionResponse, state, quizNextHandler, quizBackHandler, isFirstQuestion } = useContext(QuizContext);
9
+ const { state, nextQuestion, previousQuestion } = useContext(QuizContext);
10
10
  let question;
11
11
  let type;
12
12
  let hasImages = false;
13
- if (questionResponse) {
14
- question = questionResponse.next_question;
13
+ if (state?.quiz.currentQuestion) {
14
+ question = state.quiz.currentQuestion.next_question;
15
15
  type = question.type;
16
- hasImages = questionResponse.next_question.options.some((option) => option.images);
16
+ hasImages = question.options.some((option) => option.images);
17
17
  }
18
18
  const [selected, setSelected] = useState({});
19
19
  const isDisabled = Object.keys(selected).length === 0;
20
20
  useEffect(() => {
21
- if (questionResponse?.next_question?.type) {
22
- const answers = state?.answerInputs?.[questionResponse.next_question.id] || [];
21
+ if (state?.quiz.currentQuestion?.next_question?.type) {
22
+ const nextQuestionId = state.quiz.currentQuestion.next_question.id;
23
+ const answers = state.answers?.inputs?.[nextQuestionId] || [];
23
24
  const prevSelected = {};
24
25
  answers?.forEach((answer) => {
25
26
  prevSelected[Number(answer)] = true;
26
27
  });
27
28
  setSelected(prevSelected);
28
29
  }
29
- }, [questionResponse, state?.answerInputs]);
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [state?.quiz.currentQuestion?.next_question.id]);
30
32
  const toggleIdSelected = (id) => {
31
33
  if (type === QuestionTypes.SingleSelect) {
32
34
  setSelected({ [id]: true });
@@ -48,18 +50,9 @@ function SelectTypeQuestion() {
48
50
  }
49
51
  };
50
52
  const onNextClick = () => {
51
- if (quizNextHandler && !isDisabled && questionResponse) {
52
- const questionType = type === QuestionTypes.SingleSelect
53
- ? QuestionTypes.SingleSelect
54
- : QuestionTypes.MultipleSelect;
55
- quizNextHandler({
56
- type: questionType,
57
- payload: {
58
- questionId: questionResponse?.next_question.id,
59
- input: Object.keys(selected).filter((key) => selected[Number(key)]),
60
- isLastQuestion: questionResponse.is_last_question,
61
- },
62
- });
53
+ if (nextQuestion && !isDisabled && state?.quiz.currentQuestion) {
54
+ const selectedAnswers = Object.keys(selected).filter((key) => selected[Number(key)]);
55
+ nextQuestion(selectedAnswers);
63
56
  }
64
57
  };
65
58
  if (question) {
@@ -78,7 +71,7 @@ function SelectTypeQuestion() {
78
71
  }, role: 'button', tabIndex: 0, key: option.id },
79
72
  option.images ? renderImages(option.images, 'cio-question-option-image') : '',
80
73
  React.createElement("div", { className: 'cio-question-option-value' }, option?.value))))),
81
- React.createElement(ControlBar, { nextButtonHandler: onNextClick, isNextButtonDisabled: isDisabled, backButtonHandler: quizBackHandler, showBackButton: !isFirstQuestion, ctaButtonText: question?.cta_text })));
74
+ React.createElement(ControlBar, { nextButtonHandler: onNextClick, isNextButtonDisabled: isDisabled, backButtonHandler: previousQuestion, showBackButton: !state?.quiz.isFirstQuestion, ctaButtonText: question?.cta_text })));
82
75
  }
83
76
  return null;
84
77
  }
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
2
  import CTAButton from '../CTAButton/CTAButton';
3
3
  function ZeroResults(props) {
4
- const { onResetClick } = props;
4
+ const { resetQuizClickHandler } = props;
5
5
  return (React.createElement("div", { className: 'cio-zero-results' },
6
6
  React.createElement("h3", { className: 'cio-zero-results-subtitle' }, "Sorry, it seems like we couldn\u2019t find results based on your answers."),
7
7
  React.createElement("p", { className: 'cio-zero-results-description' }, "This is embarrassing \uD83D\uDE22. It might be that some of the questions are not properly set up from our end. Would you give us another try?"),
8
- React.createElement(CTAButton, { ctaText: 'Try Again', onClick: onResetClick })));
8
+ React.createElement(CTAButton, { ctaText: 'Try Again', onClick: resetQuizClickHandler })));
9
9
  }
10
10
  export default ZeroResults;
@@ -8,8 +8,9 @@ export const componentDescription = `- import \`CioQuiz\` to render in your JSX.
8
8
  - This component handles state management, data fetching, and rendering logic.
9
9
  - To use this component, \`quizId\`, \`resultsPageOptions\`, and one of \`apiKey\` or \`cioJsClient\` are required.
10
10
  - \`resultsPageOptions\` lets you configure the results page
11
- - \`addToCartCallback\` is a callback function that will be called when the "Add to cart" button is clicked
12
- - \`clickItemCallback\` is an optional callback function that will be called when the result card is clicked. The default behavior is redirecting the user to the item's URL
11
+ - \`onAddToCartClick\` is a callback function that will be called when the "Add to cart" button is clicked
12
+ - \`onQuizResultClick\` is an optional callback function that will be called when the result card is clicked. The default behavior is redirecting the user to the item's URL
13
+ - \`onQuizResultsLoaded\` is an optional callback function that will be called when the quiz results are loaded
13
14
  - \`resultCardRegularPriceKey\` is a parameter that specifies the metadata field name for the regular price
14
15
  - \`resultCardSalePriceKey\` is an optional parameter that specifies the metadata field name for the sale price
15
16
  - Use different props to configure the behavior of this component.
@@ -24,6 +25,7 @@ export const basicDescription = `Pass an \`apiKey\` and a \`quizId\` to request
24
25
  export const cioJsClientDescription = `If you are already using an instance of the \`ConstructorIOClient\`, you can pass a \`cioJsClient\` instead of an \`apiKey\` to request results from Constructor's servers
25
26
 
26
27
  > Note: \`cioJsClient\` refers to an instance of the [constructorio-client-javascript](https://www.npmjs.com/package/@constructor-io/constructorio-client-javascript)`;
28
+ export const smallContainerDescription = `If you are using the provided styles, CioQuiz component will respect the height and width of its parent container and use responsive styles based on the parent container's dimensions`;
27
29
  export var RequestStates;
28
30
  (function (RequestStates) {
29
31
  RequestStates[RequestStates["Stale"] = 0] = "Stale";
@@ -1,9 +1,10 @@
1
1
  import { useMemo } from 'react';
2
- import { getCioClient } from '../utils';
2
+ import { getCioClient } from '../services';
3
3
  const useCioClient = ({ apiKey, cioJsClient }) => {
4
4
  if (!apiKey && !cioJsClient) {
5
- console.error('Either apiKey or cioJsClient is required');
5
+ throw new Error('Either apiKey or cioJsClient is required');
6
6
  }
7
- return useMemo(() => cioJsClient || getCioClient(apiKey), [apiKey, cioJsClient]);
7
+ const memoizedCioClient = useMemo(() => cioJsClient || getCioClient(apiKey), [apiKey, cioJsClient]);
8
+ return memoizedCioClient;
8
9
  };
9
10
  export default useCioClient;
@@ -0,0 +1,18 @@
1
+ import { useEffect } from 'react';
2
+ const useConsoleErrors = (quizId, resultsPageOptions) => {
3
+ useEffect(() => {
4
+ if (!quizId) {
5
+ // eslint-disable-next-line no-console
6
+ console.error('quizId is a required field of type string');
7
+ }
8
+ if (!resultsPageOptions || Object.keys(resultsPageOptions).length === 0) {
9
+ // eslint-disable-next-line no-console
10
+ console.error('resultsPageOptions is a required field of type object');
11
+ }
12
+ if (resultsPageOptions && !resultsPageOptions?.onAddToCartClick) {
13
+ // eslint-disable-next-line no-console
14
+ console.error('resultsPageOptions.onAddToCartClick is a required field of type function');
15
+ }
16
+ }, [quizId, resultsPageOptions, resultsPageOptions?.onAddToCartClick]);
17
+ };
18
+ export default useConsoleErrors;
@@ -0,0 +1,47 @@
1
+ import useCioClient from './useCioClient';
2
+ import useConsoleErrors from './useConsoleErrors';
3
+ import useQuizApiState from './useQuizApiState';
4
+ import useQuizEvents from './useQuizEvents';
5
+ import useQuizLocalState from './useQuizLocalState';
6
+ const useQuiz = ({ quizId, apiKey, cioJsClient, quizVersionId, resultsPageOptions }) => {
7
+ // Log console errors for required parameters quizId and resultsPageOptions
8
+ useConsoleErrors(quizId, resultsPageOptions);
9
+ // Quiz Local state
10
+ const { quizLocalState, resetQuizLocalState, dispatchLocalState } = useQuizLocalState();
11
+ // Quiz Cio Client
12
+ const cioClient = useCioClient({ apiKey, cioJsClient });
13
+ // Quiz API state
14
+ const { isFirstQuestion, quizApiState, resetQuizApiState } = useQuizApiState(quizId, quizLocalState, resultsPageOptions, quizVersionId, cioClient);
15
+ // Quiz callback events
16
+ const quizEvents = useQuizEvents({
17
+ cioClient,
18
+ quizApiState,
19
+ resultsPageOptions,
20
+ dispatchLocalState,
21
+ resetQuizApiState,
22
+ resetQuizLocalState,
23
+ });
24
+ return {
25
+ cioClient,
26
+ state: {
27
+ answers: {
28
+ inputs: quizLocalState.answerInputs,
29
+ isLastAnswer: quizLocalState.isLastAnswer,
30
+ },
31
+ quiz: {
32
+ requestState: quizApiState.quizRequestState,
33
+ versionId: quizApiState.quizVersionId,
34
+ sessionId: quizApiState.quizSessionId,
35
+ firstQuestion: quizApiState.quizFirstQuestion,
36
+ currentQuestion: quizApiState.quizCurrentQuestion,
37
+ results: quizApiState.quizResults,
38
+ resultsFilters: quizApiState.quizResultsFilters,
39
+ isFirstQuestion,
40
+ },
41
+ },
42
+ events: {
43
+ ...quizEvents,
44
+ },
45
+ };
46
+ };
47
+ export default useQuiz;
@@ -0,0 +1,87 @@
1
+ import { useEffect, useReducer } from 'react';
2
+ import { QuizAPIActionTypes } from '../components/CioQuiz/actions';
3
+ import apiReducer, { initialState } from '../components/CioQuiz/quizApiReducer';
4
+ import { nextQuestion, getQuizResults } from '../services';
5
+ const useFetchQuiz = (quizId, quizLocalState, resultsPageOptions, quizVersionIdProp, cioClient) => {
6
+ const [quizApiState, dispatch] = useReducer(apiReducer, initialState);
7
+ const firstQuestionId = quizApiState.quizFirstQuestion?.next_question.id;
8
+ const currentQuestionId = quizApiState.quizCurrentQuestion?.next_question.id;
9
+ const isFirstQuestion = firstQuestionId === currentQuestionId;
10
+ useEffect(() => {
11
+ (async () => {
12
+ dispatch({
13
+ type: QuizAPIActionTypes.SET_IS_LOADING,
14
+ });
15
+ if (quizLocalState.isLastAnswer) {
16
+ try {
17
+ const quizResults = await getQuizResults(cioClient, quizId, {
18
+ answers: quizLocalState.answers,
19
+ resultsPerPage: resultsPageOptions?.numResultsToDisplay,
20
+ quizVersionId: quizApiState.quizVersionId,
21
+ quizSessionId: quizApiState.quizSessionId,
22
+ });
23
+ // Set quiz results state
24
+ dispatch({
25
+ type: QuizAPIActionTypes.SET_QUIZ_RESULTS,
26
+ payload: {
27
+ quizResults,
28
+ },
29
+ });
30
+ }
31
+ catch (error) {
32
+ dispatch({
33
+ type: QuizAPIActionTypes.SET_IS_ERROR,
34
+ });
35
+ }
36
+ }
37
+ else {
38
+ try {
39
+ let quizVersionId = quizApiState.quizVersionId || quizVersionIdProp;
40
+ let { quizSessionId } = quizApiState;
41
+ const questionResult = await nextQuestion(cioClient, quizId, {
42
+ answers: quizLocalState.answers,
43
+ quizVersionId,
44
+ quizSessionId,
45
+ });
46
+ if (!quizVersionId && questionResult?.quiz_version_id) {
47
+ quizVersionId = questionResult.quiz_version_id;
48
+ }
49
+ if (!quizSessionId && questionResult?.quiz_session_id) {
50
+ quizSessionId = questionResult.quiz_session_id;
51
+ }
52
+ // Set current question state
53
+ dispatch({
54
+ type: QuizAPIActionTypes.SET_CURRENT_QUESTION,
55
+ payload: {
56
+ quizSessionId,
57
+ quizVersionId,
58
+ quizCurrentQuestion: questionResult,
59
+ },
60
+ });
61
+ }
62
+ catch (error) {
63
+ dispatch({
64
+ type: QuizAPIActionTypes.SET_IS_ERROR,
65
+ });
66
+ }
67
+ }
68
+ })();
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [
71
+ cioClient,
72
+ quizId,
73
+ quizLocalState,
74
+ quizLocalState.isLastAnswer,
75
+ resultsPageOptions?.numResultsToDisplay,
76
+ ]);
77
+ const resetQuizApiState = () => {
78
+ dispatch({ type: QuizAPIActionTypes.RESET_QUIZ });
79
+ };
80
+ return {
81
+ cioClient,
82
+ quizApiState,
83
+ isFirstQuestion,
84
+ resetQuizApiState,
85
+ };
86
+ };
87
+ export default useFetchQuiz;
@@ -0,0 +1,33 @@
1
+ import useQuizResultsLoaded from './useQuizResultsLoaded';
2
+ import useQuizResultClick from './useQuizResultClick';
3
+ import useQuizAddToCart from './useQuizAddToCart';
4
+ import useQuizNextClick from './useQuizNextClick';
5
+ import useQuizBackClick from './useQuizBackClick';
6
+ const useQuizEvents = (options) => {
7
+ const { cioClient, quizApiState, resultsPageOptions, dispatchLocalState, resetQuizApiState, resetQuizLocalState, } = options;
8
+ const { onAddToCartClick, onQuizResultClick, onQuizResultsLoaded } = resultsPageOptions;
9
+ // Quiz Next button click
10
+ const nextQuestion = useQuizNextClick(quizApiState, dispatchLocalState);
11
+ // Quiz Back button click callback
12
+ const previousQuestion = useQuizBackClick(dispatchLocalState);
13
+ // Quiz result add to cart callback
14
+ const addToCart = useQuizAddToCart(cioClient, quizApiState, onAddToCartClick);
15
+ // Quiz result click callback
16
+ const resultClick = useQuizResultClick(cioClient, quizApiState, onQuizResultClick);
17
+ // Quiz results loaded event
18
+ useQuizResultsLoaded(cioClient, quizApiState, onQuizResultsLoaded);
19
+ const resetQuiz = () => {
20
+ if (quizApiState.quizResults) {
21
+ resetQuizApiState();
22
+ resetQuizLocalState();
23
+ }
24
+ };
25
+ return {
26
+ addToCart,
27
+ resultClick,
28
+ nextQuestion,
29
+ previousQuestion,
30
+ resetQuiz,
31
+ };
32
+ };
33
+ export default useQuizEvents;
@@ -0,0 +1,18 @@
1
+ import { useCallback } from 'react';
2
+ import { trackQuizConversion } from '../../services';
3
+ import { isFunction } from '../../utils';
4
+ const useQuizAddToCart = (cioClient, quizApiState, onAddToCartClick) => {
5
+ const addToCartClickHandler = useCallback((e, result, price) => {
6
+ e.preventDefault();
7
+ if (quizApiState.quizResults) {
8
+ // Tracking call
9
+ trackQuizConversion(cioClient, quizApiState.quizResults, result, price);
10
+ // User custom callback function
11
+ if (isFunction(onAddToCartClick)) {
12
+ onAddToCartClick(result);
13
+ }
14
+ }
15
+ }, [quizApiState, cioClient, onAddToCartClick]);
16
+ return addToCartClickHandler;
17
+ };
18
+ export default useQuizAddToCart;
@@ -0,0 +1,11 @@
1
+ import { useCallback } from 'react';
2
+ import { QuestionTypes } from '../../components/CioQuiz/actions';
3
+ const useQuizBackClick = (dispatchLocalState) => {
4
+ const quizBackHandler = useCallback(() => {
5
+ if (dispatchLocalState) {
6
+ dispatchLocalState({ type: QuestionTypes.Back });
7
+ }
8
+ }, [dispatchLocalState]);
9
+ return quizBackHandler;
10
+ };
11
+ export default useQuizBackClick;
@@ -0,0 +1,45 @@
1
+ import { useCallback } from 'react';
2
+ import { QuestionTypes } from '../../components/CioQuiz/actions';
3
+ const useQuizNextClick = (quizApiState, dispatchLocalState) => {
4
+ const quizNextHandler = useCallback((payload) => {
5
+ const questionType = quizApiState.quizCurrentQuestion?.next_question.type;
6
+ const currentQuestion = quizApiState.quizCurrentQuestion;
7
+ switch (questionType) {
8
+ case QuestionTypes.Cover:
9
+ dispatchLocalState({
10
+ type: QuestionTypes.Cover,
11
+ payload: {
12
+ isLastQuestion: currentQuestion.is_last_question,
13
+ },
14
+ });
15
+ break;
16
+ case QuestionTypes.OpenText:
17
+ dispatchLocalState({
18
+ type: QuestionTypes.OpenText,
19
+ payload: {
20
+ questionId: currentQuestion.next_question.id,
21
+ input: payload,
22
+ isLastQuestion: currentQuestion.is_last_question,
23
+ },
24
+ });
25
+ break;
26
+ case QuestionTypes.SingleSelect:
27
+ case QuestionTypes.MultipleSelect:
28
+ dispatchLocalState({
29
+ type: currentQuestion.next_question.type === QuestionTypes.SingleSelect
30
+ ? QuestionTypes.SingleSelect
31
+ : QuestionTypes.MultipleSelect,
32
+ payload: {
33
+ questionId: currentQuestion.next_question.id,
34
+ input: payload,
35
+ isLastQuestion: currentQuestion.is_last_question,
36
+ },
37
+ });
38
+ break;
39
+ default:
40
+ break;
41
+ }
42
+ }, [quizApiState, dispatchLocalState]);
43
+ return quizNextHandler;
44
+ };
45
+ export default useQuizNextClick;
@@ -0,0 +1,17 @@
1
+ import { useCallback } from 'react';
2
+ import { trackQuizResultClick } from '../../services';
3
+ import { isFunction } from '../../utils';
4
+ const useQuizResultClick = (cioClient, quizApiState, onQuizResultClick) => {
5
+ const resultClickHandler = useCallback((result, position) => {
6
+ if (quizApiState.quizResults) {
7
+ // Tracking call
8
+ trackQuizResultClick(cioClient, quizApiState.quizResults, result, position);
9
+ // User custom callback function
10
+ if (isFunction(onQuizResultClick)) {
11
+ onQuizResultClick(result, position);
12
+ }
13
+ }
14
+ }, [quizApiState, cioClient, onQuizResultClick]);
15
+ return resultClickHandler;
16
+ };
17
+ export default useQuizResultClick;
@@ -0,0 +1,19 @@
1
+ import { useEffect } from 'react';
2
+ import { trackQuizResultsLoaded } from '../../services';
3
+ import { isFunction } from '../../utils';
4
+ const useQuizResultsLoaded = (cioClient, quizApiState, onQuizResultsLoaded) => {
5
+ // Quiz results loaded
6
+ useEffect(() => {
7
+ if (quizApiState.quizResults) {
8
+ // Tracking call
9
+ trackQuizResultsLoaded(cioClient, quizApiState.quizResults);
10
+ // User custom callback function
11
+ if (onQuizResultsLoaded &&
12
+ isFunction(onQuizResultsLoaded) &&
13
+ quizApiState.quizResults.response?.results) {
14
+ onQuizResultsLoaded(quizApiState.quizResults.response.results);
15
+ }
16
+ }
17
+ }, [quizApiState, cioClient, onQuizResultsLoaded]);
18
+ };
19
+ export default useQuizResultsLoaded;
@@ -0,0 +1,17 @@
1
+ import { useReducer } from 'react';
2
+ import { QuestionTypes } from '../components/CioQuiz/actions';
3
+ import quizLocalReducer, { initialState } from '../components/CioQuiz/quizLocalReducer';
4
+ const useQuizLocalState = () => {
5
+ const [quizLocalState, dispatch] = useReducer(quizLocalReducer, initialState);
6
+ const resetQuizLocalState = () => {
7
+ dispatch({
8
+ type: QuestionTypes.Reset,
9
+ });
10
+ };
11
+ return {
12
+ quizLocalState,
13
+ resetQuizLocalState,
14
+ dispatchLocalState: dispatch,
15
+ };
16
+ };
17
+ export default useQuizLocalState;
@@ -0,0 +1,60 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+ import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
3
+ export const getCioClient = (apiKey) => {
4
+ if (apiKey) {
5
+ return new ConstructorIOClient({
6
+ apiKey,
7
+ sendTrackingEvents: true,
8
+ });
9
+ }
10
+ return undefined;
11
+ };
12
+ export const nextQuestion = (cioClient, quizId, parameters) => cioClient?.quizzes.getQuizNextQuestion(quizId, parameters);
13
+ export const getQuizResults = async (cioClient, quizId, parameters) => cioClient?.quizzes.getQuizResults(quizId, parameters);
14
+ // Tracking requests
15
+ export const trackQuizResultsLoaded = (cioClient, quizResults) => {
16
+ const { quiz_id, quiz_session_id, quiz_version_id, result_id, request, response } = quizResults;
17
+ cioClient?.tracker.trackQuizResultsLoaded({
18
+ quiz_id,
19
+ quiz_version_id,
20
+ quiz_session_id,
21
+ url: window.location.href,
22
+ section: request?.section,
23
+ result_count: response?.total_num_results,
24
+ result_page: request?.page,
25
+ result_id,
26
+ });
27
+ };
28
+ export const trackQuizResultClick = (cioClient, quizResults, result, position) => {
29
+ if (quizResults.request && quizResults.response) {
30
+ const { quiz_id, quiz_session_id, quiz_version_id, result_id, request: { section, page, num_results_per_page }, response: { total_num_results }, } = quizResults;
31
+ cioClient?.tracker.trackQuizResultClick({
32
+ quiz_id,
33
+ quiz_version_id,
34
+ quiz_session_id,
35
+ item_id: result.data?.id,
36
+ item_name: result?.value,
37
+ section,
38
+ result_count: total_num_results,
39
+ result_page: page,
40
+ result_id,
41
+ result_position_on_page: position,
42
+ num_results_per_page,
43
+ });
44
+ }
45
+ };
46
+ export const trackQuizConversion = (cioClient, quizResults, result, price) => {
47
+ if (quizResults.request) {
48
+ const { quiz_id, quiz_session_id, quiz_version_id, request: { section }, } = quizResults;
49
+ cioClient?.tracker.trackQuizConversion({
50
+ quiz_id,
51
+ quiz_version_id,
52
+ quiz_session_id,
53
+ item_id: result.data?.id,
54
+ item_name: result.value,
55
+ section,
56
+ variation_id: result.data?.variation_id,
57
+ revenue: (price && String(price)) || undefined,
58
+ });
59
+ }
60
+ };
package/lib/mjs/utils.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
3
2
  import { QuestionTypes } from './components/CioQuiz/actions';
4
3
  export const renderImages = (images, cssClasses) => {
5
4
  const { primary_url: primaryUrl, primary_alt: primaryAlt, secondary_url: secondaryUrl, secondary_alt: secondaryAlt, } = images;
@@ -36,21 +35,32 @@ ${templateCode}
36
35
  },
37
36
  };
38
37
  };
39
- export const defaultAddToCartCallbackCode = `"addToCartCallback": (item) => console.dir(item)`;
38
+ export const defaultOnAddToCartClickCode = `"onAddToCartClick": (item) => console.dir(item)`;
39
+ export const defaultOnQuizResultClickCode = `"onQuizResultClick": (result, position) => console.dir(result, position)`;
40
+ export const defaultOnQuizResultsLoadedCode = `"onQuizResultsLoaded": (results) => console.dir(results)`;
40
41
  export const stringifyWithDefaults = (obj) => {
41
- const { addToCartCallback, cioJsClient, ...rest } = obj;
42
- let res = JSON.stringify(rest, null, ' ');
42
+ const { resultsPageOptions, cioJsClient, ...rest } = obj;
43
+ const { onAddToCartClick, onQuizResultsLoaded, onQuizResultClick } = resultsPageOptions;
44
+ let res = JSON.stringify({ ...rest, resultsPageOptions }, null, ' ');
43
45
  if (cioJsClient) {
44
46
  res = res.replace('"resultsPageOptions": {', `"cioJsClient": cioJsClient,
45
47
  "resultsPageOptions": {`);
46
48
  }
47
- res = res.replace('"resultsPageOptions": {', `"resultsPageOptions": {
48
- ${defaultAddToCartCallbackCode},`);
49
+ if (onQuizResultsLoaded) {
50
+ res = res.replace('"resultsPageOptions": {', `"resultsPageOptions": {
51
+ ${defaultOnQuizResultsLoadedCode},`);
52
+ }
53
+ if (onQuizResultClick) {
54
+ res = res.replace('"resultsPageOptions": {', `"resultsPageOptions": {
55
+ ${defaultOnQuizResultClickCode},`);
56
+ }
57
+ if (onAddToCartClick) {
58
+ res = res.replace('"resultsPageOptions": {', `"resultsPageOptions": {
59
+ ${defaultOnAddToCartClickCode},`);
60
+ }
49
61
  return res;
50
62
  };
51
63
  export const stringify = (obj) => JSON.stringify(obj, null, ' ');
52
- export const getNextQuestion = (cioClient, quizId, parameters) => cioClient?.quizzes.getQuizNextQuestion(quizId, parameters);
53
- export const getQuizResults = async (cioClient, quizId, parameters) => cioClient?.quizzes.getQuizResults(quizId, parameters);
54
64
  export const getQuestionTypes = (questionType) => {
55
65
  const isOpenQuestion = questionType === QuestionTypes.OpenText;
56
66
  const isCoverQuestion = questionType === QuestionTypes.Cover;
@@ -65,15 +75,6 @@ export const getQuestionTypes = (questionType) => {
65
75
  isSelectQuestion,
66
76
  };
67
77
  };
68
- export const getCioClient = (apiKey) => {
69
- if (apiKey) {
70
- return new ConstructorIOClient({
71
- apiKey,
72
- sendTrackingEvents: true,
73
- });
74
- }
75
- return undefined;
76
- };
77
78
  export function getPreferredColorScheme() {
78
79
  let colorScheme = 'light';
79
80
  // Check if the dark-mode Media-Query matches
@@ -82,3 +83,24 @@ export function getPreferredColorScheme() {
82
83
  }
83
84
  return colorScheme;
84
85
  }
86
+ export function isFunction(fn) {
87
+ return fn && typeof fn === 'function';
88
+ }
89
+ const isValueExpression = (exp) => 'name' in exp && 'value' in exp;
90
+ const isAndFilter = (exp) => 'and' in exp;
91
+ const isOrFilter = (exp) => 'or' in exp;
92
+ export const getFilterValuesFromExpression = (exp) => {
93
+ if (!exp) {
94
+ return [];
95
+ }
96
+ if (isAndFilter(exp)) {
97
+ return exp.and.flatMap((innerExpression) => getFilterValuesFromExpression(innerExpression));
98
+ }
99
+ if (isOrFilter(exp)) {
100
+ return exp.or.flatMap((innerExpression) => getFilterValuesFromExpression(innerExpression));
101
+ }
102
+ if (isValueExpression(exp)) {
103
+ return [exp.value];
104
+ }
105
+ return [];
106
+ };