diff --git a/app/public/assets/css/channels.css b/app/public/assets/css/channels.css
index e69de29..e7541cd 100644
--- a/app/public/assets/css/channels.css
+++ b/app/public/assets/css/channels.css
@@ -0,0 +1,34 @@
+.channel-number {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 34px;
+ height: 26px;
+ margin-right: 10px;
+ border-radius: 999px;
+ background: var(--panel-3);
+ color: #dbeafe;
+ font-size: 0.85rem;
+ font-weight: 800;
+}
+
+.timeline-channel .channel-number {
+ margin-right: 0;
+}
+
+.timeline-channel-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.channel-logo {
+ width: 34px;
+ height: 24px;
+ object-fit: contain;
+ flex: 0 0 auto;
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 6px;
+ padding: 2px;
+}
diff --git a/app/public/assets/css/day.css b/app/public/assets/css/day.css
index 5e44979..fb20be2 100644
--- a/app/public/assets/css/day.css
+++ b/app/public/assets/css/day.css
@@ -79,20 +79,6 @@
justify-content: space-between;
}
-.channel-number {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 34px;
- height: 26px;
- margin-right: 10px;
- border-radius: 999px;
- background: var(--panel-3);
- color: #dbeafe;
- font-size: 0.85rem;
- font-weight: 800;
-}
-
.day-programme {
display: grid;
grid-template-columns: 92px 1fr;
@@ -101,11 +87,6 @@
border-top: 1px solid var(--border);
}
-.day-programme.is-current {
- background: rgba(34, 197, 94, 0.12);
- border-left: 5px solid var(--success);
-}
-
.day-programme.is-past {
opacity: 0.55;
}
diff --git a/app/public/assets/css/now.css b/app/public/assets/css/now.css
index e69de29..a92b2d2 100644
--- a/app/public/assets/css/now.css
+++ b/app/public/assets/css/now.css
@@ -0,0 +1,30 @@
+.day-programme.is-current {
+ background: rgba(34, 197, 94, 0.12);
+ border-left: 5px solid var(--success);
+}
+
+.timeline-programme.is-current {
+ background: rgba(34, 197, 94, 0.22);
+ border-color: var(--success);
+}
+
+.timeline-now-line {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ background: #ef4444;
+ z-index: 100;
+ pointer-events: none;
+}
+
+.timeline-now-line::before {
+ content: "";
+ position: absolute;
+ left: -5px;
+ top: -5px;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #ef4444;
+}
diff --git a/app/public/assets/css/timeline.css b/app/public/assets/css/timeline.css
index d82d655..58d9c01 100644
--- a/app/public/assets/css/timeline.css
+++ b/app/public/assets/css/timeline.css
@@ -100,27 +100,6 @@
font-weight: 800;
}
-.timeline-channel .channel-number {
- margin-right: 0;
-}
-
-.timeline-channel-name {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.channel-logo {
- width: 34px;
- height: 24px;
- object-fit: contain;
- flex: 0 0 auto;
- background: rgba(255, 255, 255, 0.08);
- border-radius: 6px;
- padding: 2px;
-}
-
.timeline-programmes {
position: relative;
min-height: 72px;
@@ -189,36 +168,10 @@
font-size: 0.75rem;
}
-.timeline-programme.is-current {
- background: rgba(34, 197, 94, 0.22);
- border-color: var(--success);
-}
-
.timeline-programme.is-past {
opacity: 0.55;
}
-.timeline-now-line {
- position: absolute;
- top: 0;
- bottom: 0;
- width: 2px;
- background: #ef4444;
- z-index: 100;
- pointer-events: none;
-}
-
-.timeline-now-line::before {
- content: "";
- position: absolute;
- left: -5px;
- top: -5px;
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background: #ef4444;
-}
-
.timeline-zoom {
display: flex;
gap: 8px;
diff --git a/app/public/assets/js/app.js b/app/public/assets/js/app.js
index 8dd1714..78e8432 100644
--- a/app/public/assets/js/app.js
+++ b/app/public/assets/js/app.js
@@ -1,215 +1,14 @@
-document.addEventListener("DOMContentLoaded", () => {
-
- const wrap = document.querySelector(".timeline-wrap");
- const line = document.querySelector(".timeline-now-line");
-
- if (!wrap || !line)
- return;
-
- wrap.scrollLeft = Math.max(
- 0,
- line.offsetLeft - wrap.clientWidth / 3
- );
-
-});
+import { initNowLine } from "./modules/now.js";
+import { initProgrammeModal } from "./modules/modal.js";
+import { initGuideSearch } from "./modules/search.js";
+import { initTimelineZoom, initTimelineDrag } from "./modules/timeline.js";
document.addEventListener("DOMContentLoaded", () => {
- const modal = document.getElementById("programmeModal");
+ initNowLine();
+ initProgrammeModal();
+ initGuideSearch();
+ initTimelineZoom();
+ initTimelineDrag();
- if (!modal) {
- return;
- }
-
- const title = document.getElementById("modalTitle");
- const subtitle = document.getElementById("modalSubtitle");
- const description = document.getElementById("modalDescription");
- const category = document.getElementById("modalCategory");
- const time = document.getElementById("modalTime");
-
- const openModal = (programmeElement) => {
- const id = programmeElement.dataset.programmeId;
- const programme = window.TVGUIDE_PROGRAMMES?.[id];
-
- if (!programme) {
- return;
- }
-
- title.textContent = programme.title || "";
- subtitle.textContent = programme.subtitle || "";
- description.textContent = programme.description || "Keine Beschreibung vorhanden.";
- category.textContent = programme.category || "";
- time.textContent = `${programme.channel || ""} · ${programme.start || ""} – ${programme.stop || ""}`;
-
- modal.hidden = false;
- document.body.classList.add("modal-open");
- };
-
- const closeModal = () => {
- modal.hidden = true;
- document.body.classList.remove("modal-open");
- };
-
- document.querySelectorAll(".timeline-programme").forEach((programme) => {
- programme.addEventListener("click", () => openModal(programme));
- });
-
- modal.querySelectorAll("[data-modal-close]").forEach((element) => {
- element.addEventListener("click", closeModal);
- });
-
- document.addEventListener("keydown", (event) => {
- if (event.key === "Escape" && !modal.hidden) {
- closeModal();
- }
- });
+ console.log("TVGuide loaded");
});
-
-document.addEventListener("DOMContentLoaded", () => {
- const search = document.getElementById("guideSearch");
- const results = document.getElementById("guideSearchResults");
- const programmes = window.TVGUIDE_PROGRAMMES || {};
-
- if (!search || !results) {
- return;
- }
-
- const openProgrammeById = (id) => {
- const element = document.querySelector(`[data-programme-id="${CSS.escape(id)}"]`);
- if (element) {
- element.click();
- }
- };
-
- search.addEventListener("input", () => {
- const query = search.value.trim().toLowerCase();
-
- if (query.length < 2) {
- results.hidden = true;
- results.innerHTML = "";
- return;
- }
-
- const matches = Object.values(programmes)
- .filter((programme) => {
- return [
- programme.title,
- programme.subtitle,
- programme.description,
- programme.category,
- programme.channel
- ].some((value) => (value || "").toLowerCase().includes(query));
- })
- .slice(0, 12);
-
- if (!matches.length) {
- results.innerHTML = `
Nichts gefunden
`;
- results.hidden = false;
- return;
- }
-
- results.innerHTML = matches.map((programme) => `
-
- ${programme.title || "Ohne Titel"}
- ${programme.channel || ""} · ${programme.start || ""} – ${programme.stop || ""}
-
- `).join("");
-
- results.hidden = false;
- });
-
- results.addEventListener("click", (event) => {
- const item = event.target.closest("[data-search-programme-id]");
- if (!item) {
- return;
- }
-
- openProgrammeById(item.dataset.searchProgrammeId);
- results.hidden = true;
- search.value = "";
- });
-
- document.addEventListener("click", (event) => {
- if (!event.target.closest(".guide-search")) {
- results.hidden = true;
- }
- });
-});
-
-document.addEventListener("DOMContentLoaded", () => {
- const timelineWrap = document.querySelector(".timeline-wrap");
- const zoomButtons = document.querySelectorAll("[data-zoom]");
-
- if (!timelineWrap || !zoomButtons.length) {
- return;
- }
-
- const savedZoom = localStorage.getItem("tvguideTimelineZoom") || "2";
-
- const applyZoom = (zoom) => {
- timelineWrap.style.setProperty("--minute-width", `${zoom}px`);
-
- zoomButtons.forEach((button) => {
- button.classList.toggle("active", button.dataset.zoom === zoom);
- });
-
- localStorage.setItem("tvguideTimelineZoom", zoom);
- };
-
- zoomButtons.forEach((button) => {
- button.addEventListener("click", () => {
- applyZoom(button.dataset.zoom);
- });
- });
-
- applyZoom(savedZoom);
-});
-
-document.addEventListener("DOMContentLoaded", () => {
-
- const timeline = document.querySelector(".timeline-wrap");
-
- if (!timeline)
- return;
-
- let isDragging = false;
- let startX = 0;
- let scrollLeft = 0;
-
- timeline.addEventListener("mousedown", (e) => {
-
- // nicht wenn auf eine Sendung geklickt wird
- if (e.target.closest(".timeline-programme"))
- return;
-
- isDragging = true;
-
- timeline.classList.add("dragging");
-
- startX = e.pageX;
- scrollLeft = timeline.scrollLeft;
- });
-
- document.addEventListener("mouseup", () => {
-
- isDragging = false;
-
- timeline.classList.remove("dragging");
- });
-
- document.addEventListener("mousemove", (e) => {
-
- if (!isDragging)
- return;
-
- e.preventDefault();
-
- const walk = e.pageX - startX;
-
- timeline.scrollLeft = scrollLeft - walk;
-
- });
-
-});
-
-console.log('TVGuide loaded');
-
diff --git a/app/public/assets/js/modules/modal.js b/app/public/assets/js/modules/modal.js
new file mode 100644
index 0000000..ce68720
--- /dev/null
+++ b/app/public/assets/js/modules/modal.js
@@ -0,0 +1,50 @@
+export function initProgrammeModal() {
+ const modal = document.getElementById("programmeModal");
+
+ if (!modal) {
+ return;
+ }
+
+ const title = document.getElementById("modalTitle");
+ const subtitle = document.getElementById("modalSubtitle");
+ const description = document.getElementById("modalDescription");
+ const category = document.getElementById("modalCategory");
+ const time = document.getElementById("modalTime");
+
+ const openModal = (programmeElement) => {
+ const id = programmeElement.dataset.programmeId;
+ const programme = window.TVGUIDE_PROGRAMMES?.[id];
+
+ if (!programme) {
+ return;
+ }
+
+ title.textContent = programme.title || "";
+ subtitle.textContent = programme.subtitle || "";
+ description.textContent = programme.description || "Keine Beschreibung vorhanden.";
+ category.textContent = programme.category || "";
+ time.textContent = `${programme.channel || ""} · ${programme.start || ""} – ${programme.stop || ""}`;
+
+ modal.hidden = false;
+ document.body.classList.add("modal-open");
+ };
+
+ const closeModal = () => {
+ modal.hidden = true;
+ document.body.classList.remove("modal-open");
+ };
+
+ document.querySelectorAll(".timeline-programme").forEach((programme) => {
+ programme.addEventListener("click", () => openModal(programme));
+ });
+
+ modal.querySelectorAll("[data-modal-close]").forEach((element) => {
+ element.addEventListener("click", closeModal);
+ });
+
+ document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape" && !modal.hidden) {
+ closeModal();
+ }
+ });
+}
diff --git a/app/public/assets/js/modules/now.js b/app/public/assets/js/modules/now.js
new file mode 100644
index 0000000..b3d60b7
--- /dev/null
+++ b/app/public/assets/js/modules/now.js
@@ -0,0 +1,13 @@
+export function initNowLine() {
+ const wrap = document.querySelector(".timeline-wrap");
+ const line = document.querySelector(".timeline-now-line");
+
+ if (!wrap || !line) {
+ return;
+ }
+
+ wrap.scrollLeft = Math.max(
+ 0,
+ line.offsetLeft - wrap.clientWidth / 3
+ );
+}
diff --git a/app/public/assets/js/modules/search.js b/app/public/assets/js/modules/search.js
new file mode 100644
index 0000000..a69b5e7
--- /dev/null
+++ b/app/public/assets/js/modules/search.js
@@ -0,0 +1,72 @@
+export function initGuideSearch() {
+ const search = document.getElementById("guideSearch");
+ const results = document.getElementById("guideSearchResults");
+ const programmes = window.TVGUIDE_PROGRAMMES || {};
+
+ if (!search || !results) {
+ return;
+ }
+
+ const openProgrammeById = (id) => {
+ const element = document.querySelector(`[data-programme-id="${CSS.escape(id)}"]`);
+
+ if (element) {
+ element.click();
+ }
+ };
+
+ search.addEventListener("input", () => {
+ const query = search.value.trim().toLowerCase();
+
+ if (query.length < 2) {
+ results.hidden = true;
+ results.innerHTML = "";
+ return;
+ }
+
+ const matches = Object.values(programmes)
+ .filter((programme) => {
+ return [
+ programme.title,
+ programme.subtitle,
+ programme.description,
+ programme.category,
+ programme.channel
+ ].some((value) => (value || "").toLowerCase().includes(query));
+ })
+ .slice(0, 12);
+
+ if (!matches.length) {
+ results.innerHTML = `Nichts gefunden
`;
+ results.hidden = false;
+ return;
+ }
+
+ results.innerHTML = matches.map((programme) => `
+
+ ${programme.title || "Ohne Titel"}
+ ${programme.channel || ""} · ${programme.start || ""} – ${programme.stop || ""}
+
+ `).join("");
+
+ results.hidden = false;
+ });
+
+ results.addEventListener("click", (event) => {
+ const item = event.target.closest("[data-search-programme-id]");
+
+ if (!item) {
+ return;
+ }
+
+ openProgrammeById(item.dataset.searchProgrammeId);
+ results.hidden = true;
+ search.value = "";
+ });
+
+ document.addEventListener("click", (event) => {
+ if (!event.target.closest(".guide-search")) {
+ results.hidden = true;
+ }
+ });
+}
diff --git a/app/public/assets/js/modules/timeline.js b/app/public/assets/js/modules/timeline.js
new file mode 100644
index 0000000..3c75531
--- /dev/null
+++ b/app/public/assets/js/modules/timeline.js
@@ -0,0 +1,68 @@
+export function initTimelineZoom() {
+ const timelineWrap = document.querySelector(".timeline-wrap");
+ const zoomButtons = document.querySelectorAll("[data-zoom]");
+
+ if (!timelineWrap || !zoomButtons.length) {
+ return;
+ }
+
+ const savedZoom = localStorage.getItem("tvguideTimelineZoom") || "2";
+
+ const applyZoom = (zoom) => {
+ timelineWrap.style.setProperty("--minute-width", `${zoom}px`);
+
+ zoomButtons.forEach((button) => {
+ button.classList.toggle("active", button.dataset.zoom === zoom);
+ });
+
+ localStorage.setItem("tvguideTimelineZoom", zoom);
+ };
+
+ zoomButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ applyZoom(button.dataset.zoom);
+ });
+ });
+
+ applyZoom(savedZoom);
+}
+
+export function initTimelineDrag() {
+ const timeline = document.querySelector(".timeline-wrap");
+
+ if (!timeline) {
+ return;
+ }
+
+ let isDragging = false;
+ let startX = 0;
+ let scrollLeft = 0;
+
+ timeline.addEventListener("mousedown", (event) => {
+ if (event.target.closest(".timeline-programme")) {
+ return;
+ }
+
+ isDragging = true;
+ timeline.classList.add("dragging");
+
+ startX = event.pageX;
+ scrollLeft = timeline.scrollLeft;
+ });
+
+ document.addEventListener("mouseup", () => {
+ isDragging = false;
+ timeline.classList.remove("dragging");
+ });
+
+ document.addEventListener("mousemove", (event) => {
+ if (!isDragging) {
+ return;
+ }
+
+ event.preventDefault();
+
+ const walk = event.pageX - startX;
+ timeline.scrollLeft = scrollLeft - walk;
+ });
+}
diff --git a/app/templates/layout.php b/app/templates/layout.php
index b94a705..516d066 100644
--- a/app/templates/layout.php
+++ b/app/templates/layout.php
@@ -19,6 +19,6 @@
-
+