From 6d31cd8e0d54a463b67d59fb987780d305357434 Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Sun, 5 Sep 2021 18:21:27 +0200 Subject: [PATCH] dev21w35c --- public/assets/css/cards.css | 0 public/assets/css/modal.css | 36 +++++++++ public/assets/css/style.css | 76 ++++++++++++++++++- public/assets/js/modules/Logging.mjs | 10 ++- public/assets/js/modules/Modals.mjs | 102 ++++++++++++++++++++++++++ public/assets/js/modules/UI.mjs | 74 ++++++++++++++++--- public/assets/js/script.js | 25 +++++-- public/assets/pages/contact_card.html | 0 public/index.html | 22 ++++-- 9 files changed, 322 insertions(+), 23 deletions(-) create mode 100644 public/assets/css/cards.css create mode 100644 public/assets/css/modal.css create mode 100644 public/assets/js/modules/Modals.mjs create mode 100644 public/assets/pages/contact_card.html diff --git a/public/assets/css/cards.css b/public/assets/css/cards.css new file mode 100644 index 0000000..e69de29 diff --git a/public/assets/css/modal.css b/public/assets/css/modal.css new file mode 100644 index 0000000..77ca2d6 --- /dev/null +++ b/public/assets/css/modal.css @@ -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; +} \ No newline at end of file diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 5799aa9..0d9d814 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1,4 +1,5 @@ /* Copyright © Victor Westerlund - No libraries! 😲 */ + :root { --comp-background: 255,255,255; --comp-contrast: 33,33,33; @@ -10,6 +11,8 @@ --padding: 20px; --border-radius: 10px; + --header-height: 100px; + --transition: 300ms; } .dark { @@ -53,6 +56,10 @@ color: var(--color-accent); } +picture { + display: contents; +} + html, body { width: 100%; @@ -146,10 +153,20 @@ main.active { color: var(--color-contrast); } +.button.loading p { + opacity: 0; +} + +.button.loading::after { + position: absolute; + content: "loading..."; + opacity: 1; +} + /* -- Screens -- */ header { - --size: 100px; + --size: var(--header-height,100px); box-sizing: border-box; padding: var(--padding); height: var(--size); @@ -206,14 +223,69 @@ header .logo { /* -- 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 .content { display: flex; flex-direction: column; + align-items: center; 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; +} + +@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; + } } \ No newline at end of file diff --git a/public/assets/js/modules/Logging.mjs b/public/assets/js/modules/Logging.mjs index 662b405..3e4d95b 100644 --- a/public/assets/js/modules/Logging.mjs +++ b/public/assets/js/modules/Logging.mjs @@ -1,4 +1,6 @@ -export class Logging { +// Copyright © Victor Westerlund - No libraries! 😲 + +class Logging { constructor() { this.endpoint = "/log/"; this.data = new URLSearchParams(); @@ -24,4 +26,10 @@ export class Logging { fetch(url).catch(response => console.log(response)); } } +} + +export default class Log { + constructor(value,key = "u") { + console.log(key,value); + } } \ No newline at end of file diff --git a/public/assets/js/modules/Modals.mjs b/public/assets/js/modules/Modals.mjs new file mode 100644 index 0000000..1034a7d --- /dev/null +++ b/public/assets/js/modules/Modals.mjs @@ -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(); + } +} \ No newline at end of file diff --git a/public/assets/js/modules/UI.mjs b/public/assets/js/modules/UI.mjs index 06b8f2d..63782e1 100644 --- a/public/assets/js/modules/UI.mjs +++ b/public/assets/js/modules/UI.mjs @@ -1,4 +1,6 @@ -import { Logging } from "./Logging.mjs"; +// Copyright © Victor Westerlund - No libraries! 😲 + +import { default as Logging } from "./Logging.mjs"; const interactions = { toggleMenu: () => { @@ -8,27 +10,81 @@ const interactions = { menu.style.setProperty("transition",`${speed}ms`); menu.classList.toggle("active"); 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 { - constructor() { - super(); - this.attribute = "data-action"; +// Remove an element and its subtree +export function destroy(family) { + while(family.firstChild) { + family.removeChild(family.lastChild); + } + family.parentNode.removeChild(family); +} - const elements = document.querySelectorAll(`[${this.attribute}]`); +// General-purpose scoped event handler +export default class Interaction extends Logging { + constructor(scope = document.body) { + super(); + this.attribute = "data-action"; // Target elements with this attribute + + // Bind listeners to the target attribute within the provided scope + const elements = scope.querySelectorAll(`[${this.attribute}]`); 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 action = target?.getAttribute(this.attribute) ?? null; if(!target || !action || !Object.keys(interactions).includes(action)) { + // Exit if the interaction is invalid or action doesn't exist 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")); + } } } \ No newline at end of file diff --git a/public/assets/js/script.js b/public/assets/js/script.js index 22f7ca7..00abbb0 100644 --- a/public/assets/js/script.js +++ b/public/assets/js/script.js @@ -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")) { -// element.addEventListener("click",() => toggleMenu()); -//} +const theme = window.matchMedia("(prefers-color-scheme: dark)"); +const main = new Interaction(); -const interaction = new Interaction(); \ No newline at end of file +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(); \ No newline at end of file diff --git a/public/assets/pages/contact_card.html b/public/assets/pages/contact_card.html new file mode 100644 index 0000000..e69de29 diff --git a/public/index.html b/public/index.html index 83662ca..7dd7330 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ - + Victor Westerlund @@ -14,19 +14,29 @@
-
+

victor westerlund

-
+
+
+
+
+ + + + + + +