Files
kakeibo/frontend/src/components/form-field.tsx
T
suphicakir e188922c0f feat(kayıt): yeni hane kaydı yalnızca davetle (REGISTRATION=invite_only)
- REGISTRATION ortam değişkeni: invite_only (varsayılan) | open
- invite_only: ilk hane serbest, sonrasında yalnızca davet koduyla katılım
- Backend'de zorunlu: 403 registration_closed; GET /api/auth/registration
- Kayıt ekranı: kayıt kapalıyken yalnızca davet kodu ve açıklama
- Entegrasyon testleri: kayıt politikası (2)
- Prettier ayarı eklendi (proje stiliyle uyumlu), kaynaklar biçimlendirildi

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 01:04:48 +03:00

90 lines
2.5 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 { 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>
)
}