Add effect because I coldn't resist
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import cors from "@elysiajs/cors";
|
import cors from "@elysiajs/cors";
|
||||||
import { swagger } from "@elysiajs/swagger";
|
import { swagger } from "@elysiajs/swagger";
|
||||||
import { AttachmentId, PieceId, RequestId, SessionId, Sha256 } from "common";
|
import { AttachmentId, PieceId, RequestId, SessionId, Sha256, UserId } from "common";
|
||||||
import * as Function from "common/Function";
|
import * as Function from "common/Function";
|
||||||
import { Elysia, error, form, t } from "elysia";
|
import { Elysia, error, t } from "elysia";
|
||||||
import { sql } from "kysely";
|
import { sql } from "kysely";
|
||||||
import { generateSessionId, initDatabase } from "./database";
|
import { generateSessionId, initDatabase } from "./database";
|
||||||
|
|
||||||
@@ -148,6 +148,35 @@ const app = new Elysia()
|
|||||||
.execute();
|
.execute();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- MARK: USER MANAGEMENT -----------------------------------------------
|
||||||
|
|
||||||
|
.get("/user/:userId", async ({ db, params: { userId }, user }) => {
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await db
|
||||||
|
.selectFrom("User")
|
||||||
|
.select(["userId", "username", "admin"])
|
||||||
|
.where("userId", "=", userId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (res === undefined) {
|
||||||
|
return error("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: res.userId,
|
||||||
|
username: res.username,
|
||||||
|
admin: res.admin !== 0,
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
params: t.Object({
|
||||||
|
userId: tbranded<UserId>(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
// --- MARK: PIECE CRUD ----------------------------------------------------
|
// --- MARK: PIECE CRUD ----------------------------------------------------
|
||||||
|
|
||||||
.post("/piece", async ({ db, body: { name, composer, lyricist, arranger }, user }) => {
|
.post("/piece", async ({ db, body: { name, composer, lyricist, arranger }, user }) => {
|
||||||
@@ -182,19 +211,66 @@ const app = new Elysia()
|
|||||||
|
|
||||||
let q = db
|
let q = db
|
||||||
.selectFrom("Piece")
|
.selectFrom("Piece")
|
||||||
.selectAll()
|
.select("pieceId")
|
||||||
.orderBy(["name", "composer", "arranger"])
|
.orderBy(["name", "composer", "arranger"])
|
||||||
.limit(100);
|
.offset(query.offset ?? 0)
|
||||||
|
.limit(query.limit ?? 100);
|
||||||
|
|
||||||
if (query.id !== undefined) {
|
if (query.name !== undefined) {
|
||||||
q = q.where("pieceId", "=", query.id);
|
q = q.where("name", "like", "%" + query.name + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.author !== undefined) {
|
||||||
|
q = q.where((eb) => eb.or([
|
||||||
|
eb("composer", "like", "%" + query.author + "%"),
|
||||||
|
eb("arranger", "like", "%" + query.author + "%"),
|
||||||
|
eb("lyricist", "like", "%" + query.author + "%"),
|
||||||
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await q.execute();
|
const res = await q.execute();
|
||||||
return res;
|
return res.map(({ pieceId }) => pieceId);
|
||||||
}, {
|
}, {
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
id: t.Optional(tbranded<PieceId>()),
|
name: t.Optional(t.String()),
|
||||||
|
author: t.Optional(t.String()),
|
||||||
|
offset: t.Optional(t.Integer({ minimum: 0 })),
|
||||||
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
.get("/piece/:pieceId", async ({ db, query, params: { pieceId }, user }) => {
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const piece = await db
|
||||||
|
.selectFrom("Piece")
|
||||||
|
.selectAll()
|
||||||
|
.where("pieceId", "=", pieceId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (piece === undefined) {
|
||||||
|
return error("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments = await db
|
||||||
|
.selectFrom("Attachment")
|
||||||
|
.selectAll()
|
||||||
|
.where("pieceId", "=", pieceId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...piece,
|
||||||
|
attachments: attachments.map(({ sha256, ...rest }) => ({
|
||||||
|
sha256: Buffer.from(sha256).toString("hex"),
|
||||||
|
...rest,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
params: t.Object({
|
||||||
|
pieceId: tbranded<PieceId>(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -277,7 +353,10 @@ const app = new Elysia()
|
|||||||
.returningAll()
|
.returningAll()
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
return res;
|
return {
|
||||||
|
...res,
|
||||||
|
sha256: Buffer.from(res.sha256).toString("hex"),
|
||||||
|
};
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
filename: t.String({ minLength: 1 }),
|
filename: t.String({ minLength: 1 }),
|
||||||
@@ -289,26 +368,6 @@ const app = new Elysia()
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
.get("piece/:pieceId/attachment", async ({ db, params: { pieceId }, user }) => {
|
|
||||||
|
|
||||||
if (user === null) {
|
|
||||||
return error("Unauthorized");
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await db
|
|
||||||
.selectFrom("Attachment")
|
|
||||||
.selectAll()
|
|
||||||
.where("pieceId", "=", pieceId)
|
|
||||||
.orderBy("filename")
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, {
|
|
||||||
params: t.Object({
|
|
||||||
pieceId: tbranded<PieceId>(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
/* NOTE The piece ID is reduntant, because attachment IDs are unique for the
|
/* NOTE The piece ID is reduntant, because attachment IDs are unique for the
|
||||||
* entire DB, not just per piece. However, we consider a piece to be the
|
* entire DB, not just per piece. However, we consider a piece to be the
|
||||||
* sole owner of an attachment, i.e. attachments are not shared (attachments
|
* sole owner of an attachment, i.e. attachments are not shared (attachments
|
||||||
@@ -316,7 +375,7 @@ const app = new Elysia()
|
|||||||
* reflect the ownership in the URLs.
|
* reflect the ownership in the URLs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.get("piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, user }) => {
|
.get("piece/:pieceId/attachment/:attachmentId", async ({ db, params: { pieceId, attachmentId }, user, set }) => {
|
||||||
|
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
return error("Unauthorized");
|
return error("Unauthorized");
|
||||||
@@ -336,11 +395,8 @@ const app = new Elysia()
|
|||||||
return error("Not Found");
|
return error("Not Found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return form({
|
set.headers["content-disposition"] = `attachment; filename="${res.filename}"`;
|
||||||
filename: res.filename,
|
return Bun.file(res.data, { type: res.mediaType });
|
||||||
mediaType: res.mediaType,
|
|
||||||
data: new File([res.data], res.filename, { type: res.mediaType }),
|
|
||||||
});
|
|
||||||
}, {
|
}, {
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
pieceId: tbranded<PieceId>(),
|
pieceId: tbranded<PieceId>(),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@radix-ui/react-slot": "catalog:",
|
"@radix-ui/react-slot": "catalog:",
|
||||||
"clsx": "catalog:",
|
"clsx": "catalog:",
|
||||||
"common": "workspace:^",
|
"common": "workspace:^",
|
||||||
|
"effect": "catalog:",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"opensheetmusicdisplay": "catalog:",
|
"opensheetmusicdisplay": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
|
|||||||
111
packages/frontend/src/cache.ts
Normal file
111
packages/frontend/src/cache.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import type * as Db from "backend/database";
|
||||||
|
import { AttachmentId, PieceId, UserId } from "common";
|
||||||
|
import { Cache, Duration, Effect, Option, pipe } from "effect";
|
||||||
|
import { client, mapResponse } from "./client";
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
readonly userId: UserId;
|
||||||
|
readonly username: string;
|
||||||
|
readonly admin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemInformation {
|
||||||
|
readonly createdBy: Option.Option<User>;
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly modifiedBy: Option.Option<User>;
|
||||||
|
readonly modifiedAt: Option.Option<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Attachment extends SystemInformation {
|
||||||
|
readonly attachmentId: AttachmentId;
|
||||||
|
readonly pieceId: PieceId;
|
||||||
|
readonly sha256: string;
|
||||||
|
readonly filename: string;
|
||||||
|
readonly mediaType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Piece extends SystemInformation {
|
||||||
|
readonly pieceId: PieceId;
|
||||||
|
readonly name: string;
|
||||||
|
readonly composer: Option.Option<string>;
|
||||||
|
readonly lyricist: Option.Option<string>;
|
||||||
|
readonly arranger: Option.Option<string>;
|
||||||
|
readonly attachments: readonly Attachment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DbSystemInformation {
|
||||||
|
readonly createdBy: UserId | null;
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly modifiedBy: UserId | null;
|
||||||
|
readonly modifiedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const denormalizeSystemInformation = <T extends DbSystemInformation>({
|
||||||
|
createdBy,
|
||||||
|
modifiedBy,
|
||||||
|
modifiedAt,
|
||||||
|
...rest
|
||||||
|
}: T) => pipe(
|
||||||
|
Effect.all({
|
||||||
|
createdBy: pipe(
|
||||||
|
createdBy,
|
||||||
|
Effect.fromNullable,
|
||||||
|
Effect.flatMap((userId) => Effect.uninterruptible(userCache.get(userId))),
|
||||||
|
Effect.optionFromOptional,
|
||||||
|
),
|
||||||
|
modifiedBy: pipe(
|
||||||
|
modifiedBy,
|
||||||
|
Effect.fromNullable,
|
||||||
|
Effect.flatMap((userId) => Effect.uninterruptible(userCache.get(userId))),
|
||||||
|
Effect.optionFromOptional,
|
||||||
|
),
|
||||||
|
}, { concurrency: "unbounded" }),
|
||||||
|
Effect.map((si) => Object.freeze({
|
||||||
|
...rest,
|
||||||
|
...si,
|
||||||
|
modifiedAt: Option.fromNullable(modifiedAt),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const denormalizePiece = ({
|
||||||
|
composer,
|
||||||
|
lyricist,
|
||||||
|
arranger,
|
||||||
|
attachments,
|
||||||
|
...rest
|
||||||
|
}: Db.Piece & { attachments: (Omit<Db.Attachment, "sha256"> & { sha256: string })[] }) => pipe(
|
||||||
|
Effect.all({
|
||||||
|
attachments: Effect.all(attachments.map(denormalizeSystemInformation), { concurrency: "unbounded" }),
|
||||||
|
}, { concurrency: "unbounded" }),
|
||||||
|
Effect.map((piece) => Object.freeze({
|
||||||
|
...rest,
|
||||||
|
...piece,
|
||||||
|
composer: Option.fromNullable(composer),
|
||||||
|
lyricist: Option.fromNullable(lyricist),
|
||||||
|
arranger: Option.fromNullable(arranger),
|
||||||
|
})),
|
||||||
|
Effect.flatMap(denormalizeSystemInformation),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userLookup = (userId: UserId) => pipe(
|
||||||
|
Effect.promise((signal) => client.user({ userId }).get({ fetch: { signal } })),
|
||||||
|
Effect.flatMap(mapResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const pieceLookup = (pieceId: PieceId) => pipe(
|
||||||
|
Effect.promise((signal) => client.piece({ pieceId }).get({ fetch: { signal } })),
|
||||||
|
Effect.flatMap(mapResponse),
|
||||||
|
Effect.flatMap(denormalizePiece),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userCache = Effect.runSync(Cache.make({
|
||||||
|
capacity: Infinity,
|
||||||
|
timeToLive: Duration.infinity,
|
||||||
|
lookup: userLookup,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const pieceCache = Effect.runSync(Cache.make({
|
||||||
|
capacity: Infinity,
|
||||||
|
timeToLive: Infinity,
|
||||||
|
lookup: pieceLookup,
|
||||||
|
}));
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { treaty } from "@elysiajs/eden";
|
import { Treaty, treaty } from "@elysiajs/eden";
|
||||||
import type { App } from "backend/app";
|
import type { App } from "backend/app";
|
||||||
|
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>("localhost:3000", { fetch: { credentials: "include" } });
|
export const client = treaty<App>("localhost:3000", { fetch: { credentials: "include" } });
|
||||||
|
|
||||||
|
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);
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const DropdownMenuCheckboxItem = forwardRef<
|
|||||||
"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={checked}
|
{...(checked !== undefined) ? { checked } : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { mapProp, Updater, useStore } from "@/hooks/useStore";
|
import { mapProp, Updater, useStore } from "@/hooks/useStore";
|
||||||
import { Treaty } from "@elysiajs/eden";
|
import { Treaty } from "@elysiajs/eden";
|
||||||
import { ELYSIA_FORM_DATA } from "elysia";
|
import { Effect, Fiber, pipe } from "effect";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
type FormDataFix<T> = T extends { [ELYSIA_FORM_DATA]: infer U } ? U : T;
|
|
||||||
|
|
||||||
export type LoadingResult<R extends Record<number, unknown>> =
|
export type LoadingResult<R extends Record<number, unknown>> =
|
||||||
| {
|
| {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
@@ -13,9 +11,9 @@ export type LoadingResult<R extends Record<number, unknown>> =
|
|||||||
error: null,
|
error: null,
|
||||||
} | {
|
} | {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
data: FormDataFix<R[200]>,
|
data: R[200],
|
||||||
error: null,
|
error: null,
|
||||||
setData: Updater<FormDataFix<R[200]>>,
|
setData: Updater<R[200]>,
|
||||||
} | {
|
} | {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
data: null,
|
data: null,
|
||||||
@@ -25,7 +23,7 @@ export type LoadingResult<R extends Record<number, unknown>> =
|
|||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
||||||
export function useLoading<R extends Record<number, unknown>>(fn: () => Promise<Treaty.TreatyResponse<R>>) {
|
export function useLoading<R extends Record<number, unknown>>(fn: () => Promise<Treaty.TreatyResponse<R>>, deps: React.DependencyList) {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -56,6 +54,69 @@ export function useLoading<R extends Record<number, unknown>>(fn: () => Promise<
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
|
}, deps);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IS_LOADING = Object.freeze({
|
||||||
|
isLoading: true,
|
||||||
|
data: null,
|
||||||
|
error: 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>) {
|
||||||
|
|
||||||
|
const [result, setResult] = useState<LoadingEffectResult<A, E>>(IS_LOADING);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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.runFork,
|
||||||
|
);
|
||||||
|
const interruptEffect = Fiber.interrupt(fiber);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Effect.runFork(interruptEffect);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ export default function Attachment() {
|
|||||||
const pieceId = PieceId(params.pieceId!);
|
const pieceId = PieceId(params.pieceId!);
|
||||||
const attachmentId = AttachmentId(params.attachmentId!);
|
const attachmentId = AttachmentId(params.attachmentId!);
|
||||||
|
|
||||||
const { isLoading, error, data } = useLoading(() => client.piece({ pieceId }).attachment({ attachmentId }).get());
|
const { isLoading, error, data } = useLoading(() => client.piece({ pieceId }).attachment({ attachmentId }).get(), [pieceId, attachmentId]);
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading || error !== null) return;
|
if (isLoading || error !== null) return;
|
||||||
|
|
||||||
const url = URL.createObjectURL(data?.data);
|
const url = URL.createObjectURL(data);
|
||||||
|
|
||||||
const render = () => osmd.render();
|
const render = () => osmd.render();
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
|
import { pieceCache, User } from "@/cache";
|
||||||
import { client } from "@/client";
|
import { client } from "@/client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading, useLoadingEffect } from "@/hooks/useLoading";
|
||||||
|
import { PieceId } from "common";
|
||||||
|
import { Cause, Effect, Option } from "effect";
|
||||||
import { Loader2, Plus } from "lucide-react";
|
import { Loader2, Plus } from "lucide-react";
|
||||||
import { FormEventHandler, ReactNode, useId, useState } from "react";
|
import { FormEventHandler, ReactNode, useId, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
|
|
||||||
const { isLoading, error, data } = useLoading(() => client.piece.get({ query: {} }));
|
const [name, setName] = useState("");
|
||||||
|
const [author, setAuthor] = useState("");
|
||||||
|
|
||||||
|
const { isLoading, error, data: pieceIds } = useLoading(() => client.piece.get({
|
||||||
|
query: {
|
||||||
|
...(name !== "" ? { name } : undefined),
|
||||||
|
...(author !== "" ? { author } : undefined),
|
||||||
|
},
|
||||||
|
}), [name, author]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -24,16 +35,32 @@ export function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-4 overflow-y-auto flex flex-col items-start gap-4">
|
<div className="p-4 overflow-y-auto flex flex-col items-start gap-4">
|
||||||
{error !== null ? (
|
{error !== null ? (
|
||||||
`Wystąpił błąd: ${error.value}`
|
Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`
|
||||||
) : (<>
|
) : (<>
|
||||||
<Dialog>
|
<div className="flex flex-row gap-4">
|
||||||
<DialogTrigger asChild>
|
<Dialog>
|
||||||
<Button variant="outline">
|
<DialogTrigger asChild>
|
||||||
<Plus />Dodaj utwór
|
<Button variant="outline">
|
||||||
</Button>
|
<Plus />Dodaj utwór
|
||||||
</DialogTrigger>
|
</Button>
|
||||||
<AddPieceDialogContent />
|
</DialogTrigger>
|
||||||
</Dialog>
|
<AddPieceDialogContent />
|
||||||
|
</Dialog>
|
||||||
|
<Input
|
||||||
|
className="w-[32ch]"
|
||||||
|
type="text"
|
||||||
|
placeholder="Tytuł"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="w-[32ch]"
|
||||||
|
type="text"
|
||||||
|
placeholder="Autor"
|
||||||
|
value={author}
|
||||||
|
onChange={(e) => setAuthor(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -44,29 +71,7 @@ export function Home() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((piece) => {
|
{pieceIds.map((pieceId) => <PieceRow key={pieceId} pieceId={pieceId} />)}
|
||||||
const composerParts: ReactNode[] = [];
|
|
||||||
if (piece.composer !== null) composerParts.push(piece.composer);
|
|
||||||
if (piece.arranger !== null) composerParts.push(`opracowanie: ${piece.arranger}`);
|
|
||||||
if (piece.lyricist !== null) composerParts.push(`słowa: ${piece.lyricist}`);
|
|
||||||
if (composerParts.length === 0) composerParts.push(<em>Nieznani</em>);
|
|
||||||
return (
|
|
||||||
<TableRow key={piece.pieceId}>
|
|
||||||
<TableCell><Link className="underline" to={`piece/${piece.pieceId}`}>{piece.name}</Link></TableCell>
|
|
||||||
<TableCell>{...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x, <br />] : [x])}</TableCell>
|
|
||||||
<TableCell className="text-center font-mono text-xs">
|
|
||||||
{piece.createdAt}
|
|
||||||
{piece.createdBy !== null && <><br />przez {piece.createdBy}</>}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center font-mono text-xs">
|
|
||||||
{piece.modifiedAt === null && piece.modifiedBy === null ? "\u2014"
|
|
||||||
: piece.modifiedAt !== null && piece.modifiedBy === null ? piece.modifiedAt
|
|
||||||
: piece.modifiedAt === null ? `przez ${piece.createdBy}`
|
|
||||||
: <>{piece.createdAt}<br />przez {piece.createdBy}</>}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</>)}
|
</>)}
|
||||||
@@ -74,6 +79,61 @@ export function Home() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace PieceRow {
|
||||||
|
export interface Props {
|
||||||
|
readonly pieceId: PieceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function PieceRow(props: PieceRow.Props) {
|
||||||
|
|
||||||
|
const { isLoading, error, data: piece } = useLoadingEffect(Effect.uninterruptible(pieceCache.get(props.pieceId)));
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4}>Ładowanie…</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error !== null) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4}>Wystąpił błąd: {error.value}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const composerParts: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (Option.isSome(piece.composer)) composerParts.push(piece.composer.value);
|
||||||
|
if (Option.isSome(piece.arranger)) composerParts.push(`opracowanie: ${piece.arranger.value}`);
|
||||||
|
if (Option.isSome(piece.lyricist)) composerParts.push(`słowa: ${piece.lyricist.value}`);
|
||||||
|
if (composerParts.length === 0) composerParts.push(<em>Nieznani</em>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Link className="underline" to={`piece/${piece.pieceId}`}>{piece.name}</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{...composerParts.flatMap((x, i, a) => i < a.length - 1 ? [x, <br />] : [x])}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center text-xs">
|
||||||
|
{piece.createdAt}
|
||||||
|
{Option.isSome(piece.createdBy) && <><br />przez {piece.createdBy.value.username}</>}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center text-xs">
|
||||||
|
{Option.isNone(piece.modifiedAt) && Option.isNone(piece.modifiedBy) ? "\u2014"
|
||||||
|
: Option.isSome(piece.modifiedAt) && Option.isNone(piece.modifiedBy) ? piece.modifiedAt.value
|
||||||
|
: Option.isNone(piece.modifiedAt) ? `przez ${(piece.modifiedBy as Option.Some<User>).value.username}`
|
||||||
|
: <>{piece.modifiedAt.value}<br />przez {(piece.modifiedBy as Option.Some<User>).value.username}</>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AddPieceDialogContent() {
|
function AddPieceDialogContent() {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function Login() {
|
|||||||
password: loginPassword,
|
password: loginPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error !== null) {
|
||||||
console.error(error.value);
|
console.error(error.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
|
import { Attachment, denormalizeSystemInformation, type Piece, pieceCache, User } from "@/cache";
|
||||||
import { client } from "@/client";
|
import { client } from "@/client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } 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 { useLoading } from "@/hooks/useLoading";
|
import { useLoadingEffect } from "@/hooks/useLoading";
|
||||||
import { Updater } from "@/hooks/useStore";
|
import { mapProp, Update, Updater } from "@/hooks/useStore";
|
||||||
import { timeout } from "@/lib/utils";
|
import { timeout } from "@/lib/utils";
|
||||||
import { Label } from "@radix-ui/react-label";
|
import { Label } from "@radix-ui/react-label";
|
||||||
import type { Attachment, Piece } from "backend/database";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { PieceId } from "common";
|
import { PieceId } from "common";
|
||||||
import { getMediaTypeForFilename } from "common/MediaType";
|
import { getMediaTypeForFilename } from "common/MediaType";
|
||||||
import { ELYSIA_FORM_DATA } from "elysia";
|
import { Cause, Effect, Option } from "effect";
|
||||||
|
import { constant } from "effect/Function";
|
||||||
import { Download, Loader2, Trash, UploadCloud } from "lucide-react";
|
import { Download, Loader2, Trash, UploadCloud } from "lucide-react";
|
||||||
import { DragEventHandler, FormEventHandler, MouseEvent, useCallback, useId, useState } from "react";
|
import { DragEventHandler, FormEventHandler, MouseEvent, useCallback, useId, useState } from "react";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
@@ -19,10 +20,14 @@ export function Piece() {
|
|||||||
|
|
||||||
const id = PieceId(useParams().pieceId!);
|
const id = PieceId(useParams().pieceId!);
|
||||||
|
|
||||||
const piece = useLoading(() => client.piece.get({ query: { id } }));
|
const { isLoading, error, data, setData } = useLoadingEffect(Effect.uninterruptible(pieceCache.get(id)));
|
||||||
const attachments = useLoading(() => client.piece({ pieceId: id }).attachment.get());
|
|
||||||
|
|
||||||
if (piece.isLoading || attachments.isLoading) {
|
const setAttachments = useCallback((action: Update<readonly Attachment[]>) => {
|
||||||
|
setData!(mapProp("attachments", action));
|
||||||
|
Effect.runFork(pieceCache.invalidate(id));
|
||||||
|
}, [setData]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
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>Ładowanie…</div>
|
<div>Ładowanie…</div>
|
||||||
@@ -32,18 +37,16 @@ export function Piece() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 overflow-y-auto flex flex-wrap items-start gap-4">
|
<div className="p-4 overflow-y-auto flex flex-wrap items-start gap-4">
|
||||||
{piece.error !== null || attachments.error !== null ? (
|
{error !== null ? (
|
||||||
`Wystąpił błąd: ${[piece.error?.value, attachments.error?.value].filter(Boolean).join(", ")}`
|
Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`
|
||||||
) : piece.data[0] === undefined ? (
|
|
||||||
"Utwór nie istnieje"
|
|
||||||
) : (<>
|
) : (<>
|
||||||
<div className="flex flex-col gap-4 p-4 border rounded">
|
<div className="flex flex-col gap-4 p-4 border rounded">
|
||||||
<h3 className="font-bold text-lg">Utwór</h3>
|
<h3 className="font-bold text-lg">Utwór</h3>
|
||||||
<PieceForm piece={piece.data[0]} />
|
<PieceForm piece={data} />
|
||||||
<h3 className="font-bold text-lg">Załączniki</h3>
|
<h3 className="font-bold text-lg">Załączniki</h3>
|
||||||
<AttachmentForm pieceId={piece.data[0].pieceId} setAttachments={attachments.setData} />
|
<AttachmentForm pieceId={id} setAttachments={setAttachments} />
|
||||||
</div>
|
</div>
|
||||||
<Attachments pieceId={piece.data[0].pieceId} attachments={attachments.data} setAttachments={attachments.setData} />
|
<Attachments pieceId={id} attachments={data.attachments} setAttachments={setAttachments} />
|
||||||
</>)}
|
</>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -58,9 +61,9 @@ namespace PieceForm {
|
|||||||
function PieceForm(props: PieceForm.Props) {
|
function PieceForm(props: PieceForm.Props) {
|
||||||
|
|
||||||
const [name, setName] = useState(props.piece.name);
|
const [name, setName] = useState(props.piece.name);
|
||||||
const [composer, setComposer] = useState(props.piece.composer ?? "");
|
const [composer, setComposer] = useState(() => Option.getOrElse(props.piece.composer, constant("")));
|
||||||
const [lyricist, setLyricist] = useState(props.piece.lyricist ?? "");
|
const [lyricist, setLyricist] = useState(() => Option.getOrElse(props.piece.lyricist, constant("")));
|
||||||
const [arranger, setArranger] = useState(props.piece.arranger ?? "");
|
const [arranger, setArranger] = useState(() => Option.getOrElse(props.piece.arranger, constant("")));
|
||||||
|
|
||||||
const nameId = useId();
|
const nameId = useId();
|
||||||
const composerId = useId();
|
const composerId = useId();
|
||||||
@@ -83,7 +86,7 @@ function PieceForm(props: PieceForm.Props) {
|
|||||||
arranger: arranger.length > 0 ? arranger : null,
|
arranger: arranger.length > 0 ? arranger : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error !== null) {
|
||||||
console.error(error.value);
|
console.error(error.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,7 +147,7 @@ namespace Attachments {
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
readonly pieceId: PieceId;
|
readonly pieceId: PieceId;
|
||||||
readonly attachments: readonly Attachment[];
|
readonly attachments: readonly Attachment[];
|
||||||
readonly setAttachments: Updater<Attachment[]>;
|
readonly setAttachments: Updater<readonly Attachment[]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,28 +181,27 @@ function Attachments(props: Attachments.Props) {
|
|||||||
namespace AttachmentRow {
|
namespace AttachmentRow {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly attachment: Attachment;
|
readonly attachment: Attachment;
|
||||||
readonly setAttachments: Updater<Attachment[]>;
|
readonly setAttachments: Updater<readonly Attachment[]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function AttachmentRow(props: AttachmentRow.Props) {
|
function AttachmentRow(props: AttachmentRow.Props) {
|
||||||
|
|
||||||
const download = useCallback(async () => {
|
const download = useCallback(async () => {
|
||||||
const { error, data: _data } = await client
|
const { error, data } = await client
|
||||||
.piece({ pieceId: props.attachment.pieceId })
|
.piece({ pieceId: props.attachment.pieceId })
|
||||||
.attachment({ attachmentId: props.attachment.attachmentId })
|
.attachment({ attachmentId: props.attachment.attachmentId })
|
||||||
.get();
|
.get()
|
||||||
|
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
console.error(error.value);
|
console.error(error.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = _data as unknown as typeof _data[ELYSIA_FORM_DATA];
|
const url = URL.createObjectURL(data);
|
||||||
const url = URL.createObjectURL(data.data);
|
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = data.filename;
|
a.download = data.name;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}, [props.attachment.attachmentId, props.attachment.pieceId]);
|
}, [props.attachment.attachmentId, props.attachment.pieceId]);
|
||||||
@@ -211,7 +213,7 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const { error, data: _data } = await client
|
const { error, data } = await client
|
||||||
.piece({ pieceId: props.attachment.pieceId })
|
.piece({ pieceId: props.attachment.pieceId })
|
||||||
.attachment({ attachmentId: props.attachment.attachmentId })
|
.attachment({ attachmentId: props.attachment.attachmentId })
|
||||||
.get();
|
.get();
|
||||||
@@ -221,8 +223,7 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = _data as unknown as typeof _data[ELYSIA_FORM_DATA];
|
const url = URL.createObjectURL(data);
|
||||||
const url = URL.createObjectURL(data.data);
|
|
||||||
window.open(url, "_target");
|
window.open(url, "_target");
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}, [props.attachment.mediaType, props.attachment.attachmentId, props.attachment.pieceId]);
|
}, [props.attachment.mediaType, props.attachment.attachmentId, props.attachment.pieceId]);
|
||||||
@@ -256,16 +257,18 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
props.attachment.filename
|
props.attachment.filename
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{props.attachment.mediaType}</TableCell>
|
<TableCell>
|
||||||
<TableCell className="text-center font-mono text-xs">
|
{props.attachment.mediaType}
|
||||||
{props.attachment.createdAt}
|
|
||||||
{props.attachment.createdBy !== null && <><br />przez {props.attachment.createdBy}</>}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center font-mono text-xs">
|
<TableCell className="text-center text-xs">
|
||||||
{props.attachment.modifiedAt === null && props.attachment.modifiedBy === null ? "\u2014"
|
{props.attachment.createdAt}
|
||||||
: props.attachment.modifiedAt !== null && props.attachment.modifiedBy === null ? props.attachment.modifiedAt
|
{Option.isSome(props.attachment.createdBy) && <><br />przez {props.attachment.createdBy.value.username}</>}
|
||||||
: props.attachment.modifiedAt === null ? `przez ${props.attachment.createdBy}`
|
</TableCell>
|
||||||
: <>{props.attachment.createdAt}<br />przez {props.attachment.createdBy}</>}
|
<TableCell className="text-center text-xs">
|
||||||
|
{Option.isNone(props.attachment.modifiedAt) && Option.isNone(props.attachment.modifiedBy) ? "\u2014"
|
||||||
|
: Option.isSome(props.attachment.modifiedAt) && Option.isNone(props.attachment.modifiedBy) ? props.attachment.modifiedAt.value
|
||||||
|
: Option.isNone(props.attachment.modifiedAt) ? `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}>
|
<Button type="button" variant="ghost" size="icon" title="Pobierz" onClick={download}>
|
||||||
@@ -282,7 +285,7 @@ function AttachmentRow(props: AttachmentRow.Props) {
|
|||||||
namespace AttachmentForm {
|
namespace AttachmentForm {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly pieceId: PieceId;
|
readonly pieceId: PieceId;
|
||||||
readonly setAttachments: Updater<Attachment[]>;
|
readonly setAttachments: Updater<readonly Attachment[]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,8 +326,10 @@ function AttachmentForm(props: AttachmentForm.Props) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachment = await Effect.runPromise(denormalizeSystemInformation(data));
|
||||||
|
|
||||||
props.setAttachments((prev) => {
|
props.setAttachments((prev) => {
|
||||||
const next = [...prev, data];
|
const next = [...prev, attachment];
|
||||||
next.sort((a, b) => a.filename.localeCompare(b.filename));
|
next.sort((a, b) => a.filename.localeCompare(b.filename));
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -48,6 +48,9 @@ catalogs:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
effect:
|
||||||
|
specifier: ^3.11.4
|
||||||
|
version: 3.11.4
|
||||||
elysia:
|
elysia:
|
||||||
specifier: ^1.1.25
|
specifier: ^1.1.25
|
||||||
version: 1.1.25
|
version: 1.1.25
|
||||||
@@ -156,6 +159,9 @@ importers:
|
|||||||
common:
|
common:
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../common
|
version: link:../common
|
||||||
|
effect:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 3.11.4
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.462.0(react@18.3.1)
|
version: 0.462.0(react@18.3.1)
|
||||||
@@ -1185,6 +1191,9 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
effect@3.11.4:
|
||||||
|
resolution: {integrity: sha512-0tooO72Ii109A1SUY2A73hnO3y4AoglWupbBwaUHitV/B13/EVWRhWfuEDpKjJaEDNj96fhkeU8UCWvFrh6aSg==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.4:
|
electron-to-chromium@1.5.4:
|
||||||
resolution: {integrity: sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==}
|
resolution: {integrity: sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==}
|
||||||
|
|
||||||
@@ -1242,6 +1251,10 @@ packages:
|
|||||||
exponential-backoff@3.1.1:
|
exponential-backoff@3.1.1:
|
||||||
resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
|
resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
|
||||||
|
|
||||||
|
fast-check@3.23.1:
|
||||||
|
resolution: {integrity: sha512-u/MudsoQEgBUZgR5N1v87vEgybeVYus9VnDVaIkxkkGP2jt54naghQ3PCQHJiogS8U/GavZCUPFfx3Xkp+NaHw==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
fast-decode-uri-component@1.0.1:
|
fast-decode-uri-component@1.0.1:
|
||||||
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
||||||
|
|
||||||
@@ -1758,6 +1771,9 @@ packages:
|
|||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||||
|
|
||||||
|
pure-rand@6.1.0:
|
||||||
|
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
|
||||||
|
|
||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -3061,6 +3077,10 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
effect@3.11.4:
|
||||||
|
dependencies:
|
||||||
|
fast-check: 3.23.1
|
||||||
|
|
||||||
electron-to-chromium@1.5.4: {}
|
electron-to-chromium@1.5.4: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.62: {}
|
electron-to-chromium@1.5.62: {}
|
||||||
@@ -3130,6 +3150,10 @@ snapshots:
|
|||||||
exponential-backoff@3.1.1:
|
exponential-backoff@3.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fast-check@3.23.1:
|
||||||
|
dependencies:
|
||||||
|
pure-rand: 6.1.0
|
||||||
|
|
||||||
fast-decode-uri-component@1.0.1: {}
|
fast-decode-uri-component@1.0.1: {}
|
||||||
|
|
||||||
fast-glob@3.3.2:
|
fast-glob@3.3.2:
|
||||||
@@ -3704,6 +3728,8 @@ snapshots:
|
|||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
pure-rand@6.1.0: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
rc@1.2.8:
|
rc@1.2.8:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ catalog:
|
|||||||
autoprefixer: '^10.4.20'
|
autoprefixer: '^10.4.20'
|
||||||
class-variance-authority: '^0.7.1'
|
class-variance-authority: '^0.7.1'
|
||||||
clsx: '^2.1.1'
|
clsx: '^2.1.1'
|
||||||
|
effect: '^3.11.4'
|
||||||
elysia: '^1.1.25'
|
elysia: '^1.1.25'
|
||||||
kysely: '^0.27.4'
|
kysely: '^0.27.4'
|
||||||
kysely-bun-sqlite: '^0.3.2'
|
kysely-bun-sqlite: '^0.3.2'
|
||||||
|
|||||||
Reference in New Issue
Block a user