diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 4713612..0c79b6f 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 828feec..89c5fd9 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,54 +2,43 @@ - - - - - - - - - - - - + + + + + + + - - - - - + + - - - - - + + + - - - - + + - - - - + + + + + @@ -82,7 +71,7 @@ - + @@ -156,7 +145,10 @@ - + + + + diff --git a/README.md b/README.md index 6228e35..f74053b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Timeline -[Click for live version](https://schmelczer.dev) - An easily configurable portfolio. +> [Click for live version](https://schmelczer.dev) + +## Configuration +- The actual content is in [portfolio.ts](src/portfolio.ts). +- The assets referenced by this file should be located in [src/static](src/static). + + + diff --git a/package.json b/package.json index e876267..a94840e 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "html-webpack-plugin": "^3.2.0", "image-webpack-loader": "^6.0.0", "mini-css-extract-plugin": "^0.9.0", - "node-sass": "^4.13.0", + "sass": "^1.24.2", "optimize-css-assets-webpack-plugin": "^5.0.3", "postcss-loader": "^3.0.0", "prettier": "^1.19.1", @@ -56,5 +56,8 @@ "webpack": "^4.41.4", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.10.1" + }, + "dependencies": { + "sass": "latest" } } diff --git a/src/framework/container-page.ts b/src/framework/container-page.ts index fb1e1d5..7521a04 100644 --- a/src/framework/container-page.ts +++ b/src/framework/container-page.ts @@ -3,16 +3,18 @@ import { PageEventType } from './page-event'; export class ContainerPage extends PageElement { public constructor(rootElement: HTMLElement, children: Array) { - children.forEach(e => rootElement.appendChild(e.element)); + children + .filter(e => e.element) + .forEach(e => rootElement.appendChild(e.element)); super(rootElement, children); } public setAsMain() { - this.broadcastEvent({ type: PageEventType.onLoad }, this); - this.broadcastEvent( { type: PageEventType.eventBroadcasterChanged, data: this }, this ); + + this.broadcastEvent({ type: PageEventType.onLoad }, this); } } diff --git a/src/framework/helper/dark-mode.ts b/src/framework/helper/dark-mode.ts new file mode 100644 index 0000000..5750422 --- /dev/null +++ b/src/framework/helper/dark-mode.ts @@ -0,0 +1,9 @@ +export const isSystemLevelDarkModeEnabled = (): boolean => + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches; + +export const turnOnDarkMode = () => + document.body.parentElement.setAttribute('theme', 'dark'); + +export const turnOnLightMode = () => + document.body.parentElement.setAttribute('theme', 'light'); diff --git a/src/framework/helper/mix-colors.ts b/src/framework/helper/mix-colors.ts index 81ceb6d..2f89fd1 100644 --- a/src/framework/helper/mix-colors.ts +++ b/src/framework/helper/mix-colors.ts @@ -20,16 +20,12 @@ export const mixColors = ( const hexToRGB = (hex: hex): rgb => { const [r1, r2, g1, g2, b1, b2] = normalizeHex(hex); - return [ - Number.parseInt(r1 + r2, 16), - Number.parseInt(g1 + g2, 16), - Number.parseInt(b1 + b2, 16), - ]; + return [parseInt(r1 + r2, 16), parseInt(g1 + g2, 16), parseInt(b1 + b2, 16)]; }; const normalizeHex = (hex: hex): hex => { hex = hex.trim(); - if (hex.startsWith('#')) { + if (hex[0] === '#') { hex = hex.substr(1); } return hex; @@ -38,4 +34,4 @@ const normalizeHex = (hex: hex): hex => { const mix = (a: number, b: number, q: number): number => a * q + b * (1 - q); const rgbToHex = (rgb: rgb): hex => - '#' + rgb.map(n => Math.round(n).toString(16)).join(''); + '#' + rgb.map(n => (n < 16 ? '0' : '') + Math.round(n).toString(16)).join(''); diff --git a/src/framework/helper/random.ts b/src/framework/helper/random.ts index 68a2c05..aad6370 100644 --- a/src/framework/helper/random.ts +++ b/src/framework/helper/random.ts @@ -1,5 +1,9 @@ +import { addImul } from '../polyfills'; + export class Random { - public constructor(private seed: number) {} + public constructor(private seed: number) { + addImul(); + } public get next(): number { // result is in [0, 1) diff --git a/src/framework/page-element.ts b/src/framework/page-element.ts index d06e3fa..e18c9d9 100644 --- a/src/framework/page-element.ts +++ b/src/framework/page-element.ts @@ -5,7 +5,7 @@ export abstract class PageElement implements EventBroadcaster { protected eventBroadcaster: EventBroadcaster; protected constructor( - private readonly rootElement: HTMLElement, + private readonly rootElement?: HTMLElement, private readonly children: Array = [] ) {} diff --git a/src/framework/page-event.ts b/src/framework/page-event.ts index 0a682eb..71447b0 100644 --- a/src/framework/page-event.ts +++ b/src/framework/page-event.ts @@ -6,5 +6,6 @@ export class PageEvent { export enum PageEventType { onLoad, onBodyDimensionsChanged, - eventBroadcasterChanged + eventBroadcasterChanged, + pageThemeChanged, } diff --git a/src/framework/polyfills.ts b/src/framework/polyfills.ts new file mode 100644 index 0000000..bc31a07 --- /dev/null +++ b/src/framework/polyfills.ts @@ -0,0 +1,17 @@ +export const addImul = () => { + if (!Math.imul) + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul + Math.imul = function(opA, opB) { + opB |= 0; // ensure that opB is an integer. opA will automatically be coerced. + // floating points give us 53 bits of precision to work with plus 1 sign bit + // automatically handled for our convenience: + // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001 + // 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + let result = (opA & 0x003fffff) * opB; + // 2. We can remove an integer coercion from the statement above because: + // 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001 + // 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + if (opA & 0xffc00000 /*!== 0*/) result += ((opA & 0xffc00000) * opB) | 0; + return result | 0; + }; +}; diff --git a/src/framework/primitives/primitives.scss b/src/framework/primitives/primitives.scss index 705bd52..38194c6 100644 --- a/src/framework/primitives/primitives.scss +++ b/src/framework/primitives/primitives.scss @@ -1,25 +1,27 @@ @import '../../style/vars'; @import '../../style/mixins'; -.figure-container { - font-size: 0; - box-shadow: inset $shadow1, inset $shadow2; - pointer-events: none; - cursor: pointer; +@include responsive() using ($vars) { + .figure-container { + font-size: 0; + box-shadow: inset map_get($vars, $shadow1), inset map_get($vars, $shadow2); + pointer-events: none; + cursor: pointer; - * { - pointer-events: all; - position: relative; - z-index: -2; + * { + pointer-events: all; + position: relative; + z-index: -2; + } + } + + .primitive-text, + .primitive-anchor, + .figure-container { + margin-top: map_get($vars, $line-height); + } + + .primitive-text { + text-align: left; } } - -.primitive-text, -.primitive-anchor, -.figure-container { - margin-top: $line-height; -} - -.primitive-text { - text-align: left; -} diff --git a/src/index.html b/src/index.html index d8367a6..2e1f704 100644 --- a/src/index.html +++ b/src/index.html @@ -17,8 +17,8 @@ Portfolio - AndrĂ¡s Schmelczer -
+
-
+ diff --git a/src/page/about/about.scss b/src/page/about/about.scss index 778458d..7c2c02e 100644 --- a/src/page/about/about.scss +++ b/src/page/about/about.scss @@ -1,64 +1,75 @@ @import '../../style/mixins'; @import '../../style/vars'; -#about { - @include important-card(); +@include responsive() using ($vars) { + #about { + @include card-base($vars); - $img-size: 125px; + background-color: map_get($vars, $important-card-color); - h1, - img, - .placeholder, - & { - @include title-font(); - } - - img { - @include square($img-size); - border-radius: 100%; - } - - p { - @include main-font(); - text-align: justify; - margin-top: $small-margin; - } - - h1 { - hyphens: none; - } - - @media (max-width: $breakpoint-width) { - h1 { - margin-top: $small-margin; + * { + color: map_get($vars, $important-card-text-color); } - } - @media (min-width: $breakpoint-width) { - $img-size: 190px; + $img-size: 125px; - width: $body-width; - margin: calc(#{$normal-margin} + #{$img-size} * 1 / 3) auto 0 auto; - position: relative; - border-radius: $border-radius; + h1, + img, + .placeholder, + & { + @include title-font(); + } img { @include square($img-size); - position: absolute; - left: 0; - top: 0; - transform: translateY(-$img-size * 1/3) translateX(-$img-size * 1/3); + border-radius: 100%; } - .placeholder { - @include square(calc(#{$img-size} * 2 / 3 - #{$normal-margin})); - box-sizing: content-box; - float: left; - margin: 0 0.75ex 0.75ex 0; + p { + @include main-font(); + text-align: justify; + margin-top: map_get($vars, $small-margin); } h1 { - text-align: left; + hyphens: none; + } + + @include on-small-screen { + h1 { + margin-top: map_get($vars, $small-margin); + } + } + + @include on-large-screen { + $img-size: 190px; + + width: map_get($vars, $body-width); + margin: calc(#{map_get($vars, $normal-margin)} + #{$img-size} * 1 / 3) + auto 0 auto; + position: relative; + border-radius: map_get($vars, $border-radius); + + img { + @include square($img-size); + position: absolute; + left: 0; + top: 0; + transform: translateY(-$img-size * 1/3) translateX(-$img-size * 1/3); + } + + .placeholder { + @include square( + calc(#{$img-size} * 2 / 3 - #{map_get($vars, $normal-margin)}) + ); + box-sizing: content-box; + float: left; + margin: 0 0.75ex 0.75ex 0; + } + + h1 { + text-align: left; + } } } } diff --git a/src/page/about/about.ts b/src/page/about/about.ts index 5bb90bb..2f0ad5e 100644 --- a/src/page/about/about.ts +++ b/src/page/about/about.ts @@ -5,9 +5,13 @@ import { PageElement } from '../../framework/page-element'; import { generate } from './about.html'; import { createElement } from '../../framework/helper/create-element'; import { ContainerPage } from '../../framework/container-page'; +import { PageThemeSwitcher } from '../theme-switcher/theme-switcher'; export class PageHeader extends ContainerPage { public constructor(header: Header) { - super(createElement(generate(header)), [new PageContent(header.about)]); + super(createElement(generate(header)), [ + new PageContent(header.about), + new PageThemeSwitcher(), + ]); } } diff --git a/src/page/background/background.html.ts b/src/page/background/background.html.ts index 533b9ed..d8adcc7 100644 --- a/src/page/background/background.html.ts +++ b/src/page/background/background.html.ts @@ -2,7 +2,5 @@ import { html } from '../../model/misc'; import './background.scss'; export const generate = (): html => ` -
-
-
+
`; diff --git a/src/page/background/background.scss b/src/page/background/background.scss index 554c572..ce91f5a 100644 --- a/src/page/background/background.scss +++ b/src/page/background/background.scss @@ -1,43 +1,39 @@ @import '../../style/vars'; @import '../../style/mixins'; -#background-container { - position: fixed; - left: 0; - top: 0; - height: 100vh; - width: 100%; +@include responsive() using ($vars) { + div.background-element { + position: -webkit-sticky; + position: absolute; + left: 0; + top: 0; + border-radius: 100px; + width: 140px; - z-index: -1; - -webkit-overflow-scrolling: touch; - perspective: 5px; - perspective-origin: center center; - overflow: hidden; + overflow: visible !important; // IE11 fix for disappearing elements - #background { - overflow: hidden; - will-change: width, height; - transition: height $long-transition-time, width $long-transition-time; - transform-style: flat; + @media (prefers-reduced-motion: reduce) { + & { + display: none; + } + } - div { - position: -webkit-sticky; - position: absolute; - left: 0; - top: 0; - border-radius: 100px; - width: 140px; + @media print { + & { + display: none; + } + } - transition: transform $long-transition-time, opacity $long-transition-time; - will-change: transform, opacity; - animation: fade-in 1s linear; - @keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + transition: transform map_get($vars, $long-transition-time), + opacity map_get($vars, $long-transition-time); + will-change: transform, opacity; + animation: fade-in 1s linear; + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; } } } diff --git a/src/page/background/background.ts b/src/page/background/background.ts index 75ff8a5..3122bc2 100644 --- a/src/page/background/background.ts +++ b/src/page/background/background.ts @@ -1,8 +1,6 @@ import { PageElement } from '../../framework/page-element'; import { PageEvent, PageEventType } from '../../framework/page-event'; -import { createElement } from '../../framework/helper/create-element'; import { Blob } from './blob'; -import { generate } from './background.html'; import { Random } from '../../framework/helper/random'; import { getHeight } from '../../framework/helper/get-height'; import { sum } from '../../framework/helper/sum'; @@ -12,7 +10,7 @@ export class PageBackground extends PageElement { private readonly blobSpacing = 350; public constructor(private start: PageElement, private end: PageElement) { - super(createElement(generate())); + super(); Blob.initialize(10, 30, 5); } @@ -21,37 +19,33 @@ export class PageBackground extends PageElement { this.bindListeners(parent); } else if (event.type === PageEventType.onBodyDimensionsChanged) { this.resize(parent, event.data?.deltaHeight); + } else if (event.type === PageEventType.pageThemeChanged) { + Blob.changeTheme(event.data); + this.blobs.forEach(b => b.decideColor()); } } private bindListeners(parent: PageElement) { window.addEventListener('resize', () => this.resize(parent)); window.addEventListener('load', () => this.resize(parent)); - parent.element.addEventListener( - 'scroll', - () => (this.element.scrollTop = parent.element.scrollTop) - ); } private resize(parent: PageElement, heightChange?: number) { - const siblings: Array = this.getSiblings(parent); + const siblings: Array = PageBackground.getSiblings(parent); - const width = parent.element.clientWidth; + const width = document.body.clientWidth; let height = sum(siblings.map(getHeight)); if (heightChange) { height += heightChange; } - this.query('#background').style.width = `${width}px`; - this.query('#background').style.height = `${height}px`; - const requiredBlobCount = Math.round( (width * height) / this.blobSpacing ** 2 ); while (requiredBlobCount > this.blobs.length) { const blob = new Blob(); - this.query('#background').appendChild(blob.htmlElement); + parent.element.appendChild(blob.htmlElement); this.blobs.push(blob); } @@ -74,9 +68,9 @@ export class PageBackground extends PageElement { }); } - private getSiblings(parent: PageElement): Array { + private static getSiblings(parent: PageElement): Array { return Array.prototype.slice .call(parent.element.children) - .filter(e => e !== this.element); + .filter((e: HTMLElement) => !e.classList.contains('background-element')); } } diff --git a/src/page/background/blob.ts b/src/page/background/blob.ts index ada2eb7..7485cbc 100644 --- a/src/page/background/blob.ts +++ b/src/page/background/blob.ts @@ -1,10 +1,14 @@ import { mixColors } from '../../framework/helper/mix-colors'; import { createElement } from '../../framework/helper/create-element'; import { Random } from '../../framework/helper/random'; +import { generate } from './background.html'; export class Blob { private static readonly creatorRandom = new Random(44); - private static readonly colors = ['#fff9e0', '#ffd6d6']; + private static colorPickerRandom = new Random(132); + private static readonly lightColors = ['#fff9e0', '#ffd6d6']; + private static readonly darkColors = ['#2C477A']; + private static isDarkThemed = false; private static zMin: number; private static zMax: number; private static perspective: number; @@ -14,25 +18,36 @@ export class Blob { Blob.perspective = perspective; } + public static changeTheme(isDarkThemed: boolean) { + Blob.colorPickerRandom = new Random(132); + Blob.isDarkThemed = isDarkThemed; + } + private readonly z = Blob.creatorRandom.randomInInterval( Blob.zMin, Blob.zMax ); - private readonly element: HTMLElement = createElement('
'); + private readonly element: HTMLElement = createElement(generate()); constructor() { - this.element.style.backgroundColor = mixColors( - '#ffffff', - Blob.creatorRandom.choose(Blob.colors), - (this.z - Blob.zMin) / (Blob.zMax - Blob.zMin) - ); - this.element.style.zIndex = (-this.z).toString(); + this.decideColor(); + this.element.style.zIndex = Math.round(-this.z).toString(); this.element.style.height = `${Blob.creatorRandom.randomInInterval( 160, 740 )}px`; } + public decideColor() { + this.element.style.backgroundColor = mixColors( + Blob.isDarkThemed ? '#242638 ' : '#ffffff', + Blob.colorPickerRandom.choose( + Blob.isDarkThemed ? Blob.darkColors : Blob.lightColors + ), + (this.z - Blob.zMin) / (Blob.zMax - Blob.zMin) + ); + } + get htmlElement(): HTMLElement { return this.element; } diff --git a/src/page/content/content.html.ts b/src/page/content/content.html.ts new file mode 100644 index 0000000..5cfe3ef --- /dev/null +++ b/src/page/content/content.html.ts @@ -0,0 +1,10 @@ +import { Content } from '../../model/portfolio'; +import { html } from '../../model/misc'; + +import './content.scss'; + +export const generate = (content: Content): html => ` +
+ ${content.map(element => element.toHTML()).join('\n')} +
+`; diff --git a/src/page/content/content.scss b/src/page/content/content.scss index 90d64b6..e70a4d2 100644 --- a/src/page/content/content.scss +++ b/src/page/content/content.scss @@ -1,5 +1,8 @@ @import '../../style/vars'; +@import '../../style/mixins'; -.content { - margin-top: $small-margin; +@include responsive() using ($vars) { + .content { + margin-top: map_get($vars, $small-margin); + } } diff --git a/src/page/content/content.ts b/src/page/content/content.ts index 9115b4e..2a0b163 100644 --- a/src/page/content/content.ts +++ b/src/page/content/content.ts @@ -1,16 +1,10 @@ -import './content.scss'; import { PageElement } from '../../framework/page-element'; import { createElement } from '../../framework/helper/create-element'; import { Content } from '../../model/portfolio'; +import { generate } from './content.html'; export class PageContent extends PageElement { public constructor(content: Content) { - super( - createElement(` -
- ${content.map(element => element.toHTML()).join('\n')} -
- `) - ); + super(createElement(generate(content))); } } diff --git a/src/page/footer/footer.html.ts b/src/page/footer/footer.html.ts index 1e5fb47..c5a1a15 100644 --- a/src/page/footer/footer.html.ts +++ b/src/page/footer/footer.html.ts @@ -1,7 +1,5 @@ import { Footer } from '../../model/portfolio'; import { html } from '../../model/misc'; -import emailIcon from '../../static/icons/at.svg'; -import cvIcon from '../../static/icons/cv.svg'; import './footer.scss'; @@ -20,13 +18,23 @@ export const generate = ({ .map( cv => `
  • - CV + + + + + + + + + ${cv.name}
  • ` ) .join('\n')}
  • - email + + + ${email}
  • diff --git a/src/page/footer/footer.scss b/src/page/footer/footer.scss index e2dd0b0..b1855ff 100644 --- a/src/page/footer/footer.scss +++ b/src/page/footer/footer.scss @@ -1,51 +1,58 @@ @import '../../style/mixins'; @import '../../style/vars'; -footer#page-footer { - text-align: center; +@include responsive() using ($vars) { + footer#page-footer { + text-align: center; - margin: $large-margin auto 0 auto; - width: 100%; + margin: map_get($vars, $large-margin) auto 0 auto; + width: 100%; - h2 { - @include title-font(); - } + h2 { + @include title-font(); + } - ul { - list-style: none; - display: inline-block; - margin-top: $normal-margin; - text-align: left; + ul { + list-style: none; + display: inline-block; + margin-top: map_get($vars, $normal-margin); + text-align: left; - li { - @include center-children(); - justify-content: flex-start; + li { + @include center-children(); + justify-content: flex-start; - &:not(:first-child) { - padding-top: $line-height; - } + &:not(:first-child) { + padding-top: map_get($vars, $line-height); + } - img { - @include max-square($icon-size); - margin-right: $small-margin; - } + img, + svg { + @include max-square(map_get($vars, $icon-size)); + * { + fill: map_get($vars, $normal-text-color); + } + margin-right: map_get($vars, $small-margin); + } - a { - font-size: 1.4rem; + a { + font-size: 1.4rem; + } } } - } - aside.other { - @include center-children(); - flex-direction: column; - margin: $large-margin auto $line-height auto; - width: $body-width; + aside.other { + @include center-children(); + flex-direction: column; + margin: map_get($vars, $large-margin) auto map_get($vars, $line-height) + auto; + width: map_get($vars, $body-width); - h6 { - @include insignificant-font(); - display: inline; - opacity: 0.75; + h6 { + @include insignificant-font(); + display: inline; + opacity: 0.75; + } } } } diff --git a/src/page/image-viewer/image-viewer.html.ts b/src/page/image-viewer/image-viewer.html.ts index 8900362..2ccf763 100644 --- a/src/page/image-viewer/image-viewer.html.ts +++ b/src/page/image-viewer/image-viewer.html.ts @@ -1,10 +1,11 @@ -import { html } from "../../model/misc"; -import cancel from "../../static/icons/cancel.svg"; -import "./image-viewer.scss"; +import { html } from '../../model/misc'; +import cancel from '../../static/icons/cancel.svg'; + +import './image-viewer.scss'; export const generate = (): html => `
    - currently opened photo +
    cancel
    `; diff --git a/src/page/image-viewer/image-viewer.scss b/src/page/image-viewer/image-viewer.scss index da86d99..e405df2 100644 --- a/src/page/image-viewer/image-viewer.scss +++ b/src/page/image-viewer/image-viewer.scss @@ -1,30 +1,41 @@ @import '../../style/vars'; @import '../../style/mixins'; -#image-viewer { - @include center-children(); - display: none; - position: fixed; - width: 100%; - height: 100%; - left: 0; - top: 0; - margin: 0; - z-index: 2; - background-color: rgba(0, 0, 0, 0.75); - - #photo { - max-width: 80vw; - max-height: 80vh; - } - - #cancel { - @include square($icon-size); - position: absolute; - box-sizing: content-box; - padding: $normal-margin; - right: 0; +@include responsive() using ($vars) { + #image-viewer { + @include center-children(); + display: none; + position: fixed; + width: 100%; + height: 100%; + left: 0; top: 0; - cursor: pointer; + margin: 0; + z-index: 2; + background-color: rgba(0, 0, 0, 0.85); + + @include on-large-screen { + #container > * { + max-width: 80vw; + max-height: 80vh; + } + } + + @include on-small-screen { + #container > * { + max-width: 95vw; + max-height: 95vh; + } + } + + #cancel { + @include square(map_get($vars, $icon-size)); + position: absolute; + box-sizing: content-box; + padding: map_get($vars, $normal-margin); + right: 0; + top: 0; + cursor: pointer; + } } } diff --git a/src/page/image-viewer/image-viewer.ts b/src/page/image-viewer/image-viewer.ts index 67c1769..8a4c5b7 100644 --- a/src/page/image-viewer/image-viewer.ts +++ b/src/page/image-viewer/image-viewer.ts @@ -6,9 +6,8 @@ import { createElement } from '../../framework/helper/create-element'; export class PageImageViewer extends PageElement { public constructor() { - const root = createElement(generate()); - root.onclick = () => PageImageViewer.hide(root); - super(root); + super(createElement(generate())); + this.element.onclick = () => PageImageViewer.hide(this.element); } protected handleEvent(event: PageEvent, parent: PageElement) { @@ -18,25 +17,26 @@ export class PageImageViewer extends PageElement { document.body.addEventListener('keydown', this.handleKeydown.bind(this)); - const images = Array.prototype.slice.call( - parent.element.querySelectorAll('img') + const media = Array.prototype.slice.call( + parent.element.querySelectorAll('img, video') ); - images + + media .filter( - (img: HTMLImageElement) => - img.parentElement !== this.element && - !img.classList.contains('no-open') + (e: HTMLElement) => + e.parentElement !== this.element && !e.classList.contains('no-open') ) .forEach( - (img: HTMLImageElement) => (img.onclick = this.handleClick.bind(this)) + (e: HTMLImageElement) => (e.onclick = this.handleClick.bind(this)) ); } private handleClick(event: Event) { - (this.query( - '#photo' - ) as HTMLImageElement).src = (event.target as HTMLImageElement).src; - + const container = this.query('#container'); + Array.prototype.forEach.call(container.childNodes, (e: HTMLElement) => + e.remove() + ); + container.appendChild((event.target as HTMLElement).cloneNode()); PageImageViewer.show(this.element); } diff --git a/src/page/index.ts b/src/page/index.ts index 4bfacf7..f41e352 100644 --- a/src/page/index.ts +++ b/src/page/index.ts @@ -12,7 +12,7 @@ export const create = ({ header, timeline, footer }: Portfolio) => { new ContainerPage(document.body, [ new PageImageViewer(), - new ContainerPage(document.body.querySelector('main'), [ + new ContainerPage(document.body.querySelector('.main'), [ pageHeader, new PageTimeline(timeline), pageFooter, diff --git a/src/page/theme-switcher/theme-switcher.html.ts b/src/page/theme-switcher/theme-switcher.html.ts new file mode 100644 index 0000000..ce0a54b --- /dev/null +++ b/src/page/theme-switcher/theme-switcher.html.ts @@ -0,0 +1,7 @@ +import { html } from '../../model/misc'; + +import './theme-switcher.scss'; + +export const generate = (): html => ` + +`; diff --git a/src/page/theme-switcher/theme-switcher.scss b/src/page/theme-switcher/theme-switcher.scss new file mode 100644 index 0000000..bbaad22 --- /dev/null +++ b/src/page/theme-switcher/theme-switcher.scss @@ -0,0 +1,94 @@ +@import '../../style/mixins'; +@import '../../style/vars'; + +@include responsive using($vars) { + input[type='checkbox']#theme-switcher { + @include on-large-screen { + position: fixed; + top: map_get($vars, $large-margin); + right: map_get($vars, $large-margin); + } + + @include on-small-screen { + position: relative; + margin-top: map_get($vars, $small-margin); + } + + $size: map_get($vars, $icon-size); + $small-size: 4 / 5 * $size; + + z-index: 10; + + -webkit-appearance: none; + -moz-appearance: none; + + width: 2 * $size; + height: $size; + + border-radius: 1000px; + box-shadow: map_get($vars, $shadow1), map_get($vars, $shadow2); + + cursor: pointer; + + &:before, + &:after { + content: ''; + position: absolute; + display: block; + border-radius: 1000px; + + @include square($size); + + transition: height map_get($vars, $long-transition-time), + width map_get($vars, $long-transition-time), + left map_get($vars, $long-transition-time), + top map_get($vars, $long-transition-time), + transform map_get($vars, $long-transition-time), + background-color map_get($vars, $long-transition-time); + } + + &:before { + left: 0; + background-color: transparent; + } + + &:after { + $delta: 4px; + top: -$delta / 2; + left: $size; + @include square($size + $delta); + background-color: map_get($vars, $theme-switcher-color); + + animation: shine 3s linear alternate infinite; + + @keyframes shine { + from { + filter: brightness(1.01); + box-shadow: 0 0 4px 2px map_get($vars, $theme-switcher-color); + } + + to { + filter: brightness(1.1); + box-shadow: 0 0 15px 2px map_get($vars, $theme-switcher-color); + } + } + } + + &:checked { + &:before { + background-color: map_get($vars, $normal-text-color); + } + &:after { + @include square($small-size); + $offset: $small-size / 5.5; + left: $size - $small-size + $offset; + top: ($size - $small-size) / 2; + background-color: map_get($vars, $background); + } + } + + &:focus { + outline: 0; + } + } +} diff --git a/src/page/theme-switcher/theme-switcher.ts b/src/page/theme-switcher/theme-switcher.ts new file mode 100644 index 0000000..08d43fe --- /dev/null +++ b/src/page/theme-switcher/theme-switcher.ts @@ -0,0 +1,40 @@ +import { PageElement } from '../../framework/page-element'; +import { createElement } from '../../framework/helper/create-element'; +import { generate } from './theme-switcher.html'; +import { + isSystemLevelDarkModeEnabled, + turnOnDarkMode, + turnOnLightMode, +} from '../../framework/helper/dark-mode'; +import { PageEvent, PageEventType } from '../../framework/page-event'; +import { EventBroadcaster } from '../../framework/event-broadcaster'; + +export class PageThemeSwitcher extends PageElement { + public constructor() { + super(createElement(generate())); + if (isSystemLevelDarkModeEnabled()) { + (this.element as HTMLInputElement).checked = true; + } + this.element.onchange = this.handleThemeChange.bind(this); + } + + protected handleEvent(event: PageEvent, parent: EventBroadcaster) { + if (event.type === PageEventType.onLoad) { + console.log('a'); + this.handleThemeChange(); + } + } + + private handleThemeChange() { + const isDark = (this.element as HTMLInputElement).checked; + if (isDark) { + turnOnDarkMode(); + } else { + turnOnLightMode(); + } + this.eventBroadcaster.broadcastEvent({ + type: PageEventType.pageThemeChanged, + data: isDark, + }); + } +} diff --git a/src/page/timeline/timeline-element/timeline-element.scss b/src/page/timeline/timeline-element/timeline-element.scss index 1350f30..6b78212 100644 --- a/src/page/timeline/timeline-element/timeline-element.scss +++ b/src/page/timeline/timeline-element/timeline-element.scss @@ -1,112 +1,126 @@ @import '../../../style/mixins'; @import '../../../style/vars'; -.timeline-element { - display: flex; - - .line { - position: relative; - border-left: $line-width solid $accent-color; - - &:before { - content: ''; - @include square($icon-size); - position: absolute; - left: calc(-0.5 * #{$icon-size} - (1.5 * #{$line-width})); - border: $line-width solid $accent-color; - border-radius: 100%; - background: $background; - } - - .date { - @include insignificant-font(); - } - } - - @media (min-width: $breakpoint-width) { - &:not(:first-of-type) .card { - margin-top: $large-margin; - } +@include responsive() using ($vars) { + .timeline-element { + display: flex; + width: map_get($vars, $body-width); + margin: auto; .line { - &:before { - top: calc(33% - #{$icon-size} / 2); - } - - .date { - position: relative; - top: calc(33% + #{$icon-size} / 2 + 1ch); - transform: rotate(30deg); - margin: 0 $normal-margin 0 calc(#{$line-width} + 1ex); - width: 100px; - } - } - } - - @media (max-width: $breakpoint-width) { - flex-direction: column; - align-items: center; - - &:before { - top: calc(50% - #{$icon-size} / 2); - } - - .line { - @include center-children(); - justify-content: flex-start; - height: 150px; - width: 50%; - - .date { - margin-left: calc(#{$icon-size} / 2 + #{$small-margin}); - width: 200px; - } - } - } - - .card { - @include card(); - overflow: hidden; - - & > *:not(:first-child) { - margin-top: $line-height; - } - - h2 { - @include sub-title-font(); - } - - & > p { - font-style: italic; - text-align: center; - } - - .more { - overflow: hidden; - height: 0; - margin-top: 0; - transition: height $long-transition-time; - } - - .buttons { position: relative; - margin-top: $small-margin; + border-left: map_get($vars, $line-width) solid + map_get($vars, $accent-color); - * { - transition: opacity $long-transition-time; - } - - .show-more { - opacity: 1; - } - - .show-less { - opacity: 0; - visibility: hidden; + &:before { + content: ''; + @include square(map_get($vars, $icon-size)); position: absolute; - left: 50%; - top: 50%; - transform: translateX(-50%) translateY(-50%); + left: calc( + -0.5 * #{map_get($vars, $icon-size)} - + (1.5 * #{map_get($vars, $line-width)}) + ); + border: map_get($vars, $line-width) solid map_get($vars, $accent-color); + border-radius: 100%; + background: map_get($vars, $background); + } + + .date { + @include insignificant-font(); + color: map_get($vars, $accent-color); + } + } + + @include on-large-screen { + &:not(:first-of-type) .card { + margin-top: map_get($vars, $large-margin); + } + + .line { + &:before { + top: calc(33% - #{map_get($vars, $icon-size)} / 2); + } + + .date { + position: relative; + top: calc(33% + #{map_get($vars, $icon-size)} / 2 + 1ch); + transform: rotate(30deg); + margin: 0 map_get($vars, $normal-margin) 0 + calc(#{map_get($vars, $line-width)} + 1ex); + width: 100px; + } + } + } + + @include on-small-screen { + flex-direction: column; + align-items: center; + + &:before { + top: calc(50% - #{map_get($vars, $icon-size)} / 2); + } + + .line { + @include center-children(); + justify-content: flex-start; + height: 150px; + width: 50%; + + .date { + margin-left: calc( + #{map_get($vars, $icon-size)} / 2 + #{map_get($vars, $small-margin)} + ); + width: 200px; + } + } + } + + .card { + @include card-base($vars); + border-radius: map_get($vars, $border-radius); + background-color: map_get($vars, $card-color); + overflow: hidden; + + & > *:not(:first-child) { + margin-top: map_get($vars, $line-height); + } + + h2 { + @include sub-title-font(); + } + + & > p { + font-style: italic; + text-align: center; + } + + .more { + overflow: hidden; + height: 0; + margin-top: 0; + transition: height map_get($vars, $long-transition-time); + } + + .buttons { + position: relative; + margin-top: map_get($vars, $small-margin); + + * { + transition: opacity map_get($vars, $long-transition-time); + } + + .show-more { + opacity: 1; + } + + .show-less { + opacity: 0; + visibility: hidden; + position: absolute; + left: 50%; + top: 50%; + transform: translateX(-50%) translateY(-50%); + } } } } diff --git a/src/page/timeline/timeline.scss b/src/page/timeline/timeline.scss index 9fc157e..0784420 100644 --- a/src/page/timeline/timeline.scss +++ b/src/page/timeline/timeline.scss @@ -1,10 +1,13 @@ @import '../../style/vars'; +@import '../../style/mixins'; -#timeline { - width: $body-width; - margin: $large-margin auto 0 auto; - - @media (max-width: $breakpoint-width) { - margin: auto; +@include responsive() using ($vars) { + #timeline { + @include on-large-screen { + // workaround for IE + & > :first-child { + margin-top: map_get($vars, $large-margin); + } + } } } diff --git a/src/static/icons/at.svg b/src/static/icons/at.svg deleted file mode 100644 index f4dd5d6..0000000 --- a/src/static/icons/at.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/static/icons/cv.svg b/src/static/icons/cv.svg deleted file mode 100644 index 0589ead..0000000 --- a/src/static/icons/cv.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/style/a.scss b/src/style/a.scss index 65da6ed..8b635f8 100644 --- a/src/style/a.scss +++ b/src/style/a.scss @@ -1,48 +1,51 @@ @import 'vars'; @import 'mixins'; -a { - @include insignificant-font(); - text-decoration: none; - position: relative; - cursor: pointer; - color: $accent-color; - display: inline-block; - overflow: hidden; +@include responsive() using ($vars) { + a { + @include insignificant-font(); + text-decoration: none; + position: relative; + cursor: pointer; + color: map_get($vars, $accent-color); + display: inline-block; + overflow: hidden; - $border-shift: 10px; + $border-shift: 10px; - transition: transform $long-transition-time; + transition: transform map_get($vars, $long-transition-time); - &:before { - content: ''; - display: block; - position: absolute; - width: 100%; - height: $line-width; - bottom: 0; - z-index: 1; - background: linear-gradient( - 90deg, - $card-color 0, - transparentize($card-color, 1) 4px, - transparentize($card-color, 1) calc(100% - 4px), - $card-color 100% - ); - } + &:before { + content: ''; + display: block; + position: absolute; + width: 100%; + height: map_get($vars, $line-width); + bottom: 0; + z-index: 1; + background: linear-gradient( + 90deg, + map_get($vars, $card-color) 0, + transparentize(map_get($vars, $card-color), 1) 4px, + transparentize(map_get($vars, $card-color), 1) calc(100% - 4px), + map_get($vars, $card-color) 100% + ); + } - &:after { - content: ''; - display: block; - width: calc(100% + #{$border-shift}); - z-index: 0; - border-bottom: $line-width dashed $accent-color; - transition: transform $long-transition-time; - } - - &:hover { &:after { - transform: translateX(-$border-shift); + content: ''; + display: block; + width: calc(100% + #{$border-shift}); + z-index: 0; + border-bottom: map_get($vars, $line-width) dashed + map_get($vars, $accent-color); + transition: transform map_get($vars, $long-transition-time); + } + + &:hover { + &:after { + transform: translateX(-$border-shift); + } } } } diff --git a/src/style/mixins.scss b/src/style/mixins.scss index c4f4290..02c0857 100644 --- a/src/style/mixins.scss +++ b/src/style/mixins.scss @@ -1,41 +1,57 @@ @import 'vars'; +@mixin on-small-screen() { + @media (max-width: $breakpoint-width) { + @content; + } +} + +@mixin on-large-screen() { + @media (min-width: $breakpoint-width) { + @content; + } +} + +@mixin responsive() { + html { + @include on-small-screen { + @content (map_merge($small-screen-variables, $light-theme-variables)); + &[theme='dark'] { + @content (map_merge($small-screen-variables, $dark-theme-variables)); + } + } + } + @include on-large-screen { + html { + @content (map_merge($large-screen-variables, $light-theme-variables)); + &[theme='dark'] { + @content (map_merge($large-screen-variables, $dark-theme-variables)); + } + } + } +} + @mixin center-children() { display: flex; align-items: center; justify-content: center; } -%card { +@mixin card-base($vars) { text-align: center; - padding: $normal-margin; - box-shadow: $shadow1, $shadow2; + padding: map_get($vars, $normal-margin); + box-shadow: map_get($vars, $shadow1), map_get($vars, $shadow2); z-index: 1; - @media (min-width: $breakpoint-width) { - transition: box-shadow $long-transition-time; + @include on-large-screen { + transition: box-shadow map_get($vars, $long-transition-time); &:hover { - box-shadow: $shadow3, $shadow2; + box-shadow: map_get($vars, $shadow3), map_get($vars, $shadow2); } } } -@mixin card() { - @extend %card; - border-radius: $border-radius; - background-color: $card-color; -} - -@mixin important-card() { - @extend %card; - background-color: $accent-color; - - * { - color: white; - } -} - @mixin square($size) { width: $size; height: $size; @@ -51,7 +67,7 @@ font-style: normal; line-height: 1; - @media (max-width: $breakpoint-width) { + @include on-small-screen { font-size: 3rem; line-height: 1.1; } diff --git a/src/style/vars.scss b/src/style/vars.scss index 4f6d69f..ff0ff2c 100644 --- a/src/style/vars.scss +++ b/src/style/vars.scss @@ -1,51 +1,114 @@ @import 'fonts'; -$background: #ffffff; +$_id_value: 0; +@function id() { + $_id_value: $_id_value + 1 !global; + @return $_id_value; +} -$normal-text-color: #31343f; -$light-text-color: #7a7d8e; -$inverse-text-color: #ffffff; +/* NAMES */ +$background: id(); -$card-color: #ffffff; -$accent-color: #aa4465; -$scrollbar-color: #ffd6d6; +$normal-text-color: id(); +$light-text-color: id(); +$important-card-text-color: id(); + +$inverse-text-color: id(); +$card-color: id(); +$important-card-color: id(); +$theme-switcher-color: id(); + +$accent-color: id(); +$scrollbar-color: id(); + +$short-transition-time: id(); +$long-transition-time: id(); +$line-width: id(); +$border-radius: id(); +$breakpoint-width: id(); + +$large-margin: id(); +$normal-margin: id(); +$small-margin: id(); +$line-height: id(); + +$shadow1: id(); +$shadow2: id(); +$shadow3: id(); + +$body-width: id(); + +$icon-size: id(); +/**/ -$short-transition-time: 220ms; -$long-transition-time: 350ms; -$line-width: 3px; -$border-radius: var(--border-radius); $breakpoint-width: 925px; -$large-margin: var(--large-margin); -$normal-margin: var(--normal-margin); -$small-margin: var(--small-margin); -$line-height: 18px; +$universal-variables: ( + $short-transition-time: 220ms, + $long-transition-time: 350ms, + $line-width: 3px, + $line-height: 18px, + $shadow3: 0 0 15px 4px rgba(0, 0, 0, 0.1), +); -$shadow1: var(--shadow1); -$shadow2: var(--shadow2); -$shadow3: 0 0 15px 4px rgba(0, 0, 0, 0.1); +$light-theme-variables: map_merge( + ( + $background: #ffffff, + $normal-text-color: #31343f, + $light-text-color: #7a7d8e, + $inverse-text-color: #ffffff, + $theme-switcher-color: #f7f78c, + $card-color: #ffffff, + $important-card-color: #aa4465, + $important-card-text-color: #ffffff, + $accent-color: #aa4465, + $scrollbar-color: #ffd6d6, + ), + $universal-variables +); -$icon-size: var(--icon-size); -$body-width: var(--body-width); +$dark-theme-variables: map_merge( + ( + $background: #242638, + $normal-text-color: #ffffff, + $light-text-color: #fff9e0, + $inverse-text-color: #242638, + $theme-switcher-color: #ffff92, + $card-color: #263551, + $important-card-text-color: #fff9e0, + $accent-color: #f7f78c, + $important-card-color: #263551, + $scrollbar-color: #ffd6d6, + $shadow1: 0 0 10px 2px rgba(0, 0, 0, 0.175), + $shadow2: 0 0 1px rgba(0, 0, 0, 0.4), + ), + $universal-variables +); -:root { - --large-margin: 70px; - --normal-margin: 45px; - --small-margin: 25px; - --icon-size: 35px; - --body-width: 765px; - --shadow1: 0 0 10px 2px rgba(0, 0, 0, 0.075); - --shadow2: 0 0 1px rgba(0, 0, 0, 0.2); - --border-radius: 15px; +$large-screen-variables: map_merge( + ( + $border-radius: 15px, + $large-margin: 70px, + $normal-margin: 45px, + $small-margin: 25px, + $shadow1: 0 0 10px 2px rgba(0, 0, 0, 0.075), + $shadow2: 0 0 1px rgba(0, 0, 0, 0.2), + $icon-size: 35px, + $body-width: 765px, + ), + $universal-variables +); - @media (max-width: $breakpoint-width) { - --large-margin: 60px; - --normal-margin: 30px; - --small-margin: 15px; - --icon-size: 25px; - --body-width: 90%; - --shadow1: 0 0 10px 2px rgba(0, 0, 0, 0.05); - --shadow2: 0 0 1px rgba(0, 0, 0, 0.125); - --border-radius: 10px; - } -} +$small-screen-variables: map_merge( + ( + $border-radius: 10px, + $large-margin: 60px, + $normal-margin: 30px, + $small-margin: 15px, + $shadow1: 0 0 10px 2px rgba(0, 0, 0, 0.05), + $shadow2: 0 0 1px rgba(0, 0, 0, 0.125), + $icon-size: 25px, + $body-width: 90%, + ), + $universal-variables +); diff --git a/src/styles.scss b/src/styles.scss index 5cf2b4f..b8450c8 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -3,68 +3,87 @@ @import 'style/a'; @import 'framework/primitives/primitives'; -* { - margin: 0; - padding: 0; - box-sizing: border-box; - color: $normal-text-color; - hyphens: auto; -} - -::-moz-selection { - background: $accent-color; - color: $inverse-text-color; -} -::selection { - background: $accent-color; - color: $inverse-text-color; -} - -html { - background-color: $background; - height: 100%; -} - -body { - @include main-font(); - height: 100%; - //noinspection CssInvalidFunction - padding: env(safe-area-inset-top, 20px) env(safe-area-inset-right, 20px) - env(safe-area-inset-bottom, 20px) env(safe-area-inset-left, 20px); - - & > main { +@include responsive() using ($vars) { + & { + background-color: map_get($vars, $background); height: 100%; + + @include on-small-screen { + font-size: 0.8em; + } + } + + svg { + fill: hotpink; + } + + body { + @include main-font(); + + //noinspection CssInvalidFunction + padding: env(safe-area-inset-top, 20px) env(safe-area-inset-right, 20px) + env(safe-area-inset-bottom, 20px) env(safe-area-inset-left, 20px); + + height: 100%; + overflow: hidden; + } + + * { + margin: 0; + padding: 0; + box-sizing: border-box; + color: map_get($vars, $normal-text-color); + hyphens: auto; + + :focus { + /* border: 2px solid map_get($vars, $accent-color); + outline: 0; + border-radius: map_get($vars, $border-radius);*/ + } + } + + img, + video { + width: 100%; + height: auto; + object-fit: contain; + } + + ::-moz-selection { + background: map_get($vars, $accent-color); + color: map_get($vars, $inverse-text-color); + } + ::selection { + background: map_get($vars, $accent-color); + color: map_get($vars, $inverse-text-color); + } + + .main { + height: 100vh; overflow-y: scroll; overflow-x: hidden; + will-change: transform; + + -webkit-overflow-scrolling: touch; + perspective: 5px; + perspective-origin: center center; + noscript { @include square(100%); @include center-children(); } - @media (min-width: $breakpoint-width) { + @include on-large-screen { &::-webkit-scrollbar-track, &::-webkit-scrollbar { background-color: transparent; width: 12px; } &::-webkit-scrollbar-thumb { - background-color: $accent-color; - border-radius: $border-radius; + background-color: map_get($vars, $accent-color); + border-radius: map_get($vars, $border-radius); } } } } - -img, -video { - width: 100%; - height: auto; - object-fit: contain; -} - -@media (max-width: $breakpoint-width) { - html { - font-size: 0.8em; - } -} diff --git a/webpack.config.js b/webpack.config.js index ef757ef..6e740e5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,6 +6,7 @@ const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); const Sharp = require('responsive-loader/sharp'); +const Sass = require('sass'); const isProduction = process.env.NODE_ENV === 'production'; @@ -15,7 +16,7 @@ module.exports = { }, devServer: { host: '0.0.0.0', - // disableHostCheck: true, + //disableHostCheck: true, }, optimization: { minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], @@ -134,6 +135,7 @@ module.exports = { loader: 'sass-loader', options: { sourceMap: true, + implementation: Sass, }, }, ],