@@ -15,7 +15,8 @@ export default function RegisterPage() {
1515 const [ step , setStep ] = useState < number > ( 1 ) ;
1616 const [ submitting , setSubmitting ] = useState < boolean > ( false ) ;
1717 const [ message , setMessage ] = useState < string > ( "" ) ;
18- const [ debug , setDebug ] = useState < string > ( "" ) ;
18+ const [ pin , setPin ] = useState < string > ( "" ) ;
19+ const [ confirmPin , setConfirmPin ] = useState < string > ( "" ) ;
1920
2021 async function handleCaptured ( args : {
2122 embedding : number [ ] ;
@@ -38,9 +39,6 @@ export default function RegisterPage() {
3839 : ( args . embedding || [ ] ) . length ,
3940 modelVersion : args . modelVersion ,
4041 } ;
41- setDebug (
42- `payload: len=${ payload . embedding . length } , dim=${ payload . embeddingDim } `
43- ) ;
4442 setMessage ( "Registering your face template..." ) ;
4543 try {
4644 const res = await fetch ( "/api/face/register" , {
@@ -63,6 +61,39 @@ export default function RegisterPage() {
6361 setSubmitting ( false ) ;
6462 }
6563 }
64+
65+ function handleBack ( ) {
66+ if ( submitting ) return ;
67+ setMessage ( "" ) ;
68+ setStep ( ( s ) => Math . max ( 1 , s - 1 ) ) ;
69+ }
70+
71+ function handleNext ( ) {
72+ if ( submitting ) return ;
73+ setMessage ( "" ) ;
74+ if ( step === 2 ) {
75+ const normalized = pin . trim ( ) ;
76+ if ( normalized . length !== 6 || / \D / . test ( normalized ) ) {
77+ setMessage ( "Enter a 6-digit PIN." ) ;
78+ return ;
79+ }
80+ setStep ( 3 ) ;
81+ return ;
82+ }
83+ if ( step === 3 ) {
84+ const normalized = confirmPin . trim ( ) ;
85+ if ( normalized . length !== 6 || / \D / . test ( normalized ) ) {
86+ setMessage ( "Confirm with a 6-digit PIN." ) ;
87+ return ;
88+ }
89+ if ( normalized !== pin ) {
90+ setMessage ( "PINs do not match." ) ;
91+ return ;
92+ }
93+ setStep ( 4 ) ;
94+ return ;
95+ }
96+ }
6697 return (
6798 < div className = "min-h-screen bg-gradient-to-br from-white to-indigo-50" >
6899 < main className = "mx-auto max-w-4xl px-6 py-12" >
@@ -74,47 +105,87 @@ export default function RegisterPage() {
74105 </ p >
75106 </ div >
76107
77- { /* Stepper */ }
78- < ol className = "flex w-full items-center justify-center gap-4 text-sm text-gray-600" >
79- < li className = "flex items-center gap-2" >
80- < span className = "h-6 w-6 rounded-full bg-indigo-600 text-white text-xs grid place-items-center" >
81- 1
82- </ span >
83- Face
84- </ li >
85- < div className = "h-px w-10 bg-gray-300" />
86- < li className = "flex items-center gap-2 opacity-60" >
87- < span className = "h-6 w-6 rounded-full bg-gray-200 text-gray-700 text-xs grid place-items-center" >
88- 2
89- </ span >
90- PIN
91- </ li >
92- < div className = "h-px w-10 bg-gray-300" />
93- < li className = "flex items-center gap-2 opacity-60" >
94- < span className = "h-6 w-6 rounded-full bg-gray-200 text-gray-700 text-xs grid place-items-center" >
95- 3
96- </ span >
97- Confirm
98- </ li >
99- < div className = "h-px w-10 bg-gray-300" />
100- < li className = "flex items-center gap-2 opacity-60" >
101- < span className = "h-6 w-6 rounded-full bg-gray-200 text-gray-700 text-xs grid place-items-center" >
102- 4
103- </ span >
104- Sign
105- </ li >
106- < div className = "h-px w-10 bg-gray-300" />
107- < li className = "flex items-center gap-2 opacity-60" >
108- < span className = "h-6 w-6 rounded-full bg-gray-200 text-gray-700 text-xs grid place-items-center" >
109- 5
110- </ span >
111- Email
112- </ li >
113- </ ol >
108+ { /* Stepper with colored circles and connectors */ }
109+ { ( ( ) => {
110+ const stepsLabels = [ "Face" , "PIN" , "Confirm" , "Proof" , "Email" ] ;
111+ return (
112+ < div className = "flex w-full items-center justify-center gap-4 text-sm" >
113+ { stepsLabels . map ( ( label , idx ) => {
114+ const circleActive = idx < step ; // 1-based step
115+ const connectorActive = idx < step - 1 ; // connectors behind current step
116+ return (
117+ < div key = { label } className = "flex items-center gap-4" >
118+ < div className = "flex items-center gap-2" >
119+ < span
120+ className = { `h-6 w-6 rounded-full grid place-items-center text-xs ${
121+ circleActive
122+ ? "bg-indigo-600 text-white"
123+ : "bg-gray-200 text-gray-700"
124+ } `}
125+ >
126+ { idx + 1 }
127+ </ span >
128+ < span
129+ className = {
130+ circleActive ? "text-gray-900" : "text-gray-500"
131+ }
132+ >
133+ { label }
134+ </ span >
135+ </ div >
136+ { idx < stepsLabels . length - 1 && (
137+ < div
138+ className = { `h-0.5 w-10 ${
139+ connectorActive ? "bg-indigo-600" : "bg-gray-300"
140+ } `}
141+ />
142+ ) }
143+ </ div >
144+ ) ;
145+ } ) }
146+ </ div >
147+ ) ;
148+ } ) ( ) }
114149
115150 < div className = "mt-4" >
116151 { step === 1 ? (
117152 < FaceCapture onCaptured = { handleCaptured } />
153+ ) : step === 2 ? (
154+ < div className = "w-full max-w-sm mx-auto" >
155+ < label className = "block text-sm font-medium text-gray-700" >
156+ Set a PIN (6 digits)
157+ </ label >
158+ < input
159+ type = "password"
160+ inputMode = "numeric"
161+ pattern = "[0-9]*"
162+ value = { pin }
163+ onChange = { ( e ) =>
164+ setPin ( e . target . value . replace ( / \D / g, "" ) . slice ( 0 , 6 ) )
165+ }
166+ maxLength = { 6 }
167+ className = "mt-2 w-full rounded-lg border border-gray-300 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-400"
168+ placeholder = "••••••"
169+ />
170+ </ div >
171+ ) : step === 3 ? (
172+ < div className = "w-full max-w-sm mx-auto" >
173+ < label className = "block text-sm font-medium text-gray-700" >
174+ Confirm your PIN
175+ </ label >
176+ < input
177+ type = "password"
178+ inputMode = "numeric"
179+ pattern = "[0-9]*"
180+ value = { confirmPin }
181+ onChange = { ( e ) =>
182+ setConfirmPin ( e . target . value . replace ( / \D / g, "" ) . slice ( 0 , 6 ) )
183+ }
184+ maxLength = { 6 }
185+ className = "mt-2 w-full rounded-lg border border-gray-300 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-400"
186+ placeholder = "••••••"
187+ />
188+ </ div >
118189 ) : (
119190 < div className = "relative h-64 w-64 rounded-full bg-green-50 border border-green-200 shadow grid place-items-center" >
120191 < span className = "text-green-700" > Face captured ✓</ span >
@@ -123,36 +194,31 @@ export default function RegisterPage() {
123194 </ div >
124195
125196 < div className = "w-full max-w-md" >
126- < div className = "h-2 w-full rounded bg-gray-200" >
127- < div
128- className = "h-2 rounded bg-indigo-600 transition-all"
129- style = { { width : `${ ( step / 5 ) * 100 } %` } }
130- />
131- </ div >
132197 < div className = "mt-2 text-center text-sm text-gray-600 min-h-5" >
133198 { message }
134199 </ div >
135- { debug && (
136- < div className = "mt-1 text-center text-xs text-gray-400 break-all" >
137- { debug }
138- </ div >
139- ) }
140200 </ div >
141201 < div className = "flex gap-3" >
142202 < button
203+ onClick = { handleBack }
143204 disabled = { step === 1 || submitting }
144205 className = "rounded-lg border border-gray-300 px-4 py-2 text-gray-700 disabled:opacity-50"
145206 >
146207 Back
147208 </ button >
148209 < button
149- disabled = { step < 1 || submitting }
210+ onClick = { handleNext }
211+ disabled = { step === 1 || submitting }
150212 className = "rounded-lg bg-indigo-600 px-5 py-2 text-white font-semibold disabled:opacity-50"
151213 >
152214 { step === 1
153215 ? "Awaiting face..."
154216 : step === 2
155217 ? "Set PIN"
218+ : step === 3
219+ ? "Confirm PIN"
220+ : step === 4
221+ ? "Generate proof"
156222 : "Next" }
157223 </ button >
158224 </ div >
0 commit comments