23 changed files with 98 additions and 2184 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
-
197src/app/details/[section]/detail-section-client.tsx
-
18src/app/details/[section]/page.tsx
-
46src/app/details/complete/page.tsx
-
185src/app/details/page.tsx
-
366src/app/globals.css
-
186src/app/intro/page.tsx
-
30src/app/layout.tsx
-
158src/app/page.tsx
-
209src/app/questions/page.tsx
-
163src/app/rules/page.tsx
-
11src/app/video-2/page.tsx
-
5src/app/video/page.tsx
-
68src/components/dev/dev-click-to-component.tsx
-
202src/components/screens/video-step-screen.tsx
-
214src/components/ui/fabric-mobile.tsx
-
169src/lib/detailed-questions.ts
-
40src/plugins/add-data-locator.js
@ -1,8 +0,0 @@ |
|||||
{ |
|
||||
"presets": ["next/babel"], |
|
||||
"env": { |
|
||||
"development": { |
|
||||
"plugins": ["./src/plugins/add-data-locator.js"] |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,5 +0,0 @@ |
|||||
<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> |
|
||||
|
Before Width: 375 | Height: 813 | Size: 185 KiB |
|
Before Width: 375 | Height: 813 | Size: 154 KiB |
|
Before Width: 344 | Height: 235 | Size: 127 KiB |
@ -1,197 +0,0 @@ |
|||||
"use client"; |
|
||||
|
|
||||
import Link from "next/link"; |
|
||||
import { useEffect, useMemo, useState } from "react"; |
|
||||
import { |
|
||||
BackIcon, |
|
||||
FabricCard, |
|
||||
FabricIconLink, |
|
||||
FabricPill, |
|
||||
FabricProgress, |
|
||||
FabricScreen, |
|
||||
FabricStatusBar, |
|
||||
fabricInputClass, |
|
||||
fabricMutedPanelClass, |
|
||||
fabricSecondaryButtonClass, |
|
||||
fabricTextareaClass, |
|
||||
} from "@/components/ui/fabric-mobile"; |
|
||||
import { |
|
||||
countCompletedDetailedAnswers, |
|
||||
type DetailedSection, |
|
||||
getDetailedSectionProgress, |
|
||||
getDetailedSectionStorageKey, |
|
||||
} from "@/lib/detailed-questions"; |
|
||||
|
|
||||
export default function DetailSectionClient({ |
|
||||
section, |
|
||||
}: { |
|
||||
section: DetailedSection; |
|
||||
}) { |
|
||||
const [answers, setAnswers] = useState<Record<string, string>>({}); |
|
||||
const [isHydrated, setIsHydrated] = useState(false); |
|
||||
|
|
||||
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), |
|
||||
); |
|
||||
}, [answers, isHydrated, 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 ( |
|
||||
<FabricScreen> |
|
||||
<FabricStatusBar /> |
|
||||
|
|
||||
<div className="mt-5 flex items-center justify-between"> |
|
||||
<FabricIconLink aria-label="Back to detailed questions" href="/details"> |
|
||||
<BackIcon /> |
|
||||
</FabricIconLink> |
|
||||
|
|
||||
<div className="text-center"> |
|
||||
<p className="fabric-kicker">Section details</p> |
|
||||
<h1 className="fabric-display mt-2 text-[26px] leading-none text-[#2E211E]"> |
|
||||
{section.title} |
|
||||
</h1> |
|
||||
</div> |
|
||||
|
|
||||
<FabricPill className="min-w-[72px] justify-center px-3 py-2 text-[11px]"> |
|
||||
{progress}% |
|
||||
</FabricPill> |
|
||||
</div> |
|
||||
|
|
||||
<FabricCard className="mt-6 px-5 py-5"> |
|
||||
<p className="fabric-kicker">Section progress</p> |
|
||||
<h2 className="fabric-display mt-3 text-[30px] leading-[1.02] text-[#2E211E]"> |
|
||||
{section.title} |
|
||||
</h2> |
|
||||
<p className="mt-4 text-[14px] leading-7 text-[#6E5E58]"> |
|
||||
{section.description} |
|
||||
</p> |
|
||||
|
|
||||
<FabricProgress className="mt-5 h-2.5" value={progress} /> |
|
||||
|
|
||||
<p className="mt-3 text-[12px] font-medium tracking-[0.08em] text-[#8A746D]"> |
|
||||
{completedCount} of {section.questions.length} questions answered |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<div className={`${fabricMutedPanelClass} mt-4 px-4 py-4`}> |
|
||||
<p className="text-[13px] leading-6 text-[#695853]"> |
|
||||
Answers are saved automatically while you type, so you can return to |
|
||||
the section list at any time. |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<div className="fabric-scroll mt-4 flex-1 space-y-4 overflow-y-auto pb-2"> |
|
||||
{section.questions.map((question, index) => { |
|
||||
const value = answers[question.id] ?? ""; |
|
||||
|
|
||||
return ( |
|
||||
<FabricCard className="px-5 py-5" key={question.id}> |
|
||||
<div className="flex items-start justify-between gap-4"> |
|
||||
<div> |
|
||||
<p className="fabric-kicker">Prompt {index + 1}</p> |
|
||||
<h3 className="mt-3 text-[18px] font-semibold leading-7 text-[#31252A]"> |
|
||||
{question.label} |
|
||||
</h3> |
|
||||
</div> |
|
||||
<span className="rounded-full bg-[#F7E4DC] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-[#97565C]"> |
|
||||
{value.trim().length > 0 ? "Done" : "Open"} |
|
||||
</span> |
|
||||
</div> |
|
||||
|
|
||||
<p className="mt-3 text-[13px] leading-6 text-[#72686E]"> |
|
||||
{question.description} |
|
||||
</p> |
|
||||
|
|
||||
<div className="mt-4"> |
|
||||
{question.type === "textarea" ? ( |
|
||||
<textarea |
|
||||
className={fabricTextareaClass} |
|
||||
onChange={(event) => |
|
||||
handleChange(question.id, event.target.value) |
|
||||
} |
|
||||
placeholder={question.placeholder} |
|
||||
value={value} |
|
||||
/> |
|
||||
) : null} |
|
||||
|
|
||||
{question.type === "text" ? ( |
|
||||
<input |
|
||||
className={fabricInputClass} |
|
||||
onChange={(event) => |
|
||||
handleChange(question.id, event.target.value) |
|
||||
} |
|
||||
placeholder={question.placeholder} |
|
||||
type="text" |
|
||||
value={value} |
|
||||
/> |
|
||||
) : null} |
|
||||
|
|
||||
{question.type === "number" ? ( |
|
||||
<input |
|
||||
className={fabricInputClass} |
|
||||
inputMode="numeric" |
|
||||
onChange={(event) => |
|
||||
handleChange(question.id, event.target.value) |
|
||||
} |
|
||||
placeholder={question.placeholder} |
|
||||
type="number" |
|
||||
value={value} |
|
||||
/> |
|
||||
) : null} |
|
||||
|
|
||||
{question.type === "date" ? ( |
|
||||
<input |
|
||||
className={fabricInputClass} |
|
||||
onChange={(event) => |
|
||||
handleChange(question.id, event.target.value) |
|
||||
} |
|
||||
type="date" |
|
||||
value={value} |
|
||||
/> |
|
||||
) : null} |
|
||||
</div> |
|
||||
</FabricCard> |
|
||||
); |
|
||||
})} |
|
||||
</div> |
|
||||
|
|
||||
<div className="mt-4"> |
|
||||
<Link className={fabricSecondaryButtonClass} href="/details"> |
|
||||
Back To List |
|
||||
</Link> |
|
||||
</div> |
|
||||
</FabricScreen> |
|
||||
); |
|
||||
} |
|
||||
@ -1,18 +0,0 @@ |
|||||
import { notFound } from "next/navigation"; |
|
||||
import { getDetailedSection } from "@/lib/detailed-questions"; |
|
||||
import DetailSectionClient from "./detail-section-client"; |
|
||||
|
|
||||
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} />; |
|
||||
} |
|
||||
@ -1,46 +0,0 @@ |
|||||
import Link from "next/link"; |
|
||||
import { |
|
||||
CheckIcon, |
|
||||
FabricCard, |
|
||||
FabricPill, |
|
||||
FabricScreen, |
|
||||
FabricStatusBar, |
|
||||
fabricSecondaryButtonClass, |
|
||||
} from "@/components/ui/fabric-mobile"; |
|
||||
|
|
||||
export default function DetailedQuestionsCompletePage() { |
|
||||
return ( |
|
||||
<FabricScreen contentClassName="justify-center"> |
|
||||
<FabricStatusBar /> |
|
||||
|
|
||||
<div className="mt-auto flex flex-col items-center text-center"> |
|
||||
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-[linear-gradient(135deg,#AF5568_0%,#D6765C_100%)] text-white shadow-[0_24px_44px_rgba(175,85,104,0.25)]"> |
|
||||
<CheckIcon /> |
|
||||
</div> |
|
||||
|
|
||||
<FabricPill className="mt-6">All sections complete</FabricPill> |
|
||||
|
|
||||
<h1 className="fabric-display mt-6 text-[34px] leading-[1.06] text-[#2E2327]"> |
|
||||
Your profile details are complete |
|
||||
</h1> |
|
||||
|
|
||||
<p className="mt-5 max-w-[290px] 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> |
|
||||
|
|
||||
<FabricCard className="mt-8 w-full px-5 py-5 text-left"> |
|
||||
<p className="fabric-kicker">What happens next</p> |
|
||||
<p className="mt-3 text-[14px] leading-7 text-[#665953]"> |
|
||||
Your answers stay attached to this intake flow, and the team can now |
|
||||
review the completed profile as one unified submission. |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<Link className={`${fabricSecondaryButtonClass} mt-8`} href="/"> |
|
||||
Return Home |
|
||||
</Link> |
|
||||
</div> |
|
||||
</FabricScreen> |
|
||||
); |
|
||||
} |
|
||||
@ -1,185 +0,0 @@ |
|||||
"use client"; |
|
||||
|
|
||||
import Link from "next/link"; |
|
||||
import { useRouter } from "next/navigation"; |
|
||||
import { useEffect, useMemo, useState } from "react"; |
|
||||
import { |
|
||||
BackIcon, |
|
||||
FabricCard, |
|
||||
FabricIconLink, |
|
||||
FabricPill, |
|
||||
FabricProgress, |
|
||||
FabricScreen, |
|
||||
FabricStatusBar, |
|
||||
fabricMutedPanelClass, |
|
||||
fabricSecondaryButtonClass, |
|
||||
} from "@/components/ui/fabric-mobile"; |
|
||||
import { |
|
||||
countCompletedDetailedAnswers, |
|
||||
detailedSections, |
|
||||
getDetailedQuestionCount, |
|
||||
getDetailedSectionProgress, |
|
||||
getDetailedSectionStorageKey, |
|
||||
} from "@/lib/detailed-questions"; |
|
||||
|
|
||||
export default function DetailsOverviewPage() { |
|
||||
const [progressBySection, setProgressBySection] = useState< |
|
||||
Record<string, { completed: number; progress: number }> |
|
||||
>({}); |
|
||||
const router = useRouter(); |
|
||||
const totalQuestionCount = getDetailedQuestionCount(); |
|
||||
|
|
||||
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); |
|
||||
}; |
|
||||
|
|
||||
loadProgress(); |
|
||||
window.addEventListener("focus", loadProgress); |
|
||||
window.addEventListener("pageshow", loadProgress); |
|
||||
|
|
||||
return () => { |
|
||||
window.removeEventListener("focus", loadProgress); |
|
||||
window.removeEventListener("pageshow", loadProgress); |
|
||||
}; |
|
||||
}, []); |
|
||||
|
|
||||
const completedTotal = useMemo( |
|
||||
() => |
|
||||
Object.values(progressBySection).reduce( |
|
||||
(total, section) => total + section.completed, |
|
||||
0, |
|
||||
), |
|
||||
[progressBySection], |
|
||||
); |
|
||||
const isComplete = completedTotal === totalQuestionCount; |
|
||||
const overallProgress = |
|
||||
totalQuestionCount === 0 |
|
||||
? 0 |
|
||||
: Math.round((completedTotal / totalQuestionCount) * 100); |
|
||||
|
|
||||
return ( |
|
||||
<FabricScreen> |
|
||||
<FabricStatusBar /> |
|
||||
|
|
||||
<div className="mt-5 flex items-center justify-between"> |
|
||||
<FabricIconLink aria-label="Back to questions" href="/questions"> |
|
||||
<BackIcon /> |
|
||||
</FabricIconLink> |
|
||||
|
|
||||
<div className="text-center"> |
|
||||
<p className="fabric-kicker">Step 5</p> |
|
||||
<h1 className="fabric-display mt-2 text-[28px] leading-none text-[#2E211E]"> |
|
||||
Detailed Profile |
|
||||
</h1> |
|
||||
</div> |
|
||||
|
|
||||
<FabricPill className="min-w-[72px] justify-center px-3 py-2 text-[11px]"> |
|
||||
{overallProgress}% |
|
||||
</FabricPill> |
|
||||
</div> |
|
||||
|
|
||||
<FabricCard className="mt-6 px-5 py-5"> |
|
||||
<div className="flex items-start justify-between gap-4"> |
|
||||
<div> |
|
||||
<p className="fabric-kicker">Complete your profile</p> |
|
||||
<h2 className="fabric-display mt-3 text-[30px] leading-[1.02] text-[#2E211E]"> |
|
||||
Finish each section below |
|
||||
</h2> |
|
||||
</div> |
|
||||
<FabricPill className="shrink-0">{completedTotal} done</FabricPill> |
|
||||
</div> |
|
||||
|
|
||||
<p className="mt-4 text-[14px] leading-7 text-[#6E5E58]"> |
|
||||
Open every section, answer the prompts, and return here when each one |
|
||||
shows full progress. |
|
||||
</p> |
|
||||
|
|
||||
<FabricProgress className="mt-5 h-2.5" value={overallProgress} /> |
|
||||
|
|
||||
<p className="mt-3 text-[12px] font-medium tracking-[0.08em] text-[#8A746D]"> |
|
||||
{completedTotal} of {totalQuestionCount} detailed questions answered |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<div className={`${fabricMutedPanelClass} mt-4 px-4 py-4`}> |
|
||||
<p className="text-[13px] leading-6 text-[#695853]"> |
|
||||
Your progress is stored by section, so you can move in and out of each |
|
||||
group without losing what you already entered. |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<div className="fabric-scroll mt-4 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-[28px] border border-white/70 bg-[linear-gradient(180deg,rgba(255,255,255,0.94)_0%,rgba(255,249,244,0.84)_100%)] px-5 py-5 shadow-[0_20px_45px_rgba(99,63,50,0.13)] 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> |
|
||||
<p className="fabric-kicker">Section</p> |
|
||||
<h2 className="mt-3 text-[20px] font-semibold text-[#30231F]"> |
|
||||
{section.title} |
|
||||
</h2> |
|
||||
<p className="mt-2 text-[13px] leading-6 text-[#6B5A54]"> |
|
||||
{section.description} |
|
||||
</p> |
|
||||
</div> |
|
||||
<FabricPill className="shrink-0"> |
|
||||
{sectionProgress.progress}% |
|
||||
</FabricPill> |
|
||||
</div> |
|
||||
|
|
||||
<FabricProgress |
|
||||
className="mt-4 h-2" |
|
||||
value={sectionProgress.progress} |
|
||||
/> |
|
||||
|
|
||||
<p className="mt-3 text-[12px] font-medium tracking-[0.08em] text-[#8A746D]"> |
|
||||
{sectionProgress.completed} of {section.questions.length}{" "} |
|
||||
answered |
|
||||
</p> |
|
||||
</Link> |
|
||||
); |
|
||||
})} |
|
||||
</div> |
|
||||
|
|
||||
<div className="mt-4"> |
|
||||
<button |
|
||||
className={fabricSecondaryButtonClass} |
|
||||
disabled={!isComplete} |
|
||||
onClick={() => router.push("/details/complete")} |
|
||||
type="button" |
|
||||
> |
|
||||
Next |
|
||||
</button> |
|
||||
</div> |
|
||||
</FabricScreen> |
|
||||
); |
|
||||
} |
|
||||
@ -1,360 +1,26 @@ |
|||||
@import "tailwindcss"; |
@import "tailwindcss"; |
||||
|
|
||||
:root { |
:root { |
||||
--fabric-display: |
|
||||
"Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif; |
|
||||
--fabric-body: "Aptos", "Trebuchet MS", "Segoe UI", sans-serif; |
|
||||
--fabric-ink: #30211d; |
|
||||
--fabric-muted: #6f5e58; |
|
||||
--fabric-rose: #af5568; |
|
||||
--fabric-rust: #d6765c; |
|
||||
--fabric-paper: #fff9f4; |
|
||||
--fabric-paper-soft: #fff4ed; |
|
||||
--fabric-shell: #f2dfd0; |
|
||||
--fabric-stroke: #e4d0c4; |
|
||||
|
--background: #ffffff; |
||||
|
--foreground: #171717; |
||||
} |
} |
||||
|
|
||||
body { |
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; |
|
||||
} |
|
||||
|
|
||||
.fabric-body { |
|
||||
color: var(--fabric-ink); |
|
||||
font-family: var(--fabric-body); |
|
||||
} |
|
||||
|
|
||||
.fabric-display { |
|
||||
font-family: var(--fabric-display); |
|
||||
} |
|
||||
|
|
||||
.fabric-stage { |
|
||||
align-items: center; |
|
||||
background: radial-gradient( |
|
||||
circle at top, |
|
||||
#6d5b58 0%, |
|
||||
#514544 42%, |
|
||||
#392f31 100% |
|
||||
); |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
min-height: 100vh; |
|
||||
padding: 0.75rem; |
|
||||
} |
|
||||
|
|
||||
.fabric-phone { |
|
||||
aspect-ratio: 375 / 813; |
|
||||
background: linear-gradient(180deg, #f8eee5 0%, #f0ddcf 100%); |
|
||||
border: 1px solid rgb(255 255 255 / 15%); |
|
||||
border-radius: 32px; |
|
||||
box-shadow: 0 32px 90px rgb(25 12 14 / 42%); |
|
||||
max-width: 390px; |
|
||||
overflow: hidden; |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.fabric-phone::before { |
|
||||
background: |
|
||||
radial-gradient( |
|
||||
circle at 15% 0%, |
|
||||
rgb(255 255 255 / 82%) 0%, |
|
||||
rgb(255 255 255 / 0%) 28% |
|
||||
), |
|
||||
radial-gradient( |
|
||||
circle at 100% 85%, |
|
||||
rgb(212 137 110 / 18%) 0%, |
|
||||
rgb(212 137 110 / 0%) 28% |
|
||||
), |
|
||||
linear-gradient( |
|
||||
180deg, |
|
||||
rgb(255 255 255 / 40%) 0%, |
|
||||
rgb(255 255 255 / 0%) 30%, |
|
||||
rgb(157 107 90 / 8%) 100% |
|
||||
); |
|
||||
content: ""; |
|
||||
inset: 0; |
|
||||
position: absolute; |
|
||||
} |
|
||||
|
|
||||
.fabric-phone::after { |
|
||||
background-image: |
|
||||
repeating-linear-gradient( |
|
||||
0deg, |
|
||||
rgb(157 117 99 / 18%) 0 1px, |
|
||||
transparent 1px 12px |
|
||||
), |
|
||||
repeating-linear-gradient( |
|
||||
90deg, |
|
||||
rgb(255 255 255 / 28%) 0 1px, |
|
||||
transparent 1px 16px |
|
||||
); |
|
||||
content: ""; |
|
||||
inset: 0; |
|
||||
mix-blend-mode: soft-light; |
|
||||
opacity: 0.24; |
|
||||
position: absolute; |
|
||||
} |
|
||||
|
|
||||
.fabric-screen { |
|
||||
background: linear-gradient( |
|
||||
180deg, |
|
||||
rgb(255 252 248 / 72%) 0%, |
|
||||
rgb(246 236 226 / 62%) 100% |
|
||||
); |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
height: 100%; |
|
||||
padding: 1.25rem; |
|
||||
position: relative; |
|
||||
z-index: 1; |
|
||||
} |
|
||||
|
|
||||
.fabric-status { |
|
||||
align-items: center; |
|
||||
color: var(--fabric-ink); |
|
||||
display: flex; |
|
||||
font-size: 0.875rem; |
|
||||
font-weight: 600; |
|
||||
justify-content: space-between; |
|
||||
letter-spacing: 0.01em; |
|
||||
padding: 0 0.4rem; |
|
||||
} |
|
||||
|
|
||||
.fabric-signal { |
|
||||
align-items: center; |
|
||||
display: flex; |
|
||||
gap: 0.35rem; |
|
||||
opacity: 0.85; |
|
||||
} |
|
||||
|
|
||||
.fabric-signal-bar { |
|
||||
border: 1px solid currentcolor; |
|
||||
border-radius: 0.3rem; |
|
||||
display: block; |
|
||||
height: 0.45rem; |
|
||||
width: 1.15rem; |
|
||||
} |
|
||||
|
|
||||
.fabric-signal-dot { |
|
||||
background: currentcolor; |
|
||||
border-radius: 999px; |
|
||||
display: block; |
|
||||
height: 0.45rem; |
|
||||
width: 0.45rem; |
|
||||
} |
|
||||
|
|
||||
.fabric-nav-button { |
|
||||
align-items: center; |
|
||||
backdrop-filter: blur(8px); |
|
||||
background: linear-gradient( |
|
||||
180deg, |
|
||||
rgb(255 255 255 / 92%) 0%, |
|
||||
rgb(255 247 243 / 78%) 100% |
|
||||
); |
|
||||
border: 1px solid rgb(255 255 255 / 72%); |
|
||||
border-radius: 1.15rem; |
|
||||
box-shadow: 0 14px 28px rgb(79 48 38 / 12%); |
|
||||
color: var(--fabric-ink); |
|
||||
display: flex; |
|
||||
height: 2.85rem; |
|
||||
justify-content: center; |
|
||||
transition: |
|
||||
box-shadow 180ms ease, |
|
||||
opacity 180ms ease, |
|
||||
transform 180ms ease; |
|
||||
width: 2.85rem; |
|
||||
} |
|
||||
|
|
||||
.fabric-nav-button:hover { |
|
||||
box-shadow: 0 18px 32px rgb(79 48 38 / 16%); |
|
||||
transform: translateY(-1px); |
|
||||
} |
|
||||
|
|
||||
.fabric-nav-button:disabled { |
|
||||
box-shadow: none; |
|
||||
opacity: 0.38; |
|
||||
transform: none; |
|
||||
} |
|
||||
|
|
||||
.fabric-card { |
|
||||
backdrop-filter: blur(8px); |
|
||||
background: linear-gradient( |
|
||||
180deg, |
|
||||
rgb(255 255 255 / 94%) 0%, |
|
||||
rgb(255 249 244 / 84%) 100% |
|
||||
); |
|
||||
border: 1px solid rgb(255 255 255 / 70%); |
|
||||
border-radius: 1.75rem; |
|
||||
box-shadow: 0 20px 45px rgb(99 63 50 / 13%); |
|
||||
} |
|
||||
|
|
||||
.fabric-muted-panel { |
|
||||
background: rgb(251 244 239 / 80%); |
|
||||
border: 1px solid rgb(228 208 196 / 80%); |
|
||||
border-radius: 1.35rem; |
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 55%); |
|
||||
|
@theme inline { |
||||
|
--color-background: var(--background); |
||||
|
--color-foreground: var(--foreground); |
||||
|
--font-sans: var(--font-geist-sans); |
||||
|
--font-mono: var(--font-geist-mono); |
||||
} |
} |
||||
|
|
||||
.fabric-kicker { |
|
||||
color: #a05d63; |
|
||||
font-size: 0.72rem; |
|
||||
font-weight: 700; |
|
||||
letter-spacing: 0.22em; |
|
||||
text-transform: uppercase; |
|
||||
|
@media (prefers-color-scheme: dark) { |
||||
|
:root { |
||||
|
--background: #0a0a0a; |
||||
|
--foreground: #ededed; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
.fabric-pill { |
|
||||
align-items: center; |
|
||||
background: #f6e4dd; |
|
||||
border-radius: 999px; |
|
||||
color: #99535b; |
|
||||
display: inline-flex; |
|
||||
font-size: 0.75rem; |
|
||||
font-weight: 700; |
|
||||
gap: 0.35rem; |
|
||||
letter-spacing: 0.08em; |
|
||||
padding: 0.45rem 0.85rem; |
|
||||
text-transform: uppercase; |
|
||||
} |
|
||||
|
|
||||
.fabric-progress-track { |
|
||||
background: #ead4ca; |
|
||||
border-radius: 999px; |
|
||||
height: 0.55rem; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.fabric-progress-fill { |
|
||||
background: linear-gradient( |
|
||||
135deg, |
|
||||
var(--fabric-rose) 0%, |
|
||||
var(--fabric-rust) 100% |
|
||||
); |
|
||||
border-radius: 999px; |
|
||||
box-shadow: 0 4px 12px rgb(175 85 104 / 32%); |
|
||||
height: 100%; |
|
||||
transition: width 260ms ease; |
|
||||
} |
|
||||
|
|
||||
.fabric-input { |
|
||||
background: linear-gradient(180deg, #fffcfa 0%, #fff7f1 100%); |
|
||||
border: 1px solid var(--fabric-stroke); |
|
||||
border-radius: 1.25rem; |
|
||||
box-shadow: |
|
||||
inset 0 1px 0 rgb(255 255 255 / 85%), |
|
||||
0 10px 24px rgb(121 84 70 / 6%); |
|
||||
color: var(--fabric-ink); |
|
||||
font-family: var(--fabric-body); |
|
||||
font-size: 1rem; |
|
||||
min-height: 3.6rem; |
|
||||
outline: none; |
|
||||
padding: 0 1rem; |
|
||||
transition: |
|
||||
border-color 180ms ease, |
|
||||
box-shadow 180ms ease, |
|
||||
transform 180ms ease; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.fabric-input::placeholder { |
|
||||
color: #9e8a83; |
|
||||
} |
|
||||
|
|
||||
.fabric-input:focus, |
|
||||
.fabric-textarea:focus { |
|
||||
border-color: var(--fabric-rose); |
|
||||
box-shadow: |
|
||||
inset 0 1px 0 rgb(255 255 255 / 85%), |
|
||||
0 0 0 4px rgb(245 213 205 / 60%), |
|
||||
0 12px 28px rgb(121 84 70 / 8%); |
|
||||
} |
|
||||
|
|
||||
.fabric-textarea { |
|
||||
line-height: 1.7; |
|
||||
min-height: 9.5rem; |
|
||||
padding: 1rem; |
|
||||
resize: none; |
|
||||
} |
|
||||
|
|
||||
.fabric-primary-button, |
|
||||
.fabric-secondary-button { |
|
||||
align-items: center; |
|
||||
border-radius: 1rem; |
|
||||
display: flex; |
|
||||
font-family: var(--fabric-body); |
|
||||
font-size: 1.05rem; |
|
||||
font-weight: 700; |
|
||||
justify-content: center; |
|
||||
min-height: 3.25rem; |
|
||||
transition: |
|
||||
box-shadow 180ms ease, |
|
||||
filter 180ms ease, |
|
||||
opacity 180ms ease, |
|
||||
transform 180ms ease; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.fabric-primary-button { |
|
||||
background: linear-gradient( |
|
||||
135deg, |
|
||||
var(--fabric-rose) 0%, |
|
||||
var(--fabric-rust) 100% |
|
||||
); |
|
||||
box-shadow: 0 18px 36px rgb(175 85 104 / 28%); |
|
||||
color: #fff; |
|
||||
} |
|
||||
|
|
||||
.fabric-primary-button:hover, |
|
||||
.fabric-secondary-button:hover { |
|
||||
transform: translateY(-1px); |
|
||||
} |
|
||||
|
|
||||
.fabric-primary-button:disabled, |
|
||||
.fabric-secondary-button:disabled { |
|
||||
box-shadow: none; |
|
||||
filter: saturate(0.7); |
|
||||
opacity: 0.52; |
|
||||
transform: none; |
|
||||
} |
|
||||
|
|
||||
.fabric-secondary-button { |
|
||||
background: #2f241f; |
|
||||
box-shadow: 0 14px 30px rgb(47 36 31 / 20%); |
|
||||
color: #fff; |
|
||||
} |
|
||||
|
|
||||
.fabric-link-disabled { |
|
||||
filter: saturate(0.7); |
|
||||
opacity: 0.48; |
|
||||
pointer-events: none; |
|
||||
} |
|
||||
|
|
||||
.fabric-scroll { |
|
||||
scrollbar-width: none; |
|
||||
} |
|
||||
|
|
||||
.fabric-scroll::-webkit-scrollbar { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
.fabric-accent-dot { |
|
||||
background: linear-gradient( |
|
||||
135deg, |
|
||||
var(--fabric-rose) 0%, |
|
||||
var(--fabric-rust) 100% |
|
||||
); |
|
||||
border-radius: 999px; |
|
||||
box-shadow: 0 0 0 6px rgb(245 213 205 / 50%); |
|
||||
height: 0.55rem; |
|
||||
width: 0.55rem; |
|
||||
} |
|
||||
|
|
||||
.fabric-divider { |
|
||||
background: linear-gradient( |
|
||||
90deg, |
|
||||
rgb(190 160 146 / 0%) 0%, |
|
||||
rgb(190 160 146 / 70%) 50%, |
|
||||
rgb(190 160 146 / 0%) 100% |
|
||||
); |
|
||||
height: 1px; |
|
||||
|
body { |
||||
|
background: var(--background); |
||||
|
color: var(--foreground); |
||||
|
font-family: Arial, Helvetica, sans-serif; |
||||
} |
} |
||||
@ -1,186 +0,0 @@ |
|||||
"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); |
|
||||
const completedSegmentColor = |
|
||||
currentIndex === 0 ? "bg-white" : "bg-[#F76C93]"; |
|
||||
|
|
||||
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 fillWidth = isCompleted ? "100%" : "0%"; |
|
||||
|
|
||||
return ( |
|
||||
<div |
|
||||
className={`relative flex h-full flex-1 items-center justify-end overflow-hidden px-[2px] ${ |
|
||||
isActive ? "rounded-r-full" : "" |
|
||||
} ${isFirstSegment ? "rounded-l-full" : ""}`}
|
|
||||
key={segment} |
|
||||
> |
|
||||
<div |
|
||||
aria-hidden="true" |
|
||||
className={`absolute inset-y-0 left-0 transition-[width] duration-500 ease-out ${completedSegmentColor} ${ |
|
||||
isActive ? "rounded-r-full" : "" |
|
||||
} ${isFirstSegment ? "rounded-l-full" : ""}`}
|
|
||||
style={{ width: fillWidth }} |
|
||||
/> |
|
||||
|
|
||||
<span |
|
||||
aria-hidden="true" |
|
||||
className={`relative z-10 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,105 +1,65 @@ |
|||||
import Link from "next/link"; |
|
||||
import { FabricPill } from "@/components/ui/fabric-mobile"; |
|
||||
|
|
||||
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", |
|
||||
}, |
|
||||
]; |
|
||||
|
import Image from "next/image"; |
||||
|
|
||||
export default function Home() { |
export default function Home() { |
||||
return ( |
return ( |
||||
<main className="fabric-stage fabric-body px-6 py-10"> |
|
||||
<section className="w-full max-w-4xl rounded-[36px] border border-white/12 bg-[linear-gradient(180deg,rgba(255,252,248,0.12)_0%,rgba(255,252,248,0.04)_100%)] p-4 shadow-[0_32px_80px_rgba(25,12,14,0.22)] backdrop-blur"> |
|
||||
<div className="grid gap-4 md:grid-cols-[1.05fr_0.95fr]"> |
|
||||
<div className="rounded-[30px] border border-white/65 bg-[linear-gradient(180deg,rgba(255,255,255,0.96)_0%,rgba(255,247,241,0.86)_100%)] px-7 py-8 shadow-[0_24px_48px_rgba(99,63,50,0.14)]"> |
|
||||
<FabricPill>Habib Marriage</FabricPill> |
|
||||
<h1 className="fabric-display mt-5 text-[42px] leading-[0.96] text-[#2E211E] sm:text-[54px]"> |
|
||||
Screen previews for the app flow |
|
||||
</h1> |
|
||||
<p className="mt-5 max-w-[30rem] text-[15px] leading-8 text-[#685751]"> |
|
||||
The non-intro screens now share one warmer fabric-inspired visual |
|
||||
system. Use this page to move through each route while checking |
|
||||
the updated flow. |
|
||||
</p> |
|
||||
|
|
||||
<div className="mt-8 grid gap-3 sm:grid-cols-2"> |
|
||||
<div className="rounded-[24px] bg-[#F7E4DC] px-4 py-4"> |
|
||||
<p className="text-[12px] font-semibold uppercase tracking-[0.16em] text-[#9A595E]"> |
|
||||
Surfaces |
|
||||
</p> |
|
||||
<p className="mt-2 text-[14px] leading-7 text-[#5E4D47]"> |
|
||||
Unified cards, inputs, and action buttons across the flow. |
|
||||
</p> |
|
||||
</div> |
|
||||
<div className="rounded-[24px] bg-[#F2ECE7] px-4 py-4"> |
|
||||
<p className="text-[12px] font-semibold uppercase tracking-[0.16em] text-[#7A655D]"> |
|
||||
Navigation |
|
||||
</p> |
|
||||
<p className="mt-2 text-[14px] leading-7 text-[#5E4D47]"> |
|
||||
Shared headers, progress states, and clearer completion cues. |
|
||||
</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div className="rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(78,58,58,0.72)_0%,rgba(47,35,37,0.86)_100%)] px-5 py-6 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]"> |
|
||||
<p className="text-[12px] font-semibold uppercase tracking-[0.24em] text-white/72"> |
|
||||
Routes |
|
||||
</p> |
|
||||
<div className="mt-5 space-y-3"> |
|
||||
{previews.map((preview) => ( |
|
||||
<Link |
|
||||
className="block rounded-[24px] border border-white/8 bg-white/6 px-5 py-4 transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white/10" |
|
||||
href={preview.href} |
|
||||
key={preview.href} |
|
||||
> |
|
||||
<div className="flex items-start justify-between gap-4"> |
|
||||
<div> |
|
||||
<p className="text-[18px] font-semibold text-white"> |
|
||||
{preview.title} |
|
||||
</p> |
|
||||
<p className="mt-2 text-[13px] leading-6 text-white/68"> |
|
||||
{preview.description} |
|
||||
</p> |
|
||||
</div> |
|
||||
<span className="rounded-full bg-white/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-white/80"> |
|
||||
Open |
|
||||
</span> |
|
||||
</div> |
|
||||
</Link> |
|
||||
))} |
|
||||
</div> |
|
||||
</div> |
|
||||
|
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> |
||||
|
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> |
||||
|
<Image |
||||
|
className="dark:invert" |
||||
|
src="/next.svg" |
||||
|
alt="Next.js logo" |
||||
|
width={100} |
||||
|
height={20} |
||||
|
priority |
||||
|
/> |
||||
|
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"> |
||||
|
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> |
||||
|
To get started, edit the page.tsx file. |
||||
|
</h1> |
||||
|
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> |
||||
|
Looking for a starting point or more instructions? Head over to{" "} |
||||
|
<a |
||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" |
||||
|
className="font-medium text-zinc-950 dark:text-zinc-50" |
||||
|
> |
||||
|
Templates |
||||
|
</a>{" "} |
||||
|
or the{" "} |
||||
|
<a |
||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" |
||||
|
className="font-medium text-zinc-950 dark:text-zinc-50" |
||||
|
> |
||||
|
Learning |
||||
|
</a>{" "} |
||||
|
center. |
||||
|
</p> |
||||
|
</div> |
||||
|
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row"> |
||||
|
<a |
||||
|
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]" |
||||
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" |
||||
|
target="_blank" |
||||
|
rel="noopener noreferrer" |
||||
|
> |
||||
|
<Image |
||||
|
className="dark:invert" |
||||
|
src="/vercel.svg" |
||||
|
alt="Vercel logomark" |
||||
|
width={16} |
||||
|
height={16} |
||||
|
/> |
||||
|
Deploy Now |
||||
|
</a> |
||||
|
<a |
||||
|
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" |
||||
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" |
||||
|
target="_blank" |
||||
|
rel="noopener noreferrer" |
||||
|
> |
||||
|
Documentation |
||||
|
</a> |
||||
</div> |
</div> |
||||
</section> |
|
||||
</main> |
|
||||
|
</main> |
||||
|
</div> |
||||
); |
); |
||||
} |
} |
||||
@ -1,209 +0,0 @@ |
|||||
"use client"; |
|
||||
|
|
||||
import Link from "next/link"; |
|
||||
import { useRouter } from "next/navigation"; |
|
||||
import { useState } from "react"; |
|
||||
import { |
|
||||
BackIcon, |
|
||||
FabricCard, |
|
||||
FabricIconButton, |
|
||||
FabricProgress, |
|
||||
FabricScreen, |
|
||||
FabricStatusBar, |
|
||||
fabricInputClass, |
|
||||
fabricMutedPanelClass, |
|
||||
fabricPrimaryButtonClass, |
|
||||
fabricTextareaClass, |
|
||||
} from "@/components/ui/fabric-mobile"; |
|
||||
|
|
||||
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; |
|
||||
|
|
||||
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 progressPercent = ((currentSlide + 1) / slides.length) * 100; |
|
||||
|
|
||||
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 ( |
|
||||
<FabricScreen> |
|
||||
<FabricStatusBar /> |
|
||||
|
|
||||
<div className="mt-5 flex items-center justify-between gap-3"> |
|
||||
<FabricIconButton |
|
||||
aria-label="Go to previous question" |
|
||||
disabled={currentSlide === 0} |
|
||||
onClick={handleBack} |
|
||||
> |
|
||||
<BackIcon /> |
|
||||
</FabricIconButton> |
|
||||
|
|
||||
<div className="min-w-0 flex-1"> |
|
||||
<p className="text-center text-[11px] font-semibold uppercase tracking-[0.2em] text-[#8F6C67]"> |
|
||||
Profile intake |
|
||||
</p> |
|
||||
<FabricProgress className="mt-2 h-2" value={progressPercent} /> |
|
||||
</div> |
|
||||
|
|
||||
<Link |
|
||||
className="rounded-full bg-[#F6E4DD] px-4 py-2 text-[12px] font-semibold uppercase tracking-[0.12em] text-[#95535A]" |
|
||||
href="/video-2" |
|
||||
> |
|
||||
Exit |
|
||||
</Link> |
|
||||
</div> |
|
||||
|
|
||||
<FabricCard className="mt-6 px-5 py-5"> |
|
||||
<div className="flex items-start justify-between gap-4"> |
|
||||
<div> |
|
||||
<p className="fabric-kicker">Step 4</p> |
|
||||
<h1 className="fabric-display mt-3 text-[30px] leading-[1.02] text-[#2E211E]"> |
|
||||
{activeSlide.title} |
|
||||
</h1> |
|
||||
</div> |
|
||||
<span className="rounded-full bg-[#F6E4DD] px-4 py-2 text-[12px] font-semibold uppercase tracking-[0.12em] text-[#95535A]"> |
|
||||
{currentSlide + 1} / {slides.length} |
|
||||
</span> |
|
||||
</div> |
|
||||
|
|
||||
<p className="mt-4 text-[16px] leading-7 text-[#5F4E49]"> |
|
||||
{activeSlide.question} |
|
||||
</p> |
|
||||
<p className="mt-3 text-[13px] leading-6 text-[#8B736C]"> |
|
||||
{activeSlide.helperText} |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<FabricCard className="mt-5 px-5 py-5"> |
|
||||
<div className="flex items-center justify-between gap-3"> |
|
||||
<p className="fabric-kicker">{activeSlide.helper}</p> |
|
||||
<span className="text-[12px] font-medium tracking-[0.08em] text-[#8A746D]"> |
|
||||
Required |
|
||||
</span> |
|
||||
</div> |
|
||||
|
|
||||
<div className="mt-4"> |
|
||||
{activeSlide.inputType === "number" ? ( |
|
||||
<input |
|
||||
className={fabricInputClass} |
|
||||
inputMode="numeric" |
|
||||
min="18" |
|
||||
onChange={(event) => handleChange(event.target.value)} |
|
||||
placeholder={activeSlide.placeholder} |
|
||||
type="number" |
|
||||
value={currentAnswer} |
|
||||
/> |
|
||||
) : null} |
|
||||
|
|
||||
{activeSlide.inputType === "textarea" ? ( |
|
||||
<textarea |
|
||||
className={fabricTextareaClass} |
|
||||
onChange={(event) => handleChange(event.target.value)} |
|
||||
placeholder={activeSlide.placeholder} |
|
||||
value={currentAnswer} |
|
||||
/> |
|
||||
) : null} |
|
||||
|
|
||||
{activeSlide.inputType === "date" ? ( |
|
||||
<input |
|
||||
className={fabricInputClass} |
|
||||
max="2010-12-31" |
|
||||
onChange={(event) => handleChange(event.target.value)} |
|
||||
type="date" |
|
||||
value={currentAnswer} |
|
||||
/> |
|
||||
) : null} |
|
||||
</div> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<div className={`${fabricMutedPanelClass} mt-4 px-4 py-4`}> |
|
||||
<p className="text-[12px] font-semibold uppercase tracking-[0.16em] text-[#9A5A60]"> |
|
||||
Current answer |
|
||||
</p> |
|
||||
<p className="mt-2 text-[14px] leading-7 text-[#64534D]"> |
|
||||
{currentAnswer || "Complete the field above to continue."} |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<div className="mt-auto pt-4"> |
|
||||
{currentSlide === lastSlideIndex ? ( |
|
||||
<button |
|
||||
className={fabricPrimaryButtonClass} |
|
||||
disabled={!hasAnswer} |
|
||||
onClick={handleFinish} |
|
||||
type="button" |
|
||||
> |
|
||||
Finish |
|
||||
</button> |
|
||||
) : ( |
|
||||
<button |
|
||||
className={fabricPrimaryButtonClass} |
|
||||
disabled={!hasAnswer} |
|
||||
onClick={handleNext} |
|
||||
type="button" |
|
||||
> |
|
||||
Next |
|
||||
</button> |
|
||||
)} |
|
||||
</div> |
|
||||
</FabricScreen> |
|
||||
); |
|
||||
} |
|
||||
@ -1,163 +0,0 @@ |
|||||
"use client"; |
|
||||
|
|
||||
import Link from "next/link"; |
|
||||
import { useEffect, useState } from "react"; |
|
||||
import { |
|
||||
BackIcon, |
|
||||
FabricCard, |
|
||||
FabricIconLink, |
|
||||
FabricPill, |
|
||||
FabricScreen, |
|
||||
FabricStatusBar, |
|
||||
fabricDisabledLinkClass, |
|
||||
fabricMutedPanelClass, |
|
||||
fabricSecondaryButtonClass, |
|
||||
} from "@/components/ui/fabric-mobile"; |
|
||||
|
|
||||
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 ( |
|
||||
<FabricScreen> |
|
||||
<FabricStatusBar /> |
|
||||
|
|
||||
<div className="mt-5 flex items-center justify-between"> |
|
||||
<FabricIconLink aria-label="Back to video page" href="/video"> |
|
||||
<BackIcon /> |
|
||||
</FabricIconLink> |
|
||||
|
|
||||
<div className="text-center"> |
|
||||
<p className="fabric-kicker">Step 2</p> |
|
||||
<h1 className="fabric-display mt-2 text-[28px] leading-none text-[#2E211E]"> |
|
||||
Terms & Values |
|
||||
</h1> |
|
||||
</div> |
|
||||
|
|
||||
<FabricPill className="min-w-[68px] justify-center px-3 py-2 text-[11px]"> |
|
||||
{secondsLeft}s |
|
||||
</FabricPill> |
|
||||
</div> |
|
||||
|
|
||||
<FabricCard className="mt-6 px-5 py-5"> |
|
||||
<div className="flex items-start justify-between gap-4"> |
|
||||
<div> |
|
||||
<p className="fabric-kicker">Before you continue</p> |
|
||||
<h2 className="fabric-display mt-3 text-[30px] leading-[1.02] text-[#2E211E]"> |
|
||||
Read the platform guidelines once |
|
||||
</h2> |
|
||||
</div> |
|
||||
<FabricPill className="shrink-0">Step 2</FabricPill> |
|
||||
</div> |
|
||||
|
|
||||
<p className="mt-4 text-[14px] leading-7 text-[#6E5E58]"> |
|
||||
Habib Marriage is built for permanent Islamic marriage with privacy, |
|
||||
identity verification, and family involvement at the center. |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<div className={`${fabricMutedPanelClass} mt-4 px-4 py-4`}> |
|
||||
<div className="flex items-start gap-3"> |
|
||||
<span className="fabric-accent-dot mt-1 shrink-0" /> |
|
||||
<p className="text-[13px] leading-6 text-[#6D5B55]"> |
|
||||
Dating, casual browsing, and public catalogs are not part of this |
|
||||
process. Introductions are deliberate and one-to-one. |
|
||||
</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div className="fabric-scroll mt-4 flex-1 space-y-4 overflow-y-auto pb-2"> |
|
||||
<FabricCard className="px-5 py-5"> |
|
||||
<div className="flex items-start gap-4"> |
|
||||
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#F7E4DC] text-[13px] font-bold tracking-[0.16em] text-[#9A545B]"> |
|
||||
01 |
|
||||
</span> |
|
||||
<div> |
|
||||
<h2 className="text-[18px] font-semibold leading-7 text-[#2F221E]"> |
|
||||
A secure and purposeful path to permanent union |
|
||||
</h2> |
|
||||
<div className="mt-3 space-y-3 text-[13px] leading-6 text-[#5E4E48]"> |
|
||||
<p> |
|
||||
This is not a typical matchmaking feed. The goal is a |
|
||||
confidential path toward permanent marriage among Muslims. |
|
||||
</p> |
|
||||
<p> |
|
||||
To protect dignity, profiles are not publicly listed and |
|
||||
identity checks are required for everyone who participates. |
|
||||
</p> |
|
||||
<p> |
|
||||
Family and guardian involvement are treated as part of the |
|
||||
process rather than an optional afterthought. |
|
||||
</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<div className="grid grid-cols-2 gap-3"> |
|
||||
<FabricCard className="px-4 py-4"> |
|
||||
<p className="fabric-kicker">Privacy</p> |
|
||||
<p className="mt-3 text-[14px] leading-6 text-[#5E4E48]"> |
|
||||
No public catalog of candidates. |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<FabricCard className="px-4 py-4"> |
|
||||
<p className="fabric-kicker">Verification</p> |
|
||||
<p className="mt-3 text-[14px] leading-6 text-[#5E4E48]"> |
|
||||
Identity checks are part of the process. |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<FabricCard className="px-4 py-4"> |
|
||||
<p className="fabric-kicker">Intention</p> |
|
||||
<p className="mt-3 text-[14px] leading-6 text-[#5E4E48]"> |
|
||||
Permanent marriage only, not casual dating. |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<FabricCard className="px-4 py-4"> |
|
||||
<p className="fabric-kicker">Family</p> |
|
||||
<p className="mt-3 text-[14px] leading-6 text-[#5E4E48]"> |
|
||||
Wali and family participation are expected. |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div className="mt-4 space-y-3"> |
|
||||
<p className="px-2 text-center text-[13px] leading-6 text-[#75645D]"> |
|
||||
{isEnabled |
|
||||
? "You can continue to the next step." |
|
||||
: `The continue button unlocks in ${secondsLeft} seconds so the guidelines stay visible.`} |
|
||||
</p> |
|
||||
|
|
||||
<Link |
|
||||
aria-disabled={!isEnabled} |
|
||||
className={`${fabricSecondaryButtonClass} ${ |
|
||||
!isEnabled ? fabricDisabledLinkClass : "" |
|
||||
}`}
|
|
||||
href="/video-2" |
|
||||
tabIndex={isEnabled ? 0 : -1} |
|
||||
> |
|
||||
{isEnabled ? "Next" : `Next in ${secondsLeft}s`} |
|
||||
</Link> |
|
||||
</div> |
|
||||
</FabricScreen> |
|
||||
); |
|
||||
} |
|
||||
@ -1,11 +0,0 @@ |
|||||
import VideoStepScreen from "@/components/screens/video-step-screen"; |
|
||||
|
|
||||
export default function VideoPageTwo() { |
|
||||
return ( |
|
||||
<VideoStepScreen |
|
||||
backHref="/rules" |
|
||||
nextHref="/questions" |
|
||||
stepLabel="Step 3" |
|
||||
/> |
|
||||
); |
|
||||
} |
|
||||
@ -1,5 +0,0 @@ |
|||||
import VideoStepScreen from "@/components/screens/video-step-screen"; |
|
||||
|
|
||||
export default function VideoPage() { |
|
||||
return <VideoStepScreen backHref="/" nextHref="/rules" stepLabel="Step 1" />; |
|
||||
} |
|
||||
@ -1,68 +0,0 @@ |
|||||
"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; |
|
||||
} |
|
||||
@ -1,202 +0,0 @@ |
|||||
"use client"; |
|
||||
|
|
||||
import Image from "next/image"; |
|
||||
import Link from "next/link"; |
|
||||
import { useEffect, useState } from "react"; |
|
||||
import { |
|
||||
BackIcon, |
|
||||
FabricCard, |
|
||||
FabricIconButton, |
|
||||
FabricIconLink, |
|
||||
FabricPill, |
|
||||
FabricProgress, |
|
||||
FabricScreen, |
|
||||
FabricStatusBar, |
|
||||
fabricDisabledLinkClass, |
|
||||
fabricMutedPanelClass, |
|
||||
fabricSecondaryButtonClass, |
|
||||
HistoryIcon, |
|
||||
PlayIcon, |
|
||||
} from "@/components/ui/fabric-mobile"; |
|
||||
|
|
||||
const metrics = [ |
|
||||
{ label: "Marriage applicants", value: "120" }, |
|
||||
{ label: "Successful marriages", value: "120" }, |
|
||||
] as const; |
|
||||
|
|
||||
const mockVideoDuration = 12; |
|
||||
|
|
||||
export default function VideoStepScreen({ |
|
||||
backHref, |
|
||||
nextHref, |
|
||||
stepLabel, |
|
||||
}: { |
|
||||
backHref: string; |
|
||||
nextHref: string; |
|
||||
stepLabel: string; |
|
||||
}) { |
|
||||
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 ( |
|
||||
<FabricScreen> |
|
||||
<FabricStatusBar /> |
|
||||
|
|
||||
<div className="mt-5 flex items-center justify-between"> |
|
||||
<FabricIconLink aria-label="Go back" href={backHref}> |
|
||||
<BackIcon /> |
|
||||
</FabricIconLink> |
|
||||
|
|
||||
<div className="text-center"> |
|
||||
<p className="fabric-kicker">Guided intake</p> |
|
||||
<h1 className="fabric-display mt-2 text-[28px] leading-none text-[#2E211E]"> |
|
||||
Habib Marriage |
|
||||
</h1> |
|
||||
</div> |
|
||||
|
|
||||
<FabricIconButton aria-label="Open watch history"> |
|
||||
<HistoryIcon /> |
|
||||
</FabricIconButton> |
|
||||
</div> |
|
||||
|
|
||||
<FabricCard className="mt-6 px-5 py-5"> |
|
||||
<div className="flex items-start justify-between gap-4"> |
|
||||
<div> |
|
||||
<p className="fabric-kicker">Orientation video</p> |
|
||||
<h2 className="fabric-display mt-3 text-[30px] leading-[1.02] text-[#2E211E]"> |
|
||||
Watch this short introduction before continuing |
|
||||
</h2> |
|
||||
</div> |
|
||||
<FabricPill className="shrink-0">{stepLabel}</FabricPill> |
|
||||
</div> |
|
||||
|
|
||||
<p className="mt-4 text-[14px] leading-7 text-[#6E5E58]"> |
|
||||
This preview explains the tone of the platform and unlocks the next |
|
||||
step once the full clip has been watched. |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
|
|
||||
<div className="mt-5 px-1"> |
|
||||
<div className="relative overflow-hidden rounded-[30px] border border-white/65 bg-[#241914] shadow-[0_24px_48px_rgba(56,31,27,0.28)]"> |
|
||||
<Image |
|
||||
alt="Video preview for Habib Marriage" |
|
||||
height={420} |
|
||||
priority |
|
||||
sizes="(max-width: 390px) 100vw, 340px" |
|
||||
src="/assets/images/Rectangle 3077.png" |
|
||||
width={640} |
|
||||
/> |
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(18,12,10,0.12)_0%,rgba(18,12,10,0.52)_100%)]" /> |
|
||||
|
|
||||
<div className="absolute inset-x-0 top-0 px-5 py-4 text-white"> |
|
||||
<div className="flex items-center justify-between"> |
|
||||
<FabricPill className="bg-white/18 text-white backdrop-blur"> |
|
||||
12s walkthrough |
|
||||
</FabricPill> |
|
||||
<span className="text-[12px] font-semibold uppercase tracking-[0.18em] text-white/82"> |
|
||||
{watchedSeconds}s / {mockVideoDuration}s |
|
||||
</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div className="absolute inset-x-5 bottom-5"> |
|
||||
<FabricProgress |
|
||||
className="h-2 bg-white/25" |
|
||||
fillClassName="bg-[linear-gradient(135deg,#E68C79_0%,#F7C29A_100%)] shadow-none" |
|
||||
value={progressPercent} |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<button |
|
||||
aria-label={ |
|
||||
isCompleted |
|
||||
? "Replay the introduction" |
|
||||
: isPlaying |
|
||||
? "Pause the introduction" |
|
||||
: "Play the introduction" |
|
||||
} |
|
||||
className="absolute left-1/2 top-1/2 flex h-[88px] w-[88px] -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white/28 bg-[radial-gradient(circle_at_top,#C16777_0%,#A14556_48%,#7E2F3B_100%)] text-white shadow-[0_20px_42px_rgba(126,47,59,0.42)] backdrop-blur" |
|
||||
onClick={handlePlay} |
|
||||
type="button" |
|
||||
> |
|
||||
<PlayIcon /> |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div className="mt-4 grid grid-cols-2 gap-3 px-1"> |
|
||||
{metrics.map((metric) => ( |
|
||||
<FabricCard className="px-4 py-4 text-center" key={metric.label}> |
|
||||
<p className="fabric-display text-[28px] leading-none text-[#2F221E]"> |
|
||||
{metric.value} |
|
||||
</p> |
|
||||
<p className="mt-3 text-[12px] font-semibold uppercase tracking-[0.14em] text-[#7A655D]"> |
|
||||
{metric.label} |
|
||||
</p> |
|
||||
</FabricCard> |
|
||||
))} |
|
||||
</div> |
|
||||
|
|
||||
<div className="mt-auto space-y-3"> |
|
||||
<div className={`${fabricMutedPanelClass} px-4 py-4`}> |
|
||||
<div className="flex items-start gap-3"> |
|
||||
<span className="fabric-accent-dot mt-1 shrink-0" /> |
|
||||
<div> |
|
||||
<p className="text-[14px] font-semibold text-[#47302A]"> |
|
||||
Watch until the end to unlock the next screen. |
|
||||
</p> |
|
||||
<p className="mt-1 text-[13px] leading-6 text-[#7A665F]"> |
|
||||
Your progress bar fills as the preview runs. Replay is available |
|
||||
after completion. |
|
||||
</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<Link |
|
||||
aria-disabled={!isCompleted} |
|
||||
className={`${fabricSecondaryButtonClass} ${ |
|
||||
!isCompleted ? fabricDisabledLinkClass : "" |
|
||||
}`}
|
|
||||
href={nextHref} |
|
||||
tabIndex={isCompleted ? 0 : -1} |
|
||||
> |
|
||||
Next |
|
||||
</Link> |
|
||||
</div> |
|
||||
</FabricScreen> |
|
||||
); |
|
||||
} |
|
||||
@ -1,214 +0,0 @@ |
|||||
import Link from "next/link"; |
|
||||
import type { |
|
||||
ButtonHTMLAttributes, |
|
||||
ComponentPropsWithoutRef, |
|
||||
ReactNode, |
|
||||
} from "react"; |
|
||||
|
|
||||
function mergeClasses(...classes: Array<string | false | null | undefined>) { |
|
||||
return classes.filter(Boolean).join(" "); |
|
||||
} |
|
||||
|
|
||||
export function FabricScreen({ |
|
||||
children, |
|
||||
className, |
|
||||
contentClassName, |
|
||||
}: { |
|
||||
children: ReactNode; |
|
||||
className?: string; |
|
||||
contentClassName?: string; |
|
||||
}) { |
|
||||
return ( |
|
||||
<main className="fabric-stage fabric-body"> |
|
||||
<section className={mergeClasses("fabric-phone", className)}> |
|
||||
<div className={mergeClasses("fabric-screen", contentClassName)}> |
|
||||
{children} |
|
||||
</div> |
|
||||
</section> |
|
||||
</main> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export function FabricStatusBar() { |
|
||||
return ( |
|
||||
<div className="fabric-status"> |
|
||||
<span>9:41</span> |
|
||||
<div className="fabric-signal"> |
|
||||
<span className="fabric-signal-bar" /> |
|
||||
<span className="fabric-signal-dot" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export function FabricCard({ |
|
||||
children, |
|
||||
className, |
|
||||
}: { |
|
||||
children: ReactNode; |
|
||||
className?: string; |
|
||||
}) { |
|
||||
return ( |
|
||||
<div className={mergeClasses("fabric-card", className)}>{children}</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export function FabricPill({ |
|
||||
children, |
|
||||
className, |
|
||||
}: { |
|
||||
children: ReactNode; |
|
||||
className?: string; |
|
||||
}) { |
|
||||
return ( |
|
||||
<span className={mergeClasses("fabric-pill", className)}>{children}</span> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export function FabricProgress({ |
|
||||
className, |
|
||||
fillClassName, |
|
||||
value, |
|
||||
}: { |
|
||||
className?: string; |
|
||||
fillClassName?: string; |
|
||||
value: number; |
|
||||
}) { |
|
||||
const clampedValue = Math.max(0, Math.min(value, 100)); |
|
||||
|
|
||||
return ( |
|
||||
<div className={mergeClasses("fabric-progress-track", className)}> |
|
||||
<div |
|
||||
className={mergeClasses("fabric-progress-fill", fillClassName)} |
|
||||
style={{ width: `${clampedValue}%` }} |
|
||||
/> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export function FabricIconButton({ |
|
||||
children, |
|
||||
className, |
|
||||
type = "button", |
|
||||
...props |
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & { |
|
||||
children: ReactNode; |
|
||||
className?: string; |
|
||||
}) { |
|
||||
return ( |
|
||||
<button |
|
||||
className={mergeClasses("fabric-nav-button", className)} |
|
||||
type={type} |
|
||||
{...props} |
|
||||
> |
|
||||
{children} |
|
||||
</button> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export function FabricIconLink({ |
|
||||
children, |
|
||||
className, |
|
||||
...props |
|
||||
}: ComponentPropsWithoutRef<typeof Link> & { |
|
||||
children: ReactNode; |
|
||||
className?: string; |
|
||||
}) { |
|
||||
return ( |
|
||||
<Link className={mergeClasses("fabric-nav-button", className)} {...props}> |
|
||||
{children} |
|
||||
</Link> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export 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 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> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export 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> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export function CheckIcon() { |
|
||||
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="m7.5 14.5 4.25 4.25 8.75-9" |
|
||||
stroke="currentColor" |
|
||||
strokeLinecap="round" |
|
||||
strokeLinejoin="round" |
|
||||
strokeWidth="2.2" |
|
||||
/> |
|
||||
</svg> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export const fabricInputClass = "fabric-input"; |
|
||||
export const fabricTextareaClass = "fabric-input fabric-textarea"; |
|
||||
export const fabricPrimaryButtonClass = "fabric-primary-button"; |
|
||||
export const fabricSecondaryButtonClass = "fabric-secondary-button"; |
|
||||
export const fabricMutedPanelClass = "fabric-muted-panel"; |
|
||||
export const fabricDisabledLinkClass = "fabric-link-disabled"; |
|
||||
@ -1,169 +0,0 @@ |
|||||
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, |
|
||||
); |
|
||||
} |
|
||||
@ -1,40 +0,0 @@ |
|||||
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