diff --git a/packages/frontend/src/app.tsx b/packages/frontend/src/app.tsx index 4426f0d..37e8797 100644 --- a/packages/frontend/src/app.tsx +++ b/packages/frontend/src/app.tsx @@ -1,6 +1,7 @@ import { Home } from "@/routes/Home"; import { Login } from "@/routes/Login"; import { Piece } from "@/routes/Piece"; +import { Repertoire } from "@/routes/Repertoire"; import { Root } from "@/routes/Root"; import { Settings } from "@/routes/Settings"; import { lazy, StrictMode } from "react"; @@ -32,6 +33,10 @@ const router = createBrowserRouter([ }, ], }, + { + path: "repertoire", + Component: Repertoire, + }, { path: "settings", Component: Settings, diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 729ba6d..33900d8 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -1,5 +1,5 @@ import type * as Db from "backend/database"; -import { AttachmentId, PieceId, UserId } from "common"; +import { AttachmentId, PieceId, RepertoireId, UserId } from "common"; import { Cache, Duration, Effect, Option, pipe } from "effect"; import { client, mapResponse } from "./client"; @@ -87,8 +87,36 @@ export const denormalizePiece = ({ Effect.flatMap(denormalizeSystemInformation), ); +export const denormalizeRepertoireEntry = ({ + pieceId, + ...rest +}: Db.RepertoireEntry) => pipe( + Effect.all({ + piece: Effect.uninterruptible(pieceCache.get(pieceId)), + }, { concurrency: "unbounded" }), + Effect.map((entry) => Object.freeze({ + ...rest, + ...entry, + })), +); + +export const denormalizeRepertoire = ({ + entries, + ...rest +}: Db.Repertoire & { entries: Db.RepertoireEntry[] }) => pipe( + Effect.all({ + entries: Effect.all(entries.map(denormalizeRepertoireEntry), { concurrency: "unbounded" }), + }, { concurrency: "unbounded" }), + Effect.map((repertoire) => Object.freeze({ + ...rest, + ...repertoire, + })), + Effect.flatMap(denormalizeSystemInformation), +); + const UserSemaphore = Effect.unsafeMakeSemaphore(1); const CacheSemaphore = Effect.unsafeMakeSemaphore(4); +const RepertoireSemaphore = Effect.unsafeMakeSemaphore(1); export const userLookup = (userId: UserId) => pipe( Effect.promise((signal) => client.user({ userId }).get({ fetch: { signal } })), @@ -103,6 +131,13 @@ export const pieceLookup = (pieceId: PieceId) => pipe( CacheSemaphore.withPermits(1), ); +export const repertoireLookup = (repertoireId: RepertoireId) => pipe( + Effect.promise((signal) => client.repertoire({ repertoireId }).get({ fetch: { signal } })), + Effect.flatMap(mapResponse), + Effect.flatMap(denormalizeRepertoire), + RepertoireSemaphore.withPermits(1), +); + export const userCache = Effect.runSync(Cache.make({ capacity: Infinity, timeToLive: Duration.days(1), @@ -114,3 +149,9 @@ export const pieceCache = Effect.runSync(Cache.make({ timeToLive: Duration.days(1), lookup: pieceLookup, })); + +export const repertoireCache = Effect.runSync(Cache.make({ + capacity: Infinity, + timeToLive: Duration.days(1), + lookup: repertoireLookup, +})); diff --git a/packages/frontend/src/routes/Repertoire.tsx b/packages/frontend/src/routes/Repertoire.tsx new file mode 100644 index 0000000..8db41ac --- /dev/null +++ b/packages/frontend/src/routes/Repertoire.tsx @@ -0,0 +1,204 @@ +import { repertoireCache, User } 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 { useLoadingEffect } from "@/hooks/useLoading"; +import { RepertoireId } from "common"; +import { Cause, Clock, Duration, Effect, Option } from "effect"; +import { Loader2, Plus } from "lucide-react"; +import { FormEventHandler, ReactNode, useId, useState } from "react"; + +export function Repertoire() { + + const [name, setName] = useState(""); + + const { isLoading, error, data: repertoireIds } = useLoadingEffect(Effect.gen(function* () { + yield* Clock.sleep(Duration.millis(500)); + const { error, data } = yield* Effect.promise((signal) => client.repertoire.get({ + query: { + ...(name !== "" ? { name } : undefined), + }, + fetch: { signal }, + })); + + if (error !== null) { + return yield* Effect.fail(error); + } else { + return data; + } + }), [name]); + + return ( +