Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a00fd1909 | ||
|
|
b2fa2a98e0 |
No files matched your search
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Set up Bun
|
- name: Set up Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: "1.4.0"
|
bun-version: "1.3.14"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
@@ -207,7 +207,7 @@ jobs:
|
|||||||
- name: Set up Bun
|
- name: Set up Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: "1.4.0"
|
bun-version: "1.3.14"
|
||||||
|
|
||||||
- name: Install Nix
|
- name: Install Nix
|
||||||
uses: DeterminateSystems/nix-installer-action@v16
|
uses: DeterminateSystems/nix-installer-action@v16
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
# MCPedia Frontend Redesign — Spec
|
|
||||||
|
|
||||||
Status: DRAFT — minta approval sebelum coding
|
|
||||||
Date: 2026-09-08 WIB
|
|
||||||
Author: Hermes
|
|
||||||
|
|
||||||
## 1. Tujuan
|
|
||||||
|
|
||||||
Merombak `apps/web` agar:
|
|
||||||
- Satu sumber kebenaran: semua data berasal dari `@mcpedia/core` types & `@mcpedia/config` section presets; tidak ada shape duplikat di FE.
|
|
||||||
- Selaras dengan gateway/BE data flow: tRPC `apps/api` (`/trpc/*`) + REST `/api/*` + MCP `:4021` semua mengembalikan shape yang sama (DocumentMeta, SectionInfo, SearchHit, ExportData).
|
|
||||||
- Ganti pola fetch ad-hoc (`fetch("/api/...")` tersebar) dengan **SWR** terpusat — cache, revalidate, mutate, optimistic UI, error boundary konsisten.
|
|
||||||
- Hapus sisa TanStack (saat ini tidak ada deps TanStack — audit `apps/web/package.json` clean; tapi pola akan dibakukan supaya tidak masuk lagi).
|
|
||||||
- UI: Archival Precision tetap (monochrome, hairline, mono telemetry), tapi lebih live dan terisi data real (bukan placeholder).
|
|
||||||
|
|
||||||
## 2. Audit Status Quo
|
|
||||||
|
|
||||||
### FE sekarang
|
|
||||||
- Next.js 16.3.4, App Router, `force-dynamic` di semua page yang baca DB.
|
|
||||||
- RSC langsung import `@mcpedia/core` (`listDocuments`, `getDocument`, `listSections`, `getRelated`, `listRevisions`, `getExportDocuments`). SEO bagus, tapi client components (Sidebar, Header, Search, CommandMenu) fetch ulang via REST tanpa cache sharing.
|
|
||||||
- REST: `/api/search?q&mode&limit`, `/api/docs` (list), `/api/sections`, `/api/export?path&sort`, `/api/revisions/restore`, `/api/auth/login`.
|
|
||||||
- tRPC `apps/api` (Hono) expose: search/semanticSearch/hybridSearch, getDocument, listDocuments, sections, related, revisions, CRUD (gated `x-webhook-secret`), jobStatus, reindex hooks, `/health`, `/metrics`.
|
|
||||||
- MCP `:4021` Streamable HTTP `/mcp` expose 13 tools (tidak dipakai FE, tapi shape-nya sama).
|
|
||||||
- Tidak ada `swr` / `@tanstack/react-query` di `apps/web/package.json` — jadi migrasi = greenfield SWR layer, bukan replace.
|
|
||||||
- Design tokens sudah rapi di `globals.css` (CSS vars light/dark + `--brand`, `--bg-*`, `--text-*`, `--code-*`). Header/Sidebar/Markdown/TOC/DocActions sudah pakai tokens.
|
|
||||||
|
|
||||||
### Masalah yang diselesaikan redesign
|
|
||||||
1. Sidebar & Header fetch `/api/docs` + `/api/sections` tanpa deduplikasi — tiap navigasi fetch ulang, tidak ada stale-while-revalidate.
|
|
||||||
2. Search page client-side `fetch` manual tanpa debounce terpusat, tanpa cache antar-mode (hybrid/keyword/semantic saling overwrite).
|
|
||||||
3. Doc page RSC tidak live: setelah create/update/delete atau reindex, harus hard refresh. Tidak ada `mutate` cross-component.
|
|
||||||
4. Shape drift: Search `snippet` vs `SearchHit` vs `ChunkHit` tidak konsisten antara `/api/search` dan `@mcpedia/search` — perlu typed fetcher.
|
|
||||||
5. Tidak ada surface untuk queue/embeddings status (BE punya `jobStatus`, `queueStatus`, `INDEX_QUEUE` BullMQ) — FE buta terhadap indexing.
|
|
||||||
|
|
||||||
## 3. Arsitektur Target
|
|
||||||
|
|
||||||
```
|
|
||||||
apps/web/app/
|
|
||||||
lib/
|
|
||||||
fetcher.ts — typed fetcher (fetch JSON + error envelope)
|
|
||||||
swr.ts — SWRConfig global (dedupingInterval, focusThrottle, errorRetry)
|
|
||||||
api.ts — typed client untuk /api/* (listDocuments, search, sections, export, revisions)
|
|
||||||
hooks/
|
|
||||||
useSections.ts — SWR '/api/sections' -> SectionInfo[]
|
|
||||||
useDocuments.ts — SWR '/api/docs?section&status' -> DocumentMeta[]
|
|
||||||
useDocument.ts — SWR '/api/docs/:slug' (baru, GET single) -> Document
|
|
||||||
useSearch.ts — SWR key [q, mode, limit] -> SearchHit[] + debounced
|
|
||||||
useRelated.ts — SWR '/api/related?slug'
|
|
||||||
useRevisions.ts — SWR '/api/revisions?slug'
|
|
||||||
useQueueStatus.ts — SWR '/api/queue/status' poll 5s (opsional, admin only)
|
|
||||||
components/
|
|
||||||
(existing: Header, Sidebar, CommandMenu, Markdown, TOC, DocActions, ...)
|
|
||||||
SearchBar.tsx — reusable, dipakai Header + Search page + CommandMenu
|
|
||||||
SectionCard.tsx — extract dari page.tsx
|
|
||||||
DocumentCard.tsx — reusable card (recent, related, folder list)
|
|
||||||
TagCloud.tsx — baru: agregasi tags dari docs
|
|
||||||
QueueBadge.tsx — baru: show indexing status (waiting/active/failed)
|
|
||||||
EmptyState.tsx / ErrorState.tsx
|
|
||||||
app/
|
|
||||||
layout.tsx — tambah <SWRProvider> wrapper (client boundary)
|
|
||||||
page.tsx — refactor: RSC shell + client islands via SWR
|
|
||||||
[section]/page.tsx — sama
|
|
||||||
[section]/[...slug]/page.tsx — RSC hero + Markdown, client islands (related/revisions via SWR)
|
|
||||||
search/page.tsx — fully SWR, hapus useState fetch manual
|
|
||||||
create/page.tsx — mutate after create
|
|
||||||
dashboard/page.tsx — BARU (optional phase 2): queue, sections, recent, tag stats
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prinsip SWR
|
|
||||||
- Satu `fetcher` generic: `fetcher<T>(url: string): Promise<T>` — throw on !ok dengan `info` + `status` untuk error boundary.
|
|
||||||
- `SWRConfig` di `layout.tsx` client wrapper: `dedupingInterval: 4000`, `focusThrottleInterval: 10000`, `shouldRetryOnError: false` untuk search, `refreshInterval` hanya untuk queue.
|
|
||||||
- Semua hooks pakai key array/string yang deterministic supaya `mutate` cross-page bisa: `mutate('/api/docs')`, `mutate('/api/sections')`.
|
|
||||||
- Debounce search di hook, bukan di component: `useDebouncedValue(q, 180ms)` internal ke `useSearch`.
|
|
||||||
- Optimistic mutate untuk create/update/delete: `mutate('/api/docs', optimisticList, false)` lalu revalidate.
|
|
||||||
|
|
||||||
## 4. Kontrak Data (FE ↔ BE)
|
|
||||||
|
|
||||||
Semua types import dari `@mcpedia/types` — tidak ada re-declare shape di FE.
|
|
||||||
|
|
||||||
| FE Hook | BE Source | Endpoint | Shape |
|
|
||||||
|---------|-----------|----------|-------|
|
|
||||||
| useSections | `listSections()` | `GET /api/sections` | `SectionInfo[]` |
|
|
||||||
| useDocuments | `listDocuments({section,status})` | `GET /api/docs?section=&status=` | `DocumentMeta[]` |
|
|
||||||
| useDocument | `getDocument(slug)` | `GET /api/docs/:slug` (baru) | `Document` |
|
|
||||||
| useSearch | `keywordSearch/hybridSearch/semanticSearch` | `GET /api/search?q&mode&limit` | `{results: {slug,title,section,score,snippet}[]}` |
|
|
||||||
| useRelated | `getRelated(slug)` | `GET /api/related?slug=` (baru, atau reuse via core) | `DocumentMeta[]` |
|
|
||||||
| useRevisions | `listRevisions(slug)` | `GET /api/revisions?slug=` (baru) | `Revision[]` |
|
|
||||||
| useQueueStatus | BullMQ `getQueue()` | `GET /api/queue/status` (baru, proxy ke apps/api /metrics atau queue) | `{waiting,active,completed,failed,delayed}` |
|
|
||||||
|
|
||||||
**Tambahan endpoint yang perlu dibuat (tipis, proxy ke core):**
|
|
||||||
- `GET /api/docs/[slug]` — single doc (hindari RSC-only fetch, enable SWR).
|
|
||||||
- `GET /api/related?slug=&limit=` — wrapper `getRelated`.
|
|
||||||
- `GET /api/revisions?slug=&limit=` — wrapper `listRevisions`.
|
|
||||||
- `GET /api/queue/status` — wrapper `getQueue().get*Count()` (admin-only atau public read-only).
|
|
||||||
- (Opsional) `GET /api/tags` — agregasi tags dari `listDocuments` untuk TagCloud.
|
|
||||||
|
|
||||||
Semua endpoint return envelope sama: `{data}` atau `{results}` + `{error}` on 500, dengan `Content-Type: application/json`. Auth untuk mutasi tetap `x-webhook-secret` atau cookie `mcpedia_admin`.
|
|
||||||
|
|
||||||
## 5. Perubahan Per-File (Rincian)
|
|
||||||
|
|
||||||
### Phase 0 — Foundation (wajib)
|
|
||||||
- `apps/web/package.json`: tambah `swr: ^2.3.x` (latest 2.x). Tidak tambah TanStack. `bun install` + cek `typecheck`.
|
|
||||||
- `apps/web/app/lib/fetcher.ts` (baru): `export const fetcher = <T>(url: string): Promise<T> => fetch(url).then(r => { if(!r.ok) throw ...; return r.json() })`
|
|
||||||
- `apps/web/app/lib/swr.tsx` (baru): `SWRProvider` client component wrapping `SWRConfig`.
|
|
||||||
- `apps/web/app/lib/api.ts` (baru): typed helpers `searchDocs(q,mode,limit)`, `listDocs()`, `listSections()` — dipakai hooks.
|
|
||||||
- `apps/web/app/layout.tsx`: bungkus children dengan `<SWRProvider>` (client boundary minimal).
|
|
||||||
|
|
||||||
### Phase 1 — Hooks + Refactor Existing Pages
|
|
||||||
- `hooks/useSections.ts`, `useDocuments.ts`, `useSearch.ts`: ganti `useEffect+fetch` di `Header.tsx`, `Sidebar.tsx`, `search/page.tsx`, `CommandMenu.tsx`, `page.tsx` (home).
|
|
||||||
- `Header.tsx`: `useSections()` alih-alih `useState+useEffect fetch /api/sections`.
|
|
||||||
- `Sidebar.tsx`: `useDocuments()` + `useSections()` + filter memo — hilangkan `useEffect fetch /api/docs`.
|
|
||||||
- `search/page.tsx`: `useSearch(q, mode)` — hapus `handleSearch` manual, pakai `isLoading`, `isValidating`, `error` dari SWR. Mode & section filter tetap local state, tapi key SWR mencakup keduanya.
|
|
||||||
- `CommandMenu.tsx`: `useSearch(query)` debounced — hapus timer manual.
|
|
||||||
- `app/page.tsx`: tetap RSC untuk SEO (listDocuments/listSections server), tapi `SectionTree` dan `Recently Updated` bisa jadi client islands yang pakai SWR untuk live update (opsional: keep RSC + `mutate` on focus).
|
|
||||||
- `[section]/page.tsx` & `[section]/[...slug]/page.tsx`: RSC hero + Markdown tetap, `related` & `revisions` pindah ke client islands `RelatedGrid` & `RevisionList` pakai `useRelated`/`useRevisions`.
|
|
||||||
|
|
||||||
### Phase 2 — Fitur Baru (detail)
|
|
||||||
1. **Dashboard `/dashboard` (baru, admin-aware):**
|
|
||||||
- Cards: total docs, sections, queue (waiting/active/failed), embedding provider status.
|
|
||||||
- Section breakdown bar (docCount per section).
|
|
||||||
- Tag cloud (top 20 tags dengan count).
|
|
||||||
- Recent activity (last 10 updated docs).
|
|
||||||
- Queue status poll 5s via `useQueueStatus` (hanya jika admin cookie ada, else hide).
|
|
||||||
2. **Tag Explorer `/tags` atau sidebar widget:**
|
|
||||||
- `GET /api/tags` agregasi, klik tag → `/search?tag=...` atau filter docs.
|
|
||||||
3. **Doc page enhancements:**
|
|
||||||
- `QueueBadge` di hero jika doc sedang di-index (query queue by slug).
|
|
||||||
- Optimistic revision restore: `mutate('/api/revisions?slug=...')` setelah POST restore.
|
|
||||||
- Related docs pakai SWR + `keepPreviousData`.
|
|
||||||
4. **Search enhancements:**
|
|
||||||
- Persist `mode` di URL (`?q=&mode=hybrid`) — sudah ada, pertahankan + SWR key sync.
|
|
||||||
- Highlight snippet `mark` sudah ada — pertahankan.
|
|
||||||
- Empty & error states pakai `EmptyState`/`ErrorState` components.
|
|
||||||
5. **Create/Edit:**
|
|
||||||
- Setelah `POST /api/docs`, `mutate('/api/docs')` + `mutate('/api/sections')` + `router.push('/${slug}')`.
|
|
||||||
- Custom fields (extraFields) tetap flat → `splitPayload` di BE tidak berubah.
|
|
||||||
6. **Export:**
|
|
||||||
- `PdfExportView` tetap, tapi tambah SWR preload `getExportDocuments` untuk preview count sebelum export.
|
|
||||||
|
|
||||||
### Phase 3 — Polish & Cleanup
|
|
||||||
- Hapus semua `fetch` ad-hoc yang tersisa — semua lewat hooks.
|
|
||||||
- Pastikan `globals.css` tokens dipakai konsisten (tidak ada hard-coded hex baru).
|
|
||||||
- `CommandMenu` jadi satu-satunya omnisearch entry — `Header` search icon di mobile buka CommandMenu, bukan link `/search`.
|
|
||||||
- A11y: `aria-live` untuk search results count, `role="status"` untuk queue badge.
|
|
||||||
- Print/PDF styles sudah ada — tidak diubah.
|
|
||||||
|
|
||||||
## 6. File yang Disentuh (Checklist)
|
|
||||||
|
|
||||||
Baru:
|
|
||||||
- `apps/web/app/lib/fetcher.ts`
|
|
||||||
- `apps/web/app/lib/swr.tsx`
|
|
||||||
- `apps/web/app/lib/api.ts`
|
|
||||||
- `apps/web/app/hooks/useSections.ts`
|
|
||||||
- `apps/web/app/hooks/useDocuments.ts`
|
|
||||||
- `apps/web/app/hooks/useSearch.ts`
|
|
||||||
- `apps/web/app/hooks/useRelated.ts`
|
|
||||||
- `apps/web/app/hooks/useRevisions.ts`
|
|
||||||
- `apps/web/app/hooks/useQueueStatus.ts`
|
|
||||||
- `apps/web/app/hooks/useTags.ts` (opsional)
|
|
||||||
- `apps/web/app/components/SearchBar.tsx`
|
|
||||||
- `apps/web/app/components/SectionCard.tsx`
|
|
||||||
- `apps/web/app/components/DocumentCard.tsx`
|
|
||||||
- `apps/web/app/components/TagCloud.tsx`
|
|
||||||
- `apps/web/app/components/QueueBadge.tsx`
|
|
||||||
- `apps/web/app/components/EmptyState.tsx`
|
|
||||||
- `apps/web/app/dashboard/page.tsx` (baru)
|
|
||||||
- `apps/web/app/api/docs/[slug]/route.ts` (baru)
|
|
||||||
- `apps/web/app/api/related/route.ts` (baru)
|
|
||||||
- `apps/web/app/api/revisions/route.ts` (baru GET)
|
|
||||||
- `apps/web/app/api/queue/status/route.ts` (baru)
|
|
||||||
- `apps/web/app/api/tags/route.ts` (baru, opsional)
|
|
||||||
|
|
||||||
Edit:
|
|
||||||
- `apps/web/package.json` (+ swr)
|
|
||||||
- `apps/web/app/layout.tsx` (+ SWRProvider)
|
|
||||||
- `apps/web/app/page.tsx` (extract SectionCard, optional SWR islands)
|
|
||||||
- `apps/web/app/[section]/page.tsx` (DocumentCard)
|
|
||||||
- `apps/web/app/[section]/[...slug]/page.tsx` (related/revisions islands)
|
|
||||||
- `apps/web/app/search/page.tsx` (full SWR)
|
|
||||||
- `apps/web/app/components/Header.tsx` (useSections)
|
|
||||||
- `apps/web/app/components/Sidebar.tsx` (useDocuments/useSections)
|
|
||||||
- `apps/web/app/components/CommandMenu.tsx` (useSearch)
|
|
||||||
- `apps/web/app/create/page.tsx` (mutate)
|
|
||||||
- `apps/web/app/components/DocForm.tsx` (mutate setelah submit)
|
|
||||||
|
|
||||||
Tidak disentuh:
|
|
||||||
- `packages/core`, `packages/types`, `packages/config`, `packages/db` (hanya dibaca)
|
|
||||||
- `apps/api`, `apps/mcp`, `apps/worker` (hanya diproxy)
|
|
||||||
- `globals.css`, `next.config.ts`, `tailwind` config (kecuali perlu token baru)
|
|
||||||
|
|
||||||
## 7. Urutan Implementasi
|
|
||||||
|
|
||||||
1. Phase 0 foundation → `bun install && bun run typecheck` green.
|
|
||||||
2. Phase 1 hooks + Header/Sidebar/Search/CommandMenu → manual test + `next build` smoke.
|
|
||||||
3. Phase 2 doc page islands + create mutate → test CRUD flow dengan `x-webhook-secret` / admin cookie.
|
|
||||||
4. Phase 2 dashboard + tags → behind `canEdit` guard jika perlu.
|
|
||||||
5. Phase 3 cleanup, hapus fetch manual, final `bun run lint && typecheck && build`.
|
|
||||||
|
|
||||||
## 8. Verifikasi
|
|
||||||
|
|
||||||
- `bun run typecheck` — tidak ada error (SWR types + @mcpedia/types).
|
|
||||||
- `bun run lint` — rules Next 16 ok.
|
|
||||||
- `bun --cwd apps/web run build` — semua route `force-dynamic` OK, tidak ada import-time DB throw.
|
|
||||||
- Manual:
|
|
||||||
- Sidebar filter + section count live setelah create doc.
|
|
||||||
- Search mode switch (hybrid/keyword/semantic) cache terpisah, tidak flicker.
|
|
||||||
- CommandMenu ⌘K debounce 180ms, Enter navigasi benar.
|
|
||||||
- Doc related & revisions muncul via SWR tanpa hard refresh.
|
|
||||||
- Queue badge (jika ada) poll 5s, tidak spam (dedupingInterval).
|
|
||||||
- Dark/light toggle tetap work (SWRProvider tidak break `ThemeToggle`).
|
|
||||||
- Staging: `curl /api/search?q=test&mode=hybrid`, `/api/sections`, `/api/docs` return shape konsisten dengan tRPC.
|
|
||||||
|
|
||||||
## 9. Risiko & Mitigasi
|
|
||||||
|
|
||||||
- RSC + SWR double fetch: mitigasi dengan `fallbackData` dari RSC props ke SWR hook di islands (opsi, tidak wajib v1).
|
|
||||||
- Peningkatan request ke DB: SWR deduping 4s + `revalidateOnFocus: false` untuk search, `true` untuk sections/docs.
|
|
||||||
- Auth untuk `/api/queue/status`: jadikan public read-only (hanya counts, bukan detail job) — aman.
|
|
||||||
|
|
||||||
## 10. Keputusan yang Perlu Kamu Konfirmasi
|
|
||||||
|
|
||||||
1. **Dashboard `/dashboard` perlu tidak?** Atau cukup queue badge di existing pages? Rekomendasi: bikin, tapi Phase 2 — bisa ditunda.
|
|
||||||
2. **Endpoint baru (`/api/docs/[slug]`, `/api/related`, `/api/revisions` GET, `/api/queue/status`, `/api/tags`) setuju?** Alternatif: FE langsung pakai tRPC client (`@trpc/client`) — tapi spec ini pilih REST `/api/*` supaya Next cache & SWR fetcher simpel.
|
|
||||||
3. **SWR vs tRPC+SWR?** Spec ini pakai SWR+REST (paling simpel, tidak tambah `@trpc/client` di FE). Jika mau tRPC, tambah `createTRPCClient` + `httpBatchLink` — tradeoff: lebih typed tapi lebih berat. Default: SWR+REST.
|
|
||||||
4. **Keep RSC server fetch untuk SEO atau full client SWR?** Rekomendasi: keep RSC untuk initial HTML (SEO), SWR untuk live revalidation di client islands — hybrid, bukan full SWR.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Jawab: `Lanjut Phase 0+1` untuk eksekusi, atau beri koreksi poin 10.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# MCPedia — Nix-based CI/CD Migration Plan
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Migrate from tarball+SSH deploy to Nix-native build-and-deploy (like GMW).
|
|
||||||
CI builds with Nix in GitHub Actions → `nix copy` closure to VPS →
|
|
||||||
`nix-env --set` + `systemctl restart` on VPS. VPS never builds.
|
|
||||||
|
|
||||||
## Services to migrate
|
|
||||||
| Service | Build | Runtime | systemd unit |
|
|
||||||
|---------|-------|---------|-------------|
|
|
||||||
| web (Next.js) | `bun run build` → `.next` | `next start` | `mcpedia-web` |
|
|
||||||
| api (Hono) | `bun build src/index.ts` → `dist/index.js` | `bun run src/index.ts` | `mcpedia-api` |
|
|
||||||
| mcp (MCP server) | `bun build src/http.ts` → `dist/http.js` | `bun run src/http.ts` | `mcpedia-mcp` |
|
|
||||||
| worker (BullMQ) | `bun build src/index.ts` → `dist/index.js` | `bun run src/index.ts` | `mcpedia-worker` |
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
### 1. flake.nix (new)
|
|
||||||
- `nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"` (Bun >=1.3)
|
|
||||||
- 4 packages: `web`, `api`, `mcp`, `worker`
|
|
||||||
- Each: `mkDerivation` with `bun` as nativeBuildInput, `sandbox=false`
|
|
||||||
- Filter source to exclude `.next`, `node_modules`, `.git`
|
|
||||||
|
|
||||||
### 2. CI workflow (.github/workflows/ci.yml — rewrite)
|
|
||||||
- `test` job: typecheck + test (Bun, as before)
|
|
||||||
- `build-and-deploy` job: matrix [web, api, mcp, worker]
|
|
||||||
- `DeterminateSystems/nix-installer-action@v16` with `determinate: false`, `sandbox=false`
|
|
||||||
- `DeterminateSystems/magic-nix-cache-action@v14` with `use-flakehub: false`
|
|
||||||
- `nix build .#<service>` → store path
|
|
||||||
- SSH setup (key sanitization)
|
|
||||||
- `nix copy --to ssh://...`
|
|
||||||
- `nix-env --profile /nix/var/nix/profiles/mcpedia-<service> --set <path>`
|
|
||||||
- `systemctl restart mcpedia-<service>`
|
|
||||||
|
|
||||||
### 3. systemd units (update on VPS)
|
|
||||||
- Change `ExecStart` from `bun run` to Nix profile binary path
|
|
||||||
- Profile path: `/nix/var/nix/profiles/mcpedia-web/bin/mcpedia-web`
|
|
||||||
- Keep `EnvironmentFile` pointing to `.env`
|
|
||||||
- Keep `Restart=always`
|
|
||||||
|
|
||||||
### 4. Deploy secrets
|
|
||||||
- `SSH_PRIVATE_KEY` (already exists as `SSH_DEPLOY_KEY`)
|
|
||||||
- `VPS_HOST`, `VPS_USER`, `VPS_SSH_PORT`
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
- `nix build .#web --impure` builds
|
|
||||||
- `gh run list` shows all 4 services `completed success`
|
|
||||||
- `curl https://wiki.asepharyana.my.id/` returns 200
|
|
||||||
- `journalctl -u mcpedia-web` shows Next.js started
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"@mcpedia/queue": "workspace:*",
|
"@mcpedia/queue": "workspace:*",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
"hono": "^4.6.0",
|
"hono": "^4.6.0",
|
||||||
"zod": "^4.5.4"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { getDocument, listDocuments, getExportDocuments } from "@mcpedia/core";
|
import { getDocument, getRelated, listRevisions, listDocuments, getExportDocuments } from "@mcpedia/core";
|
||||||
import { WEBHOOK_SECRET } from "@mcpedia/config";
|
import { WEBHOOK_SECRET } from "@mcpedia/config";
|
||||||
import Markdown from "@/components/Markdown";
|
import Markdown from "@/components/Markdown";
|
||||||
import DocForm from "@/components/DocForm";
|
import DocForm from "@/components/DocForm";
|
||||||
import TOC from "@/components/TOC";
|
import TOC from "@/components/TOC";
|
||||||
import DocActions from "@/components/DocActions";
|
import DocActions from "@/components/DocActions";
|
||||||
import PdfExportView from "@/components/PdfExportView";
|
import PdfExportView from "@/components/PdfExportView";
|
||||||
import RelatedGrid from "@/components/RelatedGrid";
|
|
||||||
import RevisionList from "@/components/RevisionList";
|
|
||||||
import { classifyPath, extractFoldersForSection } from "@mcpedia/core";
|
import { classifyPath, extractFoldersForSection } from "@mcpedia/core";
|
||||||
import { getSectionMeta } from "@mcpedia/config";
|
import { getSectionMeta } from "@mcpedia/config";
|
||||||
import type { DocumentMeta } from "@mcpedia/core";
|
import type { DocumentMeta } from "@mcpedia/core";
|
||||||
@@ -17,8 +15,10 @@ import {
|
|||||||
Folder,
|
Folder,
|
||||||
FileText,
|
FileText,
|
||||||
Clock,
|
Clock,
|
||||||
|
History,
|
||||||
FileDown,
|
FileDown,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
Sparkles,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Target,
|
Target,
|
||||||
@@ -382,6 +382,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const related = await getRelated(fullSlug, 4);
|
||||||
|
const revisions = await listRevisions(fullSlug, 10);
|
||||||
const sectionInfo = getSectionInfo(doc.section);
|
const sectionInfo = getSectionInfo(doc.section);
|
||||||
const readingTime = calculateReadingTime(doc.body);
|
const readingTime = calculateReadingTime(doc.body);
|
||||||
|
|
||||||
@@ -495,8 +497,89 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
<Markdown source={doc.body} />
|
<Markdown source={doc.body} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RelatedGrid slug={fullSlug} />
|
{/* Related Documents Grid */}
|
||||||
<RevisionList slug={fullSlug} canEdit={canEdit} />
|
{related.length > 0 && (
|
||||||
|
<aside className="mt-12 pt-8 border-t border-[var(--border-color)]">
|
||||||
|
<div className="flex items-center gap-2 mb-3.5">
|
||||||
|
<Sparkles className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Related Knowledge
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{related.map((r) => {
|
||||||
|
const rInfo = getSectionInfo(r.section);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={r.slug}
|
||||||
|
href={`/${r.slug}`}
|
||||||
|
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg p-4 transition-colors shadow-xs"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2 mb-1.5">
|
||||||
|
<span className="text-[10px] text-[var(--text-muted)] font-mono uppercase font-semibold">
|
||||||
|
{rInfo.icon} {r.section}
|
||||||
|
</span>
|
||||||
|
<time className="text-[11px] text-[var(--text-dim)] font-mono">
|
||||||
|
{new Date(r.updatedAt).toLocaleDateString(undefined, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xs sm:text-sm font-semibold text-[var(--text-primary)] group-hover:underline line-clamp-1">
|
||||||
|
{r.title}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Revision History */}
|
||||||
|
{revisions.length > 0 && (
|
||||||
|
<aside className="mt-10 pt-8 border-t border-[var(--border-color)]">
|
||||||
|
<div className="flex items-center justify-between mb-3.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<History className="w-4 h-4 text-[var(--text-muted)]" />
|
||||||
|
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
|
||||||
|
Revision History ({revisions.length})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{revisions.map((rev) => (
|
||||||
|
<div
|
||||||
|
key={rev.id}
|
||||||
|
className="flex items-center justify-between gap-3 p-3 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-xs shadow-xs"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0 font-mono">
|
||||||
|
<span className="text-[var(--text-primary)] bg-[var(--bg-elevated)] px-1.5 py-0.5 rounded border border-[var(--border-color)] font-bold text-[10px]">
|
||||||
|
v{rev.revisionNo}
|
||||||
|
</span>
|
||||||
|
<span className="text-[var(--text-secondary)] font-sans truncate text-xs">
|
||||||
|
{rev.reason || "Updated document in PostgreSQL"}
|
||||||
|
</span>
|
||||||
|
<span className="text-[var(--text-dim)] hidden sm:inline text-[11px]">
|
||||||
|
· {new Date(rev.createdAt).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{canEdit && (
|
||||||
|
<form action="/api/revisions/restore" method="post">
|
||||||
|
<input type="hidden" name="id" value={rev.id} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="text-xs text-[var(--text-primary)] hover:text-black dark:hover:text-white bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] transition-colors font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{/* Desktop Sticky Table of Contents Column */}
|
{/* Desktop Sticky Table of Contents Column */}
|
||||||
|
|||||||
@@ -78,15 +78,8 @@ function splitPayload(body: Record<string, unknown>): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/docs — list all documents (for sidebar navigation).
|
// GET /api/docs — list all documents (for sidebar navigation).
|
||||||
// Supports ?section=&status= filters.
|
export async function GET() {
|
||||||
export async function GET(req: NextRequest) {
|
const docs = await listDocuments();
|
||||||
const { searchParams } = new URL(req.url);
|
|
||||||
const section = searchParams.get("section") ?? undefined;
|
|
||||||
const status = searchParams.get("status") ?? undefined;
|
|
||||||
const docs = await listDocuments({
|
|
||||||
...(section ? { section } : {}),
|
|
||||||
...(status ? { status } : {}),
|
|
||||||
});
|
|
||||||
return NextResponse.json(docs);
|
return NextResponse.json(docs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
// GET /api/queue/status — BullMQ queue counts (read-only, public)
|
|
||||||
// Falls back gracefully when Redis is unavailable so the UI never breaks.
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const { getQueue } = await import("@mcpedia/queue");
|
|
||||||
const q = getQueue();
|
|
||||||
const [waiting, active, completed, failed, delayed] = await Promise.all([
|
|
||||||
q.getWaitingCount(),
|
|
||||||
q.getActiveCount(),
|
|
||||||
q.getCompletedCount(),
|
|
||||||
q.getFailedCount(),
|
|
||||||
q.getDelayedCount(),
|
|
||||||
]);
|
|
||||||
return NextResponse.json({ waiting, active, completed, failed, delayed });
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
// Return zeroed status so the FE can render gracefully
|
|
||||||
return NextResponse.json(
|
|
||||||
{ waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, error: msg },
|
|
||||||
{ status: 200 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { getRelated } from "@mcpedia/core";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
// GET /api/related?slug=...&limit=5
|
|
||||||
export async function GET(req: NextRequest) {
|
|
||||||
const slug = new URL(req.url).searchParams.get("slug") ?? "";
|
|
||||||
const limit = Math.min(Math.max(Number(new URL(req.url).searchParams.get("limit") ?? 5), 1), 20);
|
|
||||||
if (!slug.trim()) {
|
|
||||||
return NextResponse.json({ results: [] });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const results = await getRelated(slug, limit);
|
|
||||||
return NextResponse.json({ results });
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
return NextResponse.json({ results: [], error: msg }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { listRevisions } from "@mcpedia/core";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
// GET /api/revisions?slug=...&limit=10
|
|
||||||
export async function GET(req: NextRequest) {
|
|
||||||
const { searchParams } = new URL(req.url);
|
|
||||||
const slug = searchParams.get("slug") ?? "";
|
|
||||||
const limit = Math.min(Math.max(Number(searchParams.get("limit") ?? 10), 1), 50);
|
|
||||||
if (!slug.trim()) {
|
|
||||||
return NextResponse.json({ revisions: [] });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const revisions = await listRevisions(slug, limit);
|
|
||||||
return NextResponse.json({ revisions });
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
return NextResponse.json({ revisions: [], error: msg }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { listDocuments } from "@mcpedia/core";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
// GET /api/tags?limit=30 — aggregated tag counts
|
|
||||||
export async function GET(req: Request) {
|
|
||||||
const limit = Math.min(Math.max(Number(new URL(req.url).searchParams.get("limit") ?? 30), 1), 100);
|
|
||||||
try {
|
|
||||||
const docs = await listDocuments();
|
|
||||||
const counts = new Map<string, number>();
|
|
||||||
for (const d of docs) {
|
|
||||||
for (const t of d.tags ?? []) {
|
|
||||||
const tag = String(t).trim();
|
|
||||||
if (!tag) continue;
|
|
||||||
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tags = [...counts.entries()]
|
|
||||||
.map(([tag, count]) => ({ tag, count }))
|
|
||||||
.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag))
|
|
||||||
.slice(0, limit);
|
|
||||||
return NextResponse.json({ tags });
|
|
||||||
} catch (e) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
return NextResponse.json({ tags: [], error: msg }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSearch } from "@/hooks/useSearch";
|
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -11,35 +10,74 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
Flag,
|
Flag,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface Hit {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
section: string;
|
||||||
|
snippet?: string;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface QuickAction {
|
interface QuickAction {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: any;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
action?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CommandMenu() {
|
export default function CommandMenu() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const [results, setResults] = useState<Hit[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const { hits, isSearching, isValidating } = useSearch(query, "hybrid", 10);
|
|
||||||
const results = query.trim() ? hits : [];
|
|
||||||
const loading = isSearching || isValidating;
|
|
||||||
|
|
||||||
const quickActions: QuickAction[] = [
|
const quickActions: QuickAction[] = [
|
||||||
{ id: "create-doc", title: "Create New Document", subtitle: "Write technical doc, writeup, or note", icon: Plus, href: "/create" },
|
{
|
||||||
{ id: "hybrid-search", title: "Multi-Modal Hybrid Search", subtitle: "Full-text + Semantic vector search console", icon: Search, href: "/search" },
|
id: "create-doc",
|
||||||
{ id: "browse-docs", title: "Documentation Hub", subtitle: "System setup, API references, protocol specs", icon: BookOpen, href: "/docs" },
|
title: "Create New Document",
|
||||||
{ id: "browse-writeups", title: "CTF & Writeups Directory", subtitle: "Pwn, Reverse, Web, Crypto writeups and solutions", icon: Flag, href: "/writeups" },
|
subtitle: "Write technical doc, writeup, or note",
|
||||||
{ id: "browse-research", title: "Research & Benchmarks", subtitle: "Architecture notes, benchmarks, experiments", icon: Sparkles, href: "/research" },
|
icon: Plus,
|
||||||
|
href: "/create",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hybrid-search",
|
||||||
|
title: "Multi-Modal Hybrid Search",
|
||||||
|
subtitle: "Full-text + Semantic vector search console",
|
||||||
|
icon: Search,
|
||||||
|
href: "/search",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browse-docs",
|
||||||
|
title: "Documentation Hub",
|
||||||
|
subtitle: "System setup, API references, protocol specs",
|
||||||
|
icon: BookOpen,
|
||||||
|
href: "/docs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browse-writeups",
|
||||||
|
title: "CTF & Writeups Directory",
|
||||||
|
subtitle: "Pwn, Reverse, Web, Crypto writeups and solutions",
|
||||||
|
icon: Flag,
|
||||||
|
href: "/writeups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "browse-research",
|
||||||
|
title: "Research & Benchmarks",
|
||||||
|
subtitle: "Architecture notes, benchmarks, experiments",
|
||||||
|
icon: Sparkles,
|
||||||
|
href: "/research",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Global shortcut ⌘K or /
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
||||||
@@ -60,12 +98,31 @@ export default function CommandMenu() {
|
|||||||
if (open) {
|
if (open) {
|
||||||
setTimeout(() => inputRef.current?.focus(), 50);
|
setTimeout(() => inputRef.current?.focus(), 50);
|
||||||
setQuery("");
|
setQuery("");
|
||||||
|
setResults([]);
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// Debounced search
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!query.trim()) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=10`);
|
||||||
|
const data = await res.json();
|
||||||
|
setResults(data.results || []);
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
|
} catch {
|
||||||
|
setResults([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, 120);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
function handleNavigate(href: string) {
|
function handleNavigate(href: string) {
|
||||||
@@ -101,15 +158,24 @@ export default function CommandMenu() {
|
|||||||
>
|
>
|
||||||
<Search className="w-3.5 h-3.5 text-[var(--text-dim)] group-hover:text-[var(--text-primary)] transition-colors" />
|
<Search className="w-3.5 h-3.5 text-[var(--text-dim)] group-hover:text-[var(--text-primary)] transition-colors" />
|
||||||
<span className="font-sans">Search...</span>
|
<span className="font-sans">Search...</span>
|
||||||
<kbd className="ml-2 font-mono bg-[var(--bg-surface)] px-1.5 py-0.2 rounded border border-[var(--border-color)] text-[10px] text-[var(--text-muted)]">⌘K</kbd>
|
<kbd className="ml-2 font-mono bg-[var(--bg-surface)] px-1.5 py-0.2 rounded border border-[var(--border-color)] text-[10px] text-[var(--text-muted)]">
|
||||||
|
⌘K
|
||||||
|
</kbd>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
|
<div className="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
|
||||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs transition-opacity" onClick={() => setOpen(false)} />
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/60 backdrop-blur-xs transition-opacity"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
<div className="relative w-full max-w-2xl bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-2xl overflow-hidden animate-fade-in">
|
<div className="relative w-full max-w-2xl bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-2xl overflow-hidden animate-fade-in">
|
||||||
|
{/* Search input bar */}
|
||||||
<div className="flex items-center gap-3 px-4 sm:px-5 py-3.5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
|
<div className="flex items-center gap-3 px-4 sm:px-5 py-3.5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
|
||||||
<Search className="w-4 h-4 text-[var(--text-muted)] shrink-0" />
|
<Search className="w-4 h-4 text-[var(--text-muted)] shrink-0" />
|
||||||
<input
|
<input
|
||||||
@@ -121,14 +187,25 @@ export default function CommandMenu() {
|
|||||||
placeholder="Search documents, writeups, concepts, or tools..."
|
placeholder="Search documents, writeups, concepts, or tools..."
|
||||||
className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm focus:outline-none"
|
className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm focus:outline-none"
|
||||||
/>
|
/>
|
||||||
{loading && <div className="w-4 h-4 border-2 border-[var(--text-muted)] border-t-transparent rounded-full animate-spin shrink-0" />}
|
{loading && (
|
||||||
<button onClick={() => setOpen(false)} type="button" className="text-[10px] font-mono text-[var(--text-dim)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)]">ESC</button>
|
<div className="w-4 h-4 border-2 border-[var(--text-muted)] border-t-transparent rounded-full animate-spin shrink-0" />
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
type="button"
|
||||||
|
className="text-[10px] font-mono text-[var(--text-dim)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)]"
|
||||||
|
>
|
||||||
|
ESC
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Results / Quick Actions List */}
|
||||||
<div className="max-h-[22rem] overflow-y-auto p-2">
|
<div className="max-h-[22rem] overflow-y-auto p-2">
|
||||||
{query.trim() === "" ? (
|
{query.trim() === "" ? (
|
||||||
<div className="p-1 space-y-0.5">
|
<div className="p-1 space-y-0.5">
|
||||||
<div className="px-2.5 py-1 text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">Quick Shortcuts</div>
|
<div className="px-2.5 py-1 text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">
|
||||||
|
Quick Shortcuts
|
||||||
|
</div>
|
||||||
{quickActions.map((action, idx) => {
|
{quickActions.map((action, idx) => {
|
||||||
const Icon = action.icon;
|
const Icon = action.icon;
|
||||||
const isSelected = idx === selectedIndex;
|
const isSelected = idx === selectedIndex;
|
||||||
@@ -137,15 +214,23 @@ export default function CommandMenu() {
|
|||||||
key={action.id}
|
key={action.id}
|
||||||
onClick={() => action.href && handleNavigate(action.href)}
|
onClick={() => action.href && handleNavigate(action.href)}
|
||||||
onMouseEnter={() => setSelectedIndex(idx)}
|
onMouseEnter={() => setSelectedIndex(idx)}
|
||||||
className={`flex items-center justify-between gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors ${isSelected ? "bg-[var(--bg-elevated)] text-[var(--text-primary)]" : "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)]"}`}
|
className={`flex items-center justify-between gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--bg-elevated)] text-[var(--text-primary)]"
|
||||||
|
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)]"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className={`p-1.5 rounded ${isSelected ? "bg-[var(--brand)] text-[var(--brand-fg)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
|
<div className={`p-1.5 rounded ${isSelected ? "bg-[var(--brand)] text-[var(--brand-fg)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
|
||||||
<Icon className="w-3.5 h-3.5" />
|
<Icon className="w-3.5 h-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-semibold text-[var(--text-primary)]">{action.title}</div>
|
<div className="text-xs font-semibold text-[var(--text-primary)]">
|
||||||
<div className="text-[11px] text-[var(--text-muted)]">{action.subtitle}</div>
|
{action.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-[var(--text-muted)]">
|
||||||
|
{action.subtitle}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ArrowRight className={`w-3.5 h-3.5 text-[var(--text-dim)] ${isSelected ? "text-[var(--text-primary)]" : ""}`} />
|
<ArrowRight className={`w-3.5 h-3.5 text-[var(--text-dim)] ${isSelected ? "text-[var(--text-primary)]" : ""}`} />
|
||||||
@@ -166,22 +251,31 @@ export default function CommandMenu() {
|
|||||||
key={hit.slug}
|
key={hit.slug}
|
||||||
onClick={() => handleNavigate(`/${hit.slug}`)}
|
onClick={() => handleNavigate(`/${hit.slug}`)}
|
||||||
onMouseEnter={() => setSelectedIndex(index)}
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors ${isSelected ? "bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)]" : "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent"}`}
|
className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? "bg-[var(--bg-elevated)] text-[var(--text-primary)] border border-[var(--border-color)]"
|
||||||
|
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2 mb-0.5">
|
<div className="flex items-center gap-2 mb-0.5">
|
||||||
<span className="text-[10px] font-mono px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] uppercase font-semibold">{hit.section}</span>
|
<span className="text-[10px] font-mono px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] uppercase font-semibold">
|
||||||
<span className="text-xs sm:text-sm font-semibold text-[var(--text-primary)] truncate">{hit.title}</span>
|
{hit.section}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs sm:text-sm font-semibold ${isSelected ? "text-[var(--text-primary)]" : "text-[var(--text-primary)]"} truncate`}>
|
||||||
|
{hit.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{hit.snippet && (
|
{hit.snippet && (
|
||||||
<p
|
<p
|
||||||
className="text-xs text-[var(--text-muted)] line-clamp-1 mt-1 [&>mark]:bg-[var(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
className="text-xs text-[var(--text-muted)] line-clamp-1 mt-1 [&>mark]:bg-[var(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
||||||
// snippet is server-sanitized ts_headline output
|
|
||||||
dangerouslySetInnerHTML={{ __html: hit.snippet }}
|
dangerouslySetInnerHTML={{ __html: hit.snippet }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] font-mono text-[var(--text-dim)] shrink-0 self-center">/{hit.slug}</span>
|
<span className="text-[10px] font-mono text-[var(--text-dim)] shrink-0 self-center">
|
||||||
|
/{hit.slug}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -189,9 +283,20 @@ export default function CommandMenu() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
<div className="flex items-center justify-between px-4 py-2 border-t border-[var(--border-color)] bg-[var(--bg-elevated)] text-[10px] text-[var(--text-muted)] font-mono">
|
<div className="flex items-center justify-between px-4 py-2 border-t border-[var(--border-color)] bg-[var(--bg-elevated)] text-[10px] text-[var(--text-muted)] font-mono">
|
||||||
<div className="flex items-center gap-2"><span>[ ↑↓ NAVIGATE ]</span><span>[ ↵ SELECT ]</span></div>
|
<div className="flex items-center gap-2">
|
||||||
<Link href={`/search?q=${encodeURIComponent(query)}`} onClick={() => setOpen(false)} className="hover:text-[var(--text-primary)] font-medium flex items-center gap-1"><span>Full Search Console</span><ArrowRight className="w-3 h-3" /></Link>
|
<span>[ ↑↓ NAVIGATE ]</span>
|
||||||
|
<span>[ ↵ SELECT ]</span>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={`/search?q=${encodeURIComponent(query)}`}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="hover:text-[var(--text-primary)] font-medium flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span>Full Search Console</span>
|
||||||
|
<ArrowRight className="w-3 h-3" />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { mutate } from "swr";
|
|
||||||
import Markdown from "./Markdown";
|
import Markdown from "./Markdown";
|
||||||
import {
|
import {
|
||||||
Bold,
|
Bold,
|
||||||
@@ -211,12 +210,7 @@ export default function DocForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
// Invalidate SWR caches so Sidebar/Header/search reflect the change immediately
|
|
||||||
mutate("/api/docs");
|
|
||||||
mutate("/api/sections");
|
|
||||||
mutate((key) => typeof key === "string" && key.startsWith("/api/tags"));
|
|
||||||
router.push(result.doc?.slug ? `/${result.doc.slug}` : "/");
|
router.push(result.doc?.slug ? `/${result.doc.slug}` : "/");
|
||||||
router.refresh();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { Clock, FileText } from "lucide-react";
|
|
||||||
import type { DocumentMeta } from "@mcpedia/types";
|
|
||||||
import { getSectionMeta } from "@mcpedia/config/sections";
|
|
||||||
|
|
||||||
export function DocumentCard({
|
|
||||||
doc,
|
|
||||||
showSection = false,
|
|
||||||
score,
|
|
||||||
}: {
|
|
||||||
doc: DocumentMeta;
|
|
||||||
showSection?: boolean;
|
|
||||||
score?: number;
|
|
||||||
}) {
|
|
||||||
const sInfo = getSectionMeta(doc.section);
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={`/${doc.slug}`}
|
|
||||||
className="group flex items-center justify-between gap-4 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-xl transition-all shadow-xs"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
<FileText className="w-4 h-4 text-[var(--text-muted)] shrink-0" />
|
|
||||||
<div className="min-w-0">
|
|
||||||
<h3 className="text-xs sm:text-sm font-semibold text-[var(--text-primary)] group-hover:underline truncate">
|
|
||||||
{doc.title}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
|
||||||
{showSection && (
|
|
||||||
<span className="text-[10px] font-mono px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] uppercase font-semibold">
|
|
||||||
{sInfo.icon} {doc.section}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{doc.tags.slice(0, 4).map((t: string) => (
|
|
||||||
<span
|
|
||||||
key={t}
|
|
||||||
className="text-[10px] px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] font-mono"
|
|
||||||
>
|
|
||||||
#{t}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{typeof score === "number" && (
|
|
||||||
<span className="text-[10px] font-mono text-[var(--text-dim)] bg-[var(--bg-elevated)] px-1.5 py-0.2 rounded">
|
|
||||||
{score.toFixed(3)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0 flex items-center gap-1">
|
|
||||||
<Clock className="w-3 h-3" />
|
|
||||||
<span>
|
|
||||||
{new Date(doc.updatedAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })}
|
|
||||||
</span>
|
|
||||||
</time>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CompactDocCard({ doc, snippet, score }: { doc: DocumentMeta; snippet?: string; score?: number }) {
|
|
||||||
const sInfo = getSectionMeta(doc.section);
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={`/${doc.slug}`}
|
|
||||||
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-xl p-4.5 transition-all shadow-xs"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-3 mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-[10px] font-mono px-1.5 py-0.2 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] uppercase font-semibold">
|
|
||||||
{sInfo.icon} {doc.section}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-[var(--text-dim)] font-mono truncate">/{doc.slug}</span>
|
|
||||||
</div>
|
|
||||||
{typeof score === "number" && (
|
|
||||||
<span className="text-xs font-mono text-[var(--text-dim)] bg-[var(--bg-elevated)] px-2 py-0.5 rounded">
|
|
||||||
{score.toFixed(3)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h2 className="text-sm font-semibold text-[var(--text-primary)] group-hover:underline mb-1.5">{doc.title}</h2>
|
|
||||||
{snippet && (
|
|
||||||
<p
|
|
||||||
className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
|
||||||
dangerouslySetInnerHTML={{ __html: snippet }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { FileQuestion, SearchX, AlertCircle, Inbox } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export function EmptyState({
|
|
||||||
title = "No results",
|
|
||||||
description,
|
|
||||||
icon: Icon = Inbox,
|
|
||||||
action,
|
|
||||||
}: {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
icon?: typeof Inbox;
|
|
||||||
action?: { label: string; href: string };
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-12 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-xs space-y-3">
|
|
||||||
<div className="w-10 h-10 rounded-xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center mx-auto text-[var(--text-muted)]">
|
|
||||||
<Icon className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-sm font-bold text-[var(--text-primary)]">{title}</h3>
|
|
||||||
{description && <p className="text-xs text-[var(--text-muted)] max-w-sm mx-auto leading-relaxed">{description}</p>}
|
|
||||||
{action && (
|
|
||||||
<Link
|
|
||||||
href={action.href}
|
|
||||||
className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)] text-[var(--brand-fg)] px-3 py-1.5 rounded-md font-semibold mt-2"
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorState({ message, onRetry }: { message?: string; onRetry?: () => void }) {
|
|
||||||
return (
|
|
||||||
<div className="p-4 rounded-xl border border-amber-500/30 bg-amber-500/10 text-amber-900 dark:text-amber-200 text-xs flex items-start gap-2.5">
|
|
||||||
<AlertCircle className="w-4 h-4 shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="font-semibold">Something went wrong</div>
|
|
||||||
{message && <div className="text-[11px] opacity-80 mt-1 break-words">{message}</div>}
|
|
||||||
</div>
|
|
||||||
{onRetry && (
|
|
||||||
<button
|
|
||||||
onClick={onRetry}
|
|
||||||
type="button"
|
|
||||||
className="shrink-0 px-2.5 py-1 rounded bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-primary)] font-semibold hover:bg-[var(--bg-elevated)]"
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { DEFAULT_SECTIONS } from "@mcpedia/config/sections";
|
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
|
||||||
import { useSections } from "@/hooks/useSections";
|
|
||||||
import ThemeToggle from "@/components/ThemeToggle";
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
import CommandMenu from "@/components/CommandMenu";
|
import CommandMenu from "@/components/CommandMenu";
|
||||||
import {
|
import {
|
||||||
@@ -18,8 +17,18 @@ import {
|
|||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const { data: liveSections } = useSections();
|
const [sections, setSections] = useState<SectionConfig[]>(DEFAULT_SECTIONS);
|
||||||
const sections = liveSections && liveSections.length > 0 ? liveSections : DEFAULT_SECTIONS;
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/sections")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
setSections(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="site-header no-print sticky top-0 z-40 border-b border-[var(--border-color)] glass-nav transition-all">
|
<header className="site-header no-print sticky top-0 z-40 border-b border-[var(--border-color)] glass-nav transition-all">
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
import { Clock, CheckCircle2, AlertTriangle, Hourglass, Layers } from "lucide-react";
|
|
||||||
|
|
||||||
export default function QueueBadge() {
|
|
||||||
const { data, error, isLoading } = useSWR<{ waiting: number; active: number; completed: number; failed: number; delayed: number }>(
|
|
||||||
"/api/queue/status",
|
|
||||||
fetcher,
|
|
||||||
{ refreshInterval: 8_000, dedupingInterval: 4_000, revalidateOnFocus: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1.5 text-[11px] font-mono text-[var(--text-dim)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-2.5 py-1 rounded">
|
|
||||||
<Hourglass className="w-3 h-3 animate-spin" /> queue…
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error || !data) return null;
|
|
||||||
|
|
||||||
const total = data.waiting + data.active + data.delayed;
|
|
||||||
const hasWork = total > 0 || data.failed > 0;
|
|
||||||
|
|
||||||
if (!hasWork) {
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1.5 text-[11px] font-mono text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2.5 py-1 rounded">
|
|
||||||
<CheckCircle2 className="w-3 h-3" /> index idle
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center gap-1.5 text-[11px] font-mono px-2.5 py-1 rounded border ${
|
|
||||||
data.failed > 0
|
|
||||||
? "text-amber-700 dark:text-amber-400 bg-amber-500/10 border-amber-500/30"
|
|
||||||
: "text-[var(--text-secondary)] bg-[var(--bg-elevated)] border-[var(--border-color)]"
|
|
||||||
}`}
|
|
||||||
title={`waiting:${data.waiting} active:${data.active} delayed:${data.delayed} failed:${data.failed}`}
|
|
||||||
role="status"
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{data.failed > 0 ? <AlertTriangle className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
|
|
||||||
<span>
|
|
||||||
q:{data.waiting}w · {data.active}a
|
|
||||||
{data.delayed ? ` · ${data.delayed}d` : ""}
|
|
||||||
{data.failed ? ` · ${data.failed} ✕` : ""}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QueueStrip() {
|
|
||||||
const { data } = useSWR<{ waiting: number; active: number; completed: number; failed: number; delayed: number }>(
|
|
||||||
"/api/queue/status",
|
|
||||||
fetcher,
|
|
||||||
{ refreshInterval: 6_000, revalidateOnFocus: false },
|
|
||||||
);
|
|
||||||
if (!data) return null;
|
|
||||||
const items: { label: string; value: number; tone: string }[] = [
|
|
||||||
{ label: "waiting", value: data.waiting, tone: "text-[var(--text-muted)]" },
|
|
||||||
{ label: "active", value: data.active, tone: "text-sky-600 dark:text-sky-400" },
|
|
||||||
{ label: "delayed", value: data.delayed, tone: "text-[var(--text-muted)]" },
|
|
||||||
{ label: "failed", value: data.failed, tone: data.failed ? "text-amber-600 dark:text-amber-400" : "text-[var(--text-dim)]" },
|
|
||||||
{ label: "completed", value: data.completed, tone: "text-emerald-600 dark:text-emerald-400" },
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-2 text-[11px] font-mono">
|
|
||||||
{items.map((it) => (
|
|
||||||
<span key={it.label} className={`px-2 py-1 rounded bg-[var(--bg-surface)] border border-[var(--border-color)] ${it.tone}`}>
|
|
||||||
{it.label}: <strong>{it.value}</strong>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRelated } from "@/hooks/useRelated";
|
|
||||||
import { getSectionMeta } from "@mcpedia/config/sections";
|
|
||||||
import { Sparkles } from "lucide-react";
|
|
||||||
|
|
||||||
export default function RelatedGrid({ slug }: { slug: string }) {
|
|
||||||
const { related, isLoading, error } = useRelated(slug, 4);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 animate-pulse">
|
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
|
||||||
<div key={i} className="h-24 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error || related.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="mt-12 pt-8 border-t border-[var(--border-color)]">
|
|
||||||
<div className="flex items-center gap-2 mb-3.5">
|
|
||||||
<Sparkles className="w-4 h-4 text-[var(--text-muted)]" />
|
|
||||||
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">Related Knowledge</h2>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
{related.map((r) => {
|
|
||||||
const rInfo = getSectionMeta(r.section);
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={r.slug}
|
|
||||||
href={`/${r.slug}`}
|
|
||||||
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg p-4 transition-colors shadow-xs"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2 mb-1.5">
|
|
||||||
<span className="text-[10px] text-[var(--text-muted)] font-mono uppercase font-semibold">
|
|
||||||
{rInfo.icon} {r.section}
|
|
||||||
</span>
|
|
||||||
<time className="text-[11px] text-[var(--text-dim)] font-mono">
|
|
||||||
{new Date(r.updatedAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xs sm:text-sm font-semibold text-[var(--text-primary)] group-hover:underline line-clamp-1">{r.title}</h3>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { mutate } from "swr";
|
|
||||||
import { useRevisions } from "@/hooks/useRevisions";
|
|
||||||
import { History } from "lucide-react";
|
|
||||||
|
|
||||||
export default function RevisionList({ slug, canEdit }: { slug: string; canEdit: boolean }) {
|
|
||||||
const { revisions, isLoading, error, mutate: revalidate } = useRevisions(slug, 10);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2 animate-pulse">
|
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
|
||||||
<div key={i} className="h-12 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p className="text-xs text-amber-600 dark:text-amber-400">Failed to load revisions: {(error as Error).message}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (revisions.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="mt-10 pt-8 border-t border-[var(--border-color)]">
|
|
||||||
<div className="flex items-center justify-between mb-3.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<History className="w-4 h-4 text-[var(--text-muted)]" />
|
|
||||||
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">Revision History ({revisions.length})</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{revisions.map((rev) => (
|
|
||||||
<div key={rev.id} className="flex items-center justify-between gap-3 p-3 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-xs shadow-xs">
|
|
||||||
<div className="flex items-center gap-2 min-w-0 font-mono">
|
|
||||||
<span className="text-[var(--text-primary)] bg-[var(--bg-elevated)] px-1.5 py-0.5 rounded border border-[var(--border-color)] font-bold text-[10px]">v{rev.revisionNo}</span>
|
|
||||||
<span className="text-[var(--text-secondary)] font-sans truncate text-xs">{rev.reason || "Updated document in PostgreSQL"}</span>
|
|
||||||
<span className="text-[var(--text-dim)] hidden sm:inline text-[11px]">· {new Date(rev.createdAt).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
{canEdit && (
|
|
||||||
<form
|
|
||||||
action="/api/revisions/restore"
|
|
||||||
method="post"
|
|
||||||
onSubmit={() => {
|
|
||||||
// Optimistically revalidate after form post navigates back
|
|
||||||
setTimeout(() => {
|
|
||||||
revalidate();
|
|
||||||
mutate((k) => typeof k === "string" && k.startsWith("/api/docs"));
|
|
||||||
}, 800);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="id" value={rev.id} />
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="text-xs text-[var(--text-primary)] hover:text-black dark:hover:text-white bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] transition-colors font-medium cursor-pointer"
|
|
||||||
>
|
|
||||||
Restore
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Search, X } from "lucide-react";
|
|
||||||
|
|
||||||
interface SearchBarProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (v: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
onClear?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SearchBar({ value, onChange, placeholder, autoFocus, onClear }: SearchBarProps) {
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
placeholder={placeholder ?? "Search knowledge base..."}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
className="w-full pl-10 pr-9 py-3 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--text-primary)] text-sm transition-all"
|
|
||||||
/>
|
|
||||||
<Search className="w-4 h-4 text-[var(--text-muted)] absolute left-3.5 top-3.5 pointer-events-none" />
|
|
||||||
{value && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
onChange("");
|
|
||||||
onClear?.();
|
|
||||||
}}
|
|
||||||
className="absolute right-3 top-3 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded"
|
|
||||||
aria-label="Clear query"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InlineSearchBar({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
onChange: (v: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
placeholder={placeholder ?? "Filter index..."}
|
|
||||||
className="w-full pl-8 pr-7 py-1.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-md text-xs text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--text-primary)] transition-all"
|
|
||||||
/>
|
|
||||||
<Search className="w-3.5 h-3.5 text-[var(--text-dim)] absolute left-2.5 top-2.5 pointer-events-none" />
|
|
||||||
{value && (
|
|
||||||
<button
|
|
||||||
onClick={() => onChange("")}
|
|
||||||
className="absolute right-2 top-2 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded"
|
|
||||||
aria-label="Clear filter"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<X className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export function SectionCard({
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
desc,
|
|
||||||
docCount,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
icon: string;
|
|
||||||
desc: string;
|
|
||||||
docCount: number;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="group flex flex-col justify-between bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-xl p-5 transition-all shadow-xs">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between gap-2 mb-3">
|
|
||||||
<span className="text-2xl">{icon}</span>
|
|
||||||
<Link
|
|
||||||
href={`/${id}`}
|
|
||||||
className="text-xs font-mono text-[var(--text-muted)] hover:text-[var(--text-primary)] font-semibold"
|
|
||||||
>
|
|
||||||
{docCount} docs →
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<h3 className="font-bold text-sm text-[var(--text-primary)] mb-1">{label}</h3>
|
|
||||||
<p className="text-xs text-[var(--text-muted)] mb-4 line-clamp-2 leading-relaxed">{desc}</p>
|
|
||||||
</div>
|
|
||||||
<div className="pt-3 border-t border-[var(--border-color)] text-xs text-[var(--text-dim)] font-mono">
|
|
||||||
/{id} · {docCount} document{docCount !== 1 ? "s" : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { getSectionMeta } from "@mcpedia/config/sections";
|
import { getSectionMeta } from "@mcpedia/config/sections";
|
||||||
import { useDocuments } from "@/hooks/useDocuments";
|
|
||||||
import {
|
import {
|
||||||
Folder,
|
Folder,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
@@ -15,8 +14,6 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import type { DocumentMeta } from "@mcpedia/types";
|
|
||||||
|
|
||||||
interface Doc {
|
interface Doc {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -185,11 +182,21 @@ function TreeNodeItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const { docs } = useDocuments();
|
const [docs, setDocs] = useState<Doc[]>([]);
|
||||||
const [filterText, setFilterText] = useState("");
|
const [filterText, setFilterText] = useState("");
|
||||||
const [collapsedMap, setCollapsedMap] = useState<Record<string, boolean>>({});
|
const [collapsedMap, setCollapsedMap] = useState<Record<string, boolean>>({});
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/docs")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (Array.isArray(data)) setDocs(data);
|
||||||
|
else if (data?.docs) setDocs(data.docs);
|
||||||
|
})
|
||||||
|
.catch(() => setDocs([]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
function toggleCollapse(slug: string) {
|
function toggleCollapse(slug: string) {
|
||||||
setCollapsedMap((prev) => ({ ...prev, [slug]: !prev[slug] }));
|
setCollapsedMap((prev) => ({ ...prev, [slug]: !prev[slug] }));
|
||||||
}
|
}
|
||||||
@@ -201,7 +208,7 @@ export default function Sidebar() {
|
|||||||
(d) =>
|
(d) =>
|
||||||
d.title.toLowerCase().includes(lower) ||
|
d.title.toLowerCase().includes(lower) ||
|
||||||
d.slug.toLowerCase().includes(lower) ||
|
d.slug.toLowerCase().includes(lower) ||
|
||||||
d.tags.some((t: string) => t.toLowerCase().includes(lower)),
|
d.tags.some((t) => t.toLowerCase().includes(lower)),
|
||||||
);
|
);
|
||||||
}, [docs, filterText]);
|
}, [docs, filterText]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
import { Tag, TrendingUp } from "lucide-react";
|
|
||||||
|
|
||||||
export default function TagCloud({ limit = 24 }: { limit?: number }) {
|
|
||||||
const { data, isLoading } = useSWR<{ tags: { tag: string; count: number }[] }>(
|
|
||||||
`/api/tags?limit=${limit}`,
|
|
||||||
fetcher,
|
|
||||||
{ revalidateOnFocus: false },
|
|
||||||
);
|
|
||||||
const tags = data?.tags ?? [];
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-1.5 animate-pulse">
|
|
||||||
{Array.from({ length: 8 }).map((_, i) => (
|
|
||||||
<div key={i} className="h-6 w-16 bg-[var(--bg-elevated)] rounded" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags.length === 0) return null;
|
|
||||||
|
|
||||||
const max = Math.max(...tags.map((t) => t.count), 1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Tag className="w-3.5 h-3.5 text-[var(--text-muted)]" />
|
|
||||||
<h3 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">Popular Tags</h3>
|
|
||||||
<span className="text-[11px] text-[var(--text-dim)] font-mono">({tags.length})</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{tags.map(({ tag, count }) => {
|
|
||||||
const intensity = count / max;
|
|
||||||
const opacity = 0.45 + intensity * 0.55;
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={tag}
|
|
||||||
href={`/search?q=${encodeURIComponent(tag)}`}
|
|
||||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-xs font-mono transition-all hover:scale-[1.02] bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border-[var(--border-color)] hover:border-[var(--text-muted)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
||||||
style={{ opacity }}
|
|
||||||
title={`${count} document${count !== 1 ? "s" : ""}`}
|
|
||||||
>
|
|
||||||
<span>#{tag}</span>
|
|
||||||
<span className="text-[10px] bg-[var(--bg-elevated)] px-1 py-0.2 rounded font-semibold">{count}</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
|
|
||||||
export default function DashboardQueueStrip() {
|
|
||||||
const { data } = useSWR<{ waiting: number; active: number; completed: number; failed: number; delayed: number }>("/api/queue/status", fetcher, {
|
|
||||||
refreshInterval: 6000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
if (!data) return null;
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-2 text-[11px] font-mono" role="status" aria-live="polite">
|
|
||||||
<span className="px-2.5 py-1 rounded bg-[var(--bg-surface)] border border-[var(--border-color)]">waiting: <strong>{data.waiting}</strong></span>
|
|
||||||
<span className="px-2.5 py-1 rounded bg-[var(--bg-surface)] border border-[var(--border-color)] text-sky-600 dark:text-sky-400">active: <strong>{data.active}</strong></span>
|
|
||||||
<span className={`px-2.5 py-1 rounded bg-[var(--bg-surface)] border ${data.failed ? "border-amber-500/40 text-amber-600 dark:text-amber-400" : "border-[var(--border-color)] text-[var(--text-dim)]"}`}>failed: <strong>{data.failed}</strong></span>
|
|
||||||
<span className="px-2.5 py-1 rounded bg-[var(--bg-surface)] border border-[var(--border-color)] text-emerald-600 dark:text-emerald-400">completed: <strong>{data.completed}</strong></span>
|
|
||||||
{data.delayed ? <span className="px-2.5 py-1 rounded bg-[var(--bg-surface)] border border-[var(--border-color)]">delayed: <strong>{data.delayed}</strong></span> : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import TagCloud from "@/components/TagCloud";
|
|
||||||
|
|
||||||
export default function TagCloudClient() {
|
|
||||||
return <TagCloud limit={30} />;
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { listDocuments, listSections } from "@mcpedia/core";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
|
||||||
const [all, sections] = await Promise.all([listDocuments(), listSections()]);
|
|
||||||
|
|
||||||
const totalTags = new Set(all.flatMap((d) => d.tags)).size;
|
|
||||||
const recentDocs = [...all].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, 8);
|
|
||||||
|
|
||||||
// Tag aggregation server-side for SSR snapshot (client TagCloud will hydrate live via SWR)
|
|
||||||
const tagCounts = new Map<string, number>();
|
|
||||||
for (const d of all) for (const t of d.tags) tagCounts.set(t, (tagCounts.get(t) ?? 0) + 1);
|
|
||||||
const topTags = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-8 max-w-5xl">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">Dashboard</h1>
|
|
||||||
<p className="text-xs sm:text-sm text-[var(--text-muted)] mt-1">Live overview of the MCPedia knowledge base. Data via PostgreSQL Core + BullMQ queue.</p>
|
|
||||||
</div>
|
|
||||||
<Link href="/create" className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 bg-[var(--brand)] text-[var(--brand-fg)] rounded-md text-xs font-semibold shadow-xs">
|
|
||||||
+ New Doc
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Metric strip — SSR snapshot */}
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
|
||||||
<div className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-xs">
|
|
||||||
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">DB Documents</div>
|
|
||||||
<div className="text-2xl font-bold font-mono text-[var(--text-primary)] mt-1">{all.length}</div>
|
|
||||||
<div className="text-[11px] text-[var(--text-dim)] font-mono">{totalTags} unique tags</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-xs">
|
|
||||||
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">Active Sections</div>
|
|
||||||
<div className="text-2xl font-bold font-mono text-[var(--text-primary)] mt-1">{sections.length}</div>
|
|
||||||
<div className="text-[11px] text-[var(--text-dim)] font-mono truncate">{sections.map((s) => s.id).join(" · ")}</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-xs">
|
|
||||||
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">Search</div>
|
|
||||||
<div className="text-2xl font-bold font-mono text-[var(--text-primary)] mt-1">RRF</div>
|
|
||||||
<div className="text-[11px] text-[var(--text-dim)] font-mono">FTS + Cosine Vectors</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-xs">
|
|
||||||
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">MCP Tools</div>
|
|
||||||
<div className="text-2xl font-bold font-mono text-[var(--text-primary)] mt-1">13</div>
|
|
||||||
<div className="text-[11px] text-[var(--text-dim)] font-mono">HTTP :4021</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Queue strip — client component polls live */}
|
|
||||||
<DashboardQueueStrip />
|
|
||||||
|
|
||||||
{/* Section breakdown */}
|
|
||||||
<section className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-5 shadow-xs">
|
|
||||||
<h2 className="text-xs font-bold uppercase tracking-wider text-[var(--text-muted)] font-mono mb-3">Section Breakdown</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{sections.map((s) => {
|
|
||||||
const pct = all.length ? Math.round((s.docCount / all.length) * 100) : 0;
|
|
||||||
return (
|
|
||||||
<Link key={s.id} href={`/${s.id}`} className="flex items-center gap-3 group">
|
|
||||||
<span className="text-sm w-6 text-center">{s.icon}</span>
|
|
||||||
<span className="text-xs font-semibold text-[var(--text-primary)] group-hover:underline w-28 truncate">{s.label}</span>
|
|
||||||
<div className="flex-1 h-2 bg-[var(--bg-elevated)] rounded-full overflow-hidden border border-[var(--border-color)]">
|
|
||||||
<div className="h-full bg-[var(--brand)] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] font-mono text-[var(--text-muted)] w-14 text-right">{s.docCount} docs · {pct}%</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Top tags */}
|
|
||||||
{topTags.length > 0 && (
|
|
||||||
<section className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-5 shadow-xs">
|
|
||||||
<h2 className="text-xs font-bold uppercase tracking-wider text-[var(--text-muted)] font-mono mb-3">Top Tags</h2>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{topTags.map(([tag, count]) => (
|
|
||||||
<Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`} className="px-2.5 py-1 rounded-full bg-[var(--bg-elevated)] border border-[var(--border-color)] text-xs font-mono text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-muted)]">
|
|
||||||
#{tag} <span className="text-[10px] bg-[var(--bg-surface)] px-1 py-0.2 rounded ml-1">{count}</span>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-3">
|
|
||||||
<TagCloudClient />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Recent */}
|
|
||||||
{recentDocs.length > 0 && (
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xs font-bold uppercase tracking-wider text-[var(--text-muted)] font-mono mb-3">Recently Updated</h2>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
{recentDocs.map((d) => (
|
|
||||||
<Link key={d.slug} href={`/${d.slug}`} className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-xl shadow-xs group">
|
|
||||||
<div className="text-[10px] font-mono uppercase text-[var(--text-muted)]">{d.section} · {new Date(d.updatedAt).toLocaleDateString()}</div>
|
|
||||||
<div className="text-sm font-semibold text-[var(--text-primary)] group-hover:underline line-clamp-1 mt-1">{d.title}</div>
|
|
||||||
{d.tags.length > 0 && <div className="flex gap-1 mt-2 flex-wrap">{d.tags.slice(0, 3).map((t) => <span key={t} className="text-[10px] px-1.5 py-0.2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded font-mono text-[var(--text-muted)]">#{t}</span>)}</div>}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client islands — isolated so the page stays RSC
|
|
||||||
import DashboardQueueStrip from "./QueueStrip";
|
|
||||||
import TagCloudClient from "./TagCloudClient";
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
import type { DocumentMeta } from "@mcpedia/types";
|
|
||||||
|
|
||||||
type DocsEnvelope = DocumentMeta[] | { docs: DocumentMeta[] };
|
|
||||||
|
|
||||||
function normalize(data: DocsEnvelope | undefined): DocumentMeta[] {
|
|
||||||
if (!data) return [];
|
|
||||||
if (Array.isArray(data)) return data;
|
|
||||||
return (data as { docs: DocumentMeta[] }).docs ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDocuments(section?: string, status = "published") {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (section) params.set("section", section);
|
|
||||||
if (status) params.set("status", status);
|
|
||||||
const qs = params.toString();
|
|
||||||
const key = qs ? `/api/docs?${qs}` : "/api/docs";
|
|
||||||
|
|
||||||
const swr = useSWR<DocsEnvelope>(key, fetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...swr,
|
|
||||||
docs: normalize(swr.data),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
|
|
||||||
export interface QueueStatus {
|
|
||||||
waiting: number;
|
|
||||||
active: number;
|
|
||||||
completed: number;
|
|
||||||
failed: number;
|
|
||||||
delayed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useQueueStatus(enabled = false) {
|
|
||||||
return useSWR<QueueStatus>(enabled ? "/api/queue/status" : null, fetcher, {
|
|
||||||
refreshInterval: 5_000,
|
|
||||||
dedupingInterval: 4_000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTags(limit = 30) {
|
|
||||||
const key = `/api/tags?limit=${limit}`;
|
|
||||||
return useSWR<{ tags: { tag: string; count: number }[] }>(key, fetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
import type { DocumentMeta } from "@mcpedia/types";
|
|
||||||
|
|
||||||
export function useRelated(slug: string, limit = 4) {
|
|
||||||
const key = slug ? `/api/related?slug=${encodeURIComponent(slug)}&limit=${limit}` : null;
|
|
||||||
const swr = useSWR<{ results: DocumentMeta[] }>(key, fetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
keepPreviousData: true,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...swr,
|
|
||||||
related: swr.data?.results ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
|
|
||||||
export interface RevisionSummary {
|
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
revisionNo: number;
|
|
||||||
title: string;
|
|
||||||
reason: string;
|
|
||||||
createdAt: string;
|
|
||||||
bodyLength: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRevisions(slug: string, limit = 10) {
|
|
||||||
const key = slug ? `/api/revisions?slug=${encodeURIComponent(slug)}&limit=${limit}` : null;
|
|
||||||
const swr = useSWR<{ revisions: RevisionSummary[] }>(key, fetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
keepPreviousData: true,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...swr,
|
|
||||||
revisions: swr.data?.revisions ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
|
|
||||||
export interface SearchHit {
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
section: string;
|
|
||||||
score: number;
|
|
||||||
snippet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchMode = "hybrid" | "keyword" | "semantic";
|
|
||||||
|
|
||||||
function useDebounced<T>(value: T, delayMs: number): T {
|
|
||||||
const [debounced, setDebounced] = useState(value);
|
|
||||||
useEffect(() => {
|
|
||||||
const t = setTimeout(() => setDebounced(value), delayMs);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
}, [value, delayMs]);
|
|
||||||
return debounced;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSearch(q: string, mode: SearchMode = "hybrid", limit = 30) {
|
|
||||||
const debouncedQ = useDebounced(q, 180);
|
|
||||||
const trimmed = debouncedQ.trim();
|
|
||||||
const key = trimmed ? `/api/search?q=${encodeURIComponent(trimmed)}&mode=${mode}&limit=${limit}` : null;
|
|
||||||
|
|
||||||
const swr = useSWR<{ results: SearchHit[] }>(key, fetcher, {
|
|
||||||
keepPreviousData: true,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
dedupingInterval: 2_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...swr,
|
|
||||||
hits: swr.data?.results ?? [],
|
|
||||||
isSearching: !!trimmed && swr.isLoading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { fetcher } from "@/lib/fetcher";
|
|
||||||
import type { SectionInfo } from "@mcpedia/types";
|
|
||||||
|
|
||||||
export function useSections() {
|
|
||||||
return useSWR<SectionInfo[]>("/api/sections", fetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import type { Metadata } from "next";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import SWRProvider from "@/lib/swr";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Database } from "lucide-react";
|
import { Database } from "lucide-react";
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ export default function RootLayout({
|
|||||||
<div id="a11y-live-region" aria-live="polite" aria-atomic="true" />
|
<div id="a11y-live-region" aria-live="polite" aria-atomic="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SWRProvider>
|
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="mx-auto max-w-7xl w-full flex-1 flex flex-col">
|
<div className="mx-auto max-w-7xl w-full flex-1 flex flex-col">
|
||||||
@@ -72,10 +70,6 @@ export default function RootLayout({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-[11px] text-[var(--text-muted)]">
|
<div className="flex items-center gap-4 text-[11px] text-[var(--text-muted)]">
|
||||||
<Link href="/dashboard" className="hover:text-[var(--text-primary)] transition-colors">
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
<span>·</span>
|
|
||||||
<Link href="/search" className="hover:text-[var(--text-primary)] transition-colors">
|
<Link href="/search" className="hover:text-[var(--text-primary)] transition-colors">
|
||||||
RRF Hybrid Search
|
RRF Hybrid Search
|
||||||
</Link>
|
</Link>
|
||||||
@@ -88,7 +82,6 @@ export default function RootLayout({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</SWRProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import type { Document, DocumentMeta, SectionInfo } from "@mcpedia/types";
|
|
||||||
import { fetcher } from "./fetcher";
|
|
||||||
|
|
||||||
export interface SearchApiHit {
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
section: string;
|
|
||||||
score: number;
|
|
||||||
snippet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchApiResponse {
|
|
||||||
results: SearchApiHit[];
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocsApiResponse {
|
|
||||||
docs?: DocumentMeta[];
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SectionsApiResponse extends Array<SectionInfo> {}
|
|
||||||
|
|
||||||
export interface SingleDocResponse {
|
|
||||||
doc: Document;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RelatedResponse {
|
|
||||||
results: DocumentMeta[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RevisionsResponse {
|
|
||||||
revisions: { id: string; slug: string; revisionNo: number; title: string; reason: string; createdAt: string; bodyLength: number }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueueStatusResponse {
|
|
||||||
waiting: number;
|
|
||||||
active: number;
|
|
||||||
completed: number;
|
|
||||||
failed: number;
|
|
||||||
delayed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TagsResponse {
|
|
||||||
tags: { tag: string; count: number }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function docsUrl(section?: string, status = "published"): string {
|
|
||||||
const p = new URLSearchParams();
|
|
||||||
if (section) p.set("section", section);
|
|
||||||
if (status) p.set("status", status);
|
|
||||||
const qs = p.toString();
|
|
||||||
return qs ? `/api/docs?${qs}` : "/api/docs";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function searchUrl(q: string, mode = "hybrid", limit = 30): string {
|
|
||||||
return `/api/search?q=${encodeURIComponent(q)}&mode=${mode}&limit=${limit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FetchDocs = () => Promise<DocumentMeta[]>;
|
|
||||||
export const fetchDocs: FetchDocs = async () => {
|
|
||||||
const data = await fetcher<DocsApiResponse | DocumentMeta[]>("/api/docs");
|
|
||||||
if (Array.isArray(data)) return data;
|
|
||||||
return data.docs ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchSections = () => fetcher<SectionInfo[]>("/api/sections");
|
|
||||||
export const fetchSearch = (q: string, mode = "hybrid", limit = 30) =>
|
|
||||||
fetcher<SearchApiResponse>(searchUrl(q, mode, limit));
|
|
||||||
export const fetchRelated = (slug: string, limit = 5) =>
|
|
||||||
fetcher<RelatedResponse>(`/api/related?slug=${encodeURIComponent(slug)}&limit=${limit}`);
|
|
||||||
export const fetchRevisions = (slug: string, limit = 20) =>
|
|
||||||
fetcher<RevisionsResponse>(`/api/revisions?slug=${encodeURIComponent(slug)}&limit=${limit}`);
|
|
||||||
export const fetchSingleDoc = (slug: string) =>
|
|
||||||
fetcher<SingleDocResponse>(`/api/docs/${encodeURIComponent(slug)}`);
|
|
||||||
export const fetchQueueStatus = () => fetcher<QueueStatusResponse>("/api/queue/status");
|
|
||||||
export const fetchTags = () => fetcher<TagsResponse>("/api/tags");
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export class FetchError extends Error {
|
|
||||||
status: number;
|
|
||||||
info: unknown;
|
|
||||||
constructor(message: string, status: number, info: unknown) {
|
|
||||||
super(message);
|
|
||||||
this.name = "FetchError";
|
|
||||||
this.status = status;
|
|
||||||
this.info = info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetcher<T>(url: string): Promise<T> {
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (!res.ok) {
|
|
||||||
let info: unknown = null;
|
|
||||||
try {
|
|
||||||
info = await res.json();
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
info = await res.text();
|
|
||||||
} catch {
|
|
||||||
info = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const msg =
|
|
||||||
(info as { error?: string })?.error ||
|
|
||||||
(typeof info === "string" ? info : null) ||
|
|
||||||
`Request failed: ${res.status} ${res.statusText}`;
|
|
||||||
throw new FetchError(msg, res.status, info);
|
|
||||||
}
|
|
||||||
return (await res.json()) as T;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { SWRConfig } from "swr";
|
|
||||||
|
|
||||||
export default function SWRProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<SWRConfig
|
|
||||||
value={{
|
|
||||||
dedupingInterval: 4_000,
|
|
||||||
focusThrottleInterval: 10_000,
|
|
||||||
revalidateOnFocus: true,
|
|
||||||
shouldRetryOnError: false,
|
|
||||||
errorRetryCount: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SWRConfig>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+165
-62
@@ -1,17 +1,26 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, Suspense, useEffect } from "react";
|
import { useState, useEffect, useCallback, Suspense } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useSections } from "@/hooks/useSections";
|
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
|
||||||
import { useSearch, type SearchMode } from "@/hooks/useSearch";
|
import {
|
||||||
import SearchBar from "@/components/SearchBar";
|
Search,
|
||||||
import { EmptyState, ErrorState } from "@/components/EmptyState";
|
X,
|
||||||
import { Search, SearchX } from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface DocHit {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
section: string;
|
||||||
|
score: number;
|
||||||
|
snippet: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchMode = "hybrid" | "keyword" | "semantic";
|
||||||
|
|
||||||
function SearchContent() {
|
function SearchContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
|
||||||
const initialQ = searchParams.get("q") ?? "";
|
const initialQ = searchParams.get("q") ?? "";
|
||||||
const initialMode = (searchParams.get("mode") as SearchMode) ?? "hybrid";
|
const initialMode = (searchParams.get("mode") as SearchMode) ?? "hybrid";
|
||||||
|
|
||||||
@@ -19,57 +28,107 @@ function SearchContent() {
|
|||||||
const [mode, setMode] = useState<SearchMode>(
|
const [mode, setMode] = useState<SearchMode>(
|
||||||
["hybrid", "keyword", "semantic"].includes(initialMode) ? initialMode : "hybrid",
|
["hybrid", "keyword", "semantic"].includes(initialMode) ? initialMode : "hybrid",
|
||||||
);
|
);
|
||||||
|
const [sections, setSections] = useState<SectionConfig[]>(DEFAULT_SECTIONS);
|
||||||
const [selectedSection, setSelectedSection] = useState<string>("all");
|
const [selectedSection, setSelectedSection] = useState<string>("all");
|
||||||
|
const [results, setResults] = useState<DocHit[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { data: liveSections } = useSections();
|
|
||||||
const sections = liveSections ?? [];
|
|
||||||
|
|
||||||
// SWR search — debounced inside hook (180ms)
|
|
||||||
const { hits, error, isValidating, isLoading } = useSearch(query, mode, 30);
|
|
||||||
const loading = isLoading || isValidating;
|
|
||||||
|
|
||||||
// Keep URL in sync (debounced via hook timing is fine — push without blocking render)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const q = query.trim();
|
fetch("/api/sections")
|
||||||
const target = q ? `/search?q=${encodeURIComponent(q)}&mode=${mode}` : `/search?mode=${mode}`;
|
.then((r) => r.json())
|
||||||
const current = `${searchParams.toString() ? `/search?${searchParams.toString()}` : "/search"}`;
|
.then((data) => {
|
||||||
if (target !== current && q) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
router.replace(target);
|
setSections(data);
|
||||||
}
|
}
|
||||||
}, [query, mode]); // eslint-disable-line react-hooks/exhaustive-deps
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const filteredHits = selectedSection === "all" ? hits : hits.filter((r) => r.section === selectedSection);
|
const handleSearch = useCallback(async (q: string, searchMode: SearchMode) => {
|
||||||
|
if (!q.trim()) {
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/search?q=${encodeURIComponent(q)}&mode=${searchMode}&limit=30`,
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
setResults(data.results || []);
|
||||||
|
} catch {
|
||||||
|
setResults([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialQ) {
|
||||||
|
handleSearch(initialQ, mode);
|
||||||
|
}
|
||||||
|
}, [initialQ, handleSearch, mode]);
|
||||||
|
|
||||||
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const val = e.target.value;
|
||||||
|
setQuery(val);
|
||||||
|
handleSearch(val, mode);
|
||||||
|
}
|
||||||
|
|
||||||
function handleModeChange(newMode: SearchMode) {
|
function handleModeChange(newMode: SearchMode) {
|
||||||
setMode(newMode);
|
setMode(newMode);
|
||||||
const q = query.trim();
|
if (query.trim()) {
|
||||||
const qs = q ? `?q=${encodeURIComponent(q)}&mode=${newMode}` : `?mode=${newMode}`;
|
handleSearch(query, newMode);
|
||||||
router.replace(`/search${qs}`);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestions = [
|
const filteredResults =
|
||||||
"stack alignment",
|
selectedSection === "all"
|
||||||
"ret2win exploit",
|
? results
|
||||||
"bullmq worker",
|
: results.filter((r) => r.section === selectedSection);
|
||||||
"streamable http",
|
|
||||||
"full-text search",
|
|
||||||
"caddy reverse proxy",
|
|
||||||
"pgvector cosine",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl space-y-8">
|
<div className="max-w-4xl space-y-8">
|
||||||
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-extrabold text-[var(--text-primary)] tracking-tight mb-2">Multi-Modal Search Console</h1>
|
<h1 className="text-3xl font-extrabold text-[var(--text-primary)] tracking-tight mb-2">
|
||||||
|
Multi-Modal Search Console
|
||||||
|
</h1>
|
||||||
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed">
|
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed">
|
||||||
Query across knowledge documents and CTF writeups using PostgreSQL Full-Text Search (tsvector), Cosine Vector Embeddings, or Reciprocal Rank Fusion (RRF).
|
Query across knowledge documents and CTF writeups using PostgreSQL Full-Text Search (tsvector), Cosine Vector Embeddings, or Reciprocal Rank Fusion (RRF).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search Box & Controls */}
|
||||||
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-4 sm:p-5 shadow-xs space-y-4">
|
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-4 sm:p-5 shadow-xs space-y-4">
|
||||||
<SearchBar value={query} onChange={setQuery} placeholder="Search by keywords, challenge names, tools, concepts..." autoFocus onClear={() => setQuery("")} />
|
{/* Search Input Bar */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={query}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Search by keywords, challenge names, tools, concepts..."
|
||||||
|
className="w-full pl-10 pr-9 py-3 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--text-primary)] text-sm transition-all"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Search className="w-4 h-4 text-[var(--text-muted)] absolute left-3.5 top-3.5 pointer-events-none" />
|
||||||
|
{query && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setQuery("");
|
||||||
|
setResults([]);
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-3 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded"
|
||||||
|
aria-label="Clear query"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Algorithm & Filter Controls */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2.5 border-t border-[var(--border-color)] text-xs">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2.5 border-t border-[var(--border-color)] text-xs">
|
||||||
|
{/* Mode Selector */}
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
<span className="text-[var(--text-dim)] font-mono uppercase text-[10px] mr-1">Algorithm:</span>
|
<span className="text-[var(--text-dim)] font-mono uppercase text-[10px] mr-1">Algorithm:</span>
|
||||||
{[
|
{[
|
||||||
@@ -82,18 +141,27 @@ function SearchContent() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleModeChange(m.id as SearchMode)}
|
onClick={() => handleModeChange(m.id as SearchMode)}
|
||||||
title={m.desc}
|
title={m.desc}
|
||||||
className={`px-3 py-1 rounded-md border text-xs font-medium transition-all cursor-pointer ${mode === m.id ? "bg-[var(--brand)] border-[var(--brand)] text-[var(--brand-fg)] font-semibold shadow-xs" : "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"}`}
|
className={`px-3 py-1 rounded-md border text-xs font-medium transition-all cursor-pointer ${
|
||||||
|
mode === m.id
|
||||||
|
? "bg-[var(--brand)] border-[var(--brand)] text-[var(--brand-fg)] font-semibold shadow-xs"
|
||||||
|
: "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{m.label}
|
{m.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Section Filter Pills */}
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
<div className="flex items-center gap-1 flex-wrap">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedSection("all")}
|
onClick={() => setSelectedSection("all")}
|
||||||
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${selectedSection === "all" ? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-semibold" : "text-[var(--text-dim)] hover:text-[var(--text-muted)]"}`}
|
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${
|
||||||
|
selectedSection === "all"
|
||||||
|
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-semibold"
|
||||||
|
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</button>
|
</button>
|
||||||
@@ -102,7 +170,11 @@ function SearchContent() {
|
|||||||
key={s.id}
|
key={s.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedSection(s.id)}
|
onClick={() => setSelectedSection(s.id)}
|
||||||
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${selectedSection === s.id ? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-semibold" : "text-[var(--text-dim)] hover:text-[var(--text-muted)]"}`}
|
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${
|
||||||
|
selectedSection === s.id
|
||||||
|
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-semibold"
|
||||||
|
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{s.label}
|
{s.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -111,31 +183,44 @@ function SearchContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <ErrorState message={(error as Error).message} onRetry={() => setQuery((q) => q + " ")} />}
|
{/* Loading Spinner */}
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center gap-3 py-10 text-[var(--text-muted)] text-xs font-mono" role="status" aria-live="polite">
|
<div className="flex items-center justify-center gap-3 py-10 text-[var(--text-muted)] text-xs font-mono">
|
||||||
<div className="w-4 h-4 border-2 border-[var(--text-muted)] border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-[var(--text-muted)] border-t-transparent rounded-full animate-spin" />
|
||||||
<span>SEARCHING_VECTOR_SPACE...</span>
|
<span>SEARCHING_VECTOR_SPACE...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && query.trim() && filteredHits.length === 0 && !error && (
|
{/* Empty State */}
|
||||||
<EmptyState
|
{!loading && query && filteredResults.length === 0 && (
|
||||||
title="No matching documents found"
|
<div className="text-center py-12 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-xs space-y-2 animate-fade-in">
|
||||||
description="Try switching algorithm mode to Hybrid or Semantic, or search with different keywords."
|
<h3 className="text-sm font-bold text-[var(--text-primary)]">No matching documents found</h3>
|
||||||
icon={SearchX}
|
<p className="text-xs text-[var(--text-muted)] max-w-sm mx-auto">
|
||||||
/>
|
Try switching algorithm mode to <strong>Hybrid</strong> or <strong>Semantic</strong>, or search with different keywords.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !query.trim() && (
|
{/* Initial Suggestions */}
|
||||||
|
{!loading && !query && (
|
||||||
<div className="p-6 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3 shadow-xs">
|
<div className="p-6 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3 shadow-xs">
|
||||||
<p className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">Popular Queries:</p>
|
<p className="text-[10px] font-mono uppercase tracking-wider text-[var(--text-muted)]">Popular Queries:</p>
|
||||||
<div className="flex justify-center flex-wrap gap-2 text-xs">
|
<div className="flex justify-center flex-wrap gap-2 text-xs">
|
||||||
{suggestions.map((term) => (
|
{[
|
||||||
|
"stack alignment",
|
||||||
|
"ret2win exploit",
|
||||||
|
"bullmq worker",
|
||||||
|
"streamable http",
|
||||||
|
"full-text search",
|
||||||
|
"caddy reverse proxy",
|
||||||
|
"pgvector cosine",
|
||||||
|
].map((term) => (
|
||||||
<button
|
<button
|
||||||
key={term}
|
key={term}
|
||||||
onClick={() => setQuery(term)}
|
onClick={() => {
|
||||||
|
setQuery(term);
|
||||||
|
handleSearch(term, mode);
|
||||||
|
}}
|
||||||
className="px-3 py-1 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all font-mono text-xs cursor-pointer"
|
className="px-3 py-1 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--text-muted)] rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all font-mono text-xs cursor-pointer"
|
||||||
>
|
>
|
||||||
{term}
|
{term}
|
||||||
@@ -145,14 +230,16 @@ function SearchContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && filteredHits.length > 0 && (
|
{/* Results List */}
|
||||||
|
{!loading && filteredResults.length > 0 && (
|
||||||
<div className="space-y-3 animate-fade-in">
|
<div className="space-y-3 animate-fade-in">
|
||||||
<div className="flex items-center justify-between text-xs text-[var(--text-dim)] font-mono" aria-live="polite">
|
<div className="flex items-center justify-between text-xs text-[var(--text-dim)] font-mono">
|
||||||
<span>Found {filteredHits.length} result{filteredHits.length !== 1 ? "s" : ""}</span>
|
<span>Found {filteredResults.length} result{filteredResults.length !== 1 ? "s" : ""}</span>
|
||||||
<span>Ranked by {mode} score</span>
|
<span>Ranked by {mode} score</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
{filteredHits.map((r) => (
|
{filteredResults.map((r) => (
|
||||||
<Link
|
<Link
|
||||||
key={r.slug}
|
key={r.slug}
|
||||||
href={`/${r.slug}`}
|
href={`/${r.slug}`}
|
||||||
@@ -160,16 +247,25 @@ function SearchContent() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3 mb-2">
|
<div className="flex items-center justify-between gap-3 mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px] font-mono px-1.5 py-0.2 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] uppercase font-semibold">{r.section}</span>
|
<span className="text-[10px] font-mono px-1.5 py-0.2 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] uppercase font-semibold">
|
||||||
<span className="text-xs text-[var(--text-dim)] font-mono truncate">/{r.slug}</span>
|
{r.section}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[var(--text-dim)] font-mono truncate">
|
||||||
|
/{r.slug}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-mono text-[var(--text-dim)] bg-[var(--bg-elevated)] px-2 py-0.5 rounded">{r.score.toFixed(3)}</span>
|
<span className="text-xs font-mono text-[var(--text-dim)] bg-[var(--bg-elevated)] px-2 py-0.5 rounded">
|
||||||
|
{r.score.toFixed(3)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm font-semibold text-[var(--text-primary)] group-hover:underline mb-1.5">{r.title}</h2>
|
|
||||||
|
<h2 className="text-sm font-semibold text-[var(--text-primary)] group-hover:underline mb-1.5">
|
||||||
|
{r.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{r.snippet && (
|
{r.snippet && (
|
||||||
<p
|
<p
|
||||||
className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--bg-elevated-hover)] [&>mark]:text-[var(--text-primary)] [&>mark]:font-semibold"
|
||||||
// snippet from Postgres ts_headline — sanitized by DB
|
|
||||||
dangerouslySetInnerHTML={{ __html: r.snippet }}
|
dangerouslySetInnerHTML={{ __html: r.snippet }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -184,7 +280,14 @@ function SearchContent() {
|
|||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<div className="max-w-4xl py-12"><h1 className="text-2xl font-bold text-[var(--text-primary)] mb-4">Search</h1><p className="text-sm text-[var(--text-muted)]">Loading search module...</p></div>}>
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="max-w-4xl py-12">
|
||||||
|
<h1 className="text-2xl font-bold text-[var(--text-primary)] mb-4">Search</h1>
|
||||||
|
<p className="text-sm text-[var(--text-muted)]">Loading search module...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SearchContent />
|
<SearchContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const nextConfig: NextConfig = {
|
|||||||
"@mcpedia/config",
|
"@mcpedia/config",
|
||||||
"@mcpedia/parser",
|
"@mcpedia/parser",
|
||||||
"@mcpedia/search",
|
"@mcpedia/search",
|
||||||
"@mcpedia/queue",
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mcpedia/config": "workspace:*",
|
"@mcpedia/config": "workspace:*",
|
||||||
"@mcpedia/core": "workspace:*",
|
"@mcpedia/core": "workspace:*",
|
||||||
"@mcpedia/queue": "workspace:*",
|
|
||||||
"@mcpedia/types": "workspace:*",
|
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"lucide-react": "^1.33.0",
|
"lucide-react": "^1.33.0",
|
||||||
"next": "16.3.5",
|
"next": "16.3.3",
|
||||||
"react": "19.3.0",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.2.8",
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1"
|
||||||
"swr": "^2.5.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -32,8 +29,8 @@
|
|||||||
"@types/node": "^26",
|
"@types/node": "^26",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^10",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.3.5",
|
"eslint-config-next": "16.3.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"@mcpedia/queue": "workspace:*",
|
"@mcpedia/queue": "workspace:*",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
"hono": "^4.6.0",
|
"hono": "^4.6.0",
|
||||||
"zod": "^4.5.4",
|
"zod": "^3.23.8",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
@@ -70,19 +70,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mcpedia/config": "workspace:*",
|
"@mcpedia/config": "workspace:*",
|
||||||
"@mcpedia/core": "workspace:*",
|
"@mcpedia/core": "workspace:*",
|
||||||
"@mcpedia/queue": "workspace:*",
|
|
||||||
"@mcpedia/types": "workspace:*",
|
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"lucide-react": "^1.33.0",
|
"lucide-react": "^1.33.0",
|
||||||
"next": "16.3.5",
|
"next": "16.3.3",
|
||||||
"react": "19.3.0",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.2.8",
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"swr": "^2.5.1",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -90,8 +87,8 @@
|
|||||||
"@types/node": "^26",
|
"@types/node": "^26",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^10",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.3.5",
|
"eslint-config-next": "16.3.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
},
|
},
|
||||||
@@ -309,19 +306,19 @@
|
|||||||
|
|
||||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
||||||
|
|
||||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
|
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||||
|
|
||||||
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||||
|
|
||||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="],
|
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="],
|
||||||
|
|
||||||
"@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="],
|
"@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="],
|
||||||
|
|
||||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||||
|
|
||||||
"@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="],
|
"@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="],
|
||||||
|
|
||||||
@@ -337,57 +334,57 @@
|
|||||||
|
|
||||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||||
|
|
||||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="],
|
"@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.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="],
|
"@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.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="],
|
"@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.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="],
|
"@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.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="],
|
"@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.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="],
|
"@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.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="],
|
"@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.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="],
|
"@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.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="],
|
"@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.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="],
|
"@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.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="],
|
"@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.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="],
|
"@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.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="],
|
"@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.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="],
|
"@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.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="],
|
"@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.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="],
|
"@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.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="],
|
"@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.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="],
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
|
||||||
|
|
||||||
"@ioredis/commands": ["@ioredis/commands@2.0.0", "", {}, "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg=="],
|
"@ioredis/commands": ["@ioredis/commands@2.0.0", "", {}, "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg=="],
|
||||||
|
|
||||||
@@ -445,25 +442,25 @@
|
|||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||||
|
|
||||||
"@next/env": ["@next/env@16.3.5", "", {}, "sha512-NWEXVDMqoEo0ktmU6u0sE2Vg0LOcsD7NnOTJNo3/fEaTfsg+F1bMIxuDmQbda4e3yTIQwVdUREF2yIuMOusKtg=="],
|
"@next/env": ["@next/env@16.3.3", "", {}, "sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g=="],
|
||||||
|
|
||||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.3.5", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "fast-glob": "3.3.1" } }, "sha512-PGfSeItHJ12DH8t+6sEbuMe59NE5rAhCfgk06QKTH2ne9VUL1JlaXdYXY3B8RaiF2SO50rDYvd39TulP6A6xZQ=="],
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.3.3", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "fast-glob": "3.3.1" } }, "sha512-pbEh30vvjKpDoTAmo1v3q2uM4JUi8QaEBpbmjWvGfoec2jLghy/WNtvzAT0bk+Ik9oz6etjt4YjXEk4BQnicCw=="],
|
||||||
|
|
||||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pMmGgETfKvElucLHtVaeiMRbp2zUbvKx7b1yGko0liBz3cw1mKSggWN/Rp/wPz8z+E1O82u3r4L1Co+ZS5hokQ=="],
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw=="],
|
||||||
|
|
||||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-76VaGYvf6HPa5/w12yLkE3dXTn9AfdEviI79oEL3aZoAmRLc9rWitjWqyjViVysK/ht/y9YKzFkBrUdi/wGkow=="],
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-zKDELJ5jSQMHeO/hmXUQsAzagX4bQD4OiMi3pQ5FbUj+yK506oLVHnKA2YXMlbg1EHHqJYtyePOgByIDXD1lqw=="],
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg=="],
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-7Vql0pgzCoHagv6+FNOZoqmJqA52c6zeVbhtS/47qFozO1MSx4ms7x7GHiciY8R5CDsSMKMQjJEryoJLcsBIbA=="],
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-NH/xzehyHEFWE2nlcZon7TB/0+H4shfWCi7S1zka815XCOhJDYZhoeJtOYy0dh0WVRWACVXSyGNFFytoMxUhRg=="],
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA=="],
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-lV4+EhWMfS8jcC+EH2nn/Cm5cn6XsgbE07bU9tMH8fCo0tNAqhyzi1b5wQ/Tn6NGFTvKDY65w3ZH95EjwBRAnQ=="],
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg=="],
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-/wKzAREX2RF++MhicjDbg8tGn2AiBIM0+EFeTFKoUEUbW5D6amCJehd5Z5G1H5/gxNdgnwoXMcHz24H/c2tGkQ=="],
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg=="],
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-LNdCHzgLFc+UeqMS84LzXPaeBRKyqDN9OMyFAr1OrB0XrNw78IRrEVtZvvA7245W/HsaoeVOQX9jPjPk8jojwA=="],
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
@@ -531,8 +528,6 @@
|
|||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||||
|
|
||||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
"@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/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||||
@@ -665,13 +660,13 @@
|
|||||||
|
|
||||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
|
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
"brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
@@ -815,9 +810,9 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@10.9.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "Line truncated
|
"eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "crosLine truncated
|
||||||
|
|
||||||
"eslint-config-next": ["eslint-config-next@16.3.5", "", { "dependencies": { "@next/eslint-plugin-next": "16.3.5", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typeLine truncated
|
"eslint-config-next": ["eslint-config-next@16.3.3", "", { "dependencies": { "@next/eslint-plugin-next": "16.3.3", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typeLine truncated
|
||||||
|
|
||||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
|
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
|
||||||
|
|
||||||
@@ -833,11 +828,11 @@
|
|||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||||
|
|
||||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||||
|
|
||||||
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||||
|
|
||||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||||
|
|
||||||
@@ -1249,7 +1244,7 @@
|
|||||||
|
|
||||||
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
|
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
@@ -1267,7 +1262,7 @@
|
|||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
"next": ["next@16.3.5", "", { "dependencies": { "@next/env": "16.3.5", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.5", "@next/swc-darwin-x64": "16.3.5", "@next/swc-linux-arm64-gnu": "16.3.5", "@next/swc-linux-arm64-musl": "16.3.5", "@next/swc-linux-x64-gnu": "16.3.5", "@next/swc-linux-x64-musl": "16.3.5", "@next/swc-win32-arm64-msvc": Line truncated
|
"next": ["next@16.3.3", "", { "dependencies": { "@next/env": "16.3.3", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.3", "@next/swc-darwin-x64": "16.3.3", "@next/swc-linux-arm64-gnu": "16.3.3", "@next/swc-linux-arm64-musl": "16.3.3", "@next/swc-linux-x64-gnu": "16.3.3", "@next/swc-linux-x64-musl": "16.3.3", "@next/swc-win32-arm64-msvc": Line truncated
|
||||||
|
|
||||||
"node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="],
|
"node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="],
|
||||||
|
|
||||||
@@ -1357,9 +1352,9 @@
|
|||||||
|
|
||||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
@@ -1411,7 +1406,7 @@
|
|||||||
|
|
||||||
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||||
|
|
||||||
@@ -1431,7 +1426,7 @@
|
|||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linLine truncated
|
"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-linLine truncated
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
@@ -1495,8 +1490,6 @@
|
|||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
"swr": ["swr@2.5.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BRw55e8r0B7SpDN20CAzoQAHl7y1yP7/Zt7oqUjMv0vSt2u2Xnkm88Ws+VypbV9BXHQVuSuyVq7zMjO16wSExw=="],
|
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
@@ -1563,8 +1556,6 @@
|
|||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
@@ -1595,7 +1586,7 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||||
|
|
||||||
@@ -1615,14 +1606,10 @@
|
|||||||
|
|
||||||
"@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
"@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
|
||||||
|
|
||||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
|
"@eslint/eslintrc/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
|
||||||
|
|
||||||
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
"@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@mcpedia/mcp/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"@mcpedia/mcp/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
@@ -1649,6 +1636,10 @@
|
|||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
|
|
||||||
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||||
|
|
||||||
"docx/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="],
|
"docx/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="],
|
||||||
@@ -1661,20 +1652,8 @@
|
|||||||
|
|
||||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-plugin-import/eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chLine truncated
|
|
||||||
|
|
||||||
"eslint-plugin-import/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "Line truncated
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chaLine truncated
|
|
||||||
|
|
||||||
"eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"eslint-plugin-react-hooks/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
@@ -1749,72 +1728,14 @@
|
|||||||
|
|
||||||
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
|
||||||
|
|
||||||
"@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
|
|
||||||
|
|
||||||
"@next/eslint-plugin-next/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@next/eslint-plugin-next/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
||||||
|
|
||||||
"docx/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
"docx/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="],
|
|
||||||
|
|
||||||
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"next/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
"next/postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||||
@@ -1871,24 +1792,6 @@
|
|||||||
|
|
||||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
||||||
|
|
||||||
"eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
||||||
|
|
||||||
"eslint-plugin-jsx-a11y/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
||||||
|
|
||||||
"eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,17 +12,6 @@
|
|||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config = { allowUnfree = true; };
|
config = { allowUnfree = true; };
|
||||||
overlays = [
|
|
||||||
(self: super: {
|
|
||||||
bun = super.bun.overrideAttrs (old: rec {
|
|
||||||
version = "1.4.0";
|
|
||||||
src = super.fetchzip {
|
|
||||||
url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip";
|
|
||||||
sha256 = "sha256:Poy0vf7yJ/hzk33QiQj5gnshI5Q7dfbaMD7xgwiyDKw=";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bunBin = pkgs.bun + "/bin/bun";
|
bunBin = pkgs.bun + "/bin/bun";
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "mcpedia",
|
"name": "mcpedia",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1.4.0",
|
"packageManager": "bun@1.3.14",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|||||||
Reference in new issue
Block a user