Files
VdcScoreLive/docs/03-architecture.md
2026-05-05 17:12:06 +01:00

6.5 KiB

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

// 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
// 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

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();
    }
  }
}