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

@@ -79,11 +79,11 @@ const app = new Elysia()
indexHTML: false,
}))
.group("/api", (app) => app
.group("/api/v1", (app) => app
// --- MARK: AUTHENTICATION --------------------------------------------
.get("/me", async ({ user }) => {
.get("/me", ({ user }) => {
if (user === null) {
return error("Unauthorized");
@@ -235,7 +235,7 @@ const app = new Elysia()
eb("composer", "like", "%" + query.author + "%"),
eb("arranger", "like", "%" + query.author + "%"),
eb("lyricist", "like", "%" + query.author + "%"),
]))
]));
}
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) {
return error("Unauthorized");
@@ -277,7 +277,7 @@ const app = new Elysia()
sha256: Buffer.from(sha256).toString("hex"),
...rest,
})),
}
};
}, {
params: t.Object({
pieceId: tbranded<PieceId>(),
@@ -311,7 +311,7 @@ const app = new Elysia()
}),
params: t.Object({
pieceId: tbranded<PieceId>(),
})
}),
})
.delete("/piece/:pieceId", async ({ db, params: { pieceId }, set, user }) => {
@@ -471,13 +471,13 @@ const app = new Elysia()
pieceId: tbranded<PieceId>(),
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;

View File

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

View File

@@ -1,3 +1,4 @@
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;

View File

@@ -28,8 +28,8 @@ const router = createBrowserRouter([
{
path: "attachment/:attachmentId",
Component: Attachment,
}
]
},
],
},
],
},
@@ -45,5 +45,5 @@ const root = createRoot(rootElement);
root.render(
<StrictMode>
<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 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: {
credentials: "include",
},
@@ -19,6 +21,10 @@ export const client = treaty<App>(process.env.NODE_ENV === "production" ? "" : "
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";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
className={cn(buttonVariants({ variant, size }), className)}
ref={ref}
{...props}
/>

View File

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

View File

@@ -16,7 +16,7 @@ const DialogOverlay = forwardRef<
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
className,
)}
{...props}
/>
@@ -33,7 +33,7 @@ const DialogContent = forwardRef<
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
className,
)}
{...props}
>
@@ -54,7 +54,7 @@ const DialogHeader = ({
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
className,
)}
{...props}
/>
@@ -68,7 +68,7 @@ const DialogFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
@@ -83,7 +83,7 @@ const DialogTitle = forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
className,
)}
{...props}
/>
@@ -103,5 +103,5 @@ const DialogDescription = forwardRef<
DialogDescription.displayName = DialogPrimitive.Description.displayName;
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(
"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
className,
)}
{...props}
>
@@ -39,7 +39,7 @@ const DropdownMenuSubContent = forwardRef<
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
className,
)}
{...props}
/>
@@ -56,7 +56,7 @@ const DropdownMenuContent = forwardRef<
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
className,
)}
{...props}
/>
@@ -75,7 +75,7 @@ const DropdownMenuItem = forwardRef<
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
className,
)}
{...props}
/>
@@ -90,7 +90,7 @@ const DropdownMenuCheckboxItem = forwardRef<
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
className,
)}
{...(checked !== undefined) ? { checked } : undefined}
{...props}
@@ -113,7 +113,7 @@ const DropdownMenuRadioItem = forwardRef<
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
className,
)}
{...props}
>
@@ -130,7 +130,7 @@ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean,
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
@@ -138,7 +138,7 @@ const DropdownMenuLabel = forwardRef<
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
className,
)}
{...props}
/>
@@ -166,7 +166,7 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
@@ -174,6 +174,6 @@ export {
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuTrigger
DropdownMenuSubTrigger, DropdownMenuTrigger,
};

View File

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

View File

@@ -43,7 +43,7 @@ const TableFooter = forwardRef<
ref={ref}
className={cn(
"border-t bg-stone-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-stone-800/50",
className
className,
)}
{...props}
/>
@@ -58,7 +58,7 @@ const TableRow = forwardRef<
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
className,
)}
{...props}
/>
@@ -73,7 +73,7 @@ const TableHead = forwardRef<
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
className,
)}
{...props}
/>
@@ -105,5 +105,5 @@ const TableCaption = forwardRef<
TableCaption.displayName = "TableCaption";
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 { Effect, Fiber, pipe } from "effect";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
export type LoadingResult<R extends Record<number, unknown>> =
| {
isLoading: true,
data: null,
error: 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 namespace Loading {
export interface Pending {
readonly isLoading: true;
readonly data: null;
readonly error: null;
readonly setData: null;
}
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) {
const navigate = useNavigate();
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(() => {
setResult(IS_LOADING);
let cancelled = false;
fn().then(({ error, data }) => {
@@ -43,14 +61,23 @@ export function useLoading<R extends Record<number, unknown>>(fn: () => Promise<
navigate("/login");
return;
}
}
setResult({
isLoading: false,
error,
data,
setData: (action) => (setResult as any)(mapProp("data", action)),
} as LoadingResult<R>);
setResult(Object.freeze<Loading.Error<ErrorResponses<R>>>({
isLoading: false,
data: null,
error: error as ErrorResponses<R>,
setData: null,
}));
} 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; };
@@ -66,51 +93,30 @@ const IS_LOADING = Object.freeze({
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) {
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(() => {
setResult(IS_LOADING);
const fiber = pipe(
effect,
Effect.match({
onSuccess: mapSuccess,
onFailure: mapFailure,
}),
Effect.tap((result) => {
setResult({
...result,
setData: ((action: any) => (setResult as any)(mapProp("data", action))) as any,
});
Effect.tapBoth({
onSuccess: (data) => setResultEffect(Object.freeze<Loading.Success<A>>({
isLoading: false,
data,
error: null,
setData: (action) => setResult(mapProp("data", action) as Update<Loading<A, E>>),
})),
onFailure: (error) => setResultEffect(Object.freeze<Loading.Error<E>>({
isLoading: false,
data: null,
error,
setData: null,
})),
}),
Effect.runFork,
);

View File

@@ -32,7 +32,7 @@ export default function Attachment() {
//renderSingleHorizontalStaffline: true,
});
osmd.load(url).then(render);
osmd.load(url).then(render, (error) => console.error(error));
window.addEventListener("resize", render);
@@ -51,9 +51,11 @@ export default function Attachment() {
}
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 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" />;

View File

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

View File

@@ -1,6 +1,6 @@
import { Attachment, denormalizeSystemInformation, type Piece, pieceCache, User } from "@/cache";
import { client } from "@/client";
import { Button } from "@/components/ui/button";
import { API_URL_PREFIX, client } from "@/client";
import { Button, buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useLoadingEffect } from "@/hooks/useLoading";
@@ -214,24 +214,7 @@ namespace AttachmentRow {
function AttachmentRow(props: AttachmentRow.Props) {
const download = useCallback(async () => {
const { error, data } = await client
.piece({ pieceId: props.attachment.pieceId })
.attachment({ attachmentId: props.attachment.attachmentId })
.get()
if (error !== null) {
console.error(error.value);
return;
}
const url = URL.createObjectURL(data);
const a = document.createElement("a");
a.href = url;
a.download = props.attachment.filename; // TODO Use `data.name` after Content-Disposition parser is implemented
a.click();
URL.revokeObjectURL(url);
}, [props.attachment.attachmentId, props.attachment.pieceId]);
const url = `${API_URL_PREFIX}/api/v1/piece/${props.attachment.pieceId}/attachment/${props.attachment.attachmentId}`;
const open = useCallback(async (event: MouseEvent<HTMLAnchorElement>) => {
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}</>}
</TableCell>
<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 />
</Button>
</a>
<Button type="button" variant="ghost" size="icon" title="Usuń" onClick={doDelete}>
<Trash />
</Button>
@@ -320,7 +303,7 @@ function AttachmentForm(props: AttachmentForm.Props) {
const [isLoading, setIsLoading] = useState(false);
const onDragOver: DragEventHandler<HTMLElement> = async (e) => {
const onDragOver: DragEventHandler<HTMLElement> = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
};
@@ -365,7 +348,7 @@ function AttachmentForm(props: AttachmentForm.Props) {
await delay;
setIsLoading(false);
}
}
};
return (
<div

View File

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