Add eslint and fix eslint errors, other minor improvements

This commit is contained in:
Szymon Nowakowski
2024-12-25 17:26:20 +01:00
parent 8c15df8e06
commit 135d54908f
21 changed files with 911 additions and 144 deletions

View File

@@ -13,6 +13,7 @@ RUN pnpm install --frozen-lockfile
COPY . . COPY . .
RUN pnpm exec tsc --build RUN pnpm exec tsc --build
RUN pnpm exec eslint .
RUN pnpm --filter frontend exec vite build RUN pnpm --filter frontend exec vite build
FROM oven/bun:1 FROM oven/bun:1

47
eslint.config.mjs Normal file
View File

@@ -0,0 +1,47 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import stylistic from "@stylistic/eslint-plugin";
export default tseslint.config({
extends: [
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
],
files: ["packages/*/src/**/*.{ts,tsx}"],
plugins: {
"@stylistic": stylistic,
},
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@stylistic/comma-dangle": ["error", "always-multiline"],
"@stylistic/member-delimiter-style": ["error", {
singleline: {
delimiter: "comma",
requireLast: false,
},
multiline: {
delimiter: "comma",
requireLast: true,
},
overrides: {
interface: {
singleline: {
delimiter: "semi",
},
multiline: {
delimiter: "semi",
},
},
},
}],
"@stylistic/semi": ["error", "always"],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
},
});

View File

@@ -11,6 +11,9 @@
"frontend:dev": "pnpm --filter frontend exec vite --open" "frontend:dev": "pnpm --filter frontend exec vite --open"
}, },
"devDependencies": { "devDependencies": {
"typescript": "catalog:" "@eslint/js": "catalog:",
"@stylistic/eslint-plugin": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "catalog:"
} }
} }

View File

@@ -79,11 +79,11 @@ const app = new Elysia()
indexHTML: false, indexHTML: false,
})) }))
.group("/api", (app) => app .group("/api/v1", (app) => app
// --- MARK: AUTHENTICATION -------------------------------------------- // --- MARK: AUTHENTICATION --------------------------------------------
.get("/me", async ({ user }) => { .get("/me", ({ user }) => {
if (user === null) { if (user === null) {
return error("Unauthorized"); return error("Unauthorized");
@@ -235,7 +235,7 @@ const app = new Elysia()
eb("composer", "like", "%" + query.author + "%"), eb("composer", "like", "%" + query.author + "%"),
eb("arranger", "like", "%" + query.author + "%"), eb("arranger", "like", "%" + query.author + "%"),
eb("lyricist", "like", "%" + query.author + "%"), eb("lyricist", "like", "%" + query.author + "%"),
])) ]));
} }
const res = await q.execute(); const res = await q.execute();
@@ -249,7 +249,7 @@ const app = new Elysia()
}), }),
}) })
.get("/piece/:pieceId", async ({ db, query, params: { pieceId }, user }) => { .get("/piece/:pieceId", async ({ db, params: { pieceId }, user }) => {
if (user === null) { if (user === null) {
return error("Unauthorized"); return error("Unauthorized");
@@ -277,7 +277,7 @@ const app = new Elysia()
sha256: Buffer.from(sha256).toString("hex"), sha256: Buffer.from(sha256).toString("hex"),
...rest, ...rest,
})), })),
} };
}, { }, {
params: t.Object({ params: t.Object({
pieceId: tbranded<PieceId>(), pieceId: tbranded<PieceId>(),
@@ -311,7 +311,7 @@ const app = new Elysia()
}), }),
params: t.Object({ params: t.Object({
pieceId: tbranded<PieceId>(), pieceId: tbranded<PieceId>(),
}) }),
}) })
.delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, user }) => { .delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, user }) => {
@@ -471,13 +471,13 @@ const app = new Elysia()
pieceId: tbranded<PieceId>(), pieceId: tbranded<PieceId>(),
attachmentId: tbranded<AttachmentId>(), attachmentId: tbranded<AttachmentId>(),
}), }),
// eslint-disable-next-line @stylistic/comma-dangle -- a comma would confuse the TS compiler here
}) })
) )
.get("*", () => Bun.file("packages/frontend/build/index.html")) .get("*", () => Bun.file("packages/frontend/build/index.html"));
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
.listen(process.env.PORT || 3000); app.listen(process.env.PORT || 3000);
export type App = typeof app; export type App = typeof app;

View File

@@ -10,7 +10,7 @@ export type ConstructorTypeId = typeof ConstructorTypeId;
export interface Brand<in out K extends string | symbol> { export interface Brand<in out K extends string | symbol> {
readonly [BrandTypeId]: { readonly [BrandTypeId]: {
readonly [k in K]: K; readonly [k in K]: K;
} };
} }
export declare namespace Brand { export declare namespace Brand {

View File

@@ -1,3 +1,4 @@
export const identity = <A>(a: A): A => a; export const identity = <A>(a: A): A => a;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- unsafe by design
export const unsafeCoerce: <A, B>(a: A) => B = identity as any; export const unsafeCoerce: <A, B>(a: A) => B = identity as any;

View File

@@ -28,8 +28,8 @@ const router = createBrowserRouter([
{ {
path: "attachment/:attachmentId", path: "attachment/:attachmentId",
Component: Attachment, Component: Attachment,
} },
] ],
}, },
], ],
}, },
@@ -45,5 +45,5 @@ const root = createRoot(rootElement);
root.render( root.render(
<StrictMode> <StrictMode>
<RouterProvider router={router} /> <RouterProvider router={router} />
</StrictMode> </StrictMode>,
); );

View File

@@ -5,7 +5,9 @@ import { Effect } from "effect";
export type ResponseEffect<R extends Record<number, unknown>> = Effect.Effect<R[200], Exclude<keyof R, 200> extends never ? never : { [Status in keyof R]: { status: Status, value: R[Status] } }[Exclude<keyof R, 200>]>; export type ResponseEffect<R extends Record<number, unknown>> = Effect.Effect<R[200], Exclude<keyof R, 200> extends never ? never : { [Status in keyof R]: { status: Status, value: R[Status] } }[Exclude<keyof R, 200>]>;
export const client = treaty<App>(process.env.NODE_ENV === "production" ? "" : "http://localhost:3000", { export const API_URL_PREFIX = process.env.NODE_ENV === "production" ? "" : "http://localhost:3000";
export const client = treaty<App>(API_URL_PREFIX, {
fetch: { fetch: {
credentials: "include", credentials: "include",
}, },
@@ -19,6 +21,10 @@ export const client = treaty<App>(process.env.NODE_ENV === "production" ? "" : "
return file; return file;
} }
}, },
}).api; }).api.v1;
export const mapResponse = <R extends Record<number, unknown>>({ error, data }: Treaty.TreatyResponse<R>): ResponseEffect<R> => error !== null ? Effect.fail(error) as any : Effect.succeed(data); export const mapResponse = <R extends Record<number, unknown>>({ error, data }: Treaty.TreatyResponse<R>): ResponseEffect<R> => {
return error !== null
? Effect.fail(error as Exclude<keyof R, 200> extends never ? never : { [Status in keyof R]: { status: Status, value: R[Status] } }[Exclude<keyof R, 200>])
: Effect.succeed(data);
};

View File

@@ -40,7 +40,7 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size }), className)}
ref={ref} ref={ref}
{...props} {...props}
/> />

View File

@@ -9,7 +9,7 @@ const Card = forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
/> />
@@ -36,7 +36,7 @@ const CardTitle = forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"text-2xl font-semibold leading-none tracking-tight", "text-2xl font-semibold leading-none tracking-tight",
className className,
)} )}
{...props} {...props}
/> />

View File

@@ -16,7 +16,7 @@ const DialogOverlay = forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
/> />
@@ -33,7 +33,7 @@ const DialogContent = forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
> >
@@ -54,7 +54,7 @@ const DialogHeader = ({
<div <div
className={cn( className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left", "flex flex-col space-y-1.5 text-center sm:text-left",
className className,
)} )}
{...props} {...props}
/> />
@@ -68,7 +68,7 @@ const DialogFooter = ({
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className className,
)} )}
{...props} {...props}
/> />
@@ -83,7 +83,7 @@ const DialogTitle = forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"text-lg font-semibold leading-none tracking-tight", "text-lg font-semibold leading-none tracking-tight",
className className,
)} )}
{...props} {...props}
/> />
@@ -103,5 +103,5 @@ const DialogDescription = forwardRef<
DialogDescription.displayName = DialogPrimitive.Description.displayName; DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger,
}; };

View File

@@ -21,7 +21,7 @@ const DropdownMenuSubTrigger = forwardRef<
className={cn( 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", "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", inset && "pl-8",
className className,
)} )}
{...props} {...props}
> >
@@ -39,7 +39,7 @@ const DropdownMenuSubContent = forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
/> />
@@ -56,7 +56,7 @@ const DropdownMenuContent = forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
/> />
@@ -75,7 +75,7 @@ const DropdownMenuItem = forwardRef<
className={cn( 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", "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", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
@@ -90,7 +90,7 @@ const DropdownMenuCheckboxItem = forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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 className,
)} )}
{...(checked !== undefined) ? { checked } : undefined} {...(checked !== undefined) ? { checked } : undefined}
{...props} {...props}
@@ -113,7 +113,7 @@ const DropdownMenuRadioItem = forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
> >
@@ -130,7 +130,7 @@ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = forwardRef< const DropdownMenuLabel = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean,
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
@@ -138,7 +138,7 @@ const DropdownMenuLabel = forwardRef<
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold", "px-2 py-1.5 text-sm font-semibold",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
@@ -166,7 +166,7 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} {...props}
/> />
) );
}; };
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
@@ -174,6 +174,6 @@ export {
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuSub, DropdownMenuShortcut, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuTrigger DropdownMenuSubTrigger, DropdownMenuTrigger,
}; };

View File

@@ -8,7 +8,7 @@ const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
type={type} type={type}
className={cn( 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", "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 className,
)} )}
ref={ref} ref={ref}
{...props} {...props}

View File

@@ -43,7 +43,7 @@ const TableFooter = forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"border-t bg-stone-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-stone-800/50", "border-t bg-stone-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-stone-800/50",
className className,
)} )}
{...props} {...props}
/> />
@@ -58,7 +58,7 @@ const TableRow = forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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 className,
)} )}
{...props} {...props}
/> />
@@ -73,7 +73,7 @@ const TableHead = forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"h-12 px-4 text-left align-middle font-medium text-stone-500 [&:has([role=checkbox])]:pr-0 dark:text-stone-400", "h-12 px-4 text-left align-middle font-medium text-stone-500 [&:has([role=checkbox])]:pr-0 dark:text-stone-400",
className className,
)} )}
{...props} {...props}
/> />
@@ -105,5 +105,5 @@ const TableCaption = forwardRef<
TableCaption.displayName = "TableCaption"; TableCaption.displayName = "TableCaption";
export { export {
Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow,
}; };

View File

@@ -1,37 +1,55 @@
import { mapProp, Updater, useStore } from "@/hooks/useStore"; import { mapProp, Update, Updater, useStore } from "@/hooks/useStore";
import { Treaty } from "@elysiajs/eden"; import { Treaty } from "@elysiajs/eden";
import { Effect, Fiber, pipe } from "effect"; import { Effect, Fiber, pipe } from "effect";
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export type LoadingResult<R extends Record<number, unknown>> = export namespace Loading {
| { export interface Pending {
isLoading: true, readonly isLoading: true;
data: null, readonly data: null;
error: null, readonly error: null;
} | { readonly setData: null;
isLoading: false,
data: R[200],
error: null,
setData: Updater<R[200]>,
} | {
isLoading: false,
data: null,
error: Exclude<keyof R, 200 | 401> extends never
? { status: unknown, value: unknown }
: { [Status in keyof R]: { status: Status, value: R[Status] } }[Exclude<keyof R, 200 | 401>],
} }
export interface Success<A> {
readonly isLoading: false;
readonly data: A;
readonly error: null;
readonly setData: Updater<A>;
}
export interface Error<E> {
readonly isLoading: false;
readonly data: null;
readonly error: E;
readonly setData: null;
}
}
export type Loading<A, E> =
| Loading.Pending
| Loading.Success<A>
| Loading.Error<E>
; ;
export type ErrorResponses<R extends Record<number, unknown>> =
Exclude<keyof R, 200 | 401> extends never
? { status: unknown, value: unknown }
: { [Status in keyof R]: { status: Status, value: R[Status] } }[Exclude<keyof R, 200 | 401>];
export type LoadingResult<R extends Record<number, unknown>> = Loading<R[200], ErrorResponses<R>>;
export function useLoading<R extends Record<number, unknown>>(fn: () => Promise<Treaty.TreatyResponse<R>>, deps: React.DependencyList) { export function useLoading<R extends Record<number, unknown>>(fn: () => Promise<Treaty.TreatyResponse<R>>, deps: React.DependencyList) {
const navigate = useNavigate(); const navigate = useNavigate();
const setUser = useStore(state => state.setUser); const setUser = useStore(state => state.setUser);
const [result, setResult] = useState<LoadingResult<R>>(() => ({ isLoading: true, data: null, error: null })); const [result, setResult] = useState<LoadingResult<R>>(IS_LOADING);
useEffect(() => { useEffect(() => {
setResult(IS_LOADING);
let cancelled = false; let cancelled = false;
fn().then(({ error, data }) => { fn().then(({ error, data }) => {
@@ -43,14 +61,23 @@ export function useLoading<R extends Record<number, unknown>>(fn: () => Promise<
navigate("/login"); navigate("/login");
return; return;
} }
}
setResult({ setResult(Object.freeze<Loading.Error<ErrorResponses<R>>>({
isLoading: false, isLoading: false,
error, data: null,
data, error: error as ErrorResponses<R>,
setData: (action) => (setResult as any)(mapProp("data", action)), setData: null,
} as LoadingResult<R>); }));
} else {
setResult({
isLoading: false,
error,
data,
setData: (action) => setResult(mapProp("data", action) as Update<LoadingResult<R>>),
} as LoadingResult<R>);
}
}, (error) => {
console.error(error);
}); });
return () => { cancelled = true; }; return () => { cancelled = true; };
@@ -66,51 +93,30 @@ const IS_LOADING = Object.freeze({
setData: null, setData: null,
}); });
export type LoadingEffectResult<A, E> =
| {
readonly isLoading: true,
readonly data: null,
readonly error: null,
readonly setData: null,
} | {
readonly isLoading: false,
readonly data: A,
readonly error: null,
readonly setData: Updater<A>,
} | {
readonly isLoading: false,
readonly data: null,
readonly error: E,
readonly setData: null,
}
;
function mapSuccess<A>(data: A) {
return Object.freeze({ isLoading: false as const, data, error: null });
}
function mapFailure<E>(error: E) {
return Object.freeze({ isLoading: false as const, data: null, error });
}
export function useLoadingEffect<A, E>(effect: Effect.Effect<A, E>, deps: React.DependencyList) { export function useLoadingEffect<A, E>(effect: Effect.Effect<A, E>, deps: React.DependencyList) {
const [result, setResult] = useState<LoadingEffectResult<A, E>>(IS_LOADING); const [result, setResult] = useState<Loading<A, E>>(IS_LOADING);
const setResultEffect = useCallback((action: Loading<A, E>) => Effect.sync(() => setResult(action)), []);
useEffect(() => { useEffect(() => {
setResult(IS_LOADING); setResult(IS_LOADING);
const fiber = pipe( const fiber = pipe(
effect, effect,
Effect.match({ Effect.tapBoth({
onSuccess: mapSuccess, onSuccess: (data) => setResultEffect(Object.freeze<Loading.Success<A>>({
onFailure: mapFailure, isLoading: false,
}), data,
Effect.tap((result) => { error: null,
setResult({ setData: (action) => setResult(mapProp("data", action) as Update<Loading<A, E>>),
...result, })),
setData: ((action: any) => (setResult as any)(mapProp("data", action))) as any, onFailure: (error) => setResultEffect(Object.freeze<Loading.Error<E>>({
}); isLoading: false,
data: null,
error,
setData: null,
})),
}), }),
Effect.runFork, Effect.runFork,
); );

View File

@@ -32,7 +32,7 @@ export default function Attachment() {
//renderSingleHorizontalStaffline: true, //renderSingleHorizontalStaffline: true,
}); });
osmd.load(url).then(render); osmd.load(url).then(render, (error) => console.error(error));
window.addEventListener("resize", render); window.addEventListener("resize", render);
@@ -51,9 +51,11 @@ export default function Attachment() {
} }
if (error !== null) { if (error !== null) {
<div className="w-full h-full overflow-hidden flex items-center justify-center"> return (
<div>Wystąpił błąd: {error.value}</div> <div className="w-full h-full overflow-hidden flex items-center justify-center">
</div> <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

@@ -55,13 +55,13 @@ export function Home() {
<Input <Input
className="w-[32ch]" className="w-[32ch]"
type="text" type="text"
placeholder="Autor" placeholder="Twórcy"
value={author} value={author}
onChange={(e) => setAuthor(e.target.value)} onChange={(e) => setAuthor(e.target.value)}
/> />
</div> </div>
<Table> <Table>
<TableHeader> <TableHeader className="bg-white sticky top-0">
<TableRow> <TableRow>
<TableHead>Tytuł</TableHead> <TableHead>Tytuł</TableHead>
<TableHead>Twórcy</TableHead> <TableHead>Twórcy</TableHead>
@@ -187,7 +187,7 @@ function AddPieceDialogContent() {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
} };
return ( return (
<DialogContent> <DialogContent>

View File

@@ -1,6 +1,6 @@
import { Attachment, denormalizeSystemInformation, type Piece, pieceCache, User } from "@/cache"; import { Attachment, denormalizeSystemInformation, type Piece, pieceCache, User } from "@/cache";
import { client } from "@/client"; import { API_URL_PREFIX, client } from "@/client";
import { Button } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useLoadingEffect } from "@/hooks/useLoading"; import { useLoadingEffect } from "@/hooks/useLoading";
@@ -214,24 +214,7 @@ namespace AttachmentRow {
function AttachmentRow(props: AttachmentRow.Props) { function AttachmentRow(props: AttachmentRow.Props) {
const download = useCallback(async () => { const url = `${API_URL_PREFIX}/api/v1/piece/${props.attachment.pieceId}/attachment/${props.attachment.attachmentId}`;
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<HTMLAnchorElement>) => { const open = useCallback(async (event: MouseEvent<HTMLAnchorElement>) => {
if (props.attachment.mediaType !== "application/pdf") { if (props.attachment.mediaType !== "application/pdf") {
@@ -298,9 +281,9 @@ function AttachmentRow(props: AttachmentRow.Props) {
: <>{props.attachment.modifiedAt.value}<br />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}</>}
</TableCell> </TableCell>
<TableCell className="text-center flex justify-center gap-4"> <TableCell className="text-center flex justify-center gap-4">
<Button type="button" variant="ghost" size="icon" title="Pobierz" onClick={download}> <a href={url} className={buttonVariants({ variant: "ghost", size: "icon" })} title="Pobierz" download={props.attachment.filename}>
<Download /> <Download />
</Button> </a>
<Button type="button" variant="ghost" size="icon" title="Usuń" onClick={doDelete}> <Button type="button" variant="ghost" size="icon" title="Usuń" onClick={doDelete}>
<Trash /> <Trash />
</Button> </Button>
@@ -320,7 +303,7 @@ function AttachmentForm(props: AttachmentForm.Props) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const onDragOver: DragEventHandler<HTMLElement> = async (e) => { const onDragOver: DragEventHandler<HTMLElement> = (e) => {
e.preventDefault(); e.preventDefault();
e.dataTransfer.dropEffect = "copy"; e.dataTransfer.dropEffect = "copy";
}; };
@@ -365,7 +348,7 @@ function AttachmentForm(props: AttachmentForm.Props) {
await delay; await delay;
setIsLoading(false); setIsLoading(false);
} }
} };
return ( return (
<div <div

View File

@@ -26,7 +26,7 @@ export function Root() {
setUser(data); setUser(data);
}; };
useEffect(() => { init(); }, []); useEffect(() => void(init()), []);
const onLogoutClick = async () => { const onLogoutClick = async () => {
const { error } = await client.logout.post(); const { error } = await client.logout.post();

731
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,12 @@ catalog:
'@elysiajs/eden': '^1.1.3' '@elysiajs/eden': '^1.1.3'
'@elysiajs/static': '^1.1.1' '@elysiajs/static': '^1.1.1'
'@elysiajs/swagger': '^1.1.6' '@elysiajs/swagger': '^1.1.6'
'@eslint/js': '^9.17.0'
'@radix-ui/react-dialog': '^1.1.2' '@radix-ui/react-dialog': '^1.1.2'
'@radix-ui/react-dropdown-menu': '^2.1.2' '@radix-ui/react-dropdown-menu': '^2.1.2'
'@radix-ui/react-label': '^2.1.0' '@radix-ui/react-label': '^2.1.0'
'@radix-ui/react-slot': '^1.1.0' '@radix-ui/react-slot': '^1.1.0'
'@stylistic/eslint-plugin': '^2.12.1'
'@types/bun': '^1.1.13' '@types/bun': '^1.1.13'
'@types/react': '^18.3.12' '@types/react': '^18.3.12'
'@types/react-dom': '^18.3.1' '@types/react-dom': '^18.3.1'
@@ -31,4 +33,5 @@ catalog:
tailwindcss: '^3.4.15' tailwindcss: '^3.4.15'
tailwindcss-animate: '^1.0.7' tailwindcss-animate: '^1.0.7'
typescript: '^5.7.2' typescript: '^5.7.2'
typescript-eslint: '^8.18.2'
vite: '^5.4.11' vite: '^5.4.11'