Files
TeleWave/frontend/src/routes/settings.tsx
T
Leonid PershinandClaude Opus 5 0606ea3e6e Add Prettier to the frontend and gate formatting in CI
Форматтера у фронтенда не было: стиль держался вручную и успел разъехаться в 50 файлах.
Ставим Prettier с настройками под уже сложившийся стиль (без точек с запятой, одинарные кавычки,
ширина 100 — подобрана замером: при 100 расходится меньше файлов, чем при 96 или 110) и
прогоняем его по коду. `src/routeTree.gen.ts` исключён — его переписывает плагин роутера.

Чтобы форматирование больше не расходилось незаметно, добавлены проверки в CI: `csharpier check`
для бэкенда (его отсутствие и позволило накопиться 79 неотформатированным файлам) и
`prettier --check` для фронтенда. Версии форматтеров прибиты точно, без кареток: минорка меняет
вывод и красит CI на файлах, которых никто не трогал.

`.editorconfig` задаёт редакторам те же отступы и LF ещё до форматтера; значения совпадают с
настройками csharpier и Prettier намеренно — оба его читают.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:38:12 +03:00

149 lines
4.8 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { z } from 'zod'
import { changePassword, changeUserName, clearSession, deleteAccount } from '@/features/auth/api'
import { useAuthStore } from '@/features/auth/store'
import { useRequireAuth } from '@/features/auth/guards'
import { HttpError } from '@/shared/api/client'
import { Button } from '@/shared/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
import { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label'
import { toast } from '@/shared/ui/toast-store'
export const Route = createFileRoute('/settings')({ component: SettingsPage })
const userNameSchema = z.object({ newUserName: z.string().min(3).max(64) })
const passwordSchema = z.object({
currentPassword: z.string().min(1),
newPassword: z.string().min(8),
})
function SettingsPage() {
const { t } = useTranslation()
const { isReady } = useRequireAuth()
const navigate = useNavigate()
const userNameForm = useForm<z.infer<typeof userNameSchema>>({
resolver: zodResolver(userNameSchema),
})
const passwordForm = useForm<z.infer<typeof passwordSchema>>({
resolver: zodResolver(passwordSchema),
})
if (!isReady) return null
const onSaveUserName = async (values: z.infer<typeof userNameSchema>) => {
try {
await changeUserName(values.newUserName)
useAuthStore
.getState()
.setUser({ ...useAuthStore.getState().user!, userName: values.newUserName })
toast.success(t('settings.saved'))
userNameForm.reset()
} catch (error) {
toast.error(
error instanceof HttpError && error.status === 409
? t('auth.userNameTaken')
: t('common.error'),
)
}
}
const onSavePassword = async (values: z.infer<typeof passwordSchema>) => {
try {
await changePassword(values.currentPassword, values.newPassword)
toast.success(t('settings.saved'))
passwordForm.reset()
} catch {
toast.error(t('common.error'))
}
}
const onDeleteAccount = async () => {
if (!window.confirm(t('settings.deleteAccountConfirm'))) return
try {
await deleteAccount()
clearSession()
void navigate({ to: '/' })
} catch {
toast.error(t('common.error'))
}
}
return (
<div className="flex flex-col gap-6">
<h1 className="crt-glow text-2xl font-bold">{t('settings.title')}</h1>
<Card>
<CardHeader>
<CardTitle>{t('settings.changeUserName')}</CardTitle>
</CardHeader>
<CardContent>
<form
className="flex flex-col gap-4"
onSubmit={userNameForm.handleSubmit(onSaveUserName)}
>
<div className="flex flex-col gap-1.5">
<Label htmlFor="newUserName">{t('settings.newUserName')}</Label>
<Input id="newUserName" {...userNameForm.register('newUserName')} />
</div>
<Button
type="submit"
className="self-start"
disabled={userNameForm.formState.isSubmitting}
>
{t('common.save')}
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('settings.changePassword')}</CardTitle>
</CardHeader>
<CardContent>
<form
className="flex flex-col gap-4"
onSubmit={passwordForm.handleSubmit(onSavePassword)}
>
<div className="flex flex-col gap-1.5">
<Label htmlFor="currentPassword">{t('settings.currentPassword')}</Label>
<Input
id="currentPassword"
type="password"
{...passwordForm.register('currentPassword')}
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="newPassword">{t('settings.newPassword')}</Label>
<Input id="newPassword" type="password" {...passwordForm.register('newPassword')} />
</div>
<Button
type="submit"
className="self-start"
disabled={passwordForm.formState.isSubmitting}
>
{t('common.save')}
</Button>
</form>
</CardContent>
</Card>
<Card className="border-red-700/40">
<CardHeader>
<CardTitle className="text-red-500">{t('settings.dangerZone')}</CardTitle>
</CardHeader>
<CardContent>
<Button variant="destructive" onClick={() => void onDeleteAccount()}>
{t('settings.deleteAccount')}
</Button>
</CardContent>
</Card>
</div>
)
}