feature: add quickaction email verify page

This commit is contained in:
DeathKaioken 2025-10-04 00:20:17 +02:00
parent 54f946461c
commit 80d66300cd

View 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>
)
}