feat: added languages
This commit is contained in:
+65
-144
@@ -1,67 +1,64 @@
|
||||
import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, inject, NgZone } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, inject, NgZone } from '@angular/core';
|
||||
import { NgOptimizedImage } from '@angular/common';
|
||||
import { faArrowRight, faArrowDown, faEnvelope, faPhone } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
||||
import type { IconDefinition } from '@fortawesome/angular-fontawesome';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import type { SafeUrl } from '@angular/platform-browser';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { interval } from 'rxjs';
|
||||
|
||||
import type { BioTextEntry, Global, PortraitHighlight, PortfolioProject } from '../../global';
|
||||
import { global } from '../../global';
|
||||
import type {
|
||||
LanguageBioTextEntry,
|
||||
LanguagePortraitHighlight,
|
||||
LanguageProject,
|
||||
} from '../../languages/language.types';
|
||||
import { FooterComponent } from '../footer/footer.component';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [FooterComponent, FaIconComponent, NgOptimizedImage],
|
||||
templateUrl: './home.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
private readonly zone = inject(NgZone);
|
||||
private readonly languageService = inject(LanguageService);
|
||||
protected readonly content = this.languageService.content;
|
||||
|
||||
private scrollAnimationFrameId: number | null = null;
|
||||
private projectEntryAnimationFrameId: number | null = null;
|
||||
private aboutSectionAnimationFrameId: number | null = null;
|
||||
private readonly scheduleNameGradientUpdate: () => void = (): void => {
|
||||
if (this.scrollAnimationFrameId !== null || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.scrollAnimationFrameId !== null || typeof window === 'undefined') return;
|
||||
this.scrollAnimationFrameId = window.requestAnimationFrame((): void => {
|
||||
this.scrollAnimationFrameId = null;
|
||||
this.updateNameGradientProgress();
|
||||
});
|
||||
};
|
||||
private readonly scheduleProjectEntryRevealUpdate: () => void = (): void => {
|
||||
if (this.projectEntryAnimationFrameId !== null || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.projectEntryAnimationFrameId !== null || typeof window === 'undefined') return;
|
||||
this.projectEntryAnimationFrameId = window.requestAnimationFrame((): void => {
|
||||
this.projectEntryAnimationFrameId = null;
|
||||
this.updateProjectEntryRevealProgress();
|
||||
});
|
||||
};
|
||||
private readonly scheduleAboutSectionScrollAnimationUpdate: () => void = (): void => {
|
||||
if (this.aboutSectionAnimationFrameId !== null || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.aboutSectionAnimationFrameId !== null || typeof window === 'undefined') return;
|
||||
this.aboutSectionAnimationFrameId = window.requestAnimationFrame((): void => {
|
||||
this.aboutSectionAnimationFrameId = null;
|
||||
this.updateAboutSectionScrollProgress();
|
||||
});
|
||||
};
|
||||
|
||||
protected readonly global: Global = global;
|
||||
protected readonly age: number = this.calculateAge(global.birthdate);
|
||||
|
||||
protected isLongBioShown: boolean = false;
|
||||
protected isLongBioMounted: boolean = false;
|
||||
protected readonly global = global;
|
||||
protected readonly age = this.calculateAge(global.birthdate);
|
||||
protected readonly contactMailHref = `mailto:${global.contactMail}`;
|
||||
protected isLongBioShown = false;
|
||||
protected isLongBioMounted = false;
|
||||
protected readonly faArrowRight: IconDefinition = faArrowRight;
|
||||
protected readonly faArrowDown: IconDefinition = faArrowDown;
|
||||
protected readonly faEnvelope: IconDefinition = faEnvelope;
|
||||
@@ -70,28 +67,44 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
readonly faGithub: IconDefinition = faGithub;
|
||||
readonly faLinkedin: IconDefinition = faLinkedin;
|
||||
|
||||
protected readonly contactSafeMail: SafeUrl;
|
||||
protected readonly portraitHighlights: PortraitHighlight[] = global.portraitHighlights;
|
||||
protected currentPortraitHighlightIndex: number = 0;
|
||||
protected isPortraitSwitching: boolean = false;
|
||||
|
||||
protected currentPortraitHighlightIndex = 0;
|
||||
protected isPortraitSwitching = false;
|
||||
protected currentIndex = 0;
|
||||
protected readonly projects: PortfolioProject[] = global.projects;
|
||||
protected readonly fallbackBioEntry: BioTextEntry = { label: 'my stack', value: '' };
|
||||
protected readonly fallbackBioEntry: LanguageBioTextEntry = { label: 'my stack', value: '' };
|
||||
private sub!: Subscription;
|
||||
private bioHideTimeoutId: number | null = null;
|
||||
|
||||
constructor() {
|
||||
this.contactSafeMail = this.sanitizer.bypassSecurityTrustUrl(`mailto:${global.contactMail}`);
|
||||
protected get portraitHighlights(): LanguagePortraitHighlight[] {
|
||||
return this.content().portraitHighlights;
|
||||
}
|
||||
protected get projects(): LanguageProject[] {
|
||||
return this.content().projects;
|
||||
}
|
||||
protected get currentPortraitHighlight(): LanguagePortraitHighlight | null {
|
||||
return this.portraitHighlights[this.currentPortraitHighlightIndex] ?? null;
|
||||
}
|
||||
protected get currentBioEntry(): LanguageBioTextEntry {
|
||||
return (
|
||||
this.content().bioTextsList[this.currentIndex] ?? {
|
||||
label: this.content().home.fallbackBioLabel,
|
||||
value: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected interpolate(text: string): string {
|
||||
return text
|
||||
.replaceAll('{{firstname}}', this.global.firstname)
|
||||
.replaceAll('{{age}}', String(this.age));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initNameGradientScrollAnimation();
|
||||
|
||||
this.zone.runOutsideAngular((): void => {
|
||||
this.sub = interval(1000).subscribe((): void => {
|
||||
this.zone.run((): void => {
|
||||
this.currentIndex = (this.currentIndex + 1) % this.global.bioTextsList.length;
|
||||
const len = this.content().bioTextsList.length;
|
||||
this.currentIndex = len > 0 ? (this.currentIndex + 1) % len : 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -101,7 +114,6 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.initProjectEntryRevealObserver();
|
||||
this.initAboutSectionScrollAnimation();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
if (this.bioHideTimeoutId !== null) {
|
||||
@@ -117,9 +129,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (this.isLongBioShown) {
|
||||
this.isLongBioShown = false;
|
||||
if (typeof window !== 'undefined') {
|
||||
if (this.bioHideTimeoutId !== null) {
|
||||
window.clearTimeout(this.bioHideTimeoutId);
|
||||
}
|
||||
if (this.bioHideTimeoutId !== null) window.clearTimeout(this.bioHideTimeoutId);
|
||||
this.bioHideTimeoutId = window.setTimeout((): void => {
|
||||
this.isLongBioMounted = false;
|
||||
this.bioHideTimeoutId = null;
|
||||
@@ -129,12 +139,10 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.bioHideTimeoutId !== null && typeof window !== 'undefined') {
|
||||
window.clearTimeout(this.bioHideTimeoutId);
|
||||
this.bioHideTimeoutId = null;
|
||||
}
|
||||
|
||||
this.isLongBioMounted = true;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.requestAnimationFrame((): void => {
|
||||
@@ -146,88 +154,52 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.isLongBioShown = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected get currentPortraitHighlight(): PortraitHighlight | null {
|
||||
return this.portraitHighlights[this.currentPortraitHighlightIndex] ?? null;
|
||||
}
|
||||
|
||||
protected get currentBioEntry(): BioTextEntry {
|
||||
return this.global.bioTextsList[this.currentIndex] ?? this.fallbackBioEntry;
|
||||
}
|
||||
|
||||
protected showNextPortraitHighlight(): void {
|
||||
if (this.portraitHighlights.length <= 1 || this.isPortraitSwitching) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.portraitHighlights.length <= 1 || this.isPortraitSwitching) return;
|
||||
this.isPortraitSwitching = true;
|
||||
|
||||
setTimeout((): void => {
|
||||
this.currentPortraitHighlightIndex =
|
||||
(this.currentPortraitHighlightIndex + 1) % this.portraitHighlights.length;
|
||||
}, 125);
|
||||
|
||||
setTimeout((): void => {
|
||||
this.isPortraitSwitching = false;
|
||||
}, 250);
|
||||
}
|
||||
|
||||
protected scrollToAboutSection(): void {
|
||||
this.scrollToElementById('about', this.getResponsiveScrollOffset(0.035));
|
||||
}
|
||||
|
||||
protected scrollToProjectsSection(): void {
|
||||
this.scrollToElementById('projects', this.getResponsiveScrollOffset(0.035));
|
||||
}
|
||||
|
||||
protected scrollToContactLinks(): void {
|
||||
this.scrollToElementById('contact-links', this.getResponsiveScrollOffset(0.05));
|
||||
}
|
||||
|
||||
private initNameGradientScrollAnimation(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
window.addEventListener('scroll', this.scheduleNameGradientUpdate, { passive: true });
|
||||
window.addEventListener('resize', this.scheduleNameGradientUpdate, { passive: true });
|
||||
this.scheduleNameGradientUpdate();
|
||||
}
|
||||
|
||||
private initProjectEntryRevealObserver(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
window.addEventListener('scroll', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
||||
window.addEventListener('resize', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
||||
this.scheduleProjectEntryRevealUpdate();
|
||||
}
|
||||
|
||||
private destroyProjectEntryRevealObserver(): void {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
window.removeEventListener('scroll', this.scheduleProjectEntryRevealUpdate);
|
||||
window.removeEventListener('resize', this.scheduleProjectEntryRevealUpdate);
|
||||
|
||||
if (this.projectEntryAnimationFrameId !== null) {
|
||||
window.cancelAnimationFrame(this.projectEntryAnimationFrameId);
|
||||
this.projectEntryAnimationFrameId = null;
|
||||
}
|
||||
|
||||
const projectEntries: NodeListOf<HTMLElement> = document.querySelectorAll('.project-entry');
|
||||
projectEntries.forEach((entry: HTMLElement): void => {
|
||||
document.querySelectorAll<HTMLElement>('.project-entry').forEach((entry: HTMLElement): void => {
|
||||
entry.style.removeProperty('--project-entry-progress');
|
||||
});
|
||||
}
|
||||
|
||||
private initAboutSectionScrollAnimation(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
window.addEventListener('scroll', this.scheduleAboutSectionScrollAnimationUpdate, {
|
||||
passive: true,
|
||||
});
|
||||
@@ -236,45 +208,28 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
this.scheduleAboutSectionScrollAnimationUpdate();
|
||||
}
|
||||
|
||||
private destroyAboutSectionScrollAnimation(): void {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
window.removeEventListener('scroll', this.scheduleAboutSectionScrollAnimationUpdate);
|
||||
window.removeEventListener('resize', this.scheduleAboutSectionScrollAnimationUpdate);
|
||||
|
||||
if (this.aboutSectionAnimationFrameId !== null) {
|
||||
window.cancelAnimationFrame(this.aboutSectionAnimationFrameId);
|
||||
this.aboutSectionAnimationFrameId = null;
|
||||
}
|
||||
|
||||
const aboutSection: HTMLElement | null = document.getElementById('about');
|
||||
aboutSection?.style.removeProperty('--about-scroll-progress');
|
||||
document.getElementById('about')?.style.removeProperty('--about-scroll-progress');
|
||||
}
|
||||
|
||||
private destroyNameGradientScrollAnimation(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
window.removeEventListener('scroll', this.scheduleNameGradientUpdate);
|
||||
window.removeEventListener('resize', this.scheduleNameGradientUpdate);
|
||||
|
||||
if (this.scrollAnimationFrameId !== null) {
|
||||
window.cancelAnimationFrame(this.scrollAnimationFrameId);
|
||||
this.scrollAnimationFrameId = null;
|
||||
}
|
||||
|
||||
document.documentElement.style.removeProperty('--name-pride-progress');
|
||||
}
|
||||
|
||||
private updateNameGradientProgress(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
const root: HTMLElement = document.documentElement;
|
||||
const portfolioEyebrow: HTMLElement | null = document.getElementById('portfolio-eyebrow');
|
||||
const portfolioTriggerY: number =
|
||||
@@ -292,20 +247,12 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
window.scrollY > 0
|
||||
? Math.max(acceleratedProgress, isMobileViewport ? 0.18 : 0.05)
|
||||
: acceleratedProgress;
|
||||
|
||||
root.style.setProperty('--name-pride-progress', progress.toFixed(4));
|
||||
}
|
||||
|
||||
private updateAboutSectionScrollProgress(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return;
|
||||
const aboutSection: HTMLElement | null = document.getElementById('about');
|
||||
if (aboutSection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aboutSection === null) return;
|
||||
const sectionRect: DOMRect = aboutSection.getBoundingClientRect();
|
||||
const viewportHeight: number = window.innerHeight || 1;
|
||||
const startY: number = viewportHeight * 0.96;
|
||||
@@ -313,21 +260,15 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const totalDistance: number = Math.max(startY - endY, 1);
|
||||
const rawProgress: number = (startY - sectionRect.top) / totalDistance;
|
||||
const clampedProgress: number = Math.min(Math.max(rawProgress, 0), 1);
|
||||
|
||||
aboutSection.style.setProperty('--about-scroll-progress', clampedProgress.toFixed(4));
|
||||
}
|
||||
|
||||
private updateProjectEntryRevealProgress(): void {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
const projectEntries: NodeListOf<HTMLElement> = document.querySelectorAll('.project-entry');
|
||||
const viewportHeight: number = window.innerHeight || 1;
|
||||
const startY: number = viewportHeight * 0.98;
|
||||
const endY: number = viewportHeight * 0.56;
|
||||
const totalDistance: number = Math.max(startY - endY, 1);
|
||||
|
||||
projectEntries.forEach((entry: HTMLElement): void => {
|
||||
const entryRect: DOMRect = entry.getBoundingClientRect();
|
||||
const rawProgress: number = (startY - entryRect.top) / totalDistance;
|
||||
@@ -335,7 +276,6 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
entry.style.setProperty('--project-entry-progress', clampedProgress.toFixed(4));
|
||||
});
|
||||
}
|
||||
|
||||
private calculateAge(birthDateString: string): number {
|
||||
const birthDate: Date = new Date(birthDateString);
|
||||
const now: Date = new Date();
|
||||
@@ -343,40 +283,21 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const hasBirthdayPassedThisYear: boolean =
|
||||
now.getMonth() > birthDate.getMonth() ||
|
||||
(now.getMonth() === birthDate.getMonth() && now.getDate() >= birthDate.getDate());
|
||||
|
||||
if (!hasBirthdayPassedThisYear) {
|
||||
age -= 1;
|
||||
}
|
||||
|
||||
if (!hasBirthdayPassedThisYear) age -= 1;
|
||||
return age;
|
||||
}
|
||||
|
||||
private scrollToElementById(elementId: string, offsetPx: number = 0): void {
|
||||
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
||||
const element: HTMLElement | null = document.getElementById(elementId);
|
||||
if (element === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (element === null) return;
|
||||
const targetTop: number = Math.max(
|
||||
element.getBoundingClientRect().top + window.scrollY - offsetPx,
|
||||
0,
|
||||
);
|
||||
|
||||
window.scrollTo({
|
||||
top: targetTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
window.scrollTo({ top: targetTop, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
private getResponsiveScrollOffset(viewportRatio: number = 0.05): number {
|
||||
if (typeof window === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') return 0;
|
||||
const minOffsetPx: number = 20;
|
||||
const maxOffsetPx: number = 48;
|
||||
const responsiveOffsetPx: number = window.innerHeight * viewportRatio;
|
||||
|
||||
Reference in New Issue
Block a user