diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts
index f0d34ee..92b893f 100644
--- a/packages/backend/src/app.ts
+++ b/packages/backend/src/app.ts
@@ -802,11 +802,7 @@ const app = new Elysia()
const res = await q.execute();
return res.map(({ repertoireId }) => repertoireId);
}, {
- query: t.Object({
- name: t.Optional(t.String()),
- offset: t.Optional(t.Integer({ minimum: 0 })),
- limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
- }),
+ query: Model.Repertoire_Query,
response: {
200: t.Array(Model.RepertoireId),
401: t.Literal("Session invalid or expired"),
diff --git a/packages/backend/src/model.ts b/packages/backend/src/model.ts
index 46cc05b..a0136a4 100644
--- a/packages/backend/src/model.ts
+++ b/packages/backend/src/model.ts
@@ -22,8 +22,8 @@ const SystemInformation = Object.freeze({
});
const Pagination = Object.freeze({
- offset: t.Optional(t.Integer({ minimum: 0 })),
- limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
+ offset: t.Optional(t.Numeric({ minimum: 0 })),
+ limit: t.Optional(t.Numeric({ minimum: 1, maximum: 100 })),
});
export const AccessLog = t.Object({
@@ -74,6 +74,11 @@ export const Repertoire = t.Object({
...SystemInformation,
});
+export const Repertoire_Query = t.Object({
+ name: t.Optional(t.String()),
+ ...Pagination,
+});
+
export const User = t.Object({
userId: UserId,
username: t.String(),
@@ -103,6 +108,7 @@ export type Piece = typeof Piece.static;
export type Piece_Post = typeof Piece_Post.static;
export type Piece_Query = typeof Piece_Query.static;
export type Repertoire = typeof Repertoire.static;
+export type Repertoire_Query = typeof Repertoire_Query.static;
export type User = typeof User.static;
export type User_Patch = typeof User_Patch.static;
export type User_Post = typeof User_Post.static;
diff --git a/packages/frontend/src/routes/Repertoire.tsx b/packages/frontend/src/routes/Repertoire.tsx
index 2a07c06..7a8686c 100644
--- a/packages/frontend/src/routes/Repertoire.tsx
+++ b/packages/frontend/src/routes/Repertoire.tsx
@@ -1,17 +1,18 @@
-import { Piece, type Repertoire, repertoireCache } from "@/cache";
+import { Piece, pieceCache, type Repertoire, repertoireCache } from "@/cache";
import { client } from "@/client";
import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, 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 { useLoadingEffect } from "@/hooks/useLoading";
import { mapProp, Update, Updater } from "@/hooks/useStore";
-import { authors, saveDelay } from "@/snippets";
+import { authors, DEBOUNCE, saveDelay } from "@/snippets";
import { PieceId, RepertoireId } from "common";
-import { Array, Cause, Effect, pipe } from "effect";
-import { CircleMinus, Loader2 } from "lucide-react";
-import { FormEventHandler, useCallback, useId, useState } from "react";
-import { useNavigate, useParams } from "react-router-dom";
+import { Array, Cause, Effect, Option, pipe } from "effect";
+import { ChevronDown, ChevronUp, CircleMinus, Loader2, Plus } from "lucide-react";
+import { FormEventHandler, useCallback, useId, useMemo, useRef, useState } from "react";
+import { Link, useNavigate, useParams } from "react-router-dom";
export function Repertoire() {
@@ -141,12 +142,15 @@ namespace Entries {
}
function Entries(props: Entries.Props) {
+
+ const [dialogOpen, setDialogOpen] = useState(false);
+
return (
-
+
- Lp.
+ Lp.
Tytuł
Twórcy
Akcje
@@ -164,6 +168,14 @@ function Entries(props: Entries.Props) {
))}
+
);
}
@@ -177,18 +189,41 @@ namespace EntryRow {
}
}
-function EntryRow(props: EntryRow.Props) {
+function EntryRow({
+ repertoire,
+ piece,
+ no,
+ setEntries,
+}: EntryRow.Props) {
- const remove = useCallback(async () => {
+ const moveUpAction = useCallback((entries: readonly Piece[]) => pipe(
+ entries,
+ Array.remove(no - 1),
+ Array.insertAt(no - 2, piece),
+ Option.getOrThrow,
+ ), [no, piece]);
+
+ const moveDownAction = useCallback((entries: readonly Piece[]) => pipe(
+ entries,
+ Array.remove(no - 1),
+ Array.insertAt(no, piece),
+ Option.getOrThrow,
+ ), [no, piece]);
+
+ const removeAction = useCallback((entries: readonly Piece[]) => pipe(
+ entries,
+ Array.filter((p) => p.pieceId !== piece.pieceId),
+ ), [piece.pieceId]);
+
+ const update = useCallback(async (action: (prev: readonly Piece[]) => readonly Piece[]) => {
- const filter = Array.filter
((piece) => piece.pieceId !== props.piece.pieceId);
const mapToId = Array.map(({ pieceId }) => pieceId);
const { error } = await client
- .repertoire({ repertoireId: props.repertoire.repertoireId })
+ .repertoire({ repertoireId: repertoire.repertoireId })
.put({
- name: props.repertoire.name,
- entries: pipe(props.repertoire.entries, filter, mapToId),
+ name: repertoire.name,
+ entries: pipe(repertoire.entries, action, mapToId),
});
if (error !== null) {
@@ -196,22 +231,33 @@ function EntryRow(props: EntryRow.Props) {
return;
}
- props.setEntries(filter);
+ setEntries(action);
+ }, [repertoire.entries, repertoire.name, repertoire.repertoireId, setEntries]);
- }, [props]);
+ const moveUp = useMemo(() => update.bind(undefined, moveUpAction), [moveUpAction, update]);
+ const moveDown = useMemo(() => update.bind(undefined, moveDownAction), [moveDownAction, update]);
+ const remove = useMemo(() => update.bind(undefined, removeAction), [removeAction, update]);
return (
-
- {props.no}
+
+ {no}
- {props.piece.name}
+
+ {piece.name}
+
- {authors(props.piece)}
+ {authors(piece)}
+
+
@@ -219,3 +265,164 @@ function EntryRow(props: EntryRow.Props) {
);
}
+
+const ADD_ENTRY_DIALOG_LIMIT = 10;
+
+namespace AddEntryDialogContent {
+ export interface Props {
+ readonly repertoire: Repertoire;
+ readonly setEntries: Updater;
+ readonly setDialogOpen: Updater;
+ }
+}
+
+function AddEntryDialogContent(props: AddEntryDialogContent.Props) {
+
+ const [name, setName] = useState("");
+ const [author, setAuthor] = useState("");
+
+ const debounce = useRef(Effect.void);
+
+ const { isLoading, error, data: pieceIds } = useLoadingEffect(Effect.gen(function* () {
+ yield* debounce.current;
+ const { error, data } = yield* Effect.promise((signal) => client.piece.get({
+ query: {
+ ...(name !== "" ? { name } : undefined),
+ ...(author !== "" ? { author } : undefined),
+ limit: ADD_ENTRY_DIALOG_LIMIT,
+ },
+ fetch: { signal },
+ }));
+
+ if (error !== null) {
+ return yield* Effect.fail(error);
+ } else {
+ return data;
+ }
+ }), [name, author]);
+
+ return (
+
+
+ Dodaj utwór
+
+
+
+ );
+}
+
+namespace EntryDialogPieceRow {
+ export interface Props {
+ readonly repertoire: Repertoire;
+ readonly pieceId: PieceId;
+ readonly setEntries: Updater;
+ readonly setDialogOpen: Updater;
+ }
+}
+
+function EntryDialogPieceRow(props: EntryDialogPieceRow.Props) {
+
+ const { isLoading, error, data: piece } = useLoadingEffect(Effect.uninterruptible(pieceCache.get(props.pieceId)), [props.pieceId]);
+
+ const onClick = useCallback(async () => {
+
+ const action = Array.append(piece!);
+ const mapToId = Array.map(({ pieceId }) => pieceId);
+
+ const { error } = await client
+ .repertoire({ repertoireId: props.repertoire.repertoireId })
+ .put({
+ name: props.repertoire.name,
+ entries: pipe(props.repertoire.entries, action, mapToId),
+ });
+
+ if (error !== null) {
+ console.error(error.value);
+ return;
+ }
+
+ props.setEntries(action);
+ props.setDialogOpen(false);
+
+ }, [piece, props]);
+
+ if (isLoading) {
+ return (
+
+ Ładowanie…
+
+ );
+ }
+
+ if (error !== null) {
+ return (
+
+ Wystąpił błąd: {error.value}
+
+ );
+ }
+
+ return (
+
+
+ {piece.name}
+
+
+ {authors(piece)}
+
+
+ );
+}