import { Attachment, denormalizeSystemInformation, type Piece, pieceCache, User } from "@/cache"; import { client } from "@/client"; import { Button } 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 { Label } from "@radix-ui/react-label"; import clsx from "clsx"; import { PieceId } from "common"; import { getMediaTypeForFilename } from "common/MediaType"; import { Cause, Effect, Option } from "effect"; import { constant } from "effect/Function"; import { Download, Loader2, Trash, UploadCloud } from "lucide-react"; import { DragEventHandler, FormEventHandler, MouseEvent, useCallback, useId, useState } from "react"; import { Link, useParams } from "react-router-dom"; export function Piece() { const id = PieceId(useParams().pieceId!); const { isLoading, error, data, setData } = useLoadingEffect(Effect.uninterruptible(pieceCache.get(id))); const setAttachments = useCallback((action: Update) => { setData!(mapProp("attachments", action)); Effect.runFork(pieceCache.invalidate(id)); }, [setData]); if (isLoading) { return (
Ładowanie…
); } return (
{error !== null ? ( Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}` ) : (<>

Utwór

Załączniki

)}
); } namespace PieceForm { export interface Props { readonly piece: Piece; } } function PieceForm(props: PieceForm.Props) { const [name, setName] = useState(props.piece.name); const [composer, setComposer] = useState(() => Option.getOrElse(props.piece.composer, constant(""))); const [lyricist, setLyricist] = useState(() => Option.getOrElse(props.piece.lyricist, constant(""))); const [arranger, setArranger] = useState(() => Option.getOrElse(props.piece.arranger, constant(""))); const nameId = useId(); const composerId = useId(); const lyricistId = useId(); const arrangerId = useId(); const [isLoading, setIsLoading] = useState(false); const onSubmit: FormEventHandler = async (e) => { e.preventDefault(); const delay = timeout(250); try { setIsLoading(true); const { error } = await client.piece({ pieceId: props.piece.pieceId }).put({ name, composer: composer.length > 0 ? composer : null, lyricist: lyricist.length > 0 ? lyricist : null, arranger: arranger.length > 0 ? arranger : null, }); if (error !== null) { console.error(error.value); return; } } finally { await delay; setIsLoading(false); } } return (
setName(e.target.value)} /> setComposer(e.target.value)} /> setLyricist(e.target.value)} /> setArranger(e.target.value)} />
); } namespace Attachments { export interface Props { readonly pieceId: PieceId; readonly attachments: readonly Attachment[]; readonly setAttachments: Updater; } } function Attachments(props: Attachments.Props) { return (
Nazwa pliku Typ Dodano Zmodyfikowano Akcje {props.attachments.map((attachment) => ( ))}
); } namespace AttachmentRow { export interface Props { readonly attachment: Attachment; readonly setAttachments: Updater; } } function AttachmentRow(props: AttachmentRow.Props) { const download = useCallback(async () => { const { error, data } = await client .piece({ pieceId: props.attachment.pieceId }) .attachment({ attachmentId: props.attachment.attachmentId }) .get() if (error !== null) { console.error(error.value); return; } const url = URL.createObjectURL(data); const a = document.createElement("a"); a.href = url; a.download = props.attachment.filename; // TODO Use `data.name` after Content-Disposition parser is implemented a.click(); URL.revokeObjectURL(url); }, [props.attachment.attachmentId, props.attachment.pieceId]); const open = useCallback(async (event: MouseEvent) => { if (props.attachment.mediaType !== "application/pdf") { return; } event.preventDefault(); const { error, data } = await client .piece({ pieceId: props.attachment.pieceId }) .attachment({ attachmentId: props.attachment.attachmentId }) .get(); if (error !== null) { console.error(error.value); return; } const url = URL.createObjectURL(data); window.open(url, "_target"); URL.revokeObjectURL(url); }, [props.attachment.mediaType, props.attachment.attachmentId, props.attachment.pieceId]); const doDelete = useCallback(async () => { const { error } = await client .piece({ pieceId: props.attachment.pieceId }) .attachment({ attachmentId: props.attachment.attachmentId }) .delete(); if (error !== null) { console.error(error.value); return; } props.setAttachments((prev) => prev.filter((a) => a.attachmentId !== props.attachment.attachmentId)); }, [props.attachment.attachmentId, props.attachment.pieceId]); return ( {props.attachment.mediaType === "application/vnd.recordare.musicxml" || props.attachment.mediaType === "application/vnd.recordare.musicxml+xml" || props.attachment.mediaType === "application/pdf" ? ( {props.attachment.filename} ) : ( props.attachment.filename )} {props.attachment.mediaType} {props.attachment.createdAt} {Option.isSome(props.attachment.createdBy) && <>
przez {props.attachment.createdBy.value.username}}
{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}}
); } namespace AttachmentForm { export interface Props { readonly pieceId: PieceId; readonly setAttachments: Updater; } } function AttachmentForm(props: AttachmentForm.Props) { const [isLoading, setIsLoading] = useState(false); const onDragOver: DragEventHandler = async (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }; const onDrop: DragEventHandler = async (e) => { e.preventDefault(); if (isLoading) { return; } const delay = timeout(250); try { setIsLoading(true); for (const file of e.dataTransfer.files) { const mediaType = getMediaTypeForFilename(file.name); if (mediaType === undefined) { continue; } const { data, error } = await client.piece({ pieceId: props.pieceId }).attachment.post({ filename: file.name, mediaType, data: file, }); if (error !== null) { console.error(error.value); continue; } const attachment = await Effect.runPromise(denormalizeSystemInformation(data)); props.setAttachments((prev) => { const next = [...prev, attachment]; next.sort((a, b) => a.filename.localeCompare(b.filename)); return next; }); } } finally { await delay; setIsLoading(false); } } return (
{isLoading ? (<>
Wysyłanie załączników…
) : (<>
Przeciągnij i upuść tutaj załączniki
)}
); }