23 changed files with 98 additions and 2184 deletions
-
8.babelrc
-
2AGENTS.md
-
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"; |
|||
|
|||
: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() { |
|||
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> |
|||
</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