-
-
-
+
+
);
}
diff --git a/src/app/questions/page.tsx b/src/app/questions/page.tsx
new file mode 100644
index 0000000..8350dc7
--- /dev/null
+++ b/src/app/questions/page.tsx
@@ -0,0 +1,261 @@
+"use client";
+
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+
+function BackIcon() {
+ return (
+
+
+
+ );
+}
+
+const slides = [
+ {
+ helperText: "Enter your age in years.",
+ helper: "Question 1",
+ inputType: "number",
+ placeholder: "Type your age",
+ question: "How old are you?",
+ title: "Let us begin with your age",
+ },
+ {
+ helperText: "A short description is enough for now.",
+ helper: "Question 2",
+ inputType: "textarea",
+ placeholder:
+ "Write a few lines about yourself, your goals, or your family values",
+ question:
+ "How would you introduce yourself for a serious Islamic marriage?",
+ title: "Share a brief introduction",
+ },
+ {
+ helperText: "Use your actual date of birth.",
+ helper: "Question 3",
+ inputType: "date",
+ question: "What is your date of birth?",
+ title: "Confirm your birth date",
+ },
+] as const;
+
+const progressWidth = 132;
+
+function ProgressBar({
+ currentIndex,
+ totalSlides,
+}: {
+ currentIndex: number;
+ totalSlides: number;
+}) {
+ const segments = Array.from({ length: totalSlides }, (_, index) => index);
+
+ return (
+
+
+ {segments.map((segment) => {
+ const isActive = segment <= currentIndex;
+
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default function QuestionsPage() {
+ const [currentSlide, setCurrentSlide] = useState(0);
+ const [answers, setAnswers] = useState
>({});
+ const router = useRouter();
+ const lastSlideIndex = slides.length - 1;
+ const activeSlide = slides[currentSlide];
+ const currentAnswer = answers[currentSlide] ?? "";
+ const hasAnswer = currentAnswer.trim().length > 0;
+
+ const handleChange = (value: string) => {
+ setAnswers((currentAnswers) => ({
+ ...currentAnswers,
+ [currentSlide]: value,
+ }));
+ };
+
+ const handleBack = () => {
+ setCurrentSlide((previousSlide) => Math.max(previousSlide - 1, 0));
+ };
+
+ const handleNext = () => {
+ if (!hasAnswer) {
+ return;
+ }
+
+ setCurrentSlide((previousSlide) =>
+ Math.min(previousSlide + 1, lastSlideIndex),
+ );
+ };
+
+ const handleFinish = () => {
+ if (!hasAnswer) {
+ return;
+ }
+
+ router.push("/details");
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {activeSlide.helper}
+
+
+ {activeSlide.title}
+
+
+ {activeSlide.question}
+
+
+ {activeSlide.helperText}
+
+
+
+
+ {activeSlide.inputType === "number" ? (
+ handleChange(event.target.value)}
+ placeholder={activeSlide.placeholder}
+ type="number"
+ value={currentAnswer}
+ />
+ ) : null}
+
+ {activeSlide.inputType === "textarea" ? (
+
+
+
+
+ Current answer: {currentAnswer || "Complete the field above"}
+
+
+ {currentSlide === lastSlideIndex ? (
+
+ Finish
+
+ ) : (
+
+ Next
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/rules/page.tsx b/src/app/rules/page.tsx
new file mode 100644
index 0000000..aea25b9
--- /dev/null
+++ b/src/app/rules/page.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import Link from "next/link";
+import { useEffect, useState } from "react";
+
+function BackIcon() {
+ return (
+
+
+
+ );
+}
+
+const waitDuration = 20;
+
+export default function RulesPage() {
+ const [secondsLeft, setSecondsLeft] = useState(waitDuration);
+
+ useEffect(() => {
+ if (secondsLeft <= 0) {
+ return;
+ }
+
+ const timer = window.setInterval(() => {
+ setSecondsLeft((currentValue) => Math.max(currentValue - 1, 0));
+ }, 1000);
+
+ return () => window.clearInterval(timer);
+ }, [secondsLeft]);
+
+ const isEnabled = secondsLeft === 0;
+
+ return (
+
+
+
+
+
+
+
+ Habib Marriage is a secure platform exclusively for permanent
+ Islamic marriage. Its core principles include a zero-tolerance
+ policy for casual dating or temporary marriage, absolute privacy
+ through one-on-one matching instead of public catalogs, mandatory
+ identity verification for all users, and a focus on traditional
+ guardian (Wali) and family involvement.
+
+
+
+
+
+ ①
+
+
+ A Secure and Purposeful Path to Permanent Union
+
+
+
+
+
+ Habib Marriage is not a typical matchmaking network. We have
+ come together with the goal of creating a secure and
+ confidential path for "permanent marriage" among
+ Muslims.
+
+
+ To preserve your dignity, no catalog of individuals is
+ displayed here, introductions are strictly purposeful and
+ one-on-one, and identity checks are required for all users.
+
+
+ Please review our system regulations and red lines, including
+ the strict prohibition of dating before the confirmation box,
+ so that you may proceed on this path with confidence.
+
+
+
+
+
+
+
+ {isEnabled ? "Next" : `Next (${secondsLeft}s)`}
+
+
+
+
+
+ );
+}
diff --git a/src/app/video-2/page.tsx b/src/app/video-2/page.tsx
new file mode 100644
index 0000000..1aa9e87
--- /dev/null
+++ b/src/app/video-2/page.tsx
@@ -0,0 +1,237 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+
+function BackIcon() {
+ return (
+
+
+
+ );
+}
+
+function HistoryIcon() {
+ return (
+
+
+
+
+ );
+}
+
+function PlayIcon() {
+ return (
+
+
+
+ );
+}
+
+const metrics = [
+ { label: "marriage applicants", value: "120" },
+ { label: "Successful marriage", value: "120" },
+];
+
+const mockVideoDuration = 12;
+
+export default function VideoPageTwo() {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [watchedSeconds, setWatchedSeconds] = useState(0);
+ const isCompleted = watchedSeconds >= mockVideoDuration;
+ const progressPercent = Math.min(
+ (watchedSeconds / mockVideoDuration) * 100,
+ 100,
+ );
+
+ useEffect(() => {
+ if (!isPlaying || isCompleted) {
+ return;
+ }
+
+ const timer = window.setInterval(() => {
+ setWatchedSeconds((currentValue) => {
+ const nextValue = Math.min(currentValue + 1, mockVideoDuration);
+
+ if (nextValue >= mockVideoDuration) {
+ setIsPlaying(false);
+ }
+
+ return nextValue;
+ });
+ }, 1000);
+
+ return () => window.clearInterval(timer);
+ }, [isCompleted, isPlaying]);
+
+ const handlePlay = () => {
+ if (isCompleted) {
+ setWatchedSeconds(0);
+ }
+
+ setIsPlaying((currentValue) => !currentValue || isCompleted);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Habib Marriage
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mock video
+ {watchedSeconds}s / 12s
+
+
+
+
+
+
+
+
+
+
+ Watch the full mock video to unlock the next step.
+
+
+
+ {metrics.map((metric) => (
+
+
{metric.value}
+
+ {metric.label}
+
+
+ ))}
+
+
+
+
+ Information recording
+
+
+
+ Next
+
+
+
+
+
+ );
+}
diff --git a/src/app/video/page.tsx b/src/app/video/page.tsx
new file mode 100644
index 0000000..d31a5c6
--- /dev/null
+++ b/src/app/video/page.tsx
@@ -0,0 +1,237 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+
+function BackIcon() {
+ return (
+
+
+
+ );
+}
+
+function HistoryIcon() {
+ return (
+
+
+
+
+ );
+}
+
+function PlayIcon() {
+ return (
+
+
+
+ );
+}
+
+const metrics = [
+ { label: "marriage applicants", value: "120" },
+ { label: "Successful marriage", value: "120" },
+];
+
+const mockVideoDuration = 12;
+
+export default function VideoPage() {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [watchedSeconds, setWatchedSeconds] = useState(0);
+ const isCompleted = watchedSeconds >= mockVideoDuration;
+ const progressPercent = Math.min(
+ (watchedSeconds / mockVideoDuration) * 100,
+ 100,
+ );
+
+ useEffect(() => {
+ if (!isPlaying || isCompleted) {
+ return;
+ }
+
+ const timer = window.setInterval(() => {
+ setWatchedSeconds((currentValue) => {
+ const nextValue = Math.min(currentValue + 1, mockVideoDuration);
+
+ if (nextValue >= mockVideoDuration) {
+ setIsPlaying(false);
+ }
+
+ return nextValue;
+ });
+ }, 1000);
+
+ return () => window.clearInterval(timer);
+ }, [isCompleted, isPlaying]);
+
+ const handlePlay = () => {
+ if (isCompleted) {
+ setWatchedSeconds(0);
+ }
+
+ setIsPlaying((currentValue) => !currentValue || isCompleted);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Habib Marriage
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mock video
+ {watchedSeconds}s / 12s
+
+
+
+
+
+
+
+
+
+
+ Watch the full mock video to unlock the next step.
+
+
+
+ {metrics.map((metric) => (
+
+
{metric.value}
+
+ {metric.label}
+
+
+ ))}
+
+
+
+
+ Information recording
+
+
+
+ Next
+
+
+
+
+
+ );
+}
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..7d020f4
--- /dev/null
+++ b/src/components/dev/dev-click-to-component.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { useEffect } from "react";
+
+const IDE_SCHEMES: Record string> = {
+ antigravity: (locator) => `antigravity://file/${locator}`,
+ cursor: (locator) => `cursor://file/${locator}`,
+ vscode: (locator) => `vscode://file/${locator}`,
+ webstorm: (locator) => `webstorm://open?file=${locator}`,
+ sublime: (locator) => `subl://open?url=file://${locator}`,
+ atom: (locator) => `atom://open?url=file://${locator}`,
+};
+
+function resolveIdeUrl(locator: string) {
+ const preferredIde =
+ process.env.NEXT_PUBLIC_CLICK_TO_COMPONENT_IDE?.toLowerCase() ?? "vscode";
+
+ const userAgent = navigator.userAgent.toLowerCase();
+ const detectedIde =
+ (Object.keys(IDE_SCHEMES).find((ide) => userAgent.includes(ide)) as
+ | keyof typeof IDE_SCHEMES
+ | undefined) ?? preferredIde;
+
+ const scheme =
+ IDE_SCHEMES[detectedIde] ?? IDE_SCHEMES[preferredIde] ?? IDE_SCHEMES.vscode;
+
+ return scheme(locator);
+}
+
+export function DevClickToComponent() {
+ useEffect(() => {
+ const handleClick = (event: MouseEvent) => {
+ if (!event.altKey) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ const locator = target
+ .closest("[data-locator]")
+ ?.getAttribute("data-locator");
+
+ if (!locator) {
+ return;
+ }
+
+ try {
+ window.location.assign(resolveIdeUrl(locator));
+ } catch (error) {
+ console.error("Failed to open file in IDE:", error);
+ }
+ };
+
+ document.addEventListener("click", handleClick, true);
+
+ return () => {
+ document.removeEventListener("click", handleClick, true);
+ };
+ }, []);
+
+ return null;
+}
diff --git a/src/lib/detailed-questions.ts b/src/lib/detailed-questions.ts
new file mode 100644
index 0000000..709b166
--- /dev/null
+++ b/src/lib/detailed-questions.ts
@@ -0,0 +1,169 @@
+export type DetailedQuestionField =
+ | {
+ description: string;
+ id: string;
+ label: string;
+ placeholder: string;
+ type: "number" | "text";
+ }
+ | {
+ description: string;
+ id: string;
+ label: string;
+ type: "date";
+ }
+ | {
+ description: string;
+ id: string;
+ label: string;
+ placeholder: string;
+ type: "textarea";
+ };
+
+export type DetailedSection = {
+ description: string;
+ id: string;
+ questions: DetailedQuestionField[];
+ title: string;
+};
+
+export const detailedSections: DetailedSection[] = [
+ {
+ description:
+ "Add a few practical details about your work, income readiness, and future planning.",
+ id: "financial-status",
+ questions: [
+ {
+ description:
+ "Enter an approximate monthly amount in your local currency.",
+ id: "monthly_income",
+ label: "What is your approximate monthly income?",
+ placeholder: "e.g. 4500",
+ type: "number",
+ },
+ {
+ description:
+ "Mention your profession, current work situation, or study path if relevant.",
+ id: "work_summary",
+ label: "How would you describe your work or financial situation?",
+ placeholder:
+ "Write a short summary about your work and financial stability",
+ type: "textarea",
+ },
+ {
+ description:
+ "Pick a realistic date for when you feel prepared to marry.",
+ id: "marriage_ready_date",
+ label: "By what date do you hope to be financially ready for marriage?",
+ type: "date",
+ },
+ ],
+ title: "Financial Status",
+ },
+ {
+ description:
+ "Share background that helps frame family expectations, household dynamics, and support.",
+ id: "family-background",
+ questions: [
+ {
+ description:
+ "Include city, country, or current living arrangement if useful.",
+ id: "family_home",
+ label: "Where is your family currently based?",
+ placeholder: "e.g. Tehran, Iran",
+ type: "text",
+ },
+ {
+ description:
+ "A short note about parental support, guardian involvement, or family culture is enough.",
+ id: "family_expectations",
+ label:
+ "How would you describe your family environment and expectations?",
+ placeholder:
+ "Describe your family involvement and expectations around marriage",
+ type: "textarea",
+ },
+ {
+ description: "Use the date that matters most for your family process.",
+ id: "family_meeting_date",
+ label: "When would your family be ready for formal introductions?",
+ type: "date",
+ },
+ ],
+ title: "Family Background",
+ },
+ {
+ description:
+ "Capture a few core details about routines, values, and the way you want to build your home.",
+ id: "personal-practice",
+ questions: [
+ {
+ description:
+ "A simple number helps set expectations about schedule and location.",
+ id: "weekly_schedule",
+ label:
+ "How many evenings per week are you usually free for family time?",
+ placeholder: "e.g. 4",
+ type: "number",
+ },
+ {
+ description:
+ "Explain the values, habits, and home atmosphere you hope to maintain.",
+ id: "home_values",
+ label:
+ "What kind of home life and religious practice do you hope to maintain?",
+ placeholder:
+ "Write about the values and rhythm you want in married life",
+ type: "textarea",
+ },
+ {
+ description:
+ "Pick a date that reflects when you want to begin active spouse meetings.",
+ id: "search_start",
+ label: "When would you like to begin serious spouse discussions?",
+ type: "date",
+ },
+ ],
+ title: "Personal Practice",
+ },
+];
+
+export function getDetailedSection(sectionId: string) {
+ return detailedSections.find((section) => section.id === sectionId);
+}
+
+export function getDetailedSectionStorageKey(sectionId: string) {
+ return `habib-detailed-section-${sectionId}`;
+}
+
+export function countCompletedDetailedAnswers(
+ section: DetailedSection,
+ answers: Record,
+) {
+ return section.questions.filter((question) => {
+ const value = answers[question.id];
+ return typeof value === "string" && value.trim().length > 0;
+ }).length;
+}
+
+export function getDetailedSectionProgress(
+ section: DetailedSection,
+ answers: Record,
+) {
+ if (section.questions.length === 0) {
+ return 0;
+ }
+
+ return Math.round(
+ (countCompletedDetailedAnswers(section, answers) /
+ section.questions.length) *
+ 100,
+ );
+}
+
+export function getDetailedQuestionCount() {
+ return detailedSections.reduce(
+ (total, section) => total + section.questions.length,
+ 0,
+ );
+}
diff --git a/src/plugins/add-data-locator.js b/src/plugins/add-data-locator.js
new file mode 100644
index 0000000..c7e7d6d
--- /dev/null
+++ b/src/plugins/add-data-locator.js
@@ -0,0 +1,40 @@
+module.exports = function addDataLocator({ types: t }) {
+ return {
+ name: "add-data-locator",
+ visitor: {
+ JSXOpeningElement(path, state) {
+ const filePath = state.file.opts.filename;
+
+ if (
+ !filePath ||
+ filePath.includes("node_modules") ||
+ filePath.includes(".next")
+ ) {
+ return;
+ }
+
+ const attributeExists = path.node.attributes.some(
+ (attribute) =>
+ attribute?.type === "JSXAttribute" &&
+ attribute.name?.type === "JSXIdentifier" &&
+ 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),
+ ),
+ );
+ },
+ },
+ };
+};