Deduplicate, don't debounce on first visit

This commit is contained in:
2024-12-29 23:19:55 +01:00
parent bf4361d207
commit c59c0e106b
6 changed files with 131 additions and 65 deletions

View File

@@ -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 (
<div className="p-4 overflow-y-auto flex flex-wrap items-start justify-center gap-4">
<Link to="piece">
<div className="w-64 h-64 p-4 flex flex-col items-center justify-center gap-4 rounded-md text-lg font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-stone-950 dark:focus-visible:ring-stone-300 border border-stone-200 bg-white hover:bg-stone-100 hover:text-stone-900 dark:border-stone-800 dark:bg-stone-950 dark:hover:bg-stone-800 dark:hover:text-stone-50">
<Music3 size={32} absoluteStrokeWidth />
<div>Utwory</div>
</div>
</Link>
<Link to="repertoire">
<div className="w-64 h-64 p-4 flex flex-col items-center justify-center gap-4 rounded-md text-lg font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-stone-950 dark:focus-visible:ring-stone-300 border border-stone-200 bg-white hover:bg-stone-100 hover:text-stone-900 dark:border-stone-800 dark:bg-stone-950 dark:hover:bg-stone-800 dark:hover:text-stone-50">
<ListMusic size={32} absoluteStrokeWidth />
<div>Repertuary</div>
</div>
</Link>
<LinkCard to="piece">
<Music3 size={32} absoluteStrokeWidth />
<div>Utwory</div>
</LinkCard>
<LinkCard to="repertoire">
<ListMusic size={32} absoluteStrokeWidth />
<div>Repertuary</div>
</LinkCard>
</div>
);
}
namespace LinkCard {
export interface Props {
readonly to: To;
readonly children: ReactNode;
}
}
function LinkCard(props: LinkCard.Props) {
return (
<Link to={props.to}>
<div className="w-64 h-64 p-4 flex flex-col items-center justify-center gap-4 rounded-md text-lg font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-stone-950 dark:focus-visible:ring-stone-300 border border-stone-200 bg-white hover:bg-stone-100 hover:text-stone-900 dark:border-stone-800 dark:bg-stone-950 dark:hover:bg-stone-800 dark:hover:text-stone-50">
{props.children}
</div>
</Link>
);
}

View File

@@ -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<HTMLFormElement> = 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}
</TableCell>
<TableCell className="text-center text-xs">
{props.attachment.createdAt}
{Option.isSome(props.attachment.createdBy) && <><br />przez {props.attachment.createdBy.value.username}</>}
{created(props.attachment)}
</TableCell>
<TableCell className="text-center text-xs">
{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<User>).value.username}`
: <>{props.attachment.modifiedAt.value}<br />przez {(props.attachment.modifiedBy as Option.Some<User>).value.username}</>}
{modified(props.attachment)}
</TableCell>
<TableCell className="text-center flex justify-center gap-4">
<a href={url} className={buttonVariants({ variant: "ghost", size: "icon" })} title="Pobierz" download={props.attachment.filename}>
@@ -315,7 +311,7 @@ function AttachmentForm(props: AttachmentForm.Props) {
return;
}
const delay = timeout(250);
const delay = saveDelay();
try {
setIsLoading(true);

View File

@@ -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;
}}
/>
<Input
className="w-[32ch]"
type="text"
placeholder="Twórcy"
value={author}
onChange={(e) => setAuthor(e.target.value)}
onChange={(e) => {
setAuthor(e.target.value);
debounce.current = DEBOUNCE;
}}
/>
</div>
<Table>
@@ -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(<em>Nieznani</em>);
return (
<TableRow>
<TableCell>
<Link className="underline" to={piece.pieceId}>{piece.name}</Link>
</TableCell>
<TableCell>
{...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x, <br />] : [x])}
{authors(piece)}
</TableCell>
<TableCell className="text-center text-xs">
{piece.createdAt}
{Option.isSome(piece.createdBy) && <><br />przez {piece.createdBy.value.username}</>}
{created(piece)}
</TableCell>
<TableCell className="text-center text-xs">
{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<User>).value.username}`
: <>{piece.modifiedAt.value}<br />przez {(piece.modifiedBy as Option.Some<User>).value.username}</>}
{modified(piece)}
</TableCell>
</TableRow>
);

View File

@@ -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<HTMLFormElement> = 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(<em>Nieznani</em>);
return (
<TableRow>
<TableCell>
@@ -216,7 +209,7 @@ function EntryRow(props: EntryRow.Props) {
{props.piece.name}
</TableCell>
<TableCell>
{...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x, <br />] : [x])}
{authors(props.piece)}
</TableCell>
<TableCell className="text-center flex justify-center gap-4">
<Button type="button" variant="ghost" size="icon" title="Usuń" onClick={remove}>

View File

@@ -6,18 +6,21 @@ 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 { created, DEBOUNCE, modified } from "@/snippets";
import { RepertoireId } from "common";
import { Cause, Clock, Duration, Effect, Option } from "effect";
import { Cause, Effect, Option } from "effect";
import { Loader2, Plus } from "lucide-react";
import { FormEventHandler, ReactNode, useId, useState } from "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 } = useLoadingEffect(Effect.gen(function* () {
yield* Clock.sleep(Duration.millis(500));
yield* debounce.current;
const { error, data } = yield* Effect.promise((signal) => client.repertoire.get({
query: {
...(name !== "" ? { name } : undefined),
@@ -48,7 +51,10 @@ export function Repertoires() {
type="text"
placeholder="Nazwa"
value={name}
onChange={(e) => setName(e.target.value)}
onChange={(e) => {
setName(e.target.value);
debounce.current = DEBOUNCE;
}}
/>
</div>
<Table>
@@ -134,14 +140,10 @@ function RepertoireRow(props: RepertoireRow.Props) {
{...piecesParts}
</TableCell>
<TableCell className="text-center text-xs">
{repertoire.createdAt}
{Option.isSome(repertoire.createdBy) && <><br />przez {repertoire.createdBy.value.username}</>}
{created(repertoire)}
</TableCell>
<TableCell className="text-center text-xs">
{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<User>).value.username}`
: <>{repertoire.modifiedAt.value}<br />przez {(repertoire.modifiedBy as Option.Some<User>).value.username}</>}
{modified(repertoire)}
</TableCell>
</TableRow>
);

View File

@@ -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 <em>Nieznani</em>;
}
return nodes.flatMap((x, i, a) => i < a.length - 1 ? [x, <br />] : [x]);
}
export function created({ createdAt, createdBy }: SystemInformation): ReactNode {
const nodes: ReactNode[] = [createdAt];
if (Option.isSome(createdBy)) {
nodes.push(<br />);
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(<br />);
nodes.push(`przez ${modifiedBy.value.username}`);
}
return nodes;
}