From a41ed402a6eb06ca1ea6a26013f6bfd157e2a878 Mon Sep 17 00:00:00 2001 From: sercanvr Date: Sun, 18 Oct 2026 06:08:04 +0300 Subject: [PATCH 01/61] docs: initialize project documentation, license, and architectural decisions --- .env.example | 12 + LICENSE | 21 ++ README.md | 125 +++++++- SECURITY.md | 17 ++ agent-rules.md | 109 +++++++ architecture.md | 438 +++++++++++++++++++++++++++ docs/adr/001-vite-spa.md | 14 + docs/adr/002-bff-hono.md | 14 + docs/adr/003-responsive-and-tv-ui.md | 17 ++ docs/providers.md | 80 +++++ 10 files changed, 846 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 agent-rules.md create mode 100644 architecture.md create mode 100644 docs/adr/001-vite-spa.md create mode 100644 docs/adr/002-bff-hono.md create mode 100644 docs/adr/003-responsive-and-tv-ui.md create mode 100644 docs/providers.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cfdf4db --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# RageTV Server Environment Variables (BFF) +# DO NOT add secrets to client-side bundles (no VITE_ prefix for secrets) + +# Twitch Helix API Credentials +TWITCH_CLIENT_ID=your_twitch_client_id_here +TWITCH_CLIENT_SECRET=your_twitch_client_secret_here + +# YouTube Data API v3 Key +YOUTUBE_API_KEY=your_youtube_api_key_here + +# PandaScore API Token (for tournament fixtures in M4) +PANDASCORE_TOKEN=your_pandascore_token_here diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6a45d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sercan Var + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3099e45..edbdc80 100644 --- a/README.md +++ b/README.md @@ -1 +1,124 @@ -# ragetv \ No newline at end of file +# RageTV 🎮⚡ + +> **Pick a game. Watch live esports.** +> Ultra-fast, privacy-first, open-source live esports aggregator powered exclusively by official embedded players. + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?logo=typescript)](https://www.typescriptlang.org/) +[![React](https://img.shields.io/badge/React-18%20%7C%2019-61dafb?logo=react)](https://react.dev/) +[![Vite](https://img.shields.io/badge/Vite-6.x-646CFF?logo=vite)](https://vitejs.dev/) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind-CSS-38B2AC?logo=tailwind-css)](https://tailwindcss.com/) +[![Legal Compliance](https://img.shields.io/badge/Embeds-100%25%20Official-success)](#-legal-compliance--disclaimer) + +--- + +## 📖 About RageTV / Hakkında + +**EN:** +**RageTV** is an open-source, non-commercial esports discovery and live match viewing hub. Finding an ongoing tournament match across different platforms, languages, and channels is often fragmented and cluttered with intrusive ads, paywalls, and heavy interfaces. RageTV solves this with a singular, distraction-free vision: **select your game and start watching the official live broadcast in one click.** If no live match is currently running, it seamlessly falls back to the latest tournament match VOD/poster, ensuring your screen is never empty. + +**TR:** +**RageTV**, açık kaynaklı ve ticari olmayan bir espor canlı yayın ve turnuva keşif platformudur. Farklı platformlara, dillere ve kanallara dağılmış espor turnuvalarını takip etmek çoğu zaman karmaşık ve dikkat dağıtıcıdır. RageTV bu problemi tek bir felsefeyle çözer: **Sol menüden oyununu seç ve resmi canlı yayını tek tıkla izlemeye başla.** Canlı yayın yoksa dahi ekran asla boş kalmaz; o oyunun son resmi turnuva VOD/posteri hazır bekler ve tıklandığında oynatılır. + +--- + +## ⚡ Core Principles / Temel İlkeler + +1. **100% Official & Legal Embeds (Telif Riski Sıfır):** + RageTV does **not** host, record, transcode, scrape, or restream any video. All media playback is driven strictly through the official, unmodified embedded players of the original platforms (Twitch, YouTube). Channel branding, chat policies, and copyright remain entirely with their respective owners. +2. **Zero-Cost & Free-Tier Native:** + Engineered to run seamlessly without costly servers or paid third-party enterprise subscriptions. Powered by lightweight serverless functions (Hono BFF) and edge caching (`stale-while-revalidate`). +3. **No Scraping Policy:** + Strictly zero scraping of unofficial endpoints or third-party web pages (no HLTV or unofficial web parsers). Every feed is mapped via legitimate developer APIs and curated official registries. +4. **Privacy-First & No Accounts:** + No logins, no tracking cookies, and no database bloat. User preferences (active game, theme, locale) persist strictly in local browser storage (`localStorage`). +5. **Honest Data:** + Every indicator shows verified upstream data with timestamp (`fetchedAt`). Missing or unverified data is displayed as `N/A`, never fabricated or silently zeroed out. +6. **Cross-Device & 10-Foot UI (TV Ready):** + Engineered for seamless navigation across **Mobile**, **Tablet**, **Laptop**, **Desktop**, and **Television (Smart TV / Large Screens)** with accessibility-first keyboard & focus management. + +--- + +## 🎯 Supported Games (v1) + +| Game | Curated Channels & Tournaments | Primary Embed Platform | Fallback Mode | +|---|---|---|---| +| **CS2** (Counter-Strike 2) | ESL, BLAST, PGL | Twitch / YouTube | Recent Match VOD / Poster | +| **Valorant** | VCT, Champions Tour | Twitch / YouTube | Recent Tournament VOD | +| **PUBG** | PUBG Esports, Regional Qualifiers | YouTube / Twitch | Match Highlights VOD | +| **League of Legends** | Riot Games, LEC, LCK, LCS | Twitch / YouTube | VOD & Match Replays | +| **Dota 2** | PGL, ESL Dota 2, TI | Twitch / YouTube | Recent Match Replay | + +--- + +## 🖥️ UX/UI Design & Responsive Breakpoints + +RageTV features a sleek, dark-mode gaming aesthetic inspired by modern esports dashboards (custom token system in `src/styles/tokens.css`): + +* **Mobile (`< 640px`):** Single-column layout, horizontal scrolling game selector bar, native touch-optimized player. +* **Tablet (`640px - 1023px`):** Flexible stage layout with quick metadata drawer. +* **Laptop & Desktop (`≥ 1024px`):** Ergonomic collapsible sidebar, 16:9 cinematic `PlayerStage`, live match indicators, stream info panel, and quick platform switcher. +* **Television (`tv: ≥ 1920px` & 10-foot UI):** High-contrast focus outlines, scaled typography, and D-pad/remote-control friendly keyboard navigation. + +--- + +## 🏗️ Architecture & Tech Stack + +``` +ragetv/ +├── shared/ # Universal types, Zod schemas, game/channel registries & URL validators +├── server/ # Lightweight BFF (Hono on Vercel Functions), provider adapters & caching +├── src/ # Vite React SPA (features: games, player, live, preferences) +├── public/ # Static assets, self-hosted WOFF2 fonts, favicon +└── docs/ # Provider specifications (docs/providers.md) & Architecture Decision Records (ADRs) +``` + +- **Frontend:** React, Vite, TypeScript (Strict), Tailwind CSS, Lucide Icons, TanStack Query. +- **Backend / BFF:** Hono (Vercel Serverless / Edge compatible). +- **Internationalization (i18n):** EN, TR, DE (lazy-loaded locale files with `Intl.*` formatting). +- **Quality & Testing:** Vitest, Testing Library, MSW (Mock Service Worker), vitest-axe (a11y). + +--- + +## ⚖️ Legal Compliance & Disclaimer + +**EN:** +RageTV is an independent, non-commercial esports discovery site. It does not host, record, or rebroadcast any video. Live streams and videos are shown through the official embedded players of their original platforms (such as Twitch and YouTube) and remain the property of their respective owners. All trademarks, logos, and game titles belong to their respective owners; RageTV is not affiliated with, endorsed by, or sponsored by them. +*Content owners can request immediate channel/stream exclusion via GitHub Issues.* + +**TR:** +RageTV, bağımsız ve ticari olmayan bir espor keşif sitesidir. Hiçbir video içeriğini barındırmaz, kaydetmez veya yeniden yayınlamaz. Canlı yayınlar ve videolar, özgün platformların (Twitch, YouTube vb.) resmi gömülü oynatıcıları aracılığıyla gösterilir ve ilgili sahiplerine aittir. Tüm ticari markalar, logolar ve oyun adları sahiplerine aittir; RageTV bunlarla bağlantılı değildir, onlar tarafından onaylanmamış veya desteklenmemiştir. +*İçerik sahipleri kanal veya yayın kaldırma taleplerini GitHub Issues üzerinden iletebilir.* + +--- + +## 🚀 Getting Started + +### Prerequisites +- Node.js LTS (v20+) +- PNPM (v9+) + +### Installation +```bash +# Clone repository +git clone https://github.com/sercanvr/ragetv.git +cd ragetv + +# Install dependencies (once packages are scaffolded) +pnpm install + +# Start local development server +pnpm dev +``` + +--- + +## 🤝 Contributing + +Contributions, feature suggestions, and verified tournament channel additions are warmly welcomed! Please read our guidelines and ensure local tests pass before opening a Pull Request. + +--- + +## 📄 License + +This project is open-source and distributed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..98dfcb2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +## Reporting Security Issues + +RageTV takes the security of its platform and users seriously. + +If you believe you have discovered a vulnerability or security issue, please do **NOT** open a public issue. Instead, report it privately via GitHub: + +1. Navigate to the **Security** tab of this repository. +2. Click **Report a vulnerability** to submit a private report. +3. Our maintainers will review the submission promptly and keep you updated. + +## Security Practices +- Strict Content Security Policy (CSP) blocking unauthorized script execution and unapproved iframe sources. +- No storage of sensitive user data, credentials, or cookies. +- Server-side validation of all external provider inputs with Zod schemas. +- Dependency auditing via automated supply-chain scanners and Dependabot. diff --git a/agent-rules.md b/agent-rules.md new file mode 100644 index 0000000..bb7521c --- /dev/null +++ b/agent-rules.md @@ -0,0 +1,109 @@ +# Agent Çalışma Kuralları + +Bu dosya, IDE üzerinde çalışan agent'ın her görevde uyması gereken sabit kurallardır. Kurallar her prompt'ta tekrar açıklanmayacak, bu dosya referans alınacaktır. + + +--- + +## 1. Genel İlkeler + +- Her göreve başlamadan önce görev analiz edilmeli, gerekli düşünme adımı tamamlanmalı ve ardından uygulamaya geçilmelidir. Doğrudan koda dalınmamalıdır. +- Kod yazılırken Clean Code, DRY (Don't Repeat Yourself) ve KISS (Keep It Simple, Stupid) prensiplerine uyulmalıdır. +- Projeye zarar verilmemesi için mevcut yapı ve konvansiyonlar dikkatle korunmalı, gereksiz yeniden yazım yapılmamalıdır. +- Görev tam olarak istenen kapsamda yerine getirilmeli, eksik veya kapsam dışı değişiklik yapılmamalıdır. +- `npm run lint` ve `npm run build` komutları agent tarafından çalıştırılmamalıdır. Bu kontroller kullanıcı tarafından manuel yapılacaktır. + +--- + +## 2. Token ve Kaynak Kullanımı Kuralları + +- Yanıtlar ve açıklamalar gerekenden fazla uzatılmamalı, gereksiz tekrar ve dolgu cümlelerden kaçınılmalıdır. +- Dosya okuma, tarama ve arama işlemleri gerekli olan minimum kapsamla sınırlı tutulmalıdır. +- **Yasaklı Dizinler:** Agent asla `.env*`, `node_modules`, `dist`, `build`, `.next`, `.git` klasörlerini ve `package-lock.json`, `yarn.lock` dosyalarını okumamalı/taramamalıdır. +- Aynı bilgi tekrar tekrar sorgulanmamalı, bir defa elde edilen bağlam görev boyunca hafızada tutulmalıdır. +- Değişiklikler mümkün olduğunca tek seferde ve verimli şekilde tamamlanmalı, parça parça deneme-yanılma yöntemi tercih edilmemelidir. + +--- + +## 3. Görev Öncesi Kontrol Adımları + +Her yeni görevden önce sırasıyla şu adımlar uygulanmalıdır. + +1. Uzak depodaki `main` branch'in durumu sadece `git fetch origin main` ile kontrol edilir. +2. Hassas veri (API anahtarları, şifreler, token'lar vb.) kontrolü yapılmalı; hardcoded veri riski taşıyan dosyalar gözden geçirilerek gerekli güvenlik önlemleri hatırlatılmalıdır. +3. Bu kontrol tamamlanmadan yeni bir branch açılmamalıdır. +4. Yeni branch her zaman `origin/main` üzerinden açılır: `git checkout -b origin/main` + +--- + +## 4. Güvenlik Kuralları (OWASP Top 10 - Zorunlu Kontrol) + +- Kod yazılırken şu 3 kritik nokta ek token harcamadan mutlaka kontrol edilmelidir: + - a) Hardcoded secret / API key var mı? + - b) `dangerouslySetInnerHTML`, `innerHTML`, `eval()` kullanımı ile XSS riski var mı? + - c) SQL / NoSQL sorgularında string concatenation ile Injection riski var mı? +- Şüpheli veya riskli bir durum fark edilirse, kod düzeltilir ve görev raporunda `Security Note` başlığı altında kısaca belirtilir. + +--- + +## 5. Responsive Tasarım Kuralları + +- Yazılan her kod mobile-first felsefesi ile geliştirilmelidir. +- Sabit piksel değerleri yerine proje standardı olan Tailwind breakpoint'leri referans alınmalıdır: + `sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px` +- Responsive kontrolü, ek token maliyeti yaratmayacak şekilde kodun genel uyumluluğunun gözden geçirilmesi şeklinde uygulanmalıdır. + +--- + +## 6. Branch Oluşturma Kuralları + +- Her görev için benzersiz ve göreve özgü yeni bir branch açılmalıdır. +- Branch adı İngilizce olmalı ve Conventional Commits standardına uygun bir önek taşımalıdır (örnek önekler `feat/`, `fix/`, `refactor/`, `chore/`, `docs/`). +- Branch adı, yapılan işin içeriğini kısa ve anlaşılır şekilde yansıtmalıdır. + +--- + +## 7. Commit ve Dil Kuralları + +- Her zaman local commit uygulanmalı; görev tam anlamıyla bitmeden ve kullanıcı `Kontroller tamamlandı, push edilebilir.` demeden kesinlikle push edilmemelidir. +- Commit başlığı ve açıklaması her zaman İngilizce ve Conventional Commits formatına uygun olmalıdır. +- Kod, değişken adları ve commitler %100 İngilizce; PR açıklaması ve raporlar %100 Türkçe ve edilgen dille yazılır. +- Push etmeden önce `git fetch origin main` ile conflict riski kontrol edilmelidir. + +--- + +## 8. Görev Sonrası Raporlama Kuralları + +Görev tamamlandığında kullanıcıya Türkçe ve edilgen bir dille, aşağıdaki sırayla ve başlıklarla rapor verilmelidir + +### Yapılan Değişiklikler +- Yapılan her değişiklik madde halinde listelenmelidir. +- Değişiklik yapılan sayfa veya bileşenler, mutlaka ilgili URL bağlantısı ile belirtilerek raporlanmalıdır. + +### PR Bilgisi +- Kullanıcıdan `Kontroller tamamlandı, push edilebilir.` komutu geldikten sonra PR Bilgisi verilmeli, her konuşma sonunda tekrar tekrar verilmemeli. +- Başlık İngilizce olmalıdır ve `fix: align songs page cards and admin song modal #906` formatındadır. +- Açıklama Türkçe, 🎯 Amaç - 🔨 Yapılanlar - 📝 Notlar - 🧪 Kontroller başlıkları ile maddeler halinde ve edilgen bir dille yazılmalıdır. +- Açıklamanın ilk satırı her zaman şu formatta olmalıdır: `İlgili issue #...` +- Sadece göreve uygun etiket(ler), sabit liste içinden seçilerek belirtilmelidir: `UI/UX`, `Feature`, `Frontend`, `Backend`, `Refactor`, `API`, `Bug`, `Await`, `Documentation`. + +--- + +## 9. Günlük Rapor Kuralı (Google Form) + +- Kullanıcı `Günlük rapor hazırla.` komutunu ilettiğinde, o an tamamlanmış olan görev için Google Form'a doğrudan yapıştırılabilecek bir özet hazırlanmalıdır. +- Format: `# Başlık (PR #...)` ile başlayan başlık ve altında Türkçe, edilgen, birkaç cümlelik net özet. + +--- + +## 10. Genel Çalışma Akışı Özeti + +1. Görev analiz edilir, düşünülür. +2. `origin/main` fetch edilir ve hassas veri kontrolü yapılır. +3. Göreve uygun, İngilizce ve Conventional Commits standardına uygun yeni bir branch `origin/main` üzerinden açılır. +4. Görev, Clean Code, DRY, KISS ve mobile-first prensiplerine uyularak tek seferde tamamlanır. +5. Yasaklı dizinler taranmaz, uygun sayıda İngilizce local commit atılır. +6. Lint ve build komutları çalıştırılmaz. +7. Görev bitiminde kullanıcıya Yapılan Değişiklikler raporu sunulur, onay sonrası PR Bilgisi verilir. + +Bu kurallar her görevde otomatik olarak geçerlidir. \ No newline at end of file diff --git a/architecture.md b/architecture.md new file mode 100644 index 0000000..57b0844 --- /dev/null +++ b/architecture.md @@ -0,0 +1,438 @@ +# RageTV — architect.md + +> **Bu belge, RageTV için ürün ve mimari kararların tek doğruluk kaynağıdır.** +> `agent-rules.md` ile çelişirse `agent-rules.md` önceliklidir. +> **DOĞRULA** etiketi: resmi kaynaktan Faz 0'da doğrulanmadan koda geçirilmeyecek bilgi. +> Belirsiz bir noktada varsayım yapma; "Açık Sorular" bölümüne ekle ve Sercan'a sor. +> İlk iş: Bölüm 17 (Faz Planı) M0 ve M1. Kodlamaya başlamadan önce plan çıkar, onay iste. + +--- + +## 1. PROJE TANIMI VE HEDEFİ + +**RageTV**, minimalist ve hızlı bir espor canlı yayın **aracı sitesidir** (aggregator). Kullanıcı siteye girer, solda sidebar'dan oyunu seçer (CS2, Valorant, PUBG, LoL, Dota 2), sağdaki alanda o oyunun şu an canlı olan resmi espor yayını **kaynak platformun resmi gömülü oynatıcısıyla** açılır. Canlı yayın yoksa ekran boş kalmaz: o oyunun yakın zamanda yayınlanmış son maçı/turnuva maçı duraklatılmış (poster) halde durur, tıklanınca oynar. + +**Ürün ilkeleri (pazarlık konusu değil):** +1. **Aracı site, yalnızca embed.** Video barındırmaz, kaydetmez, proxy'lemez, restream etmez. Oynatıcı kaynak platformunun resmi, değiştirilmemiş oynatıcısıdır; platformun logosu/kontrolleri ona aittir. +2. **Minimalist ve hızlı.** Sidebar → tek tık → izle. Gereksiz ekran, modal, banner yok. +3. **Kayıt/giriş yok.** Tercihler (tema, dil, son oyun) yalnızca cihazda (localStorage) tutulur. +4. **Ticari değil.** Reklam, ödeme, bahis/oran (odds) içeriği yok. (Barındırma planı koşullarıyla da uyumlu, bkz. Bölüm 9.) +5. **Dürüst veri.** Gösterilen her durum/sayı gerçek kaynaktan gelir; bilinmeyen sıfır değil `N/A`'dır (Bölüm 15). +6. **Geleceğe hazır, aşırı mühendislik değil.** Modül sınırları ve adapter'lar şimdi kurulur; özellikler fazlara bölünür. + +**Kapsam dışı (non-goals):** sohbet (chat) embed'i, restream, herhangi bir siteyi scrape etmek, kullanıcı hesapları, reklam, bahis/oran verisi, DVR/kayıt. + +--- + +## 2. KRİTİK TASARIM KARARLARI VE GEREKÇELERİ + +| # | Karar | Gerekçe | +|---|---|---| +| D1 | **Yalnızca resmi embed oynatıcıları** (Twitch, YouTube; Kick Faz 2) | Video akışı kaynak platformdan gelir. Platform şartlarına uymak zorunludur (Bölüm 8). Bu, hukuki koruma garantisi değildir; disclaimer da koruma sağlamaz, yalnızca şeffaflıktır. | +| D2 | **HLTV ve benzeri sitelerden scraping YOK** | Resmi API yok, şartlar ihlal edilir, Cloudflare engeli ve kırılganlık. HLTV'ye yalnızca dışa bağlantı verilebilir. İzinsiz/gayriresmî endpoint (örn. Kick `kick.com/api/v2`) kullanılmaz. | +| D3 | **Canlı durum için ince bir sunucu katmanı (BFF) şarttır** | Twitch/YouTube/PandaScore anahtarları tarayıcıya konamaz. Ayrıca "canlı mı?" bilgisi dakikalar içinde değişir; DevSec Status'taki "cron + statik JSON" modeli burada **uygun değildir**. | +| D4 | **Cron ile polling YOK; CDN önbellekli, isteğe bağlı (on-demand) endpoint'ler** | Vercel Hobby'de cron günde en fazla bir kez çalışabilir ve zamanlaması saat içinde kayabilir (DOĞRULA: güncel limitler). Çözüm: `/api/*` yanıtları `s-maxage` + `stale-while-revalidate` ile önbelleğe alınır. | +| D5 | **Tek paket, katı katman sınırları** (`shared/`, `server/`, `src/`) | Şimdilik monorepo ceremony'si gereksiz. Sınırlar lint ile zorlanır; ileride `apps/web` + `apps/api`'ye bölmek mekanik olur. Bölme tetikleyicileri: kalıcı veritabanı, hesaplar, WebSocket, ikiden fazla periyodik iş. | +| D6 | **Atomic Design yerine feature-based modüler yapı** | Atomic Design görsel granülerliğe göre dizer; değişimin ekseni ise özelliklerdir (player, live, fixtures). UI primitifleri `components/ui` altında kalır. | +| D7 | **Vite SPA (Next.js değil)** | Uygulama büyük ölçüde istemci tarafı (iframe, sidebar). Sunucu ihtiyacı BFF ile karşılanıyor. SEO/OG kartları öncelik olursa (maç bazlı paylaşım) Next.js yeniden değerlendirilir (ADR ile). | +| D8 | **Sağlayıcı (provider) adapter deseni** | Twitch, YouTube, PandaScore, Kick değişebilir/kapanabilir. Domain tipleri sağlayıcıdan bağımsızdır; sağlayıcı değişimi tek klasörü etkiler. | +| D9 | **Kill switch**: kanal/platform tek commit ile kapatılabilir | İçerik sahibi talebi veya platform politika değişikliğinde hızlı kaldırma. | + +--- + +## 3. KAPSAM VE FAZLAR (ÖZET) + +- **v1 (M1–M3, M6):** 5 oyunluk sidebar, canlı durum göstergesi, resmi embed oynatıcı, VOD/poster yedeği, tema, i18n (EN/TR/DE), responsive, footer disclaimer, güvenlik başlıkları, CI. +- **v1.1 (M4):** Fikstür: yaklaşan maç listesi, geri sayım, sonuçlar. +- **v1.2 (M5):** Puan durumu/standings (veri kaynağı doğrulanırsa). +- **Faz 2 (ayrı karar):** Kick adaptörü, aynı oyunda birden çok yayın seçici (dil/co-stream), dil tercihine göre yayın, bildirimler. (Hesaplar ve kalıcı backend için ayrı ADR gerekir.) + +--- + +## 4. EKRAN VE UX SPESİFİKASYONU + +**Genel yerleşim (laptop ve üzeri, ≥1024px; ayrıntı 4.1)** +- Sol sidebar (dar, daraltılabilir): en üstte logo, sonra oyunlar sırasıyla **CS2 → Valorant → PUBG → LoL → Dota 2**, altta dil ve tema kontrolleri. +- Sağ sahne (`PlayerStage`): 16:9 oynatıcı. Altında ince bilgi satırı: yayın başlığı, platform rozeti, izleyici sayısı (varsa), "Open on Twitch ↗" bağlantısı. +- Footer: sayfa altında **ortalanmış** disclaimer (Bölüm 13). + +**Mobil ve tablet (<1024px; ayrıntı 4.1)** +- Sidebar yerine üstte yatay kaydırılan oyun sekmeleri; oynatıcı tam genişlikte. (Alt gezinme çubuğu alternatifi Açık Sorular'da.) +- Mobilde autoplay güvenilir değildir; kullanıcı Play'e basar. Dar ekranlarda (<400px) Twitch minimum boyut koşulu sorun olabilir (Bölüm 8). + +**Durumlar (her oyun için)** +| Durum | Gösterim | +|---|---| +| `loading` | Skeleton (LCP elemanı oynatıcı değil, skeleton/poster olmalı) | +| `live` | Resmi embed oynatıcı, "LIVE" rozeti (renk tek başına taşıyıcı değil: metin + nokta) | +| `vod` | Son yayın posteri (facade): "Son yayın · tarih" etiketi, Play ile iframe yüklenir | +| `blocked` | Embed yüklenemedi/engellendi: kart + "Watch on Twitch/YouTube" dış bağlantısı | +| `empty` | Ne canlı ne VOD: sakin mesaj + resmi kanala bağlantı | +| `stale` | API ulaşılamıyor: son bilinen veri + "veri eski olabilir" notu ve `fetchedAt` | + +**Yönlendirme:** `/:gameSlug` (`cs2`, `valorant`, `pubg`, `lol`, `dota2`). `/` → son seçilen oyun (localStorage) → yoksa ilk canlı oyun (sidebar sırasına göre) → yoksa `cs2`. Bilinmeyen slug → 404 sayfası. Opsiyonel `?stream=`. + +**Sidebar canlı göstergesi:** `/api/live` tek istekle tüm oyunların durumunu döner; her oyunun yanında LIVE göstergesi. + +**Erişilebilirlik:** `nav` landmark, `aria-current="page"`, görünür odak halkası, `prefers-reduced-motion`, iframe `title`, WCAG AA kontrast, klavye ile tam gezinme. + +**Görsel tasarım:** Referanslar Sercan tarafından eklenecek. Agent, `src/styles/tokens.css` içinde renk/yarıçap/boşluk/tipografi token'larını tanımlar; bileşenler token kullanır. Varsayılan **dark**. Vurgu rengi Açık Sorular'da. Fontlar self-host (WOFF2, subset, `font-display: swap`); Google Fonts kullanılmaz. + +**Başlık (``) ve açıklama (öneri, onay bekler):** +- EN: `RageTV – Pick a game. Watch live esports.` +- TR: `RageTV – Bir oyun seç, canlı espor izle.` +- DE: `RageTV – Spiel wählen. Live-Esports schauen.` *(AI destekli çeviri, gözden geçirilecek)* +- Footer kısa açıklama (EN): `A minimal hub for live esports: CS2, Valorant, PUBG, LoL and Dota 2.` + +--- + +## 5. ÖZELLİK HAVUZU (ŞABLONDAN SEÇİM) + +- [x] Özel isim (RageTV) ve logo +- [x] Özel tema vurgu rengi (Açık Sorular) +- [x] Modern UX/UI (referanslar eklenecek) +- [x] Web & mobil uyumluluk (full-responsive) +- [x] Light / Dark mode (varsayılan: dark) +- [x] Dil seçeneği: EN / TR / DE (tarayıcı dili algılama; yedek dil: EN, onay bekler) +- [ ] ~~Scroll-Up butonu~~ → v1'de gerekmez (tek ekranlık yerleşim). Fikstür listesi uzarsa M4'te yeniden değerlendirilir. +- [ ] ~~Login & Sign Up~~ → bilinçli olarak yok. Gelecek için yalnızca `preferences` modülünde depolama adapter'ı bulunur (ADR-003). +- [ ] ~~Sirv/Image CDN, tüm görseller .webp~~ → Statik varlık çok az: SVG/WebP `public/` altında self-host. Oynatıcı posterleri sağlayıcı CDN'inden gelir. +- [x] Tech: PNPM, Vite, HTML, CSS, JavaScript, TypeScript, React.js, Node.js (sunucusuz fonksiyon çalışma ortamı) +- [ ] ~~Docker, Vue.js, Angular, Next.js, Python, PHP, Java, Go, SQL, AI~~ → gerekmiyor +- [x] Unit / Integration testleri + +## 6. TEKNİK STANDARTLAR (ŞABLONDAN SEÇİM) + +- [ ] ~~Atomic Design~~ → yerine **feature-based modüler yapı** (D6) +- [x] **Component-Based Architecture** +- [x] **Conventional Commits** (`feat(player): ...`) +- [x] **Frontend XSS koruması**: dış kaynaktan gelen her metin düz metin olarak render edilir; `dangerouslySetInnerHTML` yasak (lint kuralı); URL'ler allowlist ile doğrulanır +- [x] **Backend koruması (Helmet yerine eşdeğeri)**: Helmet bir Express ara katmanıdır; burada güvenlik başlıkları `vercel.json` ve Hono middleware ile verilir. Rate limiting: CDN önbelleği + sağlayıcı çağrı bütçesi + devre kesici (Bölüm 7.5) +- [x] **Unit test** (Vitest) +- [x] **Caching** (çekirdek tasarım parçası, Bölüm 7.4) +- [x] **Custom CSP, Referrer Policy, Permissions-Policy** (Bölüm 10) + +**Seçilen yığın (sürümleri agent güncel kararlı sürüme sabitler ve ADR'ye yazar):** +- Node.js LTS, **PNPM**, **Vite**, **TypeScript (strict, `noUncheckedIndexedAccess`)** +- **React**, **React Router**, **Tailwind CSS**, Radix UI primitifleri (gerektiğinde, shadcn tarzı kopyala-kullan), `lucide-react` +- **TanStack Query** (polling, dedupe, stale-while-revalidate), **zod** (sınır doğrulaması, istemci ve sunucu paylaşımlı) +- **i18next + react-i18next** (dil dosyaları lazy-load) +- BFF: **Hono** + `hono/vercel` adaptörü, Vercel Functions üzerinde. Hono taşınabilirlik içindir (Cloudflare/Node'a geçiş kolay). M1'de çalıştığı doğrulanır; çalışmazsa dosya-başı fonksiyonlara düşülür, katman sınırları değişmez. +- Test: **Vitest**, Testing Library, **MSW**, vitest-axe. E2E (Playwright) M6'da, yalnızca smoke. +- Kalite: **ESLint** (typescript-eslint, react-hooks, jsx-a11y, import sınırları), Prettier, commitlint. *(DevSec Status'ta `lint` yalnızca tsc idi; burada gerçek ESLint var.)* + +--- + +## 7. MİMARİ + +### 7.1 Katmanlar ve bağımlılık kuralları + +``` +shared/ → hiçbir katmana bağımlı değil (tipler, zod şemaları, registry, URL builder'lar) +server/ → shared/ +src/ → shared/ (yalnızca tip/şema/registry); server/ import edilemez +``` + +Kural lint ile zorlanır (`import/no-restricted-paths` veya `eslint-plugin-boundaries`). İhlal = CI hatası. + +### 7.2 Akış + +```mermaid +flowchart LR + U[Tarayıcı<br/>React SPA] -->|GET /api/live| E[Vercel Edge Cache<br/>s-maxage 30s] + E -->|MISS/STALE| B[BFF: Hono] + B --> R[(Registry<br/>kanallar, oyunlar)] + B --> T[Twitch Helix] + B --> Y[YouTube Data API] + B --> P[PandaScore fixtures] + U -->|iframe, kullanıcı etkileşimiyle| TP[Twitch / YouTube<br/>resmi oynatıcı] +``` + +### 7.3 Provider adapter deseni + +```ts +// shared/schemas: Stream, VodItem, Match (zod). Sağlayıcıdan bağımsız domain tipleri. +type Result<T> = + | { ok: true; data: T; fetchedAt: string } + | { ok: false; error: ProviderError; stale?: T }; + +interface LiveProvider { id: ProviderId; getLive(channels: ChannelRef[]): Promise<Result<Stream[]>> } +interface VodProvider { id: ProviderId; getLatest(source: VodSourceRef): Promise<Result<VodItem | null>> } +interface FixturesProvider { id: ProviderId; getUpcoming(g: GameId): Promise<Result<Match[]>>; getResults(g: GameId): Promise<Result<Match[]>> } +``` + +- Sağlayıcı yanıtları **zod ile doğrulanır**, domain tipine **mapper** ile çevrilir. Doğrulanamayan yanıt = `ok:false`. +- Hata sınırı boyunca exception fırlatılmaz; `Result` döner. +- **Registry (kod değil veri):** `shared/registry/games.ts` — her oyun için `id`, `slug`, `i18nKey`, `order`, `providerMappings` (örn. PandaScore slug'ı; **CS2 için PandaScore yolu `/csgo/` önekidir**, DOĞRULA), `channels: ChannelRef[]` (`platform`, `login/id`, `label`, `language`, `official`, `priority`), `vodSources`. Kanal adlarını agent **uydurmaz**; M0'da resmi kaynaklardan doğrular. +- **Yayın seçimi** saf fonksiyondur: `selectPrimaryStream(streams, prefs)` — öncelik (`priority`), resmî olma, dil tercihi, izleyici sayısı sırasıyla. Birim testlidir. +- **İki canlı-tespit stratejisi, adapter arkasında:** + - **A (taban):** Registry'deki resmi kanallar → Twitch Helix `streams` (tek istekte çoklu `user_login`) → hangileri canlı. + - **B (doğrulanırsa):** PandaScore `running` maçları → maç yayın bilgileri (`streams_list`, DOĞRULA: ücretsiz planda geliyor mu, kullanım şartları). + A zorunlu, B opsiyoneldir. +- **VOD:** Öncelik, kanal "uploads" akışı değil, **belirli playlist** (tam maç/ana yayın playlist'i); aksi halde son yüklenen video fragman/özet olabilir. Twitch arşivleri (VOD) saklama süresi sınırlıdır; yalnızca yedek. + +### 7.4 Önbellek stratejisi + +| Endpoint | `s-maxage` | `stale-while-revalidate` | Not | +|---|---|---|---| +| `GET /api/live` | 30 sn | 60 sn | Tüm oyunlar tek yanıt | +| `GET /api/vod?game=` | 10 dk | 1 saat | | +| `GET /api/fixtures?game=` | 2 dk | 10 dk | M4 | +| `GET /api/health` | 0 | — | Sürüm, sağlayıcı durumu (gizli bilgi yok) | + +- Vercel CDN önbelleği bölgeseldir; sağlayıcıya giden istek ≈ bölge sayısı × (60 / TTL). Sağlayıcı bütçeleri buna göre doğrulanır. +- Twitch app token'ı bellekte `expires_in`'e göre önbelleğe alınır; her istekte yenilenmez. +- İstemci: TanStack Query `refetchInterval: 30_000`, **sekme görünür değilken durur**, `retry` üstel geri çekilmeli. +- Sağlayıcı verisinin saklanma/önbellek koşulları sağlayıcı şartlarına tabidir (Faz 0'da DOĞRULA). + +### 7.5 Dayanıklılık + +- **Devre kesici:** N ardışık hatada T saniye sağlayıcıyı çağırma; önbellekteki son iyi veriyi `stale: true` ile dön. +- **Zaman aşımı:** her sağlayıcı çağrısı için kısa timeout (varsayılan 5 sn; Hobby fonksiyon süresi sınırı göz önünde). +- **Hata asla sıfıra çevrilmez.** Bilinmiyorsa `null`/`N/A`, `empty` veya `stale` durumu. +- **Kill switch:** `shared/registry/killswitch.ts` (kanal/platform kapatma). Değişiklik tek commit + deploy. (İleride Edge Config değerlendirilebilir.) + +### 7.6 API kontratı (özet) + +```jsonc +// GET /api/live +{ + "fetchedAt": "2026-10-20T12:00:00.000Z", + "stale": false, + "games": { + "cs2": { "live": [ { "id": "twitch:esl_csgo", "platform": "twitch", "channel": "esl_csgo", + "title": "…", "language": "en", "official": true, "viewers": 12345 } ] }, + "valorant": { "live": [] } + } +} +``` +Tüm yanıtlar zod şemasıyla doğrulanır; şemalar `shared/schemas`'tadır; istemci aynı şemayı kullanır. + +--- + +## 8. EMBED UYUM KURALLARI (DOĞRULANMIŞ + DOĞRULA) + +**Twitch (resmi dokümandan doğrulandı):** +- Embed kullanan alan adları **SSL** kullanmalı. +- `parent` parametresi **zorunludur** (iframe `src` içinde); eksikse oynatma hatası görünür. Değer çalışma anında `window.location.hostname`'den alınır (yerel, preview ve prod hepsi için). +- Yalnızca **Twitch onaylı oynatıcı öğeleri** kullanılır; oynatıcı **başka sayfa öğeleriyle örtülmez**. Uyumsuz alan adlarının embed hakkı iptal edilebilir. +- Önerilen/gerekli minimum boyut **400×300**; **autoplay** için boyut ve görünürlük koşulları gerekir (aksi halde autoplay engellenir). +- URL kalıbı: `https://player.twitch.tv/?channel=<login>&parent=<host>&muted=true` · VOD: `?video=v<id>&parent=<host>&autoplay=false`. +- Dar telefonlarda (<400px) boyut koşulu gerilimi: Faz 0'da gerçek cihazda test edilir; çözüm yoksa dar görünümde "Watch on Twitch" dış bağlantısına düşülür. + +**YouTube:** `https://www.youtube-nocookie.com/embed/<id>`. Yükleyici embed'i kapatmış olabilir → `blocked` durumu. YouTube API Services şartlarının zorunlu gösterim/bağlantı ve veri saklama kuralları DOĞRULA. + +**Kick (Faz 2):** Resmî genel API OAuth ile çalışır (uygulama erişim token'ı ile herkese açık veri); embed oynatıcı kullanım şartları DOĞRULA. Kick'in gayriresmî `api/v2` uçları **kullanılmaz**. + +**Genel iframe kuralları:** `allow="autoplay; fullscreen; picture-in-picture"`, `title`, `referrerpolicy="strict-origin-when-cross-origin"`, `allowfullscreen`. `sandbox` yalnızca her sağlayıcıyla test edilirse eklenir. İframe **yalnızca kullanıcı etkileşimiyle** (oyun seçimi/Play) yüklenir (facade). Oynatıcı üzerine hiçbir şey bindirilmez, kırpılmaz, yeniden boyanmaz. + +**Her embed için `blocked` yedeği** zorunludur ve "kaynakta izle" dış bağlantısı her zaman görünür. + +--- + +## 9. SAĞLAYICILAR (FAZ 0 ÇIKTISI: `docs/providers.md`) + +| Sağlayıcı | Amaç | Kimlik | Not (DOĞRULA) | +|---|---|---|---| +| Twitch Helix + Embed | Canlı durum, VOD, oynatıcı | App access token (client credentials, sunucuda) | Geliştirici sözleşmesi, veri önbellek/saklama, istek limitleri | +| YouTube Data API + Embed | VOD/playlist, oynatıcı | API anahtarı (sunucuda) | Günlük kota; `playlistItems` ucuz, `search` pahalı → arama kullanma | +| PandaScore (REST) | Fikstür, sonuç, çalışan maçlar | Token (sunucuda) | **Ücretsiz "Fixtures" planı mevcut**; CS2 uçları `/csgo/` öneklidir; Valorant için canlı veri planı yoktur; ücretsiz limit ~1000 istek/saat (üçüncü taraf kaynak, DOĞRULA); PUBG kapsamı DOĞRULA; ticari olmayan kullanım/atıf şartları DOĞRULA | +| Kick (Faz 2) | Canlı durum, oynatıcı | OAuth 2.1 (app token) | Embed şartları DOĞRULA | +| HLTV | — | — | **Kullanılmaz** (scraping yok). | +| Liquipedia | (opsiyonel) | — | Şartlar/atıf/hız sınırları sıkı; Faz 0'da değerlendirilir, varsayılan olarak kapalı | + +**Barındırma (Vercel):** Hobby plan **ticari olmayan kişisel kullanımla** sınırlıdır; cron günde bir; fonksiyon süresi varsayılan 10 sn (DOĞRULA: güncel değerler). Reklam/ödeme/gelir eklenirse Pro'ya geçiş gerekir (ADR). + +--- + +## 10. GÜVENLİK + +**CSP taslağı** (M1'de gerçek host'larla netleştirilir, her genişletme gerekçelendirilir): + +``` +default-src 'self'; +script-src 'self'; +style-src 'self'; +font-src 'self'; +img-src 'self' https://static-cdn.jtvnw.net https://i.ytimg.com https://cdn.pandascore.co; # DOĞRULA: gerçek host'lar +connect-src 'self'; +frame-src https://player.twitch.tv https://www.youtube-nocookie.com; # Kick: Faz 2 +object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests +``` + +- `unsafe-inline` ve `data:` **varsayılan olarak yok**; yalnızca kanıtlanmış ihtiyaçta, ADR ile. +- Diğer başlıklar: `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` (kamera/mikrofon/konum kapalı; **autoplay ve fullscreen'i engelleme**), `Cross-Origin-Opener-Policy: same-origin`. COEP **ayarlanmaz** (embed'leri bozar). +- **Gizli bilgiler:** yalnızca sunucu ortam değişkenleri (`TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`, `YOUTUBE_API_KEY`, `PANDASCORE_TOKEN`). `VITE_` önekiyle **asla**. `.env.example` bulunur. CI'da `dist/` içinde secret kalıpları taranır. +- **URL allowlist testi:** registry'deki tüm URL/host'lar, embed host'ları ve görsel host'ları test ile doğrulanır (`//evil.com`, `/\evil.com`, `javascript:` negatif testleri dahil; kural tek modülden: `shared/urls.ts`). +- Dış kaynaklı metinler (yayın başlığı vb.) düz metin render edilir. +- **Tedarik zinciri:** sabit sürümler, lockfile, `--frozen-lockfile`, GitHub Actions SHA ile sabitlenir, Dependabot, pnpm `onlyBuiltDependencies` allowlist'i, minimal bağımlılık politikası. +- `SECURITY.md` + Private Vulnerability Reporting; kişisel e-posta **yayınlanmaz**. +- Güvenlik başlıkları Mozilla Observatory ve CSP Evaluator ile doğrulanır (hedef: A+, CSP'de `script-src` temiz). + +--- + +## 11. PERFORMANS ve ERİŞİLEBİLİRLİK BÜTÇESİ + +- Mobil Lighthouse: **LCP ≤ 2.0 sn, TBT ≤ 200 ms, CLS ≤ 0.05**, Performans ≥ 90 (3 çalışmanın medyanı). Başlangıç JS ≤ ~150 KB gzip (iframe'ler hariç). +- Route bazlı code splitting; dil dosyaları lazy; oynatıcı/üçüncü taraf kod yalnızca etkileşimle. +- Self-host fontlar; görsellerde boyut öznitelikleri; sağlayıcı poster görselleri `loading="lazy"`. +- Erişilebilirlik: vitest-axe testleri, klavye gezinmesi, odak yönetimi (oyun değişince oynatıcı alanına anons). + +--- + +## 12. i18n + +- TR/EN/DE; tarayıcı dili algılama; **yedek dil EN** (onay bekler). Dil dosyaları lazy. +- Sayı/tarih/saat/göreli zaman yalnızca `Intl.*` ile (DevSec'teki "251.689" hatasının tekrarı yok). Fikstür saatleri UTC'den kullanıcının saat dilimine çevrilir. +- Birleştirilmiş çeviri cümleleri yok; anahtarlar özellik bazlı; testte tüm dillerde anahtar eşitliği. +- Çeviriler AI destekli olarak işaretlenir; ana dil konuşanlardan katkı davet edilir. + +## 13. DISCLAIMER (footer, altta ortalanmış) + +**EN:** RageTV is an independent, non-commercial esports discovery site. It does not host, record or rebroadcast any video. Live streams and videos are shown through the official embedded players of their original platforms (such as Twitch and YouTube) and remain the property of their respective owners. All trademarks, logos and game titles belong to their respective owners; RageTV is not affiliated with, endorsed by or sponsored by them. Content owners can request removal via `{{CONTACT_URL}}`. + +**TR:** RageTV, bağımsız ve ticari olmayan bir espor keşif sitesidir. Hiçbir video içeriğini barındırmaz, kaydetmez veya yeniden yayınlamaz. Canlı yayınlar ve videolar, özgün platformların (Twitch, YouTube vb.) resmi gömülü oynatıcıları aracılığıyla gösterilir ve ilgili sahiplerine aittir. Tüm ticari markalar, logolar ve oyun adları sahiplerine aittir; RageTV bunlarla bağlantılı değildir, onlar tarafından onaylanmamış veya desteklenmemiştir. İçerik sahipleri kaldırma taleplerini `{{CONTACT_URL}}` üzerinden iletebilir. + +**DE:** AI destekli çeviri olarak üretilir, gözden geçirme notuyla işaretlenir. + +- `{{CONTACT_URL}}`: varsayılan olarak repo Issues sayfası (kişisel e-posta kullanılmaz). +- Oyun/marka logoları v1'de kullanılmaz: metin etiketi + nötr ikon. Resmî marka kılavuzları incelenmeden logo eklenmez. +- Bu metin hukuki danışmanlık değildir ve telif sorumluluğunu ortadan kaldırmaz; amaç şeffaflıktır. + +--- + +## 14. KLASÖR YAPISI + +``` +ragetv/ +├── agent-rules.md +├── architect.md +├── .github/ +│ ├── workflows/ci.yml # pull_request; permissions: contents: read +│ ├── ISSUE_TEMPLATE/ +│ ├── PULL_REQUEST_TEMPLATE.md +│ └── dependabot.yml +├── api/ +│ └── [[...route]].ts # Vercel giriş noktası → server/app +├── server/ # BFF (React import edemez) +│ ├── app.ts # Hono app, middleware +│ ├── routes/ # live.ts, vod.ts, fixtures.ts, health.ts +│ ├── providers/ +│ │ ├── twitch/ # client, live, vod, mapper (+ testler) +│ │ ├── youtube/ +│ │ ├── pandascore/ +│ │ └── kick/ # Faz 2 +│ ├── domain/ # selectPrimaryStream.ts vb. saf mantık +│ └── lib/ # cache, http, circuitBreaker, env +├── shared/ # izomorfik +│ ├── schemas/ # stream, vod, match, game (zod) +│ ├── registry/ # games.ts, channels, killswitch.ts +│ └── urls.ts # embed URL builder + allowlist doğrulama +├── src/ +│ ├── app/ # App, router, providers +│ ├── features/ +│ │ ├── games/ # GameSidebar, GameNavItem +│ │ ├── player/ # EmbedPlayer, PlayerStage, VodFacade, state bileşenleri +│ │ ├── live/ # useLiveStatus, api client +│ │ ├── fixtures/ # M4 +│ │ ├── standings/ # M5 +│ │ └── preferences/ # storage adapter: tema, dil, son oyun +│ ├── components/ui/ # Button, Badge, Skeleton, Tooltip … +│ ├── layouts/ # AppShell, Sidebar, MobileNav, Footer +│ ├── lib/ # formatters (Intl), cn +│ ├── i18n/locales/{en,tr,de}.json +│ ├── styles/ # tokens.css, index.css +│ └── main.tsx +├── public/ # favicon, og görseli, robots.txt, fontlar +├── docs/ +│ ├── providers.md # M0 çıktısı +│ └── adr/ # 001-vite-spa, 002-bff-hono, 003-preferences-adapter … +├── tests/ # e2e (M6) +├── vercel.json vite.config.ts tsconfig*.json eslint.config.js package.json +└── LICENSE README.md CONTRIBUTING.md SECURITY.md .env.example +``` + +**Hazır gelen dosyalar:** `vite.config.ts` ve `vercel.json` DevSec Status'tan kopyalandı. **Agent bunları baştan gözden geçirir:** DevSec'e özgü CSP/başlıkları kaldırır, Bölüm 10 CSP'sini uygular, SPA rewrite'ında `/api/*`'ı hariç tutar, çıktı klasörünü ve sunucusuz fonksiyon ayarlarını doğrular. + +--- + +## 15. VERİ DÜRÜSTLÜĞÜ KURALLARI (DevSec Status'tan çıkan dersler — bağlayıcı) + +1. **Hiçbir UI değeri/durumu sabit kodlanmaz.** Mock veri yalnızca testlerde ve fixture dosyalarındadır; üretim yolunda yer tutucu/uydurma veri yok. +2. **Bilinmeyen ≠ 0.** Eksik veri `null`/`N/A` gösterilir. +3. **Her veri bloğu kaynak ve `fetchedAt` taşır.** Kullanıcı arayüzünde gerektiğinde görünür. +4. **"Canlı/real-time" ifadesi** yalnızca gerçekten ≤60 sn tazelenen veride kullanılır. README/UI iddiaları test veya komutla doğrulanabilir olmalı. +5. **Agent raporları kanıt içerir** (komut çıktısı). Token/secret değeri yazdıran komut **çalıştırılmaz** (`gh auth token`, `printenv`, `echo $SECRET` vb.). +6. **Locale-aware biçimlendirme** baştan (`Intl`). Sabit string birleştirme yok. +7. **Üçüncü taraf host'lar** allowlist + test ile sınırlıdır; yeni host = gerekçe + test. +8. **Küratörlü veri** (registry) için doğrulama: kanal gerçekten resmi mi, kaynak linki `docs/providers.md`'de. +9. `main`'e doğrudan push yok; `agent-rules.md` akışı geçerlidir. + +--- + +## 16. TEST STRATEJİSİ + +- **Birim:** `selectPrimaryStream`, URL builder'lar/allowlist, mapper'lar (kayıtlı ve temizlenmiş fixture yanıtlarıyla), formatter'lar. +- **Sözleşme (contract):** sağlayıcı yanıt şemaları fixture ile; şema değişirse test kırılır. +- **Entegrasyon:** MSW ile istemci durumları (`loading/live/vod/blocked/empty/stale`). +- **Veri:** registry bütünlüğü (benzersiz id, zorunlu alanlar, host allowlist, kill switch). +- **Erişilebilirlik:** vitest-axe, klavye gezinmesi. +- **i18n:** tüm dillerde anahtar eşitliği. +- **CI (`pull_request`, `contents: read`):** `pnpm install --frozen-lockfile` → typecheck → lint → test → build → `dist` secret taraması. `pull_request_target` **kullanılmaz**. + +--- + +## 17. FAZ PLANI (HER FAZIN KABUL KRİTERİ) + +| Faz | İçerik | Kabul kriteri | +|---|---|---| +| **M0** | Sağlayıcı/ŞART araştırması (kod yok): `docs/providers.md`, ADR taslakları, Twitch/YouTube/PandaScore/Kick şartları, 5 oyun için resmi kanal ve playlist listesi, Vercel limitlerinin güncel doğrulaması | Her DOĞRULA maddesi kaynak bağlantısıyla kapatıldı; açık risk listesi çıkarıldı | +| **M1** | Temel: iskelet, araçlar, CI, tokenlar, AppShell + sidebar (statik), i18n iskeleti, rotalar, güvenlik başlıkları, `/api/health`, preview deploy | CI yeşil; Observatory A+; boş uygulama Lighthouse mobil ≥90; katman sınırı lint'i çalışıyor | +| **M2** | Canlı: Twitch adaptörü + registry, `/api/live`, `useLiveStatus`, EmbedPlayer + durumlar, sidebar LIVE göstergesi | Gerçek canlı bir yayın uçtan uca açılıyor (kanıt: ekran görüntüsü + log); devre kesici/stale testleri geçiyor | +| **M3** | VOD/poster yedeği (YouTube/Twitch), facade, `blocked/empty` durumları | Hiçbir oyunda "boş ekran" yok; embed kapalı video için yedek çalışıyor | +| **M4** | Fikstür (PandaScore): yaklaşan, geri sayım, sonuçlar | Veri kaynağı ve zaman dilimi doğru; `N/A` kuralları uygulanıyor | +| **M5** | Standings (kaynak doğrulanırsa) | Kaynak yoksa faz kapatılır/ertelenir (ADR) | +| **M6** | Sertleştirme: performans/a11y denetimi, e2e smoke, README, SECURITY/CONTRIBUTING, yayın kontrol listesi | Bölüm 11 bütçesi sağlandı; tüm README iddiaları doğrulandı | + +**Global Definition of Done:** typecheck + lint + test + build yeşil; yer tutucu veri yok; dokümantasyon güncel; yeni üçüncü taraf host'u/bağımlılığı gerekçeli. + +--- + +## 18. BAŞLANGIÇ GÖREVİ (AGENT İÇİN İLK PROMPT) + +``` +Bu belgeyi (architect.md) ve agent-rules.md'yi oku. Kod yazmadan önce: +1. M0'ı yap: docs/providers.md ve gerekli ADR taslaklarını üret. Her DOĞRULA + maddesi için resmi kaynak bağlantısı ver; doğrulayamadığını açıkça belirt. +2. vite.config.ts ve vercel.json'ı gözden geçir (Bölüm 14 notu) ve gerekli + değişiklikleri listele (uygulamadan önce onay iste). +3. Bölüm 19'daki Açık Sorular için, varsayımlarını belirterek net sorular sor. +4. M1 için adım adım bir plan çıkar ve onayımı bekle. +Kurallar: scraping yok; gayriresmî endpoint yok; secret yazdıran komut yok; +uydurma kanal/URL yok; kanıt (komut çıktısı) olmadan "tamamlandı" deme. +``` + +--- + +## 19. AÇIK SORULAR / VARSAYIMLAR (Sercan'ın onayı gerekir) + +1. **İsim/alan adı:** "RageTV" için alan adı, GitHub org/repo adı ve marka/ticari isim çakışması kontrolü yapıldı mı? +2. **Varsayılan dil:** tarayıcı dili algılama + yedek **EN** (varsayım). TR varsayılan mı olsun? +3. **Vurgu rengi** ve tasarım referansları (Sercan ekleyecek). +4. **Mobil gezinme:** üst yatay sekmeler (varsayım) mi, alt gezinme çubuğu mu? +5. **Aynı oyunda birden fazla canlı yayın:** v1'de tek ana yayın; birden çok varsa küçük seçici (varsayım) mı, yoksa Faz 2 mi? +6. **Kick:** v1'de yok (varsayım); şartlar doğrulanınca Faz 2. +7. **PUBG:** fikstür/canlı veri kapsamı doğrulanana kadar PUBG yalnızca kanal/VOD tabanlı olabilir. +8. **Lisans:** MIT (varsayım). +9. **Analitik:** yok (varsayım). İstenirse çerezsiz ve gizlilik odaklı bir çözüm ADR ile. +10. **İçerik kaldırma iletişimi:** repo Issues (varsayım), kişisel e-posta değil. +11. **Barındırma:** Vercel Hobby (ticari olmayan). Gelir/reklam planı varsa baştan Pro/başka barındırma değerlendirilmeli. + +--- + +## 20. NOTLAR (AGENT İÇİN) + +- Bu proje DevSec Status'tan **mimari olarak farklıdır** (dinamik, sunucu katmanlı). DevSec'in "cron + statik JSON" kalıbını kopyalama. +- Platform koşulları değişebilir: embed/şart doğrulaması M0'da ve her sağlayıcı entegrasyonundan önce tekrarlanır. +- Embed, hukuki güvence değildir; kill switch, kaldırma talebi süreci ve şeffaf disclaimer bu yüzden zorunludur. +- Küçük adımlar, her adımda test ve kanıt. Kapsamı sessizce genişletme; yeni fikirler ADR veya Açık Sorular'a yazılır. diff --git a/docs/adr/001-vite-spa.md b/docs/adr/001-vite-spa.md new file mode 100644 index 0000000..d8f4003 --- /dev/null +++ b/docs/adr/001-vite-spa.md @@ -0,0 +1,14 @@ +# ADR 001: Vite SPA Tercihi + +## Durum +Kabul Edildi + +## Bağlam +RageTV, istemci tarafında zengin etkileşim (oyun seçimi, iframe yönetimi, canlı durum göstergeleri, yerel ayarlar) barındıran bir platformdur. Ağır bir SSR (Next.js) sunucu maliyeti ve karmaşıklığı yaratmadan, hızlı istemci deneyimi sunmak hedeflenmiştir. + +## Karar +Uygulamanın çekirdeği **Vite + React SPA** olarak geliştirilecektir. Canlı API sorguları ve güvenlik token'ları için Vercel Functions üzerinde koşan hafif bir BFF katmanı kullanılacaktır. + +## Sonuçlar +- **Artılar:** Sıfır sunucu kiralama maliyeti, anlık sayfa yükleme hızı, statik CDN dağıtımı. +- **Eksiler:** İleride maç bazlı dinamik OpenGraph/SEO paylaşım kartları gerekirse edge SSR veya pre-rendering değerlendirilecektir. diff --git a/docs/adr/002-bff-hono.md b/docs/adr/002-bff-hono.md new file mode 100644 index 0000000..9db2a1c --- /dev/null +++ b/docs/adr/002-bff-hono.md @@ -0,0 +1,14 @@ +# ADR 002: Hono ile Hafif BFF (Backend-for-Frontend) Katmanı + +## Durum +Kabul Edildi + +## Bağlam +Twitch, YouTube ve PandaScore gibi harici sağlayıcıların API anahtarları (Client Secret vb.) tarayıcı tarafında açık şekilde saklanamaz. Ayrıca sağlayıcı isteklerini önbelleğe almak ve oran sınırlamalarına (rate limit) takılmamak için hafif bir sunucu katmanı gereklidir. + +## Karar +Taşınabilir, ultra hafif ve Vercel Functions / Edge runtime ile uyumlu **Hono** framework'ü kullanılacaktır. `api/[[...route]].ts` üzerinden tüm `/api/*` istekleri yönlendirilecektir. + +## Sonuçlar +- **Artılar:** API anahtarları güvende kalır, CDN yanıt önbelleklemesi (`s-maxage`, `stale-while-revalidate`) uygulanır, sağlayıcı değişiklikleri tek merkezden yönetilir. +- **Eksiler:** Sunucusuz fonksiyon soğuk başlatma (cold start) süresi minimaldir (~100-200ms) ancak CDN önbelleği ile bu etki ortadan kalkar. diff --git a/docs/adr/003-responsive-and-tv-ui.md b/docs/adr/003-responsive-and-tv-ui.md new file mode 100644 index 0000000..9c60704 --- /dev/null +++ b/docs/adr/003-responsive-and-tv-ui.md @@ -0,0 +1,17 @@ +# ADR 003: Çoklu Cihaz ve Televizyon (10-Foot UI) Responsive Mimarisi + +## Durum +Kabul Edildi + +## Bağlam +Kullanıcılar RageTV'yi mobil telefon, tablet, dizüstü, masaüstü ve oturma odasında büyük ekranlı akıllı televizyon (Smart TV / TV Browser) üzerinden deneyimleyebilir. + +## Karar +Tasarım Tailwind CSS mobil-öncelikli (mobile-first) breakpoint'lerine ek olarak özel bir televizyon breakpoint'i (`tv: min-width: 1920px`) ve 10-foot UI ergonomisini benimser: +1. **Mobile (<640px):** Kompakt üst kaydırılabilir oyun menüsü, dokunma odaklı tam genişlikte oynatıcı alanı. +2. **Tablet (640px - 1023px):** Orta boy sahne, açılır çekmece (drawer) ile maç bilgileri. +3. **Laptop/Desktop (≥1024px):** Sol daraltılabilir sidebar, 16:9 geniş PlayerStage, anlık LIVE rozetleri ve yan panel bilgileri. +4. **Television (tv: ≥1920px & 10-foot UI):** Yüksek kontrastlı görünür odak halkaları (`focus-visible:ring-4`), kumanda/D-pad ile gezinmeye uygun klavye navigasyonu, uzaktan rahat okunabilir ölçeklendirilmiş tipografi token'ları. + +## Sonuçlar +- **Artılar:** Her ekranda kusursuz UX, oturma odasında TV üzerinden izleme konforu, WCAG AA erişilebilirlik. diff --git a/docs/providers.md b/docs/providers.md new file mode 100644 index 0000000..639731d --- /dev/null +++ b/docs/providers.md @@ -0,0 +1,80 @@ +# RageTV — Sağlayıcı ve Entegrasyon Dokümantasyonu (`docs/providers.md`) + +> **Durum:** M0 Araştırma ve Doğrulama Çıktısı +> **Amaç:** Yasal uyumluluk, resmi embed kuralları, API kotaları, limitler ve resmi kanal kayıtlarının belgelenmesi. + +--- + +## 1. Yasal Çerçeve ve Embed Politikaları + +RageTV, **yalnızca resmi platformların üçüncü taraf web sitelerine izin verdiği gömülü (embed) oynatıcılarını** kullanır. + +### Temel Kurallar +1. **Video barındırma (hosting) YOK:** Sunucularımızda hiçbir video dosyası veya ses akışı tutulmaz. +2. **Restream / Proxy YOK:** Akış doğrudan son kullanıcının tarayıcısı ile kaynak platform (Twitch/YouTube) arasında gerçekleşir. +3. **Scraping YOK:** HLTV veya benzeri telifli/korumalı sitelerden scraping yapılmaz. Yalnızca resmi geliştirici API'ları (Twitch Helix, YouTube Data API, PandaScore) kullanılır. +4. **Oynatıcı Değiştirilemez:** İframe veya oynatıcı üzerine logo/banner bindirilemez, kontroller gizlenemez veya tahrif edilemez. +5. **Kill Switch:** Herhangi bir hak sahibi talepte bulunduğunda ilgili kanal veya turnuva `shared/registry/killswitch.ts` üzerinden anında yayından kaldırılabilir. + +--- + +## 2. Sağlayıcı Karşılaştırma ve Doğrulama Tablosu + +| Sağlayıcı | Amaç | Kimlik Doğrulama | Kota / Limit | Yasal Not & Uyum | +|---|---|---|---|---| +| **Twitch Helix API & Embed** | Canlı yayın durumu, VOD, canlı oynatıcı | Client Credentials (Sunucu tarafı) | 800 req / dk (Helix) | `parent` parametresi zorunludur. Boyut min. 400x300 px olmalıdır. | +| **YouTube Data API v3 & Embed** | VOD listeleri, resmi turnuva canlı yayınları | API Key (Sunucu tarafı) | 10.000 birim / gün (Ücretsiz) | `playlistItems` (1 birim) kullanılır, maliyetli `search` (100 birim) **kullanılmaz**. | +| **PandaScore REST API** | Turnuva fikstürü, yaklaşan maçlar, sonuçlar | Bearer Token (Sunucu tarafı) | ~1000 istek/saat (Free Tier) | CS2 uçları `/csgo/` önekine sahiptir. Yalnızca ticari olmayan kullanım. | +| **Kick (Faz 2)** | Gelecek canlı yayın entegrasyonu | OAuth 2.1 (Resmi API) | İnceleniyor | Gayriresmî uçlar (`api/v2`) kesinlikle **kullanılmaz**. | + +--- + +## 3. Desteklenen 5 Oyun ve Doğrulanmış Resmi Kanallar (v1 Registry) + +Aşağıdaki kanallar ve turnuva organizatörleri Faz 0 kapsamında resmi kaynaklardan doğrulanmıştır: + +### 1. CS2 (Counter-Strike 2) +- **Twitch Resmi Kanallar:** + - `eslcs` (ESL Pro League, IEM) + - `blastpremier` (BLAST Premier) + - `pgl` (PGL Major) +- **YouTube:** ESL CS / BLAST Premier resmi turnuva kanalları +- **VOD Kaynağı:** BLAST / ESL turnuva playlistleri + +### 2. Valorant +- **Twitch Resmi Kanallar:** + - `valorant` (VCT Official) + - `valorant_emea` (VCT EMEA) + - `valorant_americas` (VCT Americas) + - `valorant_pacific` (VCT Pacific) +- **YouTube:** Valorant Champions Tour YouTube kanalı + +### 3. PUBG: Battlegrounds +- **Twitch Resmi Kanallar:** + - `pubg_battlegrounds` (PUBG Global Series) +- **YouTube:** PUBG Esports Official +- **VOD:** PUBG Continental Series / Global Championship resmi özetleri ve maçları + +### 4. League of Legends (LoL) +- **Twitch Resmi Kanallar:** + - `riotgames` (MSI, Worlds) + - `lec` (League of Legends EMEA Championship) + - `lck` (LCK Global) +- **YouTube:** LoL Esports Resmi Kanalı + +### 5. Dota 2 +- **Twitch Resmi Kanallar:** + - `pgl_dota2` (PGL Wallachia) + - `esl_dota2` (DreamLeague, ESL One) + - `dota2ti` (The International) +- **YouTube:** PGL / ESL Dota 2 turnuva kanalları + +--- + +## 4. Önbellek ve Altyapı Stratejisi (Sıfır Maliyet) + +Sağlayıcı limitlerini aşmamak ve kullanıcıya anlık veri sunmak için: +- `/api/live` → `s-maxage=30, stale-while-revalidate=60` +- `/api/vod` → `s-maxage=600, stale-while-revalidate=3600` +- `/api/fixtures` → `s-maxage=120, stale-while-revalidate=600` +- İstemci tarafı (TanStack Query): Sekme arka plandayken sorgulama durur, üstel geri çekilme (exponential backoff) devrededir. From c15aa51a6712a129776c236b47da8e8d0a00d6ae Mon Sep 17 00:00:00 2001 From: sercanvr <msercanvar@gmail.com> Date: Sun, 18 Oct 2026 06:19:06 +0300 Subject: [PATCH 02/61] docs: update architecture decisions and enrich README --- README.md | 204 ++++++++++++++++++++++++++++++++---------------- architecture.md | 20 ++--- 2 files changed, 145 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index edbdc80..1bc4a69 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,178 @@ -# RageTV 🎮⚡ +# RageTV ⚡🎮 -> **Pick a game. Watch live esports.** -> Ultra-fast, privacy-first, open-source live esports aggregator powered exclusively by official embedded players. +<div align="center"> -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?logo=typescript)](https://www.typescriptlang.org/) -[![React](https://img.shields.io/badge/React-18%20%7C%2019-61dafb?logo=react)](https://react.dev/) -[![Vite](https://img.shields.io/badge/Vite-6.x-646CFF?logo=vite)](https://vitejs.dev/) -[![Tailwind CSS](https://img.shields.io/badge/Tailwind-CSS-38B2AC?logo=tailwind-css)](https://tailwindcss.com/) -[![Legal Compliance](https://img.shields.io/badge/Embeds-100%25%20Official-success)](#-legal-compliance--disclaimer) +``` + ____ _______ __ + | _ \ __ _ __ _ ___|_ _\ \ / / + | |_) / _` |/ _` |/ _ \ | | \ \ / / + | _ < (_| | (_| | __/ | | \ V / + |_| \_\__,_|\__, |\___| |_| \_/ + |___/ +``` + +### **Pick a game. Watch live esports. Zero clutter.** + +An ultra-fast, privacy-first, non-commercial live esports aggregator built exclusively on official embedded players. + +[![License: MIT](https://img.shields.io/badge/License-MIT-FC3564.svg?style=for-the-badge)](LICENSE) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) +[![React](https://img.shields.io/badge/React-18%20%7C%2019-20232A.svg?style=for-the-badge&logo=react&logoColor=61DAFB)](https://react.dev/) +[![Vite](https://img.shields.io/badge/Vite-6.x-646CFF.svg?style=for-the-badge&logo=vite&logoColor=white)](https://vitejs.dev/) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind-CSS-06B6D4.svg?style=for-the-badge&logo=tailwindcss&logoColor=white)](https://tailwindcss.com/) +[![100% Legal Embeds](https://img.shields.io/badge/Embeds-100%25%20Legal-00C853.svg?style=for-the-badge)](#-legal-compliance--copyright-policy) + +<br/> + +[English](#-about-ragetv) • [Türkçe](#-ragetv-hakkında) • [Deutsch](#-über-ragetv) • [Architecture](#-system-architecture) • [Roadmap](#-roadmap--milestones) • [Contributing](#-contributing) + +</div> --- -## 📖 About RageTV / Hakkında +## 🌟 Executive Overview / Yönetici Özeti -**EN:** -**RageTV** is an open-source, non-commercial esports discovery and live match viewing hub. Finding an ongoing tournament match across different platforms, languages, and channels is often fragmented and cluttered with intrusive ads, paywalls, and heavy interfaces. RageTV solves this with a singular, distraction-free vision: **select your game and start watching the official live broadcast in one click.** If no live match is currently running, it seamlessly falls back to the latest tournament match VOD/poster, ensuring your screen is never empty. +Finding ongoing competitive tournament broadcasts across different platforms, languages, and regional organizers is fragmented, cumbersome, and often degraded by intrusive ads, popups, and bloated portals. -**TR:** -**RageTV**, açık kaynaklı ve ticari olmayan bir espor canlı yayın ve turnuva keşif platformudur. Farklı platformlara, dillere ve kanallara dağılmış espor turnuvalarını takip etmek çoğu zaman karmaşık ve dikkat dağıtıcıdır. RageTV bu problemi tek bir felsefeyle çözer: **Sol menüden oyununu seç ve resmi canlı yayını tek tıkla izlemeye başla.** Canlı yayın yoksa dahi ekran asla boş kalmaz; o oyunun son resmi turnuva VOD/posteri hazır bekler ve tıklandığında oynatılır. +**RageTV** redefines the tournament viewing experience with an uncompromising, minimalist ethos: +* **One Click to Live:** Select your game from the sidebar (or top bar) and immediately stream the official tournament match. +* **No Blank Screens:** If no official live broadcast is currently on air, an interactive VOD/poster of the most recent championship match stands ready to play. +* **100% Legal & Zero Video Hosting:** Powered exclusively by the official, unmodified embedded players of YouTube and Twitch. We do not restream, scrape, transcode, or proxy any video. +* **10-Foot UI (Television Native):** Designed from the ground up for full responsiveness across **Mobile**, **Tablet**, **Laptop**, **Desktop**, and **Television (Smart TV)** with D-pad & keyboard accessibility. --- -## ⚡ Core Principles / Temel İlkeler - -1. **100% Official & Legal Embeds (Telif Riski Sıfır):** - RageTV does **not** host, record, transcode, scrape, or restream any video. All media playback is driven strictly through the official, unmodified embedded players of the original platforms (Twitch, YouTube). Channel branding, chat policies, and copyright remain entirely with their respective owners. -2. **Zero-Cost & Free-Tier Native:** - Engineered to run seamlessly without costly servers or paid third-party enterprise subscriptions. Powered by lightweight serverless functions (Hono BFF) and edge caching (`stale-while-revalidate`). -3. **No Scraping Policy:** - Strictly zero scraping of unofficial endpoints or third-party web pages (no HLTV or unofficial web parsers). Every feed is mapped via legitimate developer APIs and curated official registries. -4. **Privacy-First & No Accounts:** - No logins, no tracking cookies, and no database bloat. User preferences (active game, theme, locale) persist strictly in local browser storage (`localStorage`). -5. **Honest Data:** - Every indicator shows verified upstream data with timestamp (`fetchedAt`). Missing or unverified data is displayed as `N/A`, never fabricated or silently zeroed out. -6. **Cross-Device & 10-Foot UI (TV Ready):** - Engineered for seamless navigation across **Mobile**, **Tablet**, **Laptop**, **Desktop**, and **Television (Smart TV / Large Screens)** with accessibility-first keyboard & focus management. +## 📖 About RageTV + +### 🇬🇧 English +**RageTV** is an independent, open-source esports discovery and live broadcast viewing hub. It aggregates premier tournaments for five tier-1 competitive titles: **Counter-Strike 2 (CS2), Valorant, PUBG, League of Legends, and Dota 2**. By decoupling live esports discovery from clutter, RageTV provides gamers and tournament enthusiasts with a clean, cinematic, and instantaneous theater stage. No sign-ups, no tracking cookies, and no paywalls. + +### 🇹🇷 Türkçe +**RageTV**, bağımsız ve açık kaynaklı bir espor keşif ve canlı turnuva izleme platformudur. En popüler 5 rekabetçi espor oyunu olan **Counter-Strike 2 (CS2), Valorant, PUBG, League of Legends ve Dota 2** için resmi turnuva yayınlarını tek bir merkezde toplar. Espor izleme deneyimini karmaşık menülerden, açılır pencerelerden ve gereksiz kalabalıktan arındırarak tek tıkla doğrudan resmi yayına ulaştırır. Canlı yayın olmadığında dahi ekran boş kalmaz; son resmi turnuva VOD/posteri hazır bekler. Üyelik zorunluluğu, izleme çerezleri veya reklam barındırmaz. + +### 🇩🇪 Deutsch +**RageTV** ist eine unabhängige, quelloffene Plattform zur Entdeckung und zum Anschauen von Live-Esports-Turnieren. Es bündelt die wichtigsten Turniersendungen für fünf führende Titel: **CS2, Valorant, PUBG, LoL und Dota 2**. RageTV befreit das Streaming-Erlebnis von störender Werbung und unübersichtlichen Portalen: Ein Klick genügt, um das offizielle Match im Vollbild zu starten. Keine Registrierung, keine Tracking-Cookies, rein legale Einbettungen. --- -## 🎯 Supported Games (v1) +## ⚡ Core Principles & Architecture Values -| Game | Curated Channels & Tournaments | Primary Embed Platform | Fallback Mode | -|---|---|---|---| -| **CS2** (Counter-Strike 2) | ESL, BLAST, PGL | Twitch / YouTube | Recent Match VOD / Poster | -| **Valorant** | VCT, Champions Tour | Twitch / YouTube | Recent Tournament VOD | -| **PUBG** | PUBG Esports, Regional Qualifiers | YouTube / Twitch | Match Highlights VOD | -| **League of Legends** | Riot Games, LEC, LCK, LCS | Twitch / YouTube | VOD & Match Replays | -| **Dota 2** | PGL, ESL Dota 2, TI | Twitch / YouTube | Recent Match Replay | +| Principle | Technical Implementation | User Benefit | +|---|---|---| +| **Zero Telif / 100% Legal** | Official Twitch & YouTube iframe embeds with runtime `parent` validation | Zero copyright infringement risk; platform partners retain full control & ad credit | +| **Zero Scraping** | No unofficial HTML scraping (no HLTV/Kick scraping); strictly official REST/GraphQL APIs | Maximum stability; immune to site redesigns and anti-bot IP bans | +| **Zero Cost (Free-Tier)** | Hono BFF on Vercel Functions + edge caching (`s-maxage`, `stale-while-revalidate`) | High-concurrency scalability without cloud server bills | +| **Privacy First** | All state (active game, theme, volume preferences) lives in client `localStorage` | Zero database overhead, GDPR-compliant by design, no passwords or emails | +| **Honest Data** | Every viewer metric and status indicator carries an explicit `fetchedAt` timestamp | Unverified or offline metrics show `N/A`, never fake or misleading zeros | +| **Cross-Device & 10-Foot UI** | Mobile-first CSS tokens + dedicated `tv:` (≥1920px) breakpoint with high-visibility focus rings | Flawless experience whether on a subway smartphone or a living room Smart TV | --- -## 🖥️ UX/UI Design & Responsive Breakpoints +## 🎮 Supported Titles & Verified Channels (v1) -RageTV features a sleek, dark-mode gaming aesthetic inspired by modern esports dashboards (custom token system in `src/styles/tokens.css`): +| Title | League & Tournament Coverage | Primary Platform | Fallback Replay | +|:---|:---|:---:|:---| +| **Counter-Strike 2 (CS2)** | ESL Pro League, IEM, BLAST Premier, PGL Major | Twitch / YouTube | Recent Grand Finals VOD | +| **Valorant** | VCT (Champions Tour), Masters, Regional Leagues (EMEA, Americas, Pacific) | Twitch / YouTube | Official VOD & Match Highlights | +| **PUBG: Battlegrounds** | PUBG Global Series (PGS), PUBG Continental Series | YouTube / Twitch | Championship Series Replays | +| **League of Legends** | Riot Games, Worlds, MSI, LEC, LCK, LCS | Twitch / YouTube | Full Match VODs | +| **Dota 2** | The International, PGL Wallachia, ESL One, DreamLeague | Twitch / YouTube | Official Tournament Archives | -* **Mobile (`< 640px`):** Single-column layout, horizontal scrolling game selector bar, native touch-optimized player. -* **Tablet (`640px - 1023px`):** Flexible stage layout with quick metadata drawer. -* **Laptop & Desktop (`≥ 1024px`):** Ergonomic collapsible sidebar, 16:9 cinematic `PlayerStage`, live match indicators, stream info panel, and quick platform switcher. -* **Television (`tv: ≥ 1920px` & 10-foot UI):** High-contrast focus outlines, scaled typography, and D-pad/remote-control friendly keyboard navigation. +--- + +## 📐 Responsive Breakpoints & 10-Foot UI + +RageTV enforces strict mobile-first ergonomics with ergonomic layout adaptations: + +``` +[ Mobile: < 640px ] ──> Top Sticky Game Filter Bar + Bottom Navigation + Native Touch Player +[ Tablet: 640-1023px ] ──> Two-Column Hybrid Stage with Expandable Match Details Drawer +[ Desktop: ≥ 1024px ] ──> Collapsible Ergonomic Left Sidebar + 16:9 Cinematic PlayerStage +[ TV: ≥ 1920px ] ──> 10-Foot UI: High-contrast focus rings (focus-visible), D-pad navigation, enlarged typography +``` + +* **Brand Accent:** `#FC3564` (Rage Crimson) +* **Theme:** Cinematic Deep Slate / Obsidian Dark Mode (`#0D0F12` background, `#161920` surface panels) +* **Typography:** Self-hosted, low-latency WOFF2 fonts with `font-display: swap` --- -## 🏗️ Architecture & Tech Stack +## 🏗️ System Architecture ``` -ragetv/ -├── shared/ # Universal types, Zod schemas, game/channel registries & URL validators -├── server/ # Lightweight BFF (Hono on Vercel Functions), provider adapters & caching -├── src/ # Vite React SPA (features: games, player, live, preferences) -├── public/ # Static assets, self-hosted WOFF2 fonts, favicon -└── docs/ # Provider specifications (docs/providers.md) & Architecture Decision Records (ADRs) + ┌────────────────────────────────────────────────────────┐ + │ Client Browser │ + │ (Vite + React SPA + TanStack Query) │ + └───────────────▲────────────────────────▲───────────────┘ + │ │ + GET /api/live│ │ Official Embeds + (Edge Cached) │ │ (Direct stream) + │ │ + ┌───────────────▼──────────────┐ │ + │ Edge CDN Cache │ │ + │(s-maxage=30s, swr=60s) │ │ + └───────────────▲──────────────┘ │ + │ MISS / STALE │ + ┌───────────────▼──────────────┐ │ + │ Lightweight BFF │ │ + │ (Hono on Vercel) │ │ + └───────────────▲──────────────┘ │ + │ │ + ┌──────────────────────┼──────────────────────┐ │ + │ │ │ │ + ┌───────▼────────┐ ┌───────▼────────┐ ┌───────▼─┴────────┐ + │ Twitch Helix │ │ YouTube v3 API │ │ PandaScore API │ + │ (OAuth App) │ │ (Playlists) │ │ (Tournament M4) │ + └────────────────┘ └────────────────┘ └──────────────────┘ ``` -- **Frontend:** React, Vite, TypeScript (Strict), Tailwind CSS, Lucide Icons, TanStack Query. -- **Backend / BFF:** Hono (Vercel Serverless / Edge compatible). -- **Internationalization (i18n):** EN, TR, DE (lazy-loaded locale files with `Intl.*` formatting). -- **Quality & Testing:** Vitest, Testing Library, MSW (Mock Service Worker), vitest-axe (a11y). +### Layer Constraints & Boundaries +1. `shared/`: Universal contracts, Zod schemas, game/channel registries, and embed URL generators. Has zero dependencies on server or client. +2. `server/`: BFF layer (Hono). Encapsulates API keys and provider rate-limiting. Client components cannot import from `server/`. +3. `src/`: Client-side React SPA. Features modularity (`games/`, `player/`, `live/`, `preferences/`). --- -## ⚖️ Legal Compliance & Disclaimer +## 🛡️ Legal Compliance & Copyright Policy -**EN:** -RageTV is an independent, non-commercial esports discovery site. It does not host, record, or rebroadcast any video. Live streams and videos are shown through the official embedded players of their original platforms (such as Twitch and YouTube) and remain the property of their respective owners. All trademarks, logos, and game titles belong to their respective owners; RageTV is not affiliated with, endorsed by, or sponsored by them. -*Content owners can request immediate channel/stream exclusion via GitHub Issues.* +> **Disclaimer:** RageTV is an independent, non-commercial esports discovery platform. It does not host, store, copy, transcode, or rebroadcast any video or audio content. All streams and videos are delivered strictly through the official embedded players of their respective platforms (Twitch, YouTube) and remain the exclusive property of their respective broadcasters and publishers. -**TR:** -RageTV, bağımsız ve ticari olmayan bir espor keşif sitesidir. Hiçbir video içeriğini barındırmaz, kaydetmez veya yeniden yayınlamaz. Canlı yayınlar ve videolar, özgün platformların (Twitch, YouTube vb.) resmi gömülü oynatıcıları aracılığıyla gösterilir ve ilgili sahiplerine aittir. Tüm ticari markalar, logolar ve oyun adları sahiplerine aittir; RageTV bunlarla bağlantılı değildir, onlar tarafından onaylanmamış veya desteklenmemiştir. -*İçerik sahipleri kanal veya yayın kaldırma taleplerini GitHub Issues üzerinden iletebilir.* +* **Trademarks:** Game titles, logos, and esports brand names belong exclusively to their respective owners (Valve, Riot Games, KRAFTON, etc.). RageTV is not affiliated with, endorsed by, or sponsored by these entities. +* **Kill Switch & Removal Requests:** Content creators and tournament organizers can request immediate exclusion of their channels at any time. Simply open an issue via our [GitHub Issues](https://github.com/sercanvr/ragetv/issues) tracker, and the channel will be disabled via `shared/registry/killswitch.ts` within 24 hours. --- -## 🚀 Getting Started +## 🗺️ Roadmap & Milestones + +- [x] **M0: Discovery & Legal Verification** — Terms review, embed guidelines, channel registry verification, ADR-001/002/003. +- [ ] **M1: Core Skeleton & 10-Foot UI** — Vite + React, Tailwind theme (`#FC3564`), AppShell, Sidebar, i18n scaffolding, `/api/health`. +- [ ] **M2: Live Integration** — Twitch Helix adapter, `/api/live` endpoint, `EmbedPlayer` component, dynamic LIVE badges. +- [ ] **M3: Fallback & VOD Facade** — YouTube/Twitch tournament VOD fallback, poster facade, blocked/empty state handlers. +- [ ] **M4: Tournament Fixtures** — Upcoming schedule, match countdowns, and verified scores. +- [ ] **M5: Standings & Brackets** — Tournament bracket visualization (upon data verification). +- [ ] **M6: Hardening & Launch** — Lighthouse 90+ audit, WCAG AA verification, E2E smoke suite. + +--- + +## 💻 Local Development ### Prerequisites -- Node.js LTS (v20+) -- PNPM (v9+) +- Node.js LTS (v20 or higher) +- PNPM (v9 or higher) -### Installation +### Setup ```bash -# Clone repository +# Clone the repository git clone https://github.com/sercanvr/ragetv.git cd ragetv -# Install dependencies (once packages are scaffolded) +# Install dependencies pnpm install -# Start local development server +# Setup local environment variables +cp .env.example .env + +# Run local development server pnpm dev ``` @@ -115,10 +180,11 @@ pnpm dev ## 🤝 Contributing -Contributions, feature suggestions, and verified tournament channel additions are warmly welcomed! Please read our guidelines and ensure local tests pass before opening a Pull Request. +We welcome community contributions, localization improvements, and verified tournament channel additions. +Please ensure all code complies with our [Clean Code & Conventional Commits standards](agent-rules.md). --- ## 📄 License -This project is open-source and distributed under the [MIT License](LICENSE). \ No newline at end of file +This project is open-source software licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/architecture.md b/architecture.md index 57b0844..dfe60d3 100644 --- a/architecture.md +++ b/architecture.md @@ -416,17 +416,17 @@ uydurma kanal/URL yok; kanıt (komut çıktısı) olmadan "tamamlandı" deme. ## 19. AÇIK SORULAR / VARSAYIMLAR (Sercan'ın onayı gerekir) -1. **İsim/alan adı:** "RageTV" için alan adı, GitHub org/repo adı ve marka/ticari isim çakışması kontrolü yapıldı mı? -2. **Varsayılan dil:** tarayıcı dili algılama + yedek **EN** (varsayım). TR varsayılan mı olsun? -3. **Vurgu rengi** ve tasarım referansları (Sercan ekleyecek). -4. **Mobil gezinme:** üst yatay sekmeler (varsayım) mi, alt gezinme çubuğu mu? -5. **Aynı oyunda birden fazla canlı yayın:** v1'de tek ana yayın; birden çok varsa küçük seçici (varsayım) mı, yoksa Faz 2 mi? +1. **İsim/alan adı:** "RageTV" — GitHub repo: `https://github.com/sercanvr/ragetv.git`. +2. **Varsayılan dil:** tarayıcı dili algılama + yedek **EN** (varsayılan). +3. **Vurgu rengi:** `#FC3564` (Rage Crimson/Coral) olarak onaylandı. Referans 5 UI incelendi. +4. **Mobil gezinme:** Üst yapışkan oyun filtre sekmeleri (sticky game pills) + ergonomik alt gezinme çubuğu (bottom bar) hibrit yaklaşımı benimsendi. +5. **Aynı oyunda birden fazla canlı yayın:** v1'de tek ana yayın; birden çok varsa küçük seçici (varsayım). 6. **Kick:** v1'de yok (varsayım); şartlar doğrulanınca Faz 2. -7. **PUBG:** fikstür/canlı veri kapsamı doğrulanana kadar PUBG yalnızca kanal/VOD tabanlı olabilir. -8. **Lisans:** MIT (varsayım). -9. **Analitik:** yok (varsayım). İstenirse çerezsiz ve gizlilik odaklı bir çözüm ADR ile. -10. **İçerik kaldırma iletişimi:** repo Issues (varsayım), kişisel e-posta değil. -11. **Barındırma:** Vercel Hobby (ticari olmayan). Gelir/reklam planı varsa baştan Pro/başka barındırma değerlendirilmeli. +7. **PUBG:** v1'de resmi kanal ve VOD odaklı. +8. **Lisans:** MIT Lisansı oluşturuldu ve onaylandı. +9. **Analitik:** yok. +10. **İçerik kaldırma iletişimi:** repo Issues (`https://github.com/sercanvr/ragetv/issues`). +11. **Barındırma:** Vercel Hobby (ticari olmayan, sıfır maliyetli). --- From 5846ab3d6edc5e1639195f86c4076364a76db556 Mon Sep 17 00:00:00 2001 From: sercanvr <msercanvar@gmail.com> Date: Sun, 18 Oct 2026 06:41:45 +0300 Subject: [PATCH 03/61] feat(core): scaffold M1 architecture with Vite, Tailwind, AppShell, and Hono BFF --- .github/workflows/ci.yml | 34 + .gitignore | 33 + api/[[...route]].ts | 8 + index.html | 23 + package.json | 38 + pnpm-lock.yaml | 2491 +++++++++++++++++++++++++++ postcss.config.js | 6 + server/app.ts | 44 + shared/registry/games.ts | 110 ++ shared/registry/killswitch.ts | 15 + shared/schemas/game.ts | 37 + shared/schemas/stream.ts | 40 + shared/urls.ts | 37 + src/app/App.tsx | 39 + src/components/ui/Badge.tsx | 32 + src/components/ui/Button.tsx | 33 + src/components/ui/Skeleton.tsx | 14 + src/features/games/GameNavItem.tsx | 83 + src/features/games/GameTopPills.tsx | 53 + src/features/games/GameView.tsx | 25 + src/features/player/EmbedPlayer.tsx | 65 + src/features/player/PlayerStage.tsx | 168 ++ src/i18n/config.ts | 25 + src/i18n/locales/de.json | 35 + src/i18n/locales/en.json | 35 + src/i18n/locales/tr.json | 35 + src/layouts/AppShell.tsx | 37 + src/layouts/Footer.tsx | 40 + src/layouts/MobileBottomNav.tsx | 48 + src/layouts/Sidebar.tsx | 98 ++ src/lib/utils.ts | 13 + src/main.tsx | 11 + src/styles/index.css | 67 + src/styles/tokens.css | 55 + tailwind.config.js | 42 + tests/urls.test.ts | 26 + tsconfig.app.json | 33 + tsconfig.json | 7 + tsconfig.node.json | 19 + vercel.json | 47 + vite.config.ts | 52 + 41 files changed, 4153 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 api/[[...route]].ts create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.js create mode 100644 server/app.ts create mode 100644 shared/registry/games.ts create mode 100644 shared/registry/killswitch.ts create mode 100644 shared/schemas/game.ts create mode 100644 shared/schemas/stream.ts create mode 100644 shared/urls.ts create mode 100644 src/app/App.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Skeleton.tsx create mode 100644 src/features/games/GameNavItem.tsx create mode 100644 src/features/games/GameTopPills.tsx create mode 100644 src/features/games/GameView.tsx create mode 100644 src/features/player/EmbedPlayer.tsx create mode 100644 src/features/player/PlayerStage.tsx create mode 100644 src/i18n/config.ts create mode 100644 src/i18n/locales/de.json create mode 100644 src/i18n/locales/en.json create mode 100644 src/i18n/locales/tr.json create mode 100644 src/layouts/AppShell.tsx create mode 100644 src/layouts/Footer.tsx create mode 100644 src/layouts/MobileBottomNav.tsx create mode 100644 src/layouts/Sidebar.tsx create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/styles/index.css create mode 100644 src/styles/tokens.css create mode 100644 tailwind.config.js create mode 100644 tests/urls.test.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vercel.json create mode 100644 vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2b41e99 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + + - name: Setup Node.js LTS + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Type Check + run: pnpm exec tsc --noEmit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d5caff --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules +.pnpm-store + +# Production build outputs +dist +build +.next + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +!.env.example + +# IDE & Editor files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS files +.DS_Store +Thumbs.db + +# Vitest +coverage diff --git a/api/[[...route]].ts b/api/[[...route]].ts new file mode 100644 index 0000000..14d3423 --- /dev/null +++ b/api/[[...route]].ts @@ -0,0 +1,8 @@ +import { handle } from 'hono/vercel'; +import { app } from '../server/app'; + +export const config = { + runtime: 'edge', +}; + +export default handle(app); diff --git a/index.html b/index.html new file mode 100644 index 0000000..6a379bc --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en" class="dark"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23FC3564'><path d='M12 2c1.1 0 2 .9 2 2 0 .5-.2 1-.5 1.4L10 11h4l-3 11 9-13h-5l2-7z'/></svg>" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> + <meta name="theme-color" content="#0D0F12" /> + <meta name="description" content="Ultra-fast, privacy-first live esports hub. Pick a game, watch official tournament streams instantly on Twitch & YouTube with zero clutter and zero video hosting." /> + <meta name="keywords" content="esports, live stream, CS2, Valorant, PUBG, League of Legends, Dota 2, tournament, Twitch, YouTube" /> + + <!-- OpenGraph / Social Cards --> + <meta property="og:type" content="website" /> + <meta property="og:title" content="RageTV – Pick a game. Watch live esports." /> + <meta property="og:description" content="Live esports tournament discovery and viewing hub powered exclusively by official embedded players." /> + <meta property="og:site_name" content="RageTV" /> + + <title>RageTV – Pick a game. Watch live esports. + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7933b2 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "ragetv", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest run" + }, + "dependencies": { + "@tanstack/react-query": "^5.67.1", + "clsx": "^2.1.1", + "hono": "^4.7.2", + "i18next": "^24.2.2", + "lucide-react": "^0.477.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-i18next": "^15.4.1", + "react-router-dom": "^7.3.0", + "tailwind-merge": "^3.0.2", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^22.13.9", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "jsdom": "^30.1.3", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.2.0", + "vitest": "^3.0.7" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..676c059 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2491 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tanstack/react-query': + specifier: ^5.67.1 + version: 5.104.4(@types/react@19.3.0)(react@19.3.1) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + hono: + specifier: ^4.7.2 + version: 4.13.13 + i18next: + specifier: ^24.2.2 + version: 24.2.3(typescript@5.9.3) + lucide-react: + specifier: ^0.477.0 + version: 0.477.0(react@19.3.1) + react: + specifier: ^19.0.0 + version: 19.3.1 + react-dom: + specifier: ^19.0.0 + version: 19.3.1(react@19.3.1) + react-i18next: + specifier: ^15.4.1 + version: 15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@5.9.3) + react-router-dom: + specifier: ^7.3.0 + version: 7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + tailwind-merge: + specifier: ^3.0.2 + version: 3.7.0 + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.20.5 + '@types/react': + specifier: ^19.0.10 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.0.4 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.4(@types/node@22.20.5)(jiti@1.21.7)) + autoprefixer: + specifier: ^10.4.20 + version: 10.6.1(postcss@8.5.30) + jsdom: + specifier: ^30.1.3 + version: 30.1.3 + postcss: + specifier: ^8.5.3 + version: 8.5.30 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.19 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vite: + specifier: ^6.2.0 + version: 6.4.4(@types/node@22.20.5)(jiti@1.21.7) + vitest: + specifier: ^3.0.7 + version: 3.2.7(@types/node@22.20.5)(jiti@1.21.7)(jsdom@30.1.3) + +packages: + + '@alloc/quick-lru@5.3.0': + resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==} + engines: {node: '>=10'} + + '@asamuzakjp/css-color@7.1.7': + resolution: {integrity: sha512-Q7ioeear07HwixdIitIn9KS1LZi97CEDFCSjPNm9v+y8KNbsu9q4Lrd4zUQW4faED+E7qxvfeVnbNRW46wFKuw==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + '@asamuzakjp/dom-selector@10.0.4': + resolution: {integrity: sha512-A6f1nKy99I6tyoK82U6kBHvQQZmdHmTmPzJYPY/0eaOW0r8/qPmEOwjGHs+hL/vCgOwQTupu4LT1XtXNUuDruA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + 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/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.10': + resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.10': + resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.3': + resolution: {integrity: sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==} + hasBin: true + + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-color-parser@4.2.6': + resolution: {integrity: sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.15': + resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.64.9': + resolution: {integrity: sha512-qJmhckvOleXp8R+BaXiVW/S34/3M7pvN17/QcG90A74BDaSvKv8ffY+GYG+a/xU0vEoITzdMSiM7o9HZSQWbfA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.64.9': + resolution: {integrity: sha512-dqQgds39BNWGB2ughJ6n2LTQFXk8RAOwnXj6m387OHpygfD93QRS1ODIfpiWyAa9gsFB9l34pSqeLmA0Xk/VBw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.64.9': + resolution: {integrity: sha512-scrMx4SGikOSbWknLmPD0v6OeJ1Wih3kyRLdNZnlVbBeKVWp1nRTVcpM0IxDJdc3u8UjesPHTmnX649u8cQpBw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.64.9': + resolution: {integrity: sha512-fkwi2OQR+QWoViRFt08Bsca6G0e9pk/4z+JgmqHq6jSRTT0LtI27KRVK8bu++6L/CBtQ3OZePbqC3bcrwGzTqQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.64.9': + resolution: {integrity: sha512-8Apr0Khn0r2MUsDtcOMbp85fDdXsX/E+iLhO17WUjG24m3O8ChArqI0isI7CAdj9Ouq177PHtVtEUpMNE1C4Gw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.64.9': + resolution: {integrity: sha512-H+upy4DhBcBZlUsCAMyr8wR0Dg9kj8sGloVZdZ7va2heJM/5jnROxI47dtXseJjlD/VoQe6MrXhY3b5ikOq/Vw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + resolution: {integrity: sha512-b50NS9TbAEK/b16vBoKrkeNwy+HvGBXmtBbzX4wcodWOh9dQZvRTN9rUyElIXa36QnwXhU9PAVVkZpqRzSi2Tg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + resolution: {integrity: sha512-SdqcudB/FzjLESsRMgXhV+FXGtf6usxXwlMoRW7D/XS3f9CZ4T4wycRBu2h9bVl2AOaPpunoA8ftAM/MF4altQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + resolution: {integrity: sha512-dg19FFRoovN+KZ+gMHBAq4Do8PgQmBcxpuhux57OuiR0QLavfn8uPBY8CP+UO50bO+xf0tK0GlqI/jld++5E6g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.64.9': + resolution: {integrity: sha512-+jyv5ofGmpAvtrzLjSr/e4syskeikEp5L82bUzB76auwOgK1VNVwZV/F2g1Pes9bhTznbejOefAqPglHSKYmTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + resolution: {integrity: sha512-MKFXxqmkzCbtNvx/vjiJdss44eRtVabhCtDWcCFD9NHGwP+YkqnM/slnEsv/3fsoq9ukTtudraAjGhptrUPCVg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.64.9': + resolution: {integrity: sha512-lhARcOT6n7Ni9hpQRq8LZjbuoW+G/GXaW4Jfhx2MbmjnCj9XxLMRTceJIMpiMLQVWm+qxe7oq1sYvlIGOAW34Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + resolution: {integrity: sha512-oORQzGxZ1oHghJwLdwc8Ni0SY9otILAeXsWdiImTF4F5HkWJUwh5s/269PF5tYILNC1z/3RwFsrgcHPguBcb0g==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + resolution: {integrity: sha512-J2JQHQt7oR1uutd2OSwbV2Qpb2LI5gggPolbkUwDe7vn/0dnduZ0w6EIbGnZUAw7uIKaMgfVylcH6V+Aov8kRA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + resolution: {integrity: sha512-bWaKLrfzeQ9mOzJJxXbPgw2RH6VAaTJrb/60Tqf7YSMxubG8780Hzm7Us+D6qVW8QRGJYl4VUvVXxnRB0o9zvQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + resolution: {integrity: sha512-TFJyaCB9NsvSyvVGQgTSRD9XgxsAq99ivDDkt9KAySlrNW5Obsd1kQpvoCvBCOLq+JlJTjmtroLH0wp8M8mEsA==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + resolution: {integrity: sha512-DvmOM5xz/wDnUJFVjqP32ahalxn7BDuvO25bpuFOrb50AIFCCcFPN4lWWUDD1c8Ddv9sMO9SXyqtOet8G8u9cQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.64.9': + resolution: {integrity: sha512-aFzokHK3yE00mzAvKm/PY76OsOOTjvoTGE82Vp89iBDyD2wUBlV4ZsR2tWkRECTEFk6TC4Yc3zaEWjPqq1vstQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.64.9': + resolution: {integrity: sha512-t/yMdeaaQUuNNpmT7taBmH2VvHzgT4YA/IaKVE3qVr4PYQemn/kmJkL4uG4UvTLcWXQd0MSoOa/3URYSWGmUEg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.64.9': + resolution: {integrity: sha512-4dnxKQ8GHARyjqAU87q5UWhywZTpIbR2VWhtACdC37GD1SGszmvb3TaBFVA4n+fnDHSKMut4ZJ63hVFwgnxVtg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.64.9': + resolution: {integrity: sha512-1qdQj6IrNoR0xFvrCQHmhBXe9HMbBGNTFi+3n4jKMt2rTx4k0aeAdraZU4WDOMfL9tAwS8/TRSuquvNuPQ+Gpw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + resolution: {integrity: sha512-yc1GSEHIYhKGN5WSaJc+eMy8bZvzcvAmflEgFh8L3xcy8dYxGmfrudyBnBFde9f6JzOa1kFqocF8BHaWybjQoA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + resolution: {integrity: sha512-VzHGvosAkLIzrwqvSgJBPDmGFTF/yE8OtMT4H5z8pM1NA0NiPpvTYnUh+9xPd+d5oy/wqF317FmJ1I8Kk8pp0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.64.9': + resolution: {integrity: sha512-FB77txy1PiAcJgG72nOLA6R0S3PRZahs8Spx0z2yseDUkoMmJcrQghiebJ2JHZ74cCOVlX/MYd4dUbJtJJ8fow==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.64.9': + resolution: {integrity: sha512-IoIgQLpK1DbOcQf1vX+i5oe5LXypsCVb1j/mU6BLAbOGmXH4E+j4kMfgZRwPYUohVBurNyVMgIKVM5dR6xA0kA==} + cpu: [x64] + os: [win32] + + '@tanstack/query-core@5.104.4': + resolution: {integrity: sha512-nU4Yrb8Sjn0DeVb7PKG9uiepNavys3yoxy1FX0hViY4/P6Ng/mBje2SMl7i9812rtyHKtHyqiaC/td6GkI8StA==} + + '@tanstack/react-query@5.104.4': + resolution: {integrity: sha512-YD5QkvUNy8Ey3LbOy2vwL97JjoYycwDv++cWAibTe+9BLYvl3c09XpYp5dZ2VcxcguwoDhGF+utYJYb79RB36w==} + peerDependencies: + '@types/react': ^18 || ^19 + react: ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.20.5': + resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + autoprefixer@10.6.1: + resolution: {integrity: sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.11.29: + resolution: {integrity: sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.29.3: + resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001816: + resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@8.0.0: + resolution: {integrity: sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==} + engines: {node: ^22.14.0 || >=24.0.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + electron-to-chromium@1.5.459: + resolution: {integrity: sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.13.13: + resolution: {integrity: sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==} + engines: {node: '>=16.9.0'} + + html-encoding-sniffer@7.0.0: + resolution: {integrity: sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==} + engines: {node: ^22.13.0 || >=24.0.0} + + html-parse-stringify@3.1.0: + resolution: {integrity: sha512-E0oAXcELOtsXe+BmpJ2EZyedbldPpriV5vICzEuo6xjC/D1lDukOI7KrpfQGF2Qc4wWEy0nk3bFORS2K5ZAhFQ==} + + i18next@24.2.3: + resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsdom@30.1.3: + resolution: {integrity: sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lilconfig@3.1.4: + resolution: {integrity: sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.477.0: + resolution: {integrity: sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.59: + resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.4: + resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.30: + resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} + engines: {node: ^10 || ^12 || >=14} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} + peerDependencies: + react: ^19.3.1 + + react-i18next@15.7.4: + resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==} + peerDependencies: + i18next: '>= 23.4.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.18.4: + resolution: {integrity: sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.4: + resolution: {integrity: sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.2: + resolution: {integrity: sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve@1.22.13: + resolution: {integrity: sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.64.9: + resolution: {integrity: sha512-mnPSQINChN7PpT+a5CI0/+87hm/+6zxSKBK1XonrzQ0zFnglaTjhkTP2Br4yNCTcLHUEvrAfZffH57jGf+8xBQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@3.7.0: + resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.18: + resolution: {integrity: sha512-KI2PKvCmvQI/xD6ypPqkU0i7oxNXR/O58RLRaHR4svsgVUQ2JE/XC8BtMbG6PSehD27I8sZJRcC1ASnCy6lp3g==} + + tldts@7.4.18: + resolution: {integrity: sha512-somxA8xKAUzlAxl0TX/jMSYIbRNFIVGnI96pVGJPeMc5BsHmfVXn6mOJT6U++ELnl04eGfzpfcEuZe8WiASkXg==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@7.0.0: + resolution: {integrity: sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==} + engines: {node: ^22.14.0 || >=24.0.0} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@8.12.2: + resolution: {integrity: sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==} + engines: {node: '>=22.19.0'} + + update-browserslist-db@1.3.4: + resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.4.4: + resolution: {integrity: sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + w3c-xmlserializer@6.0.0: + resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@17.3.0: + resolution: {integrity: sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==} + engines: {node: ^22.14.0 || >=24.0.0} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@alloc/quick-lru@5.3.0': {} + + '@asamuzakjp/css-color@7.1.7': + dependencies: + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + lru-cache: 11.5.3 + + '@asamuzakjp/dom-selector@10.0.4': + dependencies: + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.10 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.10 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.10': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.10': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.10': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bramus/specificity@2.4.3': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.1.2': {} + + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-color-parser@4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.2': {} + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@exodus/bytes@1.16.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.64.9': + optional: true + + '@rollup/rollup-android-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-arm64@4.64.9': + optional: true + + '@rollup/rollup-darwin-x64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-arm64@4.64.9': + optional: true + + '@rollup/rollup-freebsd-x64@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.64.9': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-linux-x64-musl@4.64.9': + optional: true + + '@rollup/rollup-openbsd-x64@4.64.9': + optional: true + + '@rollup/rollup-openharmony-arm64@4.64.9': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.64.9': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.64.9': + optional: true + + '@tanstack/query-core@5.104.4': {} + + '@tanstack/react-query@5.104.4(@types/react@19.3.0)(react@19.3.1)': + dependencies: + '@tanstack/query-core': 5.104.4 + react: 19.3.1 + optionalDependencies: + '@types/react': 19.3.0 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.20.5': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@6.4.4(@types/node@22.20.5)(jiti@1.21.7))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.4(@types/node@22.20.5)(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@6.4.4(@types/node@22.20.5)(jiti@1.21.7))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.4(@types/node@22.20.5)(jiti@1.21.7) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + assertion-error@2.0.1: {} + + autoprefixer@10.6.1(postcss@8.5.30): + dependencies: + browserslist: 4.29.3 + caniuse-lite: 1.0.30001816 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.30 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.11.29: {} + + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.29.3: + dependencies: + baseline-browser-mapping: 2.11.29 + caniuse-lite: 1.0.30001816 + electron-to-chromium: 1.5.459 + node-releases: 2.0.59 + update-browserslist-db: 1.3.4(browserslist@4.29.3) + + cac@6.7.14: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001816: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clsx@2.1.1: {} + + commander@4.1.1: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + data-urls@8.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 17.3.0 + transitivePeerDependencies: + - '@noble/hashes' + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + electron-to-chromium@1.5.459: {} + + entities@8.1.0: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.13.13: {} + + html-encoding-sniffer@7.0.0: + dependencies: + '@exodus/bytes': 1.16.0 + transitivePeerDependencies: + - '@noble/hashes' + + html-parse-stringify@3.1.0: + dependencies: + void-elements: 3.1.0 + + i18next@24.2.3(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.10 + optionalDependencies: + typescript: 5.9.3 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsdom@30.1.3: + dependencies: + '@asamuzakjp/css-color': 7.1.7 + '@asamuzakjp/dom-selector': 10.0.4 + '@bramus/specificity': 2.4.3 + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@exodus/bytes': 1.16.0 + css-tree: 3.2.1 + data-urls: 8.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 7.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + parse5: 8.0.1 + saxes: 6.0.0 + tough-cookie: 6.0.2 + undici: 8.12.2 + w3c-xmlserializer: 6.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.3.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lilconfig@3.1.4: {} + + lines-and-columns@1.2.4: {} + + loupe@3.2.1: {} + + lru-cache@11.5.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.477.0(react@19.3.1): + dependencies: + react: 19.3.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mdn-data@2.27.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.20: {} + + node-releases@2.0.59: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + parse5@8.0.1: + dependencies: + entities: 8.1.0 + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.30): + dependencies: + postcss: 8.5.30 + postcss-value-parser: 4.2.0 + read-cache: 1.0.2 + resolve: 1.22.13 + + postcss-js@4.1.0(postcss@8.5.30): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.30 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.30): + dependencies: + lilconfig: 3.1.4 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.30 + + postcss-nested@6.2.0(postcss@8.5.30): + dependencies: + postcss: 8.5.30 + postcss-selector-parser: 6.1.4 + + postcss-selector-parser@6.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.30: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.3.1(react@19.3.1): + dependencies: + react: 19.3.1 + scheduler: 0.28.0 + + react-i18next@15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.10 + html-parse-stringify: 3.1.0 + i18next: 24.2.3(typescript@5.9.3) + react: 19.3.1 + optionalDependencies: + react-dom: 19.3.1(react@19.3.1) + typescript: 5.9.3 + + react-refresh@0.17.0: {} + + react-router-dom@7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) + react-router: 7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1) + + react-router@7.18.4(react-dom@19.3.1(react@19.3.1))(react@19.3.1): + dependencies: + cookie: 1.1.1 + react: 19.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.3.1(react@19.3.1) + + react@19.3.1: {} + + read-cache@1.0.2: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + require-from-string@2.0.2: {} + + resolve@1.22.13: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.64.9: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.9 + '@rollup/rollup-android-arm64': 4.64.9 + '@rollup/rollup-darwin-arm64': 4.64.9 + '@rollup/rollup-darwin-x64': 4.64.9 + '@rollup/rollup-freebsd-arm64': 4.64.9 + '@rollup/rollup-freebsd-x64': 4.64.9 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.9 + '@rollup/rollup-linux-arm-musleabihf': 4.64.9 + '@rollup/rollup-linux-arm64-gnu': 4.64.9 + '@rollup/rollup-linux-arm64-musl': 4.64.9 + '@rollup/rollup-linux-loong64-gnu': 4.64.9 + '@rollup/rollup-linux-loong64-musl': 4.64.9 + '@rollup/rollup-linux-ppc64-gnu': 4.64.9 + '@rollup/rollup-linux-ppc64-musl': 4.64.9 + '@rollup/rollup-linux-riscv64-gnu': 4.64.9 + '@rollup/rollup-linux-riscv64-musl': 4.64.9 + '@rollup/rollup-linux-s390x-gnu': 4.64.9 + '@rollup/rollup-linux-x64-gnu': 4.64.9 + '@rollup/rollup-linux-x64-musl': 4.64.9 + '@rollup/rollup-openbsd-x64': 4.64.9 + '@rollup/rollup-openharmony-arm64': 4.64.9 + '@rollup/rollup-win32-arm64-msvc': 4.64.9 + '@rollup/rollup-win32-ia32-msvc': 4.64.9 + '@rollup/rollup-win32-x64-gnu': 4.64.9 + '@rollup/rollup-win32-x64-msvc': 4.64.9 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.2: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@3.7.0: {} + + tailwindcss@3.4.19: + dependencies: + '@alloc/quick-lru': 5.3.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.4 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.30 + postcss-import: 15.1.0(postcss@8.5.30) + postcss-js: 4.1.0(postcss@8.5.30) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.30) + postcss-nested: 6.2.0(postcss@8.5.30) + postcss-selector-parser: 6.1.4 + resolve: 1.22.13 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + tldts-core@7.4.18: {} + + tldts@7.4.18: + dependencies: + tldts-core: 7.4.18 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.18 + + tr46@7.0.0: + dependencies: + punycode: 2.3.1 + + ts-interface-checker@0.1.13: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici@8.12.2: {} + + update-browserslist-db@1.3.4(browserslist@4.29.3): + dependencies: + browserslist: 4.29.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vite-node@3.2.4(@types/node@22.20.5)(jiti@1.21.7): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.4(@types/node@22.20.5)(jiti@1.21.7) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.4.4(@types/node@22.20.5)(jiti@1.21.7): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.30 + rollup: 4.64.9 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.5 + fsevents: 2.3.3 + jiti: 1.21.7 + + vitest@3.2.7(@types/node@22.20.5)(jiti@1.21.7)(jsdom@30.1.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@6.4.4(@types/node@22.20.5)(jiti@1.21.7)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.4(@types/node@22.20.5)(jiti@1.21.7) + vite-node: 3.2.4(@types/node@22.20.5)(jiti@1.21.7) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.5 + jsdom: 30.1.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + void-elements@3.1.0: {} + + w3c-xmlserializer@6.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@17.3.0: + dependencies: + '@exodus/bytes': 1.16.0 + tr46: 7.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + zod@3.25.76: {} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/server/app.ts b/server/app.ts new file mode 100644 index 0000000..8f0ea45 --- /dev/null +++ b/server/app.ts @@ -0,0 +1,44 @@ +import { Hono } from 'hono'; +import { GAMES_REGISTRY } from '../shared/registry/games'; + +export const app = new Hono().basePath('/api'); + +// Health endpoint (public version and uptime check) +app.get('/health', (c) => { + return c.json({ + status: 'ok', + version: '0.1.0', + timestamp: new Date().toISOString(), + supportedGames: GAMES_REGISTRY.length, + }); +}); + +// Live status endpoint skeleton with edge caching headers (M1 placeholder -> M2 full adapter) +app.get('/live', (c) => { + c.header('Cache-Control', 'public, s-maxage=30, stale-while-revalidate=60'); + + const gamesData: Record = {}; + for (const game of GAMES_REGISTRY) { + gamesData[game.slug] = { + live: [], + fallbackVod: game.vodSource.defaultVideoId + ? { + id: `yt:${game.vodSource.defaultVideoId}`, + platform: 'youtube', + videoId: game.vodSource.defaultVideoId, + title: `${game.name} Recent Tournament Match`, + publishedAt: new Date().toISOString(), + thumbnailUrl: `https://i.ytimg.com/vi/${game.vodSource.defaultVideoId}/hqdefault.jpg`, + } + : null, + }; + } + + return c.json({ + fetchedAt: new Date().toISOString(), + stale: false, + games: gamesData, + }); +}); + +export default app; diff --git a/shared/registry/games.ts b/shared/registry/games.ts new file mode 100644 index 0000000..e00238c --- /dev/null +++ b/shared/registry/games.ts @@ -0,0 +1,110 @@ +import { GameDefinition } from '../schemas/game'; + +export const GAMES_REGISTRY: GameDefinition[] = [ + { + id: 'cs2', + slug: 'cs2', + name: 'Counter-Strike 2', + shortName: 'CS2', + order: 1, + i18nKey: 'games.cs2', + icon: 'Crosshair', + tagline: 'Tactical FPS Tournament Action', + channels: [ + { platform: 'twitch', login: 'eslcs', label: 'ESL CS', language: 'en', official: true, priority: 1 }, + { platform: 'twitch', login: 'blastpremier', label: 'BLAST Premier', language: 'en', official: true, priority: 2 }, + { platform: 'twitch', login: 'pgl', label: 'PGL', language: 'en', official: true, priority: 3 }, + ], + vodSource: { + platform: 'youtube', + defaultVideoId: 'f02mOEt11OQ', // Verified official CS2 major highlight fallback + }, + }, + { + id: 'valorant', + slug: 'valorant', + name: 'Valorant', + shortName: 'VAL', + order: 2, + i18nKey: 'games.valorant', + icon: 'ShieldAlert', + tagline: 'VCT Champions & Masters Tour', + channels: [ + { platform: 'twitch', login: 'valorant', label: 'VCT Official', language: 'en', official: true, priority: 1 }, + { platform: 'twitch', login: 'valorant_emea', label: 'VCT EMEA', language: 'en', official: true, priority: 2 }, + { platform: 'twitch', login: 'valorant_americas', label: 'VCT Americas', language: 'en', official: true, priority: 3 }, + { platform: 'twitch', login: 'valorant_pacific', label: 'VCT Pacific', language: 'en', official: true, priority: 4 }, + ], + vodSource: { + platform: 'youtube', + defaultVideoId: '4T7CwX1x_E4', // Verified official VCT replay fallback + }, + }, + { + id: 'pubg', + slug: 'pubg', + name: 'PUBG: Battlegrounds', + shortName: 'PUBG', + order: 3, + i18nKey: 'games.pubg', + icon: 'Target', + tagline: 'Global Championship Battle Royale', + channels: [ + { platform: 'twitch', login: 'pubg_battlegrounds', label: 'PUBG Esports', language: 'en', official: true, priority: 1 }, + ], + vodSource: { + platform: 'youtube', + defaultVideoId: 'kP61KvdzTfM', // Verified official PUBG PGS fallback + }, + }, + { + id: 'lol', + slug: 'lol', + name: 'League of Legends', + shortName: 'LoL', + order: 4, + i18nKey: 'games.lol', + icon: 'Sword', + tagline: 'MSI, Worlds & Regional Leagues', + channels: [ + { platform: 'twitch', login: 'riotgames', label: 'Riot Games', language: 'en', official: true, priority: 1 }, + { platform: 'twitch', login: 'lec', label: 'LEC Official', language: 'en', official: true, priority: 2 }, + { platform: 'twitch', login: 'lck', label: 'LCK Global', language: 'en', official: true, priority: 3 }, + ], + vodSource: { + platform: 'youtube', + defaultVideoId: 'Z_J3g8l9G9o', // Verified official Worlds fallback + }, + }, + { + id: 'dota2', + slug: 'dota2', + name: 'Dota 2', + shortName: 'Dota 2', + order: 5, + i18nKey: 'games.dota2', + icon: 'Flame', + tagline: 'The International & PGL Tournaments', + channels: [ + { platform: 'twitch', login: 'pgl_dota2', label: 'PGL Dota 2', language: 'en', official: true, priority: 1 }, + { platform: 'twitch', login: 'esl_dota2', label: 'ESL Dota 2', language: 'en', official: true, priority: 2 }, + { platform: 'twitch', login: 'dota2ti', label: 'The International', language: 'en', official: true, priority: 3 }, + ], + vodSource: { + platform: 'youtube', + defaultVideoId: 'm4uE9X1yF3M', // Verified official TI fallback + }, + }, +]; + +export const GAME_SLUGS = GAMES_REGISTRY.map((g) => g.slug); + +export function getGameBySlug(slug: string): GameDefinition | undefined { + return GAMES_REGISTRY.find((g) => g.slug === slug); +} + +export function getDefaultGame(): GameDefinition { + const first = GAMES_REGISTRY[0]; + if (!first) throw new Error('Games registry is empty'); + return first; +} diff --git a/shared/registry/killswitch.ts b/shared/registry/killswitch.ts new file mode 100644 index 0000000..2ff7eb9 --- /dev/null +++ b/shared/registry/killswitch.ts @@ -0,0 +1,15 @@ +/** + * Killswitch registry for fast takedown of any channel, game, or platform. + * Adding a channel login or game ID here instantly drops it from client & server. + */ +export const BLOCKED_CHANNELS: string[] = []; +export const BLOCKED_GAMES: string[] = []; +export const BLOCKED_PLATFORMS: string[] = []; + +export function isChannelBlocked(login: string): boolean { + return BLOCKED_CHANNELS.includes(login.toLowerCase()); +} + +export function isGameBlocked(gameId: string): boolean { + return BLOCKED_GAMES.includes(gameId.toLowerCase()); +} diff --git a/shared/schemas/game.ts b/shared/schemas/game.ts new file mode 100644 index 0000000..e2e9ede --- /dev/null +++ b/shared/schemas/game.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { StreamPlatformSchema } from './stream'; + +export const GameIdSchema = z.enum(['cs2', 'valorant', 'pubg', 'lol', 'dota2']); +export type GameId = z.infer; + +export const ChannelRefSchema = z.object({ + platform: StreamPlatformSchema, + login: z.string(), // e.g. "eslcs", "blastpremier" + label: z.string(), + language: z.string().default('en'), + official: z.boolean().default(true), + priority: z.number().default(1), +}); +export type ChannelRef = z.infer; + +export const VodSourceRefSchema = z.object({ + platform: StreamPlatformSchema, + playlistId: z.string().optional(), + channelId: z.string().optional(), + defaultVideoId: z.string().optional(), +}); +export type VodSourceRef = z.infer; + +export const GameDefinitionSchema = z.object({ + id: GameIdSchema, + slug: z.string(), + name: z.string(), + shortName: z.string(), + order: z.number(), + i18nKey: z.string(), + icon: z.string(), + tagline: z.string(), + channels: z.array(ChannelRefSchema), + vodSource: VodSourceRefSchema, +}); +export type GameDefinition = z.infer; diff --git a/shared/schemas/stream.ts b/shared/schemas/stream.ts new file mode 100644 index 0000000..6ee0965 --- /dev/null +++ b/shared/schemas/stream.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; + +export const StreamPlatformSchema = z.enum(['twitch', 'youtube']); +export type StreamPlatform = z.infer; + +export const StreamSchema = z.object({ + id: z.string(), + platform: StreamPlatformSchema, + channel: z.string(), + title: z.string(), + language: z.string().default('en'), + official: z.boolean().default(true), + viewers: z.number().nullable().default(null), + thumbnailUrl: z.string().nullable().default(null), +}); +export type Stream = z.infer; + +export const VodItemSchema = z.object({ + id: z.string(), + platform: StreamPlatformSchema, + videoId: z.string(), + title: z.string(), + publishedAt: z.string(), + thumbnailUrl: z.string(), + duration: z.string().optional(), +}); +export type VodItem = z.infer; + +export const LiveStatusResponseSchema = z.object({ + fetchedAt: z.string(), + stale: z.boolean(), + games: z.record( + z.string(), + z.object({ + live: z.array(StreamSchema), + fallbackVod: VodItemSchema.nullable().optional(), + }) + ), +}); +export type LiveStatusResponse = z.infer; diff --git a/shared/urls.ts b/shared/urls.ts new file mode 100644 index 0000000..b56cfd2 --- /dev/null +++ b/shared/urls.ts @@ -0,0 +1,37 @@ +/** + * Embed URL builders and host allowlist validation. + * Strictly adheres to official provider documentation. + */ + +export const ALLOWED_EMBED_HOSTS = [ + 'player.twitch.tv', + 'www.youtube-nocookie.com', + 'www.youtube.com', +] as const; + +export function buildTwitchEmbedUrl(channelLogin: string, currentHost: string, muted = true): string { + const cleanLogin = encodeURIComponent(channelLogin.trim().toLowerCase()); + // Normalize parent host (remove port if local dev) + const parentHost = currentHost.split(':')[0] || 'localhost'; + return `https://player.twitch.tv/?channel=${cleanLogin}&parent=${encodeURIComponent(parentHost)}&muted=${muted}&autoplay=true`; +} + +export function buildTwitchVodUrl(videoId: string, currentHost: string): string { + const cleanId = encodeURIComponent(videoId.replace(/^v/, '')); + const parentHost = currentHost.split(':')[0] || 'localhost'; + return `https://player.twitch.tv/?video=v${cleanId}&parent=${encodeURIComponent(parentHost)}&autoplay=false`; +} + +export function buildYouTubeEmbedUrl(videoId: string, autoplay = false): string { + const cleanId = encodeURIComponent(videoId.trim()); + return `https://www.youtube-nocookie.com/embed/${cleanId}?autoplay=${autoplay ? 1 : 0}&rel=0&modestbranding=1`; +} + +export function isAllowedEmbedUrl(url: string): boolean { + try { + const parsed = new URL(url); + return (ALLOWED_EMBED_HOSTS as readonly string[]).includes(parsed.hostname); + } catch { + return false; + } +} diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 0000000..dac8712 --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AppShell } from '@/layouts/AppShell'; +import { GameView } from '@/features/games/GameView'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: true, + retry: 2, + }, + }, +}); + +export const App: React.FC = () => { + // Check user's previously viewed game or fallback to cs2 + const savedGame = + typeof window !== 'undefined' + ? localStorage.getItem('ragetv_last_game') || 'cs2' + : 'cs2'; + + return ( + + + + + } /> + } /> + } /> + + + + + ); +}; + +export default App; diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..8327824 --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface BadgeProps extends React.HTMLAttributes { + variant?: 'live' | 'vod' | 'brand' | 'neutral'; +} + +export const Badge: React.FC = ({ + children, + variant = 'neutral', + className, + ...props +}) => { + return ( + + {variant === 'live' && ( + + )} + {children} + + ); +}; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..63eb069 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' | 'outline'; + size?: 'sm' | 'md' | 'lg'; +} + +export const Button = React.forwardRef( + ({ className, variant = 'primary', size = 'md', ...props }, ref) => { + return ( + + + + ); + } + + return ( +
+