-
+
+
+
+ Profile registration
+
+
+
+
+
Bookings Terms & Conditions
+
+
+
+
+ - You will be contacted by your consultant.
+ - The call may start 10–15 minutes earlier or later than scheduled.
+ - Make sure you are available and in a quiet place at least 10 minutes before the session.
+
+
+
+
+ {
+ QUESTIONS.map((question) => (
+
+ ))
+ }
);
diff --git a/src/app/questions-list/[slug]/page.tsx b/src/app/questions-list/[slug]/page.tsx
new file mode 100644
index 0000000..9511af2
--- /dev/null
+++ b/src/app/questions-list/[slug]/page.tsx
@@ -0,0 +1,92 @@
+import { notFound } from "next/navigation";
+
+import QuestionButton from "@/components/questions/question-button";
+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 QuestionRadio from "@/components/questions/question-radio";
+import QuestionSlider from "@/components/questions/question-slider";
+import QuestionText from "@/components/questions/question-text";
+import Button from "@/components/ui/button";
+import InformationSheet from "@/components/ui/information-sheet";
+import { PageBackground } from "@/components/utils/page-background";
+import {
+ getQuestionListItemBySlug,
+ type QuestionField,
+ questionListItems,
+} from "@/data/question-data";
+
+export function generateStaticParams() {
+ return questionListItems.map((item) => ({
+ slug: item.slug,
+ }));
+}
+
+type QuestionDetailPageProps = {
+ params: Promise<{
+ slug: string;
+ }>;
+};
+
+function renderQuestion(question: QuestionField) {
+ switch (question.type) {
+ case "button":
+ return
;
+ case "date":
+ return
;
+ case "dropdown":
+ return
;
+ case "file":
+ return
;
+ case "number":
+ return
;
+ case "radio":
+ return
;
+ case "slider":
+ return
;
+ case "text":
+ return
;
+ default:
+ return null;
+ }
+}
+
+export default async function QuestionDetailPage({
+ params,
+}: QuestionDetailPageProps) {
+ const { slug } = await params;
+ const item = getQuestionListItemBySlug(slug);
+
+ if (!item) {
+ notFound();
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ {item.questions.map((question) => (
+
+ {renderQuestion(question)}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx
new file mode 100644
index 0000000..9e6ef19
--- /dev/null
+++ b/src/app/questions-list/page.tsx
@@ -0,0 +1,55 @@
+import BookingTermsCard from "@/components/questions/booking-terms-card";
+import QuestionCard from "@/components/questions/question-card";
+import Button from "@/components/ui/button";
+import NavigationButton from "@/components/ui/navigation-button";
+import { PageBackground } from "@/components/utils/page-background";
+import { bookingTerms, questionListItems } from "@/data/question-data";
+
+export default function QuestionsListPage() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Profile registration
+
+
+
+
+
+
+
+ {questionListItems.map((item) => (
+
+ ))}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/dev/dev-click-to-component.tsx b/src/components/dev/dev-click-to-component.tsx
new file mode 100644
index 0000000..ea17ecb
--- /dev/null
+++ b/src/components/dev/dev-click-to-component.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import { useEffect } from "react";
+
+const IDE_SCHEMES = [
+ {
+ matches: ["antigravity"],
+ createUrl: (locator: string) => `antigravity://file/${locator}`,
+ },
+ {
+ matches: ["cursor"],
+ createUrl: (locator: string) => `cursor://file/${locator}`,
+ },
+ {
+ matches: ["vscode", "code"],
+ createUrl: (locator: string) => `vscode://file/${locator}`,
+ },
+ {
+ matches: ["webstorm", "intellij"],
+ createUrl: (locator: string) => `webstorm://open?file=${locator}`,
+ },
+ {
+ matches: ["sublime"],
+ createUrl: (locator: string) => `subl://open?url=file://${locator}`,
+ },
+ {
+ matches: ["atom", "nova"],
+ createUrl: (locator: string) => `atom://open?url=file://${locator}`,
+ },
+] as const;
+
+function parseLocator(locator: string) {
+ const match = locator.match(/^(.*):(\d+|unknown):(\d+|unknown)$/);
+
+ if (!match) {
+ return { filePath: locator, line: null, column: null };
+ }
+
+ const [, filePath, line, column] = match;
+
+ return {
+ filePath,
+ line: line === "unknown" ? null : Number(line),
+ column: column === "unknown" ? null : Number(column),
+ };
+}
+
+export function DevClickToComponent() {
+ useEffect(() => {
+ const userAgent = navigator.userAgent.toLowerCase();
+ const handleClick = (event: MouseEvent) => {
+ if (!event.altKey) {
+ return;
+ }
+
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ const locator = target
+ .closest
("[data-locator]")
+ ?.getAttribute("data-locator");
+
+ if (!locator) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ const { filePath, line, column } = parseLocator(locator);
+ const positionSuffix =
+ line === null ? "" : `:${line}${column === null ? "" : `:${column}`}`;
+
+ const ideUrl =
+ IDE_SCHEMES.find(({ matches }) =>
+ matches.some((match) => userAgent.includes(match)),
+ )?.createUrl(`${filePath}${positionSuffix}`) ??
+ `vscode://file/${filePath}${positionSuffix}`;
+
+ try {
+ window.location.href = ideUrl;
+ } catch {
+ window.open(`file://${filePath}`, "_blank", "noopener,noreferrer");
+ }
+ };
+
+ document.addEventListener("click", handleClick, true);
+
+ return () => {
+ document.removeEventListener("click", handleClick, true);
+ };
+ }, []);
+
+ return null;
+}
+
+export default DevClickToComponent;
diff --git a/src/components/dev/locator-paths.ts b/src/components/dev/locator-paths.ts
new file mode 100644
index 0000000..c854c92
--- /dev/null
+++ b/src/components/dev/locator-paths.ts
@@ -0,0 +1,29 @@
+export const LOCATORS = {
+ appHomePage: "D:/sajjadi/marriage/src/app/page.tsx",
+ appIntroPage: "D:/sajjadi/marriage/src/app/intro/page.tsx",
+ appQuestionsListPage: "D:/sajjadi/marriage/src/app/questions-list/page.tsx",
+ appQuestionDetailPage:
+ "D:/sajjadi/marriage/src/app/questions-list/[slug]/page.tsx",
+ appSliderPage: "D:/sajjadi/marriage/src/app/slider/page.tsx",
+ bookingTermsCard:
+ "D:/sajjadi/marriage/src/components/questions/booking-terms-card.tsx",
+ questionCard:
+ "D:/sajjadi/marriage/src/components/questions/question-card.tsx",
+ sliderPage: "D:/sajjadi/marriage/src/components/sliders/slider-page.tsx",
+ sliderSlide: "D:/sajjadi/marriage/src/components/sliders/slider-slide.tsx",
+ sliderSlideOne:
+ "D:/sajjadi/marriage/src/components/sliders/slider-slide-one.tsx",
+ sliderSlideTwo:
+ "D:/sajjadi/marriage/src/components/sliders/slider-slide-two.tsx",
+ sliderSlideThree:
+ "D:/sajjadi/marriage/src/components/sliders/slider-slide-three.tsx",
+ sliderSlideFour:
+ "D:/sajjadi/marriage/src/components/sliders/slider-slide-four.tsx",
+ button: "D:/sajjadi/marriage/src/components/ui/button.tsx",
+ infoProgressCard:
+ "D:/sajjadi/marriage/src/components/ui/info-progress-card.tsx",
+ navigationButton:
+ "D:/sajjadi/marriage/src/components/ui/navigation-button.tsx",
+ pageBackground:
+ "D:/sajjadi/marriage/src/components/utils/page-background.tsx",
+} as const;
diff --git a/src/components/questions/booking-terms-card.tsx b/src/components/questions/booking-terms-card.tsx
new file mode 100644
index 0000000..139f9f6
--- /dev/null
+++ b/src/components/questions/booking-terms-card.tsx
@@ -0,0 +1,29 @@
+import { IoChevronUp } from "react-icons/io5";
+
+type BookingTermsCardProps = {
+ title: string;
+ items: readonly string[];
+};
+
+export function BookingTermsCard({ title, items }: BookingTermsCardProps) {
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+ {items.map((item) => (
+ - {item}
+ ))}
+
+
+ );
+}
+
+export default BookingTermsCard;
diff --git a/src/components/questions/question-button.tsx b/src/components/questions/question-button.tsx
new file mode 100644
index 0000000..de8b5ab
--- /dev/null
+++ b/src/components/questions/question-button.tsx
@@ -0,0 +1,30 @@
+import type { QuestionField } from "@/data/question-data";
+import { IoInformation } from "react-icons/io5";
+
+type QuestionButtonProps = {
+ question: QuestionField;
+};
+
+export function QuestionButton({ question }: QuestionButtonProps) {
+ return (
+
+
+
{question.title}
+ {question.tooltip ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {question.description}
+
+
+
+ );
+}
+
+export default QuestionButton;
diff --git a/src/components/questions/question-card.tsx b/src/components/questions/question-card.tsx
new file mode 100644
index 0000000..b1ae7e7
--- /dev/null
+++ b/src/components/questions/question-card.tsx
@@ -0,0 +1,119 @@
+import Link from "next/link";
+import type { IconType } from "react-icons";
+import {
+ IoCheckbox,
+ IoDocumentText,
+ IoInformation,
+ IoPeople,
+ IoPerson,
+ IoSchool,
+} from "react-icons/io5";
+import type { QuestionCardIcon, QuestionListItem } from "@/data/question-data";
+
+type QuestionCardProps = {
+ item: QuestionListItem;
+};
+
+const RADIUS = 8;
+const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
+
+const iconMap: Record = {
+ profile: IoPerson,
+ education: IoSchool,
+ details: IoDocumentText,
+ checklist: IoCheckbox,
+ contact: IoPeople,
+};
+
+export function QuestionCard({ item }: QuestionCardProps) {
+ const normalizedProgress = Math.max(0, Math.min(item.progress, 100));
+ const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE;
+ const CardIcon = iconMap[item.icon];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {item.title}
+
+ {item.required ? (
+
+ (Required)
+
+ ) : null}
+
+
+ Estimate time: {item.estimate}
+
+
+
+
+ {item.showInfoBadge ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ {normalizedProgress}%
+
+
+
+
+
+ {item.note ? (
+
+ {item.note}
+
+ ) : null}
+
+
+ );
+}
+
+export default QuestionCard;
diff --git a/src/components/questions/question-date.tsx b/src/components/questions/question-date.tsx
new file mode 100644
index 0000000..e31f63e
--- /dev/null
+++ b/src/components/questions/question-date.tsx
@@ -0,0 +1,11 @@
+import type { QuestionField } from "@/data/question-data";
+
+type QuestionDateProps = {
+ question: QuestionField;
+};
+
+export function QuestionDate({ question }: QuestionDateProps) {
+ return ;
+}
+
+export default QuestionDate;
diff --git a/src/components/questions/question-dropdown.tsx b/src/components/questions/question-dropdown.tsx
new file mode 100644
index 0000000..0662f25
--- /dev/null
+++ b/src/components/questions/question-dropdown.tsx
@@ -0,0 +1,11 @@
+import type { QuestionField } from "@/data/question-data";
+
+type QuestionDropdownProps = {
+ question: QuestionField;
+};
+
+export function QuestionDropdown({ question }: QuestionDropdownProps) {
+ return ;
+}
+
+export default QuestionDropdown;
diff --git a/src/components/questions/question-file.tsx b/src/components/questions/question-file.tsx
new file mode 100644
index 0000000..c7c39b3
--- /dev/null
+++ b/src/components/questions/question-file.tsx
@@ -0,0 +1,11 @@
+import type { QuestionField } from "@/data/question-data";
+
+type QuestionFileProps = {
+ question: QuestionField;
+};
+
+export function QuestionFile({ question }: QuestionFileProps) {
+ return ;
+}
+
+export default QuestionFile;
diff --git a/src/components/questions/question-number.tsx b/src/components/questions/question-number.tsx
new file mode 100644
index 0000000..19a7a17
--- /dev/null
+++ b/src/components/questions/question-number.tsx
@@ -0,0 +1,11 @@
+import type { QuestionField } from "@/data/question-data";
+
+type QuestionNumberProps = {
+ question: QuestionField;
+};
+
+export function QuestionNumber({ question }: QuestionNumberProps) {
+ return ;
+}
+
+export default QuestionNumber;
diff --git a/src/components/questions/question-radio.tsx b/src/components/questions/question-radio.tsx
new file mode 100644
index 0000000..b091e8d
--- /dev/null
+++ b/src/components/questions/question-radio.tsx
@@ -0,0 +1,11 @@
+import type { QuestionField } from "@/data/question-data";
+
+type QuestionRadioProps = {
+ question: QuestionField;
+};
+
+export function QuestionRadio({ question }: QuestionRadioProps) {
+ return ;
+}
+
+export default QuestionRadio;
diff --git a/src/components/questions/question-slider.tsx b/src/components/questions/question-slider.tsx
new file mode 100644
index 0000000..f330de8
--- /dev/null
+++ b/src/components/questions/question-slider.tsx
@@ -0,0 +1,11 @@
+import type { QuestionField } from "@/data/question-data";
+
+type QuestionSliderProps = {
+ question: QuestionField;
+};
+
+export function QuestionSlider({ question }: QuestionSliderProps) {
+ return ;
+}
+
+export default QuestionSlider;
diff --git a/src/components/questions/question-text.tsx b/src/components/questions/question-text.tsx
new file mode 100644
index 0000000..317ed59
--- /dev/null
+++ b/src/components/questions/question-text.tsx
@@ -0,0 +1,11 @@
+import type { QuestionField } from "@/data/question-data";
+
+type QuestionTextProps = {
+ question: QuestionField;
+};
+
+export function QuestionText({ question }: QuestionTextProps) {
+ return ;
+}
+
+export default QuestionText;
diff --git a/src/components/sliders/slider-slide-three.tsx b/src/components/sliders/slider-slide-three.tsx
index b32e54a..bbc78a6 100644
--- a/src/components/sliders/slider-slide-three.tsx
+++ b/src/components/sliders/slider-slide-three.tsx
@@ -1,30 +1,40 @@
-import { SliderSlide, type SliderSlideProps } from "@/components/sliders/slider-slide";
import Image from "next/image";
+import type { SliderSlideProps } from "@/components/sliders/slider-slide";
export function SliderSlideThree({ index }: SliderSlideProps) {
- return (
-
-
-
Submit Process
-
- {index + 1}/4 terms & conditions
-
-
+ return (
+
+
+
Submit Process
+
+ {index + 1}/4 terms & conditions
+
+
-
-
-
+
+
+
+ );
}
diff --git a/src/components/ui/info-progress-card.tsx b/src/components/ui/info-progress-card.tsx
index 730b440..cbe80cc 100644
--- a/src/components/ui/info-progress-card.tsx
+++ b/src/components/ui/info-progress-card.tsx
@@ -1,10 +1,14 @@
-import { IoInformation, IoInformationCircle, IoPerson } from "react-icons/io5";
+import Image from "next/image";
+import Link from "next/link";
+import { useId } from "react";
type InfoProgressCardProps = {
title: string;
- requiredLabel?: string;
+ requiredLabel: boolean;
estimate: string;
progress: number;
+ tooltip?: string;
+ slug: string;
};
const RADIUS = 12;
@@ -12,81 +16,115 @@ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
export function InfoProgressCard({
title,
- requiredLabel = "(Required)",
+ requiredLabel = false,
estimate,
progress,
+ tooltip,
+ slug,
}: InfoProgressCardProps) {
+ const tooltipId = useId();
const normalizedProgress = Math.max(0, Math.min(progress, 100));
const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE;
return (
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
- {title}{" "}
-
- {requiredLabel}
-
-
-
- Estimate time: {estimate}
-
-
+
+
+
+
+ {title}{" "}
+
+ {requiredLabel && "(Required)"}
+
+
+
-
-
+ {tooltip ? (
+
+ ) : null}
+
-
-
-
-
- {normalizedProgress}%
-
+
+
+ Estimate time: {estimate}
+
+
+ {normalizedProgress < 100 ? (
+
+ ) : (
+
+ )}
+
+ {normalizedProgress}%
+
+
-
-
+
+
);
}
diff --git a/src/components/ui/information-sheet.tsx b/src/components/ui/information-sheet.tsx
new file mode 100644
index 0000000..49b5f56
--- /dev/null
+++ b/src/components/ui/information-sheet.tsx
@@ -0,0 +1,206 @@
+"use client";
+
+import Image, { type StaticImageData } from "next/image";
+import type { HTMLAttributes, ReactNode } from "react";
+import { useEffect, useState } from "react";
+import Button from "@/components/ui/button";
+
+type InformationSheetPresetIcon =
+ | "play"
+ | "warning"
+ | "coin"
+ | "stash_play-solid.svg"
+ | "warning.svg"
+ | "coin.svg";
+
+type InformationSheetIcon =
+ | InformationSheetPresetIcon
+ | string
+ | {
+ src: string | StaticImageData;
+ alt?: string;
+ width?: number;
+ height?: number;
+ };
+
+export type InformationSheetProps = Omit<
+ HTMLAttributes
,
+ "children" | "title"
+> & {
+ icon?: InformationSheetIcon;
+ title: ReactNode;
+ description?: ReactNode;
+ buttons?: ReactNode;
+ closeOnOutside?: boolean;
+};
+
+const DEFAULT_ICON = {
+ src: "/assets/images/stash_play-solid.svg",
+ alt: "Play",
+ width: 50,
+ height: 50,
+} as const;
+
+const ICON_PRESETS: Record<
+ InformationSheetPresetIcon,
+ {
+ src: string;
+ alt: string;
+ width: number;
+ height: number;
+ }
+> = {
+ play: DEFAULT_ICON,
+ "stash_play-solid.svg": DEFAULT_ICON,
+ warning: {
+ src: "/assets/images/Vector.svg",
+ alt: "Warning",
+ width: 36,
+ height: 36,
+ },
+ "warning.svg": {
+ src: "/assets/images/Vector.svg",
+ alt: "Warning",
+ width: 36,
+ height: 36,
+ },
+ coin: {
+ src: "/assets/images/Inner Plugdsain Iframe.svg",
+ alt: "Coin",
+ width: 50,
+ height: 50,
+ },
+ "coin.svg": {
+ src: "/assets/images/Inner Plugdsain Iframe.svg",
+ alt: "Coin",
+ width: 50,
+ height: 50,
+ },
+};
+
+function resolveIcon(icon: InformationSheetIcon | undefined) {
+ if (!icon) {
+ return DEFAULT_ICON;
+ }
+
+ if (typeof icon === "string") {
+ return (
+ ICON_PRESETS[icon as InformationSheetPresetIcon] ?? {
+ src: icon,
+ alt: "Information",
+ width: DEFAULT_ICON.width,
+ height: DEFAULT_ICON.height,
+ }
+ );
+ }
+
+ return {
+ src: icon.src,
+ alt: icon.alt ?? "Information",
+ width: icon.width ?? DEFAULT_ICON.width,
+ height: icon.height ?? DEFAULT_ICON.height,
+ };
+}
+
+export function InformationSheet({
+ icon,
+ title,
+ description,
+ buttons,
+ closeOnOutside = true,
+ className,
+ ...props
+}: InformationSheetProps) {
+ const [isOpen, setIsOpen] = useState(true);
+ const resolvedIcon = resolveIcon(icon);
+ const resolvedButtons =
+ typeof buttons === "string" ? (
+
+ ) : (
+ buttons
+ );
+
+ useEffect(() => {
+ if (!isOpen) {
+ return;
+ }
+
+ const previousBodyOverflow = document.body.style.overflow;
+ const previousHtmlOverflow = document.documentElement.style.overflow;
+
+ document.body.style.overflow = "hidden";
+ document.documentElement.style.overflow = "hidden";
+
+ return () => {
+ document.body.style.overflow = previousBodyOverflow;
+ document.documentElement.style.overflow = previousHtmlOverflow;
+ };
+ }, [isOpen]);
+
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+ {
+ if (closeOnOutside && event.target === event.currentTarget) {
+ setIsOpen(false);
+ }
+ }}
+ onKeyDown={(event) => {
+ if (
+ closeOnOutside &&
+ event.target === event.currentTarget &&
+ (event.key === "Escape" || event.key === "Enter" || event.key === " ")
+ ) {
+ event.preventDefault();
+ setIsOpen(false);
+ }
+ }}
+ >
+
+
+
+
+
+ {title}
+
+
+ {description ? (
+
{description}
+ ) : null}
+
+ {resolvedButtons ? (
+
+ {resolvedButtons}
+
+ ) : null}
+
+
+
+ );
+}
+
+export default InformationSheet;
diff --git a/src/components/utils/page-background.tsx b/src/components/utils/page-background.tsx
index 6c95ec0..fb66646 100644
--- a/src/components/utils/page-background.tsx
+++ b/src/components/utils/page-background.tsx
@@ -14,7 +14,9 @@ export function PageBackground({
useEffect(() => {
const { body } = document;
const previousMode = body.dataset.pageBackground;
- const previousImage = body.style.getPropertyValue("--page-background-image");
+ const previousImage = body.style.getPropertyValue(
+ "--page-background-image",
+ );
if (disabled) {
body.dataset.pageBackground = "none";
diff --git a/src/data/mock-questions.json b/src/data/mock-questions.json
new file mode 100644
index 0000000..d8e5f9b
--- /dev/null
+++ b/src/data/mock-questions.json
@@ -0,0 +1,266 @@
+[
+ {
+ "title": "Personal Information",
+ "icon": "user-circle",
+ "slug": "personal-information",
+ "required": true,
+ "estimateTime": "5 minutes",
+ "tooltip": "Basic identity and profile details for the applicant.",
+ "progress": 35,
+ "description": "Collects general personal details to start the marriage application flow.",
+ "questions": [
+ {
+ "title": "Full Name",
+ "type": "text",
+ "required": true,
+ "description": "Enter your legal full name as it appears on official documents.",
+ "tooltip": "Use your passport or national ID spelling.",
+ "extras": {
+ "placeHolder": "e.g. Sara Ahmadi",
+ "range": [0, 0],
+ "options": []
+ }
+ },
+ {
+ "title": "Date of Birth",
+ "type": "date",
+ "required": true,
+ "description": "Select your date of birth.",
+ "tooltip": "Make sure the date matches your official record.",
+ "extras": {
+ "placeHolder": "YYYY-MM-DD",
+ "range": [0, 0],
+ "options": []
+ }
+ },
+ {
+ "title": "Gender",
+ "type": "radio",
+ "required": true,
+ "description": "Choose the gender option that applies to you.",
+ "tooltip": "Only one option can be selected.",
+ "extras": {
+ "placeHolder": "",
+ "range": [0, 0],
+ "options": ["Female", "Male", "Prefer not to say"]
+ }
+ },
+ {
+ "title": "Age",
+ "type": "number",
+ "required": true,
+ "description": "Provide your current age in years.",
+ "tooltip": "Numbers only.",
+ "extras": {
+ "placeHolder": "e.g. 29",
+ "range": [18, 80],
+ "options": []
+ }
+ }
+ ]
+ },
+ {
+ "title": "Preferences",
+ "icon": "heart-handshake",
+ "slug": "preferences",
+ "required": false,
+ "estimateTime": "7 minutes",
+ "tooltip": "Relationship expectations and lifestyle preferences.",
+ "progress": 60,
+ "description": "Captures values, preferences, and match expectations.",
+ "questions": [
+ {
+ "title": "Preferred City",
+ "type": "dropdown",
+ "required": false,
+ "description": "Select the city you prefer to live in after marriage.",
+ "tooltip": "You can use this to help with compatibility matching.",
+ "extras": {
+ "placeHolder": "Choose a city",
+ "range": [0, 0],
+ "options": ["Tehran", "Isfahan", "Shiraz", "Tabriz", "Mashhad"]
+ }
+ },
+ {
+ "title": "Describe Your Ideal Partner",
+ "type": "text",
+ "required": false,
+ "description": "Write a short description of the qualities you value most.",
+ "tooltip": "Keep it concise and specific.",
+ "extras": {
+ "placeHolder": "Kind, family-oriented, emotionally mature...",
+ "range": [0, 0],
+ "options": []
+ }
+ },
+ {
+ "title": "Importance of Family Values",
+ "type": "slider",
+ "required": true,
+ "description": "Rate how important family values are to you.",
+ "tooltip": "Move the slider from low to high importance.",
+ "extras": {
+ "placeHolder": "",
+ "range": [1, 10],
+ "options": []
+ }
+ },
+ {
+ "title": "Ready to Continue",
+ "type": "button",
+ "required": false,
+ "description": "Confirms that you want to proceed to the next section.",
+ "tooltip": "This can be used as a UI action trigger.",
+ "extras": {
+ "placeHolder": "Continue",
+ "range": [0, 0],
+ "options": ["Continue"]
+ }
+ }
+ ]
+ },
+ {
+ "title": "Documents",
+ "icon": "file-text",
+ "slug": "documents",
+ "required": true,
+ "estimateTime": "3 minutes",
+ "tooltip": "Upload supporting documents required for verification.",
+ "progress": 100,
+ "description": "Handles document uploads and verification-related information.",
+ "questions": [
+ {
+ "title": "National ID Upload",
+ "type": "file",
+ "required": true,
+ "description": "Upload a clear image or PDF of your national ID.",
+ "tooltip": "Accepted formats can be restricted in the UI layer.",
+ "extras": {
+ "placeHolder": "Choose a file",
+ "range": [0, 0],
+ "options": [".jpg", ".png", ".pdf"]
+ }
+ },
+ {
+ "title": "Marriage Timeline",
+ "type": "dropdown",
+ "required": false,
+ "description": "Choose your preferred timeline for marriage.",
+ "tooltip": "This helps prioritize urgency and compatibility.",
+ "extras": {
+ "placeHolder": "Select a timeline",
+ "range": [0, 0],
+ "options": ["Within 6 months", "6-12 months", "1-2 years", "Flexible"]
+ }
+ }
+ ]
+ },
+ {
+ "title": "Question Type Showcase",
+ "icon": "layout-grid",
+ "slug": "question-type-showcase",
+ "required": false,
+ "estimateTime": "10 minutes",
+ "tooltip": "A complete sample box that demonstrates every supported question type with all fields populated.",
+ "progress": 0,
+ "description": "Includes one example of each question type so the UI can be tested against a complete dataset.",
+ "questions": [
+ {
+ "title": "Short Biography",
+ "type": "text",
+ "required": true,
+ "description": "Provide a short written introduction.",
+ "tooltip": "This exercises the text input type.",
+ "extras": {
+ "placeHolder": "Write a short introduction about yourself",
+ "range": [0, 0],
+ "options": []
+ }
+ },
+ {
+ "title": "Preferred Wedding Date",
+ "type": "date",
+ "required": false,
+ "description": "Select your preferred wedding date.",
+ "tooltip": "This exercises the date picker type.",
+ "extras": {
+ "placeHolder": "YYYY-MM-DD",
+ "range": [0, 0],
+ "options": []
+ }
+ },
+ {
+ "title": "Preferred Contact Method",
+ "type": "radio",
+ "required": true,
+ "description": "Choose one preferred contact method.",
+ "tooltip": "This exercises the radio selection type.",
+ "extras": {
+ "placeHolder": "Select one option",
+ "range": [0, 0],
+ "options": ["Phone", "WhatsApp", "Email"]
+ }
+ },
+ {
+ "title": "Household Size Preference",
+ "type": "number",
+ "required": false,
+ "description": "Enter the household size you are comfortable with.",
+ "tooltip": "This exercises the number input type.",
+ "extras": {
+ "placeHolder": "e.g. 4",
+ "range": [1, 12],
+ "options": []
+ }
+ },
+ {
+ "title": "Current City",
+ "type": "dropdown",
+ "required": true,
+ "description": "Choose the city you currently live in.",
+ "tooltip": "This exercises the dropdown type.",
+ "extras": {
+ "placeHolder": "Select your city",
+ "range": [0, 0],
+ "options": ["Tehran", "Karaj", "Shiraz", "Mashhad"]
+ }
+ },
+ {
+ "title": "Importance of Shared Goals",
+ "type": "slider",
+ "required": true,
+ "description": "Rate how important shared long-term goals are to you.",
+ "tooltip": "This exercises the slider type.",
+ "extras": {
+ "placeHolder": "Move the slider",
+ "range": [1, 10],
+ "options": []
+ }
+ },
+ {
+ "title": "Review Answers",
+ "type": "button",
+ "required": false,
+ "description": "Use this action to review the current section.",
+ "tooltip": "This exercises the button type.",
+ "extras": {
+ "placeHolder": "Review",
+ "range": [0, 0],
+ "options": ["Review"]
+ }
+ },
+ {
+ "title": "Profile Photo Upload",
+ "type": "file",
+ "required": false,
+ "description": "Upload a profile photo or supporting image.",
+ "tooltip": "This exercises the file upload type.",
+ "extras": {
+ "placeHolder": "Choose an image",
+ "range": [0, 0],
+ "options": [".jpg", ".jpeg", ".png"]
+ }
+ }
+ ]
+ }
+]
diff --git a/src/data/question-data.ts b/src/data/question-data.ts
new file mode 100644
index 0000000..580a95c
--- /dev/null
+++ b/src/data/question-data.ts
@@ -0,0 +1,83 @@
+import rawQuestions from "@/data/mock-questions.json";
+
+export const bookingTerms = [
+ "You will be contacted by your consultant.",
+ "The call may start 10-15 minutes earlier or later than scheduled.",
+ "Make sure you are available and in a quiet place at least 10 minutes before the session.",
+] as const;
+
+export type QuestionCardIcon =
+ | "profile"
+ | "education"
+ | "details"
+ | "checklist"
+ | "contact";
+
+type QuestionExtras = {
+ placeHolder: string;
+ range: [number, number];
+ options: string[];
+};
+
+export type QuestionField = {
+ title: string;
+ type: string;
+ required: boolean;
+ description: string;
+ tooltip: string;
+ extras: QuestionExtras;
+};
+
+export type QuestionListItem = {
+ slug: string;
+ title: string;
+ estimate: string;
+ progress: number;
+ icon: QuestionCardIcon;
+ required?: boolean;
+ note?: string;
+ showInfoBadge?: boolean;
+ summary: string;
+ checkpoints: readonly string[];
+ tooltip: string;
+ questions: readonly QuestionField[];
+};
+
+type RawQuestionListItem = {
+ title: string;
+ icon: string;
+ slug: string;
+ required?: boolean;
+ estimateTime: string;
+ tooltip: string;
+ progress: number;
+ description: string;
+ questions: QuestionField[];
+};
+
+const iconMap: Record = {
+ "user-circle": "profile",
+ "heart-handshake": "details",
+ "file-text": "contact",
+ "layout-grid": "checklist",
+};
+
+export const questionListItems: readonly QuestionListItem[] = (
+ rawQuestions as RawQuestionListItem[]
+).map((item) => ({
+ slug: item.slug,
+ title: item.title,
+ estimate: item.estimateTime,
+ progress: item.progress,
+ icon: iconMap[item.icon] ?? "details",
+ required: item.required,
+ showInfoBadge: Boolean(item.tooltip),
+ summary: item.description,
+ checkpoints: item.questions.map((question) => question.title),
+ tooltip: item.tooltip,
+ questions: item.questions,
+}));
+
+export function getQuestionListItemBySlug(slug: string) {
+ return questionListItems.find((item) => item.slug === slug);
+}
diff --git a/src/plugins/add-data-locator.cjs b/src/plugins/add-data-locator.cjs
new file mode 100644
index 0000000..af2482b
--- /dev/null
+++ b/src/plugins/add-data-locator.cjs
@@ -0,0 +1,40 @@
+module.exports = function addDataLocator({ types: t }) {
+ return {
+ name: "add-data-locator",
+ visitor: {
+ JSXOpeningElement(path, state) {
+ if (process.env.NODE_ENV !== "development") {
+ return;
+ }
+
+ const filePath = state.file.opts.filename;
+
+ if (!filePath || filePath.includes("node_modules")) {
+ return;
+ }
+
+ const attributeExists = path.node.attributes.some(
+ (attribute) =>
+ t.isJSXAttribute(attribute) &&
+ t.isJSXIdentifier(attribute.name) &&
+ attribute.name.name === "data-locator",
+ );
+
+ if (attributeExists) {
+ return;
+ }
+
+ const lineNumber = path.node.loc?.start.line ?? "unknown";
+ const columnNumber = path.node.loc?.start.column ?? "unknown";
+ const locatorValue = `${filePath}:${lineNumber}:${columnNumber}`;
+
+ path.node.attributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier("data-locator"),
+ t.stringLiteral(locatorValue),
+ ),
+ );
+ },
+ },
+ };
+};