Compare commits

...

7 Commits

Author SHA1 Message Date
Tomas Dvorak 4f3164956a Add slug-to-numeric resolution for blog URLs
- Modified blog handler to resolve slugs to numeric files when slug files don't exist
- This allows slug URLs to work without creating duplicate slug files
- Fixes sorting issues while maintaining slug URL compatibility
2026-03-14 11:27:08 +01:00
Tomas Dvorak f8a6abf391 Fix blog sorting to show newest first
- When files have recent timestamps (from setup script), sort by numeric ID
- Higher numeric IDs represent newer blogs
- This ensures proper chronological ordering in admin interface
2026-03-14 11:21:31 +01:00
Tomas Dvorak 45facf7aa0 Fix image paths and remove duplicate slug files
- Remove duplicate slug files that were causing duplicate entries
- Add logic to find corresponding numeric ID for image paths
- This fixes broken images in admin interface for slug-based blogs
2026-03-14 11:19:09 +01:00
Tomas Dvorak c9c322ff95 Fix duplicate blog entries and image paths
- Add deduplication logic to avoid showing same blog twice
- Add extractBlogID function to get correct numeric ID from meta tags
- Use blogID for image paths instead of filename ID
- This fixes duplicate entries in admin interface and broken images
2026-03-14 11:17:49 +01:00
Tomas Dvorak 3a7c1bbcba Fix blog API endpoints and path issues
- Fix listLatestBlogs function to use correct blog directory path
- Add /data/blog-list.json endpoint for frontend blog listing
- Update remoteBlogDir references to use blogDir
- Resolve blog listing and admin interface loading issues
2026-03-14 11:12:40 +01:00
Tomas Dvorak 03b9abd32d fix 2026-03-14 10:52:14 +01:00
Tomas Dvorak 21574a8b30 Add slug support and new admin features 2026-03-14 10:40:04 +01:00
31 changed files with 37597 additions and 36032 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
{ {
"livePreview.defaultPreviewPath": "/ukol.html" "livePreview.defaultPreviewPath": "/ukol.html"
} }
+172
View File
@@ -0,0 +1,172 @@
# Remote Blog Management - Complete Guide
## 🎯 Goal
Remove local blog files and work exclusively with remote server blogs.
## 📋 Options Available
### Option 1: Quick Remove Local Blogs
```bash
# Run this in your bizoni directory
./remove-local-blogs.sh
```
### Option 2: Ubuntu Server Management Script
```bash
# Upload to your Ubuntu server and run
./ubuntu-remote-blogs.sh migrate
```
### Option 3: Backend Configuration (Recommended)
Update backend to work with remote blogs only.
## 🚀 Recommended Deployment Steps
### Step 1: Remove Local Blogs
```bash
cd /home/tdvorak/Desktop/HTML_Projekty/bizoni
./remove-local-blogs.sh
```
### Step 2: Update Backend Configuration
The backend is now configured to work with remote blogs at `/var/www/bizoni/blog`.
You can set the remote path with environment variable:
```bash
export REMOTE_BLOG_DIR="/var/www/bizoni/blog"
```
### Step 3: Deploy Backend to Server
1. Build the updated backend
2. Deploy to your server
3. Set REMOTE_BLOG_DIR environment variable
### Step 4: Run Migration on Server
```bash
# On your Ubuntu server
./ubuntu-remote-blogs.sh migrate
```
## 📁 File Structure After Changes
### Local (Development)
```
bizoni/
├── backend/main.go # Updated for remote blogs
├── admin/new.html # Updated with new fields
├── js/admin-auth.js # Login persistence
├── tools/migrate_slugs.go # Migration tool
├── remove-local-blogs.sh # Local cleanup script
└── ubuntu-remote-blogs.sh # Server management script
```
### Server (Production)
```
/var/www/bizoni/
├── blog/
│ ├── 0000.html # Original numeric files
│ ├── 0001.html
│ ├── jdeme-do-finale.html # New slug files
│ └── 1-zapas-final-score.html
├── img/blog/
│ ├── 0000.png
│ └── 0001.png
└── backend # Updated backend
```
## 🔧 Ubuntu Server Script Usage
### List All Blogs
```bash
./ubuntu-remote-blogs.sh list
```
### Migrate All Blogs to Slugs
```bash
./ubuntu-remote-blogs.sh migrate
```
### Show Blog Info
```bash
./ubuntu-remote-blogs.sh info 0030
```
### Add Slug to Specific Blog
```bash
./ubuntu-remote-blogs.sh add-slug 0030
```
### Create Backup
```bash
./ubuntu-remote-blogs.sh backup
```
## 🌐 URL Structure After Migration
### Before
- `/blog/0030.html`
- `/blog/0001.html`
### After
- `/blog/jdeme-do-finale` (clean URL)
- `/blog/1-zapas-final-score`
- `/blog/0030.html` (still works for backward compatibility)
## ⚠️ Important Notes
1. **Backup First**: Always create backup before migration
2. **Test Locally**: Test backend with REMOTE_BLOG_DIR set to local copy
3. **Deploy Gradually**: Deploy backend first, then run migration
4. **Environment Variables**: Use REMOTE_BLOG_DIR for flexibility
## 🔄 Environment Variables
Set these on your server:
```bash
# Path to remote blog directory
export REMOTE_BLOG_DIR="/var/www/bizoni/blog"
# Port for backend (if needed)
export PORT="8080"
# Static files path
export STATIC_PATH="/var/www/bizoni"
```
## 🚨 Troubleshooting
### Backend Can't Find Blogs
```bash
# Check if directory exists
ls -la /var/www/bizoni/blog
# Set correct path
export REMOTE_BLOG_DIR="/correct/path/to/blogs"
```
### Migration Script Fails
```bash
# Make script executable
chmod +x ubuntu-remote-blogs.sh
# Update paths in script
nano ubuntu-remote-blogs.sh
```
### Permission Issues
```bash
# Fix permissions on server
sudo chown -R www-data:www-data /var/www/bizoni/blog
sudo chmod -R 755 /var/www/bizoni/blog
```
## 📞 Next Steps
1. **Choose your option** (1, 2, or 3)
2. **Remove local blogs** with the provided script
3. **Deploy updated backend** to server
4. **Run migration** on server
5. **Test new URLs** and admin interface
Your blog system will then work entirely with remote server blogs! 🎉
+113 -4
View File
@@ -62,6 +62,23 @@
<label for="title">Titulek</label> <label for="title">Titulek</label>
<input type="text" id="title" name="title" placeholder="Např. FC Bizoni vyhráli finále" required /> <input type="text" id="title" name="title" placeholder="Např. FC Bizoni vyhráli finále" required />
</div> </div>
<div>
<label for="slug">URL slug (použije se v adrese)</label>
<input type="text" id="slug" name="slug" placeholder="napr-fc-bizoni-vyhrali-finale" pattern="[a-z0-9-]+" title="Pouze malá písmena, číslice a pomlčky" />
<div class="muted" style="margin-top:6px">Automaticky se vygeneruje z titulku, pokud nezadáte vlastní.</div>
</div>
<div>
<label for="annotation">Anotace (krátký popis pro SEO a sociální sítě)</label>
<input type="text" id="annotation" name="annotation" placeholder="Krátký popis článku (150-300 znaků)" maxlength="300" />
<div class="muted" style="margin-top:6px">Použije se pro SEO description a při sdílení na sociálních sítích.</div>
</div>
<div>
<label for="content-mode">Způsob zadávání obsahu</label>
<select id="content-mode" name="content-mode" style="width: 100%; padding: 10px; border:1px solid #d1d5db; border-radius: 8px; font-size: 14px;">
<option value="visual">Vizuální editor (Quill)</option>
<option value="html">HTML kód</option>
</select>
</div>
<div> <div>
<label for="categories">Kategorie (oddělené čárkou)</label> <label for="categories">Kategorie (oddělené čárkou)</label>
<input type="text" id="categories" name="categories" placeholder="Zápasy, O nás" /> <input type="text" id="categories" name="categories" placeholder="Zápasy, O nás" />
@@ -87,7 +104,12 @@
</div> </div>
<div> <div>
<label for="editor">Obsah (vizuální editor)</label> <label for="editor">Obsah (vizuální editor)</label>
<div id="editor"></div> <div id="visual-editor-wrapper">
<div id="editor"></div>
</div>
<div id="html-editor-wrapper" style="display: none;">
<textarea id="html-content" name="html-content" rows="12" placeholder="Zadejte HTML kód obsahu..." style="width: 100%; padding: 10px; border:1px solid #d1d5db; border-radius: 8px; font-size: 14px; font-family: 'Courier New', monospace;"></textarea>
</div>
<!-- Hidden textarea to submit HTML (kept focusable-safe by moving offscreen) --> <!-- Hidden textarea to submit HTML (kept focusable-safe by moving offscreen) -->
<textarea id="content" name="content" rows="12" style="position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden;"></textarea> <textarea id="content" name="content" rows="12" style="position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden;"></textarea>
<div class="muted">Obsah bude vložen do sekce <code>&lt;div class="text lte-text-page clearfix"&gt;...&lt;/div&gt;</code> podle šablony <code>blog/0030.html</code>.</div> <div class="muted">Obsah bude vložen do sekce <code>&lt;div class="text lte-text-page clearfix"&gt;...&lt;/div&gt;</code> podle šablony <code>blog/0030.html</code>.</div>
@@ -140,6 +162,60 @@
const inputId = document.getElementById('post-id'); const inputId = document.getElementById('post-id');
const inputTitle = document.getElementById('title'); const inputTitle = document.getElementById('title');
const inputCats = document.getElementById('categories'); const inputCats = document.getElementById('categories');
const inputSlug = document.getElementById('slug');
const inputAnnotation = document.getElementById('annotation');
const contentModeSelect = document.getElementById('content-mode');
const visualEditorWrapper = document.getElementById('visual-editor-wrapper');
const htmlEditorWrapper = document.getElementById('html-editor-wrapper');
const htmlContentTextarea = document.getElementById('html-content');
// Content mode switching
contentModeSelect.addEventListener('change', () => {
const mode = contentModeSelect.value;
if (mode === 'visual') {
visualEditorWrapper.style.display = 'block';
htmlEditorWrapper.style.display = 'none';
} else {
visualEditorWrapper.style.display = 'none';
htmlEditorWrapper.style.display = 'block';
// Sync current content to HTML textarea
const currentContent = quill.root.innerHTML;
htmlContentTextarea.value = currentContent;
}
});
// Sync content from HTML to visual when switching back
contentModeSelect.addEventListener('change', () => {
if (contentModeSelect.value === 'visual') {
const htmlContent = htmlContentTextarea.value;
if (htmlContent.trim()) {
quill.root.innerHTML = htmlContent;
}
}
});
// Auto-generate slug from title
function generateSlug(text) {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphen
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
}
// Update slug when title changes (if slug is empty)
inputTitle.addEventListener('input', () => {
if (!inputSlug.value || inputSlug.dataset.autoGenerated === 'true') {
inputSlug.value = generateSlug(inputTitle.value);
inputSlug.dataset.autoGenerated = 'true';
}
});
// Mark slug as manually edited when user types in it
inputSlug.addEventListener('input', () => {
inputSlug.dataset.autoGenerated = 'false';
});
let pastedBlob = null; // holds clipboard/fetched blob if provided let pastedBlob = null; // holds clipboard/fetched blob if provided
@@ -209,8 +285,21 @@
const data = await res.json(); const data = await res.json();
inputId.value = data.id || id; inputId.value = data.id || id;
inputTitle.value = data.title || ''; inputTitle.value = data.title || '';
inputSlug.value = data.slug || '';
inputAnnotation.value = data.annotation || '';
inputCats.value = Array.isArray(data.categories) ? data.categories.join(', ') : ''; inputCats.value = Array.isArray(data.categories) ? data.categories.join(', ') : '';
quill.root.innerHTML = data.content_html || '';
// Set content mode and load content
if (data.content_mode === 'html') {
contentModeSelect.value = 'html';
htmlContentTextarea.value = data.content_html || '';
visualEditorWrapper.style.display = 'none';
htmlEditorWrapper.style.display = 'block';
} else {
contentModeSelect.value = 'visual';
quill.root.innerHTML = data.content_html || '';
}
// show current image preview // show current image preview
preview.style.display = 'flex'; preview.style.display = 'flex';
previewImg.src = '/img/blog/' + (data.id || id) + '.png'; previewImg.src = '/img/blog/' + (data.id || id) + '.png';
@@ -245,10 +334,22 @@
// Move Quill HTML into the hidden textarea // Move Quill HTML into the hidden textarea
// Merge category checkboxes into text input // Merge category checkboxes into text input
reconcileCategories(); reconcileCategories();
const html = quill.root.innerHTML;
// Get content based on mode
let html;
if (contentModeSelect.value === 'html') {
html = htmlContentTextarea.value.trim();
} else {
html = quill.root.innerHTML;
}
document.getElementById('content').value = html; document.getElementById('content').value = html;
// Validate content is not empty (avoid browser required on hidden field) // Validate content is not empty (avoid browser required on hidden field)
const plain = quill.getText().trim(); const plain = contentModeSelect.value === 'html' ?
html.replace(/<[^>]*>/g, '').trim() : // Strip HTML for validation
quill.getText().trim();
if (!plain) { if (!plain) {
result.textContent = 'Vyplňte obsah článku.'; result.textContent = 'Vyplňte obsah článku.';
result.classList.add('err'); result.classList.add('err');
@@ -256,6 +357,10 @@
return; return;
} }
const fd = new FormData(form); const fd = new FormData(form);
// Add annotation and content mode to form data
fd.set('annotation', inputAnnotation.value);
fd.set('content_mode', contentModeSelect.value);
// Prefer pasted/fetched blob if present when no file was chosen // Prefer pasted/fetched blob if present when no file was chosen
if (pastedBlob && !(imageInput.files && imageInput.files[0])) { if (pastedBlob && !(imageInput.files && imageInput.files[0])) {
const ext = (pastedBlob.type === 'image/jpeg') ? 'jpg' : 'png'; const ext = (pastedBlob.type === 'image/jpeg') ? 'jpg' : 'png';
@@ -300,6 +405,10 @@
result.appendChild(a); result.appendChild(a);
form.reset(); preview.style.display = 'none'; form.reset(); preview.style.display = 'none';
quill.setContents([]); quill.setContents([]);
htmlContentTextarea.value = '';
contentModeSelect.value = 'visual';
visualEditorWrapper.style.display = 'block';
htmlEditorWrapper.style.display = 'none';
} }
} catch (err) { } catch (err) {
result.textContent = 'Chyba: ' + (err.message || err); result.textContent = 'Chyba: ' + (err.message || err);
+119 -119
View File
@@ -1,120 +1,120 @@
Search Clubs Search Clubs
GET /club/search?q=QUERY GET /club/search?q=QUERY
Find clubs on fotbal.cz. Supports football and futsal clubs. Find clubs on fotbal.cz. Supports football and futsal clubs.
Example: https://facr.tdvorak.dev/club/search?q=Sparta Example: https://facr.tdvorak.dev/club/search?q=Sparta
Response shape Response shape
{ {
"query": "Sparta", "query": "Sparta",
"count": 2, "count": 2,
"results": [ "results": [
{ {
"name": "AC Sparta Praha", "name": "AC Sparta Praha",
"club_id": "", "club_id": "",
"club_type": "football", "club_type": "football",
"url": "https://www.fotbal.cz/...", "url": "https://www.fotbal.cz/...",
"logo_url": "https://.../logo.png", "logo_url": "https://.../logo.png",
"category": "Muži", "category": "Muži",
"address": "..." "address": "..."
} }
] ]
} }
Club Info + Matches Club Info + Matches
GET /club/{type}/{id} - id must be provided in the setup of the club GET /club/{type}/{id} - id must be provided in the setup of the club
{type}: football {type}: football
{id}: club UUID from fotbal.cz {id}: club UUID from fotbal.cz
Example: https://facr.tdvorak.dev/club/football/00000000-0000-0000-0000-000000000000 Example: https://facr.tdvorak.dev/club/football/00000000-0000-0000-0000-000000000000
Response shape Response shape
{ {
"name": "AC Sparta Praha", "name": "AC Sparta Praha",
"club_id": "00000000-0000-0000-0000-000000000000", "club_id": "00000000-0000-0000-0000-000000000000",
"club_type": "football", "club_type": "football",
"club_internal_id": "123456", "club_internal_id": "123456",
"url": "https://www.fotbal.cz/...", "url": "https://www.fotbal.cz/...",
"logo_url": "https://is1.fotbal.cz/media/kluby/.../logo.jpg", "logo_url": "https://is1.fotbal.cz/media/kluby/.../logo.jpg",
"address": "Milady Horákové 98, 160 00 Praha 6", "address": "Milady Horákové 98, 160 00 Praha 6",
"category": "Muži A", "category": "Muži A",
"competitions": [ "competitions": [
{ {
"id": "12345", "id": "12345",
"code": "1. LIGA", "code": "1. LIGA",
"name": "Fortuna Liga", "name": "Fortuna Liga",
"team_count": "16", "team_count": "16",
"matches_link": "https://www.fotbal.cz/...", "matches_link": "https://www.fotbal.cz/...",
"matches": [ "matches": [
{ {
"date_time": "12.08.2023 18:00", "date_time": "12.08.2023 18:00",
"home": "AC Sparta Praha", "home": "AC Sparta Praha",
"home_id": "00000000-0000-0000-0000-000000000000", "home_id": "00000000-0000-0000-0000-000000000000",
"home_logo_url": "https://.../sparta.png", "home_logo_url": "https://.../sparta.png",
"away": "SK Slavia Praha", "away": "SK Slavia Praha",
"away_id": "11111111-1111-1111-1111-111111111111", "away_id": "11111111-1111-1111-1111-111111111111",
"away_logo_url": "https://.../slavia.png", "away_logo_url": "https://.../slavia.png",
"score": "2:1", "score": "2:1",
"venue": "Stadion Letná", "venue": "Stadion Letná",
"match_id": "match12345", "match_id": "match12345",
"report_url": "https://www.fotbal.cz/..." "report_url": "https://www.fotbal.cz/..."
} }
] ]
} }
] ]
} }
Club Tables (Standings) Club Tables (Standings)
GET /club/{type}/{id}/table - id must be provided in the setup of the club GET /club/{type}/{id}/table - id must be provided in the setup of the club
Returns standings (overall table) for each competition of the club. Returns standings (overall table) for each competition of the club.
Example: https://facr.tdvorak.dev/club/football/00000000-0000-0000-0000-000000000000/table Example: https://facr.tdvorak.dev/club/football/00000000-0000-0000-0000-000000000000/table
Response shape Response shape
{ {
"name": "AC Sparta Praha", "name": "AC Sparta Praha",
"club_id": "00000000-0000-0000-0000-000000000000", "club_id": "00000000-0000-0000-0000-000000000000",
"club_type": "football", "club_type": "football",
"club_internal_id": "123456", "club_internal_id": "123456",
"url": "https://www.fotbal.cz/...", "url": "https://www.fotbal.cz/...",
"logo_url": "https://is1.fotbal.cz/media/kluby/.../logo.jpg", "logo_url": "https://is1.fotbal.cz/media/kluby/.../logo.jpg",
"competitions": [ "competitions": [
{ {
"id": "12345", "id": "12345",
"code": "1. LIGA", "code": "1. LIGA",
"name": "Fortuna Liga", "name": "Fortuna Liga",
"team_count": "16", "team_count": "16",
"matches_link": "https://www.fotbal.cz/...", "matches_link": "https://www.fotbal.cz/...",
"table": { "table": {
"overall": [ "overall": [
{ {
"rank": "1", "rank": "1",
"team": "AC Sparta Praha", "team": "AC Sparta Praha",
"team_id": "00000000-0000-0000-0000-000000000000", "team_id": "00000000-0000-0000-0000-000000000000",
"team_logo_url": "https://.../sparta.png", "team_logo_url": "https://.../sparta.png",
"played": "10", "played": "10",
"wins": "8", "wins": "8",
"draws": "2", "draws": "2",
"losses": "0", "losses": "0",
"score": "25:5", "score": "25:5",
"points": "26" "points": "26"
}, },
{ {
"rank": "2", "rank": "2",
"team": "SK Slavia Praha", "team": "SK Slavia Praha",
"team_id": "11111111-1111-1111-1111-111111111111", "team_id": "11111111-1111-1111-1111-111111111111",
"team_logo_url": "https://.../slavia.png", "team_logo_url": "https://.../slavia.png",
"played": "10", "played": "10",
"wins": "7", "wins": "7",
"draws": "2", "draws": "2",
"losses": "1", "losses": "1",
"score": "20:8", "score": "20:8",
"points": "23" "points": "23"
} }
] ]
} }
} }
] ]
} }
+15 -15
View File
@@ -1,15 +1,15 @@
FROM golang:1.22-alpine AS build FROM golang:1.22-alpine AS build
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN go build -o server ./main.go RUN go build -o server ./main.go
FROM alpine:3.20 FROM alpine:3.20
WORKDIR /app WORKDIR /app
# HTTPS for /img/clean proxy requires CA bundle # HTTPS for /img/clean proxy requires CA bundle
RUN apk add --no-cache ca-certificates \ RUN apk add --no-cache ca-certificates \
&& update-ca-certificates && update-ca-certificates
# Optional: timezone data for precise Prague time # Optional: timezone data for precise Prague time
# RUN apk add --no-cache tzdata # RUN apk add --no-cache tzdata
COPY --from=build /app/server /app/server COPY --from=build /app/server /app/server
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/app/server"] ENTRYPOINT ["/app/server"]
+581 -153
View File
@@ -6,6 +6,11 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"image"
"image/color"
"image/draw"
_ "image/jpeg"
"image/png"
"io" "io"
"log" "log"
"net/http" "net/http"
@@ -19,11 +24,6 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"image"
"image/color"
"image/draw"
"image/png"
_ "image/jpeg"
) )
const ( const (
@@ -44,107 +44,131 @@ func dataPath() string {
// ---------------- Image normalization for blog thumbnails ---------------- // ---------------- Image normalization for blog thumbnails ----------------
// Target dimensions for blog images // Target dimensions for blog images
const ( const (
blogImgW = 1600 blogImgW = 1600
blogImgH = 969 blogImgH = 969
) )
// avgLuma computes average luminance of an image (0..255) // avgLuma computes average luminance of an image (0..255)
func avgLuma(img image.Image) float64 { func avgLuma(img image.Image) float64 {
b := img.Bounds() b := img.Bounds()
if b.Empty() { return 0 } if b.Empty() {
var sum uint64 return 0
var n uint64 }
for y := b.Min.Y; y < b.Max.Y; y += 4 { // sample every 4th row for speed var sum uint64
for x := b.Min.X; x < b.Max.X; x += 4 { // sample every 4th column var n uint64
r,g,bv,_ := img.At(x,y).RGBA() for y := b.Min.Y; y < b.Max.Y; y += 4 { // sample every 4th row for speed
r8 := float64(r>>8) for x := b.Min.X; x < b.Max.X; x += 4 { // sample every 4th column
g8 := float64(g>>8) r, g, bv, _ := img.At(x, y).RGBA()
b8 := float64(bv>>8) r8 := float64(r >> 8)
// Rec. 601 approximate luma g8 := float64(g >> 8)
l := 0.299*r8 + 0.587*g8 + 0.114*b8 b8 := float64(bv >> 8)
if l < 0 { l = 0 } // Rec. 601 approximate luma
if l > 255 { l = 255 } l := 0.299*r8 + 0.587*g8 + 0.114*b8
sum += uint64(l) if l < 0 {
n++ l = 0
} }
} if l > 255 {
if n == 0 { return 0 } l = 255
return float64(sum)/float64(n) }
sum += uint64(l)
n++
}
}
if n == 0 {
return 0
}
return float64(sum) / float64(n)
} }
// fitWithin returns destination size that fits source into max size, without upscaling // fitWithin returns destination size that fits source into max size, without upscaling
func fitWithin(sw, sh, mw, mh int) (int, int) { func fitWithin(sw, sh, mw, mh int) (int, int) {
if sw <= 0 || sh <= 0 { return 0, 0 } if sw <= 0 || sh <= 0 {
if sw <= mw && sh <= mh { return 0, 0
return sw, sh }
} if sw <= mw && sh <= mh {
wr := float64(mw) / float64(sw) return sw, sh
hr := float64(mh) / float64(sh) }
r := wr wr := float64(mw) / float64(sw)
if hr < wr { r = hr } hr := float64(mh) / float64(sh)
dw := int(float64(sw) * r) r := wr
dh := int(float64(sh) * r) if hr < wr {
if dw < 1 { dw = 1 } r = hr
if dh < 1 { dh = 1 } }
return dw, dh dw := int(float64(sw) * r)
dh := int(float64(sh) * r)
if dw < 1 {
dw = 1
}
if dh < 1 {
dh = 1
}
return dw, dh
} }
// scaleNearest performs nearest-neighbor downscaling from src to a new RGBA of size (dw, dh) // scaleNearest performs nearest-neighbor downscaling from src to a new RGBA of size (dw, dh)
func scaleNearest(src image.Image, dw, dh int) *image.RGBA { func scaleNearest(src image.Image, dw, dh int) *image.RGBA {
dst := image.NewRGBA(image.Rect(0,0,dw,dh)) dst := image.NewRGBA(image.Rect(0, 0, dw, dh))
sb := src.Bounds() sb := src.Bounds()
sw := sb.Dx() sw := sb.Dx()
sh := sb.Dy() sh := sb.Dy()
for y := 0; y < dh; y++ { for y := 0; y < dh; y++ {
sy := sb.Min.Y + int(float64(y)*float64(sh)/float64(dh)) sy := sb.Min.Y + int(float64(y)*float64(sh)/float64(dh))
for x := 0; x < dw; x++ { for x := 0; x < dw; x++ {
sx := sb.Min.X + int(float64(x)*float64(sw)/float64(dw)) sx := sb.Min.X + int(float64(x)*float64(sw)/float64(dw))
dst.Set(x, y, src.At(sx, sy)) dst.Set(x, y, src.At(sx, sy))
} }
} }
return dst return dst
} }
// normalizeBlogImage decodes any supported image (PNG/JPEG) and writes a 1600x969 PNG with letterboxing (black/white) // normalizeBlogImage decodes any supported image (PNG/JPEG) and writes a 1600x969 PNG with letterboxing (black/white)
func normalizeBlogImage(r io.Reader, outPath string) error { func normalizeBlogImage(r io.Reader, outPath string) error {
img, _, err := image.Decode(r) img, _, err := image.Decode(r)
if err != nil { if err != nil {
return fmt.Errorf("decode image: %w", err) return fmt.Errorf("decode image: %w", err)
} }
// Choose background based on average luminance // Choose background based on average luminance
l := avgLuma(img) l := avgLuma(img)
bg := color.Black bg := color.Black
if l > 160 { // bright image -> white bg; tweak threshold as needed if l > 160 { // bright image -> white bg; tweak threshold as needed
bg = color.White bg = color.White
} }
// Compute fitted size (no upscaling) // Compute fitted size (no upscaling)
srcB := img.Bounds() srcB := img.Bounds()
dw, dh := fitWithin(srcB.Dx(), srcB.Dy(), blogImgW, blogImgH) dw, dh := fitWithin(srcB.Dx(), srcB.Dy(), blogImgW, blogImgH)
var scaled image.Image var scaled image.Image
if dw == srcB.Dx() && dh == srcB.Dy() { if dw == srcB.Dx() && dh == srcB.Dy() {
scaled = img scaled = img
} else { } else {
scaled = scaleNearest(img, dw, dh) scaled = scaleNearest(img, dw, dh)
} }
// Compose centered on canvas // Compose centered on canvas
canvas := image.NewRGBA(image.Rect(0,0,blogImgW,blogImgH)) canvas := image.NewRGBA(image.Rect(0, 0, blogImgW, blogImgH))
draw.Draw(canvas, canvas.Bounds(), &image.Uniform{C:bg}, image.Point{}, draw.Src) draw.Draw(canvas, canvas.Bounds(), &image.Uniform{C: bg}, image.Point{}, draw.Src)
offX := (blogImgW - dw) / 2 offX := (blogImgW - dw) / 2
offY := (blogImgH - dh) / 2 offY := (blogImgH - dh) / 2
draw.Draw(canvas, image.Rect(offX, offY, offX+dw, offY+dh), scaled, scaled.Bounds().Min, draw.Over) draw.Draw(canvas, image.Rect(offX, offY, offX+dw, offY+dh), scaled, scaled.Bounds().Min, draw.Over)
// Write PNG atomically // Write PNG atomically
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { return err } if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
tmp := outPath + ".tmp" return err
f, err := os.Create(tmp) }
if err != nil { return err } tmp := outPath + ".tmp"
enc := png.Encoder{CompressionLevel: png.BestSpeed} f, err := os.Create(tmp)
if err := enc.Encode(f, canvas); err != nil { if err != nil {
f.Close(); _ = os.Remove(tmp); return fmt.Errorf("encode png: %w", err) return err
} }
f.Close() enc := png.Encoder{CompressionLevel: png.BestSpeed}
_ = os.Remove(outPath) if err := enc.Encode(f, canvas); err != nil {
if err := os.Rename(tmp, outPath); err != nil { return err } f.Close()
return nil _ = os.Remove(tmp)
return fmt.Errorf("encode png: %w", err)
}
f.Close()
_ = os.Remove(outPath)
if err := os.Rename(tmp, outPath); err != nil {
return err
}
return nil
} }
func staticPath() string { func staticPath() string {
@@ -391,12 +415,72 @@ func ytChannel() string {
// BlogItem represents a simple blog card item for the homepage // BlogItem represents a simple blog card item for the homepage
type BlogItem struct { type BlogItem struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Link string `json:"link"` Slug string `json:"slug"`
Image string `json:"image"` Link string `json:"link"`
MTime time.Time `json:"mtime"` Image string `json:"image"`
Categories []string `json:"categories,omitempty"` MTime time.Time `json:"mtime"`
Categories []string `json:"categories,omitempty"`
}
// generateSlug creates a URL-friendly slug from a title
func generateSlug(title string) string {
slug := strings.ToLower(title)
// Replace Czech characters with their ASCII equivalents
replacements := map[string]string{
"á": "a", "ä": "a", "č": "c", "ď": "d", "é": "e", "ě": "e", "í": "i", "ľ": "l",
"ň": "n", "ó": "o", "ö": "o", "ô": "o", "ř": "r", "š": "s", "ť": "t", "ú": "u",
"ů": "u", "ý": "y", "ž": "z",
"Á": "a", "Ä": "a", "Č": "c", "Ď": "d", "É": "e", "Ě": "e", "Í": "i", "Ľ": "l",
"Ň": "n", "Ó": "o", "Ö": "o", "Ô": "o", "Ř": "r", "Š": "s", "Ť": "t", "Ú": "u",
"Ů": "u", "Ý": "y", "Ž": "z",
}
for czech, ascii := range replacements {
slug = strings.ReplaceAll(slug, czech, ascii)
}
// Remove any character that isn't alphanumeric, space, or hyphen
re := regexp.MustCompile(`[^a-z0-9\s-]`)
slug = re.ReplaceAllString(slug, "")
// Replace spaces and multiple hyphens with a single hyphen
re = regexp.MustCompile(`[\s-]+`)
slug = re.ReplaceAllString(slug, "-")
// Remove leading and trailing hyphens
slug = strings.Trim(slug, "-")
return slug
}
// ensureUniqueSlug ensures the slug is unique by appending a number if needed
func ensureUniqueSlug(siteRoot, baseSlug string) string {
blogDir := filepath.Join(siteRoot, "blog")
entries, err := os.ReadDir(blogDir)
if err != nil {
return baseSlug
}
existingSlugs := make(map[string]bool)
for _, e := range entries {
if !strings.HasSuffix(e.Name(), ".html") {
continue
}
// Extract slug from filename if it follows the new pattern
name := strings.TrimSuffix(e.Name(), ".html")
// Check if it's a slug-based filename (contains letters, not just numbers)
if regexp.MustCompile(`[a-z]`).MatchString(name) {
existingSlugs[name] = true
}
}
if !existingSlugs[baseSlug] {
return baseSlug
}
// Try baseSlug-2, baseSlug-3, etc.
for i := 2; i < 100; i++ {
testSlug := fmt.Sprintf("%s-%d", baseSlug, i)
if !existingSlugs[testSlug] {
return testSlug
}
}
// Fallback to timestamp
return fmt.Sprintf("%s-%d", baseSlug, time.Now().Unix())
} }
func extractCategories(path string) []string { func extractCategories(path string) []string {
@@ -415,55 +499,192 @@ func extractCategories(path string) []string {
return categories return categories
} }
// listLatestBlogs scans the blog and image folders under the provided site root and returns the latest N posts // extractAnnotation extracts the annotation from blog HTML meta tags
func extractAnnotation(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
s := string(b)
// Try to find description in meta tag first
re := regexp.MustCompile(`(?is)<meta name="description" content="([^"]+)"`)
m := re.FindStringSubmatch(s)
if len(m) >= 2 {
return m[1]
}
return ""
}
// extractContentMode extracts the content mode from blog HTML meta tags
func extractContentMode(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
s := string(b)
// Try to find content_mode in meta tag first
re := regexp.MustCompile(`(?is)<meta name="content_mode" content="([^"]+)"`)
m := re.FindStringSubmatch(s)
if len(m) >= 2 {
return m[1]
}
// Default to visual if not found
return "visual"
}
// extractSlug extracts the slug from blog HTML meta tags or generates from filename
func extractSlug(path, filename string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
s := string(b)
// Try to find slug in meta tag first
re := regexp.MustCompile(`(?is)<meta name="slug" content="([^"]+)"`)
m := re.FindStringSubmatch(s)
if len(m) >= 2 {
return m[1]
}
// Fallback: generate slug from title
title := extractTitle(path)
if title != "" {
return generateSlug(title)
}
// Final fallback: use filename without extension
return strings.TrimSuffix(filename, ".html")
}
func extractBlogID(path, filename string) string {
b, err := os.ReadFile(path)
if err != nil {
return strings.TrimSuffix(filename, ".html")
}
s := string(b)
// Try to find ID in meta tag first
re := regexp.MustCompile(`(?is)<meta name="id" content="([^"]+)"`)
m := re.FindStringSubmatch(s)
if len(m) >= 2 {
return m[1]
}
// Fallback: use filename without extension
return strings.TrimSuffix(filename, ".html")
}
func listLatestBlogs(siteRoot string, limit int) ([]BlogItem, error) { func listLatestBlogs(siteRoot string, limit int) ([]BlogItem, error) {
// Use the siteRoot path where blogs are actually located
blogDir := filepath.Join(siteRoot, "blog") blogDir := filepath.Join(siteRoot, "blog")
imgDir := filepath.Join(siteRoot, "img", "blog")
// For local development, you can override with environment variable
if envPath := os.Getenv("REMOTE_BLOG_DIR"); envPath != "" {
blogDir = envPath
}
// If blog directory doesn't exist, return error
if _, err := os.Stat(blogDir); os.IsNotExist(err) {
return nil, fmt.Errorf("blog directory not found: %s. Set REMOTE_BLOG_DIR environment variable or ensure blog directory exists", blogDir)
}
imgDir := filepath.Join(filepath.Dir(blogDir), "img", "blog")
entries, err := os.ReadDir(blogDir) entries, err := os.ReadDir(blogDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("readdir blog: %w", err) return nil, fmt.Errorf("readdir blog: %w", err)
} }
re := regexp.MustCompile(`^(\d{4})\.html$`) // Match both numeric (0001.html) and slug-based filenames
re := regexp.MustCompile(`^(\d{4}|[a-z0-9-]+)\.html$`)
var items []BlogItem var items []BlogItem
seenIDs := make(map[string]bool) // Track seen IDs to avoid duplicates
for _, e := range entries { for _, e := range entries {
name := e.Name() name := e.Name()
if !re.MatchString(name) { if !re.MatchString(name) {
continue continue
} }
id := strings.TrimSuffix(name, ".html") id := strings.TrimSuffix(name, ".html")
// Skip if this ID was already processed (deduplication)
if seenIDs[id] {
continue
}
// Title and categories extraction from blog HTML // Title and categories extraction from blog HTML
blogPath := filepath.Join(blogDir, name) blogPath := filepath.Join(blogDir, name)
title := extractTitle(blogPath) title := extractTitle(blogPath)
slug := extractSlug(blogPath, name)
cats := extractCategories(blogPath) cats := extractCategories(blogPath)
// Mark this ID as seen
seenIDs[id] = true
// Determine mod time - prefer image modtime if exists, else html // Determine mod time - prefer image modtime if exists, else html
mtime := time.Time{} mtime := time.Time{}
htmlInfo, err1 := os.Stat(filepath.Join(blogDir, name)) htmlInfo, err1 := os.Stat(filepath.Join(blogDir, name))
if err1 == nil { if err1 == nil {
mtime = htmlInfo.ModTime() mtime = htmlInfo.ModTime()
} }
if imgInfo, err2 := os.Stat(filepath.Join(imgDir, id+".png")); err2 == nil { // For image path, try to find corresponding numeric ID
imageID := id
if regexp.MustCompile(`^[a-z]`).MatchString(id) {
// This is a slug, try to find corresponding numeric file
numericFiles, _ := filepath.Glob(filepath.Join(blogDir, "????.html"))
for _, numericFile := range numericFiles {
numericID := strings.TrimSuffix(filepath.Base(numericFile), ".html")
numericPath := filepath.Join(blogDir, numericFile)
numericSlug := extractSlug(numericPath, numericFile)
if numericSlug == id {
imageID = numericID
break
}
}
}
if imgInfo, err2 := os.Stat(filepath.Join(imgDir, imageID+".png")); err2 == nil {
// If image is newer, use that as a proxy for recency // If image is newer, use that as a proxy for recency
if imgInfo.ModTime().After(mtime) { if imgInfo.ModTime().After(mtime) {
mtime = imgInfo.ModTime() mtime = imgInfo.ModTime()
} }
} }
// Use slug-based link if slug exists and is not just numeric, otherwise use numeric
var link string
if slug != "" && regexp.MustCompile(`[a-z]`).MatchString(slug) {
link = "/blog/" + slug
} else {
link = "/blog/" + id + ".html"
}
items = append(items, BlogItem{ items = append(items, BlogItem{
ID: id, ID: id,
Title: title, Title: title,
Link: "/blog/" + id + ".html", Slug: slug,
Image: "/img/blog/" + id + ".png", Link: link,
MTime: mtime, Image: "/img/blog/" + imageID + ".png",
Categories: cats, MTime: mtime,
Categories: cats,
}) })
} }
sort.Slice(items, func(i, j int) bool { sort.Slice(items, func(i, j int) bool {
// Descending by mod time, fallback to numeric ID desc // Check if files were recently processed (all have same timestamp from setup script)
recentThreshold := time.Now().Add(-24 * time.Hour)
allRecent := items[i].MTime.After(recentThreshold) && items[j].MTime.After(recentThreshold)
if allRecent {
// If both files are recent (from setup script), sort by numeric ID (higher = newer)
ii, err1 := strconv.Atoi(items[i].ID)
jj, err2 := strconv.Atoi(items[j].ID)
if err1 == nil && err2 == nil {
return ii > jj
}
// If not numeric, fall back to string comparison
return items[i].ID > items[j].ID
}
// Otherwise, use modification time (newest first)
if !items[i].MTime.Equal(items[j].MTime) { if !items[i].MTime.Equal(items[j].MTime) {
return items[i].MTime.After(items[j].MTime) return items[i].MTime.After(items[j].MTime)
} }
ii, _ := strconv.Atoi(items[i].ID)
jj, _ := strconv.Atoi(items[j].ID) // If times are equal and not recent, fallback to numeric ID
return ii > jj ii, err1 := strconv.Atoi(items[i].ID)
jj, err2 := strconv.Atoi(items[j].ID)
if err1 == nil && err2 == nil {
return ii > jj
}
return items[i].ID > items[j].ID
}) })
if limit > 0 && len(items) > limit { if limit > 0 && len(items) > limit {
items = items[:limit] items = items[:limit]
@@ -652,6 +873,25 @@ func main() {
w.Write([]byte(";")) w.Write([]byte(";"))
}) })
// Blog list JSON for frontend
mux.HandleFunc("/data/blog-list.json", func(w http.ResponseWriter, r *http.Request) {
okCORS(w)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
limit := 50 // Default limit for blog list
if q := r.URL.Query().Get("limit"); q != "" {
if n, err := strconv.Atoi(q); err == nil && n > 0 {
limit = n
}
}
items, err := listLatestBlogs(staticPath(), limit)
if err != nil {
log.Printf("blog-list.json error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(items)
})
// Blog API: latest N posts from filesystem // Blog API: latest N posts from filesystem
mux.HandleFunc("/api/blog/latest", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/blog/latest", func(w http.ResponseWriter, r *http.Request) {
okCORS(w) okCORS(w)
@@ -758,12 +998,15 @@ func main() {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
// Expect multipart form with: title, content (HTML), image (png), categories (comma-separated) // Expect multipart form with: title, slug, annotation, content_mode, content (HTML), image (png), categories (comma-separated)
if err := r.ParseMultipartForm(20 << 20); err != nil { // 20MB if err := r.ParseMultipartForm(20 << 20); err != nil { // 20MB
http.Error(w, "invalid form", http.StatusBadRequest) http.Error(w, "invalid form", http.StatusBadRequest)
return return
} }
title := strings.TrimSpace(r.FormValue("title")) title := strings.TrimSpace(r.FormValue("title"))
slugInput := strings.TrimSpace(r.FormValue("slug"))
annotation := strings.TrimSpace(r.FormValue("annotation"))
contentMode := strings.TrimSpace(r.FormValue("content_mode"))
htmlContent := strings.TrimSpace(r.FormValue("content")) htmlContent := strings.TrimSpace(r.FormValue("content"))
catsRaw := strings.TrimSpace(r.FormValue("categories")) catsRaw := strings.TrimSpace(r.FormValue("categories"))
var cats []string var cats []string
@@ -779,6 +1022,10 @@ func main() {
http.Error(w, "missing title or content", http.StatusBadRequest) http.Error(w, "missing title or content", http.StatusBadRequest)
return return
} }
// Default content mode to visual if not specified
if contentMode == "" {
contentMode = "visual"
}
f, fh, err := r.FormFile("image") f, fh, err := r.FormFile("image")
if err != nil { if err != nil {
http.Error(w, "missing image", http.StatusBadRequest) http.Error(w, "missing image", http.StatusBadRequest)
@@ -799,6 +1046,18 @@ func main() {
return return
} }
idStr := fmt.Sprintf("%04d", nid) idStr := fmt.Sprintf("%04d", nid)
// Determine slug
var finalSlug string
if slugInput != "" {
// Use provided slug (already validated by frontend)
finalSlug = slugInput
} else {
// Generate from title
baseSlug := generateSlug(title)
finalSlug = ensureUniqueSlug(site, baseSlug)
}
// Write image (normalize to 1600x969 with letterboxing) // Write image (normalize to 1600x969 with letterboxing)
imgDir := filepath.Join(site, "img", "blog") imgDir := filepath.Join(site, "img", "blog")
if err := os.MkdirAll(imgDir, 0755); err != nil { if err := os.MkdirAll(imgDir, 0755); err != nil {
@@ -836,29 +1095,102 @@ func main() {
for _, c := range cats { for _, c := range cats {
meta += "<meta name=\"category\" content=\"" + htmlEscape(c) + "\">\n" meta += "<meta name=\"category\" content=\"" + htmlEscape(c) + "\">\n"
} }
reHead := regexp.MustCompile(`(?is)</head>`) reHead := regexp.MustCompile(`(?is)</head>`)
s = reHead.ReplaceAllString(s, meta+"</head>") s = reHead.ReplaceAllString(s, meta+"</head>")
} }
// Write new blog html // Inject slug as <meta name="slug" content="..."> before </head>
slugMeta := "<meta name=\"slug\" content=\"" + htmlEscape(finalSlug) + "\">\n"
reHeadSlug := regexp.MustCompile(`(?is)</head>`)
s = reHeadSlug.ReplaceAllString(s, slugMeta+"</head>")
// Inject annotation as <meta name="description" content="..."> before </head>
if annotation != "" {
annotationMeta := "<meta name=\"description\" content=\"" + htmlEscape(annotation) + "\">\n"
reHeadAnnotation := regexp.MustCompile(`(?is)</head>`)
s = reHeadAnnotation.ReplaceAllString(s, annotationMeta+"</head>")
}
// Inject content mode as <meta name="content_mode" content="..."> before </head>
contentModeMeta := "<meta name=\"content_mode\" content=\"" + htmlEscape(contentMode) + "\">\n"
reHeadContentMode := regexp.MustCompile(`(?is)</head>`)
s = reHeadContentMode.ReplaceAllString(s, contentModeMeta+"</head>")
// Write new blog html with both numeric and slug filenames
blogDir := filepath.Join(site, "blog") blogDir := filepath.Join(site, "blog")
if err := os.MkdirAll(blogDir, 0755); err != nil { if err := os.MkdirAll(blogDir, 0755); err != nil {
http.Error(w, "storage error: blog dir", http.StatusInternalServerError) http.Error(w, "storage error: blog dir", http.StatusInternalServerError)
return return
} }
// Write numeric file (for backward compatibility)
htmlPath := filepath.Join(blogDir, idStr+".html") htmlPath := filepath.Join(blogDir, idStr+".html")
if err := os.WriteFile(htmlPath, []byte(s), 0644); err != nil { if err := os.WriteFile(htmlPath, []byte(s), 0644); err != nil {
http.Error(w, "cannot write blog (is STATIC_PATH read-only?)", http.StatusInternalServerError) http.Error(w, "cannot write blog (is STATIC_PATH read-only?)", http.StatusInternalServerError)
return return
} }
// Write slug file (new format)
slugPath := filepath.Join(blogDir, finalSlug+".html")
if err := os.WriteFile(slugPath, []byte(s), 0644); err != nil {
http.Error(w, "cannot write slug blog (is STATIC_PATH read-only?)", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{ _ = json.NewEncoder(w).Encode(map[string]any{
"id": idStr, "id": idStr,
"link": "/blog/" + idStr + ".html", "slug": finalSlug,
"link": "/blog/" + finalSlug,
"image": "/img/blog/" + idStr + ".png", "image": "/img/blog/" + idStr + ".png",
"message": "created", "message": "created",
}) })
}) })
// Blog slug resolution: convert slug to numeric ID
mux.HandleFunc("/api/blog/resolve", func(w http.ResponseWriter, r *http.Request) {
okCORS(w)
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
slug := strings.TrimSpace(r.URL.Query().Get("slug"))
if slug == "" {
http.Error(w, "missing slug", http.StatusBadRequest)
return
}
// Validate slug format
if !regexp.MustCompile(`^[a-z0-9-]+$`).MatchString(slug) {
http.Error(w, "invalid slug format", http.StatusBadRequest)
return
}
site := staticPath()
blogDir := filepath.Join(site, "blog")
// First try to find slug-based file
slugPath := filepath.Join(blogDir, slug+".html")
if b, err := os.ReadFile(slugPath); err == nil {
s := string(b)
// Try to find the corresponding numeric file
entries, _ := os.ReadDir(blogDir)
for _, e := range entries {
name := e.Name()
if !regexp.MustCompile(`^\d{4}\.html$`).MatchString(name) {
continue
}
numericPath := filepath.Join(blogDir, name)
numericContent, _ := os.ReadFile(numericPath)
if string(numericContent) == s {
// Found matching numeric file
numericID := strings.TrimSuffix(name, ".html")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"id": numericID, "slug": slug})
return
}
}
}
// If not found, return 404
http.Error(w, "slug not found", http.StatusNotFound)
})
// Blog fetch (admin): returns title, content html, image for editing // Blog fetch (admin): returns title, content html, image for editing
mux.HandleFunc("/api/blog/get", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/blog/get", func(w http.ResponseWriter, r *http.Request) {
okCORS(w) okCORS(w)
@@ -901,9 +1233,12 @@ func main() {
content = strings.TrimSpace(mc[1]) content = strings.TrimSpace(mc[1])
} }
cats := extractCategories(path) cats := extractCategories(path)
slug := extractSlug(path, id+".html")
annotation := extractAnnotation(path)
contentMode := extractContentMode(path)
img := "/img/blog/" + id + ".png" img := "/img/blog/" + id + ".png"
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"id": id, "title": title, "content_html": content, "image": img, "categories": cats}) _ = json.NewEncoder(w).Encode(map[string]any{"id": id, "title": title, "slug": slug, "annotation": annotation, "content_mode": contentMode, "content_html": content, "image": img, "categories": cats})
}) })
// Blog edit (admin): update title/content and optionally replace image // Blog edit (admin): update title/content and optionally replace image
@@ -921,7 +1256,7 @@ func main() {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
// Expect multipart form with: title, content (HTML), image (png), categories (comma-separated) // Expect multipart form with: title, slug, annotation, content_mode, content (HTML), image (png), categories (comma-separated)
if err := r.ParseMultipartForm(25 << 20); err != nil { if err := r.ParseMultipartForm(25 << 20); err != nil {
http.Error(w, "invalid form", http.StatusBadRequest) http.Error(w, "invalid form", http.StatusBadRequest)
return return
@@ -933,6 +1268,9 @@ func main() {
return return
} }
title := strings.TrimSpace(r.FormValue("title")) title := strings.TrimSpace(r.FormValue("title"))
slugInput := strings.TrimSpace(r.FormValue("slug"))
annotation := strings.TrimSpace(r.FormValue("annotation"))
contentMode := strings.TrimSpace(r.FormValue("content_mode"))
htmlContent := strings.TrimSpace(r.FormValue("content")) htmlContent := strings.TrimSpace(r.FormValue("content"))
catsRaw := strings.TrimSpace(r.FormValue("categories")) catsRaw := strings.TrimSpace(r.FormValue("categories"))
var cats []string var cats []string
@@ -948,6 +1286,10 @@ func main() {
http.Error(w, "missing title or content", http.StatusBadRequest) http.Error(w, "missing title or content", http.StatusBadRequest)
return return
} }
// Default content mode to visual if not specified
if contentMode == "" {
contentMode = "visual"
}
site := staticPath() site := staticPath()
if f, fh, err := r.FormFile("image"); err == nil { if f, fh, err := r.FormFile("image"); err == nil {
defer f.Close() defer f.Close()
@@ -986,9 +1328,29 @@ func main() {
for _, c := range cats { for _, c := range cats {
meta += "<meta name=\"category\" content=\"" + htmlEscape(c) + "\">\n" meta += "<meta name=\"category\" content=\"" + htmlEscape(c) + "\">\n"
} }
reHead := regexp.MustCompile(`(?is)</head>`) reHead := regexp.MustCompile(`(?is)</head>`)
s = reHead.ReplaceAllString(s, meta+"</head>") s = reHead.ReplaceAllString(s, meta+"</head>")
} }
// Update or add slug meta tag
reSlug := regexp.MustCompile(`(?is)<meta name="slug" content="[^"]*"\s*/?>\s*`)
s = reSlug.ReplaceAllString(s, "")
slugMeta := "<meta name=\"slug\" content=\"" + htmlEscape(slugInput) + "\">\n"
reHeadSlug := regexp.MustCompile(`(?is)</head>`)
s = reHeadSlug.ReplaceAllString(s, slugMeta+"</head>")
// Update or add annotation meta tag
reAnnotation := regexp.MustCompile(`(?is)<meta name="description" content="[^"]*"\s*/?>\s*`)
s = reAnnotation.ReplaceAllString(s, "")
if annotation != "" {
annotationMeta := "<meta name=\"description\" content=\"" + htmlEscape(annotation) + "\">\n"
reHeadAnnotation := regexp.MustCompile(`(?is)</head>`)
s = reHeadAnnotation.ReplaceAllString(s, annotationMeta+"</head>")
}
// Update or add content mode meta tag
reContentMode := regexp.MustCompile(`(?is)<meta name="content_mode" content="[^"]*"\s*/?>\s*`)
s = reContentMode.ReplaceAllString(s, "")
contentModeMeta := "<meta name=\"content_mode\" content=\"" + htmlEscape(contentMode) + "\">\n"
reHeadContentMode := regexp.MustCompile(`(?is)</head>`)
s = reHeadContentMode.ReplaceAllString(s, contentModeMeta+"</head>")
if err := os.WriteFile(hPath, []byte(s), 0644); err != nil { if err := os.WriteFile(hPath, []byte(s), 0644); err != nil {
http.Error(w, "cannot write", http.StatusInternalServerError) http.Error(w, "cannot write", http.StatusInternalServerError)
return return
@@ -1019,44 +1381,110 @@ func main() {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
}) })
// Static file server for the frontend // Static file server for the frontend
sp := staticPath() sp := staticPath()
log.Printf("serving static from: %s", sp) log.Printf("serving static from: %s", sp)
fs := http.FileServer(http.Dir(sp)) fs := http.FileServer(http.Dir(sp))
// Serve common asset prefixes explicitly // Serve common asset prefixes explicitly
mux.Handle("/img/", fs) mux.Handle("/img/", fs)
mux.Handle("/css/", fs) mux.Handle("/css/", fs)
mux.Handle("/js/", fs) mux.Handle("/js/", fs)
// Protect /admin/ with Basic Auth // Protect /admin/ with Basic Auth
mux.Handle("/admin/", basicAuth(http.StripPrefix("/admin/", http.FileServer(http.Dir(filepath.Join(staticPath(), "admin")))))) mux.Handle("/admin/", basicAuth(http.StripPrefix("/admin/", http.FileServer(http.Dir(filepath.Join(staticPath(), "admin"))))))
mux.Handle("/blog/", fs)
mux.Handle("/zapasy/", fs)
// Fallback: serve index.html at root, otherwise delegate to static file server
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
http.ServeFile(w, r, filepath.Join(sp, "index.html"))
return
}
fs.ServeHTTP(w, r)
})
port := os.Getenv("PORT") // Custom slug-based blog routing
if port == "" { port = "8080" } mux.HandleFunc("/blog/", func(w http.ResponseWriter, r *http.Request) {
srv := &http.Server{ // Extract the slug/ID from the path
Addr: ":" + port, path := strings.TrimPrefix(r.URL.Path, "/blog/")
Handler: mux, if path == "" || path == "/" {
} // Serve the main blog listing page
go func() { http.ServeFile(w, r, filepath.Join(sp, "blog.html"))
log.Printf("server listening on :%s", port) return
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { }
log.Fatalf("server error: %v", err)
}
}()
<-ctx.Done() // Remove .html extension if present
ctxShut, cancel := context.WithTimeout(context.Background(), 5*time.Second) if strings.HasSuffix(path, ".html") {
defer cancel() path = strings.TrimSuffix(path, ".html")
_ = srv.Shutdown(ctxShut) }
// Check if it's a numeric ID (legacy format)
if regexp.MustCompile(`^\d{4}$`).MatchString(path) {
// Serve numeric file directly
numericPath := filepath.Join(sp, "blog", path+".html")
if _, err := os.Stat(numericPath); err == nil {
http.ServeFile(w, r, numericPath)
return
}
}
// Check if it's a slug (new format)
if regexp.MustCompile(`^[a-z0-9-]+$`).MatchString(path) {
slugPath := filepath.Join(sp, "blog", path+".html")
if _, err := os.Stat(slugPath); err == nil {
http.ServeFile(w, r, slugPath)
return
}
// If slug file doesn't exist, try to resolve slug to numeric file
blogDir := filepath.Join(sp, "blog")
entries, err := os.ReadDir(blogDir)
if err == nil {
for _, entry := range entries {
name := entry.Name()
if !regexp.MustCompile(`^\d{4}\.html$`).MatchString(name) {
continue
}
numericPath := filepath.Join(blogDir, name)
// Check if this numeric file has the matching slug
b, err := os.ReadFile(numericPath)
if err != nil {
continue
}
s := string(b)
re := regexp.MustCompile(`(?is)<meta name="slug" content="([^"]+)"`)
m := re.FindStringSubmatch(s)
if len(m) >= 2 && m[1] == path {
http.ServeFile(w, r, numericPath)
return
}
}
}
}
// If not found, serve 404
http.NotFound(w, r)
})
mux.Handle("/zapasy/", fs)
// Fallback: serve index.html at root, otherwise delegate to static file server
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
http.ServeFile(w, r, filepath.Join(sp, "index.html"))
return
}
fs.ServeHTTP(w, r)
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
srv := &http.Server{
Addr: ":" + port,
Handler: mux,
}
go func() {
log.Printf("server listening on :%s", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
<-ctx.Done()
ctxShut, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctxShut)
} }
func okCORS(w http.ResponseWriter) { func okCORS(w http.ResponseWriter) {
+31231 -31231
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
File diff suppressed because one or more lines are too long
+162 -162
View File
@@ -1,163 +1,163 @@
.lt-custom-popup { .lt-custom-popup {
position: fixed; position: fixed;
right: 8px; right: 8px;
top: 50%; top: 50%;
z-index: 1000; z-index: 1000;
display: none; display: none;
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
text-align: center; text-align: center;
border-radius: 64px; border-radius: 64px;
box-shadow: 0 0 25px rgba(0,0,0,.08); box-shadow: 0 0 25px rgba(0,0,0,.08);
padding: 20px 10px 20px 10px; padding: 20px 10px 20px 10px;
background-color: #fff; background-color: #fff;
} }
.lt-custom-popup img { .lt-custom-popup img {
margin-bottom: 12px; margin-bottom: 12px;
} }
.lt-custom-popup .close { .lt-custom-popup .close {
font-size: 14px; font-size: 14px;
border-radius: 50%; border-radius: 50%;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
font-size: 20px; font-size: 20px;
color: #CE4F4D; color: #CE4F4D;
z-index: 20; z-index: 20;
text-align: center; text-align: center;
line-height: 20px; line-height: 20px;
width: 20px; width: 20px;
height: 20px; height: 20px;
display: block; display: block;
opacity: 1; opacity: 1;
background-color: #EEEEEE; background-color: #EEEEEE;
transition: all 0.5s ease; transition: all 0.5s ease;
} }
.lt-custom-popup.closed { .lt-custom-popup.closed {
padding: 15px 8px 6px 10px; padding: 15px 8px 6px 10px;
right: 0; right: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
transition: all 0.25s ease; transition: all 0.25s ease;
} }
.lt-custom-popup.closed:hover { .lt-custom-popup.closed:hover {
padding-right: 14px; padding-right: 14px;
} }
.ltx-font-selector:hover { .ltx-font-selector:hover {
cursor: pointer; cursor: pointer;
} }
.lt-custom-popup.closed img:hover { .lt-custom-popup.closed img:hover {
cursor: pointer; cursor: pointer;
} }
.lt-custom-popup.closed .close { .lt-custom-popup.closed .close {
display: none; display: none;
} }
.lt-custom-popup.closed div { .lt-custom-popup.closed div {
display: none; display: none;
} }
.lt-custom-popup.closed .img { .lt-custom-popup.closed .img {
display: block; display: block;
overflow: hidden; overflow: hidden;
height: 35px; height: 35px;
width: 35px; width: 35px;
text-align: center; text-align: center;
} }
.lt-custom-popup.closed .img img { .lt-custom-popup.closed .img img {
margin: 0; margin: 0;
} }
.lt-custom-popup .close:hover { .lt-custom-popup .close:hover {
opacity: 1; opacity: 1;
color: #000; color: #000;
} }
@media (max-width: 991px) { @media (max-width: 991px) {
.lt-custom-popup { .lt-custom-popup {
display: none !important; display: none !important;
} }
} }
.ltx-font-selector div, .ltx-font-selector div,
.ltx-color-selector div { .ltx-color-selector div {
border-radius: 50%; border-radius: 50%;
width: 35px; width: 35px;
height: 35px; height: 35px;
border: 4px solid #fff; border: 4px solid #fff;
display: block; display: block;
margin: 4px auto 0; margin: 4px auto 0;
box-shadow: 0 0 5px rgba(0,0,0,.1); box-shadow: 0 0 5px rgba(0,0,0,.1);
cursor: pointer; cursor: pointer;
transition: all 0.5s ease; transition: all 0.5s ease;
} }
.ltx-font-selector div:hover, .ltx-font-selector div:hover,
.ltx-color-selector div:hover { .ltx-color-selector div:hover {
box-shadow: 0 0 5px rgba(0,0,0,.2); box-shadow: 0 0 5px rgba(0,0,0,.2);
} }
.lt-custom-field { .lt-custom-field {
padding: 0; padding: 0;
border: 0; border: 0;
width: 20px; width: 20px;
height: 20px; height: 20px;
border-radius: 50%; border-radius: 50%;
margin-bottom: 14px; margin-bottom: 14px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
cursor: pointer; cursor: pointer;
display: block; display: block;
} }
/* FACR upcoming mobile visibility overrides */ /* FACR upcoming mobile visibility overrides */
@media (max-width: 767px) { @media (max-width: 767px) {
/* Show the Zápasy (x/y) header on phones */ /* Show the Zápasy (x/y) header on phones */
.lte-football-upcoming .lte-header-upcoming { .lte-football-upcoming .lte-header-upcoming {
display: inline-block !important; display: inline-block !important;
font-size: 18px; font-size: 18px;
line-height: 1.2; line-height: 1.2;
padding: 2px 8px; padding: 2px 8px;
} }
/* Ensure countdown line is visible and centered */ /* Ensure countdown line is visible and centered */
#facr-countdown.lte-football-date { #facr-countdown.lte-football-date {
display: block !important; display: block !important;
text-align: center !important; text-align: center !important;
} }
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+145 -145
View File
@@ -1,146 +1,146 @@
.lt-custom-popup { .lt-custom-popup {
position: fixed; position: fixed;
right: 8px; right: 8px;
top: 50%; top: 50%;
z-index: 1000; z-index: 1000;
display: none; display: none;
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%); -ms-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
text-align: center; text-align: center;
border-radius: 64px; border-radius: 64px;
box-shadow: 0 0 25px rgba(0,0,0,.08); box-shadow: 0 0 25px rgba(0,0,0,.08);
padding: 20px 10px 20px 10px; padding: 20px 10px 20px 10px;
background-color: #fff; background-color: #fff;
} }
.lt-custom-popup img { .lt-custom-popup img {
margin-bottom: 12px; margin-bottom: 12px;
} }
.lt-custom-popup .close { .lt-custom-popup .close {
font-size: 14px; font-size: 14px;
border-radius: 50%; border-radius: 50%;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
font-size: 20px; font-size: 20px;
color: #CE4F4D; color: #CE4F4D;
z-index: 20; z-index: 20;
text-align: center; text-align: center;
line-height: 20px; line-height: 20px;
width: 20px; width: 20px;
height: 20px; height: 20px;
display: block; display: block;
opacity: 1; opacity: 1;
background-color: #EEEEEE; background-color: #EEEEEE;
transition: all 0.5s ease; transition: all 0.5s ease;
} }
.lt-custom-popup.closed { .lt-custom-popup.closed {
padding: 15px 8px 6px 10px; padding: 15px 8px 6px 10px;
right: 0; right: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
transition: all 0.25s ease; transition: all 0.25s ease;
} }
.lt-custom-popup.closed:hover { .lt-custom-popup.closed:hover {
padding-right: 14px; padding-right: 14px;
} }
.ltx-font-selector:hover { .ltx-font-selector:hover {
cursor: pointer; cursor: pointer;
} }
.lt-custom-popup.closed img:hover { .lt-custom-popup.closed img:hover {
cursor: pointer; cursor: pointer;
} }
.lt-custom-popup.closed .close { .lt-custom-popup.closed .close {
display: none; display: none;
} }
.lt-custom-popup.closed div { .lt-custom-popup.closed div {
display: none; display: none;
} }
.lt-custom-popup.closed .img { .lt-custom-popup.closed .img {
display: block; display: block;
overflow: hidden; overflow: hidden;
height: 35px; height: 35px;
width: 35px; width: 35px;
text-align: center; text-align: center;
} }
.lt-custom-popup.closed .img img { .lt-custom-popup.closed .img img {
margin: 0; margin: 0;
} }
.lt-custom-popup .close:hover { .lt-custom-popup .close:hover {
opacity: 1; opacity: 1;
color: #000; color: #000;
} }
@media (max-width: 991px) { @media (max-width: 991px) {
.lt-custom-popup { .lt-custom-popup {
display: none !important; display: none !important;
} }
} }
.ltx-font-selector div, .ltx-font-selector div,
.ltx-color-selector div { .ltx-color-selector div {
border-radius: 50%; border-radius: 50%;
width: 35px; width: 35px;
height: 35px; height: 35px;
border: 4px solid #fff; border: 4px solid #fff;
display: block; display: block;
margin: 4px auto 0; margin: 4px auto 0;
box-shadow: 0 0 5px rgba(0,0,0,.1); box-shadow: 0 0 5px rgba(0,0,0,.1);
cursor: pointer; cursor: pointer;
transition: all 0.5s ease; transition: all 0.5s ease;
} }
.ltx-font-selector div:hover, .ltx-font-selector div:hover,
.ltx-color-selector div:hover { .ltx-color-selector div:hover {
box-shadow: 0 0 5px rgba(0,0,0,.2); box-shadow: 0 0 5px rgba(0,0,0,.2);
} }
.lt-custom-field { .lt-custom-field {
padding: 0; padding: 0;
border: 0; border: 0;
width: 20px; width: 20px;
height: 20px; height: 20px;
border-radius: 50%; border-radius: 50%;
margin-bottom: 14px; margin-bottom: 14px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
cursor: pointer; cursor: pointer;
display: block; display: block;
} }
+171 -171
View File
@@ -1,172 +1,172 @@
.wpcf7 .screen-reader-response { .wpcf7 .screen-reader-response {
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
clip: rect(1px, 1px, 1px, 1px); clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%); clip-path: inset(50%);
height: 1px; height: 1px;
width: 1px; width: 1px;
margin: -1px; margin: -1px;
padding: 0; padding: 0;
border: 0; border: 0;
word-wrap: normal !important; word-wrap: normal !important;
} }
.wpcf7 form .wpcf7-response-output { .wpcf7 form .wpcf7-response-output {
margin: 2em 0.5em 1em; margin: 2em 0.5em 1em;
padding: 0.2em 1em; padding: 0.2em 1em;
border: 2px solid #00a0d2; /* Blue */ border: 2px solid #00a0d2; /* Blue */
} }
.wpcf7 form.init .wpcf7-response-output, .wpcf7 form.init .wpcf7-response-output,
.wpcf7 form.resetting .wpcf7-response-output, .wpcf7 form.resetting .wpcf7-response-output,
.wpcf7 form.submitting .wpcf7-response-output { .wpcf7 form.submitting .wpcf7-response-output {
display: none; display: none;
} }
.wpcf7 form.sent .wpcf7-response-output { .wpcf7 form.sent .wpcf7-response-output {
border-color: #46b450; /* Green */ border-color: #46b450; /* Green */
} }
.wpcf7 form.failed .wpcf7-response-output, .wpcf7 form.failed .wpcf7-response-output,
.wpcf7 form.aborted .wpcf7-response-output { .wpcf7 form.aborted .wpcf7-response-output {
border-color: #dc3232; /* Red */ border-color: #dc3232; /* Red */
} }
.wpcf7 form.spam .wpcf7-response-output { .wpcf7 form.spam .wpcf7-response-output {
border-color: #f56e28; /* Orange */ border-color: #f56e28; /* Orange */
} }
.wpcf7 form.invalid .wpcf7-response-output, .wpcf7 form.invalid .wpcf7-response-output,
.wpcf7 form.unaccepted .wpcf7-response-output, .wpcf7 form.unaccepted .wpcf7-response-output,
.wpcf7 form.payment-required .wpcf7-response-output { .wpcf7 form.payment-required .wpcf7-response-output {
border-color: #ffb900; /* Yellow */ border-color: #ffb900; /* Yellow */
} }
.wpcf7-form-control-wrap { .wpcf7-form-control-wrap {
position: relative; position: relative;
} }
.wpcf7-not-valid-tip { .wpcf7-not-valid-tip {
color: #dc3232; /* Red */ color: #dc3232; /* Red */
font-size: 1em; font-size: 1em;
font-weight: normal; font-weight: normal;
display: block; display: block;
} }
.use-floating-validation-tip .wpcf7-not-valid-tip { .use-floating-validation-tip .wpcf7-not-valid-tip {
position: relative; position: relative;
top: -2ex; top: -2ex;
left: 1em; left: 1em;
z-index: 100; z-index: 100;
border: 1px solid #dc3232; border: 1px solid #dc3232;
background: #fff; background: #fff;
padding: .2em .8em; padding: .2em .8em;
width: 24em; width: 24em;
} }
.wpcf7-list-item { .wpcf7-list-item {
display: inline-block; display: inline-block;
margin: 0 0 0 1em; margin: 0 0 0 1em;
} }
.wpcf7-list-item-label::before, .wpcf7-list-item-label::before,
.wpcf7-list-item-label::after { .wpcf7-list-item-label::after {
content: " "; content: " ";
} }
.wpcf7-spinner { .wpcf7-spinner {
visibility: hidden; visibility: hidden;
display: inline-block; display: inline-block;
background-color: #23282d; /* Dark Gray 800 */ background-color: #23282d; /* Dark Gray 800 */
opacity: 0.75; opacity: 0.75;
width: 24px; width: 24px;
height: 24px; height: 24px;
border: none; border: none;
border-radius: 100%; border-radius: 100%;
padding: 0; padding: 0;
margin: 0 24px; margin: 0 24px;
position: relative; position: relative;
} }
form.submitting .wpcf7-spinner { form.submitting .wpcf7-spinner {
visibility: visible; visibility: visible;
} }
.wpcf7-spinner::before { .wpcf7-spinner::before {
content: ''; content: '';
position: absolute; position: absolute;
background-color: #fbfbfc; /* Light Gray 100 */ background-color: #fbfbfc; /* Light Gray 100 */
top: 4px; top: 4px;
left: 4px; left: 4px;
width: 6px; width: 6px;
height: 6px; height: 6px;
border: none; border: none;
border-radius: 100%; border-radius: 100%;
transform-origin: 8px 8px; transform-origin: 8px 8px;
animation-name: spin; animation-name: spin;
animation-duration: 1000ms; animation-duration: 1000ms;
animation-timing-function: linear; animation-timing-function: linear;
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.wpcf7-spinner::before { .wpcf7-spinner::before {
animation-name: blink; animation-name: blink;
animation-duration: 2000ms; animation-duration: 2000ms;
} }
} }
@keyframes spin { @keyframes spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes blink { @keyframes blink {
from { from {
opacity: 0; opacity: 0;
} }
50% { 50% {
opacity: 1; opacity: 1;
} }
to { to {
opacity: 0; opacity: 0;
} }
} }
.wpcf7 [inert] { .wpcf7 [inert] {
opacity: 0.5; opacity: 0.5;
} }
.wpcf7 input[type="file"] { .wpcf7 input[type="file"] {
cursor: pointer; cursor: pointer;
} }
.wpcf7 input[type="file"]:disabled { .wpcf7 input[type="file"]:disabled {
cursor: default; cursor: default;
} }
.wpcf7 .wpcf7-submit:disabled { .wpcf7 .wpcf7-submit:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.wpcf7 input[type="url"], .wpcf7 input[type="url"],
.wpcf7 input[type="email"], .wpcf7 input[type="email"],
.wpcf7 input[type="tel"] { .wpcf7 input[type="tel"] {
direction: ltr; direction: ltr;
} }
.wpcf7-reflection > output { .wpcf7-reflection > output {
display: list-item; display: list-item;
list-style: none; list-style: none;
} }
.wpcf7-reflection > output[hidden] { .wpcf7-reflection > output[hidden] {
display: none; display: none;
} }
+531 -531
View File
File diff suppressed because it is too large Load Diff
+219 -219
View File
@@ -1,219 +1,219 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<team> <team>
<category name="men"> <category name="men">
<member> <member>
<name>Janečka Martin</name> <name>Janečka Martin</name>
<number>13</number> <number>13</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Janečka_Martin_1.png</image> <image>img/muzi/Janečka_Martin_1.png</image>
</member> </member>
<member> <member>
<name>Hubinka Adam</name> <name>Hubinka Adam</name>
<number>18</number> <number>18</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Hubinka_Adam_1.png</image> <image>img/muzi/Hubinka_Adam_1.png</image>
</member> </member>
<member> <member>
<name>Zapletal Martin</name> <name>Zapletal Martin</name>
<number>8</number> <number>8</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Zapletal_Martin_1.png</image> <image>img/muzi/Zapletal_Martin_1.png</image>
</member> </member>
<member> <member>
<name>Lapčík Martin</name> <name>Lapčík Martin</name>
<number>5</number> <number>5</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Lapcik_Martin_1.png</image> <image>img/muzi/Lapcik_Martin_1.png</image>
</member> </member>
<member> <member>
<name>Drobný Michal</name> <name>Drobný Michal</name>
<number>3</number> <number>3</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Drobný_Michal_1.png</image> <image>img/muzi/Drobný_Michal_1.png</image>
</member> </member>
<member> <member>
<name>Janečka Radek</name> <name>Janečka Radek</name>
<number>16</number> <number>16</number>
<role>Brankář</role> <role>Brankář</role>
<image>img/muzi/Janečka_Radek_1.png</image> <image>img/muzi/Janečka_Radek_1.png</image>
</member> </member>
<member> <member>
<name>Mačuda Jakub</name> <name>Mačuda Jakub</name>
<number>33</number> <number>33</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Mačuda_Jakub_1.png</image> <image>img/muzi/Mačuda_Jakub_1.png</image>
</member> </member>
<member> <member>
<name>Příplata Filip</name> <name>Příplata Filip</name>
<number>9</number> <number>9</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Příplata_Filip_1.png</image> <image>img/muzi/Příplata_Filip_1.png</image>
</member> </member>
<member> <member>
<name>Brázdil Petr</name> <name>Brázdil Petr</name>
<number>12</number> <number>12</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Brázdil_Petr_1.png</image> <image>img/muzi/Brázdil_Petr_1.png</image>
</member> </member>
<member> <member>
<name>Svízela Jakub</name> <name>Svízela Jakub</name>
<number>19</number> <number>19</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Svízela_Jakub_1.png</image> <image>img/muzi/Svízela_Jakub_1.png</image>
</member> </member>
<member> <member>
<name>Šipka Jan</name> <name>Šipka Jan</name>
<number>14</number> <number>14</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Šipka_Jan_1.png</image> <image>img/muzi/Šipka_Jan_1.png</image>
</member> </member>
<member> <member>
<name>Polák David</name> <name>Polák David</name>
<number>11</number> <number>11</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Polák_David_1.png</image> <image>img/muzi/Polák_David_1.png</image>
</member> </member>
<member> <member>
<name>Ohnutek Tomáš</name> <name>Ohnutek Tomáš</name>
<number>21</number> <number>21</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Tomas_Ohnutek_1.png</image> <image>img/muzi/Tomas_Ohnutek_1.png</image>
</member> </member>
<member> <member>
<name>Kočiš Lukáš</name> <name>Kočiš Lukáš</name>
<number>6</number> <number>6</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Kočiš_Lukáš_1.png</image> <image>img/muzi/Kočiš_Lukáš_1.png</image>
</member> </member>
<member> <member>
<name>Malý Lukáš</name> <name>Malý Lukáš</name>
<number>21</number> <number>21</number>
<role>Hráč</role> <role>Hráč</role>
<image>img/muzi/Malý_Lukáš_1.png</image> <image>img/muzi/Malý_Lukáš_1.png</image>
</member> </member>
<member> <member>
<name>Řičica Jakub</name> <name>Řičica Jakub</name>
<number></number> <number></number>
<role>Člen VV</role> <role>Člen VV</role>
<image>img/muzi/Řičica_Jakub_1.png</image> <image>img/muzi/Řičica_Jakub_1.png</image>
</member> </member>
<member> <member>
<name>Moravec David</name> <name>Moravec David</name>
<number>44</number> <number>44</number>
<role>Místopředseda</role> <role>Místopředseda</role>
<image>img/muzi/Moravec_David_1.png</image> <image>img/muzi/Moravec_David_1.png</image>
</member> </member>
<member> <member>
<name>Janečková Vladimíra</name> <name>Janečková Vladimíra</name>
<number></number> <number></number>
<role>vedoucí týmu</role> <role>vedoucí týmu</role>
<image>img/muzi/Janečková_Vladimíra_1.png</image> <image>img/muzi/Janečková_Vladimíra_1.png</image>
</member> </member>
<member> <member>
<name>Stodůlka Štěpán</name> <name>Stodůlka Štěpán</name>
<number>96</number> <number>96</number>
<role>Předseda</role> <role>Předseda</role>
<image>img/muzi/Stodůlka_Štěpán_1.png</image> <image>img/muzi/Stodůlka_Štěpán_1.png</image>
</member> </member>
<member> <member>
<name>Stojaspal Marek</name> <name>Stojaspal Marek</name>
<number></number> <number></number>
<role>Trenér mládeže</role> <role>Trenér mládeže</role>
<image>img/muzi/Stojaspal_Marek_1.png</image> <image>img/muzi/Stojaspal_Marek_1.png</image>
</member> </member>
<member> <member>
<name>Puškáč Lubomír</name> <name>Puškáč Lubomír</name>
<number></number> <number></number>
<role>Vedoucí týmu</role> <role>Vedoucí týmu</role>
<image>img/muzi/Puškáč_Lubomír_1.png</image> <image>img/muzi/Puškáč_Lubomír_1.png</image>
</member> </member>
<member> <member>
<name>Náplava Jaroslav</name> <name>Náplava Jaroslav</name>
<number>88</number> <number>88</number>
<role>Člen VV</role> <role>Člen VV</role>
<image>img/muzi/Náplava_Jaroslav_1.png</image> <image>img/muzi/Náplava_Jaroslav_1.png</image>
</member> </member>
</category> </category>
<category name="women"> <category name="women">
<member> <member>
<name>Šrámková Sára</name> <name>Šrámková Sára</name>
<number>12</number> <number>12</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Šrámková_Sára_1.png</image> <image>img/zeny/Šrámková_Sára_1.png</image>
</member> </member>
<member> <member>
<name>Baná Barbora</name> <name>Baná Barbora</name>
<number>21</number> <number>21</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Baná_Barbora_1.png</image> <image>img/zeny/Baná_Barbora_1.png</image>
</member> </member>
<member> <member>
<name>Majerechová Eliška</name> <name>Majerechová Eliška</name>
<number>69</number> <number>69</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Majerechová_Eliška_1.png</image> <image>img/zeny/Majerechová_Eliška_1.png</image>
</member> </member>
<member> <member>
<name>Maleňáková Adriana</name> <name>Maleňáková Adriana</name>
<number>11</number> <number>11</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Maleňáková_Adriana_1.png</image> <image>img/zeny/Maleňáková_Adriana_1.png</image>
</member> </member>
<member> <member>
<name>Adamíková Andrea</name> <name>Adamíková Andrea</name>
<number>6</number> <number>6</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Adamíková_Andrea_1.png</image> <image>img/zeny/Adamíková_Andrea_1.png</image>
</member> </member>
<member> <member>
<name>Dufková Martina</name> <name>Dufková Martina</name>
<number>17</number> <number>17</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Dufková_Martina_1.png</image> <image>img/zeny/Dufková_Martina_1.png</image>
</member> </member>
<member> <member>
<name>Gorčíková Anna</name> <name>Gorčíková Anna</name>
<number>16</number> <number>16</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Gorčíková_Anna_1.png</image> <image>img/zeny/Gorčíková_Anna_1.png</image>
</member> </member>
<member> <member>
<name>Mrázková Denisa</name> <name>Mrázková Denisa</name>
<number>4</number> <number>4</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Mrázková_Denisa_1.png</image> <image>img/zeny/Mrázková_Denisa_1.png</image>
</member> </member>
<member> <member>
<name>Prokešová Terezie</name> <name>Prokešová Terezie</name>
<number>1</number> <number>1</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Prokešová_Terezie_1.png</image> <image>img/zeny/Prokešová_Terezie_1.png</image>
</member> </member>
<member> <member>
<name>Sekaninová Markéta</name> <name>Sekaninová Markéta</name>
<number>7</number> <number>7</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Sekaninová_Markéta_1.png</image> <image>img/zeny/Sekaninová_Markéta_1.png</image>
</member> </member>
<member> <member>
<name>Chudárková Kristýna</name> <name>Chudárková Kristýna</name>
<number></number> <number></number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Chudárková_Kristýna_1.png</image> <image>img/zeny/Chudárková_Kristýna_1.png</image>
</member> </member>
<member> <member>
<name>Štichová Tereza</name> <name>Štichová Tereza</name>
<number>10</number> <number>10</number>
<role>hráč</role> <role>hráč</role>
<image>img/zeny/Štichová_Tereza_1.png</image> <image>img/zeny/Štichová_Tereza_1.png</image>
</member> </member>
<member> <member>
<name>Prokeš Martin</name> <name>Prokeš Martin</name>
<number></number> <number></number>
<role>Trenér</role> <role>Trenér</role>
<image>img/muzi/Prokeš_Martin_1.png</image> <image>img/muzi/Prokeš_Martin_1.png</image>
</member> </member>
</category> </category>
</team> </team>
+13 -13
View File
@@ -1,13 +1,13 @@
services: services:
app: app:
build: ./backend build: ./backend
container_name: bizoni-app container_name: bizoni-app
environment: environment:
- STATIC_PATH=/app/site - STATIC_PATH=/app/site
- PORT=8080 - PORT=8080
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./:/app/site - ./:/app/site
restart: unless-stopped restart: unless-stopped
+471 -471
View File
@@ -1,471 +1,471 @@
(function(){ (function(){
const DATA_URL_JSON = '/data/club.json'; const DATA_URL_JSON = '/data/club.json';
const TZ = 'Europe/Prague'; const TZ = 'Europe/Prague';
let state = { let state = {
data: null, data: null,
compIndex: 0, compIndex: 0,
matchIndex: 0, matchIndex: 0,
intervalId: null, intervalId: null,
upcomingTimerId: null, upcomingTimerId: null,
}; };
function parseCZDate(s){ function parseCZDate(s){
try{ try{
// format: 02.01.2006 15:04 // format: 02.01.2006 15:04
const [d, t] = s.split(' '); const [d, t] = s.split(' ');
const [day, month, year] = d.split('.').map(Number); const [day, month, year] = d.split('.').map(Number);
const [hour, minute] = t.split(':').map(Number); const [hour, minute] = t.split(':').map(Number);
// Interpret as local time (Europe/Prague). Using local constructor avoids UTC offset skew. // Interpret as local time (Europe/Prague). Using local constructor avoids UTC offset skew.
const dt = new Date(year, month-1, day, hour, minute); const dt = new Date(year, month-1, day, hour, minute);
return dt; return dt;
}catch(e){ return null; } }catch(e){ return null; }
} }
// using original logo URLs; no cleaning/proxy // using original logo URLs; no cleaning/proxy
function ensureFacrStyles(){ function ensureFacrStyles(){
if(document.getElementById('facr-styles')) return; if(document.getElementById('facr-styles')) return;
const css = ` const css = `
/* logo background unchanged */ /* logo background unchanged */
.facr-nav{ background:#ffffff22; border:1px solid #ffffff55; color:#fff; padding:6px 10px; border-radius:6px; cursor:pointer; backdrop-filter: blur(2px); } .facr-nav{ background:#ffffff22; border:1px solid #ffffff55; color:#fff; padding:6px 10px; border-radius:6px; cursor:pointer; backdrop-filter: blur(2px); }
.facr-nav:hover{ background:#ffffff40; } .facr-nav:hover{ background:#ffffff40; }
.facr-nav:disabled{ opacity:.5; cursor:default; } .facr-nav:disabled{ opacity:.5; cursor:default; }
.facr-tab{ padding:6px 10px; margin:4px; border-radius:16px; border:1px solid #c42221; color:#c42221; background:#ffffff; font-weight:600; } .facr-tab{ padding:6px 10px; margin:4px; border-radius:16px; border:1px solid #c42221; color:#c42221; background:#ffffff; font-weight:600; }
.facr-tab.active{ background:#c42221; color:#ffffff; } .facr-tab.active{ background:#c42221; color:#ffffff; }
.facr-tab:hover{ background:#c42221cc; color:#ffffff; } .facr-tab:hover{ background:#c42221cc; color:#ffffff; }
.facr-inline-status{ margin-left:8px; font-weight:700; font-size:14px; white-space:nowrap; color:inherit; display:inline-block; vertical-align:middle; } .facr-inline-status{ margin-left:8px; font-weight:700; font-size:14px; white-space:nowrap; color:inherit; display:inline-block; vertical-align:middle; }
/* Default (desktop): show middle only */ /* Default (desktop): show middle only */
#facr-countdown{ display:none !important; } #facr-countdown{ display:none !important; }
.facr-inline-status{ display:none !important; } .facr-inline-status{ display:none !important; }
.facr-mob-center-score{ display:none; font-weight:700; font-size:24px; line-height:1; } .facr-mob-center-score{ display:none; font-weight:700; font-size:24px; line-height:1; }
/* Mobile: keep only the bottom countdown by default */ /* Mobile: keep only the bottom countdown by default */
@media (max-width: 767px){ @media (max-width: 767px){
#facr-mid{ display:none !important; } #facr-mid{ display:none !important; }
#facr-countdown{ display:block !important; } #facr-countdown{ display:block !important; }
.facr-inline-status{ display:none !important; } .facr-inline-status{ display:none !important; }
.facr-mob-center-score{ display:inline-block !important; } .facr-mob-center-score{ display:inline-block !important; }
/* If finished, hide the bottom countdown */ /* If finished, hide the bottom countdown */
.facr-finished #facr-countdown{ display:none !important; } .facr-finished #facr-countdown{ display:none !important; }
} }
@media (max-width: 480px){ @media (max-width: 480px){
#facr-mid{ font-size:28px !important; min-width:100px; } #facr-mid{ font-size:28px !important; min-width:100px; }
.facr-tab{ padding:4px 8px; font-size:14px; } .facr-tab{ padding:4px 8px; font-size:14px; }
.facr-inline-status{ font-size:12px; margin-left:6px; } .facr-inline-status{ font-size:12px; margin-left:6px; }
.facr-nav{ padding:4px 8px; } .facr-nav{ padding:4px 8px; }
} }
`; `;
const style = document.createElement('style'); const style = document.createElement('style');
style.id = 'facr-styles'; style.id = 'facr-styles';
style.type = 'text/css'; style.type = 'text/css';
style.appendChild(document.createTextNode(css)); style.appendChild(document.createTextNode(css));
document.head.appendChild(style); document.head.appendChild(style);
} }
function fmtCountdown(ms){ function fmtCountdown(ms){
if(ms <= 0) return '0m'; if(ms <= 0) return '0m';
const totalMin = Math.floor(ms/60000); const totalMin = Math.floor(ms/60000);
const d = Math.floor(totalMin/(60*24)); const d = Math.floor(totalMin/(60*24));
const h = Math.floor((totalMin - d*60*24)/60); const h = Math.floor((totalMin - d*60*24)/60);
const m = totalMin % 60; const m = totalMin % 60;
const parts = []; const parts = [];
if(d) parts.push(`${d}d`); if(d) parts.push(`${d}d`);
if(h || d) parts.push(`${h}h`); if(h || d) parts.push(`${h}h`);
parts.push(`${m}m`); parts.push(`${m}m`);
return parts.join(' '); return parts.join(' ');
} }
function fmtCountdownLong(ms){ function fmtCountdownLong(ms){
if(ms < 0) ms = 0; if(ms < 0) ms = 0;
const totalSec = Math.floor(ms/1000); const totalSec = Math.floor(ms/1000);
const d = Math.floor(totalSec / (24*3600)); const d = Math.floor(totalSec / (24*3600));
const h = Math.floor((totalSec % (24*3600)) / 3600); const h = Math.floor((totalSec % (24*3600)) / 3600);
const m = Math.floor((totalSec % 3600) / 60); const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60; const s = totalSec % 60;
const dd = d > 0 ? `${d}d ` : ''; const dd = d > 0 ? `${d}d ` : '';
const hh = String(h).padStart(2,'0'); const hh = String(h).padStart(2,'0');
const mm = String(m).padStart(2,'0'); const mm = String(m).padStart(2,'0');
const ss = String(s).padStart(2,'0'); const ss = String(s).padStart(2,'0');
return `${dd}${hh}:${mm}:${ss}`.trim(); return `${dd}${hh}:${mm}:${ss}`.trim();
} }
function todayCZ(){ function todayCZ(){
const now = new Date(); const now = new Date();
const dd = String(now.getDate()).padStart(2,'0'); const dd = String(now.getDate()).padStart(2,'0');
const mm = String(now.getMonth()+1).padStart(2,'0'); const mm = String(now.getMonth()+1).padStart(2,'0');
const yyyy = now.getFullYear(); const yyyy = now.getFullYear();
return `${dd}.${mm}.${yyyy}`; return `${dd}.${mm}.${yyyy}`;
} }
async function fetchData(){ async function fetchData(){
const res = await fetch(DATA_URL_JSON, { cache: 'no-cache' }); const res = await fetch(DATA_URL_JSON, { cache: 'no-cache' });
if(!res.ok) throw new Error('Failed to fetch data'); if(!res.ok) throw new Error('Failed to fetch data');
const data = await res.json(); const data = await res.json();
state.data = data; state.data = data;
return data; return data;
} }
function escapeHTML(s){ function escapeHTML(s){
if(s == null) return ''; if(s == null) return '';
return String(s) return String(s)
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
.replace(/"/g, '&quot;') .replace(/"/g, '&quot;')
.replace(/'/g, '&#39;'); .replace(/'/g, '&#39;');
} }
function truncate(s, max){ function truncate(s, max){
const str = s == null ? '' : String(s); const str = s == null ? '' : String(s);
if(max <= 0) return ''; if(max <= 0) return '';
return str.length > max ? str.slice(0, max - 1) + '…' : str; return str.length > max ? str.slice(0, max - 1) + '…' : str;
} }
function isWithinMatchWindow(){ function isWithinMatchWindow(){
if(!state.data) return false; if(!state.data) return false;
const now = new Date(); const now = new Date();
for(const comp of state.data.club_detail.competitions || []){ for(const comp of state.data.club_detail.competitions || []){
for(const m of comp.matches || []){ for(const m of comp.matches || []){
const dt = parseCZDate(m.date_time); const dt = parseCZDate(m.date_time);
if(!dt) continue; if(!dt) continue;
const diffMs = Math.abs(now - dt); const diffMs = Math.abs(now - dt);
if(diffMs <= 2*60*60*1000) return true; if(diffMs <= 2*60*60*1000) return true;
} }
} }
return false; return false;
} }
function upcomingMatchesAll(){ function upcomingMatchesAll(){
const list = []; const list = [];
if(!state.data) return list; if(!state.data) return list;
const now = new Date(); const now = new Date();
const windowStart = new Date(now.getTime() - 3*24*60*60*1000); const windowStart = new Date(now.getTime() - 3*24*60*60*1000);
for(const comp of state.data.club_detail.competitions || []){ for(const comp of state.data.club_detail.competitions || []){
const candidates = (comp.matches || []) const candidates = (comp.matches || [])
.map(m=>({ comp, match: m, dt: parseCZDate(m.date_time) })) .map(m=>({ comp, match: m, dt: parseCZDate(m.date_time) }))
.filter(x=> x.dt && x.dt >= windowStart) .filter(x=> x.dt && x.dt >= windowStart)
.sort((a,b)=> a.dt - b.dt); .sort((a,b)=> a.dt - b.dt);
if(candidates.length === 0) continue; if(candidates.length === 0) continue;
// Prefer the latest recently finished match within the 3-day window; // Prefer the latest recently finished match within the 3-day window;
// if none, then choose the next upcoming; if none, fall back to the latest overall in window // if none, then choose the next upcoming; if none, fall back to the latest overall in window
let pick = null; let pick = null;
// find latest finished (dt < now) within window // find latest finished (dt < now) within window
for(let i = candidates.length - 1; i >= 0; i--){ for(let i = candidates.length - 1; i >= 0; i--){
if(candidates[i].dt < now){ pick = candidates[i]; break; } if(candidates[i].dt < now){ pick = candidates[i]; break; }
} }
if(!pick){ if(!pick){
pick = candidates.find(x=> x.dt >= now) || candidates[candidates.length - 1]; pick = candidates.find(x=> x.dt >= now) || candidates[candidates.length - 1];
} }
list.push(pick); list.push(pick);
} }
// sort resulting per-competition picks by time ascending for navigation // sort resulting per-competition picks by time ascending for navigation
list.sort((a,b)=> a.dt - b.dt); list.sort((a,b)=> a.dt - b.dt);
return list; return list;
} }
function renderUpcoming(){ function renderUpcoming(){
const root = document.getElementById('facr-upcoming'); const root = document.getElementById('facr-upcoming');
if(!root) return; if(!root) return;
ensureFacrStyles(); ensureFacrStyles();
// clear any previous per-second timer // clear any previous per-second timer
if(state.upcomingTimerId){ if(state.upcomingTimerId){
clearInterval(state.upcomingTimerId); clearInterval(state.upcomingTimerId);
state.upcomingTimerId = null; state.upcomingTimerId = null;
} }
const items = upcomingMatchesAll(); const items = upcomingMatchesAll();
if(items.length === 0){ if(items.length === 0){
root.innerHTML = '<div class="lte-football-upcoming"><span class="lte-header lte-header-upcoming">Žádné nadcházející zápasy</span></div>'; root.innerHTML = '<div class="lte-football-upcoming"><span class="lte-header lte-header-upcoming">Žádné nadcházející zápasy</span></div>';
return; return;
} }
// Selection policy: // Selection policy:
// 1) If there is any finished match within the last 3 days across competitions, // 1) If there is any finished match within the last 3 days across competitions,
// show the latest such finished match (keep result visible for 3 days) // show the latest such finished match (keep result visible for 3 days)
// 2) Otherwise, show the first future match // 2) Otherwise, show the first future match
// 3) If none, show the latest overall (shouldn't happen as items filtered by 3d window per-comp) // 3) If none, show the latest overall (shouldn't happen as items filtered by 3d window per-comp)
const now = new Date(); const now = new Date();
const threeDms = 3*24*60*60*1000; const threeDms = 3*24*60*60*1000;
let latestRecentIdx = -1; let latestRecentIdx = -1;
let latestRecentTime = -Infinity; let latestRecentTime = -Infinity;
items.forEach((it, i) => { items.forEach((it, i) => {
const dtms = it.dt.getTime(); const dtms = it.dt.getTime();
if(dtms <= now.getTime() && now.getTime() - dtms <= threeDms){ if(dtms <= now.getTime() && now.getTime() - dtms <= threeDms){
if(dtms > latestRecentTime){ latestRecentTime = dtms; latestRecentIdx = i; } if(dtms > latestRecentTime){ latestRecentTime = dtms; latestRecentIdx = i; }
} }
}); });
let preferredIdx = latestRecentIdx; let preferredIdx = latestRecentIdx;
if(preferredIdx === -1){ if(preferredIdx === -1){
preferredIdx = items.findIndex(it => it.dt >= now); preferredIdx = items.findIndex(it => it.dt >= now);
if(preferredIdx === -1) preferredIdx = items.length - 1; if(preferredIdx === -1) preferredIdx = items.length - 1;
} }
const idx = Math.min(state.matchIndex || preferredIdx, items.length-1); const idx = Math.min(state.matchIndex || preferredIdx, items.length-1);
const { comp, match:m } = items[idx]; const { comp, match:m } = items[idx];
const compName = truncate(escapeHTML(comp.name || comp.code || 'Soutěž'), 60); const compName = truncate(escapeHTML(comp.name || comp.code || 'Soutěž'), 60);
const homeLogo = m.home_logo_url || 'img/logo.png'; const homeLogo = m.home_logo_url || 'img/logo.png';
const awayLogo = m.away_logo_url || 'img/logo.png'; const awayLogo = m.away_logo_url || 'img/logo.png';
const facrLink = m.facr_link || comp.matches_link || state.data.club_detail.url || '#'; const facrLink = m.facr_link || comp.matches_link || state.data.club_detail.url || '#';
const UP_MAX = 24; const UP_MAX = 24;
const homeName = truncate(escapeHTML(m.home), UP_MAX); const homeName = truncate(escapeHTML(m.home), UP_MAX);
const awayName = truncate(escapeHTML(m.away), UP_MAX); const awayName = truncate(escapeHTML(m.away), UP_MAX);
const dateVenue = truncate(escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:'')), 40); const dateVenue = truncate(escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:'')), 40);
// Determine if match is today (CZ date) and precompute mid display text // Determine if match is today (CZ date) and precompute mid display text
const matchDayCZ = (m.date_time || '').split(' ')[0]; const matchDayCZ = (m.date_time || '').split(' ')[0];
const isToday = matchDayCZ === todayCZ(); const isToday = matchDayCZ === todayCZ();
const startDt = m.date_time ? parseCZDate(m.date_time) : null; const startDt = m.date_time ? parseCZDate(m.date_time) : null;
const diffMsPre = startDt ? (startDt.getTime() - new Date().getTime()) : 0; const diffMsPre = startDt ? (startDt.getTime() - new Date().getTime()) : 0;
const midText = (diffMsPre > 0) ? `Za ${fmtCountdownLong(diffMsPre)}` : (m.score || '-'); const midText = (diffMsPre > 0) ? `Za ${fmtCountdownLong(diffMsPre)}` : (m.score || '-');
// Determine status flags and parse score parts if any // Determine status flags and parse score parts if any
const now2_forTpl = new Date(); const now2_forTpl = new Date();
const startMs_forTpl = m.date_time ? parseCZDate(m.date_time).getTime() : 0; const startMs_forTpl = m.date_time ? parseCZDate(m.date_time).getTime() : 0;
const diff_forTpl = startMs_forTpl - now2_forTpl.getTime(); const diff_forTpl = startMs_forTpl - now2_forTpl.getTime();
const twoH_forTpl = 2*60*60*1000; const twoH_forTpl = 2*60*60*1000;
const threeD_forTpl = 3*24*60*60*1000; const threeD_forTpl = 3*24*60*60*1000;
const isFuture = diff_forTpl > 0; const isFuture = diff_forTpl > 0;
const isLive = Math.abs(diff_forTpl) <= twoH_forTpl; const isLive = Math.abs(diff_forTpl) <= twoH_forTpl;
const isRecentFinished = (!isFuture && !isLive && -diff_forTpl < threeD_forTpl); const isRecentFinished = (!isFuture && !isLive && -diff_forTpl < threeD_forTpl);
const scoreStr = m.score || ''; const scoreStr = m.score || '';
const s1 = scoreStr && scoreStr.includes(':') ? escapeHTML(scoreStr.split(':')[0]) : ''; const s1 = scoreStr && scoreStr.includes(':') ? escapeHTML(scoreStr.split(':')[0]) : '';
const s2 = scoreStr && scoreStr.includes(':') ? escapeHTML(scoreStr.split(':')[1]) : ''; const s2 = scoreStr && scoreStr.includes(':') ? escapeHTML(scoreStr.split(':')[1]) : '';
// Date/time formatting for display lines // Date/time formatting for display lines
const dtForDisp = startDt || (m.date_time ? parseCZDate(m.date_time) : null); const dtForDisp = startDt || (m.date_time ? parseCZDate(m.date_time) : null);
const dd = dtForDisp ? String(dtForDisp.getDate()).padStart(1,'') : ''; const dd = dtForDisp ? String(dtForDisp.getDate()).padStart(1,'') : '';
const mm = dtForDisp ? String(dtForDisp.getMonth()+1).padStart(1,'') : ''; const mm = dtForDisp ? String(dtForDisp.getMonth()+1).padStart(1,'') : '';
const yyyy = dtForDisp ? dtForDisp.getFullYear() : ''; const yyyy = dtForDisp ? dtForDisp.getFullYear() : '';
const HH = dtForDisp ? String(dtForDisp.getHours()).padStart(2,'0') : ''; const HH = dtForDisp ? String(dtForDisp.getHours()).padStart(2,'0') : '';
const MM = dtForDisp ? String(dtForDisp.getMinutes()).padStart(2,'0') : ''; const MM = dtForDisp ? String(dtForDisp.getMinutes()).padStart(2,'0') : '';
const dateOnly = dtForDisp ? `${dd}. ${mm}. ${yyyy}` : ''; const dateOnly = dtForDisp ? `${dd}. ${mm}. ${yyyy}` : '';
const timeToken = (m.date_time || '').split(' ')[1] || ''; const timeToken = (m.date_time || '').split(' ')[1] || '';
const timeOnly = dtForDisp ? `${HH}:${MM}` : ''; const timeOnly = dtForDisp ? `${HH}:${MM}` : '';
const timeDisplay = dtForDisp ? ((timeToken === '' || timeToken === '00:00') ? 'Bude upřesněno' : timeOnly) : ''; const timeDisplay = dtForDisp ? ((timeToken === '' || timeToken === '00:00') ? 'Bude upřesněno' : timeOnly) : '';
const venue = m.venue || ''; const venue = m.venue || '';
const wrapperExtraClass = (isRecentFinished && s1 && s2) ? ' facr-finished' : ''; const wrapperExtraClass = (isRecentFinished && s1 && s2) ? ' facr-finished' : '';
const headerLabel = isLive ? 'Aktuální zápas' : (isFuture ? 'Nadcházející zápas' : (isRecentFinished ? 'Poslední zápas' : `Zápasy (${idx+1}/${items.length})`)); const headerLabel = isLive ? 'Aktuální zápas' : (isFuture ? 'Nadcházející zápas' : (isRecentFinished ? 'Poslední zápas' : `Zápasy (${idx+1}/${items.length})`));
root.innerHTML = ` root.innerHTML = `
<div class="lte-football-upcoming${wrapperExtraClass}"> <div class="lte-football-upcoming${wrapperExtraClass}">
<div class="facr-comp-title lte-football-date" style="text-align:center; margin-bottom:6px;">${compName}</div> <div class="facr-comp-title lte-football-date" style="text-align:center; margin-bottom:6px;">${compName}</div>
<div class="facr-upcoming-header"> <div class="facr-upcoming-header">
<button id="facr-prev" class="facr-nav">◀</button> <button id="facr-prev" class="facr-nav">◀</button>
<span class="lte-header lte-header-upcoming">${headerLabel}</span> <span class="lte-header lte-header-upcoming">${headerLabel}</span>
<button id="facr-next" class="facr-nav">▶</button> <button id="facr-next" class="facr-nav">▶</button>
</div> </div>
<div class="lte-teams"> <div class="lte-teams">
<span class="lte-team-name lte-team-1 lte-header" title="${escapeHTML(m.home)}"> <span class="lte-team-name lte-team-1 lte-header" title="${escapeHTML(m.home)}">
<span class="lte-team-logo"><img decoding="async" src="${homeLogo}" alt="${escapeHTML(m.home)}"></span>${homeName} <span class="lte-team-logo"><img decoding="async" src="${homeLogo}" alt="${escapeHTML(m.home)}"></span>${homeName}
<span id="facr-inline-status" class="facr-inline-status" aria-live="polite"></span> <span id="facr-inline-status" class="facr-inline-status" aria-live="polite"></span>
${isRecentFinished && s1 ? `<span class="lte-team-count-mob">${s1}</span>` : ''} ${isRecentFinished && s1 ? `<span class="lte-team-count-mob">${s1}</span>` : ''}
</span> </span>
<span class="lte-team-count"> <span class="lte-team-count">
<span id="facr-mid" style="font-size:32px; line-height:1; font-weight:700; display:inline-block; min-width:120px; text-align:center;">${midText}</span> <span id="facr-mid" style="font-size:32px; line-height:1; font-weight:700; display:inline-block; min-width:120px; text-align:center;">${midText}</span>
${isRecentFinished && s1 && s2 ? `<span class="facr-mob-center-score">${s1}<span>:</span>${s2}</span>` : ''} ${isRecentFinished && s1 && s2 ? `<span class="facr-mob-center-score">${s1}<span>:</span>${s2}</span>` : ''}
</span> </span>
<span class="lte-team-name lte-team-2 lte-header" title="${escapeHTML(m.away)}"> <span class="lte-team-name lte-team-2 lte-header" title="${escapeHTML(m.away)}">
${isRecentFinished && s2 ? `<span class=\"lte-team-count-mob\">${s2}</span>` : ''}${awayName}<span class="lte-team-logo"><img decoding="async" src="${awayLogo}" alt="${escapeHTML(m.away)}"></span> ${isRecentFinished && s2 ? `<span class=\"lte-team-count-mob\">${s2}</span>` : ''}${awayName}<span class="lte-team-logo"><img decoding="async" src="${awayLogo}" alt="${escapeHTML(m.away)}"></span>
</span> </span>
</div> </div>
<span class="lte-football-date" style="text-align:center;" title="${escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:''))}">${escapeHTML(dateOnly + (venue?`, ${venue}`:''))}</span> <span class="lte-football-date" style="text-align:center;" title="${escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:''))}">${escapeHTML(dateOnly + (venue?`, ${venue}`:''))}</span>
${timeDisplay ? `<span class="lte-football-time" style="display:block; text-align:center;">${escapeHTML(timeDisplay)}</span>` : ''} ${timeDisplay ? `<span class="lte-football-time" style="display:block; text-align:center;">${escapeHTML(timeDisplay)}</span>` : ''}
<span id="facr-countdown" class="lte-football-date" style="display:block; text-align:center;"></span> <span id="facr-countdown" class="lte-football-date" style="display:block; text-align:center;"></span>
<br> <br>
<a class="lte-football-date" target="_blank" href="${facrLink}" style="text-align:center; background-color:#c42221; color:#ffffff; opacity:1;">Detail na FACR</a> <a class="lte-football-date" target="_blank" href="${facrLink}" style="text-align:center; background-color:#c42221; color:#ffffff; opacity:1;">Detail na FACR</a>
<span style="display:block; margin-top:6px;"></span> <span style="display:block; margin-top:6px;"></span>
<a class="lte-football-date" href="#tabulka" style="text-align:center; background-color:#ffffff43; color:#ffffff; opacity:1; width:49%; display:inline-block;">Tabulka bodů</a> <a class="lte-football-date" href="#tabulka" style="text-align:center; background-color:#ffffff43; color:#ffffff; opacity:1; width:49%; display:inline-block;">Tabulka bodů</a>
<a class="lte-football-date" href="/zapasy/vsechny.html" style="text-align:center; background-color:#ffffff43; color:#ffffff; opacity:1; width:49%; display:inline-block;">Všechny zápasy</a> <a class="lte-football-date" href="/zapasy/vsechny.html" style="text-align:center; background-color:#ffffff43; color:#ffffff; opacity:1; width:49%; display:inline-block;">Všechny zápasy</a>
</div>`; </div>`;
const prev = document.getElementById('facr-prev'); const prev = document.getElementById('facr-prev');
const next = document.getElementById('facr-next'); const next = document.getElementById('facr-next');
if(prev) prev.onclick = ()=>{ state.matchIndex = (idx - 1 + items.length) % items.length; renderUpcoming(); }; if(prev) prev.onclick = ()=>{ state.matchIndex = (idx - 1 + items.length) % items.length; renderUpcoming(); };
if(next) next.onclick = ()=>{ state.matchIndex = (idx + 1) % items.length; renderUpcoming(); }; if(next) next.onclick = ()=>{ state.matchIndex = (idx + 1) % items.length; renderUpcoming(); };
// setup countdown / status text // setup countdown / status text
const cd = document.getElementById('facr-countdown'); const cd = document.getElementById('facr-countdown');
const inlineStatus = document.getElementById('facr-inline-status'); const inlineStatus = document.getElementById('facr-inline-status');
if(cd || inlineStatus){ if(cd || inlineStatus){
const now2 = new Date(); const now2 = new Date();
const startMs = m.date_time ? parseCZDate(m.date_time).getTime() : 0; const startMs = m.date_time ? parseCZDate(m.date_time).getTime() : 0;
const diff = startMs - now2.getTime(); const diff = startMs - now2.getTime();
const twoH = 2*60*60*1000; const twoH = 2*60*60*1000;
const threeD = 3*24*60*60*1000; const threeD = 3*24*60*60*1000;
let text = ''; let text = '';
if(diff > 0){ if(diff > 0){
text = `Začátek za ${fmtCountdown(diff)}`; text = `Začátek za ${fmtCountdown(diff)}`;
}else if(Math.abs(diff) <= twoH){ }else if(Math.abs(diff) <= twoH){
text = 'Právě probíhá'; text = 'Právě probíhá';
}else if(-diff < threeD){ }else if(-diff < threeD){
text = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); text = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno');
}else{ }else{
text = ''; text = '';
} }
if(cd) cd.textContent = text; if(cd) cd.textContent = text;
if(inlineStatus) inlineStatus.textContent = text; if(inlineStatus) inlineStatus.textContent = text;
} }
// Live countdown in the middle area when future and not today // Live countdown in the middle area when future and not today
const midEl = document.getElementById('facr-mid'); const midEl = document.getElementById('facr-mid');
if(midEl){ if(midEl){
const startTime = m.date_time ? parseCZDate(m.date_time).getTime() : 0; const startTime = m.date_time ? parseCZDate(m.date_time).getTime() : 0;
function tick(){ function tick(){
const now = Date.now(); const now = Date.now();
const diff = startTime - now; const diff = startTime - now;
if(diff > 0){ if(diff > 0){
const longTxt = `Za ${fmtCountdownLong(diff)}`; const longTxt = `Za ${fmtCountdownLong(diff)}`;
midEl.textContent = longTxt; midEl.textContent = longTxt;
const inlineEl = document.getElementById('facr-inline-status'); const inlineEl = document.getElementById('facr-inline-status');
if(inlineEl) inlineEl.textContent = `Začátek za ${fmtCountdown(diff)}`; if(inlineEl) inlineEl.textContent = `Začátek za ${fmtCountdown(diff)}`;
const cdEl = document.getElementById('facr-countdown'); const cdEl = document.getElementById('facr-countdown');
if(cdEl) cdEl.textContent = `Začátek za ${fmtCountdown(diff)}`; if(cdEl) cdEl.textContent = `Začátek za ${fmtCountdown(diff)}`;
}else{ }else{
// switch to score at/after kickoff // switch to score at/after kickoff
const scoreTxt = m.score || '-'; const scoreTxt = m.score || '-';
midEl.textContent = scoreTxt; midEl.textContent = scoreTxt;
const inlineEl = document.getElementById('facr-inline-status'); const inlineEl = document.getElementById('facr-inline-status');
if(inlineEl) inlineEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); if(inlineEl) inlineEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno');
const cdEl = document.getElementById('facr-countdown'); const cdEl = document.getElementById('facr-countdown');
if(cdEl) cdEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); if(cdEl) cdEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno');
if(state.upcomingTimerId){ clearInterval(state.upcomingTimerId); state.upcomingTimerId = null; } if(state.upcomingTimerId){ clearInterval(state.upcomingTimerId); state.upcomingTimerId = null; }
} }
} }
// Run live countdown for any future match (including today) // Run live countdown for any future match (including today)
if(startTime > Date.now()){ if(startTime > Date.now()){
tick(); tick();
state.upcomingTimerId = setInterval(tick, 1000); state.upcomingTimerId = setInterval(tick, 1000);
} else { } else {
// Ensure inline status reflects finished state on initial render // Ensure inline status reflects finished state on initial render
const inlineEl = document.getElementById('facr-inline-status'); const inlineEl = document.getElementById('facr-inline-status');
if(inlineEl) inlineEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno'); if(inlineEl) inlineEl.textContent = (m.score ? `Výsledek: ${m.score}` : 'Ukončeno');
} }
} }
} }
function renderCompetitionTabs(){ function renderCompetitionTabs(){
const tabs = document.getElementById('facr-comp-tabs'); const tabs = document.getElementById('facr-comp-tabs');
if(!tabs || !state.data) return; if(!tabs || !state.data) return;
ensureFacrStyles(); ensureFacrStyles();
const comps = state.data.club_table.competitions || []; const comps = state.data.club_table.competitions || [];
tabs.innerHTML = comps.map((c,i)=> tabs.innerHTML = comps.map((c,i)=>
`<button class="facr-tab ${i===state.compIndex?'active':''}" data-idx="${i}">${c.name || c.code || 'Soutěž'}</button>` `<button class="facr-tab ${i===state.compIndex?'active':''}" data-idx="${i}">${c.name || c.code || 'Soutěž'}</button>`
).join(''); ).join('');
tabs.querySelectorAll('button').forEach(btn=>{ tabs.querySelectorAll('button').forEach(btn=>{
btn.addEventListener('click', ()=>{ btn.addEventListener('click', ()=>{
state.compIndex = Number(btn.dataset.idx)||0; state.compIndex = Number(btn.dataset.idx)||0;
renderCompetitionTabs(); renderCompetitionTabs();
renderTable(); renderTable();
}); });
}); });
} }
function renderTable(){ function renderTable(){
const tbody = document.getElementById('facr-table-body'); const tbody = document.getElementById('facr-table-body');
const badge = document.getElementById('facr-comp-badge'); const badge = document.getElementById('facr-comp-badge');
if(!tbody || !state.data) return; if(!tbody || !state.data) return;
const comps = state.data.club_table.competitions || []; const comps = state.data.club_table.competitions || [];
if(comps.length === 0){ tbody.innerHTML = ''; return; } if(comps.length === 0){ tbody.innerHTML = ''; return; }
const comp = comps[Math.min(state.compIndex, comps.length-1)]; const comp = comps[Math.min(state.compIndex, comps.length-1)];
if(badge){ badge.textContent = comp.name || comp.code || 'Soutěž'; } if(badge){ badge.textContent = comp.name || comp.code || 'Soutěž'; }
const rows = comp.table && comp.table.overall ? comp.table.overall : []; const rows = comp.table && comp.table.overall ? comp.table.overall : [];
tbody.innerHTML = rows.map(r=>` tbody.innerHTML = rows.map(r=>`
<tr> <tr>
<td class="lte-row"><span>${r.rank}</span></td> <td class="lte-row"><span>${r.rank}</span></td>
<td class="lte-club-logo"><img decoding="async" src="${r.team_logo_url || 'img/logo.png'}"></td> <td class="lte-club-logo"><img decoding="async" src="${r.team_logo_url || 'img/logo.png'}"></td>
<td class="lte-name">${r.team}</td> <td class="lte-name">${r.team}</td>
<td class="lte-rate">${r.played}</td> <td class="lte-rate">${r.played}</td>
<td class="lte-rate">${r.wins}</td> <td class="lte-rate">${r.wins}</td>
<td class="lte-rate">${r.draws}</td> <td class="lte-rate">${r.draws}</td>
<td class="lte-rate">${r.losses}</td> <td class="lte-rate">${r.losses}</td>
<td class="lte-rate">${r.score}</td> <td class="lte-rate">${r.score}</td>
<td class="lte-summary">${r.points}</td> <td class="lte-summary">${r.points}</td>
</tr> </tr>
`).join(''); `).join('');
} }
function renderAllMatches(){ function renderAllMatches(){
const container = document.getElementById('facr-all-matches'); const container = document.getElementById('facr-all-matches');
if(!container || !state.data) return; if(!container || !state.data) return;
const comps = state.data.club_detail.competitions || []; const comps = state.data.club_detail.competitions || [];
const sections = []; const sections = [];
for(const comp of comps){ for(const comp of comps){
const matches = (comp.matches || []) const matches = (comp.matches || [])
.map(m=>({ m, dt: parseCZDate(m.date_time) })) .map(m=>({ m, dt: parseCZDate(m.date_time) }))
.filter(x=>!!x.dt) .filter(x=>!!x.dt)
.sort((a,b)=> b.dt - a.dt); .sort((a,b)=> b.dt - a.dt);
if(matches.length === 0) continue; if(matches.length === 0) continue;
const compName = truncate(escapeHTML(comp.name || comp.code || 'Soutěž'), 40); const compName = truncate(escapeHTML(comp.name || comp.code || 'Soutěž'), 40);
const itemsHtml = matches.map(({m})=>{ const itemsHtml = matches.map(({m})=>{
const homeLogo = m.home_logo_url || '../img/logo.png'; const homeLogo = m.home_logo_url || '../img/logo.png';
const awayLogo = m.away_logo_url || '../img/logo.png'; const awayLogo = m.away_logo_url || '../img/logo.png';
const facrLink = m.report_url || comp.matches_link || state.data.club_detail.url || '#'; const facrLink = m.report_url || comp.matches_link || state.data.club_detail.url || '#';
const score = m.score || '-'; const score = m.score || '-';
const GRID_MAX = 22; const GRID_MAX = 22;
const home = truncate(escapeHTML(m.home), GRID_MAX); const home = truncate(escapeHTML(m.home), GRID_MAX);
const away = truncate(escapeHTML(m.away), GRID_MAX); const away = truncate(escapeHTML(m.away), GRID_MAX);
const dateVenue = truncate(escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:'')), 36); const dateVenue = truncate(escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:'')), 36);
const s1 = escapeHTML((score.split(':')[0]||'-')); const s1 = escapeHTML((score.split(':')[0]||'-'));
const s2 = escapeHTML((score.split(':')[1]||'-')); const s2 = escapeHTML((score.split(':')[1]||'-'));
return ` return `
<a href="${facrLink}" target="_blank" class="lte-item swiper-slide"> <a href="${facrLink}" target="_blank" class="lte-item swiper-slide">
<div class="lte-teams lte-match-time-public"> <div class="lte-teams lte-match-time-public">
<span class="lte-team-name lte-team-1 lte-header" title="${escapeHTML(m.home)}"> <span class="lte-team-name lte-team-1 lte-header" title="${escapeHTML(m.home)}">
<span class="lte-team-logo"><img src="${homeLogo}" alt="${escapeHTML(m.home)}"></span>${home}</span> <span class="lte-team-logo"><img src="${homeLogo}" alt="${escapeHTML(m.home)}"></span>${home}</span>
<span class="lte-score-mob lte-score-1">${s1}</span> <span class="lte-score-mob lte-score-1">${s1}</span>
<span class="lte-team-count"> <span class="lte-team-count">
<span class="lte-c lte-score-1">${s1}</span> <span class="lte-c lte-score-1">${s1}</span>
<span class="lte-d">:</span> <span class="lte-d">:</span>
<span class="lte-c lte-score-4">${s2}</span> <span class="lte-c lte-score-4">${s2}</span>
</span> </span>
<span class="lte-team-name lte-team-2 lte-header" title="${escapeHTML(m.away)}">${away} <span class="lte-team-name lte-team-2 lte-header" title="${escapeHTML(m.away)}">${away}
<span class="lte-team-logo"><img src="${awayLogo}" alt="${escapeHTML(m.away)}"></span> <span class="lte-team-logo"><img src="${awayLogo}" alt="${escapeHTML(m.away)}"></span>
</span> </span>
<span class="lte-score-mob lte-score-4">${s2}</span> <span class="lte-score-mob lte-score-4">${s2}</span>
</div> </div>
<div class="lte-footer"> <div class="lte-footer">
<span class="lte-football-date" title="${escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:''))}">${dateVenue}</span> <span class="lte-football-date" title="${escapeHTML(m.date_time + (m.venue?`, ${m.venue}`:''))}">${dateVenue}</span>
</div> </div>
</a>`; </a>`;
}).join(''); }).join('');
sections.push(` sections.push(`
<div class="lte-section"> <div class="lte-section">
<h3 class="lte-header" style="margin: 20px 0 10px;">${compName}</h3> <h3 class="lte-header" style="margin: 20px 0 10px;">${compName}</h3>
<div class="lte-football-matches inner-page">${itemsHtml}</div> <div class="lte-football-matches inner-page">${itemsHtml}</div>
</div> </div>
`); `);
} }
container.innerHTML = sections.join(''); container.innerHTML = sections.join('');
} }
function schedule(){ function schedule(){
if(state.intervalId) clearInterval(state.intervalId); if(state.intervalId) clearInterval(state.intervalId);
const intervalMs = isWithinMatchWindow() ? 2*60*1000 : 30*60*1000; const intervalMs = isWithinMatchWindow() ? 2*60*1000 : 30*60*1000;
state.intervalId = setInterval(async ()=>{ state.intervalId = setInterval(async ()=>{
try{ try{
await fetchData(); await fetchData();
renderUpcoming(); renderUpcoming();
renderCompetitionTabs(); renderCompetitionTabs();
renderTable(); renderTable();
renderAllMatches(); renderAllMatches();
schedule(); // reevaluate interval if window changed schedule(); // reevaluate interval if window changed
}catch(e){ console.warn('refresh failed', e); } }catch(e){ console.warn('refresh failed', e); }
}, intervalMs); }, intervalMs);
} }
async function init(){ async function init(){
try{ try{
await fetchData(); await fetchData();
renderUpcoming(); renderUpcoming();
renderCompetitionTabs(); renderCompetitionTabs();
renderTable(); renderTable();
renderAllMatches(); renderAllMatches();
schedule(); schedule();
}catch(e){ }catch(e){
console.error('FACR init failed', e); console.error('FACR init failed', e);
} }
} }
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
})(); })();
+552 -552
View File
File diff suppressed because it is too large Load Diff
+237 -237
View File
@@ -1,238 +1,238 @@
/** /**
* jQuery plugin paroller.js v1.4.7 * jQuery plugin paroller.js v1.4.7
* https://github.com/tgomilar/paroller.js * https://github.com/tgomilar/paroller.js
* preview: https://tgomilar.github.io/paroller/ * preview: https://tgomilar.github.io/paroller/
* author: Tanja Gomilar * author: Tanja Gomilar
**/ **/
(function (factory) { (function (factory) {
'use strict'; 'use strict';
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
define('parollerjs', ['jquery'], factory); define('parollerjs', ['jquery'], factory);
} else if (typeof module === 'object' && typeof module.exports === 'object') { } else if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory(require('jquery')); module.exports = factory(require('jquery'));
} }
else { else {
factory(jQuery); factory(jQuery);
} }
})(function ($) { })(function ($) {
'use strict'; 'use strict';
var working = false; var working = false;
var scrollAction = function() { var scrollAction = function() {
working = false; working = false;
}; };
var setDirection = { var setDirection = {
bgVertical: function (elem, bgOffset) { bgVertical: function (elem, bgOffset) {
return elem.css({'background-position': 'center ' + -bgOffset + 'px'}); return elem.css({'background-position': 'center ' + -bgOffset + 'px'});
}, },
bgHorizontal: function (elem, bgOffset) { bgHorizontal: function (elem, bgOffset) {
return elem.css({'background-position': -bgOffset + 'px' + ' center'}); return elem.css({'background-position': -bgOffset + 'px' + ' center'});
}, },
vertical: function (elem, elemOffset, transition, oldTransform) { vertical: function (elem, elemOffset, transition, oldTransform) {
(oldTransform === 'none' ? oldTransform = '' : true); (oldTransform === 'none' ? oldTransform = '' : true);
return elem.css({ return elem.css({
'-webkit-transform': 'translateY(' + elemOffset + 'px)' + oldTransform, '-webkit-transform': 'translateY(' + elemOffset + 'px)' + oldTransform,
'-moz-transform': 'translateY(' + elemOffset + 'px)' + oldTransform, '-moz-transform': 'translateY(' + elemOffset + 'px)' + oldTransform,
'transform': 'translateY(' + elemOffset + 'px)' + oldTransform, 'transform': 'translateY(' + elemOffset + 'px)' + oldTransform,
'transition': transition, 'transition': transition,
'will-change': 'transform' 'will-change': 'transform'
}); });
}, },
horizontal: function (elem, elemOffset, transition, oldTransform) { horizontal: function (elem, elemOffset, transition, oldTransform) {
(oldTransform === 'none' ? oldTransform = '' : true); (oldTransform === 'none' ? oldTransform = '' : true);
return elem.css({ return elem.css({
'-webkit-transform': 'translateX(' + elemOffset + 'px)' + oldTransform, '-webkit-transform': 'translateX(' + elemOffset + 'px)' + oldTransform,
'-moz-transform': 'translateX(' + elemOffset + 'px)' + oldTransform, '-moz-transform': 'translateX(' + elemOffset + 'px)' + oldTransform,
'transform': 'translateX(' + elemOffset + 'px)' + oldTransform, 'transform': 'translateX(' + elemOffset + 'px)' + oldTransform,
'transition': transition, 'transition': transition,
'will-change': 'transform' 'will-change': 'transform'
}); });
} }
}; };
var setMovement = { var setMovement = {
factor: function (elem, width, options) { factor: function (elem, width, options) {
var dataFactor = elem.data('paroller-factor'); var dataFactor = elem.data('paroller-factor');
var factor = (dataFactor) ? dataFactor : options.factor; var factor = (dataFactor) ? dataFactor : options.factor;
if (width < 576) { if (width < 576) {
var dataFactorXs = elem.data('paroller-factor-xs'); var dataFactorXs = elem.data('paroller-factor-xs');
var factorXs = (dataFactorXs) ? dataFactorXs : options.factorXs; var factorXs = (dataFactorXs) ? dataFactorXs : options.factorXs;
return (factorXs) ? factorXs : factor; return (factorXs) ? factorXs : factor;
} }
else if (width <= 768) { else if (width <= 768) {
var dataFactorSm = elem.data('paroller-factor-sm'); var dataFactorSm = elem.data('paroller-factor-sm');
var factorSm = (dataFactorSm) ? dataFactorSm : options.factorSm; var factorSm = (dataFactorSm) ? dataFactorSm : options.factorSm;
return (factorSm) ? factorSm : factor; return (factorSm) ? factorSm : factor;
} }
else if (width <= 1024) { else if (width <= 1024) {
var dataFactorMd = elem.data('paroller-factor-md'); var dataFactorMd = elem.data('paroller-factor-md');
var factorMd = (dataFactorMd) ? dataFactorMd : options.factorMd; var factorMd = (dataFactorMd) ? dataFactorMd : options.factorMd;
return (factorMd) ? factorMd : factor; return (factorMd) ? factorMd : factor;
} }
else if (width <= 1200) { else if (width <= 1200) {
var dataFactorLg = elem.data('paroller-factor-lg'); var dataFactorLg = elem.data('paroller-factor-lg');
var factorLg = (dataFactorLg) ? dataFactorLg : options.factorLg; var factorLg = (dataFactorLg) ? dataFactorLg : options.factorLg;
return (factorLg) ? factorLg : factor; return (factorLg) ? factorLg : factor;
} else if (width <= 1920) { } else if (width <= 1920) {
var dataFactorXl = elem.data('paroller-factor-xl'); var dataFactorXl = elem.data('paroller-factor-xl');
var factorXl = (dataFactorXl) ? dataFactorXl : options.factorXl; var factorXl = (dataFactorXl) ? dataFactorXl : options.factorXl;
return (factorXl) ? factorXl : factor; return (factorXl) ? factorXl : factor;
} else { } else {
return factor; return factor;
} }
}, },
bgOffset: function (offset, factor) { bgOffset: function (offset, factor) {
return Math.round(offset * factor); return Math.round(offset * factor);
}, },
transform: function (offset, factor, windowHeight, height) { transform: function (offset, factor, windowHeight, height) {
return Math.round((offset - (windowHeight / 2) + height) * factor); return Math.round((offset - (windowHeight / 2) + height) * factor);
} }
}; };
var clearPositions = { var clearPositions = {
background: function (elem) { background: function (elem) {
return elem.css({'background-position': 'unset'}); return elem.css({'background-position': 'unset'});
}, },
foreground: function (elem) { foreground: function (elem) {
return elem.css({ return elem.css({
'transform' : 'unset', 'transform' : 'unset',
'transition' : 'unset' 'transition' : 'unset'
}); });
} }
}; };
$.fn.paroller = function (options) { $.fn.paroller = function (options) {
var windowHeight = $(window).height(); var windowHeight = $(window).height();
var documentHeight = $(document).height(); var documentHeight = $(document).height();
// default options // default options
var options = $.extend({ var options = $.extend({
factor: 0, // - to + factor: 0, // - to +
factorXs: 0, // - to + factorXs: 0, // - to +
factorSm: 0, // - to + factorSm: 0, // - to +
factorMd: 0, // - to + factorMd: 0, // - to +
factorLg: 0, // - to + factorLg: 0, // - to +
factorXl: 0, // - to + factorXl: 0, // - to +
transition: 'translate 0.1s ease', // CSS transition transition: 'translate 0.1s ease', // CSS transition
type: 'background', // foreground type: 'background', // foreground
direction: 'vertical', // horizontal direction: 'vertical', // horizontal
offsetVal : 0, // horizontal offsetVal : 0, // horizontal
}, options); }, options);
return this.each(function () { return this.each(function () {
var $this = $(this); var $this = $(this);
var width = $(window).width(); var width = $(window).width();
var offset = $this.offset().top; var offset = $this.offset().top;
var height = $this.outerHeight(); var height = $this.outerHeight();
var dataType = $this.data('paroller-type'); var dataType = $this.data('paroller-type');
var dataDirection = $this.data('paroller-direction'); var dataDirection = $this.data('paroller-direction');
var dataTransition = $this.data('paroller-transition'); var dataTransition = $this.data('paroller-transition');
var oldTransform = $this.css('transform'); var oldTransform = $this.css('transform');
var offsetVal = parseInt($this.data('offset')); var offsetVal = parseInt($this.data('offset'));
var transition = (dataTransition) ? dataTransition : options.transition; var transition = (dataTransition) ? dataTransition : options.transition;
var type = (dataType) ? dataType : options.type; var type = (dataType) ? dataType : options.type;
var direction = (dataDirection) ? dataDirection : options.direction; var direction = (dataDirection) ? dataDirection : options.direction;
var factor = 0; var factor = 0;
var bgOffset = setMovement.bgOffset(offset, factor); var bgOffset = setMovement.bgOffset(offset, factor);
var transform = setMovement.transform(offset, factor, windowHeight, height); var transform = setMovement.transform(offset, factor, windowHeight, height);
if (type === 'background') { if (type === 'background') {
if (direction === 'vertical') { if (direction === 'vertical') {
setDirection.bgVertical($this, bgOffset); setDirection.bgVertical($this, bgOffset);
} }
else if (direction === 'horizontal') { else if (direction === 'horizontal') {
setDirection.bgHorizontal($this, bgOffset); setDirection.bgHorizontal($this, bgOffset);
} }
} }
else if (type === 'foreground') { else if (type === 'foreground') {
if (direction === 'vertical') { if (direction === 'vertical') {
setDirection.vertical($this, transform, transition, oldTransform); setDirection.vertical($this, transform, transition, oldTransform);
} }
else if (direction === 'horizontal') { else if (direction === 'horizontal') {
setDirection.horizontal($this, transform, transition, oldTransform); setDirection.horizontal($this, transform, transition, oldTransform);
} }
} }
$(window).on('resize', function () { $(window).on('resize', function () {
var scrolling = $(this).scrollTop(); var scrolling = $(this).scrollTop();
width = $(window).width(); width = $(window).width();
offset = $this.offset().top; offset = $this.offset().top;
height = $this.outerHeight(); height = $this.outerHeight();
factor = setMovement.factor($this, width, options); factor = setMovement.factor($this, width, options);
bgOffset = Math.round(offset * factor); bgOffset = Math.round(offset * factor);
transform = Math.round((offset - (windowHeight / 2) + height) * factor); transform = Math.round((offset - (windowHeight / 2) + height) * factor);
/*if ( offsetVal != 0 )*/ /*if ( offsetVal != 0 )*/
offset = offset - 400; offset = offset - 400;
if (! working) { if (! working) {
window.requestAnimationFrame(scrollAction); window.requestAnimationFrame(scrollAction);
working = true; working = true;
} }
if (type === 'background') { if (type === 'background') {
clearPositions.background($this); clearPositions.background($this);
if (direction === 'vertical') { if (direction === 'vertical') {
setDirection.bgVertical($this, bgOffset); setDirection.bgVertical($this, bgOffset);
} }
else if (direction === 'horizontal') { else if (direction === 'horizontal') {
setDirection.bgHorizontal($this, bgOffset); setDirection.bgHorizontal($this, bgOffset);
} }
} }
else if ((type === 'foreground') && (scrolling <= documentHeight)) { else if ((type === 'foreground') && (scrolling <= documentHeight)) {
clearPositions.foreground($this); clearPositions.foreground($this);
if (direction === 'vertical') { if (direction === 'vertical') {
setDirection.vertical($this, transform, transition); setDirection.vertical($this, transform, transition);
} }
else if (direction === 'horizontal') { else if (direction === 'horizontal') {
setDirection.horizontal($this, transform, transition); setDirection.horizontal($this, transform, transition);
} }
} }
}); });
$(window).on('scroll', function () { $(window).on('scroll', function () {
var scrolling = $(this).scrollTop(); var scrolling = $(this).scrollTop();
var scrollTop = $(document).scrollTop(); var scrollTop = $(document).scrollTop();
if (scrollTop === 0) { if (scrollTop === 0) {
factor = 0; factor = 0;
} else { } else {
factor = setMovement.factor($this, width, options); factor = setMovement.factor($this, width, options);
} }
bgOffset = Math.round((offset - scrolling) * factor); bgOffset = Math.round((offset - scrolling) * factor);
transform = Math.round(((offset - (windowHeight / 2) + height) - scrolling) * factor); transform = Math.round(((offset - (windowHeight / 2) + height) - scrolling) * factor);
if (! working) { if (! working) {
window.requestAnimationFrame(scrollAction); window.requestAnimationFrame(scrollAction);
working = true; working = true;
} }
if (type === 'background') { if (type === 'background') {
if (direction === 'vertical') { if (direction === 'vertical') {
setDirection.bgVertical($this, bgOffset); setDirection.bgVertical($this, bgOffset);
} }
else if (direction === 'horizontal') { else if (direction === 'horizontal') {
setDirection.bgHorizontal($this, bgOffset); setDirection.bgHorizontal($this, bgOffset);
} }
} }
else if ((type === 'foreground') && (scrolling <= documentHeight)) { else if ((type === 'foreground') && (scrolling <= documentHeight)) {
if (direction === 'vertical') { if (direction === 'vertical') {
setDirection.vertical($this, transform, transition, oldTransform); setDirection.vertical($this, transform, transition, oldTransform);
} }
else if (direction === 'horizontal') { else if (direction === 'horizontal') {
setDirection.horizontal($this, transform, transition, oldTransform); setDirection.horizontal($this, transform, transition, oldTransform);
} }
} }
}); });
}); });
}; };
}); });
+409 -409
View File
@@ -1,409 +1,409 @@
/* /*
* zoomSlider - v1.0.2 Fork * zoomSlider - v1.0.2 Fork
* CSS3 background zoom slideshow * CSS3 background zoom slideshow
* http://mingthings.com * http://mingthings.com
* *
* Made by Ming Yeung * Made by Ming Yeung
* Under MIT License * Under MIT License
*/ */
;(function ( $, window, document, undefined ) { ;(function ( $, window, document, undefined ) {
var pluginName = "zoomSlider", var pluginName = "zoomSlider",
defaults = { defaults = {
src: null, src: null,
src2: null, src2: null,
speed: 8000, speed: 8000,
initzoom: 1.2, initzoom: 1.2,
switchSpeed: 1000, switchSpeed: 1000,
interval: 4600, interval: 4600,
autoplay: true, autoplay: true,
bullets: true, bullets: true,
overlay: 'plain' // false, plain, dots overlay: 'plain' // false, plain, dots
}; };
// The actual plugin constructor // The actual plugin constructor
function Plugin ( element, options ) { function Plugin ( element, options ) {
this.element = element; this.element = element;
this.$el = $(element); this.$el = $(element);
this._defaults = defaults; this._defaults = defaults;
this._name = pluginName; this._name = pluginName;
var elData = this.$el.data(); var elData = this.$el.data();
var elDataObj = {}; var elDataObj = {};
for (var key in elData) { for (var key in elData) {
if ( elData.hasOwnProperty(key) ) { if ( elData.hasOwnProperty(key) ) {
if ( key.match(/zs[A-Z]/) ) { if ( key.match(/zs[A-Z]/) ) {
var keyName = key.substr(2); var keyName = key.substr(2);
keyName = keyName.charAt(0).toLowerCase() + keyName.slice(1); keyName = keyName.charAt(0).toLowerCase() + keyName.slice(1);
elDataObj[keyName] = elData[key] elDataObj[keyName] = elData[key]
} }
} }
} }
this.settings = $.extend( {}, defaults, elDataObj, options ); this.settings = $.extend( {}, defaults, elDataObj, options );
if ( this.settings.src == null || this.settings.src.length < 1 ) { if ( this.settings.src == null || this.settings.src.length < 1 ) {
console.log('ZoomSlider terminated - invalid input.'); console.log('ZoomSlider terminated - invalid input.');
return; return;
} }
this.init(); this.init();
} }
// Avoid Plugin.prototype conflicts // Avoid Plugin.prototype conflicts
$.extend(Plugin.prototype, { $.extend(Plugin.prototype, {
init: function () { init: function () {
// Place initialization logic here // Place initialization logic here
// You already have access to the DOM element and // You already have access to the DOM element and
// the options via the instance, e.g. this.element // the options via the instance, e.g. this.element
// and this.settings // and this.settings
// you can add more functions like the one below and // you can add more functions like the one below and
// call them like so: this.yourOtherFunction(this.element, this.settings). // call them like so: this.yourOtherFunction(this.element, this.settings).
// make sure src is an Array // make sure src is an Array
if ($.isArray(this.settings.src) == false) { if ($.isArray(this.settings.src) == false) {
this.settings.src = [this.settings.src]; this.settings.src = [this.settings.src];
} }
if ($.isArray(this.settings.src2) == false) { if ($.isArray(this.settings.src2) == false) {
this.settings.src2 = [this.settings.src2]; this.settings.src2 = [this.settings.src2];
} }
// https://github.com/twitter/bootstrap/issues/2870 // https://github.com/twitter/bootstrap/issues/2870
this.transEndEventNames = { this.transEndEventNames = {
'WebkitTransition' : 'webkitTransitionEnd', 'WebkitTransition' : 'webkitTransitionEnd',
'MozTransition' : 'transitionend', 'MozTransition' : 'transitionend',
'OTransition' : 'oTransitionEnd', 'OTransition' : 'oTransitionEnd',
'msTransition' : 'MSTransitionEnd', 'msTransition' : 'MSTransitionEnd',
'transition' : 'transitionend' 'transition' : 'transitionend'
}; };
this.transEndEventName = this.transEndEventNames[ Modernizr.prefixed( 'transition' ) ]; this.transEndEventName = this.transEndEventNames[ Modernizr.prefixed( 'transition' ) ];
// suport for css transforms and css transitions // suport for css transforms and css transitions
this.support = Modernizr.csstransitions && Modernizr.csstransforms; this.support = Modernizr.csstransitions && Modernizr.csstransforms;
// set inline CSS3 transition properties // set inline CSS3 transition properties
var transformPrefixed = Modernizr.prefixed('transform'); var transformPrefixed = Modernizr.prefixed('transform');
transformPrefixed = transformPrefixed.replace(/([A-Z])/g, function(transformPrefixed,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-'); transformPrefixed = transformPrefixed.replace(/([A-Z])/g, function(transformPrefixed,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-');
this.transitionProp = { this.transitionProp = {
'transition': transformPrefixed+' '+this.settings.speed+'ms ease-out, opacity '+this.settings.switchSpeed+'ms' 'transition': transformPrefixed+' '+this.settings.speed+'ms ease-out, opacity '+this.settings.switchSpeed+'ms'
}; };
this.numSlides = this.settings.src.length; this.numSlides = this.settings.src.length;
// make sure the container is not [position: static] // make sure the container is not [position: static]
switch(this.$el.css('position')) { switch(this.$el.css('position')) {
case 'relative': case 'relative':
case 'absolute': case 'absolute':
case 'fixed': case 'fixed':
break; break;
default: default:
this.$el.css('position', 'relative'); this.$el.css('position', 'relative');
break; break;
} }
// make sure the first image has been loaded. // make sure the first image has been loaded.
var self = this; var self = this;
var $img = $('<img />'); var $img = $('<img />');
//$img.load( function() { //$img.load( function() {
if (self.numSlides == 1) { if (self.numSlides == 1) {
self.initSingle(); self.initSingle();
} else { } else {
self.initSlideshow(); self.initSlideshow();
} }
//}); //});
$img.attr('src', this.settings.src[0]); $img.attr('src', this.settings.src[0]);
}, },
initSlideshow: function () { initSlideshow: function () {
var self = this; var self = this;
var $slideshow = $('<div class="zs-slideshow"></div>'), var $slideshow = $('<div class="zs-slideshow"></div>'),
$slidesWrap = $('<div class="zs-slides"></div>'), $slidesWrap = $('<div class="zs-slides"></div>'),
$arrowsWrap = $('<div class="zs-arrows"></div>'), $arrowsWrap = $('<div class="zs-arrows"></div>'),
$zslayer = $('<div class="zs-layer"></div>'), $zslayer = $('<div class="zs-layer"></div>'),
$bulletsWrap = $('<div class="zs-bullets"></div>'), $bulletsWrap = $('<div class="zs-bullets"></div>'),
$ww = $(window).width(); $ww = $(window).width();
for (i = 0; i < this.numSlides; i++) { for (i = 0; i < this.numSlides; i++) {
var $slide = $('<div class="zs-slide zs-slide-' + i + '"></div>'); var $slide = $('<div class="zs-slide zs-slide-' + i + '"></div>');
if ( $ww <= 767 && this.settings.src2[i].length ) { if ( $ww <= 767 && this.settings.src2[i].length ) {
$slide.css({ 'background-image': "url('" + this.settings.src2[i] + "')" }).appendTo( $slidesWrap ); $slide.css({ 'background-image': "url('" + this.settings.src2[i] + "')" }).appendTo( $slidesWrap );
} }
else { else {
$slide.css({ 'background-image': "url('" + this.settings.src[i] + "')" }).appendTo( $slidesWrap ); $slide.css({ 'background-image': "url('" + this.settings.src[i] + "')" }).appendTo( $slidesWrap );
} }
var $bullet = $('<div class="zs-bullet zs-bullet-' + i + '"></div>') var $bullet = $('<div class="zs-bullet zs-bullet-' + i + '"></div>')
$bullet.appendTo( $bulletsWrap ); $bullet.appendTo( $bulletsWrap );
if (i == 0) { if (i == 0) {
$slide.addClass('active').css('opacity', 1); $slide.addClass('active').css('opacity', 1);
$bullet.addClass('active'); $bullet.addClass('active');
$('.zs-enabled .lte-zs-slider-inner.lte-zs-slide-' + i).addClass('visible'); $('.zs-enabled .lte-zs-slider-inner.lte-zs-slide-' + i).addClass('visible');
} }
} }
self._promoteChildren(); self._promoteChildren();
$slideshow.append( $zslayer ); $slideshow.append( $zslayer );
$slideshow.append( $slidesWrap ).prependTo( this.$el ); $slideshow.append( $slidesWrap ).prependTo( this.$el );
if ( this.settings.bullets != false || this.settings.bullets == 'outside' ) { if ( this.settings.bullets != false || this.settings.bullets == 'outside' ) {
if ( this.settings.bullets == 'outside' ) { if ( this.settings.bullets == 'outside' ) {
$slideshow.after( $bulletsWrap ); $slideshow.after( $bulletsWrap );
} }
else { else {
$slideshow.append( $bulletsWrap ); $slideshow.append( $bulletsWrap );
} }
$slideshow.on('click', '.zs-bullet', function(e){ $slideshow.on('click', '.zs-bullet', function(e){
self.jump( $(this).index() ); self.jump( $(this).index() );
}); });
} }
if ( this.settings.arrows == true || this.settings.arrows == 'right' || this.settings.arrows == 'bottom' ) { if ( this.settings.arrows == true || this.settings.arrows == 'right' || this.settings.arrows == 'bottom' ) {
var container_class = ''; var container_class = '';
if ( this.settings.arrows == 'bottom' ) { if ( this.settings.arrows == 'bottom' ) {
container_class = 'container'; container_class = 'container';
} }
$('<div class="'+container_class+'"><span class="lte-arrow-left">'+this.settings.prev+'</span><span class="lte-arrow-right">'+this.settings.next+'</span></div>').appendTo( $arrowsWrap ); $('<div class="'+container_class+'"><span class="lte-arrow-left">'+this.settings.prev+'</span><span class="lte-arrow-right">'+this.settings.next+'</span></div>').appendTo( $arrowsWrap );
this.$el.append( $arrowsWrap ); this.$el.append( $arrowsWrap );
this.$el.on('click', '.lte-arrow-left', function(e){ this.$el.on('click', '.lte-arrow-left', function(e){
self.prev(); self.prev();
}); });
this.$el.on('click', '.lte-arrow-right', function(e){ this.$el.on('click', '.lte-arrow-right', function(e){
self.next() ; self.next() ;
}); });
} }
this.pos = 0; this.pos = 0;
this.pending = null; this.pending = null;
this.switching = false; this.switching = false;
this.$slideshow = $slideshow; this.$slideshow = $slideshow;
this.$slides = $slidesWrap.children( '.zs-slide' ); this.$slides = $slidesWrap.children( '.zs-slide' );
this.$bullets = $bulletsWrap.children( '.zs-bullet' ); this.$bullets = $bulletsWrap.children( '.zs-bullet' );
this.$el.addClass('zs-enabled'); this.$el.addClass('zs-enabled');
var $firstBlock = $('.zs-enabled .lte-zs-slider-inner'); var $firstBlock = $('.zs-enabled .lte-zs-slider-inner');
$('.zs-enabled .lte-zs-slider-inner').css('opacity', ''); $('.zs-enabled .lte-zs-slider-inner').css('opacity', '');
var minHeight = 0; var minHeight = 0;
$('.lte-zs-slider-inner').each(function(i, el) { $('.lte-zs-slider-inner').each(function(i, el) {
if ( $(el).height() > minHeight ) { if ( $(el).height() > minHeight ) {
minHeight = $(el).height(); minHeight = $(el).height();
} }
}); });
if (this.support) { if (this.support) {
var $firstSlide = this.$slides.eq(0); var $firstSlide = this.$slides.eq(0);
var $initzoom = this.settings.initzoom; var $initzoom = this.settings.initzoom;
$firstSlide.css('opacity', 0).css( this.transitionProp ); $firstSlide.css('opacity', 0).css( this.transitionProp );
$('.lte-zs-slider-wrapper').css('min-height', (minHeight ) + 'px' ); $('.lte-zs-slider-wrapper').css('min-height', (minHeight ) + 'px' );
$('.zs-slideshow').css('min-height', (minHeight - 2 ) + 'px' ); $('.zs-slideshow').css('min-height', (minHeight - 2 ) + 'px' );
jQuery(window).on('resize', function(){ jQuery(window).on('resize', function(){
var minHeight = 0; var minHeight = 0;
$('.lte-zs-slider-inner').each(function(i, el) { $('.lte-zs-slider-inner').each(function(i, el) {
if ( $(el).height() > minHeight ) { if ( $(el).height() > minHeight ) {
minHeight = $(el).height(); minHeight = $(el).height();
} }
}); });
$('.lte-zs-slider-wrapper').css('min-height', (minHeight ) + 'px' ); $('.lte-zs-slider-wrapper').css('min-height', (minHeight ) + 'px' );
$('.zs-slideshow').css('min-height', (minHeight - 2 ) + 'px' ); $('.zs-slideshow').css('min-height', (minHeight - 2 ) + 'px' );
}); });
setTimeout(function(){ setTimeout(function(){
$firstSlide.css( { 'opacity': 1.0, 'transform': 'scale('+ $initzoom +', '+ $initzoom +')', 'z-index': 2 } ); $firstSlide.css( { 'opacity': 1.0, 'transform': 'scale('+ $initzoom +', '+ $initzoom +')', 'z-index': 2 } );
}, 50); }, 50);
} }
if (this.settings.autoplay == true) { if (this.settings.autoplay == true) {
this.play(); this.play();
} }
}, },
initSingle: function() { initSingle: function() {
var self = this; var self = this;
var $slideshow = $('<div class="zs-slideshow"></div>'), var $slideshow = $('<div class="zs-slideshow"></div>'),
$slidesWrap = $('<div class="zs-slides"></div>'), $slidesWrap = $('<div class="zs-slides"></div>'),
$slide = $('<div class="zs-slide zs-slide-0"></div>'); $slide = $('<div class="zs-slide zs-slide-0"></div>');
$slide.css({ 'background-image': "url('" + this.settings.src[0] + "')" }).appendTo( $slidesWrap ); $slide.css({ 'background-image': "url('" + this.settings.src[0] + "')" }).appendTo( $slidesWrap );
$slide.addClass('active').css('opacity', 1); $slide.addClass('active').css('opacity', 1);
$('.zs-enabled .lte-zs-slider-inner.lte-zs-slide-0').addClass('visible').addClass('single'); $('.zs-enabled .lte-zs-slider-inner.lte-zs-slide-0').addClass('visible').addClass('single');
self._promoteChildren(); self._promoteChildren();
$slideshow.append( $slidesWrap ).prependTo( this.$el ); $slideshow.append( $slidesWrap ).prependTo( this.$el );
this.$el.addClass('zs-enabled'); this.$el.addClass('zs-enabled');
if (this.settings.overlay == 'dots') { if (this.settings.overlay == 'dots') {
this.$el.addClass('overlay-dots'); this.$el.addClass('overlay-dots');
} else if (this.settings.overlay == 'plain') { } else if (this.settings.overlay == 'plain') {
this.$el.addClass('overlay-plain') this.$el.addClass('overlay-plain')
} }
if (this.support) { if (this.support) {
$slide.css('opacity', 1).css( this.transitionProp ); $slide.css('opacity', 1).css( this.transitionProp );
if (this.settings) { if (this.settings) {
setTimeout(function(){ setTimeout(function(){
$slide.css( { 'opacity': 1.0, 'transform': 'scale(1)', 'z-index': 2 } ) $slide.css( { 'opacity': 1.0, 'transform': 'scale(1)', 'z-index': 2 } )
}, 50); }, 50);
} }
} }
}, },
_promoteChildren: function() { _promoteChildren: function() {
// make sure every children have high enough z-index // make sure every children have high enough z-index
this.$el.children().each(function(index){ this.$el.children().each(function(index){
$this = $(this); $this = $(this);
if ($this.css('z-index') == 'auto') { if ($this.css('z-index') == 'auto') {
$this.css('z-index', 2); $this.css('z-index', 2);
} }
if ($this.css('position') == 'static') { if ($this.css('position') == 'static') {
$this.css('position', 'relative'); $this.css('position', 'relative');
} }
}); });
}, },
jump: function( pos ) { jump: function( pos ) {
if ( pos >= this.numSlides ) { if ( pos >= this.numSlides ) {
console.log('ZoomSlider: jump(pos) aborted. supplied index out of range.'); console.log('ZoomSlider: jump(pos) aborted. supplied index out of range.');
return; return;
} }
if ( this.pos == pos ) return; if ( this.pos == pos ) return;
if ( this.switching ) { if ( this.switching ) {
this.pending = pos; this.pending = pos;
return; return;
} }
var self = this; var self = this;
var $lastSlide = this.$slides.eq( this.pos ); var $lastSlide = this.$slides.eq( this.pos );
var $nowSlide = this.$slides.eq( pos ); var $nowSlide = this.$slides.eq( pos );
$('.zs-enabled .lte-zoompages .current').html(pos + 1); $('.zs-enabled .lte-zoompages .current').html(pos + 1);
$('.zs-enabled .lte-zs-slider-inner.visible').removeClass('visible'); $('.zs-enabled .lte-zs-slider-inner.visible').removeClass('visible');
$('.zs-enabled .lte-zs-slider-inner.lte-zs-slide-' + pos).addClass('visible'); $('.zs-enabled .lte-zs-slider-inner.lte-zs-slide-' + pos).addClass('visible');
if ( this.support ) { if ( this.support ) {
this.switching = true; this.switching = true;
$lastSlide.css('z-index', 1); $lastSlide.css('z-index', 1);
$nowSlide.addClass('active') $nowSlide.addClass('active')
.css( this.transitionProp ) .css( this.transitionProp )
.css( { 'opacity': 1.0, 'transform': 'scale('+this.settings.initzoom+', '+this.settings.initzoom+')', 'z-index': 2 } ) .css( { 'opacity': 1.0, 'transform': 'scale('+this.settings.initzoom+', '+this.settings.initzoom+')', 'z-index': 2 } )
.on( this.transEndEventName, function(e) { .on( this.transEndEventName, function(e) {
if (e.originalEvent.propertyName == 'opacity') { if (e.originalEvent.propertyName == 'opacity') {
lastSlideBg = $lastSlide.css('background-image'); lastSlideBg = $lastSlide.css('background-image');
$lastSlide.removeClass('active') $lastSlide.removeClass('active')
.removeAttr('style') .removeAttr('style')
.css('background-image', lastSlideBg); .css('background-image', lastSlideBg);
$nowSlide.off( self.transEndEventName ); $nowSlide.off( self.transEndEventName );
self.switching = false; self.switching = false;
if ( self.pending != null ) { if ( self.pending != null ) {
setTimeout(function(){ setTimeout(function(){
var newPos = self.pending; var newPos = self.pending;
self.pending = null; self.pending = null;
self.$bullets.eq(newPos).click(); self.$bullets.eq(newPos).click();
}, 30) }, 30)
} }
} }
}); });
} else { } else {
$lastSlide.removeClass('active'); $lastSlide.removeClass('active');
$nowSlide.addClass('active'); $nowSlide.addClass('active');
} }
this.$bullets.eq(this.pos).removeClass('active'); this.$bullets.eq(this.pos).removeClass('active');
this.$bullets.eq(pos).addClass('active'); this.$bullets.eq(pos).addClass('active');
this.pos = pos; this.pos = pos;
if (this.settings.autoplay) { if (this.settings.autoplay) {
this.play(); this.play();
} }
}, },
prev: function() { prev: function() {
var posPrev = this.pos - 1; var posPrev = this.pos - 1;
if (posPrev < 0) posPrev = this.numSlides - 1; if (posPrev < 0) posPrev = this.numSlides - 1;
this.jump( posPrev ); this.jump( posPrev );
}, },
next: function() { next: function() {
var posNext = this.pos + 1; var posNext = this.pos + 1;
if (posNext >= this.numSlides) posNext = 0; if (posNext >= this.numSlides) posNext = 0;
this.jump( posNext ); this.jump( posNext );
}, },
play: function() { play: function() {
// clear any existing timer // clear any existing timer
if (this.timer != null) { if (this.timer != null) {
clearInterval(this.timer); clearInterval(this.timer);
} }
var self = this; var self = this;
this.settings.autoplay = true; this.settings.autoplay = true;
// add timer // add timer
this.timer = setInterval( function(){ this.timer = setInterval( function(){
self.next(); self.next();
}, this.settings.interval ); }, this.settings.interval );
}, },
stop: function() { stop: function() {
this.settings.autoplay = false; this.settings.autoplay = false;
clearInterval(this.timer); clearInterval(this.timer);
this.timer = null; this.timer = null;
} }
}); });
// A really lightweight plugin wrapper around the constructor, // A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations // preventing against multiple instantiations
$.fn[ pluginName ] = function ( options ) { $.fn[ pluginName ] = function ( options ) {
return this.each(function() { return this.each(function() {
if ( !$.data( this, "plugin_" + pluginName ) ) { if ( !$.data( this, "plugin_" + pluginName ) ) {
$.data( this, "plugin_" + pluginName, new Plugin( this, options ) ); $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
} }
}); });
}; };
var WidgetZoomsliderHandler = function ($scope, $) { var WidgetZoomsliderHandler = function ($scope, $) {
// auto create slideshow on [data-zs-enabled] instances. // auto create slideshow on [data-zs-enabled] instances.
var $instances = $('[data-zs-src]'); var $instances = $('[data-zs-src]');
if ($instances.length > 0) { if ($instances.length > 0) {
$instances.each( function(index) { $instances.each( function(index) {
var $this = $(this); var $this = $(this);
$this.zoomSlider(); $this.zoomSlider();
}); });
} }
}; };
$(window).on('elementor/frontend/init', function () { $(window).on('elementor/frontend/init', function () {
elementorFrontend.hooks.addAction('frontend/element_ready/lte-zoomslider.default', WidgetZoomsliderHandler); elementorFrontend.hooks.addAction('frontend/element_ready/lte-zoomslider.default', WidgetZoomsliderHandler);
}); });
})( jQuery, window, document ); })( jQuery, window, document );
+40 -40
View File
@@ -1,41 +1,41 @@
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Get the buttons and the navbar element // Get the buttons and the navbar element
const openButton = document.getElementById('open-button'); const openButton = document.getElementById('open-button');
const closeButton = document.getElementById('close-button'); const closeButton = document.getElementById('close-button');
const navbar = document.getElementById('navbar'); const navbar = document.getElementById('navbar');
// Log to check if elements exist // Log to check if elements exist
console.log('Open button:', openButton); console.log('Open button:', openButton);
console.log('Close button:', closeButton); console.log('Close button:', closeButton);
console.log('Navbar:', navbar); console.log('Navbar:', navbar);
// Ensure that buttons and navbar exist // Ensure that buttons and navbar exist
if (openButton && closeButton && navbar) { if (openButton && closeButton && navbar) {
console.log('Elements found and event listeners ready.'); console.log('Elements found and event listeners ready.');
// Add event listener to the open button to remove the collapse class // Add event listener to the open button to remove the collapse class
openButton.addEventListener('click', function() { openButton.addEventListener('click', function() {
console.log('Open button clicked'); console.log('Open button clicked');
if (navbar.classList.contains('collapse')) { if (navbar.classList.contains('collapse')) {
navbar.classList.remove('collapse'); navbar.classList.remove('collapse');
console.log('Collapse class removed'); console.log('Collapse class removed');
} else { } else {
console.log('Navbar is already open (no collapse class).'); console.log('Navbar is already open (no collapse class).');
} }
}); });
// Add event listener to the close button to add the collapse class // Add event listener to the close button to add the collapse class
closeButton.addEventListener('click', function() { closeButton.addEventListener('click', function() {
console.log('Close button clicked'); console.log('Close button clicked');
if (!navbar.classList.contains('collapse')) { if (!navbar.classList.contains('collapse')) {
navbar.classList.add('collapse'); navbar.classList.add('collapse');
console.log('Collapse class added'); console.log('Collapse class added');
} else { } else {
console.log('Navbar is already collapsed (collapse class exists).'); console.log('Navbar is already collapsed (collapse class exists).');
} }
}); });
} else { } else {
console.error('Error: Buttons or navbar element not found.'); console.error('Error: Buttons or navbar element not found.');
} }
}); });
+12 -12
View File
File diff suppressed because one or more lines are too long
+328 -328
View File
@@ -1,328 +1,328 @@
// team-switcher.js // team-switcher.js
// Loads team data from XML and populates the team slider. Adds a men/women switcher. // Loads team data from XML and populates the team slider. Adds a men/women switcher.
(function () { (function () {
const XML_URL = 'data/team.xml'; const XML_URL = 'data/team.xml';
const SWITCHER_ID = 'gender-switcher'; const SWITCHER_ID = 'gender-switcher';
const WRAPPER_ID = 'team-swiper-wrapper-1'; const WRAPPER_ID = 'team-swiper-wrapper-1';
const PRELOADER_ID = 'team-preloader-1'; const PRELOADER_ID = 'team-preloader-1';
const SECTION_ID = 'team-section-1'; const SECTION_ID = 'team-section-1';
let teamData = null; // cached parsed XML data let teamData = null; // cached parsed XML data
let currentGender = 'men'; let currentGender = 'men';
// Autoscroll timers // Autoscroll timers
let autoTimer = null; let autoTimer = null;
let resumeTimer = null; let resumeTimer = null;
const AUTO_DELAY = 3500; // 5s const AUTO_DELAY = 3500; // 5s
const RESUME_AFTER = 10000; // resume 10s after user interaction const RESUME_AFTER = 10000; // resume 10s after user interaction
function qs(sel, root = document) { return root.querySelector(sel); } function qs(sel, root = document) { return root.querySelector(sel); }
function qsa(sel, root = document) { return Array.from(root.querySelectorAll(sel)); } function qsa(sel, root = document) { return Array.from(root.querySelectorAll(sel)); }
async function loadXML() { async function loadXML() {
if (teamData) return teamData; if (teamData) return teamData;
const res = await fetch(XML_URL, { cache: 'no-cache' }); const res = await fetch(XML_URL, { cache: 'no-cache' });
if (!res.ok) throw new Error('Failed to fetch team.xml'); if (!res.ok) throw new Error('Failed to fetch team.xml');
const text = await res.text(); const text = await res.text();
const parser = new DOMParser(); const parser = new DOMParser();
const xml = parser.parseFromString(text, 'application/xml'); const xml = parser.parseFromString(text, 'application/xml');
const parseError = xml.querySelector('parsererror'); const parseError = xml.querySelector('parsererror');
if (parseError) throw new Error('Invalid XML in team.xml'); if (parseError) throw new Error('Invalid XML in team.xml');
teamData = xml; teamData = xml;
return xml; return xml;
} }
function getMembersByCategory(xml, categoryName) { function getMembersByCategory(xml, categoryName) {
const cat = Array.from(xml.querySelectorAll('team > category')) const cat = Array.from(xml.querySelectorAll('team > category'))
.find(c => (c.getAttribute('name') || '').toLowerCase() === categoryName); .find(c => (c.getAttribute('name') || '').toLowerCase() === categoryName);
if (!cat) return []; if (!cat) return [];
return Array.from(cat.querySelectorAll('member')).map(m => ({ return Array.from(cat.querySelectorAll('member')).map(m => ({
name: (m.querySelector('name')?.textContent || '').trim(), name: (m.querySelector('name')?.textContent || '').trim(),
number: (m.querySelector('number')?.textContent || '').trim(), number: (m.querySelector('number')?.textContent || '').trim(),
role: (m.querySelector('role')?.textContent || '').trim(), role: (m.querySelector('role')?.textContent || '').trim(),
image: (m.querySelector('image')?.textContent || '').trim(), image: (m.querySelector('image')?.textContent || '').trim(),
})); }));
} }
function slideHTML(member) { function slideHTML(member) {
const numHTML = member.number ? `<div class="lte-num">${member.number}</div>` : '<div class="lte-num"></div>'; const numHTML = member.number ? `<div class="lte-num">${member.number}</div>` : '<div class="lte-num"></div>';
const safeImg = member.image || ''; const safeImg = member.image || '';
return ( return (
`<div class="lte-item swiper-slide"> `<div class="lte-item swiper-slide">
<div class="lte-team-item"> <div class="lte-team-item">
<a class="lte-image" style="background-image: url()"> <a class="lte-image" style="background-image: url()">
<img loading="lazy" decoding="async" width="800" height="1200" src="${safeImg}" class="attachment-full size-full" /> <img loading="lazy" decoding="async" width="800" height="1200" src="${safeImg}" class="attachment-full size-full" />
</a> </a>
<div class="lte-descr"> <div class="lte-descr">
${numHTML} ${numHTML}
<a href="${safeImg}" target="_blank"> <a href="${safeImg}" target="_blank">
<h4 class="lte-header">${member.name}</h4> <h4 class="lte-header">${member.name}</h4>
</a> </a>
<p class="lte-subheader" style="color: #c42221">${member.role} <p class="lte-subheader" style="color: #c42221">${member.role}
</p> </p>
</div> </div>
</div> </div>
</div>` </div>`
); );
} }
function renderMembers(members) { function renderMembers(members) {
const wrapper = document.getElementById(WRAPPER_ID); const wrapper = document.getElementById(WRAPPER_ID);
if (!wrapper) return; if (!wrapper) return;
const swiperEl = wrapper.closest('.swiper-container'); const swiperEl = wrapper.closest('.swiper-container');
const swiper = swiperEl && swiperEl.swiper; const swiper = swiperEl && swiperEl.swiper;
// Use DOM-based rendering to match theme's slider expectations // Use DOM-based rendering to match theme's slider expectations
wrapper.innerHTML = members.map(slideHTML).join(''); wrapper.innerHTML = members.map(slideHTML).join('');
// Strong refresh // Strong refresh
if (swiper) { if (swiper) {
try { try {
if (typeof swiper.updateSlides === 'function') swiper.updateSlides(); if (typeof swiper.updateSlides === 'function') swiper.updateSlides();
if (typeof swiper.updateSize === 'function') swiper.updateSize(); if (typeof swiper.updateSize === 'function') swiper.updateSize();
if (typeof swiper.updateAutoHeight === 'function') swiper.updateAutoHeight(0); if (typeof swiper.updateAutoHeight === 'function') swiper.updateAutoHeight(0);
if (typeof swiper.slideTo === 'function') swiper.slideTo(0, 0, false); if (typeof swiper.slideTo === 'function') swiper.slideTo(0, 0, false);
if (typeof swiper.update === 'function') swiper.update(); if (typeof swiper.update === 'function') swiper.update();
} catch (e) {} } catch (e) {}
} }
// Ask the theme to re-init this slider completely so arrows/loop/order are consistent // Ask the theme to re-init this slider completely so arrows/loop/order are consistent
const sliderContainer = wrapper.closest('.lte-swiper-slider'); const sliderContainer = wrapper.closest('.lte-swiper-slider');
if (sliderContainer) sliderContainer.classList.remove('lte-inited'); if (sliderContainer) sliderContainer.classList.remove('lte-inited');
if (typeof window.initSwiperWrappers === 'function') { if (typeof window.initSwiperWrappers === 'function') {
try { window.initSwiperWrappers(); } catch (_) {} try { window.initSwiperWrappers(); } catch (_) {}
} }
// Remove any duplicate arrow bars the theme may have added on re-init // Remove any duplicate arrow bars the theme may have added on re-init
cleanupDuplicateArrows(); cleanupDuplicateArrows();
setTimeout(() => window.dispatchEvent(new Event('resize')), 0); setTimeout(() => window.dispatchEvent(new Event('resize')), 0);
// Ensure arrows exist and are bound; manual endless wrap // Ensure arrows exist and are bound; manual endless wrap
setupEndlessNavigation(swiperEl); setupEndlessNavigation(swiperEl);
setupDragWrap(swiper); setupDragWrap(swiper);
// Restart autoscroll on fresh render // Restart autoscroll on fresh render
stopAutoScroll(); stopAutoScroll();
startAutoScroll(); startAutoScroll();
} }
// Keep only one arrows bar; prefer the one whose anchors already have our data-ts-bound // Keep only one arrows bar; prefer the one whose anchors already have our data-ts-bound
function cleanupDuplicateArrows() { function cleanupDuplicateArrows() {
const wrapper = document.getElementById(WRAPPER_ID); const wrapper = document.getElementById(WRAPPER_ID);
if (!wrapper) return; if (!wrapper) return;
const slider = wrapper.closest('.lte-swiper-slider'); const slider = wrapper.closest('.lte-swiper-slider');
if (!slider) return; if (!slider) return;
// Arrows can be siblings of slider or children inside slider depending on theme config // Arrows can be siblings of slider or children inside slider depending on theme config
const candidates = []; const candidates = [];
const parent = slider.parentElement; const parent = slider.parentElement;
if (parent) { if (parent) {
Array.from(parent.children).forEach((el) => { if (el.classList && el.classList.contains('lte-arrows')) candidates.push(el); }); Array.from(parent.children).forEach((el) => { if (el.classList && el.classList.contains('lte-arrows')) candidates.push(el); });
} }
Array.from(slider.children).forEach((el) => { if (el.classList && el.classList.contains('lte-arrows')) candidates.push(el); }); Array.from(slider.children).forEach((el) => { if (el.classList && el.classList.contains('lte-arrows')) candidates.push(el); });
if (candidates.length <= 1) return; if (candidates.length <= 1) return;
// Prefer the one that already has data-ts-bound anchors // Prefer the one that already has data-ts-bound anchors
const hasBound = candidates.find(a => a.querySelector('a[data-ts-bound="1"]')); const hasBound = candidates.find(a => a.querySelector('a[data-ts-bound="1"]'));
const keep = hasBound || candidates[0]; const keep = hasBound || candidates[0];
candidates.forEach((a) => { if (a !== keep && a.parentElement) a.parentElement.removeChild(a); }); candidates.forEach((a) => { if (a !== keep && a.parentElement) a.parentElement.removeChild(a); });
} }
async function switchGender(gender) { async function switchGender(gender) {
currentGender = gender; currentGender = gender;
try { try {
showPreloader(); showPreloader();
const xml = await loadXML(); const xml = await loadXML();
// Keep the order as in XML so the first visible is the first listed (e.g., Janečka Martin) // Keep the order as in XML so the first visible is the first listed (e.g., Janečka Martin)
const list = getMembersByCategory(xml, gender); const list = getMembersByCategory(xml, gender);
renderMembers(list); renderMembers(list);
updateActiveButton(); updateActiveButton();
markReady(); markReady();
hidePreloader(); hidePreloader();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
hidePreloader(); hidePreloader();
} }
} }
function updateActiveButton() { function updateActiveButton() {
const container = document.getElementById(SWITCHER_ID); const container = document.getElementById(SWITCHER_ID);
if (!container) return; if (!container) return;
qsa('button[data-gender]', container).forEach(btn => { qsa('button[data-gender]', container).forEach(btn => {
btn.classList.toggle('active', btn.dataset.gender === currentGender); btn.classList.toggle('active', btn.dataset.gender === currentGender);
}); });
} }
function bindUI() { function bindUI() {
const container = document.getElementById(SWITCHER_ID); const container = document.getElementById(SWITCHER_ID);
if (!container) return; if (!container) return;
container.addEventListener('click', (e) => { container.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-gender]'); const btn = e.target.closest('button[data-gender]');
if (!btn) return; if (!btn) return;
const gender = btn.dataset.gender; const gender = btn.dataset.gender;
if (gender && gender !== currentGender) switchGender(gender); if (gender && gender !== currentGender) switchGender(gender);
}); });
} }
function ensureBasicStyles() { function ensureBasicStyles() {
const css = ` const css = `
#${SWITCHER_ID}{display:flex;gap:.5rem;justify-content:center;margin:10px 0} #${SWITCHER_ID}{display:flex;gap:.5rem;justify-content:center;margin:10px 0}
#${SWITCHER_ID} .switch-btn{background:#eee;border:1px solid #ccc;border-radius:20px;padding:.35rem .9rem;font-weight:600;cursor:pointer} #${SWITCHER_ID} .switch-btn{background:#eee;border:1px solid #ccc;border-radius:20px;padding:.35rem .9rem;font-weight:600;cursor:pointer}
#${SWITCHER_ID} .switch-btn.active{background:#111;color:#fff;border-color:#111} #${SWITCHER_ID} .switch-btn.active{background:#111;color:#fff;border-color:#111}
#${PRELOADER_ID}{display:none;align-items:center;justify-content:center;gap:.6rem;color:#fff;padding:8px 0} #${PRELOADER_ID}{display:none;align-items:center;justify-content:center;gap:.6rem;color:#fff;padding:8px 0}
#${PRELOADER_ID}.visible{display:flex} #${PRELOADER_ID}.visible{display:flex}
#${PRELOADER_ID} .spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:ts-spin .8s linear infinite} #${PRELOADER_ID} .spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:ts-spin .8s linear infinite}
@keyframes ts-spin{to{transform:rotate(360deg)}} @keyframes ts-spin{to{transform:rotate(360deg)}}
#${SECTION_ID}.not-ready .lte-swiper-slider-wrapper{visibility:hidden} #${SECTION_ID}.not-ready .lte-swiper-slider-wrapper{visibility:hidden}
`; `;
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = css; style.textContent = css;
document.head.appendChild(style); document.head.appendChild(style);
} }
function getSwiperInstance() { function getSwiperInstance() {
const wrapper = document.getElementById(WRAPPER_ID); const wrapper = document.getElementById(WRAPPER_ID);
const swiperEl = wrapper && wrapper.closest('.swiper-container'); const swiperEl = wrapper && wrapper.closest('.swiper-container');
return swiperEl && swiperEl.swiper ? { el: swiperEl, api: swiperEl.swiper } : null; return swiperEl && swiperEl.swiper ? { el: swiperEl, api: swiperEl.swiper } : null;
} }
function setupEndlessNavigation(swiperContainerEl) { function setupEndlessNavigation(swiperContainerEl) {
const inst = getSwiperInstance(); const inst = getSwiperInstance();
if (!inst) return; if (!inst) return;
const { el, api } = inst; const { el, api } = inst;
// Ensure only one set of arrows remains before binding // Ensure only one set of arrows remains before binding
cleanupDuplicateArrows(); cleanupDuplicateArrows();
// Theme uses .lte-arrow-left / .lte-arrow-right (see frontend.js init) // Theme uses .lte-arrow-left / .lte-arrow-right (see frontend.js init)
let nextBtn = el.parentElement && el.parentElement.querySelector('.lte-arrows .lte-arrow-right'); let nextBtn = el.parentElement && el.parentElement.querySelector('.lte-arrows .lte-arrow-right');
let prevBtn = el.parentElement && el.parentElement.querySelector('.lte-arrows .lte-arrow-left'); let prevBtn = el.parentElement && el.parentElement.querySelector('.lte-arrows .lte-arrow-left');
// Fallback to common Swiper classes if theme structure changes // Fallback to common Swiper classes if theme structure changes
if (!nextBtn) nextBtn = el.querySelector('.swiper-button-next, .lte-swiper-button-next, .lte-next, .lte-arrow-next, .lte-arrow-right'); if (!nextBtn) nextBtn = el.querySelector('.swiper-button-next, .lte-swiper-button-next, .lte-next, .lte-arrow-next, .lte-arrow-right');
if (!prevBtn) prevBtn = el.querySelector('.swiper-button-prev, .lte-swiper-button-prev, .lte-prev, .lte-arrow-prev, .lte-arrow-left'); if (!prevBtn) prevBtn = el.querySelector('.swiper-button-prev, .lte-swiper-button-prev, .lte-prev, .lte-arrow-prev, .lte-arrow-left');
// Do not create fallback arrows; rely on theme arrows only // Do not create fallback arrows; rely on theme arrows only
function bind(btn, dir) { function bind(btn, dir) {
if (!btn || btn.dataset.tsBound) return; if (!btn || btn.dataset.tsBound) return;
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
if (!api) return; if (!api) return;
// User override: pause and schedule resume // User override: pause and schedule resume
stopAutoScroll(); stopAutoScroll();
scheduleAutoResume(); scheduleAutoResume();
if (dir === 'next') { if (dir === 'next') {
if (typeof api.slideNext === 'function') api.slideNext(400); if (typeof api.slideNext === 'function') api.slideNext(400);
else api.slideTo((api.activeIndex || 0) + 1, 400, false); else api.slideTo((api.activeIndex || 0) + 1, 400, false);
} else { } else {
if (typeof api.slidePrev === 'function') api.slidePrev(400); if (typeof api.slidePrev === 'function') api.slidePrev(400);
else api.slideTo(Math.max((api.activeIndex || 0) - 1, 0), 400, false); else api.slideTo(Math.max((api.activeIndex || 0) - 1, 0), 400, false);
} }
}); });
btn.dataset.tsBound = '1'; btn.dataset.tsBound = '1';
} }
bind(nextBtn, 'next'); bind(nextBtn, 'next');
bind(prevBtn, 'prev'); bind(prevBtn, 'prev');
// Hover pause/resume on the whole slider area // Hover pause/resume on the whole slider area
if (el && !el.__tsHoverBound) { if (el && !el.__tsHoverBound) {
el.addEventListener('mouseenter', () => stopAutoScroll()); el.addEventListener('mouseenter', () => stopAutoScroll());
el.addEventListener('mouseleave', () => startAutoScroll()); el.addEventListener('mouseleave', () => startAutoScroll());
el.__tsHoverBound = true; el.__tsHoverBound = true;
} }
} }
function setupDragWrap(swiper) { function setupDragWrap(swiper) {
if (!swiper || !swiper.on) return; if (!swiper || !swiper.on) return;
if (!swiper.__tsWrapBound) { if (!swiper.__tsWrapBound) {
swiper.on('reachEnd', () => { swiper.slideTo(0, 400, false); }); swiper.on('reachEnd', () => { swiper.slideTo(0, 400, false); });
swiper.on('reachBeginning', () => { swiper.on('reachBeginning', () => {
const last = (swiper.slides && swiper.slides.length ? swiper.slides.length - 1 : 0); const last = (swiper.slides && swiper.slides.length ? swiper.slides.length - 1 : 0);
swiper.slideTo(last, 400, false); swiper.slideTo(last, 400, false);
}); });
swiper.__tsWrapBound = true; swiper.__tsWrapBound = true;
} }
// Pause autoscroll on user touch/drag and schedule resume on release // Pause autoscroll on user touch/drag and schedule resume on release
if (!swiper.__tsAutoBound) { if (!swiper.__tsAutoBound) {
try { try {
swiper.on('touchStart', () => { stopAutoScroll(); }); swiper.on('touchStart', () => { stopAutoScroll(); });
swiper.on('touchEnd', () => { scheduleAutoResume(); }); swiper.on('touchEnd', () => { scheduleAutoResume(); });
swiper.on('pointerDown', () => { stopAutoScroll(); }); swiper.on('pointerDown', () => { stopAutoScroll(); });
swiper.on('pointerUp', () => { scheduleAutoResume(); }); swiper.on('pointerUp', () => { scheduleAutoResume(); });
} catch (_) {} } catch (_) {}
swiper.__tsAutoBound = true; swiper.__tsAutoBound = true;
} }
} }
function startAutoScroll() { function startAutoScroll() {
const inst = getSwiperInstance(); const inst = getSwiperInstance();
if (!inst) return; if (!inst) return;
const { api } = inst; const { api } = inst;
stopAutoScroll(); stopAutoScroll();
autoTimer = window.setInterval(() => { autoTimer = window.setInterval(() => {
if (!api) return; if (!api) return;
try { try {
// If not looping, wrap to first when at end // If not looping, wrap to first when at end
const loop = api.params && api.params.loop; const loop = api.params && api.params.loop;
if (!loop && api.isEnd) { if (!loop && api.isEnd) {
api.slideTo(0, 600, false); api.slideTo(0, 600, false);
} else if (typeof api.slideNext === 'function') { } else if (typeof api.slideNext === 'function') {
api.slideNext(600); api.slideNext(600);
} }
} catch (_) {} } catch (_) {}
}, AUTO_DELAY); }, AUTO_DELAY);
} }
function stopAutoScroll() { function stopAutoScroll() {
if (autoTimer) { if (autoTimer) {
clearInterval(autoTimer); clearInterval(autoTimer);
autoTimer = null; autoTimer = null;
} }
if (resumeTimer) { if (resumeTimer) {
clearTimeout(resumeTimer); clearTimeout(resumeTimer);
resumeTimer = null; resumeTimer = null;
} }
} }
function scheduleAutoResume() { function scheduleAutoResume() {
if (resumeTimer) { if (resumeTimer) {
clearTimeout(resumeTimer); clearTimeout(resumeTimer);
resumeTimer = null; resumeTimer = null;
} }
resumeTimer = window.setTimeout(() => { resumeTimer = window.setTimeout(() => {
startAutoScroll(); startAutoScroll();
}, RESUME_AFTER); }, RESUME_AFTER);
} }
function showPreloader() { function showPreloader() {
const el = document.getElementById(PRELOADER_ID); const el = document.getElementById(PRELOADER_ID);
if (el) el.classList.add('visible'); if (el) el.classList.add('visible');
} }
function hidePreloader() { function hidePreloader() {
const el = document.getElementById(PRELOADER_ID); const el = document.getElementById(PRELOADER_ID);
if (el) el.classList.remove('visible'); if (el) el.classList.remove('visible');
} }
function markNotReady() { function markNotReady() {
const sec = document.getElementById(SECTION_ID); const sec = document.getElementById(SECTION_ID);
if (sec) sec.classList.add('not-ready'); if (sec) sec.classList.add('not-ready');
} }
function markReady() { function markReady() {
const sec = document.getElementById(SECTION_ID); const sec = document.getElementById(SECTION_ID);
if (sec) sec.classList.remove('not-ready'); if (sec) sec.classList.remove('not-ready');
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
ensureBasicStyles(); ensureBasicStyles();
markNotReady(); markNotReady();
showPreloader(); showPreloader();
bindUI(); bindUI();
}); });
// Defer initial population until all assets and theme scripts (e.g., sliders) are fully initialized // Defer initial population until all assets and theme scripts (e.g., sliders) are fully initialized
window.addEventListener('load', () => { window.addEventListener('load', () => {
switchGender(currentGender); switchGender(currentGender);
}); });
})(); })();
+23 -23
View File
@@ -1,23 +1,23 @@
server { server {
listen 80; listen 80;
server_name _; server_name _;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Serve static assets # Serve static assets
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Proxy data API to backend # Proxy data API to backend
location /data/ { location /data/ {
proxy_pass http://backend:8080/data/; proxy_pass http://backend:8080/data/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Origin *;
} }
location /healthz { location /healthz {
proxy_pass http://backend:8080/healthz; proxy_pass http://backend:8080/healthz;
} }
} }
+810 -810
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# Remove local blog files and keep only remote blogs
# Run this in your bizoni directory
echo "🗑️ Removing local blog files..."
# Remove all blog files
rm -rf blog/
echo "✅ Local blog files removed!"
echo ""
echo "📝 Next steps:"
echo "1. Deploy updated backend to server"
echo "2. Backend will now work with remote blogs only"
echo "3. Admin interface will connect to server API"
echo ""
echo "🌐 Your live blogs will remain untouched on the server"
+300
View File
@@ -0,0 +1,300 @@
#!/bin/bash
# 🚀 Bizoni Remote Blog Setup - One Command Script
# Run this to setup remote blog management
set -e
echo "🚀 Bizoni Remote Blog Setup"
echo "=========================="
# Configuration - UPDATE THESE
SERVER_USER="your_username" # Your SSH username
SERVER_HOST="your_server.com" # Your server domain/IP
SERVER_BLOG_DIR="/var/www/bizoni/blog" # Blog directory on server
LOCAL_PROJECT_DIR="/home/tdvorak/Desktop/HTML_Projekty/bizoni" # Your local project
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
print_status() {
echo -e "${BLUE}$1${NC}"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
# Check if local blogs exist
check_local_blogs() {
print_status "Checking local blog files..."
if [ -d "$LOCAL_PROJECT_DIR/blog" ]; then
blog_count=$(find "$LOCAL_PROJECT_DIR/blog" -name "*.html" -type f 2>/dev/null | wc -l)
if [ "$blog_count" -gt 0 ]; then
print_warning "Found $blog_count local blog files"
read -p "Remove local blog files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf "$LOCAL_PROJECT_DIR/blog"
print_success "Local blog files removed"
else
print_warning "Skipping local blog removal"
fi
else
print_success "No local blog files found"
fi
else
print_success "No local blog directory found"
fi
}
# Create Ubuntu management script
create_server_script() {
print_status "Creating server management script..."
cat > /tmp/ubuntu-blog-manager.sh << 'EOF'
#!/bin/bash
# Ubuntu Server Blog Management Script
set -e
BLOG_DIR="/var/www/bizoni/blog"
BACKUP_DIR="/var/backups/bizoni-blogs"
# Create backup
create_backup() {
echo "📦 Creating backup..."
mkdir -p "$BACKUP_DIR"
timestamp=$(date +%Y%m%d_%H%M%S)
tar -czf "$BACKUP_DIR/blogs_backup_$timestamp.tar.gz" -C "$BLOG_DIR" . 2>/dev/null || echo "No files to backup"
echo "✅ Backup created"
}
# Generate slug from title
generate_slug() {
local title="$1"
echo "$title" | tr '[:upper:]' '[:lower:]' | \
sed 's/á/a/g; s/ä/a/g; s/č/c/g; s/ď/d/g; s/é/e/g; s/ě/e/g; s/í/i/g; s/ľ/l/g; s/ň/n/g; s/ó/o/g; s/ö/o/g; s/ô/o/g; s/ř/r/g; s/š/s/g; s/ť/t/g; s/ú/u/g; s/ů/u/g; s/ý/y/g; s/ž/z/g' | \
sed 's/Á/a/g; s/Ä/a/g; s/Č/c/g; s/Ď/d/g; s/É/e/g; s/Ě/e/g; s/Í/i/g; s/Ľ/l/g; s/Ň/n/g; s/Ó/o/g; s/Ö/o/g; s/Ô/o/g; s/Ř/r/g; s/Š/s/g; s/Ť/t/g; s/Ú/u/g; s/Ů/u/g; s/Ý/y/g; s/Ž/z/g' | \
sed 's/[^a-z0-9\s-]//g' | \
sed 's/[\s-]\+/ -/g' | \
sed 's/^-\|-$//g'
}
# Add slug to blog
add_slug_to_blog() {
local blog_id="$1"
local blog_file="$BLOG_DIR/$blog_id.html"
if [ ! -f "$blog_file" ]; then
echo "❌ Blog not found: $blog_id.html"
return 1
fi
# Check if slug already exists
if grep -q '<meta name="slug"' "$blog_file"; then
echo "️ $blog_id already has slug"
return 0
fi
# Extract title
title=$(grep -o '<h1[^>]*class="[^"]*lte-header[^"]*"[^>]*>.*</h1>' "$blog_file" 2>/dev/null | sed 's/<[^>]*>//g' | xargs || echo "")
if [ -z "$title" ]; then
echo "❌ Could not extract title from $blog_id"
return 1
fi
# Generate slug
slug=$(generate_slug "$title")
# Check uniqueness
counter=2
original_slug="$slug"
while [ -f "$BLOG_DIR/$slug.html" ]; do
slug="$original_slug-$counter"
((counter++))
done
echo "📝 $blog_id: $title → $slug"
# Add slug meta tag
sed -i "s|</head>|<meta name=\"slug\" content=\"$slug\">\n</head>|" "$blog_file"
# Create slug file
cp "$blog_file" "$BLOG_DIR/$slug.html"
echo "✅ Added slug: $slug.html"
}
# Migrate all blogs
migrate_all() {
echo "🔄 Migrating all blogs..."
if [ ! -d "$BLOG_DIR" ]; then
echo "❌ Blog directory not found: $BLOG_DIR"
exit 1
fi
create_backup
count=0
for blog_file in "$BLOG_DIR"/*.html; do
if [ -f "$blog_file" ]; then
filename=$(basename "$blog_file")
if [[ "$filename" =~ ^([0-9]{4})\.html$ ]]; then
blog_id="${BASH_REMATCH[1]}"
add_slug_to_blog "$blog_id"
((count++))
fi
fi
done
echo "✅ Migration completed! Processed $count blogs"
}
# List blogs
list_blogs() {
echo "📋 Blogs on server:"
if [ -d "$BLOG_DIR" ]; then
for file in "$BLOG_DIR"/*.html; do
if [ -f "$file" ]; then
filename=$(basename "$file")
if [[ "$filename" =~ ^[0-9]{4}\.html$ ]]; then
echo " 📄 $filename (numeric)"
elif [[ "$filename" =~ ^[a-z0-9-]+\.html$ ]]; then
echo " 🔗 $filename (slug)"
fi
fi
done
else
echo " ❌ Blog directory not found"
fi
}
case "${1:-}" in
"migrate")
migrate_all
;;
"list")
list_blogs
;;
"backup")
create_backup
;;
*)
echo "Ubuntu Blog Manager"
echo "=================="
echo "Usage: $0 <command>"
echo ""
echo "Commands:"
echo " migrate - Add slugs to all blogs"
echo " list - List all blogs"
echo " backup - Create backup"
echo ""
echo "Example: $0 migrate"
exit 1
;;
esac
EOF
chmod +x /tmp/ubuntu-blog-manager.sh
print_success "Server script created"
}
# Deploy to server
deploy_to_server() {
print_status "Deploying to server..."
# Upload server script
scp /tmp/ubuntu-blog-manager.sh "$SERVER_USER@$SERVER_HOST:/tmp/"
# Move script to server location and make executable
ssh "$SERVER_USER@$SERVER_HOST" "sudo mv /tmp/ubuntu-blog-manager.sh /usr/local/bin/blog-manager && sudo chmod +x /usr/local/bin/blog-manager"
print_success "Server script deployed to /usr/local/bin/blog-manager"
}
# Test server connection
test_connection() {
print_status "Testing server connection..."
if ssh "$SERVER_USER@$SERVER_HOST" "echo 'Connection successful'" 2>/dev/null; then
print_success "Server connection OK"
else
print_error "Cannot connect to server. Please check:"
echo " - Username: $SERVER_USER"
echo " - Host: $SERVER_HOST"
echo " - SSH key or password setup"
exit 1
fi
}
# Run migration on server
run_migration() {
print_status "Running migration on server..."
ssh "$SERVER_USER@$SERVER_HOST" "sudo blog-manager migrate"
print_success "Migration completed on server"
}
# Show results
show_results() {
print_status "Showing results..."
echo ""
ssh "$SERVER_USER@$SERVER_HOST" "blog-manager list"
echo ""
print_success "Setup completed!"
echo ""
echo "🌐 Your blogs now have clean URLs:"
echo " Old: /blog/0030.html"
echo " New: /blog/jdeme-do-finale"
echo ""
echo "🔧 Server commands you can use:"
echo " ssh $SERVER_USER@$SERVER_HOST 'blog-manager list'"
echo " ssh $SERVER_USER@$SERVER_HOST 'blog-manager backup'"
echo ""
}
# Main execution
main() {
echo ""
print_warning "Please update the configuration in this script:"
echo " SERVER_USER=\"$SERVER_USER\""
echo " SERVER_HOST=\"$SERVER_HOST\""
echo " SERVER_BLOG_DIR=\"$SERVER_BLOG_DIR\""
echo ""
read -p "Continue with current settings? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Please edit the script and run again"
exit 1
fi
check_local_blogs
test_connection
create_server_script
deploy_to_server
run_migration
show_results
}
# Run main function
main "$@"
+158
View File
@@ -0,0 +1,158 @@
#!/bin/bash
# Simple Server Setup Script - Run this ON YOUR SERVER after git push
# Usage: ./setup-server.sh
set -e
echo "🚀 Bizoni Server Setup Script"
echo "============================"
# Configuration - UPDATE THESE IF NEEDED
BLOG_DIR="/var/www/bizoni/blog"
IMG_DIR="/var/www/bizoni/img/blog"
BACKUP_DIR="/var/backups/bizoni"
echo "📁 Blog directory: $BLOG_DIR"
echo "🖼️ Image directory: $IMG_DIR"
echo ""
# Check if directories exist
if [ ! -d "$BLOG_DIR" ]; then
echo "❌ Blog directory not found: $BLOG_DIR"
echo "Please update BLOG_DIR in this script"
exit 1
fi
if [ ! -d "$IMG_DIR" ]; then
echo "❌ Image directory not found: $IMG_DIR"
echo "Please update IMG_DIR in this script"
exit 1
fi
echo "✅ Directories found"
echo ""
# Create backup
echo "📦 Creating backup..."
mkdir -p "$BACKUP_DIR"
timestamp=$(date +%Y%m%d_%H%M%S)
tar -czf "$BACKUP_DIR/blogs_backup_$timestamp.tar.gz" -C "$(dirname "$BLOG_DIR")" "$(basename "$BLOG_DIR")"
echo "✅ Backup created: $BACKUP_DIR/blogs_backup_$timestamp.tar.gz"
echo ""
# Count existing blogs
total_blogs=$(ls "$BLOG_DIR"/*.html 2>/dev/null | wc -l)
numeric_blogs=$(ls "$BLOG_DIR"/[0-9][0-9][0-9][0-9].html 2>/dev/null | wc -l)
slug_blogs=$(ls "$BLOG_DIR"/[a-z]*.html 2>/dev/null | wc -l)
echo "📊 Current blog status:"
echo " Total blogs: $total_blogs"
echo " Numeric files: $numeric_blogs"
echo " Slug files: $slug_blogs"
echo ""
# Function to generate slug from title
generate_slug() {
local title="$1"
echo "$title" | tr '[:upper:]' '[:lower:]' | \
sed 's/á/a/g; s/ä/a/g; s/č/c/g; s/ď/d/g; s/é/e/g; s/ě/e/g; s/í/i/g; s/ľ/l/g; s/ň/n/g; s/ó/o/g; s/ö/o/g; s/ô/o/g; s/ř/r/g; s/š/s/g; s/ť/t/g; s/ú/u/g; s/ů/u/g; s/ý/y/g; s/ž/z/g' | \
sed 's/Á/a/g; s/Ä/a/g; s/Č/c/g; s/Ď/d/g; s/É/e/g; s/Ě/e/g; s/Í/i/g; s/Ľ/l/g; s/Ň/n/g; s/Ó/o/g; s/Ö/o/g; s/Ô/o/g; s/Ř/r/g; s/Š/s/g; s/Ť/t/g; s/Ú/u/g; s/Ů/u/g; s/Ý/y/g; s/Ž/z/g' | \
iconv -c -f utf-8 -t ascii//TRANSLIT | \
sed 's/\s\+/-/g' | \
sed 's/[^a-z0-9\-]//g' | \
sed 's/-\+/-/g' | \
sed 's/^\-\|\-$//g'
}
# Function to extract title from HTML
extract_title() {
local file="$1"
grep -o '<h1[^>]*class="[^"]*lte-header[^"]*"[^>]*>.*</h1>' "$file" | sed 's/<[^>]*>//g' | xargs || echo ""
}
# Function to check if slug exists
slug_exists() {
local slug="$1"
[ -f "$BLOG_DIR/$slug.html" ]
}
# Process numeric blogs that don't have slugs yet
echo "🔄 Processing blogs without slugs..."
processed=0
for blog_file in "$BLOG_DIR"/[0-9][0-9][0-9][0-9].html; do
if [ ! -f "$blog_file" ]; then
continue
fi
filename=$(basename "$blog_file")
blog_id="${filename%.html}"
# Check if slug meta tag already exists
if grep -q '<meta name="slug"' "$blog_file"; then
echo " ⏭️ Skipping $filename (slug already exists)"
continue
fi
# Extract title
title=$(extract_title "$blog_file")
if [ -z "$title" ]; then
echo " ⚠️ Skipping $filename (no title found)"
continue
fi
# Generate slug
base_slug=$(generate_slug "$title")
slug="$base_slug"
# Make slug unique
counter=2
while slug_exists "$slug"; do
slug="${base_slug}-${counter}"
((counter++))
done
echo " 📝 $filename: '$title' → '$slug'"
# Add slug meta tag before </head>
sed -i "s|</head>|<meta name=\"slug\" content=\"$slug\">\n</head>|" "$blog_file"
# Create slug file (copy of original)
cp "$blog_file" "$BLOG_DIR/$slug.html"
((processed++))
done
echo ""
echo "✅ Migration completed!"
echo "📊 Processed $processed blogs"
echo ""
# Show final status
total_blogs=$(ls "$BLOG_DIR"/*.html 2>/dev/null | wc -l)
numeric_blogs=$(ls "$BLOG_DIR"/[0-9][0-9][0-9][0-9].html 2>/dev/null | wc -l)
slug_blogs=$(ls "$BLOG_DIR"/[a-z]*.html 2>/dev/null | wc -l)
echo "📊 Final blog status:"
echo " Total blogs: $total_blogs"
echo " Numeric files: $numeric_blogs"
echo " Slug files: $slug_blogs"
echo ""
# Show some example URLs
echo "🌐 Example URLs now available:"
echo " /blog/$(ls "$BLOG_DIR"/[a-z]*.html 2>/dev/null | head -1 | xargs basename -s .html || echo 'jdeme-do-finale')"
echo " /blog/$(ls "$BLOG_DIR"/[a-z]*.html 2>/dev/null | head -2 | tail -1 | xargs basename -s .html || echo '1-zapas-final-score')"
echo ""
echo "🎉 Setup complete! Your blogs now support:"
echo " ✅ Clean URLs (slugs)"
echo " ✅ SEO meta tags"
echo " ✅ Backward compatibility"
echo " ✅ New admin features"
echo ""
echo "📝 Next steps:"
echo " 1. Restart your backend service"
echo " 2. Test new URLs in browser"
echo " 3. Try admin interface with new features"
+11 -11
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset <urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com --> <!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
<url> <url>
<loc>https://www.bizoniuh.cz/</loc> <loc>https://www.bizoniuh.cz/</loc>
@@ -166,7 +166,7 @@
<loc>https://www.bizoniuh.cz/zapasy/bizoni-hombres2.html</loc> <loc>https://www.bizoniuh.cz/zapasy/bizoni-hombres2.html</loc>
<lastmod>2024-11-13T09:24:05+00:00</lastmod> <lastmod>2024-11-13T09:24:05+00:00</lastmod>
<priority>0.64</priority> <priority>0.64</priority>
</url> </url>
</urlset> </urlset>
+166
View File
@@ -0,0 +1,166 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: ./migrate_slugs.go <site_root>")
fmt.Println("Example: ./migrate_slugs.go /home/tdvorak/Desktop/HTML_Projekty/bizoni")
os.Exit(1)
}
siteRoot := os.Args[1]
blogDir := filepath.Join(siteRoot, "blog")
// Read all blog files
entries, err := os.ReadDir(blogDir)
if err != nil {
log.Fatalf("Failed to read blog directory: %v", err)
}
// Pattern to match numeric blog files
numericPattern := regexp.MustCompile(`^(\d{4})\.html$`)
for _, entry := range entries {
if entry.IsDir() {
continue
}
filename := entry.Name()
matches := numericPattern.FindStringSubmatch(filename)
if len(matches) != 2 {
continue // Skip non-numeric files
}
blogPath := filepath.Join(blogDir, filename)
// Read the blog file
content, err := os.ReadFile(blogPath)
if err != nil {
log.Printf("Failed to read %s: %v", filename, err)
continue
}
contentStr := string(content)
// Check if slug already exists
slugPattern := regexp.MustCompile(`(?is)<meta name="slug" content="([^"]+)"`)
if slugPattern.MatchString(contentStr) {
log.Printf("Skipping %s - slug already exists", filename)
continue
}
// Extract title
titlePattern := regexp.MustCompile(`(?is)<h1[^>]*class="[^"]*\blte-header\b[^"]*"[^>]*>(.*?)</h1>`)
titleMatches := titlePattern.FindStringSubmatch(contentStr)
if len(titleMatches) < 2 {
log.Printf("Skipping %s - could not find title", filename)
continue
}
title := titleMatches[1]
// Remove any HTML tags from title
title = regexp.MustCompile(`(?is)<[^>]+>`).ReplaceAllString(title, "")
title = strings.TrimSpace(title)
// Generate slug
slug := generateSlug(title)
slug = ensureUniqueSlug(siteRoot, slug)
// Find where to insert the slug meta tag (before </head>)
headPattern := regexp.MustCompile(`(?is)</head>`)
if !headPattern.MatchString(contentStr) {
log.Printf("Skipping %s - could not find </head> tag", filename)
continue
}
// Insert slug meta tag
slugMeta := fmt.Sprintf(`<meta name="slug" content="%s">`, slug)
newContent := headPattern.ReplaceAllString(contentStr, slugMeta+"\n</head>")
// Write the updated content to both files
err = os.WriteFile(blogPath, []byte(newContent), 0644)
if err != nil {
log.Printf("Failed to write updated %s: %v", filename, err)
continue
}
// Create slug-based file
slugPath := filepath.Join(blogDir, slug+".html")
err = os.WriteFile(slugPath, []byte(newContent), 0644)
if err != nil {
log.Printf("Failed to create slug file %s: %v", slugPath, err)
continue
}
log.Printf("Processed %s: title='%s' -> slug='%s'", filename, title, slug)
}
log.Println("Migration completed!")
}
// generateSlug creates a URL-friendly slug from a title
func generateSlug(title string) string {
slug := strings.ToLower(title)
// Replace Czech characters with their ASCII equivalents
replacements := map[string]string{
"á": "a", "ä": "a", "č": "c", "ď": "d", "é": "e", "ě": "e", "í": "i", "ľ": "l",
"ň": "n", "ó": "o", "ö": "o", "ô": "o", "ř": "r", "š": "s", "ť": "t", "ú": "u",
"ů": "u", "ý": "y", "ž": "z",
"Á": "a", "Ä": "a", "Č": "c", "Ď": "d", "É": "e", "Ě": "e", "Í": "i", "Ľ": "l",
"Ň": "n", "Ó": "o", "Ö": "o", "Ô": "o", "Ř": "r", "Š": "s", "Ť": "t", "Ú": "u",
"Ů": "u", "Ý": "y", "Ž": "z",
}
for czech, ascii := range replacements {
slug = strings.ReplaceAll(slug, czech, ascii)
}
// Remove any character that isn't alphanumeric, space, or hyphen
re := regexp.MustCompile(`[^a-z0-9\s-]`)
slug = re.ReplaceAllString(slug, "")
// Replace spaces and multiple hyphens with a single hyphen
re = regexp.MustCompile(`[\s-]+`)
slug = re.ReplaceAllString(slug, "-")
// Remove leading and trailing hyphens
slug = strings.Trim(slug, "-")
return slug
}
// ensureUniqueSlug ensures the slug is unique by appending a number if needed
func ensureUniqueSlug(siteRoot, baseSlug string) string {
blogDir := filepath.Join(siteRoot, "blog")
entries, err := os.ReadDir(blogDir)
if err != nil {
return baseSlug
}
existingSlugs := make(map[string]bool)
for _, e := range entries {
if !strings.HasSuffix(e.Name(), ".html") {
continue
}
// Extract slug from filename if it follows the new pattern
name := strings.TrimSuffix(e.Name(), ".html")
// Check if it's a slug-based filename (contains letters, not just numbers)
if regexp.MustCompile(`[a-z]`).MatchString(name) {
existingSlugs[name] = true
}
}
if !existingSlugs[baseSlug] {
return baseSlug
}
// Try baseSlug-2, baseSlug-3, etc.
for i := 2; i < 100; i++ {
testSlug := fmt.Sprintf("%s-%d", baseSlug, i)
if !existingSlugs[testSlug] {
return testSlug
}
}
// Fallback to timestamp
return fmt.Sprintf("%s-%d", baseSlug, 1234567890)
}
+214
View File
@@ -0,0 +1,214 @@
#!/bin/bash
# Ubuntu Server Script: Remote Blog Management
# This script manages blogs on the remote server only
set -e
# Configuration - UPDATE THESE PATHS
SERVER_BLOG_DIR="/var/www/bizoni/blog" # Path to blogs on your server
BACKUP_DIR="/var/backups/bizoni-blogs" # Backup location
SITE_ROOT="/var/www/bizoni" # Site root on server
echo "🚀 Bizoni Remote Blog Management Script"
echo "====================================="
# Function to create backup
create_backup() {
echo "📦 Creating backup..."
mkdir -p "$BACKUP_DIR"
timestamp=$(date +%Y%m%d_%H%M%S)
tar -czf "$BACKUP_DIR/blogs_backup_$timestamp.tar.gz" -C "$SERVER_BLOG_DIR" .
echo "✅ Backup created: $BACKUP_DIR/blogs_backup_$timestamp.tar.gz"
}
# Function to list blogs
list_blogs() {
echo "📋 Current blogs on server:"
if [ -d "$SERVER_BLOG_DIR" ]; then
ls -la "$SERVER_BLOG_DIR"/*.html | while read line; do
filename=$(basename "$line")
if [[ "$filename" =~ ^[0-9]{4}\.html$ ]]; then
echo " 📄 $filename (numeric)"
elif [[ "$filename" =~ ^[a-z0-9-]+\.html$ ]]; then
echo " 🔗 $filename (slug)"
fi
done
else
echo " ❌ Blog directory not found: $SERVER_BLOG_DIR"
fi
}
# Function to add slug to existing blog
add_slug_to_blog() {
local blog_id="$1"
local blog_file="$SERVER_BLOG_DIR/$blog_id.html"
if [ ! -f "$blog_file" ]; then
echo "❌ Blog file not found: $blog_file"
return 1
fi
echo "🔧 Adding slug to blog: $blog_id"
# Extract title from blog file
title=$(grep -o '<h1[^>]*class="[^"]*lte-header[^"]*"[^>]*>.*</h1>' "$blog_file" | sed 's/<[^>]*>//g' | xargs)
if [ -z "$title" ]; then
echo "❌ Could not extract title from $blog_file"
return 1
fi
echo "📝 Title: $title"
# Generate slug
slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' | \
sed 's/á/a/g; s/ä/a/g; s/č/c/g; s/ď/d/g; s/é/e/g; s/ě/e/g; s/í/i/g; s/ľ/l/g; s/ň/n/g; s/ó/o/g; s/ö/o/g; s/ô/o/g; s/ř/r/g; s/š/s/g; s/ť/t/g; s/ú/u/g; s/ů/u/g; s/ý/y/g; s/ž/z/g' | \
sed 's/Á/a/g; s/Ä/a/g; s/Č/c/g; s/Ď/d/g; s/É/e/g; s/Ě/e/g; s/Í/i/g; s/Ľ/l/g; s/Ň/n/g; s/Ó/o/g; s/Ö/o/g; s/Ô/o/g; s/Ř/r/g; s/Š/s/g; s/Ť/t/g; s/Ú/u/g; s/Ů/u/g; s/Ý/y/g; s/Ž/z/g' | \
sed 's/[^a-z0-9\s-]//g' | \
sed 's/[\s-]\+/ -/g' | \
sed 's/^-\|-$//g')
# Check if slug file already exists
slug_file="$SERVER_BLOG_DIR/$slug.html"
if [ -f "$slug_file" ]; then
# Add number suffix
i=2
while [ -f "$SERVER_BLOG_DIR/${slug}-${i}.html" ]; do
((i++))
done
slug="${slug}-${i}"
slug_file="$SERVER_BLOG_DIR/${slug}.html"
fi
echo "🔗 Generated slug: $slug"
# Check if slug meta tag already exists
if grep -q '<meta name="slug"' "$blog_file"; then
echo "️ Slug meta tag already exists"
return 0
fi
# Add slug meta tag before </head>
sed -i "s|</head>|<meta name=\"slug\" content=\"$slug\">\n</head>|" "$blog_file"
# Create slug file (copy of original)
cp "$blog_file" "$slug_file"
echo "✅ Slug added and slug file created: $slug.html"
}
# Function to migrate all blogs to slugs
migrate_all_blogs() {
echo "🔄 Migrating all blogs to slugs..."
if [ ! -d "$SERVER_BLOG_DIR" ]; then
echo "❌ Blog directory not found: $SERVER_BLOG_DIR"
echo "Please update SERVER_BLOG_DIR in this script"
exit 1
fi
create_backup
# Process all numeric blog files
for blog_file in "$SERVER_BLOG_DIR"/*.html; do
if [ -f "$blog_file" ]; then
filename=$(basename "$blog_file")
if [[ "$filename" =~ ^([0-9]{4})\.html$ ]]; then
blog_id="${BASH_REMATCH[1]}"
add_slug_to_blog "$blog_id"
echo ""
fi
fi
done
echo "✅ Migration completed!"
}
# Function to show blog info
show_blog_info() {
local blog_id="$1"
local blog_file="$SERVER_BLOG_DIR/$blog_id.html"
if [ ! -f "$blog_file" ]; then
echo "❌ Blog file not found: $blog_file"
return 1
fi
echo "📄 Blog Info for: $blog_id"
echo "========================"
# Extract title
title=$(grep -o '<h1[^>]*class="[^"]*lte-header[^"]*"[^>]*>.*</h1>' "$blog_file" | sed 's/<[^>]*>//g' | xargs)
echo "📝 Title: $title"
# Extract slug
slug=$(grep -o '<meta name="slug" content="[^"]*"' "$blog_file" | sed 's/.*content="\([^"]*\)".*/\1/')
echo "🔗 Slug: $slug"
# Extract categories
categories=$(grep -o '<meta name="category" content="[^"]*"' "$blog_file" | sed 's/.*content="\([^"]*\)".*/\1/' | tr '\n' ', ')
echo "🏷️ Categories: $categories"
# File size
size=$(du -h "$blog_file" | cut -f1)
echo "📊 Size: $size"
# Check if slug file exists
if [ -n "$slug" ] && [ -f "$SERVER_BLOG_DIR/$slug.html" ]; then
echo "✅ Slug file exists: $slug.html"
else
echo "❌ Slug file missing"
fi
}
# Main menu
case "${1:-}" in
"list")
list_blogs
;;
"migrate")
migrate_all_blogs
;;
"info")
if [ -z "${2:-}" ]; then
echo "Usage: $0 info <blog_id>"
echo "Example: $0 info 0030"
exit 1
fi
show_blog_info "$2"
;;
"add-slug")
if [ -z "${2:-}" ]; then
echo "Usage: $0 add-slug <blog_id>"
echo "Example: $0 add-slug 0030"
exit 1
fi
add_slug_to_blog "$2"
;;
"backup")
create_backup
;;
*)
echo "Bizoni Remote Blog Management"
echo "============================"
echo ""
echo "Usage: $0 <command> [options]"
echo ""
echo "Commands:"
echo " list - List all blogs on server"
echo " migrate - Migrate all blogs to use slugs"
echo " info <blog_id> - Show info about specific blog"
echo " add-slug <blog_id> - Add slug to specific blog"
echo " backup - Create backup of all blogs"
echo ""
echo "Examples:"
echo " $0 list"
echo " $0 migrate"
echo " $0 info 0030"
echo " $0 add-slug 0030"
echo ""
echo "⚠️ Make sure to update SERVER_BLOG_DIR path in this script!"
exit 1
;;
esac
+369 -369
View File
@@ -1,370 +1,370 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en-US"> <html lang="en-US">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>Bizoni UH - Zápasy</title> <title>Bizoni UH - Zápasy</title>
<link rel="icon" type="image/x-icon" href="../img/logo.png"> <link rel="icon" type="image/x-icon" href="../img/logo.png">
<!-- Stylesheets --> <!-- Stylesheets -->
<link rel="stylesheet" id="swiper-css" href="../css/swiper.css" type="text/css" media="all" /> <link rel="stylesheet" id="swiper-css" href="../css/swiper.css" type="text/css" media="all" />
<link rel="stylesheet" id="bootstrap-css" href="../css/bootstrap.css" type="text/css" media="all" /> <link rel="stylesheet" id="bootstrap-css" href="../css/bootstrap.css" type="text/css" media="all" />
<link rel="stylesheet" id="atleticos-theme-style-css" href="../css/bizoni.css" type="text/css" media="all" /> <link rel="stylesheet" id="atleticos-theme-style-css" href="../css/bizoni.css" type="text/css" media="all" />
<link rel="stylesheet" id="elementor-icons-css" href="../css/elementor-icons.min.css" type="text/css" media="all" /> <link rel="stylesheet" id="elementor-icons-css" href="../css/elementor-icons.min.css" type="text/css" media="all" />
<link rel="stylesheet" id="elementor-frontend-css" href="../css/custom-frontend.min.css" type="text/css" media="all" /> <link rel="stylesheet" id="elementor-frontend-css" href="../css/custom-frontend.min.css" type="text/css" media="all" />
<link rel="stylesheet" id="elementor-post-13200-css" href="../css/post-13200.css" type="text/css" media="all" /> <link rel="stylesheet" id="elementor-post-13200-css" href="../css/post-13200.css" type="text/css" media="all" />
<!-- External Stylesheets --> <!-- External Stylesheets -->
<link rel="stylesheet" id="elementor-post-32647-css" href="../css/post-32647.css" type="text/css" media="all" /> <link rel="stylesheet" id="elementor-post-32647-css" href="../css/post-32647.css" type="text/css" media="all" />
<link rel="stylesheet" id="event-tickets-rsvp-css" href="../css/rsvp.min.css" type="text/css" media="all" /> <link rel="stylesheet" id="event-tickets-rsvp-css" href="../css/rsvp.min.css" type="text/css" media="all" />
<link rel="stylesheet" id="magnific-popup-css" href="../css/magnific-popup.css" type="text/css" media="all" /> <link rel="stylesheet" id="magnific-popup-css" href="../css/magnific-popup.css" type="text/css" media="all" />
<script type="text/javascript" src="../js/jquery.nicescroll.js" id="nicescroll-js"></script> <script type="text/javascript" src="../js/jquery.nicescroll.js" id="nicescroll-js"></script>
<link rel="stylesheet" id="atleticos-google-fonts-css" href="//fonts.googleapis.com/css?family=Open+Sans:400,400i,600,700%7CSofia+Sans+Extra+Condensed:800,300i" type="text/css" media="all" /> <link rel="stylesheet" id="atleticos-google-fonts-css" href="//fonts.googleapis.com/css?family=Open+Sans:400,400i,600,700%7CSofia+Sans+Extra+Condensed:800,300i" type="text/css" media="all" />
<link rel="stylesheet" id="font-awesome-shims-css" href="../css/v4-shims.min.css" type="text/css" media="all" /> <link rel="stylesheet" id="font-awesome-shims-css" href="../css/v4-shims.min.css" type="text/css" media="all" />
<link rel="stylesheet" id="lte-font-css" href="../css/lte-font-codes.css" type="text/css" media="all" /> <link rel="stylesheet" id="lte-font-css" href="../css/lte-font-codes.css" type="text/css" media="all" />
<link rel="stylesheet" id="google-fonts-1-css" href="https://fonts.googleapis.com/css?family=Open+Sans%3A100%2C100italic%2C200%2C200italic%2C300%2C300italic%2C400%2C400italic%2C500%2C500italic%2C600%2C600italic%2C700%2C700italic%2C800%2C800italic%2C900%2C900italic%7CMarcellus%7CTangerine&#038;display=auto&#038;ver=6.4.5" type="text/css" media="all" /> <link rel="stylesheet" id="google-fonts-1-css" href="https://fonts.googleapis.com/css?family=Open+Sans%3A100%2C100italic%2C200%2C200italic%2C300%2C300italic%2C400%2C400italic%2C500%2C500italic%2C600%2C600italic%2C700%2C700italic%2C800%2C800italic%2C900%2C900italic%7CMarcellus%7CTangerine&#038;display=auto&#038;ver=6.4.5" type="text/css" media="all" />
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Scripts --> <!-- Scripts -->
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script> <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script> <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<script type="text/javascript" src="../js/jquery.min.js" id="jquery-core-js"></script> <script type="text/javascript" src="../js/jquery.min.js" id="jquery-core-js"></script>
<script type="text/javascript" src="../js/jquery-migrate.min.js" id="jquery-migrate-js"></script> <script type="text/javascript" src="../js/jquery-migrate.min.js" id="jquery-migrate-js"></script>
<script type="text/javascript" src="../js/jquery.blockUI.min.js" id="jquery-blockui-js" defer="defer"></script> <script type="text/javascript" src="../js/jquery.blockUI.min.js" id="jquery-blockui-js" defer="defer"></script>
<script type="text/javascript" src="../js/jquery.paroller.js" id="jquery-paroller-js"></script> <script type="text/javascript" src="../js/jquery.paroller.js" id="jquery-paroller-js"></script>
<script type="text/javascript" src="../js/modernizr-2.6.2.min.js" id="modernizr-js"></script> <script type="text/javascript" src="../js/modernizr-2.6.2.min.js" id="modernizr-js"></script>
<script type="text/javascript" src="../js/script.js"></script> <script type="text/javascript" src="../js/script.js"></script>
</head> </head>
<body class="home page-template page-template-page-templates page-template-full-width page page-id-32647 theme-atleticos woocommerce-no-js tribe-no-js tec-no-tickets-on-recurring tec-no-rsvp-on-recurring full-width lte-fw-loaded lte-color-scheme-default lte-body-white lte-background-white paceloader-disabled no-sidebar elementor-default elementor-kit-13200 elementor-page elementor-page-32647 tribe-theme-atleticos"> <body class="home page-template page-template-page-templates page-template-full-width page page-id-32647 theme-atleticos woocommerce-no-js tribe-no-js tec-no-tickets-on-recurring tec-no-rsvp-on-recurring full-width lte-fw-loaded lte-color-scheme-default lte-body-white lte-background-white paceloader-disabled no-sidebar elementor-default elementor-kit-13200 elementor-page elementor-page-32647 tribe-theme-atleticos">
<div class="lte-content-wrapper lte-layout-transparent-full"> <div class="lte-content-wrapper lte-layout-transparent-full">
<div class="lte-header-wrapper header-h1 header-parallax lte-header-overlay lte-layout-transparent-full lte-pageheader-disabled"> <div class="lte-header-wrapper header-h1 header-parallax lte-header-overlay lte-layout-transparent-full lte-pageheader-disabled">
<div id="lte-nav-wrapper" class="lte-layout-transparent-full lte-nav-color-white"> <div id="lte-nav-wrapper" class="lte-layout-transparent-full lte-nav-color-white">
<nav class="lte-navbar affix" data-spy="affix" data-offset-top="0"> <nav class="lte-navbar affix" data-spy="affix" data-offset-top="0">
<div class="container"> <div class="container">
<!-- Logo --> <!-- Logo -->
<div class="lte-navbar-logo"> <div class="lte-navbar-logo">
<a class="lte-logo" href="../index.html"> <a class="lte-logo" href="../index.html">
<img src="../img/logo.png"> <img src="../img/logo.png">
</a> </a>
</div> </div>
<!-- Navigation Items --> <!-- Navigation Items -->
<div class="lte-navbar-items navbar-mobile-black navbar-collapse collapse" id="navbar" data-mobile-screen-width="1198"> <div class="lte-navbar-items navbar-mobile-black navbar-collapse collapse" id="navbar" data-mobile-screen-width="1198">
<div class="toggle-wrap"> <div class="toggle-wrap">
<a class="lte-logo" href="../index.html"> <a class="lte-logo" href="../index.html">
<img src="../img/logo.png"> <img src="../img/logo.png">
</a> </a>
<button type="button" class="lte-navbar-toggle collapsed" id="close-button"> <button type="button" class="lte-navbar-toggle collapsed" id="close-button">
<span class="close">&times;</span> <span class="close">&times;</span>
</button> </button>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<!-- Navigation Menu --> <!-- Navigation Menu -->
<ul id="menu-main-menu" class="lte-ul-nav"> <ul id="menu-main-menu" class="lte-ul-nav">
<li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent"> <li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent">
<a href="../index.html"> <a href="../index.html">
<span>Domů</span> <span>Domů</span>
</a> </a>
</li> </li>
<li id="menu-item-29540" class="menu-item menu-item-type-post_type menu-item-object-page"> <li id="menu-item-29540" class="menu-item menu-item-type-post_type menu-item-object-page">
<a href="../o-nas.html"> <a href="../o-nas.html">
<span>O nás</span> <span>O nás</span>
</a> </a>
</li> </li>
<li id="menu-item-59" class="menu-item menu-item-type-custom"> <li id="menu-item-59" class="menu-item menu-item-type-custom">
<a href="../blog.html"> <a href="../blog.html">
<span>Blog</span> <span>Blog</span>
</a> </a>
</li> </li>
<li id="menu-item-13613" class="menu-item menu-item-type-post_type menu-item-object-page"> <li id="menu-item-13613" class="menu-item menu-item-type-post_type menu-item-object-page">
<a href="../kontakt.html"> <a href="../kontakt.html">
<span>Kontakt</span> <span>Kontakt</span>
</a> </a>
</li> </li>
<li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent"> <li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent">
<a href="../index.html#tym"> <a href="../index.html#tym">
<span>Tým</span> <span>Tým</span>
</a> </a>
</li> </li>
<li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent"> <li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent">
<a href="../index.html#sponzori"> <a href="../index.html#sponzori">
<span>Sponzoři</span> <span>Sponzoři</span>
</a> </a>
</li> </li>
<li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent"> <li id="menu-item-20758" class="menu-item menu-item-type-custom current-menu-ancestor current-menu-parent">
<a target="_blank" href="https://eu.zonerama.com/Fcbizoni/1419417"> <a target="_blank" href="https://eu.zonerama.com/Fcbizoni/1419417">
<span>Fotogalerie</span> <span>Fotogalerie</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Mobile Menu Toggle --> <!-- Mobile Menu Toggle -->
<button type="button" class="lte-navbar-toggle" id="open-button"> <button type="button" class="lte-navbar-toggle" id="open-button">
<span class="icon-bar top-bar"></span> <span class="icon-bar top-bar"></span>
<span class="icon-bar middle-bar"></span> <span class="icon-bar middle-bar"></span>
<span class="icon-bar bottom-bar"></span> <span class="icon-bar bottom-bar"></span>
</button> </button>
</div> </div>
</nav> </nav>
</div> </div>
<header class="lte-page-header lte-parallax-yes"> <header class="lte-page-header lte-parallax-yes">
<div class="container"> <div class="container">
<div class="lte-header-h1-wrapper"> <div class="lte-header-h1-wrapper">
<h1 class="lte-header">Všechny zápasy</h1> <h1 class="lte-header">Všechny zápasy</h1>
</div> </div>
</div> </div>
</header> </header>
</div> </div>
<div class="container main-wrapper"> <div class="container main-wrapper">
<div class="inner-page margin-default"> <div class="inner-page margin-default">
<div id="facr-all-matches" class="lte-football-matches inner-page"></div> <div id="facr-all-matches" class="lte-football-matches inner-page"></div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="lte-footer-wrapper lte-footer-layout-default"> <div class="lte-footer-wrapper lte-footer-layout-default">
<div class="footer-wrapper"> <div class="footer-wrapper">
<div class="lte-container"> <div class="lte-container">
<div class="footer-block lte-footer-widget-area"> <div class="footer-block lte-footer-widget-area">
<div data-elementor-type="wp-post" data-elementor-id="29393" class="elementor elementor-29393"> <div data-elementor-type="wp-post" data-elementor-id="29393" class="elementor elementor-29393">
<div class="elementor-element elementor-element-a939976 lte-background-black e-flex e-con-boxed e-con e-parent" data-id="a939976" data-element_type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}" data-core-v316-plus="true"> <div class="elementor-element elementor-element-a939976 lte-background-black e-flex e-con-boxed e-con e-parent" data-id="a939976" data-element_type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}" data-core-v316-plus="true">
<div class="e-con-inner" style="padding-bottom: 92px;"> <div class="e-con-inner" style="padding-bottom: 92px;">
<div class="elementor-element elementor-element-f2b730e e-con-full e-flex e-con e-child" data-id="f2b730e" data-element_type="container"> <div class="elementor-element elementor-element-f2b730e e-con-full e-flex e-con e-child" data-id="f2b730e" data-element_type="container">
<div class="elementor-element elementor-element-81a7a24 elementor-widget__width-initial elementor-widget elementor-widget-shortcode" data-id="81a7a24" data-element_type="widget" data-widget_type="shortcode.default"> <div class="elementor-element elementor-element-81a7a24 elementor-widget__width-initial elementor-widget elementor-widget-shortcode" data-id="81a7a24" data-element_type="widget" data-widget_type="shortcode.default">
<div class="elementor-widget-container"> <div class="elementor-widget-container">
<div class="elementor-shortcode"> <div class="elementor-shortcode">
<a class="lte-logo" href="../index.html"> <a class="lte-logo" href="../index.html">
<img src="../img/logo.png" style="filter: drop-shadow(9px -1px 23px black);"> <img src="../img/logo.png" style="filter: drop-shadow(9px -1px 23px black);">
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<div class="elementor-element elementor-element-86345d3 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="86345d3" data-element_type="widget" data-widget_type="text-editor.default"> <div class="elementor-element elementor-element-86345d3 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="86345d3" data-element_type="widget" data-widget_type="text-editor.default">
<div class="elementor-widget-container"> <div class="elementor-widget-container">
<p> <p>
<span class="text-sm"> <span class="text-sm">
<a href="https://maps.app.goo.gl/kEc9CJuXTxqNUhgj8" target="_blank">Stonky 559, 686 01 Uherské Hradiště 1</a> <a href="https://maps.app.goo.gl/kEc9CJuXTxqNUhgj8" target="_blank">Stonky 559, 686 01 Uherské Hradiště 1</a>
<br>fcbizoni@gmail.com </span> <br>fcbizoni@gmail.com </span>
</p> </p>
</div> </div>
</div> </div>
<div class="elementor-element elementor-element-475baf0 elementor-widget elementor-widget-lte-elements" data-id="475baf0" data-element_type="widget" data-widget_type="lte-elements.default"> <div class="elementor-element elementor-element-475baf0 elementor-widget elementor-widget-lte-elements" data-id="475baf0" data-element_type="widget" data-widget_type="lte-elements.default">
<div class="elementor-widget-container"> <div class="elementor-widget-container">
<div class="lte-social lte-nav-second lte-type-"> <div class="lte-social lte-nav-second lte-type-">
<ul> <ul>
<li> <li>
<a href="https://www.facebook.com/bizoniuh" target="_blank"> <a href="https://www.facebook.com/bizoniuh" target="_blank">
<ion-icon name="logo-facebook" style="height: 22px; width: 22px;"></ion-icon> <ion-icon name="logo-facebook" style="height: 22px; width: 22px;"></ion-icon>
</a> </a>
</li> </li>
<li> <li>
<a href="https://www.instagram.com/fcbizoni_uh/" target="_blank"> <a href="https://www.instagram.com/fcbizoni_uh/" target="_blank">
<ion-icon name="logo-instagram" style="height: 22px; width: 22px;"></ion-icon> <ion-icon name="logo-instagram" style="height: 22px; width: 22px;"></ion-icon>
</a> </a>
</li> </li>
<li> <li>
<a href="https://www.youtube.com/@FCBizoniUH" target="_blank"> <a href="https://www.youtube.com/@FCBizoniUH" target="_blank">
<ion-icon name="logo-youtube" style="height: 22px; width: 22px;"></ion-icon> <ion-icon name="logo-youtube" style="height: 22px; width: 22px;"></ion-icon>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<footer class="copyright-block copyright-layout-copyright-transparent"> <footer class="copyright-block copyright-layout-copyright-transparent">
<div class="container"> <div class="container">
<p> <p>
<a href="https://tdvorak.dev" target="_blank">TDvorak</a> © Všechna práva vyhrazena - 2024 <a href="https://tdvorak.dev" target="_blank">TDvorak</a> © Všechna práva vyhrazena - 2024
</p> </p>
</div> </div>
</footer> </footer>
</div> </div>
<a href="#" class="lte-go-top floating lte-go-top-icon"> <a href="#" class="lte-go-top floating lte-go-top-icon">
<span class="go-top-icon-v2 icon"> <span class="go-top-icon-v2 icon">
<ion-icon name="football-outline" style="padding-right: 2px;"></ion-icon> <ion-icon name="football-outline" style="padding-right: 2px;"></ion-icon>
</span> </span>
<span class="go-top-header">Nahoru</span> <span class="go-top-header">Nahoru</span>
</a> </a>
<script src="../js/facr-frontend.js"></script> <script src="../js/facr-frontend.js"></script>
<link rel='stylesheet' id='elementor-post-36123-css' href='../css/post-36123.css' type='text/css' media='all' /> <link rel='stylesheet' id='elementor-post-36123-css' href='../css/post-36123.css' type='text/css' media='all' />
<link rel='stylesheet' id='elementor-post-36124-css' href='../css/post-36124.css' type='text/css' media='all' /> <link rel='stylesheet' id='elementor-post-36124-css' href='../css/post-36124.css' type='text/css' media='all' />
<link rel='stylesheet' id='elementor-post-35532-css' href='../css/post-35532.css' type='text/css' media='all' /> <link rel='stylesheet' id='elementor-post-35532-css' href='../css/post-35532.css' type='text/css' media='all' />
<link rel='stylesheet' id='elementor-post-36129-css' href='../css/post-36129.css' type='text/css' media='all' /> <link rel='stylesheet' id='elementor-post-36129-css' href='../css/post-36129.css' type='text/css' media='all' />
<link rel='stylesheet' id='elementor-post-36131-css' href='../css/post-36131.css' type='text/css' media='all' /> <link rel='stylesheet' id='elementor-post-36131-css' href='../css/post-36131.css' type='text/css' media='all' />
<link rel='stylesheet' id='lte-zoomslider-css' href='../css/zoom-slider.css' type='text/css' media='all' /> <link rel='stylesheet' id='lte-zoomslider-css' href='../css/zoom-slider.css' type='text/css' media='all' />
<link rel='stylesheet' id='elementor-post-20251-css' href='../css/post-20251.css' type='text/css' media='all' /> <link rel='stylesheet' id='elementor-post-20251-css' href='../css/post-20251.css' type='text/css' media='all' />
<link rel='stylesheet' id='elementor-post-29393-css' href='../css/post-29393.css' type='text/css' media='all' /> <link rel='stylesheet' id='elementor-post-29393-css' href='../css/post-29393.css' type='text/css' media='all' />
<script type="text/javascript" src="../js/parallax-js.js" id="parallax-js-js"></script> <script type="text/javascript" src="../js/parallax-js.js" id="parallax-js-js"></script>
<script type="text/javascript" src="../js/scripts.js" id="atleticos-scripts-js"></script> <script type="text/javascript" src="../js/scripts.js" id="atleticos-scripts-js"></script>
<script type="text/javascript" src="../js/swiper.min.js" id="swiper-js"></script> <script type="text/javascript" src="../js/swiper.min.js" id="swiper-js"></script>
<script type="text/javascript" src="../js/frontend.js" id="lte-frontend-js"></script> <script type="text/javascript" src="../js/frontend.js" id="lte-frontend-js"></script>
<script type="text/javascript" src="../js/jquery.zoomslider.js" id="lte-zoomslider-js"></script> <script type="text/javascript" src="../js/jquery.zoomslider.js" id="lte-zoomslider-js"></script>
<script type="text/javascript" src="../js/webpack.runtime.min.js" id="elementor-webpack-runtime-js"></script> <script type="text/javascript" src="../js/webpack.runtime.min.js" id="elementor-webpack-runtime-js"></script>
<script type="text/javascript" src="../js/frontend-modules.min.js" id="elementor-frontend-modules-js"></script> <script type="text/javascript" src="../js/frontend-modules.min.js" id="elementor-frontend-modules-js"></script>
<script type="text/javascript" src="../js/waypoints.min.js" id="elementor-waypoints-js"></script> <script type="text/javascript" src="../js/waypoints.min.js" id="elementor-waypoints-js"></script>
<script type="text/javascript" src="../js/core.min.js" id="jquery-ui-core-js"></script> <script type="text/javascript" src="../js/core.min.js" id="jquery-ui-core-js"></script>
<script type="text/javascript" id="elementor-frontend-js-before"> <script type="text/javascript" id="elementor-frontend-js-before">
/* /*
<![CDATA[ */ <![CDATA[ */
var elementorFrontendConfig = { var elementorFrontendConfig = {
"environmentMode": { "environmentMode": {
"edit": false, "edit": false,
"wpPreview": false, "wpPreview": false,
"isScriptDebug": false "isScriptDebug": false
}, },
"i18n": { "i18n": {
"shareOnFacebook": "Share on Facebook", "shareOnFacebook": "Share on Facebook",
"shareOnTwitter": "Share on Twitter", "shareOnTwitter": "Share on Twitter",
"pinIt": "Pin it", "pinIt": "Pin it",
"download": "Download", "download": "Download",
"downloadImage": "Download image", "downloadImage": "Download image",
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"zoom": "Zoom", "zoom": "Zoom",
"share": "Share", "share": "Share",
"playVideo": "Play Video", "playVideo": "Play Video",
"previous": "Previous", "previous": "Previous",
"next": "Next", "next": "Next",
"close": "Close", "close": "Close",
"a11yCarouselWrapperAriaLabel": "Carousel | Horizontal scrolling: Arrow Left & Right", "a11yCarouselWrapperAriaLabel": "Carousel | Horizontal scrolling: Arrow Left & Right",
"a11yCarouselPrevSlideMessage": "Previous slide", "a11yCarouselPrevSlideMessage": "Previous slide",
"a11yCarouselNextSlideMessage": "Next slide", "a11yCarouselNextSlideMessage": "Next slide",
"a11yCarouselFirstSlideMessage": "This is the first slide", "a11yCarouselFirstSlideMessage": "This is the first slide",
"a11yCarouselLastSlideMessage": "This is the last slide", "a11yCarouselLastSlideMessage": "This is the last slide",
"a11yCarouselPaginationBulletMessage": "Go to slide" "a11yCarouselPaginationBulletMessage": "Go to slide"
}, },
"is_rtl": false, "is_rtl": false,
"breakpoints": { "breakpoints": {
"xs": 0, "xs": 0,
"sm": 480, "sm": 480,
"md": 768, "md": 768,
"lg": 1200, "lg": 1200,
"xl": 1440, "xl": 1440,
"xxl": 1600 "xxl": 1600
}, },
"responsive": { "responsive": {
"breakpoints": { "breakpoints": {
"mobile": { "mobile": {
"label": "Mobile Portrait", "label": "Mobile Portrait",
"value": 767, "value": 767,
"default_value": 767, "default_value": 767,
"direction": "max", "direction": "max",
"is_enabled": true "is_enabled": true
}, },
"mobile_extra": { "mobile_extra": {
"label": "Mobile Landscape", "label": "Mobile Landscape",
"value": 991, "value": 991,
"default_value": 880, "default_value": 880,
"direction": "max", "direction": "max",
"is_enabled": true "is_enabled": true
}, },
"tablet": { "tablet": {
"label": "Tablet Portrait", "label": "Tablet Portrait",
"value": 1199, "value": 1199,
"default_value": 1024, "default_value": 1024,
"direction": "max", "direction": "max",
"is_enabled": true "is_enabled": true
}, },
"tablet_extra": { "tablet_extra": {
"label": "Tablet Landscape", "label": "Tablet Landscape",
"value": 1366, "value": 1366,
"default_value": 1200, "default_value": 1200,
"direction": "max", "direction": "max",
"is_enabled": true "is_enabled": true
}, },
"laptop": { "laptop": {
"label": "Laptop", "label": "Laptop",
"value": 1599, "value": 1599,
"default_value": 1366, "default_value": 1366,
"direction": "max", "direction": "max",
"is_enabled": true "is_enabled": true
}, },
"widescreen": { "widescreen": {
"label": "Widescreen", "label": "Widescreen",
"value": 1900, "value": 1900,
"default_value": 2400, "default_value": 2400,
"direction": "min", "direction": "min",
"is_enabled": true "is_enabled": true
} }
} }
}, },
"version": "3.20.1", "version": "3.20.1",
"is_static": false, "is_static": false,
"experimentalFeatures": { "experimentalFeatures": {
"e_optimized_assets_loading": true, "e_optimized_assets_loading": true,
"additional_custom_breakpoints": true, "additional_custom_breakpoints": true,
"container": true, "container": true,
"e_swiper_latest": true, "e_swiper_latest": true,
"block_editor_assets_optimize": true, "block_editor_assets_optimize": true,
"ai-layout": true, "ai-layout": true,
"landing-pages": true, "landing-pages": true,
"nested-elements": true, "nested-elements": true,
"e_image_loading_optimization": true "e_image_loading_optimization": true
}, },
"urls": { "urls": {
"assets": ".../js/text-editor.2c35aafbe5bf0e127950.bundle.min.js" "assets": ".../js/text-editor.2c35aafbe5bf0e127950.bundle.min.js"
}, },
"swiperClass": "swiper", "swiperClass": "swiper",
"settings": { "settings": {
"page": [], "page": [],
"editorPreferences": [] "editorPreferences": []
}, },
"kit": { "kit": {
"viewport_tablet": 1199, "viewport_tablet": 1199,
"viewport_mobile": 767, "viewport_mobile": 767,
"active_breakpoints": ["viewport_mobile", "viewport_mobile_extra", "viewport_tablet", "viewport_tablet_extra", "viewport_laptop", "viewport_widescreen"], "active_breakpoints": ["viewport_mobile", "viewport_mobile_extra", "viewport_tablet", "viewport_tablet_extra", "viewport_laptop", "viewport_widescreen"],
"viewport_mobile_extra": 991, "viewport_mobile_extra": 991,
"viewport_laptop": 1599, "viewport_laptop": 1599,
"viewport_widescreen": 1900, "viewport_widescreen": 1900,
"viewport_tablet_extra": 1366, "viewport_tablet_extra": 1366,
"lightbox_enable_counter": "yes", "lightbox_enable_counter": "yes",
"lightbox_enable_fullscreen": "yes", "lightbox_enable_fullscreen": "yes",
"lightbox_enable_zoom": "yes", "lightbox_enable_zoom": "yes",
"lightbox_enable_share": "yes", "lightbox_enable_share": "yes",
"lightbox_title_src": "title", "lightbox_title_src": "title",
"lightbox_description_src": "description" "lightbox_description_src": "description"
}, },
"post": { "post": {
"id": 32647, "id": 32647,
"title": "", "title": "",
"excerpt": "", "excerpt": "",
"featuredImage": false "featuredImage": false
} }
}; };
/* ]]> */ /* ]]> */
</script> </script>
<script type="text/javascript" src="../js/frontend.min.js" id="elementor-frontend-js"></script> <script type="text/javascript" src="../js/frontend.min.js" id="elementor-frontend-js"></script>
<script> <script>
// Ensure the DOM is fully loaded before adding event listeners // Ensure the DOM is fully loaded before adding event listeners
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Get the buttons and the navbar element // Get the buttons and the navbar element
const openButton = document.getElementById('open-button'); const openButton = document.getElementById('open-button');
const closeButton = document.getElementById('close-button'); const closeButton = document.getElementById('close-button');
const navbar = document.getElementById('navbar'); const navbar = document.getElementById('navbar');
// Log to check if elements exist // Log to check if elements exist
console.log('Open button:', openButton); console.log('Open button:', openButton);
console.log('Close button:', closeButton); console.log('Close button:', closeButton);
console.log('Navbar:', navbar); console.log('Navbar:', navbar);
// Ensure that buttons and navbar exist // Ensure that buttons and navbar exist
if (openButton && closeButton && navbar) { if (openButton && closeButton && navbar) {
console.log('Elements found and event listeners ready.'); console.log('Elements found and event listeners ready.');
// Add event listener to the open button // Add event listener to the open button
openButton.addEventListener('click', function() { openButton.addEventListener('click', function() {
console.log('Open button clicked'); console.log('Open button clicked');
}); });
// Add event listener to the close button // Add event listener to the close button
closeButton.addEventListener('click', function() { closeButton.addEventListener('click', function() {
console.log('Close button clicked'); console.log('Close button clicked');
}); });
} else { } else {
console.error('Error: Buttons or navbar element not found.'); console.error('Error: Buttons or navbar element not found.');
} }
}); });
</script> </script>
</body> </body>
</html> </html>