This commit is contained in:
Victor Westerlund 2022-11-03 16:49:50 +01:00 committed by GitHub
commit dea413aca6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 738 additions and 192 deletions

View file

@ -1,205 +1,239 @@
:root { /* -- Cornerstones -- */
--color-base: 0, 0, 0;
--color-contrast: 256, 256, 256;
--padding: clamp(40px, 2vw, 2vw); :root {
--border-size: clamp(4px, .25vw, .25vw); --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;
} }
/* -- Cornerstones -- */ @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; margin: 0;
font-family: "Monaco", "Consolas", monospace, sans-serif; box-sizing: border-box;
color: rgb(var(--color-contrast)); font-family: "Roboto Mono", "Courier", sans-serif;
font-size: 20px;
color: inherit;
} }
*::selection { ::selection {
background-color: rgb(var(--color-contrast)); background-color: var(--color-contrast);
color: rgb(var(--color-base)); color: var(--color-base);
} }
html, ::placeholder {
body { color: rgba(var(--primer-color-contrast), .5);
width: 100%;
height: 100%;
overflow-x: hidden;
} }
html { html {
background-color: rgba(var(--color-base), .7);
background-size: cover; background-size: cover;
background-blend-mode: overlay; background-position: 50% 50%;
background-position: center;
background-attachment: fixed;
} }
picture { body {
display: contents; display: flex;
flex-direction: column;
align-items: center;
color: var(--color-contrast);
background-color: rgba(var(--primer-color-base), .95);
gap: var(--padding);
padding: var(--padding) 0;
padding-bottom: 30vh;
} }
h1 { a:not(p > a) {
font-size: clamp(45px, 7vw, 6vh); text-decoration: none;
}
p, a {
font-size: clamp(20px, 3vw, 2vh);
text-align: justify;
} }
/* -- Components -- */ /* -- 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; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-items: center; justify-content: center;
gap: var(--padding, 30px); gap: var(--padding);
height: 3em;
} }
body > div { input,
padding: calc(var(--padding) / 2); .button.phantom {
background-color: transparent;
border: solid 2px var(--color-contrast);
color: var(--color-contrast);
} }
:is(#intro, #card) a { img,
--padding-vert: clamp(17px, 1.1vw, 1.1vw); .button {
display: inline-block;
text-decoration: none;
text-align: center;
user-select: none; 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);
} }
/* --- */ .button :not(p) {
height: 2em;
#intro {
padding: calc(var(--padding) / 2);
} }
#intro a { .interact::before {
padding: var(--padding-vert) 2vw; content: "tap ";
border-radius: 100px; }
border: solid var(--border-size) rgba(var(--color-contrast), 0);
/* ---- */
.spacer {
display: grid;
justify-items: center;
}
body > .spacer {
margin: var(--padding) 0; margin: var(--padding) 0;
width: calc(100% - ((var(--padding) / 2) + var(--border-size)));
} }
#intro p { .spacer div {
margin: 1vh 0; width: 205px;
font-size: clamp(20px, 3vw, 3vh); height: 7px;
background-color: var(--color-contrast);
transform: rotate(-4deg);
} }
/* --- */ /* -- Content -- */
#card, form,
#card > div { section {
max-width: 600px;
margin: var(--padding) calc(var(--padding) * 1.5);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: calc(var(--padding) / 2); text-align: justify;
gap: var(--padding);
} }
#card { section > *,
--portrait-size: clamp(128px, 12vw, 12vh); section picture > img {
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 {
width: 100%; width: 100%;
padding: var(--padding-vert) 0;
margin-top: calc(var(--padding) / 2);
border-radius: clamp(9px, .5vw, .5vw);
} }
/* -- Media Queries -- */ section#code {
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))))) { section#intro {
#card { max-width: 250px;
background-color: rgba(var(--color-base), .7); text-align: center;
}
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 {
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), .1);
}
/* ---- */
.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) { @media (pointer: fine) {
:is(#intro, #card) a { .button {
--transition-speed: 200ms; cursor: pointer;
transition:
var(--transition-speed) background-color,
var(--transition-speed) box-shadow,
var(--transition-speed) border-color;
} }
:is(#intro, #card) a:hover { .interact::before {
background-color: rgba(var(--color-contrast), .2); content: "click ";
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), @media (prefers-color-scheme: dark) {
0 .3vh .6vh rgba(0, 0, 0, .23); :root {
--primer-color-base: 0, 0, 0;
--primer-color-contrast: 255, 255, 255;
} }
:is(#intro, #card) a:active { img:not(picture img) {
background-color: rgba(var(--color-contrast), .15); filter: invert(1);
} }
} }
@media (max-width: 330px) { @media (max-width: 330px) {
p, a { section#sky .button img {
text-align: left; display: none;
font-size: 18px;
}
#card {
padding: calc(var(--padding) / 2);
} }
} }
@media (min-aspect-ratio: 14/9) and (min-height: 600px) { @media (min-width: 660px) {
body { input,
.button {
height: 3.5em;
}
section#code {
display: grid; display: grid;
grid-template-columns: 1fr 40px 1fr;
width: 100%;
}
section#contact > div {
grid-template-rows: 1fr;
grid-template-columns: repeat(2, 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;
}
}

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,217 @@
export class Dialog {
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") {
this.dialog = {
title: "",
content: ""
}
}
// 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);
}
}
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,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();
}
}

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,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";

View file

@ -1,14 +1,75 @@
import { default as Glitch } from "./glitch/Glitch.mjs"; import TimeUpdate from "./modules/TimeUpdate.mjs";
const logging = "https://victorwesterlund-logging-dnzfgzf6za-lz.a.run.app"; // 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;
// Log link clicks // Animate a progress bar as media is playing
for(let link of document.getElementsByTagName("a")) { audioPlayer.addEventListener("playing", () => {
link.addEventListener("click", event => { const color = "rgba(var(--primer-color-contrast), .1)";
event.preventDefault();
navigator?.sendBeacon(logging, event); const keyframes = [
window.location.href = event.target.href; { 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 });
});
// 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});
}); });
} }
window.glitch = new Glitch(document.body.parentElement); // 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.056 22.944"><path d="M3.143 11.479s6.452-20.136 18.27 0 21.315 0 21.315 0" fill="none" stroke="#000" stroke-linecap="round" stroke-width="5"/></svg>

After

Width:  |  Height:  |  Size: 204 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:image' content='//victorwesterlund.com/assets/media/banner.jpg'>
<meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'> <meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'>
<meta property='og:url' content='https://victorwesterlund.com'> <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:no-preference)">
<link rel="icon" href="/assets/media/favicon-dark.png" media="(prefers-color-scheme:light)"> <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="icon" href="assets/media/favicon-light.png" media="(prefers-color-scheme:dark)">
<link rel="stylesheet" href="/assets/css/style.css"> <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> </head>
<body> <body>
<div> <section>
<div id="intro"> <h1>Oh, Im in the wrong place, man!</h1>
<h1>there is nothing here</h1> <p>404 - Not Found</p>
<p>and that's all I know</p> </section>
<a href="/">take me home -></a> <div class="spacer"><div></div></div>
</div> <section>
</div> <h1></h1>
<script type="module" src="/assets/js/script.mjs"></script> <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> </body>
</html> </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,44 +6,95 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Full-stack web developer from Stockholm, Sweden."> <meta name="description" content="Full-stack web developer from Stockholm, Sweden.">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<meta property='og:title' content='Victor Westerlund'> <meta property='og:title' content='Victor Westerlund'>
<meta property='og:image' content='//victorwesterlund.com/assets/media/banner.jpg'> <meta property='og:image' content='assets/media/banner.jpg'>
<meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'> <meta property='og:description' content='Full-stack web developer from Stockholm, Sweden.'>
<meta property='og:url' content='https://victorwesterlund.com'> <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:no-preference)">
<link rel="icon" href="assets/media/favicon-dark.png" media="(prefers-color-scheme:light)"> <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="icon" href="assets/media/favicon-light.png" media="(prefers-color-scheme:dark)">
<link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="assets/css/style.css">
</head> </head>
<body> <body>
<div> <section id="intro">
<div id="intro"> <p><strong>victor westerlund</strong></p>
<p>hello, my name is</p> <div class="spacer">
<h1>victor</h1> <div></div>
<p>I'm a</p>
<h1>full-stack</h1>
<h1>developer</h1>
<p>from Sweden</p>
<a href="https://github.com/VictorWesterlund">my github -></a>
</div> </div>
<p>I make things with code and coffee</p>
</section>
<section id="code">
<a href="https://github.com/VictorWesterlund" target="_blank" title="Github profile" rel="noreferrer noopener">
<div class="button">
<img src="assets/media/github.svg" alt="GitHub"/>
<p>on github</p>
</div>
</a>
<p>and</p>
<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 <span id="time">00:00</span> in Sweden, and I will reply as soon as possible.</p>
<div>
<a href="#" title="Copy mail address">
<div class="button">
<p>copy email address</p>
</div>
</a>
<a href="contact" title="Contact me">
<div class="button phantom dialog">
<p>or use this form</p>
</div>
</a>
</div>
</section>
<div class="banner">
<p><strong>random stuff</strong></p>
</div> </div>
<div> <section id="sky">
<div id="card"> <picture>
<picture> <source srcset="https://friday.victorwesterlund.com/sky/original.webp" type="image/webp">
<source srcset="assets/media/pfp/256.webp" type="image/webp" media="(min-width: 600px)"> <img src="https://friday.victorwesterlund.com/sky/original.jpg" style="aspect-ratio: 16 / 9" alt="Webcam stillframe of the sky from my balcony"/>
<source srcset="assets/media/pfp/128.webp" type="image/webp"> </picture>
<source srcset="assets/media/pfp/256.jpg" type="image/jpg" media="(min-width: 600px)"> <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>
<img src="assets/media/pfp/128.jpg" alt="portrait of victor"/> <a href="#" title="Sky timelapse">
</picture> <div class="button">
<div> <p>view timelapse</p>
<p>I create things with code. When I'm not creating things with code, I enjoy skiing, watching movies and some occasional gaming</p>
<p>And beyond computer science, I'm also an armchair rabbit-holer for engineering, physics and astronomy</p>
</div> </div>
<div> </a>
<p>...and ☕, full-time</p> <div class="button phantom player" data-src="https://friday.victorwesterlund.com/sky/robot36.wav">
</div> <img src="assets/media/wave.svg"/>
<!--<a href="contact">contact me</a>--> <p><strong>Robot36</strong><br><span class="interact"></span>to <span class="playstate">play</span></p>
</div> </div>
<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 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>
</div>
<section id="sneaky">
<p>Enter your top sneaky code here. You will know when you've struck the jackpot.</p>
<input placeholder="uuddlrlrabs"></input>
<p>So you don't have a sneaky code?<br><a href="#">Solve this puzzle</a> and I'll buy you a cup of coffee.</p>
</section>
<div class="spacer">
<div></div>
</div> </div>
<script type="module" src="assets/js/script.mjs"></script> <script type="module" src="assets/js/script.mjs"></script>
</body> </body>