Browse Source

Refactor video pages to use a shared VideoStepScreen component; streamline UI components and improve code organization

master
sina_sajjadi 3 months ago
parent
commit
122bded456
  1. 165
      src/app/details/[section]/detail-section-client.tsx
  2. 2
      src/app/details/[section]/page.tsx
  3. 49
      src/app/details/complete/page.tsx
  4. 166
      src/app/details/page.tsx
  5. 355
      src/app/globals.css
  6. 24
      src/app/intro/page.tsx
  7. 62
      src/app/page.tsx
  8. 176
      src/app/questions/page.tsx
  9. 184
      src/app/rules/page.tsx
  10. 236
      src/app/video-2/page.tsx
  11. 236
      src/app/video/page.tsx
  12. 202
      src/components/screens/video-step-screen.tsx
  13. 214
      src/components/ui/fabric-mobile.tsx

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

@ -1,38 +1,27 @@
"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,
fabricInputClass,
fabricMutedPanelClass,
fabricSecondaryButtonClass,
fabricTextareaClass,
} from "@/components/ui/fabric-mobile";
import {
countCompletedDetailedAnswers,
detailedSections,
type DetailedSection,
getDetailedQuestionCount,
getDetailedSectionProgress,
getDetailedSectionStorageKey,
} from "@/lib/detailed-questions";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
export default function DetailSectionClient({
section,
}: {
@ -40,7 +29,6 @@ export default function DetailSectionClient({
}) {
const [answers, setAnswers] = useState<Record<string, string>>({});
const [isHydrated, setIsHydrated] = useState(false);
const router = useRouter();
useEffect(() => {
const savedAnswers = window.localStorage.getItem(
@ -62,26 +50,7 @@ export default function DetailSectionClient({
getDetailedSectionStorageKey(section.id),
JSON.stringify(answers),
);
const completedTotal = detailedSections.reduce((total, currentSection) => {
const sectionAnswers =
currentSection.id === section.id
? answers
: ((JSON.parse(
window.localStorage.getItem(
getDetailedSectionStorageKey(currentSection.id),
) ?? "{}",
) as Record<string, string>) ?? {});
return (
total + countCompletedDetailedAnswers(currentSection, sectionAnswers)
);
}, 0);
if (completedTotal === getDetailedQuestionCount()) {
router.replace("/details/complete");
}
}, [answers, isHydrated, router, section, section.id]);
}, [answers, isHydrated, section.id]);
const completedCount = useMemo(
() => countCompletedDetailedAnswers(section, answers),
@ -100,86 +69,75 @@ export default function DetailSectionClient({
};
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFB8CE_0%,rgba(255,184,206,0.26)_12%,rgba(255,255,255,0)_34%),linear-gradient(180deg,#FFFDFD_0%,#F9F4F6_100%)]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-5 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<FabricScreen>
<FabricStatusBar />
<div className="mt-4 flex items-center justify-between">
<Link
aria-label="Back to detailed questions"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/details"
>
<div className="mt-5 flex items-center justify-between">
<FabricIconLink aria-label="Back to detailed questions" href="/details">
<BackIcon />
</Link>
</FabricIconLink>
<h1
className="text-[20px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
<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>
<span className="w-11 text-right text-[12px] font-semibold text-[#6E656B]">
<FabricPill className="min-w-[72px] justify-center px-3 py-2 text-[11px]">
{progress}%
</span>
</FabricPill>
</div>
<div className="mt-6 rounded-[26px] bg-white/90 px-5 py-6 shadow-[0_18px_40px_rgba(0,0,0,0.07)]">
<p className="text-[12px] font-semibold uppercase tracking-[0.22em] text-[#F05A93]">
Section Progress
</p>
<h2
className="mt-3 text-[27px] font-semibold leading-[1.15] text-[#2E2327]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
<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-3 text-[14px] leading-6 text-[#675E64]">
<p className="mt-4 text-[14px] leading-7 text-[#6E5E58]">
{section.description}
</p>
<div className="mt-5 h-[10px] overflow-hidden rounded-full bg-[#F7D9E4]">
<div
className="h-full rounded-full bg-linear-to-r from-[#F04C99] to-[#FF8575] transition-[width] duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<FabricProgress className="mt-5 h-2.5" value={progress} />
<p className="mt-3 text-[12px] font-medium tracking-[0.04em] text-[#8B8086]">
<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="mt-5 flex-1 space-y-4 overflow-y-auto pb-2">
{section.questions.map((question) => {
<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 (
<article
className="rounded-[22px] bg-white/92 px-5 py-5 shadow-[0_14px_34px_rgba(0,0,0,0.06)]"
key={question.id}
>
<h3 className="text-[17px] font-semibold leading-7 text-[#31252A]">
<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>
<p className="mt-2 text-[13px] leading-6 text-[#72686E]">
</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="min-h-[132px] w-full resize-none rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 py-3 text-[15px] leading-7 text-[#33292D] outline-none transition focus:border-[#F05A93]"
className={fabricTextareaClass}
onChange={(event) =>
handleChange(question.id, event.target.value)
}
@ -190,7 +148,7 @@ export default function DetailSectionClient({
{question.type === "text" ? (
<input
className="h-[54px] w-full rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 text-[15px] text-[#33292D] outline-none transition focus:border-[#F05A93]"
className={fabricInputClass}
onChange={(event) =>
handleChange(question.id, event.target.value)
}
@ -202,7 +160,7 @@ export default function DetailSectionClient({
{question.type === "number" ? (
<input
className="h-[54px] w-full rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 text-[15px] text-[#33292D] outline-none transition focus:border-[#F05A93]"
className={fabricInputClass}
inputMode="numeric"
onChange={(event) =>
handleChange(question.id, event.target.value)
@ -215,7 +173,7 @@ export default function DetailSectionClient({
{question.type === "date" ? (
<input
className="h-[54px] w-full rounded-[18px] border border-[#F3DFE7] bg-[#FFFDFE] px-4 text-[15px] text-[#33292D] outline-none transition focus:border-[#F05A93]"
className={fabricInputClass}
onChange={(event) =>
handleChange(question.id, event.target.value)
}
@ -224,21 +182,16 @@ export default function DetailSectionClient({
/>
) : null}
</div>
</article>
</FabricCard>
);
})}
</div>
<div className="mt-4">
<Link
className="flex h-[48px] items-center justify-center rounded-[14px] bg-[#242424] text-[18px] font-semibold text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
href="/details"
>
<Link className={fabricSecondaryButtonClass} href="/details">
Back To List
</Link>
</div>
</div>
</section>
</main>
</FabricScreen>
);
}

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

@ -1,6 +1,6 @@
import { notFound } from "next/navigation";
import DetailSectionClient from "./detail-section-client";
import { getDetailedSection } from "@/lib/detailed-questions";
import DetailSectionClient from "./detail-section-client";
export default async function DetailedSectionPage({
params,

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

@ -1,37 +1,46 @@
import Link from "next/link";
import {
CheckIcon,
FabricCard,
FabricPill,
FabricScreen,
FabricStatusBar,
fabricSecondaryButtonClass,
} from "@/components/ui/fabric-mobile";
export default function DetailedQuestionsCompletePage() {
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFBDD2_0%,rgba(255,189,210,0.24)_14%,rgba(255,255,255,0)_36%),linear-gradient(180deg,#FFFDFD_0%,#FAF4F7_100%)]" />
<FabricScreen contentClassName="justify-center">
<FabricStatusBar />
<div className="relative z-10 flex h-full flex-col items-center justify-center px-7 text-center">
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-linear-to-r from-[#F04C99] to-[#FF8575] text-[34px] font-semibold text-white shadow-[0_24px_44px_rgba(240,76,153,0.25)]">
<div 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>
<h1
className="mt-10 text-[32px] font-semibold leading-[1.15] text-[#2E2327]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
<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-[280px] text-[16px] leading-8 text-[#665D63]">
We have received all of the required information. Our review may
take a little time, and we will notify you as soon as there is an
update.
<p 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="mt-12 flex h-[48px] w-full max-w-[290px] items-center justify-center rounded-[14px] bg-[#242424] text-[18px] font-semibold text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
href="/"
>
<Link className={`${fabricSecondaryButtonClass} mt-8`} href="/">
Return Home
</Link>
</div>
</section>
</main>
</FabricScreen>
);
}

166
src/app/details/page.tsx

@ -2,7 +2,18 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
BackIcon,
FabricCard,
FabricIconLink,
FabricPill,
FabricProgress,
FabricScreen,
FabricStatusBar,
fabricMutedPanelClass,
fabricSecondaryButtonClass,
} from "@/components/ui/fabric-mobile";
import {
countCompletedDetailedAnswers,
detailedSections,
@ -11,32 +22,12 @@ import {
getDetailedSectionStorageKey,
} from "@/lib/detailed-questions";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
export default function DetailsOverviewPage() {
const [progressBySection, setProgressBySection] = useState<
Record<string, { completed: number; progress: number }>
>({});
const router = useRouter();
const totalQuestionCount = getDetailedQuestionCount();
useEffect(() => {
const loadProgress = () => {
@ -60,15 +51,6 @@ export default function DetailsOverviewPage() {
);
setProgressBySection(nextProgress);
const completedTotal = Object.values(nextProgress).reduce(
(total, section) => total + section.completed,
0,
);
if (completedTotal === getDetailedQuestionCount()) {
router.replace("/details/complete");
}
};
loadProgress();
@ -79,52 +61,74 @@ export default function DetailsOverviewPage() {
window.removeEventListener("focus", loadProgress);
window.removeEventListener("pageshow", loadProgress);
};
}, [router]);
}, []);
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 (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFB8CE_0%,rgba(255,184,206,0.28)_12%,rgba(255,255,255,0)_32%),linear-gradient(180deg,#FFFDFD_0%,#F9F4F6_100%)]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-5 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<FabricScreen>
<FabricStatusBar />
<div className="mt-4 flex items-center justify-between">
<Link
aria-label="Back to questions"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/questions"
>
<div className="mt-5 flex items-center justify-between">
<FabricIconLink aria-label="Back to questions" href="/questions">
<BackIcon />
</Link>
</FabricIconLink>
<h1
className="text-[22px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Detailed Questions
<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>
<span className="w-11" />
<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>
<div className="mt-10 rounded-[28px] bg-white/88 px-6 py-7 shadow-[0_18px_45px_rgba(63,30,40,0.1)] backdrop-blur-[2px]">
<p className="text-[12px] font-semibold uppercase tracking-[0.22em] text-[#F05A93]">
Complete Your Profile
<p 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>
<p className="mt-4 text-[16px] leading-7 text-[#625960]">
Open each section below and answer the related questions. Every
box tracks its own completion progress.
<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="mt-5 flex-1 space-y-4 overflow-y-auto pb-2">
<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,
@ -133,32 +137,31 @@ export default function DetailsOverviewPage() {
return (
<Link
className="block rounded-[24px] bg-white/92 px-5 py-5 shadow-[0_14px_36px_rgba(0,0,0,0.06)] transition-transform duration-200 hover:-translate-y-0.5"
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>
<h2 className="text-[19px] font-semibold text-[#302428]">
<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-[#6B6468]">
<p className="mt-2 text-[13px] leading-6 text-[#6B5A54]">
{section.description}
</p>
</div>
<span className="rounded-full bg-[#FFF1F7] px-3 py-1 text-[12px] font-semibold text-[#F05A93]">
<FabricPill className="shrink-0">
{sectionProgress.progress}%
</span>
</FabricPill>
</div>
<div className="mt-4 h-[9px] overflow-hidden rounded-full bg-[#F7D9E4]">
<div
className="h-full rounded-full bg-linear-to-r from-[#F04C99] to-[#FF8575] transition-[width] duration-300"
style={{ width: `${sectionProgress.progress}%` }}
<FabricProgress
className="mt-4 h-2"
value={sectionProgress.progress}
/>
</div>
<p className="mt-3 text-[12px] font-medium tracking-[0.04em] text-[#8B8086]">
<p className="mt-3 text-[12px] font-medium tracking-[0.08em] text-[#8A746D]">
{sectionProgress.completed} of {section.questions.length}{" "}
answered
</p>
@ -166,8 +169,17 @@ export default function DetailsOverviewPage() {
);
})}
</div>
<div className="mt-4">
<button
className={fabricSecondaryButtonClass}
disabled={!isComplete}
onClick={() => router.push("/details/complete")}
type="button"
>
Next
</button>
</div>
</section>
</main>
</FabricScreen>
);
}

355
src/app/globals.css

@ -1,5 +1,360 @@
@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;
}
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%);
}
.fabric-kicker {
color: #a05d63;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.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;
}

24
src/app/intro/page.tsx

@ -32,6 +32,8 @@ function ProgressBar({
totalSlides: number;
}) {
const segments = Array.from({ length: totalSlides }, (_, index) => index + 1);
const completedSegmentColor =
currentIndex === 0 ? "bg-white" : "bg-[#F76C93]";
return (
<div
@ -48,22 +50,26 @@ function ProgressBar({
const isActive = segment - 1 === currentIndex;
const isCompleted = segment - 1 <= currentIndex;
const isFirstSegment = segment === 1;
const completedSegmentColor =
currentIndex === 0 ? "bg-white" : "bg-[#F76C93]";
const fillWidth = isCompleted ? "100%" : "0%";
return (
<div
className={`flex h-full flex-1 items-center justify-end px-[2px] transition-colors duration-500 ${
isCompleted ? completedSegmentColor : "bg-transparent"
}
${isActive ? "rounded-r-full" : ""}
${isFirstSegment ? "rounded-l-full" : ""}
`}
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={`h-[4px] w-[4px] rounded-full transition-colors duration-500 ${
className={`relative z-10 h-[4px] w-[4px] rounded-full transition-colors duration-500 ${
isActive ? "bg-[#F05A93]" : "bg-white"
}`}
/>

62
src/app/page.tsx

@ -1,4 +1,5 @@
import Link from "next/link";
import { FabricPill } from "@/components/ui/fabric-mobile";
const previews = [
{
@ -35,34 +36,69 @@ const previews = [
export default function Home() {
return (
<main className="flex min-h-screen items-center justify-center bg-[#3F3F43] p-6">
<section className="w-full max-w-md rounded-[28px] bg-white p-8 shadow-[0_30px_70px_rgba(0,0,0,0.28)]">
<p className="text-sm font-medium uppercase tracking-[0.28em] text-[#F05A93]">
Habib Marriage
</p>
<h1 className="mt-3 text-3xl font-semibold text-[#252525]">
Screen previews
<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-3 text-sm leading-6 text-[#6A6A74]">
Open either mobile layout from here.
<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 space-y-4">
<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-[20px] border border-[#F1D2DE] bg-[linear-gradient(135deg,#FFF7FB_0%,#FFFFFF_100%)] px-5 py-4 transition-transform duration-200 hover:-translate-y-0.5"
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="text-lg font-semibold text-[#252525]">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-[18px] font-semibold text-white">
{preview.title}
</p>
<p className="mt-1 text-sm text-[#6A6A74]">
<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>
</section>
</main>
);

176
src/app/questions/page.tsx

@ -3,27 +3,18 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
import {
BackIcon,
FabricCard,
FabricIconButton,
FabricProgress,
FabricScreen,
FabricStatusBar,
fabricInputClass,
fabricMutedPanelClass,
fabricPrimaryButtonClass,
fabricTextareaClass,
} from "@/components/ui/fabric-mobile";
const slides = [
{
@ -53,47 +44,6 @@ const slides = [
},
] as const;
const progressWidth = 132;
function ProgressBar({
currentIndex,
totalSlides,
}: {
currentIndex: number;
totalSlides: number;
}) {
const segments = Array.from({ length: totalSlides }, (_, index) => index);
return (
<div
aria-label={`Question ${currentIndex + 1} of ${totalSlides}`}
aria-valuemax={totalSlides}
aria-valuemin={1}
aria-valuenow={currentIndex + 1}
className="h-[8px] overflow-hidden rounded-full bg-[#F8D7E4]"
role="progressbar"
style={{ width: `${progressWidth}px` }}
>
<div className="flex h-full w-full">
{segments.map((segment) => {
const isActive = segment <= currentIndex;
return (
<div className="flex flex-1 px-[2px]" key={segment}>
<span
aria-hidden="true"
className={`h-full w-full rounded-full transition-colors duration-300 ${
isActive ? "bg-[#F05A93]" : "bg-transparent"
}`}
/>
</div>
);
})}
</div>
</div>
);
}
export default function QuestionsPage() {
const [currentSlide, setCurrentSlide] = useState(0);
const [answers, setAnswers] = useState<Record<number, string>>({});
@ -102,6 +52,7 @@ export default function QuestionsPage() {
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) => ({
@ -133,65 +84,66 @@ export default function QuestionsPage() {
};
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFB2C8_0%,rgba(255,178,200,0.3)_14%,rgba(255,255,255,0)_36%),linear-gradient(180deg,#FFFDFD_0%,#FBF5F7_100%)]" />
<FabricScreen>
<FabricStatusBar />
<div className="relative z-10 flex h-full flex-col px-4 pb-5 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between">
<button
<div className="mt-5 flex items-center justify-between gap-3">
<FabricIconButton
aria-label="Go to previous question"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)] disabled:opacity-40"
disabled={currentSlide === 0}
onClick={handleBack}
type="button"
>
<BackIcon />
</button>
</FabricIconButton>
<ProgressBar
currentIndex={currentSlide}
totalSlides={slides.length}
/>
<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="text-sm font-semibold text-[#6F6770]"
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>
<div className="mt-12 rounded-[28px] bg-white/88 px-6 py-7 shadow-[0_18px_45px_rgba(63,30,40,0.1)] backdrop-blur-[2px]">
<p className="text-[12px] font-semibold uppercase tracking-[0.2em] text-[#F05A93]">
{activeSlide.helper}
</p>
<h1
className="mt-3 text-[28px] font-semibold leading-[1.2] text-[#2D2226]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
<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>
<p className="mt-4 text-[16px] leading-7 text-[#625960]">
</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-[#8A7E85]">
<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-6">
<div className="mt-4">
{activeSlide.inputType === "number" ? (
<input
className="h-[58px] w-full rounded-[20px] border border-white/70 bg-white/90 px-5 text-[18px] text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
className={fabricInputClass}
inputMode="numeric"
min="18"
onChange={(event) => handleChange(event.target.value)}
@ -203,7 +155,7 @@ export default function QuestionsPage() {
{activeSlide.inputType === "textarea" ? (
<textarea
className="min-h-[170px] w-full resize-none rounded-[20px] border border-white/70 bg-white/90 px-5 py-4 text-[16px] leading-7 text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
className={fabricTextareaClass}
onChange={(event) => handleChange(event.target.value)}
placeholder={activeSlide.placeholder}
value={currentAnswer}
@ -212,7 +164,7 @@ export default function QuestionsPage() {
{activeSlide.inputType === "date" ? (
<input
className="h-[58px] w-full rounded-[20px] border border-white/70 bg-white/90 px-5 text-[18px] text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
className={fabricInputClass}
max="2010-12-31"
onChange={(event) => handleChange(event.target.value)}
type="date"
@ -220,19 +172,21 @@ export default function QuestionsPage() {
/>
) : null}
</div>
</FabricCard>
<div className="mt-auto space-y-3">
<div className="rounded-[18px] bg-white/80 px-4 py-3 text-[13px] leading-6 text-[#6C6469] shadow-[0_10px_24px_rgba(0,0,0,0.04)]">
Current answer: {currentAnswer || "Complete the field above"}
<div 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={`h-[48px] w-full rounded-[14px] text-[18px] font-semibold transition-opacity ${
hasAnswer
? "bg-linear-to-r from-[#F04C99] to-[#FF8575] text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
: "bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
className={fabricPrimaryButtonClass}
disabled={!hasAnswer}
onClick={handleFinish}
type="button"
@ -241,11 +195,7 @@ export default function QuestionsPage() {
</button>
) : (
<button
className={`h-[48px] w-full rounded-[14px] text-[18px] font-semibold transition-opacity ${
hasAnswer
? "bg-linear-to-r from-[#F04C99] to-[#FF8575] text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
: "bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
className={fabricPrimaryButtonClass}
disabled={!hasAnswer}
onClick={handleNext}
type="button"
@ -254,8 +204,6 @@ export default function QuestionsPage() {
</button>
)}
</div>
</div>
</section>
</main>
</FabricScreen>
);
}

184
src/app/rules/page.tsx

@ -2,27 +2,17 @@
import Link from "next/link";
import { useEffect, useState } from "react";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
import {
BackIcon,
FabricCard,
FabricIconLink,
FabricPill,
FabricScreen,
FabricStatusBar,
fabricDisabledLinkClass,
fabricMutedPanelClass,
fabricSecondaryButtonClass,
} from "@/components/ui/fabric-mobile";
const waitDuration = 20;
@ -44,82 +34,130 @@ export default function RulesPage() {
const isEnabled = secondsLeft === 0;
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="relative flex h-full flex-col bg-[linear-gradient(180deg,#FFFFFF_0%,#FBF7F8_100%)]">
<header className="rounded-b-[16px] bg-[#F26793] px-4 pb-4 pt-5 text-white shadow-[0_10px_24px_rgba(242,103,147,0.22)]">
<div className="relative flex items-center justify-center">
<Link
aria-label="Back to video page"
className="absolute left-0 flex h-9 w-9 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-[2px]"
href="/video"
>
<FabricScreen>
<FabricStatusBar />
<div className="mt-5 flex items-center justify-between">
<FabricIconLink aria-label="Back to video page" href="/video">
<BackIcon />
</Link>
</FabricIconLink>
<h1 className="max-w-[230px] text-center text-[15px] font-semibold leading-6">
What&apos;s Habib Marriage (terms &amp; conditions)
<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>
</header>
<div className="flex-1 overflow-y-auto px-4 pb-28 pt-8">
<div className="rounded-[14px] border border-[#46C9E7] bg-[#EFFFFA] px-4 py-3 text-center text-[10px] leading-5 text-[#1D9AB8]">
Habib Marriage is a secure platform exclusively for permanent
Islamic marriage. Its core principles include a zero-tolerance
policy for casual dating or temporary marriage, absolute privacy
through one-on-one matching instead of public catalogs, mandatory
identity verification for all users, and a focus on traditional
guardian (Wali) and family involvement.
<FabricPill className="min-w-[68px] justify-center px-3 py-2 text-[11px]">
{secondsLeft}s
</FabricPill>
</div>
<article className="mt-3 rounded-[20px] bg-white px-5 py-4 text-[#333333] shadow-[0_14px_34px_rgba(0,0,0,0.07)]">
<div className="flex items-start gap-2">
<span className="mt-0.5 text-[18px] leading-none text-[#9B9B9B]">
</span>
<h2 className="text-[18px] font-semibold leading-[1.35] text-[#2D2D2D]">
A Secure and Purposeful Path to Permanent Union
<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="mt-4 space-y-3 text-[13px] leading-6 text-[#494949]">
<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>
Habib Marriage is not a typical matchmaking network. We have
come together with the goal of creating a secure and
confidential path for &quot;permanent marriage&quot; among
Muslims.
This is not a typical matchmaking feed. The goal is a
confidential path toward permanent marriage among Muslims.
</p>
<p>
To preserve your dignity, no catalog of individuals is
displayed here, introductions are strictly purposeful and
one-on-one, and identity checks are required for all users.
To protect dignity, profiles are not publicly listed and
identity checks are required for everyone who participates.
</p>
<p>
Please review our system regulations and red lines, including
the strict prohibition of dating before the confirmation box,
so that you may proceed on this path with confidence.
Family and guardian involvement are treated as part of the
process rather than an optional afterthought.
</p>
</div>
</article>
</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>
<div className="absolute inset-x-0 bottom-0 bg-[linear-gradient(180deg,rgba(251,247,248,0)_0%,#FBF7F8_24%)] px-5 pb-6 pt-5">
<Link
aria-disabled={!isEnabled}
className={`h-[48px] w-full rounded-[12px] text-[18px] font-semibold transition-colors ${
isEnabled
? "flex items-center justify-center bg-[#242424] text-white"
: "pointer-events-none flex items-center justify-center bg-[#C9C9CC] text-[#3C3C3F]"
className={`${fabricSecondaryButtonClass} ${
!isEnabled ? fabricDisabledLinkClass : ""
}`}
href={isEnabled ? "/video-2" : "/rules"}
href="/video-2"
tabIndex={isEnabled ? 0 : -1}
>
{isEnabled ? "Next" : `Next (${secondsLeft}s)`}
{isEnabled ? "Next" : `Next in ${secondsLeft}s`}
</Link>
</div>
</div>
</section>
</main>
</FabricScreen>
);
}

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

@ -1,237 +1,11 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
function HistoryIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 8.25a5.25 5.25 0 1 1 1.539 3.711"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
<path
d="M3.75 4.5v3.75H7.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
</svg>
);
}
function PlayIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="28"
viewBox="0 0 28 28"
width="28"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 8.75 20 14l-10 5.25V8.75Z" fill="white" />
</svg>
);
}
const metrics = [
{ label: "marriage applicants", value: "120" },
{ label: "Successful marriage", value: "120" },
];
const mockVideoDuration = 12;
import VideoStepScreen from "@/components/screens/video-step-screen";
export default function VideoPageTwo() {
const [isPlaying, setIsPlaying] = useState(false);
const [watchedSeconds, setWatchedSeconds] = useState(0);
const isCompleted = watchedSeconds >= mockVideoDuration;
const progressPercent = Math.min(
(watchedSeconds / mockVideoDuration) * 100,
100,
);
useEffect(() => {
if (!isPlaying || isCompleted) {
return;
}
const timer = window.setInterval(() => {
setWatchedSeconds((currentValue) => {
const nextValue = Math.min(currentValue + 1, mockVideoDuration);
if (nextValue >= mockVideoDuration) {
setIsPlaying(false);
}
return nextValue;
});
}, 1000);
return () => window.clearInterval(timer);
}, [isCompleted, isPlaying]);
const handlePlay = () => {
if (isCompleted) {
setWatchedSeconds(0);
}
setIsPlaying((currentValue) => !currentValue || isCompleted);
};
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FF9BB8_0%,rgba(255,155,184,0.32)_14%,rgba(255,255,255,0)_34%),linear-gradient(180deg,rgba(255,255,255,0.72)_0%,rgba(255,255,255,0.98)_34%,#FCF8F7_100%)]" />
<div className="absolute inset-0 opacity-40 [background-image:linear-gradient(45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(-45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,rgba(226,201,206,0.22)_75%),linear-gradient(-45deg,transparent_75%,rgba(226,201,206,0.22)_75%)] [background-position:0_0,0_8px,8px_-8px,-8px_0] [background-size:16px_16px]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-4 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between px-1">
<Link
aria-label="Back to rules"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/rules"
>
<BackIcon />
</Link>
<h1
className="text-[20px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Habib Marriage
</h1>
<button
aria-label="Open watch history"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
type="button"
>
<HistoryIcon />
</button>
</div>
<div className="mt-[110px] px-1">
<div className="relative overflow-hidden rounded-[20px] shadow-[0_18px_40px_rgba(0,0,0,0.22)]">
<Image
alt="Video preview for Habib Marriage"
height={420}
priority
sizes="(max-width: 375px) 100vw, 320px"
src="/assets/images/Rectangle 3077.png"
width={640}
/>
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(6,8,14,0.08)_0%,rgba(6,8,14,0.44)_100%)]" />
<div className="absolute inset-x-0 top-0 h-16 bg-[linear-gradient(180deg,rgba(6,8,14,0.38)_0%,rgba(6,8,14,0)_100%)] px-4 py-3 text-white">
<div className="flex items-center justify-between text-[11px] font-medium uppercase tracking-[0.16em]">
<span>Mock video</span>
<span>{watchedSeconds}s / 12s</span>
</div>
</div>
<div className="absolute inset-x-4 bottom-4">
<div className="h-1.5 overflow-hidden rounded-full bg-white/30">
<div
className="h-full rounded-full bg-[#F04C99] transition-[width] duration-700"
style={{ width: `${progressPercent}%` }}
<VideoStepScreen
backHref="/rules"
nextHref="/questions"
stepLabel="Step 3"
/>
</div>
</div>
<button
aria-label={
isCompleted
? "Replay mock video"
: isPlaying
? "Pause mock video"
: "Play mock video"
}
className="absolute left-1/2 top-1/2 flex h-20 w-20 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-[#BF2F2F] shadow-[0_18px_35px_rgba(191,47,47,0.45)]"
onClick={handlePlay}
type="button"
>
<PlayIcon />
</button>
</div>
</div>
<p className="mt-4 px-2 text-center text-[12px] font-medium text-[#6D6268]">
Watch the full mock video to unlock the next step.
</p>
<div className="mt-[102px] grid grid-cols-2 gap-4 px-4 text-center text-[#242424]">
{metrics.map((metric) => (
<div key={metric.label}>
<p className="text-[20px] font-semibold">{metric.value}</p>
<p className="mt-2 text-[13px] font-semibold leading-5">
{metric.label}
</p>
</div>
))}
</div>
<div className="mt-auto space-y-3">
<button
className="h-[44px] w-full rounded-[14px] bg-linear-to-r from-[#F04C99] to-[#FF8575] text-[18px] font-semibold text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
type="button"
>
Information recording
</button>
<Link
aria-disabled={!isCompleted}
className={`flex h-[44px] items-center justify-center rounded-[14px] text-[18px] font-semibold transition-opacity ${
isCompleted
? "bg-[#242424] text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
: "pointer-events-none bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
href={isCompleted ? "/questions" : "/video-2"}
tabIndex={isCompleted ? 0 : -1}
>
Next
</Link>
</div>
</div>
</section>
</main>
);
}

236
src/app/video/page.tsx

@ -1,237 +1,5 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
function BackIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.5 4.5 6 9l4.5 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
function HistoryIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="18"
viewBox="0 0 18 18"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 8.25a5.25 5.25 0 1 1 1.539 3.711"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
<path
d="M3.75 4.5v3.75H7.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
/>
</svg>
);
}
function PlayIcon() {
return (
<svg
aria-hidden="true"
fill="none"
height="28"
viewBox="0 0 28 28"
width="28"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 8.75 20 14l-10 5.25V8.75Z" fill="white" />
</svg>
);
}
const metrics = [
{ label: "marriage applicants", value: "120" },
{ label: "Successful marriage", value: "120" },
];
const mockVideoDuration = 12;
import VideoStepScreen from "@/components/screens/video-step-screen";
export default function VideoPage() {
const [isPlaying, setIsPlaying] = useState(false);
const [watchedSeconds, setWatchedSeconds] = useState(0);
const isCompleted = watchedSeconds >= mockVideoDuration;
const progressPercent = Math.min(
(watchedSeconds / mockVideoDuration) * 100,
100,
);
useEffect(() => {
if (!isPlaying || isCompleted) {
return;
}
const timer = window.setInterval(() => {
setWatchedSeconds((currentValue) => {
const nextValue = Math.min(currentValue + 1, mockVideoDuration);
if (nextValue >= mockVideoDuration) {
setIsPlaying(false);
}
return nextValue;
});
}, 1000);
return () => window.clearInterval(timer);
}, [isCompleted, isPlaying]);
const handlePlay = () => {
if (isCompleted) {
setWatchedSeconds(0);
}
setIsPlaying((currentValue) => !currentValue || isCompleted);
};
return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FF9BB8_0%,rgba(255,155,184,0.32)_14%,rgba(255,255,255,0)_34%),linear-gradient(180deg,rgba(255,255,255,0.72)_0%,rgba(255,255,255,0.98)_34%,#FCF8F7_100%)]" />
<div className="absolute inset-0 opacity-40 [background-image:linear-gradient(45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(-45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,rgba(226,201,206,0.22)_75%),linear-gradient(-45deg,transparent_75%,rgba(226,201,206,0.22)_75%)] [background-position:0_0,0_8px,8px_-8px,-8px_0] [background-size:16px_16px]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-4 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between px-1">
<Link
aria-label="Back to home"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/"
>
<BackIcon />
</Link>
<h1
className="text-[20px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Habib Marriage
</h1>
<button
aria-label="Open watch history"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
type="button"
>
<HistoryIcon />
</button>
</div>
<div className="mt-[110px] px-1">
<div className="relative overflow-hidden rounded-[20px] shadow-[0_18px_40px_rgba(0,0,0,0.22)]">
<Image
alt="Video preview for Habib Marriage"
height={420}
priority
sizes="(max-width: 375px) 100vw, 320px"
src="/assets/images/Rectangle 3077.png"
width={640}
/>
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(6,8,14,0.08)_0%,rgba(6,8,14,0.44)_100%)]" />
<div className="absolute inset-x-0 top-0 h-16 bg-[linear-gradient(180deg,rgba(6,8,14,0.38)_0%,rgba(6,8,14,0)_100%)] px-4 py-3 text-white">
<div className="flex items-center justify-between text-[11px] font-medium uppercase tracking-[0.16em]">
<span>Mock video</span>
<span>{watchedSeconds}s / 12s</span>
</div>
</div>
<div className="absolute inset-x-4 bottom-4">
<div className="h-1.5 overflow-hidden rounded-full bg-white/30">
<div
className="h-full rounded-full bg-[#F04C99] transition-[width] duration-700"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
<button
aria-label={
isCompleted
? "Replay mock video"
: isPlaying
? "Pause mock video"
: "Play mock video"
}
className="absolute left-1/2 top-1/2 flex h-20 w-20 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-[#BF2F2F] shadow-[0_18px_35px_rgba(191,47,47,0.45)]"
onClick={handlePlay}
type="button"
>
<PlayIcon />
</button>
</div>
</div>
<p className="mt-4 px-2 text-center text-[12px] font-medium text-[#6D6268]">
Watch the full mock video to unlock the next step.
</p>
<div className="mt-[102px] grid grid-cols-2 gap-4 px-4 text-center text-[#242424]">
{metrics.map((metric) => (
<div key={metric.label}>
<p className="text-[20px] font-semibold">{metric.value}</p>
<p className="mt-2 text-[13px] font-semibold leading-5">
{metric.label}
</p>
</div>
))}
</div>
<div className="mt-auto space-y-3">
<button
className="h-[44px] w-full rounded-[14px] bg-linear-to-r from-[#F04C99] to-[#FF8575] text-[18px] font-semibold text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
type="button"
>
Information recording
</button>
<Link
aria-disabled={!isCompleted}
className={`flex h-[44px] items-center justify-center rounded-[14px] text-[18px] font-semibold transition-opacity ${
isCompleted
? "bg-[#242424] text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
: "pointer-events-none bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
href={isCompleted ? "/rules" : "/video"}
tabIndex={isCompleted ? 0 : -1}
>
Next
</Link>
</div>
</div>
</section>
</main>
);
return <VideoStepScreen backHref="/" nextHref="/rules" stepLabel="Step 1" />;
}

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

@ -0,0 +1,202 @@
"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

@ -0,0 +1,214 @@
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";
Loading…
Cancel
Save