Add eslint and fix eslint errors, other minor improvements
This commit is contained in:
@@ -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
47
eslint.config.mjs
Normal 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 }],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(Object.freeze<Loading.Error<ErrorResponses<R>>>({
|
||||||
|
isLoading: false,
|
||||||
|
data: null,
|
||||||
|
error: error as ErrorResponses<R>,
|
||||||
|
setData: null,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
setResult({
|
setResult({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error,
|
error,
|
||||||
data,
|
data,
|
||||||
setData: (action) => (setResult as any)(mapProp("data", action)),
|
setData: (action) => setResult(mapProp("data", action) as Update<LoadingResult<R>>),
|
||||||
} as 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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
return (
|
||||||
<div className="w-full h-full overflow-hidden flex items-center justify-center">
|
<div className="w-full h-full overflow-hidden flex items-center justify-center">
|
||||||
<div>Wystąpił błąd: {error.value}</div>
|
<div>Wystąpił błąd: {error.value}</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div ref={containerRef} className="w-full h-full overflow-scroll" />;
|
return <div ref={containerRef} className="w-full h-full overflow-scroll" />;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
731
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user