criação do website

This commit is contained in:
2026-05-05 17:12:06 +01:00
parent 9c36b714f1
commit 732e7276b7
46 changed files with 9844 additions and 0 deletions

105
docs/01-project-plan.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
})
}
```