Browse Source

feat: implement question flow system with localized support and UI components

front-test-2
ghorbani 3 weeks ago
parent
commit
8ab2bef94d
  1. 28
      src/app/globals.css
  2. 259
      src/app/questions-list/[slug]/question-detail-client.tsx
  3. 72
      src/components/questions/conditional-questions.tsx
  4. 16
      src/components/questions/progress-helper.ts
  5. 160
      src/components/questions/question-renderer.tsx
  6. 88
      src/components/questions/question-section-flow.tsx
  7. 48
      src/components/questions/question-slider.tsx
  8. 14
      src/components/questions/question-snap-list.tsx
  9. 4
      src/components/questions/question-title.tsx
  10. 12
      src/components/sliders/slider-slide-four.tsx
  11. 26
      src/components/ui/notice-box.tsx
  12. 1
      src/data/question-data.ts
  13. 48
      src/data/questions/en.json
  14. 46
      src/data/questions/fa.json
  15. 4
      src/translations/locales/ar.json
  16. 4
      src/translations/locales/az.json
  17. 4
      src/translations/locales/bn.json
  18. 4
      src/translations/locales/da.json
  19. 4
      src/translations/locales/de.json
  20. 4
      src/translations/locales/en.json
  21. 4
      src/translations/locales/es.json
  22. 4
      src/translations/locales/fa.json
  23. 4
      src/translations/locales/fr.json
  24. 4
      src/translations/locales/gu.json
  25. 4
      src/translations/locales/ha.json
  26. 4
      src/translations/locales/he.json
  27. 4
      src/translations/locales/hi.json
  28. 4
      src/translations/locales/id.json
  29. 4
      src/translations/locales/ks.json
  30. 4
      src/translations/locales/pt.json
  31. 4
      src/translations/locales/ru.json
  32. 4
      src/translations/locales/sw.json
  33. 4
      src/translations/locales/tg.json
  34. 4
      src/translations/locales/tr.json
  35. 4
      src/translations/locales/ul.json
  36. 4
      src/translations/locales/ur.json
  37. 4
      src/translations/locales/uz.json
  38. 4
      src/translations/locales/zh.json

28
src/app/globals.css

@ -112,54 +112,54 @@ body[data-page-background="custom"] .app-shell {
} }
.question-slider-range { .question-slider-range {
height: 6px;
width: calc(100% - 18px);
margin-inline-start: 9px;
height: 18px;
width: 100%;
cursor: pointer; cursor: pointer;
appearance: none; appearance: none;
border-radius: 999px; border-radius: 999px;
outline: none; outline: none;
background: transparent;
} }
.question-slider-range::-webkit-slider-runnable-track { .question-slider-range::-webkit-slider-runnable-track {
height: 14px;
height: 10px;
border-radius: 999px; border-radius: 999px;
} }
.question-slider-range::-webkit-slider-thumb { .question-slider-range::-webkit-slider-thumb {
width: 18px; width: 18px;
height: 18px; height: 18px;
margin-top: -2px;
margin-top: -4px;
appearance: none; appearance: none;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: #ffffff; background: #ffffff;
box-shadow: box-shadow:
0 2px 7px rgb(0 0 0 / 22%),
0 0 0 2px rgb(0 0 0 / 8%);
0 3px 8px rgba(0, 0, 0, 0.12),
0 0 2px rgba(0, 0, 0, 0.15);
} }
.question-slider-range::-moz-range-track { .question-slider-range::-moz-range-track {
height: 14px;
height: 10px;
border-radius: 999px; border-radius: 999px;
background: transparent; background: transparent;
} }
.question-slider-range::-moz-range-progress { .question-slider-range::-moz-range-progress {
height: 14px;
height: 10px;
border-radius: 999px; border-radius: 999px;
background: #f43f5e;
background: #f2465f;
} }
.question-slider-range::-moz-range-thumb { .question-slider-range::-moz-range-thumb {
width: 44px;
height: 44px;
width: 18px;
height: 18px;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background: #ffffff; background: #ffffff;
box-shadow: box-shadow:
0 2px 7px rgb(0 0 0 / 22%),
0 0 0 1px rgb(0 0 0 / 8%);
0 3px 8px rgba(0, 0, 0, 0.12),
0 0 2px rgba(0, 0, 0, 0.15);
} }
/* 3 Dots Bouncing Line Circle Loader for Buttons */ /* 3 Dots Bouncing Line Circle Loader for Buttons */

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

@ -7,20 +7,9 @@ import {
QuestionAnswersProvider, QuestionAnswersProvider,
useQuestionAnswers, useQuestionAnswers,
} from "@/components/questions/question-answer-storage"; } from "@/components/questions/question-answer-storage";
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 QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button"; import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button";
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 QuestionRenderer from "@/components/questions/question-renderer";
import QuestionSectionFlow from "@/components/questions/question-section-flow"; import QuestionSectionFlow from "@/components/questions/question-section-flow";
import QuestionSlider from "@/components/questions/question-slider";
import QuestionText from "@/components/questions/question-text";
import TestIntroPage from "@/components/questions/test-intro-page"; import TestIntroPage from "@/components/questions/test-intro-page";
import TestQuestionsFlow, { import TestQuestionsFlow, {
type TestQuestion, type TestQuestion,
@ -66,6 +55,8 @@ type QuestionDetailClientProps = {
type StoredQuestionField = { type StoredQuestionField = {
label?: string; label?: string;
value?: unknown; value?: unknown;
type?: string;
key?: string;
}; };
type StoredAnswers = { type StoredAnswers = {
@ -129,7 +120,13 @@ function getStoredAge() {
const storedAnswers = JSON.parse(rawValue) as StoredAnswers; const storedAnswers = JSON.parse(rawValue) as StoredAnswers;
const ageField = storedAnswers.fields?.find( const ageField = storedAnswers.fields?.find(
(field) => field.label === "Age",
(field) =>
field.type === "number" ||
field.label === "Age" ||
field.label === "سن" ||
(typeof (field as any).key === "string" &&
((field as any).key.endsWith("_age") ||
(field as any).key.endsWith("_sn"))),
); );
if (ageField) { if (ageField) {
@ -137,7 +134,13 @@ function getStoredAge() {
} }
const dateOfBirthField = storedAnswers.fields?.find( const dateOfBirthField = storedAnswers.fields?.find(
(field) => field.label === "Date of Birth",
(field) =>
field.type === "date" ||
field.label === "Date of Birth" ||
field.label === "تاریخ تولد" ||
(typeof (field as any).key === "string" &&
((field as any).key.endsWith("_date_of_birth") ||
(field as any).key.endsWith("_tarykh_twld"))),
); );
return parseStoredAge(dateOfBirthField?.value); return parseStoredAge(dateOfBirthField?.value);
@ -146,167 +149,11 @@ function getStoredAge() {
} }
} }
function renderQuestion(
question: QuestionField,
questionIndex: number,
disabled?: boolean,
dobQuestion?: QuestionField,
dobQuestionIndex?: number,
) {
const compactTextHeight =
question.type === "text" &&
(question.title.toLowerCase().includes("name") ||
question.title.toLowerCase().includes("email") ||
question.title.toLowerCase().includes("city") ||
question.title.toLowerCase().includes("residence") ||
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")
? "h-[54px] min-h-0 py-2"
: 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":
return (
<QuestionSlider
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
case "text":
return (
<QuestionText
question={question}
questionIndex={questionIndex}
heightClassName={compactTextHeight}
disabled={disabled}
/>
);
default:
return null;
}
}
function QuestionFlowWrapper({ function QuestionFlowWrapper({
visibleQuestions, visibleQuestions,
itemSlug, itemSlug,
dobQuestion, dobQuestion,
dobQuestionIndex, dobQuestionIndex,
requiredQuestionsCount,
continueLabel, continueLabel,
questionsListHref, questionsListHref,
}: { }: {
@ -320,35 +167,47 @@ function QuestionFlowWrapper({
}) { }) {
const { getAnswerValue } = useQuestionAnswers(); const { getAnswerValue } = useQuestionAnswers();
const dynamicQuestions = useMemo(() => {
return visibleQuestions.filter((question) => {
if (question.logic?.dependsOn) {
const { title, values } = question.logic.dependsOn;
const dependentQuestionIndex = visibleQuestions.findIndex(
(q) => q.title === title,
);
if (dependentQuestionIndex !== -1) {
const dependentQuestion = visibleQuestions[dependentQuestionIndex];
const answer = getAnswerValue(
dependentQuestion,
dependentQuestionIndex,
);
return values.includes(String(answer));
}
return false;
}
return true;
});
}, [visibleQuestions, getAnswerValue]);
const requiredCount = useMemo(
() => dynamicQuestions.filter((q) => q.required).length,
[dynamicQuestions],
);
return ( return (
<QuestionSectionFlow <QuestionSectionFlow
key={itemSlug} key={itemSlug}
total={requiredQuestionsCount}
total={requiredCount}
continueLabel={continueLabel} continueLabel={continueLabel}
exitHref={questionsListHref} exitHref={questionsListHref}
optionalQuestionIndexes={visibleQuestions.flatMap((question, index) =>
optionalQuestionIndexes={dynamicQuestions.flatMap((question, index) =>
question.required ? [] : [index], question.required ? [] : [index],
)} )}
questions={dynamicQuestions}
> >
{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));
}
}
const answer = getAnswerValue(question, questionIndex);
{dynamicQuestions.map((question, index) => {
const originalIndex = visibleQuestions.indexOf(question);
const answer = getAnswerValue(question, originalIndex);
const hasAnswer = hasQuestionAnswerValue(answer ?? null); const hasAnswer = hasQuestionAnswerValue(answer ?? null);
let isAnswered = hasAnswer; let isAnswered = hasAnswer;
@ -372,17 +231,17 @@ function QuestionFlowWrapper({
key={`${itemSlug}-${question.title}`} key={`${itemSlug}-${question.title}`}
data-question-required={String(question.required)} data-question-required={String(question.required)}
data-question-optional={String(!question.required)} data-question-optional={String(!question.required)}
data-question-index={questionIndex}
data-question-disabled={String(isDisabled)}
data-question-index={index}
data-question-original-index={originalIndex}
data-question-disabled="false"
data-question-answered={String(isAnswered)} data-question-answered={String(isAnswered)}
> >
{renderQuestion(
question,
questionIndex,
isDisabled,
dobQuestion,
dobQuestionIndex,
)}
<QuestionRenderer
question={question}
questionIndex={originalIndex}
dobQuestion={dobQuestion}
dobQuestionIndex={dobQuestionIndex}
/>
</div> </div>
); );
})} })}

72
src/components/questions/conditional-questions.tsx

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

16
src/components/questions/progress-helper.ts

@ -13,13 +13,25 @@ export function getStoredAge(): number | null {
); );
if (!rawValue) return null; if (!rawValue) return null;
const storedAnswers = JSON.parse(rawValue); const storedAnswers = JSON.parse(rawValue);
const ageField = storedAnswers.fields?.find((f: any) => f.label === "Age");
const ageField = storedAnswers.fields?.find(
(f: any) =>
f.type === "number" ||
f.label === "Age" ||
f.label === "سن" ||
(typeof f.key === "string" &&
(f.key.endsWith("_age") || f.key.endsWith("_sn"))),
);
if (ageField && ageField.value !== undefined && ageField.value !== null) { if (ageField && ageField.value !== undefined && ageField.value !== null) {
const num = Number(ageField.value); const num = Number(ageField.value);
if (Number.isFinite(num)) return num; if (Number.isFinite(num)) return num;
} }
const dobField = storedAnswers.fields?.find( const dobField = storedAnswers.fields?.find(
(f: any) => f.label === "Date of Birth",
(f: any) =>
f.type === "date" ||
f.label === "Date of Birth" ||
f.label === "تاریخ تولد" ||
(typeof f.key === "string" &&
(f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld"))),
); );
if (dobField && dobField.value) { if (dobField && dobField.value) {
const dob = new Date(dobField.value); const dob = new Date(dobField.value);

160
src/components/questions/question-renderer.tsx

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

88
src/components/questions/question-section-flow.tsx

@ -12,6 +12,9 @@ import QuestionProgressTracker, {
useQuestionProgress, useQuestionProgress,
} from "./question-progress-tracker"; } from "./question-progress-tracker";
import QuestionSnapList from "./question-snap-list"; import QuestionSnapList from "./question-snap-list";
import type { QuestionField } from "@/data/question-data";
import NoticeBox from "@/components/ui/notice-box";
import { getStoredAge } from "./progress-helper";
type QuestionSectionFlowProps = { type QuestionSectionFlowProps = {
children: ReactNode; children: ReactNode;
@ -19,6 +22,7 @@ type QuestionSectionFlowProps = {
exitHref: string; exitHref: string;
total: number; total: number;
optionalQuestionIndexes: readonly number[]; optionalQuestionIndexes: readonly number[];
questions?: readonly QuestionField[];
}; };
function SectionFlowContent({ function SectionFlowContent({
@ -26,17 +30,20 @@ function SectionFlowContent({
continueLabel, continueLabel,
exitHref, exitHref,
optionalQuestionIndexes, optionalQuestionIndexes,
questions,
}: { }: {
children: ReactNode; children: ReactNode;
continueLabel: string; continueLabel: string;
exitHref: string; exitHref: string;
optionalQuestionIndexes: readonly number[]; optionalQuestionIndexes: readonly number[];
questions?: readonly QuestionField[];
}) { }) {
const router = useRouter(); const router = useRouter();
const { locale } = useI18n();
const { locale, dictionary: t } = useI18n();
const { flushAnswers } = useQuestionAnswers(); const { flushAnswers } = useQuestionAnswers();
const { isCompleted, markQuestionPassed } = useQuestionProgress(); const { isCompleted, markQuestionPassed } = useQuestionProgress();
const [isLeaving, setIsLeaving] = useState(false); const [isLeaving, setIsLeaving] = useState(false);
const [activeQuestionIndex, setActiveQuestionIndex] = useState(0);
const handleQuestionExit = useCallback(() => { const handleQuestionExit = useCallback(() => {
void flushAnswers({ force: true }); void flushAnswers({ force: true });
@ -72,36 +79,55 @@ function SectionFlowContent({
const isDisabled = !isCompleted || isLeaving; const isDisabled = !isCompleted || isLeaving;
const age = getStoredAge();
const activeQuestion = questions?.[activeQuestionIndex];
const showNotice = activeQuestion?.showGuardianNotice;
const isUnder27 =
age !== null ? age < 27 : (activeQuestion?.required ?? false);
return ( return (
<QuestionSnapList
firstQuestionHint={
<Image
src="/assets/images/Frame 1597880476.svg"
alt=""
aria-hidden="true"
width={31}
height={31}
/>
}
footer={
<Button
className={[
"rounded-[14px] py-[16px] transition-all duration-300 font-bold text-[16px]",
isDisabled
? "!bg-[#E8D9D7] !text-[#9E8E8C] !opacity-60 !shadow-none cursor-not-allowed pointer-events-none"
: "!bg-linear-to-r !from-[#F2465F] !to-[#E03950] !text-white !opacity-100 shadow-[0_12px_28px_rgba(242,70,95,0.38)] cursor-pointer hover:brightness-105 active:scale-[0.99]",
].join(" ")}
disabled={isDisabled}
onClick={() => void handleContinue()}
>
{continueLabel}
</Button>
}
onQuestionExit={handleQuestionExit}
onQuestionTransition={markOptionalQuestionsPassed}
>
{children}
</QuestionSnapList>
<>
{showNotice && (
<div className="w-full px-[17px] mb-6 shrink-0">
<NoticeBox>
{isUnder27
? t.questions.guardianNoticeUnder27
: t.questions.guardianNoticeOver27}
</NoticeBox>
</div>
)}
<QuestionSnapList
alignTop={showNotice}
firstQuestionHint={
<Image
src="/assets/images/Frame 1597880476.svg"
alt=""
aria-hidden="true"
width={31}
height={31}
/>
}
footer={
<Button
className={[
"rounded-[14px] py-[16px] transition-all duration-300 font-bold text-[16px]",
isDisabled
? "!bg-[#E8D9D7] !text-[#9E8E8C] !opacity-60 !shadow-none cursor-not-allowed pointer-events-none"
: "!bg-linear-to-r !from-[#F2465F] !to-[#E03950] !text-white !opacity-100 shadow-[0_12px_28px_rgba(242,70,95,0.38)] cursor-pointer hover:brightness-105 active:scale-[0.99]",
].join(" ")}
disabled={isDisabled}
onClick={() => void handleContinue()}
>
{continueLabel}
</Button>
}
onQuestionExit={handleQuestionExit}
onQuestionTransition={markOptionalQuestionsPassed}
onActiveIndexChange={setActiveQuestionIndex}
>
{children}
</QuestionSnapList>
</>
); );
} }
@ -111,6 +137,7 @@ export function QuestionSectionFlow({
exitHref, exitHref,
total, total,
optionalQuestionIndexes, optionalQuestionIndexes,
questions,
}: QuestionSectionFlowProps) { }: QuestionSectionFlowProps) {
return ( return (
<QuestionProgressTracker total={total}> <QuestionProgressTracker total={total}>
@ -118,6 +145,7 @@ export function QuestionSectionFlow({
continueLabel={continueLabel} continueLabel={continueLabel}
exitHref={exitHref} exitHref={exitHref}
optionalQuestionIndexes={optionalQuestionIndexes} optionalQuestionIndexes={optionalQuestionIndexes}
questions={questions}
> >
{children} {children}
</SectionFlowContent> </SectionFlowContent>

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

@ -25,12 +25,20 @@ export function QuestionSlider({
const sliderWrapperRef = useRef<HTMLDivElement>(null); const sliderWrapperRef = useRef<HTMLDivElement>(null);
const sliderRef = useRef<HTMLInputElement>(null); const sliderRef = useRef<HTMLInputElement>(null);
const thumbWidth = 18; const thumbWidth = 18;
const bubbleHalfWidth = 20;
const bubbleHalfWidth = 18;
const [bubblePosition, setBubblePosition] = useState(bubbleHalfWidth); const [bubblePosition, setBubblePosition] = useState(bubbleHalfWidth);
const steps = Array.from(
{ length: max - min + 1 },
(_, index) => min + index,
);
const steps = (() => {
const range = max - min;
const interval = range <= 15 ? 1 : range <= 50 ? 5 : range <= 150 ? 10 : 20;
const arr: number[] = [];
for (let val = min; val <= max; val += interval) {
arr.push(val);
}
if (arr[arr.length - 1] !== max) {
arr.push(max);
}
return arr;
})();
useLayoutEffect(() => { useLayoutEffect(() => {
const wrapper = sliderWrapperRef.current; const wrapper = sliderWrapperRef.current;
@ -69,15 +77,15 @@ export function QuestionSlider({
return ( return (
<div <div
className={[ className={[
"flex w-full flex-col gap-6 transition-opacity duration-200",
"flex w-full flex-col gap-3.5 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "", disabled ? "pointer-events-none opacity-30" : "",
].join(" ")} ].join(" ")}
> >
<QuestionTitle question={question} /> <QuestionTitle question={question} />
<div className="pt-9">
<div className="pt-7">
<div ref={sliderWrapperRef} className="relative"> <div ref={sliderWrapperRef} className="relative">
<div <div
className="-translate-x-1/2 absolute -top-6.5 z-10 flex items-center justify-center rounded-[7px] bg-[#F43F5E] px-2.5 group-12 font-semibold text-white shadow-sm after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F43F5E] after:content-['']"
className="-translate-x-1/2 absolute -top-7.5 z-10 flex items-center justify-center rounded-[8px] bg-[#F2465F] px-3 py-1 text-[14px] font-bold text-white shadow-md after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F2465F] after:content-['']"
style={{ left: `${bubblePosition}px` }} style={{ left: `${bubblePosition}px` }}
> >
{value} {value}
@ -99,14 +107,28 @@ export function QuestionSlider({
disabled={disabled} 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, #F2465F 0%, #F2465F ${progress}%, #FCE7EA ${progress}%, #FCE7EA 100%)`,
}} }}
/> />
</div> </div>
<div className="mt-2 grid grid-flow-col auto-cols-fr text-center group-10 text-[#B7B7B7]">
{steps.map((step) => (
<span key={step}>{step}</span>
))}
<div
className="relative mt-2 h-5 text-[#B7B7B7] text-[10px] font-medium"
style={{ width: "100%" }}
>
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={step}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div> </div>
</div> </div>
</div> </div>

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

@ -29,6 +29,8 @@ type QuestionSnapListProps = {
firstQuestionHint?: ReactNode; firstQuestionHint?: ReactNode;
onQuestionExit?: (currentIndex: number, nextIndex: number) => void; onQuestionExit?: (currentIndex: number, nextIndex: number) => void;
onQuestionTransition?: (currentIndex: number, nextIndex: number) => void; onQuestionTransition?: (currentIndex: number, nextIndex: number) => void;
onActiveIndexChange?: (index: number) => void;
alignTop?: boolean;
}; };
export function QuestionSnapList({ export function QuestionSnapList({
@ -38,6 +40,8 @@ export function QuestionSnapList({
firstQuestionHint, firstQuestionHint,
onQuestionExit, onQuestionExit,
onQuestionTransition, onQuestionTransition,
onActiveIndexChange,
alignTop,
}: QuestionSnapListProps) { }: QuestionSnapListProps) {
const { dictionary: t } = useI18n(); const { dictionary: t } = useI18n();
const { isCompleted } = useQuestionProgress(); const { isCompleted } = useQuestionProgress();
@ -164,6 +168,10 @@ export function QuestionSnapList({
}; };
}, [questions.length]); }, [questions.length]);
useEffect(() => {
onActiveIndexChange?.(activeIndex);
}, [activeIndex, onActiveIndexChange]);
useEffect(() => { useEffect(() => {
const previousActiveIndex = previousActiveIndexRef.current; const previousActiveIndex = previousActiveIndexRef.current;
@ -335,7 +343,9 @@ export function QuestionSnapList({
if (isActive) { if (isActive) {
containerStyles = containerStyles =
"top-0 left-0 bottom-0 z-10 opacity-100 scale-100 pointer-events-auto !justify-start"; "top-0 left-0 bottom-0 z-10 opacity-100 scale-100 pointer-events-auto !justify-start";
wrapperStyles = "w-full my-auto shrink-0";
wrapperStyles = alignTop
? "w-full mt-3 shrink-0"
: "w-full my-auto shrink-0";
} else if (isPrev) { } else if (isPrev) {
containerStyles = containerStyles =
"top-4 left-0 z-0 opacity-0 pointer-events-none scale-[0.98]"; "top-4 left-0 z-0 opacity-0 pointer-events-none scale-[0.98]";
@ -369,7 +379,7 @@ export function QuestionSnapList({
> >
<div className={wrapperStyles}>{question}</div> <div className={wrapperStyles}>{question}</div>
{isActive && ( {isActive && (
<div className="h-[30%] shrink min-h-0" aria-hidden="true" />
<div className="h-[20%] shrink min-h-0" aria-hidden="true" />
)} )}
</div> </div>
); );

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

@ -18,10 +18,10 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
return ( return (
<span className="block w-full"> <span className="block w-full">
{question.private ? ( {question.private ? (
<span className="mb-4 mx-auto flex w-fit items-center gap-2 rounded-full bg-[#F8D7DA] px-4 py-2 text-center text-[12.5px] font-bold text-[#D9383A]">
<span className="mb-4 mx-auto flex w-fit items-center gap-1.5 rounded-full bg-[#F8D7DA] px-3 py-1 text-center text-[10px] font-bold text-[#D9383A]">
<IoEyeOff <IoEyeOff
aria-hidden="true" aria-hidden="true"
className="shrink-0 text-[18px] text-[#D9383A]"
className="shrink-0 text-[15px] text-[#D9383A]"
/> />
<span> <span>
{t.questions.privateFieldNotice ?? "Private (Advisors Only)"} {t.questions.privateFieldNotice ?? "Private (Advisors Only)"}

12
src/components/sliders/slider-slide-four.tsx

@ -4,6 +4,7 @@ import type {
RegistrationAnswer, RegistrationAnswer,
SliderSlideProps, SliderSlideProps,
} from "@/components/sliders/slider-slide"; } from "@/components/sliders/slider-slide";
import NoticeBox from "@/components/ui/notice-box";
type SliderSlideFourProps = SliderSlideProps & { type SliderSlideFourProps = SliderSlideProps & {
selectedRegistration: RegistrationAnswer; selectedRegistration: RegistrationAnswer;
@ -34,13 +35,10 @@ export function SliderSlideFour({
{index + 1}/4 Registration Type {index + 1}/4 Registration Type
</p> </p>
</div> </div>
<div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-8">
<p className="text-[#F14B46] group-12 font-semibold text-center">
Read the terms and conditions carefully so that you don't run into
any problems in the next process - reading this section is
mandatory.
</p>
</div>
<NoticeBox className="mt-8">
Read the terms and conditions carefully so that you don't run into any
problems in the next process - reading this section is mandatory.
</NoticeBox>
<div className="flex min-h-[calc(100dvh-520px)] items-center justify-center pt-6"> <div className="flex min-h-[calc(100dvh-520px)] items-center justify-center pt-6">
<div className="w-full max-w-[440px] space-y-4"> <div className="w-full max-w-[440px] space-y-4">
{options.map((option) => { {options.map((option) => {

26
src/components/ui/notice-box.tsx

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

1
src/data/question-data.ts

@ -47,6 +47,7 @@ export type QuestionField = {
audience?: QuestionAudienceRule; audience?: QuestionAudienceRule;
requiredWhen?: QuestionAudienceRule; requiredWhen?: QuestionAudienceRule;
logic?: QuestionLogic; logic?: QuestionLogic;
showGuardianNotice?: boolean;
}; };
export type QuestionListItem = { export type QuestionListItem = {

48
src/data/questions/en.json

@ -262,6 +262,11 @@
"genders": ["female"], "genders": ["female"],
"maxAge": 26 "maxAge": 26
}, },
"audience": {
"genders": ["female"]
},
"showGuardianNotice": true,
"tooltip": "This field specifies the full name of the designated intermediary whose contact information is provided to the other party to facilitate communication.",
"extras": { "extras": {
"placeHolder": "Sarah Smith", "placeHolder": "Sarah Smith",
"range": [0, 0], "range": [0, 0],
@ -277,6 +282,9 @@
"genders": ["female"], "genders": ["female"],
"maxAge": 26 "maxAge": 26
}, },
"audience": {
"genders": ["female"]
},
"extras": { "extras": {
"placeHolder": "Select one option", "placeHolder": "Select one option",
"range": [0, 0], "range": [0, 0],
@ -303,6 +311,9 @@
"genders": ["female"], "genders": ["female"],
"maxAge": 26 "maxAge": 26
}, },
"audience": {
"genders": ["female"]
},
"extras": { "extras": {
"placeHolder": "+44 7911 123456", "placeHolder": "+44 7911 123456",
"range": [0, 0], "range": [0, 0],
@ -323,22 +334,22 @@
"questions": [ "questions": [
{ {
"title": "Height in Centimeters", "title": "Height in Centimeters",
"type": "number",
"type": "scale",
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "175", "placeHolder": "175",
"range": [50, 300],
"range": [100, 230],
"options": [] "options": []
}, },
"private": true "private": true
}, },
{ {
"title": "Weight in Kilograms", "title": "Weight in Kilograms",
"type": "number",
"type": "scale",
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "70", "placeHolder": "70",
"range": [20, 600],
"range": [40, 180],
"options": [] "options": []
}, },
"private": true "private": true
@ -362,6 +373,7 @@
"title": "Physical Health Status", "title": "Physical Health Status",
"type": "radio", "type": "radio",
"required": true, "required": true,
"private": true,
"extras": { "extras": {
"placeHolder": "Select one option", "placeHolder": "Select one option",
"range": [0, 0], "range": [0, 0],
@ -375,7 +387,17 @@
{ {
"title": "Physical Health Description", "title": "Physical Health Description",
"type": "text", "type": "text",
"required": false,
"required": true,
"private": true,
"logic": {
"dependsOn": {
"title": "Physical Health Status",
"values": [
"I have a specific or chronic illness.",
"I have a physical deformity, disability, or limitation."
]
}
},
"tooltip": "Provide more details if you have any health conditions or limitations.", "tooltip": "Provide more details if you have any health conditions or limitations.",
"extras": { "extras": {
"placeHolder": "Enter details here...", "placeHolder": "Enter details here...",
@ -387,20 +409,28 @@
"title": "Mental Health Status", "title": "Mental Health Status",
"type": "radio", "type": "radio",
"required": true, "required": true,
"private": true,
"extras": { "extras": {
"placeHolder": "Select one option", "placeHolder": "Select one option",
"range": [0, 0], "range": [0, 0],
"options": [ "options": [
"I have no specific problems.",
"I have a history of counseling and treatment.",
"I am currently undergoing counseling and treatment."
"I have no specific issues.",
"I have a history of counseling or am currently undergoing treatment.",
"I am undergoing pharmacotherapy."
] ]
} }
}, },
{ {
"title": "Use of Permanent Medications", "title": "Use of Permanent Medications",
"type": "text", "type": "text",
"required": false,
"required": true,
"logic": {
"dependsOn": {
"title": "Mental Health Status",
"values": ["I am undergoing pharmacotherapy."]
}
},
"tooltip": "This field requires the user to declare all permanent medications currently being taken for any physical, psychological, medical, or non-medical condition.",
"extras": { "extras": {
"placeHolder": "Insulin, etc.", "placeHolder": "Insulin, etc.",
"range": [0, 0], "range": [0, 0],

46
src/data/questions/fa.json

@ -262,6 +262,11 @@
"genders": ["female"], "genders": ["female"],
"maxAge": 26 "maxAge": 26
}, },
"audience": {
"genders": ["female"]
},
"showGuardianNotice": true,
"tooltip": "این فیلد نام و نام خانوادگی رابط مشخص‌شده را تعیین می‌کند که اطلاعات تماس او جهت تسهیل ارتباط در اختیار طرف مقابل قرار می‌گیرد.",
"extras": { "extras": {
"placeHolder": "سارا اسمیت", "placeHolder": "سارا اسمیت",
"range": [0, 0], "range": [0, 0],
@ -277,6 +282,9 @@
"genders": ["female"], "genders": ["female"],
"maxAge": 26 "maxAge": 26
}, },
"audience": {
"genders": ["female"]
},
"extras": { "extras": {
"placeHolder": "انتخاب کنید", "placeHolder": "انتخاب کنید",
"range": [0, 0], "range": [0, 0],
@ -303,6 +311,9 @@
"genders": ["female"], "genders": ["female"],
"maxAge": 26 "maxAge": 26
}, },
"audience": {
"genders": ["female"]
},
"extras": { "extras": {
"placeHolder": "+44 7911 123456", "placeHolder": "+44 7911 123456",
"range": [0, 0], "range": [0, 0],
@ -323,22 +334,22 @@
"questions": [ "questions": [
{ {
"title": "قد به سانتی‌متر", "title": "قد به سانتی‌متر",
"type": "number",
"type": "scale",
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "۱۷۵", "placeHolder": "۱۷۵",
"range": [50, 300],
"range": [100, 230],
"options": [] "options": []
}, },
"private": true "private": true
}, },
{ {
"title": "وزن به کیلوگرم", "title": "وزن به کیلوگرم",
"type": "number",
"type": "scale",
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "۷۰", "placeHolder": "۷۰",
"range": [20, 600],
"range": [40, 180],
"options": [] "options": []
}, },
"private": true "private": true
@ -362,6 +373,7 @@
"title": "وضعیت سلامت جسمانی", "title": "وضعیت سلامت جسمانی",
"type": "radio", "type": "radio",
"required": true, "required": true,
"private": true,
"extras": { "extras": {
"placeHolder": "یک گزینه را انتخاب کنید", "placeHolder": "یک گزینه را انتخاب کنید",
"range": [0, 0], "range": [0, 0],
@ -375,7 +387,17 @@
{ {
"title": "توضیحات سلامت جسمانی", "title": "توضیحات سلامت جسمانی",
"type": "text", "type": "text",
"required": false,
"required": true,
"private": true,
"logic": {
"dependsOn": {
"title": "وضعیت سلامت جسمانی",
"values": [
"بیماری خاص یا مزمن دارم.",
"نقص عضو، معلولیت یا محدودیت جسمی دارم."
]
}
},
"tooltip": "وضعیت بیماریهای مزمن، محدودیتهای جسمانی یا داروهای مصرفی خود را به دقت شرح داده و در صورت سلامت، عبارت «دارای سلامت کامل جسمانی» را در این کادر ثبت کنید.", "tooltip": "وضعیت بیماریهای مزمن، محدودیتهای جسمانی یا داروهای مصرفی خود را به دقت شرح داده و در صورت سلامت، عبارت «دارای سلامت کامل جسمانی» را در این کادر ثبت کنید.",
"extras": { "extras": {
"placeHolder": "توضیحات را اینجا وارد کنید...", "placeHolder": "توضیحات را اینجا وارد کنید...",
@ -387,20 +409,28 @@
"title": "وضعیت سلامت روان", "title": "وضعیت سلامت روان",
"type": "radio", "type": "radio",
"required": true, "required": true,
"private": true,
"extras": { "extras": {
"placeHolder": "یک گزینه را انتخاب کنید", "placeHolder": "یک گزینه را انتخاب کنید",
"range": [0, 0], "range": [0, 0],
"options": [ "options": [
"مشکل خاصی ندارم.", "مشکل خاصی ندارم.",
"سابقه مشاوره و درمان داشته‌ام.",
"در حال حاضر تحت مشاوره و درمان هستم."
"سابقه مشاوره داشته یا در حال حاضر تحت درمان هستم.",
"تحت دارو درمانی هستم."
] ]
} }
}, },
{ {
"title": "استفاده از داروهای دائمی", "title": "استفاده از داروهای دائمی",
"type": "text", "type": "text",
"required": false,
"required": true,
"logic": {
"dependsOn": {
"title": "وضعیت سلامت روان",
"values": ["تحت دارو درمانی هستم."]
}
},
"tooltip": "این فیلد برای ثبت تمامی داروهای دائمی است که در حال حاضر به دلایل جسمی، روانی، پزشکی یا غیرپزشکی مصرف می‌کنید.",
"extras": { "extras": {
"placeHolder": "انسولین و غیره", "placeHolder": "انسولین و غیره",
"range": [0, 0], "range": [0, 0],

4
src/translations/locales/ar.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "معلومات سرية (للمستشارين فقط)", "privateFieldNotice": "معلومات سرية (للمستشارين فقط)",
"startMatchFailed": "فشل إرسال طلب التوافق. يرجى التحقق من الاتصال والمحاولة مرة أخرى.", "startMatchFailed": "فشل إرسال طلب التوافق. يرجى التحقق من الاتصال والمحاولة مرة أخرى.",
"moveToEnd": "الانتقال إلى النهاية"
"moveToEnd": "الانتقال إلى النهاية",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/az.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/bn.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/da.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/de.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/en.json

@ -65,7 +65,9 @@
] ]
}, },
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/es.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/fa.json

@ -65,7 +65,9 @@
] ]
}, },
"startMatchFailed": "ارسال درخواست مچ انجام نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید.", "startMatchFailed": "ارسال درخواست مچ انجام نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "برای حفظ آرامش، امنیت و شأن شما، روند آشنایی در پلتفرم ما با الگوگیری از رسوم اصیل و محترمانه خانوادگی پیش می‌رود. حضور یک فرد معتمد (ترجیحاً پدر یا مادر) به عنوان رابط، علاوه بر اینکه نشان‌دهنده اصالت شماست، باعث می‌شود طرف مقابل نیز با جدیت، احترام و اطمینان کامل قدم پیش بگذارد.",
"guardianNoticeOver27": "هدف ما شکل‌گیری پیوندهای پایدار بر بستر اعتماد متقابل است. با اینکه ثبت اطلاعات رابط برای شما الزامی نیست، اما معرفی یک فرد معتمد (مانند پدر، مادر یا بزرگتر خانواده) نشان‌دهنده شفافیت و نیت جدی شما برای ازدواج است. پروفایل‌هایی که دارای رابط معتمد هستند، اعتبار بسیار بالاتری دارند و باعث ایجاد اطمینان خاطر بیشتری در خانواده طرف مقابل می‌شوند."
}, },
"match": { "match": {
"title": "گزینه جدید", "title": "گزینه جدید",

4
src/translations/locales/fr.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/gu.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/ha.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/he.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/hi.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/id.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/ks.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/pt.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/ru.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/sw.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/tg.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/tr.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Gizli (Yalnızca Danışmanlar)", "privateFieldNotice": "Gizli (Yalnızca Danışmanlar)",
"startMatchFailed": "Eşleşme isteği gönderilemedi. Lütfen bağlantınızı kontrol edip tekrar deneyin.", "startMatchFailed": "Eşleşme isteği gönderilemedi. Lütfen bağlantınızı kontrol edip tekrar deneyin.",
"moveToEnd": "Sona Taşı"
"moveToEnd": "Sona Taşı",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/ul.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/ur.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "نجی (صرف مشیران)", "privateFieldNotice": "نجی (صرف مشیران)",
"startMatchFailed": "میچ کی درخواست بھیجنے میں ناکامی۔ برائے مہربانی اپنا کنکشن چیک کریں اور دوبارہ کوشش کریں۔", "startMatchFailed": "میچ کی درخواست بھیجنے میں ناکامی۔ برائے مہربانی اپنا کنکشن چیک کریں اور دوبارہ کوشش کریں۔",
"moveToEnd": "آخر میں منتقل کریں"
"moveToEnd": "آخر میں منتقل کریں",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/uz.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

4
src/translations/locales/zh.json

@ -65,7 +65,9 @@
}, },
"privateFieldNotice": "Private (Advisors Only)", "privateFieldNotice": "Private (Advisors Only)",
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.", "startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End"
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",

Loading…
Cancel
Save