Browse Source

Revert repository to 275bd86 snapshot

master
sina_sajjadi 2 months ago
parent
commit
d57dde84ba
  1. 8
      .babelrc
  2. 5
      public/assets/images/Group 1.svg
  3. BIN
      public/assets/images/Intro-Quran.png
  4. BIN
      public/assets/images/Intro-location.png
  5. BIN
      public/assets/images/Rectangle 3077.png
  6. 197
      src/app/details/[section]/detail-section-client.tsx
  7. 18
      src/app/details/[section]/page.tsx
  8. 46
      src/app/details/complete/page.tsx
  9. 185
      src/app/details/page.tsx
  10. 366
      src/app/globals.css
  11. 186
      src/app/intro/page.tsx
  12. 30
      src/app/layout.tsx
  13. 148
      src/app/page.tsx
  14. 209
      src/app/questions/page.tsx
  15. 163
      src/app/rules/page.tsx
  16. 11
      src/app/video-2/page.tsx
  17. 5
      src/app/video/page.tsx
  18. 68
      src/components/dev/dev-click-to-component.tsx
  19. 202
      src/components/screens/video-step-screen.tsx
  20. 214
      src/components/ui/fabric-mobile.tsx
  21. 169
      src/lib/detailed-questions.ts
  22. 40
      src/plugins/add-data-locator.js

8
.babelrc

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

5
public/assets/images/Group 1.svg

@ -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>

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

Before

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

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

Before

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

BIN
public/assets/images/Rectangle 3077.png

Before

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

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

@ -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>
);
}

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

@ -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} />;
}

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

@ -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>
);
}

185
src/app/details/page.tsx

@ -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>
);
}

366
src/app/globals.css

@ -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;
} }

186
src/app/intro/page.tsx

@ -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>
);
}

30
src/app/layout.tsx

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

148
src/app/page.tsx

@ -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
<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> </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}
<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"
> >
<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}
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> </p>
</div> </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 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>
); );
} }

209
src/app/questions/page.tsx

@ -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>
);
}

163
src/app/rules/page.tsx

@ -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 &amp; 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>
);
}

11
src/app/video-2/page.tsx

@ -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"
/>
);
}

5
src/app/video/page.tsx

@ -1,5 +0,0 @@
import VideoStepScreen from "@/components/screens/video-step-screen";
export default function VideoPage() {
return <VideoStepScreen backHref="/" nextHref="/rules" stepLabel="Step 1" />;
}

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

@ -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;
}

202
src/components/screens/video-step-screen.tsx

@ -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>
);
}

214
src/components/ui/fabric-mobile.tsx

@ -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";

169
src/lib/detailed-questions.ts

@ -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,
);
}

40
src/plugins/add-data-locator.js

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