From 9f36aee9f0fef920dc667b961d1d2f950264cae7 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Mon, 18 Jul 2022 10:20:11 +0200 Subject: [PATCH 1/2] feat: add core for 'simple' concept --- public/assets/css/style.css | 298 ++++++++++++++-------------- public/assets/js/modules/Dialog.mjs | 15 ++ public/assets/js/modules/Player.mjs | 37 ++++ public/assets/js/noscript.js | 6 - public/assets/js/script.mjs | 55 ++++- public/assets/media/coffee.svg | 1 + public/assets/media/wave.svg | 1 + public/index.html | 97 ++++++--- 8 files changed, 320 insertions(+), 190 deletions(-) create mode 100644 public/assets/js/modules/Dialog.mjs create mode 100644 public/assets/js/modules/Player.mjs create mode 100644 public/assets/media/coffee.svg create mode 100644 public/assets/media/wave.svg diff --git a/public/assets/css/style.css b/public/assets/css/style.css index d67f248..5dfef78 100755 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1,205 +1,209 @@ -:root { - --color-base: 0, 0, 0; - --color-contrast: 256, 256, 256; - - --padding: clamp(40px, 2vw, 2vw); - --border-size: clamp(4px, .25vw, .25vw); -} - /* -- Cornerstones -- */ +:root { + --primer-color-base: 255, 255, 255; + --primer-color-contrast: 0, 0, 0; + + --color-base: rgb(var(--primer-color-base)); + --color-contrast: rgb(var(--primer-color-contrast)); + + --padding: 20px; +} + * { margin: 0; - font-family: "Monaco", "Consolas", monospace, sans-serif; - color: rgb(var(--color-contrast)); + box-sizing: border-box; + font-family: 'Courier', sans-serif; + font-size: 20px; + color: inherit; } -*::selection { - background-color: rgb(var(--color-contrast)); - color: rgb(var(--color-base)); +::selection { + background-color: var(--color-contrast); + color: var(--color-base); +} + +::placeholder { + color: rgba(var(--primer-color-contrast), .5); } -html, body { - width: 100%; - height: 100%; - overflow-x: hidden; + display: flex; + flex-direction: column; + align-items: center; + color: var(--color-contrast); + background-color: var(--color-base); + gap: var(--padding); + margin: var(--padding) 0; + margin-bottom: 30vh; } -html { - background-color: rgba(var(--color-base), .7); - background-size: cover; - background-blend-mode: overlay; - background-position: center; - background-attachment: fixed; -} - -picture { - display: contents; -} - -h1 { - font-size: clamp(45px, 7vw, 6vh); -} - -p, a { - font-size: clamp(20px, 3vw, 2vh); - text-align: justify; +a:not(p > a) { + text-decoration: none; } /* -- Components -- */ -body { +input { + letter-spacing: 5px; +} + +input, +.button { + background-color: var(--color-contrast); + color: var(--color-base); + padding: calc(var(--padding) / 2) calc(var(--padding) * 1.5); + text-align: left; display: flex; - flex-direction: column; align-items: center; - justify-items: center; - gap: var(--padding, 30px); + justify-content: center; + gap: var(--padding); + height: 3.5em; } -body > div { - padding: calc(var(--padding) / 2); +input, +.button.phantom { + background-color: transparent; + border: solid 3px var(--color-contrast); + color: var(--color-contrast); } -:is(#intro, #card) a { - --padding-vert: clamp(17px, 1.1vw, 1.1vw); - - display: inline-block; - text-decoration: none; - text-align: center; +img, +.button { user-select: none; - background-color: rgba(var(--color-contrast), .13); - backdrop-filter: blur(2px); - -webkit-backdrop-filter: blur(2px); - box-shadow: - inset 0 .3vh 1.6vh rgba(0, 0, 0, 0), - 0 .1vh .3vh rgba(0, 0, 0, .12), - 0 .1vh .2vh rgba(0, 0, 0, .24); } -/* --- */ - -#intro { - padding: calc(var(--padding) / 2); +.button :not(p) { + height: 2em; } -#intro a { - padding: var(--padding-vert) 2vw; - border-radius: 100px; - border: solid var(--border-size) rgba(var(--color-contrast), 0); +.interact::before { + content: "tap"; +} + +/* ---- */ + +.spacer { + display: grid; + justify-items: center; +} + +body > .spacer { margin: var(--padding) 0; - width: calc(100% - ((var(--padding) / 2) + var(--border-size))); } -#intro p { - margin: 1vh 0; - font-size: clamp(20px, 3vw, 3vh); +.spacer div { + width: 205px; + height: 7px; + background-color: var(--color-contrast); + transform: rotate(-4deg); } -/* --- */ +/* -- Content -- */ -#card, -#card > div { +section { + max-width: 600px; + margin: var(--padding) calc(var(--padding) * 1.5); display: flex; flex-direction: column; align-items: center; - gap: calc(var(--padding) / 2); + text-align: justify; + gap: var(--padding); } -#card { - --portrait-size: clamp(128px, 12vw, 12vh); - - position: relative; - max-width: 600px; - padding: var(--padding); - border-radius: clamp(18px, 1vw, 1vw); - backdrop-filter: saturate(100) brightness(.4); - -webkit-backdrop-filter: saturate(100) brightness(.4); - border: solid var(--border-size) rgba(var(--color-contrast), .1); - box-shadow: 0 1vh 2vh rgba(0, 0, 0, .19), 0 6px 6px rgba(0, 0, 0, .23); -} - -#card img { - width: var(--portrait-size); - height: var(--portrait-size); - position: absolute; - border-radius: 100%; - top: calc(((var(--portrait-size) / 2) + var(--border-size)) * -1); - background-color: rgb(var(--color-base)); - box-shadow: 0 1vh 2vh rgba(0, 0, 0 , .19), 0 6px 6px rgba(0, 0, 0 , .23); -} - -#card a { +section > *, +section picture > img { width: 100%; - padding: var(--padding-vert) 0; - margin-top: calc(var(--padding) / 2); - border-radius: clamp(9px, .5vw, .5vw); } -/* -- Media Queries -- */ +section#intro { + max-width: 250px; + text-align: center; +} -@supports ((not ((backdrop-filter: saturate(1)) and (backdrop-filter: brightness(1)))) and (not ((-webkit-backdrop-filter: saturate(1)) and (-webkit-backdrop-filter: brightness(1))))) { - #card { - background-color: rgba(var(--color-base), .7); +section#contact > div { + display: grid; + grid-template-rows: repeat(2, 1fr); + gap: var(--padding); +} + +section#sky .button img { + height: 50%; +} + +section#coffee > img { + width: 100px; + margin-bottom: calc(var(--padding) * 2); +} + +/* ---- */ + +.banner { + width: 100%; + background-color: rgba(var(--primer-color-contrast), .05); + text-align: center; + padding: calc(var(--padding) / 2) 0; + margin: var(--padding) 0; +} + +/* -- Media queries -- */ + +@media (hover: hover) { + .button:hover { + background-color: rgba(var(--primer-color-contrast), .75); + } + + .button:active { + background-color: rgba(var(--primer-color-contrast), .7); + } + + /* ---- */ + + .button.phantom:hover { + background-color: rgba(var(--primer-color-contrast), .05); + } + + .button.phantom:active { + background-color: rgba(var(--primer-color-contrast), .1); } } @media (pointer: fine) { - :is(#intro, #card) a { - --transition-speed: 200ms; - transition: - var(--transition-speed) background-color, - var(--transition-speed) box-shadow, - var(--transition-speed) border-color; + .button { + cursor: pointer; } - :is(#intro, #card) a:hover { - background-color: rgba(var(--color-contrast), .2); - border-color: rgba(var(--color-contrast), .2); - box-shadow: - inset 0 .3vh 1.6vh rgba(0, 0, 0, .16), - 0 .3vh .6vh rgba(0, 0, 0, .16), - 0 .3vh .6vh rgba(0, 0, 0, .23); - } - - :is(#intro, #card) a:active { - background-color: rgba(var(--color-contrast), .15); + .interact::before { + content: "click"; } } -@media (max-width: 330px) { - p, a { - text-align: left; - font-size: 18px; +@media (prefers-color-scheme: dark) { + :root { + --primer-color-base: 0, 0, 0; + --primer-color-contrast: 255, 255, 255; } - #card { - padding: calc(var(--padding) / 2); + section#coffee > img { + filter: invert(1); } } -@media (min-aspect-ratio: 14/9) and (min-height: 600px) { - body { +@media (max-width: 303px) { + section#sky img { + display: none; + } +} + +@media (min-width: 600px) { + section#code { display: grid; + grid-template-columns: 1fr 40px 1fr; + width: 100%; + } + + section#contact > div { + grid-template-rows: 1fr; grid-template-columns: repeat(2, 1fr); - gap: unset; } - - body > div { - display: grid; - align-items: center; - } - - body > div:last-of-type { - padding: calc(var(--padding) * 2); - } - - #intro a { - width: unset; - } - - #card { - min-width: 300px; - max-width: 30vw; - } -} +} \ No newline at end of file diff --git a/public/assets/js/modules/Dialog.mjs b/public/assets/js/modules/Dialog.mjs new file mode 100644 index 0000000..7dea7c0 --- /dev/null +++ b/public/assets/js/modules/Dialog.mjs @@ -0,0 +1,15 @@ +export class Dialog { + constructor(target) { + this.dialog = document.createElement("dialog"); + + if (typeof this.dialog.showModal !== "function") { + throw new Error("Browser does not support HTMLDialogElement"); + } + + document.body.appendChild(this.dialog); + } + + open() { + this.dialog.showModal(); + } +} \ No newline at end of file diff --git a/public/assets/js/modules/Player.mjs b/public/assets/js/modules/Player.mjs new file mode 100644 index 0000000..c6cf018 --- /dev/null +++ b/public/assets/js/modules/Player.mjs @@ -0,0 +1,37 @@ +// Create a new AudioPlayer from template +export class AudioPlayer extends Audio { + constructor(target) { + if (!target instanceof HTMLElement) { + throw new Error("Target must be an HTMLElement"); + } + + const src = "src" in target.dataset ? target.dataset.src : ""; + super(src); + + // Bind interaction with the player as play/stop + target.addEventListener("click", () => this.playState()); + + // Start playback when ready + this.addEventListener("canplaythrough", () => this.play(), { once: true }); + // Restart playback if paused (treat as stop) + this.addEventListener("pause", () => this.currentTime = 0); + } + + // Play or stop media + playState() { + // Ignore if media is still buffering + if (this.readyState !== HTMLMediaElement.HAVE_ENOUGH_DATA) { + return false; + } + + // Stop media if playing + if (this.paused === false && this.ended === false) { + this.pause(); + this.currentTime = 0; + return false; + } + + // Play media + return this.play(); + } +} \ No newline at end of file diff --git a/public/assets/js/noscript.js b/public/assets/js/noscript.js index b265ca3..e69de29 100755 --- a/public/assets/js/noscript.js +++ b/public/assets/js/noscript.js @@ -1,6 +0,0 @@ -const search = document.getElementById("search").children[0]; -const results = document.getElementById("results").children[0]; - -search.style.setProperty("display","none"); -results.classList.add("error"); -results.innerText = "Sorry, your browser isn't supported yet"; \ No newline at end of file diff --git a/public/assets/js/script.mjs b/public/assets/js/script.mjs index 2592a11..f809253 100644 --- a/public/assets/js/script.mjs +++ b/public/assets/js/script.mjs @@ -1,14 +1,47 @@ -import { default as Glitch } from "./glitch/Glitch.mjs"; +// Bind dialog box interaction +[...document.getElementsByClassName("dialog")].forEach(dialog => { + dialog.addEventListener("click", () => { + import("./modules/Dialog.mjs").then(mod => { + // Initialize dialog with interacted element + const dialogElement = new mod.Dialog(dialog); -const logging = "https://victorwesterlund-logging-dnzfgzf6za-lz.a.run.app"; - -// Log link clicks -for(let link of document.getElementsByTagName("a")) { - link.addEventListener("click", event => { - event.preventDefault(); - navigator?.sendBeacon(logging, event); - window.location.href = event.target.href; + // Will open data-src + dialogElement.open(); + }); }); -} +}); -window.glitch = new Glitch(document.body.parentElement); \ No newline at end of file +// Create AudioPlayers from template on interaction +[...document.getElementsByClassName("player")].forEach(player => { + player.addEventListener("click", () => { + import("./modules/Player.mjs").then(mod => { + // Initialize AudioPlayer from template + const audioPlayer = new mod.AudioPlayer(player); + let animation; + + // Animate a progress bar as media is playing + audioPlayer.addEventListener("playing", () => { + const color = "rgba(var(--primer-color-contrast), .1)"; + + const keyframes = [ + { boxShadow: `inset 0 0 0 0 ${color}` }, + { boxShadow: `inset ${player.offsetWidth}px 0 0 0 ${color}` } + ]; + + const timing = { + duration: 38000, // Robot36 TX + calibration header + iterations: 1 + } + + animation = player.animate(keyframes, timing); + player.querySelector(".playstate").innerText = "stop"; + }); + + // Stop animation if playback is interrupted + audioPlayer.addEventListener("pause", () => { + animation.cancel(); + player.querySelector(".playstate").innerText = "play"; + }); + }); + }, { once: true }); +}); \ No newline at end of file diff --git a/public/assets/media/coffee.svg b/public/assets/media/coffee.svg new file mode 100644 index 0000000..ec23c1a --- /dev/null +++ b/public/assets/media/coffee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/media/wave.svg b/public/assets/media/wave.svg new file mode 100644 index 0000000..312e8ef --- /dev/null +++ b/public/assets/media/wave.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.html b/public/index.html index aa2a371..cb7eb29 100755 --- a/public/index.html +++ b/public/index.html @@ -7,43 +7,88 @@ - + - + -
-
-

hello, my name is

-

victor

-

I'm a

-

full-stack

-

developer

-

from Sweden

- my github -> +
+

victor westerlund

+
+
+

I make things with code and coffee

+
+
+ +
+ Hand-drawn icon of the GitHub mascot +

on github

+
+
+

and

+ +
+

elsewhere

+
+
+
+
+

Wanna chat? The best way to get in touch with me is by sending a mail. The time is currently 00:00 in Sweden, and I will reply as soon as possible.

+
+ +
+

copy email address

+
+
+ +
+

šŸ”‘ show PGP key

+
+
+
+
+ -
-
- - - - - portrait of victor - -
-

I create things with code. When I'm not creating things with code, I enjoy skiing, watching movies and some occasional gaming

-

And beyond computer science, I'm also an armchair rabbit-holer for engineering, physics and astronomy

+
+ + + Webcam stillframe of the sky from my balcony + +

Do you have an SSTV receiver? Tune in to a sneaky surprise and this beautiful view of the sky from my balcony as it looked like 4 minutes ago. Or why not check out this pretty neat time-lapse.

+ +
+

view timelapse

-
-

...and ā˜•, full-time

-
- +
+
+ +

Robot36
to play

+

āš ļø warning: loud f*cking noise so adjust your volume accordingly.

+
+
+
+
+
+ Hand-drawn coffee icon +

Did I mention that I like coffee? Well in the past 7 days I have had 4 cups of coffee, which is roughly equivalent to 0.2 cups per day. Not too impressive given that my weekly average is 2.2 cups per day.

+
+
+
+
+
+

Enter your top sneaky code here. You will know when you've struck the jackpot.

+ +

So you don't have a sneaky code?
Solve this puzzle and I'll buy you a cup of coffee.

+
+
+
From c10e6a702751ef88b485c44a24f73affc32192cb Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Sun, 7 Aug 2022 23:07:54 +0200 Subject: [PATCH 2/2] wip(22w31a) --- public/assets/css/style.css | 58 +++-- public/assets/fonts/roboto-mono.ttf | Bin 0 -> 22224 bytes public/assets/fonts/roboto-mono.woff2 | Bin 0 -> 12312 bytes public/assets/js/modules/Dialog.mjs | 210 +++++++++++++++++- public/assets/js/modules/TimeUpdate.mjs | 87 ++++++++ .../js/{ => modules}/glitch/Generator.mjs | 0 .../assets/js/{ => modules}/glitch/Glitch.mjs | 0 .../js/{ => modules}/glitch/GlitchWorker.js | 0 public/assets/js/script.mjs | 54 +++-- public/assets/media/close.svg | 1 + public/assets/media/github.svg | 1 + public/contact.html | 31 +++ public/error.html | 53 ++++- public/humans.txt | 1 + public/index.html | 32 +-- 15 files changed, 472 insertions(+), 56 deletions(-) create mode 100644 public/assets/fonts/roboto-mono.ttf create mode 100644 public/assets/fonts/roboto-mono.woff2 create mode 100644 public/assets/js/modules/TimeUpdate.mjs rename public/assets/js/{ => modules}/glitch/Generator.mjs (100%) rename public/assets/js/{ => modules}/glitch/Glitch.mjs (100%) rename public/assets/js/{ => modules}/glitch/GlitchWorker.js (100%) create mode 100644 public/assets/media/close.svg create mode 100644 public/assets/media/github.svg create mode 100755 public/contact.html create mode 100755 public/humans.txt diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 5dfef78..988ca5f 100755 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -10,10 +10,19 @@ --padding: 20px; } +@font-face { + font-family: "Roboto Mono"; + src: + local("Roboto Mono"), + url("../fonts/roboto-mono.woff2") format("woff2"), + url("../fonts/roboto-mono.ttf") format("ttf"); + font-display: fallback; +} + * { margin: 0; box-sizing: border-box; - font-family: 'Courier', sans-serif; + font-family: "Roboto Mono", "Courier", sans-serif; font-size: 20px; color: inherit; } @@ -27,15 +36,20 @@ color: rgba(var(--primer-color-contrast), .5); } +html { + background-size: cover; + background-position: 50% 50%; +} + body { display: flex; flex-direction: column; align-items: center; color: var(--color-contrast); - background-color: var(--color-base); + background-color: rgba(var(--primer-color-base), .95); gap: var(--padding); - margin: var(--padding) 0; - margin-bottom: 30vh; + padding: var(--padding) 0; + padding-bottom: 30vh; } a:not(p > a) { @@ -58,13 +72,13 @@ input, align-items: center; justify-content: center; gap: var(--padding); - height: 3.5em; + height: 3em; } input, .button.phantom { background-color: transparent; - border: solid 3px var(--color-contrast); + border: solid 2px var(--color-contrast); color: var(--color-contrast); } @@ -78,7 +92,7 @@ img, } .interact::before { - content: "tap"; + content: "tap "; } /* ---- */ @@ -101,6 +115,7 @@ body > .spacer { /* -- Content -- */ +form, section { max-width: 600px; margin: var(--padding) calc(var(--padding) * 1.5); @@ -116,6 +131,10 @@ section picture > img { width: 100%; } +section#code { + text-align: center; +} + section#intro { max-width: 250px; text-align: center; @@ -150,11 +169,17 @@ section#coffee > img { @media (hover: hover) { .button:hover { - background-color: rgba(var(--primer-color-contrast), .75); + color: var(--color-contrast); + background-color: transparent; + border: inset 2px black; + } + + .button:hover:not(.phantom) :is(img, svg) { + filter: invert(1); } .button:active { - background-color: rgba(var(--primer-color-contrast), .7); + background-color: rgba(var(--primer-color-contrast), .1); } /* ---- */ @@ -174,7 +199,7 @@ section#coffee > img { } .interact::before { - content: "click"; + content: "click "; } } @@ -184,18 +209,23 @@ section#coffee > img { --primer-color-contrast: 255, 255, 255; } - section#coffee > img { + img:not(picture img) { filter: invert(1); } } -@media (max-width: 303px) { - section#sky img { +@media (max-width: 330px) { + section#sky .button img { display: none; } } -@media (min-width: 600px) { +@media (min-width: 660px) { + input, + .button { + height: 3.5em; + } + section#code { display: grid; grid-template-columns: 1fr 40px 1fr; diff --git a/public/assets/fonts/roboto-mono.ttf b/public/assets/fonts/roboto-mono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..de0f4852aec32cc63b89e5946bc6c650dbf81f7f GIT binary patch literal 22224 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 Jq+U??{{yY{fp`D_ literal 0 HcmV?d00001 diff --git a/public/assets/fonts/roboto-mono.woff2 b/public/assets/fonts/roboto-mono.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f8894bab50f641e86f695e85830a74dd395732fa GIT binary patch literal 12312 zcmV+zFz3&APew8T0RR9105BK;4gdfE09Tv<0584(0RR9100000000000000000000 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 { + if (mutation[0].attributeName === "open") { + this.closed(); + } + }); + + // HTMLDialogElement is not supported, use an alert() instead if (typeof this.dialog.showModal !== "function") { - throw new Error("Browser does not support HTMLDialogElement"); + this.dialog = { + title: "", + content: "" + } } - document.body.appendChild(this.dialog); + // Append header content + if ("header" in options) { + this.header(options.header); + } + + // Bind events and append to DOM + if (this.dialog instanceof HTMLDialogElement) { + this.dialog.addEventListener("click", event => { + const size = event.target.closest("dialog").getBoundingClientRect(); + + // If click happened on the area surrounding the dialog (lazy dismiss) + if (event.x < size.left || event.y < size.top || event.x > size.right || event.y > size.bottom) { + this.close(); + } + }); + + document.body.appendChild(this.dialog); + } } - open() { + setStyle(target, props) { + // Get element by selector + if (!target instanceof HTMLElement) { + target = document.querySelector(target); + } + + // Set CSS properties + for (const [name, value] of Object.entries(props)) { + target.style.setProperty(name, value); + } + } + + // Set dialog header + header(header) { + // Remove existing header wrapper + let element = this.dialog.getElementsByClassName("header")[0] ?? null; + if (element) { + element.remove(); + } + + const size = "50px"; + + // Create header wrapper + element = document.createElement("div"); + element.classList.add("header"); + this.setStyle(element, { + "display": "grid", + "grid-template-columns": `1fr ${size}`, + "align-items": "center", + "padding": "var(--padding)", + "gap": "var(--padding)", + "background-color": "rgba(var(--primer-color-contrast), .05)" + }); + + // Append a header text + if ("title" in header) { + const title = document.createElement("h1"); + title.innerText = header.title; + this.setStyle(title, { + "margin-left": "calc(var(--padding) / 2)" + }); + + element.appendChild(title); + } + + // Append a close button + if ("closeButton" in header && header.closeButton === true) { + const closeButton = document.createElement("div"); + closeButton.classList.add("button"); + closeButton.innerHTML = ''; + this.setStyle(closeButton, { + "width": size, + "height": size, + "padding": "calc(var(--padding) / 1.25)" + }); + + closeButton.addEventListener("click", () => this.close()); + element.appendChild(closeButton); + } + + this.dialog.insertAdjacentElement("afterbegin", element); + } + + // Remove content element subtree + clear() { + if (!this.dialog instanceof HTMLDialogElement || !this.content) { + return false; + } + + while (this.content.lastChild) { + this.content.lastChild.remove(); + } + } + + error(title = "Something went wrong", message = "Unknown error", data = null) { + this.header({ + title: title, + closeButton: true + }); + + this.clear(); + + const info = document.createElement("p"); + info.innerText = message; + + this.content.appendChild(info); + + // Has detailed information about error + if (data) { + // Create the element which, when clicked, will show data + const dump = document.createElement("p"); + dump.classList.add("interact"); + dump.innerText = "here for technical data"; + this.setStyle(dump, { "text-decoration": "underline" }); + + // Show detailed error data + dump.addEventListener("click", () => { + dump.classList.remove("interact"); + dump.innerText = `Returned: "${data}"`; + + this.setStyle(dump, { + "text-decoration": "initial", + "background-color": "rgba(var(--primer-color-contrast), .05)", + "padding": "var(--padding)" + }); + }, { once: true }); + + this.content.appendChild(dump); + } + } + + // Open modal with embedded page or text + open(target) { + let source; + + // Check if the thing to open is a page or some text + try { + source = target instanceof HTMLAnchorElement ? new URL(target.href) : new URL(target); + + // Perform top-level navigation instead if HTMLDialogElement is not supported + if (!this.dialog instanceof HTMLDialogElement) { + window.location.href = source.toString(); + } + + this.header({ + title: target.hasAttribute("title") ? target.getAttribute("title") : "", + closeButton: true + }); + + // Fetch page from URL and inject it into dialog DOM + this.content.innerHTML = "

āŒ› Loading...

"; + fetch(source) + .then(res => res.text()) + .then(page => this.content.innerHTML = page); + } catch { + // Looks like we're just getting text + this.content.innerText = target; + } + this.dialog.showModal(); + this.dialog.appendChild(this.content); + + this.observer.observe(this.dialog, { + attributes: true + }); + } + + // Destroy dialog + closed() { + this.observer.disconnect(); + this.dialog.remove(); + } + + // Close dialog + close() { + if (!this.dialog instanceof HTMLDialogElement) { + return false; + } + + this.dialog.close(); } } \ No newline at end of file diff --git a/public/assets/js/modules/TimeUpdate.mjs b/public/assets/js/modules/TimeUpdate.mjs new file mode 100644 index 0000000..197843e --- /dev/null +++ b/public/assets/js/modules/TimeUpdate.mjs @@ -0,0 +1,87 @@ +// Update timed DOM components +export default class TimeUpdate { + constructor() { + this.date = new Date(); + + this.myTime(); + this.skyPicture(); + this.skyAge(); + } + + // Set the innerText of an Element by its id + setTextById(id, text) { + const element = document.getElementById(id) ?? null; + if (!element) { + return false; + } + + element.innerText = text; + } + + // Set the time in the #intro section (normal clock display) + myTime() { + let time = [ + (this.date.getUTCHours() + 2) % 24, // Get hours + (this.date.getUTCMinutes()) % 60, // Get minutes + ]; + + // Prepend 0 for single-digit numbers + time = time.map(x => { + return x < 10 ? "0" + x : x; + }); + + // Concat hour and minutes with semicolon seperator + this.setTextById("time", time.join(":")); + } + + // Update the sky picture from endpoint + skyPicture() { + const image = document.querySelector("#sky > picture") ?? null; + if (!image) { + return false; + } + + // Helper function to prepare a cache breaking URL + const taggedUrl = (url) => { + url = new URL(url); + // Use current unix epoch as a search param + url.searchParams.set("t", this.date.valueOf()); + + return url.toString(); + } + + // Reload sky picture (using cache breaking URL) + [...image.children].forEach(child => { + switch (child.constructor) { + case HTMLSourceElement: + child.srcset = taggedUrl(child.srcset); + break; + + case HTMLImageElement: + child.src = taggedUrl(child.src); + break; + } + }); + } + + /* + * A new picture of the sky is taken at every 10th minute. + * So round the age down to the nearest 10 minute mark. + */ + skyAge() { + let age = this.date.getMinutes() % 10; + + if (age <= 1) { + /* + * It sometimes takes the server a little longer to transcode + * the picture, so give it an around 1 minute text - for 2 minutes. + */ + age = "~1 minute" + } else { + // Plural suffix + age += " minutes"; + } + + this.setTextById("sky_age", age); + } +} \ No newline at end of file diff --git a/public/assets/js/glitch/Generator.mjs b/public/assets/js/modules/glitch/Generator.mjs similarity index 100% rename from public/assets/js/glitch/Generator.mjs rename to public/assets/js/modules/glitch/Generator.mjs diff --git a/public/assets/js/glitch/Glitch.mjs b/public/assets/js/modules/glitch/Glitch.mjs similarity index 100% rename from public/assets/js/glitch/Glitch.mjs rename to public/assets/js/modules/glitch/Glitch.mjs diff --git a/public/assets/js/glitch/GlitchWorker.js b/public/assets/js/modules/glitch/GlitchWorker.js similarity index 100% rename from public/assets/js/glitch/GlitchWorker.js rename to public/assets/js/modules/glitch/GlitchWorker.js diff --git a/public/assets/js/script.mjs b/public/assets/js/script.mjs index f809253..4673a90 100644 --- a/public/assets/js/script.mjs +++ b/public/assets/js/script.mjs @@ -1,15 +1,4 @@ -// Bind dialog box interaction -[...document.getElementsByClassName("dialog")].forEach(dialog => { - dialog.addEventListener("click", () => { - import("./modules/Dialog.mjs").then(mod => { - // Initialize dialog with interacted element - const dialogElement = new mod.Dialog(dialog); - - // Will open data-src - dialogElement.open(); - }); - }); -}); +import TimeUpdate from "./modules/TimeUpdate.mjs"; // Create AudioPlayers from template on interaction [...document.getElementsByClassName("player")].forEach(player => { @@ -44,4 +33,43 @@ }); }); }, { once: true }); -}); \ No newline at end of file +}); + +// Log button clicks +[...document.getElementsByClassName("button")].forEach(button => { + if ("sendBeacon" in navigator) { + // Get endpoint from dns-prefetch tag + const endpoint = document.querySelector('[data-id="logging"]').href; + button.addEventListener("click", event => navigator.sendBeacon(endpoint, event)); + } +}) + +// Get coffee stats from endpoint +{ + // Get endpoint from preconnect tag + const endpoint = new URL(document.querySelector('[data-id="coffee"]').href); + + fetch(endpoint) + .then(res => res.json()) + .then(json => { + const values = json.Values.flat(1); + const targets = [...document.getElementsByClassName("coffee")]; + + // Assign each value in order of appearance + values.forEach((value, idx) => {targets[idx].innerText = value}); + }); +} + +// Update timed DOM components every absolute 10th minute +{ + const now = (new TimeUpdate()).date.valueOf(); + + const coeff = 1000 * 60; + const next = Math.ceil((now / coeff)) * coeff; + const offset = next - now; + + // Update timed elements at (roughly) every 10th minute in each absolute hour + window._timeUpdate = setTimeout(() => { + window._timeUpdate = setInterval(() => new TimeUpdate(), 60000); + }, offset); +} \ No newline at end of file diff --git a/public/assets/media/close.svg b/public/assets/media/close.svg new file mode 100644 index 0000000..515ce90 --- /dev/null +++ b/public/assets/media/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/media/github.svg b/public/assets/media/github.svg new file mode 100644 index 0000000..c869ac1 --- /dev/null +++ b/public/assets/media/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/contact.html b/public/contact.html new file mode 100755 index 0000000..abefd74 --- /dev/null +++ b/public/contact.html @@ -0,0 +1,31 @@ + + + + + Victor Westerlund + + + + + + + + + + + + + + + + + + +
+

victor westerlund

+
+
+
+
+ + diff --git a/public/error.html b/public/error.html index aa66c8f..d489ab4 100755 --- a/public/error.html +++ b/public/error.html @@ -10,19 +10,48 @@ - - - - + + + + + -
-
-

there is nothing here

-

and that's all I know

- take me home -> -
-
- +
+

Oh, I’m in the wrong place, man!

+

404 - Not Found

+
+
+
+

+ +
+

Get me out of here!

+
+
+
+ diff --git a/public/humans.txt b/public/humans.txt new file mode 100755 index 0000000..9e3aeae --- /dev/null +++ b/public/humans.txt @@ -0,0 +1 @@ +This website was created by me, Victor Westerlund. It's not much but it's honest work. \ No newline at end of file diff --git a/public/index.html b/public/index.html index cb7eb29..3744c9d 100755 --- a/public/index.html +++ b/public/index.html @@ -6,13 +6,19 @@ + + + + + + @@ -24,30 +30,30 @@

I make things with code and coffee

- +
- Hand-drawn icon of the GitHub mascot + GitHub

on github

and

- +

elsewhere

-

Wanna chat? The best way to get in touch with me is by sending a mail. The time is currently 00:00 in Sweden, and I will reply as soon as possible.

+

Wanna chat? The best way to get in touch with me is by sending a mail. The time is currently 00:00 in Sweden, and I will reply as soon as possible.

- +

copy email address

- - @@ -60,24 +66,24 @@ Webcam stillframe of the sky from my balcony -

Do you have an SSTV receiver? Tune in to a sneaky surprise and this beautiful view of the sky from my balcony as it looked like 4 minutes ago. Or why not check out this pretty neat time-lapse.

- +

Do you have an SSTV receiver? Tune in to a sneaky surprise and this beautiful view of the sky from my balcony as it looked like - minutes ago. Or why not check out this pretty neat time-lapse.

+

view timelapse

-

Robot36
to play

+

Robot36
to play

-

āš ļø warning: loud f*cking noise so adjust your volume accordingly.

+

āš ļø warning: loud f*cking noise.

Hand-drawn coffee icon -

Did I mention that I like coffee? Well in the past 7 days I have had 4 cups of coffee, which is roughly equivalent to 0.2 cups per day. Not too impressive given that my weekly average is 2.2 cups per day.

+

Did I mention that I like coffee? Well in the past 7 days I have enjoyed a baffling ? cups of coffee, which is roughly equivalent to ? cups per day! ? impressive given that my weekly average per month is ? cups per day.