From c59c0e106be4e25e134b6e301ad8176677574c97 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sun, 29 Dec 2024 23:19:55 +0100 Subject: [PATCH] Deduplicate, don't debounce on first visit --- packages/frontend/src/routes/Home.tsx | 40 +++++++++---- packages/frontend/src/routes/Piece.tsx | 16 ++--- packages/frontend/src/routes/Pieces.tsx | 38 ++++++------ packages/frontend/src/routes/Repertoire.tsx | 17 ++---- packages/frontend/src/routes/Repertoires.tsx | 22 +++---- packages/frontend/src/snippets.tsx | 63 ++++++++++++++++++++ 6 files changed, 131 insertions(+), 65 deletions(-) create mode 100644 packages/frontend/src/snippets.tsx diff --git a/packages/frontend/src/routes/Home.tsx b/packages/frontend/src/routes/Home.tsx index 3fc2d32..02df01d 100644 --- a/packages/frontend/src/routes/Home.tsx +++ b/packages/frontend/src/routes/Home.tsx @@ -1,21 +1,35 @@ import { ListMusic, Music3 } from "lucide-react"; -import { Link } from "react-router-dom"; +import { ReactNode } from "react"; +import { Link, To } from "react-router-dom"; export function Home() { return (
- -
- -
Utwory
-
- - -
- -
Repertuary
-
- + + +
Utwory
+
+ + +
Repertuary
+
); } + +namespace LinkCard { + export interface Props { + readonly to: To; + readonly children: ReactNode; + } +} + +function LinkCard(props: LinkCard.Props) { + return ( + +
+ {props.children} +
+ + ); +} diff --git a/packages/frontend/src/routes/Piece.tsx b/packages/frontend/src/routes/Piece.tsx index 8765ec6..73a2ae4 100644 --- a/packages/frontend/src/routes/Piece.tsx +++ b/packages/frontend/src/routes/Piece.tsx @@ -1,11 +1,11 @@ -import { Attachment, denormalizeSystemInformation, type Piece, pieceCache, User } from "@/cache"; +import { Attachment, denormalizeSystemInformation, type Piece, pieceCache } from "@/cache"; import { API_URL_PREFIX, client } from "@/client"; import { Button, buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { useLoadingEffect } from "@/hooks/useLoading"; import { mapProp, Update, Updater } from "@/hooks/useStore"; -import { timeout } from "@/lib/utils"; +import { created, modified, saveDelay } from "@/snippets"; import { Label } from "@radix-ui/react-label"; import clsx from "clsx"; import { PieceId } from "common"; @@ -78,7 +78,7 @@ function PieceForm(props: PieceForm.Props) { const onSubmit: FormEventHandler = async (e) => { e.preventDefault(); - const delay = timeout(250); + const delay = saveDelay(); try { setIsSaving(true); @@ -271,14 +271,10 @@ function AttachmentRow(props: AttachmentRow.Props) { {props.attachment.mediaType} - {props.attachment.createdAt} - {Option.isSome(props.attachment.createdBy) && <>
przez {props.attachment.createdBy.value.username}} + {created(props.attachment)}
- {Option.isNone(props.attachment.modifiedAt) && Option.isNone(props.attachment.modifiedBy) ? "\u2014" - : Option.isSome(props.attachment.modifiedAt) && Option.isNone(props.attachment.modifiedBy) ? props.attachment.modifiedAt.value - : Option.isNone(props.attachment.modifiedAt) ? `przez ${(props.attachment.modifiedBy as Option.Some).value.username}` - : <>{props.attachment.modifiedAt.value}
przez {(props.attachment.modifiedBy as Option.Some).value.username}} + {modified(props.attachment)}
@@ -315,7 +311,7 @@ function AttachmentForm(props: AttachmentForm.Props) { return; } - const delay = timeout(250); + const delay = saveDelay(); try { setIsLoading(true); diff --git a/packages/frontend/src/routes/Pieces.tsx b/packages/frontend/src/routes/Pieces.tsx index 96d0977..5f9bd1a 100644 --- a/packages/frontend/src/routes/Pieces.tsx +++ b/packages/frontend/src/routes/Pieces.tsx @@ -1,4 +1,4 @@ -import { pieceCache, User } from "@/cache"; +import { pieceCache } from "@/cache"; import { client } from "@/client"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; @@ -6,10 +6,11 @@ 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 { authors, created, DEBOUNCE, modified } from "@/snippets"; import { PieceId } from "common"; -import { Cause, Clock, Duration, Effect, Option } from "effect"; +import { Cause, Effect } from "effect"; import { Loader2, Plus } from "lucide-react"; -import { FormEventHandler, ReactNode, useId, useState } from "react"; +import { FormEventHandler, useId, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; export function Pieces() { @@ -17,8 +18,10 @@ export function Pieces() { const [name, setName] = useState(""); const [author, setAuthor] = useState(""); + const debounce = useRef(Effect.void); + const { isLoading, error, data: pieceIds } = useLoadingEffect(Effect.gen(function* () { - yield* Clock.sleep(Duration.millis(500)); + yield* debounce.current; const { error, data } = yield* Effect.promise((signal) => client.piece.get({ query: { ...(name !== "" ? { name } : undefined), @@ -50,14 +53,20 @@ export function Pieces() { type="text" placeholder="Tytuł" value={name} - onChange={(e) => setName(e.target.value)} + onChange={(e) => { + setName(e.target.value); + debounce.current = DEBOUNCE; + }} /> setAuthor(e.target.value)} + onChange={(e) => { + setAuthor(e.target.value); + debounce.current = DEBOUNCE; + }} /> @@ -120,30 +129,19 @@ function PieceRow(props: PieceRow.Props) { ); } - const composerParts: ReactNode[] = []; - - if (Option.isSome(piece.composer)) composerParts.push(piece.composer.value); - if (Option.isSome(piece.arranger)) composerParts.push(`opracowanie: ${piece.arranger.value}`); - if (Option.isSome(piece.lyricist)) composerParts.push(`słowa: ${piece.lyricist.value}`); - if (composerParts.length === 0) composerParts.push(Nieznani); - return ( {piece.name} - {...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x,
] : [x])} + {authors(piece)}
- {piece.createdAt} - {Option.isSome(piece.createdBy) && <>
przez {piece.createdBy.value.username}} + {created(piece)}
- {Option.isNone(piece.modifiedAt) && Option.isNone(piece.modifiedBy) ? "\u2014" - : Option.isSome(piece.modifiedAt) && Option.isNone(piece.modifiedBy) ? piece.modifiedAt.value - : Option.isNone(piece.modifiedAt) ? `przez ${(piece.modifiedBy as Option.Some).value.username}` - : <>{piece.modifiedAt.value}
przez {(piece.modifiedBy as Option.Some).value.username}} + {modified(piece)}
); diff --git a/packages/frontend/src/routes/Repertoire.tsx b/packages/frontend/src/routes/Repertoire.tsx index b5da472..2a07c06 100644 --- a/packages/frontend/src/routes/Repertoire.tsx +++ b/packages/frontend/src/routes/Repertoire.tsx @@ -6,11 +6,11 @@ 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 { timeout } from "@/lib/utils"; +import { authors, saveDelay } from "@/snippets"; import { PieceId, RepertoireId } from "common"; -import { Array, Cause, Effect, Option, pipe } from "effect"; +import { Array, Cause, Effect, pipe } from "effect"; import { CircleMinus, Loader2 } from "lucide-react"; -import { FormEventHandler, ReactNode, useCallback, useId, useState } from "react"; +import { FormEventHandler, useCallback, useId, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; export function Repertoire() { @@ -67,7 +67,7 @@ function RepertoireForm(props: RepertoireForm.Props) { const onSubmit: FormEventHandler = async (e) => { e.preventDefault(); - const delay = timeout(250); + const delay = saveDelay(); try { setIsSaving(true); @@ -200,13 +200,6 @@ function EntryRow(props: EntryRow.Props) { }, [props]); - const composerParts: ReactNode[] = []; - - if (Option.isSome(props.piece.composer)) composerParts.push(props.piece.composer.value); - if (Option.isSome(props.piece.arranger)) composerParts.push(`opracowanie: ${props.piece.arranger.value}`); - if (Option.isSome(props.piece.lyricist)) composerParts.push(`słowa: ${props.piece.lyricist.value}`); - if (composerParts.length === 0) composerParts.push(Nieznani); - return ( @@ -216,7 +209,7 @@ function EntryRow(props: EntryRow.Props) { {props.piece.name} - {...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x,
] : [x])} + {authors(props.piece)}
@@ -134,14 +140,10 @@ function RepertoireRow(props: RepertoireRow.Props) { {...piecesParts} - {repertoire.createdAt} - {Option.isSome(repertoire.createdBy) && <>
przez {repertoire.createdBy.value.username}} + {created(repertoire)}
- {Option.isNone(repertoire.modifiedAt) && Option.isNone(repertoire.modifiedBy) ? "\u2014" - : Option.isSome(repertoire.modifiedAt) && Option.isNone(repertoire.modifiedBy) ? repertoire.modifiedAt.value - : Option.isNone(repertoire.modifiedAt) ? `przez ${(repertoire.modifiedBy as Option.Some).value.username}` - : <>{repertoire.modifiedAt.value}
przez {(repertoire.modifiedBy as Option.Some).value.username}} + {modified(repertoire)}
); diff --git a/packages/frontend/src/snippets.tsx b/packages/frontend/src/snippets.tsx new file mode 100644 index 0000000..f29c47e --- /dev/null +++ b/packages/frontend/src/snippets.tsx @@ -0,0 +1,63 @@ +import { Piece, SystemInformation } from "@/cache"; +import { timeout } from "@/lib/utils"; +import { Clock, Duration, Option } from "effect"; +import { ReactNode } from "react"; + +export const DEBOUNCE = Clock.sleep(Duration.millis(250)); + +export const saveDelay = () => timeout(250); + +export function authors(piece: Piece): ReactNode { + + const nodes: ReactNode[] = []; + + if (Option.isSome(piece.composer)) { + nodes.push(piece.composer.value); + } + + if (Option.isSome(piece.arranger)) { + nodes.push(`opracowanie: ${piece.arranger.value}`); + } + + if (Option.isSome(piece.lyricist)) { + nodes.push(`słowa: ${piece.lyricist.value}`); + } + + if (nodes.length === 0) { + return Nieznani; + } + + return nodes.flatMap((x, i, a) => i < a.length - 1 ? [x,
] : [x]); +} + +export function created({ createdAt, createdBy }: SystemInformation): ReactNode { + + const nodes: ReactNode[] = [createdAt]; + + if (Option.isSome(createdBy)) { + nodes.push(
); + nodes.push(`przez ${createdBy.value.username}`); + } + + return nodes; +} + +export function modified({ modifiedAt, modifiedBy }: SystemInformation): ReactNode { + + if (Option.isNone(modifiedAt)) { + if (Option.isNone(modifiedBy)) { + return "\u2014"; + } else { + return `przez ${modifiedBy.value.username}`; + } + } + + const nodes: ReactNode[] = [modifiedAt.value]; + + if (Option.isSome(modifiedBy)) { + nodes.push(
); + nodes.push(`przez ${modifiedBy.value.username}`); + } + + return nodes; +}