Initial commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
dist
|
||||
.astro
|
||||
.git
|
||||
.vscode
|
||||
*.log
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
@@ -0,0 +1,3 @@
|
||||
# URL of the PocketBase backend (auth + database).
|
||||
# Falls back to the production instance if unset.
|
||||
POCKETBASE_URL=https://pokeshare.namarusaja.me
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
## Development
|
||||
|
||||
When starting the dev server, use background mode:
|
||||
|
||||
```
|
||||
astro dev --background
|
||||
```
|
||||
|
||||
Manage the background server with `astro dev stop`, `astro dev status`, and `astro dev logs`.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation: https://docs.astro.build
|
||||
|
||||
Consult these guides before working on related tasks:
|
||||
|
||||
- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/)
|
||||
- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/)
|
||||
- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/)
|
||||
- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/)
|
||||
- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/)
|
||||
- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/)
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# --- Build stage: install deps and build the Astro SSR bundle
|
||||
FROM oven/bun:1-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
# --- Deps stage: production-only node_modules for the runtime
|
||||
FROM oven/bun:1-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json bun.lock ./
|
||||
RUN bun install --frozen-lockfile --production
|
||||
|
||||
# --- Runtime stage: Astro Node standalone server
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=4321
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY package.json ./
|
||||
EXPOSE 4321
|
||||
CMD ["node", "dist/server/entry.mjs"]
|
||||
@@ -0,0 +1,51 @@
|
||||
# PokeShare
|
||||
|
||||
A French-language Pokémon TCG collection tracker and sharing app. Users search
|
||||
cards from the [TCGdex](https://tcgdex.dev) catalog, track their personal
|
||||
collection with Cardmarket price valuations, and share collections with friends.
|
||||
|
||||
## Stack
|
||||
|
||||
- [Astro](https://astro.build) (SSR, Node standalone adapter) — no UI framework
|
||||
- [Tailwind CSS v4](https://tailwindcss.com) + [basecoat-css](https://basecoatui.com)
|
||||
- [PocketBase](https://pocketbase.io) for database and authentication
|
||||
- [TCGdex API](https://api.tcgdex.net) (French locale) for card data, with an
|
||||
in-memory server-side cache (`src/lib/tcgdex-cache.ts`)
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Action |
|
||||
| ----------------- | -------------------------------------- |
|
||||
| `bun install` | Install dependencies |
|
||||
| `bun run dev` | Start dev server at `localhost:4321` |
|
||||
| `bun run build` | Build the production site to `./dist/` |
|
||||
| `bun run preview` | Preview the production build locally |
|
||||
|
||||
Production runs as a standalone Node server (`node dist/server/entry.mjs`),
|
||||
Node >= 22.12.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------- | -------------------------------------------- | ------------------------------------ |
|
||||
| `POCKETBASE_URL` | URL of the PocketBase backend (auth + data) | `https://pokeshare.namarusaja.me` |
|
||||
|
||||
Read at **runtime** (`process.env`), so the same Docker image can be deployed
|
||||
against different PocketBase instances. For local dev, copy `.env.example` to
|
||||
`.env`. `HOST` and `PORT` are also honored by the standalone server.
|
||||
|
||||
## Docker
|
||||
|
||||
```sh
|
||||
docker build -t pokeshare .
|
||||
docker run -p 4321:4321 -e POCKETBASE_URL=https://pb.example.com pokeshare
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
- `src/pages/` — routes (`/`, `/search`, `/mycards`, `/card/[id]`,
|
||||
`/series/[id]`, `/sets/[id]`, `/friends`, `/profile`) and JSON API endpoints
|
||||
under `src/pages/api/`
|
||||
- `src/middleware.ts` — per-request PocketBase client + `pb_auth` cookie auth
|
||||
- `src/lib/` — TCGdex cache, collection pricing helpers, type theming
|
||||
- `src/layouts/`, `src/components/` — base layout and nav
|
||||
@@ -0,0 +1,18 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import node from '@astrojs/node';
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node({
|
||||
mode: 'standalone'
|
||||
}),
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,727 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "pokeshare",
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^11.0.2",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"astro": "^7.1.3",
|
||||
"basecoat-css": "^1.0.2",
|
||||
"pocketbase": "^0.27.0",
|
||||
"tailwindcss": "^4.3.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@astrojs/compiler-binding": ["@astrojs/compiler-binding@0.3.1", "", { "optionalDependencies": { "@astrojs/compiler-binding-darwin-arm64": "0.3.1", "@astrojs/compiler-binding-darwin-x64": "0.3.1", "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.1", "@astrojs/compiler-binding-linux-arm64-musl": "0.3.1", "@astrojs/compiler-binding-linux-x64-gnu": "0.3.1", "@astrojs/compiler-binding-linux-x64-musl": "0.3.1", "@astrojs/compiler-binding-wasm32-wasi": "0.3.1", "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.1", "@astrojs/compiler-binding-win32-x64-msvc": "0.3.1" } }, "sha512-DaAUj29AIBU2XdJ8uwcab8lW5O2pk9pY8AXkcMw0sw77nVa3oeTYRcO+Dvbbpoexf6ThMc0FMWYCQ/wN1/T7oQ=="],
|
||||
|
||||
"@astrojs/compiler-binding-darwin-arm64": ["@astrojs/compiler-binding-darwin-arm64@0.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IEmEF2fUIlTHtpeE/isyEGVOB14cEyh/LZOFYt6wn3jNyVpdC8aR5OZ+RzFUR/f+8ZDM1LaMwZKvoA7eMyJeFw=="],
|
||||
|
||||
"@astrojs/compiler-binding-darwin-x64": ["@astrojs/compiler-binding-darwin-x64@0.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-GF2kIxjpPDLsn94zbZNMsxEmkU828QqnmM7kiQJnaooS3jmI+I7kk6+oI6EpwOsK3femCMdcm+wmOsEqtGrmjQ=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XJL3SDmOtVrqFhCirNcHwE91+IesJqlgNo23I4qW9QUYfwzm/TBZuH61fgqsb1ttgR1mMYz6ooPWs0JDhwMqpQ=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-xqE8BVbDoBueK/B47w30PtkVofUWJKGkwoMVE+EOMLf11rnoANxIAdA9FPqY+rng4oNI5ndHGsri1yPj2k8vZQ=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-1y0StU1qiCuDFH3rmbRJXcxdfHxFPrES1Rd+RLffosvUR7I2cH5SF5SFnBN9vXpzpkmyElZm3Yr47iJBPN7vVA=="],
|
||||
|
||||
"@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-16q0fYf7kpbmdObZEeZJEup8hQv/whgNwVjrSvT8umrKwLDSnNIWiQpm09lQQu6bweZB0XyIvHwlPitvJhC+hg=="],
|
||||
|
||||
"@astrojs/compiler-binding-wasm32-wasi": ["@astrojs/compiler-binding-wasm32-wasi@0.3.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-cB456shIwDv/PrVT+2QG7LFndpHkVge5HjqADKZgGaAc9JHVktCtjSrcdkRQ+3tbkPazNKaTLRjXLIiz2NIx9g=="],
|
||||
|
||||
"@astrojs/compiler-binding-win32-arm64-msvc": ["@astrojs/compiler-binding-win32-arm64-msvc@0.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ur/9+If/yTE69mmeX5MqSZndL0HOyx67GeNZUy3N7wVdWpLz9UTJXwyWS4UR2PUQHitghjsM5xoX0Ge56WRVQQ=="],
|
||||
|
||||
"@astrojs/compiler-binding-win32-x64-msvc": ["@astrojs/compiler-binding-win32-x64-msvc@0.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k0W+kDBzDkNZOqu4kElDvCOIbKw5Ut9S1WZ1Krj3KTgNuBERNKXsMMsRLLcbgfdMdbe7bTekQLshZrrvmYpmwA=="],
|
||||
|
||||
"@astrojs/compiler-rs": ["@astrojs/compiler-rs@0.3.1", "", { "dependencies": { "@astrojs/compiler-binding": "0.3.1" } }, "sha512-aT7xkgsbNoS6nriY5qKpbihK43slFHO41iqgHCTdOvn1ifaQxLCc5yXy+6GzAtiafoaC1zA7OwVXCXMsvUZOkg=="],
|
||||
|
||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="],
|
||||
|
||||
"@astrojs/markdown-satteri": ["@astrojs/markdown-satteri@0.3.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "satteri": "^0.9.1" } }, "sha512-6Lvt/bQZEBW+zzdhPblvfZEy5PGEYJaUsUqaCgwHeRPxZJL1gc9I+DRLKWJjjYTWDzVUTzXlMq4WwSK+X34CVw=="],
|
||||
|
||||
"@astrojs/node": ["@astrojs/node@11.0.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "send": "^1.2.1", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^7.0.0" } }, "sha512-/ijULxT+A5Cm8wSwWZ2vgqfim1b05D6B8n/a9l6MMA4FCotIH73g7fL7y76XojKXpTe75FVvQH92OxsMqea9kQ=="],
|
||||
|
||||
"@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="],
|
||||
|
||||
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||
|
||||
"@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow=="],
|
||||
|
||||
"@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q=="],
|
||||
|
||||
"@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA=="],
|
||||
|
||||
"@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ=="],
|
||||
|
||||
"@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw=="],
|
||||
|
||||
"@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q=="],
|
||||
|
||||
"@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.9.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ=="],
|
||||
|
||||
"@bruits/satteri-win32-arm64-msvc": ["@bruits/satteri-win32-arm64-msvc@0.9.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg=="],
|
||||
|
||||
"@bruits/satteri-win32-x64-msvc": ["@bruits/satteri-win32-x64-msvc@0.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g=="],
|
||||
|
||||
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="],
|
||||
|
||||
"@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
|
||||
|
||||
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
|
||||
|
||||
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@4.3.1", "", { "dependencies": { "@shikijs/primitive": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ=="],
|
||||
|
||||
"@shikijs/primitive": ["@shikijs/primitive@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@4.3.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
|
||||
|
||||
"am-i-vibing": ["am-i-vibing@0.4.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"basecoat-css": ["basecoat-css@1.0.2", "", {}, "sha512-C6I5rr7HziIAoBYNGPO5U6HaU/6FuF3iVp++IgoEqqJ836is3XoRty6l1ibXqxnoIRZNnOAYDMX8QxVC+58zhw=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
|
||||
|
||||
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
|
||||
|
||||
"cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="],
|
||||
|
||||
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="],
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||
|
||||
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||
|
||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
||||
|
||||
"fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="],
|
||||
|
||||
"fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="],
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
|
||||
|
||||
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
|
||||
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"magicast": ["magicast@0.5.3", "", { "dependencies": { "@babel/parser": "^7.29.3", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||
|
||||
"neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="],
|
||||
|
||||
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
|
||||
|
||||
"p-limit": ["p-limit@7.3.1", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q=="],
|
||||
|
||||
"p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="],
|
||||
|
||||
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.27.0", "", {}, "sha512-K5N6d93UP/BNMbMnlZ6BUfy9VPCIvLyqhJFOsNI8OsZwzvKWEAfyD36boi5K4ECIOl5HMlo0TzuaeGdKpMwizQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"process-ancestry": ["process-ancestry@0.1.0", "", {}, "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg=="],
|
||||
|
||||
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
|
||||
|
||||
"rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
|
||||
|
||||
"satteri": ["satteri@0.9.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.9.5", "@bruits/satteri-darwin-x64": "0.9.5", "@bruits/satteri-linux-arm64-gnu": "0.9.5", "@bruits/satteri-linux-arm64-musl": "0.9.5", "@bruits/satteri-linux-x64-gnu": "0.9.5", "@bruits/satteri-linux-x64-musl": "0.9.5", "@bruits/satteri-wasm32-wasi": "0.9.5", "@bruits/satteri-win32-arm64-msvc": "0.9.5", "@bruits/satteri-win32-x64-msvc": "0.9.5" } }, "sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w=="],
|
||||
|
||||
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
|
||||
|
||||
"shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"smol-toml": ["smol-toml@1.7.0", "", {}, "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
||||
"tinyclip": ["tinyclip@0.1.15", "", {}, "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||
|
||||
"ultrahtml": ["ultrahtml@1.7.0", "", {}, "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
|
||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||
}
|
||||
}
|
||||
Generated
+4727
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "pokeshare",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^11.0.2",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"astro": "^7.1.3",
|
||||
"basecoat-css": "^1.0.2",
|
||||
"pocketbase": "^0.27.0",
|
||||
"tailwindcss": "^4.3.3"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 655 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1,97 @@
|
||||
<div class="card max-w-sm mx-auto mt-16">
|
||||
<header>
|
||||
<h2>Connexion</h2>
|
||||
<p>Connectez-vous à votre compte PokeShare</p>
|
||||
</header>
|
||||
<section>
|
||||
<form id="login-form" class="grid gap-4">
|
||||
<div role="group" class="field">
|
||||
<label for="username">Nom d'utilisateur</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="votre@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div role="group" class="field">
|
||||
<label for="password">Mot de passe</label>
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="flex-col gap-2">
|
||||
<button
|
||||
class="btn w-full"
|
||||
type="submit"
|
||||
form="login-form"
|
||||
id="login-submit"
|
||||
>
|
||||
Se connecter
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById("login-form");
|
||||
const submitBtn = document.getElementById(
|
||||
"login-submit",
|
||||
) as HTMLButtonElement;
|
||||
const toaster = document.getElementById("toaster") as ToasterElement;
|
||||
|
||||
const showToast = (
|
||||
category: "success" | "error",
|
||||
title: string,
|
||||
description: string,
|
||||
) => {
|
||||
if (toaster?.toast) {
|
||||
toaster.toast({ category, title, description });
|
||||
}
|
||||
};
|
||||
|
||||
form?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form as HTMLFormElement);
|
||||
const username = formData.get("username");
|
||||
const password = formData.get("password");
|
||||
|
||||
submitBtn.disabled = true;
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = `<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg> Connexion...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast(
|
||||
"error",
|
||||
"Erreur",
|
||||
data.error ?? "Identifiants incorrects",
|
||||
);
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
const { pb } = Astro.locals;
|
||||
const user = pb.authStore.record;
|
||||
const initials = user
|
||||
? (user.name || user.email || "")
|
||||
.split(/[\s@.]+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((s: string) => s[0].toUpperCase())
|
||||
.join("")
|
||||
: "";
|
||||
---
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur"
|
||||
>
|
||||
<nav
|
||||
class="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-8"
|
||||
>
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center gap-2 text-lg font-bold no-underline"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2a10 10 0 0 1 10 10a10 10 0 0 1-10 10A10 10 0 0 1 2 12A10 10 0 0 1 12 2m0 2c-4.08 0-7.45 3.05-7.94 7h4.07c.44-1.73 2.01-3 3.87-3s3.43 1.27 3.87 3h4.07c-.49-3.95-3.86-7-7.94-7m0 16c4.08 0 7.45-3.05 7.94-7h-4.07c-.44 1.73-2.01 3-3.87 3s-3.43-1.27-3.87-3H4.06c.49 3.95 3.86 7 7.94 7m0-10a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2"
|
||||
></path>
|
||||
</svg>
|
||||
PokeShare
|
||||
</a>
|
||||
|
||||
<ul class="flex items-center gap-1 list-none m-0 p-0">
|
||||
<li>
|
||||
<a
|
||||
href="/friends"
|
||||
class="btn relative"
|
||||
data-variant="ghost"
|
||||
data-size="icon-sm"
|
||||
aria-label="Amis"
|
||||
title="Amis"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"
|
||||
></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/search"
|
||||
class="btn"
|
||||
data-variant="ghost"
|
||||
data-size="sm"
|
||||
>
|
||||
<svg
|
||||
data-icon="inline-start"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5l-1.5 1.5l-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16A6.5 6.5 0 0 1 3 9.5A6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14S14 12 14 9.5S12 5 9.5 5"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Rechercher</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/mycards"
|
||||
class="btn"
|
||||
data-variant="ghost"
|
||||
data-size="sm"
|
||||
>
|
||||
<svg
|
||||
data-icon="inline-start"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m12.1 18.55l-.1.1l-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05M16.5 3c-1.74 0-3.41.81-4.5 2.08C10.91 3.81 9.24 3 7.5 3C4.42 3 2 5.41 2 8.5c0 3.77 3.4 6.86 8.55 11.53L12 21.35l1.45-1.32C18.6 15.36 22 12.27 22 8.5C22 5.41 19.58 3 16.5 3"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Mes cartes</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{
|
||||
user ? (
|
||||
<li class="ml-2">
|
||||
<div id="user-menu" class="dropdown-menu">
|
||||
<button
|
||||
type="button"
|
||||
id="user-menu-trigger"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="user-menu-dropdown"
|
||||
aria-expanded="false"
|
||||
class="btn rounded-full"
|
||||
data-variant="ghost"
|
||||
data-size="sm"
|
||||
>
|
||||
<span class="avatar" data-size="sm">
|
||||
<span>{initials}</span>
|
||||
</span>
|
||||
<span class="hidden sm:inline ml-1 text-sm">
|
||||
{user.name || user.email}
|
||||
</span>
|
||||
<svg
|
||||
data-icon="inline-end"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
id="user-menu-popover"
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
data-align="end"
|
||||
class="w-48"
|
||||
>
|
||||
<div
|
||||
role="menu"
|
||||
id="user-menu-dropdown"
|
||||
aria-labelledby="user-menu-trigger"
|
||||
>
|
||||
<div role="group">
|
||||
<div
|
||||
role="heading"
|
||||
class="px-2 py-1.5 text-xs opacity-60"
|
||||
>
|
||||
{user.email}
|
||||
</div>
|
||||
<div
|
||||
role="menuitem"
|
||||
onclick="window.location.href='/mycards'"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m12.1 18.55-.1.1-.11-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04 1 3.57 2.36h1.86C13.46 6 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05" />
|
||||
</svg>
|
||||
Ma collection
|
||||
</div>
|
||||
<div
|
||||
role="menuitem"
|
||||
onclick="window.location.href='/profile'"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
Profil
|
||||
</div>
|
||||
</div>
|
||||
<hr role="separator" />
|
||||
<div role="group">
|
||||
<div
|
||||
role="menuitem"
|
||||
onclick="fetch('/api/logout', { method: 'POST' }).finally(() => window.location.href='/login')"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m16 17 5-5-5-5" />
|
||||
<path d="M21 12H9" />
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
</svg>
|
||||
Déconnexion
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<a
|
||||
href="/login"
|
||||
class="btn"
|
||||
data-variant="outline"
|
||||
data-size="sm"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle dark mode"
|
||||
onclick="window.basecoat.theme.toggle()"
|
||||
class="btn"
|
||||
data-variant="ghost"
|
||||
data-size="icon-sm"
|
||||
>
|
||||
<span class="hidden dark:block">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M12 2v2"></path>
|
||||
<path d="M12 20v2"></path>
|
||||
<path d="m4.93 4.93 1.41 1.41"></path>
|
||||
<path d="m17.66 17.66 1.41 1.41"></path>
|
||||
<path d="M2 12h2"></path>
|
||||
<path d="M20 12h2"></path>
|
||||
<path d="m6.34 17.66-1.41 1.41"></path>
|
||||
<path d="m19.07 4.93-1.41 1.41"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="block dark:hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
pb: import("pocketbase").default;
|
||||
user: import("pocketbase").default["authStore"]["record"];
|
||||
}
|
||||
}
|
||||
|
||||
interface ToasterElement extends HTMLDivElement {
|
||||
toast: (config: {
|
||||
category?: "success" | "error" | "info" | "warning";
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => void;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
import Nav from "../components/Nav.astro";
|
||||
import "../styles/global.css";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
const user = pb.authStore.record;
|
||||
const themePreference = user?.theme_preference ?? "System";
|
||||
|
||||
// Determine if dark mode should be active
|
||||
let isDark = false;
|
||||
if (themePreference === "Dark") {
|
||||
isDark = true;
|
||||
} else if (themePreference === "Light") {
|
||||
isDark = false;
|
||||
} else {
|
||||
// System — let the inline script handle it
|
||||
isDark = false;
|
||||
}
|
||||
---
|
||||
|
||||
<html
|
||||
lang="en"
|
||||
class={isDark ? "dark" : undefined}
|
||||
data-theme={themePreference === "Light" ? "light" : undefined}
|
||||
>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>PokeShare</title>
|
||||
<script is:inline>
|
||||
(() => {
|
||||
try {
|
||||
const html = document.documentElement;
|
||||
// Server told us to be light — don't add dark
|
||||
if (html.dataset.theme === "light") return;
|
||||
// Server told us to be dark — don't remove it
|
||||
if (html.classList.contains("dark")) return;
|
||||
|
||||
const stored = localStorage.getItem("themeMode");
|
||||
if (
|
||||
stored
|
||||
? stored === "dark"
|
||||
: matchMedia("(prefers-color-scheme: dark)").matches
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-dvh bg-background text-foreground antialiased">
|
||||
<Nav />
|
||||
<slot />
|
||||
<div id="toaster" class="toaster" data-align="end"></div>
|
||||
<script>
|
||||
import "basecoat-css/basecoat";
|
||||
import "basecoat-css/tabs";
|
||||
import "basecoat-css/dropdown-menu";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Instant client-side filtering and sorting for the "Ma collection" page.
|
||||
* Works on the data-* attributes rendered on each grid item and table row,
|
||||
* and keeps both views (grid + list) in sync.
|
||||
*/
|
||||
export function initCollectionFilter() {
|
||||
const nameInput = document.getElementById(
|
||||
"cf-name",
|
||||
) as HTMLInputElement | null;
|
||||
const setSelect = document.getElementById(
|
||||
"cf-set",
|
||||
) as HTMLSelectElement | null;
|
||||
const sortSelect = document.getElementById(
|
||||
"cf-sort",
|
||||
) as HTMLSelectElement | null;
|
||||
const resetBtn = document.getElementById("cf-reset");
|
||||
const gridView = document.getElementById("grid-view");
|
||||
const listBody = document.querySelector<HTMLElement>("#list-view tbody");
|
||||
const emptyState = document.getElementById("cf-empty");
|
||||
|
||||
if (!gridView || !listBody) return;
|
||||
|
||||
// Remember the server-rendered order ("Ajout récent") so it can be restored
|
||||
const containers = [gridView, listBody];
|
||||
for (const container of containers) {
|
||||
Array.from(container.children).forEach((el, i) => {
|
||||
(el as HTMLElement).dataset.idx = String(i);
|
||||
});
|
||||
}
|
||||
|
||||
// Single-select pill groups (variant, condition): click to activate,
|
||||
// click again to deactivate
|
||||
const pillState: Record<string, string> = {};
|
||||
const pillButtons = document.querySelectorAll<HTMLElement>("[data-cf-group]");
|
||||
pillButtons.forEach((btn) => {
|
||||
const group = btn.dataset.cfGroup!;
|
||||
pillState[group] ??= "";
|
||||
btn.addEventListener("click", () => {
|
||||
const value = btn.dataset.value ?? "";
|
||||
pillState[group] = pillState[group] === value ? "" : value;
|
||||
pillButtons.forEach((b) => {
|
||||
if (b.dataset.cfGroup === group) {
|
||||
b.classList.toggle(
|
||||
"active",
|
||||
b.dataset.value === pillState[group],
|
||||
);
|
||||
}
|
||||
});
|
||||
apply();
|
||||
});
|
||||
});
|
||||
|
||||
const num = (el: HTMLElement, key: string) =>
|
||||
parseFloat(el.dataset[key] ?? "0") || 0;
|
||||
|
||||
function comparator(mode: string) {
|
||||
switch (mode) {
|
||||
case "name":
|
||||
return (a: HTMLElement, b: HTMLElement) =>
|
||||
(a.dataset.name ?? "").localeCompare(b.dataset.name ?? "");
|
||||
case "price-asc":
|
||||
return (a: HTMLElement, b: HTMLElement) =>
|
||||
num(a, "price") - num(b, "price");
|
||||
case "price-desc":
|
||||
return (a: HTMLElement, b: HTMLElement) =>
|
||||
num(b, "price") - num(a, "price");
|
||||
case "total-desc":
|
||||
return (a: HTMLElement, b: HTMLElement) =>
|
||||
num(b, "total") - num(a, "total");
|
||||
case "qty-desc":
|
||||
return (a: HTMLElement, b: HTMLElement) =>
|
||||
num(b, "qty") - num(a, "qty");
|
||||
default:
|
||||
// "Ajout récent" — original server-rendered order
|
||||
return (a: HTMLElement, b: HTMLElement) =>
|
||||
num(a, "idx") - num(b, "idx");
|
||||
}
|
||||
}
|
||||
|
||||
function matches(el: HTMLElement): boolean {
|
||||
const name = nameInput?.value.trim().toLowerCase() ?? "";
|
||||
if (name && !(el.dataset.name ?? "").includes(name)) return false;
|
||||
if (setSelect?.value && el.dataset.set !== setSelect.value) return false;
|
||||
if (pillState.variant && el.dataset.variant !== pillState.variant)
|
||||
return false;
|
||||
if (pillState.condition && el.dataset.condition !== pillState.condition)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function apply() {
|
||||
const cmp = comparator(sortSelect?.value ?? "");
|
||||
let visibleCount = 0;
|
||||
for (const container of containers) {
|
||||
const items = Array.from(container.children) as HTMLElement[];
|
||||
items.sort(cmp).forEach((el) => container.appendChild(el));
|
||||
for (const el of items) {
|
||||
const visible = matches(el);
|
||||
el.style.display = visible ? "" : "none";
|
||||
if (visible && container === gridView) visibleCount++;
|
||||
}
|
||||
}
|
||||
emptyState?.classList.toggle("hidden", visibleCount > 0);
|
||||
}
|
||||
|
||||
nameInput?.addEventListener("input", apply);
|
||||
setSelect?.addEventListener("change", apply);
|
||||
sortSelect?.addEventListener("change", apply);
|
||||
resetBtn?.addEventListener("click", () => {
|
||||
if (nameInput) nameInput.value = "";
|
||||
if (setSelect) setSelect.value = "";
|
||||
if (sortSelect) sortSelect.value = "";
|
||||
for (const key of Object.keys(pillState)) pillState[key] = "";
|
||||
pillButtons.forEach((b) => b.classList.remove("active"));
|
||||
apply();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Shared helpers for the `mycards` PocketBase collection:
|
||||
* record typing and Cardmarket price resolution via the cached TCGdex client.
|
||||
*/
|
||||
|
||||
import { getCard } from "./tcgdex-cache";
|
||||
|
||||
export interface MyCardRecord {
|
||||
id: string;
|
||||
user: string;
|
||||
card_id: string;
|
||||
card_name: string;
|
||||
card_image: string;
|
||||
variant: string;
|
||||
quantity: number;
|
||||
condition: string;
|
||||
notes: string;
|
||||
set_name: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export type PricedCard = MyCardRecord & { price: number; totalPrice: number };
|
||||
|
||||
/** Collection variant label → Cardmarket avg price field. */
|
||||
export const variantPriceMap: Record<string, string> = {
|
||||
Base: "avg",
|
||||
Holo: "avg-holo",
|
||||
Reverse: "avg-reverse",
|
||||
"First Edition": "avg-firstEdition",
|
||||
Promo: "avg-wPromo",
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the Cardmarket unit price and line total for each collection card.
|
||||
* Card details come from the 24h in-memory cache, so repeated cards and
|
||||
* repeated page loads don't hit the TCGdex API again.
|
||||
*/
|
||||
export async function priceCollectionCards(
|
||||
cards: MyCardRecord[],
|
||||
): Promise<PricedCard[]> {
|
||||
return Promise.all(
|
||||
cards.map(async (card) => {
|
||||
let price = 0;
|
||||
const detail = await getCard(card.card_id);
|
||||
const cardmarket = detail?.pricing?.cardmarket;
|
||||
if (cardmarket) {
|
||||
const priceKey = variantPriceMap[card.variant] ?? "avg";
|
||||
price = cardmarket[priceKey] ?? cardmarket.avg ?? 0;
|
||||
}
|
||||
return { ...card, price, totalPrice: price * card.quantity };
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Pokémon TCG type system.
|
||||
* French TCGdex type names → colors, labels, and short codes.
|
||||
*/
|
||||
|
||||
export interface TcgType {
|
||||
/** French TCGdex type name */
|
||||
name: string;
|
||||
/** Primary color (hex) */
|
||||
color: string;
|
||||
/** Lighter tint for backgrounds (hex) */
|
||||
tint: string;
|
||||
/** Short label for cost icons */
|
||||
short: string;
|
||||
}
|
||||
|
||||
const types: Record<string, TcgType> = {
|
||||
Feu: { name: "Feu", color: "#ee8130", tint: "#fdd8bb", short: "F" },
|
||||
Eau: { name: "Eau", color: "#6390f0", tint: "#d3e3fd", short: "E" },
|
||||
Électrique: { name: "Électrique", color: "#f7d02c", tint: "#fdf2c0", short: "É" },
|
||||
Plante: { name: "Plante", color: "#7ac74c", tint: "#dcf0c8", short: "P" },
|
||||
Psy: { name: "Psy", color: "#f95587", tint: "#fdd3e0", short: "Y" },
|
||||
Combat: { name: "Combat", color: "#c22e28", tint: "#f0c6c4", short: "C" },
|
||||
Obscurité: { name: "Obscurité", color: "#705746", tint: "#d9cfc9", short: "O" },
|
||||
Métal: { name: "Métal", color: "#b7b7ce", tint: "#e8e8f2", short: "M" },
|
||||
Incolore: { name: "Incolore", color: "#a8a77a", tint: "#e6e6d3", short: "I" },
|
||||
Fée: { name: "Fée", color: "#d685ad", tint: "#f5d8e8", short: "F" },
|
||||
Dragon: { name: "Dragon", color: "#6f35fc", tint: "#dbc9fd", short: "D" },
|
||||
};
|
||||
|
||||
/** Get the type info for a French TCGdex type name. Falls back to Incolore. */
|
||||
export function getType(name: string): TcgType {
|
||||
return types[name] ?? types["Incolore"];
|
||||
}
|
||||
|
||||
/** Get the CSS gradient for a card's types (supports 1 or 2 types). */
|
||||
export function getTypeGradient(typeNames: string[]): string {
|
||||
if (typeNames.length === 0) return types["Incolore"].color;
|
||||
const colors = typeNames.map((t) => getType(t).color);
|
||||
if (colors.length === 1) return colors[0];
|
||||
return `linear-gradient(135deg, ${colors[0]}, ${colors[1]})`;
|
||||
}
|
||||
|
||||
/** Get the primary (first) type color for a card. */
|
||||
export function getPrimaryColor(typeNames: string[]): string {
|
||||
if (typeNames.length === 0) return types["Incolore"].color;
|
||||
return getType(typeNames[0]).color;
|
||||
}
|
||||
|
||||
/** Get the tint (light) color for a type name. */
|
||||
export function getTint(name: string): string {
|
||||
return getType(name).tint;
|
||||
}
|
||||
|
||||
/** Get the short label for a type name. */
|
||||
export function getShort(name: string): string {
|
||||
return getType(name).short;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Simple in-memory cache for TCGdex data (sets, series).
|
||||
* The module is evaluated once per server process, so values persist
|
||||
* across requests without hitting the TCGdex API every time.
|
||||
* Falls back to stale cache on network failure.
|
||||
*/
|
||||
|
||||
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
fetchedAt: number;
|
||||
}
|
||||
|
||||
export interface TcgSet {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
symbol?: string;
|
||||
cardCount: { total: number; official: number };
|
||||
serie?: { id: string; name: string };
|
||||
}
|
||||
|
||||
export interface TcgSerie {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
let setsCache: CacheEntry<TcgSet[]> | null = null;
|
||||
let seriesCache: CacheEntry<TcgSerie[]> | null = null;
|
||||
const setsBySerieCache = new Map<string, CacheEntry<TcgSet[]>>();
|
||||
const cardCache = new Map<string, CacheEntry<any>>();
|
||||
|
||||
function isFresh<T>(entry: CacheEntry<T> | null | undefined): entry is CacheEntry<T> {
|
||||
return !!entry && Date.now() - entry.fetchedAt < ONE_WEEK_MS;
|
||||
}
|
||||
|
||||
function isFreshDay<T>(entry: CacheEntry<T> | null | undefined): entry is CacheEntry<T> {
|
||||
return !!entry && Date.now() - entry.fetchedAt < ONE_DAY_MS;
|
||||
}
|
||||
|
||||
/** All sets (French), cached for one week. */
|
||||
export async function getSets(): Promise<TcgSet[]> {
|
||||
if (isFresh(setsCache)) return setsCache.data;
|
||||
|
||||
try {
|
||||
const res = await fetch("https://api.tcgdex.net/v2/fr/sets");
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as TcgSet[];
|
||||
setsCache = { data, fetchedAt: Date.now() };
|
||||
return data;
|
||||
}
|
||||
} catch {
|
||||
// fall through to stale cache
|
||||
}
|
||||
if (setsCache) return setsCache.data;
|
||||
return [];
|
||||
}
|
||||
|
||||
/** All series (French), cached for one week. */
|
||||
export async function getSeries(): Promise<TcgSerie[]> {
|
||||
if (isFresh(seriesCache)) return seriesCache.data;
|
||||
|
||||
try {
|
||||
const res = await fetch("https://api.tcgdex.net/v2/fr/series");
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as TcgSerie[];
|
||||
seriesCache = { data, fetchedAt: Date.now() };
|
||||
return data;
|
||||
}
|
||||
} catch {
|
||||
// fall through to stale cache
|
||||
}
|
||||
if (seriesCache) return seriesCache.data;
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Sets belonging to one serie, cached per serie for one week. */
|
||||
export async function getSetsBySerie(serieId: string): Promise<TcgSet[]> {
|
||||
const cached = setsBySerieCache.get(serieId);
|
||||
if (isFresh(cached)) return cached.data;
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.tcgdex.net/v2/fr/sets?serie=${encodeURIComponent(serieId)}`,
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as TcgSet[];
|
||||
setsBySerieCache.set(serieId, { data, fetchedAt: Date.now() });
|
||||
return data;
|
||||
}
|
||||
} catch {
|
||||
// fall through to stale cache
|
||||
}
|
||||
const stale = setsBySerieCache.get(serieId);
|
||||
return stale?.data ?? [];
|
||||
}
|
||||
|
||||
/** Full card detail, cached per card for 24h (pricing changes daily). */
|
||||
export async function getCard(cardId: string): Promise<any | null> {
|
||||
const cached = cardCache.get(cardId);
|
||||
if (isFreshDay(cached)) return cached.data;
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.tcgdex.net/v2/fr/cards/${encodeURIComponent(cardId)}`,
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
cardCache.set(cardId, { data, fetchedAt: Date.now() });
|
||||
return data;
|
||||
}
|
||||
} catch {
|
||||
// fall through to stale cache
|
||||
}
|
||||
const stale = cardCache.get(cardId);
|
||||
return stale?.data ?? null;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Grid/list view toggle shared by the search and collection pages.
|
||||
* Expects #view-grid-btn, #view-list-btn, #grid-view, #list-view in the DOM
|
||||
* and persists the choice in localStorage under the given key.
|
||||
*/
|
||||
export function initViewToggle(storageKey: string) {
|
||||
const gridBtn = document.getElementById("view-grid-btn");
|
||||
const listBtn = document.getElementById("view-list-btn");
|
||||
const gridView = document.getElementById("grid-view");
|
||||
const listView = document.getElementById("list-view");
|
||||
|
||||
function setView(mode: "grid" | "list") {
|
||||
if (mode === "grid") {
|
||||
gridView?.classList.remove("hidden");
|
||||
gridView?.classList.add("grid");
|
||||
listView?.classList.add("hidden");
|
||||
gridBtn?.setAttribute("data-variant", "outline");
|
||||
listBtn?.setAttribute("data-variant", "ghost");
|
||||
localStorage.setItem(storageKey, "grid");
|
||||
} else {
|
||||
gridView?.classList.add("hidden");
|
||||
gridView?.classList.remove("grid");
|
||||
listView?.classList.remove("hidden");
|
||||
listBtn?.setAttribute("data-variant", "outline");
|
||||
gridBtn?.setAttribute("data-variant", "ghost");
|
||||
localStorage.setItem(storageKey, "list");
|
||||
}
|
||||
}
|
||||
|
||||
gridBtn?.addEventListener("click", () => setView("grid"));
|
||||
listBtn?.addEventListener("click", () => setView("list"));
|
||||
|
||||
// Restore saved preference
|
||||
if (localStorage.getItem(storageKey) === "list") {
|
||||
setView("list");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { defineMiddleware } from "astro:middleware";
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
// Read at runtime (not build time) so the same Docker image can point to
|
||||
// different PocketBase instances via the POCKETBASE_URL env variable.
|
||||
const POCKETBASE_URL =
|
||||
process.env.POCKETBASE_URL ?? "https://pokeshare.namarusaja.me";
|
||||
|
||||
export const onRequest = defineMiddleware(async ({ cookies, locals }, next) => {
|
||||
const pb = new PocketBase(POCKETBASE_URL);
|
||||
|
||||
// Restore auth from the cookie if it exists
|
||||
const authCookie = cookies.get("pb_auth");
|
||||
if (authCookie) {
|
||||
pb.authStore.loadFromCookie(authCookie.value);
|
||||
|
||||
// Optionally refresh the token if it's close to expiring
|
||||
try {
|
||||
if (pb.authStore.isValid) {
|
||||
await pb.collection("users").authRefresh();
|
||||
}
|
||||
} catch {
|
||||
// Token is invalid/expired — clear it
|
||||
pb.authStore.clear();
|
||||
cookies.delete("pb_auth", { path: "/" });
|
||||
}
|
||||
}
|
||||
|
||||
// Make the PocketBase instance available to all pages and endpoints via locals
|
||||
locals.pb = pb;
|
||||
locals.user = pb.authStore.record as any;
|
||||
|
||||
return next();
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { card_id, card_name, card_image, variant, quantity, condition, notes, set_name } =
|
||||
await request.json();
|
||||
|
||||
if (!card_id || !card_name) {
|
||||
return new Response(JSON.stringify({ error: "Missing card info" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const record = await locals.pb.collection("mycards").create({
|
||||
user: locals.pb.authStore.record!.id,
|
||||
card_id,
|
||||
card_name,
|
||||
card_image: card_image ?? "",
|
||||
variant: variant ?? "Base",
|
||||
quantity: quantity ?? 1,
|
||||
condition: condition ?? "",
|
||||
notes: notes ?? "",
|
||||
set_name: set_name ?? "",
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true, record }), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to add card to collection:", err);
|
||||
return new Response(JSON.stringify({ error: "Failed to add card" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const DELETE: APIRoute = async ({ params, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
if (!id) {
|
||||
return new Response(JSON.stringify({ error: "Missing record id" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify the record belongs to the current user before deleting
|
||||
const record = await locals.pb.collection("mycards").getOne(id);
|
||||
if (record.user !== locals.pb.authStore.record!.id) {
|
||||
return new Response(JSON.stringify({ error: "Forbidden" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
await locals.pb.collection("mycards").delete(id);
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to delete card:", err);
|
||||
return new Response(JSON.stringify({ error: "Failed to delete card" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { friendshipId } = await request.json();
|
||||
|
||||
if (!friendshipId) {
|
||||
return new Response(JSON.stringify({ error: "Missing friendship id" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const friendship = await locals.pb.collection("friendships").getOne(friendshipId);
|
||||
|
||||
// Only the addressee can accept
|
||||
if (friendship.addressee !== locals.pb.authStore.record!.id) {
|
||||
return new Response(JSON.stringify({ error: "Forbidden" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
await locals.pb.collection("friendships").update(friendshipId, {
|
||||
status: "accepted",
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Accept friend failed:", err);
|
||||
return new Response(JSON.stringify({ error: "Failed to accept" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { friendshipId } = await request.json();
|
||||
|
||||
if (!friendshipId) {
|
||||
return new Response(JSON.stringify({ error: "Missing friendship id" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const friendship = await locals.pb.collection("friendships").getOne(friendshipId);
|
||||
|
||||
// Only the addressee can decline
|
||||
if (friendship.addressee !== locals.pb.authStore.record!.id) {
|
||||
return new Response(JSON.stringify({ error: "Forbidden" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
await locals.pb.collection("friendships").update(friendshipId, {
|
||||
status: "declined",
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Decline friend failed:", err);
|
||||
return new Response(JSON.stringify({ error: "Failed to decline" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { username } = await request.json();
|
||||
|
||||
if (!username || username.length < 3 || username.length > 20) {
|
||||
return new Response(JSON.stringify({ error: "Invalid username" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const requesterId = locals.pb.authStore.record!.id;
|
||||
|
||||
// Find the target user by username
|
||||
const targetUser = await locals.pb.collection("public_users").getFirstListItem(
|
||||
locals.pb.filter("username = {:username}", { username }),
|
||||
);
|
||||
|
||||
if (!targetUser) {
|
||||
return new Response(JSON.stringify({ error: "User not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
if (targetUser.id === requesterId) {
|
||||
return new Response(JSON.stringify({ error: "Cannot add yourself" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Check if friendship already exists (in either direction)
|
||||
const existing = await locals.pb.collection("friendships").getList(1, 1, {
|
||||
filter: locals.pb.filter(
|
||||
"(requester = {:requesterId} && addressee = {:targetId}) || (requester = {:targetId} && addressee = {:requesterId})",
|
||||
{ requesterId, targetId: targetUser.id },
|
||||
),
|
||||
});
|
||||
|
||||
if (existing.items.length > 0) {
|
||||
const f = existing.items[0];
|
||||
if (f.status === "accepted") {
|
||||
return new Response(JSON.stringify({ error: "Already friends" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (f.status === "pending") {
|
||||
return new Response(JSON.stringify({ error: "Invite already sent" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
// Declined — allow re-request
|
||||
await locals.pb.collection("friendships").delete(f.id);
|
||||
}
|
||||
|
||||
const record = await locals.pb.collection("friendships").create({
|
||||
requester: requesterId,
|
||||
addressee: targetUser.id,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true, record }), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("Friend request failed:", err);
|
||||
if (err.message?.includes("not found")) {
|
||||
return new Response(JSON.stringify({ error: "User not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ error: "Failed to send invite" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const GET: APIRoute = async ({ url, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const query = url.searchParams.get("q")?.trim().replace(/^@/, "");
|
||||
|
||||
if (!query || query.length < 3 || query.length > 20) {
|
||||
return new Response(JSON.stringify({ error: "Query must be 3-20 characters" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Search by username (case-insensitive), exclude self
|
||||
const users = await locals.pb.collection("public_users").getList(1, 10, {
|
||||
filter: locals.pb.filter("username ~ {:query} && id != {:selfId}", {
|
||||
query,
|
||||
selfId: locals.pb.authStore.record!.id,
|
||||
}),
|
||||
fields: "id,username,name,visibility",
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ users: users.items }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Friend search failed:", err);
|
||||
return new Response(JSON.stringify({ error: "Search failed" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies, redirect, locals }) => {
|
||||
let username = "";
|
||||
let password = "";
|
||||
|
||||
const contentType = request.headers.get("content-type") ?? "";
|
||||
|
||||
try {
|
||||
if (contentType.includes("application/json")) {
|
||||
const body = await request.json();
|
||||
username = body.username ?? "";
|
||||
password = body.password ?? "";
|
||||
} else {
|
||||
const formData = await request.formData();
|
||||
username = formData.get("username")?.toString() ?? "";
|
||||
password = formData.get("password")?.toString() ?? "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to parse login request body:", err);
|
||||
return new Response(JSON.stringify({ error: "Invalid request body" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
if (!username || !password) {
|
||||
return new Response(JSON.stringify({ error: "Username and password are required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb.collection("users").authWithPassword(username, password);
|
||||
|
||||
cookies.set("pb_auth", locals.pb.authStore.exportToCookie(), {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async ({ cookies, redirect }) => {
|
||||
cookies.delete("pb_auth", { path: "/" });
|
||||
return redirect("/login");
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies, locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, username, theme_preference, visibility } = body;
|
||||
|
||||
const updates: Record<string, string> = {};
|
||||
|
||||
if (name !== undefined) {
|
||||
updates.name = String(name).trim();
|
||||
}
|
||||
|
||||
if (username !== undefined) {
|
||||
const u = String(username).trim().toLowerCase();
|
||||
if (u.length < 3 || u.length > 20) {
|
||||
return new Response(JSON.stringify({ error: "Username must be 3-20 characters" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (!/^[a-z0-9_]+$/.test(u)) {
|
||||
return new Response(JSON.stringify({ error: "Username can only contain lowercase letters, numbers, and underscores" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
updates.username = u;
|
||||
}
|
||||
|
||||
if (visibility !== undefined) {
|
||||
if (!["Public", "Friends", "Private"].includes(visibility)) {
|
||||
return new Response(JSON.stringify({ error: "Invalid visibility" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
updates.visibility = visibility;
|
||||
}
|
||||
|
||||
if (theme_preference !== undefined) {
|
||||
if (!["System", "Light", "Dark"].includes(theme_preference)) {
|
||||
return new Response(JSON.stringify({ error: "Invalid theme preference" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
updates.theme_preference = theme_preference;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return new Response(JSON.stringify({ error: "Nothing to update" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Update the user record
|
||||
await locals.pb.collection("users").update(locals.pb.authStore.record!.id, updates);
|
||||
|
||||
// Re-set the auth cookie with the updated record
|
||||
cookies.set("pb_auth", locals.pb.authStore.exportToCookie(), {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("Failed to update profile:", err);
|
||||
if (err.message?.includes("username")) {
|
||||
return new Response(JSON.stringify({ error: "Username already taken" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ error: "Failed to update profile" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,805 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import {
|
||||
getTypeGradient,
|
||||
getPrimaryColor,
|
||||
getTint,
|
||||
getShort,
|
||||
} from "../../lib/tcg-types";
|
||||
import { getCard } from "../../lib/tcgdex-cache";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const { id } = Astro.params;
|
||||
|
||||
if (!id) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
const card = await getCard(id);
|
||||
if (!card) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
// Map variant keys to their Cardmarket avg price field
|
||||
const variantPriceMap: Record<string, string> = {
|
||||
normal: "avg",
|
||||
holo: "avg-holo",
|
||||
reverse: "avg-reverse",
|
||||
firstEdition: "avg-firstEdition",
|
||||
wPromo: "avg-wPromo",
|
||||
};
|
||||
|
||||
const variantLabels: Record<string, string> = {
|
||||
normal: "Normal",
|
||||
holo: "Holo",
|
||||
reverse: "Reverse",
|
||||
firstEdition: "First Edition",
|
||||
wPromo: "Promo",
|
||||
};
|
||||
|
||||
const cardmarket = card.pricing?.cardmarket;
|
||||
const availableVariants = card.variants
|
||||
? Object.entries(card.variants)
|
||||
.filter(([_, available]) => available)
|
||||
.map(([key]) => key)
|
||||
: [];
|
||||
|
||||
const variantPrices: { label: string; price: number }[] = [];
|
||||
|
||||
// Always include the base avg price (no variant suffix)
|
||||
if (cardmarket?.avg != null) {
|
||||
variantPrices.push({ label: "Base", price: cardmarket.avg });
|
||||
}
|
||||
|
||||
// Add prices for each available variant
|
||||
for (const variant of availableVariants) {
|
||||
if (variant === "normal") continue; // normal uses the base avg, already added
|
||||
const priceKey = variantPriceMap[variant];
|
||||
const price = cardmarket?.[priceKey];
|
||||
if (price != null) {
|
||||
variantPrices.push({ label: variantLabels[variant] ?? variant, price });
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which tabs have content
|
||||
const hasAttacks = card.attacks && card.attacks.length > 0;
|
||||
const hasAbilities = card.abilities && card.abilities.length > 0;
|
||||
const hasPricing = variantPrices.length > 0;
|
||||
|
||||
// Type theming
|
||||
const typeNames: string[] = card.types ?? [];
|
||||
const typeGradient = getTypeGradient(typeNames);
|
||||
const primaryColor = getPrimaryColor(typeNames);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<a
|
||||
href="javascript:history.back()"
|
||||
class="inline-block px-8 pt-6 mb-0 no-underline opacity-70 hover:opacity-100 text-sm"
|
||||
>
|
||||
← Retour
|
||||
</a>
|
||||
|
||||
<div class="card-detail">
|
||||
<!-- Left column: image + pricing -->
|
||||
<div class="card-left">
|
||||
<div class="card-image-section">
|
||||
<div
|
||||
class="card-image-frame"
|
||||
style={`background: ${typeGradient}`}
|
||||
>
|
||||
{
|
||||
card.image ? (
|
||||
<img
|
||||
src={`${card.image}/low.webp`}
|
||||
alt={card.name}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/assets/placeholder_card.png"
|
||||
alt={card.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
hasPricing && (
|
||||
<div class="card">
|
||||
<header>
|
||||
<h2>Prix Cardmarket</h2>
|
||||
</header>
|
||||
<section>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{variantPrices.map(({ label, price }) => (
|
||||
<div class="flex justify-between items-center py-1 px-2 rounded bg-muted">
|
||||
<span class="text-sm opacity-70">
|
||||
{label}
|
||||
</span>
|
||||
<span class="text-base font-bold">
|
||||
{price.toFixed(2)} €
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{cardmarket?.updated && (
|
||||
<small class="block mt-3 opacity-50 text-right text-xs">
|
||||
Mis à jour le{" "}
|
||||
{new Date(
|
||||
cardmarket.updated,
|
||||
).toLocaleDateString("fr-FR")}
|
||||
</small>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Right column: tabs with details -->
|
||||
<div class="card-right">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h1>{card.name}</h1>
|
||||
<span class="text-xs opacity-50">{card.id}</span>
|
||||
</div>
|
||||
<button id="add-to-collection-btn" class="btn" type="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="M12 5v14"></path>
|
||||
</svg>
|
||||
Ajouter à ma collection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs" id="card-tabs">
|
||||
<nav role="tablist" aria-orientation="horizontal">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="tab-details"
|
||||
aria-controls="panel-details"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
>
|
||||
Détails
|
||||
</button>
|
||||
{
|
||||
hasAttacks && (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="tab-attacks"
|
||||
aria-controls="panel-attacks"
|
||||
aria-selected="false"
|
||||
tabindex="-1"
|
||||
>
|
||||
Attaques
|
||||
</button>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasAbilities && (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="tab-abilities"
|
||||
aria-controls="panel-abilities"
|
||||
aria-selected="false"
|
||||
tabindex="-1"
|
||||
>
|
||||
Talents
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</nav>
|
||||
|
||||
<!-- Details Panel -->
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="panel-details"
|
||||
aria-labelledby="tab-details"
|
||||
tabindex="-1"
|
||||
aria-selected="true"
|
||||
>
|
||||
<div class="card">
|
||||
<section>
|
||||
<div class="card-meta">
|
||||
<div class="meta-item">
|
||||
<span
|
||||
class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Set</span
|
||||
>
|
||||
<span class="font-semibold"
|
||||
>{card.set.name}</span
|
||||
>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span
|
||||
class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Rareté</span
|
||||
>
|
||||
<span class="font-semibold"
|
||||
>{card.rarity}</span
|
||||
>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span
|
||||
class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Catégorie</span
|
||||
>
|
||||
<span class="font-semibold"
|
||||
>{card.category}</span
|
||||
>
|
||||
</div>
|
||||
{
|
||||
card.illustrator && (
|
||||
<div class="meta-item">
|
||||
<span class="text-xs uppercase tracking-wide opacity-70">
|
||||
Illustrateur
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{card.illustrator}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
card.hp && (
|
||||
<div class="meta-item">
|
||||
<span class="text-xs uppercase tracking-wide opacity-70">
|
||||
HP
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{card.hp}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
card.types && card.types.length > 0 && (
|
||||
<div class="meta-item">
|
||||
<span class="text-xs uppercase tracking-wide opacity-70">
|
||||
Types
|
||||
</span>
|
||||
<span class="font-semibold flex gap-1.5">
|
||||
{card.types.map((t: string) => (
|
||||
<span
|
||||
class="type-badge"
|
||||
style={`background: ${getTint(t)}; color: ${getShort(t) === "F" || getShort(t) === "É" || getShort(t) === "P" || getShort(t) === "I" || getShort(t) === "M" ? "#333" : "#fff"}`}
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
card.stage && (
|
||||
<div class="meta-item">
|
||||
<span class="text-xs uppercase tracking-wide opacity-70">
|
||||
Stage
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{card.stage}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
card.evolveFrom && (
|
||||
<div class="meta-item">
|
||||
<span class="text-xs uppercase tracking-wide opacity-70">
|
||||
Évolue de
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{card.evolveFrom}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
card.retreat !== undefined && (
|
||||
<div class="meta-item">
|
||||
<span class="text-xs uppercase tracking-wide opacity-70">
|
||||
Retraite
|
||||
</span>
|
||||
<span class="font-semibold">
|
||||
{card.retreat}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
card.description && (
|
||||
<div
|
||||
class="mt-4 p-3 rounded-lg bg-muted italic leading-relaxed"
|
||||
>
|
||||
{card.description}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(card.weaknesses?.length > 0 ||
|
||||
card.resistances?.length > 0) && (
|
||||
<div class="flex flex-wrap items-center gap-2 mt-4">
|
||||
{card.weaknesses?.map((w: any) => (
|
||||
<span
|
||||
class="badge"
|
||||
data-variant="destructive"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full inline-block mr-1.5"
|
||||
style={`background: ${getTint(w.type)}`}
|
||||
/>
|
||||
Faiblesse {w.type}
|
||||
{w.value ? ` ${w.value}` : ""}
|
||||
</span>
|
||||
))}
|
||||
{card.resistances?.map((r: any) => (
|
||||
<span
|
||||
class="badge"
|
||||
data-variant="outline"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full inline-block mr-1.5"
|
||||
style={`background: ${getTint(r.type)}`}
|
||||
/>
|
||||
Résistance {r.type}
|
||||
{r.value ? ` ${r.value}` : ""}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attacks Panel -->
|
||||
{
|
||||
hasAttacks && (
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="panel-attacks"
|
||||
aria-labelledby="tab-attacks"
|
||||
tabindex="-1"
|
||||
aria-selected="false"
|
||||
hidden
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
{card.attacks.map((attack: any) => (
|
||||
<article
|
||||
class="item"
|
||||
data-variant="outline"
|
||||
>
|
||||
<section>
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>{attack.name}</h3>
|
||||
{attack.damage && (
|
||||
<span
|
||||
class="badge"
|
||||
data-variant="secondary"
|
||||
>
|
||||
{attack.damage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{attack.cost &&
|
||||
attack.cost.length > 0 && (
|
||||
<p class="text-xs opacity-60 flex items-center gap-1">
|
||||
{attack.cost.map(
|
||||
(c: string) => (
|
||||
<span
|
||||
class="cost-icon"
|
||||
style={`background: ${getTint(c)}; color: ${getShort(c) === "F" || getShort(c) === "É" || getShort(c) === "P" || getShort(c) === "I" || getShort(c) === "M" ? "#333" : "#fff"}`}
|
||||
title={c}
|
||||
>
|
||||
{getShort(
|
||||
c,
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{attack.effect && (
|
||||
<p>{attack.effect}</p>
|
||||
)}
|
||||
</section>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Abilities Panel -->
|
||||
{
|
||||
hasAbilities && (
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="panel-abilities"
|
||||
aria-labelledby="tab-abilities"
|
||||
tabindex="-1"
|
||||
aria-selected="false"
|
||||
hidden
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
{card.abilities.map((ability: any) => (
|
||||
<article
|
||||
class="item"
|
||||
data-variant="outline"
|
||||
>
|
||||
<section>
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>{ability.name}</h3>
|
||||
<span
|
||||
class="badge"
|
||||
data-variant="secondary"
|
||||
>
|
||||
{ability.type}
|
||||
</span>
|
||||
</div>
|
||||
{ability.effect && (
|
||||
<p>{ability.effect}</p>
|
||||
)}
|
||||
</section>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add to collection dialog -->
|
||||
<dialog
|
||||
id="add-card-dialog"
|
||||
class="dialog"
|
||||
aria-labelledby="add-card-dialog-title"
|
||||
onclick="if (event.target === this) this.close()"
|
||||
>
|
||||
<div class="sm:max-w-md">
|
||||
<header>
|
||||
<h2 id="add-card-dialog-title">Ajouter à ma collection</h2>
|
||||
<p>
|
||||
Enregistrez cette carte dans votre collection avec ses
|
||||
détails.
|
||||
</p>
|
||||
</header>
|
||||
<section>
|
||||
<form id="add-card-form" class="grid gap-4">
|
||||
<input type="hidden" name="card_id" value={card.id} />
|
||||
<input type="hidden" name="card_name" value={card.name} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="card_image"
|
||||
value={card.image ?? ""}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="set_name"
|
||||
value={card.set.name}
|
||||
/>
|
||||
|
||||
<div role="group" class="field">
|
||||
<label for="variant">Variant</label>
|
||||
<select
|
||||
name="variant"
|
||||
id="variant"
|
||||
class="select w-full"
|
||||
required
|
||||
>
|
||||
<option value="Base">Base</option>
|
||||
{
|
||||
variantPrices
|
||||
.filter((v) => v.label !== "Base")
|
||||
.map(({ label }) => (
|
||||
<option value={label}>{label}</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div role="group" class="field">
|
||||
<label for="quantity">Quantité</label>
|
||||
<input
|
||||
type="number"
|
||||
name="quantity"
|
||||
id="quantity"
|
||||
class="input"
|
||||
value="1"
|
||||
min="1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div role="group" class="field">
|
||||
<label for="condition">État</label>
|
||||
<select
|
||||
name="condition"
|
||||
id="condition"
|
||||
class="select w-full"
|
||||
>
|
||||
<option value="">—</option>
|
||||
<option value="Neuf">Neuf</option>
|
||||
<option value="Bon état" selected>Bon état</option>
|
||||
<option value="Joué">Joué</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div role="group" class="field">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
id="notes"
|
||||
class="textarea"
|
||||
rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
data-variant="outline"
|
||||
onclick="this.closest('dialog').close()"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn"
|
||||
form="add-card-form"
|
||||
id="add-card-submit"
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
</footer>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
data-variant="ghost"
|
||||
data-size="icon-sm"
|
||||
aria-label="Close dialog"
|
||||
onclick="this.closest('dialog').close()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18"></path>
|
||||
<path d="m6 6 12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const btn = document.getElementById("add-to-collection-btn");
|
||||
const dialog = document.getElementById(
|
||||
"add-card-dialog",
|
||||
) as HTMLDialogElement;
|
||||
const form = document.getElementById("add-card-form") as HTMLFormElement;
|
||||
const submitBtn = document.getElementById(
|
||||
"add-card-submit",
|
||||
) as HTMLButtonElement;
|
||||
const toaster = document.getElementById("toaster") as HTMLDivElement & {
|
||||
toast: (config: {
|
||||
category?: "success" | "error" | "info" | "warning";
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
const showToast = (
|
||||
category: "success" | "error" | "info" | "warning",
|
||||
title: string,
|
||||
description: string,
|
||||
) => {
|
||||
if (toaster?.toast) {
|
||||
toaster.toast({ category, title, description });
|
||||
}
|
||||
};
|
||||
|
||||
btn?.addEventListener("click", () => {
|
||||
(dialog as any).showModal();
|
||||
});
|
||||
|
||||
form?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const body = {
|
||||
card_id: formData.get("card_id"),
|
||||
card_name: formData.get("card_name"),
|
||||
card_image: formData.get("card_image"),
|
||||
set_name: formData.get("set_name"),
|
||||
variant: formData.get("variant"),
|
||||
quantity: Number(formData.get("quantity")),
|
||||
condition: formData.get("condition"),
|
||||
notes: formData.get("notes"),
|
||||
};
|
||||
|
||||
submitBtn.disabled = true;
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = `<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg> Ajout...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/collection", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showToast(
|
||||
"success",
|
||||
"Succès",
|
||||
"Carte ajoutée à votre collection !",
|
||||
);
|
||||
form.reset();
|
||||
(dialog as any).close();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast(
|
||||
"error",
|
||||
"Erreur",
|
||||
data.error ?? "Erreur lors de l'ajout",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-detail {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 3rem;
|
||||
padding: 0 2rem 3rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
align-self: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-image-section {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-image-frame {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
padding: 4px;
|
||||
border-radius: 1.1rem;
|
||||
}
|
||||
|
||||
.card-image-frame img {
|
||||
width: 100%;
|
||||
border-radius: 0.85rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cost-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-header h1 {
|
||||
margin-bottom: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
/* Tabs content spacing */
|
||||
.tabs [role="tabpanel"] {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.card-detail {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
padding: 0 1rem 2rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
.card-left {
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
}
|
||||
.card-image-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-image-frame {
|
||||
max-width: 240px;
|
||||
}
|
||||
.card-right {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,387 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const userId = pb.authStore.record!.id;
|
||||
|
||||
// Get all friendships involving this user
|
||||
const friendships = await pb.collection("friendships").getFullList({
|
||||
filter: pb.filter("requester = {:userId} || addressee = {:userId}", {
|
||||
userId,
|
||||
}),
|
||||
expand: "requester,addressee",
|
||||
});
|
||||
|
||||
const pendingReceived = friendships.filter(
|
||||
(f) => f.addressee === userId && f.status === "pending",
|
||||
);
|
||||
const pendingSent = friendships.filter(
|
||||
(f) => f.requester === userId && f.status === "pending",
|
||||
);
|
||||
const accepted = friendships.filter((f) => f.status === "accepted");
|
||||
|
||||
function getOtherUser(friendship: any) {
|
||||
// expand contains the full user records; requester/addressee are just IDs
|
||||
if (friendship.requester === userId) {
|
||||
return friendship.expand?.addressee;
|
||||
}
|
||||
return friendship.expand?.requester;
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-2xl mx-auto px-4 md:px-8 py-8">
|
||||
<h1 class="text-xl font-bold mb-1">Amis</h1>
|
||||
<p class="text-sm opacity-70 mb-8">
|
||||
Gérez vos amis et leurs collections.
|
||||
</p>
|
||||
|
||||
<!-- Search for new friends -->
|
||||
<div class="card mb-6">
|
||||
<header>
|
||||
<h2>Ajouter un ami</h2>
|
||||
</header>
|
||||
<section>
|
||||
<form id="search-form" class="flex gap-2">
|
||||
<div class="input-group flex-1">
|
||||
<input
|
||||
type="search"
|
||||
name="q"
|
||||
placeholder="Rechercher par @username..."
|
||||
minlength="3"
|
||||
maxlength="20"
|
||||
required
|
||||
/>
|
||||
<span data-align="start" aria-hidden="true">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34"></path>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<button type="submit" class="btn">Rechercher</button>
|
||||
</form>
|
||||
<div id="search-results" class="mt-4 flex flex-col gap-2"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Pending invites received -->
|
||||
{
|
||||
pendingReceived.length > 0 && (
|
||||
<div class="card mb-6">
|
||||
<header>
|
||||
<h2>Invitations reçues</h2>
|
||||
</header>
|
||||
<section class="flex flex-col gap-3">
|
||||
{pendingReceived.map((f) => {
|
||||
const other = getOtherUser(f);
|
||||
return (
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-muted">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="avatar" data-size="sm">
|
||||
<span>
|
||||
{(
|
||||
other?.name ||
|
||||
other?.username ||
|
||||
other?.email ||
|
||||
"??"
|
||||
)
|
||||
.slice(0, 2)
|
||||
.toUpperCase()}
|
||||
</span>
|
||||
</span>
|
||||
<div>
|
||||
<div class="font-semibold text-sm">
|
||||
{other?.name || "—"}
|
||||
</div>
|
||||
<div class="text-xs opacity-60">
|
||||
@{other?.username || "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn"
|
||||
data-size="sm"
|
||||
onclick={`acceptFriend('${f.id}')`}
|
||||
>
|
||||
Accepter
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
data-variant="outline"
|
||||
data-size="sm"
|
||||
onclick={`declineFriend('${f.id}')`}
|
||||
>
|
||||
Refuser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Pending invites sent -->
|
||||
{
|
||||
pendingSent.length > 0 && (
|
||||
<div class="card mb-6">
|
||||
<header>
|
||||
<h2>Invitations envoyées</h2>
|
||||
</header>
|
||||
<section class="flex flex-col gap-3">
|
||||
{pendingSent.map((f) => {
|
||||
const other = getOtherUser(f);
|
||||
return (
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-muted">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="avatar" data-size="sm">
|
||||
<span>
|
||||
{(
|
||||
other?.name ||
|
||||
other?.username ||
|
||||
other?.email ||
|
||||
"??"
|
||||
)
|
||||
.slice(0, 2)
|
||||
.toUpperCase()}
|
||||
</span>
|
||||
</span>
|
||||
<div>
|
||||
<div class="font-semibold text-sm">
|
||||
{other?.name || "—"}
|
||||
</div>
|
||||
<div class="text-xs opacity-60">
|
||||
@{other?.username || "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="badge"
|
||||
data-variant="secondary"
|
||||
>
|
||||
En attente
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Friends list -->
|
||||
<div class="card">
|
||||
<header>
|
||||
<h2>Mes amis</h2>
|
||||
</header>
|
||||
<section>
|
||||
{
|
||||
accepted.length === 0 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<h3>Aucun ami</h3>
|
||||
<p>
|
||||
Recherchez des utilisateurs par leur
|
||||
@username pour les ajouter.
|
||||
</p>
|
||||
</header>
|
||||
</section>
|
||||
) : (
|
||||
<div class="flex flex-col gap-3">
|
||||
{accepted.map((f) => {
|
||||
const other = getOtherUser(f);
|
||||
return (
|
||||
<a
|
||||
href={`/friends/${other?.username || other?.id}`}
|
||||
class="item no-underline"
|
||||
data-variant="outline"
|
||||
>
|
||||
<figure>
|
||||
<span class="avatar">
|
||||
<span>
|
||||
{(
|
||||
other?.name ||
|
||||
other?.username ||
|
||||
other?.email ||
|
||||
"??"
|
||||
)
|
||||
.slice(0, 2)
|
||||
.toUpperCase()}
|
||||
</span>
|
||||
</span>
|
||||
</figure>
|
||||
<section>
|
||||
<h3>{other?.name || "—"}</h3>
|
||||
<p class="text-sm opacity-60">
|
||||
@{other?.username || "—"}
|
||||
</p>
|
||||
</section>
|
||||
<aside>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
</aside>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const toaster = document.getElementById("toaster") as ToasterElement;
|
||||
const showToast = (
|
||||
category: "success" | "error",
|
||||
title: string,
|
||||
description: string,
|
||||
) => {
|
||||
if (toaster?.toast) {
|
||||
toaster.toast({ category, title, description });
|
||||
}
|
||||
};
|
||||
|
||||
// Search
|
||||
const searchForm = document.getElementById("search-form");
|
||||
const searchResults = document.getElementById("search-results");
|
||||
|
||||
searchForm?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(searchForm as HTMLFormElement);
|
||||
const q = formData.get("q");
|
||||
|
||||
const res = await fetch(
|
||||
`/api/friends/search?q=${encodeURIComponent(q as string)}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
showToast("error", "Erreur", data.error ?? "Recherche échouée");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.users.length === 0) {
|
||||
searchResults!.innerHTML = `<p class="text-sm opacity-60">Aucun utilisateur trouvé.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
searchResults!.innerHTML = data.users
|
||||
.map(
|
||||
(u: any) => `
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-muted">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="avatar" data-size="sm">
|
||||
<span>${(u.name || u.username || u.email || "??").slice(0, 2).toUpperCase()}</span>
|
||||
</span>
|
||||
<div>
|
||||
<div class="font-semibold text-sm">${u.name || "—"}</div>
|
||||
<div class="text-xs opacity-60">@${u.username || "—"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" data-size="sm" onclick="sendInvite('${u.username}')">Ajouter</button>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
});
|
||||
|
||||
// Send invite
|
||||
(window as any).sendInvite = async (username: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/friends/request", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast(
|
||||
"success",
|
||||
"Invitation envoyée",
|
||||
`Invitation envoyée à @${username}.`,
|
||||
);
|
||||
searchResults!.innerHTML = "";
|
||||
(searchForm as HTMLFormElement).reset();
|
||||
} else {
|
||||
showToast("error", "Erreur", data.error ?? "Échec de l'envoi");
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
}
|
||||
};
|
||||
|
||||
// Accept
|
||||
(window as any).acceptFriend = async (friendshipId: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/friends/accept", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ friendshipId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
showToast(
|
||||
"success",
|
||||
"Ami ajouté",
|
||||
"Vous êtes maintenant amis.",
|
||||
);
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast("error", "Erreur", data.error ?? "Échec");
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
}
|
||||
};
|
||||
|
||||
// Decline
|
||||
(window as any).declineFriend = async (friendshipId: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/friends/decline", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ friendshipId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
showToast("success", "Refusé", "Invitation refusée.");
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast("error", "Erreur", data.error ?? "Échec");
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import {
|
||||
priceCollectionCards,
|
||||
type MyCardRecord,
|
||||
} from "../../lib/collection";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const { username } = Astro.params;
|
||||
|
||||
if (!username) {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
|
||||
// Find the target user by username (same public view the friends API uses)
|
||||
let targetUser;
|
||||
try {
|
||||
targetUser = await pb
|
||||
.collection("public_users")
|
||||
.getFirstListItem(pb.filter("username = {:username}", { username }));
|
||||
} catch {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
|
||||
// Check visibility — Private users' collections are never visible
|
||||
if (targetUser.visibility === "Private") {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
|
||||
// Friends-only collections require an accepted friendship
|
||||
if (targetUser.visibility !== "Public") {
|
||||
const userId = pb.authStore.record!.id;
|
||||
const friendship = await pb.collection("friendships").getList(1, 1, {
|
||||
filter: pb.filter(
|
||||
"((requester = {:userId} && addressee = {:targetId}) || (requester = {:targetId} && addressee = {:userId})) && status = 'accepted'",
|
||||
{ userId, targetId: targetUser.id },
|
||||
),
|
||||
});
|
||||
|
||||
if (friendship.items.length === 0) {
|
||||
return Astro.redirect("/friends");
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch their collection
|
||||
const myCards = (await pb.collection("mycards").getFullList({
|
||||
filter: pb.filter("user = {:userId}", { userId: targetUser.id }),
|
||||
sort: "-created",
|
||||
})) as unknown as MyCardRecord[];
|
||||
|
||||
const cardsWithPrice = await priceCollectionCards(myCards);
|
||||
|
||||
const totalValue = cardsWithPrice.reduce((s, c) => s + c.totalPrice, 0);
|
||||
const totalCards = cardsWithPrice.reduce((s, c) => s + c.quantity, 0);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-[1600px] mx-auto px-4 md:px-8 pb-12 mt-8">
|
||||
<a
|
||||
href="/friends"
|
||||
class="inline-block no-underline opacity-70 hover:opacity-100 text-sm mb-4"
|
||||
>← Amis</a
|
||||
>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="avatar" data-size="lg">
|
||||
<span
|
||||
>{
|
||||
(
|
||||
targetUser.name ||
|
||||
targetUser.username ||
|
||||
targetUser.email ||
|
||||
"??"
|
||||
)
|
||||
.slice(0, 2)
|
||||
.toUpperCase()
|
||||
}</span
|
||||
>
|
||||
</span>
|
||||
<div>
|
||||
<h1 class="m-0">{targetUser.name || "—"}</h1>
|
||||
<p class="text-sm opacity-60 m-0">
|
||||
@{targetUser.username || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-6">
|
||||
<div
|
||||
class="bg-muted rounded-lg px-6 py-3 flex flex-col items-center"
|
||||
>
|
||||
<span class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Cartes</span
|
||||
>
|
||||
<span class="text-2xl font-bold">{totalCards}</span>
|
||||
</div>
|
||||
<div
|
||||
class="bg-muted rounded-lg px-6 py-3 flex flex-col items-center"
|
||||
>
|
||||
<span class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Valeur totale</span
|
||||
>
|
||||
<span class="text-2xl font-bold"
|
||||
>{totalValue.toFixed(2)} €</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
cardsWithPrice.length === 0 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<h3>Collection vide</h3>
|
||||
<p>
|
||||
{targetUser.name || targetUser.username} n'a pas
|
||||
encore de cartes dans sa collection.
|
||||
</p>
|
||||
</header>
|
||||
</section>
|
||||
) : (
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{cardsWithPrice.map((card) => (
|
||||
<a
|
||||
href={`/card/${card.card_id}`}
|
||||
class="card no-underline hover:-translate-y-0.5 hover:shadow-lg transition-all duration-150 overflow-hidden"
|
||||
>
|
||||
<div class="aspect-2.5/3.5 overflow-hidden">
|
||||
{card.card_image ? (
|
||||
<img
|
||||
src={`${card.card_image}/low.webp`}
|
||||
alt={card.card_name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/assets/placeholder_card.png"
|
||||
alt={card.card_name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div class="p-3 flex flex-col gap-1.5">
|
||||
<h3 class="m-0 text-sm font-semibold leading-tight truncate">
|
||||
{card.card_name}
|
||||
</h3>
|
||||
{card.set_name && (
|
||||
<small class="opacity-60 text-xs truncate">
|
||||
{card.set_name}
|
||||
</small>
|
||||
)}
|
||||
<div class="flex items-center justify-between">
|
||||
<span
|
||||
class="badge"
|
||||
data-variant="secondary"
|
||||
>
|
||||
{card.variant}
|
||||
</span>
|
||||
<span class="font-semibold text-sm">
|
||||
×{card.quantity}
|
||||
</span>
|
||||
</div>
|
||||
{card.condition && (
|
||||
<span
|
||||
class="badge"
|
||||
data-variant={
|
||||
card.condition === "Neuf"
|
||||
? "primary"
|
||||
: card.condition === "Joué"
|
||||
? "destructive"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{card.condition}
|
||||
</span>
|
||||
)}
|
||||
<div class="flex items-center justify-between mt-2 pt-2 border-t border-border">
|
||||
<span class="font-bold text-base">
|
||||
{card.price.toFixed(2)} €
|
||||
</span>
|
||||
{card.quantity > 1 && (
|
||||
<span class="text-xs opacity-70">
|
||||
= {card.totalPrice.toFixed(2)} €
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style is:global>
|
||||
a.card {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import { getSeries } from "../lib/tcgdex-cache";
|
||||
import {
|
||||
priceCollectionCards,
|
||||
type MyCardRecord,
|
||||
} from "../lib/collection";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const userId = pb.authStore.record!.id;
|
||||
|
||||
const myCards = (await pb.collection("mycards").getFullList({
|
||||
filter: pb.filter("user = {:userId}", { userId }),
|
||||
})) as unknown as MyCardRecord[];
|
||||
|
||||
// Fetch prices through the 24h-cached card endpoint
|
||||
const cardsWithPrice = await priceCollectionCards(myCards);
|
||||
|
||||
// Insights
|
||||
const totalCards = cardsWithPrice.reduce((s, c) => s + c.quantity, 0);
|
||||
const totalValue = cardsWithPrice.reduce((s, c) => s + c.totalPrice, 0);
|
||||
|
||||
const bySet = new Map<string, { count: number; value: number }>();
|
||||
for (const c of cardsWithPrice) {
|
||||
const key = c.set_name || "Inconnu";
|
||||
const entry = bySet.get(key) ?? { count: 0, value: 0 };
|
||||
entry.count += c.quantity;
|
||||
entry.value += c.totalPrice;
|
||||
bySet.set(key, entry);
|
||||
}
|
||||
const topSets = [...bySet.entries()]
|
||||
.sort((a, b) => b[1].value - a[1].value)
|
||||
.slice(0, 5);
|
||||
|
||||
const topCards = [...cardsWithPrice]
|
||||
.sort((a, b) => b.totalPrice - a.totalPrice)
|
||||
.slice(0, 5);
|
||||
|
||||
// Series for the explorer
|
||||
const series = await getSeries();
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-350 mx-auto px-4 md:px-8 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Collection insights -->
|
||||
<section>
|
||||
<h2 class="text-xl font-bold mb-1">Ma collection</h2>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Aperçu de vos cartes et de leur valeur.
|
||||
</p>
|
||||
|
||||
{
|
||||
cardsWithPrice.length === 0 ? (
|
||||
<section class="empty border border-dashed">
|
||||
<header>
|
||||
<h3>Collection vide</h3>
|
||||
<p>
|
||||
Ajoutez des cartes à votre collection pour
|
||||
voir vos statistiques ici.
|
||||
</p>
|
||||
</header>
|
||||
<footer>
|
||||
<a href="/search" class="btn" data-size="sm">
|
||||
Rechercher des cartes
|
||||
</a>
|
||||
</footer>
|
||||
</section>
|
||||
) : (
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="card">
|
||||
<section class="text-center py-2">
|
||||
<p class="text-xs uppercase tracking-wide opacity-70">
|
||||
Cartes
|
||||
</p>
|
||||
<p class="text-3xl font-bold">
|
||||
{totalCards}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card">
|
||||
<section class="text-center py-2">
|
||||
<p class="text-xs uppercase tracking-wide opacity-70">
|
||||
Valeur totale
|
||||
</p>
|
||||
<p class="text-3xl font-bold">
|
||||
{totalValue.toFixed(2)} €
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<header>
|
||||
<h3>Top sets</h3>
|
||||
</header>
|
||||
<section class="flex flex-col gap-1.5">
|
||||
{topSets.map(([name, stats]) => (
|
||||
<div class="flex justify-between items-center py-1 px-2 rounded bg-muted text-sm">
|
||||
<span class="truncate">{name}</span>
|
||||
<span class="font-semibold whitespace-nowrap ml-2">
|
||||
{stats.count} × —{" "}
|
||||
{stats.value.toFixed(2)} €
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<header>
|
||||
<h3>Cartes les plus chères</h3>
|
||||
</header>
|
||||
<section class="flex flex-col gap-1.5">
|
||||
{topCards.map((card) => (
|
||||
<a
|
||||
href={`/card/${card.card_id}`}
|
||||
class="flex items-center gap-3 py-1 px-2 rounded bg-muted no-underline"
|
||||
>
|
||||
{card.card_image && (
|
||||
<img
|
||||
src={`${card.card_image}/low.webp`}
|
||||
alt={card.card_name}
|
||||
class="w-8 h-11 object-cover rounded-sm"
|
||||
/>
|
||||
)}
|
||||
<span class="flex-1 truncate text-sm">
|
||||
{card.card_name}
|
||||
</span>
|
||||
<span class="text-sm font-bold whitespace-nowrap">
|
||||
{card.totalPrice.toFixed(2)} €
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/mycards"
|
||||
class="btn self-start"
|
||||
data-variant="outline"
|
||||
data-size="sm"
|
||||
>
|
||||
Voir toute la collection →
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Explorer -->
|
||||
<section>
|
||||
<h2 class="text-xl font-bold mb-1">Explorer</h2>
|
||||
<p class="text-sm opacity-70 mb-4">
|
||||
Parcourez les cartes par série puis par extension.
|
||||
</p>
|
||||
<div class="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
{
|
||||
series.map((serie) => (
|
||||
<a
|
||||
href={`/series/${serie.id}`}
|
||||
class="card no-underline hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<section class="flex flex-col items-center justify-center gap-2 aspect-square p-2 text-center">
|
||||
{serie.logo ? (
|
||||
<img
|
||||
src={`${serie.logo}.webp`}
|
||||
alt={serie.name}
|
||||
class="h-10 max-w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="opacity-40"
|
||||
>
|
||||
<rect
|
||||
width="18"
|
||||
height="18"
|
||||
x="3"
|
||||
y="3"
|
||||
rx="2"
|
||||
/>
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||
</svg>
|
||||
)}
|
||||
<span class="text-xs font-semibold leading-tight line-clamp-2">
|
||||
{serie.name}
|
||||
</span>
|
||||
</section>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import LoginForm from "../components/LoginForm.astro";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<LoginForm />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,533 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import {
|
||||
priceCollectionCards,
|
||||
type MyCardRecord,
|
||||
} from "../lib/collection";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const userId = pb.authStore.record!.id;
|
||||
|
||||
// Fetch all user's cards
|
||||
const myCards = (await pb.collection("mycards").getFullList({
|
||||
filter: pb.filter("user = {:userId}", { userId }),
|
||||
sort: "-created",
|
||||
})) as unknown as MyCardRecord[];
|
||||
|
||||
const cardsWithPrice = await priceCollectionCards(myCards);
|
||||
|
||||
const totalValue = cardsWithPrice.reduce(
|
||||
(sum, card) => sum + card.totalPrice,
|
||||
0,
|
||||
);
|
||||
const totalCards = cardsWithPrice.reduce((sum, card) => sum + card.quantity, 0);
|
||||
|
||||
// Distinct values present in the collection, for the filter dropdowns
|
||||
const distinctSets = [
|
||||
...new Set(cardsWithPrice.map((c) => c.set_name).filter(Boolean)),
|
||||
].sort();
|
||||
const distinctVariants = [
|
||||
...new Set(cardsWithPrice.map((c) => c.variant)),
|
||||
].sort();
|
||||
const distinctConditions = [
|
||||
...new Set(cardsWithPrice.map((c) => c.condition).filter(Boolean)),
|
||||
].sort();
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-[1600px] mx-auto px-4 md:px-8 pb-12 mt-8">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<h1 class="m-0">Ma Collection</h1>
|
||||
<div class="flex items-center gap-6">
|
||||
<div
|
||||
class="bg-muted rounded-lg px-6 py-3 flex flex-col items-center"
|
||||
>
|
||||
<span class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Cartes</span
|
||||
>
|
||||
<span class="text-2xl font-bold">{totalCards}</span>
|
||||
</div>
|
||||
<div
|
||||
class="bg-muted rounded-lg px-6 py-3 flex flex-col items-center"
|
||||
>
|
||||
<span class="text-xs uppercase tracking-wide opacity-70"
|
||||
>Valeur totale</span
|
||||
>
|
||||
<span class="text-2xl font-bold"
|
||||
>{totalValue.toFixed(2)} €</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
cardsWithPrice.length === 0 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<figure>
|
||||
<svg
|
||||
class="lucide lucide-folder-open"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</figure>
|
||||
<h3>Votre collection est vide</h3>
|
||||
<p>
|
||||
Commencez par rechercher des cartes et ajoutez-les
|
||||
à votre collection.
|
||||
</p>
|
||||
</header>
|
||||
<footer>
|
||||
<a href="/search" class="btn">
|
||||
Rechercher des cartes
|
||||
</a>
|
||||
</footer>
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
<!-- Filters -->
|
||||
<div class="filter-bar mb-4">
|
||||
<div class="pills-row">
|
||||
<span class="pills-label">Variant</span>
|
||||
{distinctVariants.map((v) => (
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
data-cf-group="variant"
|
||||
data-value={v}
|
||||
>
|
||||
{v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div class="pills-row">
|
||||
<span class="pills-label">État</span>
|
||||
{distinctConditions.map((c) => (
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
data-cf-group="condition"
|
||||
data-value={c}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div class="filter-selects">
|
||||
<input
|
||||
type="search"
|
||||
id="cf-name"
|
||||
class="input"
|
||||
placeholder="Filtrer par nom..."
|
||||
/>
|
||||
<label>
|
||||
Set
|
||||
<select id="cf-set" class="select">
|
||||
<option value="">Tous</option>
|
||||
{distinctSets.map((s) => (
|
||||
<option value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Tri
|
||||
<select id="cf-sort" class="select">
|
||||
<option value="">Ajout récent</option>
|
||||
<option value="name">Nom A→Z</option>
|
||||
<option value="price-asc">Prix ↑</option>
|
||||
<option value="price-desc">Prix ↓</option>
|
||||
<option value="total-desc">
|
||||
Valeur totale ↓
|
||||
</option>
|
||||
<option value="qty-desc">Quantité ↓</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" id="cf-reset" class="pill">
|
||||
✕ Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<div class="flex justify-end mb-4">
|
||||
<div role="group" class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
id="view-grid-btn"
|
||||
class="btn"
|
||||
data-variant="outline"
|
||||
data-size="icon-sm"
|
||||
aria-label="Vue grille"
|
||||
title="Vue grille"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="view-list-btn"
|
||||
class="btn"
|
||||
data-variant="ghost"
|
||||
data-size="icon-sm"
|
||||
aria-label="Vue liste"
|
||||
title="Vue liste"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 5h.01" />
|
||||
<path d="M3 12h.01" />
|
||||
<path d="M3 19h.01" />
|
||||
<path d="M8 5h13" />
|
||||
<path d="M8 12h13" />
|
||||
<path d="M8 19h13" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div
|
||||
id="grid-view"
|
||||
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4"
|
||||
>
|
||||
{cardsWithPrice.map((card) => (
|
||||
<div
|
||||
class="relative group"
|
||||
data-name={card.card_name.toLowerCase()}
|
||||
data-set={card.set_name}
|
||||
data-variant={card.variant}
|
||||
data-condition={card.condition}
|
||||
data-price={card.price}
|
||||
data-total={card.totalPrice}
|
||||
data-qty={card.quantity}
|
||||
>
|
||||
<a
|
||||
href={`/card/${card.card_id}`}
|
||||
class="card no-underline hover:-translate-y-0.5 hover:shadow-lg transition-all duration-150 overflow-hidden"
|
||||
>
|
||||
<div class="aspect-2.5/3.5 overflow-hidden">
|
||||
{card.card_image ? (
|
||||
<img
|
||||
src={`${card.card_image}/low.webp`}
|
||||
alt={card.card_name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/assets/placeholder_card.png"
|
||||
alt={card.card_name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div class="p-3 flex flex-col gap-1.5">
|
||||
<h3 class="m-0 text-sm font-semibold leading-tight truncate">
|
||||
{card.card_name}
|
||||
</h3>
|
||||
{card.set_name && (
|
||||
<small class="opacity-60 text-xs truncate">
|
||||
{card.set_name}
|
||||
</small>
|
||||
)}
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="badge" data-variant="secondary">
|
||||
{card.variant}
|
||||
</span>
|
||||
<span class="font-semibold text-sm">
|
||||
×{card.quantity}
|
||||
</span>
|
||||
</div>
|
||||
{card.condition && (
|
||||
<span class="badge" data-variant={card.condition === "Neuf" ? "primary" : card.condition === "Joué" ? "destructive" : "outline"}>
|
||||
{card.condition}
|
||||
</span>
|
||||
)}
|
||||
<div class="flex items-center justify-between mt-2 pt-2 border-t border-border">
|
||||
<span class="font-bold text-base">
|
||||
{card.price.toFixed(2)} €
|
||||
</span>
|
||||
{card.quantity > 1 && (
|
||||
<span class="text-xs opacity-70">
|
||||
= {card.totalPrice.toFixed(2)} €
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="delete-btn"
|
||||
aria-label={`Supprimer ${card.card_name}`}
|
||||
title="Supprimer"
|
||||
onclick={`showDeleteConfirm('${card.id}', '${card.card_name.replace(/'/g, "\\'")}')`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
||||
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- List View (hidden by default) -->
|
||||
<div id="list-view" class="hidden">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16"></th>
|
||||
<th>Nom</th>
|
||||
<th>Set</th>
|
||||
<th>Variant</th>
|
||||
<th>État</th>
|
||||
<th class="text-end">Qté</th>
|
||||
<th class="text-end">Prix</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cardsWithPrice.map((card) => (
|
||||
<tr
|
||||
data-name={card.card_name.toLowerCase()}
|
||||
data-set={card.set_name}
|
||||
data-variant={card.variant}
|
||||
data-condition={card.condition}
|
||||
data-price={card.price}
|
||||
data-total={card.totalPrice}
|
||||
data-qty={card.quantity}
|
||||
>
|
||||
<td>
|
||||
<a href={`/card/${card.card_id}`}>
|
||||
{card.card_image ? (
|
||||
<img
|
||||
src={`${card.card_image}/low.webp`}
|
||||
alt={card.card_name}
|
||||
class="w-12 h-16 object-cover rounded-sm"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/assets/placeholder_card.png"
|
||||
alt={card.card_name}
|
||||
class="w-12 h-16 object-cover rounded-sm"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
</td>
|
||||
<td class="font-medium">
|
||||
<a
|
||||
href={`/card/${card.card_id}`}
|
||||
class="no-underline"
|
||||
>
|
||||
{card.card_name}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-sm opacity-70">
|
||||
{card.set_name}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" data-variant="secondary">{card.variant}</span>
|
||||
</td>
|
||||
<td>
|
||||
{card.condition && (
|
||||
<span class="badge" data-variant={card.condition === "Neuf" ? "primary" : card.condition === "Joué" ? "destructive" : "outline"}>{card.condition}</span>
|
||||
)}
|
||||
</td>
|
||||
<td class="text-end tabular-nums">
|
||||
×{card.quantity}
|
||||
</td>
|
||||
<td class="text-end tabular-nums font-semibold">
|
||||
{card.price.toFixed(2)} €
|
||||
</td>
|
||||
<td class="text-end tabular-nums font-bold">
|
||||
{card.totalPrice.toFixed(2)} €
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shown when filters hide every card -->
|
||||
<div id="cf-empty" class="hidden">
|
||||
<section class="empty">
|
||||
<header>
|
||||
<h3>Aucune carte ne correspond à ces filtres</h3>
|
||||
<p>
|
||||
Modifiez ou réinitialisez les filtres pour
|
||||
voir vos cartes.
|
||||
</p>
|
||||
</header>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<dialog id="delete-dialog" class="alert-dialog" aria-labelledby="delete-dialog-title" aria-describedby="delete-dialog-description">
|
||||
<div>
|
||||
<header>
|
||||
<figure>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 6h18" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
||||
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</figure>
|
||||
<h2 id="delete-dialog-title">Supprimer cette carte ?</h2>
|
||||
<p id="delete-dialog-description">
|
||||
Cette action est irréversible. La carte sera retirée de votre collection.
|
||||
</p>
|
||||
</header>
|
||||
<footer>
|
||||
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
<button class="btn" data-variant="destructive" id="delete-confirm-btn">Supprimer</button>
|
||||
</footer>
|
||||
</div>
|
||||
</dialog>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
import { initViewToggle } from "../lib/view-toggle";
|
||||
import { initCollectionFilter } from "../lib/collection-filter";
|
||||
|
||||
initViewToggle("collectionView");
|
||||
initCollectionFilter();
|
||||
|
||||
// Delete flow
|
||||
let deleteTargetId: string | null = null;
|
||||
const deleteDialog = document.getElementById("delete-dialog") as HTMLDialogElement;
|
||||
const deleteConfirmBtn = document.getElementById("delete-confirm-btn") as HTMLButtonElement;
|
||||
const toaster = document.getElementById("toaster") as ToasterElement;
|
||||
|
||||
function showToast(category: "success" | "error", title: string, description: string) {
|
||||
if (toaster?.toast) {
|
||||
toaster.toast({ category, title, description });
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).showDeleteConfirm = (id: string, name: string) => {
|
||||
deleteTargetId = id;
|
||||
const desc = deleteDialog.querySelector("#delete-dialog-description");
|
||||
if (desc) {
|
||||
desc.textContent = `Voulez-vous vraiment supprimer « ${name} » de votre collection ? Cette action est irréversible.`;
|
||||
}
|
||||
(deleteDialog as any).showModal();
|
||||
};
|
||||
|
||||
deleteConfirmBtn?.addEventListener("click", async () => {
|
||||
if (!deleteTargetId) return;
|
||||
|
||||
deleteConfirmBtn.disabled = true;
|
||||
const originalText = deleteConfirmBtn.innerHTML;
|
||||
deleteConfirmBtn.innerHTML = `<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg> Suppression...`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/collection/${deleteTargetId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showToast("success", "Supprimée", "Carte retirée de votre collection.");
|
||||
(deleteDialog as any).close();
|
||||
// Remove the card from the DOM
|
||||
const cardEl = deleteConfirmBtn.closest(".group");
|
||||
if (cardEl) {
|
||||
cardEl.remove();
|
||||
}
|
||||
// Reload to refresh totals
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast("error", "Erreur", data.error ?? "Échec de la suppression");
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
} finally {
|
||||
deleteConfirmBtn.disabled = false;
|
||||
deleteConfirmBtn.innerHTML = originalText;
|
||||
deleteTargetId = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
a.card {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 10;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: color-mix(in oklab, var(--destructive) 75%, transparent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 150ms ease,
|
||||
transform 150ms ease,
|
||||
background 150ms ease;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: color-mix(in oklab, var(--destructive) 95%, transparent);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.group:hover .delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,342 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const user = pb.authStore.record!;
|
||||
const initials = (user.name || user.email || "")
|
||||
.split(/[\s@.]+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((s: string) => s[0].toUpperCase())
|
||||
.join("");
|
||||
|
||||
const themePreference = user.theme_preference ?? "System";
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-2xl mx-auto px-4 md:px-8 py-8">
|
||||
<h1 class="text-xl font-bold mb-1">Profil</h1>
|
||||
<p class="text-sm opacity-70 mb-8">
|
||||
Gérez votre compte et vos préférences.
|
||||
</p>
|
||||
|
||||
<div class="tabs" id="profile-tabs">
|
||||
<nav role="tablist" aria-orientation="horizontal" class="mb-6">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="tab-account"
|
||||
aria-controls="panel-account"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
>
|
||||
Compte
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="tab-preferences"
|
||||
aria-controls="panel-preferences"
|
||||
aria-selected="false"
|
||||
tabindex="-1"
|
||||
>
|
||||
Préférences
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Account Panel -->
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="panel-account"
|
||||
aria-labelledby="tab-account"
|
||||
tabindex="-1"
|
||||
aria-selected="true"
|
||||
>
|
||||
<div class="card">
|
||||
<header>
|
||||
<h2>Compte</h2>
|
||||
</header>
|
||||
<section>
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<span class="avatar" data-size="lg">
|
||||
<span>{initials}</span>
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold"
|
||||
>{user.name || "—"}</span
|
||||
>
|
||||
<span class="text-sm opacity-70"
|
||||
>{user.email}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="account-form" class="grid gap-4">
|
||||
<div role="group" class="field">
|
||||
<label for="username">Nom d'utilisateur</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={user.username ?? ""}
|
||||
placeholder="ex: pikalover"
|
||||
minlength="3"
|
||||
maxlength="20"
|
||||
/>
|
||||
<p>
|
||||
Votre @username unique (3-20 caractères,
|
||||
lettres minuscules et chiffres).
|
||||
</p>
|
||||
</div>
|
||||
<div role="group" class="field">
|
||||
<label for="name">Nom complet</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={user.name ?? ""}
|
||||
placeholder="Votre nom complet"
|
||||
/>
|
||||
<p>Votre nom affiché dans l'application.</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer>
|
||||
<button
|
||||
class="btn"
|
||||
type="submit"
|
||||
form="account-form"
|
||||
id="account-submit"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preferences Panel -->
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="panel-preferences"
|
||||
aria-labelledby="tab-preferences"
|
||||
tabindex="-1"
|
||||
aria-selected="false"
|
||||
hidden
|
||||
>
|
||||
<div class="card">
|
||||
<header>
|
||||
<h2>Préférences</h2>
|
||||
<p>Personnalisez votre expérience.</p>
|
||||
</header>
|
||||
<section>
|
||||
<form id="profile-form" class="grid gap-4">
|
||||
<div role="group" class="field">
|
||||
<label for="theme_preference">Thème</label>
|
||||
<select
|
||||
name="theme_preference"
|
||||
id="theme_preference"
|
||||
class="select w-full"
|
||||
>
|
||||
<option
|
||||
value="System"
|
||||
selected={themePreference === "System"}
|
||||
>
|
||||
Système
|
||||
</option>
|
||||
<option
|
||||
value="Light"
|
||||
selected={themePreference === "Light"}
|
||||
>
|
||||
Clair
|
||||
</option>
|
||||
<option
|
||||
value="Dark"
|
||||
selected={themePreference === "Dark"}
|
||||
>
|
||||
Sombre
|
||||
</option>
|
||||
</select>
|
||||
<p>Choisissez le thème de l'interface.</p>
|
||||
</div>
|
||||
|
||||
<div role="group" class="field">
|
||||
<label for="visibility"
|
||||
>Visibilité de la collection</label
|
||||
>
|
||||
<select
|
||||
name="visibility"
|
||||
id="visibility"
|
||||
class="select w-full"
|
||||
>
|
||||
<option
|
||||
value="Friends"
|
||||
selected={user.visibility !==
|
||||
"Public" &&
|
||||
user.visibility !== "Private"}
|
||||
>
|
||||
Amis seulement
|
||||
</option>
|
||||
<option
|
||||
value="Public"
|
||||
selected={user.visibility === "Public"}
|
||||
>
|
||||
Publique
|
||||
</option>
|
||||
<option
|
||||
value="Private"
|
||||
selected={user.visibility === "Private"}
|
||||
>
|
||||
Privée
|
||||
</option>
|
||||
</select>
|
||||
<p>Qui peut voir votre collection.</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer>
|
||||
<button
|
||||
class="btn"
|
||||
type="submit"
|
||||
form="profile-form"
|
||||
id="profile-submit"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const toaster = document.getElementById("toaster") as ToasterElement;
|
||||
|
||||
const showToast = (
|
||||
category: "success" | "error",
|
||||
title: string,
|
||||
description: string,
|
||||
) => {
|
||||
if (toaster?.toast) {
|
||||
toaster.toast({ category, title, description });
|
||||
}
|
||||
};
|
||||
|
||||
// Account form (name)
|
||||
const accountForm = document.getElementById("account-form");
|
||||
const accountBtn = document.getElementById(
|
||||
"account-submit",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
accountForm?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(accountForm as HTMLFormElement);
|
||||
const name = formData.get("name");
|
||||
const username = formData.get("username");
|
||||
|
||||
accountBtn.disabled = true;
|
||||
const originalText = accountBtn.innerHTML;
|
||||
accountBtn.innerHTML = `<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg> Enregistrement...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/profile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, username }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showToast(
|
||||
"success",
|
||||
"Enregistré",
|
||||
"Votre nom a été mis à jour.",
|
||||
);
|
||||
// Reload to reflect the new name in the nav
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast(
|
||||
"error",
|
||||
"Erreur",
|
||||
data.error ?? "Échec de l'enregistrement",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
} finally {
|
||||
accountBtn.disabled = false;
|
||||
accountBtn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
|
||||
// Preferences form (theme)
|
||||
const profileForm = document.getElementById("profile-form");
|
||||
const profileBtn = document.getElementById(
|
||||
"profile-submit",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
profileForm?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(profileForm as HTMLFormElement);
|
||||
const themePreference = formData.get("theme_preference");
|
||||
const visibility = formData.get("visibility");
|
||||
|
||||
profileBtn.disabled = true;
|
||||
const originalText = profileBtn.innerHTML;
|
||||
profileBtn.innerHTML = `<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg> Enregistrement...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/profile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
theme_preference: themePreference,
|
||||
visibility,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showToast(
|
||||
"success",
|
||||
"Enregistré",
|
||||
"Vos préférences ont été mises à jour.",
|
||||
);
|
||||
// Apply theme instantly without refresh
|
||||
const html = document.documentElement;
|
||||
if (themePreference === "Dark") {
|
||||
html.classList.add("dark");
|
||||
html.removeAttribute("data-theme");
|
||||
} else if (themePreference === "Light") {
|
||||
html.classList.remove("dark");
|
||||
html.setAttribute("data-theme", "light");
|
||||
} else {
|
||||
// System — use system preference
|
||||
html.classList.toggle(
|
||||
"dark",
|
||||
matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
html.removeAttribute("data-theme");
|
||||
}
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast(
|
||||
"error",
|
||||
"Erreur",
|
||||
data.error ?? "Échec de l'enregistrement",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
} finally {
|
||||
profileBtn.disabled = false;
|
||||
profileBtn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,394 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import { getSets } from "../lib/tcgdex-cache";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const search = Astro.url.searchParams.get("search") ?? "";
|
||||
const serie = Astro.url.searchParams.get("serie") ?? "";
|
||||
const set = Astro.url.searchParams.get("set") ?? "";
|
||||
const sort = Astro.url.searchParams.get("sort") ?? "";
|
||||
|
||||
const hasFilter = !!(serie || set);
|
||||
|
||||
interface CardSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
localId?: string;
|
||||
image?: string;
|
||||
setId: string;
|
||||
serieId: string;
|
||||
serieName: string;
|
||||
set: { name: string };
|
||||
}
|
||||
|
||||
let cards: CardSummary[] = [];
|
||||
let serieFacets: { id: string; name: string; count: number }[] = [];
|
||||
let setFacets: { id: string; name: string; count: number }[] = [];
|
||||
|
||||
if (search.length >= 3) {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set("name", `like:${search}*`);
|
||||
if (sort === "name-asc" || sort === "name-desc") {
|
||||
params.set("sort:field", "name");
|
||||
params.set("sort:order", sort === "name-asc" ? "asc" : "desc");
|
||||
}
|
||||
|
||||
// The list endpoint returns summaries (id, name, localId, image) only,
|
||||
// so serie/set filters are derived from the results themselves: a
|
||||
// card's set id is the part of its id before the last "-" and the
|
||||
// cached set metadata maps each set to its serie. Filtering is
|
||||
// applied locally so the facet pills always reflect the full results.
|
||||
const [res, allSets] = await Promise.all([
|
||||
fetch(`https://api.tcgdex.net/v2/fr/cards?${params}`),
|
||||
getSets(),
|
||||
]);
|
||||
if (res.ok) {
|
||||
const summaries: { id: string }[] = await res.json();
|
||||
const setById = new Map(allSets.map((s) => [s.id, s]));
|
||||
|
||||
const enriched: CardSummary[] = summaries.map((card) => {
|
||||
const setId = card.id.replace(/-[^-]+$/, "");
|
||||
const meta = setById.get(setId);
|
||||
return {
|
||||
...card,
|
||||
setId,
|
||||
serieId: meta?.serie?.id ?? "",
|
||||
serieName: meta?.serie?.name ?? "",
|
||||
set: { name: meta?.name ?? setId },
|
||||
};
|
||||
});
|
||||
|
||||
const countBy = (
|
||||
key: "serieId" | "setId",
|
||||
): { id: string; name: string; count: number }[] => {
|
||||
const counts = new Map<
|
||||
string,
|
||||
{ name: string; count: number }
|
||||
>();
|
||||
for (const c of enriched) {
|
||||
const id = c[key];
|
||||
if (!id) continue;
|
||||
const name = key === "serieId" ? c.serieName : c.set.name;
|
||||
const entry = counts.get(id) ?? { name, count: 0 };
|
||||
entry.count++;
|
||||
counts.set(id, entry);
|
||||
}
|
||||
return [...counts.entries()]
|
||||
.map(([id, v]) => ({ id, ...v }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
};
|
||||
|
||||
serieFacets = countBy("serieId");
|
||||
setFacets = countBy("setId");
|
||||
|
||||
cards = enriched.filter(
|
||||
(c) =>
|
||||
(!serie || c.serieId === serie) && (!set || c.setId === set),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Keep empty results on network failure
|
||||
}
|
||||
}
|
||||
|
||||
// A search can span dozens of sets — keep the pill row to the most common ones
|
||||
const MAX_SET_PILLS = 12;
|
||||
const visibleSetFacets = setFacets.slice(0, MAX_SET_PILLS);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-[1600px] mx-auto px-4 md:px-8 py-8">
|
||||
<form class="mb-6" action="/search" method="get">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="search"
|
||||
name="search"
|
||||
value={search}
|
||||
placeholder="Rechercher une carte..."
|
||||
minlength="3"
|
||||
autofocus
|
||||
/>
|
||||
<span data-align="start" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
</span>
|
||||
{search && (
|
||||
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" data-align="end" aria-label="Effacer la recherche" onclick="this.closest('form').querySelector('input').value=''; this.closest('form').submit()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<button type="submit" class="btn" data-size="sm" data-align="end">Rechercher</button>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic filters, extracted from the results -->
|
||||
{serieFacets.length > 0 && (
|
||||
<>
|
||||
<input type="hidden" name="serie" value={serie} />
|
||||
<input type="hidden" name="set" value={set} />
|
||||
<div class="filter-bar mt-4">
|
||||
<div class="pills-row">
|
||||
<span class="pills-label">Série</span>
|
||||
{serieFacets.map((f) => (
|
||||
<button
|
||||
type="button"
|
||||
class:list={[
|
||||
"pill",
|
||||
{ active: serie === f.id },
|
||||
]}
|
||||
data-pill-group="serie"
|
||||
data-value={f.id}
|
||||
>
|
||||
{f.name}
|
||||
<span class="pill-count">{f.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div class="pills-row">
|
||||
<span class="pills-label">Set</span>
|
||||
{visibleSetFacets.map((f) => (
|
||||
<button
|
||||
type="button"
|
||||
class:list={[
|
||||
"pill",
|
||||
{ active: set === f.id },
|
||||
]}
|
||||
data-pill-group="set"
|
||||
data-value={f.id}
|
||||
>
|
||||
{f.name}
|
||||
<span class="pill-count">{f.count}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div class="filter-selects">
|
||||
<label>
|
||||
Tri
|
||||
<select
|
||||
name="sort"
|
||||
class="select"
|
||||
onchange="this.form.submit()"
|
||||
>
|
||||
<option value="">Pertinence</option>
|
||||
<option
|
||||
value="name-asc"
|
||||
selected={sort === "name-asc"}
|
||||
>
|
||||
Nom A→Z
|
||||
</option>
|
||||
<option
|
||||
value="name-desc"
|
||||
selected={sort === "name-desc"}
|
||||
>
|
||||
Nom Z→A
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
{(hasFilter || sort) && (
|
||||
<a
|
||||
href={`/search?search=${encodeURIComponent(search)}`}
|
||||
class="pill"
|
||||
>
|
||||
✕ Réinitialiser
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{
|
||||
search.length > 0 && search.length < 3 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<figure>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
</figure>
|
||||
<h3>Recherche trop courte</h3>
|
||||
<p>Entrez au moins 3 caractères pour lancer la recherche.</p>
|
||||
</header>
|
||||
</section>
|
||||
) : search.length >= 3 && cards.length === 0 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<figure>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
</figure>
|
||||
<h3>Aucun résultat</h3>
|
||||
<p>Aucune carte trouvée pour "{search}"{hasFilter && " avec ces filtres"}. Essayez d'autres critères.</p>
|
||||
</header>
|
||||
</section>
|
||||
) : cards.length > 0 ? (
|
||||
<>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-sm opacity-70 m-0">{cards.length} carte{cards.length > 1 ? "s" : ""} trouvée{cards.length > 1 ? "s" : ""}</p>
|
||||
<!-- View Toggle -->
|
||||
<div role="group" class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
id="view-grid-btn"
|
||||
class="btn"
|
||||
data-variant="outline"
|
||||
data-size="icon-sm"
|
||||
aria-label="Vue grille"
|
||||
title="Vue grille"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="view-list-btn"
|
||||
class="btn"
|
||||
data-variant="ghost"
|
||||
data-size="icon-sm"
|
||||
aria-label="Vue liste"
|
||||
title="Vue liste"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 5h.01" />
|
||||
<path d="M3 12h.01" />
|
||||
<path d="M3 19h.01" />
|
||||
<path d="M8 5h13" />
|
||||
<path d="M8 12h13" />
|
||||
<path d="M8 19h13" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div
|
||||
id="grid-view"
|
||||
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4"
|
||||
>
|
||||
{cards.map((card) => (
|
||||
<a
|
||||
href={`/card/${card.id}`}
|
||||
class="card hover:shadow-lg transition-shadow no-underline"
|
||||
>
|
||||
<img
|
||||
class="aspect-2.5/3.5 w-full object-cover"
|
||||
src={card.image ? `${card.image}/low.webp` : "/assets/placeholder_card.png"}
|
||||
alt={card.name}
|
||||
/>
|
||||
<header class="px-3 pb-3">
|
||||
<h3 class="text-sm font-semibold truncate">{card.name}</h3>
|
||||
<span class="text-xs opacity-50">{card.id}</span>
|
||||
</header>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- List View (hidden by default) -->
|
||||
<div id="list-view" class="hidden">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-16"></th>
|
||||
<th>Nom</th>
|
||||
<th>Set</th>
|
||||
<th>Numéro</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cards.map((card) => (
|
||||
<tr>
|
||||
<td>
|
||||
<a href={`/card/${card.id}`}>
|
||||
<img
|
||||
src={card.image ? `${card.image}/low.webp` : "/assets/placeholder_card.png"}
|
||||
alt={card.name}
|
||||
class="w-12 h-16 object-cover rounded-sm"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="font-medium">
|
||||
<a href={`/card/${card.id}`} class="no-underline">
|
||||
{card.name}
|
||||
<span class="text-xs opacity-50 ml-1.5">{card.id}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-sm opacity-70">
|
||||
{card.set?.name ?? "—"}
|
||||
</td>
|
||||
<td class="text-sm opacity-70">
|
||||
{card.localId ?? "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<figure>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
</figure>
|
||||
<h3>Rechercher une carte</h3>
|
||||
<p>Recherchez une carte par nom, puis filtrez les résultats par série et par set.</p>
|
||||
</header>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
import { initViewToggle } from "../lib/view-toggle";
|
||||
|
||||
initViewToggle("searchView");
|
||||
|
||||
// Pill filters: toggle the hidden input for the group, then resubmit the
|
||||
// GET form so the current search text and other filters are preserved.
|
||||
document
|
||||
.querySelectorAll<HTMLButtonElement>("[data-pill-group]")
|
||||
.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const group = btn.dataset.pillGroup!;
|
||||
const hidden = document.querySelector<HTMLInputElement>(
|
||||
`input[type="hidden"][name="${group}"]`,
|
||||
);
|
||||
if (!hidden) return;
|
||||
hidden.value =
|
||||
hidden.value === btn.dataset.value
|
||||
? ""
|
||||
: (btn.dataset.value ?? "");
|
||||
btn.closest("form")?.submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
a.card {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { getSeries, getSetsBySerie } from "../../lib/tcgdex-cache";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const { id } = Astro.params;
|
||||
|
||||
if (!id) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
const [series, sets] = await Promise.all([getSeries(), getSetsBySerie(id)]);
|
||||
const serie = series.find((s) => s.id === id);
|
||||
|
||||
if (!serie) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-350 mx-auto px-4 md:px-8 py-8">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-block no-underline opacity-70 hover:opacity-100 text-sm mb-4"
|
||||
>
|
||||
← Accueil
|
||||
</a>
|
||||
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
{
|
||||
serie.logo && (
|
||||
<img
|
||||
src={`${serie.logo}.webp`}
|
||||
alt={serie.name}
|
||||
class="h-14 w-auto object-contain"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<h1 class="m-0">{serie.name}</h1>
|
||||
<p class="text-sm opacity-70 m-0">
|
||||
{sets.length} extension{sets.length > 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
sets.length === 0 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<h3>Aucune extension</h3>
|
||||
<p>Aucune extension trouvée pour cette série.</p>
|
||||
</header>
|
||||
</section>
|
||||
) : (
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||
{sets.map((set) => (
|
||||
<a
|
||||
href={`/sets/${set.id}`}
|
||||
class="card no-underline hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<section class="flex flex-col items-center gap-2 py-4 text-center">
|
||||
{set.logo && (
|
||||
<img
|
||||
src={`${set.logo}.webp`}
|
||||
alt={set.name}
|
||||
class="h-16 max-w-full object-contain"
|
||||
/>
|
||||
)}
|
||||
{set.symbol && (
|
||||
<img
|
||||
src={`${set.symbol}.webp`}
|
||||
alt=""
|
||||
class="h-6 w-6 object-contain"
|
||||
/>
|
||||
)}
|
||||
<span class="font-semibold text-sm leading-tight">
|
||||
{set.name}
|
||||
</span>
|
||||
<span class="text-xs opacity-60">
|
||||
{set.cardCount.official} cartes
|
||||
</span>
|
||||
</section>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const { id } = Astro.params;
|
||||
|
||||
if (!id) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
|
||||
// The set detail endpoint includes the full card list (summaries)
|
||||
let set: any = null;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.tcgdex.net/v2/fr/sets/${encodeURIComponent(id)}`,
|
||||
);
|
||||
if (res.ok) {
|
||||
set = await res.json();
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
if (!set) {
|
||||
return Astro.redirect("/");
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-[1600px] mx-auto px-4 md:px-8 py-8">
|
||||
<a
|
||||
href={set.serie?.id ? `/series/${set.serie.id}` : "/"}
|
||||
class="inline-block no-underline opacity-70 hover:opacity-100 text-sm mb-4"
|
||||
>
|
||||
← {set.serie?.name ?? "Accueil"}
|
||||
</a>
|
||||
|
||||
<div class="flex items-center gap-4 mb-6 flex-wrap">
|
||||
{set.logo && (
|
||||
<img
|
||||
src={`${set.logo}.webp`}
|
||||
alt={set.name}
|
||||
class="h-14 w-auto object-contain"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h1 class="m-0">{set.name}</h1>
|
||||
<p class="text-sm opacity-70 m-0">
|
||||
{set.cards?.length ?? 0} carte{set.cards?.length > 1 ? "s" : ""}
|
||||
{set.releaseDate && (
|
||||
<> — sortie le {new Date(set.releaseDate).toLocaleDateString("fr-FR")}</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
!set.cards || set.cards.length === 0 ? (
|
||||
<section class="empty">
|
||||
<header>
|
||||
<h3>Aucune carte</h3>
|
||||
<p>Aucune carte trouvée pour cette extension.</p>
|
||||
</header>
|
||||
</section>
|
||||
) : (
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{set.cards.map((card: any) => (
|
||||
<a
|
||||
href={`/card/${card.id}`}
|
||||
class="card hover:shadow-lg transition-shadow no-underline"
|
||||
>
|
||||
<img
|
||||
class="aspect-2.5/3.5 w-full object-cover"
|
||||
src={card.image ? `${card.image}/low.webp` : "/assets/placeholder_card.png"}
|
||||
alt={card.name}
|
||||
loading="lazy"
|
||||
/>
|
||||
<header class="px-3 pb-3">
|
||||
<h3 class="text-sm font-semibold truncate">
|
||||
{card.name}
|
||||
</h3>
|
||||
<p class="text-xs opacity-60 m-0">#{card.localId}</p>
|
||||
</header>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style is:global>
|
||||
a.card {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
@import "tailwindcss";
|
||||
@import "basecoat-css/vega.css";
|
||||
|
||||
/* Global base styles */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Smooth transitions for interactive elements */
|
||||
.btn,
|
||||
.badge,
|
||||
.card,
|
||||
.item {
|
||||
transition-property: color, background-color, border-color, box-shadow, transform;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Focus visible for accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Hide the native clear button on search inputs — the pages provide their
|
||||
own clear button, and Chrome renders both, overlapping the input text */
|
||||
input[type="search"]::-webkit-search-cancel-button {
|
||||
appearance: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Filter bars (search + collection pages) */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.pills-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.pills-label {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
opacity: 0.6;
|
||||
min-width: 4.5rem;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.22rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background-color 150ms ease,
|
||||
color 150ms ease,
|
||||
border-color 150ms ease,
|
||||
filter 150ms ease;
|
||||
}
|
||||
|
||||
.pill:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.pill.active {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pill-count {
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.pill.active .pill-count {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Compact inline selects row (no full-width stacked fields) */
|
||||
.filter-selects {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-selects label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-selects select {
|
||||
width: auto;
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
.filter-selects .input {
|
||||
width: 12rem;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user