mirror of
https://codeberg.org/vlw/victorwesterlund.com.git
synced 2025-09-14 11:33:41 +02:00
dev21w35c
This commit is contained in:
parent
1015ccd609
commit
6d31cd8e0d
9 changed files with 322 additions and 23 deletions
0
public/assets/css/cards.css
Normal file
0
public/assets/css/cards.css
Normal file
36
public/assets/css/modal.css
Normal file
36
public/assets/css/modal.css
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/* -- Transition overrides -- */
|
||||||
|
|
||||||
|
body {
|
||||||
|
transition: var(--transition) background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.modalActive {
|
||||||
|
background-color: var(--color-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
body main .screen {
|
||||||
|
transition: var(--transition) transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.modalActive main .screen {
|
||||||
|
transition: 300ms;
|
||||||
|
transform: scale(.9,.95);
|
||||||
|
opacity: .5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- Boilerplate -- */
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.active {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
/* Copyright © Victor Westerlund - No libraries! 😲 */
|
/* Copyright © Victor Westerlund - No libraries! 😲 */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--comp-background: 255,255,255;
|
--comp-background: 255,255,255;
|
||||||
--comp-contrast: 33,33,33;
|
--comp-contrast: 33,33,33;
|
||||||
|
@ -10,6 +11,8 @@
|
||||||
|
|
||||||
--padding: 20px;
|
--padding: 20px;
|
||||||
--border-radius: 10px;
|
--border-radius: 10px;
|
||||||
|
--header-height: 100px;
|
||||||
|
--transition: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
@ -53,6 +56,10 @@
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
picture {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -146,10 +153,20 @@ main.active {
|
||||||
color: var(--color-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button.loading p {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.loading::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "loading...";
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* -- Screens -- */
|
/* -- Screens -- */
|
||||||
|
|
||||||
header {
|
header {
|
||||||
--size: 100px;
|
--size: var(--header-height,100px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: var(--padding);
|
padding: var(--padding);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
|
@ -206,14 +223,69 @@ header .logo {
|
||||||
|
|
||||||
/* -- Screen > Landingpage -- */
|
/* -- Screen > Landingpage -- */
|
||||||
|
|
||||||
|
.screen.landingpage .content {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen.landingpage img {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen.landingpage .pattern {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--header-height);
|
||||||
|
width: 100vw;
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen.landingpage .pattern div {
|
||||||
|
--size: clamp(100px,100vw,35vh);
|
||||||
|
position: relative;
|
||||||
|
top: calc((var(--size) - var(--header-height)) * -1);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border: solid var(--size) transparent;
|
||||||
|
border-bottom: solid calc(var(--size) * 2) rgba(var(--comp-accent),.1);
|
||||||
|
transform-origin: 50% 75%;
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
|
||||||
/* -- Screen > Menu -- */
|
/* -- Screen > Menu -- */
|
||||||
|
|
||||||
.screen.menu .content {
|
.screen.menu .content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screen.menu .button[data="contact"] {
|
.screen.menu .button {
|
||||||
|
width: calc(100% - (var(--padding) * 2));
|
||||||
|
max-width: 400px;
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen.menu .button[data-action="openContactCard"] {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 300px) {
|
||||||
|
.button svg:not(.hidden) ~ p,
|
||||||
|
header .logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 230px) {
|
||||||
|
header {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .spacer,
|
||||||
|
header p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
export class Logging {
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
|
|
||||||
|
class Logging {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.endpoint = "/log/";
|
this.endpoint = "/log/";
|
||||||
this.data = new URLSearchParams();
|
this.data = new URLSearchParams();
|
||||||
|
@ -25,3 +27,9 @@ export class Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default class Log {
|
||||||
|
constructor(value,key = "u") {
|
||||||
|
console.log(key,value);
|
||||||
|
}
|
||||||
|
}
|
102
public/assets/js/modules/Modals.mjs
Normal file
102
public/assets/js/modules/Modals.mjs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
|
|
||||||
|
import { default as Interaction, destroy } from "./UI.mjs";
|
||||||
|
|
||||||
|
// Boilerplate for creating element overlays
|
||||||
|
class Modal extends Interaction {
|
||||||
|
constructor(extendedInteractions = {}) {
|
||||||
|
const element = document.createElement("div");
|
||||||
|
let interactions = {
|
||||||
|
close: () => {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
interactions = Object.assign(interactions,extendedInteractions);
|
||||||
|
super(element,interactions);
|
||||||
|
|
||||||
|
this.element = this.applyTemplate(element);
|
||||||
|
this.importStyleSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the companion CSS rules
|
||||||
|
importStyleSheet() {
|
||||||
|
let sheet = "css/modal.css";
|
||||||
|
|
||||||
|
// Import stylesheet with CSS module script if supported
|
||||||
|
if(document.adoptedStyleSheets) {
|
||||||
|
sheet = "../../" + sheet;
|
||||||
|
const module = import(sheet, {
|
||||||
|
assert: { type: "css" }
|
||||||
|
});
|
||||||
|
module.then(style => document.adoptedStyleSheets = [style.default]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit if the stylesheet has already been imported
|
||||||
|
if(document.head.querySelector("link[data-async-modalcss]")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the stylesheet with a link tag
|
||||||
|
sheet = "assets/" + sheet;
|
||||||
|
const element = document.createElement("link");
|
||||||
|
element.setAttribute("rel","stylesheet");
|
||||||
|
element.setAttribute("href",sheet);
|
||||||
|
element.setAttribute("data-async-modalcss","");
|
||||||
|
document.head.appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a modal template to the provided element
|
||||||
|
applyTemplate(element) {
|
||||||
|
// The inner div will contain modal content
|
||||||
|
const inner = document.createElement("div");
|
||||||
|
inner.classList.add("inner");
|
||||||
|
element.appendChild(inner);
|
||||||
|
element.classList.add("modal");
|
||||||
|
|
||||||
|
// PointerEvents on the outer div will close the modal
|
||||||
|
element.addEventListener("click",() => this.close());
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
document.body.classList.add("modalActive");
|
||||||
|
this.element.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the modal and remove it from the DOM
|
||||||
|
close() {
|
||||||
|
const activeModals = document.getElementsByClassName("modal");
|
||||||
|
if(!activeModals || activeModals.length === 1) {
|
||||||
|
// Remove active effects if all modals have been closed
|
||||||
|
document.body.classList.remove("modalActive");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.element.classList.remove("active");
|
||||||
|
setTimeout(() => destroy(this.element),this.transition + 1); // Wait for transition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay with a slide-in animation from the bottom of the viewport
|
||||||
|
export class Card extends Modal {
|
||||||
|
constructor(interactions) {
|
||||||
|
super(interactions);
|
||||||
|
|
||||||
|
this.transition = 300;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent(content) {
|
||||||
|
this.element.insertAdjacentHTML("beforeend",content);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.element.classList.add("card");
|
||||||
|
document.body.appendChild(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
openPage(page) {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
import { Logging } from "./Logging.mjs";
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
|
|
||||||
|
import { default as Logging } from "./Logging.mjs";
|
||||||
|
|
||||||
const interactions = {
|
const interactions = {
|
||||||
toggleMenu: () => {
|
toggleMenu: () => {
|
||||||
|
@ -8,27 +10,81 @@ const interactions = {
|
||||||
menu.style.setProperty("transition",`${speed}ms`);
|
menu.style.setProperty("transition",`${speed}ms`);
|
||||||
menu.classList.toggle("active");
|
menu.classList.toggle("active");
|
||||||
setTimeout(() => menu.style.removeProperty("transition"),speed + 1);
|
setTimeout(() => menu.style.removeProperty("transition"),speed + 1);
|
||||||
|
},
|
||||||
|
openContactCard: () => {
|
||||||
|
const module = import("./Modals.mjs");
|
||||||
|
const interactions = {
|
||||||
|
hello: () => {
|
||||||
|
console.log("Hello world");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.then(modals => {
|
||||||
|
const card = new modals.Card(interactions);
|
||||||
|
card.openPage("contact_card");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Interaction extends Logging {
|
// Remove an element and its subtree
|
||||||
constructor() {
|
export function destroy(family) {
|
||||||
|
while(family.firstChild) {
|
||||||
|
family.removeChild(family.lastChild);
|
||||||
|
}
|
||||||
|
family.parentNode.removeChild(family);
|
||||||
|
}
|
||||||
|
|
||||||
|
// General-purpose scoped event handler
|
||||||
|
export default class Interaction extends Logging {
|
||||||
|
constructor(scope = document.body) {
|
||||||
super();
|
super();
|
||||||
this.attribute = "data-action";
|
this.attribute = "data-action"; // Target elements with this attribute
|
||||||
|
|
||||||
const elements = document.querySelectorAll(`[${this.attribute}]`);
|
// Bind listeners to the target attribute within the provided scope
|
||||||
|
const elements = scope.querySelectorAll(`[${this.attribute}]`);
|
||||||
for(const element of elements) {
|
for(const element of elements) {
|
||||||
element.addEventListener("click",event => this.clickHandler(event));
|
element.addEventListener("click",event => this.pointerEvent(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clickHandler(event) {
|
// Update the theme-color for Chrome on Android devices
|
||||||
|
setThemeColor(color) {
|
||||||
|
const meta = document.head.querySelector("meta[name='theme-color']");
|
||||||
|
const style = getComputedStyle(document.body);
|
||||||
|
|
||||||
|
if(!meta || !color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode will always use the background color
|
||||||
|
if(document.body.classList.contains("dark")) {
|
||||||
|
color = "background";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(color[0] !== "#") {
|
||||||
|
// Get CSS variable if color isn't a HEX code
|
||||||
|
color = style.getPropertyValue(`--color-${color}`);
|
||||||
|
color = color.replaceAll(" ","");
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.setAttribute("content",color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle click/touch interactions
|
||||||
|
pointerEvent(event) {
|
||||||
const target = event.target.closest(`[${this.attribute}]`);
|
const target = event.target.closest(`[${this.attribute}]`);
|
||||||
const action = target?.getAttribute(this.attribute) ?? null;
|
const action = target?.getAttribute(this.attribute) ?? null;
|
||||||
|
|
||||||
if(!target || !action || !Object.keys(interactions).includes(action)) {
|
if(!target || !action || !Object.keys(interactions).includes(action)) {
|
||||||
|
// Exit if the interaction is invalid or action doesn't exist
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
interactions[action]();
|
// Execute the function from the data-action attribute
|
||||||
|
interactions[action](event);
|
||||||
|
|
||||||
|
// The button has requested a theme-color change
|
||||||
|
if(target.hasAttribute("data-theme-color")) {
|
||||||
|
this.setThemeColor(target.getAttribute("data-theme-color"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,22 @@
|
||||||
import { Interaction } from "./modules/UI.mjs";
|
// Copyright © Victor Westerlund - No libraries! 😲
|
||||||
|
import { default as Interaction } from "./modules/UI.mjs";
|
||||||
|
|
||||||
//for(const element of document.getElementsByClassName("hamburger")) {
|
const theme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
// element.addEventListener("click",() => toggleMenu());
|
const main = new Interaction();
|
||||||
//}
|
|
||||||
|
|
||||||
const interaction = new Interaction();
|
function updateTheme() {
|
||||||
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
document.body.classList.remove("dark");
|
||||||
|
|
||||||
|
// Force dark theme on all pages
|
||||||
|
if(media.matches) {
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.setThemeColor("background");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the current page theme, and listen for changes
|
||||||
|
theme.addEventListener("change",updateTheme);
|
||||||
|
updateTheme();
|
0
public/assets/pages/contact_card.html
Normal file
0
public/assets/pages/contact_card.html
Normal file
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<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="#ffffff">
|
<meta name="theme-color" content="">
|
||||||
<link href="assets/img/favicon.png" rel="icon">
|
<link href="assets/img/favicon.png" rel="icon">
|
||||||
<link href="assets/css/style.css" rel="stylesheet">
|
<link href="assets/css/style.css" rel="stylesheet">
|
||||||
<title>Victor Westerlund</title>
|
<title>Victor Westerlund</title>
|
||||||
|
@ -14,19 +14,29 @@
|
||||||
<main>
|
<main>
|
||||||
<div class="screen landingpage">
|
<div class="screen landingpage">
|
||||||
<header>
|
<header>
|
||||||
<div data-action="toggleMenu" class="hamburger center">
|
<div class="hamburger center" data-action="toggleMenu" data-theme-color="contrast">
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="logo"></div>
|
<div class="logo"></div>
|
||||||
<p>victor westerlund</p>
|
<p>victor westerlund</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content center">
|
||||||
|
<div class="pattern center">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<picture>
|
||||||
|
<source srcset="assets/img/myface/highres.avif" type="image/avif" media="(min-width: 1920px)">
|
||||||
|
<source srcset="assets/img/myface/highres.webp" type="image/webp" media="(min-width: 1920px)">
|
||||||
|
<source srcset="assets/img/myface/mediumres.avif" type="image/avif">
|
||||||
|
<source srcset="assets/img/myface/mediumres.webp" type="image/webp">
|
||||||
|
<img src="assets/img/myface/mediumres.png" type="image/png">
|
||||||
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="screen menu dark">
|
<div class="screen menu dark">
|
||||||
<header>
|
<header>
|
||||||
<div data-action="toggleMenu" class="hamburger center">
|
<div class="hamburger center" data-action="toggleMenu" data-theme-color="background">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="28.548" height="22.828"><path d="M2.28 11.414h25.269M1.414 11.414l10-10M1.414 11.414l10 10"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="28.548" height="22.828"><path d="M2.28 11.414h25.269M1.414 11.414l10-10M1.414 11.414l10 10"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
@ -36,10 +46,10 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="button phantom">
|
<div class="button phantom">
|
||||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||||
<p>contact me</p>
|
<p>search stuff..</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="button phantom" data="contact">
|
<div class="button phantom" data-action="openContactCard">
|
||||||
<p>contact me</p>
|
<p>contact me</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue