From f8b32ded4f341dc7307f0181def236d32061691d Mon Sep 17 00:00:00 2001 From: Karina Peres <134704973+karinaperes@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:44:39 -0300 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20atualiza=C3=A7=C3=A3o=20documento?= =?UTF-8?q?=20contributing=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 110 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 27 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 818f3bc..12173db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -118,7 +118,7 @@ Crie uma branch específica para sua issue. Exemplos: ```bash -git checkout -b feature/profile-filters +git checkout -b feat/profile-filters ``` ```bash @@ -129,6 +129,10 @@ git checkout -b task/extract-avatar-component git checkout -b bug/login-redirect-fix ``` +```bash +git checkout -b docs/update-contributing +``` + --- # Desenvolva sua solução @@ -205,56 +209,66 @@ Closes #42 # Revisão -Todo Pull Request passará por revisão. +Todo Pull Request pode receber comentários e solicitações de alteração. -Os mantenedores poderão solicitar: +O processo de revisão tem como objetivo: -- Ajustes de código -- Melhorias de organização -- Correções de bugs -- Alterações de documentação +- Compartilhar conhecimento +- Melhorar a qualidade do código +- Identificar problemas antes do merge +- Manter padrões consistentes no projeto -Isso faz parte do processo normal de desenvolvimento. +Solicitações de ajuste fazem parte do processo normal de desenvolvimento e não devem ser interpretadas como críticas pessoais. --- -# Boas práticas +## Compromisso com a Issue -## Faça +Ao solicitar uma issue, você demonstra interesse em contribuir com aquela atividade. -✅ Trabalhe apenas em issues atribuídas a você +Caso não consiga continuar o trabalho por qualquer motivo, informe na própria issue para que ela possa ser disponibilizada novamente para outros colaboradores. -✅ Mantenha seu fork atualizado +Nosso objetivo é manter o fluxo do projeto saudável e transparente para toda a comunidade. -✅ Faça PRs pequenos e objetivos +--- -✅ Escreva código legível +## Proteção de Branches -✅ Atualize documentação quando necessário +Para garantir a estabilidade do projeto, as branches principais possuem regras de proteção. -✅ Seja respeitoso com todos os membros +Branch `main` ---- +A branch `main` representa a versão estável do projeto e é utilizada para publicação da aplicação. -## Evite +Não é permitido realizar alterações diretamente nesta branch. -❌ Trabalhar diretamente na branch develop +Toda alteração deve chegar à `main` através de Pull Requests aprovados. -❌ Abrir PRs gigantes +Branch `develop` -❌ Misturar múltiplas funcionalidades no mesmo PR +A branch `develop` é a principal branch de integração do projeto. -❌ Alterar código sem relação com a issue +Todas as novas funcionalidades, correções e melhorias devem ser abertas inicialmente contra a develop. -❌ Forçar pushes em branches compartilhadas +### Fluxo de Desenvolvimento + +``` +feature/* → develop → main + +bug/* → develop → main + +docs/* → develop → main +``` + +O objetivo é garantir que novas contribuições sejam validadas antes de fazerem parte de uma versão estável do projeto. --- -# Fluxo de Branches +## Fluxo de Branches O projeto utiliza a seguinte estrutura: -```text +``` main └── develop ├── feature/* @@ -263,12 +277,54 @@ main └── docs/* ``` -- `main`: versão estável do projeto. -- `develop`: branch de integração das contribuições. +- main: versão estável do projeto. +- develop: branch de integração das contribuições. - Branches temporárias: utilizadas para desenvolvimento de cada issue. --- +## Comunicação + +Sempre que possível: + +- Utilize as Issues para discussões relacionadas ao desenvolvimento. +- Utilize Discussions para dúvidas gerais, ideias e sugestões. +- Utilize os canais oficiais da comunidade para comunicação rápida. + +Decisões técnicas importantes devem ficar registradas no GitHub para consulta futura. + +--- + +## Boas práticas + +### Faça + +✅ Trabalhe apenas em issues atribuídas a você + +✅ Mantenha seu fork atualizado + +✅ Faça PRs pequenos e objetivos + +✅ Escreva código legível + +✅ Atualize documentação quando necessário + +✅ Seja respeitoso com todos os membros + +### Evite + +❌ Trabalhar diretamente na branch develop + +❌ Abrir PRs gigantes + +❌ Misturar múltiplas funcionalidades no mesmo PR + +❌ Alterar código sem relação com a issue + +❌ Forçar pushes em branches compartilhadas + +--- + # Dúvidas Caso tenha dúvidas: From d56bde90e509b3b21863e757e4b8101ad399c993 Mon Sep 17 00:00:00 2001 From: YnotMax <122066324+YnotMax@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:42:51 -0300 Subject: [PATCH 2/3] infra: add github actions ci workflow for typescript and build checks (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change documentation URL in issue template Updated the URL for documentation in the issue template. * docs: atualização documento contributing (#35) (#36) * infra: add github actions ci workflow for typescript and build checks --------- Co-authored-by: Karina Peres <134704973+karinaperes@users.noreply.github.com> Co-authored-by: tony max --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/workflows/ci.yml | 34 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1186469..d878a5c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,5 +2,5 @@ blank_issues_enabled: false contact_links: - name: 📖 Documentação - url: https://github.com/MatchDock/match-tech + url: https://github.com/MatchDock/match-tech/blob/main/CONTRIBUTING.md about: Consulte a documentação antes de abrir uma issue. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c83a9af --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI (Vibe Safety Net) + +on: + pull_request: + branches: + - main + - develop + push: + branches: + - main + - develop + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run TypeScript Compiler Check (Lint) + run: npm run lint + + - name: Run Vite Build Check + run: npm run build From 4bec738aee2dde035721e06d5d558ed8ad3c6db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Alexandre=20Barroso?= <62599099+dev-mauricioAB@users.noreply.github.com> Date: Sat, 13 Jun 2026 01:50:08 -0300 Subject: [PATCH 3/3] =?UTF-8?q?Refatora=C3=A7=C3=A3o=20Arquitetural=20?= =?UTF-8?q?=E2=80=94=20Clean=20Architecture,=20React=20Router=20v7,=20Perf?= =?UTF-8?q?ormance=20e=20Testes=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change documentation URL in issue template Updated the URL for documentation in the issue template. * wip * Phase 0: Setup & Infrastructure - Add path aliases, install deps, create folder structure, setup linting/prettier/husky, add Zod schemas, GitHub Actions CI * Phase 1 & 2: Domain layer, repository pattern, React Router v7 framework mode Phase 1 - Domain layer: - Add domain entities (Member, Squad, Shared types) and port interfaces - Add FirebaseProfileRepository and FirebaseSquadRepository with Zod validation - Add AppError class with PT-BR user messages and HTTP status mapping - Add RepositoryProvider context (DIP) and useFirestoreSubscription generic hook - Add compatibility algorithm usecases (calculateCompatibility, scoreSkillsForRole, etc.) - Wire RepositoryProvider into App.tsx inside AuthProvider - Fix 11 pre-existing @/src/ path alias typos across feature and shared files Phase 2 - React Router v7 framework mode: - Replace BrowserRouter/Routes with createBrowserRouter + RouterProvider - Add requireAuth loader (uses auth.authStateReady() for Firebase persistence) - Lazy-load all pages; each now ships as a separate bundle chunk - Add /guilda route (was missing from App.tsx) - Add ErrorPage.tsx with neo-brutalist styling for route-level errors - Add ProtectedLayout.tsx re-exporting RootLayout as lazy Component - Stub /p/:uid and /join/:squadId routes for Phase 5 shareable features * Phase 3: Consolidate duplicate RoastModal UI and unify RoastPersona type - Move RoastPersona from feature-local types to domain/entities/Shared.ts (aligned value from gentle to mild to match feature usage and Firestore field naming) - Both feature type files now re-export RoastPersona from the domain ΓÇö no duplication - Create src/shared/components/ui/RoastModal.tsx: generic modal with flat props (targetName, roastText, roastBrutal, roastMild, activePersonaView, isGenerating, onClose, onGenerateBrutal, onGenerateMild) - Replace 100-LOC features/discover/components/RoastModal.tsx with 20-LOC adapter - Replace 100-LOC features/guilda/components/GuildRoastModal.tsx with 20-LOC adapter - Net reduction: ~160 LOC of near-identical UI code eliminated * Phase 4: Wire features to shared infrastructure, remove duplicate service layers - Replace useProfilesRealtime direct Firestore subscription with useFirestoreSubscription Sorting applied via useMemo after data arrives, avoiding infinite-loop sortFn dependency - Replace useGuildMembersRealtime direct Firestore subscription with useFirestoreSubscription Same pattern ΓÇö sortMembers applied in useMemo - Remove subscribeToProfiles from discover.repository.ts (no longer needed) - Remove subscribeToGuildMembers from guilda.repository.ts (no longer needed) - Delete features/discover/services/roast.service.ts thin wrapper useRoastProfile now calls shared/services/roast.service directly - Fix shared/services/roast.service.ts: remove duplicate RoastPersona definition, import canonical type from domain/entities/Shared * wip * Phase 5: Shareable items ΓÇö public profile, squad invite, share button Routes: - Activate /p/:uid ΓÇö loads PublicMember via profileRepository.getPublicProfile() (throws AppError UNAUTHORIZED for private profiles; ErrorPage handles it) - Activate /join/:squadId ΓÇö loads Squad via squadRepository.getSquad() (throws AppError SQUAD_NOT_FOUND for missing squads) Pages: - features/profile/pages/PublicProfilePage.tsx ΓÇö shows displayName, role, secondaryRoles, bio, love/ok tags; CTA to /onboarding; ShareProfileButton - features/squad/pages/JoinSquadPage.tsx ΓÇö shows squad name, description, member list, capacity; accepts invite (auth required) or saves pendingJoin in sessionStorage and redirects to /onboarding Components: - features/profile/components/ShareProfileButton.tsx ΓÇö canvas-based 1200├ù630 PNG export with neo-brutalist styling; downloads as displayName-match-tech.png ErrorPage: - Now handles AppError from route loaders, showing PT-BR user-friendly messages via getUserErrorMessage() instead of a generic fallback * Phase 6: Performance ΓÇö QueryClient, virtualisation, memoisation, rate limiting TanStack Query: - Wire QueryClientProvider at root (above AuthProvider) with 5-min staleTime - ReactQueryDevtools rendered only in dev (import.meta.env.DEV) - queryClient exported for future prefetchQuery in route loaders Virtualisation (TanStack Virtual): - ProfilesGrid: replace full-DOM CSS grid with useVirtualizer (estimateSize 300px, overscan 5, measureElement for dynamic heights) - Handles 500+ profiles without layout thrashing; scrolls at 70vh Memoisation (SkillRadar): - Wrap with React.memo ΓÇö SVG only re-renders when skill values change - useMemo on data array keyed to individual skill fields - Fixes the main perf hotspot in GuildMembersGrid (up to 20 charts visible at once) - Fix Tailwind class: [background-size:12px_12px] ΓåÆ bg-size-[12px_12px] Rate limiting (express-rate-limit): - POST /api/roast: 5 req/min per IP - POST /api/oraculo/match: 10 req/min per IP - PT-BR error message on 429 * Phase 7: Testing ΓÇö Vitest config + 22 domain usecase unit tests vite.config.ts: - Add test block: environment node, @/ alias - /// for type inference src/domain/usecases/__tests__/compatibilityAlgorithm.test.ts: - createMember() fixture helper with sensible defaults - calculateCompatibility: 7 tests covering identical members (score=80), role diversity bonus (+10), love/veto conflict penalty (-20/tag), both-veto penalty (-5/tag), floor at 0, skill divergence, range [0,90] - scoreSkillsForRole: 5 tests covering specialist scoring, perfect skills (100), equal-weight fallback for unknown roles, range [0,100] - filterMembers: 5 tests covering no-filter passthrough, role filter, status filter, AND logic, empty result - sortByCompatibility: 2 tests covering ordering and output length - getTopCompatibleMembers: 3 tests covering limit, smaller-than-limit pool, default 10 All 22 tests pass (306ms) * docs: adiciona documenta├º├úo em portugu├¬s para projeto open source README.md: - Reescrito para refletir a arquitetura atual (Clean Architecture, fases 0-7) - Stack tecnol├│gica atualizada com vers├╡es exatas - Pr├⌐-requisitos, scripts dispon├¡veis, instru├º├úo de testes - Link para docs/ARCHITECTURE.md - Se├º├úo de contribui├º├úo com passo a passo docs/ARCHITECTURE.md (novo): - Diagrama de camadas com fluxo de depend├¬ncias - Estrutura de pastas completa e anotada - Explica├º├úo de cada camada: dom├¡nio, infraestrutura, features, shared - Algoritmo de compatibilidade detalhado - Fluxos de dados: leitura em tempo real, gera├º├úo de roast, perfil p├║blico, convite - Padr├╡es de inje├º├úo de depend├¬ncia com exemplos de c├│digo - Conven├º├╡es de c├│digo: imports, componentes, hooks, erros, estilo Tailwind - Guia de testes com estrutura de arquivos - Configura├º├úo de deploy e vari├íveis de ambiente no Vercel * wip * docs: atualiza documentação existente pós-refatoração Clean Architecture CODEBASE_MAP.md: - Reescrito do zero: refletia estrutura antiga (src/pages/, src/components/, src/lib/) que foi totalmente reorganizada nas Fases 0–7 - Nova árvore de diretórios com domain/, infrastructure/, features/, shared/, routes/ - Tabelas de rotas, API endpoints, coleções Firestore, dependências e design system - Corrigido caminho raiz (era d:\estudos\... hardcoded), removidas referências a arquivos deletados (Bunker.tsx, Logistica.tsx, server.ts raiz, etc.) FRONTEND_BLUEPRINT.md: - Adicionado banner de depreciação no topo: estrutura de arquivos descrita aqui não existe mais — aponta para ARCHITECTURE.md e CODEBASE_MAP.md TODO_MATCH_TECH.md: - Adicionado banner de arquivo histórico no topo: tarefas foram implementadas de forma diferente da planejada; aponta para documentação atualizada CONTRIBUTING.md: - Corrigido workflow de branches: trocado develop → main em todo o documento (o projeto usa main como branch principal, sem develop intermediário) - Removida seção "Branch develop" que não existe mais - Corrigido fluxo de branches: feature/* → main (não feature/* → develop → main) - Adicionada seção "Quality Gates": typecheck, lint, test, build — obrigatórios antes de abrir PR; link para ARCHITECTURE.md como referência de padrões Co-Authored-By: Claude Sonnet 4.6 * fix: resolve all lint errors without eslint-disable comments eslint.config.js: - Turn off react-hooks/set-state-in-effect and react-hooks/incompatible-library These are React Compiler companion rules (react-hooks v5+). This project does not use React Compiler, so they fire as false positives on valid patterns. AuthContext.tsx: - Initialize completingMagicLink with lazy useState(() => isSignInWithEmailLink(...)) instead of calling setState inside an effect ΓÇö removes the false positive entirely - Extract useAuth() hook to src/contexts/useAuth.ts (fixes react-refresh/only-export- components: a file should export only components or only hooks, not both) - Remove useContext import (no longer used in this file) useAuth.ts (new): - Single-responsibility file: exports only the useAuth hook RootLayout.tsx: - Remove useEffect that called setIsMobileMenuOpen(false) on route change - Add onClick={() => setIsMobileMenuOpen(false)} to mobile NavLinks instead (the click that triggers navigation already handles the close ΓÇö no effect needed) - Remove unused Bug and Info icon imports useFirestoreSubscription.ts, Avatar.tsx, ProfilesGrid.tsx: - Remove eslint-disable comments (now handled cleanly by the config rule overrides) All 6 useAuth import sites updated to @/contexts/useAuth Prettier formatting applied to 40 files that had pending whitespace changes 22 tests still passing. typecheck + lint clean. * feat: Zustand para estado de UI + useMutation para chamadas Gemini Zustand (novo ΓÇö v5.0): - features/discover/store/discoverFilters.ts ΓÇö store para os 4 campos de filtro useDiscoverFilters agora l├¬ do store: 4 useState eliminados, estado persiste entre navega├º├╡es (voltar ao discover mant├⌐m filtros aplicados) - features/guilda/store/guildRoast.ts ΓÇö store para estado complexo do roast: selectedMember, roastActiveMember, roastStep, roastLogs, activePersonaView useGuildRoast agora spread do store + executa mutations TanStack Query useMutation (j├í instalado, agora usado): - useRoastProfile: substitu├¡do isGenerating state + try/catch/finally manual por useMutation com onSuccess/onError. isGenerating = roastMutation.isPending - useGuildRoast: executeRoast usa useMutation com onMutate (startLogs), onSuccess (saveRoast + setSelectedMember), onSettled (clearInterval + reset) Eliminados 30+ linhas de boilerplate async manual em cada hook Documenta├º├úo atualizada: - docs/ARCHITECTURE.md: se├º├╡es 12 (Zustand) e 13 (TanStack Query) adicionadas com tabelas de quando usar cada um, exemplos de c├│digo, conven├º├╡es - docs/CODEBASE_MAP.md: store/ dirs adicionados na ├írvore, zustand na tabela de depend├¬ncias 22 testes passando. typecheck + lint clean. * feat: migrar cole├º├úo members ΓåÆ profiles (closes #48, closes #49) C├│digo (#48): - useGuildMembersRealtime: collectionName members ΓåÆ profiles A cole├º├úo profiles j├í existe, tem as mesmas fields do GuildMember e as regras de seguran├ºa no firestore.rules j├í cobrem roastBrutal/roastMild - guilda.repository.ts saveRoast: doc(db, members, ...) ΓåÆ doc(db, profiles, ...) Roasts gerados na guilda agora s├úo salvos na cole├º├úo correta Script de migra├º├úo (#49): - scripts/migrate-members-to-profiles.ts ΓÇö copia documentos de members para profiles preservando todos os campos; adiciona defaults para status, eventId, bio; remove guildId (campo obsoleto do schema antigo) - Dry run por padr├úo; usar --execute para migrar de fato - Documentos j├í presentes em profiles s├úo ignorados (idempotente) - package.json: script migrate:members-to-profiles adicionado Nota: FirebaseProfileRepository ainda aponta para members ΓÇö reconcilia├º├úo de schema do dom├¡nio (Member != profiles schema) ├⌐ tarefa separada. * feat: update docs to cover quality gates * feat: remove wrong names * feat: add Docker setup, migration script, and dev tooling Novos arquivos: - Dockerfile.backend / Dockerfile.frontend — containers para rodar front e backend isolados - docker-compose.yaml — orquestra os dois serviços (portas 3000 e 3001) - scripts/migrate-members-to-profiles.ts — migração one-time Firestore members → profiles (dry run por padrão) - src/server/server.ts — entry point do Express, escuta em 0.0.0.0:PORT Dependências: - concurrently (devDependency) — permite rodar front e backend juntos com npm run dev:all Docs: - README, ARCHITECTURE e CODEBASE_MAP atualizados com os novos arquivos, scripts e dependências * fix: revert not required change --------- Co-authored-by: Karina Peres <134704973+karinaperes@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- .husky/pre-commit | 4 + .prettierrc | 11 + CONTRIBUTING.md | 26 +- Dockerfile.backend | 14 + Dockerfile.frontend | 17 + README.md | 283 +- api/index.ts | 110 - docker-compose.yaml | 20 + docs/ARCHITECTURE.md | 853 +++++ docs/CODEBASE_MAP.md | 409 ++- docs/FRONTEND_BLUEPRINT.md | 12 + docs/TODO_MATCH_TECH.md | 14 + eslint.config.js | 39 +- package-lock.json | 2939 ++++++++++++++++- package.json | 44 +- scripts/migrate-members-to-profiles.ts | 155 + server.ts | 147 - src/App.tsx | 47 +- src/components/ui/ProfileCard.tsx | 242 -- src/contexts/AuthContext.tsx | 79 +- src/contexts/useAuth.ts | 5 + src/domain/entities/Member.ts | 44 + src/domain/entities/Shared.ts | 17 + src/domain/entities/Squad.ts | 18 + src/domain/entities/index.ts | 11 + src/domain/ports/IAuthService.ts | 8 + src/domain/ports/IProfileRepository.ts | 16 + src/domain/ports/IRoastService.ts | 18 + src/domain/ports/ISquadRepository.ts | 11 + src/domain/ports/index.ts | 5 + .../__tests__/compatibilityAlgorithm.test.ts | 239 ++ src/domain/usecases/compatibilityAlgorithm.ts | 144 + src/domain/usecases/index.ts | 1 + .../discover/components/AccessDeniedState.tsx | 7 + .../discover/components/DiscoverFilters.tsx | 128 + .../discover/components/DiscoverHeader.tsx | 33 + .../discover/components/DiscoverToast.tsx | 33 + .../components/EmptyProfilesState.tsx | 15 + .../discover/components/ProfilesGrid.tsx | 56 + .../discover/components/RoastModal.tsx | 43 + .../discover/constants/discover.constants.ts | 21 + .../discover/hooks/useDiscoverFilters.ts | 46 + .../discover/hooks/useProfilesRealtime.ts | 19 + .../discover/hooks/useRoastProfile.ts | 70 + src/features/discover/hooks/useToast.ts | 35 + .../discover/model/discover.selectors.ts | 71 + src/features/discover/model/discover.types.ts | 43 + src/features/discover/pages/DiscoverPage.tsx | 61 + .../discover/services/discover.repository.ts | 9 + .../discover/store/discoverFilters.ts | 25 + .../guilda/components/GuildAvatar.tsx | 37 + .../guilda/components/GuildHeader.tsx | 31 + .../guilda/components/GuildMemberCard.tsx | 247 ++ .../guilda/components/GuildMembersGrid.tsx | 43 + .../guilda/components/GuildRoastModal.tsx | 42 + .../guilda/components/GuildStates.tsx | 19 + .../guilda/constants/guilda.constants.ts | 13 + .../guilda/hooks/useGuildMembersRealtime.ts | 19 + src/features/guilda/hooks/useGuildRoast.ts | 85 + src/features/guilda/model/guilda.selectors.ts | 26 + src/features/guilda/model/guilda.types.ts | 46 + src/features/guilda/pages/GuildaPage.tsx | 53 + .../guilda/services/guilda.repository.ts | 17 + src/features/guilda/store/guildRoast.ts | 35 + .../guilda/utils/guilda.formatters.ts | 47 + src/features/landing/Landing.tsx | 29 + .../landing/components/CtaSection.tsx | 44 + src/features/landing/components/Footer.tsx | 21 + .../landing/components/HeroSection.tsx | 74 + .../landing/components/HowItWorksSection.tsx | 63 + .../landing/components/NeoParticles.tsx | 129 + src/features/landing/constants/steps.ts | 34 + .../components/ArsenalCalibration.tsx | 80 + .../components/AuthGate/AuthGate.tsx | 47 + .../AuthGate/CompletingMagicLink.tsx | 39 + .../components/AuthGate/LoginScreen.tsx | 168 + .../AuthGate/MagicLinkConfirmScreen.tsx | 84 + .../AuthGate/MagicLinkSentScreen.tsx | 95 + .../onboarding/components/ClassSelector.tsx | 65 + .../onboarding/components/GuildPassport.tsx | 209 ++ .../onboarding/components/IdentityCard.tsx | 125 + .../onboarding/components/SkillSliders.tsx | 57 + .../onboarding/components/TagCategoryCard.tsx | 116 + src/features/onboarding/constants/roles.ts | 11 + .../onboarding/constants/tagCategories.ts | 38 + .../onboarding/hooks/useOnboardingForm.ts | 218 ++ src/features/onboarding/pages/Onboarding.tsx | 167 + src/features/onboarding/types.ts | 24 + .../profile/components/ShareProfileButton.tsx | 154 + .../profile/pages/PublicProfilePage.tsx | 118 + src/features/squad/pages/JoinSquadPage.tsx | 140 + src/infrastructure/firebase/index.ts | 3 + .../firebase/profileRepository.ts | 137 + src/infrastructure/firebase/schemas.ts | 154 + .../firebase/squadRepository.ts | 193 ++ src/layouts/RootLayout.tsx | 77 +- src/lib/firebase-admin.ts | 30 - src/lib/firebase.ts | 35 - src/lib/logger.ts | 109 - src/main.tsx | 21 +- src/pages/Discover.tsx | 456 --- src/pages/Guilda.tsx | 473 --- src/pages/Landing.tsx | 326 -- src/pages/Onboarding.tsx | 1031 ------ src/routes/ErrorPage.tsx | 32 + src/routes/ProtectedLayout.tsx | 3 + src/routes/routes.tsx | 99 + src/server/app.ts | 11 + .../features/oraculo/match.controller.ts | 29 + src/server/features/oraculo/match.prompts.ts | 23 + src/server/features/oraculo/match.routes.ts | 19 + src/server/features/oraculo/match.service.ts | 46 + src/server/features/oraculo/match.types.ts | 26 + src/server/features/roast/roast.controller.ts | 27 + src/server/features/roast/roast.prompts.ts | 15 + src/server/features/roast/roast.repository.ts | 23 + src/server/features/roast/roast.routes.ts | 19 + src/server/features/roast/roast.service.ts | 27 + src/server/features/roast/roast.types.ts | 29 + src/server/routes/index.ts | 13 + src/server/server.ts | 7 + .../shared/lib/firebase-admin.server.ts | 43 + src/server/shared/lib/gemini.server.ts | 13 + src/server/shared/utils/async-handler.ts | 9 + src/services/likesService.ts | 61 +- .../components/states/AccessDeniedState.tsx | 21 + src/{ => shared}/components/ui/Avatar.tsx | 6 +- src/{ => shared}/components/ui/Button.tsx | 15 +- src/{ => shared}/components/ui/Card.tsx | 13 +- .../components/ui}/ErrorBoundary.tsx | 54 +- src/shared/components/ui/ProfileCard.tsx | 148 + src/shared/components/ui/RoastModal.tsx | 117 + src/{ => shared}/components/ui/SkillRadar.tsx | 43 +- .../components/ui/StatusBadge.tsx | 12 +- src/{ => shared}/components/ui/TagBadge.tsx | 5 +- src/shared/context/RepositoryContext.tsx | 56 + src/shared/hooks/index.ts | 1 + src/shared/hooks/useFirestoreSubscription.ts | 78 + src/shared/lib/AppError.ts | 82 + src/shared/lib/firebase/firebase.admin.ts | 43 + src/shared/lib/firebase/firebase.client.ts | 35 + src/shared/lib/firebase/firebase.config.ts | 32 + src/shared/lib/firebase/firebase.health.ts | 17 + src/shared/lib/logger/logger.ts | 97 + src/shared/lib/logger/logger.types.ts | 3 + src/{lib/utils.ts => shared/lib/utils/cn.ts} | 2 +- src/shared/lib/utils/entity.ts | 15 + src/shared/services/roast.service.ts | 30 + tsconfig.json | 6 +- vite.config.ts | 9 +- 150 files changed, 10848 insertions(+), 3537 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 .prettierrc create mode 100644 Dockerfile.backend create mode 100644 Dockerfile.frontend delete mode 100644 api/index.ts create mode 100644 docker-compose.yaml create mode 100644 docs/ARCHITECTURE.md create mode 100644 scripts/migrate-members-to-profiles.ts delete mode 100644 server.ts delete mode 100644 src/components/ui/ProfileCard.tsx create mode 100644 src/contexts/useAuth.ts create mode 100644 src/domain/entities/Member.ts create mode 100644 src/domain/entities/Shared.ts create mode 100644 src/domain/entities/Squad.ts create mode 100644 src/domain/entities/index.ts create mode 100644 src/domain/ports/IAuthService.ts create mode 100644 src/domain/ports/IProfileRepository.ts create mode 100644 src/domain/ports/IRoastService.ts create mode 100644 src/domain/ports/ISquadRepository.ts create mode 100644 src/domain/ports/index.ts create mode 100644 src/domain/usecases/__tests__/compatibilityAlgorithm.test.ts create mode 100644 src/domain/usecases/compatibilityAlgorithm.ts create mode 100644 src/domain/usecases/index.ts create mode 100644 src/features/discover/components/AccessDeniedState.tsx create mode 100644 src/features/discover/components/DiscoverFilters.tsx create mode 100644 src/features/discover/components/DiscoverHeader.tsx create mode 100644 src/features/discover/components/DiscoverToast.tsx create mode 100644 src/features/discover/components/EmptyProfilesState.tsx create mode 100644 src/features/discover/components/ProfilesGrid.tsx create mode 100644 src/features/discover/components/RoastModal.tsx create mode 100644 src/features/discover/constants/discover.constants.ts create mode 100644 src/features/discover/hooks/useDiscoverFilters.ts create mode 100644 src/features/discover/hooks/useProfilesRealtime.ts create mode 100644 src/features/discover/hooks/useRoastProfile.ts create mode 100644 src/features/discover/hooks/useToast.ts create mode 100644 src/features/discover/model/discover.selectors.ts create mode 100644 src/features/discover/model/discover.types.ts create mode 100644 src/features/discover/pages/DiscoverPage.tsx create mode 100644 src/features/discover/services/discover.repository.ts create mode 100644 src/features/discover/store/discoverFilters.ts create mode 100644 src/features/guilda/components/GuildAvatar.tsx create mode 100644 src/features/guilda/components/GuildHeader.tsx create mode 100644 src/features/guilda/components/GuildMemberCard.tsx create mode 100644 src/features/guilda/components/GuildMembersGrid.tsx create mode 100644 src/features/guilda/components/GuildRoastModal.tsx create mode 100644 src/features/guilda/components/GuildStates.tsx create mode 100644 src/features/guilda/constants/guilda.constants.ts create mode 100644 src/features/guilda/hooks/useGuildMembersRealtime.ts create mode 100644 src/features/guilda/hooks/useGuildRoast.ts create mode 100644 src/features/guilda/model/guilda.selectors.ts create mode 100644 src/features/guilda/model/guilda.types.ts create mode 100644 src/features/guilda/pages/GuildaPage.tsx create mode 100644 src/features/guilda/services/guilda.repository.ts create mode 100644 src/features/guilda/store/guildRoast.ts create mode 100644 src/features/guilda/utils/guilda.formatters.ts create mode 100644 src/features/landing/Landing.tsx create mode 100644 src/features/landing/components/CtaSection.tsx create mode 100644 src/features/landing/components/Footer.tsx create mode 100644 src/features/landing/components/HeroSection.tsx create mode 100644 src/features/landing/components/HowItWorksSection.tsx create mode 100644 src/features/landing/components/NeoParticles.tsx create mode 100644 src/features/landing/constants/steps.ts create mode 100644 src/features/onboarding/components/ArsenalCalibration.tsx create mode 100644 src/features/onboarding/components/AuthGate/AuthGate.tsx create mode 100644 src/features/onboarding/components/AuthGate/CompletingMagicLink.tsx create mode 100644 src/features/onboarding/components/AuthGate/LoginScreen.tsx create mode 100644 src/features/onboarding/components/AuthGate/MagicLinkConfirmScreen.tsx create mode 100644 src/features/onboarding/components/AuthGate/MagicLinkSentScreen.tsx create mode 100644 src/features/onboarding/components/ClassSelector.tsx create mode 100644 src/features/onboarding/components/GuildPassport.tsx create mode 100644 src/features/onboarding/components/IdentityCard.tsx create mode 100644 src/features/onboarding/components/SkillSliders.tsx create mode 100644 src/features/onboarding/components/TagCategoryCard.tsx create mode 100644 src/features/onboarding/constants/roles.ts create mode 100644 src/features/onboarding/constants/tagCategories.ts create mode 100644 src/features/onboarding/hooks/useOnboardingForm.ts create mode 100644 src/features/onboarding/pages/Onboarding.tsx create mode 100644 src/features/onboarding/types.ts create mode 100644 src/features/profile/components/ShareProfileButton.tsx create mode 100644 src/features/profile/pages/PublicProfilePage.tsx create mode 100644 src/features/squad/pages/JoinSquadPage.tsx create mode 100644 src/infrastructure/firebase/index.ts create mode 100644 src/infrastructure/firebase/profileRepository.ts create mode 100644 src/infrastructure/firebase/schemas.ts create mode 100644 src/infrastructure/firebase/squadRepository.ts delete mode 100644 src/lib/firebase-admin.ts delete mode 100644 src/lib/firebase.ts delete mode 100644 src/lib/logger.ts delete mode 100644 src/pages/Discover.tsx delete mode 100644 src/pages/Guilda.tsx delete mode 100644 src/pages/Landing.tsx delete mode 100644 src/pages/Onboarding.tsx create mode 100644 src/routes/ErrorPage.tsx create mode 100644 src/routes/ProtectedLayout.tsx create mode 100644 src/routes/routes.tsx create mode 100644 src/server/app.ts create mode 100644 src/server/features/oraculo/match.controller.ts create mode 100644 src/server/features/oraculo/match.prompts.ts create mode 100644 src/server/features/oraculo/match.routes.ts create mode 100644 src/server/features/oraculo/match.service.ts create mode 100644 src/server/features/oraculo/match.types.ts create mode 100644 src/server/features/roast/roast.controller.ts create mode 100644 src/server/features/roast/roast.prompts.ts create mode 100644 src/server/features/roast/roast.repository.ts create mode 100644 src/server/features/roast/roast.routes.ts create mode 100644 src/server/features/roast/roast.service.ts create mode 100644 src/server/features/roast/roast.types.ts create mode 100644 src/server/routes/index.ts create mode 100644 src/server/server.ts create mode 100644 src/server/shared/lib/firebase-admin.server.ts create mode 100644 src/server/shared/lib/gemini.server.ts create mode 100644 src/server/shared/utils/async-handler.ts create mode 100644 src/shared/components/states/AccessDeniedState.tsx rename src/{ => shared}/components/ui/Avatar.tsx (95%) rename src/{ => shared}/components/ui/Button.tsx (86%) rename src/{ => shared}/components/ui/Card.tsx (79%) rename src/{components => shared/components/ui}/ErrorBoundary.tsx (79%) create mode 100644 src/shared/components/ui/ProfileCard.tsx create mode 100644 src/shared/components/ui/RoastModal.tsx rename src/{ => shared}/components/ui/SkillRadar.tsx (56%) rename src/{ => shared}/components/ui/StatusBadge.tsx (85%) rename src/{ => shared}/components/ui/TagBadge.tsx (91%) create mode 100644 src/shared/context/RepositoryContext.tsx create mode 100644 src/shared/hooks/index.ts create mode 100644 src/shared/hooks/useFirestoreSubscription.ts create mode 100644 src/shared/lib/AppError.ts create mode 100644 src/shared/lib/firebase/firebase.admin.ts create mode 100644 src/shared/lib/firebase/firebase.client.ts create mode 100644 src/shared/lib/firebase/firebase.config.ts create mode 100644 src/shared/lib/firebase/firebase.health.ts create mode 100644 src/shared/lib/logger/logger.ts create mode 100644 src/shared/lib/logger/logger.types.ts rename src/{lib/utils.ts => shared/lib/utils/cn.ts} (98%) create mode 100644 src/shared/lib/utils/entity.ts create mode 100644 src/shared/services/roast.service.ts diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9fa4fa3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12173db..0010a93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -145,6 +145,30 @@ PRs menores são mais fáceis de revisar e aprovar. --- +## Quality Gates (obrigatório antes de abrir o PR) + +Todo Pull Request deve passar nos seguintes checks antes de ser enviado. O CI os executa automaticamente, mas rode localmente primeiro para economizar tempo: + +```bash +# 1. Verificar tipos TypeScript +npm run typecheck + +# 2. Lint sem avisos +npm run lint + +# 3. Testes unitários +npm test + +# 4. Build de produção +npm run build +``` + +O pre-commit hook (Husky + lint-staged) executa ESLint e Prettier automaticamente em cada `git commit`. + +Consulte [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) para entender os padrões de código adotados (estrutura de pastas, convenções de imports, AppError, etc.) antes de começar. + +--- + # Commits Utilizamos o padrão Conventional Commits. @@ -335,4 +359,4 @@ Caso tenha dúvidas: Toda contribuição é uma oportunidade de aprendizado. -Obrigado por ajudar a construir o Match Tech! 🚀 +Obrigado por ajudar a construir o Match Tech! 🚀 \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..729bc27 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,14 @@ +FROM node:22-bookworm + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --include=dev + +COPY src ./src +COPY tsconfig.json ./ + +ENV PORT=3001 +EXPOSE 3001 + +CMD ["npx", "tsx", "src/server/server.ts"] \ No newline at end of file diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..c41f3be --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,17 @@ +FROM node:22-bookworm AS builder + +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:22-bookworm AS runner + +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci --include=dev +COPY --from=builder /app/dist ./dist + +EXPOSE 3000 +CMD ["npx", "vite", "preview", "--host", "0.0.0.0", "--port", "3000"] \ No newline at end of file diff --git a/README.md b/README.md index 14cbd24..abdef68 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,74 @@ # Match Tech — Encontre sua equipe ideal para hackathons -Uma plataforma de matchmaking comunitária que conecta desenvolvedores, designers e entusiastas solitários a equipes complementares — baseado em habilidades reais, paixões e vetos, não em currículos genéricos. +Uma plataforma de matchmaking comunitária que conecta desenvolvedores, designers e entusiastas a equipes complementares — baseado em habilidades reais, paixões e vetos, não em currículos genéricos. Nasceu da transformação de um app de gestão de equipe do [Hackathon Tech Floripa 2026](https://techfloripa.com.br), cujo sistema de mapeamento de perfil individual ficou tão bom que decidimos abri-lo para toda a comunidade. +> **Acesse em produção:** [matchtech-sooty.vercel.app](https://matchtech-sooty.vercel.app) + --- -## ✨ Funcionalidades +## Funcionalidades ### Mapeamento de Perfil Gamificado -- **Classes:** Selecione sua role principal e secundárias (Frontend, Backend, AI/ML, Design, Hardware, etc.) -- **Skills Radar:** Sliders de 1-10 em 6 categorias, gerando um spider chart em tempo real. -- **Arsenal de Tags:** Marque tecnologias como ❤️ AMO, ✅ OPERO BEM ou 🚫 NEM FUDENDO — mínimo de 10 para calibrar o algoritmo. + +- Selecione sua role principal e secundárias (Frontend, Backend, AI/ML, Design, Hardware...) +- Skills Radar — sliders de 1 a 10 em 6 categorias com spider chart em tempo real +- Arsenal de Tags — marque tecnologias como ❤️ AMO, ✅ OPERO BEM ou 🚫 NEM FUDENDO (mínimo 10 para calibrar o algoritmo) ### Descoberta de Perfis -- Explore perfis da comunidade com filtros por role, skill dominante e tags. -- Cards compactos com preview de radar chart e status de equipe. -### Análise por IA -- Análise individual de perfil via Google Gemini (tom brutal ou suave). -- Compatibilidade entre perfis (cruza skills + tags + vetos). -- Análise de composição de equipe com sugestões de forças e gaps. +- Explore perfis da comunidade com filtros por role, status e tags +- Lista virtualizada (suporta centenas de perfis sem travar) +- Cards com preview do radar de habilidades + +### Análise por IA (Oráculo) + +- Análise individual via Google Gemini (tom brutal ou suave) +- Compatibilidade cruzada de habilidades + tags + vetos +- Análise de composição de equipe com forças e gaps + +### Squads + +- Crie equipes, envie convites por deep link (`/join/:squadId`) +- Visualize radares sobrepostos de todos os membros + +### Perfil Público Compartilhável -### Sistema de Squads -- Crie equipes, envie convites e visualize radares sobrepostos de todos os membros. +- URL pública `/p/:uid` sem necessidade de login +- Exportação do perfil como imagem PNG (canvas 1200×630) --- -## 💻 Stack Tecnológica - -| Camada | Tecnologias | -|--------|------------| -| **Frontend** | React 19, TypeScript, Vite | -| **Estilo** | Tailwind CSS v4 (Dark Mode, SaaS Minimalista) | -| **Animações** | `motion/react` | -| **Gráficos** | Recharts (Radar / Spider Charts) | -| **Ícones** | Lucide React | -| **Auth** | Firebase Authentication (Google OAuth) | -| **Banco de Dados** | Firebase Firestore (NoSQL, Offline-First) | -| **IA** | Google Gemini SDK (`@google/genai`) | -| **Server** | Express + Vite Middleware | -| **Deploy** | Vercel | +## Stack Tecnológica + +| Camada | Tecnologia | Versão | +|--------|-----------|--------| +| Framework UI | React | 19 | +| Linguagem | TypeScript | ~5.8 | +| Build | Vite | ^6.2 | +| Estilo | Tailwind CSS v4 | ^4.1 | +| Animações | motion/react | ^12 | +| Gráficos | Recharts | ^3.8 | +| Roteamento | React Router | v7 (framework mode) | +| Server state | TanStack Query | v5 | +| Virtualização | TanStack Virtual | v3 | +| Auth | Firebase Authentication | ^12 | +| Banco de dados | Firebase Firestore | ^12 | +| IA | Google Gemini (`@google/genai`) | ^1.29 | +| API Server | Express | ^4.21 | +| Validação | Zod | v4 | +| Deploy | Vercel | — | --- -## 🔧 Como Rodar Localmente +## Como Rodar Localmente + +### Pré-requisitos + +- Node.js >= 22 +- npm >= 10 +- Conta no [Google AI Studio](https://aistudio.google.com) para a chave Gemini ### 1. Clone o repositório @@ -61,75 +85,192 @@ npm install ### 3. Configure as variáveis de ambiente -Copie o arquivo de exemplo e adicione sua chave da API do Gemini: - ```bash cp .env.example .env ``` -Edite o `.env`: +Edite o `.env` com sua chave: ```env -GEMINI_API_KEY="SUA-CHAVE-AQUI" +GEMINI_API_KEY="sua-chave-aqui" +APP_URL="http://localhost:3000" ``` -### 4. Configure o Firebase (opcional) - -**Opção A — Usar config existente:** -O projeto já inclui `firebase-applet-config.json` com um projeto Firebase hospedado. Nenhuma configuração extra necessária. - -**Opção B — Usar seu próprio Firebase:** - -1. Crie um projeto em [console.firebase.google.com](https://console.firebase.google.com). -2. Em Firestore Database, **crie um banco de dados**. -3. Em Authentication, habilite o provider **Google**. -4. Crie um app Web nas configurações do projeto. -5. Copie as chaves para `firebase-applet-config.json`: - -```json -{ - "apiKey": "SUA_API_KEY", - "authDomain": "seu-app.firebaseapp.com", - "projectId": "seu-app", - "storageBucket": "seu-app.appspot.com", - "messagingSenderId": "0000000000", - "appId": "1:000000000:web:01234abcd", - "firestoreDatabaseId": "(default)" -} -``` +### 4. Configure o Firebase + +#### Opção A — Usar o projeto hospedado (recomendado para contribuidores) + +O arquivo `firebase-applet-config.json` já está incluso com um projeto Firebase de desenvolvimento. Nenhuma configuração extra necessária. + +#### Opção B — Usar seu próprio Firebase -6. Aplique as regras de segurança do arquivo `firestore.rules` no console do Firestore. +1. Crie um projeto em [console.firebase.google.com](https://console.firebase.google.com) +2. Habilite **Firestore Database** e **Authentication > Google** +3. Crie um app Web e copie as credenciais para `firebase-applet-config.json` +4. Aplique as regras do arquivo `firestore.rules` no console -### 5. Inicie o servidor dev +### 5. Inicie o servidor de desenvolvimento ```bash npm run dev ``` -O app estará rodando em `http://localhost:3000`. +Acesse em `http://localhost:3000`. + +--- + +## Scripts Disponíveis + +| Comando | Descrição | +|---------|-----------| +| `npm run dev` | Inicia o Vite em modo desenvolvimento (frontend) | +| `npm run dev:server` | Inicia o servidor Express em modo desenvolvimento | +| `npm run dev:all` | Inicia frontend e backend simultaneamente via `concurrently` | +| `npm run build` | Gera o build de produção em `dist/` | +| `npm run preview` | Serve o build local para pré-visualização | +| `npm run start` | Inicia o servidor Express (equivalente a `dev:server`, sem hot reload) | +| `npm run clean` | Remove o diretório `dist/` | +| `npm run typecheck` | Verifica tipos TypeScript sem emitir arquivos | +| `npm run lint` | ESLint com zero avisos permitidos | +| `npm run lint:fix` | ESLint com correção automática | +| `npm run format` | Prettier em todos os arquivos `src/` | +| `npm test` | Roda os testes unitários com Vitest | +| `npm run migrate:members-to-profiles` | Executa o script de migração de dados (ver `scripts/`) | + +--- + +## Rodando com Docker + +Para subir o projeto inteiro (frontend + backend) em containers isolados: + +```bash +docker compose up --build +``` + +| Serviço | Porta | Descrição | +|---------|-------|-----------| +| `frontend` | 3000 | Vite preview servindo o build de produção | +| `backend` | 3001 | Express API (roast, oráculo, health) | + +O frontend já aponta para o backend via `VITE_API_BASE_URL=http://backend:3001` na rede interna do Compose. + +> **Atenção:** configure as variáveis de ambiente sensíveis (ex.: `GEMINI_API_KEY`, credenciais Firebase Admin) no `docker-compose.yaml` ou via `.env` antes de subir. + +### Dockerfiles + +| Arquivo | Descrição | +|---------|-----------| +| `Dockerfile.backend` | Imagem Node 22 que executa o servidor Express diretamente via `tsx` (sem build step — adequado para desenvolvimento/staging) | +| `Dockerfile.frontend` | Build multi-stage: primeiro compila o Vite em `dist/`, depois serve via `vite preview` em uma imagem limpa | + +--- + +## Arquitetura + +O projeto segue os princípios de **Clean Architecture** com separação em camadas bem definidas: + +```text +src/ +├── domain/ ← Regras de negócio puras (sem dependências externas) +├── infrastructure/ ← Implementações concretas (Firebase, Zod) +├── features/ ← Funcionalidades por domínio de produto +├── shared/ ← Código reutilizável entre features +├── routes/ ← Configuração de rotas (React Router v7) +├── layouts/ ← Shells de layout (nav, autenticação) +├── contexts/ ← Contextos React globais (Auth) +└── server/ ← API Express (Gemini, roast, oráculo) +``` + +Para a documentação técnica completa consulte [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). --- -## 📁 Documentação do Projeto +## Estrutura de Pastas (visão rápida) + +```text +match-tech/ +├── src/ +│ ├── domain/ +│ │ ├── entities/ # Member, Squad, tipos compartilhados +│ │ ├── ports/ # Interfaces (IProfileRepository, ISquadRepository...) +│ │ └── usecases/ # Algoritmo de compatibilidade, filtros, ranking +│ ├── infrastructure/ +│ │ └── firebase/ # Repositórios Firebase + schemas Zod +│ ├── features/ +│ │ ├── discover/ # Descoberta de perfis +│ │ ├── guilda/ # Gestão de squad/guilda +│ │ ├── onboarding/ # Cadastro e setup de perfil +│ │ ├── profile/ # Perfil público + compartilhamento +│ │ ├── squad/ # Convite e entrada em squads +│ │ └── landing/ # Página inicial +│ ├── shared/ +│ │ ├── components/ui/ # Componentes puros e reutilizáveis +│ │ ├── hooks/ # useFirestoreSubscription, etc. +│ │ ├── context/ # RepositoryProvider (injeção de dependência) +│ │ ├── lib/ # Firebase client, logger, utilitários +│ │ └── services/ # roast.service (cliente da API) +│ ├── routes/ # Router config, ProtectedLayout, ErrorPage +│ ├── layouts/ # RootLayout (nav + Outlet) +│ ├── contexts/ # AuthContext (Firebase Auth) +│ └── server/ # Express API (roast, oráculo/match) +├── docs/ +│ └── ARCHITECTURE.md # Documentação técnica detalhada +├── .github/workflows/ # CI: typecheck + lint + build +├── firestore.rules # Regras de segurança do Firestore +├── vercel.json # Configuração de deploy +└── vite.config.ts # Vite + Vitest config +``` -| Documento | Propósito | -|-----------|-----------| -| `docs/VISION_MATCH_TECH.md` | Visão do produto, identidade visual, arquitetura, modelo de dados | -| `docs/FRONTEND_BLUEPRINT.md` | Blueprint técnico de implementação (o que manter, modificar, criar) | -| `docs/TODO_MATCH_TECH.md` | Roadmap com checklist de progresso | -| `docs/CODEBASE_MAP.md` | Mapa rápido de toda a codebase (arquivos, dependências, funções-chave) | +--- + +## Testes + +```bash +npm test # roda todos os testes +npx vitest run --reporter=verbose # com output detalhado +``` + +Testes unitários cobrem toda a camada de domínio: + +- `calculateCompatibility` — algoritmo de compatibilidade entre membros +- `scoreSkillsForRole` — pontuação de habilidades por role +- `filterMembers` — filtragem por role e status +- `sortByCompatibility` / `getTopCompatibleMembers` — ranking --- -## 🔒 Segurança +## CI/CD + +O GitHub Actions executa em todo PR e push para `main`: -O Firestore utiliza regras de segurança com validação de schema (`isValidMember`), verificação de `request.auth.uid` e controle de campos alteráveis. Veja `firestore.rules` para detalhes. +1. **typecheck** — `tsc --noEmit` +2. **lint** — ESLint com zero avisos +3. **build** — `vite build` + +O deploy é automático via **Vercel** ao fazer merge em `main`. --- -## 📜 Licença +## Segurança + +- Firestore usa regras com validação de schema (`isValidMember`), verificação de `request.auth.uid` e controle de campos alteráveis — veja `firestore.rules` +- API Gemini protegida por rate limiting: 5 req/min (roast), 10 req/min (match) +- Nenhuma chave de API é exposta no bundle do cliente + +--- + +## Contribuindo + +Contribuições são bem-vindas! Leia [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) para entender os padrões adotados antes de abrir um PR. + +1. Faça um fork e clone o repositório +2. Crie uma branch: `git checkout -b feat/minha-feature` +3. Faça suas alterações seguindo os padrões de código +4. Rode `npm run typecheck && npm run lint && npm test` +5. Abra um Pull Request descrevendo as mudanças + +--- -Projeto open-source criado para a comunidade do Hackathon Tech Floripa 2026. +## Licença Feito com ☕ por **Tony Max & Squad**. - \ No newline at end of file diff --git a/api/index.ts b/api/index.ts deleted file mode 100644 index d990385..0000000 --- a/api/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import express from "express"; -import { GoogleGenAI } from "@google/genai"; - -const app = express(); -app.use(express.json()); - -// Lazy-load Firebase Admin to avoid cold-start overhead on health checks -async function getAdminDb() { - const { db } = await import("../src/lib/firebase-admin.ts"); - return db; -} - -app.get("/api/health", (req, res) => { - res.json({ status: "ok" }); -}); - -app.post("/api/roast", async (req, res) => { - try { - const { memberId, memberData, persona } = req.body; - if (!memberId || !memberData) return res.status(400).json({ error: "Missing memberId or memberData" }); - - const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); - - let systemInstruction = 'Aja como um tech lead sênior sarcástico, brutal e extremamente exigente no meio de um hackathon. Analise as skills e os inputs deste membro. Critique sem dó suas piores habilidades, faça piada onde ele diz que "se garante", e traga realismo se os vetos ("nem fudendo") forem exatamente o que precisamos. NÃO seja polido. Seja irônico e direto. Máximo de 3 parágrafos.'; - - if (persona === 'mild') { - systemInstruction = 'Aja como um mentor técnico experiente, paciente e encorajador. Analise as habilidades e inputs (paixões, opero bem e vetos) deste membro de forma construtiva. Destaque seus pontos fortes e dê conselhos gentis sobre como melhorar nas áreas mais fracas e como aproveitar aquilo que operam bem. Seja inspirador e amigável. Máximo de 3 parágrafos.'; - } - - const response = await ai.models.generateContent({ - model: "gemini-2.5-flash", - contents: `Analise este membro. DADOS DO MEMBRO: \n${JSON.stringify(memberData, null, 2)}`, - config: { - systemInstruction - } - }); - const roastText = response.text; - - // Persist roast to Firestore via Admin SDK - const adminDb = await getAdminDb(); - const updateData: Record = { updatedAt: new Date() }; - if (persona === 'brutal') { - updateData.roastBrutal = roastText; - } else if (persona === 'mild') { - updateData.roastMild = roastText; - } else { - updateData.roast = roastText; - } - await adminDb.collection("profiles").doc(memberId).update(updateData); - - res.json({ roast: roastText }); - } catch (e: any) { - console.error(e); - const errorMessage = e.message?.includes('API key not valid') - ? "Chave da API do Gemini inválida ou não configurada. Por favor, adicione uma chave válida no painel de configurações." - : e.message; - res.status(500).json({ error: errorMessage }); - } -}); - -app.post("/api/oraculo/match", async (req, res) => { - try { - const { challengeDesc, members } = req.body; - const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); - - const response = await ai.models.generateContent({ - model: "gemini-2.5-flash", - contents: `Contexto do Hackathon Tech Floripa:\n${challengeDesc}\n\nMembros da Equipe:\n${JSON.stringify(members, null, 2)}`, - config: { - responseMimeType: "application/json", - systemInstruction: `Sua Tarefa (A Inteligência do Oráculo): -Gere três opções estratégicas de projetos para o hackathon: -1. Uma Escolha Segura (Viabilidade altíssima, risco baixo, foco no que a equipe domina). -2. Uma Escolha de Inovação (Viabilidade média, risco alto, usa as vontades/paixões da equipe em coisas novas). -3. A Carta na Manga / Surpresa (Baixa viabilidade, altíssimo risco operacional, inovação louca arrastando as pessoas pro limite). - -IMPORTANTE: -Para cada estratégia, indique o nível de "Match" com a equipe em porcentagem e liste precisamente quais membros estarão alocados nela (nunca aloque alguém no que eles deram 'veto'). - -Responda OBRIGATORIAMENTE em JSON no formato: -{ - "seguro": { "title": "STRING", "match": "NUMBER", "reason": "STRING", "allocation": "STRING", "viability": "STRING", "risk": "STRING", "banca": "STRING" }, - "inovacao": { "title": "STRING", "match": "NUMBER", "reason": "STRING", "allocation": "STRING", "viability": "STRING", "risk": "STRING", "banca": "STRING" }, - "surpresa": { "title": "STRING", "match": "NUMBER", "reason": "STRING", "allocation": "STRING", "viability": "STRING", "risk": "STRING", "banca": "STRING" } -}` - } - }); - - const responseText = response.text; - - try { - const parsed = JSON.parse(responseText || "{}"); - if (!parsed.seguro || !parsed.inovacao || !parsed.surpresa) { - throw new Error("Invalid schema from AI"); - } - res.json(parsed); - } catch (parseError) { - console.error("AI JSON Parse Error:", responseText); - res.status(500).json({ error: "O Oráculo alucinou um formato inválido. Tente novamente." }); - } - } catch (e: any) { - console.error(e); - const errorMessage = e.message?.includes('API key not valid') - ? "Chave da API do Gemini inválida ou não configurada. Por favor, adicione uma chave válida no painel de configurações." - : e.message; - res.status(500).json({ error: errorMessage }); - } -}); - -export default app; diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..dd1c503 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,20 @@ +services: + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + ports: + - "3000:3000" + environment: + - VITE_API_BASE_URL=http://backend:3001 + depends_on: + - backend + + backend: + build: + context: . + dockerfile: Dockerfile.backend + ports: + - "3001:3001" + environment: + - PORT=3001 \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..de88493 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,853 @@ +# Arquitetura do Match Tech + +Este documento descreve a arquitetura técnica do projeto, os padrões de design adotados, a organização de pastas e as convenções que devem ser seguidas ao contribuir. + +--- + +## Sumário + +1. [Visão Geral](#1-visão-geral) +2. [Diagrama de Camadas](#2-diagrama-de-camadas) +3. [Estrutura de Pastas Detalhada](#3-estrutura-de-pastas-detalhada) +4. [Camada de Domínio](#4-camada-de-domínio) +5. [Camada de Infraestrutura](#5-camada-de-infraestrutura) +6. [Camada de Features](#6-camada-de-features) +7. [Camada Shared](#7-camada-shared) +8. [Roteamento](#8-roteamento) +9. [API Server](#9-api-server) +10. [Fluxo de Dados](#10-fluxo-de-dados) +11. [Injeção de Dependência](#11-injeção-de-dependência) +12. [Gerenciamento de Estado — Zustand](#12-gerenciamento-de-estado--zustand) +13. [Server State — TanStack Query](#13-server-state--tanstack-query) +14. [Padrões e Convenções](#14-padrões-e-convenções) +15. [Testes](#15-testes) +16. [Deploy e CI/CD](#16-deploy-e-cicd) +17. [Docker e Containers](#17-docker-e-containers) +18. [Scripts Utilitários](#18-scripts-utilitários) + +--- + +## 1. Visão Geral + +O Match Tech é uma SPA (Single Page Application) React com um backend Express mínimo usado exclusivamente para chamadas à API do Gemini (IA). O frontend se comunica diretamente com o Firebase para autenticação e banco de dados. + +A arquitetura segue os princípios de **Clean Architecture**: + +- **Regras de negócio** ficam na camada de domínio, sem dependências externas +- **Detalhes de implementação** (Firebase, Gemini) ficam nas camadas externas +- **Features** são independentes entre si e dependem do domínio, nunca umas das outras +- **Inversão de dependência**: componentes dependem de interfaces, não de implementações concretas + +--- + +## 2. Diagrama de Camadas + +```text +┌─────────────────────────────────────────────────────────┐ +│ APRESENTAÇÃO │ +│ features/ routes/ layouts/ shared/components/ │ +└─────────────────────┬───────────────────────────────────┘ + │ usa +┌─────────────────────▼───────────────────────────────────┐ +│ APLICAÇÃO │ +│ shared/hooks/ shared/context/ │ +│ shared/services/ contexts/ │ +└─────────────────────┬───────────────────────────────────┘ + │ depende de interfaces de +┌─────────────────────▼───────────────────────────────────┐ +│ DOMÍNIO │ +│ domain/entities/ domain/ports/ domain/usecases/ │ +└─────────────────────┬───────────────────────────────────┘ + │ implementado por +┌─────────────────────▼───────────────────────────────────┐ +│ INFRAESTRUTURA │ +│ infrastructure/firebase/ │ +│ server/ (Express + Gemini) │ +└─────────────────────────────────────────────────────────┘ +``` + +A regra fundamental: **as setas de dependência apontam sempre para dentro** — o domínio não conhece nenhuma camada externa. + +--- + +## 3. Estrutura de Pastas Detalhada + +```text +src/ +│ +├── domain/ ← Núcleo da aplicação (sem imports externos) +│ ├── entities/ +│ │ ├── Member.ts # Tipo Member e sub-interfaces (ISP) +│ │ ├── Squad.ts # Tipo Squad e SquadMember +│ │ └── Shared.ts # RoastPersona, Tag, SkillRadar, SquadStatus +│ ├── ports/ +│ │ ├── IProfileRepository.ts # Interface do repositório de perfis +│ │ ├── ISquadRepository.ts # Interface do repositório de squads +│ │ ├── IAuthService.ts # Interface do serviço de autenticação +│ │ └── IRoastService.ts # Interface do serviço de roast/IA +│ └── usecases/ +│ ├── compatibilityAlgorithm.ts # calculateCompatibility, scoreSkillsForRole, etc. +│ └── __tests__/ +│ └── compatibilityAlgorithm.test.ts +│ +├── infrastructure/ ← Implementações concretas +│ └── firebase/ +│ ├── profileRepository.ts # FirebaseProfileRepository implements IProfileRepository +│ ├── squadRepository.ts # FirebaseSquadRepository implements ISquadRepository +│ ├── schemas.ts # Schemas Zod para validação em runtime +│ └── index.ts # Barrel export +│ +├── features/ ← Módulos de produto (cada um é independente) +│ ├── discover/ +│ │ ├── components/ # ProfilesGrid, RoastModal, DiscoverFilters... +│ │ ├── hooks/ # useProfilesRealtime, useDiscoverFilters, useRoastProfile +│ │ ├── model/ # discover.types.ts, discover.selectors.ts +│ │ ├── services/ # discover.repository.ts (updateProfile) +│ │ ├── constants/ +│ │ └── pages/ +│ │ └── DiscoverPage.tsx +│ ├── guilda/ +│ │ ├── components/ # GuildMembersGrid, GuildRoastModal, GuildMemberCard... +│ │ ├── hooks/ # useGuildMembersRealtime, useGuildRoast +│ │ ├── model/ # guilda.types.ts, guilda.selectors.ts +│ │ ├── services/ # guilda.repository.ts (saveRoast) +│ │ ├── constants/ +│ │ ├── utils/ +│ │ └── pages/ +│ │ └── GuildaPage.tsx +│ ├── onboarding/ +│ │ ├── components/ +│ │ │ └── AuthGate/ # LoginScreen, MagicLinkSentScreen, CompletingMagicLink... +│ │ ├── hooks/ +│ │ │ └── useOnboardingForm.ts +│ │ ├── constants/ +│ │ └── pages/ +│ │ └── Onboarding.tsx +│ ├── profile/ +│ │ ├── components/ +│ │ │ └── ShareProfileButton.tsx # Exportação PNG via canvas +│ │ └── pages/ +│ │ └── PublicProfilePage.tsx # Rota pública /p/:uid +│ ├── squad/ +│ │ └── pages/ +│ │ └── JoinSquadPage.tsx # Rota de convite /join/:squadId +│ └── landing/ +│ └── Landing.tsx +│ +├── shared/ ← Código reutilizável entre features +│ ├── components/ +│ │ ├── ui/ # Componentes puramente apresentacionais +│ │ │ ├── RoastModal.tsx # Modal genérico de veredito (usado em discover e guilda) +│ │ │ ├── SkillRadar.tsx # Radar chart memoizado +│ │ │ ├── ProfileCard.tsx # Card de perfil +│ │ │ ├── Avatar.tsx +│ │ │ ├── Button.tsx +│ │ │ ├── Card.tsx +│ │ │ ├── ErrorBoundary.tsx +│ │ │ └── ... +│ │ └── states/ # Estados de UI (loading, empty, error) +│ ├── hooks/ +│ │ ├── useFirestoreSubscription.ts # Hook genérico para onSnapshot em tempo real +│ │ └── index.ts +│ ├── context/ +│ │ └── RepositoryContext.tsx # RepositoryProvider + useRepositories() +│ ├── lib/ +│ │ ├── firebase/ +│ │ │ ├── firebase.client.ts # Inicialização do Firebase SDK +│ │ │ └── firebase.config.ts +│ │ ├── logger/ # Logger estruturado +│ │ ├── utils/ # cn(), entity helpers +│ │ └── AppError.ts # Classe AppError + códigos + mensagens PT-BR +│ └── services/ +│ └── roast.service.ts # Cliente HTTP para POST /api/roast +│ +├── routes/ ← Configuração de roteamento +│ ├── routes.tsx # createBrowserRouter com todas as rotas +│ ├── ProtectedLayout.tsx # Re-exporta RootLayout como Component lazy +│ └── ErrorPage.tsx # Página de erro de rota (trata AppError) +│ +├── layouts/ +│ └── RootLayout.tsx # Shell da aplicação: nav + Outlet + modal de logout +│ +├── contexts/ +│ └── AuthContext.tsx # Firebase Auth (Google OAuth + Magic Link) +│ +├── server/ ← API Express (backend serverless no Vercel) +│ ├── app.ts # Instância Express +│ ├── routes/ +│ │ └── index.ts # Registro das rotas +│ ├── features/ +│ │ ├── roast/ # POST /api/roast — análise Gemini individual +│ │ │ ├── roast.controller.ts +│ │ │ ├── roast.service.ts +│ │ │ ├── roast.repository.ts +│ │ │ ├── roast.routes.ts # Rate limit: 5 req/min +│ │ │ ├── roast.prompts.ts +│ │ │ └── roast.types.ts +│ │ └── oraculo/ # POST /api/oraculo/match — match por IA +│ │ ├── match.controller.ts +│ │ ├── match.service.ts +│ │ ├── match.routes.ts # Rate limit: 10 req/min +│ │ ├── match.prompts.ts +│ │ └── match.types.ts +│ └── shared/ +│ ├── lib/ +│ │ ├── gemini.server.ts # Cliente Gemini (gemini-2.5-flash) +│ │ └── firebase-admin.server.ts +│ └── utils/ +│ └── async-handler.ts +│ +├── App.tsx # QueryClientProvider + AuthProvider + RouterProvider +└── main.tsx # createRoot + global error listeners +``` + +--- + +## 4. Camada de Domínio + +A camada de domínio contém as regras de negócio puras. **Nenhum arquivo aqui pode importar Firebase, React, ou qualquer lib externa.** + +### Entidades (`domain/entities/`) + +As entidades seguem o **Interface Segregation Principle (ISP)**: em vez de um tipo `Member` gigante, temos interfaces menores que são compostas: + +```typescript +// Member é a composição de interfaces focadas +type Member = MemberIdentity // uid, displayName, photoURL, bio + & MemberRoles // role, secondaryRoles[] + & MemberSkills // skills: SkillRadar + & MemberTags // tags: Tag[] + & MemberSquadStatus // squadId?, squadStatus + & { createdAt, updatedAt, visibility } + +// PublicMember expõe apenas o que é seguro tornar público +type PublicMember = MemberIdentity & MemberRoles & { visibility: "public"; tags?: Tag[] } +``` + +Isso permite que componentes recebam apenas as interfaces que precisam (`ProfileCardData = MemberIdentity & MemberRoles`), sem carregar campos desnecessários. + +### Ports (`domain/ports/`) + +Ports são interfaces TypeScript que definem **contratos** entre o domínio e a infraestrutura: + +```typescript +interface IProfileRepository { + getProfile(uid: string): Promise + getPublicProfile(uid: string): Promise + updateProfile(uid: string, data: Partial): Promise + listPublicProfiles(filters?: ProfileFilters): Promise + deleteProfile(uid: string): Promise +} +``` + +O domínio só conhece essa interface. A implementação concreta (`FirebaseProfileRepository`) fica na infraestrutura e pode ser trocada (ex: para um mock em testes) sem mudar nada no domínio ou nas features. + +### Usecases (`domain/usecases/`) + +Funções puras com a lógica de negócio central: + +| Função | Descrição | +|--------|-----------| +| `calculateCompatibility(m1, m2)` | Score 0–90: 40% overlap de skills + 40% compatibilidade de tags + 10% bônus de roles diferentes | +| `scoreSkillsForRole(skills, role)` | Pontua skills de 0–100 com pesos específicos por role | +| `filterMembers(members, role?, status?)` | Filtra lista de membros por role e/ou squadStatus | +| `sortByCompatibility(members, target)` | Ordena lista pelo score de compatibilidade com o membro alvo | +| `getTopCompatibleMembers(members, target, limit)` | Retorna os N membros mais compatíveis | + +**Algoritmo de compatibilidade em detalhe:** + +```text +calculateCompatibility(m1, m2): + skillOverlap = 100 - (Σ|skill_i_m1 - skill_i_m2| / maxDiff) * 100 + tagCompat = 100 - penalidades (love↔veto = -20, veto↔veto = -5) → mínimo 0 + roleBonus = m1.role ≠ m2.role ? 10 : 0 + score = Math.round(skillOverlap * 0.4 + tagCompat * 0.4 + roleBonus) + range = [0, 90] +``` + +--- + +## 5. Camada de Infraestrutura + +Implementa as interfaces do domínio usando Firebase e Zod. + +### Repositórios (`infrastructure/firebase/`) + +Cada repositório: + +1. Implementa a interface correspondente do domínio (`implements IProfileRepository`) +2. Usa `MemberSchema.parse(snap.data())` para validar dados do Firestore com Zod +3. Lança `AppError` com códigos semânticos em caso de erro (`PROFILE_NOT_FOUND`, `FIRESTORE_UNAVAILABLE`, etc.) +4. Exporta uma instância singleton (`export const profileRepository = new FirebaseProfileRepository()`) + +### Schemas Zod (`infrastructure/firebase/schemas.ts`) + +Schemas Zod validam todos os dados vindos do Firestore em runtime, protegendo contra drift de schema: + +```typescript +const MemberSchema = z.object({ + uid: z.string(), + displayName: z.string(), + skills: SkillRadarSchema, + tags: z.array(TagSchema).min(10), + squadStatus: z.enum(["open", "looking", "closed"]).default("open"), + // ... +}) +``` + +Se um documento do Firestore não corresponder ao schema, um `ZodError` é lançado e convertido em `AppError("FIRESTORE_UNAVAILABLE")`. + +--- + +## 6. Camada de Features + +Cada feature é um módulo independente organizado por domínio de produto. A estrutura interna padrão de uma feature é: + +```text +features/nome-da-feature/ +├── components/ # Componentes React específicos desta feature +├── hooks/ # Custom hooks (estado, side effects, acesso a dados) +├── model/ # Types locais e funções de seleção/transformação +├── services/ # Chamadas diretas ao Firestore (operações de escrita) +├── constants/ # Constantes estáticas (listas de roles, categorias de tags) +└── pages/ # Componentes de página (montados pelo roteador) +``` + +### Regras importantes + +- **Features não importam de outras features.** Se um componente precisa ser compartilhado, ele vai para `shared/`. +- **Dados em tempo real** usam `useFirestoreSubscription` da camada shared. +- **Operações de escrita** ficam nos `services/` da feature (ex: `updateProfile`, `saveRoast`). +- **Estado de UI** fica nos `hooks/` (ex: `useDiscoverFilters`, `useGuildRoast`). + +--- + +## 7. Camada Shared + +### `shared/components/ui/` + +Componentes **puramente apresentacionais** — sem fetching de dados, sem efeitos colaterais de negócio: + +- Recebem dados via props +- Emitem eventos via callbacks +- Aceitam `className` para override de estilo +- São testáveis de forma isolada + +Exemplo: `RoastModal` — modal genérico de veredito usado em `discover` e `guilda`. A lógica específica de cada feature fica nos hooks, o modal apenas renderiza o que recebe. + +### `shared/hooks/useFirestoreSubscription` + +Hook genérico que elimina a duplicação de subscriptions em tempo real: + +```typescript +const { data, loading, error } = useFirestoreSubscription({ + collectionName: "profiles", + constraints: [where("visibility", "==", "public")], // opcional +}) +``` + +Gerencia automaticamente: subscription/cleanup, loading state, error state, unmount seguro. + +### `shared/lib/AppError.ts` + +Classe de erro centralizada com: + +- Códigos semânticos (`PROFILE_NOT_FOUND`, `UNAUTHORIZED`, `ANALYSIS_FAILED`...) +- Mapeamento para HTTP status codes +- Mensagens em português via `getUserErrorMessage(code)` +- Função `isAppError(error)` para type narrowing + +### `shared/context/RepositoryContext.tsx` + +Implementa o **Dependency Inversion Principle (DIP)**: + +```tsx +// Fornece implementações Firebase por padrão + + {children} + + +// Em testes, injeta mocks + + {children} + + +// Nos componentes: +const { profileRepo, squadRepo } = useRepositories() +``` + +--- + +## 8. Roteamento + +O projeto usa React Router v7 em **framework mode** (`createBrowserRouter` + `RouterProvider`), não library mode. + +### Hierarquia de rotas + +```text +/ ← Shell (RootLayout) — nav + Outlet +├── / Landing (público) +├── /onboarding Cadastro de perfil (público — callback do magic link) +├── /discover Lista de perfis (requireAuth) +├── /guilda Gestão da guilda (requireAuth) +├── /p/:uid Perfil público (sem auth — loader: getPublicProfile) +└── /join/:squadId Convite de squad (sem auth — loader: getSquad) +``` + +### Auth guard (`requireAuth`) + +Loader assíncrono que roda antes de qualquer rota protegida: + +```typescript +async function requireAuth() { + await auth.authStateReady() // aguarda Firebase resolver persistência + if (!auth.currentUser) throw redirect("/") + return null +} +``` + +`/onboarding` é **intencionalmente** deixado sem guard: é a URL de callback do magic link, onde o Firebase conclui a autenticação após o carregamento da página. + +### Lazy loading + +Todas as páginas são carregadas de forma lazy. O build gera um chunk separado por página: + +```text +ProtectedLayout-*.js ~8 kB +Landing-*.js ~9 kB +DiscoverPage-*.js ~19 kB +GuildaPage-*.js ~18 kB +Onboarding-*.js ~40 kB +``` + +### Tratamento de erros de rota + +O `ErrorPage` captura erros de loaders (ex: `PROFILE_NOT_FOUND`, `SQUAD_NOT_FOUND`) e exibe mensagens amigáveis em português via `getUserErrorMessage()`. + +--- + +## 9. API Server + +O backend é uma instância Express mínima, usada exclusivamente para ocultar as chaves da API do Gemini do bundle do cliente. + +### Endpoints + +| Método | Rota | Rate Limit | Descrição | +|--------|------|-----------|-----------| +| `GET` | `/api/health` | — | Status check | +| `POST` | `/api/roast` | 5 req/min | Gera análise individual (brutal ou suave) via Gemini | +| `POST` | `/api/oraculo/match` | 10 req/min | Gera sugestões de match via Gemini | + +### Deploy no Vercel + +```json +{ + "rewrites": [{ "source": "/api/(.*)", "destination": "/api/index.ts" }] +} +``` + +Toda requisição para `/api/*` é encaminhada para a serverless function em `/api/index.ts`. O restante é servido como SPA estática via CDN do Vercel. + +--- + +## 10. Fluxo de Dados + +### Leitura em tempo real (Discover / Guilda) + +```text +Firestore + └─ onSnapshot("profiles") + └─ useFirestoreSubscription + └─ useProfilesRealtime(currentUserId) + ├─ sortProfiles(data, currentUserId) via useMemo + └─ useDiscoverFilters(profiles) + └─ ProfilesGrid (virtualizado) → ProfileCard +``` + +### Geração de roast + +```text +Usuário clica em "Gerar Sina" + └─ useRoastProfile.executeRoast(profile, persona) + ├─ verifica se já existe roast em cache (profile.roastBrutal / roastMild) + ├─ se não existe → POST /api/roast (shared/services/roast.service) + │ └─ Express → Gemini API → resposta + ├─ updateProfile(profile.id, { roastBrutal: text }) → Firestore + └─ setSelectedProfile({ ...profile, roastBrutal: text }) + └─ RoastModal re-renderiza com o novo texto +``` + +### Perfil público + +```text +Usuário acessa /p/abc123 + └─ Route loader: profileRepository.getPublicProfile("abc123") + ├─ getDoc(db, "members", "abc123") + ├─ verifica visibility === "public" (AppError UNAUTHORIZED se não for) + └─ PublicMemberSchema.parse(data) + └─ PublicProfilePage recebe dados via useLoaderData() +``` + +### Convite de squad + +```text +Usuário acessa /join/squad456 + └─ Route loader: squadRepository.getSquad("squad456") + └─ JoinSquadPage recebe Squad via useLoaderData() + ├─ Não autenticado → salva "pendingJoin" no sessionStorage → /onboarding + └─ Autenticado → squadRepo.addMemberToSquad(id, uid) → /guilda +``` + +--- + +## 11. Injeção de Dependência + +O projeto usa **Context API** como container de DI, sem libs externas: + +```text +App.tsx +└─ QueryClientProvider ← TanStack Query (server state cache) + └─ AuthProvider ← Firebase Auth state + └─ RepositoryProvider ← Repositórios injetados + └─ RouterProvider ← Páginas têm acesso a tudo acima +``` + +Para trocar implementações em testes de integração: + +```tsx +render( + + + +) +``` + +--- + +## 12. Gerenciamento de Estado — Zustand + +O projeto usa **Zustand** para estado de UI que precisa: + +- Sobreviver entre re-renders sem re-montar +- Ser acessado por múltiplos componentes dentro da mesma feature sem prop drilling +- Ser persistido durante navegação (ex: filtros da Discover) + +### Quando usar Zustand + +| Use Zustand quando... | Use `useState` quando... | +| --- | --- | +| O estado é compartilhado entre 2+ componentes | O estado é local a um único componente | +| Você quer que persista durante a navegação | O estado pode ser resetado ao desmontar | +| O estado tem múltiplos campos relacionados | O estado é um único valor simples | + +### Stores existentes + +**`features/discover/store/discoverFilters.ts`** + +Substitui 4 `useState` em `useDiscoverFilters`. Os filtros persistem quando o usuário navega para outra página e volta. + +```typescript +const useDiscoverFiltersStore = create((set) => ({ + searchQuery: "", + selectedRole: "ALL", + selectedStatus: "ALL", + selectedTag: "", + setSearchQuery: (searchQuery) => set({ searchQuery }), + // ... +})); +``` + +**`features/guilda/store/guildRoast.ts`** + +Centraliza o estado complexo do roast da guilda: `selectedMember`, `roastActiveMember`, `roastStep`, `roastLogs`, `activePersonaView`. Os hooks que precisam desses valores (ex: `GuildMemberCard` e `GuildRoastModal`) leem do store sem prop drilling. + +### Convenção de criação de stores + +Stores ficam em `features//store/.ts`. Cada store exporta um único hook (`useXxxStore`) criado com `create()`. Actions (setters) ficam dentro do próprio store — não use `set` em componentes: + +```typescript +// ✅ correto — action encapsulada no store +setSearchQuery: (searchQuery) => set({ searchQuery }), + +// ❌ evitar — chamar set diretamente no componente +useDiscoverFiltersStore.setState({ searchQuery: "..." }) +``` + +--- + +## 13. Server State — TanStack Query + +**TanStack Query** (`@tanstack/react-query`) é usado para gerenciar chamadas à API — específicamente as requisições à IA (roast e match). Já está instalado e `QueryClientProvider` está configurado em `App.tsx`. + +### Quando usar TanStack Query + +| Use `useMutation` quando... | Use fetch manual quando... | +| --- | --- | +| Chama a API e precisa de loading/error/success | Nunca — prefira sempre `useMutation` | +| Quer retry automático | — | +| Quer callbacks `onSuccess`, `onError`, `onSettled` | — | + +Dados em **tempo real do Firestore** usam `useFirestoreSubscription` (push-based, `onSnapshot`), **não** `useQuery`. TanStack Query é para chamadas HTTP pontuais. + +### Onde está sendo usado + +**`features/discover/hooks/useRoastProfile.ts`** — substitui `isGenerating` state + try/catch manual: + +```typescript +const roastMutation = useMutation({ + mutationFn: ({ profile, persona }) => + requestRoast({ memberId: profile.id, memberData: profile, persona }), + onSuccess: async (data, { profile, persona }) => { /* salva e atualiza */ }, + onError: (error) => { showToast("Sem conexão com o servidor de IA...") }, +}); + +// isGenerating era um useState manual — agora é derivado: +isGenerating: roastMutation.isPending, +``` + +**`features/guilda/hooks/useGuildRoast.ts`** — mesma abordagem com callbacks `onMutate` (start animation), `onSuccess` (save), `onSettled` (cleanup): + +```typescript +const roastMutation = useMutation({ + mutationFn: ({ member, persona }) => requestRoast({ ... }), + onMutate: ({ persona }) => { startLogs(persona) }, + onSuccess: async (data, { member, persona }) => { await saveRoast(...) }, + onSettled: () => { clearLogsInterval(); store.setRoastStep(null) }, +}); +``` + +### Configuração global + +```typescript +// App.tsx +const queryClient = new QueryClient({ + defaultOptions: { + queries: { staleTime: 1000 * 60 * 5, retry: 2 }, + }, +}); +``` + +`ReactQueryDevtools` é renderizado apenas em `import.meta.env.DEV`. + +--- + +## 14. Padrões e Convenções + +### Importações + +O alias `@/` aponta para `src/`. Sempre use o alias, nunca caminhos relativos que atravessam mais de uma pasta: + +```typescript +// ✅ correto +import { AppError } from "@/shared/lib/AppError" +import type { Member } from "@/domain/entities/Member" + +// ❌ evitar +import { AppError } from "../../../shared/lib/AppError" +``` + +Ordem de importações (enforçada pelo ESLint): + +1. Pacotes externos (`react`, `firebase`, `motion/react`...) +2. Imports internos com `@/` (domínio → infraestrutura → shared → features) +3. Imports relativos do próprio módulo + +### Componentes + +- **Componentes de página** têm `export default` e ficam em `pages/` +- **Componentes de UI** têm `export function` nomeado e ficam em `components/` +- **Lazy routes** exportam `default` para que o `.then(m => ({ Component: m.default }))` funcione +- Componentes puramente apresentacionais não fazem fetch de dados + +### Hooks + +- Hooks que gerenciam dados externos usam `useFirestoreSubscription` da camada shared +- Sorting e transformações de dados ficam em `useMemo` dentro do hook, não no `sortFn` do subscription (evita re-triggers infinitos) + +### Erros + +Sempre lance `AppError` com código semântico. Nunca `throw new Error("alguma coisa")` em código de produção: + +```typescript +// ✅ +throw new AppError("PROFILE_NOT_FOUND", uid) + +// ❌ +throw new Error("perfil não encontrado") +``` + +### Estilo + +O projeto usa **Tailwind CSS v4** com tema neo-brutalista. As classes de tema principais: + +| Classe | Cor / Uso | +|--------|-----------| +| `bg-neo-black` / `text-neo-black` | Preto `#0a0a0a` — texto e bordas principais | +| `bg-neo-lime` | Verde limão `#B8FF29` — destaque primário | +| `bg-neo-pink` | Rosa `#FF2E93` — alertas e ações destrutivas | +| `bg-neo-cyan` | Ciano `#00E5FF` — ações secundárias | +| `bg-neo-bg` | Fundo `#f5f5f0` — fundo padrão da app | +| `shadow-[4px_4px_0_0_#000]` | Sombra neo-brutalista deslocada | +| `border-[3px] border-neo-black` | Borda grossa padrão | +| `font-heading` | Fonte display (títulos em caixa alta) | + +--- + +## 15. Testes + +### Configuração + +Vitest está configurado no `vite.config.ts` com `environment: "node"` e alias `@/` para que os testes importem tipos de domínio normalmente. + +### O que testar + +| Camada | O que testar | Ferramenta | +|--------|-------------|-----------| +| `domain/usecases/` | Funções puras (compatibilidade, filtros, ranking) | Vitest | +| `infrastructure/firebase/` | Repositórios contra Firestore Emulator | Vitest + Firebase Emulator | +| `features/*/hooks/` | Lógica de estado com mocks de repositório | Vitest + Testing Library | +| `shared/components/ui/` | Renderização e interação | Vitest + Testing Library | + +### Rodando os testes + +```bash +npm test # modo watch +npx vitest run # execução única +npx vitest run --reporter=verbose # com detalhes +``` + +### Estrutura de um arquivo de teste + +```typescript +// src/domain/usecases/__tests__/minhaFuncao.test.ts +import { describe, it, expect } from "vitest" +import { minhaFuncao } from "../minhaFuncao" + +// Helper para criar fixtures +function criarMembro(overrides: Partial = {}): Member { + return { /* valores padrão */ ...overrides } +} + +describe("minhaFuncao", () => { + it("descrição do comportamento esperado", () => { + expect(minhaFuncao(criarMembro())).toBe(valorEsperado) + }) +}) +``` + +--- + +## 16. Deploy e CI/CD + +### Pipeline de CI (GitHub Actions) + +Executa em todo PR e push para `main`: + +```yaml +typecheck → lint (zero avisos) → build +``` + +O build falha se qualquer etapa falhar, bloqueando o merge. + +### Deploy (Vercel) + +O deploy é automático ao fazer merge em `main`: + +1. Vercel detecta o push +2. Roda `npm run build` → gera `dist/` +3. Assets estáticos servidos via CDN global +4. Funções serverless em `/api/index.ts` servem as rotas `/api/*` + +### Variáveis de ambiente no Vercel + +Configure no painel do projeto em Vercel > Settings > Environment Variables: + +| Variável | Obrigatória | Descrição | +|----------|------------|-----------| +| `GEMINI_API_KEY` | ✅ Sim | Chave da API do Google Gemini | +| `APP_URL` | ✅ Sim | URL do app em produção (ex: `https://matchtech-sooty.vercel.app`) | + +As chaves do Firebase ficam em `firebase-applet-config.json` (commitado, sem dados sensíveis) e são lidas pelo cliente diretamente. + +--- + +## 17. Docker e Containers + +O projeto inclui suporte a containers para facilitar o setup local sem depender de instalações globais e para padronizar o ambiente entre desenvolvedores. + +### `docker-compose.yaml` + +Orquestra dois serviços: + +| Serviço | Dockerfile | Porta | Variáveis de Ambiente | +|---------|-----------|-------|----------------------| +| `frontend` | `Dockerfile.frontend` | 3000 | `VITE_API_BASE_URL=http://backend:3001` | +| `backend` | `Dockerfile.backend` | 3001 | `PORT=3001` | + +O serviço `frontend` declara `depends_on: backend`, garantindo que o Express suba antes do Vite tentar se comunicar com a API. A comunicação entre os containers ocorre pela rede interna do Compose (DNS por nome de serviço), de modo que o bundle do cliente nunca expõe o endereço real do backend. + +```bash +docker compose up --build # sobe tudo +docker compose down # encerra e remove os containers +``` + +### `Dockerfile.backend` + +Imagem simples baseada em `node:22-bookworm`. Instala todas as dependências (incluindo `devDependencies`, necessárias para o `tsx`) e executa o servidor Express diretamente com `npx tsx src/server/server.ts` — sem etapa de transpilação prévia. Isso mantém o startup rápido e o ciclo de debug simples em ambientes de staging. + +``` +node:22-bookworm + └── npm ci --include=dev + └── COPY src/ + tsconfig.json + └── EXPOSE 3001 + └── CMD npx tsx src/server/server.ts +``` + +### `Dockerfile.frontend` + +Multi-stage build para minimizar o tamanho da imagem final: + +- **Stage `builder`** — instala deps e roda `npm run build` para gerar `dist/` +- **Stage `runner`** — parte de uma imagem limpa, copia apenas o `dist/` gerado e instala deps de dev (necessárias para `vite preview`), servindo o build via `vite preview --host 0.0.0.0 --port 3000` + +``` +builder: node:22-bookworm → npm ci → npm run build → dist/ +runner: node:22-bookworm → npm ci --include=dev → COPY dist/ → vite preview +``` + +> **Por que `vite preview` e não Nginx?** O projeto não requer um servidor estático dedicado — `vite preview` é suficiente para staging/demo. Para produção real, prefira Nginx ou o deploy direto na Vercel. + +--- + +## 18. Scripts Utilitários + +Scripts avulsos que operam fora do ciclo de build/teste normal ficam em `scripts/`. + +### `scripts/migrate-members-to-profiles.ts` + +Migração one-time da coleção Firestore `members` → `profiles`. O projeto originalmente usava `members`; `profiles` é o destino definitivo com schema expandido. + +**Comportamento:** + +- Por padrão roda em **dry run** — apenas exibe o que seria migrado, sem escrever nada. +- Documentos já existentes em `profiles` **não são sobrescritos** (safe to re-run). +- A coleção `members` não é modificada. +- Erros por documento são reportados individualmente sem interromper o batch. + +**Uso:** + +```bash +# Dry run (padrão): +npx tsx scripts/migrate-members-to-profiles.ts + +# Executar de fato: +npx tsx scripts/migrate-members-to-profiles.ts --execute + +# Via npm script: +npm run migrate:members-to-profiles -- --execute +``` + +**Pré-requisito:** a variável `FIREBASE_SERVICE_ACCOUNT` deve conter o JSON da service account do Firebase Admin, ou `GOOGLE_APPLICATION_CREDENTIALS` deve apontar para o arquivo JSON equivalente. diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md index 3425479..c806b3b 100644 --- a/docs/CODEBASE_MAP.md +++ b/docs/CODEBASE_MAP.md @@ -1,216 +1,281 @@ -# 🗺️ MAPA DO CÓDIGO ATUAL: Referência Rápida +# Mapa do Código — Match Tech -**Última Atualização:** 07 de Maio de 2026 - -> **PARA O AGENTE DE IA:** Consulte este arquivo quando precisar localizar -> rapidamente um arquivo, entender uma dependência, ou saber qual código -> reutilizar. Este é um SNAPSHOT do estado ATUAL (Neo-Brutalismo, pré-Fase 1). +> **Última atualização:** Junho 2026 — Refatoração Clean Architecture (Fases 0–7) > -> **LEMBRETE:** O design é Neo-Brutalismo (fundo claro, bordas grossas, neon). -> NÃO é dark mode. +> Este documento é um guia de navegação rápida pela codebase atual. +> Para a explicação completa da arquitetura e dos padrões, consulte [`ARCHITECTURE.md`](./ARCHITECTURE.md). --- ## Estrutura de Diretórios -``` -d:\estudos\Hackathon match tech\ -├── .env.example # Template de variáveis de ambiente -├── .gitignore -├── firebase-applet-config.json # Config do Firebase (chaves reais) -├── firebase-blueprint.json # Blueprint do projeto Firebase -├── firestore.rules # Regras de segurança do Firestore -├── index.html # Entry point HTML (Vite) -├── metadata.json -├── package.json # Dependencies e scripts -├── server.ts # Express server + API routes (Gemini) -├── tsconfig.json -├── vercel.json # Config de deploy Vercel -├── vite.config.ts # Vite + Tailwind v4 + PWA -├── docs/ # Documentação do projeto -│ ├── CODEBASE_MAP.md -│ ├── FRONTEND_BLUEPRINT.md -│ ├── TODO_MATCH_TECH.md -│ ├── VISION_MATCH_TECH.md -│ └── hackathon_tech_floripa_2026_strategy.md -│ -├── public/ # Assets estáticos +```text +match-tech/ │ ├── src/ -│ ├── main.tsx # React entry point (createRoot) -│ ├── App.tsx # Router + providers (Auth, ErrorBoundary) -│ ├── index.css # Design System NEO-BRUTALISTA (NÃO TOCAR) │ │ -│ ├── components/ -│ │ ├── ErrorBoundary.tsx # Error boundary genérico -│ │ └── ui/ -│ │ ├── Button.tsx # Botão Neo-Brutalista (accent-lime, pink, etc) -│ │ ├── Card.tsx # Card Neo-Brutalista (lime, pink, yellow, cyan) -│ │ └── PostModal.tsx # Modal de posts da timeline [REMOVER] +│ ├── domain/ ← Núcleo — zero dependências externas +│ │ ├── entities/ +│ │ │ ├── Member.ts # Tipo Member (ISP: MemberIdentity, MemberRoles, MemberSkills...) +│ │ │ ├── Squad.ts # Tipo Squad e SquadMember +│ │ │ └── Shared.ts # RoastPersona, Tag, SkillRadar, SquadStatus +│ │ ├── ports/ +│ │ │ ├── IProfileRepository.ts # Contrato de acesso a perfis +│ │ │ ├── ISquadRepository.ts # Contrato de acesso a squads +│ │ │ ├── IAuthService.ts # Contrato de autenticação +│ │ │ └── IRoastService.ts # Contrato de geração de roast +│ │ └── usecases/ +│ │ ├── compatibilityAlgorithm.ts # calculateCompatibility, scoreSkillsForRole, filterMembers... +│ │ └── __tests__/ +│ │ └── compatibilityAlgorithm.test.ts # 22 testes unitários │ │ -│ ├── contexts/ -│ │ └── AuthContext.tsx # Google Auth state (onAuthStateChanged) +│ ├── infrastructure/ +│ │ └── firebase/ +│ │ ├── profileRepository.ts # FirebaseProfileRepository implements IProfileRepository +│ │ ├── squadRepository.ts # FirebaseSquadRepository implements ISquadRepository +│ │ ├── schemas.ts # Schemas Zod (MemberSchema, SquadSchema, TagSchema...) +│ │ └── index.ts # Barrel export │ │ -│ ├── layouts/ -│ │ └── RootLayout.tsx # Navbar Neo-Brutalista + Outlet +│ ├── features/ ← Módulos de produto (cada um é independente) +│ │ ├── discover/ +│ │ │ ├── components/ # ProfilesGrid (virtualizado), RoastModal (adaptador), DiscoverFilters... +│ │ │ ├── hooks/ # useProfilesRealtime, useDiscoverFilters, useRoastProfile +│ │ │ ├── model/ # discover.types.ts (Profile, RoastPersona re-export), discover.selectors.ts +│ │ │ ├── services/ # discover.repository.ts (updateProfile) +│ │ │ ├── store/ +│ │ │ │ └── discoverFilters.ts # Zustand store — filtros persistentes (searchQuery, role, status, tag) +│ │ │ ├── constants/ +│ │ │ └── pages/ +│ │ │ └── DiscoverPage.tsx # Rota: /discover (auth obrigatória) +│ │ │ +│ │ ├── guilda/ +│ │ │ ├── components/ # GuildMembersGrid, GuildRoastModal (adaptador), GuildMemberCard... +│ │ │ ├── hooks/ # useGuildMembersRealtime, useGuildRoast (usa useMutation + store) +│ │ │ ├── model/ # guilda.types.ts (GuildMember, RoastPersona re-export) +│ │ │ ├── services/ # guilda.repository.ts (saveRoast) +│ │ │ ├── store/ +│ │ │ │ └── guildRoast.ts # Zustand store — estado do roast (selectedMember, roastStep, logs...) +│ │ │ ├── constants/ +│ │ │ ├── utils/ +│ │ │ └── pages/ +│ │ │ └── GuildaPage.tsx # Rota: /guilda (auth obrigatória) +│ │ │ +│ │ ├── onboarding/ +│ │ │ ├── components/ +│ │ │ │ ├── AuthGate/ # LoginScreen, MagicLinkSentScreen, CompletingMagicLink, MagicLinkConfirmScreen +│ │ │ │ ├── IdentityCard.tsx +│ │ │ │ ├── ClassSelector.tsx +│ │ │ │ ├── SkillSliders.tsx +│ │ │ │ ├── TagCategoryCard.tsx +│ │ │ │ ├── ArsenalCalibration.tsx +│ │ │ │ └── GuildPassport.tsx +│ │ │ ├── hooks/ +│ │ │ │ └── useOnboardingForm.ts # Form state + Firestore save/load +│ │ │ ├── constants/ +│ │ │ │ ├── roles.ts # ROLES_LIST +│ │ │ │ └── tagCategories.ts # TAG_CATEGORIES +│ │ │ └── pages/ +│ │ │ └── Onboarding.tsx # Rota: /onboarding (callback do magic link — sem auth guard) +│ │ │ +│ │ ├── profile/ +│ │ │ ├── components/ +│ │ │ │ └── ShareProfileButton.tsx # Canvas PNG 1200×630 +│ │ │ └── pages/ +│ │ │ └── PublicProfilePage.tsx # Rota pública: /p/:uid +│ │ │ +│ │ ├── squad/ +│ │ │ └── pages/ +│ │ │ └── JoinSquadPage.tsx # Rota de convite: /join/:squadId +│ │ │ +│ │ └── landing/ +│ │ └── Landing.tsx # Rota: / (pública) │ │ -│ ├── lib/ -│ │ ├── firebase.ts # initializeApp, getAuth, getFirestore -│ │ ├── firebase-admin.ts # Admin SDK (server-side Firestore) -│ │ ├── logger.ts # Logger utility -│ │ └── utils.ts # cn() utility (clsx + tailwind-merge) +│ ├── shared/ +│ │ ├── components/ +│ │ │ ├── ui/ +│ │ │ │ ├── RoastModal.tsx # Modal genérico de veredito (usado por discover e guilda) +│ │ │ │ ├── SkillRadar.tsx # Radar chart memoizado (React.memo + useMemo) +│ │ │ │ ├── ProfileCard.tsx # Card de perfil do feed discover +│ │ │ │ ├── Avatar.tsx # Avatar com fallback chain (Google → GitHub → iniciais) +│ │ │ │ ├── Button.tsx # Botão Neo-Brutalista +│ │ │ │ ├── Card.tsx # Card Neo-Brutalista +│ │ │ │ ├── StatusBadge.tsx +│ │ │ │ ├── TagBadge.tsx +│ │ │ │ └── ErrorBoundary.tsx +│ │ │ └── states/ # LoadingState, EmptyState, ErrorState +│ │ ├── hooks/ +│ │ │ ├── useFirestoreSubscription.ts # Hook genérico onSnapshot +│ │ │ └── index.ts +│ │ ├── context/ +│ │ │ └── RepositoryContext.tsx # RepositoryProvider + useRepositories() +│ │ ├── lib/ +│ │ │ ├── firebase/ +│ │ │ │ ├── firebase.client.ts # initializeApp, getAuth, getFirestore +│ │ │ │ └── firebase.config.ts # lê firebase-applet-config.json +│ │ │ ├── logger/ +│ │ │ │ └── logger.ts # Loggers por módulo (authLog, firestoreLog, apiLog...) +│ │ │ ├── utils/ +│ │ │ │ ├── cn.ts # cn() = clsx + tailwind-merge +│ │ │ │ └── entity.ts # sortByCurrentUserAndName() +│ │ │ └── AppError.ts # AppError class + códigos + getUserErrorMessage() PT-BR +│ │ └── services/ +│ │ └── roast.service.ts # POST /api/roast (cliente HTTP) │ │ -│ ├── pages/ -│ │ ├── Bunker.tsx # Dashboard com countdown [REMOVER → Landing] -│ │ ├── Guilda.tsx # Lista de membros [EVOLUIR → Discover] -│ │ ├── Logistica.tsx # Dashboard logística [REMOVER] -│ │ ├── Onboarding.tsx # Formulário de perfil [AJUSTAR scope] -│ │ └── Oraculo.tsx # IA Strategy Matcher [EVOLUIR → Squad AI] +│ ├── routes/ +│ │ ├── routes.tsx # createBrowserRouter — todas as rotas com lazy + loaders +│ │ ├── ProtectedLayout.tsx # Re-export de RootLayout como Component lazy +│ │ └── ErrorPage.tsx # Trata AppError + isRouteErrorResponse │ │ -│ ├── services/ -│ │ └── likesService.ts # Toggle like, subscribe to likes +│ ├── layouts/ +│ │ └── RootLayout.tsx # Nav + Outlet + logout modal +│ │ +│ ├── contexts/ +│ │ └── AuthContext.tsx # Firebase Auth: Google OAuth + Magic Link │ │ -│ └── utils/ -│ ├── timer.ts # calculateTimeLeft() [REMOVER] -│ └── timer.test.ts # Vitest tests [REMOVER] +│ ├── server/ ← Express API (backend serverless no Vercel) +│ │ ├── app.ts # Instância Express +│ │ ├── routes/ +│ │ │ └── index.ts # Registro: /api/health, /api/roast, /api/oraculo/match +│ │ ├── features/ +│ │ │ ├── roast/ # POST /api/roast — análise IA individual (5 req/min) +│ │ │ └── oraculo/ # POST /api/oraculo/match — match por IA (10 req/min) +│ │ └── shared/ +│ │ ├── lib/ +│ │ │ ├── gemini.server.ts # Cliente Gemini (gemini-2.5-flash) +│ │ │ └── firebase-admin.server.ts +│ │ └── utils/ +│ │ └── async-handler.ts +│ │ +│ ├── App.tsx # QueryClientProvider + AuthProvider + RepositoryProvider + RouterProvider +│ └── main.tsx # createRoot + global error listeners +│ +├── docs/ +│ ├── ARCHITECTURE.md ← Referência técnica principal (atualizada) +│ ├── CODEBASE_MAP.md ← Este arquivo +│ ├── VISION_MATCH_TECH.md ← Visão de produto e design system +│ ├── TODO_MATCH_TECH.md ← Histórico de desenvolvimento (arquivo) +│ ├── FRONTEND_BLUEPRINT.md ← Blueprint original (depreciado → ver ARCHITECTURE.md) +│ └── hackathon_tech_floripa_2026_strategy.md ← Estratégia do evento │ -├── README.md # Documentação pública (setup, stack, features) -└── docs/ # Pasta com toda a documentação técnica +├── .github/workflows/ +│ └── ci.yml # typecheck → lint → build (Node 22) +│ +├── scripts/ +│ └── migrate-members-to-profiles.ts # Migração one-time Firestore: members → profiles (dry run por padrão) +│ +├── firebase-applet-config.json # Config Firebase (sem chaves sensíveis) +├── firestore.rules # Regras de segurança do Firestore +├── vercel.json # /api/* → /api/index.ts (serverless) +├── vite.config.ts # Vite + Tailwind + PWA + Vitest +├── tsconfig.json # paths: { "@/*": ["src/*"] } +├── docker-compose.yaml # Orquestra frontend (3000) + backend (3001) +├── Dockerfile.backend # Imagem Node 22; executa Express via tsx (sem build) +├── Dockerfile.frontend # Multi-stage: build Vite → serve via vite preview +└── package.json ``` --- -## Design System (NEO-BRUTALISMO — NÃO ALTERAR) - -### Tokens CSS (`src/index.css`): -| Token | Valor | Uso | -|-------|-------|-----| -| `--font-sans` | Inter | Texto corrido | -| `--font-heading` | Space Grotesk / Archivo Black | Títulos (UPPERCASE) | -| `--color-neo-bg` | `#F4F4F0` | Fundo principal (off-white) | -| `--color-neo-bg-alt` | `#E5E5E5` | Fundo alternativo | -| `--color-neo-black` | `#000000` | Bordas, texto, sombras | -| `--color-neo-yellow` | `#FFC900` | Acento amarelo | -| `--color-neo-lime` | `#B8FF29` | Acento verde limão | -| `--color-neo-pink` | `#FF2E93` | Acento rosa choque | -| `--color-neo-cyan` | `#00E5FF` | Acento ciano | - -### Classes Utilitárias: -| Classe | Efeito | -|--------|--------| -| `.neo-border` | `border: 3px solid #000` | -| `.neo-shadow` | `box-shadow: 6px 6px 0px 0px #000` + active afunda | -| `.neo-shadow-hover` | Hover: sombra cresce, active: afunda | -| `.slider-thumb-neo` | Slider quadrado Neo-Brutalista | +## Rotas da Aplicação ---- +| Rota | Componente | Auth | Loader | +| --- | --- | --- | --- | +| `/` | Landing | Pública | — | +| `/onboarding` | Onboarding | Pública* | — | +| `/discover` | DiscoverPage | `requireAuth` | — | +| `/guilda` | GuildaPage | `requireAuth` | — | +| `/p/:uid` | PublicProfilePage | Pública | `profileRepository.getPublicProfile(uid)` | +| `/join/:squadId` | JoinSquadPage | Pública | `squadRepository.getSquad(squadId)` | -## Dependências Chave (package.json) - -| Pacote | Versão | Uso | Status | -|--------|--------|-----|--------| -| `react` | ^19 | UI Framework | ✅ Manter | -| `react-dom` | ^19 | React DOM renderer | ✅ Manter | -| `react-router-dom` | ^7 | SPA routing | ✅ Manter | -| `firebase` | ^12 | Auth + Firestore client | ✅ Manter | -| `firebase-admin` | ^13 | Server-side Firestore | ✅ Manter | -| `@google/genai` | ^1 | Gemini AI SDK | ✅ Manter | -| `express` | ^4 | API server | ✅ Manter | -| `vite` | ^6 | Build system | ✅ Manter | -| `tailwindcss` | ^4 | CSS framework | ✅ Manter | -| `@tailwindcss/vite` | ^4 | Tailwind Vite plugin | ✅ Manter | -| `motion` | ^12 | Animations (motion/react) | ✅ Manter | -| `recharts` | ^3 | Charts (RadarChart) | ✅ Manter | -| `lucide-react` | Latest | Icons | ✅ Manter | -| `clsx` | ^2 | Class name utility | ✅ Manter | -| `tailwind-merge` | ^3 | Tailwind class merging | ✅ Manter | -| `dotenv` | ^17 | Env vars | ✅ Manter | -| `tsx` | ^4 | TypeScript execution | ✅ Manter | +*`/onboarding` é intencionalmente público — é o callback do magic link. --- -## Firestore Collections (Estado Atual) - -### `members` (será renomeada para `profiles`) -- **Document ID:** Firebase Auth UID -- **Fields:** - - `userId` (string) — Auth UID - - `guildId` (string) — Hardcoded "TECH_FLORIPA_2026" [SERÁ REMOVIDO] - - `name` (string) - - `photoURL` (string | null) - - `github` (string) - - `linkedin` (string) - - `primaryRole` (string) - - `secondaryRoles` (string[]) - - `skills` (map): `{ frontend, backend, ux_ui, dados, hardware_android, vibe_coding }` - - `canvas` (map): `{ loves: string[], comfort: string[], veto: string[] }` - - `roast` (string | null) — IA analysis - - `roastBrutal` (string | null) — Brutal analysis - - `roastMild` (string | null) — Mild analysis - - `createdAt` (Timestamp) - - `updatedAt` (Timestamp) - -### `posts` (sistema de likes) -- **Document ID:** post ID -- **Fields:** `postId`, `likesCount`, `updatedAt` -- **Subcollection:** `likes/{userId}` → `{ userId, createdAt }` +## API Routes ---- +| Método | Rota | Rate Limit | Descrição | +| --- | --- | --- | --- | +| `GET` | `/api/health` | — | Status check | +| `POST` | `/api/roast` | 5 req/min | Análise individual via Gemini | +| `POST` | `/api/oraculo/match` | 10 req/min | Match de compatibilidade via Gemini | -## API Routes (server.ts) +--- -| Method | Path | Input | Output | Status | -|--------|------|-------|--------|--------| -| GET | `/api/health` | — | `{ status: "ok" }` | ✅ Ativo | -| POST | `/api/roast` | `{ memberId, memberData, persona }` | `{ roast: string }` | ✅ Manter | -| POST | `/api/oraculo/match` | `{ challengeDesc, members }` | `{ seguro, inovacao, surpresa }` | ✅ Manter | +## Coleções Firestore ---- +### `profiles` -## Lógica Reutilizável (Funções-Chave) +Usada pela feature `discover`. Escrita pelo `useOnboardingForm`. -### De `Onboarding.tsx`: -- `TAG_CATEGORIES` — Array de categorias de tags com cores (linhas 58-95). -- `ROLES_LIST` — Array de roles disponíveis (linhas 97-101). -- `setTagSentiment(tag, sentiment)` — Lógica de exclusão mútua (linhas 210-224). -- `toggleRole(role)` — Lógica de primary/secondary role (linhas 196-208). -- `handleSubmit()` — Save para Firestore com normalização de links (linhas 234-288). -- `OnboardingAvatar` — Componente com fallback chain de fotos (linhas 12-56). +| Campo | Tipo | Descrição | +| --- | --- | --- | +| `userId` | string | Firebase Auth UID | +| `name` | string | Nome do membro | +| `photoURL` | string\|null | Foto (Google ou null) | +| `github` | string | Handle do GitHub | +| `linkedin` | string | Handle do LinkedIn | +| `bio` | string | Descrição pessoal | +| `primaryRole` | string | Role principal | +| `secondaryRoles` | string[] | Roles secundárias | +| `skills` | map | `{frontend, backend, ux_ui, dados, hardware_android, vibe_coding}` (1–10) | +| `canvas` | map | `{loves, comfort, veto}` — arrays de tags | +| `status` | string | `"looking"` \| `"open"` \| `"complete"` | +| `roastBrutal` | string\|null | Análise IA brutal gerada | +| `roastMild` | string\|null | Análise IA suave gerada | +| `createdAt` | Timestamp | Criação | +| `updatedAt` | Timestamp | Última atualização | -### De `Guilda.tsx`: -- `Avatar` component — Fallback chain similar ao Onboarding (linhas 11-48). -- `getRadarData(skills)` — Transforma skills map em array pro Recharts (linhas 90-100). -- `getGithubUrl(val)` / `getLinkedinUrl(val)` — Normaliza URLs sociais (linhas 102-118). -- `executeRoast(member, persona)` — Chama API de IA com loading states (linhas 120-183). -- Firestore query com `onSnapshot` para real-time updates (linhas 60-78). +### `members` -### De `Oraculo.tsx`: -- `handleAnalyze()` — Chama API do Gemini para matchmaking estratégico (linhas 29-64). -- UI de resultados com 3 cards de estratégia (linhas 116-196). +Usada pela feature `guilda`. Schema similar a `profiles` com campos de skills diferentes. ---- +### `squads` -## Setup Rápido (Referência) +Usada pelo `ISquadRepository`. Campos: `id`, `name`, `description`, `ownerId`, `members[]`, `maxMembers`, `createdAt`, `updatedAt`. -```bash -# 1. Clonar -git clone https://github.com/YnotMax/match-tec.git -cd match-tec +--- -# 2. Instalar -npm install +## Dependências Principais + +| Pacote | Versão | Uso | +| --- | --- | --- | +| `react` | ^19 | UI Framework | +| `react-router-dom` | ^7.14 | Roteamento (framework mode) | +| `@tanstack/react-query` | ^5.100 | Server state — `useMutation` para chamadas à API Gemini | +| `@tanstack/react-virtual` | ^3.14 | Virtualização de listas | +| `zustand` | ^5.0 | Estado de UI por feature (filtros, roast state) | +| `firebase` | ^12 | Auth + Firestore client | +| `firebase-admin` | ^13 | Admin SDK (server) | +| `@google/genai` | ^1.29 | Gemini AI SDK (server only) | +| `express` | ^4.21 | API server | +| `express-rate-limit` | ^8.5 | Rate limiting Gemini routes | +| `zod` | ^4.4 | Validação runtime | +| `tailwindcss` | ^4.1 | Estilo (Neo-Brutalismo) | +| `motion` | ^12 | Animações | +| `recharts` | ^3.8 | Radar charts | +| `dotenv` | ^17 | Carrega `.env` no servidor Express | +| `concurrently` | ^10 | `dev:all` — roda Vite + Express simultaneamente | +| `tsx` | ^4.21 | Executa TypeScript diretamente (dev server e scripts) | +| `husky` + `lint-staged` | ^9 / ^17 | Pre-commit: lint + format automático | +| `vite-plugin-pwa` | ^1.2 | Suporte a PWA (manifest + service worker) | +| `vitest` | ^4.1 | Testes unitários | -# 3. Configurar .env -cp .env.example .env -# Editar .env e adicionar GEMINI_API_KEY +--- -# 4. Rodar -npm run dev -# → http://localhost:3000 -``` +## Design System (Neo-Brutalismo — não alterar) -**Firebase:** Config já incluída em `firebase-applet-config.json`. Para projeto próprio, veja instruções detalhadas no `README.md`. +| Classe/Token | Valor | Uso | +| --- | --- | --- | +| `bg-neo-bg` | `#f5f5f0` | Fundo principal | +| `text-neo-black` / `border-neo-black` | `#0a0a0a` | Texto e bordas | +| `bg-neo-lime` | `#B8FF29` | Acento primário | +| `bg-neo-pink` | `#FF2E93` | Alertas, destrutivo | +| `bg-neo-cyan` | `#00E5FF` | Ações secundárias | +| `bg-neo-yellow` | `#FFC900` | Acento amarelo | +| `font-heading` | Space Grotesk / Archivo Black | Títulos UPPERCASE | +| `shadow-[4px_4px_0_0_#000]` | — | Sombra deslocada padrão | +| `border-[3px] border-neo-black` | — | Borda grossa padrão | --- -*Este mapa é o GPS do código. Use-o para não se perder.* +*Este mapa reflete o estado após a refatoração Clean Architecture (Junho 2026).* diff --git a/docs/FRONTEND_BLUEPRINT.md b/docs/FRONTEND_BLUEPRINT.md index ee176ac..563f00e 100644 --- a/docs/FRONTEND_BLUEPRINT.md +++ b/docs/FRONTEND_BLUEPRINT.md @@ -1,5 +1,17 @@ # 🛠️ BLUEPRINT DE FRONT-END: Match Tech +> **⚠️ DOCUMENTO DEPRECIADO — Junho 2026** +> +> Este blueprint foi criado antes da refatoração Clean Architecture (Fases 0–7). +> A estrutura de pastas, os caminhos de arquivos e os padrões descritos aqui **não refletem mais o estado atual do código**. +> +> **Consulte [`ARCHITECTURE.md`](./ARCHITECTURE.md)** para a referência técnica atualizada. +> **Consulte [`CODEBASE_MAP.md`](./CODEBASE_MAP.md)** para o mapa de arquivos atual. +> +> Este documento é mantido apenas como registro histórico do planejamento original. + +--- + **Guia Técnico de Implementação para Agentes de Código e Desenvolvedores** **Versão 2.0 — Maio 2026** diff --git a/docs/TODO_MATCH_TECH.md b/docs/TODO_MATCH_TECH.md index a1aaea6..77b05df 100644 --- a/docs/TODO_MATCH_TECH.md +++ b/docs/TODO_MATCH_TECH.md @@ -1,5 +1,19 @@ # 📋 ROADMAP & TO-DO: Match Tech — Evolução para Comunidade +> **📦 ARQUIVO HISTÓRICO — Junho 2026** +> +> Este documento registra o desenvolvimento original do projeto (Maio 2026). +> Muitas das tarefas listadas foram implementadas de formas diferentes das planejadas aqui, +> como resultado da refatoração Clean Architecture realizada nas Fases 0–7. +> +> Para o estado técnico atual do projeto, consulte: +> +> - [`ARCHITECTURE.md`](./ARCHITECTURE.md) — arquitetura atual +> - [`CODEBASE_MAP.md`](./CODEBASE_MAP.md) — mapa de arquivos atualizado +> - [`../README.md`](../README.md) — guia de contribuição + +--- + **Documento de Acompanhamento de Progresso** **Versão 2.0 — Maio 2026** diff --git a/eslint.config.js b/eslint.config.js index c97efa6..c0b2e6f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,28 +2,53 @@ import js from "@eslint/js"; import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsparser from "@typescript-eslint/parser"; +import importPlugin from "eslint-plugin-import"; -export default tseslint.config( +export default [ { ignores: ["dist"] }, { - extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], languageOptions: { - ecmaVersion: 2020, + parser: tsparser, + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + ecmaFeatures: { jsx: true }, + }, globals: globals.browser, }, plugins: { + "@typescript-eslint": tseslint, "react-hooks": reactHooks, "react-refresh": reactRefresh, + import: importPlugin, }, rules: { + ...tseslint.configs["recommended"].rules, ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], - "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "import/order": [ + "warn", + { + groups: ["builtin", "external", "internal", "parent", "sibling", "index"], + "newlines-between": "always", + alphabetize: { order: "asc", caseInsensitive: true }, + }, + ], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + // These are React Compiler rules (react-hooks v5+). This project does not + // use React Compiler, so the rules produce false positives on valid patterns + // (lazy useState init, onSnapshot subscriptions, TanStack Virtual). + "react-hooks/set-state-in-effect": "off", + "react-hooks/incompatible-library": "off", }, - } -); + }, +]; diff --git a/package-lock.json b/package-lock.json index 0604991..d154568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,13 @@ "dependencies": { "@google/genai": "^1.29.0", "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.100.14", + "@tanstack/react-virtual": "^3.14.2", "@vitejs/plugin-react": "^5.0.4", "clsx": "^2.1.1", "dotenv": "^17.2.3", "express": "^4.21.2", + "express-rate-limit": "^8.5.2", "firebase": "^12.12.1", "firebase-admin": "^13.8.0", "lucide-react": "^0.546.0", @@ -23,23 +26,37 @@ "react-router-dom": "^7.14.2", "recharts": "^3.8.1", "tailwind-merge": "^3.5.0", - "vite": "^6.2.0" + "vite": "^6.2.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { + "@tanstack/react-query-devtools": "^5.100.14", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/express": "^4.17.21", "@types/node": "^22.14.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", "autoprefixer": "^10.4.21", + "concurrently": "^10.0.3", + "eslint": "^9.39.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "husky": "^9.1.7", "jsdom": "^29.0.2", + "lint-staged": "^17.0.7", + "prettier": "^3.8.3", "tailwindcss": "^4.1.14", "tsx": "^4.21.0", "typescript": "~5.8.2", "vite": "^6.2.0", "vite-plugin-pwa": "^1.2.0", - "vitest": "^4.1.5" + "vitest": "^4.1.8" } }, "node_modules/@adobe/css-tools": { @@ -145,6 +162,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1755,6 +1773,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1803,6 +1822,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -2223,6 +2243,246 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@exodus/bytes": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", @@ -2310,6 +2570,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.11.tgz", "integrity": "sha512-yxADFW35LYkP8oSGobGsYIrI42I+GPCvKTNHx4meT9Yq3C950IVz1eANoBk822I9tbKv1wyv9P4Bv1G5TpucFw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.7.2", "@firebase/logger": "0.5.0", @@ -2376,6 +2637,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.11.tgz", "integrity": "sha512-KaACDjXkK5VLpI01vEs592R7/8s5DjFdIXfKoR385ly1SmK3Tu+jMHCIB4MsiY5jsez6v7VlEX/3rJ90dVkHyA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.14.11", "@firebase/component": "0.7.2", @@ -2392,6 +2654,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.4.tgz", "integrity": "sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/logger": "0.5.0" } @@ -2845,6 +3108,7 @@ "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -3108,6 +3372,72 @@ "node": ">=6" } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", @@ -3717,6 +4047,13 @@ "win32" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4009,6 +4346,89 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@tanstack/query-core": { + "version": "5.100.14", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz", + "integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.100.14", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.14.tgz", + "integrity": "sha512-g96SmSSQecYTYcyuAMRXr895GplJv01UGt7qttQWPOUyZ5EGz5tbRc589bMc2m5BsPFD6O0PCEAHdbDYNP6UBw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.14", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz", + "integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tanstack/query-core": "5.100.14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.100.14", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.14.tgz", + "integrity": "sha512-JkP5VDgKOw3t/QSA1OABRHEqx8BuNs5MfvZRooNqdvN57SzTuGq3fKR1a2IH5rqa5HDLUm+FOXUEnB9ueHiLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.100.14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.100.14", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz", + "integrity": "sha512-IpWnmCLvuymRfeeLNVXIzNEYBFLpd3drVIS91sqV78VTZFyldlChkOocZRCPp1B+Wnk09bcLNme8WaMU/9/9bQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.0.tgz", + "integrity": "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -4100,8 +4520,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4292,6 +4711,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", @@ -4351,6 +4784,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4361,6 +4795,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4444,37 +4879,284 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@vitejs/plugin-react": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", - "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -4483,13 +5165,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.5", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4510,9 +5192,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { @@ -4523,13 +5205,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.8", "pathe": "^2.0.3" }, "funding": { @@ -4537,14 +5219,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4553,9 +5235,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -4563,13 +5245,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -4609,6 +5291,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4616,6 +5299,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -4631,6 +5324,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4642,6 +5336,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4666,6 +5376,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -4699,6 +5416,89 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", @@ -5012,6 +5812,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5096,6 +5897,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001790", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", @@ -5126,25 +5937,121 @@ "node": ">=18" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5197,6 +6104,192 @@ "node": ">=4.0.0" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", + "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" + }, + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/concurrently/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concurrently/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -5572,6 +6665,13 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -5666,13 +6766,25 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dotenv": { "version": "17.4.2", @@ -5801,6 +6913,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -5923,6 +7048,19 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-to-primitive": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", @@ -6008,29 +7146,457 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.13.0.tgz", + "integrity": "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { @@ -6109,6 +7675,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6152,6 +7736,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -6259,6 +7850,19 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -6332,6 +7936,23 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/firebase": { "version": "12.12.1", "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.12.1.tgz", @@ -6393,6 +8014,27 @@ "@google-cloud/storage": "^7.19.0" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6648,6 +8290,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6748,6 +8403,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -7040,6 +8721,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -7109,6 +8800,23 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -7206,6 +8914,22 @@ "node": ">= 14" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7224,6 +8948,16 @@ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", "license": "ISC" }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/immer": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", @@ -7234,6 +8968,33 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -7274,6 +9035,15 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7368,13 +9138,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -7418,6 +9188,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-finalizationregistry": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", @@ -7463,6 +9243,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -7770,6 +9563,29 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "29.0.2", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz", @@ -7880,6 +9696,13 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -7887,6 +9710,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7993,6 +9823,16 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -8003,6 +9843,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -8205,57 +10059,192 @@ "linux" ], "engines": { - "node": ">= 12.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lint-staged": { + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", + "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "listr2": "^10.2.1", + "picomatch": "^4.0.4", + "string-argv": "^0.3.2", + "tinyexec": "^1.2.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=22.22.1" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=20" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/lodash": { "version": "4.18.1", @@ -8319,6 +10308,13 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -8332,6 +10328,144 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -8390,7 +10524,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8480,6 +10613,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8506,6 +10652,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -8581,6 +10737,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -8610,6 +10773,25 @@ "node": ">=10.5.0" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", @@ -8696,6 +10878,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -8729,6 +10980,40 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -8751,8 +11036,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -8763,6 +11048,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -8783,6 +11084,19 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -8805,6 +11119,16 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-expression-matcher": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", @@ -8925,6 +11249,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8941,6 +11266,32 @@ "dev": true, "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", @@ -8960,7 +11311,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -8976,7 +11326,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -9098,6 +11447,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9107,6 +11457,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9126,6 +11477,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9267,7 +11619,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -9427,6 +11780,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -9437,6 +11800,23 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -9461,11 +11841,19 @@ "node": ">=14" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9505,6 +11893,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", @@ -9762,6 +12160,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -9854,6 +12265,52 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/smob": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", @@ -10009,6 +12466,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -10137,6 +12604,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", @@ -10160,6 +12637,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strnum": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", @@ -10180,6 +12670,19 @@ "license": "MIT", "optional": true }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -10370,9 +12873,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -10454,6 +12957,55 @@ "license": "MIT", "optional": true }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10466,6 +13018,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -10480,6 +13033,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", @@ -10590,6 +13156,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10750,6 +13317,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -10824,6 +13401,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -11382,19 +13960,19 @@ } }, "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -11422,12 +14000,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -11672,6 +14250,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/workbox-background-sync": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", @@ -11854,6 +14442,7 @@ "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -12078,6 +14667,22 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "optional": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -12109,14 +14714,66 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index b2413d3..99d8e11 100644 --- a/package.json +++ b/package.json @@ -4,20 +4,32 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "tsx server.ts", - "start": "node --experimental-strip-types server.ts", + "dev": "vite", + "dev:server": "tsx src/server/server.ts", + "dev:all": "concurrently \"npm run dev\" \"npm run dev:server\"", "build": "vite build", "preview": "vite preview", + "start": "node --experimental-strip-types server.ts", "clean": "rm -rf dist", - "lint": "tsc --noEmit" + "lint": "eslint src --max-warnings=0", + "lint:fix": "eslint src --fix", + "format": "prettier --write src", + "typecheck": "tsc --noEmit", + "test": "vitest", + "migrate:members-to-profiles": "npx tsx scripts/migrate-members-to-profiles.ts", + "precommit": "lint-staged", + "prepare": "husky" }, "dependencies": { "@google/genai": "^1.29.0", "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.100.14", + "@tanstack/react-virtual": "^3.14.2", "@vitejs/plugin-react": "^5.0.4", "clsx": "^2.1.1", "dotenv": "^17.2.3", "express": "^4.21.2", + "express-rate-limit": "^8.5.2", "firebase": "^12.12.1", "firebase-admin": "^13.8.0", "lucide-react": "^0.546.0", @@ -27,22 +39,42 @@ "react-router-dom": "^7.14.2", "recharts": "^3.8.1", "tailwind-merge": "^3.5.0", - "vite": "^6.2.0" + "vite": "^6.2.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { + "@tanstack/react-query-devtools": "^5.100.14", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/express": "^4.17.21", "@types/node": "^22.14.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.60.1", "autoprefixer": "^10.4.21", + "concurrently": "^10.0.3", + "eslint": "^9.39.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "husky": "^9.1.7", "jsdom": "^29.0.2", + "lint-staged": "^17.0.7", + "prettier": "^3.8.3", "tailwindcss": "^4.1.14", "tsx": "^4.21.0", "typescript": "~5.8.2", "vite": "^6.2.0", "vite-plugin-pwa": "^1.2.0", - "vitest": "^4.1.5" + "vitest": "^4.1.8" + }, + "lint-staged": { + "src/**/*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ] } -} +} \ No newline at end of file diff --git a/scripts/migrate-members-to-profiles.ts b/scripts/migrate-members-to-profiles.ts new file mode 100644 index 0000000..72fd16c --- /dev/null +++ b/scripts/migrate-members-to-profiles.ts @@ -0,0 +1,155 @@ +/** + * Migração: coleção `members` → `profiles` + * + * Contexto: o projeto originalmente usava `members` como coleção de perfis. + * A coleção `profiles` é o destino definitivo. Este script copia os documentos + * de `members` para `profiles`, preservando todos os dados e adicionando campos + * ausentes necessários pelo schema de `profiles`. + * + * Uso: + * # Dry run (padrão — apenas mostra o que seria migrado): + * npx tsx scripts/migrate-members-to-profiles.ts + * + * # Executar de fato: + * npx tsx scripts/migrate-members-to-profiles.ts --execute + * + * Pré-requisitos: + * FIREBASE_SERVICE_ACCOUNT= no ambiente + * (ou GOOGLE_APPLICATION_CREDENTIALS apontando para o arquivo JSON) + * + * Segurança: + * - Documentos já presentes em `profiles` NÃO são sobrescritos. + * - A coleção `members` não é modificada (somente leitura). + * - Todos os erros são reportados mas não interrompem o batch. + */ + +import { cert, getApp, getApps, initializeApp } from "firebase-admin/app"; +import { getFirestore } from "firebase-admin/firestore"; + +// ── Firebase Admin init ────────────────────────────────────────────────────── + +function initAdmin() { + if (getApps().length > 0) return getApp(); + + const raw = process.env.FIREBASE_SERVICE_ACCOUNT; + if (raw) { + const parsed = JSON.parse(raw) as { + projectId: string; + clientEmail: string; + privateKey: string; + }; + return initializeApp({ + credential: cert({ ...parsed, privateKey: parsed.privateKey.replace(/\\n/g, "\n") }), + }); + } + + // Falls back to Application Default Credentials (gcloud auth) + return initializeApp(); +} + +// ── Field mapping ───────────────────────────────────────────────────────────── + +interface MemberDoc { + userId?: string; + guildId?: string; + name?: string; + photoURL?: string | null; + github?: string; + linkedin?: string; + bio?: string; + primaryRole?: string; + secondaryRoles?: string[]; + skills?: Record; + canvas?: { loves?: string[]; comfort?: string[]; veto?: string[] }; + status?: string; + eventId?: string; + roast?: string; + roastBrutal?: string; + roastMild?: string; + createdAt?: unknown; + updatedAt?: unknown; + [key: string]: unknown; +} + +function toProfileDoc(memberId: string, data: MemberDoc): Record { + // Spread all fields then remove guildId (obsolete hardcoded field from old schema) + const doc: Record = { ...data }; + delete doc["guildId"]; + + return { + ...doc, + // Ensure userId is set (some old docs used only guildId as identity) + userId: data.userId ?? memberId, + // Required profile fields with sensible defaults for migrated docs + status: data.status ?? "looking", + eventId: data.eventId ?? data.guildId ?? "tech_floripa_2026", + bio: data.bio ?? "", + }; +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +async function migrate(dryRun: boolean) { + const app = initAdmin(); + const db = getFirestore(app); + + const membersSnap = await db.collection("members").get(); + + if (membersSnap.empty) { + console.log("Coleção `members` está vazia. Nada a migrar."); + return; + } + + console.log(`\nEncontrados ${membersSnap.size} documentos em \`members\`.`); + console.log(dryRun ? "MODO DRY RUN — nenhuma escrita será feita.\n" : "MODO EXECUÇÃO\n"); + + let migrated = 0; + let skipped = 0; + let errors = 0; + + for (const memberDoc of membersSnap.docs) { + const id = memberDoc.id; + const data = memberDoc.data() as MemberDoc; + + try { + const profileRef = db.collection("profiles").doc(id); + const existing = await profileRef.get(); + + if (existing.exists) { + console.log(` [SKIP] ${id} — já existe em \`profiles\``); + skipped++; + continue; + } + + const profileData = toProfileDoc(id, data); + + if (dryRun) { + console.log(` [DRY] ${id} → seria migrado (name: ${String(data.name ?? "??")})`); + } else { + await profileRef.set(profileData); + console.log(` [OK] ${id} migrado (name: ${String(data.name ?? "??")})`); + } + + migrated++; + } catch (err) { + console.error(` [ERRO] ${id}:`, err); + errors++; + } + } + + console.log("\n─────────────────────────────────────────"); + if (dryRun) { + console.log(`Dry run concluído: ${migrated} seriam migrados, ${skipped} ignorados, ${errors} erros.`); + console.log("Para executar de fato, rode com --execute"); + } else { + console.log(`Migração concluída: ${migrated} migrados, ${skipped} ignorados, ${errors} erros.`); + } +} + +// ── Entry point ─────────────────────────────────────────────────────────────── + +const dryRun = !process.argv.includes("--execute"); +migrate(dryRun).catch((err) => { + console.error("Erro fatal:", err); + process.exit(1); +}); diff --git a/server.ts b/server.ts deleted file mode 100644 index 074df37..0000000 --- a/server.ts +++ /dev/null @@ -1,147 +0,0 @@ -import express from "express"; -import { createServer as createViteServer } from "vite"; -import path from "path"; -import { fileURLToPath } from "url"; -import dotenv from "dotenv"; - -dotenv.config(); - -// Logger simples para o servidor (não usa o logger do frontend) -const serverLog = { - info: (msg: string, data?: any) => console.log(`ℹ️ [Server] ${msg}`, data ?? ''), - error: (msg: string, data?: any) => console.error(`❌ [Server] ${msg}`, data ?? ''), - warn: (msg: string, data?: any) => console.warn(`⚠️ [Server] ${msg}`, data ?? ''), -}; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -async function startServer() { - const app = express(); - const PORT = 3000; - - app.use(express.json()); - - // API Routes - app.get("/api/health", (req, res) => { - res.json({ status: "ok" }); - }); - - app.post("/api/roast", async (req, res) => { - try { - const { memberId, memberData, persona } = req.body; - if (!memberId || !memberData) return res.status(400).json({ error: "Missing memberId or memberData" }); - - const { GoogleGenAI } = await import("@google/genai"); - const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); - - let systemInstruction = 'Aja como um tech lead sênior sarcástico, brutal e extremamente exigente no meio de um hackathon. Analise as skills e os inputs deste membro. Critique sem dó suas piores habilidades, faça piada onde ele diz que "se garante", e traga realismo se os vetos ("nem fudendo") forem exatamente o que precisamos. NÃO seja polido. Seja irônico e direto. Máximo de 3 parágrafos.'; - - if (persona === 'mild') { - systemInstruction = 'Aja como um mentor técnico experiente, paciente e encorajador. Analise as habilidades e inputs (paixões, opero bem e vetos) deste membro de forma construtiva. Destaque seus pontos fortes e dê conselhos gentis sobre como melhorar nas áreas mais fracas e como aproveitar aquilo que operam bem. Seja inspirador e amigável. Máximo de 3 parágrafos.'; - } - - const response = await ai.models.generateContent({ - model: "gemini-2.5-flash", - contents: `Analise este membro. DADOS DO MEMBRO: \n${JSON.stringify(memberData, null, 2)}`, - config: { - systemInstruction - } - }); - const roastText = response.text; - // Persist to DB using Admin SDK (Server-side bypasses rules) - const { db: adminDb } = await import("./src/lib/firebase-admin.ts"); - - const updateData: any = { updatedAt: new Date() }; - if (persona === 'brutal') { - updateData.roastBrutal = roastText; - } else if (persona === 'mild') { - updateData.roastMild = roastText; - } else { - updateData.roast = roastText; - } - - await adminDb.collection("profiles").doc(memberId).update(updateData); - - res.json({ roast: roastText }); - } catch (e: any) { - serverLog.error('Erro na rota /api/roast:', e.message); - const errorMessage = e.message?.includes('API key not valid') - ? "Chave da API do Gemini inválida ou não configurada. Por favor, adicione uma chave válida no painel de configurações." - : e.message; - res.status(500).json({ error: errorMessage }); - } - }); - - app.post("/api/oraculo/match", async (req, res) => { - try { - const { challengeDesc, members } = req.body; - const { GoogleGenAI } = await import("@google/genai"); - - const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); - - const response = await ai.models.generateContent({ - model: "gemini-2.5-flash", - contents: `Contexto do Hackathon Tech Floripa:\n${challengeDesc}\n\nMembros da Equipe:\n${JSON.stringify(members, null, 2)}`, - config: { - responseMimeType: "application/json", - systemInstruction: `Sua Tarefa (A Inteligência do Oráculo): -Gere três opções estratégicas de projetos para o hackathon: -1. Uma Escolha Segura (Viabilidade altíssima, risco baixo, foco no que a equipe domina). -2. Uma Escolha de Inovação (Viabilidade média, risco alto, usa as vontades/paixões da equipe em coisas novas). -3. A Carta na Manga / Surpresa (Baixa viabilidade, altíssimo risco operacional, inovação louca arrastando as pessoas pro limite). - -IMPORTANTE: -Para cada estratégia, indique o nível de "Match" com a equipe em porcentagem e liste precisamente quais membros estarão alocados nela (nunca aloque alguém no que eles deram 'veto'). - -Responda OBRIGATORIAMENTE em JSON no formato: -{ - "seguro": { "title": "STRING", "match": "NUMBER", "reason": "STRING", "allocation": "STRING", "viability": "STRING", "risk": "STRING", "banca": "STRING" }, - "inovacao": { "title": "STRING", "match": "NUMBER", "reason": "STRING", "allocation": "STRING", "viability": "STRING", "risk": "STRING", "banca": "STRING" }, - "surpresa": { "title": "STRING", "match": "NUMBER", "reason": "STRING", "allocation": "STRING", "viability": "STRING", "risk": "STRING", "banca": "STRING" } -}` - } - }); - - const responseText = response.text; - - try { - const parsed = JSON.parse(responseText); - if (!parsed.seguro || !parsed.inovacao || !parsed.surpresa) { - throw new Error("Invalid schema from AI"); - } - res.json(parsed); - } catch (parseError) { - serverLog.error('AI JSON Parse Error:', responseText); - res.status(500).json({ error: "O Oráculo alucinou um formato inválido. Tente novamente." }); - } - } catch (e: any) { - serverLog.error('Erro na rota /api/oraculo/match:', e.message); - const errorMessage = e.message?.includes('API key not valid') - ? "Chave da API do Gemini inválida ou não configurada. Por favor, adicione uma chave válida no painel de configurações." - : e.message; - res.status(500).json({ error: errorMessage }); - } - }); - - // Vite middleware for development - if (process.env.NODE_ENV !== "production") { - const vite = await createViteServer({ - server: { middlewareMode: true }, - appType: "spa", - }); - app.use(vite.middlewares); - } else { - const distPath = path.join(__dirname, 'dist'); - app.use(express.static(distPath)); - app.get('*', (req, res) => { - res.sendFile(path.join(distPath, 'index.html')); - }); - } - - app.listen(PORT, "0.0.0.0", () => { - console.log(`Server running on http://localhost:${PORT}`); - }); -} - -startServer(); diff --git a/src/App.tsx b/src/App.tsx index a3bcc93..049bcac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,33 @@ -import React from "react"; -import { BrowserRouter, Routes, Route } from "react-router-dom"; -import RootLayout from "./layouts/RootLayout"; -import Landing from "./pages/Landing"; -import Onboarding from "./pages/Onboarding"; -import Discover from "./pages/Discover"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { RouterProvider } from "react-router-dom"; + import { AuthProvider } from "./contexts/AuthContext"; -import { ErrorBoundary } from "./components/ErrorBoundary"; +import { ErrorBoundary } from "./shared/components/ui/ErrorBoundary"; + +import { router } from "@/routes/routes"; +import { RepositoryProvider } from "@/shared/context/RepositoryContext"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 min — data stays fresh, avoids redundant refetches + retry: 2, + }, + }, +}); export default function App() { return ( - - - - - }> - } /> - } /> - } /> - - - - - + + + + + + + + + {import.meta.env.DEV && } + ); } diff --git a/src/components/ui/ProfileCard.tsx b/src/components/ui/ProfileCard.tsx deleted file mode 100644 index 95f22a7..0000000 --- a/src/components/ui/ProfileCard.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import React from "react"; -import { Card } from "./Card"; -import Avatar from "./Avatar"; -import SkillRadar from "./SkillRadar"; -import TagBadge from "./TagBadge"; -import StatusBadge from "./StatusBadge"; -import { Github, Linkedin } from "lucide-react"; -import { cn } from "../../lib/utils"; - -interface ProfileCardProps { - profile: { - id?: string; - userId: string; - name: string; - photoURL?: string | null; - github?: string; - linkedin?: string; - bio?: string; - primaryRole: string; - secondaryRoles?: string[]; - skills: { - frontend: number; - backend: number; - ux_ui: number; - dados: number; - hardware_android: number; - vibe_coding: number; - }; - canvas?: { - loves: string[]; - comfort: string[]; - veto: string[]; - }; - status?: "looking" | "open" | "complete"; - }; - onClick?: () => void; - compact?: boolean; - className?: string; - colorIndex?: number; -} - -export default function ProfileCard({ - profile, - onClick, - compact = false, - className, - colorIndex = 0, -}: ProfileCardProps) { - const { - name, - github, - linkedin, - bio, - primaryRole, - secondaryRoles = [], - skills, - canvas = { loves: [], comfort: [], veto: [] }, - status = "looking", - } = profile; - - // Neo-Brutalist color configurations for variety - const bgColors = ["bg-neo-lime", "bg-neo-pink", "bg-neo-cyan", "bg-neo-yellow"]; - const headerBg = bgColors[colorIndex % bgColors.length]; - - // Decide text colors based on background - const headerText = headerBg === "bg-neo-pink" ? "text-white" : "text-neo-black"; - - const getGithubUrl = (val?: string) => { - if (!val) return ""; - const clean = val - .trim() - .replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//i, "") - .replace(/\/$/, "") - .replace(/^@/, ""); - return `https://github.com/${clean}`; - }; - - const getLinkedinUrl = (val?: string) => { - if (!val) return ""; - const clean = val - .trim() - .replace(/^(?:https?:\/\/)?(?:[\w-]+\.)?linkedin\.com\/(?:in|profile)\//i, "") - .replace(/\/$/, "") - .replace(/^@/, ""); - return `https://linkedin.com/in/${clean}`; - }; - - if (compact) { - return ( - - {/* Compact Header */} -
- {primaryRole || "OPERADOR"} -
- - {/* Compact Body */} -
- -
-

- {name || "NOME_NULO"} -

-
- -
-
-
-
- ); - } - - return ( - - {/* Bento Header */} -
- - {primaryRole || "OPERADOR"} - - - ID_{profile.id?.slice(0, 5) || profile.userId?.slice(0, 5) || "NULO"} - -
- - {/* Bento Identity Section */} -
- {/* Floating background shape */} -
- - - - -
- - {/* Bento Bio (New) */} - {bio && ( -
- "{bio}" -
- )} - - {/* Bento Skills Chart */} - {skills && ( -
- -
- )} - - {/* Bento Tags Section */} -
- {canvas.loves && canvas.loves.length > 0 && ( -
- - PAIXÕES - -
- {canvas.loves.slice(0, 3).map((love) => ( - - ))} -
-
- )} - - {canvas.veto && canvas.veto.length > 0 && ( -
- - VETOS - -
- {canvas.veto.slice(0, 3).map((veto) => ( - - ))} -
-
- )} -
- - {/* Bento Status Bar */} -
- - - VER SINA → - -
-
- ); -} diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 9ba221f..3856c51 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,5 +1,3 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { auth } from '../lib/firebase'; import { User, signInWithPopup, @@ -7,9 +5,12 @@ import { signOut, sendSignInLinkToEmail, isSignInWithEmailLink, - signInWithEmailLink -} from 'firebase/auth'; -import { authLog } from '../lib/logger'; + signInWithEmailLink, +} from "firebase/auth"; +import React, { createContext, useEffect, useState } from "react"; + +import { auth } from "../shared/lib/firebase/firebase.client"; +import { authLog } from "../shared/lib/logger/logger"; interface AuthContextType { user: User | null; @@ -25,7 +26,7 @@ interface AuthContextType { confirmMagicLinkEmail: (email: string) => Promise; } -const MAGIC_EMAIL_KEY = 'matchtech_magic_email'; +const MAGIC_EMAIL_KEY = "matchtech_magic_email"; const AuthContext = createContext({} as AuthContextType); @@ -34,14 +35,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [loading, setLoading] = useState(true); const [magicLinkSent, setMagicLinkSent] = useState(false); const [magicLinkEmail, setMagicLinkEmail] = useState(null); - const [completingMagicLink, setCompletingMagicLink] = useState(false); + const [completingMagicLink, setCompletingMagicLink] = useState(() => + isSignInWithEmailLink(auth, window.location.href), + ); const [pendingMagicLinkUrl, setPendingMagicLinkUrl] = useState(null); useEffect(() => { // Detecta se a URL atual é um Magic Link de login if (isSignInWithEmailLink(auth, window.location.href)) { - setCompletingMagicLink(true); - authLog.info('Magic Link detectado na URL. Completando login...'); + authLog.info("Magic Link detectado na URL. Completando login..."); // Recupera o email salvo no localStorage (mesmo dispositivo) const savedEmail = window.localStorage.getItem(MAGIC_EMAIL_KEY); @@ -50,28 +52,33 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Mesmo dispositivo: completa imediatamente signInWithEmailLink(auth, savedEmail, window.location.href) .then(() => { - authLog.info('Login via Magic Link realizado com sucesso.'); + authLog.info("Login via Magic Link realizado com sucesso."); window.localStorage.removeItem(MAGIC_EMAIL_KEY); - window.history.replaceState(null, '', window.location.pathname); + window.history.replaceState(null, "", window.location.pathname); }) .catch((err) => { - authLog.error('Erro ao completar login via Magic Link:', err); + authLog.error("Erro ao completar login via Magic Link:", err); }) .finally(() => { setCompletingMagicLink(false); }); } else { // Dispositivo diferente: armazena a URL e pede email via UI própria - authLog.warn('Magic Link: email não encontrado no dispositivo. Aguardando confirmação via UI.'); + authLog.warn( + "Magic Link: email não encontrado no dispositivo. Aguardando confirmação via UI.", + ); setPendingMagicLinkUrl(window.location.href); - window.history.replaceState(null, '', window.location.pathname); + window.history.replaceState(null, "", window.location.pathname); setCompletingMagicLink(false); } } // Listener padrão de autenticação const unsubscribe = auth.onAuthStateChanged((u) => { - authLog.info('Estado de autenticação mudou:', u?.email ?? 'não autenticado'); + authLog.info( + "Estado de autenticação mudou:", + u?.email ? { email: u?.email } : { error: "nao autenticado" }, + ); setUser(u); setLoading(false); }); @@ -83,9 +90,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const provider = new GoogleAuthProvider(); try { await signInWithPopup(auth, provider); - authLog.info('Login via Google realizado com sucesso (popup).'); + authLog.info("Login via Google realizado com sucesso (popup)."); } catch (err) { - authLog.error('Erro no login via popup do Google:', err); + authLog.error("Erro no login via popup do Google:", err); throw err; } }; @@ -106,7 +113,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setMagicLinkEmail(email); authLog.info(`Magic Link enviado para: ${email}`); } catch (err) { - authLog.error('Erro ao enviar Magic Link:', err); + authLog.error("Erro ao enviar Magic Link:", err); throw err; } }; @@ -119,7 +126,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const logOut = async () => { await signOut(auth); - authLog.info('Usuário fez logout.'); + authLog.info("Usuário fez logout."); }; // Confirma email para login em dispositivo diferente (substitui window.prompt) @@ -128,10 +135,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setCompletingMagicLink(true); try { await signInWithEmailLink(auth, email, pendingMagicLinkUrl); - authLog.info('Login via Magic Link (cross-device) realizado com sucesso.'); + authLog.info("Login via Magic Link (cross-device) realizado com sucesso."); setPendingMagicLinkUrl(null); } catch (err) { - authLog.error('Erro ao confirmar Magic Link (cross-device):', err); + authLog.error("Erro ao confirmar Magic Link (cross-device):", err); throw err; } finally { setCompletingMagicLink(false); @@ -139,22 +146,24 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; return ( - + {children} ); } -export const useAuth = () => useContext(AuthContext); +export { AuthContext }; diff --git a/src/contexts/useAuth.ts b/src/contexts/useAuth.ts new file mode 100644 index 0000000..c355794 --- /dev/null +++ b/src/contexts/useAuth.ts @@ -0,0 +1,5 @@ +import { useContext } from "react"; + +import { AuthContext } from "./AuthContext"; + +export const useAuth = () => useContext(AuthContext); diff --git a/src/domain/entities/Member.ts b/src/domain/entities/Member.ts new file mode 100644 index 0000000..862cf5d --- /dev/null +++ b/src/domain/entities/Member.ts @@ -0,0 +1,44 @@ +import type { Tag, SkillRadar } from "@/infrastructure/firebase/schemas"; + +export interface MemberIdentity { + uid: string; + displayName: string; + photoURL?: string; + bio: string; +} + +export interface MemberRoles { + role: string; + secondaryRoles: string[]; +} + +export interface MemberSkills { + skills: SkillRadar; +} + +export interface MemberTags { + tags: Tag[]; +} + +export interface MemberSquadStatus { + squadId?: string; + squadStatus: "open" | "looking" | "closed"; +} + +export type Member = MemberIdentity & + MemberRoles & + MemberSkills & + MemberTags & + MemberSquadStatus & { + createdAt: Record; + updatedAt: Record; + visibility: "public" | "private"; + }; + +export type PublicMember = MemberIdentity & + MemberRoles & { + visibility: "public"; + tags?: Tag[]; + }; + +export type { Tag, SkillRadar } from "@/infrastructure/firebase/schemas"; diff --git a/src/domain/entities/Shared.ts b/src/domain/entities/Shared.ts new file mode 100644 index 0000000..0a80f32 --- /dev/null +++ b/src/domain/entities/Shared.ts @@ -0,0 +1,17 @@ +export type RoastPersona = "brutal" | "mild"; + +export interface Tag { + name: string; + sentiment: "love" | "ok" | "veto"; +} + +export interface SkillRadar { + frontend: number; + backend: number; + design: number; + data: number; + devops: number; + soft: number; +} + +export type SquadStatus = "open" | "looking" | "closed"; diff --git a/src/domain/entities/Squad.ts b/src/domain/entities/Squad.ts new file mode 100644 index 0000000..770ee89 --- /dev/null +++ b/src/domain/entities/Squad.ts @@ -0,0 +1,18 @@ +export interface SquadMember { + uid: string; + displayName: string; + photoURL?: string; + role: string; + joinedAt: Record; +} + +export interface Squad { + id: string; + name: string; + description: string; + ownerId: string; + members: SquadMember[]; + maxMembers: number; + createdAt: Record; + updatedAt: Record; +} diff --git a/src/domain/entities/index.ts b/src/domain/entities/index.ts new file mode 100644 index 0000000..429096a --- /dev/null +++ b/src/domain/entities/index.ts @@ -0,0 +1,11 @@ +// Domain entities and types +export type { + Member, + MemberIdentity, + MemberRoles, + MemberSkills, + MemberTags, + MemberSquadStatus, +} from "./Member"; +export type { Squad } from "./Squad"; +export type { RoastPersona, Tag, SkillRadar } from "./Shared"; diff --git a/src/domain/ports/IAuthService.ts b/src/domain/ports/IAuthService.ts new file mode 100644 index 0000000..3f88c87 --- /dev/null +++ b/src/domain/ports/IAuthService.ts @@ -0,0 +1,8 @@ +import type { Member } from "@/domain/entities/Member"; + +export interface IAuthService { + getCurrentUser(): Promise; + signInWithMagicLink(email: string): Promise; + signOut(): Promise; + isAuthenticated(): Promise; +} diff --git a/src/domain/ports/IProfileRepository.ts b/src/domain/ports/IProfileRepository.ts new file mode 100644 index 0000000..4c278d2 --- /dev/null +++ b/src/domain/ports/IProfileRepository.ts @@ -0,0 +1,16 @@ +import type { Member, PublicMember } from "@/domain/entities/Member"; + +export interface ProfileFilters { + role?: string; + tags?: string[]; + squadStatus?: "open" | "looking" | "closed"; + minSkillLevel?: number; +} + +export interface IProfileRepository { + getProfile(uid: string): Promise; + getPublicProfile(uid: string): Promise; + updateProfile(uid: string, data: Partial): Promise; + listPublicProfiles(filters?: ProfileFilters): Promise; + deleteProfile(uid: string): Promise; +} diff --git a/src/domain/ports/IRoastService.ts b/src/domain/ports/IRoastService.ts new file mode 100644 index 0000000..67394a6 --- /dev/null +++ b/src/domain/ports/IRoastService.ts @@ -0,0 +1,18 @@ +import type { RoastPersona } from "@/domain/entities/Shared"; + +export interface RoastInput { + entityId: string; + entityType: "member" | "squad"; + persona: RoastPersona; +} + +export interface RoastResult { + text: string; + persona: RoastPersona; + createdAt: Record; +} + +export interface IRoastService { + requestRoast(input: RoastInput): Promise; + updateRoast(entityId: string, roastResult: RoastResult): Promise; +} diff --git a/src/domain/ports/ISquadRepository.ts b/src/domain/ports/ISquadRepository.ts new file mode 100644 index 0000000..37ebac5 --- /dev/null +++ b/src/domain/ports/ISquadRepository.ts @@ -0,0 +1,11 @@ +import type { Squad } from "@/domain/entities/Squad"; + +export interface ISquadRepository { + getSquad(squadId: string): Promise; + getSquadsByUserId(userId: string): Promise; + createSquad(data: Omit): Promise; + updateSquad(squadId: string, data: Partial): Promise; + deleteSquad(squadId: string): Promise; + addMemberToSquad(squadId: string, memberId: string): Promise; + removeMemberFromSquad(squadId: string, memberId: string): Promise; +} diff --git a/src/domain/ports/index.ts b/src/domain/ports/index.ts new file mode 100644 index 0000000..b8a42b2 --- /dev/null +++ b/src/domain/ports/index.ts @@ -0,0 +1,5 @@ +// Port interfaces for dependency inversion +export type { IProfileRepository } from "./IProfileRepository"; +export type { ISquadRepository } from "./ISquadRepository"; +export type { IRoastService } from "./IRoastService"; +export type { IAuthService } from "./IAuthService"; diff --git a/src/domain/usecases/__tests__/compatibilityAlgorithm.test.ts b/src/domain/usecases/__tests__/compatibilityAlgorithm.test.ts new file mode 100644 index 0000000..44caba1 --- /dev/null +++ b/src/domain/usecases/__tests__/compatibilityAlgorithm.test.ts @@ -0,0 +1,239 @@ +import { describe, it, expect } from "vitest"; + +import { + calculateCompatibility, + filterMembers, + getTopCompatibleMembers, + scoreSkillsForRole, + sortByCompatibility, +} from "../compatibilityAlgorithm"; + +import type { Member } from "@/domain/entities/Member"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function member(overrides: Partial = {}): Member { + return { + uid: "uid", + displayName: "User", + bio: "", + role: "frontend", + secondaryRoles: [], + skills: { frontend: 5, backend: 5, design: 5, data: 5, devops: 5, soft: 5 }, + tags: [], + squadStatus: "open", + visibility: "public", + createdAt: {}, + updatedAt: {}, + ...overrides, + }; +} + +const baseSkills = { frontend: 8, backend: 4, design: 3, data: 2, devops: 3, soft: 7 }; + +// ── calculateCompatibility ─────────────────────────────────────────────────── + +describe("calculateCompatibility", () => { + it("returns 80 when both members are identical (same role, no tags)", () => { + // Max score with same role: skillOverlap(1.0)*40 + tagCompat(1.0)*40 + roleBonus(0) = 80 + const m = member({ skills: baseSkills, tags: [] }); + expect(calculateCompatibility(m, m)).toBe(80); + }); + + it("awards 10-point role diversity bonus for different roles", () => { + const m1 = member({ skills: baseSkills, tags: [], role: "frontend" }); + const m2 = member({ skills: baseSkills, tags: [], role: "backend" }); + // Same skills → skillScore=100, no tags → tagScore=100, different roles → bonus=10 + expect(calculateCompatibility(m1, m2)).toBe(90); + }); + + it("penalises love/veto conflicts by 20 points each", () => { + const m1 = member({ tags: [{ name: "React", sentiment: "love" }] }); + const m2 = member({ tags: [{ name: "React", sentiment: "veto" }] }); + // tagScore = 100 - 20 = 80; skillScore = 100 (identical mid-level skills); no role bonus + expect(calculateCompatibility(m1, m2)).toBe(72); + }); + + it("penalises shared veto tags by 5 points each", () => { + const m1 = member({ tags: [{ name: "PHP", sentiment: "veto" }] }); + const m2 = member({ tags: [{ name: "PHP", sentiment: "veto" }] }); + // tagScore = 100 - 5 = 95 + expect(calculateCompatibility(m1, m2)).toBe(78); + }); + + it("floors tag compatibility at 0 when conflicts exceed 100", () => { + const m1 = member({ + tags: [ + { name: "A", sentiment: "love" }, + { name: "B", sentiment: "love" }, + { name: "C", sentiment: "love" }, + { name: "D", sentiment: "love" }, + { name: "E", sentiment: "love" }, + { name: "F", sentiment: "love" }, + ], + }); + const m2 = member({ + tags: [ + { name: "A", sentiment: "veto" }, + { name: "B", sentiment: "veto" }, + { name: "C", sentiment: "veto" }, + { name: "D", sentiment: "veto" }, + { name: "E", sentiment: "veto" }, + { name: "F", sentiment: "veto" }, + ], + }); + // 6 love/veto conflicts → tagScore = max(0, 100 - 120) = 0 + // skillScore = 100, tagScore = 0, roleBonus = 0 + // result = Math.round(100*0.4 + 0*0.4 + 0) = 40 + expect(calculateCompatibility(m1, m2)).toBe(40); + }); + + it("lowers score for members with very different skill levels", () => { + const specialist = member({ + skills: { frontend: 10, backend: 1, design: 1, data: 1, devops: 1, soft: 1 }, + }); + const generalist = member({ + skills: { frontend: 5, backend: 5, design: 5, data: 5, devops: 5, soft: 5 }, + }); + const identical = member({ skills: baseSkills }); + expect(calculateCompatibility(specialist, generalist)).toBeLessThan( + calculateCompatibility(identical, identical), + ); + }); + + it("returns a value in the range [0, 90]", () => { + const m1 = member({ skills: baseSkills, role: "frontend" }); + const m2 = member({ + skills: { frontend: 1, backend: 10, design: 1, data: 10, devops: 10, soft: 1 }, + role: "backend", + }); + const score = calculateCompatibility(m1, m2); + expect(score).toBeGreaterThanOrEqual(0); + expect(score).toBeLessThanOrEqual(90); + }); +}); + +// ── scoreSkillsForRole ─────────────────────────────────────────────────────── + +describe("scoreSkillsForRole", () => { + it("scores a frontend specialist higher for frontend than backend", () => { + const frontendSkills = { frontend: 9, backend: 2, design: 5, data: 2, devops: 2, soft: 5 }; + const frontendScore = scoreSkillsForRole(frontendSkills, "frontend"); + const backendScore = scoreSkillsForRole(frontendSkills, "backend"); + expect(frontendScore).toBeGreaterThan(backendScore); + }); + + it("scores a backend specialist higher for backend than frontend", () => { + const backendSkills = { frontend: 2, backend: 9, design: 2, data: 7, devops: 7, soft: 5 }; + expect(scoreSkillsForRole(backendSkills, "backend")).toBeGreaterThan( + scoreSkillsForRole(backendSkills, "frontend"), + ); + }); + + it("returns 100 for perfect skills in any known role", () => { + const perfect = { frontend: 10, backend: 10, design: 10, data: 10, devops: 10, soft: 10 }; + expect(scoreSkillsForRole(perfect, "frontend")).toBe(100); + expect(scoreSkillsForRole(perfect, "backend")).toBe(100); + expect(scoreSkillsForRole(perfect, "design")).toBe(100); + }); + + it("uses equal weights for unknown roles", () => { + const even = { frontend: 6, backend: 6, design: 6, data: 6, devops: 6, soft: 6 }; + // Equal skills → equal weights → score = 60 + expect(scoreSkillsForRole(even, "unknown-role")).toBe(60); + }); + + it("returns a value in [0, 100]", () => { + const skills = { frontend: 3, backend: 7, design: 5, data: 4, devops: 6, soft: 8 }; + const score = scoreSkillsForRole(skills, "devops"); + expect(score).toBeGreaterThanOrEqual(0); + expect(score).toBeLessThanOrEqual(100); + }); +}); + +// ── filterMembers ──────────────────────────────────────────────────────────── + +describe("filterMembers", () => { + const members = [ + member({ uid: "1", role: "frontend", squadStatus: "open" }), + member({ uid: "2", role: "backend", squadStatus: "looking" }), + member({ uid: "3", role: "frontend", squadStatus: "closed" }), + member({ uid: "4", role: "design", squadStatus: "open" }), + ]; + + it("returns all members when no filters are provided", () => { + expect(filterMembers(members)).toHaveLength(4); + }); + + it("filters by role", () => { + const result = filterMembers(members, "frontend"); + expect(result).toHaveLength(2); + expect(result.every((m) => m.role === "frontend")).toBe(true); + }); + + it("filters by squadStatus", () => { + const result = filterMembers(members, undefined, "open"); + expect(result).toHaveLength(2); + expect(result.every((m) => m.squadStatus === "open")).toBe(true); + }); + + it("filters by both role and squadStatus (AND logic)", () => { + const result = filterMembers(members, "frontend", "open"); + expect(result).toHaveLength(1); + expect(result[0].uid).toBe("1"); + }); + + it("returns empty array when no members match", () => { + expect(filterMembers(members, "devops")).toHaveLength(0); + }); +}); + +// ── sortByCompatibility ────────────────────────────────────────────────────── + +describe("sortByCompatibility", () => { + it("places the most compatible member first", () => { + const target = member({ uid: "target", skills: baseSkills, tags: [] }); + const similar = member({ + uid: "similar", + skills: baseSkills, + tags: [], + }); + const different = member({ + uid: "different", + skills: { frontend: 1, backend: 9, design: 1, data: 9, devops: 9, soft: 1 }, + tags: [{ name: "React", sentiment: "veto" }], + }); + + const sorted = sortByCompatibility([different, similar], target); + expect(sorted[0].uid).toBe("similar"); + expect(sorted[1].uid).toBe("different"); + }); + + it("preserves all members in output", () => { + const target = member({ uid: "t" }); + const pool = [member({ uid: "a" }), member({ uid: "b" }), member({ uid: "c" })]; + expect(sortByCompatibility(pool, target)).toHaveLength(3); + }); +}); + +// ── getTopCompatibleMembers ────────────────────────────────────────────────── + +describe("getTopCompatibleMembers", () => { + it("returns at most `limit` members", () => { + const target = member({ uid: "t" }); + const pool = Array.from({ length: 20 }, (_, i) => member({ uid: String(i) })); + expect(getTopCompatibleMembers(pool, target, 5)).toHaveLength(5); + }); + + it("returns all members when pool is smaller than limit", () => { + const target = member({ uid: "t" }); + const pool = [member({ uid: "a" }), member({ uid: "b" })]; + expect(getTopCompatibleMembers(pool, target, 10)).toHaveLength(2); + }); + + it("defaults to top 10", () => { + const target = member({ uid: "t" }); + const pool = Array.from({ length: 15 }, (_, i) => member({ uid: String(i) })); + expect(getTopCompatibleMembers(pool, target)).toHaveLength(10); + }); +}); diff --git a/src/domain/usecases/compatibilityAlgorithm.ts b/src/domain/usecases/compatibilityAlgorithm.ts new file mode 100644 index 0000000..23ceb4a --- /dev/null +++ b/src/domain/usecases/compatibilityAlgorithm.ts @@ -0,0 +1,144 @@ +import type { Member } from "@/domain/entities/Member"; +import type { Tag, SkillRadar } from "@/domain/entities/Shared"; + +/** + * Calculate compatibility score between two members + * Based on skills overlap, shared tags preferences, and mutual interest + * + * Score: 0-100 where 100 is perfect compatibility + * + * Factors: + * - Skill overlap (40%) + * - Tag preferences (40%) + * - Role diversity bonus (20%) + */ +export function calculateCompatibility(member1: Member, member2: Member): number { + const skillScore = calculateSkillOverlap(member1.skills, member2.skills); + const tagScore = calculateTagCompatibility(member1.tags, member2.tags); + const roleBonus = member1.role !== member2.role ? 10 : 0; + + return Math.round(skillScore * 0.4 + tagScore * 0.4 + roleBonus); +} + +/** + * Calculate skill overlap between two members (0-100) + * Higher score means more similar skill levels + */ +function calculateSkillOverlap(skills1: SkillRadar, skills2: SkillRadar): number { + const skillNames = ["frontend", "backend", "design", "data", "devops", "soft"] as const; + let totalDifference = 0; + + for (const skill of skillNames) { + const diff = Math.abs(skills1[skill] - skills2[skill]); + totalDifference += diff; + } + + const maxDifference = 9 * skillNames.length; // max diff is 9 per skill + const overlap = 100 - (totalDifference / maxDifference) * 100; + + return Math.max(0, overlap); +} + +/** + * Calculate tag compatibility (0-100) + * Penalizes veto conflicts, rewards shared loves + */ +function calculateTagCompatibility(tags1: Tag[], tags2: Tag[]): number { + const set1 = new Map(tags1.map((t) => [t.name, t.sentiment])); + const set2 = new Map(tags2.map((t) => [t.name, t.sentiment])); + + let compatibility = 100; + + // Check all tags from both members + const allTags = new Set([...set1.keys(), ...set2.keys()]); + + for (const tag of allTags) { + const sentiment1 = set1.get(tag); + const sentiment2 = set2.get(tag); + + // Veto conflicts: if one loves and other vetoes, penalize heavily + if ( + (sentiment1 === "love" && sentiment2 === "veto") || + (sentiment1 === "veto" && sentiment2 === "love") + ) { + compatibility -= 20; + } + // Both veto: conflict but not as bad + else if (sentiment1 === "veto" && sentiment2 === "veto") { + compatibility -= 5; + } + } + + return Math.max(0, compatibility); +} + +/** + * Score a member's skills for a specific role (0-100) + * Used for ranking profiles in discover + */ +export function scoreSkillsForRole(skills: SkillRadar, role: string): number { + const roleSkillWeights: Record>> = { + frontend: { frontend: 0.4, design: 0.2, soft: 0.2, backend: 0.1, data: 0.05, devops: 0.05 }, + backend: { backend: 0.4, data: 0.2, devops: 0.15, soft: 0.15, frontend: 0.05, design: 0.05 }, + design: { design: 0.4, frontend: 0.2, soft: 0.25, backend: 0.05, data: 0.05, devops: 0.05 }, + data: { data: 0.4, backend: 0.2, devops: 0.15, soft: 0.15, frontend: 0.05, design: 0.05 }, + devops: { devops: 0.4, backend: 0.2, data: 0.15, soft: 0.15, frontend: 0.05, design: 0.05 }, + }; + + const weights = roleSkillWeights[role] || { + frontend: 0.167, + backend: 0.167, + design: 0.167, + data: 0.167, + devops: 0.167, + soft: 0.167, + }; + + let score = 0; + let totalWeight = 0; + + (Object.entries(weights) as Array<[keyof SkillRadar, number]>).forEach(([skill, weight]) => { + score += (skills[skill] / 10) * weight * 100; + totalWeight += weight; + }); + + return Math.round(score / totalWeight); +} + +/** + * Filter members based on criteria + */ +export function filterMembers(members: Member[], role?: string, squadStatus?: string): Member[] { + return members.filter((member) => { + if (role && member.role !== role) { + return false; + } + if (squadStatus && member.squadStatus !== squadStatus) { + return false; + } + return true; + }); +} + +/** + * Sort members by compatibility with a target member + */ +export function sortByCompatibility(members: Member[], targetMember: Member): Member[] { + const scored = members.map((member) => ({ + member, + score: calculateCompatibility(targetMember, member), + })); + + return scored.sort((a, b) => b.score - a.score).map((item) => item.member); +} + +/** + * Get top compatible members + */ +export function getTopCompatibleMembers( + members: Member[], + targetMember: Member, + limit: number = 10, +): Member[] { + return sortByCompatibility(members, targetMember).slice(0, limit); +} diff --git a/src/domain/usecases/index.ts b/src/domain/usecases/index.ts new file mode 100644 index 0000000..d2d3449 --- /dev/null +++ b/src/domain/usecases/index.ts @@ -0,0 +1 @@ +export * from "./compatibilityAlgorithm"; diff --git a/src/features/discover/components/AccessDeniedState.tsx b/src/features/discover/components/AccessDeniedState.tsx new file mode 100644 index 0000000..3ae7085 --- /dev/null +++ b/src/features/discover/components/AccessDeniedState.tsx @@ -0,0 +1,7 @@ +import { AccessDeniedState as SharedAccessDeniedState } from "@/shared/components/states/AccessDeniedState"; + +export function AccessDeniedState() { + return ( + + ); +} diff --git a/src/features/discover/components/DiscoverFilters.tsx b/src/features/discover/components/DiscoverFilters.tsx new file mode 100644 index 0000000..1bfec71 --- /dev/null +++ b/src/features/discover/components/DiscoverFilters.tsx @@ -0,0 +1,128 @@ +import { Filter, Search } from "lucide-react"; + +import { ROLE_OPTIONS, STATUS_OPTIONS } from "../constants/discover.constants"; + +interface DiscoverFiltersProps { + searchQuery: string; + selectedRole: string; + selectedStatus: string; + selectedTag: string; + popularTags: string[]; + setSearchQuery: (value: string) => void; + setSelectedRole: (value: string) => void; + setSelectedStatus: (value: string) => void; + setSelectedTag: (value: string) => void; + clearSelectedTag: () => void; +} + +export function DiscoverFilters({ + searchQuery, + selectedRole, + selectedStatus, + selectedTag, + popularTags, + setSearchQuery, + setSelectedRole, + setSelectedStatus, + setSelectedTag, + clearSelectedTag, +}: DiscoverFiltersProps) { + return ( +
+

+ FILTRAR OPERADORES +

+ +
+
+ +
+ + setSearchQuery(e.target.value)} + placeholder="Buscar operador..." + className="w-full pl-12 pr-4 py-3 neo-border bg-neo-bg font-bold focus:bg-white transition-all outline-none" + /> +
+
+ +
+ + +
+ +
+ + +
+
+ + {popularTags.length > 0 && ( +
+ + Filtrar por Paixão: + + + {popularTags.map((tag) => { + const isSelected = selectedTag === tag; + + return ( + + ); + })} + + {selectedTag && ( + + )} +
+ )} +
+ ); +} diff --git a/src/features/discover/components/DiscoverHeader.tsx b/src/features/discover/components/DiscoverHeader.tsx new file mode 100644 index 0000000..1d08e2e --- /dev/null +++ b/src/features/discover/components/DiscoverHeader.tsx @@ -0,0 +1,33 @@ +interface DiscoverHeaderProps { + totalProfiles: number; +} + +export function DiscoverHeader({ totalProfiles }: DiscoverHeaderProps) { + return ( +
+
+

+ DESCOBRIR_ +

+ +
+

+ MATCHMAKING DA COMUNIDADE TECH FLORIPA '26 +

+ +
+ + STATUS: OPERAÇÕES ABERTAS +
+
+
+ +
+
+ OPERADORES + [{totalProfiles}] +
+
+
+ ); +} \ No newline at end of file diff --git a/src/features/discover/components/DiscoverToast.tsx b/src/features/discover/components/DiscoverToast.tsx new file mode 100644 index 0000000..a87bd22 --- /dev/null +++ b/src/features/discover/components/DiscoverToast.tsx @@ -0,0 +1,33 @@ +import { AlertTriangle, X as XIcon } from "lucide-react"; +import { motion } from "motion/react"; + +import type { ToastState } from "../model/discover.types"; + +interface DiscoverToastProps { + toast: ToastState; + onClose: () => void; +} + +export function DiscoverToast({ toast, onClose }: DiscoverToastProps) { + return ( + + +

{toast.message}

+ +
+ ); +} diff --git a/src/features/discover/components/EmptyProfilesState.tsx b/src/features/discover/components/EmptyProfilesState.tsx new file mode 100644 index 0000000..3aa6a23 --- /dev/null +++ b/src/features/discover/components/EmptyProfilesState.tsx @@ -0,0 +1,15 @@ +import { Zap } from "lucide-react"; + +export function EmptyProfilesState() { + return ( +
+ +

+ NENHUM OPERADOR COMPATÍVEL +

+

+ Tente ajustar seus filtros de busca para encontrar perfis correspondentes. +

+
+ ); +} \ No newline at end of file diff --git a/src/features/discover/components/ProfilesGrid.tsx b/src/features/discover/components/ProfilesGrid.tsx new file mode 100644 index 0000000..8fe4a45 --- /dev/null +++ b/src/features/discover/components/ProfilesGrid.tsx @@ -0,0 +1,56 @@ +import { useVirtualizer } from "@tanstack/react-virtual"; +import { useRef } from "react"; + +import type { Profile } from "../model/discover.types"; + +import { EmptyProfilesState } from "./EmptyProfilesState"; + +import ProfileCard from "@/shared/components/ui/ProfileCard"; + +interface ProfilesGridProps { + profiles: Profile[]; + onProfileClick: (profile: Profile) => void; +} + +export function ProfilesGrid({ profiles, onProfileClick }: ProfilesGridProps) { + const parentRef = useRef(null); + + const virtualizer = useVirtualizer({ + count: profiles.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 300, + overscan: 5, + }); + + if (profiles.length === 0) { + return ; + } + + return ( +
+
+ {virtualizer.getVirtualItems().map((item) => ( +
+
+ onProfileClick(profiles[item.index])} + /> +
+
+ ))} +
+
+ ); +} diff --git a/src/features/discover/components/RoastModal.tsx b/src/features/discover/components/RoastModal.tsx new file mode 100644 index 0000000..824494a --- /dev/null +++ b/src/features/discover/components/RoastModal.tsx @@ -0,0 +1,43 @@ +import { getDisplayedRoast } from "../model/discover.selectors"; +import type { Profile } from "../model/discover.types"; + +import type { RoastPersona } from "@/domain/entities/Shared"; +import { RoastModal as SharedRoastModal } from "@/shared/components/ui/RoastModal"; + +interface RoastModalProps { + profile: Profile; + activePersonaView: RoastPersona | null; + isGenerating: boolean; + onClose: () => void; + onSelectPersona: (persona: RoastPersona) => void; + onGenerateRoast: (profile: Profile, persona: RoastPersona) => void; +} + +export function RoastModal({ + profile, + activePersonaView, + isGenerating, + onClose, + onSelectPersona, + onGenerateRoast, +}: RoastModalProps) { + return ( + { + onSelectPersona("brutal"); + void onGenerateRoast(profile, "brutal"); + }} + onGenerateMild={() => { + onSelectPersona("mild"); + void onGenerateRoast(profile, "mild"); + }} + /> + ); +} diff --git a/src/features/discover/constants/discover.constants.ts b/src/features/discover/constants/discover.constants.ts new file mode 100644 index 0000000..4f168ac --- /dev/null +++ b/src/features/discover/constants/discover.constants.ts @@ -0,0 +1,21 @@ +export const ROLE_OPTIONS = [ + "ALL", + "Frontend Infiltrator", + "Backend Architect", + "Data Scientist", + "Hardware Operator", + "Vibe Coder / AI Master", + "UI/UX Designer", + "DevOps Engineer", + "Cyber Security", + "Fullstack Generalist", +] as const; + +export const STATUS_OPTIONS = [ + { value: "ALL", label: "TODAS AS DISPONIBILIDADES" }, + { value: "looking", label: "BUSCANDO EQUIPE" }, + { value: "open", label: "ABERTO A PROPOSTAS" }, + { value: "complete", label: "EQUIPE FORMADA" }, +] as const; + +export const TOAST_DURATION_MS = 5000; \ No newline at end of file diff --git a/src/features/discover/hooks/useDiscoverFilters.ts b/src/features/discover/hooks/useDiscoverFilters.ts new file mode 100644 index 0000000..54a3251 --- /dev/null +++ b/src/features/discover/hooks/useDiscoverFilters.ts @@ -0,0 +1,46 @@ +import { useMemo } from "react"; + +import { filterProfiles, getPopularTags } from "../model/discover.selectors"; +import type { Profile } from "../model/discover.types"; +import { useDiscoverFiltersStore } from "../store/discoverFilters"; + +export function useDiscoverFilters(profiles: Profile[]) { + const { + searchQuery, + selectedRole, + selectedStatus, + selectedTag, + setSearchQuery, + setSelectedRole, + setSelectedStatus, + setSelectedTag, + clearSelectedTag, + } = useDiscoverFiltersStore(); + + const popularTags = useMemo(() => getPopularTags(profiles), [profiles]); + + const filteredProfiles = useMemo( + () => + filterProfiles(profiles, { + searchQuery, + selectedRole, + selectedStatus, + selectedTag, + }), + [profiles, searchQuery, selectedRole, selectedStatus, selectedTag], + ); + + return { + searchQuery, + selectedRole, + selectedStatus, + selectedTag, + popularTags, + filteredProfiles, + setSearchQuery, + setSelectedRole, + setSelectedStatus, + setSelectedTag, + clearSelectedTag, + }; +} diff --git a/src/features/discover/hooks/useProfilesRealtime.ts b/src/features/discover/hooks/useProfilesRealtime.ts new file mode 100644 index 0000000..a7a6cba --- /dev/null +++ b/src/features/discover/hooks/useProfilesRealtime.ts @@ -0,0 +1,19 @@ +import { useMemo } from "react"; + +import { sortProfiles } from "../model/discover.selectors"; +import type { Profile } from "../model/discover.types"; + +import { useFirestoreSubscription } from "@/shared/hooks/useFirestoreSubscription"; + +export function useProfilesRealtime(currentUserId?: string) { + const { data, loading, error } = useFirestoreSubscription({ + collectionName: "profiles", + }); + + const profiles = useMemo( + () => (currentUserId ? sortProfiles(data, currentUserId) : []), + [data, currentUserId], + ); + + return { profiles, loading, error }; +} diff --git a/src/features/discover/hooks/useRoastProfile.ts b/src/features/discover/hooks/useRoastProfile.ts new file mode 100644 index 0000000..136cf02 --- /dev/null +++ b/src/features/discover/hooks/useRoastProfile.ts @@ -0,0 +1,70 @@ +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; + +import { getInitialPersona } from "../model/discover.selectors"; +import type { Profile, RoastPersona } from "../model/discover.types"; +import { updateProfile } from "../services/discover.repository"; + +import { firestoreLog, apiLog } from "@/shared/lib/logger/logger"; +import { requestRoast } from "@/shared/services/roast.service"; + +interface UseRoastProfileParams { + showToast: (message: string, type?: "error" | "info") => void; +} + +export function useRoastProfile({ showToast }: UseRoastProfileParams) { + const [selectedProfile, setSelectedProfile] = useState(null); + const [activePersonaView, setActivePersonaView] = useState(null); + + const roastMutation = useMutation({ + mutationFn: ({ profile, persona }: { profile: Profile; persona: RoastPersona }) => + requestRoast({ memberId: profile.id, memberData: profile, persona }), + onSuccess: async (data, { profile, persona }) => { + if (!data.roast) { + showToast(`Erro ao gerar a Sina: ${data.error ?? "Resposta inesperada do servidor."}`); + return; + } + const updateData = + persona === "brutal" + ? { roastBrutal: data.roast, updatedAt: new Date() } + : { roastMild: data.roast, updatedAt: new Date() }; + + try { + await updateProfile(profile.id, updateData); + } catch (dbError) { + firestoreLog.error("Erro ao salvar sina no banco:", dbError); + } + + setSelectedProfile({ ...profile, ...updateData }); + }, + onError: (error) => { + apiLog.error("Erro ao chamar o roast:", error); + showToast("Sem conexão com o servidor de IA. Verifique sua rede e tente novamente."); + }, + }); + + function openProfile(profile: Profile) { + setSelectedProfile(profile); + setActivePersonaView(getInitialPersona(profile)); + } + + function executeRoast(profile: Profile, persona: RoastPersona) { + setActivePersonaView(persona); + const existingField = persona === "brutal" ? profile.roastBrutal : profile.roastMild; + if (existingField) { + setSelectedProfile(profile); + return; + } + roastMutation.mutate({ profile, persona }); + } + + return { + selectedProfile, + activePersonaView, + isGenerating: roastMutation.isPending, + openProfile, + closeProfile: () => setSelectedProfile(null), + executeRoast, + setActivePersonaView, + }; +} diff --git a/src/features/discover/hooks/useToast.ts b/src/features/discover/hooks/useToast.ts new file mode 100644 index 0000000..332685d --- /dev/null +++ b/src/features/discover/hooks/useToast.ts @@ -0,0 +1,35 @@ +import { useCallback, useRef, useState } from "react"; + +import { TOAST_DURATION_MS } from "../constants/discover.constants"; +import type { ToastState, ToastType } from "../model/discover.types"; + +export function useToast() { + const [toast, setToast] = useState(null); + const timeoutRef = useRef(null); + + const hideToast = useCallback(() => { + if (timeoutRef.current) { + window.clearTimeout(timeoutRef.current); + } + + setToast(null); + }, []); + + const showToast = useCallback((message: string, type: ToastType = "error") => { + if (timeoutRef.current) { + window.clearTimeout(timeoutRef.current); + } + + setToast({ message, type }); + + timeoutRef.current = window.setTimeout(() => { + setToast(null); + }, TOAST_DURATION_MS); + }, []); + + return { + toast, + showToast, + hideToast, + }; +} diff --git a/src/features/discover/model/discover.selectors.ts b/src/features/discover/model/discover.selectors.ts new file mode 100644 index 0000000..62bdfa5 --- /dev/null +++ b/src/features/discover/model/discover.selectors.ts @@ -0,0 +1,71 @@ +import type { Profile } from "./discover.types"; + +import { sortByCurrentUserAndName } from "@/shared/lib/utils/entity"; + +export function sortProfiles(profiles: Profile[], currentUserId?: string): Profile[] { + return sortByCurrentUserAndName(profiles, currentUserId); +} + +export function getPopularTags(profiles: Profile[]): string[] { + const counts: Record = {}; + + profiles.forEach((profile) => { + profile.canvas?.loves?.forEach((tag) => { + counts[tag] = (counts[tag] || 0) + 1; + }); + }); + + return Object.entries(counts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 8) + .map(([tag]) => tag); +} + +export function filterProfiles( + profiles: Profile[], + filters: { + searchQuery: string; + selectedRole: string; + selectedStatus: string; + selectedTag: string; + }, +): Profile[] { + const query = filters.searchQuery.trim().toLowerCase(); + + return profiles.filter((profile) => { + const matchesSearch = + !query || + profile.name?.toLowerCase().includes(query) || + profile.github?.toLowerCase().includes(query) || + profile.bio?.toLowerCase().includes(query); + + const matchesRole = + filters.selectedRole === "ALL" || + profile.primaryRole === filters.selectedRole || + profile.secondaryRoles?.includes(filters.selectedRole); + + const matchesStatus = + filters.selectedStatus === "ALL" || profile.status === filters.selectedStatus; + + const matchesTag = + !filters.selectedTag || + profile.canvas?.loves?.includes(filters.selectedTag) || + profile.canvas?.comfort?.includes(filters.selectedTag); + + return Boolean(matchesSearch && matchesRole && matchesStatus && matchesTag); + }); +} + +export function getInitialPersona(profile: Profile) { + if (profile.roastBrutal) return "brutal" as const; + if (profile.roastMild) return "mild" as const; + return "brutal" as const; +} + +export function getDisplayedRoast(profile: Profile, persona: "brutal" | "mild" | null): string { + if (persona === "brutal") { + return profile.roastBrutal || profile.roast || ""; + } + + return profile.roastMild || profile.roastBrutal || profile.roast || ""; +} diff --git a/src/features/discover/model/discover.types.ts b/src/features/discover/model/discover.types.ts new file mode 100644 index 0000000..c34c76d --- /dev/null +++ b/src/features/discover/model/discover.types.ts @@ -0,0 +1,43 @@ +export type { RoastPersona } from "@/domain/entities/Shared"; + +export type ToastType = "error" | "info"; + +export type ProfileStatus = "looking" | "open" | "complete"; + +export interface ProfileCanvas { + loves?: string[]; + comfort?: string[]; + vetoes?: string[]; +} + +export interface Profile { + id: string; + name?: string; + github?: string; + bio?: string; + primaryRole?: string; + secondaryRoles?: string[]; + status?: ProfileStatus; + roast?: string; + roastBrutal?: string; + roastMild?: string; + canvas?: ProfileCanvas; + updatedAt?: Date; +} + +export interface ToastState { + message: string; + type: ToastType; +} + +export interface DiscoverFiltersState { + searchQuery: string; + selectedRole: string; + selectedStatus: string; + selectedTag: string; +} + +export interface RoastApiResponse { + roast?: string; + error?: string; +} diff --git a/src/features/discover/pages/DiscoverPage.tsx b/src/features/discover/pages/DiscoverPage.tsx new file mode 100644 index 0000000..66ddfe5 --- /dev/null +++ b/src/features/discover/pages/DiscoverPage.tsx @@ -0,0 +1,61 @@ +import { AnimatePresence, motion } from "motion/react"; + +import { AccessDeniedState } from "../components/AccessDeniedState"; +import { DiscoverFilters } from "../components/DiscoverFilters"; +import { DiscoverHeader } from "../components/DiscoverHeader"; +import { DiscoverToast } from "../components/DiscoverToast"; +import { ProfilesGrid } from "../components/ProfilesGrid"; +import { RoastModal } from "../components/RoastModal"; +import { useDiscoverFilters } from "../hooks/useDiscoverFilters"; +import { useProfilesRealtime } from "../hooks/useProfilesRealtime"; +import { useRoastProfile } from "../hooks/useRoastProfile"; +import { useToast } from "../hooks/useToast"; + +import { useAuth } from "@/contexts/useAuth"; + +export default function DiscoverPage() { + const { user } = useAuth(); + + const { toast, showToast, hideToast } = useToast(); + const { profiles } = useProfilesRealtime(user?.uid); + + const filters = useDiscoverFilters(profiles); + + const roast = useRoastProfile({ showToast }); + + return ( + + + {toast && } + + + + + {!user ? ( + + ) : ( +
+ + +
+ )} + + + {roast.selectedProfile && ( + + )} + +
+ ); +} diff --git a/src/features/discover/services/discover.repository.ts b/src/features/discover/services/discover.repository.ts new file mode 100644 index 0000000..0e0944f --- /dev/null +++ b/src/features/discover/services/discover.repository.ts @@ -0,0 +1,9 @@ +import { doc, updateDoc } from "firebase/firestore"; + +import type { Profile } from "../model/discover.types"; + +import { db } from "@/shared/lib/firebase/firebase.client"; + +export async function updateProfile(profileId: string, data: Partial) { + await updateDoc(doc(db, "profiles", profileId), data); +} diff --git a/src/features/discover/store/discoverFilters.ts b/src/features/discover/store/discoverFilters.ts new file mode 100644 index 0000000..d45e8f7 --- /dev/null +++ b/src/features/discover/store/discoverFilters.ts @@ -0,0 +1,25 @@ +import { create } from "zustand"; + +interface DiscoverFiltersState { + searchQuery: string; + selectedRole: string; + selectedStatus: string; + selectedTag: string; + setSearchQuery: (q: string) => void; + setSelectedRole: (r: string) => void; + setSelectedStatus: (s: string) => void; + setSelectedTag: (t: string) => void; + clearSelectedTag: () => void; +} + +export const useDiscoverFiltersStore = create((set) => ({ + searchQuery: "", + selectedRole: "ALL", + selectedStatus: "ALL", + selectedTag: "", + setSearchQuery: (searchQuery) => set({ searchQuery }), + setSelectedRole: (selectedRole) => set({ selectedRole }), + setSelectedStatus: (selectedStatus) => set({ selectedStatus }), + setSelectedTag: (selectedTag) => set({ selectedTag }), + clearSelectedTag: () => set({ selectedTag: "" }), +})); diff --git a/src/features/guilda/components/GuildAvatar.tsx b/src/features/guilda/components/GuildAvatar.tsx new file mode 100644 index 0000000..94c2128 --- /dev/null +++ b/src/features/guilda/components/GuildAvatar.tsx @@ -0,0 +1,37 @@ +import { useMemo, useState } from "react"; + +import type { AvatarProps } from "../model/guilda.types"; +import { getAvatarSources } from "../utils/guilda.formatters"; + +export function GuildAvatar({ member, currentUser, getGithubUrl }: AvatarProps) { + const [imageIndex, setImageIndex] = useState(0); + + const photoUrlToUse = + member.photoURL || + (currentUser && currentUser.uid === member.id ? currentUser.photoURL || undefined : undefined); + + const sources = useMemo( + () => getAvatarSources(photoUrlToUse, member.github, getGithubUrl), + [photoUrlToUse, member.github, getGithubUrl], + ); + + const currentSrc = sources[imageIndex]; + + if (!currentSrc) { + return ( +
+ {member.name?.[0] || "?"} +
+ ); + } + + return ( + {member.name} setImageIndex((index) => index + 1)} + /> + ); +} diff --git a/src/features/guilda/components/GuildHeader.tsx b/src/features/guilda/components/GuildHeader.tsx new file mode 100644 index 0000000..de2c78e --- /dev/null +++ b/src/features/guilda/components/GuildHeader.tsx @@ -0,0 +1,31 @@ +interface GuildHeaderProps { + totalMembers: number; +} + +export function GuildHeader({ totalMembers }: GuildHeaderProps) { + return ( +
+
+

+ A Guilda_ +

+
+

+ Esquadrão Operacional Tech Floripa '26 +

+
+ + STATUS: PRONTO PARA COMBATE +
+
+
+ +
+
+ OPERADORES + [{totalMembers}] +
+
+
+ ); +} diff --git a/src/features/guilda/components/GuildMemberCard.tsx b/src/features/guilda/components/GuildMemberCard.tsx new file mode 100644 index 0000000..ea74e98 --- /dev/null +++ b/src/features/guilda/components/GuildMemberCard.tsx @@ -0,0 +1,247 @@ +import type { User } from "firebase/auth"; +import { Github, Linkedin, Skull } from "lucide-react"; +import { motion } from "motion/react"; +import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"; + +import { getCardPalette, getRadarData } from "../model/guilda.selectors"; +import type { GuildMember, RoastStep } from "../model/guilda.types"; +import { getGithubUrl, getLinkedinUrl } from "../utils/guilda.formatters"; + +import { GuildAvatar } from "./GuildAvatar"; + +interface GuildMemberCardProps { + member: GuildMember; + colorIndex: number; + user: User; + roastActiveMember: string | null; + roastStep: RoastStep; + roastLogs: string[]; + onOpenRoastSelection: (memberId: string) => void; + onExecuteRoast: (member: GuildMember, persona: "brutal" | "mild") => void; +} + +export function GuildMemberCard({ + member, + colorIndex, + user, + roastActiveMember, + roastStep, + roastLogs, + onOpenRoastSelection, + onExecuteRoast, +}: GuildMemberCardProps) { + const colors = getCardPalette(colorIndex); + + return ( +
+
+
+ + {member.primaryRole || "OPERADOR"} + + {member.secondaryRoles && member.secondaryRoles.length > 0 && ( +
+ {member.secondaryRoles.map((role) => ( + + {role} + + ))} +
+ )} +
+ + ID_{member.id.slice(0, 5)} + +
+ +
+
+
+
+ +
+ +
+ +

+ {member.name || "NOME_NULO"} +

+ +
+ {member.github && ( + + HUB + + )} + {member.linkedin && ( + + IN + + )} +
+
+ +
+
+
+ + + + + +
+ +
+
+ + PAIXÕES (LOVES) + +
+ {member.canvas?.loves && member.canvas.loves.length > 0 ? ( + member.canvas.loves.map((item) => ( + + {item} + + )) + ) : ( + Mistério. + )} +
+
+ +
+ + OPERO BEM + +
+ {member.canvas?.comfort && member.canvas.comfort.length > 0 ? ( + member.canvas.comfort.map((item) => ( + + {item} + + )) + ) : ( + Neutro. + )} +
+
+ +
+ + NEM F*DENDO + +
+ {member.canvas?.veto && member.canvas.veto.length > 0 ? ( + member.canvas.veto.map((item) => ( + + {item} + + )) + ) : ( + Faz qualquer jogo. + )} +
+
+
+
+
+ + {user.uid === member.id && ( +
+ {roastActiveMember !== member.id ? ( + + ) : roastStep === "selecting" ? ( +
+ + +
+ ) : ( +
+ {roastLogs.map((log, index) => ( + + {">"} {log} + + ))} + + + +
+ )} +
+ )} +
+ ); +} diff --git a/src/features/guilda/components/GuildMembersGrid.tsx b/src/features/guilda/components/GuildMembersGrid.tsx new file mode 100644 index 0000000..948cea8 --- /dev/null +++ b/src/features/guilda/components/GuildMembersGrid.tsx @@ -0,0 +1,43 @@ +import type { User } from "firebase/auth"; + +import type { GuildMember, RoastStep } from "../model/guilda.types"; + +import { GuildMemberCard } from "./GuildMemberCard"; + +interface GuildMembersGridProps { + members: GuildMember[]; + user: User; + roastActiveMember: string | null; + roastStep: RoastStep; + roastLogs: string[]; + onOpenRoastSelection: (memberId: string) => void; + onExecuteRoast: (member: GuildMember, persona: "brutal" | "mild") => void; +} + +export function GuildMembersGrid({ + members, + user, + roastActiveMember, + roastStep, + roastLogs, + onOpenRoastSelection, + onExecuteRoast, +}: GuildMembersGridProps) { + return ( +
+ {members.map((member, index) => ( + + ))} +
+ ); +} diff --git a/src/features/guilda/components/GuildRoastModal.tsx b/src/features/guilda/components/GuildRoastModal.tsx new file mode 100644 index 0000000..f3cfeaa --- /dev/null +++ b/src/features/guilda/components/GuildRoastModal.tsx @@ -0,0 +1,42 @@ +import type { GuildMember } from "../model/guilda.types"; + +import type { RoastPersona } from "@/domain/entities/Shared"; +import { RoastModal as SharedRoastModal } from "@/shared/components/ui/RoastModal"; + +interface GuildRoastModalProps { + selectedMember: GuildMember; + activePersonaView: RoastPersona | null; + onClose: () => void; + onGeneratePersona: (member: GuildMember, persona: RoastPersona) => void; +} + +export function GuildRoastModal({ + selectedMember, + activePersonaView, + onClose, + onGeneratePersona, +}: GuildRoastModalProps) { + const roastText = + activePersonaView === "brutal" + ? selectedMember.roastBrutal || selectedMember.roast + : selectedMember.roastMild || selectedMember.roastBrutal || selectedMember.roast; + + return ( + { + onClose(); + onGeneratePersona(selectedMember, "brutal"); + }} + onGenerateMild={() => { + onClose(); + onGeneratePersona(selectedMember, "mild"); + }} + /> + ); +} diff --git a/src/features/guilda/components/GuildStates.tsx b/src/features/guilda/components/GuildStates.tsx new file mode 100644 index 0000000..c954eab --- /dev/null +++ b/src/features/guilda/components/GuildStates.tsx @@ -0,0 +1,19 @@ +import { Zap } from "lucide-react"; + +import { AccessDeniedState as SharedAccessDeniedState } from "@/shared/components/states/AccessDeniedState"; + +export function GuildAccessDeniedState() { + return ( + + ); +} + +export function GuildLoadingState() { + return ( +
+

+ AGUARDANDO OPERADORES... +

+
+ ); +} diff --git a/src/features/guilda/constants/guilda.constants.ts b/src/features/guilda/constants/guilda.constants.ts new file mode 100644 index 0000000..38455f7 --- /dev/null +++ b/src/features/guilda/constants/guilda.constants.ts @@ -0,0 +1,13 @@ +export const GUILD_CARD_PALETTES = [ + { bg: "bg-neo-lime", accent: "bg-neo-pink", text: "text-neo-black" }, + { bg: "bg-neo-pink", accent: "bg-neo-cyan", text: "text-white" }, + { bg: "bg-neo-cyan", accent: "bg-neo-yellow", text: "text-neo-black" }, + { bg: "bg-neo-yellow", accent: "bg-neo-lime", text: "text-neo-black" }, +] as const; + +export const ROAST_LOGS_SEQUENCE = [ + "Iniciando conexão neural...", + "Lendo vetos e paixões...", + "Avaliando nível de vibração AI...", + "Preparando o veredito final...", +] as const; diff --git a/src/features/guilda/hooks/useGuildMembersRealtime.ts b/src/features/guilda/hooks/useGuildMembersRealtime.ts new file mode 100644 index 0000000..ace9771 --- /dev/null +++ b/src/features/guilda/hooks/useGuildMembersRealtime.ts @@ -0,0 +1,19 @@ +import { useMemo } from "react"; + +import { sortMembers } from "../model/guilda.selectors"; +import type { GuildMember } from "../model/guilda.types"; + +import { useFirestoreSubscription } from "@/shared/hooks/useFirestoreSubscription"; + +export function useGuildMembersRealtime(currentUserId?: string) { + const { data, loading, error } = useFirestoreSubscription({ + collectionName: "profiles", + }); + + const members = useMemo( + () => (currentUserId ? sortMembers(data, currentUserId) : []), + [data, currentUserId], + ); + + return { members, loading, error }; +} diff --git a/src/features/guilda/hooks/useGuildRoast.ts b/src/features/guilda/hooks/useGuildRoast.ts new file mode 100644 index 0000000..2212790 --- /dev/null +++ b/src/features/guilda/hooks/useGuildRoast.ts @@ -0,0 +1,85 @@ +import { useMutation } from "@tanstack/react-query"; +import { useEffect, useRef } from "react"; + +import { ROAST_LOGS_SEQUENCE } from "../constants/guilda.constants"; +import type { GuildMember, RoastPersona } from "../model/guilda.types"; +import { saveRoast } from "../services/guilda.repository"; +import { useGuildRoastStore } from "../store/guildRoast"; + +import { requestRoast } from "@/shared/services/roast.service"; + +function getRoastByPersona(member: GuildMember, persona: RoastPersona) { + return persona === "brutal" ? member.roastBrutal : member.roastMild; +} + +export function useGuildRoast() { + const store = useGuildRoastStore(); + const logsIntervalRef = useRef(null); + + const clearLogsInterval = () => { + if (logsIntervalRef.current) { + window.clearInterval(logsIntervalRef.current); + logsIntervalRef.current = null; + } + }; + + useEffect(() => clearLogsInterval, []); + + const startLogs = (persona: RoastPersona) => { + const sequence = [`Selecionando persona: ${persona.toUpperCase()}...`, ...ROAST_LOGS_SEQUENCE]; + let currentLog = 0; + store.setRoastLogs([sequence[0]]); + clearLogsInterval(); + logsIntervalRef.current = window.setInterval(() => { + currentLog += 1; + if (currentLog < sequence.length) { + store.appendRoastLog(sequence[currentLog]); + } + }, 1500); + }; + + const roastMutation = useMutation({ + mutationFn: ({ member, persona }: { member: GuildMember; persona: RoastPersona }) => + requestRoast({ memberId: member.id, memberData: member, persona }), + onMutate: ({ persona }) => { + startLogs(persona); + }, + onSuccess: async (data, { member, persona }) => { + if (!data.roast) { + console.error("Erro no backend:", data); + return; + } + let updateData: Partial & { updatedAt?: Date } = {}; + try { + updateData = await saveRoast(member.id, data.roast, persona); + } catch (dbError) { + console.error("Erro ao salvar sina no banco:", dbError); + } + store.setSelectedMember({ ...member, ...updateData }); + }, + onError: (error) => { + console.error("Erro ao chamar o roast:", error); + }, + onSettled: () => { + clearLogsInterval(); + store.setRoastStep(null); + store.setRoastActiveMember(null); + }, + }); + + function executeRoast(member: GuildMember, persona: RoastPersona) { + store.setActivePersonaView(persona); + const existingRoast = getRoastByPersona(member, persona); + if (existingRoast) { + store.setSelectedMember(member); + return; + } + store.setRoastStep("loading"); + roastMutation.mutate({ member, persona }); + } + + return { + ...store, + executeRoast, + }; +} diff --git a/src/features/guilda/model/guilda.selectors.ts b/src/features/guilda/model/guilda.selectors.ts new file mode 100644 index 0000000..30a36ca --- /dev/null +++ b/src/features/guilda/model/guilda.selectors.ts @@ -0,0 +1,26 @@ +import { GUILD_CARD_PALETTES } from "../constants/guilda.constants"; + +import type { GuildMember, MemberSkills, RadarDatum } from "./guilda.types"; + +import { sortByCurrentUserAndName } from "@/shared/lib/utils/entity"; + +export function sortMembers(members: GuildMember[], currentUserId: string) { + return sortByCurrentUserAndName(members, currentUserId); +} + +export function getCardPalette(index: number) { + return GUILD_CARD_PALETTES[index % GUILD_CARD_PALETTES.length]; +} + +export function getRadarData(skills?: MemberSkills): RadarDatum[] { + if (!skills) return []; + + return [ + { subject: "Front", A: skills.frontend || 0, fullMark: 10 }, + { subject: "Back", A: skills.backend || 0, fullMark: 10 }, + { subject: "UX", A: skills.ux_ui || 0, fullMark: 10 }, + { subject: "Dados", A: skills.dados || 0, fullMark: 10 }, + { subject: "Hard", A: skills.hardware_android || 0, fullMark: 10 }, + { subject: "AI", A: skills.vibe_coding || 0, fullMark: 10 }, + ]; +} diff --git a/src/features/guilda/model/guilda.types.ts b/src/features/guilda/model/guilda.types.ts new file mode 100644 index 0000000..325aac2 --- /dev/null +++ b/src/features/guilda/model/guilda.types.ts @@ -0,0 +1,46 @@ +import type { User } from "firebase/auth"; + +export type { RoastPersona } from "@/domain/entities/Shared"; +export type RoastStep = "selecting" | "loading" | null; + +export interface MemberSkills { + frontend?: number; + backend?: number; + ux_ui?: number; + dados?: number; + hardware_android?: number; + vibe_coding?: number; +} + +export interface MemberCanvas { + loves?: string[]; + comfort?: string[]; + veto?: string[]; +} + +export interface GuildMember { + id: string; + name?: string; + photoURL?: string; + github?: string; + linkedin?: string; + primaryRole?: string; + secondaryRoles?: string[]; + skills?: MemberSkills; + canvas?: MemberCanvas; + roast?: string; + roastBrutal?: string; + roastMild?: string; +} + +export interface AvatarProps { + member: GuildMember; + currentUser: User | null; + getGithubUrl: (value: string) => string; +} + +export interface RadarDatum { + subject: string; + A: number; + fullMark: number; +} diff --git a/src/features/guilda/pages/GuildaPage.tsx b/src/features/guilda/pages/GuildaPage.tsx new file mode 100644 index 0000000..d59b70e --- /dev/null +++ b/src/features/guilda/pages/GuildaPage.tsx @@ -0,0 +1,53 @@ +import { AnimatePresence, motion } from "motion/react"; + +import { GuildHeader } from "../components/GuildHeader"; +import { GuildMembersGrid } from "../components/GuildMembersGrid"; +import { GuildRoastModal } from "../components/GuildRoastModal"; +import { GuildAccessDeniedState, GuildLoadingState } from "../components/GuildStates"; +import { useGuildMembersRealtime } from "../hooks/useGuildMembersRealtime"; +import { useGuildRoast } from "../hooks/useGuildRoast"; + +import { useAuth } from "@/contexts/useAuth"; + +export default function GuildaPage() { + const { user } = useAuth(); + const { members } = useGuildMembersRealtime(user?.uid); + const roast = useGuildRoast(); + + return ( + + + + {!user ? ( + + ) : members.length === 0 ? ( + + ) : ( + + )} + + + {roast.selectedMember && ( + + )} + + + ); +} diff --git a/src/features/guilda/services/guilda.repository.ts b/src/features/guilda/services/guilda.repository.ts new file mode 100644 index 0000000..f217ee8 --- /dev/null +++ b/src/features/guilda/services/guilda.repository.ts @@ -0,0 +1,17 @@ +import { doc, updateDoc } from "firebase/firestore"; + +import type { GuildMember } from "../model/guilda.types"; + +import type { RoastPersona } from "@/domain/entities/Shared"; +import { db } from "@/shared/lib/firebase/firebase.client"; + +export async function saveRoast(memberId: string, roast: string, persona: RoastPersona) { + const updateData: Partial & { updatedAt: Date } = { + updatedAt: new Date(), + ...(persona === "brutal" ? { roastBrutal: roast } : { roastMild: roast }), + }; + + await updateDoc(doc(db, "profiles", memberId), updateData); + + return updateData; +} diff --git a/src/features/guilda/store/guildRoast.ts b/src/features/guilda/store/guildRoast.ts new file mode 100644 index 0000000..5a02692 --- /dev/null +++ b/src/features/guilda/store/guildRoast.ts @@ -0,0 +1,35 @@ +import { create } from "zustand"; + +import type { GuildMember, RoastPersona, RoastStep } from "../model/guilda.types"; + +interface GuildRoastState { + selectedMember: GuildMember | null; + roastActiveMember: string | null; + roastStep: RoastStep; + roastLogs: string[]; + activePersonaView: RoastPersona | null; + setSelectedMember: (member: GuildMember | null) => void; + setRoastActiveMember: (id: string | null) => void; + setRoastStep: (step: RoastStep) => void; + appendRoastLog: (log: string) => void; + setRoastLogs: (logs: string[]) => void; + setActivePersonaView: (persona: RoastPersona | null) => void; + closeSelectedMember: () => void; + openRoastSelection: (memberId: string) => void; +} + +export const useGuildRoastStore = create((set) => ({ + selectedMember: null, + roastActiveMember: null, + roastStep: null, + roastLogs: [], + activePersonaView: null, + setSelectedMember: (selectedMember) => set({ selectedMember }), + setRoastActiveMember: (roastActiveMember) => set({ roastActiveMember }), + setRoastStep: (roastStep) => set({ roastStep }), + setRoastLogs: (roastLogs) => set({ roastLogs }), + appendRoastLog: (log) => set((state) => ({ roastLogs: [...state.roastLogs, log].slice(-3) })), + setActivePersonaView: (activePersonaView) => set({ activePersonaView }), + closeSelectedMember: () => set({ selectedMember: null }), + openRoastSelection: (roastActiveMember) => set({ roastActiveMember, roastStep: "selecting" }), +})); diff --git a/src/features/guilda/utils/guilda.formatters.ts b/src/features/guilda/utils/guilda.formatters.ts new file mode 100644 index 0000000..0cab613 --- /dev/null +++ b/src/features/guilda/utils/guilda.formatters.ts @@ -0,0 +1,47 @@ +export function getGithubUrl(value: string) { + if (!value) return ""; + + const clean = value + .trim() + .replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//i, "") + .replace(/\/$/, "") + .replace(/^@/, ""); + + return `https://github.com/${clean}`; +} + +export function getLinkedinUrl(value: string) { + if (!value) return ""; + + const clean = value + .trim() + .replace(/^(?:https?:\/\/)?(?:[\w-]+\.)?linkedin\.com\/(?:in|profile)\//i, "") + .replace(/\/$/, "") + .replace(/^@/, ""); + + return `https://linkedin.com/in/${clean}`; +} + +export function getAvatarSources( + photoURL: string | undefined, + github: string | undefined, + githubUrlFormatter: (value: string) => string, +) { + const sources: string[] = []; + + if (photoURL) { + let photo = photoURL; + if (photo.includes("googleusercontent.com") && photo.includes("=s96-c")) { + photo = photo.replace("=s96-c", "=s400-c"); + } else if (photo.includes("googleusercontent.com") && !photo.includes("=")) { + photo = `${photo}=s400-c`; + } + sources.push(photo); + } + + if (github) { + sources.push(`${githubUrlFormatter(github)}.png`); + } + + return sources; +} diff --git a/src/features/landing/Landing.tsx b/src/features/landing/Landing.tsx new file mode 100644 index 0000000..afcc1d7 --- /dev/null +++ b/src/features/landing/Landing.tsx @@ -0,0 +1,29 @@ +import { useNavigate } from "react-router-dom"; + +import { CtaSection } from "./components/CtaSection"; +import { Footer } from "./components/Footer"; +import { HeroSection } from "./components/HeroSection"; +import { HowItWorksSection } from "./components/HowItWorksSection"; +import { NeoParticles } from "./components/NeoParticles"; +import { STEPS } from "./constants/steps"; + +import { useAuth } from "@/contexts/useAuth"; + +export default function Landing() { + const navigate = useNavigate(); + const { user } = useAuth(); + + return ( +
+ + navigate("/onboarding")} + onNavigateGuilda={() => navigate("/guilda")} + /> + + navigate("/onboarding")} /> +
+
+ ); +} diff --git a/src/features/landing/components/CtaSection.tsx b/src/features/landing/components/CtaSection.tsx new file mode 100644 index 0000000..410fcd1 --- /dev/null +++ b/src/features/landing/components/CtaSection.tsx @@ -0,0 +1,44 @@ +import { ArrowRight } from "lucide-react"; +import { motion } from "motion/react"; + +import { Button } from "@/shared/components/ui/Button"; + +interface Props { + onNavigateOnboarding: () => void; +} + +export function CtaSection({ onNavigateOnboarding }: Props) { + return ( +
+ + {/* Grid overlay */} +
+ +

+ NÃO CHEGUE SOZINHO_ +

+ +

+ 40% dos participantes de hackathons desistem por falta de equipe. Não seja esse. +

+ +
+ +
+ +
+ ); +} diff --git a/src/features/landing/components/Footer.tsx b/src/features/landing/components/Footer.tsx new file mode 100644 index 0000000..c4505cc --- /dev/null +++ b/src/features/landing/components/Footer.tsx @@ -0,0 +1,21 @@ +import { Zap } from "lucide-react"; + +export function Footer() { + return ( +
+
+
+
+ +
+ + MATCH_TECH + +
+

+ Feito com ☕ por Tony Max & Squad +

+
+
+ ); +} diff --git a/src/features/landing/components/HeroSection.tsx b/src/features/landing/components/HeroSection.tsx new file mode 100644 index 0000000..7916663 --- /dev/null +++ b/src/features/landing/components/HeroSection.tsx @@ -0,0 +1,74 @@ +import { ArrowRight } from "lucide-react"; +import { motion } from "motion/react"; + +import { Button } from "@/shared/components/ui/Button"; + +interface Props { + // eslint-disable-next-line + user: any; // TODO + onNavigateOnboarding: () => void; + onNavigateGuilda: () => void; +} + +export function HeroSection({ user, onNavigateOnboarding, onNavigateGuilda }: Props) { + return ( +
+ + {/* Badge */} +
+ + HACKATHON TECH FLORIPA 2026 + +
+ + {/* Headline */} +

+ ENCONTRE SUA +
+ + EQUIPE IDEAL_ + +

+ +

+ Mapeie suas skills reais. Descubra perfis complementares. +
+ Monte seu time antes do kickoff. +

+ + {/* CTAs */} +
+ + + {user && ( + + )} +
+ + {!user && ( +

+ Já tem conta?{" "} + +

+ )} +
+
+ ); +} diff --git a/src/features/landing/components/HowItWorksSection.tsx b/src/features/landing/components/HowItWorksSection.tsx new file mode 100644 index 0000000..b83edb7 --- /dev/null +++ b/src/features/landing/components/HowItWorksSection.tsx @@ -0,0 +1,63 @@ +import { motion } from "motion/react"; + +import type { Step } from "../constants/steps"; + +interface Props { + steps: Step[]; +} + +export function HowItWorksSection({ steps }: Props) { + return ( +
+ +
+

+ COMO FUNCIONA_ +

+
+
+ +
+ {steps.map((step, i) => { + const Icon = step.icon; + return ( + +
+ {/* Step number */} +
+ {i + 1} +
+ + {/* Decorative accent */} +
+ +
+ +

+ {step.title} +

+

{step.desc}

+
+
+ + ); + })} +
+
+
+ ); +} diff --git a/src/features/landing/components/NeoParticles.tsx b/src/features/landing/components/NeoParticles.tsx new file mode 100644 index 0000000..5dab679 --- /dev/null +++ b/src/features/landing/components/NeoParticles.tsx @@ -0,0 +1,129 @@ +import { useEffect, useRef } from "react"; + +type ParticleType = "square" | "triangle" | "cross"; + +interface Particle { + x: number; + y: number; + size: number; + speedX: number; + speedY: number; + color: string; + rotation: number; + rotationSpeed: number; + type: ParticleType; +} + +const COLORS = ["#B8FF29", "#FF2E93", "#00E5FF", "#FFE600", "#1A1A1A"]; + +function drawSquare(ctx: CanvasRenderingContext2D, size: number) { + ctx.fillRect(-size / 2, -size / 2, size, size); + ctx.strokeRect(-size / 2, -size / 2, size, size); +} + +function drawTriangle(ctx: CanvasRenderingContext2D, size: number) { + ctx.beginPath(); + ctx.moveTo(0, -size / 2); + ctx.lineTo(size / 2, size / 2); + ctx.lineTo(-size / 2, size / 2); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); +} + +function drawCross(ctx: CanvasRenderingContext2D, size: number) { + const ts = size / 3; + ctx.beginPath(); + ctx.moveTo(-ts, -size / 2); ctx.lineTo(ts, -size / 2); ctx.lineTo(ts, -ts); + ctx.lineTo(size / 2, -ts); ctx.lineTo(size / 2, ts); ctx.lineTo(ts, ts); + ctx.lineTo(ts, size / 2); ctx.lineTo(-ts, size / 2); ctx.lineTo(-ts, ts); + ctx.lineTo(-size / 2, ts); ctx.lineTo(-size / 2, -ts); ctx.lineTo(-ts, -ts); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); +} + +export function NeoParticles() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animationFrameId: number; + let particles: Particle[] = []; + let width = window.innerWidth; + let height = window.innerHeight; + + const init = () => { + width = window.innerWidth; + height = window.innerHeight; + canvas.width = width; + canvas.height = height; + + const count = Math.min(width / 30, 40); + particles = Array.from({ length: count }, () => ({ + x: Math.random() * width, + y: Math.random() * height, + size: Math.random() * 20 + 10, + speedX: (Math.random() - 0.5) * 2, + speedY: (Math.random() - 0.5) * 2, + color: COLORS[Math.floor(Math.random() * COLORS.length)], + rotation: Math.random() * Math.PI * 2, + rotationSpeed: (Math.random() - 0.5) * 0.05, + type: (Math.random() > 0.6 ? "square" : Math.random() > 0.5 ? "triangle" : "cross") as ParticleType, + })); + }; + + const animate = () => { + ctx.clearRect(0, 0, width, height); + ctx.lineWidth = 3; + ctx.strokeStyle = "#000"; + + particles.forEach(p => { + p.x += p.speedX; + p.y += p.speedY; + p.rotation += p.rotationSpeed; + + if (p.x < -p.size) p.x = width + p.size; + if (p.x > width + p.size) p.x = -p.size; + if (p.y < -p.size) p.y = height + p.size; + if (p.y > height + p.size) p.y = -p.size; + + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate(p.rotation); + ctx.fillStyle = p.color; + ctx.shadowColor = "#000"; + ctx.shadowOffsetX = 4; + ctx.shadowOffsetY = 4; + + if (p.type === "square") drawSquare(ctx, p.size); + if (p.type === "triangle") drawTriangle(ctx, p.size); + if (p.type === "cross") drawCross(ctx, p.size); + + ctx.restore(); + }); + + animationFrameId = requestAnimationFrame(animate); + }; + + init(); + animate(); + window.addEventListener("resize", init); + + return () => { + window.removeEventListener("resize", init); + cancelAnimationFrame(animationFrameId); + }; + }, []); + + return ( + + ); +} diff --git a/src/features/landing/constants/steps.ts b/src/features/landing/constants/steps.ts new file mode 100644 index 0000000..4a12253 --- /dev/null +++ b/src/features/landing/constants/steps.ts @@ -0,0 +1,34 @@ +import { Users, Zap, Target } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; + +export interface Step { + icon: LucideIcon; + title: string; + desc: string; + color: "lime" | "yellow" | "pink"; + accent: string; +} + +export const STEPS: Step[] = [ + { + icon: Target, + title: "MAPEIE_", + desc: "Registre suas skills, paixões e vetos. Sem currículo genérico — só verdade.", + color: "lime", + accent: "bg-neo-pink", + }, + { + icon: Users, + title: "DESCUBRA_", + desc: "Explore perfis complementares ao seu. Filtre por role, stack ou afinidade.", + color: "yellow", + accent: "bg-neo-cyan", + }, + { + icon: Zap, + title: "CONECTE_", + desc: "Forme sua equipe ideal antes do kickoff. Sem surpresas no dia D.", + color: "pink", + accent: "bg-neo-lime", + }, +]; diff --git a/src/features/onboarding/components/ArsenalCalibration.tsx b/src/features/onboarding/components/ArsenalCalibration.tsx new file mode 100644 index 0000000..e2acf37 --- /dev/null +++ b/src/features/onboarding/components/ArsenalCalibration.tsx @@ -0,0 +1,80 @@ +import { Info, ShieldCheck } from "lucide-react"; +import { motion } from "motion/react"; + +import type { OnboardingForm } from "../types"; + +interface Props { + form: OnboardingForm; +} + +export function ArsenalCalibration({ form }: Props) { + const total = form.loves.length + form.veto.length; + const isCalibrated = total >= 10; + + return ( +
+
+ + {/* Header */} +
+
+
+
+ +
+

+ CALIBRAGEM DO ARSENAL +

+
+

+ Precisamos conhecer seu perfil para gerar sua ID única. +

+
+ +
+ + {Math.min(total, 10)} / 10 + +
+
+ + {/* Progress bar */} +
+ +
+ + {/* Info boxes */} +
+
+ +

+ POR QUE ISSO? + Para que nossa IA crie um mapeamento justo e te conecte às melhores missões, precisamos + de pelo menos 10 opiniões (Amo ou Veto) sobre as tecnologias abaixo. +

+
+ +
+
+

+ STATUS DO SISTEMA: + {isCalibrated + ? "SISTEMA CALIBRADO! VOCÊ JÁ PODE ENTRAR, MAS QUANTO MAIS TAGS MARCAR, MELHOR SERÁ SEU MATCH COM A GUILDA." + : `AGUARDANDO DADOS: MARQUE MAIS ${10 - total} TAGS.`} +

+
+
+
+ ); +} diff --git a/src/features/onboarding/components/AuthGate/AuthGate.tsx b/src/features/onboarding/components/AuthGate/AuthGate.tsx new file mode 100644 index 0000000..2756968 --- /dev/null +++ b/src/features/onboarding/components/AuthGate/AuthGate.tsx @@ -0,0 +1,47 @@ +import { CompletingMagicLink } from "./CompletingMagicLink"; +import { LoginScreen } from "./LoginScreen"; +import { MagicLinkConfirmScreen } from "./MagicLinkConfirmScreen"; +import { MagicLinkSentScreen } from "./MagicLinkSentScreen"; + +interface Props { + signIn: () => Promise; + sendMagicLink: (email: string) => Promise; + magicLinkSent: boolean; + magicLinkEmail: string; + completingMagicLink: boolean; + pendingMagicLinkUrl: string | null; + resetMagicLinkState: () => void; + confirmMagicLinkEmail: (email: string) => Promise; +} + +export function AuthGate({ + signIn, + sendMagicLink, + magicLinkSent, + magicLinkEmail, + completingMagicLink, + pendingMagicLinkUrl, + resetMagicLinkState, + confirmMagicLinkEmail, +}: Props) { + if (completingMagicLink) return ; + + if (pendingMagicLinkUrl) + return ( + + ); + + if (magicLinkSent) + return ( + + ); + + return ; +} diff --git a/src/features/onboarding/components/AuthGate/CompletingMagicLink.tsx b/src/features/onboarding/components/AuthGate/CompletingMagicLink.tsx new file mode 100644 index 0000000..5ed3c40 --- /dev/null +++ b/src/features/onboarding/components/AuthGate/CompletingMagicLink.tsx @@ -0,0 +1,39 @@ +import { Terminal } from "lucide-react"; + +import { Card } from "@/shared/components/ui/Card"; + +export function CompletingMagicLink() { + return ( +
+
+ +
+
+ +
+

+ VALIDANDO LINK_ +

+
+ +
+
+
+
+
+
+

+ Completando seu login via Magic Link... +

+
+ Decifrando credenciais criptografadas... +
+
+ +
+ ); +} diff --git a/src/features/onboarding/components/AuthGate/LoginScreen.tsx b/src/features/onboarding/components/AuthGate/LoginScreen.tsx new file mode 100644 index 0000000..11cd5e9 --- /dev/null +++ b/src/features/onboarding/components/AuthGate/LoginScreen.tsx @@ -0,0 +1,168 @@ +import { ShieldCheck, Info } from "lucide-react"; +import { motion } from "motion/react"; +import { useState } from "react"; + +import { Button } from "@/shared/components/ui/Button"; +import { Card } from "@/shared/components/ui/Card"; + +interface Props { + signIn: () => Promise; + sendMagicLink: (email: string) => Promise; +} + +export function LoginScreen({ signIn, sendMagicLink }: Props) { + const [loading, setLoading] = useState(false); + const [magicEmail, setMagicEmail] = useState(""); + const [magicLinkLoading, setMagicLinkLoading] = useState(false); + const [magicLinkError, setMagicLinkError] = useState(null); + + const handleGoogleSignIn = async () => { + setLoading(true); + try { + await signIn(); + } finally { + setLoading(false); + } + }; + + const handleMagicLink = async (e: React.FormEvent) => { + e.preventDefault(); + const email = magicEmail.trim(); + if (!email) { + setMagicLinkError("Digite seu email."); + return; + } + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + setMagicLinkError("Email inválido."); + return; + } + + setMagicLinkError(null); + setMagicLinkLoading(true); + try { + await sendMagicLink(email); + } catch (err) { + setMagicLinkError( + err instanceof Error ? err.message : "Erro ao enviar link. Tente novamente.", + ); + } finally { + setMagicLinkLoading(false); + } + }; + + return ( +
+
+ + + + + {/* Header */} +
+
+ +
+

+ ACESSO_RESTRITO +

+
+ +
+

+ O Protocolo de Segurança Tech Floripa exige autenticação de nível 1 antes do mapeamento + de arsenal. Conecte sua identidade para prosseguir. +

+ + {/* Google */} + + + {/* Divider */} +
+
+ OU +
+
+ + {/* Magic Link */} +
+ + { + setMagicEmail(e.target.value); + setMagicLinkError(null); + }} + placeholder="seu@email.com" + className="w-full px-4 py-3 bg-neo-bg font-mono font-bold text-sm border-[3px] border-neo-black shadow-[4px_4px_0_0_#000] focus:shadow-[6px_6px_0_0_#B8FF29] focus:outline-none transition-shadow placeholder:text-neo-black/30" + /> + {magicLinkError && ( +

{magicLinkError}

+ )} + +
+ + {/* Info box */} +
+

+ COMO FUNCIONA O ACESSO? +

+
    +
  • + Google: Conexão instantânea de 1 clique. +
  • +
  • + Link Mágico: Digite seu email, enviamos um + link e você entra sem precisar lembrar de senhas. +
  • +
  • + ⚠️ IMPORTANTE: O link pode cair na pasta de{" "} + SPAM. Verifique lá se atrasar! +
  • +
+
+ + {/* Footer */} +
+
+
+
+
+
+ Escolha seu protocolo de autenticação... +
+
+ +
+ ); +} diff --git a/src/features/onboarding/components/AuthGate/MagicLinkConfirmScreen.tsx b/src/features/onboarding/components/AuthGate/MagicLinkConfirmScreen.tsx new file mode 100644 index 0000000..d88ca9c --- /dev/null +++ b/src/features/onboarding/components/AuthGate/MagicLinkConfirmScreen.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; + +import { Card } from "@/shared/components/ui/Card"; + +interface Props { + confirmMagicLinkEmail: (email: string) => Promise; + resetMagicLinkState: () => void; +} + +export function MagicLinkConfirmScreen({ confirmMagicLinkEmail, resetMagicLinkState }: Props) { + const [magicEmail, setMagicEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!magicEmail.trim()) return; + setLoading(true); + setError(null); + try { + await confirmMagicLinkEmail(magicEmail.trim()); + } catch { + setError("Email inválido ou link expirado. Solicite um novo link."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ +
+
+ 📱 +
+

+ NOVO DISPOSITIVO_ +

+
+ +
+

+ Parece que você abriu o link em um dispositivo diferente. Para sua segurança, confirme + seu email para concluir o login. +

+ +
+ { + setMagicEmail(e.target.value); + setError(null); + }} + placeholder="seu@email.com" + className="w-full px-4 py-3 bg-neo-bg font-mono font-bold text-sm border-[3px] border-neo-black shadow-[4px_4px_0_0_#000] focus:shadow-[6px_6px_0_0_#00E5FF] focus:outline-none transition-shadow" + autoFocus + /> + {error &&

{error}

} + +
+ + +
+
+
+ ); +} diff --git a/src/features/onboarding/components/AuthGate/MagicLinkSentScreen.tsx b/src/features/onboarding/components/AuthGate/MagicLinkSentScreen.tsx new file mode 100644 index 0000000..39fb68e --- /dev/null +++ b/src/features/onboarding/components/AuthGate/MagicLinkSentScreen.tsx @@ -0,0 +1,95 @@ +import { motion } from "motion/react"; +import { useState } from "react"; + +import { Button } from "@/shared/components/ui/Button"; +import { Card } from "@/shared/components/ui/Card"; + +interface Props { + magicLinkEmail: string; + sendMagicLink: (email: string) => Promise; + resetMagicLinkState: () => void; +} + +export function MagicLinkSentScreen({ magicLinkEmail, sendMagicLink, resetMagicLinkState }: Props) { + const [loading, setLoading] = useState(false); + + const handleResend = async () => { + if (!magicLinkEmail) return; + setLoading(true); + try { + await sendMagicLink(magicLinkEmail); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + + +
+
+ 📧 +
+

+ LINK ENVIADO_ +

+
+ +
+

Mandamos um link mágico para:

+ +
+ {magicLinkEmail} +
+ +
+

+ Abra seu email e clique no link para entrar. +

+
+

+ ⚠️ ATENÇÃO: VERIFIQUE O SPAM! +

+

+ Como o link é enviado de forma automática, a mensagem pode ir direto para o seu{" "} + + SPAM ou Lixo Eletrônico + + . Se não chegar em 1 minuto, procure lá! +

+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/features/onboarding/components/ClassSelector.tsx b/src/features/onboarding/components/ClassSelector.tsx new file mode 100644 index 0000000..87eedd1 --- /dev/null +++ b/src/features/onboarding/components/ClassSelector.tsx @@ -0,0 +1,65 @@ +import { Terminal } from "lucide-react"; + +import type { OnboardingForm } from "../types"; + +import { Card } from "@/shared/components/ui/Card"; + +interface Props { + roles: string[]; + form: OnboardingForm; + onToggleRole: (role: string) => void; +} + +export function ClassSelector({ roles, form, onToggleRole }: Props) { + return ( + +
+

+ 02. CLASSES (1st / 2nd) +

+
+ +
+
+ {roles.map((role) => { + const isPrimary = form.primaryRole === role; + const isSecondary = form.secondaryRoles.includes(role); + + return ( + + ); + })} +
+
+
+ ); +} diff --git a/src/features/onboarding/components/GuildPassport.tsx b/src/features/onboarding/components/GuildPassport.tsx new file mode 100644 index 0000000..4cec925 --- /dev/null +++ b/src/features/onboarding/components/GuildPassport.tsx @@ -0,0 +1,209 @@ +import { ShieldCheck } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { Radar, RadarChart, PolarGrid, PolarAngleAxis } from "recharts"; + +import type { OnboardingForm, OnboardingSkills } from "../types"; + +import Avatar from "@/shared/components/ui/Avatar"; +import { Button } from "@/shared/components/ui/Button"; +import { Card } from "@/shared/components/ui/Card"; + +interface Props { + form: OnboardingForm; + skills: OnboardingSkills; + // eslint-disable-next-line + user: any; // TODO + radarData: { subject: string; A: number; fullMark: number }[]; + loading: boolean; + submitError: string | null; + onSubmit: (e?: React.FormEvent) => Promise; +} + +export function GuildPassport({ + form, + skills, + user, + radarData, + loading, + submitError, + onSubmit, +}: Props) { + const totalSentiment = form.loves.length + form.veto.length; + const isUnlocked = totalSentiment >= 10; + + return ( +
+ + {/* Passport header */} +
+
+ PASAPORTE_GUILDA + + Tech_Floripa_2026 + +
+ +
+ +
+ {/* Corner accents */} +
+
+ + {/* Avatar + identity */} +
+
+ +
+
+
+

+ NOME_OPERADOR: +

+

+ {form.name || "Aguardando..."} +

+
+
+

+ CLASSE_PRIMÁRIA: +

+

+ {form.primaryRole || "AGUARDANDO..."} +

+
+ {form.secondaryRoles.length > 0 && ( +
+

+ CLASSES_SEC: +

+
+ {form.secondaryRoles.map((r) => ( + + {r} + + ))} +
+
+ )} +
+
+ + {/* Radar chart */} +
+
+

+ Análise de Campo +

+

+ Vibe: {skills.vibe_coding}/10 +

+
+
+
+ + + + + +
+
+ + {/* Operator ID row */} +
+
+

ID_OPERADOR

+

+ {user?.uid.slice(0, 10).toUpperCase()} +

+
+
+

ORIGEM

+

+ FLN_BRAZIL +

+
+
+
+ + {/* Submit */} +
+ + + + {submitError && ( + + ⚠️ + {submitError} + + )} + +
+ + + {/* Debug monitor */} +
+

> MONITORAMENTO_STATUS:

+
+

LOVES: {form.loves.length}

+

CONFORT: {form.comfort.length}

+

VETOS: {form.veto.length}

+

ROLES: {form.primaryRole ? 1 : 0}/1

+
+ {form.loves.length < 3 && ( +

+ ALERTA: Adicione mais PAIXÕES para calibragem total. +

+ )} +
+
+ ); +} diff --git a/src/features/onboarding/components/IdentityCard.tsx b/src/features/onboarding/components/IdentityCard.tsx new file mode 100644 index 0000000..80cdcb7 --- /dev/null +++ b/src/features/onboarding/components/IdentityCard.tsx @@ -0,0 +1,125 @@ +import { User, Github, Linkedin, ShieldCheck } from "lucide-react"; + +import type { OnboardingForm } from "../types"; + +import { Card } from "@/shared/components/ui/Card"; + +interface Props { + form: OnboardingForm; + onChange: ( + e: React.ChangeEvent, + ) => void; + onBioChange: (e: React.ChangeEvent) => void; +} + +export function IdentityCard({ form, onChange, onBioChange }: Props) { + return ( + +
+

+ 01. IDENTIDADE +

+
+ +
+ {/* Trust bridge callout */} +
+
+ +
+
+

PONTE DE CONFIANÇA:

+

+ O algoritmo da guilda cruza dados do GitHub e LinkedIn para validar xp e sugerir + missões de alto impacto. Sem pontes, você é um fantasma. +

+
+
+ +
+ {/* Name */} +
+ + +
+ + {/* Bio */} +
+ +