criação do website
This commit is contained in:
105
docs/01-project-plan.md
Normal file
105
docs/01-project-plan.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 01 — Plano do Projeto
|
||||
|
||||
## Objetivo
|
||||
|
||||
Criar um **website de administração** que sirva de backend para uma aplicação de liga de futebol. A aplicação cliente já está desenvolvida e lê dados em tempo real da Firebase. Este dashboard é o único ponto de escrita.
|
||||
|
||||
---
|
||||
|
||||
## Problema a Resolver
|
||||
|
||||
A aplicação cliente mostra dados em tempo real mas não tem interface de edição. É necessário um painel de administração que permita:
|
||||
- Atualizar resultados durante os jogos (em tempo real)
|
||||
- Gerir toda a estrutura da liga (clubes, jogadores, jornadas)
|
||||
- Manter estatísticas atualizadas automaticamente
|
||||
|
||||
---
|
||||
|
||||
## Âmbito do Projeto
|
||||
|
||||
### In Scope
|
||||
- Dashboard de administração web (SPA)
|
||||
- Autenticação segura (apenas administradores)
|
||||
- CRUD completo de jogos, clubes, jogadores
|
||||
- Atualização de resultados em tempo real (live)
|
||||
- Gestão de jornadas e classificação
|
||||
- Estatísticas automáticas (calculadas a partir dos resultados)
|
||||
- Interface responsiva (desktop-first, mas funcional em tablet)
|
||||
|
||||
### Out of Scope
|
||||
- Aplicação cliente (já existe)
|
||||
- API REST separada (Firebase é o backend direto)
|
||||
- App móvel de administração
|
||||
- Sistema de utilizadores múltiplos com permissões granulares (v1)
|
||||
|
||||
---
|
||||
|
||||
## Fases de Desenvolvimento
|
||||
|
||||
### Fase 0 — Planeamento e Design (ATUAL)
|
||||
- [x] Definição de requisitos
|
||||
- [x] Stack tecnológica
|
||||
- [x] Arquitetura do sistema
|
||||
- [x] Design system
|
||||
- [x] Firebase schema
|
||||
- [x] Mockup interativo
|
||||
- [ ] Review e aprovação
|
||||
|
||||
### Fase 1 — Setup e Fundação
|
||||
**Estimativa:** 1-2 sessões
|
||||
- [ ] Inicializar projeto Vite + React + TypeScript
|
||||
- [ ] Configurar Firebase SDK
|
||||
- [ ] Configurar React Router
|
||||
- [ ] Implementar autenticação Firebase Auth
|
||||
- [ ] Layout base (sidebar + header + content area)
|
||||
- [ ] Design system (cores, tipografia, componentes base)
|
||||
|
||||
### Fase 2 — Módulo de Jogos (Core)
|
||||
**Estimativa:** 2-3 sessões
|
||||
- [ ] Listagem de jogos por jornada
|
||||
- [ ] Criação de jogo
|
||||
- [ ] **Live Score Editor** — atualização em tempo real
|
||||
- [ ] Finalizar jogo (resultado final)
|
||||
- [ ] Histórico de jogos
|
||||
|
||||
### Fase 3 — Módulo de Gestão
|
||||
**Estimativa:** 2 sessões
|
||||
- [ ] CRUD de Clubes
|
||||
- [ ] CRUD de Jogadores (com clube associado)
|
||||
- [ ] Gestão de Jornadas
|
||||
- [ ] Classificação (calculada automaticamente)
|
||||
|
||||
### Fase 4 — Estatísticas e Extras
|
||||
**Estimativa:** 1-2 sessões
|
||||
- [ ] Artilheiros (golos por jogador)
|
||||
- [ ] Assistências
|
||||
- [ ] Cartões (amarelos/vermelhos)
|
||||
- [ ] Dashboard overview com métricas
|
||||
|
||||
### Fase 5 — Polish e Deploy
|
||||
**Estimativa:** 1 sessão
|
||||
- [ ] Testes e bug fixes
|
||||
- [ ] Otimização de performance
|
||||
- [ ] Deploy (Firebase Hosting)
|
||||
- [ ] Configuração de domínio
|
||||
|
||||
---
|
||||
|
||||
## Critérios de Sucesso
|
||||
|
||||
1. Administrador consegue atualizar um resultado em menos de 10 segundos
|
||||
2. A alteração aparece na app cliente em menos de 2 segundos (Firebase real-time)
|
||||
3. Zero inconsistências na classificação (calculada automaticamente)
|
||||
4. Interface funcional em desktop e tablet
|
||||
5. Acesso protegido por autenticação
|
||||
|
||||
---
|
||||
|
||||
## Riscos e Mitigações
|
||||
|
||||
| Risco | Probabilidade | Mitigação |
|
||||
|---|---|---|
|
||||
| Schema Firebase incompatível com app cliente | Alta | Documentar schema existente antes de escrever |
|
||||
| Conflitos de escrita simultânea | Baixa | Usar Firebase transactions |
|
||||
| Performance com muitos listeners real-time | Média | Limitar listeners ativos, usar pagination |
|
||||
| Segurança (acesso não autorizado) | Alta | Firebase Security Rules rigorosas |
|
||||
164
docs/02-tech-stack.md
Normal file
164
docs/02-tech-stack.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# 02 — Stack Tecnológica
|
||||
|
||||
## Decisão Final
|
||||
|
||||
| Camada | Tecnologia | Versão |
|
||||
|---|---|---|
|
||||
| Framework | React | 18+ |
|
||||
| Build Tool | Vite | 5+ |
|
||||
| Linguagem | TypeScript | 5+ |
|
||||
| Routing | React Router | v6 |
|
||||
| Estado Global | Zustand | 4+ |
|
||||
| Base de Dados | Firebase Firestore | SDK v10 |
|
||||
| Autenticação | Firebase Auth | SDK v10 |
|
||||
| Hosting | Firebase Hosting | — |
|
||||
| Styling | Tailwind CSS | v3 |
|
||||
| Componentes UI | shadcn/ui | latest |
|
||||
| Ícones | Lucide React | latest |
|
||||
| Formulários | React Hook Form + Zod | latest |
|
||||
| Notificações | Sonner (toast) | latest |
|
||||
| Data/Hora | date-fns | latest |
|
||||
| Testes | Vitest + Testing Library | latest |
|
||||
|
||||
---
|
||||
|
||||
## Justificação das Escolhas
|
||||
|
||||
### React + Vite + TypeScript
|
||||
- React é o mais familiar e com maior ecossistema
|
||||
- Vite oferece HMR instantâneo, essencial para desenvolvimento rápido
|
||||
- TypeScript previne erros em runtime, especialmente importante com o schema Firebase
|
||||
|
||||
### Firebase SDK v10 (Modular)
|
||||
- Já é a base de dados usada pela app cliente — **obrigatório** para compatibilidade
|
||||
- SDK v10 modular tem bundle size menor
|
||||
- Firestore real-time listeners são nativos — zero configuração extra para real-time
|
||||
|
||||
### Zustand (em vez de Redux/Context)
|
||||
- Muito mais simples que Redux para este tamanho de projeto
|
||||
- Funciona bem com Firebase listeners
|
||||
- Boilerplate mínimo
|
||||
|
||||
### Tailwind CSS + shadcn/ui
|
||||
- Tailwind permite customização total sem CSS files separados
|
||||
- shadcn/ui oferece componentes acessíveis (Radix UI) com design neutro que se adapta ao nosso design system
|
||||
- shadcn copia o código para o projeto — sem vendor lock-in
|
||||
|
||||
### React Hook Form + Zod
|
||||
- Formulários performantes (sem re-renders desnecessários)
|
||||
- Zod faz validação e inferência de tipos em simultâneo
|
||||
- Schema Zod pode espelhar o schema Firebase
|
||||
|
||||
---
|
||||
|
||||
## Estrutura de Ficheiros do Projeto
|
||||
|
||||
```
|
||||
football-admin/
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── App.tsx # Router principal
|
||||
│ │ └── routes.tsx # Definição de rotas
|
||||
│ ├── components/
|
||||
│ │ ├── ui/ # shadcn/ui components
|
||||
│ │ ├── layout/
|
||||
│ │ │ ├── Sidebar.tsx
|
||||
│ │ │ ├── Header.tsx
|
||||
│ │ │ └── Layout.tsx
|
||||
│ │ ├── games/
|
||||
│ │ │ ├── GameCard.tsx
|
||||
│ │ │ ├── LiveScoreEditor.tsx
|
||||
│ │ │ └── GameForm.tsx
|
||||
│ │ ├── clubs/
|
||||
│ │ ├── players/
|
||||
│ │ └── shared/
|
||||
│ │ ├── DataTable.tsx
|
||||
│ │ ├── ConfirmDialog.tsx
|
||||
│ │ └── LoadingSpinner.tsx
|
||||
│ ├── pages/
|
||||
│ │ ├── Dashboard.tsx
|
||||
│ │ ├── Games.tsx
|
||||
│ │ ├── LiveGame.tsx
|
||||
│ │ ├── Clubs.tsx
|
||||
│ │ ├── Players.tsx
|
||||
│ │ ├── Standings.tsx
|
||||
│ │ ├── Scorers.tsx
|
||||
│ │ └── Login.tsx
|
||||
│ ├── hooks/
|
||||
│ │ ├── useGames.ts
|
||||
│ │ ├── useClubs.ts
|
||||
│ │ ├── usePlayers.ts
|
||||
│ │ └── useAuth.ts
|
||||
│ ├── store/
|
||||
│ │ ├── authStore.ts
|
||||
│ │ └── leagueStore.ts
|
||||
│ ├── lib/
|
||||
│ │ ├── firebase.ts # Firebase config e inicialização
|
||||
│ │ ├── firestore.ts # Helpers de Firestore
|
||||
│ │ └── utils.ts
|
||||
│ ├── types/
|
||||
│ │ └── index.ts # Todos os tipos TypeScript
|
||||
│ └── main.tsx
|
||||
├── .env.local # Firebase config (não commitar)
|
||||
├── .env.example
|
||||
├── firebase.json
|
||||
├── firestore.rules
|
||||
├── vite.config.ts
|
||||
├── tailwind.config.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variáveis de Ambiente
|
||||
|
||||
```bash
|
||||
# .env.local (copiar de .env.example)
|
||||
VITE_FIREBASE_API_KEY=
|
||||
VITE_FIREBASE_AUTH_DOMAIN=
|
||||
VITE_FIREBASE_PROJECT_ID=
|
||||
VITE_FIREBASE_STORAGE_BUCKET=
|
||||
VITE_FIREBASE_MESSAGING_SENDER_ID=
|
||||
VITE_FIREBASE_APP_ID=
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scripts NPM
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"deploy": "npm run build && firebase deploy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comandos de Setup
|
||||
|
||||
```bash
|
||||
# 1. Criar projeto
|
||||
npm create vite@latest football-admin -- --template react-ts
|
||||
cd football-admin
|
||||
|
||||
# 2. Instalar dependências
|
||||
npm install firebase react-router-dom zustand react-hook-form zod @hookform/resolvers date-fns sonner lucide-react
|
||||
|
||||
# 3. Instalar Tailwind
|
||||
npm install -D tailwindcss postcss autoprefixer
|
||||
npx tailwindcss init -p
|
||||
|
||||
# 4. Instalar shadcn/ui
|
||||
npx shadcn@latest init
|
||||
|
||||
# 5. Adicionar componentes shadcn necessários
|
||||
npx shadcn@latest add button input label card badge dialog table tabs select
|
||||
```
|
||||
207
docs/03-architecture.md
Normal file
207
docs/03-architecture.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# 03 — Arquitetura do Sistema
|
||||
|
||||
## Diagrama de Arquitetura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ FIREBASE │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
|
||||
│ │ Firestore │ │ Auth │ │ Hosting │ │
|
||||
│ │ (Database) │ │ (Admin only)│ │ (Deploy) │ │
|
||||
│ └──────┬──────┘ └──────────────┘ └───────────────┘ │
|
||||
│ │ │
|
||||
└──────────┼────────────────────────────────────────────────── ┘
|
||||
│ Real-time listeners
|
||||
┌──────┴──────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────────┐
|
||||
│ ADMIN DASHBOARD │ │ APLICAÇÃO CLIENTE │
|
||||
│ (Este projeto) │ │ (Já desenvolvida) │
|
||||
│ │ │ │
|
||||
│ - Escreve dados │ │ - Lê dados │
|
||||
│ - Autenticado │ │ - Sem autenticação │
|
||||
│ - React SPA │ │ - Apenas leitura │
|
||||
└──────────────────┘ └──────────────────────┘
|
||||
▲ WRITE ▲ READ
|
||||
│ │
|
||||
└────────── Firebase ────────┘
|
||||
(fonte única de verdade)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de Dados
|
||||
|
||||
### Atualização de Resultado em Tempo Real
|
||||
|
||||
```
|
||||
Admin clica "+1 Golo" no Live Score Editor
|
||||
│
|
||||
▼
|
||||
React Hook Form / estado local atualiza
|
||||
│
|
||||
▼
|
||||
Firebase transaction (atómica):
|
||||
- Atualiza game.score
|
||||
- Cria evento no subcollection game.events
|
||||
- Atualiza stats do jogador (se golo com autor)
|
||||
- Recalcula standings
|
||||
│
|
||||
▼
|
||||
Firebase Firestore (source of truth)
|
||||
│
|
||||
├──▶ Admin Dashboard: listener onSnapshot atualiza UI
|
||||
│
|
||||
└──▶ App Cliente: listener onSnapshot recebe atualização
|
||||
(< 2 segundos latência)
|
||||
```
|
||||
|
||||
### Fluxo de Autenticação
|
||||
|
||||
```
|
||||
Utilizador acede ao site
|
||||
│
|
||||
▼
|
||||
Firebase Auth verifica sessão
|
||||
│
|
||||
├── Não autenticado ──▶ Redireciona para /login
|
||||
│
|
||||
└── Autenticado ──▶ Verifica role em Firestore
|
||||
│
|
||||
├── role: "admin" ──▶ Acesso total
|
||||
│
|
||||
└── outros ──▶ Redireciona para /login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arquitetura de Componentes
|
||||
|
||||
```
|
||||
App
|
||||
├── AuthProvider (contexto de autenticação)
|
||||
│ └── Router
|
||||
│ ├── /login → LoginPage
|
||||
│ └── ProtectedRoute (requer auth)
|
||||
│ └── Layout
|
||||
│ ├── Sidebar
|
||||
│ ├── Header
|
||||
│ └── Outlet (conteúdo da página)
|
||||
│ ├── /dashboard → DashboardPage
|
||||
│ ├── /games → GamesPage
|
||||
│ │ └── /games/:id/live → LiveGamePage ⭐
|
||||
│ ├── /clubs → ClubsPage
|
||||
│ │ └── /clubs/:id → ClubDetailPage
|
||||
│ ├── /players → PlayersPage
|
||||
│ ├── /standings → StandingsPage
|
||||
│ └── /scorers → ScorersPage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gestão de Estado
|
||||
|
||||
### Zustand Stores
|
||||
|
||||
```typescript
|
||||
// authStore — autenticação
|
||||
{
|
||||
user: FirebaseUser | null,
|
||||
isAdmin: boolean,
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
// leagueStore — dados da liga (cache local)
|
||||
{
|
||||
clubs: Club[],
|
||||
players: Player[],
|
||||
currentSeason: string,
|
||||
activeGameId: string | null // jogo a decorrer
|
||||
}
|
||||
```
|
||||
|
||||
### Firebase Hooks (React Query pattern)
|
||||
|
||||
Cada módulo tem um hook custom que:
|
||||
1. Subscreve ao Firestore com `onSnapshot`
|
||||
2. Guarda dados no estado local do hook
|
||||
3. Expõe funções de mutação (create, update, delete)
|
||||
4. Faz cleanup do listener no unmount
|
||||
|
||||
```typescript
|
||||
// Exemplo
|
||||
function useGames(jornadaId?: string) {
|
||||
const [games, setGames] = useState<Game[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const q = jornadaId
|
||||
? query(gamesRef, where("jornadaId", "==", jornadaId))
|
||||
: gamesRef
|
||||
|
||||
const unsubscribe = onSnapshot(q, (snapshot) => {
|
||||
setGames(snapshot.docs.map(d => ({ id: d.id, ...d.data() })))
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return () => unsubscribe() // cleanup
|
||||
}, [jornadaId])
|
||||
|
||||
return { games, loading }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Firebase Security Rules
|
||||
|
||||
```javascript
|
||||
rules_version = '2';
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
|
||||
// Função helper: verifica se é admin
|
||||
function isAdmin() {
|
||||
return request.auth != null &&
|
||||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "admin";
|
||||
}
|
||||
|
||||
// Jogos: leitura pública, escrita apenas admin
|
||||
match /games/{gameId} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
|
||||
match /events/{eventId} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
// Clubes: leitura pública, escrita apenas admin
|
||||
match /clubs/{clubId} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
// Jogadores: leitura pública, escrita apenas admin
|
||||
match /players/{playerId} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
// Standings: leitura pública, escrita apenas admin
|
||||
match /standings/{doc} {
|
||||
allow read: if true;
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
|
||||
// Utilizadores: apenas o próprio ou admin
|
||||
match /users/{userId} {
|
||||
allow read: if request.auth.uid == userId || isAdmin();
|
||||
allow write: if isAdmin();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
184
docs/04-design-system.md
Normal file
184
docs/04-design-system.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# 04 — Design System
|
||||
|
||||
## Conceito Visual
|
||||
|
||||
**"Control Room"** — Um dashboard de administração que transmite profissionalismo e controlo. Inspirado em interfaces de broadcasting desportivo e software de gestão profissional. Dark theme como padrão (ambiente de trabalho, ecrãs durante os jogos).
|
||||
|
||||
**Palavras-chave:** Preciso. Funcional. Desportivo. Autoritativo.
|
||||
|
||||
---
|
||||
|
||||
## Paleta de Cores
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Backgrounds */
|
||||
--bg-base: #0a0e1a; /* Azul noite — fundo principal */
|
||||
--bg-surface: #111827; /* Cards e painéis */
|
||||
--bg-elevated: #1a2235; /* Elementos elevados */
|
||||
--bg-overlay: #243047; /* Hover states, dropdowns */
|
||||
|
||||
/* Brand */
|
||||
--brand-primary: #22c55e; /* Verde campo — ação principal */
|
||||
--brand-accent: #3b82f6; /* Azul — informação, links */
|
||||
--brand-danger: #ef4444; /* Vermelho — perigo, cartões vermelhos */
|
||||
--brand-warning: #f59e0b; /* Amarelo — cartões amarelos */
|
||||
|
||||
/* Texto */
|
||||
--text-primary: #f9fafb; /* Texto principal */
|
||||
--text-secondary: #9ca3af; /* Texto secundário */
|
||||
--text-muted: #4b5563; /* Texto inativo */
|
||||
|
||||
/* Bordas */
|
||||
--border: #1e2d45; /* Bordas subtis */
|
||||
--border-active: #22c55e; /* Borda ativa */
|
||||
|
||||
/* Estados */
|
||||
--live-pulse: #ef4444; /* Indicador LIVE */
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--info: #3b82f6;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tipografia
|
||||
|
||||
```css
|
||||
/* Display / Headings — Força e presença */
|
||||
--font-display: 'Bebas Neue', 'Impact', sans-serif;
|
||||
|
||||
/* Interface / Corpo — Legibilidade técnica */
|
||||
--font-body: 'IBM Plex Sans', 'Menlo', monospace-adjacent, sans-serif;
|
||||
|
||||
/* Dados / Números — Clareza em scoreboards */
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
```
|
||||
|
||||
**Escala tipográfica:**
|
||||
```
|
||||
xs: 11px — Labels, metadados
|
||||
sm: 13px — Texto secundário
|
||||
base: 15px — Corpo de texto
|
||||
lg: 17px — Subtítulos
|
||||
xl: 20px — Títulos de secção
|
||||
2xl: 24px — Títulos de página
|
||||
3xl: 32px — Scores (Bebas Neue)
|
||||
4xl: 48px — Score principal live
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Componentes Base
|
||||
|
||||
### Scoreboard (Live)
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ ● LIVE 75' │
|
||||
│ │
|
||||
│ FC Porto 2 — 1 SLB │
|
||||
│ (crest) [grande] (crest)│
|
||||
│ │
|
||||
│ [−] [+] [−] [+] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
- Números do score em Bebas Neue 64px
|
||||
- Indicador LIVE com pulse animation (vermelho)
|
||||
- Botões +/- grandes (acessíveis em tablet)
|
||||
|
||||
### Game Card
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Jornada 15 • Sáb 15 Mar • 20:30 │
|
||||
│ │
|
||||
│ FC Porto — Benfica │
|
||||
│ │
|
||||
│ [Iniciar Jogo] [Editar] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Status Badges
|
||||
- `AGENDADO` — cinza
|
||||
- `A DECORRER` — verde com pulse
|
||||
- `INTERVALO` — amarelo
|
||||
- `TERMINADO` — azul
|
||||
- `ADIADO` — laranja
|
||||
|
||||
### Sidebar
|
||||
```
|
||||
┌─────────┐
|
||||
│ ⚽ ADM │
|
||||
├─────────┤
|
||||
│ Dashboard│
|
||||
│ Jogos │ ← item ativo tem barra verde à esquerda
|
||||
│ Clubes │
|
||||
│ Jogadores│
|
||||
│ Classif. │
|
||||
│ Artilh. │
|
||||
├─────────┤
|
||||
│ Settings │
|
||||
│ Logout │
|
||||
└─────────┘
|
||||
```
|
||||
Largura: 240px desktop, colapsável para 60px (ícones apenas)
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ HEADER (64px) │
|
||||
│ [☰ Logo] Jornada atual: 15/34 [● LIVE: Porto-B] │
|
||||
├───────────┬──────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ SIDEBAR │ CONTENT AREA │
|
||||
│ (240px) │ (flex 1) │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
└───────────┴──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Animações e Microinterações
|
||||
|
||||
| Elemento | Animação | Duração |
|
||||
|---|---|---|
|
||||
| Indicador LIVE | Pulse (scale + opacity) | 1.5s infinite |
|
||||
| Score update | Flash verde → normal | 600ms |
|
||||
| Sidebar item hover | Translate X 4px | 150ms |
|
||||
| Card hover | BoxShadow + Y -2px | 200ms |
|
||||
| Toast notifications | Slide in from right | 300ms |
|
||||
| Página load | Fade in staggered | 400ms |
|
||||
| Botão click | Scale 0.96 | 100ms |
|
||||
|
||||
---
|
||||
|
||||
## Responsividade
|
||||
|
||||
| Breakpoint | Layout |
|
||||
|---|---|
|
||||
| `< 768px` (mobile) | Sidebar escondida (drawer), conteúdo full-width |
|
||||
| `768px-1024px` (tablet) | Sidebar colapsada (ícones), otimizado para live scoring |
|
||||
| `> 1024px` (desktop) | Layout completo |
|
||||
|
||||
**Nota:** O Live Score Editor deve ser especialmente otimizado para tablet — é o mais provável de ser usado durante os jogos.
|
||||
|
||||
---
|
||||
|
||||
## Ícones
|
||||
|
||||
Usar **Lucide React** exclusivamente. Ícones principais:
|
||||
- `Trophy` — classificação
|
||||
- `Users` — clubes
|
||||
- `User` — jogadores
|
||||
- `Calendar` — jornadas
|
||||
- `BarChart` — estatísticas
|
||||
- `Zap` — live/em tempo real
|
||||
- `Plus` / `Minus` — adicionar/remover golos
|
||||
- `Flag` — cartões
|
||||
- `Clock` — tempo de jogo
|
||||
214
docs/05-features.md
Normal file
214
docs/05-features.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# 05 — Funcionalidades Detalhadas
|
||||
|
||||
## Módulo 1: Dashboard (Visão Geral)
|
||||
|
||||
**Rota:** `/dashboard`
|
||||
**Prioridade:** Alta
|
||||
|
||||
### O que mostra:
|
||||
- **Cartão de jogo LIVE** (se houver jogo a decorrer) com link rápido para Live Editor
|
||||
- **Próximos jogos** (2-3 próximas partidas)
|
||||
- **Últimos resultados** (2-3 jogos recentes)
|
||||
- **Métricas rápidas:** Total de jogos, golos marcados, jornada atual
|
||||
- **Top 3 artilheiros**
|
||||
- **Top 3 da classificação**
|
||||
|
||||
### Comportamento:
|
||||
- Se houver jogo LIVE, o cartão aparece em destaque no topo com pulse animation
|
||||
- Dados atualizados em tempo real via Firestore listeners
|
||||
- Links diretos para edição de cada entidade
|
||||
|
||||
---
|
||||
|
||||
## Módulo 2: Gestão de Jogos
|
||||
|
||||
**Rota:** `/games`
|
||||
**Prioridade:** Crítica
|
||||
|
||||
### Lista de Jogos (`/games`)
|
||||
- Filtro por jornada (dropdown ou tabs)
|
||||
- Filtro por estado: Todos / Agendados / Terminados / Live
|
||||
- Cards de jogo com: clubes, data/hora, resultado (se existir), estado
|
||||
- Botão "Novo Jogo"
|
||||
- Botão "Iniciar" (para jogos agendados)
|
||||
- Botão "Live" (para jogos a decorrer — destaque verde)
|
||||
|
||||
### Criar/Editar Jogo (`/games/new` e `/games/:id/edit`)
|
||||
**Campos:**
|
||||
- Jornada (select)
|
||||
- Data e hora
|
||||
- Clube casa (select com crest)
|
||||
- Clube fora (select com crest)
|
||||
- Local/estádio
|
||||
- Estado (agendado/adiado)
|
||||
|
||||
### Live Score Editor (`/games/:id/live`) ⭐ FUNCIONALIDADE CORE
|
||||
**Este é o ecrã mais importante do projeto.**
|
||||
|
||||
#### Layout do ecrã:
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ● LIVE • Jornada 15 • [Pausar] [Terminar] │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ FC PORTO 2 — 1 BENFICA │
|
||||
│ (grande, Bebas Neue) │
|
||||
│ │
|
||||
│ [−] [+Golo] [+Outro] [+Golo] [+] │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ EVENTOS DO JOGO [+ Evento] │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ ⚽ 23' — Mehdi Taremi (FC Porto) │ │
|
||||
│ │ 🟨 31' — João Mário (Benfica) │ │
|
||||
│ │ ⚽ 67' — Gonçalo Ramos (Benfica) │ │
|
||||
│ │ ⚽ 71' — Evanilson (FC Porto) │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Funcionalidades:
|
||||
- **Cronómetro:** Inicia ao clicar "Iniciar Jogo", mostra minuto atual
|
||||
- **Botões de golo:** `+Golo` abre modal para selecionar jogador + minuto
|
||||
- **Outros eventos:** Cartão amarelo, cartão vermelho, substituição
|
||||
- **Desfazer:** Botão de undo no último evento (até 30 segundos)
|
||||
- **Intervalo:** Pausa o cronómetro, muda estado para "Intervalo"
|
||||
- **Terminar:** Confirma resultado final, fecha o jogo
|
||||
|
||||
#### Modal de Golo:
|
||||
- Selecionar jogador (do clube que marcou, lista filtrada)
|
||||
- Minuto (pré-preenchido com minuto atual, editável)
|
||||
- Tipo: Golo normal / Penálti / Autogolo
|
||||
- Assistência (opcional, select com jogadores)
|
||||
- Botão "Confirmar Golo" → Firestore transaction
|
||||
|
||||
#### Firestore Transaction no Golo:
|
||||
```
|
||||
1. Adicionar evento ao subcollection games/:id/events
|
||||
2. Incrementar game.score.home ou game.score.away
|
||||
3. Incrementar player.stats.goals
|
||||
4. Se assistência: incrementar player.stats.assists
|
||||
5. Recalcular standings para ambos os clubes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Módulo 3: Clubes
|
||||
|
||||
**Rota:** `/clubs`
|
||||
**Prioridade:** Média
|
||||
|
||||
### Lista de Clubes
|
||||
- Grid de cards com: escudo, nome, cidade, nº de jogadores
|
||||
- Barra de pesquisa
|
||||
- Botão "Novo Clube"
|
||||
|
||||
### Criar/Editar Clube
|
||||
**Campos:**
|
||||
- Nome do clube
|
||||
- Abreviatura (3 letras, ex: "FCP")
|
||||
- Cidade
|
||||
- Estádio
|
||||
- Cores (principal + secundária — color pickers)
|
||||
- Escudo (upload de imagem — Firebase Storage)
|
||||
- Ano de fundação
|
||||
|
||||
### Detalhe do Clube (`/clubs/:id`)
|
||||
- Info do clube
|
||||
- Lista de jogadores do clube (com link para edição)
|
||||
- Estatísticas: jogos, vitórias, empates, derrotas, golos
|
||||
|
||||
---
|
||||
|
||||
## Módulo 4: Jogadores
|
||||
|
||||
**Rota:** `/players`
|
||||
**Prioridade:** Média
|
||||
|
||||
### Lista de Jogadores
|
||||
- Tabela com: foto, nome, clube (badge), posição, nº camisola, golos, assistências
|
||||
- Filtro por clube
|
||||
- Filtro por posição (GR, DEF, MED, ATA)
|
||||
- Barra de pesquisa
|
||||
- Botão "Novo Jogador"
|
||||
|
||||
### Criar/Editar Jogador
|
||||
**Campos:**
|
||||
- Nome completo
|
||||
- Nome curto (para scoreboards)
|
||||
- Data de nascimento
|
||||
- Nacionalidade
|
||||
- Clube (select)
|
||||
- Posição (GR / DEF / MED / ATA)
|
||||
- Número de camisola
|
||||
- Foto (upload — Firebase Storage)
|
||||
- Estado (ativo / lesionado / suspenso)
|
||||
|
||||
---
|
||||
|
||||
## Módulo 5: Classificação
|
||||
|
||||
**Rota:** `/standings`
|
||||
**Prioridade:** Alta
|
||||
|
||||
### Tabela de Classificação
|
||||
- Posição, clube (escudo + nome), J, V, E, D, GM, GS, DG, Pts
|
||||
- Linha a separar lugares de Champions/Europa/Descida (configurável)
|
||||
- Atualização em tempo real
|
||||
- Possibilidade de **editar manualmente** (override) um valor (com confirmação)
|
||||
|
||||
### Recálculo Automático
|
||||
A classificação é recalculada automaticamente sempre que um jogo é finalizado. Pode também ser acionado manualmente por "Recalcular Classificação".
|
||||
|
||||
---
|
||||
|
||||
## Módulo 6: Artilheiros / Estatísticas
|
||||
|
||||
**Rota:** `/scorers`
|
||||
**Prioridade:** Média
|
||||
|
||||
### Artilheiros
|
||||
- Posição, jogador (foto + nome), clube, golos, assistências
|
||||
- Filtro por tipo (golos / assistências / cartões)
|
||||
|
||||
### Cartões
|
||||
- Tabela de jogadores com mais cartões (amarelos e vermelhos)
|
||||
- Destaque para jogadores suspensos
|
||||
|
||||
---
|
||||
|
||||
## Módulo 7: Jornadas
|
||||
|
||||
**Rota:** `/rounds`
|
||||
**Prioridade:** Média
|
||||
|
||||
### Gestão de Jornadas
|
||||
- Lista de jornadas (1 a N)
|
||||
- Criar nova jornada
|
||||
- Ver jogos de cada jornada
|
||||
- Marcar jornada como atual
|
||||
|
||||
---
|
||||
|
||||
## Funcionalidades Transversais
|
||||
|
||||
### Autenticação
|
||||
- Login com email/password (Firebase Auth)
|
||||
- Proteção de todas as rotas (exceto `/login`)
|
||||
- Sessão persistente
|
||||
- Logout
|
||||
|
||||
### Notificações (Toast)
|
||||
- Sucesso ao guardar dados
|
||||
- Erro com mensagem descritiva
|
||||
- Confirmação de ações destrutivas (modal de confirmação)
|
||||
|
||||
### Confirmação de Ações Destrutivas
|
||||
Sempre que o admin vai:
|
||||
- Apagar um jogo, clube ou jogador
|
||||
- Terminar um jogo
|
||||
- Fazer override na classificação
|
||||
→ Aparece modal de confirmação com texto descritivo do que vai acontecer
|
||||
|
||||
### Auditoria (Nice to Have — v2)
|
||||
Guardar log de todas as alterações: quem fez, o quê, quando.
|
||||
135
docs/06-agent-handoff.md
Normal file
135
docs/06-agent-handoff.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 06 — Guia de Agent Handoff
|
||||
|
||||
> Este documento existe para garantir que qualquer sessão de desenvolvimento pode ser retomada sem perda de contexto.
|
||||
|
||||
---
|
||||
|
||||
## Para o Agente que está a Retomar
|
||||
|
||||
### Passo 1: Lê o estado atual
|
||||
Abre `docs/07-progress-tracker.md` e identifica:
|
||||
- O que está feito ✅
|
||||
- O que está em progresso 🔄
|
||||
- O que é o próximo passo ⏭️
|
||||
|
||||
### Passo 2: Entende o contexto
|
||||
- **O projeto é:** Painel de administração para gerir uma liga de futebol
|
||||
- **A app cliente:** Já existe, só lê dados da Firebase
|
||||
- **Este site:** É o único que escreve na Firebase
|
||||
- **A funcionalidade mais importante:** Live Score Editor (atualização de resultados em tempo real)
|
||||
|
||||
### Passo 3: Verifica a estrutura de ficheiros
|
||||
Confirma que a estrutura em `docs/02-tech-stack.md` foi seguida. Se houver desvios, atualiza a documentação.
|
||||
|
||||
### Passo 4: Atualiza o progress tracker
|
||||
Ao terminar a sessão, atualiza `docs/07-progress-tracker.md` com o que foi feito.
|
||||
|
||||
---
|
||||
|
||||
## Princípios de Desenvolvimento a Seguir
|
||||
|
||||
### 1. Firebase primeiro
|
||||
- Antes de criar qualquer componente de UI, confirma o schema Firebase em `docs/08-firebase-schema.md`
|
||||
- Qualquer alteração ao schema deve ser documentada imediatamente
|
||||
- Usar sempre TypeScript types que espelhem o schema
|
||||
|
||||
### 2. Real-time por defeito
|
||||
- Usar `onSnapshot` em vez de `getDoc` sempre que possível
|
||||
- O utilizador nunca deve precisar de fazer refresh manual
|
||||
|
||||
### 3. Transações para operações compostas
|
||||
- Sempre que uma ação modifica múltiplos documentos (ex: marcar golo), usar `runTransaction`
|
||||
- Nunca fazer writes paralelos sem transação — risco de inconsistência
|
||||
|
||||
### 4. UX do Live Editor é sagrada
|
||||
- O Live Score Editor deve funcionar com latência mínima
|
||||
- Botões grandes (min 48px de toque)
|
||||
- Feedback visual imediato (antes mesmo da Firebase confirmar)
|
||||
- Otimistic updates: atualiza UI localmente, reverte se Firebase falhar
|
||||
|
||||
### 5. Não breaking changes no schema
|
||||
- A app cliente já está a ler dados. Qualquer alteração ao schema pode quebrar a app cliente.
|
||||
- Adicionar campos novos é seguro (additive changes)
|
||||
- Renomear ou remover campos exige coordenação com a app cliente
|
||||
|
||||
---
|
||||
|
||||
## Convenções de Código
|
||||
|
||||
### Nomeação
|
||||
```
|
||||
Ficheiros: PascalCase para componentes (GameCard.tsx)
|
||||
camelCase para hooks, utils, stores (useGames.ts)
|
||||
Variáveis: camelCase
|
||||
Constantes: UPPER_SNAKE_CASE
|
||||
Types/Types: PascalCase (Game, Club, Player)
|
||||
Firestore: camelCase nas keys (homeScore, awayScore)
|
||||
```
|
||||
|
||||
### Estrutura de um Hook Firebase
|
||||
```typescript
|
||||
// Padrão obrigatório para todos os hooks de dados
|
||||
export function useGames(options?: { jornadaId?: string }) {
|
||||
const [games, setGames] = useState<Game[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onSnapshot(query,
|
||||
(snap) => {
|
||||
setGames(snap.docs.map(parseGame))
|
||||
setLoading(false)
|
||||
},
|
||||
(err) => {
|
||||
setError(err)
|
||||
setLoading(false)
|
||||
}
|
||||
)
|
||||
return unsubscribe // cleanup obrigatório
|
||||
}, [options?.jornadaId])
|
||||
|
||||
// Funções de mutação
|
||||
const createGame = async (data: CreateGameInput) => { ... }
|
||||
const updateGame = async (id: string, data: Partial<Game>) => { ... }
|
||||
const deleteGame = async (id: string) => { ... }
|
||||
|
||||
return { games, loading, error, createGame, updateGame, deleteGame }
|
||||
}
|
||||
```
|
||||
|
||||
### Commits (se usar Git)
|
||||
```
|
||||
feat: adicionar live score editor
|
||||
fix: corrigir cálculo de standings após golo
|
||||
docs: atualizar schema Firebase
|
||||
style: ajustar layout do sidebar
|
||||
refactor: extrair lógica de golo para hook
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Perguntas Frequentes
|
||||
|
||||
**Q: Onde está o schema da Firebase?**
|
||||
A: `docs/08-firebase-schema.md`
|
||||
|
||||
**Q: Qual o design system a seguir?**
|
||||
A: `docs/04-design-system.md` — usa as CSS variables definidas lá
|
||||
|
||||
**Q: Posso mudar a stack tecnológica?**
|
||||
A: Não sem atualizar `docs/02-tech-stack.md` e ter uma boa razão documentada
|
||||
|
||||
**Q: A app cliente vai quebrar se eu alterar o schema?**
|
||||
A: Possivelmente. Adicionar campos novos é seguro. Remover/renomear requer coordenação.
|
||||
|
||||
**Q: Como sei se o Live Editor está a funcionar?**
|
||||
A: Abre a app cliente em paralelo. A alteração deve aparecer em menos de 2 segundos.
|
||||
|
||||
---
|
||||
|
||||
## Contactos / Dependências Externas
|
||||
|
||||
- **Firebase Project:** [preencher com o project ID]
|
||||
- **Firebase Console:** https://console.firebase.google.com
|
||||
- **App Cliente:** [link para o repositório ou descrição]
|
||||
- **Design Mockup:** `mockup/index.html` (abrir no browser)
|
||||
141
docs/07-progress-tracker.md
Normal file
141
docs/07-progress-tracker.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 07 — Progress Tracker
|
||||
|
||||
> Atualizar este ficheiro no início e fim de cada sessão de desenvolvimento.
|
||||
|
||||
---
|
||||
|
||||
## Estado Geral
|
||||
|
||||
| Fase | Estado | Progresso |
|
||||
|---|---|---|
|
||||
| Fase 0: Planeamento | ✅ Completo | 100% |
|
||||
| Fase 1: Setup e Fundação | ✅ Completo | 100% |
|
||||
| Fase 2: Módulo de Jogos (Live) | 🔄 Em Progresso | 0% |
|
||||
| Fase 3: Módulo de Gestão | ⏳ Pendente | 0% |
|
||||
| Fase 4: Estatísticas | ⏳ Pendente | 0% |
|
||||
| Fase 5: Polish e Deploy | ⏳ Pendente | 0% |
|
||||
|
||||
---
|
||||
|
||||
## Próximo Passo Imediato
|
||||
|
||||
**⏭️ Fase 1 — Setup inicial do projeto**
|
||||
|
||||
1. `npm create vite@latest football-admin -- --template react-ts`
|
||||
2. Instalar dependências (ver `docs/02-tech-stack.md`)
|
||||
3. Configurar Firebase (criar `.env.local` com as credenciais)
|
||||
4. Implementar autenticação básica
|
||||
5. Criar layout base (sidebar + header)
|
||||
|
||||
---
|
||||
|
||||
## Log de Sessões
|
||||
|
||||
### Sessão 1 — [Data]
|
||||
**O que foi feito:**
|
||||
- [x] Planeamento completo do projeto
|
||||
- [x] Documentação: project plan, tech stack, arquitetura, design system, features, handoff guide, progress tracker, firebase schema
|
||||
- [x] Mockup interativo (HTML)
|
||||
- [x] Criação do zip com toda a documentação
|
||||
|
||||
**Próxima sessão deve começar em:**
|
||||
- Fase 1: Setup do projeto
|
||||
|
||||
---
|
||||
|
||||
## Checklist Detalhada
|
||||
|
||||
### Fase 1: Setup e Fundação
|
||||
- [ ] Projeto Vite criado
|
||||
- [ ] TypeScript configurado
|
||||
- [ ] Firebase SDK instalado e configurado
|
||||
- [ ] `.env.local` com credenciais Firebase
|
||||
- [ ] React Router configurado
|
||||
- [ ] Zustand store inicial (auth)
|
||||
- [ ] Firebase Auth implementado
|
||||
- [ ] Página de login
|
||||
- [ ] Hook useAuth
|
||||
- [ ] ProtectedRoute component
|
||||
- [ ] Redirect lógica
|
||||
- [ ] Layout base
|
||||
- [ ] Sidebar component
|
||||
- [ ] Header component
|
||||
- [ ] Layout wrapper component
|
||||
- [ ] Design tokens (CSS variables)
|
||||
- [ ] Tailwind configurado com tema custom
|
||||
- [ ] shadcn/ui configurado
|
||||
- [ ] Rota `/dashboard` funcional (placeholder)
|
||||
|
||||
### Fase 2: Módulo de Jogos
|
||||
- [ ] Firebase hook `useGames`
|
||||
- [ ] Página `/games` — listagem
|
||||
- [ ] Filtro por jornada
|
||||
- [ ] Filtro por estado
|
||||
- [ ] Game cards
|
||||
- [ ] Formulário criar jogo (`/games/new`)
|
||||
- [ ] Formulário editar jogo (`/games/:id/edit`)
|
||||
- [ ] **Live Score Editor** (`/games/:id/live`) ⭐
|
||||
- [ ] Layout do ecrã
|
||||
- [ ] Cronómetro
|
||||
- [ ] Botões de golo (casa e fora)
|
||||
- [ ] Modal de golo (jogador + minuto + tipo)
|
||||
- [ ] Modal de cartão
|
||||
- [ ] Lista de eventos
|
||||
- [ ] Botão intervalo
|
||||
- [ ] Botão terminar jogo
|
||||
- [ ] Firebase transaction para golo
|
||||
- [ ] Recálculo automático de standings
|
||||
- [ ] Optimistic updates
|
||||
- [ ] Undo último evento (30s)
|
||||
|
||||
### Fase 3: Módulo de Gestão
|
||||
- [ ] Firebase hook `useClubs`
|
||||
- [ ] Página `/clubs` — listagem
|
||||
- [ ] Formulário criar/editar clube
|
||||
- [ ] Detalhe do clube (`/clubs/:id`)
|
||||
- [ ] Firebase hook `usePlayers`
|
||||
- [ ] Página `/players` — listagem + filtros
|
||||
- [ ] Formulário criar/editar jogador
|
||||
- [ ] Gestão de jornadas (`/rounds`)
|
||||
- [ ] Classificação (`/standings`)
|
||||
- [ ] Tabela completa
|
||||
- [ ] Edição manual
|
||||
- [ ] Recalcular manualmente
|
||||
|
||||
### Fase 4: Estatísticas
|
||||
- [ ] Artilheiros (`/scorers`)
|
||||
- [ ] Tabela de cartões
|
||||
- [ ] Dashboard overview
|
||||
- [ ] Widget jogo live
|
||||
- [ ] Próximos jogos
|
||||
- [ ] Últimos resultados
|
||||
- [ ] Métricas rápidas
|
||||
|
||||
### Fase 5: Polish e Deploy
|
||||
- [ ] Testes (hooks críticos)
|
||||
- [ ] Error boundaries
|
||||
- [ ] Loading states em todos os componentes
|
||||
- [ ] Empty states em todas as listas
|
||||
- [ ] Responsividade (tablet para Live Editor)
|
||||
- [ ] Firebase Security Rules finais
|
||||
- [ ] `firebase.json` configurado
|
||||
- [ ] `npm run deploy` funcional
|
||||
- [ ] Domínio configurado (se aplicável)
|
||||
|
||||
---
|
||||
|
||||
## Problemas Conhecidos / Technical Debt
|
||||
|
||||
*Nenhum ainda — projeto em fase de planeamento*
|
||||
|
||||
---
|
||||
|
||||
## Decisões Tomadas e Razões
|
||||
|
||||
| Decisão | Razão |
|
||||
|---|---|
|
||||
| Firebase como único backend | App cliente já usa Firebase; consistência |
|
||||
| Dark theme como padrão | Usado durante jogos (ambientes escuros) |
|
||||
| Optimistic updates no Live Editor | UX responsiva sem esperar pela Firebase |
|
||||
| shadcn/ui em vez de Material/Chakra | Mais customizável, sem vendor lock-in |
|
||||
| Zustand em vez de Redux | Menor complexidade para este tamanho |
|
||||
340
docs/08-firebase-schema.md
Normal file
340
docs/08-firebase-schema.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# 08 — Firebase Schema (Firestore)
|
||||
|
||||
> ⚠️ CRÍTICO: Este schema deve ser compatível com o que a app cliente já lê.
|
||||
> Antes de desenvolver, confirmar com o utilizador se este schema corresponde ao que já existe na Firebase.
|
||||
|
||||
---
|
||||
|
||||
## Coleções Principais
|
||||
|
||||
```
|
||||
firestore/
|
||||
├── users/ # Utilizadores admin
|
||||
├── clubs/ # Clubes da liga
|
||||
├── players/ # Jogadores
|
||||
├── seasons/ # Temporadas
|
||||
├── rounds/ # Jornadas
|
||||
├── games/ # Jogos
|
||||
│ └── {gameId}/
|
||||
│ └── events/ # Eventos do jogo (subcollection)
|
||||
└── standings/ # Classificação
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema Detalhado
|
||||
|
||||
### `users/{userId}`
|
||||
```typescript
|
||||
type User = {
|
||||
uid: string; // Firebase Auth UID
|
||||
email: string;
|
||||
displayName: string;
|
||||
role: "admin" | "viewer";
|
||||
createdAt: Timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `clubs/{clubId}`
|
||||
```typescript
|
||||
type Club = {
|
||||
id: string; // auto-generated
|
||||
name: string; // "FC Porto"
|
||||
shortName: string; // "FCP"
|
||||
city: string; // "Porto"
|
||||
stadium: string; // "Estádio do Dragão"
|
||||
primaryColor: string; // "#004899" (hex)
|
||||
secondaryColor: string; // "#FFFFFF" (hex)
|
||||
crestUrl: string; // Firebase Storage URL
|
||||
foundedYear: number; // 1893
|
||||
active: boolean;
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `players/{playerId}`
|
||||
```typescript
|
||||
type Player = {
|
||||
id: string;
|
||||
clubId: string; // ref to clubs/{clubId}
|
||||
name: string; // "Mehdi Taremi"
|
||||
shortName: string; // "Taremi"
|
||||
nationality: string; // "Iran"
|
||||
dateOfBirth: Timestamp;
|
||||
position: "GK" | "DEF" | "MID" | "FWD";
|
||||
jerseyNumber: number;
|
||||
photoUrl: string; // Firebase Storage URL
|
||||
status: "active" | "injured" | "suspended" | "inactive";
|
||||
|
||||
// Estatísticas (atualizadas a cada evento)
|
||||
stats: {
|
||||
goals: number;
|
||||
assists: number;
|
||||
yellowCards: number;
|
||||
redCards: number;
|
||||
gamesPlayed: number;
|
||||
minutesPlayed: number;
|
||||
};
|
||||
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `seasons/{seasonId}`
|
||||
```typescript
|
||||
type Season = {
|
||||
id: string;
|
||||
name: string; // "2024/2025"
|
||||
startDate: Timestamp;
|
||||
endDate: Timestamp;
|
||||
active: boolean; // apenas uma temporada ativa de cada vez
|
||||
totalRounds: number; // 34
|
||||
currentRound: number; // 15
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `rounds/{roundId}`
|
||||
```typescript
|
||||
type Round = {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
number: number; // 15
|
||||
name: string; // "Jornada 15"
|
||||
startDate: Timestamp;
|
||||
endDate: Timestamp;
|
||||
status: "upcoming" | "active" | "completed";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `games/{gameId}`
|
||||
```typescript
|
||||
type Game = {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
roundId: string;
|
||||
roundNumber: number; // desnormalizado para queries mais simples
|
||||
|
||||
homeClubId: string;
|
||||
awayClubId: string;
|
||||
|
||||
// Desnormalizado para evitar joins no cliente
|
||||
homeClubName: string;
|
||||
awayClubName: string;
|
||||
homeClubShortName: string;
|
||||
awayClubShortName: string;
|
||||
homeClubCrestUrl: string;
|
||||
awayClubCrestUrl: string;
|
||||
|
||||
scheduledAt: Timestamp; // data e hora agendada
|
||||
startedAt: Timestamp | null;
|
||||
endedAt: Timestamp | null;
|
||||
|
||||
status: "scheduled" | "live" | "halftime" | "finished" | "postponed" | "cancelled";
|
||||
|
||||
score: {
|
||||
home: number; // 0
|
||||
away: number; // 0
|
||||
};
|
||||
|
||||
currentMinute: number | null; // minuto atual do jogo (se live)
|
||||
|
||||
venue: string; // nome do estádio
|
||||
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `games/{gameId}/events/{eventId}` (subcollection)
|
||||
```typescript
|
||||
type GameEvent = {
|
||||
id: string;
|
||||
gameId: string;
|
||||
|
||||
type: "goal" | "yellow_card" | "red_card" | "substitution" | "penalty_missed" | "own_goal";
|
||||
|
||||
minute: number; // 23
|
||||
extraTime: number | null; // minutos de compensação (ex: 90+3 → minute:90, extraTime:3)
|
||||
|
||||
// Para golos e cartões
|
||||
playerId: string | null;
|
||||
playerName: string | null; // desnormalizado
|
||||
clubId: string;
|
||||
|
||||
// Específico de golos
|
||||
goalType: "normal" | "penalty" | "free_kick" | "header" | null;
|
||||
assistPlayerId: string | null;
|
||||
assistPlayerName: string | null;
|
||||
|
||||
// Específico de substituições
|
||||
playerOutId: string | null;
|
||||
playerInId: string | null;
|
||||
playerOutName: string | null;
|
||||
playerInName: string | null;
|
||||
|
||||
createdAt: Timestamp;
|
||||
createdBy: string; // userId do admin que registou
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `standings/{seasonId}`
|
||||
```typescript
|
||||
type Standings = {
|
||||
seasonId: string;
|
||||
updatedAt: Timestamp;
|
||||
|
||||
table: StandingsEntry[]; // ordenado por pontos
|
||||
}
|
||||
|
||||
type StandingsEntry = {
|
||||
position: number;
|
||||
clubId: string;
|
||||
clubName: string; // desnormalizado
|
||||
clubShortName: string;
|
||||
clubCrestUrl: string;
|
||||
|
||||
played: number;
|
||||
won: number;
|
||||
drawn: number;
|
||||
lost: number;
|
||||
goalsFor: number;
|
||||
goalsAgainst: number;
|
||||
goalDifference: number; // calculado: goalsFor - goalsAgainst
|
||||
points: number; // calculado: won*3 + drawn*1
|
||||
|
||||
// Últimos 5 jogos (para a app cliente mostrar forma)
|
||||
form: ("W" | "D" | "L")[]; // ["W", "W", "D", "L", "W"]
|
||||
|
||||
// Override manual (admin pode forçar um valor)
|
||||
manualOverride: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Queries Mais Usadas
|
||||
|
||||
```typescript
|
||||
// Jogos de uma jornada
|
||||
query(collection(db, "games"),
|
||||
where("roundId", "==", roundId),
|
||||
orderBy("scheduledAt")
|
||||
)
|
||||
|
||||
// Jogo live atual
|
||||
query(collection(db, "games"),
|
||||
where("status", "==", "live"),
|
||||
limit(1)
|
||||
)
|
||||
|
||||
// Eventos de um jogo (ordenados)
|
||||
query(collection(db, "games", gameId, "events"),
|
||||
orderBy("minute"),
|
||||
orderBy("createdAt")
|
||||
)
|
||||
|
||||
// Jogadores de um clube
|
||||
query(collection(db, "players"),
|
||||
where("clubId", "==", clubId),
|
||||
where("status", "==", "active"),
|
||||
orderBy("jerseyNumber")
|
||||
)
|
||||
|
||||
// Top artilheiros
|
||||
query(collection(db, "players"),
|
||||
orderBy("stats.goals", "desc"),
|
||||
limit(20)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Índices Firestore Necessários
|
||||
|
||||
```
|
||||
// firestore.indexes.json
|
||||
{
|
||||
"indexes": [
|
||||
{
|
||||
"collectionGroup": "games",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{ "fieldPath": "roundId", "order": "ASCENDING" },
|
||||
{ "fieldPath": "scheduledAt", "order": "ASCENDING" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "players",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{ "fieldPath": "clubId", "order": "ASCENDING" },
|
||||
{ "fieldPath": "stats.goals", "order": "DESCENDING" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "players",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{ "fieldPath": "clubId", "order": "ASCENDING" },
|
||||
{ "fieldPath": "jerseyNumber", "order": "ASCENDING" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Algoritmo de Recálculo de Standings
|
||||
|
||||
Chamado após cada jogo finalizado ou manualmente:
|
||||
|
||||
```typescript
|
||||
async function recalculateStandings(seasonId: string, db: Firestore) {
|
||||
// 1. Buscar todos os jogos terminados da temporada
|
||||
const games = await getDocs(query(
|
||||
collection(db, "games"),
|
||||
where("seasonId", "==", seasonId),
|
||||
where("status", "==", "finished")
|
||||
))
|
||||
|
||||
// 2. Inicializar stats por clube
|
||||
const stats: Record<string, StandingsEntry> = {}
|
||||
|
||||
// 3. Iterar todos os jogos e acumular stats
|
||||
for (const game of games.docs) {
|
||||
const { homeClubId, awayClubId, score } = game.data()
|
||||
// ... calcular vitória/empate/derrota, golos, etc.
|
||||
}
|
||||
|
||||
// 4. Ordenar: pontos DESC, goalDifference DESC, goalsFor DESC
|
||||
const table = Object.values(stats).sort((a, b) =>
|
||||
b.points - a.points ||
|
||||
b.goalDifference - a.goalDifference ||
|
||||
b.goalsFor - a.goalsFor
|
||||
).map((entry, i) => ({ ...entry, position: i + 1 }))
|
||||
|
||||
// 5. Atualizar documento de standings
|
||||
await setDoc(doc(db, "standings", seasonId), {
|
||||
seasonId,
|
||||
table,
|
||||
updatedAt: serverTimestamp()
|
||||
})
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user