b269411823
- Giriş: satır içi doğrulama, şifre göster/gizle, beni hatırla, hoş geldin bildirimi - Kayıt: yeni hane kur / davetle katıl, /kayit?davet=KOD bağlantısı - Sunucu hataları ilgili alanda gösterilir (kayıtlı e-posta, geçersiz davet) - react-hook-form + zod, sonner bildirimleri, shadcn input/label/checkbox/card - Ortak form bileşenleri (FormField, PasswordInput, FormError) - 8 form kuralı testi (toplam 47); api.http demo hesaplara güncellendi - Faz 2 tamamlandı Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
6.0 KiB
TypeScript
169 lines
6.0 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
||
import { Home, Loader2, UserPlus, Users } from 'lucide-react'
|
||
import { Controller, useForm, useWatch } from 'react-hook-form'
|
||
import { Link, useSearchParams } from 'react-router'
|
||
import { toast } from 'sonner'
|
||
import { FormError, FormField, PasswordInput } from '@/components/form-field'
|
||
import { paths } from '@/components/layout/nav-items'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { ApiError } from '@/lib/api'
|
||
import { useRegister } from '@/lib/auth'
|
||
import { t } from '@/lib/strings'
|
||
import { cn } from '@/lib/utils'
|
||
import { AuthShell } from './auth-shell'
|
||
import { normalizeInviteCode, registerSchema, type RegisterForm } from './schemas'
|
||
|
||
const s = t.auth.register
|
||
|
||
export function RegisterPage() {
|
||
const registerAccount = useRegister()
|
||
const [params] = useSearchParams()
|
||
// Davet bağlantısı: /kayit?davet=ABCD-EFGH → "Davetle katıl" seçili ve kod dolu
|
||
const invite = params.get('davet') ?? ''
|
||
|
||
const {
|
||
register,
|
||
control,
|
||
handleSubmit,
|
||
setError,
|
||
formState: { errors, isSubmitting },
|
||
} = useForm<RegisterForm>({
|
||
resolver: zodResolver(registerSchema),
|
||
mode: 'onTouched',
|
||
defaultValues: {
|
||
name: '',
|
||
email: '',
|
||
password: '',
|
||
mode: invite ? 'join' : 'new',
|
||
household_name: '',
|
||
invite_code: invite,
|
||
},
|
||
})
|
||
const mode = useWatch({ control, name: 'mode' })
|
||
const name = useWatch({ control, name: 'name' }).trim()
|
||
|
||
const onSubmit = handleSubmit(async (v) => {
|
||
try {
|
||
const me = await registerAccount.mutateAsync({
|
||
name: v.name,
|
||
email: v.email,
|
||
password: v.password,
|
||
...(v.mode === 'join'
|
||
? { invite_code: normalizeInviteCode(v.invite_code) }
|
||
: { household_name: v.household_name || undefined }),
|
||
})
|
||
toast.success(v.mode === 'join' ? s.joined(me.household.name) : s.welcome(me.household.name))
|
||
} catch (e) {
|
||
if (!(e instanceof ApiError)) return setError('root', { message: String(e) })
|
||
// Sunucu hatasını ilgili alana bağla
|
||
if (e.code === 'conflict') setError('email', { message: e.message }, { shouldFocus: true })
|
||
else if (v.mode === 'join' && e.code === 'bad_request' && e.message.includes('Davet'))
|
||
setError('invite_code', { message: e.message }, { shouldFocus: true })
|
||
else setError('root', { message: e.message })
|
||
}
|
||
})
|
||
|
||
const donus = params.get('donus')
|
||
const loginLink = donus ? `${paths.login}?donus=${encodeURIComponent(donus)}` : paths.login
|
||
|
||
return (
|
||
<AuthShell
|
||
title={s.title}
|
||
description={s.description}
|
||
footer={
|
||
<>
|
||
{s.hasAccount}{' '}
|
||
<Link to={loginLink} className="font-medium text-primary underline-offset-4 hover:underline">
|
||
{s.toLogin}
|
||
</Link>
|
||
</>
|
||
}
|
||
>
|
||
<form onSubmit={onSubmit} noValidate className="space-y-5">
|
||
<FormField label={s.name} error={errors.name}>
|
||
{(f) => <Input {...f} {...register('name')} autoComplete="given-name" autoFocus />}
|
||
</FormField>
|
||
|
||
<FormField label={s.email} error={errors.email}>
|
||
{(f) => (
|
||
<Input
|
||
{...f}
|
||
{...register('email')}
|
||
type="email"
|
||
inputMode="email"
|
||
autoComplete="email"
|
||
autoCapitalize="none"
|
||
/>
|
||
)}
|
||
</FormField>
|
||
|
||
<FormField label={s.password} error={errors.password} hint={s.passwordHint}>
|
||
{(f) => <PasswordInput {...f} {...register('password')} autoComplete="new-password" />}
|
||
</FormField>
|
||
|
||
{/* Yeni hane mi, davetle katılma mı? */}
|
||
<fieldset className="space-y-3">
|
||
<legend className="mb-2 text-sm font-medium">{s.modeLabel}</legend>
|
||
<Controller
|
||
control={control}
|
||
name="mode"
|
||
render={({ field }) => (
|
||
<div role="radiogroup" className="grid grid-cols-2 gap-2 rounded-lg border bg-muted p-1">
|
||
{(
|
||
[
|
||
['new', s.modeNew, Home],
|
||
['join', s.modeJoin, Users],
|
||
] as const
|
||
).map(([value, label, Icon]) => (
|
||
<button
|
||
key={value}
|
||
type="button"
|
||
role="radio"
|
||
aria-checked={field.value === value}
|
||
onClick={() => field.onChange(value)}
|
||
className={cn(
|
||
'flex min-h-11 items-center justify-center gap-2 rounded-md text-sm font-medium text-muted-foreground transition-colors',
|
||
field.value === value && 'bg-background text-foreground shadow-sm',
|
||
)}
|
||
>
|
||
<Icon className="size-4" aria-hidden />
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
/>
|
||
|
||
{mode === 'new' ? (
|
||
<FormField label={s.householdName} error={errors.household_name} hint={s.householdHint(name)}>
|
||
{(f) => <Input {...f} {...register('household_name')} placeholder={name ? `${name} Hanesi` : ''} />}
|
||
</FormField>
|
||
) : (
|
||
<FormField label={s.inviteCode} error={errors.invite_code} hint={s.inviteHint}>
|
||
{(f) => (
|
||
<Input
|
||
{...f}
|
||
{...register('invite_code')}
|
||
autoCapitalize="characters"
|
||
autoComplete="off"
|
||
spellCheck={false}
|
||
maxLength={12}
|
||
className="font-mono tracking-widest uppercase"
|
||
/>
|
||
)}
|
||
</FormField>
|
||
)}
|
||
</fieldset>
|
||
|
||
<FormError message={errors.root?.message} />
|
||
|
||
<Button type="submit" size="lg" className="w-full" disabled={isSubmitting}>
|
||
{isSubmitting ? <Loader2 className="animate-spin" /> : <UserPlus />}
|
||
{isSubmitting ? s.submitting : s.submit}
|
||
</Button>
|
||
</form>
|
||
</AuthShell>
|
||
)
|
||
}
|