Browse Source

feat: add questions, rules, and video pages with navigation and progress tracking

- Implemented QuestionsPage component for user input with a multi-slide format.
- Created RulesPage component with a countdown timer and terms for marriage.
- Developed VideoPage and VideoPageTwo components for video playback and progress tracking.
- Added DevClickToComponent for IDE integration via data-locator attributes.
- Introduced detailed questions structure and utility functions for managing user responses.
- Enhanced user experience with responsive design and accessibility features.
master
sina_sajjadi 3 months ago
parent
commit
2f16c54ab0
  1. 8
      .babelrc
  2. 2
      AGENTS.md
  3. 5
      public/assets/images/Group 1.svg
  4. BIN
      public/assets/images/Intro-Quran.png
  5. BIN
      public/assets/images/Intro-location.png
  6. BIN
      public/assets/images/Rectangle 3077.png
  7. 244
      src/app/details/[section]/detail-section-client.tsx
  8. 18
      src/app/details/[section]/page.tsx
  9. 37
      src/app/details/complete/page.tsx
  10. 173
      src/app/details/page.tsx
  11. 23
      src/app/globals.css
  12. 180
      src/app/intro/page.tsx
  13. 30
      src/app/layout.tsx
  14. 120
      src/app/page.tsx
  15. 261
      src/app/questions/page.tsx
  16. 125
      src/app/rules/page.tsx
  17. 237
      src/app/video-2/page.tsx
  18. 237
      src/app/video/page.tsx
  19. 68
      src/components/dev/dev-click-to-component.tsx
  20. 169
      src/lib/detailed-questions.ts
  21. 40
      src/plugins/add-data-locator.js

8
.babelrc

@ -0,0 +1,8 @@
{
"presets": ["next/babel"],
"env": {
"development": {
"plugins": ["./src/plugins/add-data-locator.js"]
}
}
}

2
AGENTS.md

@ -2,4 +2,4 @@
# This is NOT the Next.js you know # This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
<!-- END:nextjs-agent-rules -->

5
public/assets/images/Group 1.svg

@ -0,0 +1,5 @@
<svg width="21" height="17" viewBox="0 0 21 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.2163 8.2793H1.0625" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 8.2793L8.24972 15.5585" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 8.27919L8.24972 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

BIN
public/assets/images/Intro-Quran.png

After

Width: 375  |  Height: 813  |  Size: 185 KiB

BIN
public/assets/images/Intro-location.png

After

Width: 375  |  Height: 813  |  Size: 154 KiB

BIN
public/assets/images/Rectangle 3077.png

After

Width: 344  |  Height: 235  |  Size: 127 KiB

244
src/app/details/[section]/detail-section-client.tsx

@ -0,0 +1,244 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import {
countCompletedDetailedAnswers,
detailedSections,
type DetailedSection,
getDetailedQuestionCount,
getDetailedSectionProgress,
getDetailedSectionStorageKey,
} from "@/lib/detailed-questions";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
export default function DetailSectionClient({
section,
}: {
section: DetailedSection;
}) {
const [answers, setAnswers] = useState<Record<string, string>>({});
const [isHydrated, setIsHydrated] = useState(false);
const router = useRouter();
useEffect(() => {
const savedAnswers = window.localStorage.getItem(
getDetailedSectionStorageKey(section.id),
);
setAnswers(
savedAnswers ? (JSON.parse(savedAnswers) as Record<string, string>) : {},
);
setIsHydrated(true);
}, [section.id]);
useEffect(() => {
if (!isHydrated) {
return;
}
window.localStorage.setItem(
getDetailedSectionStorageKey(section.id),
JSON.stringify(answers),
);
const completedTotal = detailedSections.reduce((total, currentSection) => {
const sectionAnswers =
currentSection.id === section.id
? answers
: ((JSON.parse(
window.localStorage.getItem(
getDetailedSectionStorageKey(currentSection.id),
) ?? "{}",
) as Record<string, string>) ?? {});
return (
total + countCompletedDetailedAnswers(currentSection, sectionAnswers)
);
}, 0);
if (completedTotal === getDetailedQuestionCount()) {
router.replace("/details/complete");
}
}, [answers, isHydrated, router, section, section.id]);
const completedCount = useMemo(
() => countCompletedDetailedAnswers(section, answers),
[answers, section],
);
const progress = useMemo(
() => getDetailedSectionProgress(section, answers),
[answers, section],
);
const handleChange = (questionId: string, value: string) => {
setAnswers((currentAnswers) => ({
...currentAnswers,
[questionId]: value,
}));
};
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFB8CE_0%,rgba(255,184,206,0.26)_12%,rgba(255,255,255,0)_34%),linear-gradient(180deg,#FFFDFD_0%,#F9F4F6_100%)]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-5 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between">
<Link
aria-label="Back to detailed questions"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/details"
>
<BackIcon />
</Link>
<h1
className="text-[20px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
{section.title}
</h1>
<span className="w-11 text-right text-[12px] font-semibold text-[#6E656B]">
{progress}%
</span>
</div>
<div className="mt-6 rounded-[26px] bg-white/90 px-5 py-6 shadow-[0_18px_40px_rgba(0,0,0,0.07)]">
<p className="text-[12px] font-semibold uppercase tracking-[0.22em] text-[#F05A93]">
Section Progress
</p>
<h2
className="mt-3 text-[27px] font-semibold leading-[1.15] text-[#2E2327]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
{section.title}
</h2>
<p className="mt-3 text-[14px] leading-6 text-[#675E64]">
{section.description}
</p>
<div className="mt-5 h-[10px] overflow-hidden rounded-full bg-[#F7D9E4]">
<div
className="h-full rounded-full bg-linear-to-r from-[#F04C99] to-[#FF8575] transition-[width] duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<p className="mt-3 text-[12px] font-medium tracking-[0.04em] text-[#8B8086]">
{completedCount} of {section.questions.length} questions answered
</p>
</div>
<div className="mt-5 flex-1 space-y-4 overflow-y-auto pb-2">
{section.questions.map((question) => {
const value = answers[question.id] ?? "";
return (
<article
className="rounded-[22px] bg-white/92 px-5 py-5 shadow-[0_14px_34px_rgba(0,0,0,0.06)]"
key={question.id}
>
<h3 className="text-[17px] font-semibold leading-7 text-[#31252A]">
{question.label}
</h3>
<p className="mt-2 text-[13px] leading-6 text-[#72686E]">
{question.description}
</p>
<div className="mt-4">
{question.type === "textarea" ? (
<textarea
className="min-h-[132px] w-full resize-none rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 py-3 text-[15px] leading-7 text-[#33292D] outline-none transition focus:border-[#F05A93]"
onChange={(event) =>
handleChange(question.id, event.target.value)
}
placeholder={question.placeholder}
value={value}
/>
) : null}
{question.type === "text" ? (
<input
className="h-[54px] w-full rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 text-[15px] text-[#33292D] outline-none transition focus:border-[#F05A93]"
onChange={(event) =>
handleChange(question.id, event.target.value)
}
placeholder={question.placeholder}
type="text"
value={value}
/>
) : null}
{question.type === "number" ? (
<input
className="h-[54px] w-full rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 text-[15px] text-[#33292D] outline-none transition focus:border-[#F05A93]"
inputMode="numeric"
onChange={(event) =>
handleChange(question.id, event.target.value)
}
placeholder={question.placeholder}
type="number"
value={value}
/>
) : null}
{question.type === "date" ? (
<input
className="h-[54px] w-full rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 text-[15px] text-[#33292D] outline-none transition focus:border-[#F05A93]"
onChange={(event) =>
handleChange(question.id, event.target.value)
}
type="date"
value={value}
/>
) : null}
</div>
</article>
);
})}
</div>
<div className="mt-4">
<Link
className="flex h-[48px] items-center justify-center rounded-[14px] bg-[#242424] text-[18px] font-semibold text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
href="/details"
>
Back To List
</Link>
</div>
</div>
</section>
</main>
);
}

18
src/app/details/[section]/page.tsx

@ -0,0 +1,18 @@
import { notFound } from "next/navigation";
import DetailSectionClient from "./detail-section-client";
import { getDetailedSection } from "@/lib/detailed-questions";
export default async function DetailedSectionPage({
params,
}: {
params: Promise<{ section: string }>;
}) {
const { section } = await params;
const sectionData = getDetailedSection(section);
if (!sectionData) {
notFound();
}
return <DetailSectionClient section={sectionData} />;
}

37
src/app/details/complete/page.tsx

@ -0,0 +1,37 @@
import Link from "next/link";
export default function DetailedQuestionsCompletePage() {
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFBDD2_0%,rgba(255,189,210,0.24)_14%,rgba(255,255,255,0)_36%),linear-gradient(180deg,#FFFDFD_0%,#FAF4F7_100%)]" />
<div className="relative z-10 flex h-full flex-col items-center justify-center px-7 text-center">
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-linear-to-r from-[#F04C99] to-[#FF8575] text-[34px] font-semibold text-white shadow-[0_24px_44px_rgba(240,76,153,0.25)]">
</div>
<h1
className="mt-10 text-[32px] font-semibold leading-[1.15] text-[#2E2327]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Your profile details are complete
</h1>
<p className="mt-5 max-w-[280px] text-[16px] leading-8 text-[#665D63]">
We have received all of the required information. Our review may
take a little time, and we will notify you as soon as there is an
update.
</p>
<Link
className="mt-12 flex h-[48px] w-full max-w-[290px] items-center justify-center rounded-[14px] bg-[#242424] text-[18px] font-semibold text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
href="/"
>
Return Home
</Link>
</div>
</section>
</main>
);
}

173
src/app/details/page.tsx

@ -0,0 +1,173 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import {
countCompletedDetailedAnswers,
detailedSections,
getDetailedQuestionCount,
getDetailedSectionProgress,
getDetailedSectionStorageKey,
} from "@/lib/detailed-questions";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
export default function DetailsOverviewPage() {
const [progressBySection, setProgressBySection] = useState<
Record<string, { completed: number; progress: number }>
>({});
const router = useRouter();
useEffect(() => {
const loadProgress = () => {
const nextProgress = Object.fromEntries(
detailedSections.map((section) => {
const rawAnswers = window.localStorage.getItem(
getDetailedSectionStorageKey(section.id),
);
const parsedAnswers = rawAnswers
? (JSON.parse(rawAnswers) as Record<string, string>)
: {};
return [
section.id,
{
completed: countCompletedDetailedAnswers(section, parsedAnswers),
progress: getDetailedSectionProgress(section, parsedAnswers),
},
];
}),
);
setProgressBySection(nextProgress);
const completedTotal = Object.values(nextProgress).reduce(
(total, section) => total + section.completed,
0,
);
if (completedTotal === getDetailedQuestionCount()) {
router.replace("/details/complete");
}
};
loadProgress();
window.addEventListener("focus", loadProgress);
window.addEventListener("pageshow", loadProgress);
return () => {
window.removeEventListener("focus", loadProgress);
window.removeEventListener("pageshow", loadProgress);
};
}, [router]);
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFB8CE_0%,rgba(255,184,206,0.28)_12%,rgba(255,255,255,0)_32%),linear-gradient(180deg,#FFFDFD_0%,#F9F4F6_100%)]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-5 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between">
<Link
aria-label="Back to questions"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/questions"
>
<BackIcon />
</Link>
<h1
className="text-[22px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Detailed Questions
</h1>
<span className="w-11" />
</div>
<div className="mt-10 rounded-[28px] bg-white/88 px-6 py-7 shadow-[0_18px_45px_rgba(63,30,40,0.1)] backdrop-blur-[2px]">
<p className="text-[12px] font-semibold uppercase tracking-[0.22em] text-[#F05A93]">
Complete Your Profile
</p>
<p className="mt-4 text-[16px] leading-7 text-[#625960]">
Open each section below and answer the related questions. Every
box tracks its own completion progress.
</p>
</div>
<div className="mt-5 flex-1 space-y-4 overflow-y-auto pb-2">
{detailedSections.map((section) => {
const sectionProgress = progressBySection[section.id] ?? {
completed: 0,
progress: 0,
};
return (
<Link
className="block rounded-[24px] bg-white/92 px-5 py-5 shadow-[0_14px_36px_rgba(0,0,0,0.06)] transition-transform duration-200 hover:-translate-y-0.5"
href={`/details/${section.id}`}
key={section.id}
>
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-[19px] font-semibold text-[#302428]">
{section.title}
</h2>
<p className="mt-2 text-[13px] leading-6 text-[#6B6468]">
{section.description}
</p>
</div>
<span className="rounded-full bg-[#FFF1F7] px-3 py-1 text-[12px] font-semibold text-[#F05A93]">
{sectionProgress.progress}%
</span>
</div>
<div className="mt-4 h-[9px] overflow-hidden rounded-full bg-[#F7D9E4]">
<div
className="h-full rounded-full bg-linear-to-r from-[#F04C99] to-[#FF8575] transition-[width] duration-300"
style={{ width: `${sectionProgress.progress}%` }}
/>
</div>
<p className="mt-3 text-[12px] font-medium tracking-[0.04em] text-[#8B8086]">
{sectionProgress.completed} of {section.questions.length}{" "}
answered
</p>
</Link>
);
})}
</div>
</div>
</section>
</main>
);
}

23
src/app/globals.css

@ -1,26 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body { body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
} }

180
src/app/intro/page.tsx

@ -0,0 +1,180 @@
"use client";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useState } from "react";
const slides = [
{
image: "/assets/images/Intro-Quran.png",
text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
title: "Habib Marriage",
},
{
image: "/assets/images/Intro-location.png",
text: "Stay informed about lunar cycles and global events with our curated calendar.",
title: "Lunar and International Events",
},
{
image: "/assets/images/Intro-location.png",
text: "Explore daily spiritual practices inspired by Mafatih al-Jinan for gentle, daily guidance.",
title: "Daily Practices from Mafatih",
},
] as const;
const progressWidth = 128;
function ProgressBar({
currentIndex,
totalSlides,
}: {
currentIndex: number;
totalSlides: number;
}) {
const segments = Array.from({ length: totalSlides }, (_, index) => index + 1);
return (
<div
aria-label={`Slide ${currentIndex + 1} of ${totalSlides}`}
aria-valuemax={totalSlides}
aria-valuemin={1}
aria-valuenow={currentIndex + 1}
className={`h-[8px] overflow-hidden rounded-full ${currentIndex === 0 ? "bg-[#D9659A]/65" : "bg-[#F6F6F6]"}`}
role="progressbar"
style={{ width: `${progressWidth}px` }}
>
<div className="flex h-full w-full">
{segments.map((segment) => {
const isActive = segment - 1 === currentIndex;
const isCompleted = segment - 1 <= currentIndex;
const isFirstSegment = segment === 1;
const completedSegmentColor =
currentIndex === 0 ? "bg-white" : "bg-[#F76C93]";
return (
<div
className={`flex h-full flex-1 items-center justify-end px-[2px] transition-colors duration-500 ${
isCompleted ? completedSegmentColor : "bg-transparent"
}
${isActive ? "rounded-r-full" : ""}
${isFirstSegment ? "rounded-l-full" : ""}
`}
key={segment}
>
<span
aria-hidden="true"
className={`h-[4px] w-[4px] rounded-full transition-colors duration-500 ${
isActive ? "bg-[#F05A93]" : "bg-white"
}`}
/>
</div>
);
})}
</div>
</div>
);
}
export default function IntroPage() {
const [currentSlide, setCurrentSlide] = useState(0);
const router = useRouter();
const lastSlideIndex = slides.length - 1;
const handleBack = () => {
setCurrentSlide((previousSlide) => Math.max(previousSlide - 1, 0));
};
const handleNext = () => {
if (currentSlide === lastSlideIndex) {
router.push("/video");
return;
}
setCurrentSlide((previousSlide) =>
Math.min(previousSlide + 1, lastSlideIndex),
);
};
const handleSkip = () => {
setCurrentSlide(lastSlideIndex);
};
return (
<main className="flex min-h-screen items-center justify-center bg-[#4A4A4A] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[22px] bg-white shadow-[0_30px_70px_rgba(0,0,0,0.35)]">
<div
className="flex h-full transition-transform duration-500 ease-out"
style={{ transform: `translateX(-${currentSlide * 100}%)` }}
>
{slides.map((slide) => (
<article
className="relative h-full w-full shrink-0"
key={slide.title}
>
<Image
alt=""
className="object-cover"
fill
preload
quality={100}
sizes="(max-width: 375px) 100vw, 375px"
src={slide.image}
/>
<div className="absolute inset-x-[9%] top-[64.9%] text-center text-[#384255]">
<h1 className="text-[24px] font-semibold leading-[1.15]">
{slide.title}
</h1>
<p className="mx-auto mt-4 max-w-[280px] text-[14px] leading-[1.45] text-[#6E7483]">
{slide.text}
</p>
</div>
</article>
))}
</div>
<div className="absolute inset-0 z-10">
<div className="absolute left-[5.3%] right-[5.3%] top-[7.1%] flex items-center justify-between text-white">
<button
aria-label="Go to previous slide"
className="flex h-10 w-10 items-center justify-center rounded-[16px] bg-black/5 text-white backdrop-blur-[2px] transition-opacity disabled:opacity-40"
disabled={currentSlide === 0}
onClick={handleBack}
type="button"
>
<Image
alt=""
height={18}
src="/assets/images/Group 1.svg"
width={14}
/>
</button>
<ProgressBar
currentIndex={currentSlide}
totalSlides={slides.length}
/>
<button
className="text-sm font-semibold tracking-[0.01em] text-white"
onClick={handleSkip}
type="button"
>
Skip
</button>
</div>
<div className="absolute inset-x-[5.3%] bottom-[2%]">
<button
className="h-11 w-full rounded-[13px] bg-linear-to-r from-[#ED4D9B] to-[#FF7A76] text-[18px] font-semibold text-white shadow-[0_14px_35px_rgba(237,77,155,0.28)]"
onClick={handleNext}
type="button"
>
{currentSlide === lastSlideIndex ? "Watch video" : "Next"}
</button>
</div>
</div>
</section>
</main>
);
}

30
src/app/layout.tsx

@ -1,21 +1,5 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
import { DevClickToComponent } from "@/components/dev/dev-click-to-component";
export default function RootLayout({ export default function RootLayout({
children, children,
@ -23,11 +7,13 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<html lang="en">
<body className="">
{children}
{process.env.NODE_ENV === "development" ? (
<DevClickToComponent />
) : null}
</body>
</html> </html>
); );
} }

120
src/app/page.tsx

@ -1,65 +1,69 @@
import Image from "next/image";
import Link from "next/link";
const previews = [
{
description: "Existing onboarding screen",
href: "/intro",
title: "Intro Page",
},
{
description: "New video details screen",
href: "/video",
title: "Video Page",
},
{
description: "Terms and conditions countdown screen",
href: "/rules",
title: "Rules Page",
},
{
description: "Second video screen after rules",
href: "/video-2",
title: "Video Page 2",
},
{
description: "Three-step onboarding questions slider",
href: "/questions",
title: "Questions Page",
},
{
description: "Detailed question sections with progress tracking",
href: "/details",
title: "Detailed Questions",
},
];
export default function Home() { export default function Home() {
return ( return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
<main className="flex min-h-screen items-center justify-center bg-[#3F3F43] p-6">
<section className="w-full max-w-md rounded-[28px] bg-white p-8 shadow-[0_30px_70px_rgba(0,0,0,0.28)]">
<p className="text-sm font-medium uppercase tracking-[0.28em] text-[#F05A93]">
Habib Marriage
</p>
<h1 className="mt-3 text-3xl font-semibold text-[#252525]">
Screen previews
</h1>
<p className="mt-3 text-sm leading-6 text-[#6A6A74]">
Open either mobile layout from here.
</p>
<div className="mt-8 space-y-4">
{previews.map((preview) => (
<Link
className="block rounded-[20px] border border-[#F1D2DE] bg-[linear-gradient(135deg,#FFF7FB_0%,#FFFFFF_100%)] px-5 py-4 transition-transform duration-200 hover:-translate-y-0.5"
href={preview.href}
key={preview.href}
> >
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
<p className="text-lg font-semibold text-[#252525]">
{preview.title}
</p>
<p className="mt-1 text-sm text-[#6A6A74]">
{preview.description}
</p>
</Link>
))}
</div> </div>
</main>
</div>
</section>
</main>
); );
} }

261
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 (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
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 (
<div
aria-label={`Question ${currentIndex + 1} of ${totalSlides}`}
aria-valuemax={totalSlides}
aria-valuemin={1}
aria-valuenow={currentIndex + 1}
className="h-[8px] overflow-hidden rounded-full bg-[#F8D7E4]"
role="progressbar"
style={{ width: `${progressWidth}px` }}
>
<div className="flex h-full w-full">
{segments.map((segment) => {
const isActive = segment <= currentIndex;
return (
<div className="flex flex-1 px-[2px]" key={segment}>
<span
aria-hidden="true"
className={`h-full w-full rounded-full transition-colors duration-300 ${
isActive ? "bg-[#F05A93]" : "bg-transparent"
}`}
/>
</div>
);
})}
</div>
</div>
);
}
export default function QuestionsPage() {
const [currentSlide, setCurrentSlide] = useState(0);
const [answers, setAnswers] = useState<Record<number, string>>({});
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 (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFB2C8_0%,rgba(255,178,200,0.3)_14%,rgba(255,255,255,0)_36%),linear-gradient(180deg,#FFFDFD_0%,#FBF5F7_100%)]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-5 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between">
<button
aria-label="Go to previous question"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)] disabled:opacity-40"
disabled={currentSlide === 0}
onClick={handleBack}
type="button"
>
<BackIcon />
</button>
<ProgressBar
currentIndex={currentSlide}
totalSlides={slides.length}
/>
<Link
className="text-sm font-semibold text-[#6F6770]"
href="/video-2"
>
Exit
</Link>
</div>
<div className="mt-12 rounded-[28px] bg-white/88 px-6 py-7 shadow-[0_18px_45px_rgba(63,30,40,0.1)] backdrop-blur-[2px]">
<p className="text-[12px] font-semibold uppercase tracking-[0.2em] text-[#F05A93]">
{activeSlide.helper}
</p>
<h1
className="mt-3 text-[28px] font-semibold leading-[1.2] text-[#2D2226]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
{activeSlide.title}
</h1>
<p className="mt-4 text-[16px] leading-7 text-[#625960]">
{activeSlide.question}
</p>
<p className="mt-3 text-[13px] leading-6 text-[#8A7E85]">
{activeSlide.helperText}
</p>
</div>
<div className="mt-6">
{activeSlide.inputType === "number" ? (
<input
className="h-[58px] w-full rounded-[20px] border border-white/70 bg-white/90 px-5 text-[18px] text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
inputMode="numeric"
min="18"
onChange={(event) => handleChange(event.target.value)}
placeholder={activeSlide.placeholder}
type="number"
value={currentAnswer}
/>
) : null}
{activeSlide.inputType === "textarea" ? (
<textarea
className="min-h-[170px] w-full resize-none rounded-[20px] border border-white/70 bg-white/90 px-5 py-4 text-[16px] leading-7 text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
onChange={(event) => handleChange(event.target.value)}
placeholder={activeSlide.placeholder}
value={currentAnswer}
/>
) : null}
{activeSlide.inputType === "date" ? (
<input
className="h-[58px] w-full rounded-[20px] border border-white/70 bg-white/90 px-5 text-[18px] text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
max="2010-12-31"
onChange={(event) => handleChange(event.target.value)}
type="date"
value={currentAnswer}
/>
) : null}
</div>
<div className="mt-auto space-y-3">
<div className="rounded-[18px] bg-white/80 px-4 py-3 text-[13px] leading-6 text-[#6C6469] shadow-[0_10px_24px_rgba(0,0,0,0.04)]">
Current answer: {currentAnswer || "Complete the field above"}
</div>
{currentSlide === lastSlideIndex ? (
<button
className={`h-[48px] w-full rounded-[14px] text-[18px] font-semibold transition-opacity ${
hasAnswer
? "bg-linear-to-r from-[#F04C99] to-[#FF8575] text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
: "bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
disabled={!hasAnswer}
onClick={handleFinish}
type="button"
>
Finish
</button>
) : (
<button
className={`h-[48px] w-full rounded-[14px] text-[18px] font-semibold transition-opacity ${
hasAnswer
? "bg-linear-to-r from-[#F04C99] to-[#FF8575] text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
: "bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
disabled={!hasAnswer}
onClick={handleNext}
type="button"
>
Next
</button>
)}
</div>
</div>
</section>
</main>
);
}

125
src/app/rules/page.tsx

@ -0,0 +1,125 @@
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
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 (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="relative flex h-full flex-col bg-[linear-gradient(180deg,#FFFFFF_0%,#FBF7F8_100%)]">
<header className="rounded-b-[16px] bg-[#F26793] px-4 pb-4 pt-5 text-white shadow-[0_10px_24px_rgba(242,103,147,0.22)]">
<div className="relative flex items-center justify-center">
<Link
aria-label="Back to video page"
className="absolute left-0 flex h-9 w-9 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-[2px]"
href="/video"
>
<BackIcon />
</Link>
<h1 className="max-w-[230px] text-center text-[15px] font-semibold leading-6">
What&apos;s Habib Marriage (terms &amp; conditions)
</h1>
</div>
</header>
<div className="flex-1 overflow-y-auto px-4 pb-28 pt-8">
<div className="rounded-[14px] border border-[#46C9E7] bg-[#EFFFFA] px-4 py-3 text-center text-[10px] leading-5 text-[#1D9AB8]">
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.
</div>
<article className="mt-3 rounded-[20px] bg-white px-5 py-4 text-[#333333] shadow-[0_14px_34px_rgba(0,0,0,0.07)]">
<div className="flex items-start gap-2">
<span className="mt-0.5 text-[18px] leading-none text-[#9B9B9B]">
</span>
<h2 className="text-[18px] font-semibold leading-[1.35] text-[#2D2D2D]">
A Secure and Purposeful Path to Permanent Union
</h2>
</div>
<div className="mt-4 space-y-3 text-[13px] leading-6 text-[#494949]">
<p>
Habib Marriage is not a typical matchmaking network. We have
come together with the goal of creating a secure and
confidential path for &quot;permanent marriage&quot; among
Muslims.
</p>
<p>
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.
</p>
<p>
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.
</p>
</div>
</article>
</div>
<div className="absolute inset-x-0 bottom-0 bg-[linear-gradient(180deg,rgba(251,247,248,0)_0%,#FBF7F8_24%)] px-5 pb-6 pt-5">
<Link
aria-disabled={!isEnabled}
className={`h-[48px] w-full rounded-[12px] text-[18px] font-semibold transition-colors ${
isEnabled
? "flex items-center justify-center bg-[#242424] text-white"
: "pointer-events-none flex items-center justify-center bg-[#C9C9CC] text-[#3C3C3F]"
}`}
href={isEnabled ? "/video-2" : "/rules"}
tabIndex={isEnabled ? 0 : -1}
>
{isEnabled ? "Next" : `Next (${secondsLeft}s)`}
</Link>
</div>
</div>
</section>
</main>
);
}

237
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 (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
function HistoryIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 8.25a5.25 5.25 0 1 1 1.539 3.711"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
<path
d="M3.75 4.5v3.75H7.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
</svg>
);
}
function PlayIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="28"
viewBox="0 0 28 28"
width="28"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 8.75 20 14l-10 5.25V8.75Z" fill="white" />
</svg>
);
}
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 (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FF9BB8_0%,rgba(255,155,184,0.32)_14%,rgba(255,255,255,0)_34%),linear-gradient(180deg,rgba(255,255,255,0.72)_0%,rgba(255,255,255,0.98)_34%,#FCF8F7_100%)]" />
<div className="absolute inset-0 opacity-40 [background-image:linear-gradient(45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(-45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,rgba(226,201,206,0.22)_75%),linear-gradient(-45deg,transparent_75%,rgba(226,201,206,0.22)_75%)] [background-position:0_0,0_8px,8px_-8px,-8px_0] [background-size:16px_16px]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-4 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between px-1">
<Link
aria-label="Back to rules"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/rules"
>
<BackIcon />
</Link>
<h1
className="text-[20px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Habib Marriage
</h1>
<button
aria-label="Open watch history"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
type="button"
>
<HistoryIcon />
</button>
</div>
<div className="mt-[110px] px-1">
<div className="relative overflow-hidden rounded-[20px] shadow-[0_18px_40px_rgba(0,0,0,0.22)]">
<Image
alt="Video preview for Habib Marriage"
height={420}
priority
sizes="(max-width: 375px) 100vw, 320px"
src="/assets/images/Rectangle 3077.png"
width={640}
/>
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(6,8,14,0.08)_0%,rgba(6,8,14,0.44)_100%)]" />
<div className="absolute inset-x-0 top-0 h-16 bg-[linear-gradient(180deg,rgba(6,8,14,0.38)_0%,rgba(6,8,14,0)_100%)] px-4 py-3 text-white">
<div className="flex items-center justify-between text-[11px] font-medium uppercase tracking-[0.16em]">
<span>Mock video</span>
<span>{watchedSeconds}s / 12s</span>
</div>
</div>
<div className="absolute inset-x-4 bottom-4">
<div className="h-1.5 overflow-hidden rounded-full bg-white/30">
<div
className="h-full rounded-full bg-[#F04C99] transition-[width] duration-700"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
<button
aria-label={
isCompleted
? "Replay mock video"
: isPlaying
? "Pause mock video"
: "Play mock video"
}
className="absolute left-1/2 top-1/2 flex h-20 w-20 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-[#BF2F2F] shadow-[0_18px_35px_rgba(191,47,47,0.45)]"
onClick={handlePlay}
type="button"
>
<PlayIcon />
</button>
</div>
</div>
<p className="mt-4 px-2 text-center text-[12px] font-medium text-[#6D6268]">
Watch the full mock video to unlock the next step.
</p>
<div className="mt-[102px] grid grid-cols-2 gap-4 px-4 text-center text-[#242424]">
{metrics.map((metric) => (
<div key={metric.label}>
<p className="text-[20px] font-semibold">{metric.value}</p>
<p className="mt-2 text-[13px] font-semibold leading-5">
{metric.label}
</p>
</div>
))}
</div>
<div className="mt-auto space-y-3">
<button
className="h-[44px] w-full rounded-[14px] bg-linear-to-r from-[#F04C99] to-[#FF8575] text-[18px] font-semibold text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
type="button"
>
Information recording
</button>
<Link
aria-disabled={!isCompleted}
className={`flex h-[44px] items-center justify-center rounded-[14px] text-[18px] font-semibold transition-opacity ${
isCompleted
? "bg-[#242424] text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
: "pointer-events-none bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
href={isCompleted ? "/questions" : "/video-2"}
tabIndex={isCompleted ? 0 : -1}
>
Next
</Link>
</div>
</div>
</section>
</main>
);
}

237
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 (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
function HistoryIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 8.25a5.25 5.25 0 1 1 1.539 3.711"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
<path
d="M3.75 4.5v3.75H7.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
</svg>
);
}
function PlayIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="28"
viewBox="0 0 28 28"
width="28"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 8.75 20 14l-10 5.25V8.75Z" fill="white" />
</svg>
);
}
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 (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FF9BB8_0%,rgba(255,155,184,0.32)_14%,rgba(255,255,255,0)_34%),linear-gradient(180deg,rgba(255,255,255,0.72)_0%,rgba(255,255,255,0.98)_34%,#FCF8F7_100%)]" />
<div className="absolute inset-0 opacity-40 [background-image:linear-gradient(45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(-45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,rgba(226,201,206,0.22)_75%),linear-gradient(-45deg,transparent_75%,rgba(226,201,206,0.22)_75%)] [background-position:0_0,0_8px,8px_-8px,-8px_0] [background-size:16px_16px]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-4 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between px-1">
<Link
aria-label="Back to home"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/"
>
<BackIcon />
</Link>
<h1
className="text-[20px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Habib Marriage
</h1>
<button
aria-label="Open watch history"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
type="button"
>
<HistoryIcon />
</button>
</div>
<div className="mt-[110px] px-1">
<div className="relative overflow-hidden rounded-[20px] shadow-[0_18px_40px_rgba(0,0,0,0.22)]">
<Image
alt="Video preview for Habib Marriage"
height={420}
priority
sizes="(max-width: 375px) 100vw, 320px"
src="/assets/images/Rectangle 3077.png"
width={640}
/>
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(6,8,14,0.08)_0%,rgba(6,8,14,0.44)_100%)]" />
<div className="absolute inset-x-0 top-0 h-16 bg-[linear-gradient(180deg,rgba(6,8,14,0.38)_0%,rgba(6,8,14,0)_100%)] px-4 py-3 text-white">
<div className="flex items-center justify-between text-[11px] font-medium uppercase tracking-[0.16em]">
<span>Mock video</span>
<span>{watchedSeconds}s / 12s</span>
</div>
</div>
<div className="absolute inset-x-4 bottom-4">
<div className="h-1.5 overflow-hidden rounded-full bg-white/30">
<div
className="h-full rounded-full bg-[#F04C99] transition-[width] duration-700"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
<button
aria-label={
isCompleted
? "Replay mock video"
: isPlaying
? "Pause mock video"
: "Play mock video"
}
className="absolute left-1/2 top-1/2 flex h-20 w-20 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-[#BF2F2F] shadow-[0_18px_35px_rgba(191,47,47,0.45)]"
onClick={handlePlay}
type="button"
>
<PlayIcon />
</button>
</div>
</div>
<p className="mt-4 px-2 text-center text-[12px] font-medium text-[#6D6268]">
Watch the full mock video to unlock the next step.
</p>
<div className="mt-[102px] grid grid-cols-2 gap-4 px-4 text-center text-[#242424]">
{metrics.map((metric) => (
<div key={metric.label}>
<p className="text-[20px] font-semibold">{metric.value}</p>
<p className="mt-2 text-[13px] font-semibold leading-5">
{metric.label}
</p>
</div>
))}
</div>
<div className="mt-auto space-y-3">
<button
className="h-[44px] w-full rounded-[14px] bg-linear-to-r from-[#F04C99] to-[#FF8575] text-[18px] font-semibold text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
type="button"
>
Information recording
</button>
<Link
aria-disabled={!isCompleted}
className={`flex h-[44px] items-center justify-center rounded-[14px] text-[18px] font-semibold transition-opacity ${
isCompleted
? "bg-[#242424] text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
: "pointer-events-none bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
href={isCompleted ? "/rules" : "/video"}
tabIndex={isCompleted ? 0 : -1}
>
Next
</Link>
</div>
</div>
</section>
</main>
);
}

68
src/components/dev/dev-click-to-component.tsx

@ -0,0 +1,68 @@
"use client";
import { useEffect } from "react";
const IDE_SCHEMES: Record<string, (locator: string) => 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;
}

169
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<string, string>,
) {
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<string, string>,
) {
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,
);
}

40
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),
),
);
},
},
};
};
Loading…
Cancel
Save