-
);
};
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
new file mode 100644
index 0000000..e1ae6cb
--- /dev/null
+++ b/src/pages/Login.tsx
@@ -0,0 +1,124 @@
+import React, { useState } from 'react';
+import { useAuth } from '../lib/AuthContext';
+import { useNavigate, Navigate } from 'react-router-dom';
+import { Zap, Lock, User, AlertCircle } from 'lucide-react';
+
+const Login = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const { login, isAuthenticated } = useAuth();
+ const navigate = useNavigate();
+
+ if (isAuthenticated) {
+ return
;
+ }
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ // Artificial delay for feel
+ setTimeout(() => {
+ const success = login(username, password);
+ if (success) {
+ navigate('/');
+ } else {
+ setError('Utilizador ou palavra-passe incorretos.');
+ }
+ setLoading(false);
+ }, 600);
+ };
+
+ return (
+
+ {/* Background elements for premium look */}
+
+
+
+
+
+
+
+
Admin VdcScore
+
Portal Administrativo Inter-Freguesias
+
+
+
+
+
+ © {new Date().getFullYear()} VdcScore Live • Direitos Reservados
+
+
+
+ );
+};
+
+export default Login;
diff --git a/src/pages/Players.tsx b/src/pages/Players.tsx
new file mode 100644
index 0000000..6835d96
--- /dev/null
+++ b/src/pages/Players.tsx
@@ -0,0 +1,354 @@
+import React, { useState } from 'react';
+import { useClubs } from '../hooks/useClubs';
+import { useScorers } from '../hooks/useScorers';
+import { Search, Loader2, User, Calendar, MapPin, Shield, Trophy, X } from 'lucide-react';
+import type { Player, Club } from '../types/database';
+
+interface ExtendedPlayer extends Player {
+ clubId: number | string;
+ clubNome: string;
+ clubLogo: string;
+ escalao: 'seniores' | 'juniores';
+}
+
+const Players = () => {
+ const { clubs, loading: loadingClubs, error: errorClubs } = useClubs();
+ const { scorers, loading: loadingScorers, error: errorScorers } = useScorers();
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedClubId, setSelectedClubId] = useState
('todos');
+ const [selectedEscalao, setSelectedEscalao] = useState('todos');
+
+ // State for selected player details modal
+ const [selectedPlayer, setSelectedPlayer] = useState(null);
+
+ // Build a reactive goals lookup map from the useScorers hook
+ const goalsMap = React.useMemo(() => {
+ const map: Record = {};
+ if (scorers) {
+ if (scorers.seniores) {
+ scorers.seniores.forEach((item) => {
+ if (item && item.playerName) {
+ map[item.playerName.toLowerCase().trim()] = item.goals;
+ }
+ });
+ }
+ if (scorers.juniores) {
+ scorers.juniores.forEach((item) => {
+ if (item && item.playerName) {
+ map[item.playerName.toLowerCase().trim()] = item.goals;
+ }
+ });
+ }
+ }
+ return map;
+ }, [scorers]);
+
+ // Extract and combine all players from all clubs
+ const allPlayers: ExtendedPlayer[] = React.useMemo(() => {
+ const list: ExtendedPlayer[] = [];
+ clubs.forEach((club) => {
+ if (club.jogadores) {
+ if (club.jogadores.seniores) {
+ club.jogadores.seniores.forEach((p) => {
+ const trimmedName = p.nome ? p.nome.toLowerCase().trim() : '';
+ list.push({
+ ...p,
+ clubId: club.id,
+ clubNome: club.nome,
+ clubLogo: club.imagem,
+ escalao: 'seniores',
+ golos: goalsMap[trimmedName] || 0, // Lookup goal count from scorers map
+ });
+ });
+ }
+ if (club.jogadores.juniores) {
+ club.jogadores.juniores.forEach((p) => {
+ const trimmedName = p.nome ? p.nome.toLowerCase().trim() : '';
+ list.push({
+ ...p,
+ clubId: club.id,
+ clubNome: club.nome,
+ clubLogo: club.imagem,
+ escalao: 'juniores',
+ golos: goalsMap[trimmedName] || 0, // Lookup goal count from scorers map
+ });
+ });
+ }
+ }
+ });
+ return list;
+ }, [clubs, goalsMap]);
+
+ // Filter the combined list of players
+ const filteredPlayers = React.useMemo(() => {
+ return allPlayers.filter((player) => {
+ const playerNome = player.nome || '';
+ const playerNat = player.naturalidade || '';
+ const matchesSearch = playerNome.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ playerNat.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesClub = selectedClubId === 'todos' || player.clubId.toString() === selectedClubId;
+ const matchesEscalao = selectedEscalao === 'todos' || player.escalao === selectedEscalao;
+ return matchesSearch && matchesClub && matchesEscalao;
+ });
+ }, [allPlayers, searchTerm, selectedClubId, selectedEscalao]);
+
+ // Format date of birth to a localized string
+ const formatDate = (dateStr: string) => {
+ if (!dateStr) return '---';
+ try {
+ const date = new Date(dateStr);
+ if (isNaN(date.getTime())) return dateStr;
+ return date.toLocaleDateString('pt-PT');
+ } catch {
+ return dateStr;
+ }
+ };
+
+ const loading = loadingClubs || loadingScorers;
+ const error = errorClubs || errorScorers;
+
+ return (
+
+
+
JOGADORES
+
Explora todos os jogadores e clica em qualquer cartão para ver as estatísticas e golos.
+
+
+ {/* Filters Bar */}
+
+ {/* Search */}
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {/* Club Filter */}
+
+
+
+
+ {/* Category Filter */}
+
+
+
+
+
+ {/* Counters */}
+
+
+ A mostrar {filteredPlayers.length} de {allPlayers.length} jogadores
+
+
+
+ {loading ? (
+
+
+
A carregar os dados dos jogadores...
+
+ ) : error ? (
+
+
Erro ao carregar dados!
+
{error.message}
+
+ ) : filteredPlayers.length === 0 ? (
+
+
+
Nenhum jogador encontrado com os filtros atuais.
+
+ ) : (
+
+ {filteredPlayers.map((player) => (
+
setSelectedPlayer(player)}
+ className="card group hover:scale-[1.02] hover:border-brand-primary cursor-pointer transition-all duration-300"
+ >
+
+
+
+ {/* Parent Club Subtle Watermark */}
+
+

{
+ (e.target as HTMLImageElement).src = 'https://via.placeholder.com/50?text=C';
+ }}
+ />
+
{player.clubNome}
+
+
+ {/* Escalao Badge */}
+
+ {player.escalao}
+
+
+ {/* Goals Stats Bubble on Card */}
+
+ ⚽ {player.golos || 0} Golos
+
+
+
+
+
+ {player.foto ? (
+

{
+ (e.target as HTMLImageElement).src = 'https://api.dicebear.com/7.x/adventurer/svg?seed=' + player.nome;
+ }}
+ />
+ ) : (
+
+ )}
+
+
+
+ {player.nome}
+
+
+
+
+
+ Nascimento: {formatDate(player.data_nascimento)}
+
+
+
+ Naturalidade: {player.naturalidade || '---'}
+
+
+
+ Clube: {player.clubNome}
+
+
+
+
+ ))}
+
+ )}
+
+ {/* --- PLAYER STATS / GOALS DETAILS MODAL --- */}
+ {selectedPlayer && (
+
+
+
+ {/* Header / Close button */}
+
+
+
+ Ficha do Jogador
+
+
+
+
+ {/* Profile Summary */}
+
+
+ {selectedPlayer.foto ? (
+

{
+ (e.target as HTMLImageElement).src = 'https://api.dicebear.com/7.x/adventurer/svg?seed=' + selectedPlayer.nome;
+ }}
+ />
+ ) : (
+
+ )}
+
+
+
{selectedPlayer.nome}
+
+

{ (e.target as HTMLImageElement).src = 'https://via.placeholder.com/50?text=C'; }}
+ />
+
{selectedPlayer.clubNome}
+
•
+
{selectedPlayer.escalao}
+
+
+
+
+ {/* BIG GOALS DISPLAY STAT CARD */}
+
+
+
+
+
{selectedPlayer.golos || 0}
+
Golos Marcados
+
Estatísticas atualizadas em tempo real
+
+
+ {/* Biographical Info list */}
+
+
+ Data de Nascimento
+ {formatDate(selectedPlayer.data_nascimento)}
+
+
+ Naturalidade
+ {selectedPlayer.naturalidade || '---'}
+
+
+ Clube
+ {selectedPlayer.clubNome}
+
+
+ Categoria / Escalão
+ {selectedPlayer.escalao}
+
+
+
+ {/* Action buttons */}
+
+
+
+
+ )}
+
+ );
+};
+
+export default Players;
diff --git a/src/pages/Scorers.tsx b/src/pages/Scorers.tsx
new file mode 100644
index 0000000..fe396d5
--- /dev/null
+++ b/src/pages/Scorers.tsx
@@ -0,0 +1,257 @@
+import React, { useState } from 'react';
+import { useScorers } from '../hooks/useScorers';
+import { Loader2, Trophy, Medal, User } from 'lucide-react';
+import type { ScorerItem } from '../types/database';
+
+const Scorers = () => {
+ const { scorers, loading, error } = useScorers();
+ const [selectedEscalao, setSelectedEscalao] = useState<'seniores' | 'juniores'>('seniores');
+
+ // Process and sort top 10 scorers
+ const topScorers: ScorerItem[] = React.useMemo(() => {
+ const list = scorers[selectedEscalao] || [];
+ // Sort descending by goals and slice top 10
+ return [...list]
+ .sort((a, b) => b.goals - a.goals)
+ .slice(0, 10);
+ }, [scorers, selectedEscalao]);
+
+ const maxGoals = topScorers.length > 0 ? topScorers[0].goals : 0;
+
+ // Split into podium (top 3) and remaining (4-10)
+ const podium = topScorers.slice(0, 3);
+ const remaining = topScorers.slice(3, 10);
+
+ // Position ordering for podium representation: 2nd, 1st, 3rd
+ const podiumOrdered = React.useMemo(() => {
+ const ordered: { player: ScorerItem | null; position: number }[] = [
+ { player: null, position: 2 },
+ { player: null, position: 1 },
+ { player: null, position: 3 },
+ ];
+ podium.forEach((p, idx) => {
+ const pos = idx + 1;
+ if (pos === 1) ordered[1] = { player: p, position: 1 };
+ if (pos === 2) ordered[0] = { player: p, position: 2 };
+ if (pos === 3) ordered[2] = { player: p, position: 3 };
+ });
+ return ordered;
+ }, [podium]);
+
+ return (
+
+
+
+
MELHORES MARCADORES
+
Os goleadores do campeonato que lideram a corrida à bota de ouro.
+
+
+ {/* Category Switcher */}
+
+
+
+
+
+
+ {loading ? (
+
+
+
A carregar goleadores...
+
+ ) : error ? (
+
+
Erro ao carregar dados!
+
{error.message}
+
+ ) : topScorers.length === 0 ? (
+
+
+
Nenhum registo de golos encontrado para este escalão.
+
+ ) : (
+
+ {/* --- PODIUM SECTION (TOP 3) --- */}
+ {podium.length > 0 && (
+
+ {podiumOrdered.map(({ player, position }) => {
+ if (!player) {
+ // Empty space filler if less than 3 players exist
+ return
;
+ }
+
+ const isFirst = position === 1;
+ const isSecond = position === 2;
+
+ // Podium styling tokens
+ const podiumStyle = isFirst
+ ? {
+ borderColor: 'border-brand-warning/40',
+ bgColor: 'bg-gradient-to-t from-brand-warning/10 to-transparent',
+ badgeColor: 'bg-brand-warning text-bg-base',
+ textGolos: 'text-brand-warning',
+ height: 'md:h-80',
+ icon:
,
+ }
+ : isSecond
+ ? {
+ borderColor: 'border-text-secondary/40',
+ bgColor: 'bg-gradient-to-t from-text-secondary/10 to-transparent',
+ badgeColor: 'bg-text-secondary text-bg-base',
+ textGolos: 'text-text-secondary',
+ height: 'md:h-72',
+ icon:
,
+ }
+ : {
+ borderColor: 'border-orange-500/40',
+ bgColor: 'bg-gradient-to-t from-orange-500/10 to-transparent',
+ badgeColor: 'bg-orange-500 text-bg-base',
+ textGolos: 'text-orange-500',
+ height: 'md:h-64',
+ icon:
,
+ };
+
+ return (
+
+
+
+ {position}º
+
+ {podiumStyle.icon}
+
+
+
+ {/* Photo */}
+
+ {player.playerPhoto ? (
+

{ (e.target as HTMLImageElement).src = 'https://api.dicebear.com/7.x/adventurer/svg?seed=' + player.playerName; }} />
+ ) : (
+
+ )}
+
+
+ {/* Name */}
+
{player.playerName}
+
+ {/* Club Name & Logo */}
+
+

{ (e.target as HTMLImageElement).src = 'https://via.placeholder.com/50?text=C'; }}
+ />
+
{player.clubName}
+
+
+
+
+ {player.goals}
+ Golos
+
+
+ );
+ })}
+
+ )}
+
+ {/* --- LEADERBOARD LIST (PLACES 4-10) --- */}
+ {remaining.length > 0 && (
+
+
+
POSIÇÕES 4º AO 10º
+
+
+ {remaining.map((player, idx) => {
+ const position = idx + 4;
+ // Percentage of goals relative to the top scorer for the progress bar
+ const percentage = maxGoals > 0 ? (player.goals / maxGoals) * 100 : 0;
+
+ return (
+
+ {/* Position */}
+
+ {position}º
+
+
+ {/* Photo */}
+
+ {player.playerPhoto ? (
+

{ (e.target as HTMLImageElement).src = 'https://api.dicebear.com/7.x/adventurer/svg?seed=' + player.playerName; }} />
+ ) : (
+
+ )}
+
+
+ {/* Player Info */}
+
+
+
{player.playerName}
+
+

{ (e.target as HTMLImageElement).src = 'https://via.placeholder.com/50?text=C'; }} />
+
{player.clubName}
+
+
+
+ {/* Club Logo and Name (Hidden on small screens, shown inside grid on md+) */}
+
+

{ (e.target as HTMLImageElement).src = 'https://via.placeholder.com/50?text=C'; }}
+ />
+
{player.clubName}
+
+
+ {/* Progress Bar (goals visual representation) */}
+
+
+
+ {/* Goal Count */}
+
+ {player.goals}
+ golos
+
+
+ );
+ })}
+
+
+ )}
+
+ )}
+
+ );
+};
+
+export default Scorers;
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
new file mode 100644
index 0000000..89eca13
--- /dev/null
+++ b/src/pages/Settings.tsx
@@ -0,0 +1,162 @@
+import React, { useState } from 'react';
+import {
+ Settings as SettingsIcon,
+ Database,
+ Bell,
+ Shield,
+ Save,
+ CheckCircle,
+ RefreshCw,
+ Power
+} from 'lucide-react';
+
+const Settings = () => {
+ const [saved, setSaved] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ // Example setting states
+ const [scraperEnabled, setScraperEnabled] = useState(true);
+
+
+ const handleSave = () => {
+ setLoading(true);
+ setTimeout(() => {
+ setLoading(false);
+ setSaved(true);
+ setTimeout(() => setSaved(false), 3000);
+ }, 800);
+ };
+
+ return (
+
+
+
+
+
+ Definições
+
+
Configure as preferências do sistema Admin VdcScore.
+
+
+
+
+
+
+ {/* Left Column - Tabs or sections */}
+
+
+ {/* Scraper Status Section */}
+
+
+
+
Gestão de Dados & API
+
+
+
+
+
+
Sincronização Automática (Scraper)
+
Executar o scraper a cada 30 minutos para obter atualizações automáticas.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Right Column - Widgets */}
+
+ {/* Admin Info */}
+
+
+
+ AV
+
+
+
Admin VdcScore
+
Utilizador: admvdc
+
ROOT
+
+
+
+
+
+ {/* Notifications */}
+
+
+ {/* Danger Zone */}
+
+
+
+ Zona de Perigo
+
+
Estas ações podem causar interrupção de serviço.
+
+
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/src/types/database.ts b/src/types/database.ts
index 0382c5b..f91c3c9 100644
--- a/src/types/database.ts
+++ b/src/types/database.ts
@@ -4,6 +4,7 @@ export interface Player {
foto: string;
data_nascimento: string;
naturalidade: string;
+ golos?: number;
}
export interface Club {
@@ -53,3 +54,12 @@ export interface Match {
}
export type Escalao = 'seniores' | 'juniores';
+
+export interface ScorerItem {
+ playerName: string;
+ playerPhoto: string;
+ clubName: string;
+ clubLogo: string;
+ goals: number;
+ position: number;
+}