diff --git a/README.md b/README.md index 9e1dacb4..904a524e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Member-facing overview — what it is, what members use, current status, and how | Path | Workspace | Role | | --- | --- | --- | | `sites/mainweb` | `web` | Public club site + authenticated portal (App Hosting) | -| `sites/hacklytics2027` | `hacklytics2027` | Hacklytics 2027 marketing site, static export (Firebase Hosting) | +| `sites/hacklytics2027` | `hacklytics2027` | Hacklytics 2027 marketing site, Astro static build (Firebase Hosting) | | `packages/api` | `@query/api` | tRPC routers, middleware, pricing | | `packages/auth` | `@query/auth` | NextAuth (Google, GitHub, email codes) | | `packages/db` | `@query/db` | Drizzle schema, client, membership rules | @@ -44,7 +44,7 @@ flowchart LR routes["Route handlers: /api/trpc /api/auth /api/webhooks/stripe /api/resume /api/bootcamp/materials"] end - hack["sites/hacklytics2027 (static export)"] + hack["sites/hacklytics2027 (Astro static build)"] api["@query/api (tRPC appRouter)"] auth["@query/auth (NextAuth)"] db["@query/db (Drizzle)"] diff --git a/docs/architecture.md b/docs/architecture.md index bb74951e..a061c0aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -**query** is a pnpm + Turborepo monorepo. Two Next.js sites share four internal packages. Club operations and hackathon operations share one Postgres database but are modeled as separate domains. +**query** is a pnpm + Turborepo monorepo. Two sites (the Next.js portal and an Astro marketing site) sit beside four internal packages. Club operations and hackathon operations share one Postgres database but are modeled as separate domains. ``` ┌─────────────────────────────────────────────────────────────┐ @@ -182,7 +182,7 @@ The webhook is `sites/mainweb/app/(portal)/api/webhooks/stripe/route.ts`. Linkin | Surface | Where it runs | Output | | --- | --- | --- | | `sites/mainweb` | Firebase App Hosting / Cloud Run (`apphosting.yaml`) | Next `standalone` | -| `sites/hacklytics2027` | Firebase Hosting target `hacklytics` | Static `output: "export"` | +| `sites/hacklytics2027` | Firebase Hosting target `hacklytics` | Astro static build (`astro build` writes `out/`) | The event site does not talk to the database. Interest and registration live on the portal; the marketing site links to `/login?callbackUrl=/hacklytics`. diff --git a/docs/getting-started.md b/docs/getting-started.md index 46035137..e92c9489 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -87,7 +87,7 @@ Turbo runs every workspace `dev` task. The two sites: | Site | URL | Notes | | --- | --- | --- | | Main website + portal | [http://localhost:3001](http://localhost:3001) | Next.js App Router, `output: "standalone"` | -| Hacklytics 2027 | [http://localhost:3000](http://localhost:3000) | Static-export marketing site (`--turbopack`) | +| Hacklytics 2027 | [http://localhost:3000](http://localhost:3000) | Astro static marketing site (`astro dev`, port set in `astro.config.mjs`) | Useful filters: diff --git a/docs/sites/hacklytics2027.md b/docs/sites/hacklytics2027.md index fee10fbf..15cbd58a 100644 --- a/docs/sites/hacklytics2027.md +++ b/docs/sites/hacklytics2027.md @@ -2,45 +2,73 @@ Path: `sites/hacklytics2027` Workspace name: `hacklytics2027` -Framework: Next.js 16, React 19, Tailwind 4, **static export** (`output: "export"`) +Framework: Astro 7 (static build), React 19 islands, Tailwind 4 (`@tailwindcss/vite`) Marketing site for Hacklytics 2027 (“Digital Bloom”). It has **no database** and **no tRPC**. Anything dynamic (interest list, registration) lives on the portal. ## Run ```bash -pnpm --filter hacklytics2027 dev # next dev (port 3000) -pnpm --filter hacklytics2027 build # writes static files for Firebase Hosting +pnpm --filter hacklytics2027 dev # astro dev (port 3000) +pnpm --filter hacklytics2027 build # writes static files to out/ for Firebase Hosting +pnpm --filter hacklytics2027 preview # serves out/ +pnpm --filter hacklytics2027 typecheck # tsc --noEmit ``` +`astro check` is not used: it needs the TypeScript JavaScript API, and the repo's TypeScript 7 ships only the native compiler. `tsc --noEmit` covers `.ts` and `.tsx`; `.astro` files are checked by the build. + +Astro 7 needs Vite 8 while the root `package.json` overrides `vite` to 7 for the rest of the repo, so two scoped overrides (`astro>vite`, `@astrojs/react>vite`) give this site Vite 8 and leave every other workspace on Vite 7. Astro 6 is not an option: GHSA-26w7-cxv4-gfx2 (AVIF RCE) affects every version below 7.2.8. + ## Structure | Path | Role | | --- | --- | -| `app/page.tsx` | Home: a server component around the hero and sections | -| `components/Hero.tsx` | Pixel garden hero and countdown | -| `app/layout.tsx` | Font preloads, metadata, SW registrar | -| `app/fonts.generated.css`, `lib/fonts.generated.ts`, `public/fonts/` | Output of `scripts/subset-fonts.py`, committed | -| `app/not-found.tsx` | 404 | -| `components/HomeSections.tsx` | Below-the-fold sections | -| `components/sections/*` | About, tracks, schedule, prizes/speakers, FAQ, sponsors | -| `components/pixel/*` | Pixel sprites / garden; `season.ts` holds the one bloom count | -| `components/Navbar.tsx`, `Footer.tsx` | Chrome | -| `lib/links.ts` | Portal origin + interest URL | +| `src/pages/index.astro` | Home: hero, then the sections | +| `src/pages/404.astro` | 404 (`out/404.html`) | +| `src/layouts/Layout.astro` | `` (font preloads, metadata, JSON-LD, the no-flash theme script), navbar, footer | +| `src/components/Hero.astro` | Hero. The countdown is an inline script; the garden is a React island | +| `src/components/Navbar.astro`, `Footer.astro`, `ThemeToggle.astro` | Chrome. Behaviour lives in `src/scripts/` | +| `src/components/HomeSections.astro`, `src/components/sections/*` | About, tracks, prizes, schedule, sponsors, FAQ, rendered at build time (React, no directive) | +| `src/components/pixel/*` | Pixel sprites and flower beds; `season.ts` holds the one bloom count, `GardenBeds.tsx` the four React islands | +| `src/scripts/*` | The plain scripts: `navbar`, `theme-toggle`, `bloom`, `service-worker` | +| `src/styles/globals.css`, `fonts.generated.css`, `src/lib/fonts.generated.ts`, `public/fonts/` | Styles; the fonts are output of `scripts/subset-fonts.py`, committed | +| `src/lib/links.ts` | Portal origin + interest URL | | `public/sw.js` | Service worker (Firebase header: no-cache, `Service-Worker-Allowed: /`) | -Schedule copy lives in `components/sections/Schedule/data.ts`. +Schedule copy lives in `src/components/sections/Schedule/data.ts`. + +## JavaScript and hydration + +Everything renders to HTML at build time. Only what needs the browser ships code. + +| Piece | How it runs | Why | +| --- | --- | --- | +| About, tracks, prizes, schedule, sponsors, FAQ, sprites | Build time, no JS | Nothing to do after load | +| Tracks and schedule bloom | `src/scripts/bloom.ts` (about 300 bytes) | They only need `data-bloom="on"` written when 35% is in view | +| Navbar (menu, hide on scroll, section links) | `src/scripts/navbar.ts` | Plain DOM work; React would add nothing | +| Theme toggle | `src/scripts/theme-toggle.ts`; the icon follows the theme in CSS | Right icon on first paint, even before the script runs | +| Theme with no flash | `is:inline` script in `` | Must run before first paint | +| Countdown | Inline script after the hero markup | Fills in the zeros during parsing, before first paint, so the figures never jump, then ticks | +| Service worker | `src/scripts/service-worker.ts`, production only, after `load` | Keeps it off the critical path | +| Hero flower bed | React island, `client:idle` | In the first screen but not needed to read it; hydrates off the critical path | +| Footer flower bed | React island, `client:visible` | Page bottom; hydrates when scrolled near | +| 404 flower bed | React island, `client:visible` | Same | +| Menu flower bed | React island, `client:media="(max-width: 1023px)"` | The menu only opens below 1024px, so desktop never loads it | + +The beds are the one place React stays. How many flowers are in bloom depends on the date, so the build cannot bake it, and a click plants one. The static HTML draws every plant dormant (`useInBloom` returns 0 there) and hydration brings in the real count. + +React Compiler is not enabled: the islands are small, and it would add its runtime for no change in output. ## Fonts -Instrument Sans and Silkscreen are subset by hand and committed. `next/font` is not used: it would regenerate the fallback metrics, and a separate family per file would put that fallback between the core and extended faces. +Instrument Sans and Silkscreen are subset by hand and committed. No font loader is used: one would regenerate the fallback metrics, and a separate family per file would put that fallback between the core and extended faces. Each font has a **core** file (ASCII, curly quotes, dashes, ellipsis, plus `©` and `·`, which most pages use), preloaded on every page, and an **extended** file (the rest of Latin-1 and Latin Extended-A) behind `unicode-range`, which downloads only when a page needs one of its characters. Core and extended share a family name, so the browser treats them as one font. ```bash pip install -r sites/hacklytics2027/scripts/subset-fonts.requirements.txt python sites/hacklytics2027/scripts/subset-fonts.py # rebuild the fonts -python sites/hacklytics2027/scripts/subset-fonts.py --check # after next build +python sites/hacklytics2027/scripts/subset-fonts.py --check # after astro build ``` Run `--check` after copy changes: it lists any character outside the core. Rerun the script if a character should join the core, if an axis or OpenType feature in use changes, or when the pinned upstream font moves. The header of the script has the details. @@ -82,9 +110,9 @@ Firebase Hosting target `hacklytics`, public dir `sites/hacklytics2027/out` (`fi Workflow: `.github/workflows/ci.yml`. The `verify` job builds every site; `deploy-hacklytics` ships that build live on `main` and `preview-hacklytics` to channel `pr-N` on PRs. Neither runs unless `verify` passes. -Asset caching: hashed JS and CSS `max-age=31536000, immutable`; fonts and images `max-age=86400, stale-while-revalidate=604800`, including those under `_next/static`, because the later rule wins; HTML `max-age=3600`. See `firebase.json`. +Asset caching: hashed JS and CSS under `/_astro/` are `max-age=31536000, immutable`; fonts and images `max-age=86400, stale-while-revalidate=604800`; HTML `max-age=0, must-revalidate`. See `firebase.json`. The service worker caches `/_astro/` files cache-first, since their names are fingerprinted. -Images are `unoptimized: true` because static export has no image optimizer. React Compiler is on. +Images are plain `` tags over files in `public/`; there is no image pipeline. ## Assets diff --git a/firebase.json b/firebase.json index ab4d69df..ed1b6ff8 100644 --- a/firebase.json +++ b/firebase.json @@ -13,7 +13,7 @@ "trailingSlash": true, "headers": [ { - "source": "/_next/static/**", + "source": "/_astro/**", "headers": [ { "key": "Cache-Control", diff --git a/package.json b/package.json index 8015d553..e580585a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "brace-expansion": "^5.0.9", "gaxios>uuid": "^11.1.1", "source-map-js": "^1.2.2", - "minimatch": "^11.0.3" + "minimatch": "^11.0.3", + "astro>vite": "8.3.5", + "@astrojs/react>vite": "8.3.5" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdf51bb1..2e23e05e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,8 @@ overrides: gaxios>uuid: ^11.1.1 source-map-js: ^1.2.2 minimatch: ^11.0.3 + astro>vite: 8.3.5 + '@astrojs/react>vite': 8.3.5 importers: @@ -37,10 +39,10 @@ importers: version: 2.11.10 vite: specifier: ^7.3.7 - version: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15) + version: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1) vitest: specifier: ^5.0.3 - version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15)) + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1)) packages/api: dependencies: @@ -110,10 +112,10 @@ importers: version: 7.0.2 vite: specifier: ^7.3.7 - version: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15) + version: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1) vitest: specifier: ^5.0.3 - version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15)) + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1)) packages/auth: dependencies: @@ -252,12 +254,15 @@ importers: sites/hacklytics2027: dependencies: - babel-plugin-react-compiler: - specifier: 1.0.0 - version: 1.0.0 - next: - specifier: 16.4.4 - version: 16.4.4(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + '@astrojs/react': + specifier: 7.1.1 + version: 7.1.1(@types/node@24.19.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-react-compiler@1.0.0)(esbuild@0.25.12)(jiti@2.7.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(tsx@4.23.15)(yaml@2.9.1) + '@tailwindcss/vite': + specifier: 4.3.3 + version: 4.3.3(vite@8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + astro: + specifier: 7.4.2 + version: 7.4.2(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) react: specifier: 19.3.1 version: 19.3.1 @@ -268,9 +273,6 @@ importers: '@query/oxlint-config': specifier: workspace:* version: link:../../tooling/oxlint - '@tailwindcss/postcss': - specifier: 4.3.3 - version: 4.3.3 '@types/node': specifier: 24.19.2 version: 24.19.2 @@ -280,12 +282,6 @@ importers: '@types/react-dom': specifier: ^19.3.0 version: 19.3.0(@types/react@19.3.0) - autoprefixer: - specifier: 10.6.1 - version: 10.6.1(postcss@8.5.28) - baseline-browser-mapping: - specifier: 2.11.33 - version: 2.11.33 oxlint: specifier: 1.90.0 version: 1.90.0(oxlint-tsgolint@7.0.2005) @@ -295,9 +291,6 @@ importers: playwright: specifier: 1.64.1 version: 1.64.1 - postcss: - specifier: ^8.5.23 - version: 8.5.28 tailwindcss: specifier: 4.3.3 version: 4.3.3 @@ -352,6 +345,9 @@ importers: archiver: specifier: ^8.0.0 version: 8.0.0 + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 chart.js: specifier: ^4.6.1 version: 4.6.1 @@ -563,6 +559,104 @@ packages: resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==} engines: {node: '>=10'} + '@astrojs/compiler-binding-android-arm64@0.5.2': + resolution: {integrity: sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@astrojs/compiler-binding-darwin-arm64@0.5.2': + resolution: {integrity: sha512-1ZG5Ar3dD8HtodX9XH1mRmE0qBXZlvjYXSFfpgFwocqfWnvhFuqtGUNseL8wgfImiRMztIjpQqlogQFQQtgiag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@astrojs/compiler-binding-darwin-x64@0.5.2': + resolution: {integrity: sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.2': + resolution: {integrity: sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-arm64-musl@0.5.2': + resolution: {integrity: sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-linux-x64-gnu@0.5.2': + resolution: {integrity: sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-x64-musl@0.5.2': + resolution: {integrity: sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-wasm32-wasi@0.5.2': + resolution: {integrity: sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.2': + resolution: {integrity: sha512-Tr13bh+ahTWsjXjzctK7pFpWV+dUU4FtBfETlR7gO+ueex0F+RDch7eRr0YfKysaRhk9tWm14lsCJ5uWyFPNHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@astrojs/compiler-binding-win32-x64-msvc@0.5.2': + resolution: {integrity: sha512-SfKkI+jOQYLh6wojPvVPV0lFfQnxisweN9B+qCzRvZp7DHAuGif8Y0WcC5PUgN4jb8VKy57LggQDtggR46Du1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@astrojs/compiler-binding@0.5.2': + resolution: {integrity: sha512-lEn76+0JcMQ5RDXU0wSMWJg2ZppiZvSULpghC29cfv4o9bhvWFDbQjsCtvjjXN9iVLFQ7fL3wTR2L2UU13mP8g==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/compiler-rs@0.5.2': + resolution: {integrity: sha512-IvwYLB8t1M86rKO4oPdBi9GP4gzXcgrVlNgocjwos1jeeUuEIV8d9Crn1PvYD/5IKteWOxpIHomf7e8iYlkE+Q==} + engines: {node: '>=22.12.0'} + + '@astrojs/internal-helpers@0.13.0': + resolution: {integrity: sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==} + + '@astrojs/markdown-satteri@0.4.5': + resolution: {integrity: sha512-0IeGdoFbv0f+442KJhYS44Wg8F3MuHmRsxIEYuFyqkjRubc6dPBFZ7azor5t4AeXLQoT5A5h9HyViKpd70Dl9w==} + + '@astrojs/prism@4.1.1': + resolution: {integrity: sha512-sXlAXJX6t4QfuLg+CuCDzpg7NoxWcyb4paFprgtFppLeisCBmh935h+tJ5CWXfsXEoW2IDy/ihbRMtiu28Chjw==} + engines: {node: '>=22.23.4'} + + '@astrojs/react@7.1.1': + resolution: {integrity: sha512-jHan8UuXzFEbzcmsjCRrepm9Xc5XRz+Jt57MjfE568tbOEoDKVzeMuHf9YO0Y4hHwROX8iJmLzaiT3Yr5W0UbQ==} + engines: {node: '>=22.23.4'} + peerDependencies: + '@types/react': ^17.0.50 || ^18.0.21 || ^19.0.0 + '@types/react-dom': ^17.0.17 || ^18.0.6 || ^19.0.0 + oxc-transform-react: ^0.145.0 + react: ^17.0.2 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + oxc-transform-react: + optional: true + + '@astrojs/telemetry@3.4.1': + resolution: {integrity: sha512-SMkvMsGOpM0FMAoT5pzhgI3LZC0JSPWf7Evwop+fZ3N6UkusPM7lI+sOJjMNI5OJrVoiei1/AGo1cY7sikTc5A==} + engines: {node: '>=22.23.4'} + '@auth/core@0.41.3': resolution: {integrity: sha512-sJ3JMHHkXMD3aOjopv7mOBTO1Ocw4b0fAEXJBz6k7YHLpYQI6C40jCUPc5fNvUKxXRXNE1/sRISA15UrwWJBTw==} peerDependencies: @@ -596,10 +690,23 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.13': + resolution: {integrity: sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.13': + resolution: {integrity: sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} @@ -617,6 +724,71 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + + '@bruits/satteri-darwin-arm64@0.10.5': + resolution: {integrity: sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==} + cpu: [arm64] + os: [darwin] + + '@bruits/satteri-darwin-x64@0.10.5': + resolution: {integrity: sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==} + cpu: [x64] + os: [darwin] + + '@bruits/satteri-linux-arm64-gnu@0.10.5': + resolution: {integrity: sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-arm64-musl@0.10.5': + resolution: {integrity: sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@bruits/satteri-linux-x64-gnu@0.10.5': + resolution: {integrity: sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-x64-musl@0.10.5': + resolution: {integrity: sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@bruits/satteri-wasm32-wasi@0.10.5': + resolution: {integrity: sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@bruits/satteri-win32-arm64-msvc@0.10.5': + resolution: {integrity: sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==} + cpu: [arm64] + os: [win32] + + '@bruits/satteri-win32-x64-msvc@0.10.5': + resolution: {integrity: sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==} + cpu: [x64] + os: [win32] + + '@capsizecss/unpack@4.0.1': + resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} + engines: {node: '>=18'} + + '@clack/core@1.5.1': + resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.8.1': + resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==} + engines: {node: '>= 20.12.0'} + '@dimforge/rapier3d-compat@0.12.0': resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} @@ -627,9 +799,33 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + + '@emnapi/core@2.0.0-beta.2': + resolution: {integrity: sha512-MzCVoWtcg4Pm2B3je5mMvYMvj3MmBKw+fU7vbsJc9/iT8ip6/eYIHo/Dsm/mr+wsHB6SEyJDuxB09Yy6r/8+4A==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/runtime@2.0.0-beta.2': + resolution: {integrity: sha512-JcrSPzPvZuNZvJqPTF9Fw25LbOJ8avzFVlaf2grmH4Cku0oGP47tRgqPZadcfj3YoZMHkFVnCYFbQM8I+i5QEw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + + '@emnapi/wasi-threads@2.2.0': + resolution: {integrity: sha512-JP7OYH38Q7+pyTAe14raZHLQIK7BDKbdwji+Xi3pcd/6QkJYQH3SX4wu6EplGrGJWRepoNzedfm0G3B9Z4e9Rw==} + '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} @@ -1043,6 +1239,20 @@ packages: os: [linux] libc: [glibc] + '@napi-rs/wasm-runtime@1.2.6': + resolution: {integrity: sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || 2.0.0-beta.2 + '@emnapi/runtime': ^1.7.1 || 2.0.0-beta.2 + + '@napi-rs/wasm-runtime@1.2.7': + resolution: {integrity: sha512-zJccwplOAbgfZju95IUHazLnx9RAPtPN2YGJXI6UbrgeaR9QfPjGrQL6ERkSAtnVCA4Q+83ApGyaX5zdpCaKkQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3 + '@emnapi/runtime': ^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3 + '@next/bundle-analyzer@16.4.3': resolution: {integrity: sha512-FGcLziT2rqvGZTxZUjnCzwjCANnigN9vrbCm+1hQtzXbbtc1bXBTRmkXohVqyB/ReF5bOjpM2bNJQhz0PfLk9Q==} @@ -1108,6 +1318,15 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@oxc-project/types@0.156.0': + resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} + + '@oxc-project/types@0.157.0': + resolution: {integrity: sha512-95iTox1qdt6WGWce3xwNOVayByOfLaYAq+qGPpub6NWbBh9sv0GG3+zyfh9ZBCO/jVu5A0EWI+qQI7g6fevjxg==} + '@oxlint-tsgolint/darwin-arm64@7.0.2005': resolution: {integrity: sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==} cpu: [arm64] @@ -1742,144 +1961,375 @@ packages: '@radix-ui/rect@1.1.3': resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} - '@rollup/rollup-android-arm-eabi@4.63.5': - resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==} + '@rolldown/binding-android-arm-eabi@1.2.16': + resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.63.5': - resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==} + '@rolldown/binding-android-arm-eabi@1.3.0': + resolution: {integrity: sha512-zjq+pUSE36RJG9nJXqrRfMCOxekzVhlaYMtFELHaOcRH4UqTnkWhbTbEixKEawsb0ZTVMRbn8RLYcuU4DYk8qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.16': + resolution: {integrity: sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.63.5': - resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==} + '@rolldown/binding-android-arm64@1.3.0': + resolution: {integrity: sha512-HdnCiSmMGgEILGVLU1o4dcOblLGj8sMnqEFrDY/S7KRti1nTQHAqGGs2AniPspbK90AFS3pM23K/R0J3hPQXYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.16': + resolution: {integrity: sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.63.5': - resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==} + '@rolldown/binding-darwin-arm64@1.3.0': + resolution: {integrity: sha512-TkQ2JGC8/9UxoCeH5dSJj1gxuFhzw200QTJJd2YNPRXznSSM4XvpKV2Cs+RM2BcM8sa6xa4rmXNuiJ64+1993g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.16': + resolution: {integrity: sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.63.5': - resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==} - cpu: [arm64] + '@rolldown/binding-darwin-x64@1.3.0': + resolution: {integrity: sha512-Gr+iZW50RJZo4BPUazaDh/Azx7W0Rl04sy2MkXD+H4RkxzUX5hIrjaVw1BK+XHsccKExQAmCy6DhvRLlUzaYUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.16': + resolution: {integrity: sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.63.5': - resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==} + '@rolldown/binding-freebsd-x64@1.3.0': + resolution: {integrity: sha512-RwAtx8peSqUoBf8pxE0L62xKXBNnA3BtdFV+07t13t0TQoASCtV9tnU67aKKMRZSiD06SU07chB5B8fWFdW3gQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.63.5': - resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.16': + resolution: {integrity: sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.63.5': - resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==} + '@rolldown/binding-linux-arm-gnueabihf@1.3.0': + resolution: {integrity: sha512-ow8fq6kYaIx75D0EVT5GeF4DcawcEYYXJ2CvEaOzyygW1jtVDQU/9o5YYidctHBRDUgPzGaTNB6n/VMTmd1ymw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.63.5': - resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==} + '@rolldown/binding-linux-arm64-gnu@1.2.16': + resolution: {integrity: sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.63.5': - resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==} + '@rolldown/binding-linux-arm64-gnu@1.3.0': + resolution: {integrity: sha512-PLOWuiz+rdqSgiLgN7bxvy6DjXLj8gnJxzRS243LNnLi96VeyB7VFsC3eblcKv3Xwd3jQqJfbCWRhLijTIZyvQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] + libc: [glibc] - '@rollup/rollup-linux-loong64-gnu@4.63.5': - resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==} - cpu: [loong64] + '@rolldown/binding-linux-arm64-musl@1.2.16': + resolution: {integrity: sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] os: [linux] - libc: [glibc] + libc: [musl] - '@rollup/rollup-linux-loong64-musl@4.63.5': - resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==} - cpu: [loong64] + '@rolldown/binding-linux-arm64-musl@1.3.0': + resolution: {integrity: sha512-CQ/k85cc342j5SnZ/gT9p132NXZRGTgLRhfItdYqUg+jpvL5HYFve0GB+zQDCYGwDjtDWIoqKSOitkmZMZc/tw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.63.5': - resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.16': + resolution: {integrity: sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.63.5': - resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==} + '@rolldown/binding-linux-ppc64-gnu@1.3.0': + resolution: {integrity: sha512-wk+ZG7xDP2px3cQRFoNoNp52/w1O5hTTtciHxuastreMOp8isYUTCpcG/rEYnSQBI+nycN+B9d9pBGua+qvV7w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [musl] + libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.63.5': - resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==} - cpu: [riscv64] + '@rolldown/binding-linux-s390x-gnu@1.2.16': + resolution: {integrity: sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.63.5': - resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==} - cpu: [riscv64] + '@rolldown/binding-linux-s390x-gnu@1.3.0': + resolution: {integrity: sha512-sEgyZt72Rj7V5q6OZ7A4U2I5v4EMGEv3Fp5QmoncmKbUMTrGmpyH7MwENfERGbfUYjxdBmktw4JJLys5I1aQNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] os: [linux] - libc: [musl] + libc: [glibc] - '@rollup/rollup-linux-s390x-gnu@4.63.5': - resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==} - cpu: [s390x] + '@rolldown/binding-linux-x64-gnu@1.2.16': + resolution: {integrity: sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.63.5': - resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==} + '@rolldown/binding-linux-x64-gnu@1.3.0': + resolution: {integrity: sha512-PvL0BoBUwVugK1pFd+zyVk1L9F6ucmQ7oF2dpy8zKPHNlbW2zrU9Jst3fWYfG7z3owjxqhwAvi5G4o0AikrJMw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.63.5': - resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==} + '@rolldown/binding-linux-x64-musl@1.2.16': + resolution: {integrity: sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.63.5': - resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==} + '@rolldown/binding-linux-x64-musl@1.3.0': + resolution: {integrity: sha512-5N4DdzAmLtnyuh027H6ViC99BeoA4T0RwKS6qUrl6zY70AKRsNDrnEZeWnqrKmDRFxXmv8zf9yhF2DxN/SfyGQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] - os: [openbsd] + os: [linux] + libc: [musl] - '@rollup/rollup-openharmony-arm64@4.63.5': - resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==} + '@rolldown/binding-openharmony-arm64@1.2.16': + resolution: {integrity: sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.63.5': - resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==} + '@rolldown/binding-openharmony-arm64@1.3.0': + resolution: {integrity: sha512-Tz+2b+yF5eVRGX3DWebn2JIvZkNYI1gkafI+nF2fau2zeQDOpBAS8w1PxHi4ke//NIOE6+2DNYyq1veIjzAXqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.3.0': + resolution: {integrity: sha512-ntK6OFor3nQtS1TenZP5wX59oPpkKEmour3AipGHB32u+Z8QXxjVdG+7vNSvgerRWIj6vjg4XpKu42slb0vlXQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.2.16': + resolution: {integrity: sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.63.5': - resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==} - cpu: [ia32] + '@rolldown/binding-win32-arm64-msvc@1.3.0': + resolution: {integrity: sha512-ldH38lBlOpS+b4fbSG9euQjhYXLE18PIz5hyvf6gvFfyVLodRE/Tsu4jV8UmPqjjce6lotmUUwscxPuer4lczQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.63.5': - resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==} + '@rolldown/binding-win32-x64-msvc@1.2.16': + resolution: {integrity: sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.63.5': - resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==} + '@rolldown/binding-win32-x64-msvc@1.3.0': + resolution: {integrity: sha512-HglhUzUPuXNTGFt5ZwJalSEvavhFSl+nxpQM9urkzkt+2oGI1ZSwgyytk9aGw15/0kk1TCHPc57nv3DdiG8/yQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.63.5': + resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.5': + resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.5': + resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.5': + resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.5': + resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.5': + resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.5': + resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.5': + resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.5': + resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.5': + resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.5': + resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.5': + resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.5': + resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.5': + resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.5': + resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.5': + resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.5': + resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.5': + resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.5': + resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.5': + resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.5': + resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.5': + resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.5': + resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.5': + resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.5': + resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==} + cpu: [x64] + os: [win32] + + '@shikijs/core@4.6.0': + resolution: {integrity: sha512-bfd4OKgvoyXYiYmMRuleA656P4V7M/Du9lcTl38Ngm6KszMj7bq4J1IGZwSYR4c95Tv5UQALtYNWUwaI9UT3xw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.6.0': + resolution: {integrity: sha512-K0oqxFoL+kEMdz89mHLFO15Tsbv3kFZ6WR1cXZKMvLSc+oqaM00A6pyga1qSeXB22dP0tTiSApRcGBaKWsEccg==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.6.0': + resolution: {integrity: sha512-aggd6Q7SuJOXNnsUbfG2XesxBJKBMAw/PDTUo9PyRjMaaJJ2dNrI+q0C+25ARXAK9yLHAlwxc9oRT2u68YKZ7w==} + engines: {node: '>=20'} + + '@shikijs/langs@4.6.0': + resolution: {integrity: sha512-1yMEX3oCVV54LUq/ovfpRkDQoYDkcvjSWFFa/YLUDMiSoRzMi2uxLJYbv41ahT8Lu9lFMaa7hecd3Q39trqUyg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.6.0': + resolution: {integrity: sha512-c54+TQEo9ToqgfL/AU8FpjnV09+GA9Oa7M5ROkkXteZfp5XzpAJGXjjvZQFQioJOwm/w/6Z3lIzQstAkl8coag==} + engines: {node: '>=20'} + + '@shikijs/themes@4.6.0': + resolution: {integrity: sha512-qHAVAs80RiGD+IRuH9G+JPDOEc9A/kHsng4EzCUCwB9bLzW6Z0y9WETh2YyCcADJBmfrIaJ1Jp7KBNP6FHkgtQ==} + engines: {node: '>=20'} + + '@shikijs/types@4.6.0': + resolution: {integrity: sha512-+ENG1adsKt/X7cRYw1Nl7BEpBmPOjmQ7bR/eGmHuMlT8gtOFf3KEi5l31DJYyAWhW6Y6bjP1plU7FmaxWGENFw==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.1.0': + resolution: {integrity: sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==} + '@stripe/react-stripe-js@6.12.0': resolution: {integrity: sha512-5RVfx92MTb66+ELuQTreUqX2oFvma5wybZ7MaifoNo0QgrjbJiLZcysGw5kIHU7p1TCd1Lx5JnasoqteSwlUsg==} peerDependencies: @@ -2024,6 +2474,11 @@ packages: '@tailwindcss/postcss@4.3.3': resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^7.3.7 + '@tanstack/query-core@5.105.0': resolution: {integrity: sha512-7p56vRmT/z4UJ6sctGIdl5WRUQyUv53IUpUwEKi2g97zMbO7zh8ZsCLffGAvCPBR4xD72e3VXsDyLwXHhoNTMQ==} @@ -2155,6 +2610,9 @@ packages: '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + '@types/archiver@8.0.0': resolution: {integrity: sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A==} @@ -2167,9 +2625,21 @@ packages: '@types/emscripten@1.41.6': resolution: {integrity: sha512-uN+9i8bFT5CUcZfyIEYDrSueACEyKGbUs5kC/72DGlZZoinh84sJfVV0i8UOJD1asdzkvLPBRrKs41kZ8MdEXg==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/node@24.19.2': resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} @@ -2199,6 +2669,9 @@ packages: '@types/three@0.186.0': resolution: {integrity: sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/webxr@0.5.24': resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} @@ -2322,6 +2795,25 @@ packages: cpu: [x64] os: [win32] + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} + + '@vitejs/plugin-react@6.2.1': + resolution: {integrity: sha512-9KyWCij3r0+qLG3Q62vBmv0VyevmYibAtxaCIVK9o0k6CXgaajZRIe8tpfchQcdZArJ5b/7OrIz4W3b2uxVRDA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 || ^0.3.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: '>=0.152.0 <0.157.0' + vite: ^7.3.7 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + '@vitest/mocker@5.0.3': resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: @@ -2360,6 +2852,10 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + am-i-vibing@0.5.0: + resolution: {integrity: sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==} + hasBin: true + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2368,6 +2864,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + anynum@1.0.1: resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} @@ -2379,10 +2879,24 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + astro@7.4.2: + resolution: {integrity: sha512-FAH4cdDlFaVnQGjazLTEIC5vJqGlo5XANc1wIbzQHIqAjJTll/RWTUs+SPNW1JdezG4YGxMZllaE5A41T2kzhA==} + engines: {node: '>=22.23.4', npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + peerDependencies: + '@astrojs/markdown-remark': ^7.3.0 + peerDependenciesMeta: + '@astrojs/markdown-remark': + optional: true + async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} @@ -2396,6 +2910,10 @@ packages: peerDependencies: postcss: ^8.5.23 + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} peerDependencies: @@ -2407,6 +2925,9 @@ packages: babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -2465,6 +2986,9 @@ packages: bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@5.0.12: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} @@ -2501,14 +3025,31 @@ packages: caniuse-lite@1.0.30001820: resolution: {integrity: sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.3.0: resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chart.js@4.6.1: resolution: {integrity: sha512-hJyniucsFmq//EvXYvJ6c6mahPoI3kaxdjVH8awTaJ/fDNWeSqqqArixMJWkWNN6fcYvPU3HneSANFNYtRi61g==} engines: {pnpm: '>=8'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -2529,14 +3070,32 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + compress-commons@7.0.1: resolution: {integrity: sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==} engines: {node: '>=18'} + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie@2.0.2: + resolution: {integrity: sha512-SL0cVuEdETEp7pN0FuZb01toc3vnODEfVUCooxG1SJN0yIirh7JMb9X9bvfCFf0vzzbT68RdbKDHYTiWy7a7iw==} + engines: {node: '>=22'} + copy-anything@4.1.1: resolution: {integrity: sha512-AoT6Imdr98feSpFfmFwTFN73ccdr7uFPf27cBCgYvyyRyn1BzLRxMvrHNmwXO5LJMddRy4Rdhw2b1h7vSMKsEw==} engines: {node: '>=18'} @@ -2562,6 +3121,28 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-select@6.0.0: + resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} + engines: {node: '>= 6'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2588,6 +3169,16 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -2600,21 +3191,45 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devalue@6.0.3: + resolution: {integrity: sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==} + engines: {node: '>=18'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-serializer@3.1.1: resolution: {integrity: sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==} engines: {node: '>=20.19.0'} + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domelementtype@3.0.0: resolution: {integrity: sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==} engines: {node: '>=20.19.0'} + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + domhandler@6.0.1: resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==} engines: {node: '>=20.19.0'} + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + domutils@4.0.2: resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} engines: {node: '>=20.19.0'} @@ -2727,6 +3342,10 @@ packages: sqlite3: optional: true + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -2749,6 +3368,10 @@ packages: resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@8.1.0: resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} engines: {node: '>=20.19.0'} @@ -2756,6 +3379,9 @@ packages: es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-module-lexer@3.0.4: + resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -2782,6 +3408,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} @@ -2799,6 +3428,15 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fast-xml-builder@1.3.1: resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==} @@ -2830,6 +3468,20 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fnv1a-64@0.1.2: + resolution: {integrity: sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2880,6 +3532,13 @@ packages: get-tsconfig@4.14.3: resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} + engines: {node: '>=20.20.0'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + google-auth-library@9.15.1: resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} engines: {node: '>=14'} @@ -2899,16 +3558,34 @@ packages: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + hast-util-to-html@9.0.6: + resolution: {integrity: sha512-1SZUxCN3TtJAE7e2BQN2MBHsoZ1gnmi9GoCP5Di59CSWptIezSAW09yDZyVbanb78V9eCwrKqdQsjtfVTxYHWA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@12.0.0: resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} engines: {node: '>=20.19.0'} + http-cache-semantics@4.3.0: + resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -2928,6 +3605,14 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2944,6 +3629,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-plain-object@5.1.0: resolution: {integrity: sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==} engines: {node: '>=0.10.0'} @@ -2983,6 +3672,9 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -3002,30 +3694,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -3033,6 +3755,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -3040,6 +3769,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -3047,6 +3783,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -3054,22 +3797,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -3078,6 +3844,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + lucide-react@1.48.0: resolution: {integrity: sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==} peerDependencies: @@ -3089,9 +3859,36 @@ packages: magic-string@1.4.3: resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + meshoptimizer@1.1.1: resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.3: + resolution: {integrity: sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3132,6 +3929,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + neotraverse@1.0.1: + resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==} + engines: {node: '>= 10'} + next-auth@5.0.0-beta.32: resolution: {integrity: sha512-CGlChIEWZ6LltNVxrE5yiySMID+Idpmry47JYA5lLwgD8Sx02a8M65VL0TWVz9nbnOioS/tCW/rP/0+mE7Qp4Q==} peerDependencies: @@ -3175,6 +3976,9 @@ packages: sass: optional: true + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -3183,6 +3987,9 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3196,6 +4003,9 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-mock-http@1.0.5: + resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + node-releases@2.0.57: resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} engines: {node: '>=18'} @@ -3208,6 +4018,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + oauth4webapi@3.8.6: resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} @@ -3219,9 +4032,22 @@ packages: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -3251,14 +4077,29 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@7.3.3: + resolution: {integrity: sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==} + engines: {node: '>=20'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-queue@9.3.5: + resolution: {integrity: sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==} + engines: {node: '>=20'} + + p-timeout@7.0.2: + resolution: {integrity: sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==} + engines: {node: '>=20'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -3314,6 +4155,9 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3438,6 +4282,14 @@ packages: engines: {node: '>=14'} hasBin: true + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + process-ancestry@0.1.0: + resolution: {integrity: sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==} + engines: {node: '>=18.0.0'} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -3453,6 +4305,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + qrcode.react@4.2.0: resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} peerDependencies: @@ -3463,6 +4318,9 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + react-chartjs-2@5.3.1: resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} peerDependencies: @@ -3526,6 +4384,19 @@ packages: resolution: {integrity: sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==} engines: {node: '>=18'} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3536,6 +4407,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + retry-request@9.0.1: resolution: {integrity: sha512-4kGHEBl0ME6gR+8QB1sPMyg58jUxLUcCZNeqLDrfqzk0eWQN8XJFmeu7fmZCjlZCRW0S2u64Ik1HkO/0sWgCYQ==} engines: {node: '>=22'} @@ -3544,6 +4418,16 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + rolldown@1.2.16: + resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rolldown@1.3.0: + resolution: {integrity: sha512-7j1jdVlzWzbRat9EDMsB/QmyMcc6hOO0vbcma8LGjBTmKlpS5Hem+5fpntc/HoRV5tLrG+Qo6I1ZHV/ZuL5S8g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.63.5: resolution: {integrity: sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3559,6 +4443,13 @@ packages: resolution: {integrity: sha512-2X5lC8fYla8YaT8g73Nj+HcLWXan5dLyCWLgVvaOmfw7JI78IRsyNtIDytTHdn7hBQbIkCah7Zbmd5jabzeDig==} engines: {node: '>=22.12.0'} + satteri@0.10.5: + resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + scheduler@0.28.0: resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} @@ -3595,10 +4486,21 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@4.6.0: + resolution: {integrity: sha512-imjLsVHgPuWhvpU3P+zOekvMYqnel7QmKsFC4Sn0fCuAvaaalBsxwKEsrU8Ko5BdFnZA6Go/8a0RzahS634UgA==} + engines: {node: '>=20'} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.9.1: + resolution: {integrity: sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==} + engines: {node: '>= 18'} + source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} @@ -3610,6 +4512,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -3636,6 +4541,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3672,6 +4580,11 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + svgo@4.1.0: + resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==} + engines: {node: '>=16'} + hasBin: true + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -3702,10 +4615,17 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@6.2.0: resolution: {integrity: sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==} engines: {node: '>=20.0.0'} + tinyclip@1.0.3: + resolution: {integrity: sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==} + engines: {node: ^16.14.0 || >= 17.3.0} + tinyexec@1.3.1: resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} @@ -3714,6 +4634,9 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyps@0.5.1: + resolution: {integrity: sha512-B6Ykif5HflC3AdDkS4Zst/2muYVfeXoVMy+KorqH80s/1x4vunIORpNySH681X7fjj+dFM9MDKD6/Hsccjb87Q==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3725,6 +4648,12 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -3757,9 +4686,105 @@ packages: engines: {node: '>=16.20.0'} hasBin: true + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + ultrahtml@1.7.0: + resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici@6.29.1: + resolution: {integrity: sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==} + engines: {node: '>=18.17'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@1.0.3: + resolution: {integrity: sha512-XGnEwj5sWdCbiBX01mWLSBncS0yMRjnLygau3IfdOlfjMacNIQvTGW4VH7pWBq+gGWArfSef/CensMLjMmIr/A==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.1.0: + resolution: {integrity: sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==} + + unist-util-visit@5.2.0: + resolution: {integrity: sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==} + + unstorage@1.17.6: + resolution: {integrity: sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + update-browserslist-db@1.3.3: resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} hasBin: true @@ -3793,6 +4818,16 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true + verkit@0.5.0: + resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} + engines: {node: '>=18.12.0'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@7.3.7: resolution: {integrity: sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3833,6 +4868,57 @@ packages: yaml: optional: true + vite@8.3.5: + resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 || ^0.8.0 + esbuild: ^0.25.12 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^7.3.7 + peerDependenciesMeta: + vite: + optional: true + vitest@5.0.3: resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} @@ -3939,13 +5025,25 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} @@ -3954,6 +5052,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + zip-stream@7.0.5: resolution: {integrity: sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==} engines: {node: '>=18'} @@ -3961,6 +5063,9 @@ packages: zod@4.6.6: resolution: {integrity: sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + zxing-wasm@3.1.3: resolution: {integrity: sha512-3lC9BJk4fR5ZJxcGjb0hVnDFOW7KpLXHIebiBmVd4FDRQZVeObztoTKxRUPxlWwSgxrMRONK0u50hBD1aTYEKg==} @@ -3968,6 +5073,113 @@ snapshots: '@alloc/quick-lru@5.3.0': {} + '@astrojs/compiler-binding-android-arm64@0.5.2': + optional: true + + '@astrojs/compiler-binding-darwin-arm64@0.5.2': + optional: true + + '@astrojs/compiler-binding-darwin-x64@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-musl@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-gnu@0.5.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-musl@0.5.2': + optional: true + + '@astrojs/compiler-binding-wasm32-wasi@0.5.2': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.7(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + optional: true + + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.2': + optional: true + + '@astrojs/compiler-binding-win32-x64-msvc@0.5.2': + optional: true + + '@astrojs/compiler-binding@0.5.2': + optionalDependencies: + '@astrojs/compiler-binding-android-arm64': 0.5.2 + '@astrojs/compiler-binding-darwin-arm64': 0.5.2 + '@astrojs/compiler-binding-darwin-x64': 0.5.2 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.5.2 + '@astrojs/compiler-binding-linux-arm64-musl': 0.5.2 + '@astrojs/compiler-binding-linux-x64-gnu': 0.5.2 + '@astrojs/compiler-binding-linux-x64-musl': 0.5.2 + '@astrojs/compiler-binding-wasm32-wasi': 0.5.2 + '@astrojs/compiler-binding-win32-arm64-msvc': 0.5.2 + '@astrojs/compiler-binding-win32-x64-msvc': 0.5.2 + + '@astrojs/compiler-rs@0.5.2': + dependencies: + '@astrojs/compiler-binding': 0.5.2 + + '@astrojs/internal-helpers@0.13.0': + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + picomatch: 4.0.7 + retext-smartypants: 6.2.0 + shiki: 4.6.0 + smol-toml: 1.9.1 + unified: 11.0.5 + yaml: 2.9.1 + + '@astrojs/markdown-satteri@0.4.5': + dependencies: + '@astrojs/internal-helpers': 0.13.0 + '@astrojs/prism': 4.1.1 + github-slugger: 2.0.0 + satteri: 0.10.5 + + '@astrojs/prism@4.1.1': + dependencies: + prismjs: 1.30.0 + + '@astrojs/react@7.1.1(@types/node@24.19.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(babel-plugin-react-compiler@1.0.0)(esbuild@0.25.12)(jiti@2.7.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(tsx@4.23.15)(yaml@2.9.1)': + dependencies: + '@astrojs/internal-helpers': 0.13.0 + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': 6.2.1(babel-plugin-react-compiler@1.0.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + devalue: 6.0.3 + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + ultrahtml: 1.7.0 + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + transitivePeerDependencies: + - '@rolldown/plugin-babel' + - '@types/node' + - '@vitejs/devtools' + - babel-plugin-react-compiler + - esbuild + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + '@astrojs/telemetry@3.4.1': + dependencies: + ci-info: 4.4.0 + dset: 3.1.4 + is-docker: 4.0.0 + package-manager-detector: 1.9.0 + '@auth/core@0.41.3(nodemailer@10.2.1)': dependencies: '@panva/hkdf': 1.2.1 @@ -3988,7 +5200,7 @@ snapshots: '@babel/code-frame@7.29.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -4004,8 +5216,16 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.13': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.13': + dependencies: + '@babel/types': 7.29.11 + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 @@ -4014,7 +5234,7 @@ snapshots: dependencies: '@babel/code-frame': 7.29.0 '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/types': 7.29.11 '@babel/traverse@7.29.0': dependencies: @@ -4033,17 +5253,112 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.13 + '@babel/helper-validator-identifier': 7.29.7 + + '@bruits/satteri-darwin-arm64@0.10.5': + optional: true + + '@bruits/satteri-darwin-x64@0.10.5': + optional: true + + '@bruits/satteri-linux-arm64-gnu@0.10.5': + optional: true + + '@bruits/satteri-linux-arm64-musl@0.10.5': + optional: true + + '@bruits/satteri-linux-x64-gnu@0.10.5': + optional: true + + '@bruits/satteri-linux-x64-musl@0.10.5': + optional: true + + '@bruits/satteri-wasm32-wasi@0.10.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.2.7(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@bruits/satteri-win32-arm64-msvc@0.10.5': + optional: true + + '@bruits/satteri-win32-x64-msvc@0.10.5': + optional: true + + '@capsizecss/unpack@4.0.1': + dependencies: + fontkitten: 1.0.3 + + '@clack/core@1.5.1': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.8.1': + dependencies: + '@clack/core': 1.5.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + '@dimforge/rapier3d-compat@0.12.0': {} '@discoveryjs/json-ext@0.5.7': {} '@drizzle-team/brocli@0.10.2': {} + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.9.1 + optional: true + + '@emnapi/core@1.11.3': + dependencies: + '@emnapi/wasi-threads': 1.2.3 + tslib: 2.9.1 + optional: true + + '@emnapi/core@2.0.0-beta.2': + dependencies: + '@emnapi/wasi-threads': 2.2.0 + tslib: 2.9.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.9.1 + optional: true + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.9.1 optional: true + '@emnapi/runtime@2.0.0-beta.2': + dependencies: + tslib: 2.9.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.9.1 + optional: true + + '@emnapi/wasi-threads@1.2.3': + dependencies: + tslib: 2.9.1 + optional: true + + '@emnapi/wasi-threads@2.2.0': + dependencies: + tslib: 2.9.1 + optional: true + '@epic-web/invariant@1.0.0': {} '@esbuild-kit/core-utils@3.3.2': @@ -4344,6 +5659,27 @@ snapshots: '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true + '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@2.0.0-beta.2)(@emnapi/runtime@2.0.0-beta.2)': + dependencies: + '@emnapi/core': 2.0.0-beta.2 + '@emnapi/runtime': 2.0.0-beta.2 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@napi-rs/wasm-runtime@1.2.7(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@napi-rs/wasm-runtime@1.2.7(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.4 + optional: true + '@next/bundle-analyzer@16.4.3': dependencies: webpack-bundle-analyzer: 4.10.1 @@ -4381,6 +5717,12 @@ snapshots: '@opentelemetry/api@1.9.1': {} + '@oslojs/encoding@1.1.0': {} + + '@oxc-project/types@0.156.0': {} + + '@oxc-project/types@0.157.0': {} + '@oxlint-tsgolint/darwin-arm64@7.0.2005': optional: true @@ -4928,6 +6270,105 @@ snapshots: '@radix-ui/rect@1.1.3': {} + '@rolldown/binding-android-arm-eabi@1.2.16': + optional: true + + '@rolldown/binding-android-arm-eabi@1.3.0': + optional: true + + '@rolldown/binding-android-arm64@1.2.16': + optional: true + + '@rolldown/binding-android-arm64@1.3.0': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.3.0': + optional: true + + '@rolldown/binding-darwin-x64@1.2.16': + optional: true + + '@rolldown/binding-darwin-x64@1.3.0': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.3.0': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.3.0': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.3.0': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.3.0': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.3.0': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.3.0': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.3.0': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.3.0': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.3.0': + optional: true + + '@rolldown/binding-wasm32-wasi@1.3.0': + dependencies: + '@emnapi/core': 2.0.0-beta.2 + '@emnapi/runtime': 2.0.0-beta.2 + '@napi-rs/wasm-runtime': 1.2.6(@emnapi/core@2.0.0-beta.2)(@emnapi/runtime@2.0.0-beta.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.16': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.3.0': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.3.0': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.63.5': optional: true @@ -5003,6 +6444,46 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.63.5': optional: true + '@shikijs/core@4.6.0': + dependencies: + '@shikijs/primitive': 4.6.0 + '@shikijs/types': 4.6.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.6 + + '@shikijs/engine-javascript@4.6.0': + dependencies: + '@shikijs/types': 4.6.0 + '@shikijs/vscode-textmate': 10.1.0 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.6.0': + dependencies: + '@shikijs/types': 4.6.0 + '@shikijs/vscode-textmate': 10.1.0 + + '@shikijs/langs@4.6.0': + dependencies: + '@shikijs/types': 4.6.0 + + '@shikijs/primitive@4.6.0': + dependencies: + '@shikijs/types': 4.6.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.6.0': + dependencies: + '@shikijs/types': 4.6.0 + + '@shikijs/types@4.6.0': + dependencies: + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.1.0': {} + '@stripe/react-stripe-js@6.12.0(@stripe/stripe-js@9.17.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': dependencies: '@stripe/stripe-js': 9.17.0 @@ -5099,13 +6580,20 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/postcss@4.3.3': + '@tailwindcss/postcss@4.3.3': + dependencies: + '@alloc/quick-lru': 5.3.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + postcss: 8.5.28 + tailwindcss: 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))': dependencies: - '@alloc/quick-lru': 5.3.0 '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 - postcss: 8.5.28 tailwindcss: 4.3.3 + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) '@tanstack/query-core@5.105.0': {} @@ -5186,6 +6674,11 @@ snapshots: '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.9.1 + optional: true + '@types/archiver@8.0.0': dependencies: '@types/node': 24.19.2 @@ -5200,8 +6693,24 @@ snapshots: '@types/emscripten@1.41.6': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.9': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + '@types/node@24.19.2': dependencies: undici-types: 7.24.6 @@ -5243,6 +6752,8 @@ snapshots: fflate: 0.8.3 meshoptimizer: 1.1.1 + '@types/unist@3.0.3': {} + '@types/webxr@0.5.24': {} '@typescript/typescript-aix-ppc64@7.0.2': @@ -5305,14 +6816,23 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vitest/mocker@5.0.3(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15))': + '@ungap/structured-clone@1.4.0': {} + + '@vitejs/plugin-react@6.2.1(babel-plugin-react-compiler@1.0.0)(vite@8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + optionalDependencies: + babel-plugin-react-compiler: 1.0.0 + + '@vitest/mocker@5.0.3(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1))': dependencies: '@jridgewell/trace-mapping': 0.3.31 '@vitest/spy': 5.0.3 estree-walker: 3.0.3 magic-string: 1.4.3 optionalDependencies: - vite: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15) + vite: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1) '@vitest/spy@5.0.3': {} @@ -5335,12 +6855,21 @@ snapshots: agent-base@7.1.4: {} + am-i-vibing@0.5.0: + dependencies: + process-ancestry: 0.1.0 + ansi-regex@5.0.1: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + anynum@1.0.1: {} archiver@8.0.0: @@ -5363,8 +6892,99 @@ snapshots: dependencies: tslib: 2.9.1 + aria-query@5.3.2: {} + assertion-error@2.0.1: {} + astro@7.4.2(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1): + dependencies: + '@astrojs/compiler-rs': 0.5.2 + '@astrojs/internal-helpers': 0.13.0 + '@astrojs/markdown-satteri': 0.4.5 + '@astrojs/telemetry': 3.4.1 + '@capsizecss/unpack': 4.0.1 + '@clack/prompts': 1.8.1 + '@oslojs/encoding': 1.1.0 + am-i-vibing: 0.5.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 2.0.0 + cookie: 2.0.2 + devalue: 6.0.3 + diff: 9.0.0 + dset: 3.1.4 + es-module-lexer: 3.0.4 + flattie: 1.1.1 + fontace: 0.4.1 + get-tsconfig: 5.0.0-beta.6 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.3.0 + jsonc-parser: 3.3.1 + magic-string: 1.4.3 + magicast: 0.5.5 + mrmime: 2.0.1 + neotraverse: 1.0.1 + obug: 3.0.0 + p-limit: 7.3.3 + p-queue: 9.3.5 + package-manager-detector: 1.9.0 + piccolore: 0.1.3 + picomatch: 4.0.7 + rolldown: 1.3.0 + shiki: 4.6.0 + smol-toml: 1.9.1 + svgo: 4.1.0 + tinyclip: 1.0.3 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyps: 0.5.1 + ultrahtml: 1.7.0 + unifont: 1.0.3 + unstorage: 1.17.6 + verkit: 0.5.0 + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) + vitefu: 1.1.3(vite@8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 22.0.0 + zod: 4.6.6 + optionalDependencies: + sharp: 0.35.5(@types/node@24.19.2) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - aws4fetch + - db0 + - esbuild + - idb-keyval + - ioredis + - jiti + - less + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - uploadthing + - yaml + async-retry@1.3.3: dependencies: retry: 0.13.1 @@ -5380,11 +7000,15 @@ snapshots: postcss: 8.5.28 postcss-value-parser: 4.2.0 + axobject-query@4.1.0: {} + b4a@1.8.1: {} babel-plugin-react-compiler@1.0.0: dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.11 + + bail@2.0.2: {} balanced-match@4.0.4: {} @@ -5429,6 +7053,8 @@ snapshots: bintrees@1.0.2: {} + boolbase@1.0.0: {} + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -5462,12 +7088,24 @@ snapshots: caniuse-lite@1.0.30001820: {} + ccount@2.0.1: {} + chai@6.3.0: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + chart.js@4.6.1: dependencies: '@kurkle/color': 0.3.4 + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + ci-info@4.4.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -5488,8 +7126,14 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + commander@7.2.0: {} + common-ancestor-path@2.0.0: {} + compress-commons@7.0.1: dependencies: crc-32: 1.2.2 @@ -5498,6 +7142,10 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 + cookie-es@1.2.3: {} + + cookie@2.0.2: {} + copy-anything@4.1.1: {} core-util-is@1.0.3: {} @@ -5520,6 +7168,34 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-select@6.0.0: + dependencies: + boolbase: 1.0.0 + css-what: 7.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.2 + + css-what@7.0.0: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + csstype@3.2.3: {} data-uri-to-buffer@4.0.1: {} @@ -5534,26 +7210,58 @@ snapshots: decamelize@1.2.0: {} + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + detect-libc@1.0.3: {} detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + devalue@6.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@9.0.0: {} + dijkstrajs@1.0.3: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dom-serializer@3.1.1: dependencies: domelementtype: 3.0.0 domhandler: 6.0.1 entities: 8.1.0 + domelementtype@2.3.0: {} + domelementtype@3.0.0: {} + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + domhandler@6.0.1: dependencies: domelementtype: 3.0.0 + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils@4.0.2: dependencies: dom-serializer: 3.1.1 @@ -5578,6 +7286,8 @@ snapshots: pg: 8.23.0 postgres: 3.4.9 + dset@3.1.4: {} + duplexer@0.1.2: {} duplexify@4.1.3: @@ -5604,10 +7314,14 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + entities@4.5.0: {} + entities@8.1.0: {} es-module-lexer@2.3.2: {} + es-module-lexer@3.0.4: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -5652,6 +7366,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@5.0.4: {} + events-universal@1.0.1: dependencies: bare-events: 2.9.2 @@ -5666,6 +7382,16 @@ snapshots: fast-fifo@1.3.2: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fast-xml-builder@1.3.1: dependencies: path-expression-matcher: 1.6.2 @@ -5700,6 +7426,18 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + flattie@1.1.1: {} + + fnv1a-64@0.1.2: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -5750,6 +7488,12 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-slugger@2.0.0: {} + google-auth-library@9.15.1: dependencies: base64-js: 1.5.1 @@ -5778,10 +7522,44 @@ snapshots: dependencies: duplexer: 0.1.2 + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.5 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + hast-util-to-html@9.0.6: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + html-entities@2.6.0: {} html-escaper@2.0.2: {} + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + htmlparser2@12.0.0: dependencies: domelementtype: 3.0.0 @@ -5789,6 +7567,8 @@ snapshots: domutils: 4.0.2 entities: 8.1.0 + http-cache-semantics@4.3.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -5809,6 +7589,10 @@ snapshots: inherits@2.0.4: {} + iron-webcrypto@1.2.1: {} + + is-docker@4.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -5819,6 +7603,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-plain-object@5.1.0: {} is-stream@2.0.1: {} @@ -5843,6 +7629,8 @@ snapshots: dependencies: bignumber.js: 9.3.1 + jsonc-parser@3.3.1: {} + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -5865,36 +7653,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -5911,6 +7732,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -5919,6 +7756,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lru-cache@11.5.3: {} + lucide-react@1.48.0(react@19.3.1): dependencies: react: 19.3.1 @@ -5931,8 +7770,47 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + magicast@0.5.5: + dependencies: + '@babel/parser': 7.29.13 + '@babel/types': 7.29.11 + source-map-js: 1.2.2 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.4.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.2.0 + vfile: 6.0.3 + + mdn-data@2.0.28: {} + + mdn-data@2.27.1: {} + meshoptimizer@1.1.1: {} + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.3: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5961,6 +7839,8 @@ snapshots: nanoid@3.3.20: {} + neotraverse@1.0.1: {} + next-auth@5.0.0-beta.32(next@16.4.4(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1))(nodemailer@10.2.1)(react@19.3.1): dependencies: '@auth/core': 0.41.3(nodemailer@10.2.1) @@ -6001,10 +7881,16 @@ snapshots: - '@types/node' - babel-plugin-macros + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + node-addon-api@7.1.1: {} node-domexception@1.0.0: {} + node-fetch-native@1.6.7: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -6015,22 +7901,44 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-mock-http@1.0.5: {} + node-releases@2.0.57: {} nodemailer@10.2.1: {} normalize-path@3.0.0: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + oauth4webapi@3.8.6: {} object-assign@4.1.1: {} obug@2.2.1: {} + obug@3.0.0: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + once@1.4.0: dependencies: wrappy: 1.0.2 + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + opener@1.5.2: {} oxlint-tsgolint@7.0.2005: @@ -6073,12 +7981,25 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@7.3.3: + dependencies: + yocto-queue: 1.2.2 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 + p-queue@9.3.5: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.2 + + p-timeout@7.0.2: {} + p-try@2.2.0: {} + package-manager-detector@1.9.0: {} + pako@1.0.11: {} parse-srcset@1.0.2: {} @@ -6131,6 +8052,8 @@ snapshots: dependencies: split2: 4.2.0 + piccolore@0.1.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -6185,6 +8108,10 @@ snapshots: prettier@3.9.10: {} + prismjs@1.30.0: {} + + process-ancestry@0.1.0: {} + process-nextick-args@2.0.1: {} process@0.11.10: {} @@ -6200,6 +8127,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.2.0: {} + qrcode.react@4.2.0(react@19.3.1): dependencies: react: 19.3.1 @@ -6210,6 +8139,8 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + radix3@1.1.2: {} + react-chartjs-2@5.3.1(chart.js@4.6.1)(react@19.3.1): dependencies: chart.js: 4.6.1 @@ -6279,12 +8210,30 @@ snapshots: dependencies: minimatch: 11.0.3 + readdirp@5.1.1: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + require-directory@2.1.1: {} require-main-filename@2.0.0: {} resolve-pkg-maps@1.0.0: {} + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.2.0 + retry-request@9.0.1: dependencies: extend: 3.0.2 @@ -6294,6 +8243,49 @@ snapshots: retry@0.13.1: {} + rolldown@1.2.16: + dependencies: + '@oxc-project/types': 0.156.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.16 + '@rolldown/binding-android-arm64': 1.2.16 + '@rolldown/binding-darwin-arm64': 1.2.16 + '@rolldown/binding-darwin-x64': 1.2.16 + '@rolldown/binding-freebsd-x64': 1.2.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.16 + '@rolldown/binding-linux-arm64-gnu': 1.2.16 + '@rolldown/binding-linux-arm64-musl': 1.2.16 + '@rolldown/binding-linux-ppc64-gnu': 1.2.16 + '@rolldown/binding-linux-s390x-gnu': 1.2.16 + '@rolldown/binding-linux-x64-gnu': 1.2.16 + '@rolldown/binding-linux-x64-musl': 1.2.16 + '@rolldown/binding-openharmony-arm64': 1.2.16 + '@rolldown/binding-win32-arm64-msvc': 1.2.16 + '@rolldown/binding-win32-x64-msvc': 1.2.16 + + rolldown@1.3.0: + dependencies: + '@oxc-project/types': 0.157.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.3.0 + '@rolldown/binding-android-arm64': 1.3.0 + '@rolldown/binding-darwin-arm64': 1.3.0 + '@rolldown/binding-darwin-x64': 1.3.0 + '@rolldown/binding-freebsd-x64': 1.3.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.3.0 + '@rolldown/binding-linux-arm64-gnu': 1.3.0 + '@rolldown/binding-linux-arm64-musl': 1.3.0 + '@rolldown/binding-linux-ppc64-gnu': 1.3.0 + '@rolldown/binding-linux-s390x-gnu': 1.3.0 + '@rolldown/binding-linux-x64-gnu': 1.3.0 + '@rolldown/binding-linux-x64-musl': 1.3.0 + '@rolldown/binding-openharmony-arm64': 1.3.0 + '@rolldown/binding-wasm32-wasi': 1.3.0 + '@rolldown/binding-win32-arm64-msvc': 1.3.0 + '@rolldown/binding-win32-x64-msvc': 1.3.0 + rollup@4.63.5: dependencies: '@types/estree': 1.0.9 @@ -6339,6 +8331,25 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.5.30 + satteri@0.10.5: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + optionalDependencies: + '@bruits/satteri-darwin-arm64': 0.10.5 + '@bruits/satteri-darwin-x64': 0.10.5 + '@bruits/satteri-linux-arm64-gnu': 0.10.5 + '@bruits/satteri-linux-arm64-musl': 0.10.5 + '@bruits/satteri-linux-x64-gnu': 0.10.5 + '@bruits/satteri-linux-x64-musl': 0.10.5 + '@bruits/satteri-wasm32-wasi': 0.10.5 + '@bruits/satteri-win32-arm64-msvc': 0.10.5 + '@bruits/satteri-win32-x64-msvc': 0.10.5 + + sax@1.6.1: {} + scheduler@0.28.0: {} sdp@3.2.2: {} @@ -6390,12 +8401,27 @@ snapshots: shebang-regex@3.0.0: {} + shiki@4.6.0: + dependencies: + '@shikijs/core': 4.6.0 + '@shikijs/engine-javascript': 4.6.0 + '@shikijs/engine-oniguruma': 4.6.0 + '@shikijs/langs': 4.6.0 + '@shikijs/themes': 4.6.0 + '@shikijs/types': 4.6.0 + '@shikijs/vscode-textmate': 10.1.0 + '@types/hast': 3.0.5 + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 + sisteransi@1.0.5: {} + + smol-toml@1.9.1: {} + source-map-js@1.2.2: {} source-map-support@0.5.21: @@ -6405,6 +8431,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + split2@4.2.0: {} std-env@4.3.0: {} @@ -6438,6 +8466,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -6461,6 +8494,16 @@ snapshots: dependencies: copy-anything: 4.1.1 + svgo@4.1.0: + dependencies: + commander: 11.1.0 + css-select: 6.0.0 + css-tree: 3.2.1 + css-what: 7.0.0 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.1 + tagged-tag@1.0.0: {} tailwind-merge@2.6.1: {} @@ -6506,8 +8549,12 @@ snapshots: transitivePeerDependencies: - react-native-b4a + tiny-inflate@1.0.3: {} + tinybench@6.2.0: {} + tinyclip@1.0.3: {} + tinyexec@1.3.1: {} tinyglobby@0.2.17: @@ -6515,6 +8562,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinyps@0.5.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6523,6 +8572,10 @@ snapshots: tr46@0.0.3: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + tslib@1.14.1: {} tslib@2.8.1: {} @@ -6581,8 +8634,67 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 + ufo@1.6.4: {} + + ultrahtml@1.7.0: {} + + uncrypto@0.1.3: {} + undici-types@7.24.6: {} + undici@6.29.1: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@1.0.3: + dependencies: + css-tree: 3.2.1 + fnv1a-64: 0.1.2 + std-env: 4.3.0 + undici: 6.29.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.2.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.1.0 + + unstorage@1.17.6: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.5.3 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + update-browserslist-db@1.3.3(browserslist@4.29.1): dependencies: browserslist: 4.29.1 @@ -6608,7 +8720,19 @@ snapshots: uuid@11.1.1: {} - vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15): + verkit@0.5.0: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.7) @@ -6620,13 +8744,33 @@ snapshots: '@types/node': 24.19.2 fsevents: 2.3.3 jiti: 2.7.0 - lightningcss: 1.32.0 + lightningcss: 1.33.0 + tsx: 4.23.15 + yaml: 2.9.1 + + vite@8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.30 + rolldown: 1.2.16 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.2 + esbuild: 0.25.12 + fsevents: 2.3.3 + jiti: 2.7.0 tsx: 4.23.15 + yaml: 2.9.1 + + vitefu@1.1.3(vite@8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)): + optionalDependencies: + vite: 8.3.5(@types/node@24.19.2)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1) - vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15)): + vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.2)(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.3(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15)) + '@vitest/mocker': 5.0.3(vite@7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1)) chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -6637,7 +8781,7 @@ snapshots: tinybench: 6.2.0 tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.15) + vite: 7.3.7(@types/node@24.19.2)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.15)(yaml@2.9.1) why-is-node-running: 3.2.1 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -6701,13 +8845,19 @@ snapshots: xtend@4.0.2: {} + xxhash-wasm@1.1.0: {} + y18n@4.0.3: {} + yaml@2.9.1: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 + yargs-parser@22.0.0: {} + yargs@15.4.1: dependencies: cliui: 6.0.0 @@ -6724,6 +8874,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + zip-stream@7.0.5: dependencies: compress-commons: 7.0.1 @@ -6732,6 +8884,8 @@ snapshots: zod@4.6.6: {} + zwitch@2.0.4: {} + zxing-wasm@3.1.3: dependencies: '@types/emscripten': 1.41.6 diff --git a/sites/hacklytics2027/.gitignore b/sites/hacklytics2027/.gitignore index 6a40d1a7..9c39287c 100644 --- a/sites/hacklytics2027/.gitignore +++ b/sites/hacklytics2027/.gitignore @@ -13,8 +13,8 @@ # testing /coverage -# next.js -/.next/ +# astro +/.astro/ # Build output is included for Firebase deployment # /out/ @@ -39,7 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts # Playwright /test-results/ diff --git a/sites/hacklytics2027/.oxlintrc.json b/sites/hacklytics2027/.oxlintrc.json index 54ecaa0c..4fb830b8 100644 --- a/sites/hacklytics2027/.oxlintrc.json +++ b/sites/hacklytics2027/.oxlintrc.json @@ -1,8 +1,7 @@ { "extends": [ "../../tooling/oxlint/base.json", - "../../tooling/oxlint/react.json", - "../../tooling/oxlint/next.json" + "../../tooling/oxlint/react.json" ], "ignorePatterns": ["**/*.config.*", "public/sw.js", "scripts/**"] } diff --git a/sites/hacklytics2027/README.md b/sites/hacklytics2027/README.md index ff05cf29..e485879e 100644 --- a/sites/hacklytics2027/README.md +++ b/sites/hacklytics2027/README.md @@ -1,12 +1,14 @@ # Hacklytics 2027 (`hacklytics2027`) -Static marketing site for Hacklytics 2027 (Digital Bloom). No database. Interest and registration go to the portal. +Static marketing site for Hacklytics 2027 (Digital Bloom), built with Astro. No database. Interest and registration go to the portal. **Full reference:** [docs/sites/hacklytics2027.md](../../docs/sites/hacklytics2027.md) ```bash -pnpm --filter hacklytics2027 dev # http://localhost:3000 -pnpm --filter hacklytics2027 build # static export → out/ +pnpm --filter hacklytics2027 dev # http://localhost:3000 +pnpm --filter hacklytics2027 build # static files -> out/ +pnpm --filter hacklytics2027 preview # serve out/ +pnpm --filter hacklytics2027 typecheck # tsc --noEmit over src/ ``` -Outbound links are centralized in `lib/links.ts`. +Pages are `src/pages/` (`index.astro`, `404.astro`). Most of the site renders at build time and ships no JavaScript; the flower beds are the only React, and each hydrates on its own. Outbound links are centralized in `src/lib/links.ts`. diff --git a/sites/hacklytics2027/app/layout.tsx b/sites/hacklytics2027/app/layout.tsx deleted file mode 100644 index 29b13184..00000000 --- a/sites/hacklytics2027/app/layout.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import "./fonts.generated.css"; -import "./globals.css"; -import type { Metadata, Viewport } from "next"; -import { preload } from "react-dom"; -import Navbar from "../components/Navbar"; -import ServiceWorkerRegistrar from "../components/ServiceWorkerRegistrar"; -import ThemeScript from "../components/ThemeScript"; -import Footer from "../components/Footer"; -import { FONT_PRELOADS } from "../lib/fonts.generated"; -import { INTEREST_URL } from "../lib/links"; -import { GROUND } from "../lib/theme"; - -// Fonts: Instrument Sans for text and UI, Silkscreen for display type (hero, -// section titles, track names, countdown). Silkscreen has one weight, so -// nothing sets it bold: a synthesized bold smears the pixel cells. Both come -// from scripts/subset-fonts.py, which says when to rerun it. - -// The browser bar follows the device setting by default; ThemeToggle repeats -// these when a choice is made. -export const viewport: Viewport = { - themeColor: [ - { media: "(prefers-color-scheme: light)", color: GROUND.light }, - { media: "(prefers-color-scheme: dark)", color: GROUND.dark }, - ], -}; - -export const metadata: Metadata = { - title: "Hacklytics 2027: Digital Bloom · Data Science Hackathon at Georgia Tech", - description: - "Hacklytics 2027 is a free, 36-hour data science and AI hackathon run by Data Science @ GT at Georgia Tech in Atlanta, February 26–28, 2027.", - keywords: ["hackathon", "data science", "machine learning", "AI", "Georgia Tech", "Atlanta", "coding", "competition"], - authors: [{ name: "Data Science @ GT" }], - openGraph: { - title: "Hacklytics 2027: Digital Bloom", - description: "A 36-hour data science and AI hackathon at Georgia Tech. Room for 1,000+ hackers in Atlanta.", - url: "https://hacklytics.io", - siteName: "Hacklytics", - images: [ - { - url: "/og-image.jpg", - width: 1200, - height: 630, - alt: "Hacklytics 2027 Digital Bloom", - }, - ], - type: "website", - locale: "en_US", - }, - twitter: { - card: "summary_large_image", - title: "Hacklytics 2027: Digital Bloom", - description: "A 36-hour data science and AI hackathon at Georgia Tech.", - images: ["/og-image.jpg"], - creator: "@datasciencegt", - }, - metadataBase: new URL("https://hacklytics.io"), -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - // preload(), not a : Next and React each emit a in . - FONT_PRELOADS.forEach((href) => preload(href, { as: "font", type: "font/woff2", crossOrigin: "" })); - - // JSON-LD structured data for SEO Event Schema - const jsonLd = { - "@context": "https://schema.org", - "@type": "Event", - name: "Hacklytics 2027: Digital Bloom", - startDate: "2027-02-26T17:00:00-05:00", - endDate: "2027-02-28T16:00:00-05:00", - eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", - eventStatus: "https://schema.org/EventScheduled", - location: { - "@type": "Place", - name: "Klaus Advanced Computing Building", - address: { - "@type": "PostalAddress", - streetAddress: "266 Ferst Dr NW", - addressLocality: "Atlanta", - postalCode: "30332", - addressRegion: "GA", - addressCountry: "US" - } - }, - image: [ - "https://hacklytics.io/og-image.jpg" - ], - description: "Data Science @ GT runs the Southeast’s data science hackathon: 36 hours of data science and AI at Georgia Tech.", - offers: { - "@type": "Offer", - url: INTEREST_URL, - price: "0", - priceCurrency: "USD", - // PreOrder, not InStock: registration has not opened, and the link behind - // this offer joins an interest list rather than securing a place. Search - // results that promise "register now" against a page that cannot are the - // kind of thing that gets rich results pulled. - availability: "https://schema.org/PreOrder", - validFrom: "2026-08-01T00:00:00-04:00" - }, - organizer: { - "@type": "Organization", - name: "Data Science @ GT", - url: "https://datasciencegt.org" - } - }; - - return ( - // data-scroll-behavior: route changes jump, and only in-page links glide. - - - - diff --git a/sites/hacklytics2027/components/HomeSections.tsx b/sites/hacklytics2027/src/components/HomeSections.astro similarity index 57% rename from sites/hacklytics2027/components/HomeSections.tsx rename to sites/hacklytics2027/src/components/HomeSections.astro index 30725cb4..bb596e2a 100644 --- a/sites/hacklytics2027/components/HomeSections.tsx +++ b/sites/hacklytics2027/src/components/HomeSections.astro @@ -1,19 +1,17 @@ +--- import AboutSection from "./sections/AboutSection"; import TracksSection from "./sections/TracksSection"; import ScheduleSection from "./sections/Schedule/Schedule"; import FAQSection from "./sections/FAQSection"; import PrizeAndSpeakerSection from "./sections/PrizeAndSpeakerSection"; import SponsorsSection from "./sections/Sponsor"; +--- -export default function HomeSections() { - return ( -
- - - - - - -
- ); -} +
+ + + + + + +
diff --git a/sites/hacklytics2027/src/components/MlhBadge.astro b/sites/hacklytics2027/src/components/MlhBadge.astro new file mode 100644 index 00000000..eea9c0df --- /dev/null +++ b/sites/hacklytics2027/src/components/MlhBadge.astro @@ -0,0 +1,12 @@ +--- +// Hangs from the top right of the page it sits on, which must be `relative`. +--- + + + Major League Hacking 2027 + diff --git a/sites/hacklytics2027/src/components/Navbar.astro b/sites/hacklytics2027/src/components/Navbar.astro new file mode 100644 index 00000000..a4cd56d2 --- /dev/null +++ b/sites/hacklytics2027/src/components/Navbar.astro @@ -0,0 +1,135 @@ +--- +import { INTEREST_URL } from "@/lib/links"; +import { MenuBed } from "./pixel/GardenBeds"; +import ThemeToggle from "./ThemeToggle.astro"; + +const navItems = [ + { name: "About", href: "/#about" }, + { name: "Tracks", href: "/#tracks" }, + { name: "Prizes", href: "/#prizes" }, + { name: "Schedule", href: "/#schedule" }, + { name: "Sponsors", href: "/#sponsors" }, + { name: "FAQ", href: "/#faqs" }, +]; + +// The menu is a tinted layer over the page: hidden only by opacity, and the +// open/hidden/scrolled states ride on data attributes that scripts/navbar.ts +// flips, so the markup here is the whole closed, top-of-page state. +--- + + + +{/* Hidden only by opacity, so without inert its links would stay in the tab + order and the accessibility tree while closed. */} + + + diff --git a/sites/hacklytics2027/src/components/ThemeToggle.astro b/sites/hacklytics2027/src/components/ThemeToggle.astro new file mode 100644 index 00000000..fadf59e5 --- /dev/null +++ b/sites/hacklytics2027/src/components/ThemeToggle.astro @@ -0,0 +1,62 @@ +--- +// Night / day switch. The greenhouse at night stays the default look for +// anyone whose system is dark; this lets either side pick the other. +// scripts/theme-toggle.ts wires every [data-theme-toggle] button. The icon +// follows the theme in CSS (.theme-sun, .theme-moon in styles/globals.css), so +// it is right on the first paint, before any script runs. +interface Props { + class?: string; +} +const { class: className = "" } = Astro.props; +--- + + diff --git a/sites/hacklytics2027/src/components/pixel/GardenBeds.tsx b/sites/hacklytics2027/src/components/pixel/GardenBeds.tsx new file mode 100644 index 00000000..1e3759b2 --- /dev/null +++ b/sites/hacklytics2027/src/components/pixel/GardenBeds.tsx @@ -0,0 +1,71 @@ +import { BED_SIZE, bloomSet, plantBed, withBlooms } from "./beds"; +import { PixelBed } from "./PixelBed"; +import { useInBloom } from "./season"; + +/* + * The four flower beds that need React. Each is its own island: the share in + * bloom comes from the clock and a click plants a flower, neither of which the + * static HTML can know. The seeds match the markup these beds replaced, so the + * same plants stand in the same places. + */ + +const Heading = () =>

Plant a flower

; + +// The static HTML has no count yet. A blank line holds the caption's place, so +// the bed does not grow when hydration fills it in. +const NO_CAPTION = " "; + +const HERO_BED = plantBed(0); +const MENU_BED = plantBed(2); +const FOOTER_BED = plantBed(3); + +export function HeroBed() { + const inBloom = useInBloom(); + return ( + } + caption={ + inBloom + ? `${inBloom} of ${BED_SIZE} in bloom · fills in as Feb 26 gets closer` + : NO_CAPTION + } + /> + ); +} + +export function MenuBed() { + const inBloom = useInBloom(); + return ; +} + +export function FooterBed() { + const inBloom = useInBloom(); + return ( + + ); +} + +/* "Nothing grows at this address": the only bed on the site with no plants. */ +export function NotFoundBed() { + return ( + } + /> + ); +} diff --git a/sites/hacklytics2027/components/pixel/PixelBed.tsx b/sites/hacklytics2027/src/components/pixel/PixelBed.tsx similarity index 99% rename from sites/hacklytics2027/components/pixel/PixelBed.tsx rename to sites/hacklytics2027/src/components/pixel/PixelBed.tsx index 918c22a5..119ea886 100644 --- a/sites/hacklytics2027/components/pixel/PixelBed.tsx +++ b/sites/hacklytics2027/src/components/pixel/PixelBed.tsx @@ -1,4 +1,3 @@ -"use client"; import React, { useRef, useState, useSyncExternalStore } from "react"; import { BloomSprite } from "./PixelSprite"; diff --git a/sites/hacklytics2027/components/pixel/PixelSprite.tsx b/sites/hacklytics2027/src/components/pixel/PixelSprite.tsx similarity index 100% rename from sites/hacklytics2027/components/pixel/PixelSprite.tsx rename to sites/hacklytics2027/src/components/pixel/PixelSprite.tsx diff --git a/sites/hacklytics2027/components/pixel/beds.ts b/sites/hacklytics2027/src/components/pixel/beds.ts similarity index 100% rename from sites/hacklytics2027/components/pixel/beds.ts rename to sites/hacklytics2027/src/components/pixel/beds.ts diff --git a/sites/hacklytics2027/src/components/pixel/event.ts b/sites/hacklytics2027/src/components/pixel/event.ts new file mode 100644 index 00000000..a27a09fa --- /dev/null +++ b/sites/hacklytics2027/src/components/pixel/event.ts @@ -0,0 +1,4 @@ +// Check-in opens 5pm ET (the JSON-LD startDate). Without an offset the string +// meant midnight in each visitor's own timezone. Its own module so the +// countdown script can read it without pulling in React. +export const EVENT_START = new Date("2027-02-26T17:00:00-05:00"); diff --git a/sites/hacklytics2027/components/pixel/season.ts b/sites/hacklytics2027/src/components/pixel/season.ts similarity index 83% rename from sites/hacklytics2027/components/pixel/season.ts rename to sites/hacklytics2027/src/components/pixel/season.ts index 31547c12..393bf26f 100644 --- a/sites/hacklytics2027/components/pixel/season.ts +++ b/sites/hacklytics2027/src/components/pixel/season.ts @@ -1,9 +1,7 @@ import { useSyncExternalStore } from "react"; import { BED_SIZE } from "./beds"; +import { EVENT_START } from "./event"; -// Check-in opens 5pm ET (the JSON-LD startDate). Without an offset the string -// meant midnight in each visitor's own timezone. -export const EVENT_START = new Date("2027-02-26T17:00:00-05:00"); // When the 2027 season opened. Beds fill in from here to EVENT_START: a tenth // of each in bloom at the start, all of it on the day. const SEASON_START = new Date("2026-09-01T00:00:00-04:00"); diff --git a/sites/hacklytics2027/components/pixel/spriteUri.ts b/sites/hacklytics2027/src/components/pixel/spriteUri.ts similarity index 100% rename from sites/hacklytics2027/components/pixel/spriteUri.ts rename to sites/hacklytics2027/src/components/pixel/spriteUri.ts diff --git a/sites/hacklytics2027/components/pixel/sprites.ts b/sites/hacklytics2027/src/components/pixel/sprites.ts similarity index 100% rename from sites/hacklytics2027/components/pixel/sprites.ts rename to sites/hacklytics2027/src/components/pixel/sprites.ts diff --git a/sites/hacklytics2027/components/pixel/useInView.ts b/sites/hacklytics2027/src/components/pixel/useInView.ts similarity index 98% rename from sites/hacklytics2027/components/pixel/useInView.ts rename to sites/hacklytics2027/src/components/pixel/useInView.ts index 98d1dad9..b86d1faa 100644 --- a/sites/hacklytics2027/components/pixel/useInView.ts +++ b/sites/hacklytics2027/src/components/pixel/useInView.ts @@ -1,4 +1,3 @@ -"use client"; import { useEffect, useRef } from "react"; diff --git a/sites/hacklytics2027/components/sections/AboutSection.tsx b/sites/hacklytics2027/src/components/sections/AboutSection.tsx similarity index 100% rename from sites/hacklytics2027/components/sections/AboutSection.tsx rename to sites/hacklytics2027/src/components/sections/AboutSection.tsx diff --git a/sites/hacklytics2027/components/sections/FAQSection.tsx b/sites/hacklytics2027/src/components/sections/FAQSection.tsx similarity index 97% rename from sites/hacklytics2027/components/sections/FAQSection.tsx rename to sites/hacklytics2027/src/components/sections/FAQSection.tsx index 54d103ab..7a66477b 100644 --- a/sites/hacklytics2027/components/sections/FAQSection.tsx +++ b/sites/hacklytics2027/src/components/sections/FAQSection.tsx @@ -1,5 +1,4 @@ import React from "react"; -import Link from "next/link"; import { INTEREST_URL } from "@/lib/links"; const faqItems: { q: string; a: React.ReactNode }[] = [ @@ -32,14 +31,14 @@ const faqItems: { q: string; a: React.ReactNode }[] = [ a: ( <> Applications are not open yet.{" "} - Get notified - {" "} + {" "} and we’ll tell you when they do. ), @@ -62,14 +61,14 @@ const faqItems: { q: string; a: React.ReactNode }[] = [ a: ( <> All participants follow the{" "} - MLH Code of Conduct - + . Harassment of any kind is not tolerated. ), diff --git a/sites/hacklytics2027/components/sections/PrizeAndSpeakerSection.tsx b/sites/hacklytics2027/src/components/sections/PrizeAndSpeakerSection.tsx similarity index 100% rename from sites/hacklytics2027/components/sections/PrizeAndSpeakerSection.tsx rename to sites/hacklytics2027/src/components/sections/PrizeAndSpeakerSection.tsx diff --git a/sites/hacklytics2027/components/sections/Schedule/Schedule.tsx b/sites/hacklytics2027/src/components/sections/Schedule/Schedule.tsx similarity index 95% rename from sites/hacklytics2027/components/sections/Schedule/Schedule.tsx rename to sites/hacklytics2027/src/components/sections/Schedule/Schedule.tsx index d2b6fd82..086b60b4 100644 --- a/sites/hacklytics2027/components/sections/Schedule/Schedule.tsx +++ b/sites/hacklytics2027/src/components/sections/Schedule/Schedule.tsx @@ -1,7 +1,5 @@ -"use client"; import React from "react"; import { BloomSprite } from "../../pixel/PixelSprite"; -import { useBloomInView } from "../../pixel/useInView"; import { BUD, TULIP } from "../../pixel/sprites"; import { board } from "./data"; @@ -26,7 +24,6 @@ const split = (when: string) => { export default function Schedule() { const rows = board.map((row) => ({ ...row, ...split(row.when) })); - const seedlingRef = useBloomInView(); return (
@@ -40,7 +37,7 @@ export default function Schedule() { {/* A bud where hacking begins, in bloom where it ends. */} -
+

(); - + // ribbon pink one after another, left to right. scripts/bloom.ts flips + // data-bloom on the first time the row is in view. return (

@@ -28,8 +25,8 @@ export default function TracksSection() { a row gap, which the subgrid would also put between a title and its description; -mb-14 takes it back off the last row. */}
    {tracks.map((t, i) => ( diff --git a/sites/hacklytics2027/src/layouts/Layout.astro b/sites/hacklytics2027/src/layouts/Layout.astro new file mode 100644 index 00000000..72bb7ade --- /dev/null +++ b/sites/hacklytics2027/src/layouts/Layout.astro @@ -0,0 +1,120 @@ +--- +import "../styles/fonts.generated.css"; +import "../styles/globals.css"; +import Footer from "../components/Footer.astro"; +import Navbar from "../components/Navbar.astro"; +import { FONT_PRELOADS } from "../lib/fonts.generated"; +import { INTEREST_URL } from "../lib/links"; +import { GROUND, THEME_KEY } from "../lib/theme"; + +// Fonts: Instrument Sans for text and UI, Silkscreen for display type (hero, +// section titles, track names, countdown). Silkscreen has one weight, so +// nothing sets it bold: a synthesized bold smears the pixel cells. Both come +// from scripts/subset-fonts.py, which says when to rerun it. + +interface Props { + /** Whether this is the home page; the footer's call to action and bed only belong there. */ + home?: boolean; +} +const { home = false } = Astro.props; + +const SITE = "https://hacklytics.io"; +const title = "Hacklytics 2027: Digital Bloom · Data Science Hackathon at Georgia Tech"; +const description = + "Hacklytics 2027 is a free, 36-hour data science and AI hackathon run by Data Science @ GT at Georgia Tech in Atlanta, February 26–28, 2027."; +const keywords = ["hackathon", "data science", "machine learning", "AI", "Georgia Tech", "Atlanta", "coding", "competition"]; + +// Runs before first paint so a saved light/dark choice never flashes the other +// theme, and points the theme-color metas at it so the bar matches the page. +// Kept tiny and dependency-free. +const themeScript = `try{var t=localStorage.getItem("${THEME_KEY}");if(t==="light"||t==="dark"){document.documentElement.dataset.theme=t;document.querySelectorAll('meta[name="theme-color"]').forEach(function(m){m.content=t==="light"?"${GROUND.light}":"${GROUND.dark}"})}}catch(e){}`; + +// JSON-LD structured data for SEO Event Schema +const jsonLd = { + "@context": "https://schema.org", + "@type": "Event", + name: "Hacklytics 2027: Digital Bloom", + startDate: "2027-02-26T17:00:00-05:00", + endDate: "2027-02-28T16:00:00-05:00", + eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", + eventStatus: "https://schema.org/EventScheduled", + location: { + "@type": "Place", + name: "Klaus Advanced Computing Building", + address: { + "@type": "PostalAddress", + streetAddress: "266 Ferst Dr NW", + addressLocality: "Atlanta", + postalCode: "30332", + addressRegion: "GA", + addressCountry: "US", + }, + }, + image: [`${SITE}/og-image.jpg`], + description: "Data Science @ GT runs the Southeast’s data science hackathon: 36 hours of data science and AI at Georgia Tech.", + offers: { + "@type": "Offer", + url: INTEREST_URL, + price: "0", + priceCurrency: "USD", + // PreOrder, not InStock: registration has not opened, and the link behind + // this offer joins an interest list rather than securing a place. Search + // results that promise "register now" against a page that cannot are the + // kind of thing that gets rich results pulled. + availability: "https://schema.org/PreOrder", + validFrom: "2026-08-01T00:00:00-04:00", + }, + organizer: { + "@type": "Organization", + name: "Data Science @ GT", + url: "https://datasciencegt.org", + }, +}; +--- + + + + + + + {FONT_PRELOADS.map((href) => )} + + + {/* The browser bar follows the device setting by default; the theme + toggle repeats these when a choice is made. */} + + + {title} + + + + + + + + + + + + + + + + + + + + + + {import.meta.env.PROD && } + + diff --git a/sites/hacklytics2027/lib/fonts.generated.ts b/sites/hacklytics2027/src/lib/fonts.generated.ts similarity index 100% rename from sites/hacklytics2027/lib/fonts.generated.ts rename to sites/hacklytics2027/src/lib/fonts.generated.ts diff --git a/sites/hacklytics2027/lib/links.ts b/sites/hacklytics2027/src/lib/links.ts similarity index 100% rename from sites/hacklytics2027/lib/links.ts rename to sites/hacklytics2027/src/lib/links.ts diff --git a/sites/hacklytics2027/lib/theme.ts b/sites/hacklytics2027/src/lib/theme.ts similarity index 100% rename from sites/hacklytics2027/lib/theme.ts rename to sites/hacklytics2027/src/lib/theme.ts diff --git a/sites/hacklytics2027/src/pages/404.astro b/sites/hacklytics2027/src/pages/404.astro new file mode 100644 index 00000000..672bb405 --- /dev/null +++ b/sites/hacklytics2027/src/pages/404.astro @@ -0,0 +1,31 @@ +--- +import MlhBadge from "../components/MlhBadge.astro"; +import { NotFoundBed } from "../components/pixel/GardenBeds"; +import Layout from "../layouts/Layout.astro"; +--- + + + {/* "Nothing grows at this address", so the soil here is bare, the only bed + on the site with no plants, until a visitor plants the first one. */} +
    + + +
    +

    Page not found

    +

    + 404 +

    +

    + Nothing grows at this address. The page may have moved, or it never + existed. +

    + +
    + +
    + +
    +
    +
    diff --git a/sites/hacklytics2027/src/pages/index.astro b/sites/hacklytics2027/src/pages/index.astro new file mode 100644 index 00000000..a0d2d3fd --- /dev/null +++ b/sites/hacklytics2027/src/pages/index.astro @@ -0,0 +1,23 @@ +--- +import Hero from "../components/Hero.astro"; +import HomeSections from "../components/HomeSections.astro"; +import Layout from "../layouts/Layout.astro"; +--- + + + +
    + + +
    + +
    diff --git a/sites/hacklytics2027/src/scripts/bloom.ts b/sites/hacklytics2027/src/scripts/bloom.ts new file mode 100644 index 00000000..6dbde2d5 --- /dev/null +++ b/sites/hacklytics2027/src/scripts/bloom.ts @@ -0,0 +1,28 @@ +/* + * Flowers that are plain HTML (the track plants, the schedule rail) start + * dormant and open the first time 35% of their row is in view. This writes + * data-bloom="on" straight onto the element and stops watching it; the + * stylesheet does the growing (see .flower-stem in styles/globals.css). The + * React beds do the same in pixel/useInView.ts. + * + * Under prefers-reduced-motion the CSS shows every flower open from the first + * paint, so flipping the attribute changes nothing visible. + */ +const THRESHOLD = 0.35; +const rows = document.querySelectorAll("[data-bloom-watch]"); + +if (typeof IntersectionObserver === "undefined") { + rows.forEach((row) => (row.dataset.bloom = "on")); +} else { + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.intersectionRatio < THRESHOLD) continue; + (entry.target as HTMLElement).dataset.bloom = "on"; + observer.unobserve(entry.target); + } + }, + { threshold: THRESHOLD }, + ); + rows.forEach((row) => observer.observe(row)); +} diff --git a/sites/hacklytics2027/src/scripts/navbar.ts b/sites/hacklytics2027/src/scripts/navbar.ts new file mode 100644 index 00000000..c71cd87b --- /dev/null +++ b/sites/hacklytics2027/src/scripts/navbar.ts @@ -0,0 +1,106 @@ +const find = (selector: string): T => { + const el = document.querySelector(selector); + if (!el) throw new Error(`Navbar markup is missing ${selector}`); + return el; +}; + +const header = find("[data-nav-header]"); +const toggle = find("[data-nav-toggle]"); +const menu = find("[data-nav-menu]"); + +let lastY = 0; +let clickScrolling = false; +let scrollTimer: ReturnType | undefined; + +const setHidden = (hidden: boolean) => header.toggleAttribute("data-hidden", hidden); +const setScrolled = (scrolled: boolean) => header.toggleAttribute("data-scrolled", scrolled); + +const setOpen = (open: boolean) => { + header.toggleAttribute("data-open", open); + menu.toggleAttribute("data-open", open); + menu.inert = !open; + toggle.setAttribute("aria-expanded", String(open)); + document.body.classList.toggle("overflow-hidden", open); +}; + +toggle.addEventListener("click", () => setOpen(toggle.getAttribute("aria-expanded") !== "true")); +find("[data-nav-home]").addEventListener("click", () => setOpen(false)); +menu.querySelector("a[target=_blank]")?.addEventListener("click", () => setOpen(false)); + +window.addEventListener( + "scroll", + () => { + const y = window.scrollY; + setScrolled(y > 20); + setHidden(y > lastY && !clickScrolling && y > 80); + lastY = y; + // A nav jump's length depends on how far it goes, so it counts as over + // once the scroll events stop, not after a fixed time: FAQ takes over a + // second, and the header hid itself the moment a timer ran out mid-way. + if (clickScrolling) { + clearTimeout(scrollTimer); + scrollTimer = setTimeout(() => { + clickScrolling = false; + }, 150); + } + }, + { passive: true }, +); + +window.addEventListener( + "mousemove", + (e) => { + if (e.clientY < 60) setHidden(false); + }, + { passive: true }, +); + +// Every section link on the site, whichever component renders it, scrolls +// without touching the URL. Capture, so it runs before the browser follows the +// link. +document.addEventListener( + "click", + (e) => { + // Cmd/ctrl-click opens a new tab; that is the browser's to handle. + if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; + const link = e.target instanceof Element ? e.target.closest("a[href*='#']") : null; + if (!link) return; + setOpen(false); + // Off the home page (the 404), let the link navigate to /#id. + const section = link.pathname === window.location.pathname ? document.getElementById(link.hash.slice(1)) : null; + if (!section) return; + e.preventDefault(); + clickScrolling = true; + setHidden(false); + const offset = header.offsetHeight + 16; + window.scrollTo({ + top: section.getBoundingClientRect().top + window.scrollY - offset, + behavior: "smooth", + }); + clearTimeout(scrollTimer); + scrollTimer = setTimeout(() => { + clickScrolling = false; + }, 1000); + }, + true, +); + +// Arriving at /#faqs, from a shared link or the 404: html is scroll-behavior +// smooth, so the browser animated down from the top through the whole page, +// and stalled if anything moved in the meantime. Jump there instead, then +// drop the hash so the address stays clean. Sync the header too, since a page +// opened mid-way fires no scroll event and stayed see-through. +requestAnimationFrame(() => { + const { hash } = window.location; + if (hash) { + document.getElementById(hash.slice(1))?.scrollIntoView({ behavior: "instant", block: "start" }); + history.replaceState(history.state, "", window.location.pathname); + setHidden(false); + } + // Not the scroll handler: from lastY 0, landing on the section reads as a + // scroll down and hid the header, which touch users had no way back to + // short of scrolling up. Record where the page starts; only the + // background depends on it. + lastY = window.scrollY; + setScrolled(window.scrollY > 20); +}); diff --git a/sites/hacklytics2027/src/scripts/service-worker.ts b/sites/hacklytics2027/src/scripts/service-worker.ts new file mode 100644 index 00000000..a700ef3e --- /dev/null +++ b/sites/hacklytics2027/src/scripts/service-worker.ts @@ -0,0 +1,13 @@ +// Registers the service worker in production builds, once the page has loaded +// so it never competes with first paint. +const register = () => { + navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch((error) => { + // oxlint-disable-next-line no-console + console.error("[SW] Service worker registration failed:", error); + }); +}; + +if ("serviceWorker" in navigator) { + if (document.readyState === "complete") register(); + else window.addEventListener("load", register, { once: true }); +} diff --git a/sites/hacklytics2027/src/scripts/theme-toggle.ts b/sites/hacklytics2027/src/scripts/theme-toggle.ts new file mode 100644 index 00000000..4e2bd64e --- /dev/null +++ b/sites/hacklytics2027/src/scripts/theme-toggle.ts @@ -0,0 +1,68 @@ +import { GROUND, THEME_KEY } from "@/lib/theme"; + +type Theme = "light" | "dark"; + +const root = document.documentElement; +const buttons = document.querySelectorAll("[data-theme-toggle]"); +const light = window.matchMedia("(prefers-color-scheme: light)"); + +// An explicit choice wins; otherwise the system setting decides, which is +// what globals.css does with no data-theme attribute. +const current = (): Theme => { + const set = root.dataset.theme; + if (set === "light" || set === "dark") return set; + return light.matches ? "light" : "dark"; +}; + +// Points the browser bar at the page. A chosen theme sets both theme-color +// metas to its ground; no choice leaves each on its own scheme, so the bar +// follows the device. +const syncBar = () => { + const set = root.dataset.theme; + document.querySelectorAll('meta[name="theme-color"]').forEach((meta) => { + const scheme = meta.media.includes("light") ? "light" : "dark"; + meta.content = GROUND[set === "light" || set === "dark" ? set : scheme]; + }); +}; + +// The icon follows the theme in CSS; the words for assistive tech follow here. +const syncLabels = () => { + const next: Theme = current() === "dark" ? "light" : "dark"; + buttons.forEach((button) => { + button.setAttribute("aria-label", next === "light" ? "Switch to light mode" : "Switch to dark mode"); + button.title = next === "light" ? "Light mode" : "Dark mode"; + }); +}; + +const sync = () => { + syncBar(); + syncLabels(); +}; + +buttons.forEach((button) => + button.addEventListener("click", () => { + const next: Theme = current() === "dark" ? "light" : "dark"; + root.dataset.theme = next; + try { + localStorage.setItem(THEME_KEY, next); + } catch { + // Private mode or blocked storage: the choice lasts this page view. + } + }), +); + +new MutationObserver(sync).observe(root, { attributes: true, attributeFilter: ["data-theme"] }); +light.addEventListener("change", syncLabels); + +// A choice saved in another tab (key null is storage being cleared there). +window.addEventListener("storage", (event) => { + if (event.key !== null && event.key !== THEME_KEY) return; + const saved = event.newValue; + if (saved === "light" || saved === "dark") { + root.dataset.theme = saved; + } else { + root.removeAttribute("data-theme"); + } +}); + +sync(); diff --git a/sites/hacklytics2027/app/fonts.generated.css b/sites/hacklytics2027/src/styles/fonts.generated.css similarity index 100% rename from sites/hacklytics2027/app/fonts.generated.css rename to sites/hacklytics2027/src/styles/fonts.generated.css diff --git a/sites/hacklytics2027/app/globals.css b/sites/hacklytics2027/src/styles/globals.css similarity index 94% rename from sites/hacklytics2027/app/globals.css rename to sites/hacklytics2027/src/styles/globals.css index b8854328..29bed81d 100644 --- a/sites/hacklytics2027/app/globals.css +++ b/sites/hacklytics2027/src/styles/globals.css @@ -1,4 +1,4 @@ -/* app/globals.css — Digital Bloom, redrawn from the globe key art: a flat +/* styles/globals.css — Digital Bloom, redrawn from the globe key art: a flat globe on black with a pale-pink ribbon of cherry blossoms across it. Three colours come off that art (sampled from the PNG): ribbon pink #ffbfcf, sage #96bf98 and the dark navy outline #262731. @@ -26,7 +26,7 @@ --color-sage: var(--sage); --color-on-bloom: var(--on-bloom); - /* Font variables come from next/font in app/layout.tsx. */ + /* The font variables are defined in fonts.generated.css. */ --font-display: var(--font-silkscreen), ui-monospace, monospace; --font-sans: var(--font-instrument), ui-sans-serif, system-ui, sans-serif; } @@ -320,3 +320,14 @@ body { } .flower-wilt { animation: none; } } + +/* ThemeToggle shows the sun on the night theme and the moon on the day one, + decided here so the icon is right before any script runs. Same selectors as + .px-light above. */ +.theme-moon { display: none; } +:root[data-theme="light"] .theme-sun { display: none; } +:root[data-theme="light"] .theme-moon { display: block; } +@media (prefers-color-scheme: light) { + :root:not([data-theme="dark"]) .theme-sun { display: none; } + :root:not([data-theme="dark"]) .theme-moon { display: block; } +} diff --git a/sites/hacklytics2027/tsconfig.json b/sites/hacklytics2027/tsconfig.json index 3394e2fd..b7ad376e 100644 --- a/sites/hacklytics2027/tsconfig.json +++ b/sites/hacklytics2027/tsconfig.json @@ -1,33 +1,17 @@ { + "extends": "astro/tsconfigs/base", "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], + "jsxImportSource": "react", + "types": ["astro/client"], "paths": { - "@/*": ["./*"] + "@/*": ["./src/*"] }, - "strict": false + "strict": false, + "verbatimModuleSyntax": false }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts" - ], - "exclude": ["node_modules", "scripts"] + "include": [".astro/types.d.ts", "src/**/*", "astro.config.mjs"], + "exclude": ["node_modules", "out", "scripts"] } diff --git a/sites/hacklytics2027/turbo.json b/sites/hacklytics2027/turbo.json index 6395ecf6..dfec31b6 100644 --- a/sites/hacklytics2027/turbo.json +++ b/sites/hacklytics2027/turbo.json @@ -3,7 +3,7 @@ "extends": ["//"], "tasks": { "build": { - "outputs": [".next/**", "!.next/cache/**", "out/**"] + "outputs": ["out/**"] } } } diff --git a/sites/mainweb/app/(portal)/checkin/CheckinPage.tsx b/sites/mainweb/app/(portal)/checkin/CheckinPage.tsx new file mode 100644 index 00000000..4588f3f2 --- /dev/null +++ b/sites/mainweb/app/(portal)/checkin/CheckinPage.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import { useEventCheckIn } from "@/components/portal/EventCheckIn"; +import { trpc } from "@/lib/trpc"; +import { + body, + btnPrimary, + itemTitle, + label, + page, + pageDek, + pageTitle, +} from "@/components/portal/ui"; + +/** + * Event check-in for anyone signed in. The Club Portal has the same scanner, + * but it is a members' page; attendance is not, so this route takes everyone. + */ +export default function CheckInPage() { + const { data: session, status } = useSession(); + const router = useRouter(); + const checkIn = useEventCheckIn(); + + const { data: myStats } = trpc.events.myStats.useQuery(undefined, { + enabled: !!session, + }); + + // Straight back here after signing in: the QR is on the wall in front of + // them, and a detour through the dashboard loses it. + useEffect(() => { + if (status === "unauthenticated") { + router.push(`/login?callbackUrl=${encodeURIComponent("/checkin")}`); + } + }, [status, router]); + + if (status === "loading" || !session) { + return ; + } + + const total = myStats?.totalEvents ?? 0; + + return ( +
    + {checkIn.modals} + +
    +

    Check in to an event

    +

    + Scan the QR code shown at the event. Open to everyone, no membership + needed. +

    +
    + +
    +

    At an event?

    +

    + Point your camera at the event's QR code to record your + attendance. +

    + +
    + +
    +

    Your check-ins

    +

    + + {total} + + {total === 1 ? "event" : "events"} +

    +
    +
    + ); +} diff --git a/sites/mainweb/app/(portal)/checkin/page.tsx b/sites/mainweb/app/(portal)/checkin/page.tsx index 4588f3f2..f374b000 100644 --- a/sites/mainweb/app/(portal)/checkin/page.tsx +++ b/sites/mainweb/app/(portal)/checkin/page.tsx @@ -1,85 +1,14 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import { LoadingScreen } from "@/components/portal/LoadingScreen"; -import { useEventCheckIn } from "@/components/portal/EventCheckIn"; -import { trpc } from "@/lib/trpc"; -import { - body, - btnPrimary, - itemTitle, - label, - page, - pageDek, - pageTitle, -} from "@/components/portal/ui"; - -/** - * Event check-in for anyone signed in. The Club Portal has the same scanner, - * but it is a members' page; attendance is not, so this route takes everyone. - */ -export default function CheckInPage() { - const { data: session, status } = useSession(); - const router = useRouter(); - const checkIn = useEventCheckIn(); - - const { data: myStats } = trpc.events.myStats.useQuery(undefined, { - enabled: !!session, - }); - - // Straight back here after signing in: the QR is on the wall in front of - // them, and a detour through the dashboard loses it. - useEffect(() => { - if (status === "unauthenticated") { - router.push(`/login?callbackUrl=${encodeURIComponent("/checkin")}`); - } - }, [status, router]); - - if (status === "loading" || !session) { - return ; - } - - const total = myStats?.totalEvents ?? 0; +import { PrefetchedPage } from "@/components/portal/PrefetchedPage"; +import CheckinPage from "./CheckinPage"; +export default function Page() { return ( -
    - {checkIn.modals} - -
    -

    Check in to an event

    -

    - Scan the QR code shown at the event. Open to everyone, no membership - needed. -

    -
    - -
    -

    At an event?

    -

    - Point your camera at the event's QR code to record your - attendance. -

    - -
    - -
    -

    Your check-ins

    -

    - - {total} - - {total === 1 ? "event" : "events"} -

    -
    -
    + [ + ["events.myStats", undefined, c.events.myStats()], + ]} + > + + ); } diff --git a/sites/mainweb/app/(portal)/club/ClubPage.tsx b/sites/mainweb/app/(portal)/club/ClubPage.tsx new file mode 100644 index 00000000..3355b546 --- /dev/null +++ b/sites/mainweb/app/(portal)/club/ClubPage.tsx @@ -0,0 +1,365 @@ +"use client"; + +import { venueFormat } from "@/lib/venue-time"; +import { useEffect, useState } from "react"; +import { loginHref } from "@/lib/safe-callback"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import { MemberPassCard } from "@/components/portal/MemberPassCard"; +import { useEventCheckIn } from "@/components/portal/EventCheckIn"; +import { trpc } from "@/lib/trpc"; +import { useSession } from "next-auth/react"; +import { QrCode } from "lucide-react"; +import { + body, + btnPrimary, + itemTitle, + label, + mastRule, + meta, + page, + pageDek, + pageTitle, + sectionTitle, + status as statusText, + tab as tabClass, + tabList, + textLink, +} from "@/components/portal/ui"; + +type Tab = "general" | "history" | "status"; + +const eventWhen = (date: Date | string) => + venueFormat(date, { + weekday: "short", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }); + +const statusRow = + "grid grid-cols-1 gap-1 border-b border-[var(--border-subtle)] py-3.5 sm:grid-cols-[200px_1fr] sm:gap-6"; + +export default function ClubPage() { + const { data: session, status } = useSession(); + const router = useRouter(); + const checkIn = useEventCheckIn(); + + const { data: userData } = trpc.user.me.useQuery(undefined, { + enabled: !!session, + }); + const { data: memberStatus } = trpc.member.checkStatus.useQuery(undefined, { + enabled: !!session, + }); + const { data: myStats } = trpc.events.myStats.useQuery(undefined, { + enabled: !!session, + }); + const { data: myEvents } = trpc.events.myEvents.useQuery(undefined, { + enabled: !!session, + }); + // Members could see what they had already attended but never what was + // coming, so the portal gave no reason to open it between meetings. + const { data: allEvents } = trpc.events.list.useQuery(undefined, { + enabled: !!session, + }); + + // Sessions are for enrolled members and are listed on /club/bootcamp. + const upcomingEvents = (allEvents ?? []) + .filter((event) => event.kind === "club") + // oxlint-disable-next-line react/purity + .filter((event) => new Date(event.eventDate) >= new Date()) + .sort( + (a, b) => + new Date(a.eventDate).getTime() - new Date(b.eventDate).getTime(), + ) + .slice(0, 5); + + const [activeTab, setActiveTab] = useState("general"); + + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.replace("#", "") as Tab; + if (["general", "history", "status"].includes(hash)) { + setActiveTab(hash); + } + }; + handleHashChange(); + window.addEventListener("hashchange", handleHashChange); + return () => window.removeEventListener("hashchange", handleHashChange); + }, []); + + useEffect(() => { + if (status === "unauthenticated") { + router.push(loginHref()); + } else if ( + status === "authenticated" && + memberStatus && + !memberStatus.isMember + ) { + router.push("/dashboard"); + } + }, [status, memberStatus, router]); + + if (status === "loading" || !memberStatus) { + return ; + } + + if (!session || !memberStatus.isMember) return null; + + const firstName = userData?.name?.split(" ")[0] || "Member"; + const nextEvent = upcomingEvents[0]; + + const tabs: { id: Tab; label: string }[] = [ + { id: "general", label: "Event check-in" }, + { id: "history", label: "Attendance history" }, + { id: "status", label: "Membership status" }, + ]; + + return ( +
    + {checkIn.modals} + +
    + {/* Masthead */} +
    +
    + {userData?.image ? ( + Avatar + ) : ( +
    + {firstName.charAt(0)} +
    + )} + + {userData?.name || "Member"} + + Member +
    + + Main dashboard + +
    + + {/* Headline: the next thing to show up for, or how check-in works. */} +
    +

    + {nextEvent ? `Next up: ${nextEvent.title}` : "Check in at the door"} +

    +

    + {nextEvent + ? `${eventWhen(nextEvent.eventDate)}${nextEvent.location ? ` · ${nextEvent.location}` : ""}. Scan the event code or show your pass when you arrive.` + : "Nothing is on the calendar right now. At the next meeting, scan its code or show your pass to record attendance."} +

    +

    + Looking for hackathons? Registrations, teams and projects live in + the{" "} + + hackathon hub + + . +

    +
    + + {/* Tab Navigation */} + + + {/* Tab Content */} +
    + {/* General Check-In */} + {activeTab === "general" && ( +
    +
    +

    Event check-in

    +

    + Scan the QR code displayed at the entrance of general club + meetings to record your attendance. +

    + +
    + + + +
    +
    +

    Upcoming events

    + {upcomingEvents.length === 0 ? ( +

    + Nothing scheduled yet. New events show up here as soon + as they are posted. +

    + ) : ( +
      + {upcomingEvents.map((event) => ( +
    • +

      + {event.title} +

      +

      + {eventWhen(event.eventDate)} + {event.location ? ` · ${event.location}` : ""} +

      +
    • + ))} +
    + )} +
    + +
    +

    Quick guide

    +
      +
    • + General gatherings require QR check-ins for attendance. +
    • +
    • + Hackathons have their own separate hub with registration + and team tools. +
    • +
    +
    +
    +
    + )} + + {/* Attendance History */} + {activeTab === "history" && ( +
    +

    Attendance history

    + {!myEvents || myEvents.length === 0 ? ( +
    +

    + You haven't checked into any events yet. When you do, + they will appear here. +

    + setActiveTab("general")} + className={`${textLink} mt-4`} + > + Go to event check-in + +
    + ) : ( +
      + {myEvents.map((checkIn) => ( +
    • +
      +

      + {checkIn.event.title} +

      +

      + {checkIn.event.kind === "bootcamp" + ? "Bootcamp · " + : ""} + {venueFormat(checkIn.checkedInAt)} + {checkIn.event.location + ? ` · ${checkIn.event.location}` + : ""} +

      +
      + + Verified + +
    • + ))} +
    + )} +
    + )} + + {/* Membership Status */} + {activeTab === "status" && ( +
    +

    Membership

    +

    + You have full access to the club portal, hackathons, and member + resources. +

    +
    +
    +
    Membership
    +
    + Valid +
    +
    +
    +
    Account tier
    +
    + Verified member +
    +
    +
    +
    Valid through
    +
    + {memberStatus?.expiresAt + ? venueFormat(memberStatus.expiresAt) + : "N/A"} +
    +
    +
    +
    + )} +
    +
    +
    + ); +} diff --git a/sites/mainweb/app/(portal)/club/page.tsx b/sites/mainweb/app/(portal)/club/page.tsx index ec980cde..5b8f4889 100644 --- a/sites/mainweb/app/(portal)/club/page.tsx +++ b/sites/mainweb/app/(portal)/club/page.tsx @@ -1,364 +1,18 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { loginHref } from "@/lib/safe-callback"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { LoadingScreen } from "@/components/portal/LoadingScreen"; -import { MemberPassCard } from "@/components/portal/MemberPassCard"; -import { useEventCheckIn } from "@/components/portal/EventCheckIn"; -import { trpc } from "@/lib/trpc"; -import { useSession } from "next-auth/react"; -import { QrCode } from "lucide-react"; -import { - body, - btnPrimary, - itemTitle, - label, - mastRule, - meta, - page, - pageDek, - pageTitle, - sectionTitle, - status as statusText, - tab as tabClass, - tabList, - textLink, -} from "@/components/portal/ui"; - -type Tab = "general" | "history" | "status"; - -const eventWhen = (date: Date | string) => - new Date(date).toLocaleString("en-US", { - weekday: "short", - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - }); - -const statusRow = - "grid grid-cols-1 gap-1 border-b border-[var(--border-subtle)] py-3.5 sm:grid-cols-[200px_1fr] sm:gap-6"; - -export default function ClubPage() { - const { data: session, status } = useSession(); - const router = useRouter(); - const checkIn = useEventCheckIn(); - - const { data: userData } = trpc.user.me.useQuery(undefined, { - enabled: !!session, - }); - const { data: memberStatus } = trpc.member.checkStatus.useQuery(undefined, { - enabled: !!session, - }); - const { data: myStats } = trpc.events.myStats.useQuery(undefined, { - enabled: !!session, - }); - const { data: myEvents } = trpc.events.myEvents.useQuery(undefined, { - enabled: !!session, - }); - // Members could see what they had already attended but never what was - // coming, so the portal gave no reason to open it between meetings. - const { data: allEvents } = trpc.events.list.useQuery(undefined, { - enabled: !!session, - }); - - // Sessions are for enrolled members and are listed on /club/bootcamp. - const upcomingEvents = (allEvents ?? []) - .filter((event) => event.kind === "club") - // oxlint-disable-next-line react/purity - .filter((event) => new Date(event.eventDate) >= new Date()) - .sort( - (a, b) => - new Date(a.eventDate).getTime() - new Date(b.eventDate).getTime(), - ) - .slice(0, 5); - - const [activeTab, setActiveTab] = useState("general"); - - useEffect(() => { - const handleHashChange = () => { - const hash = window.location.hash.replace("#", "") as Tab; - if (["general", "history", "status"].includes(hash)) { - setActiveTab(hash); - } - }; - handleHashChange(); - window.addEventListener("hashchange", handleHashChange); - return () => window.removeEventListener("hashchange", handleHashChange); - }, []); - - useEffect(() => { - if (status === "unauthenticated") { - router.push(loginHref()); - } else if ( - status === "authenticated" && - memberStatus && - !memberStatus.isMember - ) { - router.push("/dashboard"); - } - }, [status, memberStatus, router]); - - if (status === "loading" || !memberStatus) { - return ; - } - - if (!session || !memberStatus.isMember) return null; - - const firstName = userData?.name?.split(" ")[0] || "Member"; - const nextEvent = upcomingEvents[0]; - - const tabs: { id: Tab; label: string }[] = [ - { id: "general", label: "Event check-in" }, - { id: "history", label: "Attendance history" }, - { id: "status", label: "Membership status" }, - ]; +import { PrefetchedPage } from "@/components/portal/PrefetchedPage"; +import ClubPage from "./ClubPage"; +export default function Page() { return ( -
    - {checkIn.modals} - -
    - {/* Masthead */} -
    -
    - {userData?.image ? ( - Avatar - ) : ( -
    - {firstName.charAt(0)} -
    - )} - - {userData?.name || "Member"} - - Member -
    - - Main dashboard - -
    - - {/* Headline: the next thing to show up for, or how check-in works. */} -
    -

    - {nextEvent ? `Next up: ${nextEvent.title}` : "Check in at the door"} -

    -

    - {nextEvent - ? `${eventWhen(nextEvent.eventDate)}${nextEvent.location ? ` · ${nextEvent.location}` : ""}. Scan the event code or show your pass when you arrive.` - : "Nothing is on the calendar right now. At the next meeting, scan its code or show your pass to record attendance."} -

    -

    - Looking for hackathons? Registrations, teams and projects live in - the{" "} - - hackathon hub - - . -

    -
    - - {/* Tab Navigation */} - - - {/* Tab Content */} -
    - {/* General Check-In */} - {activeTab === "general" && ( -
    -
    -

    Event check-in

    -

    - Scan the QR code displayed at the entrance of general club - meetings to record your attendance. -

    - -
    - - - -
    -
    -

    Upcoming events

    - {upcomingEvents.length === 0 ? ( -

    - Nothing scheduled yet. New events show up here as soon - as they are posted. -

    - ) : ( -
      - {upcomingEvents.map((event) => ( -
    • -

      - {event.title} -

      -

      - {eventWhen(event.eventDate)} - {event.location ? ` · ${event.location}` : ""} -

      -
    • - ))} -
    - )} -
    - -
    -

    Quick guide

    -
      -
    • - General gatherings require QR check-ins for attendance. -
    • -
    • - Hackathons have their own separate hub with registration - and team tools. -
    • -
    -
    -
    -
    - )} - - {/* Attendance History */} - {activeTab === "history" && ( -
    -

    Attendance history

    - {!myEvents || myEvents.length === 0 ? ( -
    -

    - You haven't checked into any events yet. When you do, - they will appear here. -

    - setActiveTab("general")} - className={`${textLink} mt-4`} - > - Go to event check-in - -
    - ) : ( -
      - {myEvents.map((checkIn) => ( -
    • -
      -

      - {checkIn.event.title} -

      -

      - {checkIn.event.kind === "bootcamp" - ? "Bootcamp · " - : ""} - {new Date(checkIn.checkedInAt).toLocaleDateString()} - {checkIn.event.location - ? ` · ${checkIn.event.location}` - : ""} -

      -
      - - Verified - -
    • - ))} -
    - )} -
    - )} - - {/* Membership Status */} - {activeTab === "status" && ( -
    -

    Membership

    -

    - You have full access to the club portal, hackathons, and member - resources. -

    -
    -
    -
    Membership
    -
    - Valid -
    -
    -
    -
    Account tier
    -
    - Verified member -
    -
    -
    -
    Valid through
    -
    - {memberStatus?.expiresAt - ? new Date(memberStatus.expiresAt).toLocaleDateString() - : "N/A"} -
    -
    -
    -
    - )} -
    -
    -
    + [ + ["user.me", undefined, c.user.me()], + ["member.checkStatus", undefined, c.member.checkStatus()], + ["events.myStats", undefined, c.events.myStats()], + ["events.myEvents", undefined, c.events.myEvents()], + ["events.list", undefined, c.events.list()], + ]} + > + + ); } diff --git a/sites/mainweb/app/(portal)/dashboard/DashboardPage.tsx b/sites/mainweb/app/(portal)/dashboard/DashboardPage.tsx new file mode 100644 index 00000000..70fa809a --- /dev/null +++ b/sites/mainweb/app/(portal)/dashboard/DashboardPage.tsx @@ -0,0 +1,593 @@ +"use client"; + +import { venueDayNumber, venueFormat } from "@/lib/venue-time"; +import { useSession, signOut } from "next-auth/react"; +import { loginHref } from "@/lib/safe-callback"; +import { trpc } from "@/lib/trpc"; +import { + useInvalidatePortalContext, + usePortalContext, +} from "@/lib/use-portal-context"; +import { hackathonSlug } from "@/lib/hackathon-slug"; +import { + hackathonNextStep, + isWithinEditionDates, + nextStepQueries, + projectSubmitted, +} from "@/lib/hackathon-next-step"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import type { ReactNode } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import LinkStripeAccount from "@/components/portal/LinkStripeAccount"; +import { StatusBadge } from "@/components/hackathon/StatusBadge"; +import { NextStepStrip } from "@/components/hackathon/NextStepStrip"; +import { + MEMBERSHIP_CENTS, + SEMESTER_MEMBERSHIP_CENTS, + formatCents, +} from "@query/api/pricing"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import { ArrowRight } from "lucide-react"; +import { + body, + btnPrimary, + btnSecondary, + itemTitle, + label, + mastRule, + meta, + page, + pageDek, + pageTitle, + sectionRule, + sectionTitle, + status as statusLine, + tab, + tabList, + textLink, +} from "@/components/portal/ui"; + +export default function Dashboard() { + const { data: session, status } = useSession(); + const router = useRouter(); + + const { data: portalContext } = usePortalContext(); + const { data: userData } = trpc.user.me.useQuery(undefined, { + enabled: !!session, + }); + const { data: myRegs, isLoading: loadingRegs } = + trpc.hackathon.myRegistrations.useQuery(undefined, { + enabled: !!session, + // An accepted hacker is waiting on the door scan. While an event runs, + // re-read so the headline flips to checked in without a reload. + refetchInterval: (query) => { + const today = venueDayNumber(new Date()); + return query.state.data?.some( + (r) => + r.registrationStatus === "approved" && + venueDayNumber(r.hackathon.startDate) <= today && + today <= venueDayNumber(r.hackathon.endDate), + ) + ? 30_000 + : false; + }, + }); + + const memberStatus = portalContext?.member; + const isAdmin = portalContext?.isAdmin ?? false; + const isJudge = portalContext?.isJudge ?? false; + + /** + * Which half they land on. Held as null until they choose so the default can + * follow the data once it arrives — a paid member opens on Club, everyone + * else on Hackathon, which is the half that is open to them. + */ + const [chosenView, setChosenView] = useState<"club" | "hackathon" | null>( + null, + ); + // oxlint-disable-next-line react/purity + const now = new Date(); + // Somebody accepted to an event that is on today lands on the hackathon + // half, member or not: that is the half with their next step. + const eventReg = myRegs?.find( + (r) => + (r.registrationStatus === "approved" || + r.registrationStatus === "checked_in") && + r.hackathon.status !== "cancelled" && + isWithinEditionDates(r.hackathon, now), + ); + const view = + chosenView ?? + (eventReg ? "hackathon" : memberStatus?.isMember ? "club" : "hackathon"); + const setView = setChosenView; + + const activeRegs = + myRegs?.filter((r) => + r.hackathon.endDate ? new Date(r.hackathon.endDate) >= now : true, + ) ?? []; + const pastRegs = + myRegs?.filter((r) => + r.hackathon.endDate ? new Date(r.hackathon.endDate) < now : false, + ) ?? []; + + // The event they are at, else the latest registration still on the + // calendar. Its next step reads up to four more queries, each held back + // until the step can use it, so a guest, or a hacker whose event has not + // begun, pays for none of them. + const latestReg = eventReg ?? activeRegs[0]; + const stepHackathonId = latestReg?.hackathonId ?? ""; + const stepOn = view === "hackathon" && !!latestReg; + const needs = latestReg + ? nextStepQueries({ + now, + hackathon: latestReg.hackathon, + registrationStatus: latestReg.registrationStatus, + }) + : { submission: false, judging: false, results: false }; + const submissionWindow = trpc.team.submissionWindow.useQuery( + { hackathonId: stepHackathonId }, + { enabled: stepOn && needs.submission }, + ); + const mySubmission = trpc.team.mySubmission.useQuery( + { hackathonId: stepHackathonId }, + { enabled: stepOn && needs.submission }, + ); + const itinerary = trpc.judging.hacker.itinerary.useQuery( + { hackathonId: stepHackathonId }, + { enabled: stepOn && needs.judging }, + ); + const results = trpc.hackathon.getResults.useQuery( + { hackathonId: stepHackathonId }, + { enabled: stepOn && needs.results }, + ); + // Held back until they answer, so the strip never shows a step it is about + // to take back ("Submit" to somebody who already has). + const stepLoading = + submissionWindow.isLoading || + mySubmission.isLoading || + itinerary.isLoading || + results.isLoading; + const nextStep = latestReg + ? hackathonNextStep({ + now, + hackathon: latestReg.hackathon, + registrationStatus: latestReg.registrationStatus, + hasTeam: !!latestReg.teamId, + submissionWindow: submissionWindow.data, + submitted: projectSubmitted(mySubmission.data), + itinerary: itinerary.data, + resultsPublished: (results.data?.length ?? 0) > 0, + }) + : null; + + // A link made here changes membership, so the page has to re-read it — or it + // keeps showing the pay button to somebody who has already paid. + const utils = trpc.useUtils(); + const invalidatePortalContext = useInvalidatePortalContext(); + const { mutate: attemptAutoLink } = trpc.stripe.attemptAutoLink.useMutation({ + onSuccess: (data) => { + if (!data.success) return; + void utils.member.checkStatus.invalidate(); + invalidatePortalContext(); + }, + }); + // Once per visit. `session` changes identity on every refetch (window focus), + // and each re-fire past the first only hits the server's 10s throttle. + const autoLinkFired = useRef(false); + useEffect(() => { + if (status !== "authenticated" || autoLinkFired.current) return; + autoLinkFired.current = true; + attemptAutoLink(); + }, [status, attemptAutoLink]); + useEffect(() => { + if (status === "unauthenticated") router.push(loginHref()); + }, [status, router]); + useEffect(() => { + if (isAdmin) router.replace("/admin"); + }, [isAdmin, router]); + + if (status === "loading") + return ; + if (!session) return null; + + const roleLabel = isAdmin + ? "Admin" + : memberStatus?.isMember + ? "Member" + : "Guest"; + + const today = venueFormat(now, { + weekday: "long", + month: "long", + day: "numeric", + }); + + const daysLeft = memberStatus?.daysRemaining; + const endsSoon = typeof daysLeft === "number" && daysLeft <= 30; + const expiresOn = memberStatus?.expiresAt + ? venueFormat(memberStatus.expiresAt, { + month: "long", + day: "numeric", + year: "numeric", + }) + : null; + + // The headline is the one thing that matters on this half right now, worked + // out from what the page already loaded. + let headline: string; + let dek: string; + if (view === "hackathon") { + if (loadingRegs) { + headline = "Your hackathons"; + dek = "Checking your registrations…"; + } else if (latestReg) { + const name = latestReg.hackathon.name; + const byStatus: Record = { + approved: `You're in for ${name}.`, + checked_in: `You're checked in at ${name}.`, + pending: `Your application to ${name} is under review.`, + waitlisted: `You're on the waitlist for ${name}.`, + rejected: `Your application to ${name} wasn't accepted.`, + }; + headline = + byStatus[latestReg.registrationStatus] ?? + `You're registered for ${name}.`; + dek = latestReg.team + ? `You're on team ${latestReg.team.name}.` + : "You don't have a team yet."; + } else { + headline = "Pick a hackathon to register for."; + dek = + "Hackathons are open to anyone with an account. No membership needed."; + } + } else if (memberStatus?.isMember) { + if (endsSoon) { + headline = `Your membership ends in ${daysLeft} ${daysLeft === 1 ? "day" : "days"}.`; + dek = + "Renew now and the new term starts when this one ends, so you lose nothing by paying early."; + } else { + headline = expiresOn + ? `You're a member through ${expiresOn}.` + : "Your membership is active."; + dek = "Club events, check-ins and member resources are open to you."; + } + } else if (memberStatus?.hasLapsed) { + headline = "Your membership has run out."; + dek = "Renew below to get back into the Club Portal."; + } else { + headline = "Become a member to open the Club Portal."; + dek = `${formatCents(MEMBERSHIP_CENTS)} a year or ${formatCents(SEMESTER_MEMBERSHIP_CENTS)} a semester. Projects stay open to browse either way.`; + } + + return ( +
    + {/* ── MASTHEAD ───────────────────────────────────── */} +
    +
    + Avatar +

    + + {userData?.name ?? "Your account"} + + {userData?.email ? ` · ${userData.email}` : ""} · {roleLabel} +

    +
    +

    + {today} +

    +
    + + {/* ── HEADLINE ───────────────────────────────────── */} +
    +

    {headline}

    +

    {dek}

    + {view === "hackathon" && !loadingRegs && !stepLoading && latestReg && ( +
    + {nextStep ? ( + + ) : ( + + Open {latestReg.hackathon.name} + + )} +
    + )} +
    + + {/* ── CLUB / HACKATHON ───────────────────────────── */} + {/* Two different things this org does, and they have different rules: + the hackathon is open to anyone with an account, the club is the + paid yearly membership. Splitting them is what stops the dashboard + reading as though everything is behind the same paywall. */} +
    + {( + [ + { value: "hackathon", label: "Hackathon" }, + { value: "club", label: "Club" }, + ] as const + ).map((option) => ( + + ))} +
    + + {/* ── MY HACKATHONS ───────────────────────────────── */} + {/* Straight under the tabs, whose hairline is the divider. */} +
    +
    +

    My hackathons

    + + Browse all + +
    + + {loadingRegs ? ( + + ) : activeRegs.length > 0 ? ( +
      + {activeRegs.map((reg) => ( +
    • + +
      +
      +

      + {reg.hackathon.name} +

      + +
      +

      + {reg.hackathon.theme + ? `Theme: ${reg.hackathon.theme} · ` + : ""} + {reg.team ? `Team: ${reg.team.name}` : "No team yet"} +

      +
      + + View + + +
    • + ))} +
    + ) : ( +
    +

    + Hackathons you register for will show up here. +

    + + Browse hackathons + +
    + )} + + {/* Past events */} + {!loadingRegs && pastRegs.length > 0 && ( +
    +

    Past events

    +
      + {pastRegs.map((reg) => ( +
    • + + + {reg.hackathon.name} + + + + Details + + +
    • + ))} +
    +
    + )} +
    + + {/* ── MEMBERSHIP ─────────────────────────────────── */} + {/* Club view only, and it now also catches lapsed members: `isMember` + means paid AND unexpired, so the renew path is reachable instead of + being hidden behind the same flag that expired. */} + {view === "club" && memberStatus?.isMember && ( +
    +

    Membership active

    +

    + {expiresOn ? `Runs until ${expiresOn}` : "Active"} + {endsSoon ? ` · ${daysLeft} days left` : ""} +

    + {/* Renewing early extends from the current end date rather than + from today, so nobody loses time by paying ahead. */} + {endsSoon && ( +
    + +
    + )} +
    + )} + + {/* ── DESTINATIONS ───────────────────────────────── */} + {/* The hackathon half has no entry of its own: "Browse all" above leads + to the hub. It lists only the portals this person can open. */} + {(view === "club" || + isJudge || + isAdmin || + !!portalContext?.isBugTester) && ( +
      + {/* Club Portal — members only */} + {view === "club" && + (memberStatus?.isMember ? ( + + Club events, check-ins, and resources. + + ) : ( +
    • +

      Club Portal

      +

      + {memberStatus?.hasLapsed + ? "Your membership has run out. Renew below to get back in." + : "Membership required. Join below to unlock access."} +

      +
    • + ))} + + {/* Projects — club side, but browsing is open so anyone can see + what membership actually buys before paying for it. */} + {view === "club" && ( + + Projects the club runs year-round. Join one, or pitch your own. + + )} + + {/* Become a Member — sits beside Projects so the club view says + what is missing where the rest of the club lives. The pay UI is + the block below; this jumps to it. */} + {view === "club" && !memberStatus?.isMember && !isAdmin && ( + + {formatCents(MEMBERSHIP_CENTS)}/year or{" "} + {formatCents(SEMESTER_MEMBERSHIP_CENTS)}/semester. Opens the Club + Portal and member resources. + + )} + + {/* Judge Portal — judges only */} + {isJudge && ( + + Your judging editions, where you sit and the projects you score. + + )} + + {/* Admin Panel — admins, and bug testers read-only */} + {(isAdmin || !!portalContext?.isBugTester) && ( + + Manage events, attendees, and analytics. + + )} +
    + )} + + {/* The pay UI itself, below the destinations. The entry above is the + label; this is the block. */} + {view === "club" && !memberStatus?.isMember && !isAdmin && ( +
    +

    + {memberStatus?.hasLapsed ? "Renew your membership" : "Become a member"} +

    +
    + +
    +
    + )} + + {/* ── SIGN OUT ───────────────────────────────────── */} +
    +

    + Signed in{userData?.email ? ` as ${userData.email}` : ""}. +

    + +
    +
    + ); +} + +/** + * One place the dashboard can send you: a serif title with an arrow and a + * line saying what is there. `plain` renders an in-page anchor rather than a + * route link. + */ +function Destination({ + href, + title, + plain = false, + children, +}: { + href: string; + title: string; + plain?: boolean; + children: ReactNode; +}) { + const content = ( + <> + + + {title} + + + {children} + + ); + return ( +
  • + {plain ? ( + + {content} + + ) : ( + + {content} + + )} +
  • + ); +} diff --git a/sites/mainweb/app/(portal)/dashboard/page.tsx b/sites/mainweb/app/(portal)/dashboard/page.tsx index 4ca40bd2..313d96c2 100644 --- a/sites/mainweb/app/(portal)/dashboard/page.tsx +++ b/sites/mainweb/app/(portal)/dashboard/page.tsx @@ -1,594 +1,14 @@ -"use client"; +import { PrefetchedPage } from "@/components/portal/PrefetchedPage"; +import DashboardPage from "./DashboardPage"; -import { useSession, signOut } from "next-auth/react"; -import { loginHref } from "@/lib/safe-callback"; -import { trpc } from "@/lib/trpc"; -import { - useInvalidatePortalContext, - usePortalContext, -} from "@/lib/use-portal-context"; -import { hackathonSlug } from "@/lib/hackathon-slug"; -import { - hackathonNextStep, - isWithinEditionDates, - nextStepQueries, - projectSubmitted, -} from "@/lib/hackathon-next-step"; -import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; -import type { ReactNode } from "react"; -import Image from "next/image"; -import Link from "next/link"; -import LinkStripeAccount from "@/components/portal/LinkStripeAccount"; -import { StatusBadge } from "@/components/hackathon/StatusBadge"; -import { NextStepStrip } from "@/components/hackathon/NextStepStrip"; -import { - MEMBERSHIP_CENTS, - SEMESTER_MEMBERSHIP_CENTS, - formatCents, -} from "@query/api/pricing"; -import { LoadingScreen } from "@/components/portal/LoadingScreen"; -import { ArrowRight } from "lucide-react"; -import { - body, - btnPrimary, - btnSecondary, - itemTitle, - label, - mastRule, - meta, - page, - pageDek, - pageTitle, - sectionRule, - sectionTitle, - status as statusLine, - tab, - tabList, - textLink, -} from "@/components/portal/ui"; - -export default function Dashboard() { - const { data: session, status } = useSession(); - const router = useRouter(); - - const { data: portalContext } = usePortalContext(); - const { data: userData } = trpc.user.me.useQuery(undefined, { - enabled: !!session, - }); - const { data: myRegs, isLoading: loadingRegs } = - trpc.hackathon.myRegistrations.useQuery(undefined, { - enabled: !!session, - // An accepted hacker is waiting on the door scan. While an event runs, - // re-read so the headline flips to checked in without a reload. - refetchInterval: (query) => { - const startOfDay = (d: Date | string) => - new Date(d).setHours(0, 0, 0, 0); - const today = startOfDay(new Date()); - return query.state.data?.some( - (r) => - r.registrationStatus === "approved" && - startOfDay(r.hackathon.startDate) <= today && - today <= startOfDay(r.hackathon.endDate), - ) - ? 30_000 - : false; - }, - }); - - const memberStatus = portalContext?.member; - const isAdmin = portalContext?.isAdmin ?? false; - const isJudge = portalContext?.isJudge ?? false; - - /** - * Which half they land on. Held as null until they choose so the default can - * follow the data once it arrives — a paid member opens on Club, everyone - * else on Hackathon, which is the half that is open to them. - */ - const [chosenView, setChosenView] = useState<"club" | "hackathon" | null>( - null, - ); - // oxlint-disable-next-line react/purity - const now = new Date(); - // Somebody accepted to an event that is on today lands on the hackathon - // half, member or not: that is the half with their next step. - const eventReg = myRegs?.find( - (r) => - (r.registrationStatus === "approved" || - r.registrationStatus === "checked_in") && - r.hackathon.status !== "cancelled" && - isWithinEditionDates(r.hackathon, now), - ); - const view = - chosenView ?? - (eventReg ? "hackathon" : memberStatus?.isMember ? "club" : "hackathon"); - const setView = setChosenView; - - const activeRegs = - myRegs?.filter((r) => - r.hackathon.endDate ? new Date(r.hackathon.endDate) >= now : true, - ) ?? []; - const pastRegs = - myRegs?.filter((r) => - r.hackathon.endDate ? new Date(r.hackathon.endDate) < now : false, - ) ?? []; - - // The event they are at, else the latest registration still on the - // calendar. Its next step reads up to four more queries, each held back - // until the step can use it, so a guest, or a hacker whose event has not - // begun, pays for none of them. - const latestReg = eventReg ?? activeRegs[0]; - const stepHackathonId = latestReg?.hackathonId ?? ""; - const stepOn = view === "hackathon" && !!latestReg; - const needs = latestReg - ? nextStepQueries({ - now, - hackathon: latestReg.hackathon, - registrationStatus: latestReg.registrationStatus, - }) - : { submission: false, judging: false, results: false }; - const submissionWindow = trpc.team.submissionWindow.useQuery( - { hackathonId: stepHackathonId }, - { enabled: stepOn && needs.submission }, - ); - const mySubmission = trpc.team.mySubmission.useQuery( - { hackathonId: stepHackathonId }, - { enabled: stepOn && needs.submission }, - ); - const itinerary = trpc.judging.hacker.itinerary.useQuery( - { hackathonId: stepHackathonId }, - { enabled: stepOn && needs.judging }, - ); - const results = trpc.hackathon.getResults.useQuery( - { hackathonId: stepHackathonId }, - { enabled: stepOn && needs.results }, - ); - // Held back until they answer, so the strip never shows a step it is about - // to take back ("Submit" to somebody who already has). - const stepLoading = - submissionWindow.isLoading || - mySubmission.isLoading || - itinerary.isLoading || - results.isLoading; - const nextStep = latestReg - ? hackathonNextStep({ - now, - hackathon: latestReg.hackathon, - registrationStatus: latestReg.registrationStatus, - hasTeam: !!latestReg.teamId, - submissionWindow: submissionWindow.data, - submitted: projectSubmitted(mySubmission.data), - itinerary: itinerary.data, - resultsPublished: (results.data?.length ?? 0) > 0, - }) - : null; - - // A link made here changes membership, so the page has to re-read it — or it - // keeps showing the pay button to somebody who has already paid. - const utils = trpc.useUtils(); - const invalidatePortalContext = useInvalidatePortalContext(); - const { mutate: attemptAutoLink } = trpc.stripe.attemptAutoLink.useMutation({ - onSuccess: (data) => { - if (!data.success) return; - void utils.member.checkStatus.invalidate(); - invalidatePortalContext(); - }, - }); - // Once per visit. `session` changes identity on every refetch (window focus), - // and each re-fire past the first only hits the server's 10s throttle. - const autoLinkFired = useRef(false); - useEffect(() => { - if (status !== "authenticated" || autoLinkFired.current) return; - autoLinkFired.current = true; - attemptAutoLink(); - }, [status, attemptAutoLink]); - useEffect(() => { - if (status === "unauthenticated") router.push(loginHref()); - }, [status, router]); - useEffect(() => { - if (isAdmin) router.replace("/admin"); - }, [isAdmin, router]); - - if (status === "loading") - return ; - if (!session) return null; - - const roleLabel = isAdmin - ? "Admin" - : memberStatus?.isMember - ? "Member" - : "Guest"; - - const today = now.toLocaleDateString(undefined, { - weekday: "long", - month: "long", - day: "numeric", - }); - - const daysLeft = memberStatus?.daysRemaining; - const endsSoon = typeof daysLeft === "number" && daysLeft <= 30; - const expiresOn = memberStatus?.expiresAt - ? new Date(memberStatus.expiresAt).toLocaleDateString(undefined, { - month: "long", - day: "numeric", - year: "numeric", - }) - : null; - - // The headline is the one thing that matters on this half right now, worked - // out from what the page already loaded. - let headline: string; - let dek: string; - if (view === "hackathon") { - if (loadingRegs) { - headline = "Your hackathons"; - dek = "Checking your registrations…"; - } else if (latestReg) { - const name = latestReg.hackathon.name; - const byStatus: Record = { - approved: `You're in for ${name}.`, - checked_in: `You're checked in at ${name}.`, - pending: `Your application to ${name} is under review.`, - waitlisted: `You're on the waitlist for ${name}.`, - rejected: `Your application to ${name} wasn't accepted.`, - }; - headline = - byStatus[latestReg.registrationStatus] ?? - `You're registered for ${name}.`; - dek = latestReg.team - ? `You're on team ${latestReg.team.name}.` - : "You don't have a team yet."; - } else { - headline = "Pick a hackathon to register for."; - dek = - "Hackathons are open to anyone with an account. No membership needed."; - } - } else if (memberStatus?.isMember) { - if (endsSoon) { - headline = `Your membership ends in ${daysLeft} ${daysLeft === 1 ? "day" : "days"}.`; - dek = - "Renew now and the new term starts when this one ends, so you lose nothing by paying early."; - } else { - headline = expiresOn - ? `You're a member through ${expiresOn}.` - : "Your membership is active."; - dek = "Club events, check-ins and member resources are open to you."; - } - } else if (memberStatus?.hasLapsed) { - headline = "Your membership has run out."; - dek = "Renew below to get back into the Club Portal."; - } else { - headline = "Become a member to open the Club Portal."; - dek = `${formatCents(MEMBERSHIP_CENTS)} a year or ${formatCents(SEMESTER_MEMBERSHIP_CENTS)} a semester. Projects stay open to browse either way.`; - } - - return ( -
    - {/* ── MASTHEAD ───────────────────────────────────── */} -
    -
    - Avatar -

    - - {userData?.name ?? "Your account"} - - {userData?.email ? ` · ${userData.email}` : ""} · {roleLabel} -

    -
    -

    - {today} -

    -
    - - {/* ── HEADLINE ───────────────────────────────────── */} -
    -

    {headline}

    -

    {dek}

    - {view === "hackathon" && !loadingRegs && !stepLoading && latestReg && ( -
    - {nextStep ? ( - - ) : ( - - Open {latestReg.hackathon.name} - - )} -
    - )} -
    - - {/* ── CLUB / HACKATHON ───────────────────────────── */} - {/* Two different things this org does, and they have different rules: - the hackathon is open to anyone with an account, the club is the - paid yearly membership. Splitting them is what stops the dashboard - reading as though everything is behind the same paywall. */} -
    - {( - [ - { value: "hackathon", label: "Hackathon" }, - { value: "club", label: "Club" }, - ] as const - ).map((option) => ( - - ))} -
    - - {/* ── MY HACKATHONS ───────────────────────────────── */} - {/* Straight under the tabs, whose hairline is the divider. */} -
    -
    -

    My hackathons

    - - Browse all - -
    - - {loadingRegs ? ( - - ) : activeRegs.length > 0 ? ( -
      - {activeRegs.map((reg) => ( -
    • - -
      -
      -

      - {reg.hackathon.name} -

      - -
      -

      - {reg.hackathon.theme - ? `Theme: ${reg.hackathon.theme} · ` - : ""} - {reg.team ? `Team: ${reg.team.name}` : "No team yet"} -

      -
      - - View - - -
    • - ))} -
    - ) : ( -
    -

    - Hackathons you register for will show up here. -

    - - Browse hackathons - -
    - )} - - {/* Past events */} - {!loadingRegs && pastRegs.length > 0 && ( -
    -

    Past events

    -
      - {pastRegs.map((reg) => ( -
    • - - - {reg.hackathon.name} - - - - Details - - -
    • - ))} -
    -
    - )} -
    - - {/* ── MEMBERSHIP ─────────────────────────────────── */} - {/* Club view only, and it now also catches lapsed members: `isMember` - means paid AND unexpired, so the renew path is reachable instead of - being hidden behind the same flag that expired. */} - {view === "club" && memberStatus?.isMember && ( -
    -

    Membership active

    -

    - {expiresOn ? `Runs until ${expiresOn}` : "Active"} - {endsSoon ? ` · ${daysLeft} days left` : ""} -

    - {/* Renewing early extends from the current end date rather than - from today, so nobody loses time by paying ahead. */} - {endsSoon && ( -
    - -
    - )} -
    - )} - - {/* ── DESTINATIONS ───────────────────────────────── */} - {/* The hackathon half has no entry of its own: "Browse all" above leads - to the hub. It lists only the portals this person can open. */} - {(view === "club" || - isJudge || - isAdmin || - !!portalContext?.isBugTester) && ( -
      - {/* Club Portal — members only */} - {view === "club" && - (memberStatus?.isMember ? ( - - Club events, check-ins, and resources. - - ) : ( -
    • -

      Club Portal

      -

      - {memberStatus?.hasLapsed - ? "Your membership has run out. Renew below to get back in." - : "Membership required. Join below to unlock access."} -

      -
    • - ))} - - {/* Projects — club side, but browsing is open so anyone can see - what membership actually buys before paying for it. */} - {view === "club" && ( - - Projects the club runs year-round. Join one, or pitch your own. - - )} - - {/* Become a Member — sits beside Projects so the club view says - what is missing where the rest of the club lives. The pay UI is - the block below; this jumps to it. */} - {view === "club" && !memberStatus?.isMember && !isAdmin && ( - - {formatCents(MEMBERSHIP_CENTS)}/year or{" "} - {formatCents(SEMESTER_MEMBERSHIP_CENTS)}/semester. Opens the Club - Portal and member resources. - - )} - - {/* Judge Portal — judges only */} - {isJudge && ( - - Your judging editions, where you sit and the projects you score. - - )} - - {/* Admin Panel — admins, and bug testers read-only */} - {(isAdmin || !!portalContext?.isBugTester) && ( - - Manage events, attendees, and analytics. - - )} -
    - )} - - {/* The pay UI itself, below the destinations. The entry above is the - label; this is the block. */} - {view === "club" && !memberStatus?.isMember && !isAdmin && ( -
    -

    - {memberStatus?.hasLapsed ? "Renew your membership" : "Become a member"} -

    -
    - -
    -
    - )} - - {/* ── SIGN OUT ───────────────────────────────────── */} -
    -

    - Signed in{userData?.email ? ` as ${userData.email}` : ""}. -

    - -
    -
    - ); -} - -/** - * One place the dashboard can send you: a serif title with an arrow and a - * line saying what is there. `plain` renders an in-page anchor rather than a - * route link. - */ -function Destination({ - href, - title, - plain = false, - children, -}: { - href: string; - title: string; - plain?: boolean; - children: ReactNode; -}) { - const content = ( - <> - - - {title} - - - {children} - - ); +export default function Page() { return ( -
  • - {plain ? ( - - {content} - - ) : ( - - {content} - - )} -
  • + [ + ["user.me", undefined, c.user.me()], + ]} + > + + ); } diff --git a/sites/mainweb/app/(portal)/hackathons/[id]/HackathonDetailPage.tsx b/sites/mainweb/app/(portal)/hackathons/[id]/HackathonDetailPage.tsx new file mode 100644 index 00000000..63e8af4e --- /dev/null +++ b/sites/mainweb/app/(portal)/hackathons/[id]/HackathonDetailPage.tsx @@ -0,0 +1,384 @@ +"use client"; + +import { venueDayNumber, venueFormat, venueParts } from "@/lib/venue-time"; +import React, { + useState, + useEffect, + useOptimistic, + useRef, + startTransition, +} from "react"; +import { loginHref } from "@/lib/safe-callback"; +import { useSession } from "next-auth/react"; +import { trpc } from "@/lib/trpc"; +import { useRouter, useParams, useSearchParams } from "next/navigation"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import Link from "next/link"; +import dynamic from "next/dynamic"; +import { + meta, + page, + pageDek, + pageTitle, + status, + tab as tabClass, + tabList, +} from "@/components/portal/ui"; +import { hackathonStatus } from "@/lib/hackathon-status"; + +// Extracted Tab Components +import { InfoTab } from "@/components/hackathon/InfoTab"; +import { StatusBadge } from "@/components/hackathon/StatusBadge"; +import { HackathonUnavailable } from "@/components/hackathon/HackathonUnavailable"; +import { HackerAnnouncements } from "@/components/hackathon/HackerAnnouncements"; +import { NextStepStrip } from "@/components/hackathon/NextStepStrip"; +import { decodeHackathonParam } from "@/lib/hackathon-slug"; +import { + hackathonNextStep, + nextStepQueries, + projectSubmitted, +} from "@/lib/hackathon-next-step"; + +// Only the open tab is needed to draw, so the others stay out of the first +// load. They are fetched once the browser is idle, so a click finds them ready. +const loadScheduleTab = () => import("@/components/hackathon/ScheduleTab").then((m) => m.ScheduleTab); +const ScheduleTab = dynamic(loadScheduleTab); +const loadProjectsTab = () => import("@/components/hackathon/ProjectsTab").then((m) => m.ProjectsTab); +const ProjectsTab = dynamic(loadProjectsTab); +const loadResultsTab = () => import("@/components/hackathon/ResultsTab").then((m) => m.ResultsTab); +const ResultsTab = dynamic(loadResultsTab); +const loadTeamsTab = () => import("@/components/hackathon/TeamsTab").then((m) => m.TeamsTab); +const TeamsTab = dynamic(loadTeamsTab); +const loadHackerJudgingTab = () => import("@/components/hackathon/judging/HackerJudgingTab").then((m) => m.HackerJudgingTab); +const HackerJudgingTab = dynamic(loadHackerJudgingTab); +const loadPointsTab = () => import("@/components/hackathon/points/PointsTab").then((m) => m.PointsTab); +const PointsTab = dynamic(loadPointsTab); +const preloadTabs = () => { + for (const load of [loadScheduleTab, loadProjectsTab, loadResultsTab, loadTeamsTab, loadHackerJudgingTab, loadPointsTab]) void load(); +}; + +function formatDate(d: Date | string) { + return venueFormat(d, { + month: "short", + day: "numeric", + year: "numeric", + }); +} + +function formatDateRange(start: Date | string, end: Date | string) { + const s = venueParts(start); + const e = venueParts(end); + const short = { month: "short", day: "numeric" } as const; + if (s.month === e.month && s.year === e.year) { + return `${venueFormat(start, short)} - ${e.day}, ${e.year}`; + } + if (s.year === e.year) { + return `${venueFormat(start, short)} - ${venueFormat(end, short)}, ${e.year}`; + } + return `${formatDate(start)} - ${formatDate(end)}`; +} + +type TabType = + | "INFO" + | "SCHEDULE" + | "PROJECTS" + | "TEAMS" + | "JUDGING" + | "POINTS" + | "RESULTS"; + +const TABS: { id: TabType; label: string }[] = [ + { id: "INFO", label: "Overview" }, + { id: "SCHEDULE", label: "Schedule & pass" }, + { id: "PROJECTS", label: "Projects" }, + { id: "TEAMS", label: "Teams" }, + { id: "JUDGING", label: "Judging" }, + { id: "POINTS", label: "Points" }, + { id: "RESULTS", label: "Results" }, +]; + +// Nothing to judge, score or announce before the event runs, so these stay +// out of the strip until it does. +const EVENT_TABS = new Set(["JUDGING", "POINTS", "RESULTS"]); +const EVENT_STATUSES = new Set(["in_progress", "completed"]); + +// The strip's right edge fades out so on a phone it hints at the tabs past it. +const tabFade = + "[mask-image:linear-gradient(to_right,#000_calc(100%_-_1.5rem),transparent)]"; + +// Day granularity, on the venue's calendar: the last day counts all day. +const startOfDay = venueDayNumber; + +export default function HackathonDetailPage() { + const { data: session, status: authStatus } = useSession(); + const router = useRouter(); + const params = useParams(); + const searchParams = useSearchParams(); + const rawId = params.id; + const hackathonId = decodeHackathonParam( + Array.isArray(rawId) ? (rawId[0] ?? "") : ((rawId as string | undefined) ?? ""), + ); + + // The URL is the only source of the tab, so a same-page ?tab= link works. + // The optimistic copy shows a clicked tab at once instead of after the + // navigation lands, then hands back to whatever the URL says. + const tabParam = searchParams.get("tab"); + const urlTab = TABS.find((t) => t.id === tabParam)?.id ?? "INFO"; + const [shownTab, setShownTab] = useOptimistic(urlTab); + + useEffect(() => { + const id = window.setTimeout(preloadTabs, 1000); + return () => window.clearTimeout(id); + }, []); + + // Keep the tab in the URL so refreshing, sharing and Back all work. Next's + // history.replaceState keeps useSearchParams in step without asking the + // server to render the page again, which router.replace did on every click. + const selectTab = (next: TabType) => { + startTransition(() => { + setShownTab(next); + const qs = new URLSearchParams(searchParams.toString()); + qs.set("tab", next); + window.history.replaceState(null, "", `?${qs.toString()}`); + }); + }; + + const { + data: hackathon, + isLoading, + error, + } = trpc.hackathon.getById.useQuery({ + id: hackathonId, + }); + const { data: myRegs } = trpc.hackathon.myRegistrations.useQuery(undefined, { + enabled: !!session, + // An accepted hacker is waiting on the door scan. While the event runs, + // re-read so the page flips to checked in without a reload. + refetchInterval: (query) => { + const today = startOfDay(new Date()); + return query.state.data?.some( + (r) => + r.hackathonId === hackathon?.id && + r.registrationStatus === "approved" && + startOfDay(r.hackathon.startDate) <= today && + today <= startOfDay(r.hackathon.endDate), + ) + ? 30_000 + : false; + }, + }); + + const myReg = myRegs?.find((r) => r.hackathonId === hackathon?.id); + + // Only a flip seen while the page is open counts; the status it first + // loaded with never announces itself. + const regStatus = myReg?.registrationStatus; + const [seenStatus, setSeenStatus] = useState(regStatus); + const [justCheckedIn, setJustCheckedIn] = useState(false); + if (regStatus !== seenStatus) { + setSeenStatus(regStatus); + if (regStatus === "checked_in" && seenStatus) setJustCheckedIn(true); + } + useEffect(() => { + if (!justCheckedIn) return; + const timer = setTimeout(() => setJustCheckedIn(false), 10_000); + return () => clearTimeout(timer); + }, [justCheckedIn]); + + // The next-step strip reads up to four more queries, each held back until + // the step can use it, so only a registered hacker at the right point pays. + // oxlint-disable-next-line react/purity + const now = new Date(); + const needs = + hackathon && regStatus + ? nextStepQueries({ now, hackathon, registrationStatus: regStatus }) + : { submission: false, judging: false, results: false }; + const stepId = hackathon?.id ?? ""; + const submissionWindow = trpc.team.submissionWindow.useQuery( + { hackathonId: stepId }, + { enabled: needs.submission }, + ); + const mySubmission = trpc.team.mySubmission.useQuery( + { hackathonId: stepId }, + { enabled: needs.submission }, + ); + const itinerary = trpc.judging.hacker.itinerary.useQuery( + { hackathonId: stepId }, + { enabled: needs.judging }, + ); + const results = trpc.hackathon.getResults.useQuery( + { hackathonId: stepId }, + { enabled: needs.results }, + ); + // Held back until they answer, so the strip never shows a step it is about + // to take back ("Submit" to somebody who already has). + const stepLoading = + submissionWindow.isLoading || + mySubmission.isLoading || + itinerary.isLoading || + results.isLoading; + + useEffect(() => { + if (authStatus === "unauthenticated") router.push(loginHref()); + }, [authStatus, router]); + + // The strip scrolls sideways on a phone, so a ?tab= link to a later tab + // would land with it and its accent rule past the right edge. Scroll the + // strip itself: scrollIntoView would move the page too. `tabsMounted` is the + // render the strip first exists in; the tab alone does not change then. + const tabsRef = useRef(null); + const tabsMounted = + authStatus !== "loading" && !isLoading && !!session && !error && !!hackathon; + useEffect(() => { + const list = tabsRef.current; + const el = list?.querySelector(`#tab-${shownTab}`); + if (!list || !el) return; + const clipped = + el.offsetLeft < list.scrollLeft || + el.offsetLeft + el.offsetWidth > list.scrollLeft + list.clientWidth - 24; + if (clipped) list.scrollLeft = Math.max(0, el.offsetLeft - 24); + }, [shownTab, tabsMounted]); + + if (authStatus === "loading" || isLoading) + return ; + if (!session) return null; + // A hackathon can be missing (bad link, deleted event) or hidden. Without + // this branch the loading guard below never clears and the page spins + // forever with nothing to click. + if (error || !hackathon) return ; + + const isRegistered = !!myReg; + const nextStep = + myReg && !stepLoading + ? hackathonNextStep({ + now, + hackathon, + registrationStatus: myReg.registrationStatus, + hasTeam: !!myReg.teamId, + submissionWindow: submissionWindow.data, + submitted: projectSubmitted(mySubmission.data), + itinerary: itinerary.data, + resultsPublished: (results.data?.length ?? 0) > 0, + }) + : null; + // The status is moved by hand, so the start date counts too: an edition + // still marked closed on the weekend itself must not hide Judging. + const eventLive = + EVENT_STATUSES.has(hackathon.status) || + (hackathon.status !== "cancelled" && + // oxlint-disable-next-line react/purity + startOfDay(hackathon.startDate) <= startOfDay(new Date())); + const tabs = eventLive ? TABS : TABS.filter((t) => !EVENT_TABS.has(t.id)); + // A ?tab= for a tab that is not shown yet falls back to the overview. + const tab = tabs.some((t) => t.id === shownTab) ? shownTab : "INFO"; + const conf = hackathonStatus(hackathon.status); + + return ( +
    + + ← All hackathons + + +
    +
    + {conf.label} + {myReg && } +
    +

    {hackathon.name}

    + {hackathon.theme &&

    {hackathon.theme}

    } +

    + {[ + formatDateRange(hackathon.startDate, hackathon.endDate), + hackathon.location, + ] + .filter(Boolean) + .join(" · ")} +

    + {hackathon.maxParticipants ? ( +

    + {`${hackathon.currentParticipants} of ${hackathon.maxParticipants} spots taken`} +

    + ) : null} + {nextStep && } +
    + + {/* Mounted empty so screen readers announce the line when it lands. */} +
    + {justCheckedIn && ( +

    + You're checked in. +

    + )} +
    + + {/* What organisers announced to this hacker, emailed or in app only. + The uuid, not the route param: that may be a slug. */} + {regStatus && } + + {/* Tabs. The end padding lets the last tab scroll clear of the fade. */} +
    + {tabs.map((t) => ( + + ))} +
    + + {/* Content */} +
    + {tab === "INFO" ? ( + + ) : tab === "SCHEDULE" ? ( + + ) : tab === "JUDGING" ? ( + + ) : tab === "POINTS" ? ( + + ) : tab === "RESULTS" ? ( + + ) : tab === "PROJECTS" ? ( + + ) : ( + + )} +
    +
    + ); +} diff --git a/sites/mainweb/app/(portal)/hackathons/[id]/page.tsx b/sites/mainweb/app/(portal)/hackathons/[id]/page.tsx index 3e906bfb..ea8b93e7 100644 --- a/sites/mainweb/app/(portal)/hackathons/[id]/page.tsx +++ b/sites/mainweb/app/(portal)/hackathons/[id]/page.tsx @@ -1,362 +1,20 @@ -"use client"; - -import React, { - useState, - useEffect, - useOptimistic, - useRef, - startTransition, -} from "react"; -import { loginHref } from "@/lib/safe-callback"; -import { useSession } from "next-auth/react"; -import { trpc } from "@/lib/trpc"; -import { useRouter, useParams, useSearchParams } from "next/navigation"; -import { LoadingScreen } from "@/components/portal/LoadingScreen"; -import Link from "next/link"; -import { - meta, - page, - pageDek, - pageTitle, - status, - tab as tabClass, - tabList, -} from "@/components/portal/ui"; -import { hackathonStatus } from "@/lib/hackathon-status"; - -// Extracted Tab Components -import { InfoTab } from "@/components/hackathon/InfoTab"; -import { ScheduleTab } from "@/components/hackathon/ScheduleTab"; -import { ProjectsTab } from "@/components/hackathon/ProjectsTab"; -import { ResultsTab } from "@/components/hackathon/ResultsTab"; -import { TeamsTab } from "@/components/hackathon/TeamsTab"; -import { HackerJudgingTab } from "@/components/hackathon/judging/HackerJudgingTab"; -import { PointsTab } from "@/components/hackathon/points/PointsTab"; -import { StatusBadge } from "@/components/hackathon/StatusBadge"; -import { HackathonUnavailable } from "@/components/hackathon/HackathonUnavailable"; -import { HackerAnnouncements } from "@/components/hackathon/HackerAnnouncements"; -import { NextStepStrip } from "@/components/hackathon/NextStepStrip"; +import { PrefetchedPage } from "@/components/portal/PrefetchedPage"; import { decodeHackathonParam } from "@/lib/hackathon-slug"; -import { - hackathonNextStep, - nextStepQueries, - projectSubmitted, -} from "@/lib/hackathon-next-step"; - -function formatDate(d: Date | string) { - return new Date(d).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); -} - -function formatDateRange(start: Date | string, end: Date | string) { - const s = new Date(start); - const e = new Date(end); - if (s.getMonth() === e.getMonth() && s.getFullYear() === e.getFullYear()) { - return `${s.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${e.getDate()}, ${e.getFullYear()}`; - } - if (s.getFullYear() === e.getFullYear()) { - return `${s.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${e.toLocaleDateString("en-US", { month: "short", day: "numeric" })}, ${e.getFullYear()}`; - } - return `${formatDate(s)} - ${formatDate(e)}`; -} - -type TabType = - | "INFO" - | "SCHEDULE" - | "PROJECTS" - | "TEAMS" - | "JUDGING" - | "POINTS" - | "RESULTS"; - -const TABS: { id: TabType; label: string }[] = [ - { id: "INFO", label: "Overview" }, - { id: "SCHEDULE", label: "Schedule & pass" }, - { id: "PROJECTS", label: "Projects" }, - { id: "TEAMS", label: "Teams" }, - { id: "JUDGING", label: "Judging" }, - { id: "POINTS", label: "Points" }, - { id: "RESULTS", label: "Results" }, -]; - -// Nothing to judge, score or announce before the event runs, so these stay -// out of the strip until it does. -const EVENT_TABS = new Set(["JUDGING", "POINTS", "RESULTS"]); -const EVENT_STATUSES = new Set(["in_progress", "completed"]); - -// The strip's right edge fades out so on a phone it hints at the tabs past it. -const tabFade = - "[mask-image:linear-gradient(to_right,#000_calc(100%_-_1.5rem),transparent)]"; - -// Day granularity, on the viewer's clock: the last day counts all day. -const startOfDay = (d: Date | string) => new Date(d).setHours(0, 0, 0, 0); - -export default function HackathonDetailPage() { - const { data: session, status: authStatus } = useSession(); - const router = useRouter(); - const params = useParams(); - const searchParams = useSearchParams(); - const rawId = params.id; - const hackathonId = decodeHackathonParam( - Array.isArray(rawId) ? (rawId[0] ?? "") : ((rawId as string | undefined) ?? ""), - ); - - // The URL is the only source of the tab, so a same-page ?tab= link works. - // The optimistic copy shows a clicked tab at once instead of after the - // navigation lands, then hands back to whatever the URL says. - const tabParam = searchParams.get("tab"); - const urlTab = TABS.find((t) => t.id === tabParam)?.id ?? "INFO"; - const [shownTab, setShownTab] = useOptimistic(urlTab); - - // Keep the tab in the URL so refreshing, sharing and Back all work. - const selectTab = (next: TabType) => { - startTransition(() => { - setShownTab(next); - const qs = new URLSearchParams(searchParams.toString()); - qs.set("tab", next); - router.replace(`?${qs.toString()}`, { scroll: false }); - }); - }; - - const { - data: hackathon, - isLoading, - error, - } = trpc.hackathon.getById.useQuery({ - id: hackathonId, - }); - const { data: myRegs } = trpc.hackathon.myRegistrations.useQuery(undefined, { - enabled: !!session, - // An accepted hacker is waiting on the door scan. While the event runs, - // re-read so the page flips to checked in without a reload. - refetchInterval: (query) => { - const today = startOfDay(new Date()); - return query.state.data?.some( - (r) => - r.hackathonId === hackathon?.id && - r.registrationStatus === "approved" && - startOfDay(r.hackathon.startDate) <= today && - today <= startOfDay(r.hackathon.endDate), - ) - ? 30_000 - : false; - }, - }); - - const myReg = myRegs?.find((r) => r.hackathonId === hackathon?.id); - - // Only a flip seen while the page is open counts; the status it first - // loaded with never announces itself. - const regStatus = myReg?.registrationStatus; - const [seenStatus, setSeenStatus] = useState(regStatus); - const [justCheckedIn, setJustCheckedIn] = useState(false); - if (regStatus !== seenStatus) { - setSeenStatus(regStatus); - if (regStatus === "checked_in" && seenStatus) setJustCheckedIn(true); - } - useEffect(() => { - if (!justCheckedIn) return; - const timer = setTimeout(() => setJustCheckedIn(false), 10_000); - return () => clearTimeout(timer); - }, [justCheckedIn]); - - // The next-step strip reads up to four more queries, each held back until - // the step can use it, so only a registered hacker at the right point pays. - // oxlint-disable-next-line react/purity - const now = new Date(); - const needs = - hackathon && regStatus - ? nextStepQueries({ now, hackathon, registrationStatus: regStatus }) - : { submission: false, judging: false, results: false }; - const stepId = hackathon?.id ?? ""; - const submissionWindow = trpc.team.submissionWindow.useQuery( - { hackathonId: stepId }, - { enabled: needs.submission }, - ); - const mySubmission = trpc.team.mySubmission.useQuery( - { hackathonId: stepId }, - { enabled: needs.submission }, - ); - const itinerary = trpc.judging.hacker.itinerary.useQuery( - { hackathonId: stepId }, - { enabled: needs.judging }, - ); - const results = trpc.hackathon.getResults.useQuery( - { hackathonId: stepId }, - { enabled: needs.results }, - ); - // Held back until they answer, so the strip never shows a step it is about - // to take back ("Submit" to somebody who already has). - const stepLoading = - submissionWindow.isLoading || - mySubmission.isLoading || - itinerary.isLoading || - results.isLoading; - - useEffect(() => { - if (authStatus === "unauthenticated") router.push(loginHref()); - }, [authStatus, router]); - - // The strip scrolls sideways on a phone, so a ?tab= link to a later tab - // would land with it and its accent rule past the right edge. Scroll the - // strip itself: scrollIntoView would move the page too. `tabsMounted` is the - // render the strip first exists in; the tab alone does not change then. - const tabsRef = useRef(null); - const tabsMounted = - authStatus !== "loading" && !isLoading && !!session && !error && !!hackathon; - useEffect(() => { - const list = tabsRef.current; - const el = list?.querySelector(`#tab-${shownTab}`); - if (!list || !el) return; - const clipped = - el.offsetLeft < list.scrollLeft || - el.offsetLeft + el.offsetWidth > list.scrollLeft + list.clientWidth - 24; - if (clipped) list.scrollLeft = Math.max(0, el.offsetLeft - 24); - }, [shownTab, tabsMounted]); - - if (authStatus === "loading" || isLoading) - return ; - if (!session) return null; - // A hackathon can be missing (bad link, deleted event) or hidden. Without - // this branch the loading guard below never clears and the page spins - // forever with nothing to click. - if (error || !hackathon) return ; - - const isRegistered = !!myReg; - const nextStep = - myReg && !stepLoading - ? hackathonNextStep({ - now, - hackathon, - registrationStatus: myReg.registrationStatus, - hasTeam: !!myReg.teamId, - submissionWindow: submissionWindow.data, - submitted: projectSubmitted(mySubmission.data), - itinerary: itinerary.data, - resultsPublished: (results.data?.length ?? 0) > 0, - }) - : null; - // The status is moved by hand, so the start date counts too: an edition - // still marked closed on the weekend itself must not hide Judging. - const eventLive = - EVENT_STATUSES.has(hackathon.status) || - (hackathon.status !== "cancelled" && - // oxlint-disable-next-line react/purity - startOfDay(hackathon.startDate) <= startOfDay(new Date())); - const tabs = eventLive ? TABS : TABS.filter((t) => !EVENT_TABS.has(t.id)); - // A ?tab= for a tab that is not shown yet falls back to the overview. - const tab = tabs.some((t) => t.id === shownTab) ? shownTab : "INFO"; - const conf = hackathonStatus(hackathon.status); - +import HackathonDetailPage from "./HackathonDetailPage"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const id = decodeHackathonParam((await params).id); return ( -
    - - ← All hackathons - - -
    -
    - {conf.label} - {myReg && } -
    -

    {hackathon.name}

    - {hackathon.theme &&

    {hackathon.theme}

    } -

    - {[ - formatDateRange(hackathon.startDate, hackathon.endDate), - hackathon.location, - ] - .filter(Boolean) - .join(" · ")} -

    - {hackathon.maxParticipants ? ( -

    - {`${hackathon.currentParticipants} of ${hackathon.maxParticipants} spots taken`} -

    - ) : null} - {nextStep && } -
    - - {/* Mounted empty so screen readers announce the line when it lands. */} -
    - {justCheckedIn && ( -

    - You're checked in. -

    - )} -
    - - {/* What organisers announced to this hacker, emailed or in app only. - The uuid, not the route param: that may be a slug. */} - {regStatus && } - - {/* Tabs. The end padding lets the last tab scroll clear of the fade. */} -
    - {tabs.map((t) => ( - - ))} -
    - - {/* Content */} -
    - {tab === "INFO" ? ( - - ) : tab === "SCHEDULE" ? ( - - ) : tab === "JUDGING" ? ( - - ) : tab === "POINTS" ? ( - - ) : tab === "RESULTS" ? ( - - ) : tab === "PROJECTS" ? ( - - ) : ( - - )} -
    -
    + [ + ["hackathon.getById", { id }, c.hackathon.getById({ id })], + ]} + > + + ); } diff --git a/sites/mainweb/app/(portal)/judge/JudgePage.tsx b/sites/mainweb/app/(portal)/judge/JudgePage.tsx new file mode 100644 index 00000000..055e1bab --- /dev/null +++ b/sites/mainweb/app/(portal)/judge/JudgePage.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { venueFormat } from "@/lib/venue-time"; +import { useEffect } from "react"; +import Link from "next/link"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { ChevronRight } from "lucide-react"; +import { loginHref } from "@/lib/safe-callback"; +import { trpc } from "@/lib/trpc"; +import { useIsClient } from "@/lib/use-is-client"; +import { usePortalContext } from "@/lib/use-portal-context"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import { + body, + itemTitle, + label, + meta, + page, + pageDek, + pageTitle, + sectionRule, + status, + textLink, +} from "@/components/portal/ui"; + +// Statuses that take judge applications; judge.register enforces the same. +const APPLYING = new Set(["announced", "open", "closed", "in_progress"]); + +function formatDates(start: Date | string, end: Date | string) { + const opts: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" }; + return `${venueFormat(start, opts)} – ${venueFormat(end, { ...opts, year: "numeric" })}`; +} + +export default function JudgePage() { + const { data: session, status: authStatus } = useSession(); + const router = useRouter(); + const mounted = useIsClient(); + + // isJudge covers a judge an organiser added by email, who has no application; + // myApplications covers every edition the user applied to, approved or not. + const { data: judgeStatus, isLoading: checkingJudge } = + trpc.judge.isJudge.useQuery(undefined, { enabled: !!session }); + const { data: applications, isLoading: applicationsLoading } = + trpc.judge.myApplications.useQuery(undefined, { enabled: !!session }); + const { data: hackathons, isLoading: hackathonsLoading } = + trpc.hackathon.list.useQuery({ limit: 50 }); + // Staff can judge any edition without applying. + const { data: portal } = usePortalContext(); + + // The approval email points here, so a signed-out visitor goes to sign in. + useEffect(() => { + if (authStatus === "unauthenticated") router.push(loginHref()); + }, [authStatus, router]); + + if ( + !mounted || + authStatus === "loading" || + checkingJudge || + applicationsLoading || + hackathonsLoading + ) { + return ; + } + + if (!session) return null; + + const byId = new Map((hackathons ?? []).map((h) => [h.id, h])); + + const judgingIds = new Set( + (applications ?? []).filter((a) => a.approved).map((a) => a.hackathonId), + ); + if (judgeStatus?.isJudge && judgeStatus.hackathonId) { + judgingIds.add(judgeStatus.hackathonId); + } + if (portal?.isAdmin) { + for (const h of hackathons ?? []) { + if (h.status === "in_progress") judgingIds.add(h.id); + } + } + const waitingIds = new Set( + (applications ?? []) + .filter((a) => !a.approved && !judgingIds.has(a.hackathonId)) + .map((a) => a.hackathonId), + ); + const canApply = (hackathons ?? []).filter( + (h) => + APPLYING.has(h.status) && !judgingIds.has(h.id) && !waitingIds.has(h.id), + ); + + const judging = [...judgingIds]; + const waiting = [...waitingIds]; + + return ( +
    +
    +

    Judging

    +

    + {judging.length > 0 + ? "Open your edition on judging day, then scan the QR code at your room or table to take your seat." + : "Apply to judge an edition. An organiser approves you before judging day."} +

    +
    + + {judging.length > 0 && ( +
    +

    You are judging

    +
      + {judging.map((id) => { + const h = byId.get(id); + return ( +
    • + +
      +

      + {h?.name ?? "Hackathon"} +

      + {h && ( +

      + {formatDates(h.startDate, h.endDate)} + {h.location ? ` · ${h.location}` : ""} +

      + )} +
      +
    • + ); + })} +
    +
    + )} + + {waiting.length > 0 && ( +
    +

    Waiting for approval

    +
      + {waiting.map((id) => ( +
    • +

      + {byId.get(id)?.name ?? "Hackathon"} +

      + Applied +
    • + ))} +
    +

    + You will get an email when an organiser approves you. +

    +
    + )} + +
    +

    Taking judges

    + {canApply.length === 0 ? ( +

    + No edition is taking judge applications right now. +

    + ) : ( +
      + {canApply.map((h) => ( +
    • +
      +

      {h.name}

      +

      + {formatDates(h.startDate, h.endDate)} +

      +
      + + Apply to judge + +
    • + ))} +
    + )} +
    +
    + ); +} diff --git a/sites/mainweb/app/(portal)/judge/page.tsx b/sites/mainweb/app/(portal)/judge/page.tsx index 568d5462..9cb17ba8 100644 --- a/sites/mainweb/app/(portal)/judge/page.tsx +++ b/sites/mainweb/app/(portal)/judge/page.tsx @@ -1,196 +1,16 @@ -"use client"; - -import { useEffect } from "react"; -import Link from "next/link"; -import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import { ChevronRight } from "lucide-react"; -import { loginHref } from "@/lib/safe-callback"; -import { trpc } from "@/lib/trpc"; -import { useIsClient } from "@/lib/use-is-client"; -import { usePortalContext } from "@/lib/use-portal-context"; -import { LoadingScreen } from "@/components/portal/LoadingScreen"; -import { - body, - itemTitle, - label, - meta, - page, - pageDek, - pageTitle, - sectionRule, - status, - textLink, -} from "@/components/portal/ui"; - -// Statuses that take judge applications; judge.register enforces the same. -const APPLYING = new Set(["announced", "open", "closed", "in_progress"]); - -function formatDates(start: Date | string, end: Date | string) { - const opts: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" }; - const s = new Date(start); - const e = new Date(end); - return `${s.toLocaleDateString("en-US", opts)} – ${e.toLocaleDateString("en-US", { ...opts, year: "numeric" })}`; -} - -export default function JudgePage() { - const { data: session, status: authStatus } = useSession(); - const router = useRouter(); - const mounted = useIsClient(); - - // isJudge covers a judge an organiser added by email, who has no application; - // myApplications covers every edition the user applied to, approved or not. - const { data: judgeStatus, isLoading: checkingJudge } = - trpc.judge.isJudge.useQuery(undefined, { enabled: !!session }); - const { data: applications, isLoading: applicationsLoading } = - trpc.judge.myApplications.useQuery(undefined, { enabled: !!session }); - const { data: hackathons, isLoading: hackathonsLoading } = - trpc.hackathon.list.useQuery({ limit: 50 }); - // Staff can judge any edition without applying. - const { data: portal } = usePortalContext(); - - // The approval email points here, so a signed-out visitor goes to sign in. - useEffect(() => { - if (authStatus === "unauthenticated") router.push(loginHref()); - }, [authStatus, router]); - - if ( - !mounted || - authStatus === "loading" || - checkingJudge || - applicationsLoading || - hackathonsLoading - ) { - return ; - } - - if (!session) return null; - - const byId = new Map((hackathons ?? []).map((h) => [h.id, h])); - - const judgingIds = new Set( - (applications ?? []).filter((a) => a.approved).map((a) => a.hackathonId), - ); - if (judgeStatus?.isJudge && judgeStatus.hackathonId) { - judgingIds.add(judgeStatus.hackathonId); - } - if (portal?.isAdmin) { - for (const h of hackathons ?? []) { - if (h.status === "in_progress") judgingIds.add(h.id); - } - } - const waitingIds = new Set( - (applications ?? []) - .filter((a) => !a.approved && !judgingIds.has(a.hackathonId)) - .map((a) => a.hackathonId), - ); - const canApply = (hackathons ?? []).filter( - (h) => - APPLYING.has(h.status) && !judgingIds.has(h.id) && !waitingIds.has(h.id), - ); - - const judging = [...judgingIds]; - const waiting = [...waitingIds]; +import { PrefetchedPage } from "@/components/portal/PrefetchedPage"; +import JudgePage from "./JudgePage"; +export default function Page() { return ( -
    -
    -

    Judging

    -

    - {judging.length > 0 - ? "Open your edition on judging day, then scan the QR code at your room or table to take your seat." - : "Apply to judge an edition. An organiser approves you before judging day."} -

    -
    - - {judging.length > 0 && ( -
    -

    You are judging

    -
      - {judging.map((id) => { - const h = byId.get(id); - return ( -
    • - -
      -

      - {h?.name ?? "Hackathon"} -

      - {h && ( -

      - {formatDates(h.startDate, h.endDate)} - {h.location ? ` · ${h.location}` : ""} -

      - )} -
      -
    • - ); - })} -
    -
    - )} - - {waiting.length > 0 && ( -
    -

    Waiting for approval

    -
      - {waiting.map((id) => ( -
    • -

      - {byId.get(id)?.name ?? "Hackathon"} -

      - Applied -
    • - ))} -
    -

    - You will get an email when an organiser approves you. -

    -
    - )} - -
    -

    Taking judges

    - {canApply.length === 0 ? ( -

    - No edition is taking judge applications right now. -

    - ) : ( -
      - {canApply.map((h) => ( -
    • -
      -

      {h.name}

      -

      - {formatDates(h.startDate, h.endDate)} -

      -
      - - Apply to judge - -
    • - ))} -
    - )} -
    -
    + [ + ["judge.isJudge", undefined, c.judge.isJudge()], + ["judge.myApplications", undefined, c.judge.myApplications()], + ["hackathon.list", { limit: 50 }, c.hackathon.list({ limit: 50 })], + ]} + > + + ); } diff --git a/sites/mainweb/app/(portal)/layout.tsx b/sites/mainweb/app/(portal)/layout.tsx index 373fe337..34ca0a5d 100644 --- a/sites/mainweb/app/(portal)/layout.tsx +++ b/sites/mainweb/app/(portal)/layout.tsx @@ -4,6 +4,7 @@ export const revalidate = 0; // Disable ISR for authenticated pages import { preload } from "react-dom"; import { Providers } from "./providers"; +import { loadPortalBootstrap } from "@/lib/portal-bootstrap.server"; import "./fonts.generated.css"; import "./liquid-glass.css"; import PortalWrapper from "@/components/portal/PortalWrapper"; @@ -14,7 +15,7 @@ import { FONT_PRELOADS } from "@/lib/fonts.generated"; // stylesheet sets their variables on :root, so modals portaled to // resolve the font roles in liquid-glass.css too. -export default function PortalLayout({ +export default async function PortalLayout({ children, }: { children: React.ReactNode; @@ -23,8 +24,10 @@ export default function PortalLayout({ preload(href, { as: "font", type: "font/woff2", crossOrigin: "" }), ); + const { session, state } = await loadPortalBootstrap(); + return ( - +
    {children}
    diff --git a/sites/mainweb/app/(portal)/providers.tsx b/sites/mainweb/app/(portal)/providers.tsx index 89a60197..5d37d9fd 100644 --- a/sites/mainweb/app/(portal)/providers.tsx +++ b/sites/mainweb/app/(portal)/providers.tsx @@ -2,7 +2,12 @@ import type { ReactNode } from "react"; import { SessionProvider } from "next-auth/react"; -import { QueryClientProvider } from "@tanstack/react-query"; +import { + HydrationBoundary, + QueryClientProvider, +} from "@tanstack/react-query"; +import type { DehydratedState } from "@tanstack/react-query"; +import type { Session } from "next-auth"; import { httpBatchLink } from "@trpc/client"; import { useEffect, useState } from "react"; import superjson from "superjson"; @@ -39,7 +44,15 @@ function AppThemeProvider({ ); } -export function Providers({ children }: { children: React.ReactNode }) { +export function Providers({ + children, + session, + state, +}: { + children: React.ReactNode; + session?: Session; + state?: DehydratedState; +}) { const [queryClient] = useState(() => createAppQueryClient()); const [trpcClient] = useState(() => @@ -55,9 +68,11 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( - + - {children} + + {children} + diff --git a/sites/mainweb/app/(portal)/settings/SettingsPage.tsx b/sites/mainweb/app/(portal)/settings/SettingsPage.tsx new file mode 100644 index 00000000..5041e758 --- /dev/null +++ b/sites/mainweb/app/(portal)/settings/SettingsPage.tsx @@ -0,0 +1,518 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { loginHref } from "@/lib/safe-callback"; +import { useSession, signOut } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useTheme } from "next-themes"; +import { Check, LogOut } from "lucide-react"; +import { MembershipTab } from "@/components/portal/MembershipTab"; +import { ResumeSection } from "@/components/portal/ResumeSection"; +import Image from "next/image"; +import { trpc } from "@/lib/trpc"; +import { trpcErrorMessage } from "@/lib/trpc-error"; +import { useIsClient } from "@/lib/use-is-client"; +import { LoadingScreen } from "@/components/portal/LoadingScreen"; +import { + page, + pageTitle, + pageDek, + sectionTitle, + sectionRule, + meta, + body, + fieldLabel, + fieldHint, + input, + btnPrimary, + btnSecondary, + tabList, + tab as tabClass, + status as statusClass, +} from "@/components/portal/ui"; + +const readOnlyInput = + "w-full rounded-[var(--radius-sm)] border border-[var(--border-subtle)] bg-[var(--bg-secondary)] px-3.5 py-2.5 text-[15px] text-[var(--text-muted)] cursor-not-allowed focus:outline-none"; + +export default function SettingsPage() { + const { data: session, status } = useSession(); + const router = useRouter(); + const { theme, setTheme } = useTheme(); + const mounted = useIsClient(); + const [activeTab, setActiveTab] = useState< + "profile" | "membership" | "appearance" | "account" + >("profile"); + const [isSaving, setIsSaving] = useState(false); + const [saved, setSaved] = useState(false); + const [isUploadingImage, setIsUploadingImage] = useState(false); + + const { data: userData } = trpc.user.me.useQuery(undefined, { enabled: !!session }); + const updateProfile = trpc.user.updateProfile.useMutation(); + const updateProfileImage = trpc.user.updateProfileImage.useMutation(); + const utils = trpc.useUtils(); + + const handleImageUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setIsUploadingImage(true); + + try { + // 1. Read file as Data URL + const reader = new FileReader(); + reader.onloadend = async () => { + try { + const originalBase64 = reader.result as string; + + // 2. Client-side compression via Canvas to avoid overkilling the database + const img = new window.Image(); + img.src = originalBase64; + + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + }); + + // Max dimensions for an avatar + const MAX_SIZE = 400; + let width = img.width; + let height = img.height; + + if (width > height && width > MAX_SIZE) { + height *= MAX_SIZE / width; + width = MAX_SIZE; + } else if (height > MAX_SIZE) { + width *= MAX_SIZE / height; + height = MAX_SIZE; + } + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("Could not get canvas context"); + + ctx.drawImage(img, 0, 0, width, height); + + // Compress to WebP at 80% quality + const compressedBase64 = canvas.toDataURL("image/webp", 0.8); + + // 3. Upload the tiny payload + await updateProfileImage.mutateAsync({ base64Image: compressedBase64 }); + await utils.user.me.invalidate(); + } catch (err) { + console.error("Failed to process and upload image:", err); + // Optional: show a toast notification here + } finally { + setIsUploadingImage(false); + } + }; + + reader.onerror = () => { + console.error("FileReader failed"); + setIsUploadingImage(false); + }; + + reader.readAsDataURL(file); + } catch (e) { + console.error(e); + setIsUploadingImage(false); + } + }; + + const [form, setForm] = useState({ + name: "", + bio: "", + website: "", + location: "", + gtEmail: "", + }); + + useEffect(() => { + if (status === "unauthenticated") router.push(loginHref()); + }, [status, router]); + + // Refill the form whenever new user data arrives. Done during render rather + // than in an effect, so the stale form never paints. + const [filledFrom, setFilledFrom] = useState(undefined); + if (userData && userData !== filledFrom) { + setFilledFrom(userData); + setForm({ + name: userData.name || "", + bio: userData.bio || "", + website: userData.website || "", + location: userData.location || "", + gtEmail: userData.gtEmail || "", + }); + } + + const handleSave = async () => { + setIsSaving(true); + try { + // Sent as-is, not `|| undefined`: an emptied field means "clear it", and + // collapsing it to undefined made the server skip it while the UI still + // reported a save. + await updateProfile.mutateAsync({ + name: form.name || undefined, + bio: form.bio, + website: form.website, + location: form.location, + gtEmail: form.gtEmail.trim(), + }); + await utils.user.me.invalidate(); + setSaved(true); + setTimeout(() => setSaved(false), 3000); + } catch (e) { + console.error(e); + } finally { + setIsSaving(false); + } + }; + + if (status === "loading") return ; + if (!session) return null; + + const tabs = [ + { id: "profile", label: "Profile", desc: "Name, photo, bio and resume" }, + // The club member profile: school, major, skills, interests, socials. The + // columns and the procedures existed with no screen behind them. + { id: "membership", label: "Membership", desc: "Status, school and links" }, + { id: "appearance", label: "Appearance", desc: "Paper or night" }, + { id: "account", label: "Account", desc: "Google sign-in and sign out" }, + ] as const; + + // Each swatch sets its own theme class so it is drawn with that edition's + // tokens, whichever edition the page is in. + const themes = [ + { id: "light", name: "Paper", desc: "Bone ground, near-black ink", scope: "light" }, + { id: "dark", name: "Night", desc: "Deep ground, soft ink", scope: "dark" }, + ] as const; + + return ( +
    +

    Settings

    +

    + Your profile, club membership, theme and account. +

    + +
    + {/* Tabs: horizontal on small screens, a vertical list from lg up */} +
    + {tabs.map((t) => ( + + ))} +
    + + + {/* Content */} +
    + {/* Profile Tab */} + {activeTab === "profile" && ( +
    +

    Profile

    + + {/* Avatar */} +
    +