-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
-
+
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index 4424760..75dfcc2 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -1,4 +1,3 @@
-@use 'style/language-selector';
@use 'style/lenis';
#background-img {
@@ -60,6 +59,19 @@
flex-direction: column;
text-align: center;
align-items: center;
+
+ // for rotation
+ position: relative;
+ transform-style: preserve-3d;
+ transform-origin: left;
+ transition: all .6s ease-in-out;
+ &.rotated {
+ transition: all .4s ease-in-out;
+ //transform: rotateY(calc(1 * var(--rotate-angle)));
+ //transform: rotateX(70deg);
+ //transform: translateY(-5000px);
+ //transform: scale(0.6);
+ }
}
:host ::ng-deep {
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 5f62c0e..cf1a109 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -11,6 +11,8 @@ import 'js-circle-progress';
import {TimelineCardComponent} from "./components/timeline-card/timeline-card.component";
import {HeadingCardComponent} from "./components/heading-card/heading-card.component";
import {SkillsCardComponent} from "./components/skills-card/skills-card.component";
+import {NavbarDotComponent} from "./components/navbar-dot/navbar-dot.component";
+import {SidebarComponent} from "./sections/sidebar/sidebar.component";
gsap.registerPlugin(ScrollTrigger);
@@ -29,7 +31,10 @@ import "./js/lenis.js";
// Custom
TimelineCardComponent,
HeadingCardComponent,
- SkillsCardComponent],
+ SkillsCardComponent,
+ NavbarDotComponent,
+ SidebarComponent
+ ],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
@@ -38,8 +43,9 @@ export class AppComponent {
@ViewChild("backgroundImg", {read: ElementRef}) backgroundImage: ElementRef;
@ViewChild("outest_container", {read: ElementRef}) outestContainer: ElementRef;
@ViewChild("progress_bar", {read: ElementRef}) progressBar: ElementRef;
+ // @ViewChild("content_container", {read: ElementRef}) contentContainer: ElementRef;
@ViewChild("education_card", {read: ElementRef}) educationCard: ElementRef;
- @ViewChild("lang_selector", {read: ElementRef}) langSelector: ElementRef;
+ @ViewChild("sidebar", {read: ElementRef}) sidebar: ElementRef;
dataService: DataService;
@@ -51,7 +57,7 @@ export class AppComponent {
}
ngAfterViewInit(): void {
- this.addLangButtonAnimation();
+ this.addSidebarAnimation();
this.addProgressBarAnimation();
// @ts-ignore
for (let el of document.getElementsByClassName("alarm-clock-animated")) {
@@ -86,7 +92,7 @@ export class AppComponent {
})
}
- addLangButtonAnimation() {
+ addSidebarAnimation() {
const tl: gsap.core.Timeline = gsap.timeline({
scrollTrigger: {
start: "top 90%",
@@ -99,12 +105,12 @@ export class AppComponent {
.to(this.backgroundImage.nativeElement, {
filter: "blur(2px)"
})
- .fromTo(this.langSelector.nativeElement, {
+ .fromTo(this.sidebar.nativeElement, {
opacity: 0,
ease: "power1.in",
display: "none"
}, {
- display: "var(--display-side-elements)",
+ display: "var(--display-side-elements-flex)",
opacity: 1,
}, "<");
}
@@ -115,7 +121,7 @@ export class AppComponent {
start: "top bottom",
trigger: el,
end: "center center",
- // markers: true, // TODO remove
+ // markers: true,
scrub: true,
},
defaults: {
@@ -127,7 +133,7 @@ export class AppComponent {
start: "center center",
trigger: el,
end: "bottom top",
- // markers: true, // TODO remove
+ // markers: true,
scrub: true,
},
defaults: {
diff --git a/src/app/components/navbar-dot/navbar-dot.component.html b/src/app/components/navbar-dot/navbar-dot.component.html
new file mode 100644
index 0000000..b557c35
--- /dev/null
+++ b/src/app/components/navbar-dot/navbar-dot.component.html
@@ -0,0 +1 @@
+
diff --git a/src/app/components/navbar-dot/navbar-dot.component.scss b/src/app/components/navbar-dot/navbar-dot.component.scss
new file mode 100644
index 0000000..8607dfa
--- /dev/null
+++ b/src/app/components/navbar-dot/navbar-dot.component.scss
@@ -0,0 +1,74 @@
+:host {
+ height: 1.2rem;
+ width: 1.2rem;
+ border-radius: 50%;
+ border: 3px solid rgb(255, 255, 255);
+ transition: all .6s ease-in-out;
+ user-select: none;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0;
+
+ &:not(.extended):hover {
+ transition: all .1s ease-in-out;
+ height: 1.5rem;
+ width: 1.5rem;
+ border: 5px solid rgb(255, 255, 255);
+ background: rgba(0, 0, 0, 0.3);
+ cursor: pointer;
+ }
+
+ &.active:not(.extended) {
+ background: #e134e8 !important;
+ }
+ &.extended {
+ position: relative;
+ transition: all .4s ease-in-out;
+ width: 20rem;
+ height: 5rem;
+ margin: 0 3rem;
+ border-radius: 20px;
+ //transform: translateX(-4rem);
+ font-size: 1.3rem;
+ &.active {
+ border: 0;
+ &::before {
+ // code from: https://dev.to/afif/border-with-gradient-and-radius-387f
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: 20px;
+ padding: 5px;
+ margin: -5px;
+ background: linear-gradient(to right, #a445b2, #fa4299);
+ -webkit-mask:
+ linear-gradient(#fff 0 0) content-box,
+ linear-gradient(#fff 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ }
+ }
+ &:hover {
+ width: 21rem;
+ margin: 0 1rem;
+ height: 5.3rem;
+ font-size: 1.4rem;
+ background: rgba(0, 0, 0, 0.3);
+ cursor: pointer;
+ }
+ span {
+ display: block;
+ }
+ }
+}
+
+span {
+ display: none;
+ transition: all .4s ease-in-out;
+ color: white;
+ text-align: center;
+ margin-top: auto;
+ margin-bottom: auto;
+}
diff --git a/src/app/components/navbar-dot/navbar-dot.component.ts b/src/app/components/navbar-dot/navbar-dot.component.ts
new file mode 100644
index 0000000..5316df8
--- /dev/null
+++ b/src/app/components/navbar-dot/navbar-dot.component.ts
@@ -0,0 +1,56 @@
+import {Component, EventEmitter, HostBinding, HostListener, Input, Output} from '@angular/core';
+import {Section} from "../../data/model";
+import gsap from "gsap";
+import {ScrollToPlugin} from "gsap/ScrollToPlugin";
+
+gsap.registerPlugin(ScrollToPlugin)
+
+@Component({
+ selector: 'navbar-dot',
+ standalone: true,
+ imports: [],
+ templateUrl: './navbar-dot.component.html',
+ styleUrl: './navbar-dot.component.scss'
+})
+export class NavbarDotComponent {
+
+ @Input({required: true}) section: Section;
+
+ // @HostBinding('class.extended')
+ // @Input()
+ // extended = false;
+
+ @HostBinding('class.active')
+ @Input()
+ active = false;
+
+ // @Output() hover: EventEmitter
= new EventEmitter();
+
+ delayTimeout: any;
+
+ // @HostListener("mouseenter")
+ // onMouseover() {
+ // this.delayTimeout = setTimeout(() => this.hover.emit(), 300);
+ // }
+
+ /**
+ * In case the hovering stops before delay timeout is over, the hovering is not forwarded.
+ */
+ // @HostListener("mouseleave")
+ // onMouseleave() {
+ // clearTimeout(this.delayTimeout);
+ // }
+
+ @HostListener("click")
+ onClick() {
+ gsap.to(window, {
+ duration: 2,
+ scrollTo: {
+ y: `#${this.section.id}`,
+ offsetY: 20,
+ },
+ ease: "power1.inOut"
+ });
+ }
+
+}
diff --git a/src/app/data/data.service.ts b/src/app/data/data.service.ts
index b3c8887..d30aa0c 100644
--- a/src/app/data/data.service.ts
+++ b/src/app/data/data.service.ts
@@ -33,7 +33,7 @@ export class DataService {
// @ts-ignore
this.education = educationJson[this.lang];
// @ts-ignore
- this.languagePack = generalJson[this.lang];
+ this.languagePack = new LanguagePack(generalJson[this.lang]);
// @ts-ignore
this.experience = experienceJson[this.lang];
// @ts-ignore
diff --git a/src/app/data/model.ts b/src/app/data/model.ts
index 32f98be..b3fe537 100644
--- a/src/app/data/model.ts
+++ b/src/app/data/model.ts
@@ -33,7 +33,7 @@ export interface SkillCategory {
skills: Skill[];
}
-export interface LanguagePack {
+export interface ILanguagePack {
/**
* This is the identifier which is used in the languages array and for definition of the object.
*/
@@ -46,7 +46,51 @@ export interface LanguagePack {
isoAlpha2: string;
heading: string;
subheading: string;
+ home: string;
education: string;
experience: string;
skills: string;
+ about: string;
+}
+
+export class LanguagePack implements ILanguagePack {
+ id: string;
+ name: string;
+ isoAlpha2: string;
+ heading: string;
+ subheading: string;
+ home: string;
+ education: string;
+ experience: string;
+ skills: string;
+ about: string;
+
+ sections: Section[];
+
+ constructor(langPack: ILanguagePack) {
+ this.id = langPack.id;
+ this.name = langPack.name;
+ this.isoAlpha2 = langPack.isoAlpha2;
+ this.heading = langPack.heading;
+ this.subheading = langPack.subheading;
+ this.home = langPack.home;
+ this.education = langPack.education;
+ this.experience = langPack.experience;
+ this.skills = langPack.skills;
+ this.about = langPack.about;
+
+ this.sections = [
+ {name: this.home, id: "home-start", position: 0},
+ {name: this.education, id: "education-start", position: 1},
+ {name: this.experience, id: "experience-start", position: 2},
+ {name: this.skills, id: "skills-start", position: 3},
+ {name: this.about, id: "about-start", position: 4},
+ ];
+ }
+}
+
+export interface Section {
+ name: string;
+ id: string;
+ position: number;
}
diff --git a/src/app/sections/sidebar/sidebar.component.html b/src/app/sections/sidebar/sidebar.component.html
new file mode 100644
index 0000000..4f4fca8
--- /dev/null
+++ b/src/app/sections/sidebar/sidebar.component.html
@@ -0,0 +1,15 @@
+
+
+
diff --git a/src/app/style/language-selector.scss b/src/app/sections/sidebar/sidebar.component.scss
similarity index 56%
rename from src/app/style/language-selector.scss
rename to src/app/sections/sidebar/sidebar.component.scss
index 0e6a27d..c2a349b 100644
--- a/src/app/style/language-selector.scss
+++ b/src/app/sections/sidebar/sidebar.component.scss
@@ -1,3 +1,41 @@
+:host {
+ position: absolute;
+ z-index: 1000;
+}
+
+.sidebar-container {
+ position: fixed;
+ width: auto;
+ min-width: 5vw;
+ max-width: 30vw;
+ top: 0;
+ right: 0;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+}
+
+#lang-selector {
+ position: absolute;
+ top: 2vh;
+ transform: translateX(-32px);
+ //align-self: flex-end;
+}
+
+#navbar {
+ //position: absolute;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+ //flex-grow: 1;
+}
+
+// Lang Button
:host ::ng-deep {
.flag-icon {
width: 100%;
@@ -31,11 +69,3 @@
}
}
}
-
-#lang-selector {
- position: fixed;
- right: calc(2vh + 64px);
- top: 2vh;
- z-index: 1000;
- display: var(--display-side-elements);
-}
diff --git a/src/app/sections/sidebar/sidebar.component.ts b/src/app/sections/sidebar/sidebar.component.ts
new file mode 100644
index 0000000..6bcfcf7
--- /dev/null
+++ b/src/app/sections/sidebar/sidebar.component.ts
@@ -0,0 +1,75 @@
+import {
+ ChangeDetectorRef,
+ Component,
+ HostListener,
+ Input,
+ Output,
+ QueryList,
+ ViewChildren
+} from '@angular/core';
+import {NavbarDotComponent} from "../../components/navbar-dot/navbar-dot.component";
+import {NgForOf} from "@angular/common";
+import {SpeedDialModule} from "primeng/speeddial";
+import {Section} from "../../data/model";
+import {MenuItem} from "primeng/api";
+import {ScrollTrigger} from "gsap/ScrollTrigger";
+
+@Component({
+ selector: 'sidebar',
+ standalone: true,
+ imports: [
+ NavbarDotComponent,
+ NgForOf,
+ SpeedDialModule
+ ],
+ templateUrl: './sidebar.component.html',
+ styleUrl: './sidebar.component.scss'
+})
+export class SidebarComponent {
+
+ @Input({required: true}) sections: Section[];
+ @Input({required: true}) languagesMenuItems: MenuItem[];
+
+ // @Output() hovered: boolean = false;
+
+ @ViewChildren("navbar_dot", {read: NavbarDotComponent}) dots: QueryList;
+
+ sectionActive = 0;
+
+ changeDetectorRef: ChangeDetectorRef;
+
+ constructor(changeDetectorRef: ChangeDetectorRef) {
+ this.changeDetectorRef = changeDetectorRef;
+ }
+
+ ngAfterViewInit(): void {
+ this.addSelectedAnimation();
+ }
+
+ // @HostListener("mouseleave")
+ // onMouseleave() {
+ // this.hovered = false;
+ // }
+
+ addSelectedAnimation() {
+ for (let i = 0; i < this.sections.length; i++) {
+ const section = this.sections[i];
+ ScrollTrigger.create({
+ start: "top 50%",
+ trigger: `#${section.id}`,
+ end: "+=1",
+ // markers: true,
+ onEnter: self => {
+ console.log("Active section is ", i, "onEnter");
+ this.sectionActive = i;
+ this.changeDetectorRef.detectChanges();
+ },
+ onEnterBack: self => {
+ console.log("Active section is ", i-1, "onEnterBack");
+ this.sectionActive = i-1;
+ this.changeDetectorRef.detectChanges();
+ }
+ });
+ }
+ }
+}
diff --git a/src/data/general.json b/src/data/general.json
index 00e2efa..fdd23b0 100644
--- a/src/data/general.json
+++ b/src/data/general.json
@@ -7,9 +7,11 @@
"isoAlpha2": "de",
"heading": "Axel Herrmann",
"subheading": "Software Engineer",
+ "home": "Home",
"education": "Bildungsweg",
"experience": "Erfahrung",
- "skills": "Kenntnisse"
+ "skills": "Kenntnisse",
+ "about": "Über"
},
"en": {
"id": "en",
@@ -17,8 +19,10 @@
"isoAlpha2": "us",
"heading": "Axel Herrmann",
"subheading": "Software Engineer",
+ "home": "Home",
"education": "Education",
"experience": "Experience",
- "skills": "Skills"
+ "skills": "Skills",
+ "about": "About"
}
}
diff --git a/src/styles.scss b/src/styles.scss
index e8a9b82..4afaf50 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -45,10 +45,37 @@ html {
@media (max-width: 1000px) {
--main-content-width: 100vw;
--display-side-elements: none;
+ --display-side-elements-flex: none;
}
@media (min-width: 1000px) {
--main-content-width: max(60vw, 1000px);
--display-side-elements: block;
+ --display-side-elements-flex: flex;
}
--main-content-side-margin: calc((100vw - var(--main-content-width)) / 2);
+
+ /*******************************************************
+ * Rotation calculations *
+ *******************************************************/
+ /* Input */
+ --side-content-width-visible-percent: calc(4/5);
+
+ /* Calculations */
+ --rotated-depth-percent: calc(sqrt(1 - pow(var(--side-content-width-visible-percent),2)));
+ /* alpha = acos(ancathete / hypotenuse) */
+ --rotate-angle: calc(acos(var(--side-content-width-visible-percent)));
+
+ /*
+ (180 - alpha) / 2
+ => result has to be subtracted from 180 again because of css rotation rules
+ */
+ --side-content-rotation: calc(180deg - (180deg - var(--rotate-angle)) / 2);
+ /*
+ a² = b² + c² - 2bc * cos(alpha)
+ a = √(b² + c² - 2bc * cos(alpha))
+ a = √(1² + 1² - 2 * cos(alpha))
+ a = √(2 - 2cos(alpha))
+ */
+ /*--side-content-length-percent: calc(2 - 2 * sqrt(cos(var(--rotate-angle))));*/
+ --side-content-length-percent: sqrt(2 - 2 * cos(var(--rotate-angle)));
}