From 023dcd3a1d7ba8aacd4f1ea08db227db131faeff Mon Sep 17 00:00:00 2001 From: Vincent Boutour Date: Thu, 3 Nov 2022 22:23:46 +0100 Subject: [PATCH] refactor: extracting some file to reduce pressure on minifier Signed-off-by: Vincent Boutour --- cmd/fibr/config.go | 2 +- cmd/fibr/static/scripts/async-image.js | 132 ++++++++++ cmd/fibr/static/scripts/map.js | 129 ++++++++++ cmd/fibr/static/scripts/navigation.js | 62 +++++ cmd/fibr/static/styles/main.css | 314 ++++++++++++++++++++++++ cmd/fibr/templates/async-image.html | 135 +---------- cmd/fibr/templates/header.html | 2 +- cmd/fibr/templates/map-modal.html | 118 +-------- cmd/fibr/templates/navigation.html | 65 +---- cmd/fibr/templates/style.html | 322 ------------------------- 10 files changed, 646 insertions(+), 635 deletions(-) create mode 100644 cmd/fibr/static/scripts/async-image.js create mode 100644 cmd/fibr/static/scripts/map.js create mode 100644 cmd/fibr/static/scripts/navigation.js create mode 100644 cmd/fibr/static/styles/main.css delete mode 100644 cmd/fibr/templates/style.html diff --git a/cmd/fibr/config.go b/cmd/fibr/config.go index cc699ada..6a2d90b6 100644 --- a/cmd/fibr/config.go +++ b/cmd/fibr/config.go @@ -64,7 +64,7 @@ func newConfig() (configuration, error) { logger: logger.Flags(fs, "logger"), tracer: tracer.Flags(fs, "tracer"), prometheus: prometheus.Flags(fs, "prometheus", flags.NewOverride("Gzip", false)), - owasp: owasp.Flags(fs, "", flags.NewOverride("FrameOptions", "SAMEORIGIN"), flags.NewOverride("Csp", "default-src 'self'; base-uri 'self'; script-src 'self' 'httputils-nonce' unpkg.com/webp-hero@0.0.2/dist-cjs/ unpkg.com/leaflet@1.9.2/dist/ unpkg.com/leaflet.markercluster@1.5.1/; style-src 'httputils-nonce' unpkg.com/leaflet@1.9.2/dist/ unpkg.com/leaflet.markercluster@1.5.1/; img-src 'self' data: a.tile.openstreetmap.org b.tile.openstreetmap.org c.tile.openstreetmap.org")), + owasp: owasp.Flags(fs, "", flags.NewOverride("FrameOptions", "SAMEORIGIN"), flags.NewOverride("Csp", "default-src 'self'; base-uri 'self'; script-src 'self' 'httputils-nonce' unpkg.com/webp-hero@0.0.2/dist-cjs/ unpkg.com/leaflet@1.9.2/dist/ unpkg.com/leaflet.markercluster@1.5.1/; style-src 'self' 'httputils-nonce' unpkg.com/leaflet@1.9.2/dist/ unpkg.com/leaflet.markercluster@1.5.1/; img-src 'self' data: a.tile.openstreetmap.org b.tile.openstreetmap.org c.tile.openstreetmap.org")), basic: basicMemory.Flags(fs, "auth", flags.NewOverride("Profiles", "1:admin")), crud: crud.Flags(fs, ""), share: share.Flags(fs, "share"), diff --git a/cmd/fibr/static/scripts/async-image.js b/cmd/fibr/static/scripts/async-image.js new file mode 100644 index 00000000..2186cb12 --- /dev/null +++ b/cmd/fibr/static/scripts/async-image.js @@ -0,0 +1,132 @@ +// from https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp +function isWebPCompatible() { + const animatedImage = + 'UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA'; + + return new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => { + if (image.width > 0 && image.height > 0) { + resolve(); + } else { + reject(); + } + }; + + image.onerror = reject.bind(null, true); + image.src = `data:image/webp;base64,${animatedImage}`; + }); +} + +// From https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read#example_2_-_handling_text_line_by_line +async function* readLineByLine(response) { + const utf8Decoder = new TextDecoder('utf-8'); + const reader = response.body.getReader(); + let { value: chunk, done: readerDone } = await reader.read(); + chunk = chunk ? utf8Decoder.decode(chunk, { stream: true }) : ''; + + let re = /\r\n|\n|\r/gm; + let startIndex = 0; + + for (;;) { + const result = re.exec(chunk); + if (!result) { + if (readerDone) { + break; + } + + const remainder = chunk.substr(startIndex); + ({ value: chunk, done: readerDone } = await reader.read()); + chunk = + remainder + (chunk ? utf8Decoder.decode(chunk, { stream: true }) : ''); + startIndex = re.lastIndex = 0; + continue; + } + + yield chunk.substring(startIndex, result.index); + startIndex = re.lastIndex; + } + + if (startIndex < chunk.length) { + yield chunk.substr(startIndex); + } +} + +/** + * Async image loading + */ +async function fetchThumbnail() { + let fetchURL = document.location.search; + if (fetchURL.includes('?')) { + if (!fetchURL.endsWith('&')) { + fetchURL += '&'; + } + fetchURL += 'thumbnail'; + } else { + fetchURL += '?thumbnail'; + } + + const response = await fetch(fetchURL, { credentials: 'same-origin' }); + + if (response.status >= 400) { + throw new Error('unable to load thumbnails'); + } + + for await (let line of readLineByLine(response)) { + const parts = line.split(','); + if (parts.length != 2) { + console.error('invalid line for thumbnail:', line); + continue; + } + + const picture = document.getElementById(`picture-${parts[0]}`); + if (!picture) { + continue; + } + + const img = new Image(); + img.src = `data:image/webp;base64,${parts[1]}`; + img.alt = picture.dataset.alt; + img.dataset.src = picture.dataset.src; + img.classList.add('thumbnail', 'full', 'block'); + + replaceContent(picture, img); + } +} + +window.addEventListener( + 'load', + async () => { + const thumbnailsElem = document.querySelectorAll('[data-thumbnail]'); + if (!thumbnailsElem) { + return; + } + + thumbnailsElem.forEach((picture) => { + replaceContent(picture, generateThrobber(['throbber-white'])); + }); + + try { + await fetchThumbnail(); + } catch (e) { + console.error(e); + } + + try { + await isWebPCompatible(); + } catch (e) { + await resolveScript( + 'https://unpkg.com/webp-hero@0.0.2/dist-cjs/webp-hero.bundle.js', + 'sha512-DA6h9H5Sqn55/uVn4JI4aSPFnAWoCQYYDXUnvjOAMNVx11///hX4QaFbQt5yWsrIm9hSI5fLJYfRWt3KXneSXQ==', + 'anonymous', + ); + + const webpMachine = new webpHero.WebpMachine(); + webpMachine.polyfillDocument(); + webpMachine.clearCache(); + } + + window.dispatchEvent(new Event('thumbnail-done')); + }, + false, +); diff --git a/cmd/fibr/static/scripts/map.js b/cmd/fibr/static/scripts/map.js new file mode 100644 index 00000000..748932d2 --- /dev/null +++ b/cmd/fibr/static/scripts/map.js @@ -0,0 +1,129 @@ +async function fetchGeoJSON(geoURL) { + const response = await fetch(geoURL, { credentials: 'same-origin' }); + + if (response.status >= 400) { + throw new Error('unable to load geojson'); + } + + if (response.status === 204) { + return null; + } + + return response.json(); +} + +async function addStyle(src, integrity, crossorigin) { + return new Promise((resolve) => { + const style = document.createElement('link'); + style.rel = 'stylesheet'; + style.href = src; + style.onload = resolve; + + if (integrity) { + style.integrity = integrity; + style.crossOrigin = crossorigin; + } + + document.querySelector('head').appendChild(style); + }); +} + +async function loadLeaflet() { + const leafletVersion = '1.9.2'; + + await addStyle( + `https://unpkg.com/leaflet@${leafletVersion}/dist/leaflet.css`, + 'sha512-UkezATkM8unVC0R/Z9Kmq4gorjNoFwLMAWR/1yZpINW08I79jEKx/c8NlLSvvimcu7SL8pgeOnynxfRpe+5QpA==', + 'anonymous', + ); + await addStyle( + 'https://unpkg.com/leaflet.markercluster@1.5.1/dist/MarkerCluster.Default.css', + 'sha512-6ZCLMiYwTeli2rVh3XAPxy3YoR5fVxGdH/pz+KMCzRY2M65Emgkw00Yqmhh8qLGeYQ3LbVZGdmOX9KUjSKr0TA==', + 'anonymous', + ); + await resolveScript( + `https://unpkg.com/leaflet@${leafletVersion}/dist/leaflet.js`, + 'sha512-KMraOVM0qMVE0U1OULTpYO4gg5MZgazwPAPyMQWfOkEshpwlLQFCHZ/0lBXyviDNVL+pBGwmeXQnuvGK8Fscvg==', + 'anonymous', + ); + await resolveScript( + 'https://unpkg.com/leaflet.markercluster@1.5.1/dist/leaflet.markercluster.js', + 'sha512-+Zr0llcuE/Ho6wXRYtlWypMyWSEMxrWJxrYgeAMDRSf1FF46gQ3PAVOVp5RHdxdzikZXuHZ0soHpqRkkPkI3KA==', + 'anonymous', + ); +} + +let map; +async function renderMap(geoURL) { + if (map) { + map.invalidateSize(); // force re-render + return; + } + + const container = document.getElementById('map-container'); + const throbber = generateThrobber(['map-throbber', 'throbber-white']); + if (container) { + container.appendChild(throbber); + } + + await loadLeaflet(); + + // create Leaflet map + map = L.map('map-container', { + center: [46.227638, 2.213749], // France 🇫🇷 + zoom: 5, + }); + + // add the OpenStreetMap tiles + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: + '© OpenStreetMap contributors', + }).addTo(map); + + const geojson = await fetchGeoJSON(geoURL); + if (!geojson || !geojson.features) { + return; + } + + const markers = L.markerClusterGroup({ + zoomToBoundsOnClick: false, + }); + + const bounds = []; + geojson.features.map((f) => { + const coord = L.GeoJSON.coordsToLatLng(f.geometry.coordinates); + + bounds.push(coord); + markers.addLayer( + L.circleMarker(coord).bindPopup( + ` + Image thumbnail + +
+ ${f.properties.date}`, + { + maxWidth: 'auto', + closeButton: false, + className: 'thumbnail-popup', + }, + ), + ); + }); + + markers.on('clusterclick', (a) => { + map.fitBounds(a.layer.getAllChildMarkers().map((m) => m.getLatLng())); + }); + + // fit bounds of map + if (bounds.length) { + map.once('zoomend', () => { + container.removeChild(throbber); + }); + map.fitBounds(bounds); + } else { + container.removeChild(throbber); + } + + map.addLayer(markers); +} diff --git a/cmd/fibr/static/scripts/navigation.js b/cmd/fibr/static/scripts/navigation.js new file mode 100644 index 00000000..8d633afe --- /dev/null +++ b/cmd/fibr/static/scripts/navigation.js @@ -0,0 +1,62 @@ +/** + * Async loading of a script + * @param {String} src URL of script + * @param {String} integrity Integrity of script + * @param {String} crossorigin Crossorigin of script + * @return Promise when script is either loaded or on error + */ +function resolveScript(src, integrity, crossorigin) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = src; + script.async = true; + script.onload = resolve.bind(null, true); + script.onerror = reject.bind(null, true); + + if (integrity) { + script.integrity = integrity; + script.crossOrigin = crossorigin; + } + + document.querySelector('head').appendChild(script); + }); +} + +/** + * Handle Previous/next. + */ +window.onkeyup = (e) => { + switch (e.key) { + case 'ArrowLeft': + goToPrevious(); + break; + + case 'ArrowRight': + goToNext(); + break; + + case 'Escape': + if (typeof abort === 'function') { + abort(e); + } else { + goBack(); + } + break; + } +}; + +/** + * Remove all child and append given one. + * @param {Element} element Element to clear + * @param {Element} newContent Element to put in place + */ +function replaceContent(element, newContent) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + if (newContent) { + element.appendChild(newContent); + } +} diff --git a/cmd/fibr/static/styles/main.css b/cmd/fibr/static/styles/main.css new file mode 100644 index 00000000..1deb8887 --- /dev/null +++ b/cmd/fibr/static/styles/main.css @@ -0,0 +1,314 @@ +:root { + --primary: royalblue; + --success: limegreen; + --danger: crimson; + --dark: #272727; + --grey: #3b3b3b; + --white: aliceblue; + + --icon-size: 2.4rem; + --icon-large: 4.8rem; +} + +* { + box-sizing: border-box; +} + +html { + font-size: 62.5%; +} + +body { + -webkit-overflow-scrolling: touch; + background-color: var(--dark); + height: 100vh; +} + +body, +button, +input { + color: var(--white); + font-family: + -apple-system, + 'Segoe UI', + 'Roboto', + 'Oxygen-Sans', + 'Ubuntu', + 'Cantarell', + 'Helvetica Nue', + sans-serif; + font-size: 1.6rem; + font-style: normal; + font-weight: 400; +} + +input { + color: var(--dark); +} + +input[type="file"] { + color: var(--white); +} + +a { + color: var(--white); + text-decoration: none; +} + +a:hover { + color: var(--primary); + text-decoration: underline; +} + +.primary { + color: var(--primary); +} + +.success { + color: var(--success); +} + +.danger { + color: var(--danger); +} + +.grey { + color: var(--grey); +} + +.white { + color: var(--white); +} + +.bg-primary, +.bg-primary:hover { + background-color: var(--primary); + color: var(--white); + text-decoration: none; +} + +.bg-success, +.bg-success:hover { + background-color: var(--success); + color: var(--dark); + text-decoration: none; +} + +.bg-danger, +.bg-danger:hover { + background-color: var(--danger); + color: var(--white); + text-decoration: none; +} + +.bg-grey, +.bg-grey:hover { + background-color: var(--grey); + color: var(--white); + text-decoration: none; +} + +.button { + border-radius: 4px; + border: 0; + cursor: pointer; + display: inline-block; + margin: 0; + padding: 1rem; + text-decoration: none; +} + +.button-icon { + background-color: transparent; +} + +.icon { + background-position: center center; + background-repeat: no-repeat; + color: var(--white); + display: inline-block; + height: var(--icon-size); + text-decoration: none; + vertical-align: middle; +} + +.icon-square { + width: var(--icon-size); +} + +.icon-bottom { + vertical-align: bottom; +} + +.icon-large { + height: var(--icon-large); + width: var(--icon-large); +} + +.icon-overlay { + height: var(--icon-large); + left: calc((100% - var(--icon-large)) / 2); + pointer-events: none; + position: absolute; + top: calc((100% - var(--icon-large)) / 2); + width: var(--icon-large); +} + +.icon-overlay-small { + bottom: 1rem; + height: var(--icon-size); + position: absolute; + right: 1rem; + width: var(--icon-size); +} + +.clickable { + cursor: pointer; +} + +.modal { + align-items: flex-start; + background-color: rgba(84, 84, 84, 0.75); + display: none; + height: 100vh; + justify-content: center; + left: 0; + padding-top: 5rem; + pointer-events: none; + position: fixed; + top: 0; + width: 100vw; +} + +.modal-content { + background-color: var(--dark); + display: flex; + flex-direction: column; + max-height: 80%; + max-width: 90%; + pointer-events: auto; +} + +.header { + background-color: var(--grey); + margin-top: 0; + padding: 0.5rem 1rem; + text-align: left; +} + +.center { + text-align: center; +} + +.padding { + padding: 1rem; +} + +.no-padding { + padding: 0; +} + +.margin { + margin: 1rem; +} + +.margin-left { + margin-left: 1rem; +} + +.no-margin { + margin: 0; +} + +.no-background { + background-color: transparent; +} + +.hidden { + display: none; +} + +@media print { + body::before { + content: 'Save ink, share link.'; + } + + body > * { + display: none; + } +} + +.flex { + display: flex; +} + +.flex-center { + align-items: center; + justify-content: center; +} + +.flex-grow { + flex: 1 1; +} + +.flex-column { + flex-direction: column; +} + +.full { + width: 100%; +} + +.full-screen { + max-width: 100vw; +} + +.medium { + font-size: 2.4rem; +} + +.small { + font-size: 1.6rem; +} + +.scrollable { + overflow-y: auto; +} + +.padding-left { + padding-left: 1rem; +} + +.padding-right { + padding-right: 1rem; +} + +.ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.flex-ellipsis { + min-width: 0; +} + +.relative { + position: relative; +} + +.block { + display: block; +} + +@media screen and (max-width: 374px) { + .hide-xs { + display: none; + } +} + +@media screen and (max-width: 599px) { + .hide-s { + display: none; + } +} diff --git a/cmd/fibr/templates/async-image.html b/cmd/fibr/templates/async-image.html index 99d6f78d..6a375f5c 100644 --- a/cmd/fibr/templates/async-image.html +++ b/cmd/fibr/templates/async-image.html @@ -23,138 +23,5 @@ } - + {{ end }} diff --git a/cmd/fibr/templates/header.html b/cmd/fibr/templates/header.html index a1d20601..73453263 100644 --- a/cmd/fibr/templates/header.html +++ b/cmd/fibr/templates/header.html @@ -5,9 +5,9 @@ + {{ template "favicon" . }} {{ template "seo" . }} - {{ template "style" . }} {{ end }} diff --git a/cmd/fibr/templates/map-modal.html b/cmd/fibr/templates/map-modal.html index 2b295643..9f7bdd3d 100644 --- a/cmd/fibr/templates/map-modal.html +++ b/cmd/fibr/templates/map-modal.html @@ -60,127 +60,17 @@

You must enable Javascript to have interactive map with pictures.

} - + + {{ end }} diff --git a/cmd/fibr/templates/style.html b/cmd/fibr/templates/style.html deleted file mode 100644 index e056f9d5..00000000 --- a/cmd/fibr/templates/style.html +++ /dev/null @@ -1,322 +0,0 @@ -{{ define "style" }} - -{{ end }}