rewritten UI

This commit is contained in:
Radek Davidek 2025-11-01 13:17:13 +01:00
parent 328e2b9916
commit fb3fdb275e
4 changed files with 288 additions and 303 deletions

View File

@ -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);
} }

View File

@ -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"));
}
}
}
}

View File

@ -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();
}
} }

View File

@ -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>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"/>
<style> <style>
body { padding: 18px; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; } body {
.controls { margin-bottom: 12px; display:flex; gap:8px; flex-wrap:wrap; } font-family: system-ui, sans-serif;
.table-container { max-height: 70vh; overflow:auto; margin-top:8px; } margin: 20px;
.small-muted { color:#666; font-size:0.9rem; } background: #f5f5f5;
.img-thumb { width:60px; height:40px; object-fit:cover; border-radius:4px; } color: #333;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
.filters {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
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> </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">
<option value="">Všechny sporty</option>
</select> </select>
<select id="leagueFilter" class="form-select" style="max-width:220px"> <button id="refreshBtn">♻️ Obnovit data</button>
<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>
<div style="margin-left:auto; display:flex; gap:8px;">
<button id="refreshBtn" class="btn btn-outline-primary">Reload data</button>
</div>
</div> </div>
<div class="table-container"> <div id="loading">Načítám data...</div>
<table id="tbl" class="table table-striped table-hover"> <div id="list" class="grid" style="display: none"></div>
<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 API = '/transmissions'; const loadingEl = document.getElementById('loading');
let all = []; const searchEl = document.getElementById('search');
let filtered = []; const sportFilterEl = document.getElementById('sportFilter');
const pageSize = 25; const datePicker = document.getElementById('datePicker');
let currentPage = 1;
const searchBox = document.getElementById('searchBox');
const sportFilter = document.getElementById('sportFilter');
const leagueFilter = document.getElementById('leagueFilter');
const dateFilter = document.getElementById('dateFilter');
const sortOrder = document.getElementById('sortOrder');
const tbody = document.getElementById('tbody');
const pager = document.getElementById('pager');
const refreshBtn = document.getElementById('refreshBtn'); const refreshBtn = document.getElementById('refreshBtn');
let all = [];
function fetchAll() { // === inicializace ===
fetch(API + '?apiKey=' + encodeURIComponent(apiKey)) const today = new Date().toISOString().substring(0, 10);
.then(r => r.json()) datePicker.value = today;
fetchForDate(today);
// === změna datumu ===
datePicker.addEventListener('change', () => {
const dateStr = datePicker.value;
if (dateStr) fetchForDate(dateStr);
});
// === debounce funkce ===
function debounce(func, wait) {
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;
populateLeagueFilter(); opt.textContent = s;
sportFilterEl.appendChild(opt);
const dates = Array.from(new Set(all.map(x => x.date).filter(Boolean))).sort(); }
dateFilter.innerHTML = '<option value="">— Všechna data —</option>';
dates.forEach(d => {
const opt = document.createElement('option'); opt.value = d; opt.textContent = d; dateFilter.appendChild(opt);
});
}
function populateLeagueFilter() {
const selectedSport = sportFilter.value;
const leagues = Array.from(new Set(all.filter(x => !selectedSport || x.sport === selectedSport)
.map(x => x.league).filter(Boolean))).sort();
leagueFilter.innerHTML = '<option value="">— Všechny ligy —</option>';
leagues.forEach(l => {
const opt = document.createElement('option'); opt.value = l; opt.textContent = l; leagueFilter.appendChild(opt);
});
}
function normalizeText(s) {
if (!s) return '';
return s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
} }
// === aplikace filtrů a vykreslení ===
function applyFilters() { function applyFilters() {
const q = normalizeText(searchBox.value.trim()); const query = searchEl.value.toLowerCase();
const sport = sportFilter.value; const sport = sportFilterEl.value;
const league = leagueFilter.value; const filtered = all.filter(t => {
const date = dateFilter.value; return (!query || t.title.toLowerCase().includes(query) || (t.league||'').toLowerCase().includes(query)) &&
(!sport || t.sport === sport);
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;
}); });
renderList(filtered);
sortFiltered();
currentPage = 1;
renderTable();
renderPager();
} }
function sortFiltered() { // === formátování data a času ===
const ord = sortOrder.value; function formatDateTime(dateStr, timeStr) {
filtered.sort((a,b) => { if (!dateStr || !timeStr) return '';
if (ord === 'date_asc') return (a.date||'').localeCompare(b.date||'') || (a.time||'').localeCompare(b.time||''); const dt = new Date(dateStr + 'T' + timeStr);
if (ord === 'date_desc') return (b.date||'').localeCompare(a.date||'') || (b.time||'').localeCompare(a.time||''); return dt.toLocaleString('cs-CZ', { dateStyle: 'short', timeStyle: 'short' });
if (ord === 'time_asc') return (a.time||'').localeCompare(b.time||'');
if (ord === 'time_desc') return (b.time||'').localeCompare(a.time||'');
return 0;
});
} }
function renderTable() { // === vykreslení seznamu ===
tbody.innerHTML = ''; function renderList(items) {
const start = (currentPage-1)*pageSize; listEl.innerHTML = '';
const pageItems = filtered.slice(start, start + pageSize); if (!items.length) {
for (const t of pageItems) { listEl.innerHTML = '<div style="grid-column:1/-1;text-align:center;">Žádné přenosy</div>';
const tr = document.createElement('tr'); return;
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); for (const t of items) {
const div = document.createElement('div');
const dateTd = document.createElement('td'); dateTd.textContent = t.date; tr.appendChild(dateTd); div.className = 'card';
const timeTd = document.createElement('td'); timeTd.textContent = t.time; tr.appendChild(timeTd); const dateTimeStr = formatDateTime(t.date, t.time);
div.innerHTML = `
const titleTd = document.createElement('td'); <a href="${t.link}" target="_blank">
titleTd.innerHTML = '<strong>' + escapeHtml(t.title||'') + '</strong><div class="small-muted">' + escapeHtml(t.leaguePart||'') + '</div>'; <img src="${t.image || 'https://via.placeholder.com/400x160?text=Žádný+obrázek'}" alt="">
tr.appendChild(titleTd); <div class="card-content">
<div class="title">${t.title}</div>
const sportTd = document.createElement('td'); sportTd.textContent = t.sport; tr.appendChild(sportTd); <div class="meta">${dateTimeStr}</div>
const leagueTd = document.createElement('td'); leagueTd.textContent = t.league; tr.appendChild(leagueTd); <div class="meta">${t.sport} • ${t.league || ''} ${t.leaguePart || ''}</div>
</div>
const linkTd = document.createElement('td'); </a>
if (t.link) { `;
const a = document.createElement('a'); a.href = t.link; a.textContent = 'Otevřít'; a.target = '_blank'; listEl.appendChild(div);
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
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>