Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/app/verify-identity/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import IdentityVerificationWizard from "@/components/identity/IdentityVerificationWizard";

export default function VerifyIdentityPage() {
return (
<div className="max-w-5xl mx-auto px-6 py-16">
<span className="inline-block bg-terra/10 text-terra-deep text-xs font-semibold uppercase tracking-wide rounded-full px-3 py-1 mb-4">
Onboarding
</span>
<h1 className="font-heading text-3xl font-semibold">Verify your identity</h1>
<p className="text-muted mt-2 mb-10 max-w-xl">
A few quick steps to confirm who you are. You can save your progress and come back any
time before submitting.
</p>

<IdentityVerificationWizard />
</div>
);
}
31 changes: 21 additions & 10 deletions src/components/ProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"use client";

import { useState } from "react";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { useDropzone } from "react-dropzone";
import {
HiHome,
HiCalendar,
HiUser,
HiShieldCheck,
HiLockClosed,
HiMail,
HiQuestionMarkCircle,
Expand All @@ -30,6 +32,7 @@ const navItems = [
{ icon: HiHome, label: "Dashboard" },
{ icon: HiCalendar, label: "Appointment" },
{ icon: HiUser, label: "My Profile", active: true },
{ icon: HiShieldCheck, label: "Verify Identity", href: "/verify-identity" },
{ icon: HiLockClosed, label: "Login Details" },
{ icon: HiMail, label: "Message" },
{ icon: HiQuestionMarkCircle, label: "Help" },
Expand Down Expand Up @@ -67,16 +70,24 @@ export default function ProfileForm() {
<h2 className="font-heading text-lg font-semibold mb-8">My Account</h2>
<nav>
<ul className="flex flex-col gap-1 text-sm">
{navItems.map((item) => (
<li
key={item.label}
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl ${
item.active ? "bg-terra text-white font-medium" : "text-white/70 hover:bg-white/5"
}`}
>
<item.icon className="text-lg" /> {item.label}
</li>
))}
{navItems.map((item) => {
const itemClassName = `flex items-center gap-3 px-3 py-2.5 rounded-xl ${
item.active ? "bg-terra text-white font-medium" : "text-white/70 hover:bg-white/5"
}`;
return (
<li key={item.label}>
{item.href ? (
<Link href={item.href} className={itemClassName}>
<item.icon className="text-lg" /> {item.label}
</Link>
) : (
<span className={itemClassName}>
<item.icon className="text-lg" /> {item.label}
</span>
)}
</li>
);
})}
</ul>
</nav>
</aside>
Expand Down
49 changes: 49 additions & 0 deletions src/components/identity/DocumentDropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useDropzone } from "react-dropzone";
import { HiCheckCircle, HiUpload } from "react-icons/hi";

interface DocumentDropzoneProps {
label: string;
hint: string;
file: File | null;
onSelect: (file: File) => void;
error?: string;
}

/** A single labeled image dropzone — shared by the document-upload and
selfie steps so both look and behave the same way. */
export default function DocumentDropzone({ label, hint, file, onSelect, error }: DocumentDropzoneProps) {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop: (accepted) => {
if (accepted[0]) onSelect(accepted[0]);
},
accept: { "image/*": [] },
maxFiles: 1,
});

return (
<div>
<span className="font-medium text-ink text-sm">{label}</span>
<div
{...getRootProps()}
className={`mt-1.5 rounded-2xl border-2 border-dashed p-6 text-center cursor-pointer transition-colors ${
error ? "border-err" : "border-line hover:border-navy-2"
}`}
>
<input {...getInputProps()} />
{file ? (
<div className="flex flex-col items-center gap-2 text-ok">
<HiCheckCircle className="text-3xl" />
<p className="text-sm text-ink">{file.name}</p>
<p className="text-xs text-muted">Click or drop to replace</p>
</div>
) : (
<div className="flex flex-col items-center gap-2 text-muted">
<HiUpload className="text-2xl text-terra" />
<p className="text-sm">{isDragActive ? "Drop the image here..." : hint}</p>
</div>
)}
</div>
{error && <p className="text-err text-xs mt-1">{error}</p>}
</div>
);
}
51 changes: 51 additions & 0 deletions src/components/identity/IdentityStepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { HiCheck } from "react-icons/hi";
import { STEP_IDS, STEP_LABELS, type StepId } from "@/lib/identityVerification";

interface IdentityStepperProps {
currentIndex: number;
/** Highest index the user has successfully validated up to — steps at or
before this are clickable, steps beyond it are disclosed but inert
(progressive disclosure: visible so users know what's ahead, not yet
reachable until earlier steps are complete). */
furthestValidatedIndex: number;
onStepSelect: (index: number) => void;
}

export default function IdentityStepper({
currentIndex,
furthestValidatedIndex,
onStepSelect,
}: IdentityStepperProps) {
return (
<ol className="flex flex-wrap gap-x-6 gap-y-3 mb-8" aria-label="Verification progress">
{STEP_IDS.map((id: StepId, index) => {
const isCurrent = index === currentIndex;
const isCompleted = index < furthestValidatedIndex || (index === furthestValidatedIndex && index < currentIndex);
const isReachable = index <= furthestValidatedIndex;

return (
<li key={id} className="flex items-center gap-2">
<button
type="button"
disabled={!isReachable}
onClick={() => onStepSelect(index)}
aria-current={isCurrent ? "step" : undefined}
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold transition-colors ${
isCurrent
? "bg-navy text-white"
: isCompleted
? "bg-ok text-white"
: "bg-line text-muted"
} ${isReachable ? "cursor-pointer" : "cursor-not-allowed"}`}
>
{isCompleted ? <HiCheck /> : index + 1}
</button>
<span className={`text-sm ${isCurrent ? "font-semibold text-ink" : "text-muted"}`}>
{STEP_LABELS[id]}
</span>
</li>
);
})}
</ol>
);
}
Loading
Loading