first commit
This commit is contained in:
commit
7f971b04de
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
servers
|
||||||
|
target
|
||||||
|
bin
|
||||||
|
.settings
|
||||||
|
.metadata
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
|
||||||
74
pom.xml
Normal file
74
pom.xml
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>cz.tvcom</groupId>
|
||||||
|
<artifactId>tvcom-scraper</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- HTML parser -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jsoup</groupId>
|
||||||
|
<artifactId>jsoup</artifactId>
|
||||||
|
<version>1.17.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.18.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>11</release>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>cz.kamma.tvcom.HttpServerApp</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>cz.kamma.tvcom.HttpServerApp</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
153
src/main/java/cz/kamma/tvcom/HttpServerApp.java
Normal file
153
src/main/java/cz/kamma/tvcom/HttpServerApp.java
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package cz.kamma.tvcom;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.sun.net.httpserver.Headers;
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HttpServerApp {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
TransmissionService service = new TransmissionService();
|
||||||
|
service.loadNextDays(10);
|
||||||
|
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// /transmissions -> all data
|
||||||
|
server.createContext("/transmissions", exchange -> {
|
||||||
|
try {
|
||||||
|
setCors(exchange);
|
||||||
|
List<Transmission> all = service.getAll();
|
||||||
|
respondJson(exchange, mapper.writeValueAsString(all));
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
sendError(exchange, 500, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// /search?q=...
|
||||||
|
server.createContext("/search", exchange -> {
|
||||||
|
try {
|
||||||
|
setCors(exchange);
|
||||||
|
URI uri = exchange.getRequestURI();
|
||||||
|
String query = null;
|
||||||
|
if (uri.getQuery() != null) {
|
||||||
|
String fullq = uri.getQuery();
|
||||||
|
for (String part : fullq.split("&")) {
|
||||||
|
if (part.startsWith("q=")) {
|
||||||
|
query = java.net.URLDecoder.decode(part.substring(2), StandardCharsets.UTF_8.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Transmission> results = service.search(query);
|
||||||
|
respondJson(exchange, mapper.writeValueAsString(results));
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
sendError(exchange, 500, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ NEW: /refresh endpoint
|
||||||
|
server.createContext("/refresh", exchange -> {
|
||||||
|
try {
|
||||||
|
setCors(exchange);
|
||||||
|
if ("GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
service.reloadDataAsync();
|
||||||
|
respondJson(exchange, "{\"status\":\"ok\",\"message\":\"Data se obnovují na pozadí.\"}");
|
||||||
|
} else {
|
||||||
|
exchange.sendResponseHeaders(405, -1);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
sendError(exchange, 500, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// serve /index.html
|
||||||
|
server.createContext("/", exchange -> {
|
||||||
|
try {
|
||||||
|
setCors(exchange);
|
||||||
|
String path = exchange.getRequestURI().getPath();
|
||||||
|
if ("/".equals(path) || path.isEmpty() || path.equals("/index.html")) {
|
||||||
|
String html = readResource("/index.html");
|
||||||
|
if (html == null) {
|
||||||
|
sendError(exchange, 404, "index.html not found in resources");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
|
||||||
|
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(200, bytes.length);
|
||||||
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendError(exchange, 404, "Not found");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
sendError(exchange, 500, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.start();
|
||||||
|
System.out.println("🚀 HTTP server běží na http://localhost:8080");
|
||||||
|
System.out.println(" ➜ /transmissions (všechny přenosy JSON)");
|
||||||
|
System.out.println(" ➜ /search?q=Brno (vyhledávání JSON)");
|
||||||
|
System.out.println(" ➜ /refresh (spustí opětovné načtení dat)");
|
||||||
|
System.out.println(" ➜ / (web UI)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setCors(HttpExchange exchange) {
|
||||||
|
Headers h = exchange.getResponseHeaders();
|
||||||
|
h.set("Access-Control-Allow-Origin", "*");
|
||||||
|
h.set("Access-Control-Allow-Methods", "GET, OPTIONS");
|
||||||
|
h.set("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
try {
|
||||||
|
exchange.sendResponseHeaders(204, -1);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void respondJson(HttpExchange exchange, String json) throws IOException {
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||||
|
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(200, bytes.length);
|
||||||
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendError(HttpExchange exchange, int code, String message) throws IOException {
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
|
||||||
|
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(code, bytes.length);
|
||||||
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readResource(String resourcePath) {
|
||||||
|
try (InputStream is = HttpServerApp.class.getResourceAsStream(resourcePath)) {
|
||||||
|
if (is == null) return null;
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
sb.append(line).append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/main/java/cz/kamma/tvcom/Searcher.java
Normal file
29
src/main/java/cz/kamma/tvcom/Searcher.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/cz/kamma/tvcom/Transmission.java
Normal file
24
src/main/java/cz/kamma/tvcom/Transmission.java
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package cz.kamma.tvcom;
|
||||||
|
|
||||||
|
public class Transmission {
|
||||||
|
public String title;
|
||||||
|
public String sport;
|
||||||
|
public String league;
|
||||||
|
public String leaguePart;
|
||||||
|
public String link;
|
||||||
|
public String date;
|
||||||
|
public String time;
|
||||||
|
public String image;
|
||||||
|
|
||||||
|
public Transmission(String title, String sport, String league, String leaguePart,
|
||||||
|
String link, String date, String time, String image) {
|
||||||
|
this.title = title;
|
||||||
|
this.sport = sport;
|
||||||
|
this.league = league;
|
||||||
|
this.leaguePart = leaguePart;
|
||||||
|
this.link = link;
|
||||||
|
this.date = date;
|
||||||
|
this.time = time;
|
||||||
|
this.image = image;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/main/java/cz/kamma/tvcom/TransmissionService.java
Normal file
92
src/main/java/cz/kamma/tvcom/TransmissionService.java
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package cz.kamma.tvcom;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
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 {
|
||||||
|
private final List<Transmission> transmissions = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
public void loadNextDays(int daysToLoad) throws InterruptedException {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(5);
|
||||||
|
|
||||||
|
for (int i = 0; i < daysToLoad; i++) {
|
||||||
|
LocalDate targetDate = today.plusDays(i);
|
||||||
|
String dateStr = targetDate.format(formatter);
|
||||||
|
executor.submit(() -> loadDay(dateStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.shutdown();
|
||||||
|
executor.awaitTermination(5, TimeUnit.MINUTES);
|
||||||
|
System.out.println("✅ Načteno celkem přenosů: " + transmissions.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadDay(String dateStr) {
|
||||||
|
try {
|
||||||
|
String url = "https://www.tvcom.cz/Den/?d=" + dateStr;
|
||||||
|
System.out.println("⏳ Načítám " + dateStr);
|
||||||
|
Document doc = Jsoup.connect(url).timeout(15000).get();
|
||||||
|
|
||||||
|
Elements items = doc.select("section.video.tile a.item");
|
||||||
|
for (Element item : items) {
|
||||||
|
String href = item.attr("href");
|
||||||
|
String link = href.startsWith("http") ? href : "https://www.tvcom.cz" + href;
|
||||||
|
String title = item.select(".title").text();
|
||||||
|
String sport = item.select(".sport").text();
|
||||||
|
String league = item.select(".league").text();
|
||||||
|
String leaguePart = item.select(".leaguePart").text();
|
||||||
|
String time = item.select(".starting .time").text();
|
||||||
|
String image = item.select(".image img").attr("src");
|
||||||
|
|
||||||
|
transmissions.add(new Transmission(title, sport, league, leaguePart, link, dateStr, time, image));
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("✅ Den " + dateStr + ": " + items.size() + " přenosů");
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("⚠️ Chyba při načítání dne " + dateStr + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Transmission> getAll() {
|
||||||
|
synchronized (transmissions) {
|
||||||
|
return new ArrayList<>(transmissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Transmission> search(String query) {
|
||||||
|
if (query == null || query.isBlank()) return getAll();
|
||||||
|
String q = query.toLowerCase();
|
||||||
|
synchronized (transmissions) {
|
||||||
|
return transmissions.stream()
|
||||||
|
.filter(t -> ( (t.title == null ? "" : t.title) + " " +
|
||||||
|
(t.sport == null ? "" : t.sport) + " " +
|
||||||
|
(t.league == null ? "" : t.league) )
|
||||||
|
.toLowerCase().contains(q))
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
257
src/main/resources/index.html
Normal file
257
src/main/resources/index.html
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>TVCOM — Přenosy (lokální UI)</title>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { padding: 18px; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
|
||||||
|
.controls { margin-bottom: 12px; display:flex; gap:8px; flex-wrap:wrap; }
|
||||||
|
.table-container { max-height: 70vh; overflow:auto; margin-top:8px; }
|
||||||
|
.small-muted { color:#666; font-size:0.9rem; }
|
||||||
|
.img-thumb { width:60px; height:40px; object-fit:cover; border-radius:4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2>TVCOM — Přenosy (následujících 10 dní)</h2>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<input id="searchBox" class="form-control" style="max-width:320px" placeholder="Hledat (např. Brno, basketbal, Slavia)"/>
|
||||||
|
<select id="sportFilter" class="form-select" style="max-width:220px">
|
||||||
|
<option value="">— Všechny sporty —</option>
|
||||||
|
</select>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div style="margin-left:auto; display:flex; gap:8px;">
|
||||||
|
<button id="refreshBtn" class="btn btn-outline-primary">Reload data</button>
|
||||||
|
</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>
|
||||||
|
(function () {
|
||||||
|
const API = '/transmissions';
|
||||||
|
let all = [];
|
||||||
|
let filtered = [];
|
||||||
|
const pageSize = 25;
|
||||||
|
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');
|
||||||
|
|
||||||
|
function fetchAll() {
|
||||||
|
fetch(API).then(r => r.json()).then(data => {
|
||||||
|
all = data || [];
|
||||||
|
populateFilters();
|
||||||
|
applyFilters();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert('Chyba při načítání dat: ' + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateFilters() {
|
||||||
|
// Sport filter
|
||||||
|
const sports = Array.from(new Set(all.map(x => x.sport).filter(Boolean))).sort();
|
||||||
|
sportFilter.innerHTML = '<option value="">— Všechny sporty —</option>';
|
||||||
|
sports.forEach(s => {
|
||||||
|
const opt = document.createElement('option'); opt.value = s; opt.textContent = s; sportFilter.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
populateLeagueFilter();
|
||||||
|
|
||||||
|
// Date filter
|
||||||
|
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 '';
|
||||||
|
// odstranění diakritiky a převod na malá písmena
|
||||||
|
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;
|
||||||
|
|
||||||
|
// spojíme text a normalizujeme
|
||||||
|
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(); });
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
refreshBtn.disabled = true;
|
||||||
|
refreshBtn.textContent = "Načítám...";
|
||||||
|
const resp = await fetch('/refresh');
|
||||||
|
await resp.json();
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchAll();
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user