Browse Source
feat: add QuestionCheckbox component for handling multiple choice questions with "Doesn't matter" option
master
feat: add QuestionCheckbox component for handling multiple choice questions with "Doesn't matter" option
master
21 changed files with 2206 additions and 466 deletions
-
5src/app/layout.tsx
-
187src/app/questions-list/[slug]/question-detail-client.tsx
-
8src/components/questions/question-button.tsx
-
96src/components/questions/question-checkbox.tsx
-
29src/components/questions/question-date.tsx
-
24src/components/questions/question-dropdown.tsx
-
29src/components/questions/question-file.tsx
-
92src/components/questions/question-number.tsx
-
22src/components/questions/question-phone.tsx
-
26src/components/questions/question-photo.tsx
-
18src/components/questions/question-progress-tracker.tsx
-
54src/components/questions/question-radio.tsx
-
23src/components/questions/question-slider.tsx
-
7src/components/questions/question-snap-list.tsx
-
42src/components/questions/question-text.tsx
-
4src/components/questions/question-title.tsx
-
8src/data/question-data.ts
-
2src/hooks/marriage/types.ts
-
8src/i18n/dictionaries.ts
-
995src/i18n/locales/en/questions.json
-
993src/i18n/locales/fa/questions.json
@ -0,0 +1,96 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useId } from "react"; |
||||
|
import type { QuestionField } from "@/data/question-data"; |
||||
|
import { useQuestionAnswers } from "./question-answer-storage"; |
||||
|
import QuestionTitle from "./question-title"; |
||||
|
|
||||
|
type QuestionCheckboxProps = { |
||||
|
question: QuestionField; |
||||
|
questionIndex: number; |
||||
|
disabled?: boolean; |
||||
|
}; |
||||
|
|
||||
|
export function QuestionCheckbox({ |
||||
|
question, |
||||
|
questionIndex, |
||||
|
disabled, |
||||
|
}: QuestionCheckboxProps) { |
||||
|
const groupId = useId(); |
||||
|
const options = question.extras.options; |
||||
|
const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); |
||||
|
const rawValue = getAnswerValue(question, questionIndex); |
||||
|
|
||||
|
// Ensure value is an array
|
||||
|
const value = Array.isArray(rawValue) ? rawValue : (rawValue ? [String(rawValue)] : []); |
||||
|
|
||||
|
if (options.length === 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const toggleOption = (option: string) => { |
||||
|
let nextValue: string[]; |
||||
|
if (value.includes(option)) { |
||||
|
nextValue = value.filter((v) => v !== option); |
||||
|
} else { |
||||
|
nextValue = [...value, option]; |
||||
|
} |
||||
|
|
||||
|
// Handle "Doesn't matter" (مهم نیست) logic if it exists
|
||||
|
const doesnMatterOption = options.find(o => o.includes("مهم نیست") || o.includes("Doesn't matter")); |
||||
|
if (doesnMatterOption) { |
||||
|
if (option === doesnMatterOption) { |
||||
|
// If "Doesn't matter" is selected, clear everything else
|
||||
|
nextValue = [doesnMatterOption]; |
||||
|
} else if (nextValue.includes(doesnMatterOption)) { |
||||
|
// If something else is selected, remove "Doesn't matter"
|
||||
|
nextValue = nextValue.filter(v => v !== doesnMatterOption); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setAnswerValue(question, questionIndex, nextValue.length > 0 ? nextValue : null); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
className={[ |
||||
|
"flex w-full flex-col gap-3 transition-opacity duration-200", |
||||
|
disabled ? "pointer-events-none opacity-30" : "", |
||||
|
].join(" ")} |
||||
|
> |
||||
|
<QuestionTitle question={question} /> |
||||
|
<div className="flex flex-col gap-3"> |
||||
|
{options.map((option) => { |
||||
|
const optionId = `checkbox-${questionIndex}-${option}`; |
||||
|
const isSelected = value.includes(option); |
||||
|
|
||||
|
return ( |
||||
|
<label |
||||
|
key={option} |
||||
|
htmlFor={optionId} |
||||
|
className={[ |
||||
|
"w-full cursor-pointer rounded-[12px] px-4 py-3.5", |
||||
|
"text-start text-[13px] leading-tight font-semibold transition-all duration-200", |
||||
|
isSelected |
||||
|
? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]" |
||||
|
: "bg-[#F5F5F5] text-[#181818] hover:bg-[#EBEBEB]", |
||||
|
].join(" ")} |
||||
|
> |
||||
|
<input |
||||
|
type="checkbox" |
||||
|
id={optionId} |
||||
|
checked={isSelected} |
||||
|
disabled={disabled} |
||||
|
onChange={() => toggleOption(option)} |
||||
|
className="sr-only" |
||||
|
/> |
||||
|
{option} |
||||
|
</label> |
||||
|
); |
||||
|
})} |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default QuestionCheckbox; |
||||
@ -1,33 +1,40 @@ |
|||||
"use client"; |
"use client"; |
||||
|
|
||||
import type { QuestionField } from "@/data/question-data"; |
import type { QuestionField } from "@/data/question-data"; |
||||
import { useQuestionAnswer } from "./question-answer-storage"; |
|
||||
|
import { useQuestionAnswers } from "./question-answer-storage"; |
||||
import QuestionTitle from "./question-title"; |
import QuestionTitle from "./question-title"; |
||||
|
|
||||
type QuestionDateProps = { |
type QuestionDateProps = { |
||||
question: QuestionField; |
question: QuestionField; |
||||
questionIndex: number; |
questionIndex: number; |
||||
|
disabled?: boolean; |
||||
}; |
}; |
||||
|
|
||||
export function QuestionDate({ question, questionIndex }: QuestionDateProps) { |
|
||||
const { setValue, value } = useQuestionAnswer(question, questionIndex); |
|
||||
|
export function QuestionDate({ |
||||
|
question, |
||||
|
questionIndex, |
||||
|
disabled, |
||||
|
}: QuestionDateProps) { |
||||
|
const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); |
||||
|
const value = getAnswerValue(question, questionIndex); |
||||
const dateValue = typeof value === "string" ? value : ""; |
const dateValue = typeof value === "string" ? value : ""; |
||||
|
|
||||
return ( |
return ( |
||||
<label data-question-type={question.type} className="block space-y-3"> |
|
||||
|
<div |
||||
|
className={[ |
||||
|
"flex w-full flex-col gap-2 transition-opacity duration-200", |
||||
|
disabled ? "pointer-events-none opacity-30" : "", |
||||
|
].join(" ")} |
||||
|
> |
||||
<QuestionTitle question={question} /> |
<QuestionTitle question={question} /> |
||||
<input |
<input |
||||
type="date" |
type="date" |
||||
required={question.required} |
|
||||
value={dateValue} |
value={dateValue} |
||||
onChange={(event) => { |
|
||||
const nextValue = event.target.value; |
|
||||
|
|
||||
setValue(nextValue.length > 0 ? nextValue : null); |
|
||||
}} |
|
||||
|
onChange={(event) => setAnswerValue(question, questionIndex, event.target.value)} |
||||
|
disabled={disabled} |
||||
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 text-[15px] text-[#181818] outline-none focus:border-[#6F6F6F]" |
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 text-[15px] text-[#181818] outline-none focus:border-[#6F6F6F]" |
||||
/> |
/> |
||||
</label> |
|
||||
|
</div> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
|
|||||
@ -1,47 +1,51 @@ |
|||||
"use client"; |
"use client"; |
||||
|
|
||||
import type { QuestionField } from "@/data/question-data"; |
import type { QuestionField } from "@/data/question-data"; |
||||
import { useQuestionAnswer } from "./question-answer-storage"; |
|
||||
|
import { useQuestionAnswers } from "./question-answer-storage"; |
||||
import QuestionTitle from "./question-title"; |
import QuestionTitle from "./question-title"; |
||||
|
|
||||
type QuestionTextProps = { |
type QuestionTextProps = { |
||||
question: QuestionField; |
question: QuestionField; |
||||
questionIndex: number; |
questionIndex: number; |
||||
description?: string; |
description?: string; |
||||
|
disabled?: boolean; |
||||
heightClassName?: string; |
heightClassName?: string; |
||||
}; |
}; |
||||
|
|
||||
export function QuestionText({ |
|
||||
|
export default function QuestionText({ |
||||
question, |
question, |
||||
questionIndex, |
questionIndex, |
||||
description, |
description, |
||||
heightClassName = "min-h-[116px]", |
|
||||
|
disabled, |
||||
|
heightClassName, |
||||
}: QuestionTextProps) { |
}: QuestionTextProps) { |
||||
const { setValue, value } = useQuestionAnswer(question, questionIndex); |
|
||||
const textValue = typeof value === "string" ? value : ""; |
|
||||
|
const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); |
||||
|
const value = getAnswerValue(question, questionIndex); |
||||
|
|
||||
return ( |
return ( |
||||
<label data-question-type={question.type} className="block space-y-3"> |
|
||||
|
<div |
||||
|
className={[ |
||||
|
"flex w-full flex-col gap-2 transition-opacity duration-200", |
||||
|
disabled ? "pointer-events-none opacity-30" : "", |
||||
|
].join(" ")} |
||||
|
> |
||||
<QuestionTitle question={question} /> |
<QuestionTitle question={question} /> |
||||
<textarea |
|
||||
required={question.required} |
|
||||
rows={1} |
|
||||
|
<input |
||||
|
type="text" |
||||
|
value={String(value ?? "")} |
||||
|
onChange={(e) => setAnswerValue(question, questionIndex, e.target.value)} |
||||
placeholder={question.extras.placeHolder} |
placeholder={question.extras.placeHolder} |
||||
value={textValue} |
|
||||
onChange={(event) => { |
|
||||
const nextValue = event.target.value; |
|
||||
|
|
||||
setValue(nextValue.length > 0 ? nextValue : null); |
|
||||
}} |
|
||||
className={`w-full resize-none rounded-[15px] border border-[#8B8B8B] bg-white px-4 py-3 text-[#181818] outline-none placeholder:text-[#8B8B8B] focus:border-[#6F6F6F] ${heightClassName}`} |
|
||||
|
disabled={disabled} |
||||
|
className={[ |
||||
|
"w-full rounded-[10px] bg-[#DBDBDB] px-[14px] py-[10px] text-[13px] font-semibold text-[#181818] outline-none placeholder:text-[#808080]", |
||||
|
heightClassName || "min-h-[116px]", |
||||
|
].join(" ")} |
||||
/> |
/> |
||||
{description ? ( |
{description ? ( |
||||
<span className="block text-[10px] font-semibold text-[#747474]"> |
<span className="block text-[10px] font-semibold text-[#747474]"> |
||||
{description} |
{description} |
||||
</span> |
</span> |
||||
) : null} |
) : null} |
||||
</label> |
|
||||
|
</div> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
export default QuestionText; |
|
||||
995
src/i18n/locales/en/questions.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
993
src/i18n/locales/fa/questions.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue