diff options
| author | ivar <i@oiee.no> | 2025-10-26 11:33:38 +0100 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2025-10-26 11:33:38 +0100 |
| commit | 2e8ad7dc6d49361c6ee00dc628e119c0c06e2779 (patch) | |
| tree | becc4cde99eff6ed1d168f9e9c454c0f2bbbee9d /VegaData/wwwroot | |
| parent | 8de1187b627625b94ef8088de3e9255ccd17baf4 (diff) | |
| download | vegadata-2e8ad7dc6d49361c6ee00dc628e119c0c06e2779.tar.xz vegadata-2e8ad7dc6d49361c6ee00dc628e119c0c06e2779.zip | |
.
Diffstat (limited to 'VegaData/wwwroot')
| -rw-r--r-- | VegaData/wwwroot/17.png | bin | 0 -> 5334 bytes | |||
| -rw-r--r-- | VegaData/wwwroot/framework.js | 166 | ||||
| -rw-r--r-- | VegaData/wwwroot/index.html | 139 | ||||
| -rw-r--r-- | VegaData/wwwroot/index.js | 152 | ||||
| -rw-r--r-- | VegaData/wwwroot/version.txt | 1 |
5 files changed, 458 insertions, 0 deletions
diff --git a/VegaData/wwwroot/17.png b/VegaData/wwwroot/17.png Binary files differnew file mode 100644 index 0000000..7895530 --- /dev/null +++ b/VegaData/wwwroot/17.png diff --git a/VegaData/wwwroot/framework.js b/VegaData/wwwroot/framework.js new file mode 100644 index 0000000..d1e6cc6 --- /dev/null +++ b/VegaData/wwwroot/framework.js @@ -0,0 +1,166 @@ +const targetMap = new WeakMap(); +let activeEffect = null; + +const root = document.getElementById("root"); + +function createElement(tagName, props = {}, children = []) { + const el = document.createElement(tagName); + + for (const [key, value] of Object.entries(props || {})) { + if (key.startsWith("on") && typeof value === "function") { + el.addEventListener(key.substring(2).toLowerCase(), value); + } else if (key === "style" && typeof value === "object") { + Object.assign(el.style, value); + } else { + el.setAttribute(key, value); + } + } + + const appendChild = (child) => { + if (Array.isArray(child)) { + child.forEach(appendChild); + } else if (typeof child === "function") { + const placeholder = document.createTextNode(""); + el.appendChild(placeholder); + e(() => { + placeholder.textContent = child() ?? ""; + }); + } else if (child instanceof Node) { + el.appendChild(child); + } else if (child != null) { + el.appendChild(document.createTextNode(String(child))); + } + }; + + children.forEach(appendChild); + return el; +} + +function track(target, key) { + if (!activeEffect) { + return; + } + let depsMap = targetMap.get(target); + if (!depsMap) { + depsMap = new Map(); + targetMap.set(target, depsMap); + } + + let dep = depsMap.get(key); + if (!dep) { + dep = new Set(); + depsMap.set(key, dep); + } + + dep.add(activeEffect); +} + +function trigger(target, key) { + const depsMap = targetMap.get(target); + if (!depsMap) { + return; + } + + const dep = depsMap.get(key); + if (dep) { + dep.forEach((effect) => effect()); + } +} + +//** +// Create a reactive value, the value is at .value. +// To use this in element props you need to supply the .value read as a function. +// */ +export function r(target) { + target = { value: target }; + return new Proxy(target, { + get(obj, key, receiver) { + track(obj, key); + return Reflect.get(obj, key, receiver); + }, + set(obj, key, value, receiver) { + const result = Reflect.set(obj, key, value, receiver); + trigger(obj, key); + return result; + }, + }); +} + +//** +// Run code when value changes +// */ +export function e(fn) { + let active = true; + + const runner = () => { + if (active) { + activeEffect = runner; + fn(); + activeEffect = null; + } + }; + + runner(); + + runner.stop = () => { + active = false; + }; + + return runner; +} + +export function gqp(key) { + return new URLSearchParams(location.search).get(key) +} + +export function sqp(query) { + if ('URLSearchParams' in window) { + const url = new URL(window.location) + for (const key of Object.keys(query)) { + const value = encodeURIComponent(query[key]) + if (!value || value === "") url.searchParams.delete(key) + else url.searchParams.set(key, value) + } + history.pushState(null, '', url); + } +} + +//** +// Combine elements +// */ +export function c(a, b) { + const normalize = (x) => (x == null ? [] : Array.isArray(x) ? x : [x]); + + return [...normalize(a), ...normalize(b)]; +} + +//** +// Mount element to a target (target is #root by default) +// */ +export async function m(component, target = root) { + if (typeof component === "function") { + component = await component(); + } + if (Array.isArray(component)) { + target.append(...component); + } else { + target.appendChild(component); + } +} + +export function css(styleObject) { + return Object.entries(styleObject).map(([key, value]) => { + const kebabKey = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); + return `${kebabKey}: ${value}`; + }).join("; "); +} + +//** +// Create element +// */ +export function t(name, props, ...children) { + if (typeof name === "function") { + return name({ ...props, children }); + } + return createElement(name, props, children); +} diff --git a/VegaData/wwwroot/index.html b/VegaData/wwwroot/index.html new file mode 100644 index 0000000..23fbb8e --- /dev/null +++ b/VegaData/wwwroot/index.html @@ -0,0 +1,139 @@ +<!doctype html> +<html lang="nb"> + +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <style> + * { + box-sizing: border-box; + } + + :root { + --bg: beige; + } + + #search { + padding: 10px 12px; + min-width: 300px; + } + + html, + body { + margin: 0; + padding: 0; + background: var(--bg); + font-family: Palatino, Georgia, serif; + } + + li:target, + .activeShow { + padding-block: 2rem; + padding-inline: 1rem; + background-image: url("./17.png"); + + div:first-of-type { + padding-block: .4rem; + padding-inline-start: .5rem; + background-color: var(--bg); + } + } + + #ulShows { + height: 80vh; + overflow-x: auto; + } + + #times { + padding-left: 15px; + } + + main { + padding-inline: 10px; + } + + .show { + margin-bottom: 1rem; + width: 100%; + + .title { + position: sticky; + top: 0; + width: 100%; + display: flex; + align-items: baseline; + justify-content: space-between; + background: bisque; + padding-inline: 3px; + padding-bottom: 5px; + + h2 { + font-weight: 600; + overflow-wrap: break-word; + hyphens: manual; + margin-top: 0; + margin-bottom: 0; + } + } + } + + .time { + font-style: normal; + font-size: 1rem; + text-decoration: none; + font-weight: 400; + margin-bottom: 15px; + + .date::first-letter { + border-left: 2px solid gold; + padding-left: 2px; + } + + div { + display: flex; + flex-direction: column; + + span:first-of-type { + font-size: 1.2rem; + } + } + + .actions { + display: flex; + flex-direction: row; + gap: 1rem; + } + } + + h1 { + margin: 0; + padding: 10px; + } + + ul { + list-style: none; + margin: 10px 0 0 0; + padding: 0; + } + </style> + <title>Vega eller</title> +</head> + +<body> + <h1>Bli med på vega?</h1> + <main> + <button id="renderShowsBtn">tilbakestill</button> <br> + <input type="search" name="q1" id="search" placeholder="søk"> + <ul id="ulShows"></ul> + </main> + <script type="importmap"> + { + "imports": { + "temporal-polyfill": "https://esm.sh/temporal-polyfill@0.3.0" + } + } + </script> + <script type="module" src="index.js"></script> +</body> + +</html>
\ No newline at end of file diff --git a/VegaData/wwwroot/index.js b/VegaData/wwwroot/index.js new file mode 100644 index 0000000..45af7f2 --- /dev/null +++ b/VegaData/wwwroot/index.js @@ -0,0 +1,152 @@ +import { Temporal } from "temporal-polyfill"; +import { gqp, sqp, t } from "./framework.js"; + +let _; + +async function getShows() { + if (_) return _; + const response = await fetch("/shows"); + _ = await response.json(); + return _; +} + +const ulShows = document.getElementById("ulShows"); +const search = document.getElementById("search"); +const renderShowsBtn = document.getElementById("renderShowsBtn"); + +renderShowsBtn.addEventListener("click", () => { + renderShows(""); + search.value = ""; +}); + +function dateString(date, small = false) { + const instant = Temporal.Instant.from(`${date}Z`) + const stringOptions = { + weekday: "long", + hour: "2-digit", + timeZone: "UTC", + minute: "2-digit", + month: "long", + calendar: "gregory", + day: "numeric" + } + + if (small) { + return instant.toLocaleString("nb-NO", stringOptions); + } + + return instant.toLocaleString("nb-NO", { + year: "numeric", + era: "long", + ...stringOptions + }); +} + +function copyLink(t, e) { + const initialInnerText = t.target.innerText; + if ("clipboard" in navigator) { + navigator.clipboard.writeText(`${location.origin}/index.html#${urlId(e)}`) + t.target.innerText = `${initialInnerText} ✓` + setTimeout(() => { + t.target.innerText = initialInnerText + }, 1000) + } +} + +function vegascene(showing) { + if (showing.movieMainVersion.startsWith("KUL")) return `https://www.vegascene.no/teater/${showing.movieMainVersion}` + return `https://www.vegascene.no/film/${showing.movieMainVersion}` +} + +function urlId(e) { + return `${e.movieVersion}${e.startDateTime}`.replaceAll("-", "").replaceAll(" ", "").replaceAll(":", "") +} + +async function renderShows(query, rendered) { + query = query?.trim(); + sqp({ q: query ?? "" }) + + const shows = (await getShows()).reduce((acc, curr) => { + const key = curr.title; + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(curr); + return acc; + }, {}); + + async function share(show) { + const shareData = { + title: `${show.title} ${dateString(show.startDateTime, true)} på vega`, + url: `${location.origin}/index.html#${urlId(show)}`, + } + await navigator.share(shareData); + } + + const lis = []; + + for (const showKey of Object.keys(shows).sort((a, b) => a.localeCompare(b))) { + const times = shows[showKey].sort((a, b) => Temporal.PlainDate.compare(Temporal.PlainDate.from(a.startDateTime), Temporal.PlainDate.from(b.startDateTime))) + if (query) { + const words = [showKey.toLowerCase()] + for (const time of times) { + words.push(dateString(time.startDateTime)) + words.push(time.scene) + words.push(time.tags.join(" ")) + } + if (!words.some((word) => word.match(query.toLowerCase()))) continue; + } + + lis.push( + t("li", { class: "show", id: showKey }, [ + t("div", { class: "title" }, [ + t("h2", { class: "italic" }, showKey), + t("a", { href: vegascene(shows[showKey][0]) }, "(Åpne på vegascene.no)") + ]), + t("ul", { id: "times" }, [ + t("li", undefined, + times.filter(e => e.ticketUrl !== "").map(e => { + let tagLine = `${e.scene} - ${[e.type, ...e.tags].join(", ")}` + tagLine = tagLine[0].toUpperCase() + tagLine.slice(1) + let dateLine = dateString(e.startDateTime) + dateLine = dateLine[0].toUpperCase() + dateLine.slice(1) + return t("li", { class: `time time-${e.id}`, id: urlId(e) }, [ + t("div", undefined, [ + t("span", { title: e.startDateTime, class: "date" }, dateLine), + t("span", undefined, tagLine), + t("div", { class: "actions" }, [ + t("a", { href: e.ticketUrl }, "Billetter"), + "share" in navigator ? t("button", { onclick: () => share(e) }, "Del tid") : null, + t("button", { onclick: (t) => copyLink(t, e) }, "Kopier lenke") + ]) + ]) + ]) + })) + ]) + ]) + ) + } + + if (!lis.length) ulShows.replaceChildren(...[t("i", undefined, "Dessverre")]) + else ulShows.replaceChildren(...lis); + + const interval = setInterval(() => { + const element = document.querySelector("#ulShows"); + if (element.childNodes.length > 0) { + clearInterval(interval); + rendered(); + } + }, 50); +} + +renderShows(search.value, () => { + if (gqp("q")) search.value = gqp("q") + search.addEventListener("input", e => renderShows(e.currentTarget.value)); + const id = location.href.indexOf("#") !== -1 ? location.href.substring(location.href.indexOf("#")) : "" + if (id !== "") { + const target = document.querySelector(id) + target.scrollIntoView({ behavior: "smooth", block: "center" }) + target.classList.add("activeShow") + document.querySelectorAll(".time").forEach(el => el.id !== id.slice(1) && el.classList.remove("activeShow")) + } +});
\ No newline at end of file diff --git a/VegaData/wwwroot/version.txt b/VegaData/wwwroot/version.txt new file mode 100644 index 0000000..a97cee7 --- /dev/null +++ b/VegaData/wwwroot/version.txt @@ -0,0 +1 @@ +v29 |
