feat: cant spam email verify mail
This commit is contained in:
parent
604556ca06
commit
fb82536a09
@ -96,6 +96,32 @@ export default function QuickActionDashboardPage() {
|
|||||||
const canCompleteInfo = emailVerified && idUploaded
|
const canCompleteInfo = emailVerified && idUploaded
|
||||||
const canSignContract = emailVerified && idUploaded && additionalInfo
|
const canSignContract = emailVerified && idUploaded && additionalInfo
|
||||||
|
|
||||||
|
// NEW: resend cooldown tracking (10 minutes like verify page)
|
||||||
|
const RESEND_INTERVAL_MS = 10 * 60 * 1000
|
||||||
|
const getStorageKey = (email?: string | null) => `emailVerify:lastSent:${email || 'anon'}`
|
||||||
|
const getLastSentAt = (email?: string | null) => {
|
||||||
|
if (typeof window === 'undefined') return 0
|
||||||
|
try { return parseInt(localStorage.getItem(getStorageKey(email)) || '0', 10) || 0 } catch { return 0 }
|
||||||
|
}
|
||||||
|
const [resendRemainingSec, setResendRemainingSec] = useState(0)
|
||||||
|
const formatMmSs = (total: number) => {
|
||||||
|
const m = Math.floor(total / 60)
|
||||||
|
const s = total % 60
|
||||||
|
return `${m}:${String(s).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isClient || emailVerified) return
|
||||||
|
const last = getLastSentAt(user?.email)
|
||||||
|
const remainingMs = Math.max(0, RESEND_INTERVAL_MS - (Date.now() - last))
|
||||||
|
setResendRemainingSec(Math.ceil(remainingMs / 1000))
|
||||||
|
if (remainingMs <= 0) return
|
||||||
|
const id = setInterval(() => {
|
||||||
|
setResendRemainingSec(s => (s > 0 ? s - 1 : 0))
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [isClient, emailVerified, user?.email])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative min-h-screen w-full px-3 sm:px-4 py-10">
|
<div className="relative min-h-screen w-full px-3 sm:px-4 py-10">
|
||||||
@ -231,18 +257,28 @@ export default function QuickActionDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{/* Email Verification */}
|
{/* Email Verification */}
|
||||||
<button
|
<div className="flex flex-col">
|
||||||
onClick={handleVerifyEmail}
|
<button
|
||||||
disabled={emailVerified}
|
onClick={handleVerifyEmail}
|
||||||
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
|
disabled={emailVerified}
|
||||||
emailVerified
|
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
|
||||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
emailVerified
|
||||||
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
|
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
||||||
}`}
|
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
|
||||||
>
|
}`}
|
||||||
<EnvelopeOpenIcon className="h-6 w-6 mb-2" />
|
>
|
||||||
{emailVerified ? 'Email Verified' : 'Verify Email'}
|
<EnvelopeOpenIcon className="h-6 w-6 mb-2" />
|
||||||
</button>
|
{emailVerified ? 'Email Verified' : 'Verify Email'}
|
||||||
|
</button>
|
||||||
|
{/* NEW: resend feedback (only when not verified) */}
|
||||||
|
{!emailVerified && (
|
||||||
|
<p className="mt-2 text-[11px] text-[#112c55] text-center">
|
||||||
|
{resendRemainingSec > 0
|
||||||
|
? `Resend available in ${formatMmSs(resendRemainingSec)}`
|
||||||
|
: 'You can request a new code now'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ID Upload */}
|
{/* ID Upload */}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'
|
|||||||
import PageLayout from '../../components/PageLayout'
|
import PageLayout from '../../components/PageLayout'
|
||||||
import useAuthStore from '../../store/authStore'
|
import useAuthStore from '../../store/authStore'
|
||||||
import { useUserStatus } from '../../hooks/useUserStatus'
|
import { useUserStatus } from '../../hooks/useUserStatus'
|
||||||
|
import { useRouter } from 'next/navigation' // NEW
|
||||||
|
|
||||||
export default function EmailVerifyPage() {
|
export default function EmailVerifyPage() {
|
||||||
const user = useAuthStore(s => s.user)
|
const user = useAuthStore(s => s.user)
|
||||||
@ -16,16 +17,39 @@ export default function EmailVerifyPage() {
|
|||||||
const [resendCooldown, setResendCooldown] = useState(0)
|
const [resendCooldown, setResendCooldown] = useState(0)
|
||||||
const [initialEmailSent, setInitialEmailSent] = useState(false)
|
const [initialEmailSent, setInitialEmailSent] = useState(false)
|
||||||
const inputsRef = useRef<Array<HTMLInputElement | null>>([])
|
const inputsRef = useRef<Array<HTMLInputElement | null>>([])
|
||||||
const emailSentRef = useRef(false) // Prevent double email sending
|
const emailSentRef = useRef(false)
|
||||||
|
const router = useRouter() // NEW
|
||||||
|
|
||||||
// Send verification email automatically on page load
|
// NEW: resend and validity windows
|
||||||
|
const RESEND_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
|
||||||
|
const CODE_VALIDITY_MS = 15 * 60 * 1000 // 15 minutes (informational)
|
||||||
|
|
||||||
|
// NEW: helpers to persist per-user last sent timestamp
|
||||||
|
const getStorageKey = (email?: string | null) => `emailVerify:lastSent:${email || 'anon'}`
|
||||||
|
const getLastSentAt = (email?: string | null) => {
|
||||||
|
try { return parseInt(localStorage.getItem(getStorageKey(email)) || '0', 10) || 0 } catch { return 0 }
|
||||||
|
}
|
||||||
|
const setLastSentAt = (ts: number, email?: string | null) => {
|
||||||
|
try { localStorage.setItem(getStorageKey(email), String(ts)) } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send verification email automatically on page load (respect cooldown)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token || initialEmailSent || emailSentRef.current) return
|
if (!token || emailSentRef.current) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const last = getLastSentAt(user?.email)
|
||||||
|
const remaining = RESEND_INTERVAL_MS - (now - last)
|
||||||
|
|
||||||
|
if (last && remaining > 0) {
|
||||||
|
// Already sent recently: honor cooldown, don't resend
|
||||||
|
setInitialEmailSent(true)
|
||||||
|
setResendCooldown(Math.ceil(remaining / 1000))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const sendInitialEmail = async () => {
|
const sendInitialEmail = async () => {
|
||||||
// Set the ref immediately to prevent race conditions
|
|
||||||
emailSentRef.current = true
|
emailSentRef.current = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-verification-email`, {
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/send-verification-email`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -34,26 +58,23 @@ export default function EmailVerifyPage() {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
if (response.ok && data.success) {
|
||||||
setInitialEmailSent(true)
|
setInitialEmailSent(true)
|
||||||
setResendCooldown(30) // Start cooldown after initial send
|
setLastSentAt(Date.now(), user?.email)
|
||||||
|
setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000))
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to send initial verification email:', data.message)
|
console.error('Failed to send initial verification email:', data?.message)
|
||||||
// Reset ref on failure so it can be retried
|
|
||||||
emailSentRef.current = false
|
emailSentRef.current = false
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending initial verification email:', error)
|
console.error('Error sending initial verification email:', error)
|
||||||
// Reset ref on failure so it can be retried
|
|
||||||
emailSentRef.current = false
|
emailSentRef.current = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendInitialEmail()
|
sendInitialEmail()
|
||||||
}, [token, initialEmailSent])
|
}, [token, user])
|
||||||
|
|
||||||
// Cooldown timer
|
// Cooldown timer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -64,27 +85,85 @@ export default function EmailVerifyPage() {
|
|||||||
return () => clearInterval(t)
|
return () => clearInterval(t)
|
||||||
}, [resendCooldown])
|
}, [resendCooldown])
|
||||||
|
|
||||||
|
// Replace handleChange to support multi-digit input distribution
|
||||||
const handleChange = (idx: number, val: string) => {
|
const handleChange = (idx: number, val: string) => {
|
||||||
if (!/^\d?$/.test(val)) return
|
const digits = (val || '').replace(/\D/g, '')
|
||||||
|
if (digits.length === 0) {
|
||||||
|
const next = [...code]
|
||||||
|
next[idx] = ''
|
||||||
|
setCode(next)
|
||||||
|
setError('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distribute digits across fields starting from idx
|
||||||
const next = [...code]
|
const next = [...code]
|
||||||
next[idx] = val
|
let i = idx
|
||||||
|
for (const ch of digits) {
|
||||||
|
if (i > 5) break
|
||||||
|
next[i] = ch
|
||||||
|
i++
|
||||||
|
}
|
||||||
setCode(next)
|
setCode(next)
|
||||||
setError('')
|
setError('')
|
||||||
if (val && idx < 5) {
|
|
||||||
inputsRef.current[idx + 1]?.focus()
|
// Focus next empty or last filled
|
||||||
|
const focusTo = Math.min(i, 5)
|
||||||
|
inputsRef.current[focusTo]?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New: paste handler to fill all fields
|
||||||
|
const handlePaste = (idx: number, e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const pasted = e.clipboardData.getData('text') || ''
|
||||||
|
const digits = pasted.replace(/\D/g, '').slice(0, 6)
|
||||||
|
if (!digits) return
|
||||||
|
|
||||||
|
const next = [...code]
|
||||||
|
let i = idx
|
||||||
|
for (const ch of digits) {
|
||||||
|
if (i > 5) break
|
||||||
|
next[i] = ch
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
|
setCode(next)
|
||||||
|
|
||||||
|
// Move focus to last populated field
|
||||||
|
const focusTo = Math.min(i - 1, 5)
|
||||||
|
if (focusTo >= 0) inputsRef.current[focusTo]?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (idx: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (idx: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Backspace' && !code[idx] && idx > 0) {
|
if (e.key === 'Backspace') {
|
||||||
const prev = idx - 1
|
e.preventDefault()
|
||||||
inputsRef.current[prev]?.focus()
|
const next = [...code]
|
||||||
|
if (next[idx]) {
|
||||||
|
// If current has a value, clear it and stay
|
||||||
|
next[idx] = ''
|
||||||
|
setCode(next)
|
||||||
|
setError('')
|
||||||
|
inputsRef.current[idx]?.focus()
|
||||||
|
} else if (idx > 0) {
|
||||||
|
// If empty, move to previous and clear it
|
||||||
|
const prev = idx - 1
|
||||||
|
next[prev] = ''
|
||||||
|
setCode(next)
|
||||||
|
setError('')
|
||||||
|
inputsRef.current[prev]?.focus()
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'ArrowLeft' && idx > 0) {
|
if (e.key === 'ArrowLeft' && idx > 0) {
|
||||||
|
e.preventDefault()
|
||||||
inputsRef.current[idx - 1]?.focus()
|
inputsRef.current[idx - 1]?.focus()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'ArrowRight' && idx < 5) {
|
if (e.key === 'ArrowRight' && idx < 5) {
|
||||||
|
e.preventDefault()
|
||||||
inputsRef.current[idx + 1]?.focus()
|
inputsRef.current[idx + 1]?.focus()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,11 +172,11 @@ export default function EmailVerifyPage() {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (fullCode.length !== 6) {
|
if (fullCode.length !== 6) {
|
||||||
setError('Bitte 6-stelligen Code eingeben.')
|
setError('Please enter the 6-digit code.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setError('Nicht authentifiziert. Bitte erneut einloggen.')
|
setError('Not authenticated. Please log in again.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,18 +202,29 @@ export default function EmailVerifyPage() {
|
|||||||
window.location.href = '/quickaction-dashboard'
|
window.location.href = '/quickaction-dashboard'
|
||||||
}, 2000)
|
}, 2000)
|
||||||
} else {
|
} else {
|
||||||
setError(data.error || 'Verifizierung fehlgeschlagen. Bitte erneut versuchen.')
|
setError(data.error || 'Verification failed. Please try again.')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Email verification error:', error)
|
console.error('Email verification error:', error)
|
||||||
setError('Netzwerkfehler. Bitte erneut versuchen.')
|
setError('Network error. Please try again.')
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resend (respect 10min interval even across navigation)
|
||||||
const handleResend = useCallback(async () => {
|
const handleResend = useCallback(async () => {
|
||||||
if (resendCooldown || !token) return
|
if (submitting || success) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const last = getLastSentAt(user?.email)
|
||||||
|
const remaining = RESEND_INTERVAL_MS - (now - last)
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
setResendCooldown(Math.ceil(remaining / 1000))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!token) return
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -149,15 +239,24 @@ export default function EmailVerifyPage() {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
if (response.ok && data.success) {
|
||||||
setResendCooldown(30)
|
setLastSentAt(Date.now(), user?.email)
|
||||||
|
setResendCooldown(Math.ceil(RESEND_INTERVAL_MS / 1000))
|
||||||
|
if (!initialEmailSent) setInitialEmailSent(true)
|
||||||
} else {
|
} else {
|
||||||
setError(data.message || 'Fehler beim Senden der E-Mail.')
|
setError(data?.message || 'Error sending the email.')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Resend email error:', error)
|
console.error('Resend email error:', error)
|
||||||
setError('Netzwerkfehler beim Senden der E-Mail.')
|
setError('Network error while sending the email.')
|
||||||
}
|
}
|
||||||
}, [resendCooldown, token])
|
}, [token, submitting, success, user, initialEmailSent])
|
||||||
|
|
||||||
|
// NEW: format seconds to m:ss
|
||||||
|
const formatMmSs = (total: number) => {
|
||||||
|
const m = Math.floor(total / 60)
|
||||||
|
const s = total % 60
|
||||||
|
return `${m}:${String(s).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
@ -202,22 +301,22 @@ export default function EmailVerifyPage() {
|
|||||||
<div className="max-w-xl mx-auto">
|
<div className="max-w-xl mx-auto">
|
||||||
<div className="text-center mb-10">
|
<div className="text-center mb-10">
|
||||||
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">
|
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">
|
||||||
E-Mail verifizieren
|
Verify your email
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-gray-300 text-sm sm:text-base">
|
<p className="mt-3 text-gray-300 text-sm sm:text-base">
|
||||||
{initialEmailSent ? (
|
{initialEmailSent ? (
|
||||||
<>
|
<>
|
||||||
Wir haben einen 6-stelligen Code an{' '}
|
We sent a 6-digit code to{' '}
|
||||||
<span className="text-indigo-300 font-medium">
|
<span className="text-indigo-300 font-medium">
|
||||||
{user?.email || 'deine E-Mail'}
|
{user?.email || 'your email'}
|
||||||
</span>{' '}
|
</span>
|
||||||
gesendet. Gib ihn unten ein.
|
. Enter it below.
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
E-Mail wird gesendet an{' '}
|
Sending verification email to{' '}
|
||||||
<span className="text-indigo-300 font-medium">
|
<span className="text-indigo-300 font-medium">
|
||||||
{user?.email || 'deine E-Mail'}
|
{user?.email || 'your email'}
|
||||||
</span>
|
</span>
|
||||||
...
|
...
|
||||||
</>
|
</>
|
||||||
@ -225,7 +324,7 @@ export default function EmailVerifyPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card */}
|
{/* Card */}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
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"
|
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"
|
||||||
@ -237,12 +336,13 @@ export default function EmailVerifyPage() {
|
|||||||
key={i}
|
key={i}
|
||||||
ref={el => { inputsRef.current[i] = el }}
|
ref={el => { inputsRef.current[i] = el }}
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
aria-label={`Code Ziffer ${i + 1}`}
|
aria-label={`Code digit ${i + 1}`}
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
maxLength={1}
|
maxLength={1}
|
||||||
value={v}
|
value={v}
|
||||||
onChange={e => handleChange(i, e.target.value)}
|
onChange={e => handleChange(i, e.target.value)}
|
||||||
onKeyDown={e => handleKeyDown(i, e)}
|
onKeyDown={e => handleKeyDown(i, e)}
|
||||||
|
onPaste={e => handlePaste(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
|
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
|
${v
|
||||||
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
|
||||||
@ -260,7 +360,7 @@ export default function EmailVerifyPage() {
|
|||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
|
<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...
|
Verified! Redirecting shortly...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -272,9 +372,9 @@ export default function EmailVerifyPage() {
|
|||||||
{submitting ? (
|
{submitting ? (
|
||||||
<>
|
<>
|
||||||
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
|
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
|
||||||
Prüfe...
|
Verifying...
|
||||||
</>
|
</>
|
||||||
) : success ? 'Verifiziert' : 'Code bestätigen'}
|
) : success ? 'Verified' : 'Confirm code'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -284,17 +384,29 @@ export default function EmailVerifyPage() {
|
|||||||
className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{resendCooldown
|
{resendCooldown
|
||||||
? `Erneut senden in ${resendCooldown}s`
|
? `Resend in ${formatMmSs(resendCooldown)}`
|
||||||
: 'Code erneut senden'}
|
: 'Resend code'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* NEW: Back to dashboard button */}
|
||||||
|
<div className="mt-1 text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push('/quickaction-dashboard')}
|
||||||
|
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:underline"
|
||||||
|
>
|
||||||
|
Back to dashboard
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{/* Helper text with validity + spam/junk reminder + support */}
|
||||||
<div className="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
|
<div className="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
|
||||||
Probleme? Prüfe deinen Spam-Ordner oder{' '}
|
Didn’t receive the email? Please check your junk/spam folder. Still having issues?{' '}
|
||||||
<span className="text-indigo-600 dark:text-indigo-400 font-medium">
|
<a href="mailto:test@test.com" className="text-indigo-600 dark:text-indigo-400 hover:underline">
|
||||||
kontaktiere den Support
|
Contact support
|
||||||
</span>
|
</a>
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user