mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
* 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>
230 lines
6.8 KiB
TypeScript
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>
|
|
)
|
|
}
|