Files
biggus-dickus/web/src/vocab/vocabulary-list.tsx
T

125 lines
4.3 KiB
TypeScript

import { useState, type FormEvent } from "react";
import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useVocabularies, useCreateVocabulary, useRenameVocabulary, useDeleteVocabulary } from "../api/queries";
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ListSkeleton } from "@/components/ui/skeletons";
export function VocabularyList() {
const { t } = useTranslation();
const { data, isLoading, isError } = useVocabularies();
const create = useCreateVocabulary();
const renameVocabulary = useRenameVocabulary();
const deleteVocabulary = useDeleteVocabulary();
const [key, setKey] = useState("");
const [editingId, setEditingId] = useState<string | null>(null);
const [draftKey, setDraftKey] = useState("");
const onCreate = (event: FormEvent) => {
event.preventDefault();
if (!key.trim()) return;
create.mutate({ key: key.trim() }, { onSuccess: () => setKey("") });
};
return (
<div className="flex h-full flex-col">
<form onSubmit={onCreate} className="space-y-1 border-b p-3">
<div className="text-sm font-medium">{t("vocab.newVocabulary")}</div>
<Label htmlFor="vocab-key">{t("vocab.key")}</Label>
<div className="flex gap-2">
<Input
id="vocab-key"
value={key}
onChange={(e) => setKey(e.target.value)}
/>
<Button type="submit" size="sm" disabled={create.isPending}>
{t("vocab.create")}
</Button>
</div>
{create.isError && (
<p role="alert" className="text-xs text-destructive">
{t("form.rejected")}
</p>
)}
</form>
{isLoading ? (
<ListSkeleton className="flex-1 overflow-auto" />
) : (
<ul className="flex-1 overflow-auto">
{isError && (
<li className="p-3 text-sm text-destructive">{t("vocab.loadError")}</li>
)}
{data?.length === 0 && (
<li className="p-3 text-sm text-muted-foreground">{t("vocab.empty")}</li>
)}
{data?.map((v) => (
<li key={v.id} className="flex items-center gap-1 border-b pr-2">
{editingId === v.id ? (
<form
className="flex flex-1 flex-wrap gap-1 p-1"
onSubmit={(e) => {
e.preventDefault();
renameVocabulary.mutate(
{ id: v.id, key: draftKey.trim() },
{ onSuccess: () => setEditingId(null) },
);
}}
>
<Input
aria-label={t("vocab.key")}
value={draftKey}
onChange={(e) => setDraftKey(e.target.value)}
/>
<Button type="submit" size="sm" disabled={renameVocabulary.isPending}>
{t("actions.save")}
</Button>
<Button type="button" variant="ghost" size="sm" onClick={() => setEditingId(null)}>
{t("form.cancel")}
</Button>
{renameVocabulary.isError && (
<p role="alert" className="text-xs text-destructive">
{t("form.rejected")}
</p>
)}
</form>
) : (
<>
<NavLink
to={`/vocabularies/${v.id}`}
className={({ isActive }) =>
`block flex-1 px-3 py-2 text-sm ${isActive ? "bg-primary/10" : "hover:bg-muted"}`
}
>
{v.key}
</NavLink>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => { setEditingId(v.id); setDraftKey(v.key); }}
>
{t("actions.rename")}
</Button>
<DeleteConfirmDialog
description={t("actions.confirmDeleteVocabulary")}
onConfirm={() => deleteVocabulary.mutateAsync(v.id)}
/>
</>
)}
</li>
))}
</ul>
)}
</div>
);
}