Files
kakeibo/frontend/src/features/settings/account-page.tsx
T

152 lines
5.0 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 { 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>
)
}