Browse Source

feat: add QuestionCheckbox component for handling multiple choice questions with "Doesn't matter" option

master
sina_sajjadi 2 months ago
parent
commit
c14d577aa0
  1. 5
      src/app/layout.tsx
  2. 187
      src/app/questions-list/[slug]/question-detail-client.tsx
  3. 8
      src/components/questions/question-button.tsx
  4. 96
      src/components/questions/question-checkbox.tsx
  5. 29
      src/components/questions/question-date.tsx
  6. 24
      src/components/questions/question-dropdown.tsx
  7. 29
      src/components/questions/question-file.tsx
  8. 92
      src/components/questions/question-number.tsx
  9. 22
      src/components/questions/question-phone.tsx
  10. 26
      src/components/questions/question-photo.tsx
  11. 18
      src/components/questions/question-progress-tracker.tsx
  12. 54
      src/components/questions/question-radio.tsx
  13. 23
      src/components/questions/question-slider.tsx
  14. 7
      src/components/questions/question-snap-list.tsx
  15. 42
      src/components/questions/question-text.tsx
  16. 4
      src/components/questions/question-title.tsx
  17. 8
      src/data/question-data.ts
  18. 2
      src/hooks/marriage/types.ts
  19. 8
      src/i18n/dictionaries.ts
  20. 995
      src/i18n/locales/en/questions.json
  21. 993
      src/i18n/locales/fa/questions.json

5
src/app/layout.tsx

@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Amiri } from "next/font/google"; import { Amiri } from "next/font/google";
import localFont from "next/font/local"; import localFont from "next/font/local";
import Script from "next/script";
import DevClickToComponent from "@/components/dev/dev-click-to-component"; import DevClickToComponent from "@/components/dev/dev-click-to-component";
import Providers from "./providers"; import Providers from "./providers";
import "./globals.css"; import "./globals.css";
@ -31,7 +32,9 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<script
<Script
id="flutter-response-init"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

187
src/app/questions-list/[slug]/question-detail-client.tsx

@ -2,8 +2,12 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { QuestionAnswersProvider } from "@/components/questions/question-answer-storage";
import {
QuestionAnswersProvider,
useQuestionAnswers,
} from "@/components/questions/question-answer-storage";
import QuestionButton from "@/components/questions/question-button"; import QuestionButton from "@/components/questions/question-button";
import { QuestionCheckbox } from "@/components/questions/question-checkbox";
import QuestionDate from "@/components/questions/question-date"; import QuestionDate from "@/components/questions/question-date";
import QuestionDropdown from "@/components/questions/question-dropdown"; import QuestionDropdown from "@/components/questions/question-dropdown";
import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button"; import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button";
@ -127,6 +131,7 @@ function getStoredAge() {
function renderQuestion( function renderQuestion(
question: QuestionField, question: QuestionField,
questionIndex: number, questionIndex: number,
disabled?: boolean,
dobQuestion?: QuestionField, dobQuestion?: QuestionField,
dobQuestionIndex?: number, dobQuestionIndex?: number,
) { ) {
@ -136,8 +141,25 @@ function renderQuestion(
question.title.toLowerCase().includes("email") || question.title.toLowerCase().includes("email") ||
question.title.toLowerCase().includes("city") || question.title.toLowerCase().includes("city") ||
question.title.toLowerCase().includes("residence") || question.title.toLowerCase().includes("residence") ||
question.title.toLowerCase().includes("location")) &&
!question.title.toLowerCase().includes("describe") &&
question.title.toLowerCase().includes("location") ||
question.title.toLowerCase().includes("description") ||
question.title.toLowerCase().includes("short") ||
question.title.toLowerCase().includes("duration") ||
question.title.toLowerCase().includes("reason") ||
question.title.toLowerCase().includes("lifestyle") ||
question.title.toLowerCase().includes("marja") ||
question.title.toLowerCase().includes("range") ||
question.title.toLowerCase().includes("ethnicity") ||
question.title.toLowerCase().includes("nationality") ||
question.title.toLowerCase().includes("مدت") ||
question.title.toLowerCase().includes("علت") ||
question.title.toLowerCase().includes("بازه") ||
question.title.toLowerCase().includes("قومیت") ||
question.title.toLowerCase().includes("ملیت") ||
question.title.toLowerCase().includes("کوتاه") ||
question.title.toLowerCase().includes("سبک زندگی") ||
question.title.toLowerCase().includes("مرجع")) &&
!question.title.toLowerCase().includes("detailed") &&
!question.title.toLowerCase().includes("biography") !question.title.toLowerCase().includes("biography")
? "h-[54px] min-h-0 py-2" ? "h-[54px] min-h-0 py-2"
: undefined; : undefined;
@ -145,16 +167,44 @@ function renderQuestion(
switch (question.type) { switch (question.type) {
case "button": case "button":
return ( return (
<QuestionButton question={question} questionIndex={questionIndex} />
<QuestionButton
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "checkbox":
return (
<QuestionCheckbox
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
); );
case "date": case "date":
return <QuestionDate question={question} questionIndex={questionIndex} />;
return (
<QuestionDate
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "dropdown": case "dropdown":
return ( return (
<QuestionDropdown question={question} questionIndex={questionIndex} />
<QuestionDropdown
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
); );
case "file": case "file":
return <QuestionFile question={question} questionIndex={questionIndex} />;
return (
<QuestionFile
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "number": case "number":
if ( if (
question.title === "Age" && question.title === "Age" &&
@ -167,28 +217,49 @@ function renderQuestion(
questionIndex={questionIndex} questionIndex={questionIndex}
derivedFromQuestion={dobQuestion} derivedFromQuestion={dobQuestion}
derivedFromQuestionIndex={dobQuestionIndex} derivedFromQuestionIndex={dobQuestionIndex}
disabled={disabled}
/> />
); );
} }
return ( return (
<QuestionNumber question={question} questionIndex={questionIndex} />
<QuestionNumber
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
); );
case "phone": case "phone":
return ( return (
<QuestionPhone question={question} questionIndex={questionIndex} />
<QuestionPhone
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
); );
case "photo": case "photo":
return ( return (
<QuestionPhoto question={question} questionIndex={questionIndex} />
<QuestionPhoto
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
); );
case "radio": case "radio":
return ( return (
<QuestionRadio question={question} questionIndex={questionIndex} />
<QuestionRadio
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
); );
case "slider": case "slider":
return ( return (
<QuestionSlider question={question} questionIndex={questionIndex} />
<QuestionSlider
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
); );
case "text": case "text":
return ( return (
@ -196,6 +267,7 @@ function renderQuestion(
question={question} question={question}
questionIndex={questionIndex} questionIndex={questionIndex}
heightClassName={compactTextHeight} heightClassName={compactTextHeight}
disabled={disabled}
/> />
); );
default: default:
@ -203,6 +275,70 @@ function renderQuestion(
} }
} }
function QuestionFlowWrapper({
visibleQuestions,
itemSlug,
dobQuestion,
dobQuestionIndex,
requiredQuestionsCount,
continueLabel,
questionsListHref,
}: {
visibleQuestions: QuestionField[];
itemSlug: string;
dobQuestion?: QuestionField;
dobQuestionIndex?: number;
requiredQuestionsCount: number;
continueLabel: string;
questionsListHref: string;
}) {
const { getAnswerValue } = useQuestionAnswers();
return (
<QuestionSectionFlow
key={itemSlug}
total={requiredQuestionsCount}
continueLabel={continueLabel}
exitHref={questionsListHref}
>
{visibleQuestions.map((question, questionIndex) => {
let isDisabled = false;
if (question.logic?.dependsOn) {
const { title, values } = question.logic.dependsOn;
const dependentQuestionIndex = visibleQuestions.findIndex(
(q) => q.title === title,
);
if (dependentQuestionIndex !== -1) {
const answer = getAnswerValue(
visibleQuestions[dependentQuestionIndex],
dependentQuestionIndex,
);
isDisabled = !values.includes(String(answer));
}
}
return (
<div
key={`${itemSlug}-${question.title}`}
data-question-required={String(question.required)}
data-question-disabled={String(isDisabled)}
>
{renderQuestion(
question,
questionIndex,
isDisabled,
dobQuestion,
dobQuestionIndex,
)}
</div>
);
})}
</QuestionSectionFlow>
);
}
export default function QuestionDetailClient({ export default function QuestionDetailClient({
closeLabel, closeLabel,
continueLabel, continueLabel,
@ -298,26 +434,15 @@ export default function QuestionDetailClient({
</StickyHeader> </StickyHeader>
<div className="mx-auto flex w-full max-w-md flex-col px-[17px] pt-7"> <div className="mx-auto flex w-full max-w-md flex-col px-[17px] pt-7">
<QuestionSectionFlow
key={item.slug}
total={requiredQuestionsCount}
<QuestionFlowWrapper
visibleQuestions={visibleQuestions}
itemSlug={item.slug}
dobQuestion={dobQuestion}
dobQuestionIndex={dobQuestionIndex}
requiredQuestionsCount={requiredQuestionsCount}
continueLabel={continueLabel} continueLabel={continueLabel}
exitHref={questionsListHref}
>
{visibleQuestions.map((question, questionIndex) => (
<div
key={`${item.slug}-${question.title}`}
data-question-required={String(question.required)}
>
{renderQuestion(
question,
questionIndex,
dobQuestion,
dobQuestionIndex,
)}
</div>
))}
</QuestionSectionFlow>
questionsListHref={questionsListHref}
/>
</div> </div>
</main> </main>
</QuestionAnswersProvider> </QuestionAnswersProvider>

8
src/components/questions/question-button.tsx

@ -8,11 +8,13 @@ import QuestionTitle from "./question-title";
type QuestionButtonProps = { type QuestionButtonProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
disabled?: boolean;
}; };
export function QuestionButton({ export function QuestionButton({
question, question,
questionIndex, questionIndex,
disabled,
}: QuestionButtonProps) { }: QuestionButtonProps) {
const { setValue, value } = useQuestionAnswer(question, questionIndex); const { setValue, value } = useQuestionAnswer(question, questionIndex);
const isAnswered = value === true; const isAnswered = value === true;
@ -21,7 +23,10 @@ export function QuestionButton({
<div <div
data-question-answered={isAnswered ? "true" : undefined} data-question-answered={isAnswered ? "true" : undefined}
data-question-type={question.type} data-question-type={question.type}
className="space-y-3"
className={[
"space-y-3 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<QuestionTitle question={question} /> <QuestionTitle question={question} />
@ -35,6 +40,7 @@ export function QuestionButton({
</div> </div>
<button <button
type="button" type="button"
disabled={disabled}
onClick={() => setValue(true)} onClick={() => setValue(true)}
className="min-h-[50px] rounded-[15px] bg-[#181818] px-5 py-3 text-[15px] font-semibold text-white" className="min-h-[50px] rounded-[15px] bg-[#181818] px-5 py-3 text-[15px] font-semibold text-white"
> >

96
src/components/questions/question-checkbox.tsx

@ -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;

29
src/components/questions/question-date.tsx

@ -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>
); );
} }

24
src/components/questions/question-dropdown.tsx

@ -1,32 +1,36 @@
"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 QuestionDropdownProps = { type QuestionDropdownProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
disabled?: boolean;
}; };
export function QuestionDropdown({ export function QuestionDropdown({
question, question,
questionIndex, questionIndex,
disabled,
}: QuestionDropdownProps) { }: QuestionDropdownProps) {
const { setValue, value } = useQuestionAnswer(question, questionIndex);
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex);
const selectValue = typeof value === "string" ? value : ""; const selectValue = 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} />
<select <select
required={question.required}
value={selectValue} value={selectValue}
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]"
> >
<option value="" disabled> <option value="" disabled>
@ -38,7 +42,7 @@ export function QuestionDropdown({
</option> </option>
))} ))}
</select> </select>
</label>
</div>
); );
} }

29
src/components/questions/question-file.tsx

@ -4,26 +4,31 @@ import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media";
import { useQuestionAnswer } from "./question-answer-storage";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title"; import QuestionTitle from "./question-title";
type QuestionFileProps = { type QuestionFileProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
disabled?: boolean;
}; };
export function QuestionFile({ question, questionIndex }: QuestionFileProps) {
export function QuestionFile({
question,
questionIndex,
disabled,
}: QuestionFileProps) {
const [selectedFileName, setSelectedFileName] = useState<string | null>(null); const [selectedFileName, setSelectedFileName] = useState<string | null>(null);
const acceptedFiles = question.extras.options const acceptedFiles = question.extras.options
.map((option) => option.replace(/^\./, "")) .map((option) => option.replace(/^\./, ""))
.join(", "); .join(", ");
const { setValue } = useQuestionAnswer(question, questionIndex);
const { setAnswerValue } = useQuestionAnswers();
const uploadTmpMediaMutation = useUploadTmpMediaMutation({ const uploadTmpMediaMutation = useUploadTmpMediaMutation({
onSuccess: (response) => { onSuccess: (response) => {
setValue(response.path);
setAnswerValue(question, questionIndex, response.path);
}, },
onError: () => { onError: () => {
setValue(null);
setAnswerValue(question, questionIndex, null);
}, },
}); });
@ -32,7 +37,7 @@ export function QuestionFile({ question, questionIndex }: QuestionFileProps) {
if (!file) { if (!file) {
setSelectedFileName(null); setSelectedFileName(null);
setValue(null);
setAnswerValue(question, questionIndex, null);
return; return;
} }
@ -41,14 +46,18 @@ export function QuestionFile({ question, questionIndex }: QuestionFileProps) {
} }
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} />
<span className="relative flex aspect-[727/330] min-h-[156px] w-full cursor-pointer flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#6F6F6F] hover:border-[#777777]"> <span className="relative flex aspect-[727/330] min-h-[156px] w-full cursor-pointer flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#6F6F6F] hover:border-[#777777]">
<input <input
type="file" type="file"
required={question.required}
accept={question.extras.options.join(",")} accept={question.extras.options.join(",")}
disabled={uploadTmpMediaMutation.isPending}
disabled={disabled || uploadTmpMediaMutation.isPending}
onChange={(event) => handleFileChange(event.target.files)} onChange={(event) => handleFileChange(event.target.files)}
className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0"
/> />
@ -73,7 +82,7 @@ export function QuestionFile({ question, questionIndex }: QuestionFileProps) {
</span> </span>
) : null} ) : null}
</span> </span>
</label>
</div>
); );
} }

92
src/components/questions/question-number.tsx

@ -1,76 +1,80 @@
"use client"; "use client";
import { useI18n } from "@/i18n/provider";
import { useMemo, useEffect } from "react";
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";
import { useI18n } from "@/i18n/provider";
type QuestionNumberProps = { type QuestionNumberProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
disabled?: boolean; disabled?: boolean;
valueOverride?: string;
derivedFromQuestion?: QuestionField; derivedFromQuestion?: QuestionField;
derivedFromQuestionIndex?: number; derivedFromQuestionIndex?: number;
}; };
export function QuestionNumber({
export default function QuestionNumber({
question, question,
questionIndex, questionIndex,
disabled = false,
valueOverride,
disabled,
derivedFromQuestion, derivedFromQuestion,
derivedFromQuestionIndex, derivedFromQuestionIndex,
}: QuestionNumberProps) { }: QuestionNumberProps) {
const { dictionary } = useI18n();
const { dictionary: t } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex);
const derivedValue =
derivedFromQuestion && derivedFromQuestionIndex !== undefined
? getAnswerValue(derivedFromQuestion, derivedFromQuestionIndex)
: null;
useEffect(() => {
if (derivedFromQuestion && typeof derivedValue === "string") {
const age = calculateAge(derivedValue);
if (age !== String(value)) {
setAnswerValue(question, questionIndex, age);
}
}
}, [derivedFromQuestion, derivedValue, question, questionIndex, setAnswerValue, value]);
const [min, max] = question.extras.range; const [min, max] = question.extras.range;
const { setValue, value } = useQuestionAnswer(question, questionIndex);
const derivedAnswer = useQuestionAnswer(
derivedFromQuestion ?? question,
derivedFromQuestionIndex ?? questionIndex,
).value;
const derivedDateValue =
question.title === "Age" && typeof derivedAnswer === "string"
? calculateAge(derivedAnswer)
: undefined;
const inputValue =
valueOverride ??
derivedDateValue ??
(typeof value === "number" || typeof value === "string"
? String(value)
: "");
const numValue = typeof value === "number" ? value : (typeof value === "string" ? parseFloat(value) : NaN);
const isOutOfRange = useMemo(() => {
if (Number.isNaN(numValue)) return false;
if (min !== 0 && numValue < min) return true;
if (max !== 0 && numValue > max) return true;
return false;
}, [numValue, min, max]);
const numericValue = Number.parseFloat(inputValue);
const isOutOfRange =
!Number.isNaN(numericValue) &&
((min > 0 && numericValue < min) || (max > 0 && numericValue > max));
const inputValue = value === null ? "" : 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="number" type="number"
required={question.required}
disabled={disabled || question.title === "Age"}
required={question.required && !disabled}
disabled={disabled || Boolean(derivedFromQuestion)}
min={min || undefined} min={min || undefined}
max={max || undefined} max={max || undefined}
placeholder={question.extras.placeHolder} placeholder={question.extras.placeHolder}
value={inputValue} value={inputValue}
onChange={(event) => { onChange={(event) => {
if (disabled) {
return;
}
const nextValue = event.target.value; const nextValue = event.target.value;
if (nextValue.length === 0) {
setValue(null);
return;
if (nextValue === "") {
setAnswerValue(question, questionIndex, null);
} else {
const parsed = parseFloat(nextValue);
setAnswerValue(question, questionIndex, Number.isNaN(parsed) ? nextValue : parsed);
} }
const parsedValue = event.target.valueAsNumber;
setValue(Number.isNaN(parsedValue) ? nextValue : parsedValue);
}} }}
className={[ className={[
"h-[54px] w-full rounded-[15px] border px-4 text-[15px] text-[#181818] outline-none placeholder:text-[#9D8F8C] focus:border-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]", "h-[54px] w-full rounded-[15px] border px-4 text-[15px] text-[#181818] outline-none placeholder:text-[#9D8F8C] focus:border-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
@ -79,10 +83,10 @@ export function QuestionNumber({
/> />
{isOutOfRange ? ( {isOutOfRange ? (
<span className="block text-[10px] font-semibold text-[#F2465F]"> <span className="block text-[10px] font-semibold text-[#F2465F]">
{dictionary.common.rangeError}
{t.common.rangeError || `Please enter a value between ${min} and ${max}`}
</span> </span>
) : null} ) : null}
</label>
</div>
); );
} }
@ -106,5 +110,3 @@ function calculateAge(dateOfBirth: string) {
return String(Math.max(age, 0)); return String(Math.max(age, 0));
} }
export default QuestionNumber;

22
src/components/questions/question-phone.tsx

@ -4,13 +4,14 @@ import { useEffect, useRef, useState } from "react";
import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber"; import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types"; import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types";
import { useQuestionAnswer } from "./question-answer-storage";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title"; import QuestionTitle from "./question-title";
type QuestionPhoneProps = { type QuestionPhoneProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
countryCode?: string; countryCode?: string;
disabled?: boolean;
}; };
type PhoneValueParts = { type PhoneValueParts = {
@ -192,8 +193,10 @@ export function QuestionPhone({
question, question,
questionIndex, questionIndex,
countryCode = "+98", countryCode = "+98",
disabled,
}: QuestionPhoneProps) { }: QuestionPhoneProps) {
const { setValue, value } = useQuestionAnswer(question, questionIndex);
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex);
const defaultCodeValue = countryCode.trim() || "+98"; const defaultCodeValue = countryCode.trim() || "+98";
const initialValue = readPhoneValue(value, defaultCodeValue); const initialValue = readPhoneValue(value, defaultCodeValue);
const [codeValue, setCodeValue] = useState(initialValue.codeValue); const [codeValue, setCodeValue] = useState(initialValue.codeValue);
@ -235,14 +238,17 @@ export function QuestionPhone({
: null; : null;
lastCommittedValueRef.current = nextValue; lastCommittedValueRef.current = nextValue;
setValue(nextValue);
setAnswerValue(question, questionIndex, nextValue);
}; };
return ( return (
<label
<div
data-question-answered={isAnswered ? "true" : "false"} data-question-answered={isAnswered ? "true" : "false"}
data-question-type={question.type} data-question-type={question.type}
className="block space-y-3"
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} />
<div <div
@ -258,7 +264,7 @@ export function QuestionPhone({
inputMode="tel" inputMode="tel"
aria-label="Country code" aria-label="Country code"
maxLength={4} maxLength={4}
required={question.required}
disabled={disabled}
placeholder={defaultCodeValue} placeholder={defaultCodeValue}
value={codeValue} value={codeValue}
onChange={(event) => { onChange={(event) => {
@ -275,7 +281,7 @@ export function QuestionPhone({
<input <input
type="tel" type="tel"
inputMode="tel" inputMode="tel"
required={question.required}
disabled={disabled}
placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")} placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")}
value={phoneValue} value={phoneValue}
onChange={(event) => { onChange={(event) => {
@ -294,7 +300,7 @@ export function QuestionPhone({
Enter a valid phone number with country code. Enter a valid phone number with country code.
</span> </span>
) : null} ) : null}
</label>
</div>
); );
} }

26
src/components/questions/question-photo.tsx

@ -3,12 +3,13 @@
import Image from "next/image"; import Image from "next/image";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
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";
type QuestionPhotoProps = { type QuestionPhotoProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
description?: ReactNode; description?: ReactNode;
disabled?: boolean;
}; };
function getFileInputValue(files: FileList | null) { function getFileInputValue(files: FileList | null) {
@ -21,23 +22,31 @@ export function QuestionPhoto({
question, question,
questionIndex, questionIndex,
description, description,
disabled,
}: QuestionPhotoProps) { }: QuestionPhotoProps) {
const acceptedFiles = question.extras.options.join(","); const acceptedFiles = question.extras.options.join(",");
const descriptionContent = description ?? question.description; const descriptionContent = description ?? question.description;
const { setValue } = useQuestionAnswer(question, questionIndex);
const { setAnswerValue } = useQuestionAnswers();
return ( return (
<label
data-question-type={question.type}
className="block cursor-pointer text-center"
<div
className={[
"flex w-full flex-col items-center text-center transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
> >
<input <input
type="file" type="file"
required={question.required}
accept={acceptedFiles} accept={acceptedFiles}
onChange={(event) => setValue(getFileInputValue(event.target.files))}
onChange={(event) => setAnswerValue(question, questionIndex, getFileInputValue(event.target.files))}
disabled={disabled}
className="sr-only" className="sr-only"
id={`photo-input-${questionIndex}`}
/> />
<label
htmlFor={`photo-input-${questionIndex}`}
className="flex w-full cursor-pointer flex-col items-center"
>
<span className="flex w-full flex-col items-center"> <span className="flex w-full flex-col items-center">
<Image <Image
src="/assets/images/Frame 2095586679.svg" src="/assets/images/Frame 2095586679.svg"
@ -59,7 +68,8 @@ export function QuestionPhoto({
</span> </span>
) : null} ) : null}
</span> </span>
</label>
</label>
</div>
); );
} }

18
src/components/questions/question-progress-tracker.tsx

@ -47,10 +47,11 @@ function isQuestionAnswered(question: Element) {
export function QuestionProgressTracker({ export function QuestionProgressTracker({
children, children,
total,
total: initialTotal,
}: QuestionProgressTrackerProps) { }: QuestionProgressTrackerProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [answered, setAnswered] = useState(0); const [answered, setAnswered] = useState(0);
const [total, setTotal] = useState(initialTotal);
const safeTotal = Math.max(total, 0); const safeTotal = Math.max(total, 0);
const progress = safeTotal > 0 ? (answered / safeTotal) * 100 : 0; const progress = safeTotal > 0 ? (answered / safeTotal) * 100 : 0;
@ -61,13 +62,16 @@ export function QuestionProgressTracker({
return; return;
} }
const questions = Array.from(
container.querySelectorAll("[data-question-type]"),
).filter((el) => el.getAttribute("data-question-required") === "true");
const nextAnswered = questions.filter(isQuestionAnswered).length;
const requiredQuestions = Array.from(
container.querySelectorAll("[data-question-required='true']"),
).filter((el) => el.getAttribute("data-question-disabled") !== "true");
setAnswered(Math.min(nextAnswered, safeTotal));
}, [safeTotal]);
const nextTotal = requiredQuestions.length;
const nextAnswered = requiredQuestions.filter(isQuestionAnswered).length;
setTotal(nextTotal);
setAnswered(nextAnswered);
}, []);
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;

54
src/components/questions/question-radio.tsx

@ -3,62 +3,70 @@
import { useId } from "react"; import { useId } from "react";
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 QuestionRadioProps = { type QuestionRadioProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
disabled?: boolean;
}; };
export function QuestionRadio({ question, questionIndex }: QuestionRadioProps) {
export function QuestionRadio({
question,
questionIndex,
disabled,
}: QuestionRadioProps) {
const groupId = useId(); const groupId = useId();
const options = question.extras.options; const options = question.extras.options;
const { setValue, value } = useQuestionAnswer(question, questionIndex);
const selectedOption = typeof value === "string" ? value : undefined;
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex);
if (options.length === 0) {
if (question.extras.options.length === 0) {
return null; return null;
} }
return ( return (
<fieldset data-question-type={question.type} className="space-y-7">
<legend>
<QuestionTitle question={question} />
</legend>
<div className="flex flex-wrap gap-x-5 gap-y-4">
{options.map((option) => {
const optionId = `${groupId}-${option}`;
const isSelected = selectedOption === option;
<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">
{question.extras.options.map((option) => {
const optionId = `question-${questionIndex}-${option}`;
const isSelected = String(value) === option;
return ( return (
<label <label
key={option} key={option}
htmlFor={optionId} htmlFor={optionId}
className={[ className={[
"cursor-pointer rounded-[10px] px-3.5 py-2",
"text-center leading-none font-semibold transition-colors",
"w-full cursor-pointer rounded-[12px] px-4 py-3.5",
"text-start text-[13px] leading-tight font-semibold transition-all duration-200",
isSelected isSelected
? "bg-[#F0445B] text-white"
: "bg-[#DBDBDB] text-[#181818]",
? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]"
: "bg-[#F5F5F5] text-[#181818] hover:bg-[#EBEBEB]",
].join(" ")} ].join(" ")}
> >
<input <input
id={optionId}
className="sr-only"
type="radio" type="radio"
name={groupId}
id={optionId}
name={`question-${questionIndex}`}
value={option} value={option}
checked={isSelected} checked={isSelected}
onChange={() => setValue(option)}
disabled={disabled}
onChange={() => setAnswerValue(question, questionIndex, option)}
className="sr-only"
/> />
{option} {option}
</label> </label>
); );
})} })}
</div> </div>
</fieldset>
</div>
); );
} }

23
src/components/questions/question-slider.tsx

@ -2,24 +2,24 @@
import { useLayoutEffect, useRef, useState } from "react"; import { useLayoutEffect, useRef, useState } from "react";
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 QuestionSliderProps = { type QuestionSliderProps = {
question: QuestionField; question: QuestionField;
questionIndex: number; questionIndex: number;
disabled?: boolean;
}; };
export function QuestionSlider({ export function QuestionSlider({
question, question,
questionIndex, questionIndex,
disabled,
}: QuestionSliderProps) { }: QuestionSliderProps) {
const [min, max] = question.extras.range; const [min, max] = question.extras.range;
const initialValue = Math.round((min + max) / 2); const initialValue = Math.round((min + max) / 2);
const { setValue, value: storedValue } = useQuestionAnswer(
question,
questionIndex,
);
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question, questionIndex);
const value = typeof storedValue === "number" ? storedValue : initialValue; const value = typeof storedValue === "number" ? storedValue : initialValue;
const progress = max === min ? 0 : ((value - min) / (max - min)) * 100; const progress = max === min ? 0 : ((value - min) / (max - min)) * 100;
const sliderWrapperRef = useRef<HTMLDivElement>(null); const sliderWrapperRef = useRef<HTMLDivElement>(null);
@ -67,7 +67,12 @@ export function QuestionSlider({
}, [progress]); }, [progress]);
return ( return (
<label data-question-type={question.type} className="block space-y-6">
<div
className={[
"flex w-full flex-col gap-6 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} /> <QuestionTitle question={question} />
<div className="pt-9"> <div className="pt-9">
<div ref={sliderWrapperRef} className="relative"> <div ref={sliderWrapperRef} className="relative">
@ -80,12 +85,12 @@ export function QuestionSlider({
<input <input
ref={sliderRef} ref={sliderRef}
type="range" type="range"
required={question.required}
min={min} min={min}
max={max} max={max}
step={1} step={1}
value={value} value={value}
onChange={(event) => setValue(Number(event.target.value))}
onChange={(event) => setAnswerValue(question, questionIndex, Number(event.target.value))}
disabled={disabled}
className="question-slider-range w-full" className="question-slider-range w-full"
style={{ style={{
background: `linear-gradient(to right, #F43F5E 0%, #F43F5E ${progress}%, #FAD1D8 ${progress}%, #FAD1D8 100%)`, background: `linear-gradient(to right, #F43F5E 0%, #F43F5E ${progress}%, #FAD1D8 ${progress}%, #FAD1D8 100%)`,
@ -98,7 +103,7 @@ export function QuestionSlider({
))} ))}
</div> </div>
</div> </div>
</label>
</div>
); );
} }

7
src/components/questions/question-snap-list.tsx

@ -13,7 +13,7 @@ import {
const WHEEL_GESTURE_IDLE_MS = 320; const WHEEL_GESTURE_IDLE_MS = 320;
const TOUCH_MIN_DISTANCE = 8; const TOUCH_MIN_DISTANCE = 8;
const AUTO_FOCUS_SELECTOR = [ const AUTO_FOCUS_SELECTOR = [
'textarea:not([disabled])',
"textarea:not([disabled])",
'input[type="text"]:not([disabled])', 'input[type="text"]:not([disabled])',
'input[type="number"]:not([disabled])', 'input[type="number"]:not([disabled])',
].join(", "); ].join(", ");
@ -88,7 +88,10 @@ export function QuestionSnapList({
const valueLength = firstFocusableInput.value.length; const valueLength = firstFocusableInput.value.length;
if (valueLength > 0 && typeof firstFocusableInput.setSelectionRange === "function") {
if (
valueLength > 0 &&
typeof firstFocusableInput.setSelectionRange === "function"
) {
firstFocusableInput.setSelectionRange(valueLength, valueLength); firstFocusableInput.setSelectionRange(valueLength, valueLength);
} }
}); });

42
src/components/questions/question-text.tsx

@ -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;

4
src/components/questions/question-title.tsx

@ -10,10 +10,10 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
return ( return (
<span className="block"> <span className="block">
{question.private ? ( {question.private ? (
<span className="mb-3 flex min-h-[42px] w-full items-center gap-3 rounded-[22px] bg-[#F6D7D8] px-5 py-2 text-[14px] leading-tight font-semibold text-[#DF4146]">
<span className="mb-3 mx-auto flex w-fit items-center gap-1.5 rounded-[10px] bg-[#F6D7D8] px-2.5 py-1.5 text-[10px] leading-tight font-semibold text-[#DF4146]">
<IoEyeOff <IoEyeOff
aria-hidden="true" aria-hidden="true"
className="shrink-0 text-[24px] text-[#DF4146]"
className="shrink-0 text-[18px] text-[#DF4146]"
/> />
<span>Private Field (Visible to Habib Marriage advisors only)</span> <span>Private Field (Visible to Habib Marriage advisors only)</span>
</span> </span>

8
src/data/question-data.ts

@ -28,6 +28,13 @@ type QuestionAudienceRule = {
minAge?: number; minAge?: number;
}; };
export type QuestionLogic = {
dependsOn: {
title: string;
values: string[];
};
};
export type QuestionField = { export type QuestionField = {
title: string; title: string;
type: string; type: string;
@ -38,6 +45,7 @@ export type QuestionField = {
extras: QuestionExtras; extras: QuestionExtras;
audience?: QuestionAudienceRule; audience?: QuestionAudienceRule;
requiredWhen?: QuestionAudienceRule; requiredWhen?: QuestionAudienceRule;
logic?: QuestionLogic;
}; };
export type QuestionListItem = { export type QuestionListItem = {

2
src/hooks/marriage/types.ts

@ -32,6 +32,7 @@ export type MarriagePhoneFieldValue = {
export type MarriageFieldValue = export type MarriageFieldValue =
| string | string
| string[]
| number | number
| boolean | boolean
| MarriagePhoneFieldValue | MarriagePhoneFieldValue
@ -83,6 +84,7 @@ export type MarriageProfile = {
id: number; id: number;
status: MarriageProfileStatus; status: MarriageProfileStatus;
gender: MarriageGender | null; gender: MarriageGender | null;
age: number | null;
is_registering_for_self: boolean | null; is_registering_for_self: boolean | null;
can_edit_profile: boolean; can_edit_profile: boolean;
can_message_expert: boolean; can_message_expert: boolean;

8
src/i18n/dictionaries.ts

@ -35,10 +35,10 @@ export const dictionaries = {
requiredStepsProgress: "{completed} of {total} required steps completed", requiredStepsProgress: "{completed} of {total} required steps completed",
findMatches: "Find Matches", findMatches: "Find Matches",
findingMatch: "Finding Match", findingMatch: "Finding Match",
optionalInfoPromptTitle: "Important point",
optionalInfoPromptTitle: "Important Note",
optionalInfoPromptDescription: optionalInfoPromptDescription:
"You've completed all required fields. However, filling in all sections will help us find better matches for you", "You've completed all required fields. However, filling in all sections will help us find better matches for you",
completeNecessaryForms: "(Complete Necessary forms)",
completeNecessaryForms: "(Complete Required Forms)",
openQuestion: "Open {title}", openQuestion: "Open {title}",
answerAtYourOwnPace: "Answer at Your Own Pace", answerAtYourOwnPace: "Answer at Your Own Pace",
answerAtYourOwnPaceDescription: answerAtYourOwnPaceDescription:
@ -80,7 +80,7 @@ export const dictionaries = {
"You can now view their family's contact details and arrange further steps.", "You can now view their family's contact details and arrange further steps.",
viewContact: "View Contact", viewContact: "View Contact",
penalty: penalty:
"Please note: if you do not make contact within 2 days, a penalty may apply",
"Please note: Failure to contact within 2 days may result in a penalty",
profileLocked: "Profile is locked", profileLocked: "Profile is locked",
lockedDescription: lockedDescription:
"You can't edit your profile while we're searching for matches", "You can't edit your profile while we're searching for matches",
@ -97,7 +97,7 @@ export const dictionaries = {
}, },
candidateContact: { candidateContact: {
imageAlt: "Selected candidate contact status", imageAlt: "Selected candidate contact status",
title: "The selected candidate will contact your family shortly.",
title: "The selected candidate will be in touch with your family shortly.",
contacted: "contacted", contacted: "contacted",
noContactYet: "no contact yet ?", noContactYet: "no contact yet ?",
afterTwoDays: "(after 2 days)", afterTwoDays: "(after 2 days)",

995
src/i18n/locales/en/questions.json
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

Loading…
Cancel
Save