110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { Link } from '@tanstack/react-router'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { HttpError } from '@/shared/api/client'
|
|
import { Badge } from '@/shared/ui/badge'
|
|
import { Button } from '@/shared/ui/button'
|
|
import { Input } from '@/shared/ui/input'
|
|
import { toast } from '@/shared/ui/toast-store'
|
|
import { createChannel, listChannels } from './api'
|
|
|
|
function slugify(value: string) {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/(^-|-$)/g, '')
|
|
}
|
|
|
|
export function ChannelsPanel() {
|
|
const { t } = useTranslation()
|
|
const queryClient = useQueryClient()
|
|
const [name, setName] = useState('')
|
|
const [slug, setSlug] = useState('')
|
|
|
|
const { data, isLoading } = useQuery({ queryKey: ['admin', 'channels'], queryFn: listChannels })
|
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'channels'] })
|
|
const onError = (error: unknown) =>
|
|
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: () => createChannel({ name: name.trim(), slug: slug || slugify(name) }),
|
|
onSuccess: () => {
|
|
setName('')
|
|
setSlug('')
|
|
invalidate()
|
|
},
|
|
onError,
|
|
})
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<h2 className="crt-glow text-xl font-semibold">{t('admin.channels.title')}</h2>
|
|
|
|
<div className="flex flex-wrap items-end gap-2">
|
|
<Input
|
|
className="max-w-xs"
|
|
placeholder={t('admin.channels.name')}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
<Input
|
|
className="max-w-xs"
|
|
placeholder={t('admin.channels.slug')}
|
|
value={slug}
|
|
onChange={(e) => setSlug(slugify(e.target.value))}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
disabled={!name.trim() || createMutation.isPending}
|
|
onClick={() => createMutation.mutate()}
|
|
>
|
|
{t('common.create')}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="crt-panel overflow-x-auto rounded-md">
|
|
<table className="w-full text-sm">
|
|
<thead className="border-b border-border text-left text-muted-foreground">
|
|
<tr>
|
|
<th className="px-4 py-2 font-medium">{t('admin.channels.name')}</th>
|
|
<th className="px-4 py-2 font-medium">{t('admin.channels.slug')}</th>
|
|
<th className="px-4 py-2 font-medium">{t('admin.channels.state')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{isLoading && (
|
|
<tr>
|
|
<td className="px-4 py-3 text-muted-foreground" colSpan={3}>
|
|
{t('common.loading')}
|
|
</td>
|
|
</tr>
|
|
)}
|
|
{data?.map((channel) => (
|
|
<tr key={channel.id} className="border-b border-border last:border-0">
|
|
<td className="px-4 py-2">
|
|
<Link
|
|
to="/admin/channels/$channelId"
|
|
params={{ channelId: channel.id }}
|
|
className="text-primary hover:underline"
|
|
>
|
|
{channel.name}
|
|
</Link>
|
|
</td>
|
|
<td className="px-4 py-2 text-muted-foreground">{channel.slug}</td>
|
|
<td className="px-4 py-2">
|
|
{channel.isEnabled ? (
|
|
<Badge>{t('admin.channels.enabled')}</Badge>
|
|
) : (
|
|
<Badge variant="muted">{t('admin.channels.disabled')}</Badge>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|