From bee6ead6dc9e6320f9e8d41e2a9d250fc7c68df6 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Tue, 7 Oct 2025 23:38:41 +0200 Subject: [PATCH] Bulk import frontend utility --- packages/backend/src/config.ts | 2 - packages/frontend/src/routes/Pieces.tsx | 176 +++++++++++++++++++++++- 2 files changed, 172 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c9b327c..36e6fa2 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -24,8 +24,6 @@ export const Config = Schema.Struct({ ), OAUTH_AUTHORIZATION_ENDPOINT: Schema.String, OAUTH_TOKEN_ENDPOINT: Schema.String, - POCKET_ID_API_ORIGIN: Schema.String, - POCKET_ID_API_KEY: Schema.String, }); export type Config = typeof Config.Type; diff --git a/packages/frontend/src/routes/Pieces.tsx b/packages/frontend/src/routes/Pieces.tsx index ef925ea..d50903a 100644 --- a/packages/frontend/src/routes/Pieces.tsx +++ b/packages/frontend/src/routes/Pieces.tsx @@ -6,11 +6,15 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { useLoading } from "@/hooks/useLoading"; -import { authors, created, DEBOUNCE, modified } from "@/snippets"; +import { Updater } from "@/hooks/useStore"; +import { authors, created, DEBOUNCE, modified, SAVE_DELAY } from "@/snippets"; +import clsx from "clsx"; import { PieceId } from "common"; -import { Cause, Effect, Match, Option, Scope } from "effect"; -import { Loader2, Plus } from "lucide-react"; -import { FormEventHandler, useId, useRef, useState } from "react"; +import * as Body from "common/Body"; +import { getMediaTypeForFilename } from "common/MediaType"; +import { Array, Cause, Effect, Fiber, Iterable, Match, Option, Order, pipe, Scope, SortedMap } from "effect"; +import { Import, Loader2, Plus, UploadCloud } from "lucide-react"; +import { DragEventHandler, FormEventHandler, useId, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; export function Pieces() { @@ -18,6 +22,8 @@ export function Pieces() { const [name, setName] = useState(""); const [author, setAuthor] = useState(""); + const [importDialogOpen, setImportDialogOpen] = useState(false); + const debounce = useRef(Effect.void); const { isLoading, error, data: pieceIds } = useLoading(Effect.gen(function* () { @@ -42,6 +48,14 @@ export function Pieces() { + + + + + + ); } + +namespace ImportPiecesDialogContent { + export interface Props { + readonly setDialogOpen: Updater; + } +} + +function ImportPiecesDialogContent(props: ImportPiecesDialogContent.Props) { + + const [isLoading, setIsLoading] = useState(false); + + const [attachments, setAttachments] = useState>(() => SortedMap.empty(Order.string)); + + const onDragOver: DragEventHandler = (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + }; + + const onDrop: DragEventHandler = (e) => Effect.gen(function* () { + e.preventDefault(); + + setAttachments((value) => { + for (const file of e.dataTransfer.files) { + const mediaType = getMediaTypeForFilename(file.name); + if (mediaType === undefined) { + continue; + } + + const name = file.name.substring(0, file.name.lastIndexOf(".")); + + const entry = pipe( + value, + SortedMap.get(name), + Option.getOrElse(Array.empty), + Array.append(file), + ); + + value = SortedMap.set(value, name, entry); + } + + return value; + }); + }).pipe(Effect.runPromise); + + const onSubmit: FormEventHandler = (e) => Effect.gen(function* () { + e.preventDefault(); + + if (isLoading) { + return; + } + + const delay = yield* Effect.fork(SAVE_DELAY); + + yield* Effect.scopedWith((scope) => Effect.gen(function* () { + + yield* Scope.addFinalizer(scope, Effect.gen(function* () { + yield* Fiber.join(delay); + setIsLoading(false); + })); + + setIsLoading(true); + + yield* pipe( + attachments, + Iterable.map(([name, attachments]) => Effect.gen(function* () { + + const { pieceId } = yield* client.createPiece({ + name, + composer: Option.none(), + arranger: Option.none(), + lyricist: Option.none(), + }); + + yield* pipe( + attachments, + Array.map((file) => Effect.gen(function* () { + const mediaType = getMediaTypeForFilename(file.name); + if (mediaType === undefined) { + return; + } + + // NOTE Apparently, file.bytes is not a thing in this context + const data = new Uint8Array(yield* Body.arrayBuffer(file)); + + yield* client.createAttachment({ + pieceId, + data, + filename: file.name, + mediaType, + }); + })), + Effect.allWith({ concurrency: "unbounded" }), + ); + })), + Effect.allWith({ concurrency: "unbounded" }), + ); + + props.setDialogOpen(false); + })); + }).pipe(Effect.runPromise); + + return ( + +
+ + Importuj utwory + +
+ {SortedMap.size(attachments) > 0 && ( +
+ + + + Tytuł + Liczba plików + + + + {pipe( + attachments, + SortedMap.entries, + Iterable.map(([name, files]) => ( + + {name} + {files.length} + + )), + )} + +
+
+ )} +
+ +
Przeciągnij i upuść tutaj załączniki
+
+
+ + + +
+
+ ); +}