My-Weekly-ToDo-List/src/app/auth/verify-email/page.tsx
mARTin dea7c387ea v1.7.0: Responsive mobile layout + boot update script
- Add responsive CSS breakpoints (480px/768px/1024px)
- Auto-adjust viewDays based on screen width
- Keep time column visible on mobile (compact)
- Add boot-update.sh for auto-update on reboot
- Fix Suspense boundary in verify-email page
- Bump version to 1.7.0
2026-02-27 13:03:25 +01:00

280 lines
8.5 KiB
TypeScript

'use client';
import React, { useState, useRef, useEffect, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
function VerifyEmailContent() {
const router = useRouter();
const searchParams = useSearchParams();
const email = searchParams.get('email') || '';
const error = searchParams.get('error');
const [code, setCode] = useState(['', '', '', '', '', '']);
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [isResending, setIsResending] = useState(false);
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
useEffect(() => {
if (error === 'invalid_token') {
setErrorMsg('Verification link is invalid or expired. Please enter your code manually or request a new one.');
} else if (error === 'missing_token') {
setErrorMsg('Invalid verification link.');
} else if (error === 'server_error') {
setErrorMsg('Something went wrong. Please try again.');
}
}, [error]);
// Auto-focus first input
useEffect(() => {
inputRefs.current[0]?.focus();
}, []);
const handleInput = (index: number, value: string) => {
// Handle paste of full code
if (value.length > 1) {
const digits = value.replace(/\D/g, '').slice(0, 6).split('');
const newCode = [...code];
digits.forEach((digit, i) => {
if (index + i < 6) newCode[index + i] = digit;
});
setCode(newCode);
const nextIndex = Math.min(index + digits.length, 5);
inputRefs.current[nextIndex]?.focus();
// Auto-submit if all 6 digits entered
if (newCode.every(d => d !== '')) {
submitCode(newCode.join(''));
}
return;
}
const digit = value.replace(/\D/g, '');
const newCode = [...code];
newCode[index] = digit;
setCode(newCode);
if (digit && index < 5) {
inputRefs.current[index + 1]?.focus();
}
// Auto-submit if all 6 digits entered
if (newCode.every(d => d !== '')) {
submitCode(newCode.join(''));
}
};
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
if (e.key === 'Backspace' && !code[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const submitCode = async (fullCode: string) => {
setIsLoading(true);
setErrorMsg(null);
setMessage(null);
try {
const response = await fetch('/api/auth/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, code: fullCode }),
});
const data = await response.json();
if (!response.ok) {
setErrorMsg(data.error || 'Verification failed');
setCode(['', '', '', '', '', '']);
inputRefs.current[0]?.focus();
setIsLoading(false);
return;
}
setMessage('Email verified! Redirecting to login...');
setTimeout(() => {
router.push('/auth/login?verified=true');
}, 1500);
} catch {
setErrorMsg('Something went wrong. Please try again.');
setIsLoading(false);
}
};
const handleResend = async () => {
setIsResending(true);
setErrorMsg(null);
setMessage(null);
try {
const response = await fetch('/api/auth/resend-verification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await response.json();
if (!response.ok) {
setErrorMsg(data.error || 'Failed to resend code');
} else {
setMessage('A new verification code has been sent to your email.');
}
} catch {
setErrorMsg('Failed to resend code. Please try again.');
} finally {
setIsResending(false);
}
};
return (
<div className="weekly-auth-container">
<div className="weekly-auth-card">
{/* Logo */}
<div className="weekly-auth-logo">
My Weekly ToDo&apos;s
</div>
{/* Title */}
<h2 style={{ color: '#333', fontSize: '1.25rem', fontWeight: 600, textAlign: 'center', margin: '0 0 8px' }}>
Verify your email
</h2>
<p style={{ color: '#666', fontSize: '0.875rem', textAlign: 'center', margin: '0 0 24px' }}>
We sent a 6-digit code to <strong style={{ color: '#667eea' }}>{email}</strong>
</p>
{/* Messages */}
{errorMsg && (
<div className="weekly-auth-error">
{errorMsg}
</div>
)}
{message && (
<div style={{
background: 'rgba(52, 211, 153, 0.1)',
border: '1px solid rgba(52, 211, 153, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#059669',
fontSize: '0.875rem',
marginBottom: '16px',
textAlign: 'center',
}}>
{message}
</div>
)}
{/* Code Input */}
<div style={{
display: 'flex',
gap: '8px',
justifyContent: 'center',
marginBottom: '24px',
}}>
{code.map((digit, index) => (
<input
key={index}
ref={el => { inputRefs.current[index] = el; }}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={e => handleInput(index, e.target.value)}
onKeyDown={e => handleKeyDown(index, e)}
onPaste={e => {
e.preventDefault();
const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
if (pasted) handleInput(index, pasted);
}}
disabled={isLoading}
style={{
width: '48px',
height: '56px',
textAlign: 'center',
fontSize: '1.5rem',
fontWeight: 700,
fontFamily: "'Courier New', monospace",
background: '#f8f9fa',
border: digit ? '2px solid #667eea' : '2px solid #d0d5dd',
borderRadius: '10px',
color: '#667eea',
outline: 'none',
transition: 'border-color 0.2s, box-shadow 0.2s',
caretColor: '#667eea',
boxSizing: 'border-box',
}}
onFocus={e => {
e.target.style.borderColor = '#667eea';
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.15)';
}}
onBlur={e => {
e.target.style.borderColor = digit ? '#667eea' : '#d0d5dd';
e.target.style.boxShadow = 'none';
}}
/>
))}
</div>
{/* Verify Button */}
<button
onClick={() => submitCode(code.join(''))}
disabled={isLoading || code.some(d => d === '')}
className="weekly-auth-button primary"
style={{ marginBottom: '16px' }}
>
{isLoading ? 'Verifying...' : 'Verify Email'}
</button>
{/* Resend */}
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<span style={{ color: '#666', fontSize: '0.875rem' }}>
Didn&apos;t receive the code?{' '}
</span>
<button
onClick={handleResend}
disabled={isResending}
style={{
background: 'none',
border: 'none',
color: '#667eea',
cursor: 'pointer',
fontSize: '0.875rem',
fontWeight: 500,
textDecoration: 'underline',
opacity: isResending ? 0.5 : 1,
}}
>
{isResending ? 'Sending...' : 'Resend code'}
</button>
</div>
{/* Back to login */}
<div className="weekly-auth-links">
<Link href="/auth/login" className="weekly-auth-link">
Back to login
</Link>
</div>
</div>
<footer className="weekly-auth-footer">
<p>Simple. Beautiful. Yours.</p>
</footer>
</div>
);
}
export default function VerifyEmailPage() {
return (
<Suspense fallback={
<div className="weekly-auth-container">
<div className="weekly-auth-card">
<div className="weekly-auth-logo">My Weekly ToDo&apos;s</div>
<p style={{ textAlign: 'center', color: '#666' }}>Loading...</p>
</div>
</div>
}>
<VerifyEmailContent />
</Suspense>
);
}