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>
This commit is contained in:
2026-11-12 00:52:13 +03:00
parent c6d4c30461
commit b269411823
20 changed files with 886 additions and 62 deletions
+75
View File
@@ -0,0 +1,75 @@
import { useId, useState, type ComponentProps, type ReactNode } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import type { FieldError } from 'react-hook-form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
/**
* Etiket + giriş alanı + (varsa) hata/ipucu metni.
* Erişilebilirlik: hata `aria-invalid` ve `aria-describedby` ile alana bağlanır,
* ekran okuyucu alanı odaklayınca hatayı okur.
*/
export function FormField({
label,
error,
hint,
className,
children,
}: {
label: string
error?: FieldError
hint?: string
className?: string
/** Alanı üreten fonksiyon: kimlik ve aria özelliklerini alır */
children: (field: { id: string; 'aria-invalid': boolean; 'aria-describedby'?: string }) => ReactNode
}) {
const id = useId()
const messageId = `${id}-message`
const message = error?.message ?? hint
return (
<div className={cn('space-y-2', className)}>
<Label htmlFor={id}>{label}</Label>
{children({
id,
'aria-invalid': !!error,
'aria-describedby': message ? messageId : undefined,
})}
{message && (
<p id={messageId} className={cn('text-sm', error ? 'text-expense' : 'text-muted-foreground')}>
{message}
</p>
)}
</div>
)
}
/** Şifre alanı: göster/gizle düğmeli. */
export function PasswordInput(props: ComponentProps<typeof Input>) {
const [visible, setVisible] = useState(false)
return (
<div className="relative">
<Input {...props} type={visible ? 'text' : 'password'} className={cn('pr-12', props.className)} />
<button
type="button"
onClick={() => setVisible((v) => !v)}
className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-lg text-muted-foreground hover:text-foreground"
aria-label={visible ? 'Şifreyi gizle' : 'Şifreyi göster'}
aria-pressed={visible}
>
{visible ? <EyeOff className="size-5" /> : <Eye className="size-5" />}
</button>
</div>
)
}
/** Sunucudan gelen, belirli bir alana ait olmayan hata (ör. "E-posta veya şifre hatalı"). */
export function FormError({ message }: { message?: string }) {
if (!message) return null
return (
<p role="alert" className="rounded-lg border border-expense/30 bg-expense/10 px-3 py-2.5 text-sm text-expense">
{message}
</p>
)
}