rewritten UI
This commit is contained in:
parent
328e2b9916
commit
fb3fdb275e
@ -18,19 +18,36 @@ public class HttpServerApp {
|
|||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
TransmissionService service = new TransmissionService();
|
TransmissionService service = new TransmissionService();
|
||||||
service.loadNextDays(10);
|
|
||||||
|
|
||||||
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
// /transmissions -> all data
|
// /transmissions -> all data nebo jen pro konkrétní datum
|
||||||
server.createContext("/transmissions", exchange -> {
|
server.createContext("/transmissions", exchange -> {
|
||||||
try {
|
try {
|
||||||
setCors(exchange);
|
setCors(exchange);
|
||||||
if (!checkApiKey(exchange)) return;
|
if (!checkApiKey(exchange)) return;
|
||||||
|
|
||||||
List<Transmission> all = service.getAll();
|
URI uri = exchange.getRequestURI();
|
||||||
respondJson(exchange, mapper.writeValueAsString(all));
|
String query = uri.getQuery();
|
||||||
|
String date = null;
|
||||||
|
|
||||||
|
if (query != null) {
|
||||||
|
for (String part : query.split("&")) {
|
||||||
|
if (part.startsWith("date=")) {
|
||||||
|
date = java.net.URLDecoder.decode(part.substring(5), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Transmission> result;
|
||||||
|
if (date != null && !date.isBlank()) {
|
||||||
|
result = service.getByDateLazy(date);
|
||||||
|
} else {
|
||||||
|
result = service.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJson(exchange, mapper.writeValueAsString(result));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
sendError(exchange, 500, e.getMessage());
|
sendError(exchange, 500, e.getMessage());
|
||||||
@ -68,8 +85,28 @@ public class HttpServerApp {
|
|||||||
if (!checkApiKey(exchange)) return;
|
if (!checkApiKey(exchange)) return;
|
||||||
|
|
||||||
if ("GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
if ("GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
service.reloadDataAsync();
|
// přečti volitelné datum z query stringu
|
||||||
respondJson(exchange, "{\"status\":\"ok\",\"message\":\"Data se obnovují na pozadí.\"}");
|
String dateParam = null;
|
||||||
|
String query = exchange.getRequestURI().getQuery();
|
||||||
|
if (query != null) {
|
||||||
|
for (String part : query.split("&")) {
|
||||||
|
if (part.startsWith("date=")) {
|
||||||
|
dateParam = java.net.URLDecoder.decode(part.substring(5), StandardCharsets.UTF_8);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pokud není zadané, použij dnešní den
|
||||||
|
String dateStr = (dateParam != null && !dateParam.isBlank())
|
||||||
|
? dateParam
|
||||||
|
: java.time.LocalDate.now().toString();
|
||||||
|
|
||||||
|
// načti data pro konkrétní den
|
||||||
|
System.out.println("🔄 Obnovuji data pro den: " + dateStr);
|
||||||
|
service.reloadDayAsync(dateStr);
|
||||||
|
|
||||||
|
respondJson(exchange, "{\"status\":\"ok\",\"message\":\"Data pro den " + dateStr + " se obnovují.\"}");
|
||||||
} else {
|
} else {
|
||||||
exchange.sendResponseHeaders(405, -1);
|
exchange.sendResponseHeaders(405, -1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
package cz.kamma.tvcom;
|
|
||||||
|
|
||||||
import java.sql.*;
|
|
||||||
|
|
||||||
public class Searcher {
|
|
||||||
private static final String URL = "jdbc:mysql://server01:3306/tvcom?useSSL=false&characterEncoding=UTF-8";
|
|
||||||
private static final String USER = "tvcom";
|
|
||||||
private static final String PASS = "Passw0rd";
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
String search = args.length > 0 ? args[0] : "Nymburk";
|
|
||||||
|
|
||||||
try (Connection conn = DriverManager.getConnection(URL, USER, PASS)) {
|
|
||||||
String sql = "SELECT * FROM transmissions WHERE MATCH(title, sport, league) AGAINST (? IN NATURAL LANGUAGE MODE)";
|
|
||||||
PreparedStatement ps = conn.prepareStatement(sql);
|
|
||||||
ps.setString(1, search);
|
|
||||||
ResultSet rs = ps.executeQuery();
|
|
||||||
|
|
||||||
while (rs.next()) {
|
|
||||||
System.out.printf("%s | %s | %s | %s | %s%n",
|
|
||||||
rs.getString("date"),
|
|
||||||
rs.getString("time"),
|
|
||||||
rs.getString("title"),
|
|
||||||
rs.getString("sport"),
|
|
||||||
rs.getString("league"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +1,45 @@
|
|||||||
package cz.kamma.tvcom;
|
package cz.kamma.tvcom;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.nodes.Document;
|
import org.jsoup.nodes.Document;
|
||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class TransmissionService {
|
public class TransmissionService {
|
||||||
private final List<Transmission> transmissions = Collections.synchronizedList(new ArrayList<>());
|
private final List<Transmission> transmissions = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
public void loadNextDays(int daysToLoad) throws InterruptedException {
|
public List<Transmission> getByDateLazy(String dateStr) {
|
||||||
LocalDate today = LocalDate.now();
|
if (dateStr == null || dateStr.isBlank()) return Collections.emptyList();
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
synchronized (transmissions) {
|
||||||
ExecutorService executor = Executors.newFixedThreadPool(5);
|
boolean exists = transmissions.stream().anyMatch(t -> dateStr.equals(t.date));
|
||||||
|
if (!exists) {
|
||||||
for (int i = 0; i < daysToLoad; i++) {
|
System.out.println("📅 Data pro " + dateStr + " nejsou v paměti – načítám...");
|
||||||
LocalDate targetDate = today.plusDays(i);
|
loadDay(dateStr);
|
||||||
String dateStr = targetDate.format(formatter);
|
}
|
||||||
executor.submit(() -> loadDay(dateStr));
|
return transmissions.stream()
|
||||||
|
.filter(t -> dateStr.equals(t.date))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.shutdown();
|
public void reloadDayAsync(String dateStr) {
|
||||||
executor.awaitTermination(5, TimeUnit.MINUTES);
|
new Thread(() -> {
|
||||||
System.out.println("✅ Načteno celkem přenosů: " + transmissions.size());
|
synchronized (transmissions) {
|
||||||
|
transmissions.removeIf(t -> dateStr.equals(t.date)); // smaž jen konkrétní den
|
||||||
|
}
|
||||||
|
System.out.println("🔄 Načítám znovu data pro " + dateStr);
|
||||||
|
try {
|
||||||
|
loadDay(dateStr);
|
||||||
|
System.out.println("✅ Den " + dateStr + " znovu načten.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("❌ Chyba při načítání dne " + dateStr + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadDay(String dateStr) {
|
private void loadDay(String dateStr) {
|
||||||
@ -74,19 +86,4 @@ public class TransmissionService {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reloadDataAsync() {
|
|
||||||
new Thread(() -> {
|
|
||||||
synchronized (transmissions) {
|
|
||||||
transmissions.clear();
|
|
||||||
}
|
}
|
||||||
System.out.println("🔄 Obnovuji data z webu TVCOM...");
|
|
||||||
try {
|
|
||||||
loadNextDays(10);
|
|
||||||
System.out.println("✅ Data znovu načtena, přenosů celkem: " + transmissions.size());
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("❌ Chyba při opětovném načítání: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,258 +1,238 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>TVCOM — Přenosy (lokální UI)</title>
|
<title>TVCOM Přenosy</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"/>
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
<style>
|
.filters {
|
||||||
body { padding: 18px; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
|
display: flex;
|
||||||
.controls { margin-bottom: 12px; display:flex; gap:8px; flex-wrap:wrap; }
|
flex-wrap: wrap;
|
||||||
.table-container { max-height: 70vh; overflow:auto; margin-top:8px; }
|
justify-content: center;
|
||||||
.small-muted { color:#666; font-size:0.9rem; }
|
gap: 10px;
|
||||||
.img-thumb { width:60px; height:40px; object-fit:cover; border-radius:4px; }
|
margin-bottom: 20px;
|
||||||
</style>
|
}
|
||||||
|
|
||||||
|
input, select, button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card img {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.card img {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<h1>📺 TVCOM Přenosy</h1>
|
||||||
<h2>TVCOM — Přenosy (následujících 10 dní)</h2>
|
|
||||||
|
|
||||||
<div class="controls">
|
<div class="filters">
|
||||||
<input id="searchBox" class="form-control" style="max-width:320px" placeholder="Hledat (např. Brno, basketbal, Slavia)"/>
|
<input id="search" type="text" placeholder="Hledat přenos..." />
|
||||||
<select id="sportFilter" class="form-select" style="max-width:220px">
|
<input id="datePicker" type="date" />
|
||||||
<option value="">— Všechny sporty —</option>
|
<select id="sportFilter">
|
||||||
</select>
|
<option value="">Všechny sporty</option>
|
||||||
<select id="leagueFilter" class="form-select" style="max-width:220px">
|
|
||||||
<option value="">— Všechny ligy —</option>
|
|
||||||
</select>
|
|
||||||
<select id="dateFilter" class="form-select" style="max-width:180px">
|
|
||||||
<option value="">— Všechna data —</option>
|
|
||||||
</select>
|
|
||||||
<select id="sortOrder" class="form-select" style="max-width:180px">
|
|
||||||
<option value="date_asc">Datum (vzestupně)</option>
|
|
||||||
<option value="date_desc">Datum (sestupně)</option>
|
|
||||||
<option value="time_asc">Čas (vzestupně)</option>
|
|
||||||
<option value="time_desc">Čas (sestupně)</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
<button id="refreshBtn">♻️ Obnovit data</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-left:auto; display:flex; gap:8px;">
|
<div id="loading">Načítám data...</div>
|
||||||
<button id="refreshBtn" class="btn btn-outline-primary">Reload data</button>
|
<div id="list" class="grid" style="display: none"></div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container">
|
|
||||||
<table id="tbl" class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Obr</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Čas</th>
|
|
||||||
<th>Název / Týmy</th>
|
|
||||||
<th>Sport</th>
|
|
||||||
<th>Liga</th>
|
|
||||||
<th>Odkaz</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="tbody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<ul id="pager" class="pagination"></ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
const API_BASE = "http://localhost:8080";
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const apiKey = urlParams.get('apiKey') || '';
|
const apiKey = urlParams.get('apiKey');
|
||||||
|
const listEl = document.getElementById('list');
|
||||||
|
const loadingEl = document.getElementById('loading');
|
||||||
|
const searchEl = document.getElementById('search');
|
||||||
|
const sportFilterEl = document.getElementById('sportFilter');
|
||||||
|
const datePicker = document.getElementById('datePicker');
|
||||||
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
|
let all = [];
|
||||||
|
|
||||||
const API = '/transmissions';
|
// === inicializace ===
|
||||||
let all = [];
|
const today = new Date().toISOString().substring(0, 10);
|
||||||
let filtered = [];
|
datePicker.value = today;
|
||||||
const pageSize = 25;
|
fetchForDate(today);
|
||||||
let currentPage = 1;
|
|
||||||
|
|
||||||
const searchBox = document.getElementById('searchBox');
|
// === změna datumu ===
|
||||||
const sportFilter = document.getElementById('sportFilter');
|
datePicker.addEventListener('change', () => {
|
||||||
const leagueFilter = document.getElementById('leagueFilter');
|
const dateStr = datePicker.value;
|
||||||
const dateFilter = document.getElementById('dateFilter');
|
if (dateStr) fetchForDate(dateStr);
|
||||||
const sortOrder = document.getElementById('sortOrder');
|
});
|
||||||
const tbody = document.getElementById('tbody');
|
|
||||||
const pager = document.getElementById('pager');
|
|
||||||
const refreshBtn = document.getElementById('refreshBtn');
|
|
||||||
|
|
||||||
function fetchAll() {
|
// === debounce funkce ===
|
||||||
fetch(API + '?apiKey=' + encodeURIComponent(apiKey))
|
function debounce(func, wait) {
|
||||||
.then(r => r.json())
|
let timeout;
|
||||||
|
return function(...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// === vyhledávání ===
|
||||||
|
const debouncedApplyFilters = debounce(applyFilters, 300);
|
||||||
|
searchEl.addEventListener('input', debouncedApplyFilters);
|
||||||
|
sportFilterEl.addEventListener('change', applyFilters);
|
||||||
|
|
||||||
|
// === tlačítko obnovit ===
|
||||||
|
refreshBtn.addEventListener('click', () => {
|
||||||
|
loadingEl.style.display = 'block';
|
||||||
|
listEl.style.display = 'none';
|
||||||
|
const now = new Date().toISOString().substring(0, 10);
|
||||||
|
datePicker.value = now;
|
||||||
|
|
||||||
|
fetch(`${API_BASE}/refresh?apiKey=${apiKey}`)
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// po úspěšném refresh načteme aktuální den
|
||||||
|
const now = new Date().toISOString().substring(0, 10);
|
||||||
|
datePicker.value = now;
|
||||||
|
return fetchForDate(now);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
loadingEl.textContent = "❌ Chyba při obnově dat: " + err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === načtení dat pro daný den ===
|
||||||
|
function fetchForDate(dateStr) {
|
||||||
|
loadingEl.style.display = 'block';
|
||||||
|
listEl.style.display = 'none';
|
||||||
|
return fetch(`${API_BASE}/transmissions?date=${encodeURIComponent(dateStr)}&apiKey=${apiKey}`)
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
all = data || [];
|
all = data || [];
|
||||||
populateFilters();
|
populateSports();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
|
loadingEl.style.display = 'none';
|
||||||
|
listEl.style.display = 'grid';
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('Chyba při načítání dat: ' + err);
|
loadingEl.textContent = "❌ Chyba při načítání dat: " + err;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateFilters() {
|
// === naplnění filtrů podle dostupných sportů ===
|
||||||
const sports = Array.from(new Set(all.map(x => x.sport).filter(Boolean))).sort();
|
function populateSports() {
|
||||||
sportFilter.innerHTML = '<option value="">— Všechny sporty —</option>';
|
const sports = [...new Set(all.map(t => t.sport).filter(Boolean))].sort();
|
||||||
sports.forEach(s => {
|
sportFilterEl.innerHTML = '<option value="">Všechny sporty</option>';
|
||||||
const opt = document.createElement('option'); opt.value = s; opt.textContent = s; sportFilter.appendChild(opt);
|
for (const s of sports) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = s;
|
||||||
|
opt.textContent = s;
|
||||||
|
sportFilterEl.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === aplikace filtrů a vykreslení ===
|
||||||
|
function applyFilters() {
|
||||||
|
const query = searchEl.value.toLowerCase();
|
||||||
|
const sport = sportFilterEl.value;
|
||||||
|
const filtered = all.filter(t => {
|
||||||
|
return (!query || t.title.toLowerCase().includes(query) || (t.league||'').toLowerCase().includes(query)) &&
|
||||||
|
(!sport || t.sport === sport);
|
||||||
});
|
});
|
||||||
|
renderList(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
populateLeagueFilter();
|
// === formátování data a času ===
|
||||||
|
function formatDateTime(dateStr, timeStr) {
|
||||||
|
if (!dateStr || !timeStr) return '';
|
||||||
|
const dt = new Date(dateStr + 'T' + timeStr);
|
||||||
|
return dt.toLocaleString('cs-CZ', { dateStyle: 'short', timeStyle: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
const dates = Array.from(new Set(all.map(x => x.date).filter(Boolean))).sort();
|
// === vykreslení seznamu ===
|
||||||
dateFilter.innerHTML = '<option value="">— Všechna data —</option>';
|
function renderList(items) {
|
||||||
dates.forEach(d => {
|
listEl.innerHTML = '';
|
||||||
const opt = document.createElement('option'); opt.value = d; opt.textContent = d; dateFilter.appendChild(opt);
|
if (!items.length) {
|
||||||
});
|
listEl.innerHTML = '<div style="grid-column:1/-1;text-align:center;">Žádné přenosy</div>';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
for (const t of items) {
|
||||||
function populateLeagueFilter() {
|
const div = document.createElement('div');
|
||||||
const selectedSport = sportFilter.value;
|
div.className = 'card';
|
||||||
const leagues = Array.from(new Set(all.filter(x => !selectedSport || x.sport === selectedSport)
|
const dateTimeStr = formatDateTime(t.date, t.time);
|
||||||
.map(x => x.league).filter(Boolean))).sort();
|
div.innerHTML = `
|
||||||
leagueFilter.innerHTML = '<option value="">— Všechny ligy —</option>';
|
<a href="${t.link}" target="_blank">
|
||||||
leagues.forEach(l => {
|
<img src="${t.image || 'https://via.placeholder.com/400x160?text=Žádný+obrázek'}" alt="">
|
||||||
const opt = document.createElement('option'); opt.value = l; opt.textContent = l; leagueFilter.appendChild(opt);
|
<div class="card-content">
|
||||||
});
|
<div class="title">${t.title}</div>
|
||||||
|
<div class="meta">${dateTimeStr}</div>
|
||||||
|
<div class="meta">${t.sport} • ${t.league || ''} ${t.leaguePart || ''}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
listEl.appendChild(div);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function normalizeText(s) {
|
|
||||||
if (!s) return '';
|
|
||||||
return s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFilters() {
|
|
||||||
const q = normalizeText(searchBox.value.trim());
|
|
||||||
const sport = sportFilter.value;
|
|
||||||
const league = leagueFilter.value;
|
|
||||||
const date = dateFilter.value;
|
|
||||||
|
|
||||||
filtered = all.filter(t => {
|
|
||||||
if (sport && t.sport !== sport) return false;
|
|
||||||
if (league && t.league !== league) return false;
|
|
||||||
if (date && t.date !== date) return false;
|
|
||||||
if (!q) return true;
|
|
||||||
const text = normalizeText((t.title||'') + ' ' + (t.sport||'') + ' ' + (t.league||''));
|
|
||||||
return text.indexOf(q) !== -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
sortFiltered();
|
|
||||||
currentPage = 1;
|
|
||||||
renderTable();
|
|
||||||
renderPager();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortFiltered() {
|
|
||||||
const ord = sortOrder.value;
|
|
||||||
filtered.sort((a,b) => {
|
|
||||||
if (ord === 'date_asc') return (a.date||'').localeCompare(b.date||'') || (a.time||'').localeCompare(b.time||'');
|
|
||||||
if (ord === 'date_desc') return (b.date||'').localeCompare(a.date||'') || (b.time||'').localeCompare(a.time||'');
|
|
||||||
if (ord === 'time_asc') return (a.time||'').localeCompare(b.time||'');
|
|
||||||
if (ord === 'time_desc') return (b.time||'').localeCompare(a.time||'');
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTable() {
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
const start = (currentPage-1)*pageSize;
|
|
||||||
const pageItems = filtered.slice(start, start + pageSize);
|
|
||||||
for (const t of pageItems) {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
|
|
||||||
const imgTd = document.createElement('td');
|
|
||||||
if (t.image) {
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = t.image;
|
|
||||||
img.className = 'img-thumb';
|
|
||||||
imgTd.appendChild(img);
|
|
||||||
}
|
|
||||||
tr.appendChild(imgTd);
|
|
||||||
|
|
||||||
const dateTd = document.createElement('td'); dateTd.textContent = t.date; tr.appendChild(dateTd);
|
|
||||||
const timeTd = document.createElement('td'); timeTd.textContent = t.time; tr.appendChild(timeTd);
|
|
||||||
|
|
||||||
const titleTd = document.createElement('td');
|
|
||||||
titleTd.innerHTML = '<strong>' + escapeHtml(t.title||'') + '</strong><div class="small-muted">' + escapeHtml(t.leaguePart||'') + '</div>';
|
|
||||||
tr.appendChild(titleTd);
|
|
||||||
|
|
||||||
const sportTd = document.createElement('td'); sportTd.textContent = t.sport; tr.appendChild(sportTd);
|
|
||||||
const leagueTd = document.createElement('td'); leagueTd.textContent = t.league; tr.appendChild(leagueTd);
|
|
||||||
|
|
||||||
const linkTd = document.createElement('td');
|
|
||||||
if (t.link) {
|
|
||||||
const a = document.createElement('a'); a.href = t.link; a.textContent = 'Otevřít'; a.target = '_blank';
|
|
||||||
linkTd.appendChild(a);
|
|
||||||
}
|
|
||||||
tr.appendChild(linkTd);
|
|
||||||
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPager() {
|
|
||||||
pager.innerHTML = '';
|
|
||||||
const pages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
||||||
const maxButtons = 7;
|
|
||||||
let start = Math.max(1, currentPage - Math.floor(maxButtons/2));
|
|
||||||
let end = Math.min(pages, start + maxButtons - 1);
|
|
||||||
if (end - start + 1 < maxButtons) start = Math.max(1, end - maxButtons + 1);
|
|
||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
const li = document.createElement('li'); li.className = 'page-item' + (i===currentPage ? ' active' : '');
|
|
||||||
const a = document.createElement('a'); a.className = 'page-link'; a.href = '#';
|
|
||||||
a.textContent = i;
|
|
||||||
a.onclick = (ev => { ev.preventDefault(); currentPage = i; renderTable(); renderPager(); });
|
|
||||||
li.appendChild(a);
|
|
||||||
pager.appendChild(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
if (!s) return '';
|
|
||||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
||||||
}
|
|
||||||
|
|
||||||
let debounceTimer;
|
|
||||||
searchBox.addEventListener('input', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(applyFilters, 250); });
|
|
||||||
sportFilter.addEventListener('change', () => { populateLeagueFilter(); applyFilters(); });
|
|
||||||
leagueFilter.addEventListener('change', applyFilters);
|
|
||||||
dateFilter.addEventListener('change', applyFilters);
|
|
||||||
sortOrder.addEventListener('change', () => { sortFiltered(); renderTable(); });
|
|
||||||
|
|
||||||
function refreshData() {
|
|
||||||
refreshBtn.disabled = true;
|
|
||||||
refreshBtn.textContent = "Načítám...";
|
|
||||||
fetch('/refresh?apiKey=' + encodeURIComponent(apiKey))
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
fetchAll();
|
|
||||||
refreshBtn.disabled = false;
|
|
||||||
refreshBtn.textContent = "Refresh";
|
|
||||||
}, 7000);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
alert('Chyba při obnově dat: ' + err);
|
|
||||||
refreshBtn.disabled = false;
|
|
||||||
refreshBtn.textContent = "Refresh";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshBtn.addEventListener('click', refreshData);
|
|
||||||
|
|
||||||
fetchAll();
|
|
||||||
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user