Files
MyClub/frontend/src/index.tsx
T
Tomas Dvorak f5b6f83974 dev day #99
2025-11-21 08:44:44 +01:00

167 lines
6.1 KiB
TypeScript

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import './styles/global-enhancements.css';
import './styles/admin-enhancements.css';
import './styles/home-style-pack.css';
import './styles/sparta-styles.css';
// Quill editor styles (MUST be imported globally) - CRITICAL for rich text editor
import 'quill/dist/quill.snow.css';
import 'react-image-crop/dist/ReactCrop.css';
// Custom editor styles AFTER quill base styles to ensure proper override
import './styles/custom-editor.css';
import { theme } from './App';
import AppLazy from './App.lazy';
import { ColorModeScript } from '@chakra-ui/react';
import { HelmetProvider } from 'react-helmet-async';
import reportWebVitals from './reportWebVitals';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import { promptUserToUpdate } from './serviceWorkerRegistration';
import { installGlobalErrorHandlers, reportError } from './services/errorReporter';
// Cookie consent utilities
type Consent = { analytics?: boolean };
const getConsent = (): Consent | null => {
try {
const raw = localStorage.getItem('cookie_consent');
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
};
const onConsentChange = (cb: (c: Consent) => void) => {
window.addEventListener('cookie-consent-change', (e: Event) => {
const detail = (e as CustomEvent).detail as Consent;
cb(detail || {});
});
};
// Example analytics initializer (placeholder)
const initAnalytics = () => {
// Place your analytics loader here (e.g., GA/Matomo), gated by consent.analytics
// This function will be called only when analytics consent is granted.
};
// Error Boundary component
class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean}> {
constructor(props: {children: React.ReactNode}) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
reportError({ message: error.message, stack: error.stack, component: 'ErrorBoundary', context: { react: errorInfo.componentStack } });
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h2>Something went wrong.</h2>
<p>Please refresh the page or try again later.</p>
</div>
);
}
return this.props.children;
}
}
installGlobalErrorHandlers();
window.addEventListener('unhandledrejection', (event) => {
const reason: any = (event as any).reason;
const message = typeof reason === 'string' ? reason : (reason?.message || 'Unhandled rejection');
const stack = typeof reason === 'object' ? (reason?.stack || '') : '';
reportError({ message, stack });
});
const rootElement = document.getElementById('root');
if (!rootElement) {
// Failed to find the root element
} else {
try {
// Initialize CSS variables immediately to prevent flash of fallback colors
const r = document.documentElement;
// Try to load cached colors from localStorage for instant display
let cachedColors: any = null;
try {
const cached = localStorage.getItem('club_theme_cache');
if (cached) {
cachedColors = JSON.parse(cached);
// Use cache if it's less than 24 hours old
const age = Date.now() - (cachedColors.timestamp || 0);
if (age > 24 * 60 * 60 * 1000) {
cachedColors = null; // Cache expired
}
}
} catch (e) {
// Ignore localStorage errors
}
// Set CSS variables - use cached values if available, otherwise defaults
r.style.setProperty('--club-primary', cachedColors?.primary || '#0b5cff');
r.style.setProperty('--club-secondary', cachedColors?.secondary || '#ffd200');
r.style.setProperty('--club-accent', cachedColors?.accent || '#141414');
r.style.setProperty('--club-text-on-primary', cachedColors?.textOnPrimary || '#ffffff');
if (cachedColors?.textOnSecondary) r.style.setProperty('--club-text-on-secondary', cachedColors.textOnSecondary);
if (cachedColors?.textOnAccent) r.style.setProperty('--club-text-on-accent', cachedColors.textOnAccent);
r.style.setProperty('--club-bg-light', cachedColors?.background || '#ffffff');
r.style.setProperty('--club-text-light', cachedColors?.text || '#000000');
// Load fonts
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@600;700;800&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<HelmetProvider>
<ErrorBoundary>
{/* Ensure color mode (light/dark) persists and matches Chakra config before UI renders */}
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<AppLazy />
</ErrorBoundary>
</HelmetProvider>
</React.StrictMode>
);
// App rendered
// Initialize analytics if consent allows
const current = getConsent();
if (current?.analytics) {
initAnalytics();
}
// React to future consent changes
onConsentChange((c) => {
if (c?.analytics) {
initAnalytics();
}
});
} catch (error) {
// Optionally report to monitoring service
// console.error('Error rendering application:', error);
rootElement.innerHTML = `
<div style="padding: 20px; font-family: Arial, sans-serif; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;">
<h2>Application Error</h2>
<p>${String(error)}</p>
<p>Please check the console for more details and refresh the page.</p>
</div>
`;
}
}
// Report web vitals (disabled logging by default). Hook up your analytics here if needed.
reportWebVitals();
// Enable PWA service worker with user prompt on updates
serviceWorkerRegistration.register({ onUpdate: promptUserToUpdate });