208 lines
6.5 KiB
Markdown
208 lines
6.5 KiB
Markdown
# 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();
|
|
}
|
|
}
|
|
}
|
|
```
|