From f8fc26229092416efa0c69b0c90c470bfd7f37b2 Mon Sep 17 00:00:00 2001 From: the-eagle16 Date: Mon, 22 Jun 2026 13:28:31 +0200 Subject: [PATCH] =?UTF-8?q?Implementierung=20eines=20etxra=20Fensters=20f?= =?UTF-8?q?=C3=BCr=20jeden=20Film=20->=20Infos,=20Schauspieler,=20Trailer?= =?UTF-8?q?=20(YouTube),=20Spielzeiten=20Tabelle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 367 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 344 insertions(+), 23 deletions(-) diff --git a/main.js b/main.js index 0d3f68f..47c1ca8 100644 --- a/main.js +++ b/main.js @@ -65,7 +65,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "6", description: "In Walt Disney Animation Studios’ \"Zoomania 2\" geraten die tierischen Detektive Judy Hopps und Nick Wilde auf die rätselhafte Spur eines geheimnisvollen Reptils, das in Zoomania auftaucht und die Metropole völlig auf den Kopf stellt: Gary De’Snake! ", poster: "img/Zoomania-2.jpg", - backdrop: "img/Zoomania-2.jpg" + backdrop: "img/Zoomania-2.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=RPE_6KbMgYU&pp=ygUZem9vbWFuaWEgMiB0cmFpbGVyIGdlcm1hbg%3D%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Shelter", @@ -74,7 +76,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "16", description: "Michael Mason, ein untergetauchter Elite-Agent, lebt auf einer abgelegenen Insel in Schottland. Als er in einem schweren Sturm ein Mädchen vor dem Ertrinken rettet, setzt er damit eine Kette von Ereignissen in Gang, die sein Versteck enttarnen und ihn zurück in die Welt zwingen. Dort muss er nicht nur um das Überleben der Teenagerin kämpfen, sondern sich auch den Dämonen seiner Vergangenheit stellen…", poster: "img/shelter.jpg", - backdrop: "img/shelter.jpg" + backdrop: "img/shelter.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=_DgEucbPMZI&pp=ygUWc2hlbHRlciB0cmFpbGVyIGdlcm1hbg%3D%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Mutiny", @@ -83,7 +87,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "?", description: "Als sein milliardenschwerer Boss vor seinen Augen einem Mordkomplott zum Opfer fällt, wird Cole Reed (Jason Statham) für das Verbrechen verantwortlich gemacht. Im Bestreben dessen Tod zu rächen und die Täter zu überführen, gelangt Reed an Bord eines Frachters und stößt dabei auf eine internationale Verschwörung. Im Alleingang startet Cole eine gnadenlose Racheaktion auf hoher See…", poster: "img/mutiny.jpg", - backdrop: "img/mutiny.jpg" + backdrop: "img/mutiny.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=DU2cBNrvHA4&pp=ygUObXV0aW55IHRyYWlsZXI%3D", + actors: ["Name 1", "Name 2", "Name 3"], }, { title: "Der Austronaut - Project Hail Mary", @@ -92,7 +98,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "12", description: "Der Naturwissenschaftslehrer Ryland Grace wacht eines Tages auf einem Raumschiff auf - Lichtjahre von zu Hause entfernt und ohne Erinnerung daran, wer er ist oder wie er dorthin gekommen ist.", poster: "img/derAustronaut.jpg", - backdrop: "img/derAustronaut.jpg" + backdrop: "img/derAustronaut.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=-KXQttVDPyk&pp=ygUdZGVyIGFzdHJvbmF1dCB0cmFpbGVyIGRldXRzY2g%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Der Super Mario Galaxy Film", @@ -101,7 +109,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "6", description: "''Der Super Mario Galaxy Film'' ist der neue Animationsfilm, basierend auf der Welt von „Super Mario Bros.“!", poster: "img/mariogalaxy.jpg", - backdrop: "img/mariogalaxy.jpg" + backdrop: "img/mariogalaxy.jpg", + trailerYoutubeId:"https://www.youtube.com/watch?v=RtoRPSh0jvM&pp=ygUrZGVyIHN1cGVyIG1hcmlvIGdhbGF4eSBmaWxtIHRyYWlsZXIgZGV1dHNjaA%3D%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Hoppers", @@ -110,7 +120,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "6", description: "In Disney Pixars ''Hoppers'' hoppt Tierfreundin Mabel mithilfe neuester Technologie in einen Roboterbiber und macht die Tierwelt (un)sicher! ", poster: "img/hoppers.jpg", - backdrop: "img/hoppers.jpg" + backdrop: "img/hoppers.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=WqTZZcPe5R0&pp=ygUPaG9wcGVycyB0cmFpbGVy", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Solo Mio", @@ -119,7 +131,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "12", description: "Nachdem er in Rom am Altar stehen gelassen wurde, beschließt ein Mann, allein in die Flitterwochen zu fahren.", poster: "img/solomio.png", - backdrop: "img/solomio.png" + backdrop: "img/solomio.png", + trailerYoutubeId: "https://www.youtube.com/watch?v=tYtiinTvE3s&pp=ygUYc29sZSBtaW8gdHJhaWxlciBkZXV0c2No", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "The Mandalorian and Grogu", @@ -128,7 +142,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "?", description: "Während die Neue Republik daran arbeitet, alles zu schützen, wofür die Rebellion gekämpft hat, sichern sie sich die Unterstützung des legendären mandalorianischen Kopfgeldjägers Din Djarin und seines jungen Lehrlings Grogu. ", poster: "img/mandalorian.jpeg", - backdrop: "img/mandalorian.jpeg" + backdrop: "img/mandalorian.jpeg", + trailerYoutubeId: "https://www.youtube.com/watch?v=cMJMtGccVfs&pp=ygUlbWFuZGFsb3JpYW4gYW5kIGdyb2d1IHRyYWlsZXIgZGV1dHNjaA%3D%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Glennkill - Ein Schafskrimi", @@ -137,7 +153,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "?", description: "Schäfer George liest seinen geliebten Schafen jeden Abend Krimis vor und nimmt an, dass sie kein Wort davon verstehen. Doch als ein mysteriöser Vorfall das friedliche Leben auf der Farm durchbricht, wird den Schafen bewusst, dass sie selbst nun die Ermittlungen übernehmen müssen. Also folgen sie der Spur der Beweise und machen menschliche Verdächtige ausfindig.", poster: "img/glennkill.jpg", - backdrop: "img/glennkill.jpg" + backdrop: "img/glennkill.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=Ylu6xVTHAlk&pp=ygUZZ2xlbm5raWxsIGVpbiBzY2hhZnNrcmltaQ%3D%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Masters of the Universe", @@ -146,7 +164,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "?", description: "Nach 15 Jahren der Trennung führt das Schwert der Macht Prinz Adam zurück nach Eternia und er entdeckt, dass seine Heimat unter der Herrschaft von Skeletor in Trümmern liegt.", poster: "img/masteruniverse.jpg", - backdrop: "img/masteruniverse.jpg" + backdrop: "img/masteruniverse.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=COJU2b4XZ8U&pp=ygUfbWFzdGVycyBvZiB0aGUgdW5pdmVyc2UgdHJhaWxlcg%3D%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Minions and Monsters", @@ -155,7 +175,9 @@ document.addEventListener("DOMContentLoaded", () => { fsk: "?", description: "MINIONS & MONSTER erzählt die abgefahrene, aberwitzige und natürlich absolut wahre Geschichte von den Minions und wie sie Hollywood erobern, Filmstars werden, alles verlieren, Monster auf die Welt loslassen und sich dann heldenhaft daranmachen, den Planeten vor genau dem Chaos zu retten, das sie selbst angerichtet haben. ", poster: "img/minionsmonsters.jpg", - backdrop: "img/minionsmonsters.jpg" + backdrop: "img/minionsmonsters.jpg", + trailerYoutubeId: "https://www.youtube.com/watch?v=a31E7qNxwtI&pp=ygUcbWluaW9ucyBhbmQgbW9uc3RlcnMgdHJhaWxlctIHCQkECwGHKiGM7w%3D%3D", + actors: ["Name 1", "Name 2", "Name 3"] }, { title: "Spider Man - Brand New Day", @@ -334,6 +356,10 @@ document.addEventListener("DOMContentLoaded", () => { let heroItems = []; let heroIndex = 0; let heroTimer = null; + let activeMovieDetail = { + index: null, + sourceCard: null + }; const weekdayShort = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]; @@ -437,6 +463,29 @@ document.addEventListener("DOMContentLoaded", () => { `; }; + const getMovieDurationLabel = (movie) => { + const duration = movie?.duration ?? ""; + return Number.isFinite(Number(duration)) ? `${duration} Min.` : escapeHtml(duration || "tba"); + }; + + const getYoutubeVideoId = (input) => { + const value = String(input || "").trim(); + if (!value) { + return ""; + } + + const watchMatch = value.match(/[?&]v=([^&]+)/); + const shortMatch = value.match(/youtu\.be\/([^?&/]+)/); + const embedMatch = value.match(/embed\/([^?&/]+)/); + + return decodeURIComponent(watchMatch?.[1] || shortMatch?.[1] || embedMatch?.[1] || value); + }; + + const getYoutubeThumbnailUrl = (videoId) => { + const id = getYoutubeVideoId(videoId); + return id ? `https://i.ytimg.com/vi/${encodeURIComponent(id)}/hqdefault.jpg` : ""; + }; + const formatDateShort = (dateObj) => { const day = String(dateObj.getDate()).padStart(2, "0"); const month = String(dateObj.getMonth() + 1).padStart(2, "0"); @@ -499,6 +548,9 @@ document.addEventListener("DOMContentLoaded", () => { const buildMovieProgram = () => { movieProgram = movieCatalog.map((movie, movieIndex) => ({ ...movie, + trailerYoutubeId: getYoutubeVideoId(movie.trailerYoutubeId || movie.trailerUrl || ""), + trailerThumbnail: getYoutubeThumbnailUrl(movie.trailerYoutubeId || movie.trailerUrl || "") || movie.backdrop || movie.poster, + actors: Array.isArray(movie.actors) ? movie.actors : [], schedule: buildScheduleForMovie(movieIndex) })); heroItems = movieProgram.slice(0, 5); @@ -575,7 +627,7 @@ document.addEventListener("DOMContentLoaded", () => { } ui.nowRunningRow.innerHTML = movieProgram.map((movie, index) => ` -
+
${escapeHtml(movie.title)}

${escapeHtml(movie.title)}

@@ -637,7 +689,7 @@ document.addEventListener("DOMContentLoaded", () => {

${escapeHtml(movie.title)}

- ${movie.duration} Min. | ${escapeHtml(movie.genre)} | FSK: ${renderFskMetaLogo(movie)} + ${getMovieDurationLabel(movie)} | ${escapeHtml(movie.genre)} | FSK: ${renderFskMetaLogo(movie)}
Kinosaal @@ -663,6 +715,169 @@ document.addEventListener("DOMContentLoaded", () => { }); }; + const ensureMovieDetailOverlay = () => { + let overlay = document.getElementById("movie-detail-overlay"); + if (overlay) { + return overlay; + } + + overlay = document.createElement("div"); + overlay.id = "movie-detail-overlay"; + overlay.className = "movie-detail-overlay hidden"; + document.body.appendChild(overlay); + return overlay; + }; + + const renderMovieDetailScheduleRows = (programIndex, dayIndex) => { + const movie = movieProgram[programIndex]; + const day = movie?.schedule?.[dayIndex]; + const body = document.getElementById("movie-detail-schedule-body"); + if (!movie || !day || !body) { + return; + } + + body.innerHTML = day.showings.map((showing) => ` + + `).join(""); + }; + + const buildMovieDetailContent = (movie, programIndex) => { + const trailerId = getYoutubeVideoId(movie.trailerYoutubeId); + const trailerThumb = getYoutubeThumbnailUrl(trailerId) || movie.backdrop || movie.poster; + const actorMarkup = movie.actors.length + ? movie.actors.map((actor) => `${escapeHtml(actor)}`).join("") + : `

Noch keine Schauspieler hinterlegt.

`; + const dayTabs = movie.schedule.map((day, dayIndex) => ` + + `).join(""); + + return ` +
+ +
+
+
+
+ ${escapeHtml(movie.title)} +
+
+

Aktueller Film

+

${escapeHtml(movie.title)}

+
+ ${getMovieDurationLabel(movie)} + ${escapeHtml(movie.genre)} + ${renderFskMetaLogo(movie)} +
+

${escapeHtml(movie.description)}

+
+
+ +
+
+

Trailer

+ +
+
+ +
+
+ +
+
+

Schauspieler

+ Cast +
+
${actorMarkup}
+
+ +
+
+

Spielzeiten

+ Nur für diesen Film +
+
${dayTabs}
+
+
+ TagKinosaalUhrzeit +
+
+
+
+
+
+ `; + }; + + const closeMovieDetail = () => { + const overlay = document.getElementById("movie-detail-overlay"); + if (!overlay || overlay.classList.contains("hidden")) { + return; + } + + const targetCard = activeMovieDetail.sourceCard?.isConnected ? activeMovieDetail.sourceCard : null; + const rect = targetCard?.getBoundingClientRect(); + + overlay.classList.remove("is-open"); + overlay.classList.add("is-closing"); + + if (rect) { + overlay.style.left = `${rect.left}px`; + overlay.style.top = `${rect.top}px`; + overlay.style.width = `${rect.width}px`; + overlay.style.height = `${rect.height}px`; + overlay.style.borderRadius = getComputedStyle(targetCard).borderRadius || "20px"; + } + + setTimeout(() => { + overlay.className = "movie-detail-overlay hidden"; + overlay.removeAttribute("style"); + overlay.innerHTML = ""; + document.body.style.overflow = "auto"; + activeMovieDetail = { index: null, sourceCard: null }; + }, 430); + }; + + const openMovieDetail = (programIndex, sourceCard) => { + const movie = movieProgram[programIndex]; + if (!movie || !sourceCard) { + return; + } + + const overlay = ensureMovieDetailOverlay(); + const rect = sourceCard.getBoundingClientRect(); + activeMovieDetail = { index: programIndex, sourceCard }; + + overlay.innerHTML = buildMovieDetailContent(movie, programIndex); + overlay.className = "movie-detail-overlay"; + overlay.style.left = `${rect.left}px`; + overlay.style.top = `${rect.top}px`; + overlay.style.width = `${rect.width}px`; + overlay.style.height = `${rect.height}px`; + overlay.style.borderRadius = getComputedStyle(sourceCard).borderRadius || "20px"; + document.body.style.overflow = "hidden"; + + renderMovieDetailScheduleRows(programIndex, 0); + + requestAnimationFrame(() => { + overlay.classList.add("is-open"); + overlay.style.left = "0px"; + overlay.style.top = "0px"; + overlay.style.width = "100vw"; + overlay.style.height = "100vh"; + overlay.style.borderRadius = "0px"; + }); + }; + const initRevealAnimations = () => { const revealElements = Array.from(document.querySelectorAll(".reveal-on-scroll")); if (!revealElements.length) { @@ -749,28 +964,107 @@ document.addEventListener("DOMContentLoaded", () => { const bindProgramActions = () => { views.moviesGrid?.addEventListener("click", (event) => { const trigger = event.target.closest(".open-program-btn"); - if (!trigger) { + if (trigger) { + const programIndex = Number(trigger.dataset.programIndex || 0); + showMovieList(programIndex); return; } - const programIndex = Number(trigger.dataset.programIndex || 0); - showMovieList(programIndex); + const runningCard = event.target.closest(".running-poster"); + if (runningCard) { + const programIndex = Number(runningCard.dataset.programIndex || 0); + openMovieDetail(programIndex, runningCard); + } + }); + + views.moviesGrid?.addEventListener("keydown", (event) => { + if (event.key !== "Enter" && event.key !== " ") { + return; + } + + if (event.target.closest(".open-program-btn")) { + return; + } + + const runningCard = event.target.closest(".running-poster"); + if (!runningCard) { + return; + } + + event.preventDefault(); + const programIndex = Number(runningCard.dataset.programIndex || 0); + openMovieDetail(programIndex, runningCard); }); ui.movieProgramList?.addEventListener("click", (event) => { const dayButton = event.target.closest(".program-day-tab"); - if (!dayButton) { + if (dayButton) { + const programIndex = Number(dayButton.dataset.programIndex || 0); + const dayIndex = Number(dayButton.dataset.dayIndex || 0); + + const tabRow = dayButton.closest(".program-day-tabs"); + tabRow?.querySelectorAll(".program-day-tab").forEach((tab) => tab.classList.remove("active")); + dayButton.classList.add("active"); + + renderScheduleRows(programIndex, dayIndex); return; } - const programIndex = Number(dayButton.dataset.programIndex || 0); - const dayIndex = Number(dayButton.dataset.dayIndex || 0); + if (event.target.closest("button, a, input, select, textarea")) { + return; + } - const tabRow = dayButton.closest(".program-day-tabs"); - tabRow?.querySelectorAll(".program-day-tab").forEach((tab) => tab.classList.remove("active")); - dayButton.classList.add("active"); + const movieCard = event.target.closest(".program-card"); + if (!movieCard) { + return; + } - renderScheduleRows(programIndex, dayIndex); + const programIndex = Number(movieCard.dataset.programIndex || 0); + openMovieDetail(programIndex, movieCard); + }); + }; + + const bindMovieDetailOverlay = () => { + document.addEventListener("click", (event) => { + const overlay = event.target.closest("#movie-detail-overlay"); + if (!overlay) { + return; + } + + if (event.target.closest(".movie-detail-close")) { + closeMovieDetail(); + return; + } + + const dayButton = event.target.closest(".movie-detail-day-tab"); + if (dayButton) { + const dayIndex = Number(dayButton.dataset.dayIndex || 0); + overlay.querySelectorAll(".movie-detail-day-tab").forEach((tab) => tab.classList.remove("active")); + dayButton.classList.add("active"); + renderMovieDetailScheduleRows(activeMovieDetail.index, dayIndex); + return; + } + + const trailerPreview = event.target.closest(".movie-trailer-preview"); + if (trailerPreview) { + const trailerId = getYoutubeVideoId(trailerPreview.dataset.trailerId); + const frame = trailerPreview.closest(".movie-trailer-frame"); + if (!trailerId || !frame) { + trailerPreview.classList.add("is-missing"); + trailerPreview.querySelector("strong").textContent = "Trailer-ID in main.js hinterlegen"; + return; + } + + frame.innerHTML = ` + + `; + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closeMovieDetail(); + } }); }; @@ -872,6 +1166,10 @@ document.addEventListener("DOMContentLoaded", () => { const loginEmailInput = document.getElementById("login-email"); const loginPasswordInput = document.getElementById("login-password"); + if (typeof initAccountFieldValidation === "function") { + initAccountFieldValidation(); + } + const openModal = (modal) => modal?.classList.remove("hidden"); const closeModal = (modal) => modal?.classList.add("hidden"); const triggerLogin = () => { @@ -882,6 +1180,24 @@ document.addEventListener("DOMContentLoaded", () => { }; document.getElementById("btn-open-register")?.addEventListener("click", () => { + registerModal?.querySelectorAll("input, select").forEach((input) => { + if (input.type !== "hidden" && input.tagName !== "SELECT") { + input.value = ""; + } + if (input.tagName === "SELECT") { + input.value = "Deutschland"; + } + if (input.id === "reg-salutation") { + input.value = ""; + } + input.dataset.touched = ""; + if (typeof clearFieldError === "function") { + clearFieldError(input); + } + }); + registerModal?.querySelectorAll(".gender-selector button").forEach((button) => { + button.classList.remove("active"); + }); openModal(registerModal); }); @@ -898,6 +1214,10 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById("btn-login-account")?.addEventListener("click", triggerLogin); [loginEmailInput, loginPasswordInput].forEach((input) => { + input?.addEventListener("input", () => { + loginError?.classList.add("hidden"); + }); + input?.addEventListener("keydown", (event) => { if (event.key !== "Enter") { return; @@ -1134,6 +1454,7 @@ document.addEventListener("DOMContentLoaded", () => { initThemeToggle(); bindNavigation(); bindProgramActions(); + bindMovieDetailOverlay(); bindHomeInfoNavigation(); bindAccountActions(); bindGlobalDocumentClicks();