Convert to component based architecture

This commit is contained in:
Schmelczer András 2019-12-21 22:59:41 +01:00
parent eb2075aec5
commit cdaa423b8a
70 changed files with 1942 additions and 484 deletions

28
src/page/about/about.scss Normal file
View file

@ -0,0 +1,28 @@
@import "../../style/mixins";
@import "../../style/vars";
#about {
header {
@include center-children();
margin-top: $normal-margin;
h1,
img {
font: $title-font;
}
h1 {
text-align: center;
}
img {
@include square(4ch);
border-radius: 100%;
margin-right: 1.5ex;
}
}
* {
text-align: justify;
}
}

22
src/page/about/about.ts Normal file
View file

@ -0,0 +1,22 @@
import { PageContent } from "../content/content";
import { Header } from "../../model/portfolio";
import "./about.scss";
import { PageElement } from "../../framework/page-element";
import { createElement } from "../../framework/element-factory";
export class PageHeader extends PageElement {
public constructor({ name, picture, about }: Header, aPictureOf: string) {
const root = createElement(`
<section id="about">
<header>
<img alt="${aPictureOf} ${name}" src="${picture}"/>
<h1>${name}</h1>
</header>
</section>
`);
const content = new PageContent(about);
root.appendChild(content.getElement());
super([content]);
this.setElement(root);
}
}

View file

@ -0,0 +1,8 @@
@import "../../style/vars";
.content {
margin-top: $small-margin;
* {
margin-top: 1ch;
}
}

View file

@ -0,0 +1,35 @@
import { Content, TypedContent } from "../../model/content";
import "./content.scss";
import { PageElement } from "../../framework/page-element";
import { createElement } from "../../framework/element-factory";
export class PageContent extends PageElement {
private static isTyped(content): content is TypedContent {
return (content as TypedContent).type !== undefined;
}
public constructor(content: Content) {
super();
this.setElement(
createElement(`
<div class="content">
${content
.map(element => {
if (PageContent.isTyped(element)) {
if (element.type === "a") {
return `<a href="${element.href}" target="_blank"> ${element.text} </a>`;
}
if (element.type === "video") {
return `<video controls><source src="${element.src}" /></video>`;
}
throw new Error("Unhandled type.");
}
return `<p>${element}</p>`;
})
.join("\n")}
</div>
`)
);
}
}

View file

@ -0,0 +1,7 @@
@import "../../style/mixins";
footer {
@include card();
@include center-children();
margin-top: $normal-margin;
}

18
src/page/footer/footer.ts Normal file
View file

@ -0,0 +1,18 @@
import { Footer } from "../../model/portfolio";
import "./footer.scss";
import { PageElement } from "../../framework/page-element";
import { createElement } from "../../framework/element-factory";
export class PageFooter extends PageElement {
constructor({ email, cv }: Footer, cvName: string) {
super();
this.setElement(
createElement(`
<footer>
<a id="email" href="mailto:${email}">${email}</a>
<a id="email" href="mailto:${cv}">${cvName}</a>
</footer>
`)
);
}
}

View file

View file

@ -0,0 +1,43 @@
import "./image-viewer.scss";
import cancel from "../../static/icons/cancel.svg";
import { html } from "../../model/misc";
import { createElement } from "../../framework/element-factory";
import { PageElement } from "../../framework/page-element";
import { hide, show } from "../../framework/helpers";
export class PageImageViewer extends PageElement {
private static template: html = `
<section class="photo-viewer">
<img id="photo" alt="currently opened photo"/>
<img id="cancel" src="${cancel}" alt="cancel"/>
</section>
`;
public constructor() {
super();
const root = createElement(PageImageViewer.template);
(root.querySelector("#cancel") as HTMLElement).onclick = () => hide(root);
this.setElement(root);
}
public onAfterLoad(parent: HTMLElement) {
super.onAfterLoad(parent);
const images = Array.prototype.slice.call(parent.querySelectorAll("img"));
images
.filter(
(img: HTMLImageElement) => img.parentElement !== this.getElement()
)
.forEach(
(img: HTMLImageElement) => (img.onclick = this.handleClick.bind(this))
);
}
private handleClick(event: Event) {
(this.getElement().querySelector(
"#photo"
) as HTMLImageElement).src = (event.target as HTMLImageElement).src;
show(this.getElement());
}
}

21
src/page/index.ts Normal file
View file

@ -0,0 +1,21 @@
import { Portfolio } from "../model/portfolio";
import { PageHeader } from "./about/about";
import { PageTimeline } from "./timeline/timeline";
import { PageElement } from "../framework/page-element";
import { PageImageViewer } from "./image-viewer/image-viewer";
import { PageFooter } from "./footer/footer";
export const create = (portfolio: Portfolio) => {
const { config, header, timeline, footer } = portfolio;
document.title = header.name;
const pageElements: Array<PageElement> = [
new PageHeader(header, config.aPictureOf),
new PageTimeline(timeline, config.showMore, config.showLess),
new PageFooter(footer, config.cvName),
new PageImageViewer()
];
document.body.append(...pageElements.map(e => e.getElement()));
pageElements.forEach(e => e.onAfterLoad(document.body));
};

View file

@ -0,0 +1,80 @@
@import "../../../style/mixins";
@import "../../../style/vars";
.timeline-element {
display: flex;
.date-narrow-screen,
.date-wide-screen {
font: $text-font;
}
.line {
@media (max-width: $breakpoint-width) {
display: none;
}
position: relative;
margin: 0 $small-margin 0 $icon-size / 2;
border-left: $line-width solid $normal-text-color;
&:before {
content: "";
@include square($icon-size);
position: absolute;
top: 33%;
left: -0.5 * $icon-size - (1.5 * $line-width);
border: $line-width solid $normal-text-color;
border-radius: 100%;
background: $background;
}
.date-wide-screen {
position: relative;
top: calc(33% + #{$icon-size} + 1ch);
margin: 0 $normal-margin 0 calc(#{$line-width} + 1ex);
width: 100px;
}
}
&:not(:first-of-type) .card {
margin-top: $normal-margin;
}
.card {
@include card();
h2 {
font: $sub-title-font;
}
.date-narrow-screen {
@media (min-width: $breakpoint-width) {
display: none;
}
margin: $small-margin 0 0 0;
color: $light-text-color;
}
#more {
overflow: hidden;
height: 0;
transition: height $transition-time;
}
.buttons {
position: relative;
* {
position: absolute;
left: 50%;
transform: translateX(-50%);
transition: opacity $transition-time;
}
#show-less {
opacity: 0;
}
}
}
}

View file

@ -0,0 +1,91 @@
import { TimelineElement } from "../../../model/portfolio";
import { PageContent } from "../../content/content";
import "./timeline-element.scss";
import { PageElement } from "../../../framework/page-element";
import { createElement } from "../../../framework/element-factory";
export class PageTimelineElement extends PageElement {
private isOpen;
private more: HTMLElement;
public constructor(
{ date, title, picture, description, more, link }: TimelineElement,
showMore: string,
showLess: string
) {
const root = createElement(`
<section class="timeline-element">
<div class="line">
<p class="date-wide-screen">${date}</p>
</div>
<div class="card">
<h2>${title}</h2>
<p class="date-narrow-screen">${date}</p>
<img src="${picture}" alt="${picture}"/>
<p class="description">${description}</p>
${
more
? `
<div id="more"></div>
<div class="buttons">
<a id="show-more">${showMore}</a>
<a id="show-less">${showLess}</a>
</div>
`
: ""
}
${
link
? `
<a href="${link}" target="_blank">${link}</a>`
: ""
}
</div>
</section>
`);
if (more) {
const content = new PageContent(more);
super([content]);
this.isOpen = false;
this.more = root.querySelector("#more");
this.more.appendChild(content.getElement());
window.addEventListener("resize", this.handleResize.bind(this));
root
.querySelector(".buttons")
.addEventListener("click", this.toggleOpen.bind(this));
} else super();
this.setElement(root);
}
private toggleOpen() {
const showMore = this.getElement().querySelector(
"#show-more"
) as HTMLElement;
const showLess = this.getElement().querySelector(
"#show-less"
) as HTMLElement;
if (this.isOpen) {
this.more.style.height = "0";
showMore.style.opacity = "1";
showLess.style.opacity = "0";
} else {
this.openMoreToFullHeight();
showMore.style.opacity = "0";
showLess.style.opacity = "1";
}
this.isOpen = !this.isOpen;
}
private openMoreToFullHeight() {
this.more.style.height = `${this.more.scrollHeight.toString()}px`;
}
private handleResize() {
if (this.isOpen) {
this.more.style.height = "auto";
setTimeout(this.openMoreToFullHeight.bind(this), 200);
}
}
}

View file

@ -0,0 +1,5 @@
@import "../../style/vars";
#timeline {
margin-top: $normal-margin;
}

View file

@ -0,0 +1,21 @@
import { TimelineElement } from "../../model/portfolio";
import "./timeline.scss";
import { PageElement } from "../../framework/page-element";
import { createElement } from "../../framework/element-factory";
import { PageTimelineElement } from "./timeline-element/timeline-element";
export class PageTimeline extends PageElement {
public constructor(
timeline: Array<TimelineElement>,
showMore: string,
showLess: string
) {
const root = createElement(`<main id="timeline"></main>`);
const elements = timeline.map(
e => new PageTimelineElement(e, showMore, showLess)
);
root.append(...elements.map(e => e.getElement()));
super(elements);
this.setElement(root);
}
}