feature: add quickaction email verify page
This commit is contained in:
parent
54f946461c
commit
80d66300cd
207
src/app/quickaction-dashboard/register-email-verify/page.tsx
Normal file
207
src/app/quickaction-dashboard/register-email-verify/page.tsx
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
|
import PageLayout from '../../components/PageLayout'
|
||||||
|
import useAuthStore from '../../store/authStore'
|
||||||
|
|
||||||
|
export default function EmailVerifyPage() {
|
||||||
|
const user = useAuthStore(s => s.user)
|
||||||
|
const [code, setCode] = useState(['', '', '', '', '', ''])
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [success, setSuccess] = useState(false)
|
||||||
|
const [resendCooldown, setResendCooldown] = useState(0)
|
||||||
|
const inputsRef = useRef<Array<HTMLInputElement | null>>([])
|
||||||
|
|
||||||
|
// Cooldown timer
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resendCooldown) return
|
||||||
|
const t = setInterval(() => {
|
||||||
|
setResendCooldown(c => (c > 0 ? c - 1 : 0))
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(t)
|
||||||
|
}, [resendCooldown])
|
||||||
|
|
||||||
|
const handleChange = (idx: number, val: string) => {
|
||||||
|
if (!/^\d?$/.test(val)) return
|
||||||
|
const next = [...code]
|
||||||
|
next[idx] = val
|
||||||
|
setCode(next)
|
||||||
|
setError('')
|
||||||
|
if (val && idx < 5) {
|
||||||
|
inputsRef.current[idx + 1]?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (idx: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Backspace' && !code[idx] && idx > 0) {
|
||||||
|
const prev = idx - 1
|
||||||
|
inputsRef.current[prev]?.focus()
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowLeft' && idx > 0) {
|
||||||
|
inputsRef.current[idx - 1]?.focus()
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight' && idx < 5) {
|
||||||
|
inputsRef.current[idx + 1]?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullCode = code.join('')
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (fullCode.length !== 6) {
|
||||||
|
setError('Bitte 6-stelligen Code eingeben.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSubmitting(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
// TODO: call backend verify endpoint
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
|
setSuccess(true)
|
||||||
|
} catch {
|
||||||
|
setError('Verifizierung fehlgeschlagen. Bitte erneut versuchen.')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResend = useCallback(async () => {
|
||||||
|
if (resendCooldown) return
|
||||||
|
setError('')
|
||||||
|
// TODO: call resend endpoint
|
||||||
|
setResendCooldown(30)
|
||||||
|
}, [resendCooldown])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout>
|
||||||
|
<div className="relative min-h-screen w-full px-4 sm:px-6 py-20">
|
||||||
|
{/* Background Pattern */}
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
x="50%"
|
||||||
|
y={-1}
|
||||||
|
id="affiliate-pattern"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" />
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect fill="url(#affiliate-pattern)" width="100%" height="100%" strokeWidth={0} />
|
||||||
|
</svg>
|
||||||
|
{/* Colored Blur Effect */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
clipPath:
|
||||||
|
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)'
|
||||||
|
}}
|
||||||
|
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Gradient base */}
|
||||||
|
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
|
||||||
|
|
||||||
|
<div className="max-w-xl mx-auto">
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">
|
||||||
|
E-Mail verifizieren
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-gray-300 text-sm sm:text-base">
|
||||||
|
Gib den 6-stelligen Code ein, den wir an
|
||||||
|
{' '}
|
||||||
|
<span className="text-indigo-300 font-medium">
|
||||||
|
{user?.email || 'deine E-Mail'}
|
||||||
|
</span>{' '}
|
||||||
|
gesendet haben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card */}
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="bg-white/95 dark:bg-gray-900/95 backdrop-blur rounded-2xl shadow-2xl ring-1 ring-black/10 dark:ring-white/10 px-6 py-8 sm:px-10 sm:py-10"
|
||||||
|
>
|
||||||
|
<fieldset disabled={submitting || success} className="space-y-8">
|
||||||
|
<div className="flex justify-center gap-2 sm:gap-3">
|
||||||
|
{code.map((v, i) => (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
ref={el => { inputsRef.current[i] = el }}
|
||||||
|
inputMode="numeric"
|
||||||
|
aria-label={`Code Ziffer ${i + 1}`}
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
maxLength={1}
|
||||||
|
value={v}
|
||||||
|
onChange={e => handleChange(i, e.target.value)}
|
||||||
|
onKeyDown={e => handleKeyDown(i, e)}
|
||||||
|
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none
|
||||||
|
${v
|
||||||
|
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||||
|
: 'border-gray-300 dark:border-gray-600 bg-white/80 dark:bg-gray-800/70 text-gray-700 dark:text-gray-200'}
|
||||||
|
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
|
||||||
|
Verifiziert! Weiterleitung in Kürze...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<>
|
||||||
|
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
|
||||||
|
Prüfe...
|
||||||
|
</>
|
||||||
|
) : success ? 'Verifiziert' : 'Code bestätigen'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleResend}
|
||||||
|
disabled={!!resendCooldown || submitting || success}
|
||||||
|
className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{resendCooldown
|
||||||
|
? `Erneut senden in ${resendCooldown}s`
|
||||||
|
: 'Code erneut senden'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Probleme? Prüfe deinen Spam-Ordner oder{' '}
|
||||||
|
<span className="text-indigo-600 dark:text-indigo-400 font-medium">
|
||||||
|
kontaktiere den Support
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user