Redesign UI with shadcn

This commit is contained in:
2024-12-01 13:46:34 +01:00
parent 8c13f50378
commit 27581d0057
25 changed files with 1758 additions and 376 deletions

View File

@@ -18,6 +18,10 @@
},
"dependencies": {
"@elysiajs/eden": "catalog:",
"@radix-ui/react-dialog": "catalog:",
"@radix-ui/react-dropdown-menu": "catalog:",
"@radix-ui/react-label": "catalog:",
"@radix-ui/react-slot": "catalog:",
"clsx": "catalog:",
"common": "workspace:^",
"lucide-react": "catalog:",

View File

@@ -1,5 +1,5 @@
import { mapProp, Update } from "@/hooks/useStore";
import { getMediaTypeForFile } from "common/MediaType";
import { mapProp, Update } from "./store";
export function FileReducer(prev: FileReducer.State, action: FileReducer.Action): FileReducer.State {
switch (action.type) {

View File

@@ -1,13 +1,14 @@
import { StrictMode } from "react";
import { Home } from "@/routes/Home";
import { Login } from "@/routes/Login";
import { Piece } from "@/routes/Piece";
import { Root } from "@/routes/Root";
import { lazy, StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Attachment } from "./routes/Attachment";
import { Home } from "./routes/Home";
import { Login } from "./routes/Login";
import { Piece } from "./routes/Piece";
import { Root } from "./routes/Root";
import "./style.css";
const Attachment = lazy(() => import("@/routes/Attachment"));
const router = createBrowserRouter([
{
path: "/",

View File

@@ -0,0 +1,52 @@
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { forwardRef } from "react";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:ring-offset-stone-950 dark:focus-visible:ring-stone-300",
{
variants: {
variant: {
default: "bg-stone-900 text-stone-50 hover:bg-stone-900/90 dark:bg-stone-50 dark:text-stone-900 dark:hover:bg-stone-50/90",
destructive: "bg-red-500 text-stone-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-stone-50 dark:hover:bg-red-900/90",
outline: "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",
secondary: "bg-stone-100 text-stone-900 hover:bg-stone-100/80 dark:bg-stone-800 dark:text-stone-50 dark:hover:bg-stone-800/80",
ghost: "hover:bg-stone-100 hover:text-stone-900 dark:hover:bg-stone-800 dark:hover:text-stone-50",
link: "text-stone-900 underline-offset-4 hover:underline dark:text-stone-50",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,79 @@
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
const Card = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-stone-200 bg-white text-stone-950 shadow-sm dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
className
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-stone-500 dark:text-stone-400", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View File

@@ -0,0 +1,107 @@
import { cn } from "@/lib/utils";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { forwardRef } from "react";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-stone-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-stone-800 dark:bg-stone-950",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-stone-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-stone-100 data-[state=open]:text-stone-500 dark:ring-offset-stone-950 dark:focus:ring-stone-300 dark:data-[state=open]:bg-stone-800 dark:data-[state=open]:text-stone-400">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-stone-500 dark:text-stone-400", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger
};

View File

@@ -0,0 +1,179 @@
import { cn } from "@/lib/utils";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { forwardRef } from "react";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean,
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-stone-100 data-[state=open]:bg-stone-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus:bg-stone-800 dark:data-[state=open]:bg-stone-800",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-stone-200 bg-white p-1 text-stone-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-stone-800 dark:bg-stone-950 dark:text-stone-50",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean,
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus:bg-stone-800 dark:focus:text-stone-50",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-stone-100 focus:text-stone-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-stone-800 dark:focus:text-stone-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-stone-100 dark:bg-stone-800", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuTrigger
};

View File

@@ -0,0 +1,21 @@
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-stone-200 bg-white px-3 py-2 text-base ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-stone-950 placeholder:text-stone-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-stone-800 dark:bg-stone-950 dark:ring-offset-stone-950 dark:file:text-stone-50 dark:placeholder:text-stone-400 dark:focus-visible:ring-stone-300",
className
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,24 @@
import { cn } from "@/lib/utils";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { forwardRef } from "react";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@@ -0,0 +1,109 @@
import { cn } from "@/lib/utils";
import { forwardRef } from "react";
const Table = forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-stone-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-stone-800/50",
className
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-stone-100/50 data-[state=selected]:bg-stone-100 dark:hover:bg-stone-800/50 dark:data-[state=selected]:bg-stone-800",
className
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-stone-500 [&:has([role=checkbox])]:pr-0 dark:text-stone-400",
className
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-1 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-stone-500 dark:text-stone-400", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow
};

View File

@@ -1,8 +1,8 @@
import { useStore } from "@/hooks/useStore";
import { Treaty } from "@elysiajs/eden";
import { ELYSIA_FORM_DATA } from "elysia";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useStore } from "./store";
type FormDataFix<T> = T extends { [ELYSIA_FORM_DATA]: infer U } ? U : T;

View File

@@ -4,3 +4,7 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function timeout(timeMs: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, timeMs));
}

View File

@@ -1 +0,0 @@
export { OpenSheetMusicDisplay as default } from "opensheetmusicdisplay";

View File

@@ -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" />;
};

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -1,17 +0,0 @@
import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react";
export namespace Button {
export type Props = Omit<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "className">;
}
export const Button = forwardRef<HTMLButtonElement, Button.Props>(function Button({ children, ...props }, ref) {
return (
<button
{...props}
ref={ref}
className="p-2 bg-stone-300 border-2 border-t-stone-200 border-l-stone-200 border-r-stone-600 border-b-stone-600 active:border-t-stone-600 active:border-l-stone-600 active:border-r-stone-200 active:border-b-stone-200 rounded focus:outline focus:outline-2 focus:outline-red-500"
>
{children}
</button>
);
});

View File

@@ -1,17 +0,0 @@
import { DetailedHTMLProps, forwardRef, InputHTMLAttributes } from "react";
export namespace Input {
export type Props = Omit<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "className">;
}
export const Input = forwardRef<HTMLInputElement, Input.Props>(function Input({ children, ...props }, ref) {
return (
<input
{...props}
ref={ref}
className="w-[32ch] p-2 bg-transparent border rounded focus:outline focus:outline-2 focus:outline-red-500"
>
{children}
</input>
);
});

View File

@@ -1,3 +0,0 @@
export function timeout(timeMs: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, timeMs));
}

View File

@@ -2,10 +2,13 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["react", "react-dom"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"common": ["../common/src/index.ts"],
"common/*": ["../common/src/*.ts"],
"backend": ["../backend/src/index.ts"],
"backend/*": ["../backend/src/*.ts"],
},
},
"references": [