Files
music-repo/packages/frontend/src/routes/Repertoires.tsx

211 lines
5.6 KiB
TypeScript

import { repertoireCache } from "@/cache";
import { client } from "@/client";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useCache } from "@/hooks/useCache";
import { useLoading } from "@/hooks/useLoading";
import { created, DEBOUNCE, modified } from "@/snippets";
import { RepertoireId } from "common";
import { Cause, Effect, Match, Option, Scope } from "effect";
import { Loader2, Plus } from "lucide-react";
import { FormEventHandler, ReactNode, useId, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
export function Repertoires() {
const [name, setName] = useState("");
const debounce = useRef(Effect.void);
const { isLoading, error, data: repertoireIds } = useLoading(Effect.gen(function* () {
yield* debounce.current;
const data = yield* client.queryRepertoires({
name: name !== "" ? Option.some(name) : Option.none(),
offset: 0,
limit: 100,
});
return data;
}), [name]);
return (
<div className="p-4 overflow-y-auto flex flex-col items-start gap-4">
<div className="flex flex-row gap-4">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">
<Plus />Utwórz repertuar
</Button>
</DialogTrigger>
<AddRepertoireDialogContent />
</Dialog>
<Input
className="w-[32ch]"
type="text"
placeholder="Nazwa"
value={name}
onChange={(e) => {
setName(e.target.value);
debounce.current = DEBOUNCE;
}}
/>
</div>
<Table>
<TableHeader className="bg-white sticky top-0">
<TableRow>
<TableHead>Nazwa</TableHead>
<TableHead>Utwory</TableHead>
<TableHead className="text-center">Dodano</TableHead>
<TableHead className="text-center">Zmodyfikowano</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={4} >
<div className="flex items-center justify-center gap-2">
<Loader2 className="animate-spin" />
Ładowanie
</div>
</TableCell>
</TableRow>
) : error !== null ? (
<TableRow>
<TableCell colSpan={4} className="text-center">
{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error)}`}
</TableCell>
</TableRow>
) : (
repertoireIds.map((repertoireId) => <RepertoireRow key={repertoireId} repertoireId={repertoireId} />)
)}
</TableBody>
</Table>
</div>
);
}
namespace RepertoireRow {
export interface Props {
readonly repertoireId: RepertoireId;
}
}
function RepertoireRow(props: RepertoireRow.Props) {
const { isLoading, error, data: repertoire } = useCache(repertoireCache, props.repertoireId);
if (isLoading) {
return (
<TableRow>
<TableCell colSpan={4}>Ładowanie</TableCell>
</TableRow>
);
}
if (error !== null) {
return (
<TableRow>
<TableCell colSpan={4}>
Wystąpił błąd: {Match.value(error).pipe(
Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
Match.tag("NotFound", () => "Repertuar nie istnieje"),
Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"),
Match.exhaustive,
)}
</TableCell>
</TableRow>
);
}
const piecesParts: ReactNode[] = [];
if (repertoire.entries.length === 0) {
piecesParts.push(<em>Brak</em>);
} else {
const CUTOFF = 10;
let text = repertoire.entries.slice(0, CUTOFF).map(({ name }) => name).join(", ");
if (repertoire.entries.length > CUTOFF) {
text += ", …";
}
piecesParts.push(text);
}
return (
<TableRow>
<TableCell>
<Link className="underline" to={repertoire.repertoireId}>{repertoire.name}</Link>
</TableCell>
<TableCell>
{...piecesParts}
</TableCell>
<TableCell className="text-center text-xs">
{created(repertoire)}
</TableCell>
<TableCell className="text-center text-xs">
{modified(repertoire)}
</TableCell>
</TableRow>
);
}
function AddRepertoireDialogContent() {
const navigate = useNavigate();
const [name, setName] = useState("");
const nameId = useId();
const [isLoading, setIsLoading] = useState(false);
const onSubmit: FormEventHandler<HTMLFormElement> = (e) => Effect.gen(function* () {
e.preventDefault();
yield* Effect.scopedWith((scope) => Effect.gen(function* () {
yield* Scope.addFinalizer(scope, Effect.sync(() => setIsLoading(false)));
setIsLoading(true);
const { repertoireId } = yield* client.createRepertoire({
name,
entries: [],
});
navigate(repertoireId);
}));
}).pipe(Effect.runPromise);
return (
<DialogContent>
<form onSubmit={onSubmit}>
<DialogHeader>
<DialogTitle>Utwórz repertuar</DialogTitle>
</DialogHeader>
<div className="grid items-baseline grid-cols-4 gap-4 py-4">
<Label htmlFor={nameId} className="text-right">Nazwa</Label>
<Input
id={nameId}
className="col-span-3"
type="text"
value={name}
autoFocus
required
onChange={(e) => setName(e.target.value)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="animate-spin" />}
Utwórz
</Button>
</DialogFooter>
</form>
</DialogContent>
);
}