fix: performance
This commit is contained in:
+2
-2
@@ -34,7 +34,7 @@
|
|||||||
"input": "public"
|
"input": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"server": "src/main.server.ts",
|
"server": "src/main.server.ts",
|
||||||
"prerender": true,
|
"prerender": true,
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"output": "public"
|
"output": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { RouterOutlet } from '@angular/router';
|
|||||||
|
|
||||||
import type { LanguageCode } from '../languages/language.types';
|
import type { LanguageCode } from '../languages/language.types';
|
||||||
import type { LanguageOption } from './app.types';
|
import type { LanguageOption } from './app.types';
|
||||||
|
import { KofiWidgetService } from './services/kofi-widget.service';
|
||||||
import { LanguageService } from './services/language.service';
|
import { LanguageService } from './services/language.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,6 +24,7 @@ import { LanguageService } from './services/language.service';
|
|||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
private readonly document = inject(DOCUMENT);
|
private readonly document = inject(DOCUMENT);
|
||||||
|
private readonly kofiWidgetService = inject(KofiWidgetService);
|
||||||
private readonly languageService = inject(LanguageService);
|
private readonly languageService = inject(LanguageService);
|
||||||
|
|
||||||
protected readonly content = this.languageService.content;
|
protected readonly content = this.languageService.content;
|
||||||
@@ -53,6 +55,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
const initialLanguage = this.languageService.initializeFromStorage();
|
const initialLanguage = this.languageService.initializeFromStorage();
|
||||||
this.selectedLanguage.set(initialLanguage);
|
this.selectedLanguage.set(initialLanguage);
|
||||||
this.isLanguageSelectorOpen.set(!this.isLanguageConfirmed());
|
this.isLanguageSelectorOpen.set(!this.isLanguageConfirmed());
|
||||||
|
this.kofiWidgetService.scheduleLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected chooseLanguage(language: LanguageCode): void {
|
protected chooseLanguage(language: LanguageCode): void {
|
||||||
@@ -90,6 +93,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
this.kofiWidgetService.teardown();
|
||||||
this.document.body.style.overflow = '';
|
this.document.body.style.overflow = '';
|
||||||
this.document.documentElement.style.overflow = '';
|
this.document.documentElement.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -1,10 +1,16 @@
|
|||||||
import type { Routes } from '@angular/router';
|
import type { Routes } from '@angular/router';
|
||||||
|
|
||||||
import { HomeComponent } from './home/home.component';
|
|
||||||
import { ImprintComponent } from './imprint/imprint.component';
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: HomeComponent, pathMatch: 'full' },
|
{
|
||||||
{ path: 'imprint', component: ImprintComponent, pathMatch: 'full' },
|
path: '',
|
||||||
|
loadComponent: () => import('./home/home.component').then((module) => module.HomeComponent),
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'imprint',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./imprint/imprint.component').then((module) => module.ImprintComponent),
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
{ path: '**', redirectTo: '' },
|
{ path: '**', redirectTo: '' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -269,9 +269,15 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
const startY: number = viewportHeight * 0.98;
|
const startY: number = viewportHeight * 0.98;
|
||||||
const endY: number = viewportHeight * 0.56;
|
const endY: number = viewportHeight * 0.56;
|
||||||
const totalDistance: number = Math.max(startY - endY, 1);
|
const totalDistance: number = Math.max(startY - endY, 1);
|
||||||
projectEntries.forEach((entry: HTMLElement): void => {
|
const entriesWithTop: Array<{ entry: HTMLElement; top: number }> = Array.from(
|
||||||
const entryRect: DOMRect = entry.getBoundingClientRect();
|
projectEntries,
|
||||||
const rawProgress: number = (startY - entryRect.top) / totalDistance;
|
(entry: HTMLElement) => ({
|
||||||
|
entry,
|
||||||
|
top: entry.getBoundingClientRect().top,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
entriesWithTop.forEach(({ entry, top }): void => {
|
||||||
|
const rawProgress: number = (startY - top) / totalDistance;
|
||||||
const clampedProgress: number = Math.min(Math.max(rawProgress, 0), 1);
|
const clampedProgress: number = Math.min(Math.max(rawProgress, 0), 1);
|
||||||
entry.style.setProperty('--project-entry-progress', clampedProgress.toFixed(4));
|
entry.style.setProperty('--project-entry-progress', clampedProgress.toFixed(4));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||||
|
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class KofiWidgetService {
|
||||||
|
private readonly document = inject(DOCUMENT);
|
||||||
|
private readonly platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
|
private readonly widgetScriptUrl = 'https://storage.ko-fi.com/cdn/scripts/overlay-widget.js';
|
||||||
|
private readonly widgetIframeTitle = 'Ko-fi support chat';
|
||||||
|
private readonly fallbackDelayMs = 10000;
|
||||||
|
|
||||||
|
private loadTimeoutId: number | null = null;
|
||||||
|
private loadIdleCallbackId: number | null = null;
|
||||||
|
private loadListenerAbortController: AbortController | null = null;
|
||||||
|
private iframeObserver: MutationObserver | null = null;
|
||||||
|
private isWidgetScheduled = false;
|
||||||
|
private isWidgetLoaded = false;
|
||||||
|
private isWidgetDrawn = false;
|
||||||
|
|
||||||
|
scheduleLoad(): void {
|
||||||
|
if (!isPlatformBrowser(this.platformId) || this.isWidgetScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isWidgetScheduled = true;
|
||||||
|
|
||||||
|
const windowRef = this.document.defaultView;
|
||||||
|
if (windowRef === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueWidgetLoad = (): void => {
|
||||||
|
if (this.isWidgetLoaded || this.isWidgetDrawn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadWidget = (): void => {
|
||||||
|
this.loadIdleCallbackId = null;
|
||||||
|
void this.loadWidget();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof windowRef.requestIdleCallback === 'function') {
|
||||||
|
this.loadIdleCallbackId = windowRef.requestIdleCallback(loadWidget, { timeout: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadTimeoutId = windowRef.setTimeout(loadWidget, 1800);
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerWidgetLoad = (): void => {
|
||||||
|
this.clearStartListeners();
|
||||||
|
queueWidgetLoad();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleFallbackWidgetLoad = (): void => {
|
||||||
|
if (this.isWidgetLoaded || this.isWidgetDrawn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadTimeoutId = windowRef.setTimeout((): void => {
|
||||||
|
this.loadTimeoutId = null;
|
||||||
|
triggerWidgetLoad();
|
||||||
|
}, this.fallbackDelayMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadListenerAbortController = new AbortController();
|
||||||
|
const listenerOptions: AddEventListenerOptions = {
|
||||||
|
passive: true,
|
||||||
|
signal: this.loadListenerAbortController.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
windowRef.addEventListener('pointerdown', triggerWidgetLoad, listenerOptions);
|
||||||
|
windowRef.addEventListener('keydown', triggerWidgetLoad, listenerOptions);
|
||||||
|
windowRef.addEventListener('touchstart', triggerWidgetLoad, listenerOptions);
|
||||||
|
windowRef.addEventListener('scroll', triggerWidgetLoad, listenerOptions);
|
||||||
|
|
||||||
|
if (this.document.readyState === 'complete') {
|
||||||
|
scheduleFallbackWidgetLoad();
|
||||||
|
} else {
|
||||||
|
windowRef.addEventListener('load', scheduleFallbackWidgetLoad, {
|
||||||
|
...listenerOptions,
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown(): void {
|
||||||
|
const windowRef = this.document.defaultView;
|
||||||
|
|
||||||
|
this.clearStartListeners();
|
||||||
|
|
||||||
|
if (windowRef !== null && this.loadTimeoutId !== null) {
|
||||||
|
windowRef.clearTimeout(this.loadTimeoutId);
|
||||||
|
this.loadTimeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
windowRef !== null &&
|
||||||
|
this.loadIdleCallbackId !== null &&
|
||||||
|
typeof windowRef.cancelIdleCallback === 'function'
|
||||||
|
) {
|
||||||
|
windowRef.cancelIdleCallback(this.loadIdleCallbackId);
|
||||||
|
this.loadIdleCallbackId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iframeObserver?.disconnect();
|
||||||
|
this.iframeObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearStartListeners(): void {
|
||||||
|
this.loadListenerAbortController?.abort();
|
||||||
|
this.loadListenerAbortController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadWidget(): Promise<void> {
|
||||||
|
if (this.isWidgetLoaded || this.isWidgetDrawn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowRef = this.document.defaultView;
|
||||||
|
if (windowRef === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowRef.kofiWidgetOverlay !== undefined) {
|
||||||
|
this.isWidgetLoaded = true;
|
||||||
|
this.drawWidget();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingScript = this.document.querySelector<HTMLScriptElement>(
|
||||||
|
`script[src="${this.widgetScriptUrl}"]`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingScript !== null) {
|
||||||
|
existingScript.addEventListener(
|
||||||
|
'load',
|
||||||
|
(): void => {
|
||||||
|
this.isWidgetLoaded = true;
|
||||||
|
this.drawWidget();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject): void => {
|
||||||
|
const scriptElement = this.document.createElement('script');
|
||||||
|
scriptElement.src = this.widgetScriptUrl;
|
||||||
|
scriptElement.async = true;
|
||||||
|
scriptElement.crossOrigin = 'anonymous';
|
||||||
|
scriptElement.addEventListener(
|
||||||
|
'load',
|
||||||
|
(): void => {
|
||||||
|
this.isWidgetLoaded = true;
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
scriptElement.addEventListener(
|
||||||
|
'error',
|
||||||
|
(): void => reject(new Error('Failed to load the Ko-fi widget script.')),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
this.document.body.appendChild(scriptElement);
|
||||||
|
}).catch((): void => {
|
||||||
|
this.isWidgetLoaded = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.drawWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawWidget(): void {
|
||||||
|
if (this.isWidgetDrawn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowRef = this.document.defaultView;
|
||||||
|
if (windowRef?.kofiWidgetOverlay === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowRef.kofiWidgetOverlay.draw('fraujulian', {
|
||||||
|
type: 'floating-chat',
|
||||||
|
'floating-chat.donateButton.text': 'Coffee',
|
||||||
|
'floating-chat.donateButton.background-color': '#ff5f5f',
|
||||||
|
'floating-chat.donateButton.text-color': '#fff',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isWidgetDrawn = true;
|
||||||
|
this.ensureAccessibleIframeTitles();
|
||||||
|
this.observeInjectedIframes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureAccessibleIframeTitles(): void {
|
||||||
|
this.document
|
||||||
|
.querySelectorAll<HTMLIFrameElement>('iframe[id^="kofi-wo-container"]')
|
||||||
|
.forEach((iframe: HTMLIFrameElement): void => {
|
||||||
|
iframe.title = this.widgetIframeTitle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private observeInjectedIframes(): void {
|
||||||
|
if (this.iframeObserver !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iframeObserver = new MutationObserver((): void => {
|
||||||
|
this.ensureAccessibleIframeTitles();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.iframeObserver.observe(this.document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,15 +17,5 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|
||||||
<script src="https://storage.ko-fi.com/cdn/scripts/overlay-widget.js"></script>
|
|
||||||
<script>
|
|
||||||
kofiWidgetOverlay.draw('fraujulian', {
|
|
||||||
type: 'floating-chat',
|
|
||||||
'floating-chat.donateButton.text': 'Coffee',
|
|
||||||
'floating-chat.donateButton.background-color': '#ff5f5f',
|
|
||||||
'floating-chat.donateButton.text-color': '#fff',
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
interface KofiWidgetOverlay {
|
||||||
|
draw(userName: string, options: Record<string, string>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
kofiWidgetOverlay?: KofiWidgetOverlay;
|
||||||
|
requestIdleCallback?: (callback: IdleRequestCallback, options?: IdleRequestOptions) => number;
|
||||||
|
cancelIdleCallback?: (handle: number) => void;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user