profit-planet-frontend/src/app/password-reset/page.tsx
2026-01-18 16:39:55 +01:00

369 lines
15 KiB
TypeScript

'use client'
import { useState, useEffect, Suspense } from 'react' // CHANGED: add Suspense
import { useSearchParams, useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout'
import Waves from '../components/background/waves'
import { ToastProvider, useToast } from '../components/toast/toastComponent'
function PasswordResetPageInner() {
const searchParams = useSearchParams()
const router = useRouter()
const token = searchParams.get('token')
// Email request state
const [email, setEmail] = useState('')
const [requestLoading, setRequestLoading] = useState(false)
const [requestSuccess, setRequestSuccess] = useState(false)
const [requestError, setRequestError] = useState('')
// Reset-with-token state
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [resetLoading, setResetLoading] = useState(false)
const [resetSuccess, setResetSuccess] = useState(false)
const [resetError, setResetError] = useState('')
const { showToast } = useToast()
// Basic validators
const validEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
const validPassword = (val: string) =>
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(val)
// Auto-redirect after successful password reset
useEffect(() => {
if (resetSuccess) {
const t = setTimeout(() => router.push('/login'), 1800)
return () => clearTimeout(t)
}
}, [resetSuccess, router])
const handleRequestSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (requestLoading) return
if (!validEmail(email)) {
const msg = 'Please enter a valid email address.'
setRequestError(msg)
showToast({
variant: 'error',
title: 'Invalid email',
message: msg,
})
return
}
setRequestError('')
setRequestLoading(true)
try {
// TODO: call API endpoint: POST /auth/password-reset/request
await new Promise(r => setTimeout(r, 1100))
setRequestSuccess(true)
showToast({
variant: 'success',
title: 'Password reset email',
message: 'If this email exists, a reset link has been sent.',
})
} catch {
const msg = 'Request failed. Please try again.'
setRequestError(msg)
showToast({
variant: 'error',
title: 'Request failed',
message: msg,
})
} finally {
setRequestLoading(false)
}
}
const handleResetSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (resetLoading) return
if (!validPassword(password)) {
const msg = 'Password does not meet the requirements.'
setResetError(msg)
showToast({
variant: 'error',
title: 'Invalid password',
message: msg,
})
return
}
if (password !== confirmPassword) {
const msg = 'Passwords do not match.'
setResetError(msg)
showToast({
variant: 'error',
title: 'Passwords do not match',
message: msg,
})
return
}
setResetError('')
setResetLoading(true)
try {
// TODO: call API endpoint: POST /auth/password-reset/confirm { token, password }
await new Promise(r => setTimeout(r, 1200))
setResetSuccess(true)
showToast({
variant: 'success',
title: 'Password updated',
message: 'Your password has been changed. Redirecting to login...',
})
} catch {
const msg = 'Reset failed. Please try again.'
setResetError(msg)
showToast({
variant: 'error',
title: 'Reset failed',
message: msg,
})
} finally {
setResetLoading(false)
}
}
const passwordHints = [
{ label: 'At least 8 characters', pass: password.length >= 8 },
{ label: 'Uppercase letter (A-Z)', pass: /[A-Z]/.test(password) },
{ label: 'Lowercase letter (a-z)', pass: /[a-z]/.test(password) },
{ label: 'Number (0-9)', pass: /\d/.test(password) },
{ label: 'Special character (!@#$...)', pass: /[\W_]/.test(password) }
]
return (
<PageLayout>
<div
className="relative w-full flex flex-col min-h-screen overflow-hidden"
style={{ backgroundImage: 'none', background: 'none' }}
>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
{/* push content a bit further down while still centering */}
<main className="relative z-10 flex flex-col flex-1 items-center justify-center pt-32 sm:pt-0 pb-8 sm:pb-10">
{/* Widened container to match header */}
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
{/* Translucent form card (matching login glass style) */}
<div
className="mx-auto w-full max-w-3xl rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-10 md:py-12 md:px-14"
style={{
backgroundColor: 'rgba(255,255,255,0.55)',
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative">
<div className="mx-auto max-w-2xl text-center mb-8">
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
Reset password
</h1>
<p className="mt-3 text-slate-700 text-base sm:text-lg">
{!token
? 'Request a link to reset your password.'
: 'Set a new secure password.'}
</p>
</div>
{!token && (
<form onSubmit={handleRequestSubmit} className="space-y-6">
<div>
<label className="block text-sm font-semibold text-[#0F172A] mb-2" htmlFor="email">
Email address
</label>
<input
id="email"
type="email"
value={email}
onChange={e => { setEmail(e.target.value); setRequestError(''); setRequestSuccess(false)}}
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="your.email@example.com"
required
/>
</div>
{requestError && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{requestError}
</div>
)}
{requestSuccess && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Email sent (if the address exists). Please check your inbox.
</div>
)}
<button
type="submit"
disabled={requestLoading}
className={`w-full flex items-center justify-center rounded-xl px-6 py-3 text-base font-semibold transition-all duration-200 transform hover:-translate-y-0.5 border ${
requestLoading
? 'border-white/30 bg-white/20 text-slate-300 cursor-wait'
: 'border-white/55 bg-white/30 text-[#0F172A] shadow-[0_10px_30px_rgba(15,23,42,0.45)] hover:bg-white/40 hover:shadow-[0_16px_40px_rgba(15,23,42,0.6)] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80'
}`}
>
{requestLoading ? (
<>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
Senden...
</>
) : (
'Request reset link'
)}
</button>
<div className="text-center text-sm text-gray-700">
Remember it now?{' '}
<button
type="button"
onClick={() => router.push('/login')}
className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline font-medium"
>
Back to login
</button>
</div>
</form>
)}
{token && (
<form onSubmit={handleResetSubmit} className="space-y-6">
<div className="grid gap-6 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="password">
New password
</label>
<div className="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={e => { setPassword(e.target.value); setResetError(''); setResetSuccess(false)}}
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 pr-12 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="Your new password"
required
/>
<button
type="button"
onClick={() => setShowPassword(p => !p)}
className="absolute inset-y-0 right-0 px-3 text-xs font-medium text-indigo-700 hover:underline"
>
{showPassword ? 'Hide' : 'Show'}
</button>
</div>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-xs">
{passwordHints.map(h => (
<div
key={h.label}
className={`flex items-center gap-2 ${
h.pass ? 'text-green-600' : 'text-gray-500'
}`}
>
<span className={`inline-block size-2 rounded-full ${h.pass ? 'bg-green-500' : 'bg-gray-400'}`} />
{h.label}
</div>
))}
</div>
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="confirm">
Confirm password
</label>
<input
id="confirm"
type={showPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={e => { setConfirmPassword(e.target.value); setResetError(''); setResetSuccess(false)}}
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="Confirm password"
required
/>
{confirmPassword && password !== confirmPassword && (
<p className="mt-2 text-xs text-red-500">Passwords do not match.</p>
)}
</div>
</div>
{resetError && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">
{resetError}
</div>
)}
{resetSuccess && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Password saved. Redirecting to login...
</div>
)}
<button
type="submit"
disabled={resetLoading}
className={`w-full flex items-center justify-center rounded-xl px-6 py-3 text-base font-semibold transition-all duration-200 transform hover:-translate-y-0.5 border ${
resetLoading
? 'border-white/30 bg-white/20 text-slate-300 cursor-wait'
: 'border-white/55 bg-white/30 text-[#0F172A] shadow-[0_10px_30px_rgba(15,23,42,0.45)] hover:bg-white/40 hover:shadow-[0_16px_40px_rgba(15,23,42,0.6)] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80'
}`}
>
{resetLoading ? (
<>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
Saving...
</>
) : (
'Set new password'
)}
</button>
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
Link expired?{' '}
<button
type="button"
onClick={() => router.push('/password-reset')}
className="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
>
Request again
</button>
</div>
</form>
)}
</div>
</div>
</div>
</main>
</div>
</PageLayout>
)
}
export default function PasswordResetPage() {
return (
<ToastProvider>
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-[#8D6B1D] mx-auto mb-3" />
<p className="text-[#4A4A4A]">Loading...</p>
</div>
</div>
}
>
<PasswordResetPageInner />
</Suspense>
</ToastProvider>
)
}