wip(22w31a)

This commit is contained in:
Victor Westerlund 2022-08-07 23:07:54 +02:00
parent 9f36aee9f0
commit c10e6a7027
15 changed files with 472 additions and 56 deletions

View file

@ -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;

Binary file not shown.

Binary file not shown.

View file

@ -1,15 +1,217 @@
export class Dialog {
constructor(target) {
constructor(target, options = {}) {
this.dialog = document.createElement("dialog");
// Center the dialog modal
this.setStyle(this.dialog, {
"margin": "auto",
"display": "flex",
"flex-direction": "column",
"gap": "var(--padding)"
});
this.content = document.createElement("div");
this.content.classList.add("content");
// Use only as wrapper
this.setStyle(this.content, {
"display": "contents"
});
// Close modal when open attribute gets removed
this.observer = new MutationObserver(mutation => {
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 = '<svg viewbox="0 0 10 10"><path stroke="var(--color-base)" d="M 0,0 l 10,10 M 0,10 l 10,-10"></path></svg>';
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 = "<p>⌛ Loading...</p>";
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();
}
}

View file

@ -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);
}
}

View file

@ -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 });
});
});
// 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);
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

After

Width:  |  Height:  |  Size: 219 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill-rule="evenodd" clip-rule="evenodd" d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 964 B

31
public/contact.html Executable file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Victor Westerlund</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Full-stack web developer from Stockholm, Sweden.">
<meta name="theme-color" content="#000000">
<meta property='og:title' content='Victor Westerlund'>
<meta property='og:image' content='assets/media/banner.jpg'>
<meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'>
<meta property='og:url' content='https://victorwesterlund.com'>
<link data-id="logging" rel="dns-prefetch" href="https://logging-dnzfgzf6za-lz.a.run.app">
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:no-preference)">
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:light)">
<link rel="icon" href="assets/media/favicon-light.png" media="(prefers-color-scheme:dark)">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<a href="index"><section id="intro">
<p><strong>victor westerlund</strong></p>
<div class="spacer">
<div></div>
</div>
</section></a>
</body>
</html>

View file

@ -10,19 +10,48 @@
<meta property='og:image' content='//victorwesterlund.com/assets/media/banner.jpg'>
<meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'>
<meta property='og:url' content='https://victorwesterlund.com'>
<link rel="icon" href="/assets/media/favicon-dark.png" media="(prefers-color-scheme:no-preference)">
<link rel="icon" href="/assets/media/favicon-dark.png" media="(prefers-color-scheme:light)">
<link rel="icon" href="/assets/media/favicon-light.png" media="(prefers-color-scheme:dark)">
<link rel="stylesheet" href="/assets/css/style.css">
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:no-preference)">
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:light)">
<link rel="icon" href="assets/media/favicon-light.png" media="(prefers-color-scheme:dark)">
<link rel="stylesheet" href="assets/css/style.css">
<style>
:root {
--primer-color-base: 0, 0, 0;
--primer-color-contrast: 255, 255, 255;
}
html {
height: -webkit-fill-available;
background-size: cover;
background-position: 50% 50%;
}
body {
margin: 0;
height: 100%;
justify-content: center;
background-color: rgba(0, 0, 0, .7);
}
</style>
</head>
<body>
<div>
<div id="intro">
<h1>there is nothing here</h1>
<p>and that's all I know</p>
<a href="/">take me home -></a>
</div>
</div>
<script type="module" src="/assets/js/script.mjs"></script>
<section>
<h1>Oh, Im in the wrong place, man!</h1>
<p>404 - Not Found</p>
</section>
<div class="spacer"><div></div></div>
<section>
<h1></h1>
<a href="/">
<div class="button phantom">
<p>Get me out of here!</p>
</div>
</a>
</section>
<script type="module">
import { default as Glitch } from "./assets/js/glitch/Glitch.mjs";
window.glitch = new Glitch(document.body.parentElement);
</script>
</body>
</html>

1
public/humans.txt Executable file
View file

@ -0,0 +1 @@
This website was created by me, Victor Westerlund. It's not much but it's honest work.

View file

@ -6,13 +6,19 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Full-stack web developer from Stockholm, Sweden.">
<meta name="theme-color" content="#000000">
<meta property='og:title' content='Victor Westerlund'>
<meta property='og:image' content='assets/media/banner.jpg'>
<meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'>
<meta property='og:url' content='https://victorwesterlund.com'>
<link data-id="logging" rel="dns-prefetch" href="https://logging-dnzfgzf6za-lz.a.run.app">
<link data-id="coffee" rel="preconnect" href="https://get-coffee-dnzfgzf6za-lz.a.run.app" crossorigin="anonymous">
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:no-preference)">
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:light)">
<link rel="icon" href="assets/media/favicon-light.png" media="(prefers-color-scheme:dark)">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
@ -24,30 +30,30 @@
<p>I make things with code and coffee</p>
</section>
<section id="code">
<a href="https://github.com/VictorWesterlund" target="_blank" rel="noreferrer noopener">
<a href="https://github.com/VictorWesterlund" target="_blank" title="Github profile" rel="noreferrer noopener">
<div class="button">
<img src="assets/media/favicon-light.png" alt="Hand-drawn icon of the GitHub mascot"/>
<img src="assets/media/github.svg" alt="GitHub"/>
<p>on github</p>
</div>
</a>
<p>and</p>
<a href="#" target="_blank" rel="noreferrer noopener">
<a href="work" title="Other projects">
<div class="button">
<p>elsewhere</p>
</div>
</a>
</section>
<section id="contact">
<p>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.</p>
<p>Wanna chat? The best way to get in touch with me is by sending a mail. The time is currently <span id="time">00:00</span> in Sweden, and I will reply as soon as possible.</p>
<div>
<a href="#">
<a href="#" title="Copy mail address">
<div class="button">
<p>copy email address</p>
</div>
</a>
<a href="#">
<div class="button phantom dialog" data-src="">
<p>🔑 show PGP key</p>
<a href="contact" title="Contact me">
<div class="button phantom dialog">
<p>or use this form</p>
</div>
</a>
</div>
@ -60,24 +66,24 @@
<source srcset="https://friday.victorwesterlund.com/sky/original.webp" type="image/webp">
<img src="https://friday.victorwesterlund.com/sky/original.jpg" style="aspect-ratio: 16 / 9" alt="Webcam stillframe of the sky from my balcony"/>
</picture>
<p>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 <strong>4 minutes ago</strong>. Or why not check out this pretty neat time-lapse.</p>
<a href="#">
<p>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 <strong><span id="sky_age">- minutes</span> ago</strong>. Or why not check out this pretty neat time-lapse.</p>
<a href="#" title="Sky timelapse">
<div class="button">
<p>view timelapse</p>
</div>
</a>
<div class="button phantom player" data-src="https://friday.victorwesterlund.com/sky/robot36.wav">
<img src="assets/media/wave.svg"/>
<p><strong>Robot36</strong><br><span class="interact"></span> to <span class="playstate">play</span></p>
<p><strong>Robot36</strong><br><span class="interact"></span>to <span class="playstate">play</span></p>
</div>
<p>⚠️ <i><strong>warning:</strong> loud f*cking noise so adjust your volume accordingly.</i></p>
<p>⚠️ <i><strong>warning:</strong> loud f*cking noise.</i></p>
</section>
<div class="spacer">
<div></div>
</div>
<section id="coffee">
<img src="assets/media/coffee.svg" style="aspect-ratio: 1 / 1" alt="Hand-drawn coffee icon"/>
<p>Did I mention that I like coffee? Well in the past 7 days I have had <strong>4 cups of coffee</strong>, which is roughly equivalent to <strong>0.2 cups per day</strong>. Not too impressive given that my weekly average is 2.2 cups per day.</p>
<p>Did I mention that I like coffee? Well in the past 7 days I have enjoyed a baffling <strong class="coffee">? cups of coffee</strong>, which is roughly equivalent to <strong class="coffee">? cups per day</strong>! <span class="coffee">?</span> impressive given that my weekly average per month is <span class="coffee">?</span> cups per day.</p>
</section>
<div class="spacer">
<div></div>