feat(ayarlar): hane ve üyeler, davet bağlantısı, hesap ve şifre, bütçe varsayılanları
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import { FormError, FormField, PasswordInput } from '@/components/form-field'
|
||||
import { LogoutButton } from '@/components/layout/app-layout'
|
||||
import { PageLoading } from '@/components/page'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Section } from '@/features/planning/plan-read'
|
||||
import { useMe } from '@/lib/auth'
|
||||
import { useChangePassword, useUpdateProfile } from '@/lib/household'
|
||||
import { t } from '@/lib/strings'
|
||||
import type { MeResponse } from '@/types/MeResponse'
|
||||
import { nameSchema, passwordSchema, type NameForm, type PasswordForm } from './schemas'
|
||||
import { SettingsSubpage } from './subpage'
|
||||
|
||||
const s = t.settings.account
|
||||
const message = (e: unknown) => (e instanceof Error ? e.message : String(e))
|
||||
|
||||
/** Ayarlar → Hesabım: ad, e-posta (salt okunur), şifre değiştirme, çıkış */
|
||||
export function AccountPage() {
|
||||
const me = useMe()
|
||||
return (
|
||||
<SettingsSubpage title={s.title} description={s.description}>
|
||||
{!me.data ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
<div className="max-w-xl space-y-4">
|
||||
<Profile me={me.data} />
|
||||
<ChangePassword />
|
||||
<div className="flex justify-end">
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SettingsSubpage>
|
||||
)
|
||||
}
|
||||
|
||||
function Profile({ me }: { me: MeResponse }) {
|
||||
const update = useUpdateProfile()
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
reset,
|
||||
formState: { errors, isDirty },
|
||||
} = useForm<NameForm>({
|
||||
resolver: zodResolver(nameSchema),
|
||||
mode: 'onTouched',
|
||||
defaultValues: { name: me.user.name },
|
||||
})
|
||||
const onSubmit = handleSubmit(async (v) => {
|
||||
try {
|
||||
const next = await update.mutateAsync(v.name)
|
||||
reset({ name: next.user.name })
|
||||
toast.success(s.saved)
|
||||
} catch (e) {
|
||||
setError('name', { message: message(e) })
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Section title={s.profile}>
|
||||
<form onSubmit={onSubmit} className="space-y-4" noValidate>
|
||||
<FormField label={s.name} error={errors.name}>
|
||||
{(f) => <Input {...f} {...register('name')} autoComplete="name" maxLength={80} />}
|
||||
</FormField>
|
||||
<FormField label={s.email} hint={s.emailHint}>
|
||||
{(f) => <Input {...f} value={me.user.email} readOnly disabled />}
|
||||
</FormField>
|
||||
<p className="text-sm">
|
||||
<span className="text-muted-foreground">{s.role}: </span>
|
||||
{me.user.role === 'owner' ? t.account.owner : t.account.member} · {me.household.name}
|
||||
</p>
|
||||
<Button type="submit" disabled={!isDirty || update.isPending}>
|
||||
{update.isPending && <Loader2 className="animate-spin" aria-hidden />}
|
||||
{s.save}
|
||||
</Button>
|
||||
</form>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function ChangePassword() {
|
||||
const change = useChangePassword()
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<PasswordForm>({
|
||||
resolver: zodResolver(passwordSchema),
|
||||
mode: 'onTouched',
|
||||
defaultValues: { current_password: '', new_password: '', repeat: '' },
|
||||
})
|
||||
const onSubmit = handleSubmit(async (v) => {
|
||||
try {
|
||||
await change.mutateAsync({
|
||||
current_password: v.current_password,
|
||||
new_password: v.new_password,
|
||||
})
|
||||
reset()
|
||||
toast.success(s.changed)
|
||||
} catch (e) {
|
||||
const msg = message(e)
|
||||
// "Mevcut şifre hatalı" → ilgili alana; diğerleri formun altına
|
||||
if (/mevcut/i.test(msg)) setError('current_password', { message: msg }, { shouldFocus: true })
|
||||
else setError('root', { message: msg })
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Section title={s.password}>
|
||||
<form onSubmit={onSubmit} className="space-y-4" noValidate>
|
||||
<p className="text-sm text-muted-foreground">{s.otherDevices}</p>
|
||||
{/* Parola yöneticileri için gizli kullanıcı adı alanı */}
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
className="hidden"
|
||||
readOnly
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<FormField label={s.current} error={errors.current_password}>
|
||||
{(f) => (
|
||||
<PasswordInput
|
||||
{...f}
|
||||
{...register('current_password')}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField label={s.next} error={errors.new_password} hint={s.nextHint}>
|
||||
{(f) => (
|
||||
<PasswordInput {...f} {...register('new_password')} autoComplete="new-password" />
|
||||
)}
|
||||
</FormField>
|
||||
<FormField label={s.repeat} error={errors.repeat}>
|
||||
{(f) => <PasswordInput {...f} {...register('repeat')} autoComplete="new-password" />}
|
||||
</FormField>
|
||||
<FormError message={errors.root?.message} />
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="animate-spin" aria-hidden />}
|
||||
{s.change}
|
||||
</Button>
|
||||
</form>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user