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"; |
@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; |
||||
} |
} |
||||
@ -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() { |
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. |
|
||||
|
<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> |
</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> |
</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> |
</div> |
||||
|
</section> |
||||
</main> |
</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