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;