From dfbe14d28428070da0458cac4643e0b4a0d4b170 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Thu, 16 Jul 2026 23:03:42 +0200 Subject: [PATCH] Little rework --- .gitignore | 5 +- .pnpm-store/v11/.pnpm-needs-build-marker | 0 .pnpm-store/v11/index.db | Bin 0 -> 8192 bytes README.md | 42 +- package.json | 6 +- pnpm-lock.yaml | 859 ++++++++++++++ prisma.config.ts | 12 + .../20260716164654_init/migration.sql | 61 + .../migration.sql | 10 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 66 ++ src/app/admin/page.tsx | 1047 ++++++++--------- src/app/api/admin/affiliates/[id]/route.ts | 27 + src/app/api/admin/affiliates/route.ts | 25 + src/app/api/admin/experience/[id]/route.ts | 27 + src/app/api/admin/experience/route.ts | 25 + src/app/api/admin/login/route.ts | 26 + src/app/api/admin/logout/route.ts | 8 + src/app/api/admin/projects/[id]/route.ts | 27 + src/app/api/admin/projects/route.ts | 25 + src/app/api/admin/session/route.ts | 6 + src/app/api/affiliates/route.ts | 10 + src/app/api/experience/route.ts | 10 + src/app/api/profile-image/route.ts | 43 + src/app/api/projects/route.ts | 36 + src/app/contact/page.tsx | 29 +- src/app/fonts/ClashDisplay-Variable.ttf | Bin 0 -> 88064 bytes src/app/fonts/Satoshi-Variable.ttf | Bin 0 -> 127420 bytes src/app/fonts/Satoshi-VariableItalic.ttf | Bin 0 -> 129748 bytes src/app/globals.css | 15 +- src/app/layout.tsx | 30 +- src/app/page.tsx | 140 +-- src/app/projects/page.tsx | 39 + src/components/ExperienceCard.tsx | 79 -- src/components/ExperienceModal.tsx | 119 -- src/components/MiniProjectCard.tsx | 57 - src/components/MiniProjectModal.tsx | 125 -- src/components/Navbar.tsx | 3 +- src/components/ProjectCard.tsx | 104 +- src/components/TimezoneClockBlock.tsx | 127 -- src/components/TypingRoomIntro.tsx | 220 ---- src/components/utils.ts | 35 - src/context/ProfileContext.tsx | 169 +-- src/lib/adminRoute.ts | 33 + src/lib/auth.ts | 50 + src/lib/dto.ts | 113 ++ src/lib/loc.ts | 40 + src/lib/prisma.ts | 21 + src/sections/Activity.tsx | 6 +- src/sections/Affiliates.tsx | 155 +-- src/sections/AgentSkills.tsx | 86 -- src/sections/Contact.tsx | 10 +- src/sections/FeaturedProjects.tsx | 49 + src/sections/Footer.tsx | 9 +- src/sections/Hero.tsx | 263 ++--- src/sections/Luna.tsx | 53 + src/sections/SkillsExperience.tsx | 63 - src/sections/Stats.tsx | 25 + src/sections/TechStack.tsx | 113 -- src/sections/Uptime.tsx | 39 - src/sections/WorkExperience.tsx | 223 ++-- src/types.ts | 92 +- 62 files changed, 2760 insertions(+), 2380 deletions(-) create mode 100644 .pnpm-store/v11/.pnpm-needs-build-marker create mode 100644 .pnpm-store/v11/index.db create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20260716164654_init/migration.sql create mode 100644 prisma/migrations/20260716174720_project_sort_by_size/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/app/api/admin/affiliates/[id]/route.ts create mode 100644 src/app/api/admin/affiliates/route.ts create mode 100644 src/app/api/admin/experience/[id]/route.ts create mode 100644 src/app/api/admin/experience/route.ts create mode 100644 src/app/api/admin/login/route.ts create mode 100644 src/app/api/admin/logout/route.ts create mode 100644 src/app/api/admin/projects/[id]/route.ts create mode 100644 src/app/api/admin/projects/route.ts create mode 100644 src/app/api/admin/session/route.ts create mode 100644 src/app/api/affiliates/route.ts create mode 100644 src/app/api/experience/route.ts create mode 100644 src/app/api/profile-image/route.ts create mode 100644 src/app/api/projects/route.ts create mode 100644 src/app/fonts/ClashDisplay-Variable.ttf create mode 100644 src/app/fonts/Satoshi-Variable.ttf create mode 100644 src/app/fonts/Satoshi-VariableItalic.ttf create mode 100644 src/app/projects/page.tsx delete mode 100644 src/components/ExperienceCard.tsx delete mode 100644 src/components/ExperienceModal.tsx delete mode 100644 src/components/MiniProjectCard.tsx delete mode 100644 src/components/MiniProjectModal.tsx delete mode 100644 src/components/TimezoneClockBlock.tsx delete mode 100644 src/components/TypingRoomIntro.tsx delete mode 100644 src/components/utils.ts create mode 100644 src/lib/adminRoute.ts create mode 100644 src/lib/auth.ts create mode 100644 src/lib/dto.ts create mode 100644 src/lib/loc.ts create mode 100644 src/lib/prisma.ts delete mode 100644 src/sections/AgentSkills.tsx create mode 100644 src/sections/FeaturedProjects.tsx create mode 100644 src/sections/Luna.tsx delete mode 100644 src/sections/SkillsExperience.tsx create mode 100644 src/sections/Stats.tsx delete mode 100644 src/sections/TechStack.tsx delete mode 100644 src/sections/Uptime.tsx diff --git a/.gitignore b/.gitignore index 440f170..1c50c28 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ yarn-error.log* next-env.d.ts old/ -.next \ No newline at end of file +.next + +# prisma generated client +/src/generated \ No newline at end of file diff --git a/.pnpm-store/v11/.pnpm-needs-build-marker b/.pnpm-store/v11/.pnpm-needs-build-marker new file mode 100644 index 0000000..e69de29 diff --git a/.pnpm-store/v11/index.db b/.pnpm-store/v11/index.db new file mode 100644 index 0000000000000000000000000000000000000000..8fdf9e7d3d8db312b5d08efe43a80ea9046a773f GIT binary patch literal 8192 zcmeIuKa0XJ7zXe(90-L&2f@*I^A3(%?hDvzz+$vsP3gHPQKLxxhjh@v!PQS~E2W&1 zyF3p`-Xw39{D!}Bl^9y=4jY}&534ZFS(At{#`Bq$d#rSQ%lBEy&dc$*8rJ2U=;_+*^(@v)Gg)ot=J;^ntw1bDd~%rSDSTSJV%sESjGbd8*@wiUL3K k+WEbmsaIZ*9$$h01Rwwb2tWV=5P$##AOHafK%fw~0WYO9761SM literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 0723b2e..194a78d 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,46 @@ Personal portfolio built with Next.js, React, TypeScript, and Tailwind CSS. ## What It Includes -- Hero section with typing intro -- Work experience and skills sections -- Uptime and activity panels -- Project and mini-project showcases -- Contact page and admin page +- Hero dev-card, live-count stat, and work-experience timeline +- Projects (served from our DB) with a `/projects` page and homepage teaser +- Luna section, DB-driven affiliates, and the SHSF Code Activity graph +- Admin CMS at `/admin` for projects, work experience, and affiliates + +## Data layer + +Content (projects, work experience, affiliates) lives in **Postgres** and is +managed through Prisma + our own Next.js API routes: + +- Public reads: `GET /api/projects`, `/api/experience`, `/api/affiliates` +- Admin writes: `POST/PUT/DELETE /api/admin/{projects,experience,affiliates}/…` + (cookie session issued by `POST /api/admin/login`) +- `/api/profile-image` serves the avatar (cached in memory) +- Only the **Code Activity** widget still uses the external SHSF API. + +### Environment + +Copy `.env.example` to `.env` and set: + +| Var | Purpose | +| --- | --- | +| `DATABASE_URL` | Postgres connection string | +| `ADMIN_PASSWORD` | Password for the `/admin` login | +| `SESSION_SECRET` | Long random string used to sign the admin session cookie | +| `PROFILE_IMAGE_URL` | (optional) override for the avatar source | ## Development -Install dependencies: +Install dependencies and generate the Prisma client: ```bash pnpm install +pnpm prisma generate +``` + +Apply the schema to your database: + +```bash +pnpm prisma migrate dev ``` Run the development server: @@ -61,4 +89,6 @@ The app will be available on port `6756` via `docker-compose.yml`. ## Notes - `next.config.ts` uses `output: "standalone"` so the Docker image can ship a minimal runtime. +- `pnpm build` runs `prisma generate` before `next build`. - The activity graph on the home page is loaded from a remote SVG source. +- The container needs the same env vars (`DATABASE_URL`, `ADMIN_PASSWORD`, `SESSION_SECRET`) at runtime. diff --git a/package.json b/package.json index a258c36..e631f7d 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,16 @@ "packageManager": "pnpm@10.30.3", "scripts": { "dev": "next dev -p 5173", - "build": "next build", + "build": "prisma generate && next build", "start": "next start", "lint": "eslint" }, "dependencies": { "@danielgtmn/umami-react": "^1.1.6", + "@prisma/adapter-pg": "^7.8.0", + "@prisma/client": "^7.8.0", "clsx": "^2.1.1", + "dotenv": "^17.4.2", "framer-motion": "^12.36.0", "lucide-react": "^0.577.0", "next": "16.1.6", @@ -27,6 +30,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.1.6", + "prisma": "^7.8.0", "tailwindcss": "^4", "typescript": "^5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b7a5fb..3f4efad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,18 @@ importers: '@danielgtmn/umami-react': specifier: ^1.1.6 version: 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@prisma/adapter-pg': + specifier: ^7.8.0 + version: 7.8.0 + '@prisma/client': + specifier: ^7.8.0 + version: 7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3) clsx: specifier: ^2.1.1 version: 2.1.1 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 framer-motion: specifier: ^12.36.0 version: 12.36.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -54,6 +63,9 @@ importers: eslint-config-next: specifier: 16.1.6 version: 16.1.6(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + prisma: + specifier: ^7.8.0 + version: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) tailwindcss: specifier: ^4 version: 4.2.1 @@ -141,6 +153,20 @@ packages: react: ^18.2.0 || ^19.0.0 react-dom: ^18.2.0 || ^19.0.0 + '@electric-sql/pglite-socket@0.1.1': + resolution: {integrity: sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1': + resolution: {integrity: sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==} + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': + resolution: {integrity: sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==} + '@emnapi/core@1.9.0': resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} @@ -188,6 +214,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -373,6 +405,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -450,9 +485,149 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@prisma/adapter-pg@7.8.0': + resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==} + + '@prisma/client-runtime-utils@7.8.0': + resolution: {integrity: sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==} + + '@prisma/client@7.8.0': + resolution: {integrity: sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: '>=5.4.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@7.8.0': + resolution: {integrity: sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.8.0': + resolution: {integrity: sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==} + + '@prisma/dev@0.24.3': + resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==} + + '@prisma/driver-adapter-utils@7.8.0': + resolution: {integrity: sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==} + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': + resolution: {integrity: sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==} + + '@prisma/engines@7.8.0': + resolution: {integrity: sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==} + + '@prisma/fetch-engine@7.8.0': + resolution: {integrity: sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.8.0': + resolution: {integrity: sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/streams-local@0.1.2': + resolution: {integrity: sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==} + engines: {bun: '>=1.3.6', node: '>=22.0.0'} + + '@prisma/studio-core@0.27.3': + resolution: {integrity: sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==} + engines: {node: ^20.19 || ^22.12 || >=24.0, pnpm: '8'} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -563,6 +738,9 @@ packages: '@types/node@20.19.37': resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -746,6 +924,9 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -800,6 +981,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axe-core@4.11.1: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} @@ -820,6 +1005,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + better-result@2.9.2: + resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -836,6 +1024,14 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -859,6 +1055,14 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -876,6 +1080,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -925,6 +1132,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -933,6 +1144,16 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -941,20 +1162,35 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + effect@3.20.0: + resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + electron-to-chromium@1.5.313: resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + enhanced-resolve@5.20.0: resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -1115,6 +1351,13 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1128,6 +1371,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1163,6 +1409,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + framer-motion@12.36.0: resolution: {integrity: sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw==} peerDependencies: @@ -1187,6 +1437,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -1199,6 +1452,9 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -1210,6 +1466,10 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + giget@3.3.0: + resolution: {integrity: sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1237,6 +1497,12 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grammex@3.1.13: + resolution: {integrity: sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1270,6 +1536,17 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.12.30: + resolution: {integrity: sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==} + engines: {node: '>=16.9.0'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1357,6 +1634,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1425,6 +1705,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1536,6 +1819,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1543,6 +1829,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@0.577.0: resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==} peerDependencies: @@ -1582,6 +1872,14 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1655,6 +1953,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1686,6 +1987,46 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1697,6 +2038,9 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -1709,20 +2053,66 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prisma@7.8.0: + resolution: {integrity: sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -1752,6 +2142,10 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -1760,6 +2154,13 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1777,6 +2178,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1796,6 +2201,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1808,6 +2216,9 @@ packages: engines: {node: '>=10'} hasBin: true + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -1851,13 +2262,31 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -1995,6 +2424,14 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2020,6 +2457,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2027,6 +2468,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -2145,6 +2589,16 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': {} + '@emnapi/core@1.9.0': dependencies: '@emnapi/wasi-threads': 1.2.0 @@ -2207,6 +2661,10 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@hono/node-server@1.19.11(hono@4.12.30)': + dependencies: + hono: 4.12.30 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -2334,6 +2792,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kurkle/color@0.3.4': {} + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.0 @@ -2385,8 +2845,165 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@prisma/adapter-pg@7.8.0': + dependencies: + '@prisma/driver-adapter-utils': 7.8.0 + '@types/pg': 8.20.0 + pg: 8.22.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.8.0': {} + + '@prisma/client@7.8.0(prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.8.0 + optionalDependencies: + prisma: 7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@7.8.0': + dependencies: + c12: 3.3.4 + deepmerge-ts: 7.1.5 + effect: 3.20.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.8.0': {} + + '@prisma/dev@0.24.3(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.4.1 + '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) + '@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1) + '@hono/node-server': 1.19.11(hono@4.12.30) + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + '@prisma/streams-local': 0.1.2 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.12.30 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': {} + + '@prisma/engines@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/fetch-engine': 7.8.0 + '@prisma/get-platform': 7.8.0 + + '@prisma/fetch-engine@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/get-platform': 7.8.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/streams-local@0.1.2': + dependencies: + ajv: 8.20.0 + better-result: 2.9.2 + env-paths: 3.0.0 + proper-lockfile: 4.1.2 + + '@prisma/studio-core@0.27.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/react': 19.2.14 + chart.js: 4.5.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react-dom' + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + '@rtsao/scc@1.1.0': {} + '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2475,6 +3092,12 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pg@8.20.0': + dependencies: + '@types/node': 20.19.37 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -2646,6 +3269,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -2729,6 +3359,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-ssl-profiles@1.1.2: {} + axe-core@4.11.1: {} axobject-query@4.1.0: {} @@ -2739,6 +3371,8 @@ snapshots: baseline-browser-mapping@2.10.7: {} + better-result@2.9.2: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -2760,6 +3394,21 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.1.0 + giget: 3.3.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2786,6 +3435,14 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + client-only@0.0.1: {} clsx@2.1.1: {} @@ -2798,6 +3455,8 @@ snapshots: concat-map@0.0.1: {} + confbox@0.2.4: {} + convert-source-map@2.0.0: {} cookie@1.1.1: {} @@ -2840,6 +3499,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge-ts@7.1.5: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -2852,27 +3513,44 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.7: {} + + denque@2.1.0: {} + + destr@2.0.5: {} + detect-libc@2.1.2: {} doctrine@2.1.0: dependencies: esutils: 2.0.3 + dotenv@17.4.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 + effect@3.20.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + electron-to-chromium@1.5.313: {} emoji-regex@9.2.2: {} + empathic@2.0.0: {} + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 + env-paths@3.0.0: {} + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -3184,6 +3862,12 @@ snapshots: esutils@2.0.3: {} + exsolve@1.1.0: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -3198,6 +3882,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.3: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3230,6 +3916,11 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + framer-motion@12.36.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: motion-dom: 12.36.0 @@ -3252,6 +3943,10 @@ snapshots: functions-have-names@1.2.3: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -3269,6 +3964,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-port-please@3.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -3284,6 +3981,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + giget@3.3.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3305,6 +4004,10 @@ snapshots: graceful-fs@4.2.11: {} + grammex@3.1.13: {} + + graphmatch@1.1.1: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -3333,6 +4036,14 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hono@4.12.30: {} + + http-status-codes@2.3.0: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3423,6 +4134,8 @@ snapshots: is-number@7.0.0: {} + is-property@1.0.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -3489,6 +4202,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -3574,6 +4289,8 @@ snapshots: lodash.merge@4.6.2: {} + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -3582,6 +4299,8 @@ snapshots: dependencies: yallist: 3.1.1 + lru.min@1.1.4: {} + lucide-react@0.577.0(react@19.2.3): dependencies: react: 19.2.3 @@ -3617,6 +4336,22 @@ snapshots: ms@2.1.3: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -3698,6 +4433,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + ohash@2.0.11: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3731,12 +4468,57 @@ snapshots: path-parse@1.0.7: {} + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.3: {} + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.1.0 + pathe: 2.0.3 + possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -3751,18 +4533,62 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + prelude-ls@1.2.1: {} + prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + dependencies: + '@prisma/config': 7.8.0 + '@prisma/dev': 0.24.3(typescript@5.9.3) + '@prisma/engines': 7.8.0 + '@prisma/studio-core': 0.27.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - magicast + - react + - react-dom + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + punycode@2.3.1: {} + pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -3786,6 +4612,8 @@ snapshots: react@19.2.3: {} + readdirp@5.0.0: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -3806,6 +4634,10 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remeda@2.33.4: {} + + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3825,6 +4657,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + retry@0.12.0: {} + reusify@1.1.0: {} run-parallel@1.2.0: @@ -3850,12 +4684,16 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@6.3.1: {} semver@7.7.4: {} + seq-queue@0.0.5: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -3946,10 +4784,20 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} + split2@4.2.0: {} + + sqlstring@2.3.3: {} + stable-hash@0.0.5: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -4143,6 +4991,10 @@ snapshots: dependencies: punycode: 2.3.1 + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -4190,10 +5042,17 @@ snapshots: word-wrap@1.2.5: {} + xtend@4.0.2: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.13 + graphmatch: 1.1.1 + zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6 diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..d73df7b --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,12 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/prisma/migrations/20260716164654_init/migration.sql b/prisma/migrations/20260716164654_init/migration.sql new file mode 100644 index 0000000..d885ffb --- /dev/null +++ b/prisma/migrations/20260716164654_init/migration.sql @@ -0,0 +1,61 @@ +-- CreateEnum +CREATE TYPE "ProjectSize" AS ENUM ('Big', 'MediumSized', 'Small'); + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "label" TEXT NOT NULL, + "size" "ProjectSize" NOT NULL DEFAULT 'MediumSized', + "imageUrl" TEXT, + "link" TEXT NOT NULL, + "linkIsDemo" BOOLEAN NOT NULL DEFAULT false, + "sourceCode" TEXT, + "description" TEXT NOT NULL, + "why" TEXT NOT NULL, + "note" TEXT, + "tags" TEXT[], + "loc" INTEGER, + "locEndpoint" TEXT, + "featured" BOOLEAN NOT NULL DEFAULT false, + "sortIndex" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "WorkExperience" ( + "id" TEXT NOT NULL, + "company" TEXT NOT NULL, + "role" TEXT NOT NULL, + "fromDate" TIMESTAMP(3) NOT NULL, + "toDate" TIMESTAMP(3), + "url" TEXT, + "iconUrl" TEXT, + "summary" TEXT NOT NULL, + "tags" TEXT[], + "sortIndex" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "WorkExperience_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Affiliate" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "link" TEXT NOT NULL, + "icon" TEXT NOT NULL, + "location" TEXT NOT NULL, + "provides" TEXT[], + "good" TEXT[], + "bad" TEXT[], + "sortIndex" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Affiliate_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20260716174720_project_sort_by_size/migration.sql b/prisma/migrations/20260716174720_project_sort_by_size/migration.sql new file mode 100644 index 0000000..9ce2b2c --- /dev/null +++ b/prisma/migrations/20260716174720_project_sort_by_size/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `featured` on the `Project` table. All the data in the column will be lost. + - You are about to drop the column `sortIndex` on the `Project` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "featured", +DROP COLUMN "sortIndex"; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d44d1e9 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,66 @@ +// Prisma 7 (rust-free `prisma-client` generator + driver adapter). +// Client is generated into src/generated/prisma (gitignored) and imported +// from "../generated/prisma/client". + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +enum ProjectSize { + Big + MediumSized + Small +} + +model Project { + id String @id @default(cuid()) + name String + label String // free-type string, e.g. "Dev Tool", "SaaS" + size ProjectSize @default(MediumSized) + imageUrl String? // shown fully, nothing overlapping + link String + linkIsDemo Boolean @default(false) + sourceCode String? // github or gitea link + description String + why String + note String? // free note, replaces old {color, content} + tags String[] // max 3 enforced in the API + loc Int? // manually provided line-of-code count + locEndpoint String? // URL the backend hits for a plaintext LoC number + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model WorkExperience { + id String @id @default(cuid()) + company String + role String + fromDate DateTime + toDate DateTime? // null = Present + url String? + iconUrl String? + summary String + tags String[] // max 6 enforced in the API + sortIndex Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Affiliate { + id String @id @default(cuid()) + name String + link String + icon String + location String + provides String[] + good String[] + bad String[] + sortIndex Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 0878624..ac4c9c0 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,666 +1,543 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect, useState, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useRouter } from "next/navigation"; -import type { Project, MiniProject, Experience, RealWork, Skill } from "../../types"; +import { PROJECT_SIZES, SIZE_LABELS, type ProjectSize } from "../../types"; -const API_BASE = "https://shsf-api.reversed.dev/api/exec/4/e942538c-caa1-49d1-8953-dfab1e62f8cb"; +type Resource = "projects" | "experience" | "affiliates"; -type EditableFile = "projects.json" | "mini_projects.json" | "experience.json" | "real_work.json" | "skills.json"; +const RESOURCES: { key: Resource; label: string }[] = [ + { key: "projects", label: "Projects" }, + { key: "experience", label: "Work Experience" }, + { key: "affiliates", label: "Affiliates" }, +]; + +// A record being edited; `_new` marks an unsaved draft (POST vs PUT). +type Row = Record & { id?: string; _new?: boolean }; + +const TEMPLATES: Record Row> = { + projects: () => ({ + _new: true, + name: "", + label: "", + size: "MediumSized", + imageUrl: "", + link: "", + linkIsDemo: false, + sourceCode: "", + description: "", + why: "", + note: "", + tags: [], + loc: null, + locEndpoint: "", + }), + experience: () => ({ + _new: true, + company: "", + role: "", + fromDate: "", + toDate: "", + url: "", + iconUrl: "", + summary: "", + tags: [], + sortIndex: 0, + }), + affiliates: () => ({ + _new: true, + name: "", + link: "", + icon: "", + location: "", + provides: [], + good: [], + bad: [], + sortIndex: 0, + }), +}; + +const inputCls = + "w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm text-gray-100 outline-none focus:border-blue-500/50"; +const labelCls = "text-xs font-bold text-gray-500 uppercase"; + +function toDateInput(value: unknown): string { + if (!value) return ""; + const d = new Date(String(value)); + if (Number.isNaN(d.getTime())) return ""; + return d.toISOString().slice(0, 10); +} export default function AdminPage() { - const [password, setPassword] = useState(""); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [error, setError] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - // Editor State - const [selectedFile, setSelectedFile] = useState("real_work.json"); - const [data, setData] = useState([]); - const [isSaving, setIsSaving] = useState(false); - const [saveStatus, setSaveStatus] = useState<{ type: "success" | "error"; message: string } | null>(null); - const [newTagDrafts, setNewTagDrafts] = useState>({}); - const [newSkillListDrafts, setNewSkillListDrafts] = useState>({}); - // Import state - const [importFileName, setImportFileName] = useState(""); - const [importFileContent, setImportFileContent] = useState(null); - const [importError, setImportError] = useState(""); - const getSectionLabel = (file: EditableFile) => (file === "skills.json" ? "Agent Skills" : file.replace(".json", "").replace("_", " ")); - // Handle file import - const handleImportFile = async (e: React.ChangeEvent) => { - setImportError(""); - const file = e.target.files?.[0]; - if (!file) return; - setImportFileName(file.name); // Auto-fill filename input - const reader = new FileReader(); - reader.onload = (ev) => { - try { - const text = ev.target?.result as string; - setImportFileContent(text); - } catch (err) { - setImportError("Failed to read file"); - } - }; - reader.onerror = () => setImportError("Failed to read file"); - reader.readAsText(file); - }; - - // Apply imported file content - const handleApplyImport = () => { - setImportError(""); - if (!importFileContent) { - setImportError("No file content loaded"); - return; - } - try { - const parsed = JSON.parse(importFileContent); - setData(Array.isArray(parsed) ? parsed : []); - if (importFileName && ["projects.json", "mini_projects.json", "experience.json", "real_work.json", "skills.json"].includes(importFileName)) { - setSelectedFile(importFileName as EditableFile); - } - setImportFileContent(null); - setImportFileName(""); - } catch (err) { - setImportError("Invalid JSON file"); - } - }; - const router = useRouter(); + const [authChecked, setAuthChecked] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [password, setPassword] = useState(""); + const [loginError, setLoginError] = useState(""); + const [loggingIn, setLoggingIn] = useState(false); + + const [resource, setResource] = useState("projects"); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null); + const [savingId, setSavingId] = useState(null); + + const showToast = (type: "success" | "error", message: string) => { + setToast({ type, message }); + setTimeout(() => setToast(null), 3500); + }; + + // --- auth --- + useEffect(() => { + fetch("/api/admin/session") + .then((r) => r.json()) + .then((d) => setIsAuthenticated(Boolean(d.authenticated))) + .catch(() => setIsAuthenticated(false)) + .finally(() => setAuthChecked(true)); + }, []); + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - setIsLoading(true); - setError(""); - + setLoggingIn(true); + setLoginError(""); try { - const response = await fetch(`${API_BASE}/password`, { + const res = await fetch("/api/admin/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); - - const result = await response.text(); - if (response.ok && result.trim().toLowerCase() === "yay") { + if (res.ok) { setIsAuthenticated(true); + setPassword(""); } else { - setError("Invalid password"); + setLoginError("Invalid password"); } - } catch (err) { - setError("An error occurred. Please try again."); + } catch { + setLoginError("Network error"); } finally { - setIsLoading(false); + setLoggingIn(false); } }; - const fetchData = async (file: EditableFile) => { - setIsLoading(true); - setSaveStatus(null); + const handleLogout = async () => { + await fetch("/api/admin/logout", { method: "POST" }); + setIsAuthenticated(false); + }; + + // --- data --- + const loadRows = useCallback(async (res: Resource) => { + setLoading(true); try { - const route = file.replace(".json", ""); - const response = await fetch(`${API_BASE}/${route}`); - if (response.ok) { - const json = await response.json(); - setData(Array.isArray(json) ? json : []); - } else if (response.status === 404) { - // No data found for this file, show empty list - setData([]); + const r = await fetch(`/api/admin/${res}`); + if (r.ok) { + const json = await r.json(); + const normalized = Array.isArray(json) + ? json.map((item: Row) => + res === "experience" + ? { ...item, fromDate: toDateInput(item.fromDate), toDate: toDateInput(item.toDate) } + : item, + ) + : []; + setRows(normalized); + } else if (r.status === 401) { + setIsAuthenticated(false); } else { - setSaveStatus({ type: "error", message: `Failed to fetch data (${response.status})` }); + showToast("error", `Failed to load (${r.status})`); } - } catch (err) { - setSaveStatus({ type: "error", message: "Failed to fetch data" }); + } catch { + showToast("error", "Failed to load data"); } finally { - setIsLoading(false); + setLoading(false); } - }; - - const handleSave = async () => { - setIsSaving(true); - setSaveStatus(null); - try { - const response = await fetch(`${API_BASE}/writefile`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - password, - filename: selectedFile, - content: JSON.stringify(data, null, 2), - }), - }); - - const result = await response.text(); - if (response.ok && result.trim().toLowerCase() === "yay") { - setSaveStatus({ type: "success", message: "Changes deployed successfully!" }); - } else { - setSaveStatus({ type: "error", message: "Deploy failed." }); - } - } catch (err) { - setSaveStatus({ type: "error", message: "Network error" }); - } finally { - setIsSaving(false); - } - }; + }, []); useEffect(() => { - if (isAuthenticated) fetchData(selectedFile); - }, [isAuthenticated, selectedFile]); + if (isAuthenticated) loadRows(resource); + }, [isAuthenticated, resource, loadRows]); - const updateEntry = (index: number, field: string, value: any) => { - const newData = [...data]; - const keys = field.split("."); - if (keys.length === 2) { - newData[index][keys[0]] = { ...newData[index][keys[0]], [keys[1]]: value }; - } else { - newData[index][field] = value; + const update = (idx: number, field: string, value: unknown) => { + setRows((prev) => prev.map((row, i) => (i === idx ? { ...row, [field]: value } : row))); + }; + + const addRow = () => setRows((prev) => [TEMPLATES[resource](), ...prev]); + + const saveRow = async (idx: number) => { + const row = rows[idx]; + const isNew = row._new || !row.id; + setSavingId(row.id || `new-${idx}`); + try { + const url = isNew ? `/api/admin/${resource}` : `/api/admin/${resource}/${row.id}`; + const { _new, ...payload } = row; + const res = await fetch(url, { + method: isNew ? "POST" : "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (res.ok) { + showToast("success", "Saved"); + await loadRows(resource); + } else { + const err = await res.json().catch(() => ({})); + showToast("error", err.error || `Save failed (${res.status})`); + } + } catch { + showToast("error", "Network error"); + } finally { + setSavingId(null); } - setData(newData); }; - const addEntry = () => { - const templates: Record = { - "projects.json": { name: "", description: "", link: "", open_source: false }, - "mini_projects.json": { title: "", description: "", why: "" }, - "experience.json": { name: "", type: "experience", description: "" }, - "real_work.json": { company: "", role: "", from: "", until: "", summary: "" }, - "skills.json": { name: "", description: "", purpose: "", what_it_solves: "", link: "", emojis: [], tags: [] } - }; - setData([templates[selectedFile], ...data]); + const deleteRow = async (idx: number) => { + const row = rows[idx]; + if (row._new || !row.id) { + setRows((prev) => prev.filter((_, i) => i !== idx)); + return; + } + if (!confirm("Delete this entry permanently?")) return; + try { + const res = await fetch(`/api/admin/${resource}/${row.id}`, { method: "DELETE" }); + if (res.ok) { + showToast("success", "Deleted"); + setRows((prev) => prev.filter((_, i) => i !== idx)); + } else { + showToast("error", `Delete failed (${res.status})`); + } + } catch { + showToast("error", "Network error"); + } }; - const removeEntry = (index: number) => { - setData(data.filter((_, i) => i !== index)); - }; - - const updateRealWorkTag = (index: number, tagIndex: number, value: string) => { - const item = data[index] as RealWork; - const currentTags = [...(item.tags || [])]; - currentTags[tagIndex] = value; - updateEntry(index, "tags", currentTags); - }; - - const removeRealWorkTag = (index: number, tagIndex: number) => { - const item = data[index] as RealWork; - const nextTags = (item.tags || []).filter((_, i) => i !== tagIndex); - updateEntry(index, "tags", nextTags); - }; - - const addRealWorkTag = (index: number) => { - const draft = (newTagDrafts[index] || "").trim(); - if (!draft) return; - - const item = data[index] as RealWork; - const nextTags = [...(item.tags || []), draft]; - updateEntry(index, "tags", nextTags); - setNewTagDrafts((prev) => ({ ...prev, [index]: "" })); - }; - - const updateSkillArrayItem = (index: number, field: "emojis" | "tags", itemIndex: number, value: string) => { - const current = [...((data[index] as Skill)?.[field] || [])]; - current[itemIndex] = value; - updateEntry(index, field, current); - }; - - const removeSkillArrayItem = (index: number, field: "emojis" | "tags", itemIndex: number) => { - const current = [...((data[index] as Skill)?.[field] || [])]; - updateEntry(index, field, current.filter((_, i) => i !== itemIndex)); - }; - - const addSkillArrayItem = (index: number, field: "emojis" | "tags") => { - const key = `${index}:${field}`; - const draft = (newSkillListDrafts[key] || "").trim(); - if (!draft) return; - - const current = [...((data[index] as Skill)?.[field] || []), draft]; - updateEntry(index, field, current); - setNewSkillListDrafts((prev) => ({ ...prev, [key]: "" })); - }; - - const renderProjectEditor = (item: Project, idx: number) => ( -
-
- - updateEntry(idx, "name", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" /> -
-
- - updateEntry(idx, "link", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" /> -
-
- -