- - -
-- Dernier passage dans - () -
-diff --git a/lol/components/marquee.js b/lol/components/marquee.js
new file mode 100644
index 0000000..a0d1eb6
--- /dev/null
+++ b/lol/components/marquee.js
@@ -0,0 +1,205 @@
+const TEMPLATE = document.createElement("template");
+TEMPLATE.innerHTML = `
+
+ Dernier passage + () +
+` class NextBusElement extends HTMLElement { #stop + #autoupdateTimeout get stopId(){ return this.#stop @@ -9,15 +29,118 @@ class NextBusElement extends HTMLElement { set stopId(value){ this.#stop = value + } + + handleAutoUpdate(){ + clearTimeout(this.#autoupdateTimeout) this.updateContent() + this.#autoupdateTimeout = setTimeout(this.handleAutoUpdate.bind(this), 60000 + ((Math.random() * 10000) - 5000)) } connectedCallback(){ this.replaceChildren(TEMPLATE.content.cloneNode(true)) + this.handleAutoUpdate() } - updateContent(){ + async updateContent(){ + try { + let nextPassage = await getNextPassage(this.stopId) + let time = nextPassage.time + + if(!time){ + time = nextPassage.line.timetable.find(it => it.time.getTime() > Date.now())?.time + } + + if(!time){ + return + } + + this.querySelector(".line-picto").src = nextPassage.line.picto + this.querySelector(".line-name").textContent = nextPassage.line.displayName + this.querySelector(".stop-name").textContent = nextPassage.displayName + + let timeFormatter = new Intl.DateTimeFormat("fr-FR", { + timeStyle: "short" + }); + let timeEl = this.querySelector(".stop-passage-time") + timeEl.textContent = timeFormatter.format(time) + + let relativeTimeFOrmatter = new Intl.RelativeTimeFormat("fr-FR", { + style: "short", + numeric: "auto" + }); + + let time_minutes = Math.floor((time.getTime() - Date.now()) / 60000) + + let relative_time_str; + if(time_minutes > 60) { + let time_hours = Math.floor(time_minutes / 60) + time_minutes -= time_hours * 60 + + relative_time_str = relativeTimeFOrmatter.format( + time_hours, + "hours" + ) + if(time_minutes > 0){ + relative_time_str += ` et ${Math.abs(time_minutes)} min` + } + } else { + relative_time_str = relativeTimeFOrmatter.format( + time_minutes, + "minutes" + ) + } + this.querySelector(".stop-passage-time-relative").textContent = relative_time_str + + { + let last_passage = nextPassage.getLastPassageTime() + if(last_passage){ + let time = last_passage.time + let timeEl = this.querySelector(".last-passage-time") + timeEl.textContent = timeFormatter.format(time) + + let time_minutes = Math.floor((time.getTime() - Date.now()) / 60000) + + let relative_time_str; + if(time_minutes > 60) { + let time_hours = Math.floor(time_minutes / 60) + time_minutes -= time_hours * 60 + + relative_time_str = relativeTimeFOrmatter.format( + time_hours, + "hours" + ) + if(time_minutes > 0){ + relative_time_str += ` et ${Math.abs(time_minutes)} min` + } + } else { + relative_time_str = relativeTimeFOrmatter.format( + time_minutes, + "minutes" + ) + } + this.querySelector(".last-passage-time-relative").textContent = relative_time_str + this.querySelector(".last-passage").style.display = "" + this.querySelector(".last-passage").classList.toggle("warning", (time.getTime() - Date.now()) <= LAST_PASSAGE_WARNING_THREASHOLD_MS) + } else { + this.querySelector(".last-passage").style.display = "none" + } + + if(last_passage && last_passage.time.getTime() == time.getTime()){ + this.querySelector(".next-and-last").style.display = "" + this.querySelector(".next-passage").style.display = "none" + } else { + this.querySelector(".next-and-last").style.display = "none" + this.querySelector(".next-passage").style.display = "" + } + } + + this.style.display = "" + } catch(e){ + this.style.display = "none" + throw e + } } static observedAttributes = ["stop-id"] @@ -31,4 +154,4 @@ class NextBusElement extends HTMLElement { } } -customElements.define("camp-next-bus", NextBusElement); \ No newline at end of file +customElements.define("gavle-next-bus", NextBusElement); \ No newline at end of file diff --git a/lol/components/velov-station.js b/lol/components/velov-station.js new file mode 100644 index 0000000..62b79ef --- /dev/null +++ b/lol/components/velov-station.js @@ -0,0 +1,62 @@ +import { getNextPassage, getVelovBikeState } from "../tcl.js"; + +const TEMPLATE = document.createElement("template"); +TEMPLATE.innerHTML = ` +vélos disponibles
+` + +class VelovStationElement extends HTMLElement { + #station + #autoupdateTimeout + + get stationId(){ + return this.#station + } + + set stationId(value){ + this.#station = value + } + + handleAutoUpdate(){ + clearTimeout(this.#autoupdateTimeout) + this.updateContent() + this.#autoupdateTimeout = setTimeout(this.handleAutoUpdate.bind(this), 60000 + ((Math.random() * 10000) - 5000)) + } + + connectedCallback(){ + this.replaceChildren(TEMPLATE.content.cloneNode(true)) + this.handleAutoUpdate() + } + + async updateContent(){ + try { + if(!this.stationId) + throw new Error("Missing station id") + + let station_data = await getVelovBikeState(this.stationId) + + this.querySelector(".station-name").textContent = station_data.name + this.querySelector(".bike-available").textContent = station_data.properties.available_bikes + this.querySelector(".bike-available").parentElement.classList.toggle("plurial-content", station_data.properties.available_bikes > 1); + + this.style.display = "" + } catch(e){ + this.style.display = "none" + throw e + } + } + + static observedAttributes = ["station-id"] + + attributeChangedCallback(name, oldVal, newVal){ + switch(name){ + case "station-id": + this.stationId = newVal + break; + } + } +} + +customElements.define("gavle-velov-station", VelovStationElement); \ No newline at end of file diff --git a/lol/index.html b/lol/index.html index 647209f..d5410ab 100644 --- a/lol/index.html +++ b/lol/index.html @@ -64,135 +64,165 @@ mix-blend-mode: darken; } - #next-line { - font-family: monospace; - color: white; - - box-sizing: border-box; - padding: 15px; - font-size: 20px; - - display: grid; - gap: 10px; - grid-template-columns: min-content 1fr; - grid-template-rows: min-content 1fr; + #next-bus-ribbon { position: absolute; - top: 0; + top: 50px; left: 0; width: 100%; - background: var(--tcl-red); - border: none; - z-index: 100; + } - transform: translateY(0); - transition: ease-out 0.8s transform; + gavle-marquee { + --markee-speed: 20; + --markee-interaction-pause-delay: 1s; + + width: 100vw; + + & > gavle-next-bus { + max-width: 75vw; + } + + & > * { + padding-left: 1.5em; + } + } + + gavle-next-bus { + + display: grid; + color: white; + font-family: sans-serif; + font-size: 1.5em; + + grid-template-columns: min-content 1fr; + grid-template-rows: 1fr min-content; + column-gap: 0.75em; + row-gap: 5px; + + & > :not(img) { + mix-blend-mode: exclusion; + } + + &[hidden] { + display: none; + } & > * { grid-column: 2; margin: 0; } - & > h1 { - font-size: 1.3em; - } - - & > img:first-child { + & > img { grid-column: 1; - grid-row: 1; - height: 1.3em; - align-self: center; + grid-row: 1 / 3; + align-self: flex-start; + } + + & h1 { + font-size: inherit; + } + + & h1 small { + font-size: inherit; + font-weight: normal; + display: block; + } + + & img { + height: 2em; + max-width: 3em; + margin-top: -5px; + + --outline-size: 1px; + --outline-color: white; + filter: + drop-shadow(0px var(--outline-size) 0px var(--outline-color)) + drop-shadow(var(--outline-size) 0px 0px var(--outline-color)) + drop-shadow(var(--outline-size) var(--outline-size) 0px var(--outline-color)) + drop-shadow(0px calc(var(--outline-size) * -1) 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) 0px 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) calc(var(--outline-size) * -1) 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) var(--outline-size) 0px var(--outline-color)) + drop-shadow(var(--outline-size) calc(var(--outline-size) * -1) 0px var(--outline-color)) + ; + } + + & p { + font-size: 0.8em; + } + + .last-passage.warning { + color: white; + + --outline-size: 1px; + --outline-color:#e2001a; + filter: + drop-shadow(0px var(--outline-size) 0px var(--outline-color)) + drop-shadow(var(--outline-size) 0px 0px var(--outline-color)) + drop-shadow(var(--outline-size) var(--outline-size) 0px var(--outline-color)) + drop-shadow(0px calc(var(--outline-size) * -1) 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) 0px 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) calc(var(--outline-size) * -1) 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) var(--outline-size) 0px var(--outline-color)) + drop-shadow(var(--outline-size) calc(var(--outline-size) * -1) 0px var(--outline-color)) + ; + + text-transform: uppercase; + mix-blend-mode: normal; + font-weight: normal; } } - #next-line[hidden] { - transform: translateY(-100%); - transition-timing-function: ease-in; - } - - #last-passage { - position: absolute; - bottom: 15px; - right: 15px; - max-width: calc(100% - 30px); - max-height: calc(100% - 30px); - - font-size: 20px; - font-family: monospace; + gavle-velov-station { + display: grid; color: white; + font-family: sans-serif; + font-size: 1.5em; - & > * { - background: var(--tcl-red); - padding: 15px; + grid-template-columns: min-content 1fr; + grid-template-rows: 1fr min-content; + column-gap: 0.75em; + row-gap: 5px; + + & > img { + grid-column: 1; + grid-row: 1 / 3; + align-self: flex-start; + } + + & h1 { + font-size: inherit; margin: 0; - margin-left: auto; + padding-right: 3em; + width: fit-content; } - & .last-passage { - - display: grid; - - &[hidden] { - display: none; - } - - grid-template-columns: min-content 1fr; - grid-template-rows: 1fr min-content; - column-gap: 10px; - row-gap: 5px; - - & > * { - grid-column: 2; - margin: 0; - } - - & > img { - grid-column: 1; - grid-row: 1 / 3; - align-self: flex-start; - } - - & h1 { - font-size: inherit; - } - - & h1 small { - font-size: inherit; - font-weight: normal; - display: inline-block; - } - - & img { - height: 2em; - max-width: 3em; - } - } - - - ul, ol { - padding-left: calc(15px + 2em); - margin: 0.75em 0; - padding-left: 1em; - - & > li:not(:last-child) { - margin-bottom: 0.75em; - } - } - } - - #next-line:not([hidden]) ~ #last-passage { - color: rgba(255, 255, 255, 0.5); - & img { - opacity: 0.5 + height: 1.3em; + max-width: 3em; + margin-top: 5px; + + --outline-size: 1px; + --outline-color: white; + filter: + drop-shadow(0px var(--outline-size) 0px var(--outline-color)) + drop-shadow(var(--outline-size) 0px 0px var(--outline-color)) + drop-shadow(var(--outline-size) var(--outline-size) 0px var(--outline-color)) + drop-shadow(0px calc(var(--outline-size) * -1) 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) 0px 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) calc(var(--outline-size) * -1) 0px var(--outline-color)) + drop-shadow(calc(var(--outline-size) * -1) var(--outline-size) 0px var(--outline-color)) + drop-shadow(var(--outline-size) calc(var(--outline-size) * -1) 0px var(--outline-color)) + ; } - } - #next-line ~ #last-passage { - transition: color 0.5s steps(5); + & p { + font-size: 0.8em; + margin: 0; + } - & img { - transition: opacity 0.5s steps(5); + & > :not(img) { + mix-blend-mode: exclusion; } } @@ -208,7 +238,7 @@ font-family: "bianzhidai nobg pearl"; position: fixed; bottom: 5px; - left: 15px; + right: 15px; font-size: 17vh; z-index: 1000; color: white; @@ -219,43 +249,83 @@ animation: deux-points steps(4) 1s infinite; } } + + .plurial { + display: none; + } + + .plurial-content .plurial { + display: inline; + } - -- : - prochain départ - () -
-- Dernier passage dans - () -
-