fix: performance
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
`src/` contains the Angular application, SSR entry points, and global styles.
|
||||||
|
Feature code lives under `src/app/` with standalone components such as `home/`,
|
||||||
|
`imprint/`, `footer/`, and shared services in `src/app/services/`. Language
|
||||||
|
packs and shared language types are in `src/languages/`. Source images belong in
|
||||||
|
`src/assets/unoptimized/`; generated web assets are written to
|
||||||
|
`src/assets/optimized/`. Utility and generation scripts live in `scripts/`. CI
|
||||||
|
workflows and issue templates are under `.github/`.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
|
Use npm scripts only.
|
||||||
|
|
||||||
|
- `npm run dev` starts the local Angular dev server after generating assets.
|
||||||
|
- `npm run test` runs the Angular/Karma test suite.
|
||||||
|
- `npm run check` runs the repository validation pipeline.
|
||||||
|
- `npm run fix` applies automatic fixes for version placeholders, ESLint issues,
|
||||||
|
text cleanup, and formatting.
|
||||||
|
- `npm run lint` runs base and type-aware ESLint checks.
|
||||||
|
- `npm run format:check` verifies Prettier formatting.
|
||||||
|
|
||||||
|
For validation, use `npm run check` and `npm run test`. Do not run
|
||||||
|
`npm run build` directly.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
|
Use TypeScript with strict typing. Prefer `type`, `interface`, and `enum`
|
||||||
|
deliberately; do not use `any` or `unknown` unless unavoidable and justified.
|
||||||
|
`var` is forbidden. Only mark values nullable when `null` is a real runtime
|
||||||
|
state. Always declare the correct access modifier (`private`, `protected`,
|
||||||
|
`public`) explicitly when it improves clarity.
|
||||||
|
|
||||||
|
Formatting is enforced with Prettier and ESLint. Follow existing conventions:
|
||||||
|
2-space indentation, single quotes, semicolons, readonly where appropriate, and
|
||||||
|
Angular standalone components with files named
|
||||||
|
`feature.component.ts|html|spec.ts`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
Tests use Jasmine with Karma and live beside implementation files as
|
||||||
|
`*.spec.ts`. Add or update tests when changing component behavior, services,
|
||||||
|
routing, or asset-generation logic. Prefer focused unit tests over broad
|
||||||
|
integration-style setup. Run `npm run test` locally before opening a PR.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
Recent history favors short conventional-style subjects such as
|
||||||
|
`fix: performance`, `fix: text`, and `feat: added languages`. Keep commits
|
||||||
|
scoped and imperative. PRs should include a concise summary, linked issue when
|
||||||
|
relevant, validation notes (`npm run check`, `npm run test`), and screenshots
|
||||||
|
for UI changes, especially mobile or performance-related updates.
|
||||||
|
|
||||||
|
## Asset & Content Notes
|
||||||
|
|
||||||
|
Do not edit generated optimized assets by hand. Update the source files in
|
||||||
|
`src/assets/unoptimized/` and regenerate through the provided scripts. Keep
|
||||||
|
localized text changes synchronized between `src/languages/de.ts` and
|
||||||
|
`src/languages/en.ts`.
|
||||||
@@ -37,7 +37,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
{ code: 'de', accent: 'DE' },
|
{ code: 'de', accent: 'DE' },
|
||||||
];
|
];
|
||||||
protected readonly dialogContent = computed(() =>
|
protected readonly dialogContent = computed(() =>
|
||||||
this.languageService.getPack(this.selectedLanguage()),
|
this.languageService.getShellPack(this.selectedLanguage()),
|
||||||
);
|
);
|
||||||
protected readonly languageSwitcherLabel = computed(() =>
|
protected readonly languageSwitcherLabel = computed(() =>
|
||||||
this.getLanguageLabel(this.currentLanguageCode(), this.content()),
|
this.getLanguageLabel(this.currentLanguageCode(), this.content()),
|
||||||
@@ -66,6 +66,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
protected confirmLanguage(): void {
|
protected confirmLanguage(): void {
|
||||||
this.languageService.confirmLanguage(this.selectedLanguage());
|
this.languageService.confirmLanguage(this.selectedLanguage());
|
||||||
this.isLanguageSelectorOpen.set(false);
|
this.isLanguageSelectorOpen.set(false);
|
||||||
|
this.kofiWidgetService.prioritizeLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reopenLanguageSelector(): void {
|
protected reopenLanguageSelector(): void {
|
||||||
@@ -80,6 +81,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.selectedLanguage.set(this.currentLanguageCode());
|
this.selectedLanguage.set(this.currentLanguageCode());
|
||||||
this.isLanguageSelectorOpen.set(false);
|
this.isLanguageSelectorOpen.set(false);
|
||||||
|
this.kofiWidgetService.prioritizeLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected closeLanguageSelectorOnBackdrop(event: MouseEvent): void {
|
protected closeLanguageSelectorOnBackdrop(event: MouseEvent): void {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
|
import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ChangeDetectionStrategy, Component, inject, NgZone } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, inject, NgZone } from '@angular/core';
|
||||||
import { NgOptimizedImage } from '@angular/common';
|
import { NgOptimizedImage } from '@angular/common';
|
||||||
import { faArrowRight, faArrowDown, faEnvelope, faPhone } from '@fortawesome/free-solid-svg-icons';
|
import { faArrowRight, faArrowDown, faEnvelope, faPhone } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
import { faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
||||||
@@ -8,9 +8,12 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|||||||
import type { Subscription } from 'rxjs';
|
import type { Subscription } from 'rxjs';
|
||||||
import { interval } from 'rxjs';
|
import { interval } from 'rxjs';
|
||||||
|
|
||||||
|
import { deLanguage } from '../../languages/de';
|
||||||
|
import { enLanguage } from '../../languages/en';
|
||||||
import { global } from '../../global';
|
import { global } from '../../global';
|
||||||
import type {
|
import type {
|
||||||
LanguageBioTextEntry,
|
LanguageBioTextEntry,
|
||||||
|
LanguagePack,
|
||||||
LanguagePortraitHighlight,
|
LanguagePortraitHighlight,
|
||||||
LanguageProject,
|
LanguageProject,
|
||||||
} from '../../languages/language.types';
|
} from '../../languages/language.types';
|
||||||
@@ -27,7 +30,9 @@ import { LanguageService } from '../services/language.service';
|
|||||||
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private readonly zone = inject(NgZone);
|
private readonly zone = inject(NgZone);
|
||||||
private readonly languageService = inject(LanguageService);
|
private readonly languageService = inject(LanguageService);
|
||||||
protected readonly content = this.languageService.content;
|
protected readonly content = computed<LanguagePack>(() =>
|
||||||
|
this.languageService.languageCode() === 'de' ? deLanguage : enLanguage,
|
||||||
|
);
|
||||||
|
|
||||||
private scrollAnimationFrameId: number | null = null;
|
private scrollAnimationFrameId: number | null = null;
|
||||||
private projectEntryAnimationFrameId: number | null = null;
|
private projectEntryAnimationFrameId: number | null = null;
|
||||||
@@ -174,14 +179,27 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
protected scrollToContactLinks(): void {
|
protected scrollToContactLinks(): void {
|
||||||
this.scrollToElementById('contact-links', this.getResponsiveScrollOffset(0.05));
|
this.scrollToElementById('contact-links', this.getResponsiveScrollOffset(0.05));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldEnableScrollEffects(): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
const isSmallViewport: boolean = window.innerWidth <= 700;
|
||||||
|
const prefersReducedMotion: boolean = window.matchMedia(
|
||||||
|
'(prefers-reduced-motion: reduce)',
|
||||||
|
).matches;
|
||||||
|
const isCoarsePointer: boolean = window.matchMedia('(pointer: coarse)').matches;
|
||||||
|
|
||||||
|
return !isSmallViewport && !prefersReducedMotion && !isCoarsePointer;
|
||||||
|
}
|
||||||
|
|
||||||
private initNameGradientScrollAnimation(): void {
|
private initNameGradientScrollAnimation(): void {
|
||||||
if (typeof window === 'undefined') return;
|
if (!this.shouldEnableScrollEffects()) return;
|
||||||
window.addEventListener('scroll', this.scheduleNameGradientUpdate, { passive: true });
|
window.addEventListener('scroll', this.scheduleNameGradientUpdate, { passive: true });
|
||||||
window.addEventListener('resize', this.scheduleNameGradientUpdate, { passive: true });
|
window.addEventListener('resize', this.scheduleNameGradientUpdate, { passive: true });
|
||||||
this.scheduleNameGradientUpdate();
|
this.scheduleNameGradientUpdate();
|
||||||
}
|
}
|
||||||
private initProjectEntryRevealObserver(): void {
|
private initProjectEntryRevealObserver(): void {
|
||||||
if (typeof window === 'undefined') return;
|
if (!this.shouldEnableScrollEffects()) return;
|
||||||
window.addEventListener('scroll', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
window.addEventListener('scroll', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
||||||
window.addEventListener('resize', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
window.addEventListener('resize', this.scheduleProjectEntryRevealUpdate, { passive: true });
|
||||||
this.scheduleProjectEntryRevealUpdate();
|
this.scheduleProjectEntryRevealUpdate();
|
||||||
@@ -199,7 +217,7 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
private initAboutSectionScrollAnimation(): void {
|
private initAboutSectionScrollAnimation(): void {
|
||||||
if (typeof window === 'undefined') return;
|
if (!this.shouldEnableScrollEffects()) return;
|
||||||
window.addEventListener('scroll', this.scheduleAboutSectionScrollAnimationUpdate, {
|
window.addEventListener('scroll', this.scheduleAboutSectionScrollAnimationUpdate, {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import type { IconDefinition } from '@fortawesome/angular-fontawesome';
|
import type { IconDefinition } from '@fortawesome/angular-fontawesome';
|
||||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||||
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import { deLanguage } from '../../languages/de';
|
||||||
|
import { enLanguage } from '../../languages/en';
|
||||||
import { global } from '../../global';
|
import { global } from '../../global';
|
||||||
|
import type { LanguagePack } from '../../languages/language.types';
|
||||||
import { FooterComponent } from '../footer/footer.component';
|
import { FooterComponent } from '../footer/footer.component';
|
||||||
import { LanguageService } from '../services/language.service';
|
import { LanguageService } from '../services/language.service';
|
||||||
|
|
||||||
@@ -18,7 +21,9 @@ import { LanguageService } from '../services/language.service';
|
|||||||
export class ImprintComponent {
|
export class ImprintComponent {
|
||||||
private readonly languageService = inject(LanguageService);
|
private readonly languageService = inject(LanguageService);
|
||||||
|
|
||||||
protected readonly content = this.languageService.content;
|
protected readonly content = computed<LanguagePack>(() =>
|
||||||
|
this.languageService.languageCode() === 'de' ? deLanguage : enLanguage,
|
||||||
|
);
|
||||||
protected readonly global = global;
|
protected readonly global = global;
|
||||||
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
|
protected readonly faArrowLeft: IconDefinition = faArrowLeft;
|
||||||
protected readonly contactMailHref = `mailto:${global.contactMail}`;
|
protected readonly contactMailHref = `mailto:${global.contactMail}`;
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ export class KofiWidgetService {
|
|||||||
|
|
||||||
private readonly widgetScriptUrl = 'https://storage.ko-fi.com/cdn/scripts/overlay-widget.js';
|
private readonly widgetScriptUrl = 'https://storage.ko-fi.com/cdn/scripts/overlay-widget.js';
|
||||||
private readonly widgetIframeTitle = 'Ko-fi support chat';
|
private readonly widgetIframeTitle = 'Ko-fi support chat';
|
||||||
private readonly fallbackDelayMs = 10000;
|
private readonly fallbackDelayMs = 2500;
|
||||||
|
private readonly mobileBreakpointPx = 700;
|
||||||
|
private readonly mobileViewportOffsetPx = 24;
|
||||||
|
|
||||||
private loadTimeoutId: number | null = null;
|
private loadTimeoutId: number | null = null;
|
||||||
private loadIdleCallbackId: number | null = null;
|
private loadIdleCallbackId: number | null = null;
|
||||||
private loadListenerAbortController: AbortController | null = null;
|
private loadListenerAbortController: AbortController | null = null;
|
||||||
|
private resizeListenerAbortController: AbortController | null = null;
|
||||||
private iframeObserver: MutationObserver | null = null;
|
private iframeObserver: MutationObserver | null = null;
|
||||||
private isWidgetScheduled = false;
|
private isWidgetScheduled = false;
|
||||||
private isWidgetLoaded = false;
|
private isWidgetLoaded = false;
|
||||||
@@ -85,10 +88,50 @@ export class KofiWidgetService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown(): void {
|
prioritizeLoad(): void {
|
||||||
|
if (!isPlatformBrowser(this.platformId) || this.isWidgetLoaded || this.isWidgetDrawn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const windowRef = this.document.defaultView;
|
const windowRef = this.document.defaultView;
|
||||||
|
if (windowRef === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.clearStartListeners();
|
this.clearStartListeners();
|
||||||
|
this.clearPendingLoadTimers();
|
||||||
|
|
||||||
|
const loadWidget = (): void => {
|
||||||
|
this.loadIdleCallbackId = null;
|
||||||
|
this.loadTimeoutId = null;
|
||||||
|
void this.loadWidget();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof windowRef.requestIdleCallback === 'function') {
|
||||||
|
this.loadIdleCallbackId = windowRef.requestIdleCallback(loadWidget, { timeout: 1200 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadTimeoutId = windowRef.setTimeout(loadWidget, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown(): void {
|
||||||
|
this.clearStartListeners();
|
||||||
|
this.resizeListenerAbortController?.abort();
|
||||||
|
this.resizeListenerAbortController = null;
|
||||||
|
this.clearPendingLoadTimers();
|
||||||
|
|
||||||
|
this.iframeObserver?.disconnect();
|
||||||
|
this.iframeObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearStartListeners(): void {
|
||||||
|
this.loadListenerAbortController?.abort();
|
||||||
|
this.loadListenerAbortController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearPendingLoadTimers(): void {
|
||||||
|
const windowRef = this.document.defaultView;
|
||||||
|
|
||||||
if (windowRef !== null && this.loadTimeoutId !== null) {
|
if (windowRef !== null && this.loadTimeoutId !== null) {
|
||||||
windowRef.clearTimeout(this.loadTimeoutId);
|
windowRef.clearTimeout(this.loadTimeoutId);
|
||||||
@@ -103,14 +146,6 @@ export class KofiWidgetService {
|
|||||||
windowRef.cancelIdleCallback(this.loadIdleCallbackId);
|
windowRef.cancelIdleCallback(this.loadIdleCallbackId);
|
||||||
this.loadIdleCallbackId = null;
|
this.loadIdleCallbackId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.iframeObserver?.disconnect();
|
|
||||||
this.iframeObserver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearStartListeners(): void {
|
|
||||||
this.loadListenerAbortController?.abort();
|
|
||||||
this.loadListenerAbortController = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadWidget(): Promise<void> {
|
private async loadWidget(): Promise<void> {
|
||||||
@@ -190,7 +225,9 @@ export class KofiWidgetService {
|
|||||||
|
|
||||||
this.isWidgetDrawn = true;
|
this.isWidgetDrawn = true;
|
||||||
this.ensureAccessibleIframeTitles();
|
this.ensureAccessibleIframeTitles();
|
||||||
|
this.applyResponsiveWidgetLayout();
|
||||||
this.observeInjectedIframes();
|
this.observeInjectedIframes();
|
||||||
|
this.observeViewportChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureAccessibleIframeTitles(): void {
|
private ensureAccessibleIframeTitles(): void {
|
||||||
@@ -201,6 +238,75 @@ export class KofiWidgetService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyResponsiveWidgetLayout(): void {
|
||||||
|
const windowRef = this.document.defaultView;
|
||||||
|
if (windowRef === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMobileViewport: boolean = windowRef.innerWidth <= this.mobileBreakpointPx;
|
||||||
|
const mobileWidthPx: number = Math.max(windowRef.innerWidth - this.mobileViewportOffsetPx, 280);
|
||||||
|
const mobileHeightPx: number = Math.max(
|
||||||
|
Math.min(Math.round(windowRef.innerHeight * 0.68), 520),
|
||||||
|
360,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.document
|
||||||
|
.querySelectorAll<HTMLElement>('[id*="kofi-widget-overlay"]')
|
||||||
|
.forEach((overlay: HTMLElement): void => {
|
||||||
|
if (isMobileViewport) {
|
||||||
|
overlay.style.left = '12px';
|
||||||
|
overlay.style.right = '12px';
|
||||||
|
overlay.style.bottom = '12px';
|
||||||
|
overlay.style.width = 'auto';
|
||||||
|
overlay.style.maxWidth = `${mobileWidthPx}px`;
|
||||||
|
} else {
|
||||||
|
overlay.style.removeProperty('left');
|
||||||
|
overlay.style.removeProperty('right');
|
||||||
|
overlay.style.removeProperty('width');
|
||||||
|
overlay.style.removeProperty('max-width');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.document
|
||||||
|
.querySelectorAll<HTMLElement>(
|
||||||
|
'.floatingchat-container-wrap, .floatingchat-container-wrap-mobi',
|
||||||
|
)
|
||||||
|
.forEach((container: HTMLElement): void => {
|
||||||
|
if (isMobileViewport) {
|
||||||
|
container.style.width = `${mobileWidthPx}px`;
|
||||||
|
container.style.maxWidth = 'calc(100vw - 24px)';
|
||||||
|
container.style.left = '0';
|
||||||
|
container.style.right = '0';
|
||||||
|
} else {
|
||||||
|
container.style.removeProperty('width');
|
||||||
|
container.style.removeProperty('max-width');
|
||||||
|
container.style.removeProperty('left');
|
||||||
|
container.style.removeProperty('right');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.document
|
||||||
|
.querySelectorAll<HTMLIFrameElement>('iframe[id^="kofi-wo-container"]')
|
||||||
|
.forEach((iframe: HTMLIFrameElement): void => {
|
||||||
|
iframe.title = this.widgetIframeTitle;
|
||||||
|
|
||||||
|
if (isMobileViewport) {
|
||||||
|
iframe.style.width = `${mobileWidthPx}px`;
|
||||||
|
iframe.style.maxWidth = 'calc(100vw - 24px)';
|
||||||
|
iframe.style.height = `${mobileHeightPx}px`;
|
||||||
|
iframe.style.maxHeight = '68vh';
|
||||||
|
iframe.style.borderRadius = '18px';
|
||||||
|
} else {
|
||||||
|
iframe.style.removeProperty('width');
|
||||||
|
iframe.style.removeProperty('max-width');
|
||||||
|
iframe.style.removeProperty('height');
|
||||||
|
iframe.style.removeProperty('max-height');
|
||||||
|
iframe.style.removeProperty('border-radius');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private observeInjectedIframes(): void {
|
private observeInjectedIframes(): void {
|
||||||
if (this.iframeObserver !== null) {
|
if (this.iframeObserver !== null) {
|
||||||
return;
|
return;
|
||||||
@@ -208,6 +314,7 @@ export class KofiWidgetService {
|
|||||||
|
|
||||||
this.iframeObserver = new MutationObserver((): void => {
|
this.iframeObserver = new MutationObserver((): void => {
|
||||||
this.ensureAccessibleIframeTitles();
|
this.ensureAccessibleIframeTitles();
|
||||||
|
this.applyResponsiveWidgetLayout();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.iframeObserver.observe(this.document.body, {
|
this.iframeObserver.observe(this.document.body, {
|
||||||
@@ -215,4 +322,29 @@ export class KofiWidgetService {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private observeViewportChanges(): void {
|
||||||
|
if (this.resizeListenerAbortController !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowRef = this.document.defaultView;
|
||||||
|
if (windowRef === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resizeListenerAbortController = new AbortController();
|
||||||
|
const listenerOptions: AddEventListenerOptions = {
|
||||||
|
passive: true,
|
||||||
|
signal: this.resizeListenerAbortController.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
windowRef.addEventListener(
|
||||||
|
'resize',
|
||||||
|
(): void => {
|
||||||
|
this.applyResponsiveWidgetLayout();
|
||||||
|
},
|
||||||
|
listenerOptions,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,36 @@
|
|||||||
import { Injectable, computed, signal } from '@angular/core';
|
import { Injectable, computed, signal } from '@angular/core';
|
||||||
|
|
||||||
import { deLanguage } from '../../languages/de';
|
import type { LanguageCode, LanguageShellPack } from '../../languages/language.types';
|
||||||
import { enLanguage } from '../../languages/en';
|
|
||||||
import type { LanguageCode, LanguagePack } from '../../languages/language.types';
|
const enShellLanguage: LanguageShellPack = {
|
||||||
|
app: {
|
||||||
|
selectorTitle: 'Select language',
|
||||||
|
changeLanguage: 'Language',
|
||||||
|
changeLanguageAriaLabel: 'Change language',
|
||||||
|
closeSelector: 'Close language selection',
|
||||||
|
languageEnglish: 'English',
|
||||||
|
languageGerman: 'German',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
noscriptMessage: 'Please enable JavaScript for the full experience.',
|
||||||
|
imprintLink: 'Imprint',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deShellLanguage: LanguageShellPack = {
|
||||||
|
app: {
|
||||||
|
selectorTitle: 'Sprache wählen',
|
||||||
|
changeLanguage: 'Sprache',
|
||||||
|
changeLanguageAriaLabel: 'Sprache wechseln',
|
||||||
|
closeSelector: 'Sprachauswahl schliessen',
|
||||||
|
languageEnglish: 'Englisch',
|
||||||
|
languageGerman: 'Deutsch',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
noscriptMessage: 'Bitte aktiviere JavaScript für die volle Darstellung.',
|
||||||
|
imprintLink: 'Impressum',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LanguageService {
|
export class LanguageService {
|
||||||
@@ -12,7 +40,9 @@ export class LanguageService {
|
|||||||
|
|
||||||
readonly languageCode = this.currentLanguageCode.asReadonly();
|
readonly languageCode = this.currentLanguageCode.asReadonly();
|
||||||
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly();
|
readonly isLanguageConfirmed = this.languageConfirmed.asReadonly();
|
||||||
readonly content = computed<LanguagePack>(() => this.getPack(this.currentLanguageCode()));
|
readonly content = computed<LanguageShellPack>(() =>
|
||||||
|
this.getShellPack(this.currentLanguageCode()),
|
||||||
|
);
|
||||||
|
|
||||||
initializeFromStorage(): LanguageCode {
|
initializeFromStorage(): LanguageCode {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -37,7 +67,7 @@ export class LanguageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPack(code: LanguageCode): LanguagePack {
|
getShellPack(code: LanguageCode): LanguageShellPack {
|
||||||
return code === 'de' ? deLanguage : enLanguage;
|
return code === 'de' ? deShellLanguage : enShellLanguage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,3 +93,5 @@ export interface LanguagePack {
|
|||||||
portraitHighlights: LanguagePortraitHighlight[];
|
portraitHighlights: LanguagePortraitHighlight[];
|
||||||
projects: LanguageProject[];
|
projects: LanguageProject[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LanguageShellPack = Pick<LanguagePack, 'app' | 'footer'>;
|
||||||
|
|||||||
+114
@@ -34,6 +34,8 @@
|
|||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
|
background-color: #08080b;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.no-js {
|
html.no-js {
|
||||||
@@ -60,11 +62,13 @@ body {
|
|||||||
font-family: 'Outfit', Arial, sans-serif;
|
font-family: 'Outfit', Arial, sans-serif;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
|
background-color: #08080b;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(217, 162, 140, 0.22), transparent 26%),
|
radial-gradient(circle at top left, rgba(217, 162, 140, 0.22), transparent 26%),
|
||||||
radial-gradient(circle at bottom right, rgba(127, 78, 63, 0.28), transparent 28%),
|
radial-gradient(circle at bottom right, rgba(127, 78, 63, 0.28), transparent 28%),
|
||||||
linear-gradient(180deg, rgba(9, 9, 12, 0.4), rgba(8, 8, 11, 0.82)),
|
linear-gradient(180deg, rgba(9, 9, 12, 0.4), rgba(8, 8, 11, 0.82)),
|
||||||
url('assets/optimized/background.webp') center / cover fixed no-repeat;
|
url('assets/optimized/background.webp') center / cover fixed no-repeat;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -859,6 +863,8 @@ hr {
|
|||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
padding: 0 0 var(--space-6);
|
padding: 0 0 var(--space-6);
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 1px 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-shell {
|
.footer-shell {
|
||||||
@@ -932,6 +938,18 @@ hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(217, 162, 140, 0.18), transparent 28%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(127, 78, 63, 0.24), transparent 32%),
|
||||||
|
linear-gradient(180deg, rgba(9, 9, 12, 0.72), rgba(8, 8, 11, 0.96)),
|
||||||
|
url('assets/optimized/background.webp') center top / cover no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.page-shell,
|
.page-shell,
|
||||||
.footer-inner {
|
.footer-inner {
|
||||||
width: min(calc(100% - 2rem), var(--page-max-width));
|
width: min(calc(100% - 2rem), var(--page-max-width));
|
||||||
@@ -1069,6 +1087,102 @@ hr {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel,
|
||||||
|
.language-overlay,
|
||||||
|
.language-switcher {
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.025)), rgba(12, 12, 16, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-entry,
|
||||||
|
.about-scroll-anim-enabled .story-copy,
|
||||||
|
.about-scroll-anim-enabled .story-detail {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
will-change: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-entry {
|
||||||
|
transition:
|
||||||
|
border-color var(--transition-fast),
|
||||||
|
background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-section,
|
||||||
|
.story-section,
|
||||||
|
.legal-section {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 1px 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.action-link:hover,
|
||||||
|
.detail-toggle:hover,
|
||||||
|
.back-link:hover,
|
||||||
|
.contact-area a:hover,
|
||||||
|
.project-entry.project-entry-from-right:hover,
|
||||||
|
.project-entry.project-entry-from-left:hover,
|
||||||
|
.language-close:hover,
|
||||||
|
.language-switcher:hover,
|
||||||
|
.portrait-highlight-button:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: inherit;
|
||||||
|
border-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link-pride:hover::before {
|
||||||
|
animation: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#e40303 0%,
|
||||||
|
#e40303 16.66%,
|
||||||
|
#ff8c00 16.66%,
|
||||||
|
#ff8c00 33.33%,
|
||||||
|
#ffed00 33.33%,
|
||||||
|
#ffed00 50%,
|
||||||
|
#008026 50%,
|
||||||
|
#008026 66.66%,
|
||||||
|
#004dff 66.66%,
|
||||||
|
#004dff 83.33%,
|
||||||
|
#750787 83.33%,
|
||||||
|
#750787 100%
|
||||||
|
)
|
||||||
|
0 50%;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portrait-media-switching,
|
||||||
|
.action-link-pride:hover::before {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-entry,
|
||||||
|
.about-scroll-anim-enabled .story-copy,
|
||||||
|
.about-scroll-anim-enabled .story-detail,
|
||||||
|
.detail-panel,
|
||||||
|
.action-link,
|
||||||
|
.detail-toggle,
|
||||||
|
.back-link,
|
||||||
|
.contact-area a,
|
||||||
|
.language-close,
|
||||||
|
.language-switcher,
|
||||||
|
.portrait-highlight-button {
|
||||||
|
transition: none;
|
||||||
|
transform: none;
|
||||||
|
will-change: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.language-overlay {
|
.language-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user