diff --git a/src/data/portfolio.ts b/src/data/portfolio.ts index 1df6a35..dac7a53 100644 --- a/src/data/portfolio.ts +++ b/src/data/portfolio.ts @@ -24,11 +24,13 @@ import { sdf2d } from './projects/sdf2d'; import { towers } from './projects/towers'; import { CV, Email, GitHubLink, LinkedIn } from './shared'; +const imageViewer = new ImageViewer(); const main = new Main( new Header({ name: 'AndrĂ¡s Schmelczer', image: me, imageAltText: 'a picture of me', + imageViewer, about: [ 'With more than six years of professional software engineering experience and a degree in Computer Science, I can confidently undertake any challenge. My interests span diverse areas, allowing me to design complex — even multidisciplinary — systems with a clear understanding.', @@ -56,7 +58,7 @@ const main = new Main( platformGame, photos, leds, - ].map((p) => new TimelineElement(p, 'Show details', 'Show less')), + ].map((p) => new TimelineElement(p, 'Show details', 'Show less', imageViewer)), Contact({ title: 'Get in touch', @@ -72,6 +74,6 @@ const main = new Main( export const portfolio: Array = [ main, - new ImageViewer(), new UpArrowButton(main, 'go up'), + imageViewer, ]; diff --git a/src/data/projects/ad-astra.ts b/src/data/projects/ad-astra.ts index 6ccdf76..73a5b32 100644 --- a/src/data/projects/ad-astra.ts +++ b/src/data/projects/ad-astra.ts @@ -1,5 +1,5 @@ +import { Video } from '../../page/figure/video/video'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; -import { Video } from '../../page/video/video'; import adAstraPoster from '../media/ad_astra.jpg'; import adAstraMp4 from '../media/mp4/ad_astra.mp4'; import adAstraWebM from '../media/webm/ad_astra.webm'; diff --git a/src/data/projects/avoid.ts b/src/data/projects/avoid.ts index f2b8500..1c5fc03 100644 --- a/src/data/projects/avoid.ts +++ b/src/data/projects/avoid.ts @@ -1,4 +1,4 @@ -import { Preview } from '../../page/preview/preview'; +import { Preview } from '../../page/figure/preview/preview'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import avoidPoster from '../media/avoid.png'; import { Open } from '../shared'; diff --git a/src/data/projects/city-simulation.ts b/src/data/projects/city-simulation.ts index bc7b7dd..7b8739b 100644 --- a/src/data/projects/city-simulation.ts +++ b/src/data/projects/city-simulation.ts @@ -1,5 +1,5 @@ +import { Video } from '../../page/figure/video/video'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; -import { Video } from '../../page/video/video'; import citySimulationMp4 from '../media/mp4/simulation.mp4'; import citySimulationPoster from '../media/simulation.jpg'; import citySimulationWebM from '../media/webm/simulation.webm'; @@ -16,7 +16,7 @@ export const citySimulation: TimelineElementParameters = { }), description: 'I simulated a city where car crashes are more frequent than usual.', more: [ - 'The state of the traffic lights can be changed through a REST API. Drivers follow the instructions of the traffic lights, so if a mistake is made, there will be collisions. There is also support for displaying tweets on a HUD. This was created as the context for a cybersecurity challenge on PLCs. With the help of this program, the contestants could instantly see the effect of their work.', + 'The state of the traffic lights can be changed through a REST API. Drivers follow the instructions of the traffic lights, so if a mistake is made, there will be collisions. There is also support for displaying tweets on a HUD. This was created as the context for a cybersecurity challenge about PLCs. With the help of this program, the contestants could instantly see the effect of their work.', 'An exciting aspect of the project was building it in a server-client architecture. Every decision of the agents is calculated server-side. The real challenge was broadcasting these decisions in a fault-tolerant way using minimal bandwidth.', diff --git a/src/data/projects/colors.ts b/src/data/projects/colors.ts index c529f37..3fb9f5a 100644 --- a/src/data/projects/colors.ts +++ b/src/data/projects/colors.ts @@ -1,14 +1,13 @@ -import { Image } from '../../page/image-viewer/image/image.html'; +import { BorderedImage } from '../../page/figure/bordered-image/bordered-image'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import colorsPoster from '../media/color.jpg'; export const colors: TimelineElementParameters = { title: 'Photo colour grader', date: '2018 June', - figure: Image({ + figure: new BorderedImage({ image: colorsPoster, alt: 'a picture of the app', - container: true, }), description: 'An innovative (at least I thought so) colour grader web application.', more: [ diff --git a/src/data/projects/declared.ts b/src/data/projects/declared.ts index 28927ab..d839ded 100644 --- a/src/data/projects/declared.ts +++ b/src/data/projects/declared.ts @@ -1,4 +1,4 @@ -import { Preview } from '../../page/preview/preview'; +import { Preview } from '../../page/figure/preview/preview'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import declaredPoster from '../media/decla-red.png'; import bscThesis from '../media/sdf2d-andras-schmelczer.pdf'; diff --git a/src/data/projects/forex.ts b/src/data/projects/forex.ts index 85e5bea..50df8fb 100644 --- a/src/data/projects/forex.ts +++ b/src/data/projects/forex.ts @@ -1,5 +1,5 @@ +import { Video } from '../../page/figure/video/video'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; -import { Video } from '../../page/video/video'; import forexPoster from '../media/forex.jpg'; import forexMp4 from '../media/mp4/forex.mp4'; import forexWebM from '../media/webm/forex.webm'; diff --git a/src/data/projects/great-ai.ts b/src/data/projects/great-ai.ts index 62eba4d..c6ce5ea 100644 --- a/src/data/projects/great-ai.ts +++ b/src/data/projects/great-ai.ts @@ -1,4 +1,4 @@ -import { Image } from '../../page/image-viewer/image/image.html'; +import { BorderedImage } from '../../page/figure/bordered-image/bordered-image'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import mscThesis from '../media/great-ai-andras-schmelczer.pdf'; import greatAiPoster from '../media/great-ai.png'; @@ -7,10 +7,9 @@ import { Open, PyPi, Thesis } from '../shared'; export const greatAi: TimelineElementParameters = { title: 'GreatAI — AI deployment framework', date: '2022', - figure: Image({ + figure: new BorderedImage({ image: greatAiPoster, alt: 'some example code using GreatAI', - container: true, isEagerLoaded: true, }), description: diff --git a/src/data/projects/leds.ts b/src/data/projects/leds.ts index 7ee6cea..8e33465 100644 --- a/src/data/projects/leds.ts +++ b/src/data/projects/leds.ts @@ -1,5 +1,5 @@ +import { Video } from '../../page/figure/video/video'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; -import { Video } from '../../page/video/video'; import ledPoster from '../media/led.jpg'; import ledMp4 from '../media/mp4/led.mp4'; import ledWebM from '../media/webm/led.webm'; diff --git a/src/data/projects/my-notes.ts b/src/data/projects/my-notes.ts index de885d9..dc3cbd9 100644 --- a/src/data/projects/my-notes.ts +++ b/src/data/projects/my-notes.ts @@ -1,4 +1,4 @@ -import { Image } from '../../page/image-viewer/image/image.html'; +import { BorderedImage } from '../../page/figure/bordered-image/bordered-image'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import myNotesPoster from '../media/my-notes.png'; import { GitHub } from '../shared'; @@ -6,10 +6,9 @@ import { GitHub } from '../shared'; export const myNotes: TimelineElementParameters = { title: 'My Notes — Android app', date: '2019 November', - figure: Image({ + figure: new BorderedImage({ image: myNotesPoster, alt: 'two screenshots of the application', - container: true, }), description: 'A minimalist Android note organiser and editor powered by Markwon.', more: [ diff --git a/src/data/projects/nuclear-editor.ts b/src/data/projects/nuclear-editor.ts index 97daa7c..0489b42 100644 --- a/src/data/projects/nuclear-editor.ts +++ b/src/data/projects/nuclear-editor.ts @@ -1,14 +1,13 @@ -import { Image } from '../../page/image-viewer/image/image.html'; +import { BorderedImage } from '../../page/figure/bordered-image/bordered-image'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import nuclearEditorPoster from '../media/process-simulator-input.jpg'; export const nuclearEditor: TimelineElementParameters = { title: 'Graph editor — JavaFX', date: '2018 October - November', - figure: Image({ + figure: new BorderedImage({ image: nuclearEditorPoster, alt: "a picture of the simulator's UI", - container: true, }), description: 'An intuitive editor to create and edit input for the nuclear facility simulator (see above).', diff --git a/src/data/projects/nuclear.ts b/src/data/projects/nuclear.ts index 5fa28ed..50cd337 100644 --- a/src/data/projects/nuclear.ts +++ b/src/data/projects/nuclear.ts @@ -1,14 +1,13 @@ -import { Image } from '../../page/image-viewer/image/image.html'; +import { BorderedImage } from '../../page/figure/bordered-image/bordered-image'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import processSimulatorPoster from '../media/process-simulator.jpg'; export const nuclear: TimelineElementParameters = { title: 'Simulating the cooling system of a nuclear facility', date: '2018 October - November', - figure: Image({ + figure: new BorderedImage({ image: processSimulatorPoster, alt: 'a screenshot of the simulator', - container: true, }), description: 'The temperatures and flow volumes are dynamically calculated by two graph models on a remote server while multiple "monitoring" clients update in real-time.', diff --git a/src/data/projects/photos.ts b/src/data/projects/photos.ts index 25d19eb..fa0d8fd 100644 --- a/src/data/projects/photos.ts +++ b/src/data/projects/photos.ts @@ -1,4 +1,4 @@ -import { Image } from '../../page/image-viewer/image/image.html'; +import { BorderedImage } from '../../page/figure/bordered-image/bordered-image'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import photosPoster from '../media/photos.jpg'; import { Open } from '../shared'; @@ -6,10 +6,9 @@ import { Open } from '../shared'; export const photos: TimelineElementParameters = { title: 'Photos', date: '2016 summer', - figure: Image({ + figure: new BorderedImage({ image: photosPoster, alt: 'a picture of the website', - container: true, }), description: 'A simple webpage where you can view my photos.', more: [ diff --git a/src/data/projects/platform-game.ts b/src/data/projects/platform-game.ts index 1098f83..54d2879 100644 --- a/src/data/projects/platform-game.ts +++ b/src/data/projects/platform-game.ts @@ -1,5 +1,5 @@ +import { Video } from '../../page/figure/video/video'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; -import { Video } from '../../page/video/video'; import platformMp4 from '../media/mp4/platform.mp4'; import platformPoster from '../media/platform.png'; import platformWebM from '../media/webm/platform.webm'; diff --git a/src/data/projects/sdf2d.ts b/src/data/projects/sdf2d.ts index 2087c80..7083a02 100644 --- a/src/data/projects/sdf2d.ts +++ b/src/data/projects/sdf2d.ts @@ -1,4 +1,4 @@ -import { Preview } from '../../page/preview/preview'; +import { Preview } from '../../page/figure/preview/preview'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import sdf2dPoster from '../media/sdf2d.png'; import { NPM, Open, Youtube } from '../shared'; diff --git a/src/data/projects/towers.ts b/src/data/projects/towers.ts index 2eab88c..8420dbd 100644 --- a/src/data/projects/towers.ts +++ b/src/data/projects/towers.ts @@ -1,4 +1,4 @@ -import { Image } from '../../page/image-viewer/image/image.html'; +import { BorderedImage } from '../../page/figure/bordered-image/bordered-image'; import { TimelineElementParameters } from '../../page/timeline-element/timeline-element-parameters'; import towersPoster from '../media/towers.png'; import { GitHub, Open } from '../shared'; @@ -6,10 +6,9 @@ import { GitHub, Open } from '../shared'; export const towers: TimelineElementParameters = { title: 'Multi-device life tracking', date: '2019 August - September', - figure: Image({ + figure: new BorderedImage({ image: towersPoster, alt: 'a picture of the website', - container: true, }), description: 'An aesthetic representation of your previous and current goals/tasks.', more: [ diff --git a/src/index.scss b/src/index.scss index 5ba64db..6298818 100644 --- a/src/index.scss +++ b/src/index.scss @@ -25,15 +25,6 @@ html[animations='off'] { } } -button { - border: none; - background: none; -} - -a { - text-decoration: none; -} - html { height: 100%; @@ -74,6 +65,23 @@ body { } } +noscript { + @include square(100%); + @include center-children(); + @include sub-title-font(); +} + +.image, +video, +iframe { + user-select: none; +} + +button { + border: none; + background: none; +} + svg { stroke: var(--normal-text-color); } @@ -82,68 +90,8 @@ p { @include main-font(); } -noscript { - @include square(100%); - @include center-children(); - @include sub-title-font(); -} - -.start-button { - @include image-button(var(--large-icon-size)); - @include absolute-center; - @include square(calc(var(--large-icon-size) + var(--normal-margin) * 2)); - - // as a result of the firefox fix, it is required for iOS devices - transform: translate3d(-50%, -50%, 0.00001px); - - &:hover svg { - box-shadow: var(--shadow); - } - - svg { - border-radius: 10000px; - @include blurred-background; - transition: transform var(--transition-time), box-shadow var(--transition-time); - } - - &.inverted svg { - fill: var(--accent-color); - } -} - -.figure-container { - font-size: 0; - box-shadow: var(--inset-shadow); - border-radius: var(--border-radius) var(--border-radius) 0 0; - pointer-events: none; - position: relative; - - * { - pointer-events: auto; - } - - .image, - video, - iframe { - z-index: -1; - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - } -} - -video, -iframe { - // the picture of videos is not always visible on firefox mobile without this - transform: translate3d(0, 0, 0.00001px); -} - -.image, -video, -iframe { - user-select: none; +a { + text-decoration: none; } :focus { diff --git a/src/page/figure/bordered-image/bordered-image.scss b/src/page/figure/bordered-image/bordered-image.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/page/figure/bordered-image/bordered-image.ts b/src/page/figure/bordered-image/bordered-image.ts new file mode 100644 index 0000000..31bce5c --- /dev/null +++ b/src/page/figure/bordered-image/bordered-image.ts @@ -0,0 +1,23 @@ +import { ResponsiveImage } from '../../../types/responsive-image'; +import { ImageViewer } from '../../image-viewer/image-viewer'; +import { Image } from '../../image/image.html'; +import { Figure } from '../figure'; +import './bordered-image.scss'; + +export class BorderedImage extends Figure { + public constructor( + options: { + image: ResponsiveImage; + alt: string; + sizes?: string | null; + isEagerLoaded?: boolean; + }, + public imageViewer?: ImageViewer + ) { + super(Image(options)); + } + + protected async onClick() { + this.imageViewer?.showImage(this.query('img') as HTMLImageElement); + } +} diff --git a/src/page/figure/figure.html.ts b/src/page/figure/figure.html.ts new file mode 100644 index 0000000..8b32114 --- /dev/null +++ b/src/page/figure/figure.html.ts @@ -0,0 +1,21 @@ +import play from '../../../static/icons/play-button.svg'; +import { html } from '../../types/html'; +import './figure.scss'; + +export const generate = ({ + children, + hasButton, + invertButton, +}: { + children: html; + hasButton: boolean; + invertButton: boolean; +}): html => ` +
+ ${children} + ${ + hasButton + ? `
${play}
` + : '' + } +
`; diff --git a/src/page/figure/figure.scss b/src/page/figure/figure.scss new file mode 100644 index 0000000..33b4c5f --- /dev/null +++ b/src/page/figure/figure.scss @@ -0,0 +1,29 @@ +@use '../../style/mixins' as *; + +.figure-container { + box-shadow: var(--inset-shadow); + transition: box-shadow var(--transition-time); + + position: relative; + cursor: pointer; + + > .start-button { + @include image-button(var(--large-icon-size)); + @include absolute-center; + @include square(calc(var(--large-icon-size) + var(--normal-margin) * 2)); + + &:hover > svg { + box-shadow: var(--shadow); + } + + > svg { + border-radius: 1000px; + @include blurred-background; + transition: transform var(--transition-time), box-shadow var(--transition-time); + } + + &.inverted > svg { + fill: var(--accent-color); + } + } +} diff --git a/src/page/figure/figure.ts b/src/page/figure/figure.ts new file mode 100644 index 0000000..52cef4d --- /dev/null +++ b/src/page/figure/figure.ts @@ -0,0 +1,21 @@ +import { html } from '../../types/html'; +import { PageElement } from '../page-element'; +import { generate } from './figure.html'; + +export abstract class Figure extends PageElement { + public constructor( + children: html, + { + hasButton = false, + invertButton = false, + }: { + hasButton?: boolean; + invertButton?: boolean; + } = {} + ) { + super(generate({ children, hasButton, invertButton })); + this.htmlRoot.addEventListener('click', this.onClick.bind(this)); + } + + protected abstract onClick(): unknown; +} diff --git a/src/page/figure/preview/preview.html.ts b/src/page/figure/preview/preview.html.ts new file mode 100644 index 0000000..ab83425 --- /dev/null +++ b/src/page/figure/preview/preview.html.ts @@ -0,0 +1,21 @@ +import loading from '../../../../static/icons/loading.svg'; +import { html } from '../../../types/html'; +import { ResponsiveImage } from '../../../types/responsive-image'; +import { Image } from '../../image/image.html'; +import './preview.scss'; + +export const generate = ({ + alt, + poster, +}: { + alt: string; + poster: ResponsiveImage; +}): html => + `${Image({ + image: poster, + alt, + })} +
+
${loading}
+ +
`; diff --git a/src/page/preview/preview.scss b/src/page/figure/preview/preview.scss similarity index 58% rename from src/page/preview/preview.scss rename to src/page/figure/preview/preview.scss index a17c202..6b35e4a 100644 --- a/src/page/preview/preview.scss +++ b/src/page/figure/preview/preview.scss @@ -1,42 +1,38 @@ -@use '../../style/mixins' as *; +@use '../../../style/mixins' as *; -.preview { - position: relative; - - .overlay { +.figure-container { + > .overlay { @include square(100%); position: absolute; left: 0; top: 0; + pointer-events: none; - iframe { - position: absolute; - left: 0; - } - - .loading { + > .loading { @include square(var(--large-icon-size)); @include absolute-center; visibility: hidden; } - iframe { + > iframe { @include square(100%); border: none; - &:fullscreen { - border-radius: 0; - } + position: absolute; + left: 0; } } &.loaded { - .figure-container, - .start-button { + > .start-button { visibility: hidden; } - .loading { - visibility: visible; + > .overlay { + pointer-events: all; + + > .loading { + visibility: visible; + } } } } diff --git a/src/page/figure/preview/preview.ts b/src/page/figure/preview/preview.ts new file mode 100644 index 0000000..3d6f44c --- /dev/null +++ b/src/page/figure/preview/preview.ts @@ -0,0 +1,28 @@ +import { ResponsiveImage } from '../../../types/responsive-image'; +import { Figure } from '../figure'; +import { generate } from './preview.html'; + +export class Preview extends Figure { + public constructor(poster: ResponsiveImage, private readonly url: string, alt: string) { + super(generate({ poster, alt }), { + hasButton: true, + }); + this.url += '?portfolioView'; + } + + protected onClick() { + this.htmlRoot.classList.add('loaded'); + (this.query('iframe') as HTMLIFrameElement).src = this.url; + } + + protected initialize() { + new IntersectionObserver((e) => { + if (!e[0].isIntersecting) { + this.htmlRoot.classList.remove('loaded'); + (this.query('iframe') as HTMLIFrameElement).src = ''; + } + }).observe(this.htmlRoot.parentElement!); + + super.initialize(); + } +} diff --git a/src/page/video/video-parameters.ts b/src/page/figure/video/video-parameters.ts similarity index 55% rename from src/page/video/video-parameters.ts rename to src/page/figure/video/video-parameters.ts index a235dd1..81e4285 100644 --- a/src/page/video/video-parameters.ts +++ b/src/page/figure/video/video-parameters.ts @@ -1,5 +1,5 @@ -import { ResponsiveImage } from '../../types/responsive-image'; -import { url } from '../../types/url'; +import { ResponsiveImage } from '../../../types/responsive-image'; +import { url } from '../../../types/url'; export interface VideoParameters { mp4: url; diff --git a/src/page/figure/video/video.html.ts b/src/page/figure/video/video.html.ts new file mode 100644 index 0000000..cac5ff6 --- /dev/null +++ b/src/page/figure/video/video.html.ts @@ -0,0 +1,14 @@ +import { html } from '../../../types/html'; +import { Image } from '../../image/image.html'; +import { VideoParameters } from './video-parameters'; +import './video.scss'; + +export const generate = ({ webm, mp4, poster, altText }: VideoParameters): html => ` + ${Image({ + image: poster, + alt: altText, + })} + `; diff --git a/src/page/figure/video/video.scss b/src/page/figure/video/video.scss new file mode 100644 index 0000000..572cd04 --- /dev/null +++ b/src/page/figure/video/video.scss @@ -0,0 +1,16 @@ +@use '../../../style/mixins' as *; + +.figure-container { + &.loaded > .start-button, + &:not(.loaded) > video { + visibility: hidden; + } + + > video { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + } +} diff --git a/src/page/video/video.ts b/src/page/figure/video/video.ts similarity index 57% rename from src/page/video/video.ts rename to src/page/figure/video/video.ts index 6a45635..d0d900f 100644 --- a/src/page/video/video.ts +++ b/src/page/figure/video/video.ts @@ -1,15 +1,16 @@ -import { PageElement } from '../page-element'; +import { Figure } from '../figure'; import { VideoParameters } from './video-parameters'; import { generate } from './video.html'; -export class Video extends PageElement { +export class Video extends Figure { public constructor(options: VideoParameters) { - super(generate(options)); - - this.query('.start-button').addEventListener('click', this.startVideo.bind(this)); + super(generate(options), { + hasButton: true, + invertButton: options.invertButton, + }); } - private async startVideo() { + protected async onClick() { this.query('.start-button').style.visibility = 'hidden'; this.htmlRoot.classList.add('loaded'); diff --git a/src/page/header/header.html.ts b/src/page/header/header.html.ts index 00f9faf..0664f20 100644 --- a/src/page/header/header.html.ts +++ b/src/page/header/header.html.ts @@ -4,15 +4,13 @@ import './header.scss'; export const generate = ({ name, about, - photo, }: { name: string; about: Array; - photo: html; }): html => `
-
- ${photo} +
+
diff --git a/src/page/header/header.scss b/src/page/header/header.scss index 21ea54f..ea5df8d 100644 --- a/src/page/header/header.scss +++ b/src/page/header/header.scss @@ -12,8 +12,9 @@ } $img-size: 125px; - > .photo-container > .image { + > .profile-picture { @include square($img-size); + margin: auto; } > h1 { @@ -30,11 +31,10 @@ auto; border-radius: var(--border-radius); - > .photo-container { - position: relative; - - > .image { + > .profile-picture { + > .figure-container { @include square($img-size); + position: absolute; left: calc(#{math.div(-$img-size, 3)} - var(--normal-margin)); top: calc(#{math.div(-$img-size, 3)} - var(--normal-margin)); @@ -55,15 +55,19 @@ } > h1, - > .photo-container > .placeholder { + > .profile-picture > .placeholder { @include title-font(); } - > .photo-container { - > .image { - border-radius: 100%; - box-shadow: var(--shadow); - margin: auto; + > .profile-picture { + position: relative; + z-index: 1; + + .figure-container { + &, + > .image { + border-radius: 100%; + } } } diff --git a/src/page/header/header.ts b/src/page/header/header.ts index e127a6c..ae23f72 100644 --- a/src/page/header/header.ts +++ b/src/page/header/header.ts @@ -1,5 +1,6 @@ import { ResponsiveImage } from '../../types/responsive-image'; -import { Image } from '../image-viewer/image/image.html'; +import { BorderedImage } from '../figure/bordered-image/bordered-image'; +import { ImageViewer } from '../image-viewer/image-viewer'; import { PageElement } from '../page-element'; import { generate } from './header.html'; import { ThemeSwitcher } from './theme-switcher/theme-switcher'; @@ -10,22 +11,31 @@ export class Header extends PageElement { image, imageAltText, about, + imageViewer, }: { name: string; image: ResponsiveImage; imageAltText: string; about: Array; + imageViewer?: ImageViewer; }) { super( generate({ name, about, - photo: Image({ + }) + ); + + this.attachElementByReplacing( + 'img', + new BorderedImage( + { image, alt: imageAltText, sizes: '(max-width: 924px) 125px, 190px', - }), - }) + }, + imageViewer + ) ); this.attachElement(new ThemeSwitcher()); } diff --git a/src/page/image-viewer/image-viewer.html.ts b/src/page/image-viewer/image-viewer.html.ts index 280a688..1b724c3 100644 --- a/src/page/image-viewer/image-viewer.html.ts +++ b/src/page/image-viewer/image-viewer.html.ts @@ -4,7 +4,7 @@ import './image-viewer.scss'; export const generate = (): html => `
- +
`; diff --git a/src/page/image-viewer/image-viewer.scss b/src/page/image-viewer/image-viewer.scss index e7402c3..b6bae46 100644 --- a/src/page/image-viewer/image-viewer.scss +++ b/src/page/image-viewer/image-viewer.scss @@ -13,11 +13,9 @@ img { @include square(auto); - - @include on-large-screen { - max-width: 80%; - max-height: 80%; - } + box-shadow: var(--shadow); + max-width: 80%; + max-height: 80%; @include on-small-screen { max-width: 95%; diff --git a/src/page/image-viewer/image-viewer.ts b/src/page/image-viewer/image-viewer.ts index 63bfdb4..a5cdb5e 100644 --- a/src/page/image-viewer/image-viewer.ts +++ b/src/page/image-viewer/image-viewer.ts @@ -5,18 +5,6 @@ export class ImageViewer extends PageElement { public constructor() { super(generate()); - document.body.addEventListener('click', (event: MouseEvent) => { - const element = event.target as HTMLElement; - - if (element.classList?.contains('image')) { - this.showImage(element.querySelector('img')!); - } - - if (element instanceof HTMLImageElement) { - this.showImage(element); - } - }); - document.body.addEventListener('keydown', (event: KeyboardEvent) => { if (event.key === 'Escape') { this.hideImage(); @@ -26,11 +14,7 @@ export class ImageViewer extends PageElement { this.htmlRoot.addEventListener('click', this.hideImage.bind(this)); } - private showImage(source: HTMLImageElement) { - if (source.attributes['image-viewer-ignore'] as boolean | undefined) { - return; - } - + public showImage(source: HTMLImageElement) { const image = this.query('img') as HTMLImageElement; image.src = ''; image.src = source.src; diff --git a/src/page/image-viewer/image/image.html.ts b/src/page/image-viewer/image/image.html.ts deleted file mode 100644 index 01ca84e..0000000 --- a/src/page/image-viewer/image/image.html.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { html } from '../../../types/html'; -import { ResponsiveImage } from '../../../types/responsive-image'; -import './image.scss'; - -export const Image = ({ - image, - alt, - container = false, - isIgnoredByImageViewer = false, - sizes = null, - isEagerLoaded = false, -}: { - image: ResponsiveImage; - alt: string; - container?: boolean; - isIgnoredByImageViewer?: boolean; - sizes?: string | null; - isEagerLoaded?: boolean; -}): html => ` - ${ - container - ? `
` - : '' - } -
- ${alt} -
- ${container ? '
' : ''} -`; diff --git a/src/page/image/image.html.ts b/src/page/image/image.html.ts new file mode 100644 index 0000000..50228c7 --- /dev/null +++ b/src/page/image/image.html.ts @@ -0,0 +1,31 @@ +import { html } from '../../types/html'; +import { ResponsiveImage } from '../../types/responsive-image'; +import './image.scss'; + +export const Image = ({ + image, + alt, + sizes = null, + isEagerLoaded = false, +}: { + image: ResponsiveImage; + alt: string; + sizes?: string | null; + isEagerLoaded?: boolean; +}): html => ` +
+ ${alt} +
`; diff --git a/src/page/image-viewer/image/image.scss b/src/page/image/image.scss similarity index 57% rename from src/page/image-viewer/image/image.scss rename to src/page/image/image.scss index 8fcacc5..b84a9a1 100644 --- a/src/page/image-viewer/image/image.scss +++ b/src/page/image/image.scss @@ -1,12 +1,10 @@ .image { overflow: hidden; + position: relative; + z-index: -1; img { max-width: 100%; max-height: 100%; - - &:not([image-viewer-ignore]) { - cursor: pointer; - } } } diff --git a/src/page/preview/preview.html.ts b/src/page/preview/preview.html.ts deleted file mode 100644 index 89cbf26..0000000 --- a/src/page/preview/preview.html.ts +++ /dev/null @@ -1,28 +0,0 @@ -import loading from '../../../static/icons/loading.svg'; -import play from '../../../static/icons/play-button.svg'; -import { html } from '../../types/html'; -import { ResponsiveImage } from '../../types/responsive-image'; -import { Image } from '../image-viewer/image/image.html'; -import './preview.scss'; - -export const generate = ({ - alt, - poster, -}: { - alt: string; - poster: ResponsiveImage; -}): html => ` -
- ${Image({ - image: poster, - alt, - container: true, - isIgnoredByImageViewer: true, - })} -
-
${loading}
- -
${play}
-
-
-`; diff --git a/src/page/preview/preview.ts b/src/page/preview/preview.ts deleted file mode 100644 index 06ae013..0000000 --- a/src/page/preview/preview.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ResponsiveImage } from '../../types/responsive-image'; -import { PageElement } from '../page-element'; -import { generate } from './preview.html'; - -export class Preview extends PageElement { - public constructor(poster: ResponsiveImage, private readonly url: string, alt: string) { - super(generate({ poster, alt })); - this.url += '?portfolioView'; - this.query('.start-button').addEventListener('click', this.loadContent.bind(this)); - } - - protected initialize() { - new IntersectionObserver((e) => { - if (!e[0].isIntersecting) { - this.unloadContent(); - } - }).observe(this.htmlRoot.parentElement!); - - super.initialize(); - } - - public loadContent() { - this.htmlRoot.classList.add('loaded'); - (this.query('iframe') as HTMLIFrameElement).src = this.url; - } - - public unloadContent() { - this.htmlRoot.classList.remove('loaded'); - (this.query('iframe') as HTMLIFrameElement).src = ''; - } -} diff --git a/src/page/timeline-element/timeline-element-parameters.ts b/src/page/timeline-element/timeline-element-parameters.ts index cb87abf..fc07ce1 100644 --- a/src/page/timeline-element/timeline-element-parameters.ts +++ b/src/page/timeline-element/timeline-element-parameters.ts @@ -1,10 +1,9 @@ import { html } from '../../types/html'; -import { Preview } from '../preview/preview'; -import { Video } from '../video/video'; +import { Figure } from '../figure/figure'; export interface TimelineElementParameters { date: string; - figure: html | Video | Preview; + figure: Figure; title: string; description: string; more?: Array; diff --git a/src/page/timeline-element/timeline-element.scss b/src/page/timeline-element/timeline-element.scss index a4b658c..31fc034 100644 --- a/src/page/timeline-element/timeline-element.scss +++ b/src/page/timeline-element/timeline-element.scss @@ -105,6 +105,10 @@ background-color: var(--blurred-card-color); transition: background-color var(--transition-time); + > .figure-container { + border-radius: var(--border-radius) var(--border-radius) 0 0; + } + > .lower { > * { padding: 0 var(--normal-margin); diff --git a/src/page/timeline-element/timeline-element.ts b/src/page/timeline-element/timeline-element.ts index c0879d0..46bda47 100644 --- a/src/page/timeline-element/timeline-element.ts +++ b/src/page/timeline-element/timeline-element.ts @@ -1,4 +1,6 @@ import { titleToFragment } from '../../helper/title-to-fragment'; +import { BorderedImage } from '../figure/bordered-image/bordered-image'; +import { ImageViewer } from '../image-viewer/image-viewer'; import { PageElement } from '../page-element'; import { TimelineElementParameters } from './timeline-element-parameters'; import { generate } from './timeline-element.html'; @@ -10,7 +12,8 @@ export class TimelineElement extends PageElement { public constructor( private timelineElement: TimelineElementParameters, private readonly showMore: string, - private readonly showLess: string + private readonly showLess: string, + imageViewer?: ImageViewer ) { super(generate(timelineElement, showMore)); @@ -24,12 +27,11 @@ export class TimelineElement extends PageElement { ); } - this.attachElementByReplacing( - '.figure', - timelineElement.figure instanceof PageElement - ? timelineElement.figure - : new PageElement(timelineElement.figure) - ); + if (timelineElement.figure instanceof BorderedImage) { + timelineElement.figure.imageViewer = imageViewer; + } + + this.attachElementByReplacing('.figure', timelineElement.figure); } protected initialize(): void { diff --git a/src/page/video/video.html.ts b/src/page/video/video.html.ts deleted file mode 100644 index 388bd34..0000000 --- a/src/page/video/video.html.ts +++ /dev/null @@ -1,28 +0,0 @@ -import play from '../../../static/icons/play-button.svg'; -import { html } from '../../types/html'; -import { Image } from '../image-viewer/image/image.html'; -import { VideoParameters } from './video-parameters'; -import './video.scss'; - -export const generate = ({ - webm, - mp4, - poster, - invertButton, - altText, -}: VideoParameters): html => ` -
- ${Image({ - image: poster, - alt: altText, - isIgnoredByImageViewer: true, - })} - -
${play}
-
-`; diff --git a/src/page/video/video.scss b/src/page/video/video.scss deleted file mode 100644 index eff87af..0000000 --- a/src/page/video/video.scss +++ /dev/null @@ -1,8 +0,0 @@ -@use '../../style/mixins' as *; - -.video-container { - &.loaded > .start-button, - &:not(.loaded) > video { - visibility: hidden; - } -} diff --git a/src/style/vars.scss b/src/style/vars.scss index 8eb838b..2cc464b 100644 --- a/src/style/vars.scss +++ b/src/style/vars.scss @@ -14,6 +14,7 @@ --blurred-card-color: transparent; --blur-radius: 16px; --special-text-color: var(--accent-color); + --inset-shadow: inset 0 0 4px 1px rgba(0, 0, 0, 0.05), inset 0 0 5px rgba(0, 0, 0, 0.2); } @include on-large-screen { @@ -23,7 +24,6 @@ --normal-margin: 45px; --small-margin: 25px; --shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.2); - --inset-shadow: inset 0 -9px 7px -7px rgb(0, 0, 0, 0.15); --icon-size: 45px; --large-icon-size: 60px; --body-width: min(80%, 900px); @@ -37,7 +37,6 @@ --normal-margin: 30px; --small-margin: 15px; --shadow: 0 0 10px 2px rgba(0, 0, 0, 0.075), 0 0 1px rgba(0, 0, 0, 0.125); - --inset-shadow: inset 0 -9px 7px -7px rgb(0, 0, 0, 0.15); --icon-size: 35px; --large-icon-size: 55px; --body-width: 90%; @@ -51,5 +50,5 @@ --blurred-card-color: #212f4a77; --blur-radius: 30px; --special-text-color: #ffffff; - --inset-shadow: inset 0 0 10px 2px rgba(0, 0, 0, 0.25), inset 0 0 1px rgba(0, 0, 0, 0.4); + --inset-shadow: inset 0 0 10px 2px rgba(0, 0, 0, 0.3), inset 0 0 4px rgba(0, 0, 0, 0.5); } diff --git a/static/icons/play-button.svg b/static/icons/play-button.svg index 0c0fb87..65dcad8 100644 --- a/static/icons/play-button.svg +++ b/static/icons/play-button.svg @@ -1,4 +1,4 @@ - +