77d7a081b3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
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>
|
||
)
|
||
}
|