Files
sim/apps/sim/app/(auth)/reset-password/reset-password-form.tsx
Waleed 680c9cddf0 improvement(ui): align all public pages with dark landing theme and improve whitelabeling (#3604)
* fix(sidebar): show icon-sized skeletons in collapsed settings sidebar

* fix(sidebar): hide expanded skeletons during pre-hydration in collapsed settings

* fix(sidebar): align collapsed settings skeletons with actual icon positions

* fix(sidebar): match collapsed skeleton section grouping with loaded layout

* fix(sidebar): hoist skeleton section counts to module constant

* improvement(ui): align all public pages with dark landing theme and improve whitelabeling

* fix(ui): address PR review comments - restore StatusPageLayout wrapper, improve whitelabel detection

* fix(ui): add missing dark class to 404 page, add relative positioning to invite layout

* fix(ui): fallback to white button when whitelabeled without primary color

* improvement(seo): add x-default hreflang, speakable schema to landing and blog posts

* fix(ui): fix OTP/input light-mode styles and add missing header classes on standalone auth pages

* undo hardcoded ff config

* update old components/ui usage to emcn

* fix(ui): add SupportFooter to InviteLayout, remove duplicates from unsubscribe page

* fix(ui): fix invite layout flex centering, use BrandedButton on 404 page

* fix(ui): fix OTP group styling, props spread order in BrandedButton, invite header shrink-0

* fix: enterprise hydration error

---------

Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
2026-03-16 14:02:05 -07:00

230 lines
6.8 KiB
TypeScript

'use client'
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { Input, Label } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
interface RequestResetFormProps {
email: string
onEmailChange: (email: string) => void
onSubmit: (email: string) => Promise<void>
isSubmitting: boolean
statusType: 'success' | 'error' | null
statusMessage: string
className?: string
}
export function RequestResetForm({
email,
onEmailChange,
onSubmit,
isSubmitting,
statusType,
statusMessage,
className,
}: RequestResetFormProps) {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
onSubmit(email)
}
return (
<form onSubmit={handleSubmit} className={cn('space-y-8', className)}>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='reset-email'>Email</Label>
</div>
<Input
id='reset-email'
value={email}
onChange={(e) => onEmailChange(e.target.value)}
placeholder='Enter your email'
type='email'
disabled={isSubmitting}
required
/>
<p className='text-[#999] text-sm'>
We'll send a password reset link to this email address.
</p>
</div>
{/* Status message display */}
{statusType && statusMessage && (
<div
className={cn('text-xs', statusType === 'success' ? 'text-[#4CAF50]' : 'text-red-400')}
>
<p>{statusMessage}</p>
</div>
)}
</div>
<BrandedButton
type='submit'
disabled={isSubmitting}
loading={isSubmitting}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
</form>
)
}
interface SetNewPasswordFormProps {
token: string | null
onSubmit: (password: string) => Promise<void>
isSubmitting: boolean
statusType: 'success' | 'error' | null
statusMessage: string
className?: string
}
export function SetNewPasswordForm({
token,
onSubmit,
isSubmitting,
statusType,
statusMessage,
className,
}: SetNewPasswordFormProps) {
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [validationMessage, setValidationMessage] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (password.length < 8) {
setValidationMessage('Password must be at least 8 characters long')
return
}
if (password.length > 100) {
setValidationMessage('Password must not exceed 100 characters')
return
}
if (!/[A-Z]/.test(password)) {
setValidationMessage('Password must contain at least one uppercase letter')
return
}
if (!/[a-z]/.test(password)) {
setValidationMessage('Password must contain at least one lowercase letter')
return
}
if (!/[0-9]/.test(password)) {
setValidationMessage('Password must contain at least one number')
return
}
if (!/[^A-Za-z0-9]/.test(password)) {
setValidationMessage('Password must contain at least one special character')
return
}
if (password !== confirmPassword) {
setValidationMessage('Passwords do not match')
return
}
setValidationMessage('')
onSubmit(password)
}
return (
<form onSubmit={handleSubmit} className={cn('space-y-8', className)}>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='password'>New Password</Label>
</div>
<div className='relative'>
<Input
id='password'
type={showPassword ? 'text' : 'password'}
autoCapitalize='none'
autoComplete='new-password'
autoCorrect='off'
disabled={isSubmitting || !token}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder='Enter new password'
className={cn('pr-10', validationMessage && 'border-red-500 focus:border-red-500')}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<Label htmlFor='confirmPassword'>Confirm Password</Label>
</div>
<div className='relative'>
<Input
id='confirmPassword'
type={showConfirmPassword ? 'text' : 'password'}
autoCapitalize='none'
autoComplete='new-password'
autoCorrect='off'
disabled={isSubmitting || !token}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
placeholder='Confirm new password'
className={cn('pr-10', validationMessage && 'border-red-500 focus:border-red-500')}
/>
<button
type='button'
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
>
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
{validationMessage && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
<p>{validationMessage}</p>
</div>
)}
{statusType && statusMessage && (
<div
className={cn(
'mt-1 space-y-1 text-xs',
statusType === 'success' ? 'text-[#4CAF50]' : 'text-red-400'
)}
>
<p>{statusMessage}</p>
</div>
)}
</div>
<BrandedButton
type='submit'
disabled={isSubmitting || !token}
loading={isSubmitting}
loadingText='Resetting'
>
Reset Password
</BrandedButton>
</form>
)
}