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. 319
      src/app/details/[section]/detail-section-client.tsx
  2. 2
      src/app/details/[section]/page.tsx
  3. 61
      src/app/details/complete/page.tsx
  4. 240
      src/app/details/page.tsx
  5. 355
      src/app/globals.css
  6. 24
      src/app/intro/page.tsx
  7. 88
      src/app/page.tsx
  8. 304
      src/app/questions/page.tsx
  9. 210
      src/app/rules/page.tsx
  10. 238
      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

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

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

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

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

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

@ -1,37 +1,46 @@
import Link from "next/link"; import Link from "next/link";
import {
CheckIcon,
FabricCard,
FabricPill,
FabricScreen,
FabricStatusBar,
fabricSecondaryButtonClass,
} from "@/components/ui/fabric-mobile";
export default function DetailedQuestionsCompletePage() { export default function DetailedQuestionsCompletePage() {
return ( 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>
<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="mt-10 text-[32px] font-semibold leading-[1.15] text-[#2E2327]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Your profile details are complete
</h1>
<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> </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="/"
>
Return Home
</Link>
</div>
</section>
</main>
<Link className={`${fabricSecondaryButtonClass} mt-8`} href="/">
Return Home
</Link>
</div>
</FabricScreen>
); );
} }

240
src/app/details/page.tsx

@ -2,7 +2,18 @@
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; 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 { import {
countCompletedDetailedAnswers, countCompletedDetailedAnswers,
detailedSections, detailedSections,
@ -11,32 +22,12 @@ import {
getDetailedSectionStorageKey, getDetailedSectionStorageKey,
} from "@/lib/detailed-questions"; } 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() { export default function DetailsOverviewPage() {
const [progressBySection, setProgressBySection] = useState< const [progressBySection, setProgressBySection] = useState<
Record<string, { completed: number; progress: number }> Record<string, { completed: number; progress: number }>
>({}); >({});
const router = useRouter(); const router = useRouter();
const totalQuestionCount = getDetailedQuestionCount();
useEffect(() => { useEffect(() => {
const loadProgress = () => { const loadProgress = () => {
@ -60,15 +51,6 @@ export default function DetailsOverviewPage() {
); );
setProgressBySection(nextProgress); setProgressBySection(nextProgress);
const completedTotal = Object.values(nextProgress).reduce(
(total, section) => total + section.completed,
0,
);
if (completedTotal === getDetailedQuestionCount()) {
router.replace("/details/complete");
}
}; };
loadProgress(); loadProgress();
@ -79,95 +61,125 @@ export default function DetailsOverviewPage() {
window.removeEventListener("focus", loadProgress); window.removeEventListener("focus", loadProgress);
window.removeEventListener("pageshow", 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 ( 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>
<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> </div>
<FabricPill className="shrink-0">{completedTotal} done</FabricPill>
</div>
<div className="mt-4 flex items-center justify-between">
<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 <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"
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}
> >
<BackIcon />
</Link>
<h1
className="text-[22px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Detailed Questions
</h1>
<span className="w-11" />
</div>
<div className="mt-10 rounded-[28px] bg-white/88 px-6 py-7 shadow-[0_18px_45px_rgba(63,30,40,0.1)] backdrop-blur-[2px]">
<p className="text-[12px] font-semibold uppercase tracking-[0.22em] text-[#F05A93]">
Complete Your Profile
</p>
<p className="mt-4 text-[16px] leading-7 text-[#625960]">
Open each section below and answer the related questions. Every
box tracks its own completion progress.
</p>
</div>
<div className="mt-5 flex-1 space-y-4 overflow-y-auto pb-2">
{detailedSections.map((section) => {
const sectionProgress = progressBySection[section.id] ?? {
completed: 0,
progress: 0,
};
return (
<Link
className="block rounded-[24px] bg-white/92 px-5 py-5 shadow-[0_14px_36px_rgba(0,0,0,0.06)] transition-transform duration-200 hover:-translate-y-0.5"
href={`/details/${section.id}`}
key={section.id}
>
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-[19px] font-semibold text-[#302428]">
{section.title}
</h2>
<p className="mt-2 text-[13px] leading-6 text-[#6B6468]">
{section.description}
</p>
</div>
<span className="rounded-full bg-[#FFF1F7] px-3 py-1 text-[12px] font-semibold text-[#F05A93]">
{sectionProgress.progress}%
</span>
</div>
<div className="mt-4 h-[9px] overflow-hidden rounded-full bg-[#F7D9E4]">
<div
className="h-full rounded-full bg-linear-to-r from-[#F04C99] to-[#FF8575] transition-[width] duration-300"
style={{ width: `${sectionProgress.progress}%` }}
/>
</div>
<p className="mt-3 text-[12px] font-medium tracking-[0.04em] text-[#8B8086]">
{sectionProgress.completed} of {section.questions.length}{" "}
answered
<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> </p>
</Link>
);
})}
</div>
</div>
</section>
</main>
</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>
); );
} }

355
src/app/globals.css

@ -1,5 +1,360 @@
@import "tailwindcss"; @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 { body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 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; totalSlides: number;
}) { }) {
const segments = Array.from({ length: totalSlides }, (_, index) => index + 1); const segments = Array.from({ length: totalSlides }, (_, index) => index + 1);
const completedSegmentColor =
currentIndex === 0 ? "bg-white" : "bg-[#F76C93]";
return ( return (
<div <div
@ -48,22 +50,26 @@ function ProgressBar({
const isActive = segment - 1 === currentIndex; const isActive = segment - 1 === currentIndex;
const isCompleted = segment - 1 <= currentIndex; const isCompleted = segment - 1 <= currentIndex;
const isFirstSegment = segment === 1; const isFirstSegment = segment === 1;
const completedSegmentColor =
currentIndex === 0 ? "bg-white" : "bg-[#F76C93]";
const fillWidth = isCompleted ? "100%" : "0%";
return ( return (
<div <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} 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 <span
aria-hidden="true" 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" isActive ? "bg-[#F05A93]" : "bg-white"
}`} }`}
/> />

88
src/app/page.tsx

@ -1,4 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import { FabricPill } from "@/components/ui/fabric-mobile";
const previews = [ const previews = [
{ {
@ -35,33 +36,68 @@ const previews = [
export default function Home() { export default function Home() {
return ( 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
</h1>
<p className="mt-3 text-sm leading-6 text-[#6A6A74]">
Open either mobile layout from here.
</p>
<main className="fabric-stage fabric-body px-6 py-10">
<section className="w-full max-w-4xl rounded-[36px] border border-white/12 bg-[linear-gradient(180deg,rgba(255,252,248,0.12)_0%,rgba(255,252,248,0.04)_100%)] p-4 shadow-[0_32px_80px_rgba(25,12,14,0.22)] backdrop-blur">
<div className="grid gap-4 md:grid-cols-[1.05fr_0.95fr]">
<div className="rounded-[30px] border border-white/65 bg-[linear-gradient(180deg,rgba(255,255,255,0.96)_0%,rgba(255,247,241,0.86)_100%)] px-7 py-8 shadow-[0_24px_48px_rgba(99,63,50,0.14)]">
<FabricPill>Habib Marriage</FabricPill>
<h1 className="fabric-display mt-5 text-[42px] leading-[0.96] text-[#2E211E] sm:text-[54px]">
Screen previews for the app flow
</h1>
<p className="mt-5 max-w-[30rem] text-[15px] leading-8 text-[#685751]">
The non-intro screens now share one warmer fabric-inspired visual
system. Use this page to move through each route while checking
the updated flow.
</p>
<div className="mt-8 space-y-4">
{previews.map((preview) => (
<Link
className="block rounded-[20px] border border-[#F1D2DE] bg-[linear-gradient(135deg,#FFF7FB_0%,#FFFFFF_100%)] px-5 py-4 transition-transform duration-200 hover:-translate-y-0.5"
href={preview.href}
key={preview.href}
>
<p className="text-lg font-semibold text-[#252525]">
{preview.title}
</p>
<p className="mt-1 text-sm text-[#6A6A74]">
{preview.description}
</p>
</Link>
))}
<div className="mt-8 grid gap-3 sm:grid-cols-2">
<div className="rounded-[24px] bg-[#F7E4DC] px-4 py-4">
<p className="text-[12px] font-semibold uppercase tracking-[0.16em] text-[#9A595E]">
Surfaces
</p>
<p className="mt-2 text-[14px] leading-7 text-[#5E4D47]">
Unified cards, inputs, and action buttons across the flow.
</p>
</div>
<div className="rounded-[24px] bg-[#F2ECE7] px-4 py-4">
<p className="text-[12px] font-semibold uppercase tracking-[0.16em] text-[#7A655D]">
Navigation
</p>
<p className="mt-2 text-[14px] leading-7 text-[#5E4D47]">
Shared headers, progress states, and clearer completion cues.
</p>
</div>
</div>
</div>
<div className="rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(78,58,58,0.72)_0%,rgba(47,35,37,0.86)_100%)] px-5 py-6 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]">
<p className="text-[12px] font-semibold uppercase tracking-[0.24em] text-white/72">
Routes
</p>
<div className="mt-5 space-y-3">
{previews.map((preview) => (
<Link
className="block rounded-[24px] border border-white/8 bg-white/6 px-5 py-4 transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white/10"
href={preview.href}
key={preview.href}
>
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-[18px] font-semibold text-white">
{preview.title}
</p>
<p className="mt-2 text-[13px] leading-6 text-white/68">
{preview.description}
</p>
</div>
<span className="rounded-full bg-white/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-white/80">
Open
</span>
</div>
</Link>
))}
</div>
</div>
</div> </div>
</section> </section>
</main> </main>

304
src/app/questions/page.tsx

@ -3,27 +3,18 @@
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; 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 = [ const slides = [
{ {
@ -53,47 +44,6 @@ const slides = [
}, },
] as const; ] 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() { export default function QuestionsPage() {
const [currentSlide, setCurrentSlide] = useState(0); const [currentSlide, setCurrentSlide] = useState(0);
const [answers, setAnswers] = useState<Record<number, string>>({}); const [answers, setAnswers] = useState<Record<number, string>>({});
@ -102,6 +52,7 @@ export default function QuestionsPage() {
const activeSlide = slides[currentSlide]; const activeSlide = slides[currentSlide];
const currentAnswer = answers[currentSlide] ?? ""; const currentAnswer = answers[currentSlide] ?? "";
const hasAnswer = currentAnswer.trim().length > 0; const hasAnswer = currentAnswer.trim().length > 0;
const progressPercent = ((currentSlide + 1) / slides.length) * 100;
const handleChange = (value: string) => { const handleChange = (value: string) => {
setAnswers((currentAnswers) => ({ setAnswers((currentAnswers) => ({
@ -133,129 +84,126 @@ export default function QuestionsPage() {
}; };
return ( return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FFB2C8_0%,rgba(255,178,200,0.3)_14%,rgba(255,255,255,0)_36%),linear-gradient(180deg,#FFFDFD_0%,#FBF5F7_100%)]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-5 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between">
<button
aria-label="Go to previous question"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)] disabled:opacity-40"
disabled={currentSlide === 0}
onClick={handleBack}
type="button"
>
<BackIcon />
</button>
<ProgressBar
currentIndex={currentSlide}
totalSlides={slides.length}
/>
<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="text-sm font-semibold text-[#6F6770]"
href="/video-2"
>
Exit
</Link>
</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>
<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} {activeSlide.title}
</h1> </h1>
<p className="mt-4 text-[16px] leading-7 text-[#625960]">
{activeSlide.question}
</p>
<p className="mt-3 text-[13px] leading-6 text-[#8A7E85]">
{activeSlide.helperText}
</p>
</div>
<div className="mt-6">
{activeSlide.inputType === "number" ? (
<input
className="h-[58px] w-full rounded-[20px] border border-white/70 bg-white/90 px-5 text-[18px] text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
inputMode="numeric"
min="18"
onChange={(event) => handleChange(event.target.value)}
placeholder={activeSlide.placeholder}
type="number"
value={currentAnswer}
/>
) : null}
{activeSlide.inputType === "textarea" ? (
<textarea
className="min-h-[170px] w-full resize-none rounded-[20px] border border-white/70 bg-white/90 px-5 py-4 text-[16px] leading-7 text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
onChange={(event) => handleChange(event.target.value)}
placeholder={activeSlide.placeholder}
value={currentAnswer}
/>
) : null}
{activeSlide.inputType === "date" ? (
<input
className="h-[58px] w-full rounded-[20px] border border-white/70 bg-white/90 px-5 text-[18px] text-[#32282D] shadow-[0_10px_24px_rgba(0,0,0,0.05)] outline-none transition focus:border-[#F05A93]"
max="2010-12-31"
onChange={(event) => handleChange(event.target.value)}
type="date"
value={currentAnswer}
/>
) : null}
</div> </div>
<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>
<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>
<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>
{currentSlide === lastSlideIndex ? (
<button
className={`h-[48px] w-full rounded-[14px] text-[18px] font-semibold transition-opacity ${
hasAnswer
? "bg-linear-to-r from-[#F04C99] to-[#FF8575] text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
: "bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
disabled={!hasAnswer}
onClick={handleFinish}
type="button"
>
Finish
</button>
) : (
<button
className={`h-[48px] w-full rounded-[14px] text-[18px] font-semibold transition-opacity ${
hasAnswer
? "bg-linear-to-r from-[#F04C99] to-[#FF8575] text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
: "bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
disabled={!hasAnswer}
onClick={handleNext}
type="button"
>
Next
</button>
)}
</div>
<div 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> </div>
</section>
</main>
</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>
); );
} }

210
src/app/rules/page.tsx

@ -2,27 +2,17 @@
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; 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; const waitDuration = 20;
@ -44,82 +34,130 @@ export default function RulesPage() {
const isEnabled = secondsLeft === 0; const isEnabled = secondsLeft === 0;
return ( return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="relative flex h-full flex-col bg-[linear-gradient(180deg,#FFFFFF_0%,#FBF7F8_100%)]">
<header className="rounded-b-[16px] bg-[#F26793] px-4 pb-4 pt-5 text-white shadow-[0_10px_24px_rgba(242,103,147,0.22)]">
<div className="relative flex items-center justify-center">
<Link
aria-label="Back to video page"
className="absolute left-0 flex h-9 w-9 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-[2px]"
href="/video"
>
<BackIcon />
</Link>
<h1 className="max-w-[230px] text-center text-[15px] font-semibold leading-6">
What&apos;s Habib Marriage (terms &amp; conditions)
</h1>
</div>
</header>
<div className="flex-1 overflow-y-auto px-4 pb-28 pt-8">
<div className="rounded-[14px] border border-[#46C9E7] bg-[#EFFFFA] px-4 py-3 text-center text-[10px] leading-5 text-[#1D9AB8]">
Habib Marriage is a secure platform exclusively for permanent
Islamic marriage. Its core principles include a zero-tolerance
policy for casual dating or temporary marriage, absolute privacy
through one-on-one matching instead of public catalogs, mandatory
identity verification for all users, and a focus on traditional
guardian (Wali) and family involvement.
</div>
<FabricScreen>
<FabricStatusBar />
<article className="mt-3 rounded-[20px] bg-white px-5 py-4 text-[#333333] shadow-[0_14px_34px_rgba(0,0,0,0.07)]">
<div className="flex items-start gap-2">
<span className="mt-0.5 text-[18px] leading-none text-[#9B9B9B]">
</span>
<h2 className="text-[18px] font-semibold leading-[1.35] text-[#2D2D2D]">
A Secure and Purposeful Path to Permanent Union
</h2>
</div>
<div className="mt-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>
<div className="mt-4 space-y-3 text-[13px] leading-6 text-[#494949]">
<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> <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>
<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>
<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> </p>
</div> </div>
</article>
</div>
</div> </div>
</FabricCard>
<div className="absolute inset-x-0 bottom-0 bg-[linear-gradient(180deg,rgba(251,247,248,0)_0%,#FBF7F8_24%)] px-5 pb-6 pt-5">
<Link
aria-disabled={!isEnabled}
className={`h-[48px] w-full rounded-[12px] text-[18px] font-semibold transition-colors ${
isEnabled
? "flex items-center justify-center bg-[#242424] text-white"
: "pointer-events-none flex items-center justify-center bg-[#C9C9CC] text-[#3C3C3F]"
}`}
href={isEnabled ? "/video-2" : "/rules"}
tabIndex={isEnabled ? 0 : -1}
>
{isEnabled ? "Next" : `Next (${secondsLeft}s)`}
</Link>
</div>
<div 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>
</section>
</main>
</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>
); );
} }

238
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() { 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 ( return (
<main className="flex min-h-screen items-center justify-center bg-[#444446] p-2 sm:p-4">
<section className="relative aspect-[375/813] w-full max-w-[375px] overflow-hidden rounded-[24px] bg-[#FCF8F7] shadow-[0_30px_70px_rgba(0,0,0,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,#FF9BB8_0%,rgba(255,155,184,0.32)_14%,rgba(255,255,255,0)_34%),linear-gradient(180deg,rgba(255,255,255,0.72)_0%,rgba(255,255,255,0.98)_34%,#FCF8F7_100%)]" />
<div className="absolute inset-0 opacity-40 [background-image:linear-gradient(45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(-45deg,rgba(226,201,206,0.28)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,rgba(226,201,206,0.22)_75%),linear-gradient(-45deg,transparent_75%,rgba(226,201,206,0.22)_75%)] [background-position:0_0,0_8px,8px_-8px,-8px_0] [background-size:16px_16px]" />
<div className="relative z-10 flex h-full flex-col px-4 pb-4 pt-5">
<div className="flex items-center justify-between px-2 text-[#1C1C1E]">
<span className="text-[14px] font-semibold">9:41</span>
<div className="flex items-center gap-1.5">
<span className="block h-[7px] w-[18px] rounded-[3px] border border-current opacity-85" />
<span className="block h-[7px] w-[7px] rounded-full bg-current opacity-85" />
</div>
</div>
<div className="mt-4 flex items-center justify-between px-1">
<Link
aria-label="Back to rules"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
href="/rules"
>
<BackIcon />
</Link>
<h1
className="text-[20px] font-semibold text-[#2A1D1E]"
style={{ fontFamily: "Georgia, Times New Roman, serif" }}
>
Habib Marriage
</h1>
<button
aria-label="Open watch history"
className="flex h-11 w-11 items-center justify-center rounded-full bg-white text-[#202020] shadow-[0_10px_24px_rgba(0,0,0,0.08)]"
type="button"
>
<HistoryIcon />
</button>
</div>
<div className="mt-[110px] px-1">
<div className="relative overflow-hidden rounded-[20px] shadow-[0_18px_40px_rgba(0,0,0,0.22)]">
<Image
alt="Video preview for Habib Marriage"
height={420}
priority
sizes="(max-width: 375px) 100vw, 320px"
src="/assets/images/Rectangle 3077.png"
width={640}
/>
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(6,8,14,0.08)_0%,rgba(6,8,14,0.44)_100%)]" />
<div className="absolute inset-x-0 top-0 h-16 bg-[linear-gradient(180deg,rgba(6,8,14,0.38)_0%,rgba(6,8,14,0)_100%)] px-4 py-3 text-white">
<div className="flex items-center justify-between text-[11px] font-medium uppercase tracking-[0.16em]">
<span>Mock video</span>
<span>{watchedSeconds}s / 12s</span>
</div>
</div>
<div className="absolute inset-x-4 bottom-4">
<div className="h-1.5 overflow-hidden rounded-full bg-white/30">
<div
className="h-full rounded-full bg-[#F04C99] transition-[width] duration-700"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
<button
aria-label={
isCompleted
? "Replay mock video"
: isPlaying
? "Pause mock video"
: "Play mock video"
}
className="absolute left-1/2 top-1/2 flex h-20 w-20 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-[#BF2F2F] shadow-[0_18px_35px_rgba(191,47,47,0.45)]"
onClick={handlePlay}
type="button"
>
<PlayIcon />
</button>
</div>
</div>
<p className="mt-4 px-2 text-center text-[12px] font-medium text-[#6D6268]">
Watch the full mock video to unlock the next step.
</p>
<div className="mt-[102px] grid grid-cols-2 gap-4 px-4 text-center text-[#242424]">
{metrics.map((metric) => (
<div key={metric.label}>
<p className="text-[20px] font-semibold">{metric.value}</p>
<p className="mt-2 text-[13px] font-semibold leading-5">
{metric.label}
</p>
</div>
))}
</div>
<div className="mt-auto space-y-3">
<button
className="h-[44px] w-full rounded-[14px] bg-linear-to-r from-[#F04C99] to-[#FF8575] text-[18px] font-semibold text-white shadow-[0_16px_36px_rgba(240,76,153,0.28)]"
type="button"
>
Information recording
</button>
<Link
aria-disabled={!isCompleted}
className={`flex h-[44px] items-center justify-center rounded-[14px] text-[18px] font-semibold transition-opacity ${
isCompleted
? "bg-[#242424] text-white shadow-[0_14px_30px_rgba(0,0,0,0.18)]"
: "pointer-events-none bg-[#D9D2D5] text-[#857D82] opacity-80"
}`}
href={isCompleted ? "/questions" : "/video-2"}
tabIndex={isCompleted ? 0 : -1}
>
Next
</Link>
</div>
</div>
</section>
</main>
<VideoStepScreen
backHref="/rules"
nextHref="/questions"
stepLabel="Step 3"
/>
); );
} }

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() { 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