Files
kakeibo/frontend/src/features/auth/register-page.tsx
T
suphicakir b269411823 feat(giriş): giriş ve kayıt ekranları
- 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>
2026-11-12 00:52:13 +03:00

169 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}