Форматтера у фронтенда не было: стиль держался вручную и успел разъехаться в 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>
137 lines
4.6 KiB
TypeScript
137 lines
4.6 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { AlertTriangle } from 'lucide-react'
|
|
import { qk } from '@/shared/api/query-keys'
|
|
import { useApiError } from '@/shared/lib/use-api-error'
|
|
import { Button } from '@/shared/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
|
|
import { toast } from '@/shared/ui/toast-store'
|
|
import { listShows } from '@/features/admin/shows/api'
|
|
import { clearAllMedia, deleteAllShows, deleteShowMedia } from './api'
|
|
|
|
export function MaintenancePanel() {
|
|
const { t } = useTranslation()
|
|
const queryClient = useQueryClient()
|
|
const [showId, setShowId] = useState('')
|
|
|
|
const { data: shows } = useQuery({ queryKey: qk.shows.all, queryFn: () => listShows() })
|
|
|
|
const onError = useApiError()
|
|
|
|
// Затрагиваются медиа/шоу/каналы — сбрасываем все связанные кэши.
|
|
const invalidateAll = () => {
|
|
for (const key of [qk.media.all, qk.shows.all, qk.channels.all])
|
|
void queryClient.invalidateQueries({ queryKey: key })
|
|
}
|
|
|
|
const clearMedia = useMutation({
|
|
mutationFn: clearAllMedia,
|
|
onSuccess: (count) => {
|
|
toast.success(t('admin.maintenance.doneCount', { count }))
|
|
invalidateAll()
|
|
},
|
|
onError,
|
|
})
|
|
|
|
const clearShows = useMutation({
|
|
mutationFn: deleteAllShows,
|
|
onSuccess: (count) => {
|
|
toast.success(t('admin.maintenance.doneCount', { count }))
|
|
invalidateAll()
|
|
},
|
|
onError,
|
|
})
|
|
|
|
const clearShowMedia = useMutation({
|
|
mutationFn: () => deleteShowMedia(showId),
|
|
onSuccess: (count) => {
|
|
setShowId('')
|
|
toast.success(t('admin.maintenance.doneCount', { count }))
|
|
invalidateAll()
|
|
},
|
|
onError,
|
|
})
|
|
|
|
const confirmed = (message: string, run: () => void) => {
|
|
if (window.confirm(message)) run()
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<AlertTriangle className="h-5 w-5 text-red-500" />
|
|
<h2 className="crt-glow text-xl font-semibold">{t('admin.maintenance.title')}</h2>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">{t('admin.maintenance.warning')}</p>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('admin.maintenance.clearMedia')}</CardTitle>
|
|
<CardDescription>{t('admin.maintenance.clearMediaHint')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button
|
|
variant="destructive"
|
|
disabled={clearMedia.isPending}
|
|
onClick={() =>
|
|
confirmed(t('admin.maintenance.confirmClearMedia'), () => clearMedia.mutate())
|
|
}
|
|
>
|
|
{t('admin.maintenance.clearMedia')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('admin.maintenance.clearShowMedia')}</CardTitle>
|
|
<CardDescription>{t('admin.maintenance.clearShowMediaHint')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-wrap items-center gap-2">
|
|
<Select value={showId} onValueChange={setShowId}>
|
|
<SelectTrigger className="max-w-xs">
|
|
<SelectValue placeholder={t('admin.maintenance.pickShow')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{shows?.map((s) => (
|
|
<SelectItem key={s.id} value={s.id}>
|
|
{s.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
variant="destructive"
|
|
disabled={!showId || clearShowMedia.isPending}
|
|
onClick={() =>
|
|
confirmed(t('admin.maintenance.confirmClearShowMedia'), () => clearShowMedia.mutate())
|
|
}
|
|
>
|
|
{t('admin.maintenance.clearShowMedia')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('admin.maintenance.deleteShows')}</CardTitle>
|
|
<CardDescription>{t('admin.maintenance.deleteShowsHint')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button
|
|
variant="destructive"
|
|
disabled={clearShows.isPending}
|
|
onClick={() =>
|
|
confirmed(t('admin.maintenance.confirmDeleteShows'), () => clearShows.mutate())
|
|
}
|
|
>
|
|
{t('admin.maintenance.deleteShows')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|