Browse Source
feat: add questions, rules, and video pages with navigation and progress tracking
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
21 changed files with 1874 additions and 103 deletions
-
8.babelrc
-
5public/assets/images/Group 1.svg
-
BINpublic/assets/images/Intro-Quran.png
-
BINpublic/assets/images/Intro-location.png
-
BINpublic/assets/images/Rectangle 3077.png
-
244src/app/details/[section]/detail-section-client.tsx
-
18src/app/details/[section]/page.tsx
-
37src/app/details/complete/page.tsx
-
173src/app/details/page.tsx
-
23src/app/globals.css
-
180src/app/intro/page.tsx
-
30src/app/layout.tsx
-
114src/app/page.tsx
-
261src/app/questions/page.tsx
-
125src/app/rules/page.tsx
-
237src/app/video-2/page.tsx
-
237src/app/video/page.tsx
-
68src/components/dev/dev-click-to-component.tsx
-
169src/lib/detailed-questions.ts
-
40src/plugins/add-data-locator.js
@ -0,0 +1,8 @@ |
|||
{ |
|||
"presets": ["next/babel"], |
|||
"env": { |
|||
"development": { |
|||
"plugins": ["./src/plugins/add-data-locator.js"] |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
|
After Width: 375 | Height: 813 | Size: 185 KiB |
|
After Width: 375 | Height: 813 | Size: 154 KiB |
|
After Width: 344 | Height: 235 | Size: 127 KiB |
@ -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> |
|||
); |
|||
} |
|||
@ -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} />; |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -1,26 +1,5 @@ |
|||
@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 { |
|||
background: var(--background); |
|||
color: var(--foreground); |
|||
font-family: Arial, Helvetica, sans-serif; |
|||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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() { |
|||
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. |
|||
<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="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" |
|||
> |
|||
Learning |
|||
</a>{" "} |
|||
center. |
|||
<p className="mt-3 text-sm leading-6 text-[#6A6A74]"> |
|||
Open either mobile layout from here. |
|||
</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" |
|||
|
|||
<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} |
|||
> |
|||
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> |
|||
</section> |
|||
</main> |
|||
</div> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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's Habib Marriage (terms & 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 "permanent marriage" 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> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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, |
|||
); |
|||
} |
|||
@ -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), |
|||
), |
|||
); |
|||
}, |
|||
}, |
|||
}; |
|||
}; |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue