Browse Source
feat: implement question flow system with localized support and UI components
front-test-2
feat: implement question flow system with localized support and UI components
front-test-2
38 changed files with 607 additions and 311 deletions
-
28src/app/globals.css
-
249src/app/questions-list/[slug]/question-detail-client.tsx
-
72src/components/questions/conditional-questions.tsx
-
16src/components/questions/progress-helper.ts
-
160src/components/questions/question-renderer.tsx
-
30src/components/questions/question-section-flow.tsx
-
48src/components/questions/question-slider.tsx
-
14src/components/questions/question-snap-list.tsx
-
4src/components/questions/question-title.tsx
-
12src/components/sliders/slider-slide-four.tsx
-
26src/components/ui/notice-box.tsx
-
1src/data/question-data.ts
-
48src/data/questions/en.json
-
46src/data/questions/fa.json
-
4src/translations/locales/ar.json
-
4src/translations/locales/az.json
-
4src/translations/locales/bn.json
-
4src/translations/locales/da.json
-
4src/translations/locales/de.json
-
4src/translations/locales/en.json
-
4src/translations/locales/es.json
-
4src/translations/locales/fa.json
-
4src/translations/locales/fr.json
-
4src/translations/locales/gu.json
-
4src/translations/locales/ha.json
-
4src/translations/locales/he.json
-
4src/translations/locales/hi.json
-
4src/translations/locales/id.json
-
4src/translations/locales/ks.json
-
4src/translations/locales/pt.json
-
4src/translations/locales/ru.json
-
4src/translations/locales/sw.json
-
4src/translations/locales/tg.json
-
4src/translations/locales/tr.json
-
4src/translations/locales/ul.json
-
4src/translations/locales/ur.json
-
4src/translations/locales/uz.json
-
4src/translations/locales/zh.json
@ -0,0 +1,72 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useQuestionAnswers } from "./question-answer-storage"; |
||||
|
import type { QuestionField } from "@/data/question-data"; |
||||
|
import QuestionRenderer from "./question-renderer"; |
||||
|
|
||||
|
type ConditionalQuestionsProps = { |
||||
|
parentQuestion: QuestionField; |
||||
|
parentQuestionIndex: number; |
||||
|
rules: { |
||||
|
values: string[]; |
||||
|
subQuestions: QuestionField[]; |
||||
|
}[]; |
||||
|
dobQuestion?: QuestionField; |
||||
|
dobQuestionIndex?: number; |
||||
|
}; |
||||
|
|
||||
|
export function ConditionalQuestions({ |
||||
|
parentQuestion, |
||||
|
parentQuestionIndex, |
||||
|
rules, |
||||
|
dobQuestion, |
||||
|
dobQuestionIndex, |
||||
|
}: ConditionalQuestionsProps) { |
||||
|
const { getAnswerValue } = useQuestionAnswers(); |
||||
|
const parentAnswer = getAnswerValue(parentQuestion, parentQuestionIndex); |
||||
|
|
||||
|
// Find if there is a matching rule for the current parent answer
|
||||
|
const matchedRule = rules.find((rule) => { |
||||
|
return rule.values.includes(String(parentAnswer)); |
||||
|
}); |
||||
|
|
||||
|
if (!matchedRule) { |
||||
|
return ( |
||||
|
<QuestionRenderer |
||||
|
question={parentQuestion} |
||||
|
questionIndex={parentQuestionIndex} |
||||
|
dobQuestion={dobQuestion} |
||||
|
dobQuestionIndex={dobQuestionIndex} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="flex flex-col gap-5 w-full"> |
||||
|
<QuestionRenderer |
||||
|
question={parentQuestion} |
||||
|
questionIndex={parentQuestionIndex} |
||||
|
dobQuestion={dobQuestion} |
||||
|
dobQuestionIndex={dobQuestionIndex} |
||||
|
/> |
||||
|
<div className="flex flex-col gap-5 pl-4 border-l-2 border-[#F2465F]/20 mt-2 animate-in fade-in slide-in-from-top-4 duration-300"> |
||||
|
{matchedRule.subQuestions.map((subQuestion, index) => { |
||||
|
// Sub-question index is calculated uniquely to prevent conflicts
|
||||
|
const subIndex = parentQuestionIndex * 100 + index + 1; |
||||
|
return ( |
||||
|
<div key={subQuestion.title} className="w-full"> |
||||
|
<QuestionRenderer |
||||
|
question={subQuestion} |
||||
|
questionIndex={subIndex} |
||||
|
dobQuestion={dobQuestion} |
||||
|
dobQuestionIndex={dobQuestionIndex} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
})} |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default ConditionalQuestions; |
||||
@ -0,0 +1,160 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import type { QuestionField } from "@/data/question-data"; |
||||
|
import QuestionBirthplace from "@/components/questions/question-birthplace"; |
||||
|
import QuestionButton from "@/components/questions/question-button"; |
||||
|
import QuestionCheckbox from "@/components/questions/question-checkbox"; |
||||
|
import QuestionDate from "@/components/questions/question-date"; |
||||
|
import QuestionDropdown from "@/components/questions/question-dropdown"; |
||||
|
import QuestionFile from "@/components/questions/question-file"; |
||||
|
import QuestionNumber from "@/components/questions/question-number"; |
||||
|
import QuestionPhone from "@/components/questions/question-phone"; |
||||
|
import QuestionPhoto from "@/components/questions/question-photo"; |
||||
|
import QuestionRadio from "@/components/questions/question-radio"; |
||||
|
import QuestionSlider from "@/components/questions/question-slider"; |
||||
|
import QuestionText from "@/components/questions/question-text"; |
||||
|
|
||||
|
type QuestionRendererProps = { |
||||
|
question: QuestionField; |
||||
|
questionIndex: number; |
||||
|
disabled?: boolean; |
||||
|
dobQuestion?: QuestionField; |
||||
|
dobQuestionIndex?: number; |
||||
|
}; |
||||
|
|
||||
|
export function QuestionRenderer({ |
||||
|
question, |
||||
|
questionIndex, |
||||
|
disabled, |
||||
|
dobQuestion, |
||||
|
dobQuestionIndex, |
||||
|
}: QuestionRendererProps) { |
||||
|
const compactTextHeight = |
||||
|
question.title.toLowerCase().includes("email") || |
||||
|
question.title.includes("ایمیل") || |
||||
|
question.title.toLowerCase().includes("duration") || |
||||
|
question.title.includes("مدت") |
||||
|
? "h-[54px]" |
||||
|
: undefined; |
||||
|
|
||||
|
switch (question.type) { |
||||
|
case "birthplace": |
||||
|
return ( |
||||
|
<QuestionBirthplace |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "button": |
||||
|
return ( |
||||
|
<QuestionButton |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "checkbox": |
||||
|
return ( |
||||
|
<QuestionCheckbox |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "date": |
||||
|
return ( |
||||
|
<QuestionDate |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "dropdown": |
||||
|
return ( |
||||
|
<QuestionDropdown |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "file": |
||||
|
return ( |
||||
|
<QuestionFile |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "number": |
||||
|
if ( |
||||
|
question.title === "Age" && |
||||
|
dobQuestion && |
||||
|
dobQuestionIndex !== undefined |
||||
|
) { |
||||
|
return ( |
||||
|
<QuestionNumber |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
derivedFromQuestion={dobQuestion} |
||||
|
derivedFromQuestionIndex={dobQuestionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<QuestionNumber |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "phone": |
||||
|
return ( |
||||
|
<QuestionPhone |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "photo": |
||||
|
return ( |
||||
|
<QuestionPhoto |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "radio": |
||||
|
return ( |
||||
|
<QuestionRadio |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "slider": |
||||
|
case "scale": |
||||
|
return ( |
||||
|
<QuestionSlider |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
case "text": |
||||
|
return ( |
||||
|
<QuestionText |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
heightClassName={compactTextHeight} |
||||
|
disabled={disabled} |
||||
|
/> |
||||
|
); |
||||
|
default: |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default QuestionRenderer; |
||||
@ -0,0 +1,26 @@ |
|||||
|
import type { ReactNode } from "react"; |
||||
|
|
||||
|
type NoticeBoxProps = { |
||||
|
children?: ReactNode; |
||||
|
text?: string; |
||||
|
className?: string; |
||||
|
}; |
||||
|
|
||||
|
export function NoticeBox({ children, text, className = "" }: NoticeBoxProps) { |
||||
|
return ( |
||||
|
<div |
||||
|
className={[ |
||||
|
"mt-3 bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl text-center", |
||||
|
className, |
||||
|
] |
||||
|
.filter(Boolean) |
||||
|
.join(" ")} |
||||
|
> |
||||
|
<p className="text-[#F14B46] group-12 font-semibold text-center leading-[1.6]"> |
||||
|
{text || children} |
||||
|
</p> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default NoticeBox; |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue