import React, { useEffect, useState } from 'react';
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom';
import './styles/custom-scrollbar.css';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import AuthPage from './pages/AuthPage';
import RegisterPage from './pages/RegisterPage';
import DashboardPage from './pages/DashboardPage';
import ArticlesListPage from './pages/ArticlesListPage';
import HomePage from './pages/HomePage';
import BlogPage from './pages/BlogPage';
import ArticleDetailPage from './pages/ArticleDetailPage';
import ActivityDetailPage from './pages/ActivityDetailPage';
import MatchDetailPage from './pages/MatchDetailPage';
import ClubPage from './pages/ClubPage';
import CalendarPage from './pages/CalendarPage';
import TablesPage from './pages/TablesPage';
import MatchesPage from './pages/MatchesPage';
import PlayersPage from './pages/PlayersPage';
import PlayerDetailPage from './pages/PlayerDetailPage';
import SponsorsPage from './pages/SponsorsPage';
import ContactPage from './pages/ContactPage';
import GalleryPage from './pages/GalleryPage';
import AlbumDetailPage from './pages/AlbumDetailPage';
import ForgotPasswordPage from './pages/ForgotPasswordPage';
import ResetPasswordPage from './pages/ResetPasswordPage';
import ActivitiesCalendarPage from './pages/ActivitiesCalendarPage';
import AdminDashboardPage from './pages/admin/AdminDashboardPage';
import ArticlesAdminPage from './pages/admin/ArticlesAdminPage';
import SponsorsAdminPage from './pages/admin/SponsorsAdminPage';
import CategoriesAdminPage from './pages/admin/CategoriesAdminPage';
import MediaAdminPage from './pages/admin/MediaAdminPage';
import MatchesAdminPage from './pages/admin/MatchesAdminPage';
import PlayersAdminPage from './pages/admin/PlayersAdminPage';
import TeamsAdminPage from './pages/admin/TeamsAdminPage';
import BannersAdminPage from './pages/admin/BannersAdminPage';
import MessagesAdminPage from './pages/admin/MessagesAdminPage';
import SettingsAdminPage from './pages/admin/SettingsAdminPage';
import UsersAdminPage from './pages/admin/UsersAdminPage';
import NewsletterAdminPage from './pages/admin/NewsletterAdminPage';
import CompetitionAliasesAdminPage from './pages/admin/CompetitionAliasesAdminPage';
import PrefetchAdminPage from './pages/admin/PrefetchAdminPage';
import AdminVideosPage from './pages/admin/AdminVideosPage';
import GalleryAdminPage from './pages/admin/GalleryAdminPage';
import AdminActivitiesPage from './pages/admin/AdminActivitiesPage';
import AdminMerchPage from './pages/admin/AdminMerchPage';
import AdminResetPasswordPage from './pages/admin/AdminResetPasswordPage';
import AboutAdminPage from './pages/admin/AboutAdminPage';
import AnalyticsAdminPage from './pages/admin/AnalyticsAdminPage';
import FilesAdminPage from './pages/admin/FilesAdminPage';
import ContactsAdminPage from './pages/admin/ContactsAdminPage';
import NavigationAdminPage from './pages/admin/NavigationAdminPage';
import ShortlinksAdminPage from './pages/admin/ShortlinksAdminPage';
import CommentsAdminPage from './pages/admin/CommentsAdminPage';
import EngagementAdminPage from './pages/admin/EngagementAdminPage';
import SweepstakesAdminPage from './pages/admin/SweepstakesAdminPage';
import SweepstakeVisualPage from './pages/admin/SweepstakeVisualPage';
import SemiAdminPage from './pages/SemiAdminPage';
import PollsAdminPage from './pages/admin/PollsAdminPage';
// Admin pages render their own AdminLayout internally
import SetupPage from './pages/SetupPage';
import StylePreviewPage from './pages/StylePreviewPage';
import AboutPage from './pages/AboutPage';
import AdminDocsPage from './pages/admin/AdminDocsPage';
import ScoreboardAdminPage from './pages/admin/ScoreboardAdminPage';
import MobileScoreboardControlPage from './pages/admin/MobileScoreboardControlPage';
import { getSetupStatus } from './services/setup';
import NewsletterUnsubscribePage from './pages/NewsletterUnsubscribePage';
import NewsletterPreferencesPage from './pages/NewsletterPreferencesPage';
import { ClubThemeProvider } from './contexts/ClubThemeContext';
import CookiePolicyPage from './pages/legal/CookiePolicyPage';
import OverlayScoreboardPage from './pages/OverlayScoreboardPage';
import OverlaySponsorsPage from './pages/OverlaySponsorsPage';
import CookieBanner from './components/CookieBanner';
import DefaultSEO from './components/seo/DefaultSEO';
import ProtectedRoute from './components/ProtectedRoute';
import TermsPage from './pages/legal/TermsPage';
import PrivacyPolicyPage from './pages/legal/PrivacyPolicyPage';
import ForbiddenPage from './pages/ForbiddenPage';
import NotFoundPage from './pages/NotFoundPage';
import VideosPage from './pages/VideosPage';
import SearchPage from './pages/SearchPage';
import ShortRedirectPage from './pages/ShortRedirectPage';
import ClothingPage from './pages/ClothingPage';
import PollsPage from './pages/PollsPage';
import { useUmami } from './hooks/useUmami';
import { checkin } from './services/engagement';
import { useFontLoader } from './hooks/useFontLoader';
// Create a client with better cache configuration
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
refetchOnMount: false,
retry: 1,
},
},
});
// Theme configuration drawing colors from ClubTheme CSS variables for personalization
export const theme = extendTheme({
config: {
initialColorMode: 'light',
useSystemColorMode: false,
},
// Provide a brand color scale so colorScheme="brand" components style correctly
colors: {
brand: {
50: '#e6f7ff',
100: '#b3e0ff',
200: '#80caff',
300: '#4db3ff',
400: '#1a9cff',
500: 'var(--club-primary, #0b5cff)',
600: '#0066cc',
700: '#004d99',
800: '#003366',
900: '#001a33',
},
},
// Semantic tokens allow live updates when ClubThemeContext changes CSS variables
semanticTokens: {
colors: {
'brand.primary': {
default: 'var(--club-primary, #0b5cff)',
},
'brand.secondary': {
default: 'var(--club-secondary, #ffd200)',
},
'brand.accent': {
default: 'var(--club-accent, #141414)',
},
'text.onPrimary': {
default: 'var(--club-text-on-primary, #ffffff)',
},
'bg.app': {
default: '#f8f9fb',
_dark: '#0f1115',
},
'text.app': {
default: '#1a1a1a',
_dark: '#e8eaf0',
},
// Backdrop/outline shades
'border.subtle': {
default: 'rgba(0,0,0,0.06)',
_dark: 'rgba(255,255,255,0.12)',
},
'bg.card': {
default: '#ffffff',
_dark: '#1a1d29',
},
'bg.elevated': {
default: '#ffffff',
_dark: '#242831',
},
},
},
styles: {
global: {
'html, body, #root': {
height: '100%',
fontFamily: 'var(--font-body, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial)',
},
body: {
bg: 'bg.app',
color: 'text.app',
lineHeight: 1.5,
fontFamily: 'var(--font-body, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial)',
},
'h1, h2, h3, h4, h5, h6': {
fontFamily: 'var(--font-heading, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial)',
},
a: {
transition: 'color 0.2s ease',
},
'::selection': {
background: 'brand.accent',
color: 'black',
},
},
},
components: {
Container: {
baseStyle: {
px: { base: 4, md: 6 },
},
sizes: {
'7xl': '88rem',
},
},
Button: {
baseStyle: {
fontWeight: '700',
borderRadius: 'md',
letterSpacing: '0.4px',
_hover: { transform: 'translateY(-1px)', boxShadow: 'md' },
_active: { transform: 'translateY(0)' },
},
variants: {
solid: {
bg: 'brand.primary',
color: 'text.onPrimary',
_hover: { filter: 'brightness(0.95)' },
},
outline: {
border: '2px solid',
borderColor: 'brand.primary',
color: 'brand.primary',
_hover: { bg: 'rgba(0,0,0,0.02)' },
},
ghost: {
color: 'brand.secondary',
_hover: { bg: 'rgba(0,0,0,0.04)' },
},
},
},
Card: {
baseStyle: {
container: {
borderRadius: 'lg',
boxShadow: 'sm',
overflow: 'hidden',
transition: 'all 0.2s',
borderWidth: '1px',
borderColor: 'border.subtle',
_hover: { transform: 'translateY(-4px)', boxShadow: 'lg' },
},
},
},
Divider: {
baseStyle: {
borderColor: 'border.subtle',
},
},
Heading: {
baseStyle: {
fontFamily: 'var(--font-heading, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial)',
},
},
Text: {
baseStyle: {
fontFamily: 'var(--font-body, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial)',
},
},
},
fonts: {
heading: 'var(--font-heading, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial)',
body: 'var(--font-body, Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial)',
},
});
// Component to initialize analytics inside Router context
const AnalyticsInitializer: React.FC = () => {
useUmami();
return null;
};
// Component to load and apply club fonts
const FontLoader: React.FC = () => {
useFontLoader();
return null;
};
// Component to trigger daily check-in for authenticated users (once per day per device)
const CheckinInitializer: React.FC = () => {
const { isAuthenticated } = useAuth();
useEffect(() => {
if (!isAuthenticated) return;
let cancelled = false;
const todayKey = (() => {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `fc_checkin_${y}-${m}-${day}`;
})();
try {
if (localStorage.getItem(todayKey) === '1') return;
} catch {}
// Fire and forget; backend caps ensure idempotence server-side
(async () => {
try { await checkin(); if (!cancelled) { try { localStorage.setItem(todayKey, '1'); } catch {} } } catch {}
})();
return () => { cancelled = true; };
}, [isAuthenticated]);
return null;
};
// Redirect /news -> /blog while preserving query parameters
const NewsRedirect: React.FC = () => {
const loc = useLocation();
return ;
};
const App: React.FC = () => {
// Uses shared ProtectedRoute component for auth guard
// Public Route component - redirects to admin if already authenticated
const PublicRoute = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated, isLoading, user } = useAuth();
const [checkingSetup, setCheckingSetup] = useState(true);
const [requiresSetup, setRequiresSetup] = useState(false);
useEffect(() => {
let mounted = true;
(async () => {
try {
const s = await getSetupStatus();
if (mounted) setRequiresSetup(!!s.requires_setup);
} catch (_) {
if (mounted) setRequiresSetup(false);
} finally {
if (mounted) setCheckingSetup(false);
}
})();
return () => { mounted = false; };
}, []);
if (isLoading || checkingSetup) {
return Načítání…
;
}
if (isAuthenticated) {
const role = String(user?.role || '').toLowerCase();
if (role === 'admin') {
return ;
}
if (role === 'editor') {
return ;
}
if (role === 'fan') {
return ;
}
// Default: regular users to frontpage
return ;
}
// If setup is required, redirect to setup wizard unless already on setup
const currentPath = window.location.pathname;
if (requiresSetup && currentPath !== '/setup') {
return ;
}
return <>{children}>;
};
// Admin routes group wrapper (no layout here; pages render their own AdminLayout)
const AdminRoutesWrapper = () => {
return ;
};
return (
{/* Public routes */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Legal pages */}
} />
} />
} />
{/* Short links - forward to backend origin if frontend captured it */}
} />
} />
{/* Slug routes must precede id route to avoid conflicts */}
} />
} />
} />
{/* Internal match detail */}
} />
} />
{/* Legacy redirects */}
} />
} />
}
/>
}
/>
}
/>
}
/>
} />
} />
} />
} />
}
/>
} />
{/* Admin area (pages include AdminLayout themselves) */}
}>
} />
} />
{/* moved to editor-accessible routes below */}
} />
} />
} />
} />
} />
} />
{/* moved to editor-accessible routes below */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Remaining protected routes that don't use AdminLayout */}
}
/>
}
/>
}
/>
}
/>
}
/>
{/* Editor-accessible content pages (also allow admin) */}
}
/>
}
/>
}
/>
{/* Not found route */}
} />
{/* Cookie consent banner shown across the whole site */}
);
};
export default App;