WIP
This commit is contained in:
parent
c2dbf995cc
commit
dbb48fbde6
29 changed files with 146 additions and 94 deletions
14
src/page/main/about/about.html.ts
Normal file
14
src/page/main/about/about.html.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Header } from "../../../model/portfolio";
|
||||
import { html } from "../../../model/misc";
|
||||
import "./about.scss";
|
||||
|
||||
export const generate = (
|
||||
{ name, picture, about }: Header,
|
||||
aPictureOf: string
|
||||
): html => `
|
||||
<section id="about">
|
||||
<header>
|
||||
<img alt="${aPictureOf} ${name}" src="${picture}"/>
|
||||
<h1>${name}</h1>
|
||||
</header>
|
||||
</section>`;
|
||||
26
src/page/main/about/about.scss
Normal file
26
src/page/main/about/about.scss
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
@import "../../../style/mixins";
|
||||
@import "../../../style/vars";
|
||||
|
||||
#about {
|
||||
margin-top: $normal-margin;
|
||||
|
||||
header {
|
||||
@include center-children();
|
||||
|
||||
h1,
|
||||
img {
|
||||
@include title-font();
|
||||
}
|
||||
|
||||
img {
|
||||
@include square(4ch);
|
||||
border-radius: 100%;
|
||||
margin-right: 1.5ex;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
17
src/page/main/about/about.ts
Normal file
17
src/page/main/about/about.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { PageContent } from "../content/content";
|
||||
import { Header } from "../../../model/portfolio";
|
||||
import { PageElement } from "../../../framework/page-element";
|
||||
import { createElement } from "../../../framework/element-factory";
|
||||
|
||||
import { generate } from "./about.html";
|
||||
|
||||
export class PageHeader extends PageElement {
|
||||
public constructor(header: Header, aPictureOf: string) {
|
||||
const root = createElement(generate(header, aPictureOf));
|
||||
const content = new PageContent(header.about);
|
||||
|
||||
root.appendChild(content.getElement());
|
||||
super([content]);
|
||||
this.setElement(root);
|
||||
}
|
||||
}
|
||||
13
src/page/main/content/content.scss
Normal file
13
src/page/main/content/content.scss
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
@import "../../../style/vars";
|
||||
|
||||
.content {
|
||||
margin-top: $small-margin;
|
||||
|
||||
* {
|
||||
margin-top: $line-height;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
35
src/page/main/content/content.ts
Normal file
35
src/page/main/content/content.ts
Normal 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>
|
||||
`)
|
||||
);
|
||||
}
|
||||
}
|
||||
10
src/page/main/footer/footer.html.ts
Normal file
10
src/page/main/footer/footer.html.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Footer } from "../../../model/portfolio";
|
||||
import { html } from "../../../model/misc";
|
||||
import "./footer.scss";
|
||||
|
||||
export const generate = ({ email, cv }: Footer, cvName: string): html => `
|
||||
<footer>
|
||||
<a id="email" href="mailto:${email}">${email}</a>
|
||||
<a id="cv" href="mailto:${cv}">${cvName}</a>
|
||||
</footer>
|
||||
`;
|
||||
8
src/page/main/footer/footer.scss
Normal file
8
src/page/main/footer/footer.scss
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@import "../../../style/mixins";
|
||||
@import "../../../style/vars";
|
||||
|
||||
footer {
|
||||
@include card();
|
||||
@include center-children();
|
||||
margin: $normal-margin 0;
|
||||
}
|
||||
12
src/page/main/footer/footer.ts
Normal file
12
src/page/main/footer/footer.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Footer } from "../../../model/portfolio";
|
||||
import { PageElement } from "../../../framework/page-element";
|
||||
import { createElement } from "../../../framework/element-factory";
|
||||
|
||||
import { generate } from "./footer.html";
|
||||
|
||||
export class PageFooter extends PageElement {
|
||||
constructor(footer: Footer, cvName: string) {
|
||||
super();
|
||||
this.setElement(createElement(generate(footer, cvName)));
|
||||
}
|
||||
}
|
||||
10
src/page/main/image-viewer/image-viewer.html.ts
Normal file
10
src/page/main/image-viewer/image-viewer.html.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { html } from "../../../model/misc";
|
||||
import cancel from "../../../static/icons/cancel.svg";
|
||||
import "./image-viewer.scss";
|
||||
|
||||
export const generate = (): html => `
|
||||
<section class="photo-viewer">
|
||||
<img id="photo" alt="currently opened photo"/>
|
||||
<img id="cancel" src="${cancel}" alt="cancel"/>
|
||||
</section>
|
||||
`;
|
||||
30
src/page/main/image-viewer/image-viewer.scss
Normal file
30
src/page/main/image-viewer/image-viewer.scss
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
@import "../../../style/vars";
|
||||
@import "../../../style/mixins";
|
||||
|
||||
.photo-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: 90vw;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
#cancel {
|
||||
@include square($icon-size);
|
||||
position: absolute;
|
||||
padding: $normal-margin;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
50
src/page/main/image-viewer/image-viewer.ts
Normal file
50
src/page/main/image-viewer/image-viewer.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { createElement } from "../../../framework/element-factory";
|
||||
import { PageElement } from "../../../framework/page-element";
|
||||
|
||||
import { generate } from "./image-viewer.html";
|
||||
|
||||
export class PageImageViewer extends PageElement {
|
||||
public constructor() {
|
||||
super();
|
||||
const root = createElement(generate());
|
||||
(root.querySelector("#cancel") as HTMLElement).onclick = () =>
|
||||
PageImageViewer.hide(root);
|
||||
this.setElement(root);
|
||||
}
|
||||
|
||||
public onAfterLoad(parent: HTMLElement) {
|
||||
super.onAfterLoad(parent);
|
||||
|
||||
document.body.addEventListener("keydown", this.handleKeydown.bind(this));
|
||||
|
||||
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;
|
||||
|
||||
PageImageViewer.show(this.getElement());
|
||||
}
|
||||
|
||||
private handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
PageImageViewer.hide(this.getElement());
|
||||
}
|
||||
}
|
||||
|
||||
private static show(e: HTMLElement) {
|
||||
e.style.display = "flex";
|
||||
}
|
||||
private static hide(e: HTMLElement) {
|
||||
e.style.display = "none";
|
||||
}
|
||||
}
|
||||
6
src/page/main/main.html.ts
Normal file
6
src/page/main/main.html.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { html } from "../../model/misc";
|
||||
import "./main.scss";
|
||||
|
||||
export const generate = (): html => `
|
||||
<article id="main"></article>
|
||||
`;
|
||||
6
src/page/main/main.scss
Normal file
6
src/page/main/main.scss
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@import "../../style/vars";
|
||||
|
||||
article#main {
|
||||
width: $body-width;
|
||||
margin: auto;
|
||||
}
|
||||
25
src/page/main/main.ts
Normal file
25
src/page/main/main.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { PageElement } from "../../framework/page-element";
|
||||
import { createElement } from "../../framework/element-factory";
|
||||
import { generate } from "./main.html";
|
||||
import { Portfolio } from "../../model/portfolio";
|
||||
import { PageHeader } from "./about/about";
|
||||
import { PageTimeline } from "./timeline/timeline";
|
||||
import { PageFooter } from "./footer/footer";
|
||||
import { PageImageViewer } from "./image-viewer/image-viewer";
|
||||
|
||||
export class PageMain extends PageElement {
|
||||
constructor({ config, header, timeline, footer }: Portfolio) {
|
||||
const root = createElement(generate());
|
||||
|
||||
const pageElements: Array<PageElement> = [
|
||||
new PageHeader(header, config.aPictureOf),
|
||||
new PageTimeline(timeline, config.showMore, config.showLess),
|
||||
new PageFooter(footer, config.cvName),
|
||||
new PageImageViewer()
|
||||
];
|
||||
|
||||
root.append(...pageElements.map(e => e.getElement()));
|
||||
super(pageElements);
|
||||
this.setElement(root);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { TimelineElement } from "../../../../model/portfolio";
|
||||
import { html } from "../../../../model/misc";
|
||||
import "./timeline-element.scss";
|
||||
|
||||
export const generate = (
|
||||
{ date, title, picture, description, more, link }: TimelineElement,
|
||||
showMore: string,
|
||||
showLess: string
|
||||
): html => `
|
||||
<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>
|
||||
`;
|
||||
103
src/page/main/timeline/timeline-element/timeline-element.scss
Normal file
103
src/page/main/timeline/timeline-element/timeline-element.scss
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
@import "../../../../style/mixins";
|
||||
@import "../../../../style/vars";
|
||||
|
||||
.timeline-element {
|
||||
display: flex;
|
||||
|
||||
.date-narrow-screen,
|
||||
.date-wide-screen {
|
||||
@include insignificant-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} + 2ch);
|
||||
transform: rotate(30deg);
|
||||
margin: 0 $normal-margin 0 calc(#{$line-width} + 1ex);
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) .card {
|
||||
margin-top: $normal-margin;
|
||||
}
|
||||
|
||||
.card {
|
||||
@include card();
|
||||
overflow: hidden;
|
||||
|
||||
& > *:not(:first-child) {
|
||||
margin-top: $line-height;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include sub-title-font();
|
||||
}
|
||||
|
||||
.date-narrow-screen {
|
||||
@media (min-width: $breakpoint-width) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
margin: $small-margin 0 0 0;
|
||||
color: $light-text-color;
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#more {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
margin-top: 0;
|
||||
transition: height $slow-transition-time;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: relative;
|
||||
margin-top: $small-margin;
|
||||
|
||||
* {
|
||||
transition: opacity $slow-transition-time;
|
||||
}
|
||||
|
||||
#show-more {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#show-less {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/page/main/timeline/timeline-element/timeline-element.ts
Normal file
72
src/page/main/timeline/timeline-element/timeline-element.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { TimelineElement } from "../../../../model/portfolio";
|
||||
import { PageContent } from "../../content/content";
|
||||
import { PageElement } from "../../../../framework/page-element";
|
||||
import { createElement } from "../../../../framework/element-factory";
|
||||
import { generate } from "./timeline-element.html";
|
||||
|
||||
export class PageTimelineElement extends PageElement {
|
||||
private isOpen;
|
||||
private more: HTMLElement;
|
||||
|
||||
public constructor(
|
||||
timelineElement: TimelineElement,
|
||||
showMore: string,
|
||||
showLess: string
|
||||
) {
|
||||
const root = createElement(generate(timelineElement, showMore, showLess));
|
||||
|
||||
if (timelineElement.more) {
|
||||
const content = new PageContent(timelineElement.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";
|
||||
PageTimelineElement.show(showMore);
|
||||
PageTimelineElement.hide(showLess);
|
||||
} else {
|
||||
this.openMoreToFullHeight();
|
||||
PageTimelineElement.hide(showMore);
|
||||
PageTimelineElement.show(showLess);
|
||||
}
|
||||
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
private static hide(element: HTMLElement) {
|
||||
element.style.opacity = "0";
|
||||
setTimeout(() => (element.style.visibility = "hidden"), 350);
|
||||
}
|
||||
|
||||
private static show(element: HTMLElement) {
|
||||
element.style.visibility = "visible";
|
||||
element.style.opacity = "1";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/page/main/timeline/timeline.html.ts
Normal file
6
src/page/main/timeline/timeline.html.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { html } from "../../../model/misc";
|
||||
import "./timeline.scss";
|
||||
|
||||
export const generate = (): html => `
|
||||
<main id="timeline"></main>
|
||||
`;
|
||||
5
src/page/main/timeline/timeline.scss
Normal file
5
src/page/main/timeline/timeline.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@import "../../../style/vars";
|
||||
|
||||
main#timeline {
|
||||
margin-top: $normal-margin;
|
||||
}
|
||||
21
src/page/main/timeline/timeline.ts
Normal file
21
src/page/main/timeline/timeline.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { TimelineElement } from "../../../model/portfolio";
|
||||
import { PageElement } from "../../../framework/page-element";
|
||||
import { createElement } from "../../../framework/element-factory";
|
||||
import { PageTimelineElement } from "./timeline-element/timeline-element";
|
||||
import { generate } from "./timeline.html";
|
||||
|
||||
export class PageTimeline extends PageElement {
|
||||
public constructor(
|
||||
timeline: Array<TimelineElement>,
|
||||
showMore: string,
|
||||
showLess: string
|
||||
) {
|
||||
const root = createElement(generate());
|
||||
const elements = timeline.map(
|
||||
e => new PageTimelineElement(e, showMore, showLess)
|
||||
);
|
||||
root.append(...elements.map(e => e.getElement()));
|
||||
super(elements);
|
||||
this.setElement(root);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue