diff --git a/package-lock.json b/package-lock.json index 85198596..dc47934a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "traitify-widgets", - "version": "3.8.2", + "version": "3.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "traitify-widgets", - "version": "3.8.2", + "version": "3.9.0", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", diff --git a/package.json b/package.json index e4127bd0..3cb5e322 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "traitify-widgets", - "version": "3.8.2", + "version": "3.9.0", "description": "Traitiy Widgets", "repository": { "type": "git", diff --git a/public/index.js b/public/index.js index f50aadbe..a995ed9e 100644 --- a/public/index.js +++ b/public/index.js @@ -144,6 +144,7 @@ function createAssessment() { if(cache.get("surveyType") === "benchmark") { return createWidget(); } if(cache.get("surveyType") === "cognitive") { return createCognitiveAssessment(); } if(cache.get("surveyType") === "order") { return createWidget(); } + if(cache.get("surveyType") === "generic") { return createGenericAssessment(); } const params = { deck_id: cache.get("deckID"), @@ -240,6 +241,24 @@ function createElement(options = {}) { return element; } +function createGenericAssessment() { + const query = Traitify.GraphQL.generic.create; + const variables = { + surveyID: cache.get("surveyID"), + profileID: cache.get("profileID") + }; + + Traitify.http.post(Traitify.GraphQL.generic.path, {query, variables}).then((response) => { + try { + const id = response.data.getOrCreateGenericAssessment.id; + cache.set("assessmentID", id); + } catch(error) { + console.log(error); + } + setTimeout(createWidget, 500); + }); +} + function destroyWidget() { Traitify.destroy(); } function setupTargets() { @@ -369,6 +388,7 @@ function setupDom() { options: [ {text: "Benchmark", value: "benchmark"}, {text: "Cognitive", value: "cognitive"}, + {text: "Generic", value: "generic"}, {text: "Order", value: "order"}, {text: "Personality", value: "personality"} ], @@ -404,6 +424,9 @@ function setupDom() { row.appendChild(createOption({name: "orderID", text: "Order ID:"})); group.appendChild(row) + row = createElement({className: surveyType !== "generic" ? "hide" : "", id: "generic-options"}); + row.appendChild(createOption({name: "profileID", text: "Profile ID:"})); + group.appendChild(row); row = createElement({className: "row"}); row.appendChild(createElement({onClick: createAssessment, tag: "button", text: "Create / Load"})); group.appendChild(row); @@ -427,6 +450,29 @@ function setupCognitive() { }); } +function setupGeneric() { + const query = Traitify.GraphQL.generic.surveys; + const variables = {localeKey: cache.get("locale")}; + + Traitify.http.post(Traitify.GraphQL.generic.path, {query, variables}).then((response) => { + try { + const options = response.data.genericSurveys + .map(({id, name}) => ({text: name, value: id})) + .sort((a, b) => a.text.localeCompare(b.text)); + + document.querySelector("#generic-options").appendChild(createOption({ + name: "surveyID", + onChange: onInputChange, + options, + text: "Survey:" + })); + } catch(error) { + console.log(error); + } + }); + +} + function setupTraitify() { const environment = cache.get("environment"); @@ -455,7 +501,7 @@ function onSurveyTypeChange(e) { const name = e.target.name; const value = e.target.value; const assessmentID = cache.get(`${value}AssessmentID`); - const otherValues = ["benchmark", "cognitive", "order", "personality"].filter((type) => type !== value); + const otherValues = ["benchmark", "cognitive", "generic", "order", "personality"].filter((type) => type !== value); cache.set("assessmentID", assessmentID); @@ -468,4 +514,5 @@ function onSurveyTypeChange(e) { setupTraitify(); setupDom(); setupCognitive(); +setupGeneric(); createWidget(); diff --git a/src/components/common/modal/index.js b/src/components/common/modal/index.js index b976a043..c6a6c652 100644 --- a/src/components/common/modal/index.js +++ b/src/components/common/modal/index.js @@ -4,11 +4,12 @@ import Icon from "components/common/icon"; import useTranslate from "lib/hooks/use-translate"; import style from "./style.scss"; -export default function Modal({children, onClose, title}) { +export default function Modal({children, containerClass = null, onClose, title}) { const translate = useTranslate(); + const sectionClass = [style.modalContainer, containerClass].filter(Boolean).join(" "); return (
-
+
{title}
@@ -34,6 +35,7 @@ export default function Modal({children, onClose, title}) { Modal.propTypes = { children: PropTypes.node.isRequired, + containerClass: PropTypes.string, onClose: PropTypes.func.isRequired, title: PropTypes.string.isRequired }; diff --git a/src/components/container/hooks/use-order-effect.js b/src/components/container/hooks/use-order-effect.js index 42a11c33..b154f9c3 100644 --- a/src/components/container/hooks/use-order-effect.js +++ b/src/components/container/hooks/use-order-effect.js @@ -52,9 +52,11 @@ export default function useOrderEffect() { // NOTE: Start next assessment if(currentAssessment.completed) { const nextAssessment = order.assessments.find(({completed}) => !completed); - if(nextAssessment) { setActive({...nextAssessment}); } + if(nextAssessment) { + setActive({...nextAssessment}); - return; + return; + } } // NOTE: Load updates for active assessment diff --git a/src/components/index.js b/src/components/index.js index 2b287b6b..71eee796 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -12,6 +12,7 @@ import CareerList from "./results/career/list"; import CareerModal from "./results/career/modal"; import CognitiveResults from "./results/cognitive"; import CognitiveChart from "./results/cognitive/chart"; +import GenericResults from "./results/generic"; import ClientGuide from "./results/guide/client"; import PersonalityGuide from "./results/guide/personality"; import ArchetypeHeading from "./results/personality/archetype/heading"; @@ -30,6 +31,7 @@ import RecommendationChart from "./results/recommendation/chart"; import Status from "./status"; import Survey from "./survey"; import CognitiveSurvey from "./survey/cognitive"; +import GenericSurvey from "./survey/generic"; import PersonalitySurvey from "./survey/personality"; export default { @@ -54,6 +56,7 @@ export default { Container: CognitiveResults }, Container: Results, + Generic: GenericResults, Guide: { Client: ClientGuide, Personality: PersonalityGuide @@ -90,6 +93,7 @@ export default { Survey: { Cognitive: CognitiveSurvey, Container: Survey, + Generic: GenericSurvey, Personality: PersonalitySurvey } }; diff --git a/src/components/results/generic/breakdown.js b/src/components/results/generic/breakdown.js new file mode 100644 index 00000000..3c2e9321 --- /dev/null +++ b/src/components/results/generic/breakdown.js @@ -0,0 +1,59 @@ +import PropTypes from "prop-types"; +import {useState} from "react"; +import useTranslate from "lib/hooks/use-translate"; +import Question from "./question"; +import style from "./style.scss"; + +export default function Breakdown({assessmentResult}) { + const [showAll, setShowAll] = useState(false); + const translate = useTranslate(); + const showHideAll = () => { + setShowAll(!showAll); + }; + + return ( +
+
+
+
{translate("results.generic.breakdown")}
+ {translate("results.generic.breakdown_description")} +
+
+ +
+
+
+ {assessmentResult.responses.map((question, index) => ( + + ))} +
+
+ ); +} + +Breakdown.propTypes = { + assessmentResult: PropTypes.shape({ + responses: PropTypes.arrayOf( + PropTypes.shape({ + questionId: PropTypes.string.isRequired, + questionText: PropTypes.string, + isCorrect: PropTypes.bool.isRequired, + selectedResponseOptionId: PropTypes.string, + responseOptions: PropTypes.arrayOf( + PropTypes.shape({ + responseOptionId: PropTypes.string.isRequired, + responseOptionText: PropTypes.string.isRequired, + isCorrect: PropTypes.bool + }) + ).isRequired + }) + ).isRequired + }).isRequired +}; diff --git a/src/components/results/generic/index.js b/src/components/results/generic/index.js new file mode 100644 index 00000000..262de5f9 --- /dev/null +++ b/src/components/results/generic/index.js @@ -0,0 +1,20 @@ +import useResults from "lib/hooks/use-results"; +import Breakdown from "./breakdown"; +import Score from "./score"; +import style from "./style.scss"; + +export default function GenericResults() { + const result = useResults({surveyType: "generic"}); + if(!result) { return null; } + + return ( +
+
+
+ + +
+
+
+ ); +} diff --git a/src/components/results/generic/question.js b/src/components/results/generic/question.js new file mode 100644 index 00000000..c75faff6 --- /dev/null +++ b/src/components/results/generic/question.js @@ -0,0 +1,95 @@ +import {faCheck, faXmark, faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons"; +import PropTypes from "prop-types"; +import {useState, useEffect} from "react"; +import Icon from "components/common/icon"; +import useTranslate from "lib/hooks/use-translate"; +import style from "./style.scss"; + +export default function Question({question, index, showState}) { + const translate = useTranslate(); + const [showContent, setShowContent] = useState(false); + const longTextCondition = (option) => option.responseOptionText.length > 20; + const longTextResponses = question.responseOptions.some(longTextCondition); + const directionClass = (longTextResponses || question.setImage) ? style.flexDirectionColumn : ""; + const responsesClassName = (question.setImage) + ? [style.responsesWithImage, directionClass].join(" ") + : [style.responsesWithoutImage, directionClass].join(" "); + + useEffect(() => { + setShowContent(showState); + }, [showState]); + + const toggleContent = () => { + setShowContent(!showContent); + }; + + const optionClassName = (option) => { + if(question.isCorrect) { + return option.isCorrect ? style.correctResponse : ""; + } + if(option.isCorrect) { + return style.correctOption; + } + if(option.responseOptionId === question.selectedResponseOptionId) { + return style.incorrectResponse; + } + return ""; + }; + + return ( +
+
+
+ {question.isCorrect + ? + : } +
+
{translate("cognitive_question_alt_text")} {index + 1}
+
+ +
+
+ {showContent && ( +
+
+
{question.questionText}
+
+ {question.responseOptions.map((option) => ( +
+ {option.responseOptionText} +
+ ))} +
+
+ {question.setImage && ( +
+ {question.questionText} +
+ )} +
+ )} +
+ ); +} + +Question.propTypes = { + question: PropTypes.shape({ + questionText: PropTypes.string, + questionId: PropTypes.string.isRequired, + isCorrect: PropTypes.bool.isRequired, + selectedResponseOptionId: PropTypes.string, + setImage: PropTypes.string, + responseOptions: PropTypes.arrayOf(PropTypes.shape({ + responseOptionId: PropTypes.string.isRequired, + responseOptionText: PropTypes.string.isRequired, + isCorrect: PropTypes.bool.isRequired + })).isRequired + }).isRequired, + index: PropTypes.number.isRequired, + showState: PropTypes.bool.isRequired +}; diff --git a/src/components/results/generic/score.js b/src/components/results/generic/score.js new file mode 100644 index 00000000..dbd0e0ec --- /dev/null +++ b/src/components/results/generic/score.js @@ -0,0 +1,54 @@ +import PropTypes from "prop-types"; +import useTranslate from "lib/hooks/use-translate"; +import style from "./style.scss"; + +export default function Score({assessmentResult}) { + const translate = useTranslate(); + const totalQuestions = assessmentResult ? assessmentResult.responses.length : 0; + const totalCorrectResponses = assessmentResult ? assessmentResult.totalCorrectResponses : 0; + const totalIncorrectResponses = assessmentResult ? assessmentResult.totalIncorrectResponses : 0; + const overallScore = assessmentResult ? assessmentResult.overallScore : 0; + + return ( +
+
{translate("results.generic.score")}
+
+
+
{translate("results.generic.correct")}:
+
{totalCorrectResponses} / {totalQuestions}
+
+
+
{translate("results.generic.incorrect")}:
+
{totalIncorrectResponses} / {totalQuestions}
+
+
+
{translate("results.generic.overall_score")}:
+
{overallScore}%
+
+
+
+ ); +} + +Score.propTypes = { + assessmentResult: PropTypes.shape({ + responses: PropTypes.arrayOf( + PropTypes.shape({ + questionId: PropTypes.string.isRequired, + questionText: PropTypes.string, + isCorrect: PropTypes.bool.isRequired, + selectedResponseOptionId: PropTypes.string, + responseOptions: PropTypes.arrayOf( + PropTypes.shape({ + responseOptionId: PropTypes.string.isRequired, + responseOptionText: PropTypes.string.isRequired, + isCorrect: PropTypes.bool + }) + ).isRequired + }) + ).isRequired, + totalCorrectResponses: PropTypes.number.isRequired, + totalIncorrectResponses: PropTypes.number.isRequired, + overallScore: PropTypes.number.isRequired + }).isRequired +}; diff --git a/src/components/results/generic/style.scss b/src/components/results/generic/style.scss new file mode 100644 index 00000000..ed89d0c1 --- /dev/null +++ b/src/components/results/generic/style.scss @@ -0,0 +1,191 @@ +@import "style/helpers"; + +.breakdown { + display: flex; + flex-direction: column; + gap: $buffer-lg * 0.5; +} + +.container { + @extend %container; + color: $-black; + display: flex; + flex-direction: column; + width: 100%; + + button { + border-radius: $border-radius-sm * 0.5; + height: 48px; + margin: $buffer $buffer * 0.5; + vertical-align: middle; + + &.toggleButton { + @include buttonLight("text-dark"); + border-radius: $border-radius-sm * 0.5; + cursor: pointer; + height: $buffer * 2; + margin: $buffer-sm * 0.5; + padding: $buffer-sm * 0.5 $buffer-sm * 1.25; + border: 1px solid #DADCE0; + } + } +} + +.contentBody { + display: flex; + flex-direction: column; + @extend %container; + + .score, .breakdown { + @extend %box; + @extend %container; + margin-bottom: $buffer-section; + } +} + +.correct { + background-color: $-aquamarine; +} + +.correct, .incorrect, .overall { + border-radius: $border-radius-sm; + color: $-white; + display: flex; + height: $buffer * 2.75; + justify-content: space-between; + padding: $buffer * 0.5 $buffer; + width: 100%; +} + +.description { + align-items: center; + display: flex; + gap: $buffer * 0.5; + justify-content: space-between; + @include max-width("xs") { flex-direction: column; } +} + +.flexDirectionColumn { flex-direction: column; } + +.iconCorrect { + color: $-aquamarine; + font-size: $font-size-heading * 1.25; +} + +.iconIncorrect { + color: $-red; + font-size: $font-size-heading * 1.25; +} + +.incorrect { + background-color: $-red; +} + +.overall { + background-color: $-blue; +} + +.question { + @extend %box; +} + +.questionContent { + align-items: center; + display: flex; + gap: $buffer * 0.5; + @include max-width("xs") { flex-direction: column; } +} + +.questionImage { + flex: 1; + text-align: center; + img { + max-width: 100%; + } +} + +.questionText { + font-weight: 600; + padding: $buffer 0; +} + +.questionTitle { + align-items: center; + display: flex; + font-weight: 700; + gap: $buffer * 0.75; + justify-content: space-between; + + div:last-of-type { + align-items: center; + display: flex; + margin-left: auto; + } +} + +.questions { + display: flex; + flex-direction: column; + gap: $buffer; +} + +.responseOption { + @extend %box; + font-weight: 500; + padding: $buffer-lg * 0.25 $buffer-lg * 0.5; + text-align: center; + + &.correctOption { + border: 2px solid $-aquamarine; + } + &.incorrectResponse { + background-color: $-red; + color: $-white; + } + &.correctResponse { + background-color: $-aquamarine; + color: $-white; + } +} + +.responseOptions { + display: flex; + flex: 1; + flex-direction: column; + gap: $buffer; +} + +.responsesWithImage { + display: flex; + flex-direction: column; + gap: $buffer; +} + +.responsesWithoutImage { + display: flex; + flex-direction: row; + gap: $buffer; + justify-content: space-around; + + @include max-width("xs") { flex-direction: column; } + + .responseOption { + width: 100%; + } +} + +.score { + .scoreRow { + display: flex; + font-size: $font-size-lg; + gap: $buffer * 0.5; + justify-content: space-between; + margin-top: $buffer; + @include max-width("xs") { flex-direction: column; } + } +} + +.title { + @extend %heading; + font-size: $font-size; +} diff --git a/src/components/results/index.js b/src/components/results/index.js index 9ce4ee9f..40e17c12 100644 --- a/src/components/results/index.js +++ b/src/components/results/index.js @@ -4,6 +4,7 @@ import EmployeeReport from "components/report/employee"; import ManagerReport from "components/report/manager"; import Cognitive from "components/results/cognitive"; import FinancialRiskResults from "components/results/financial-risk"; +import GenericResults from "components/results/generic"; import Skipped from "components/status/skipped"; import useActive from "lib/hooks/use-active"; import useComponentEvents from "lib/hooks/use-component-events"; @@ -21,6 +22,7 @@ export default function Results() { if(active.skipped) { return ; } if(!active.completed) { return null; } if(active.surveyType === "cognitive") { return ; } + if(active.surveyType === "generic") { return ; } if(active.surveyType !== "personality") { return null; } if(!results) { return null; } if(results.scoring_scale === "LIKERT_CUMULATIVE_POMP") { diff --git a/src/components/survey/generic/index.js b/src/components/survey/generic/index.js new file mode 100644 index 00000000..2014e614 --- /dev/null +++ b/src/components/survey/generic/index.js @@ -0,0 +1,147 @@ +import {faArrowLeft} from "@fortawesome/free-solid-svg-icons"; +import {useEffect, useState} from "react"; +import {useRecoilRefresher_UNSTABLE as useRecoilRefresher} from "recoil"; +import Icon from "components/common/icon"; +import Loading from "components/common/loading"; +import Markdown from "components/common/markdown"; +import Modal from "components/common/modal"; +import useAssessment from "lib/hooks/use-assessment"; +import useCache from "lib/hooks/use-cache"; +import useCacheKey from "lib/hooks/use-cache-key"; +import useDidUpdate from "lib/hooks/use-did-update"; +import useGraphql from "lib/hooks/use-graphql"; +import useHttp from "lib/hooks/use-http"; +import useTranslate from "lib/hooks/use-translate"; +import {activeAssessmentQuery} from "lib/recoil"; +import ProgressBar from "./progress-bar"; +import QuestionSet from "./question-set"; +import style from "./style.scss"; + +export default function GenericSurvey() { + const [questionSetIndex, setQuestionSetIndex] = useState(0); + const [answers, setAnswers] = useState([]); + const [showInstructions, setShowInstructions] = useState(false); + const [showConclusions, setShowConclusions] = useState(false); + const [submitAttempts, setSubmitAttempts] = useState(0); + + const assessment = useAssessment({surveyType: "generic"}); + if(assessment?.completedAt) { return; } + const assessmentCacheKey = useCacheKey("assessment"); + const cache = useCache(); + const questionSets = assessment ? assessment.survey.questionSets : []; + const questionCount = questionSets.reduce((count, set) => count + set.questions.length, 0); + const currentQuestionSet = questionSets ? questionSets[questionSetIndex] : {}; + const progress = questionSetIndex >= 0 ? (questionSetIndex / questionSets.length) * 100 : 0; + const finished = questionSets.length > 0 && questionCount === answers.length; + + const graphQL = useGraphql(); + const http = useHttp(); + const translate = useTranslate(); + const refreshAssessment = useRecoilRefresher(activeAssessmentQuery); + + const updateAnswer = (questionId, selectedOptionId) => { + const currentAnswers = answers.filter((answer) => answer.questionId !== questionId); + setAnswers([...currentAnswers, + {questionId, selectedResponseOptionId: selectedOptionId}]); + }; + + const onNext = () => { setQuestionSetIndex(questionSetIndex + 1); }; + const onBack = () => { setQuestionSetIndex(questionSetIndex - 1); }; + + const onSubmit = () => { + if(submitAttempts > 3) { return; } + const query = graphQL.generic.update; + const variables = { + assessmentID: assessment.id, + answers + }; + + http.post(graphQL.generic.path, {query, variables}).then(({data, errors}) => { + if(!errors && data.submitGenericAssessmentAnswers) { + setShowConclusions(true); + setTimeout(() => { + const response = data.submitGenericAssessmentAnswers; + cache.set(assessmentCacheKey, {...response, completed: true}); + refreshAssessment(); + }, 5000); + } else { + console.warn(errors || data); // eslint-disable-line no-console + + setTimeout(() => setSubmitAttempts((x) => x + 1), 2000); + } + }); + }; + + useDidUpdate(() => { onSubmit(); }, [submitAttempts]); + + useEffect(() => { + setShowInstructions(true); + }, [assessment]); + + useEffect(() => { + if(!finished) { return; } + onSubmit(); + }, [finished]); + + if(!assessment) { return ; } + + if(showConclusions) { + return ( +
+ + {assessment.survey.conclusions} + + +
+ ); + } + + return ( +
+ + {currentQuestionSet && ( + + )} + + {questionSetIndex > 0 && ( + + )} + {showInstructions && ( + setShowInstructions(false)} + containerClass={style.modalContainer} + > + + {assessment.survey.instructions} + +
+
+ + +
+ + )} +
+ ); +} diff --git a/src/components/survey/generic/progress-bar.js b/src/components/survey/generic/progress-bar.js new file mode 100644 index 00000000..e94afeea --- /dev/null +++ b/src/components/survey/generic/progress-bar.js @@ -0,0 +1,14 @@ +import PropTypes from "prop-types"; +import style from "./style.scss"; + +export default function ProgressBar({progress}) { + return ( +
+
+
+ ); +} + +ProgressBar.propTypes = { + progress: PropTypes.number.isRequired +}; diff --git a/src/components/survey/generic/question-set.js b/src/components/survey/generic/question-set.js new file mode 100644 index 00000000..68b21025 --- /dev/null +++ b/src/components/survey/generic/question-set.js @@ -0,0 +1,48 @@ +import PropTypes from "prop-types"; +import {useEffect, useState} from "react"; +import Responses from "./responses"; +import style from "./style.scss"; + +export default function QuestionSet({onNext, questionSet, updateAnswer}) { + const questionSetClass = [style.questionSet].join(" "); + const [selectedOptions, setSelectedOptions] = useState([]); + const setFinished = questionSet.questions.length === selectedOptions.length; + const selectOption = (questionId, optionId) => { + if(!selectedOptions.includes(questionId)) setSelectedOptions([...selectedOptions, questionId]); + updateAnswer(questionId, optionId); + }; + + useEffect(() => { + if(!setFinished) return; + onNext(); + }, [setFinished]); + + return ( +
+ {questionSet.text} + {questionSet.questions.map((question, index) => ( +
+
+
{index + 1}. {question.text}
+ selectOption(question.id, optionId)} + /> +
+ ))} +
+ ); +} + +QuestionSet.propTypes = { + onNext: PropTypes.func.isRequired, + questionSet: PropTypes.shape({ + text: PropTypes.string.isRequired, + questions: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired + })).isRequired, + setImage: PropTypes.string.isRequired + }).isRequired, + updateAnswer: PropTypes.func.isRequired +}; diff --git a/src/components/survey/generic/responses.js b/src/components/survey/generic/responses.js new file mode 100644 index 00000000..8cc1e961 --- /dev/null +++ b/src/components/survey/generic/responses.js @@ -0,0 +1,42 @@ +import PropTypes from "prop-types"; +import {useState} from "react"; +import style from "./style.scss"; + +export default function Responses({responseOptions = [], updateAnswer}) { + const buttonClass = ["traitify--response-button", style.response].join(" "); + const longTextResponses = responseOptions.some((option) => option.text.length > 20); + const buttonWidth = longTextResponses ? "100%" : "auto"; + const directionClass = longTextResponses ? style.flexDirectionColumn : ""; + const responseOptionsClass = [style.responseOptions, directionClass].join(" "); + const [activeButton, setActiveButton] = useState(null); + const selectOption = (optionId) => { + setActiveButton(optionId); + updateAnswer(optionId); + }; + + return ( +
+ {responseOptions.map((option) => ( + + ))} +
+ ); +} + +Responses.propTypes = { + responseOptions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired + }) + ).isRequired, + updateAnswer: PropTypes.func.isRequired +}; diff --git a/src/components/survey/generic/style.scss b/src/components/survey/generic/style.scss new file mode 100644 index 00000000..c1f65346 --- /dev/null +++ b/src/components/survey/generic/style.scss @@ -0,0 +1,177 @@ +@import "style/helpers"; + +button.btnPrimary { + background-color: $-blue; + color: $-white; + display: inline-block; + padding: $buffer-lg * .5 $buffer; + text-align: center; + border: transparent; + max-width: 110px; + margin: $buffer-lg auto; +} + +.container { + @extend %container; + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 100%; + position: relative; + z-index: 1; + padding: $buffer-lg; + text-align: center; + + @include min-width("xs") { + aspect-ratio: 2/1; + max-width: $breakpoint-md * 0.85; + } + + @include max-width("xs") { + padding: $buffer-sm; + } + + .modalContainer { + overflow: auto; + max-width: 600px; + } + + button { + border: 1px solid #DADCE0; + border-radius: $border-radius-sm * 0.5; + margin: $buffer * 1.25 0 0; + height: 48px; + vertical-align: middle; + font-weight: 500; + + @include max-width("xs") { + height: auto; + padding: $buffer * 0.25 $buffer * 0.5; + } + + &.back { + padding: $buffer * 0.5 $buffer-lg * 0.5; + width: 150px; + @include max-width("xs") { width: 100px; } + + .icon { + margin-right: $buffer-sm; + font-size: $font-size-lg; + } + } + } + + .markdown { + text-align: center; + } + + .progress { + border-radius: $border-radius-sm; + height: 100%; + transition: width 0.3s; + width: 0%; + + @include theme("background-color", "progress-bar"); + } + .progressBar { + border-radius: $border-radius-sm; + height: $border-width-xl; + margin-bottom: $buffer * 2; + + @include theme("background-color", "border"); + } + &:after { + top: 0; + bottom: 0; + border: $border-width solid; + border-radius: $border-radius; + content: ""; + left: 0; + position: absolute; + right: 0; + z-index: -1; + + @include theme("border-color", "border"); + } +} + +.flexDirectionColumn { flex-direction: column; } + +.footer { + display: flex; + justify-content: flex-end; + gap: $buffer * 0.25; + + .cancelBtn { + @include buttonLight("text-dark"); + @include theme("border-color", "border-light"); + border: $border-width solid; + border-radius: $border-radius-sm * 0.5; + margin: $buffer * 0.5; + } + + .btnPrimary { + margin: $buffer * 0.5; + @include max-width("xs") { padding: $buffer * 0.25 $buffer; } + } +} + +.grayDivider { + color: #555555; + width: 100%; + margin: $buffer-lg auto $buffer-lg; + line-height: inherit; + clear: both; + user-select: none; + break-after: page; + border: $border-width solid #e8e8ec; + border-radius: $border-radius-sm; +} + +.questionSet { + width: 100%; + align-items: center; + text-align: center; + padding: $buffer; + + @include max-width("xs") { padding: $buffer-sm; } + + img { + margin-bottom: $buffer; + @include max-width("xs") { max-width: 100%; } + } + + .question { + text-align: left; + font-weight: 600; + } +} + +.response { + padding: $buffer * 0.25 $buffer * 0.75; + text-align: center; + + @include min-width("xs") { padding: $buffer * 0.25 $buffer * 0.75; } + @include min-width("sm") { + font-size: $font-size * 1.25; + line-height: $line-height; + padding: $buffer * 0.5 $buffer * 1.5; + } + @include theme("color", "text-light"); + &:focus { + outline-offset: -1px; + outline-style: solid; + outline-width: 1px; + } + &.btnActive { + background-color: $-blue; + color: $-white; + border-color: transparent; + } +} + +.responseOptions{ + display: flex; + justify-content: space-between; + @include max-width("xs") { flex-direction: column; } +} \ No newline at end of file diff --git a/src/components/survey/index.js b/src/components/survey/index.js index e8012f49..fdc0844e 100644 --- a/src/components/survey/index.js +++ b/src/components/survey/index.js @@ -1,6 +1,7 @@ import Status from "components/status"; import useActive from "lib/hooks/use-active"; import Cognitive from "./cognitive"; +import Generic from "./generic"; import Personality from "./personality"; export default function Survey() { @@ -9,6 +10,7 @@ export default function Survey() { if(!active) { return ; } if(active.surveyType === "cognitive") { return ; } if(active.surveyType === "external") { return ; } + if(active.surveyType === "generic") { return ; } if(active.surveyType === "personality") { return ; } return null; diff --git a/src/lib/graphql/generic.js b/src/lib/graphql/generic.js new file mode 100644 index 00000000..723ad207 --- /dev/null +++ b/src/lib/graphql/generic.js @@ -0,0 +1,97 @@ +export const surveys = ` + query($localeKey: String!) { + genericSurveys(localeKey: $localeKey) { + id + name + } + } +`; + +export const create = ` + mutation($profileID: ID!, $surveyID: ID!) { + getOrCreateGenericAssessment(profileId: $profileID, surveyId: $surveyID) { + id + surveyId + profileId + startedAt + completedAt + localeKey + } + } +`; + +export const questions = ` + query($assessmentID: ID!) { + genericSurveyQuestions(assessmentId: $assessmentID) { + id + surveyId + profileId + startedAt + completedAt + totalCorrectResponses + totalIncorrectResponses + overallScore + localeKey + survey { + id + name + conclusions + instructions + instructionButton + questionSets { + text + setImage + questions { + id + text + responseOptions { + id + text + } + } + } + } + responses { + questionId + questionText + selectedResponseOptionId + setImage + isCorrect + responseOptions { + responseOptionId + responseOptionText + isCorrect + } + } + } + } +`; + +export const update = ` + mutation($assessmentID: ID!, $answers: [Answers]!) { + submitGenericAssessmentAnswers(assessmentId: $assessmentID, answers: $answers) { + id + surveyId + profileId + startedAt + completedAt + totalCorrectResponses + totalIncorrectResponses + overallScore + responses { + questionId + questionText + selectedResponseOptionId + setImage + isCorrect + responseOptions { + responseOptionId + responseOptionText + isCorrect + } + } + } + } +`; + +export const path = "/generic-assessments/graphql"; diff --git a/src/lib/graphql/index.js b/src/lib/graphql/index.js index 57df33d2..357217c4 100644 --- a/src/lib/graphql/index.js +++ b/src/lib/graphql/index.js @@ -1,6 +1,7 @@ import * as benchmark from "./benchmark"; import * as cognitive from "./cognitive"; import * as external from "./external"; +import * as generic from "./generic"; import * as guide from "./guide"; import * as order from "./order"; import * as xavier from "./xavier"; @@ -9,6 +10,7 @@ export default { benchmark, cognitive, external, + generic, guide, order, xavier diff --git a/src/lib/i18n-data/en-us.json b/src/lib/i18n-data/en-us.json index 2b649746..76eb294c 100644 --- a/src/lib/i18n-data/en-us.json +++ b/src/lib/i18n-data/en-us.json @@ -162,6 +162,14 @@ "heading": "Cognitive Breakdown" } }, + "generic": { + "breakdown": "Breakdown", + "breakdown_description": "Here is the breakdown of the questions you answered in the assessment", + "correct": "Correct", + "incorrect": "Incorrect", + "overall_score": "Overall Score", + "score": "Score" + }, "reports": { "candidate": "Candidate Report", "employee": "Engage Employee Report", @@ -172,6 +180,7 @@ "salary_mean": "Salary Mean", "salary_mean_html": "The wage at which half of the workers in the occupation earned more than that amount and half earned less. Median wage data is from ONet Online.", "search": "Search", + "show_hide_all": "Show/Hide All", "show_less": "Show Less", "show_more": "Show More", "skill_heading_for_communication": "Speaking, writing, and sharing ideas are important. Try these tips to communicate well.", diff --git a/src/lib/recoil/assessment.js b/src/lib/recoil/assessment.js index a8680a99..84becbea 100644 --- a/src/lib/recoil/assessment.js +++ b/src/lib/recoil/assessment.js @@ -104,10 +104,43 @@ export const personalityAssessmentQuery = selectorFamily({ key: "assessment/personality" }); +export const genericAssessmentQuery = selectorFamily({ + get: (id) => async({get}) => { + if(!id) { return null; } + + const cache = get(cacheState); + const cacheKey = get(safeCacheKeyState({id, type: "assessment"})); + const cached = cache.get(cacheKey); + if(cached) { return cached; } + + const GraphQL = get(graphqlState); + const http = get(httpState); + const params = { + query: GraphQL.generic.questions, + variables: {assessmentID: id} + }; + + const response = await http.post({path: GraphQL.generic.path, params}); + if(response.errors) { + console.warn("generic-assessment", response.errors); /* eslint-disable-line no-console */ + return null; + } + + const assessment = response.data.genericSurveyQuestions; + if(!assessment?.completedAt) { return assessment; } + + cache.set(cacheKey, assessment); + + return assessment; + }, + key: "assessment/generic" +}); + export const assessmentQuery = selectorFamily({ get: ({id, surveyType}) => async({get}) => { if(surveyType === "cognitive") { return get(cognitiveAssessmentQuery(id)); } if(surveyType === "external") { return get(externalAssessmentQuery(id)); } + if(surveyType === "generic") { return get(genericAssessmentQuery(id)); } if(surveyType === "personality") { return get(personalityAssessmentQuery(id)); } return null;