criação do website
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user