fix: performance
This commit is contained in:
+2
-2
@@ -34,7 +34,7 @@
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"server": "src/main.server.ts",
|
||||
"prerender": true,
|
||||
@@ -119,7 +119,7 @@
|
||||
"output": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["node_modules/@angular/material/prebuilt-themes/rose-red.css", "src/styles.scss"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RouterOutlet } from '@angular/router';
|
||||
|
||||
import type { LanguageCode } from '../languages/language.types';
|
||||
import type { LanguageOption } from './app.types';
|
||||
import { KofiWidgetService } from './services/kofi-widget.service';
|
||||
import { LanguageService } from './services/language.service';
|
||||
|
||||
@Component({
|
||||
@@ -23,6 +24,7 @@ import { LanguageService } from './services/language.service';
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly kofiWidgetService = inject(KofiWidgetService);
|
||||
private readonly languageService = inject(LanguageService);
|
||||
|
||||
protected readonly content = this.languageService.content;
|
||||
@@ -53,6 +55,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
const initialLanguage = this.languageService.initializeFromStorage();
|
||||
this.selectedLanguage.set(initialLanguage);
|
||||
this.isLanguageSelectorOpen.set(!this.isLanguageConfirmed());
|
||||
this.kofiWidgetService.scheduleLoad();
|
||||
}
|
||||
|
||||
protected chooseLanguage(language: LanguageCode): void {
|
||||
@@ -90,6 +93,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.kofiWidgetService.teardown();
|
||||
this.document.body.style.overflow = '';
|
||||
this.document.documentElement.style.overflow = '';
|
||||
}
|
||||
|
||||
+11
-5
@@ -1,10 +1,16 @@
|
||||
import type { Routes } from '@angular/router';
|
||||
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { ImprintComponent } from './imprint/imprint.component';
|
||||
|
||||
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: '' },
|
||||
];
|
||||
|
||||
@@ -269,9 +269,15 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
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;
|
||||
const entriesWithTop: Array<{ entry: HTMLElement; top: number }> = Array.from(
|
||||
projectEntries,
|
||||
(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);
|
||||
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>
|
||||
<body>
|
||||
<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>
|
||||
</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