Public pages (Login, Register, Forgot/Reset Password, Verify Email, Survey Thank You) get descriptions for SEO. Authenticated pages (Dashboard, Flow Library, My Flows, Session History, AI Assistant, Account Settings, Step Library, My Shares, Feedback, Guides) get proper tab titles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
191 lines
7.0 KiB
TypeScript
191 lines
7.0 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Link, useSearchParams, useNavigate } from 'react-router-dom'
|
|
import { authApi } from '@/api/auth'
|
|
import { toast } from '@/lib/toast'
|
|
import { BrandLogo } from '@/components/common/BrandLogo'
|
|
import { PasswordInput } from '@/components/common/PasswordInput'
|
|
import { PageMeta } from '@/components/common/PageMeta'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
export function ResetPasswordPage() {
|
|
const [searchParams] = useSearchParams()
|
|
const navigate = useNavigate()
|
|
const token = searchParams.get('token') || ''
|
|
|
|
const [verifying, setVerifying] = useState(true)
|
|
const [valid, setValid] = useState(false)
|
|
const [email, setEmail] = useState<string | null>(null)
|
|
|
|
const [newPassword, setNewPassword] = useState('')
|
|
const [confirmPassword, setConfirmPassword] = useState('')
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (!token) {
|
|
setVerifying(false)
|
|
return
|
|
}
|
|
authApi.verifyResetToken(token).then((res) => {
|
|
setValid(res.valid)
|
|
setEmail(res.email || null)
|
|
}).catch(() => {
|
|
setValid(false)
|
|
}).finally(() => {
|
|
setVerifying(false)
|
|
})
|
|
}, [token])
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
|
|
if (!newPassword || !confirmPassword) {
|
|
setError('Please fill in all fields')
|
|
return
|
|
}
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
setError('Passwords do not match')
|
|
return
|
|
}
|
|
|
|
if (newPassword.length < 10) {
|
|
setError('Password must be at least 10 characters')
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
try {
|
|
await authApi.resetPassword(token, newPassword)
|
|
toast.success('Password reset successfully. Please sign in.')
|
|
navigate('/login', { replace: true })
|
|
} catch (err: unknown) {
|
|
if (err && typeof err === 'object' && 'response' in err) {
|
|
const axiosErr = err as { response?: { data?: { detail?: string } } }
|
|
setError(axiosErr.response?.data?.detail || 'Failed to reset password')
|
|
} else {
|
|
setError('Failed to reset password')
|
|
}
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageMeta title="Reset Password" description="Set a new password for your ResolutionFlow account" />
|
|
<div className="flex min-h-screen items-center justify-center bg-black px-4">
|
|
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
|
|
|
|
<div className="relative w-full max-w-md space-y-8">
|
|
<div className="text-center">
|
|
<div className="mb-4 flex justify-center sm:mb-6">
|
|
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
|
|
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
|
|
</div>
|
|
</div>
|
|
<h1 className="text-3xl font-bold font-heading text-foreground tracking-tight">
|
|
Reset Password
|
|
</h1>
|
|
</div>
|
|
|
|
{verifying ? (
|
|
<div className="bg-card border border-border rounded-xl p-6 text-center">
|
|
<p className="text-muted-foreground">Verifying reset link...</p>
|
|
</div>
|
|
) : !token || !valid ? (
|
|
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
|
|
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-4 text-sm text-red-400">
|
|
This reset link is invalid or has expired. Please request a new one.
|
|
</div>
|
|
<div className="text-center">
|
|
<Link
|
|
to="/forgot-password"
|
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
Request new reset link
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
|
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
|
|
{email && (
|
|
<p className="text-sm text-muted-foreground">
|
|
Resetting password for <span className="font-medium text-foreground">{email}</span>
|
|
</p>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-foreground">
|
|
New Password
|
|
</label>
|
|
<PasswordInput
|
|
id="new-password"
|
|
autoComplete="new-password"
|
|
required
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
className={cn(
|
|
'block w-full rounded-xl border border-border bg-card px-3 py-2',
|
|
'text-foreground placeholder:text-muted-foreground',
|
|
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
|
'transition-colors'
|
|
)}
|
|
placeholder="At least 10 characters"
|
|
/>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
Must include uppercase, lowercase, and a digit.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-foreground">
|
|
Confirm New Password
|
|
</label>
|
|
<PasswordInput
|
|
id="confirm-password"
|
|
autoComplete="new-password"
|
|
required
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
className={cn(
|
|
'block w-full rounded-xl border border-border bg-card px-3 py-2',
|
|
'text-foreground placeholder:text-muted-foreground',
|
|
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
|
|
'transition-colors'
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className={cn(
|
|
'w-full rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
|
|
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
|
|
'focus:outline-hidden focus:ring-2 focus:ring-primary/30 focus:ring-offset-2 focus:ring-offset-black',
|
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
'transition-all'
|
|
)}
|
|
>
|
|
{isLoading ? 'Resetting...' : 'Reset Password'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default ResetPasswordPage
|