This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Club favicon/logo -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- Optional SVG logo if available in public folder (keeps ICO as fallback) -->
<!-- <link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/logo.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Oficiální webové stránky fotbalového klubu - aktuality, zápasy, tabulky, hráči a fotogalerie"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Fotbal Club</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+25
View File
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+10
View File
@@ -0,0 +1,10 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow: /admin/
Disallow: /api/
Disallow: /login
Disallow: /setup
Allow: /
# Sitemap will be served dynamically from backend
# Sitemap: /sitemap.xml
+248
View File
@@ -0,0 +1,248 @@
/* eslint-disable no-restricted-globals */
// Service Worker for PWA support and offline functionality
const CACHE_VERSION = 'v1.0.0';
const CACHE_NAME = `fotbal-club-cache-${CACHE_VERSION}`;
// Assets to cache on install
const STATIC_ASSETS = [
'/',
'/index.html',
'/static/css/main.css',
'/static/js/main.js',
'/manifest.json',
'/favicon.ico',
'/logo192.png',
'/logo512.png',
];
// API endpoints to cache
const API_CACHE_ENDPOINTS = [
'/api/v1/settings/public',
'/api/v1/seo',
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' })));
}).catch((error) => {
console.error('[SW] Failed to cache static assets:', error);
})
);
// Activate immediately
self.skipWaiting();
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
// Take control immediately
return self.clients.claim();
});
// Fetch event - serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip Chrome extensions and non-http(s) requests
if (!url.protocol.startsWith('http')) {
return;
}
// Skip admin routes
if (url.pathname.startsWith('/admin')) {
return;
}
// Handle API requests
if (url.pathname.startsWith('/api/')) {
event.respondWith(handleAPIRequest(request));
return;
}
// Handle static assets and pages
event.respondWith(handleStaticRequest(request));
});
// Handle static requests - Cache First strategy
async function handleStaticRequest(request) {
try {
// Try cache first
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// Return cached response and update in background
fetchAndUpdateCache(request);
return cachedResponse;
}
// Not in cache - fetch from network
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('[SW] Fetch failed:', error);
// Return offline page if available
const cachedOffline = await caches.match('/offline.html');
if (cachedOffline) {
return cachedOffline;
}
// Return basic offline response
return new Response('Offline - Please check your connection', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain' },
});
}
}
// Handle API requests - Network First strategy with cache fallback
async function handleAPIRequest(request) {
try {
// Try network first for fresh data
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.log('[SW] Network failed, trying cache:', request.url);
// Fall back to cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return error response
return new Response(
JSON.stringify({ error: 'Offline - cached data not available' }),
{
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'application/json' },
}
);
}
}
// Update cache in background
async function fetchAndUpdateCache(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response);
}
} catch (error) {
// Silent fail - we already returned cached version
}
}
// Handle background sync for offline actions
self.addEventListener('sync', (event) => {
console.log('[SW] Background sync:', event.tag);
if (event.tag === 'sync-data') {
event.waitUntil(syncOfflineData());
}
});
async function syncOfflineData() {
// Implement offline data sync logic here
// For example, sync form submissions, votes, etc.
console.log('[SW] Syncing offline data...');
}
// Handle push notifications
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
const title = data.title || 'Fotbal Club';
const options = {
body: data.body || 'Nová notifikace',
icon: '/logo192.png',
badge: '/logo192.png',
data: data.url || '/',
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const urlToOpen = event.notification.data || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
// Check if there's already a window open
for (const client of clientList) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// Open new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Message handler for manual cache updates
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
event.waitUntil(
caches.delete(CACHE_NAME).then(() => {
return caches.open(CACHE_NAME);
})
);
}
});
console.log('[SW] Service Worker loaded successfully');