Redesign UI with shadcn
This commit is contained in:
@@ -1,66 +1,60 @@
|
||||
import { client } from "@/client";
|
||||
import { useLoading } from "@/hooks/useLoading.ts";
|
||||
import { AttachmentId, PieceId } from "common";
|
||||
import { lazy, useEffect, useRef } from "react";
|
||||
import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { client } from "../client";
|
||||
import { useLoading } from "../loading";
|
||||
|
||||
export const Attachment = lazy(async () => {
|
||||
export default function Attachment() {
|
||||
|
||||
const { default: OpenSheetMusicDisplay } = await import("../opensheetmusicdisplay.ts");
|
||||
const params = useParams();
|
||||
const pieceId = PieceId(params.pieceId!);
|
||||
const attachmentId = AttachmentId(params.attachmentId!);
|
||||
|
||||
return {
|
||||
default: function Attachment() {
|
||||
const { isLoading, error, data } = useLoading(() => client.piece({ pieceId }).attachment({ attachmentId }).get());
|
||||
|
||||
const params = useParams();
|
||||
const pieceId = PieceId(params.pieceId!);
|
||||
const attachmentId = AttachmentId(params.attachmentId!);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isLoading, error, data } = useLoading(() => client.piece({ pieceId }).attachment({ attachmentId }).get());
|
||||
useEffect(() => {
|
||||
if (isLoading || error !== null) return;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const url = URL.createObjectURL(data?.data);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || error !== null) return;
|
||||
const render = () => osmd.render();
|
||||
|
||||
const url = URL.createObjectURL(data?.data);
|
||||
const osmd = new OpenSheetMusicDisplay(containerRef.current!, {
|
||||
autoResize: false,
|
||||
drawTitle: false,
|
||||
drawComposer: false,
|
||||
drawMeasureNumbers: true,
|
||||
drawMeasureNumbersOnlyAtSystemStart: true,
|
||||
//measureNumberInterval: 5,
|
||||
//renderSingleHorizontalStaffline: true,
|
||||
});
|
||||
|
||||
const render = () => osmd.render();
|
||||
osmd.load(url).then(render);
|
||||
|
||||
const osmd = new OpenSheetMusicDisplay(containerRef.current!, {
|
||||
autoResize: false,
|
||||
drawTitle: false,
|
||||
drawComposer: false,
|
||||
drawMeasureNumbers: true,
|
||||
drawMeasureNumbersOnlyAtSystemStart: true,
|
||||
//measureNumberInterval: 5,
|
||||
//renderSingleHorizontalStaffline: true,
|
||||
});
|
||||
window.addEventListener("resize", render);
|
||||
|
||||
osmd.load(url).then(render);
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
window.removeEventListener("resize", render);
|
||||
};
|
||||
}, [isLoading, data]);
|
||||
|
||||
window.addEventListener("resize", render);
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden flex items-center justify-center">
|
||||
<div>Ładowanie…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
window.removeEventListener("resize", render);
|
||||
};
|
||||
}, [isLoading, data]);
|
||||
if (error !== null) {
|
||||
<div className="w-full h-full overflow-hidden flex items-center justify-center">
|
||||
<div>Wystąpił błąd: {error.value}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden flex items-center justify-center">
|
||||
<div>Ładowanie…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error !== null) {
|
||||
<div className="w-full h-full overflow-hidden flex items-center justify-center">
|
||||
<div>Wystąpił błąd: {error.value}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div ref={containerRef} className="w-full h-full overflow-scroll" />;
|
||||
},
|
||||
};
|
||||
});
|
||||
return <div ref={containerRef} className="w-full h-full overflow-scroll" />;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { FormEventHandler, ReactNode, useId, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { client } from "../client";
|
||||
import { useLoading } from "../loading";
|
||||
import { Button } from "../styled/Button";
|
||||
import { Input } from "../styled/Input";
|
||||
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 { useLoading } from "@/hooks/useLoading";
|
||||
import { Loader2, Plus } from "lucide-react";
|
||||
import { FormEventHandler, ReactNode, useId, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
export function Home() {
|
||||
|
||||
@@ -18,20 +22,28 @@ export function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 overflow-y-auto flex flex-wrap items-start gap-4">
|
||||
<div className="p-4 overflow-y-auto flex flex-col items-start gap-4">
|
||||
{error !== null ? (
|
||||
`Wystąpił błąd: ${error.value}`
|
||||
) : (
|
||||
<table className="grow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-1 border">Tytuł</th>
|
||||
<th className="p-1 border">Twórcy</th>
|
||||
<th className="p-1 border">Dodano</th>
|
||||
<th className="p-1 border">Zmodyfikowano</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
) : (<>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Plus />Dodaj utwór
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<AddPieceDialogContent />
|
||||
</Dialog>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Tytuł</TableHead>
|
||||
<TableHead>Twórcy</TableHead>
|
||||
<TableHead className="text-center">Dodano</TableHead>
|
||||
<TableHead className="text-center">Zmodyfikowano</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((piece) => {
|
||||
const composerParts: ReactNode[] = [];
|
||||
if (piece.composer !== null) composerParts.push(piece.composer);
|
||||
@@ -39,31 +51,32 @@ export function Home() {
|
||||
if (piece.lyricist !== null) composerParts.push(`słowa: ${piece.lyricist}`);
|
||||
if (composerParts.length === 0) composerParts.push(<em>Nieznani</em>);
|
||||
return (
|
||||
<tr key={piece.pieceId}>
|
||||
<td className="p-1 border"><Link className="underline" to={`piece/${piece.pieceId}`}>{piece.name}</Link></td>
|
||||
<td className="p-1 border">{...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x, <br />] : [x])}</td>
|
||||
<td className="p-1 border text-center font-mono text-sm">
|
||||
<TableRow key={piece.pieceId}>
|
||||
<TableCell><Link className="underline" to={`piece/${piece.pieceId}`}>{piece.name}</Link></TableCell>
|
||||
<TableCell>{...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x, <br />] : [x])}</TableCell>
|
||||
<TableCell className="text-center font-mono text-xs">
|
||||
{piece.createdAt}
|
||||
{piece.createdBy !== null && <><br />przez {piece.createdBy}</>}
|
||||
</td>
|
||||
<td className="p-1 border text-center font-mono text-sm">
|
||||
</TableCell>
|
||||
<TableCell className="text-center font-mono text-xs">
|
||||
{piece.modifiedAt === null && piece.modifiedBy === null ? "\u2014"
|
||||
: piece.modifiedAt !== null && piece.modifiedBy === null ? piece.modifiedAt
|
||||
: piece.modifiedAt === null ? `przez ${piece.createdBy}`
|
||||
: <>{piece.createdAt}<br />przez {piece.createdBy}</>}
|
||||
</td>
|
||||
</tr>
|
||||
: piece.modifiedAt !== null && piece.modifiedBy === null ? piece.modifiedAt
|
||||
: piece.modifiedAt === null ? `przez ${piece.createdBy}`
|
||||
: <>{piece.createdAt}<br />przez {piece.createdBy}</>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<PieceForm />
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PieceForm() {
|
||||
function AddPieceDialogContent() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [composer, setComposer] = useState("");
|
||||
@@ -75,66 +88,81 @@ function PieceForm() {
|
||||
const lyricistId = useId();
|
||||
const arrangerId = useId();
|
||||
|
||||
const autoFocusRef = useRef<HTMLInputElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { error } = await client.piece.post({
|
||||
name,
|
||||
composer: composer.length > 0 ? composer : null,
|
||||
lyricist: lyricist.length > 0 ? lyricist : null,
|
||||
arranger: arranger.length > 0 ? arranger : null,
|
||||
});
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (error) {
|
||||
console.error(error.value);
|
||||
return;
|
||||
const { data, error } = await client.piece.post({
|
||||
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;
|
||||
}
|
||||
|
||||
navigate(`piece/${data.pieceId}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setName("");
|
||||
setComposer("");
|
||||
setLyricist("");
|
||||
setArranger("");
|
||||
|
||||
autoFocusRef.current?.focus();
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="p-2 flex flex-col gap-2 border rounded" onSubmit={onSubmit}>
|
||||
<label htmlFor={nameId}>Tytuł</label>
|
||||
<Input
|
||||
ref={autoFocusRef}
|
||||
id={nameId}
|
||||
type="text"
|
||||
value={name}
|
||||
required
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={composerId}>Kompozytor</label>
|
||||
<Input
|
||||
id={composerId}
|
||||
type="text"
|
||||
value={composer}
|
||||
onChange={(e) => setComposer(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={lyricistId}>Słowa</label>
|
||||
<Input
|
||||
id={lyricistId}
|
||||
type="text"
|
||||
value={lyricist}
|
||||
onChange={(e) => setLyricist(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={arrangerId}>Opracowanie</label>
|
||||
<Input
|
||||
id={arrangerId}
|
||||
type="text"
|
||||
value={arranger}
|
||||
onChange={(e) => setArranger(e.target.value)}
|
||||
/>
|
||||
<Button type="submit">
|
||||
Dodaj
|
||||
</Button>
|
||||
</form>
|
||||
<DialogContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dodaj utwór</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid items-baseline grid-cols-4 gap-4 py-4">
|
||||
<Label htmlFor={nameId} className="text-right">Tytuł</Label>
|
||||
<Input
|
||||
id={nameId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={name}
|
||||
autoFocus
|
||||
required
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor={composerId} className="text-right">Kompozytor</Label>
|
||||
<Input
|
||||
id={composerId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={composer}
|
||||
onChange={(e) => setComposer(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor={lyricistId} className="text-right">Słowa</Label>
|
||||
<Input
|
||||
id={lyricistId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={lyricist}
|
||||
onChange={(e) => setLyricist(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor={arrangerId} className="text-right">Opracowanie</Label>
|
||||
<Input
|
||||
id={arrangerId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={arranger}
|
||||
onChange={(e) => setArranger(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
Dodaj
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { FormEventHandler, useId } from "react";
|
||||
import { client } from "@/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useStore } from "@/hooks/useStore";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { FormEventHandler, useId, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { client } from "../client";
|
||||
import { useStore } from "../store";
|
||||
import { Button } from "../styled/Button";
|
||||
import { Input } from "../styled/Input";
|
||||
|
||||
export function Login() {
|
||||
|
||||
@@ -20,50 +23,70 @@ export function Login() {
|
||||
const usernameId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { data, error } = await client.login.post({
|
||||
username: loginUsername,
|
||||
password: loginPassword,
|
||||
});
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (error) {
|
||||
console.error(error.value);
|
||||
return;
|
||||
const { data, error } = await client.login.post({
|
||||
username: loginUsername,
|
||||
password: loginPassword,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error.value);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginUsername("");
|
||||
setLoginPassword("");
|
||||
setUser(data);
|
||||
|
||||
navigate("/");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setLoginUsername("");
|
||||
setLoginPassword("");
|
||||
setUser(data);
|
||||
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<form className="p-2 flex flex-col gap-2 border rounded" onSubmit={onSubmit}>
|
||||
<header className="pb-2 border-b text-center font-bold">Repozytorium muzyczne</header>
|
||||
<label htmlFor={usernameId}>Nazwa użytkownika</label>
|
||||
<Input
|
||||
id={usernameId}
|
||||
type="text"
|
||||
value={loginUsername}
|
||||
autoFocus
|
||||
required
|
||||
onChange={(e) => setLoginUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={passwordId}>Hasło</label>
|
||||
<Input
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={loginPassword}
|
||||
required
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
/>
|
||||
<Button type="submit">
|
||||
Zaloguj się
|
||||
</Button>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Repozytorium muzyczne</CardTitle>
|
||||
<CardDescription>Zaloguj się, aby kontynuować</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Label htmlFor={usernameId}>Nazwa użytkownika</Label>
|
||||
<Input
|
||||
id={usernameId}
|
||||
className="w-[32ch]"
|
||||
type="text"
|
||||
value={loginUsername}
|
||||
autoFocus
|
||||
required
|
||||
onChange={(e) => setLoginUsername(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor={passwordId}>Hasło</Label>
|
||||
<Input
|
||||
id={passwordId}
|
||||
className="w-[32ch]"
|
||||
type="password"
|
||||
value={loginPassword}
|
||||
required
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
Zaloguj się
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
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 { FileReducer } from "@/FileReducer";
|
||||
import { useLoading } from "@/hooks/useLoading";
|
||||
import { timeout } from "@/lib/utils";
|
||||
import { Label } from "@radix-ui/react-label";
|
||||
import type { Attachment, Piece } from "backend/database";
|
||||
import { PieceId } from "common";
|
||||
import { ACCEPTED_EXTENSIONS } from "common/MediaType";
|
||||
import { ELYSIA_FORM_DATA } from "elysia";
|
||||
import { Download, Loader2, Trash } from "lucide-react";
|
||||
import { FormEventHandler, MouseEvent, useCallback, useId, useReducer, useRef, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { client } from "../client";
|
||||
import { FileReducer } from "../FileReducer";
|
||||
import { useLoading } from "../loading";
|
||||
import { Button } from "../styled/Button";
|
||||
import { Input } from "../styled/Input";
|
||||
|
||||
export function Piece() {
|
||||
|
||||
@@ -32,9 +36,13 @@ export function Piece() {
|
||||
) : piece.data[0] === undefined ? (
|
||||
"Utwór nie istnieje"
|
||||
) : (<>
|
||||
<PieceForm piece={piece.data[0]} />
|
||||
<div className="flex flex-col gap-4 p-4 border rounded">
|
||||
<h3 className="font-bold text-lg">Utwór</h3>
|
||||
<PieceForm piece={piece.data[0]} />
|
||||
<h3 className="font-bold text-lg">Załączniki</h3>
|
||||
<AttachmentForm pieceId={piece.data[0].pieceId} />
|
||||
</div>
|
||||
<Attachments pieceId={piece.data[0].pieceId} attachments={attachments.data} />
|
||||
<AttachmentForm pieceId={piece.data[0].pieceId} />
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
@@ -58,61 +66,75 @@ function PieceForm(props: PieceForm.Props) {
|
||||
const lyricistId = useId();
|
||||
const arrangerId = useId();
|
||||
|
||||
const autoFocusRef = useRef<HTMLInputElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
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,
|
||||
});
|
||||
const delay = timeout(250);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (error) {
|
||||
console.error(error.value);
|
||||
return;
|
||||
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) {
|
||||
console.error(error.value);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
await delay;
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
autoFocusRef.current?.focus();
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="p-2 flex flex-col gap-2 border rounded" onSubmit={onSubmit}>
|
||||
<label htmlFor={nameId}>Tytuł</label>
|
||||
<Input
|
||||
ref={autoFocusRef}
|
||||
id={nameId}
|
||||
type="text"
|
||||
value={name}
|
||||
required
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={composerId}>Kompozytor</label>
|
||||
<Input
|
||||
id={composerId}
|
||||
type="text"
|
||||
value={composer}
|
||||
onChange={(e) => setComposer(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={lyricistId}>Słowa</label>
|
||||
<Input
|
||||
id={lyricistId}
|
||||
type="text"
|
||||
value={lyricist}
|
||||
onChange={(e) => setLyricist(e.target.value)}
|
||||
/>
|
||||
<label htmlFor={arrangerId}>Opracowanie</label>
|
||||
<Input
|
||||
id={arrangerId}
|
||||
type="text"
|
||||
value={arranger}
|
||||
onChange={(e) => setArranger(e.target.value)}
|
||||
/>
|
||||
<Button type="submit">
|
||||
Zapisz
|
||||
</Button>
|
||||
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||
<div className="grid items-baseline grid-cols-4 gap-4">
|
||||
<Label htmlFor={nameId} className="text-right">Tytuł</Label>
|
||||
<Input
|
||||
id={nameId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={name}
|
||||
required
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor={composerId} className="text-right">Kompozytor</Label>
|
||||
<Input
|
||||
id={composerId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={composer}
|
||||
onChange={(e) => setComposer(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor={lyricistId} className="text-right">Słowa</Label>
|
||||
<Input
|
||||
id={lyricistId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={lyricist}
|
||||
onChange={(e) => setLyricist(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor={arrangerId} className="text-right">Opracowanie</Label>
|
||||
<Input
|
||||
id={arrangerId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={arranger}
|
||||
onChange={(e) => setArranger(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col-reverse items-end">
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
Zapisz
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -126,20 +148,22 @@ namespace Attachments {
|
||||
|
||||
function Attachments(props: Attachments.Props) {
|
||||
return (
|
||||
<table className="grow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-1 border">Nazwa pliku</th>
|
||||
<th className="p-1 border">Typ</th>
|
||||
<th className="p-1 border">Dodano</th>
|
||||
<th className="p-1 border">Zmodyfikowano</th>
|
||||
<th className="p-1 border">Pobierz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.attachments.map((attachment) => <AttachmentRow key={attachment.attachmentId} attachment={attachment} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="grow">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Nazwa pliku</TableHead>
|
||||
<TableHead>Typ</TableHead>
|
||||
<TableHead className="text-center">Dodano</TableHead>
|
||||
<TableHead className="text-center">Zmodyfikowano</TableHead>
|
||||
<TableHead className="text-center">Akcje</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{props.attachments.map((attachment) => <AttachmentRow key={attachment.attachmentId} attachment={attachment} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,7 +195,13 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
||||
URL.revokeObjectURL(url);
|
||||
}, [props.attachment.attachmentId, props.attachment.pieceId]);
|
||||
|
||||
const open = useCallback(async () => {
|
||||
const open = useCallback(async (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (props.attachment.mediaType !== "application/pdf") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const { error, data: _data } = await client
|
||||
.piece({ pieceId: props.attachment.pieceId })
|
||||
.attachment({ attachmentId: props.attachment.attachmentId })
|
||||
@@ -186,47 +216,41 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
||||
const url = URL.createObjectURL(data.data);
|
||||
window.open(url, "_target");
|
||||
URL.revokeObjectURL(url);
|
||||
}, [props.attachment.attachmentId, props.attachment.pieceId, props.attachment.mediaType]);
|
||||
|
||||
const onOpen = useCallback((event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (props.attachment.mediaType !== "application/pdf") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
open();
|
||||
}, [props.attachment.mediaType, open]);
|
||||
}, [props.attachment.mediaType, props.attachment.attachmentId, props.attachment.pieceId]);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="p-1 border">
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
{props.attachment.mediaType === "application/vnd.recordare.musicxml"
|
||||
|| props.attachment.mediaType === "application/vnd.recordare.musicxml+xml"
|
||||
|| props.attachment.mediaType === "application/pdf" ? (
|
||||
<Link className="underline" to={`attachment/${props.attachment.attachmentId}`} onClick={onOpen}>
|
||||
<Link className="underline" to={`attachment/${props.attachment.attachmentId}`} onClick={open}>
|
||||
{props.attachment.filename}
|
||||
</Link>
|
||||
) : (
|
||||
props.attachment.filename
|
||||
)}
|
||||
</td>
|
||||
<td className="p-1 border">{props.attachment.mediaType}</td>
|
||||
<td className="p-1 border text-center font-mono text-sm">
|
||||
</TableCell>
|
||||
<TableCell>{props.attachment.mediaType}</TableCell>
|
||||
<TableCell className="text-center font-mono text-xs">
|
||||
{props.attachment.createdAt}
|
||||
{props.attachment.createdBy !== null && <><br />przez {props.attachment.createdBy}</>}
|
||||
</td>
|
||||
<td className="p-1 border text-center font-mono text-sm">
|
||||
</TableCell>
|
||||
<TableCell className="text-center font-mono text-xs">
|
||||
{props.attachment.modifiedAt === null && props.attachment.modifiedBy === null ? "\u2014"
|
||||
: props.attachment.modifiedAt !== null && props.attachment.modifiedBy === null ? props.attachment.modifiedAt
|
||||
: props.attachment.modifiedAt === null ? `przez ${props.attachment.createdBy}`
|
||||
: <>{props.attachment.createdAt}<br />przez {props.attachment.createdBy}</>}
|
||||
</td>
|
||||
<td className="p-1 border text-center">
|
||||
<Button type="button" onClick={download}>
|
||||
Pobierz
|
||||
: props.attachment.modifiedAt === null ? `przez ${props.attachment.createdBy}`
|
||||
: <>{props.attachment.createdAt}<br />przez {props.attachment.createdBy}</>}
|
||||
</TableCell>
|
||||
<TableCell className="text-center flex justify-center gap-4">
|
||||
<Button type="button" variant="ghost" size="icon" title="Pobierz" onClick={download}>
|
||||
<Download />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<Button type="button" variant="ghost" size="icon" title="Usuń">
|
||||
<Trash />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -246,57 +270,73 @@ function AttachmentForm(props: AttachmentForm.Props) {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { error } = await client.piece({ pieceId: props.pieceId }).attachment.post({
|
||||
filename,
|
||||
mediaType,
|
||||
data: file!,
|
||||
});
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (error) {
|
||||
console.error(error.value);
|
||||
return;
|
||||
}
|
||||
const { error } = await client.piece({ pieceId: props.pieceId }).attachment.post({
|
||||
filename,
|
||||
mediaType,
|
||||
data: file!,
|
||||
});
|
||||
|
||||
reduce(FileReducer.reset);
|
||||
if (fileInputRef.current !== null) {
|
||||
fileInputRef.current.files = null;
|
||||
if (error) {
|
||||
console.error(error.value);
|
||||
return;
|
||||
}
|
||||
|
||||
reduce(FileReducer.reset);
|
||||
if (fileInputRef.current !== null) {
|
||||
fileInputRef.current.files = null;
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="p-2 flex flex-col gap-2 border rounded" onSubmit={onSubmit}>
|
||||
<label htmlFor={filenameId}>Nazwa pliku</label>
|
||||
<Input
|
||||
id={filenameId}
|
||||
type="text"
|
||||
value={filename}
|
||||
required
|
||||
onChange={(e) => reduce(FileReducer.setFilename(e.target.value))}
|
||||
/>
|
||||
<label htmlFor={mediaTypeId}>Typ</label>
|
||||
<Input
|
||||
id={mediaTypeId}
|
||||
type="text"
|
||||
value={mediaType}
|
||||
onChange={(e) => reduce(FileReducer.setMediaType(e.target.value))}
|
||||
/>
|
||||
<label htmlFor={fileId}>Plik</label>
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
id={fileId}
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.item(0) ?? null;
|
||||
reduce(FileReducer.setFile(file));
|
||||
}}
|
||||
accept={ACCEPTED_EXTENSIONS}
|
||||
/>
|
||||
<Button type="submit">
|
||||
Dodaj
|
||||
</Button>
|
||||
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
|
||||
<div className="grid items-baseline grid-cols-4 gap-4">
|
||||
<Label htmlFor={filenameId} className="text-right">Nazwa pliku</Label>
|
||||
<Input
|
||||
id={filenameId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={filename}
|
||||
required
|
||||
onChange={(e) => reduce(FileReducer.setFilename(e.target.value))}
|
||||
/>
|
||||
<Label htmlFor={mediaTypeId} className="text-right">Typ</Label>
|
||||
<Input
|
||||
id={mediaTypeId}
|
||||
className="col-span-3"
|
||||
type="text"
|
||||
value={mediaType}
|
||||
onChange={(e) => reduce(FileReducer.setMediaType(e.target.value))}
|
||||
/>
|
||||
<Label htmlFor={fileId} className="text-right">Plik</Label>
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
id={fileId}
|
||||
className="col-span-3"
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.item(0) ?? null;
|
||||
reduce(FileReducer.setFile(file));
|
||||
}}
|
||||
accept={ACCEPTED_EXTENSIONS}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col-reverse items-end">
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
Dodaj
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { client } from "@/client";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { useStore } from "@/hooks/useStore";
|
||||
import { LogOut, Settings, User } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Link, Outlet, useNavigate } from "react-router-dom";
|
||||
import { client } from "../client";
|
||||
import { useStore } from "../store";
|
||||
import { Button } from "../styled/Button";
|
||||
|
||||
export function Root() {
|
||||
|
||||
@@ -47,20 +49,25 @@ export function Root() {
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden flex flex-col items-stretch">
|
||||
<div className="flex p-4 justify-between items-baseline">
|
||||
<div>
|
||||
<Link className="underline" to="/">
|
||||
Repozytorium muzyczne
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-4">
|
||||
<div>
|
||||
{user.username}
|
||||
</div>
|
||||
<Button type="button" onClick={onLogoutClick}>
|
||||
Wyloguj się
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex p-4 justify-between items-baseline border-b">
|
||||
<Link className={buttonVariants({ variant: "outline" })} to="/">
|
||||
Repozytorium muzyczne
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="outline">
|
||||
<User />{user.username}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Settings />Ustawienia
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onLogoutClick}>
|
||||
<LogOut />Wyloguj się
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Outlet context={user} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user