e188922c0f
- 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>
90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
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>
|
||
)
|
||
}
|