mirror of
https://codeberg.org/vlw/victorwesterlund.com.git
synced 2025-09-13 19:13:42 +02:00
wip(22w31a)
This commit is contained in:
parent
9f36aee9f0
commit
c10e6a7027
15 changed files with 472 additions and 56 deletions
|
@ -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;
|
||||
|
|
BIN
public/assets/fonts/roboto-mono.ttf
Normal file
BIN
public/assets/fonts/roboto-mono.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/roboto-mono.woff2
Normal file
BIN
public/assets/fonts/roboto-mono.woff2
Normal file
Binary file not shown.
|
@ -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();
|
||||
}
|
||||
}
|
87
public/assets/js/modules/TimeUpdate.mjs
Normal file
87
public/assets/js/modules/TimeUpdate.mjs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
1
public/assets/media/close.svg
Normal file
1
public/assets/media/close.svg
Normal 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 |
1
public/assets/media/github.svg
Normal file
1
public/assets/media/github.svg
Normal 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
31
public/contact.html
Executable 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>
|
|
@ -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, I’m 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
1
public/humans.txt
Executable file
|
@ -0,0 +1 @@
|
|||
This website was created by me, Victor Westerlund. It's not much but it's honest work.
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue