@if (showNavbarAndFooter()) {
@@ -49,6 +54,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
})
export class AppComponent implements OnInit {
title = 'goals-tracker';
+ readonly isBrowser = injectIsBrowser();
loadingProgress = signal(0);
loadingText = signal('Initializing...');
loadingComplete = signal(false);
@@ -96,6 +102,12 @@ export class AppComponent implements OnInit {
}
ngOnInit(): void {
+ if (!this.isBrowser) {
+ // SSR: skip canvas/loading chrome; render content immediately.
+ this.loadingComplete.set(true);
+ return;
+ }
+
this.loadAllData();
// Track route changes for navigation loading
diff --git a/src/app/shared/components/animated-counter/animated-counter.component.ts b/src/app/shared/components/animated-counter/animated-counter.component.ts
index 62fcdf5..2bc899d 100644
--- a/src/app/shared/components/animated-counter/animated-counter.component.ts
+++ b/src/app/shared/components/animated-counter/animated-counter.component.ts
@@ -1,5 +1,6 @@
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
@Component({
selector: 'app-animated-counter',
@@ -13,6 +14,8 @@ import { CommonModule } from '@angular/common';
styles: [],
})
export class AnimatedCounterComponent implements OnInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@Input() value: number = 0;
@Input() duration: number = 2000;
@Input() decimals: number = 1;
@@ -25,6 +28,10 @@ export class AnimatedCounterComponent implements OnInit, OnDestroy {
private startValue: number = 0;
ngOnInit(): void {
+ if (!this.isBrowser) {
+ this.displayValue = this.value;
+ return;
+ }
this.startValue = this.displayValue;
this.startTime = performance.now();
this.animate();
diff --git a/src/app/shared/components/basketball-game/basketball-game.component.ts b/src/app/shared/components/basketball-game/basketball-game.component.ts
index 9ffd50e..98e70f4 100644
--- a/src/app/shared/components/basketball-game/basketball-game.component.ts
+++ b/src/app/shared/components/basketball-game/basketball-game.component.ts
@@ -7,6 +7,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
import * as THREE from 'three';
@Component({
@@ -51,6 +52,8 @@ import * as THREE from 'three';
],
})
export class BasketballGameComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef
;
private scene!: THREE.Scene;
@@ -77,6 +80,9 @@ export class BasketballGameComponent implements OnInit, AfterViewInit, OnDestroy
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.initThree();
this.createScene();
this.setupEventListeners();
diff --git a/src/app/shared/components/confetti/confetti.component.ts b/src/app/shared/components/confetti/confetti.component.ts
index 4c895d2..91ec42b 100644
--- a/src/app/shared/components/confetti/confetti.component.ts
+++ b/src/app/shared/components/confetti/confetti.component.ts
@@ -1,5 +1,6 @@
import { Component, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
interface ConfettiParticle {
x: number;
@@ -28,6 +29,8 @@ interface ConfettiParticle {
],
})
export class ConfettiComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
private ctx!: CanvasRenderingContext2D;
@@ -38,6 +41,9 @@ export class ConfettiComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
const canvas = this.canvasRef.nativeElement;
this.ctx = canvas.getContext('2d')!;
canvas.width = window.innerWidth;
diff --git a/src/app/shared/components/custom-cursor/custom-cursor.component.ts b/src/app/shared/components/custom-cursor/custom-cursor.component.ts
index 1d1a797..fbe556b 100644
--- a/src/app/shared/components/custom-cursor/custom-cursor.component.ts
+++ b/src/app/shared/components/custom-cursor/custom-cursor.component.ts
@@ -6,6 +6,7 @@ import {
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
@Component({
selector: 'app-custom-cursor',
@@ -123,6 +124,8 @@ import { CommonModule } from '@angular/common';
],
})
export class CustomCursorComponent implements OnInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
x = 0;
y = 0;
ringX = 0;
@@ -145,6 +148,9 @@ export class CustomCursorComponent implements OnInit, OnDestroy {
].join(',');
ngOnInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.animate();
this.setupClickableDetection();
}
diff --git a/src/app/shared/components/duck-shooting-game/duck-shooting-game.component.ts b/src/app/shared/components/duck-shooting-game/duck-shooting-game.component.ts
index efc7bb6..8948e25 100644
--- a/src/app/shared/components/duck-shooting-game/duck-shooting-game.component.ts
+++ b/src/app/shared/components/duck-shooting-game/duck-shooting-game.component.ts
@@ -7,6 +7,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
import * as THREE from 'three';
interface Duck {
@@ -96,6 +97,8 @@ interface Bullet {
],
})
export class DuckShootingGameComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
private scene!: THREE.Scene;
@@ -119,6 +122,9 @@ export class DuckShootingGameComponent implements OnInit, AfterViewInit, OnDestr
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.initThree();
this.createScene();
this.setupEventListeners();
diff --git a/src/app/shared/components/loading-screen/loading-screen.component.ts b/src/app/shared/components/loading-screen/loading-screen.component.ts
index 152e23e..0908fa0 100644
--- a/src/app/shared/components/loading-screen/loading-screen.component.ts
+++ b/src/app/shared/components/loading-screen/loading-screen.component.ts
@@ -9,6 +9,7 @@ import {
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
import * as THREE from 'three';
@Component({
@@ -68,6 +69,8 @@ import * as THREE from 'three';
],
})
export class LoadingScreenComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
@Input() progress = signal(0);
@Input() loadingText = signal('Loading...');
@@ -96,6 +99,9 @@ export class LoadingScreenComponent implements OnInit, AfterViewInit, OnDestroy
}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.initThree();
this.createParticles();
this.animate();
diff --git a/src/app/shared/components/magic-ball-game/magic-ball-game.component.ts b/src/app/shared/components/magic-ball-game/magic-ball-game.component.ts
index 839b0fe..c6e99f5 100644
--- a/src/app/shared/components/magic-ball-game/magic-ball-game.component.ts
+++ b/src/app/shared/components/magic-ball-game/magic-ball-game.component.ts
@@ -7,6 +7,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
import * as THREE from 'three';
interface Block {
@@ -59,6 +60,8 @@ interface Block {
],
})
export class MagicBallGameComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
private scene!: THREE.Scene;
@@ -78,6 +81,9 @@ export class MagicBallGameComponent implements OnInit, AfterViewInit, OnDestroy
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.initThree();
this.createScene();
this.setupEventListeners();
diff --git a/src/app/shared/components/particle-background/particle-background.component.ts b/src/app/shared/components/particle-background/particle-background.component.ts
index e68cc09..0e2a15d 100644
--- a/src/app/shared/components/particle-background/particle-background.component.ts
+++ b/src/app/shared/components/particle-background/particle-background.component.ts
@@ -7,6 +7,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
import * as THREE from 'three';
@Component({
@@ -31,6 +32,8 @@ import * as THREE from 'three';
],
})
export class ParticleBackgroundComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
private scene!: THREE.Scene;
@@ -61,6 +64,9 @@ export class ParticleBackgroundComponent implements OnInit, AfterViewInit, OnDes
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.initThree();
this.createParticles();
this.animate();
diff --git a/src/app/shared/components/space-invaders/space-invaders.component.ts b/src/app/shared/components/space-invaders/space-invaders.component.ts
index ef196e5..294af18 100644
--- a/src/app/shared/components/space-invaders/space-invaders.component.ts
+++ b/src/app/shared/components/space-invaders/space-invaders.component.ts
@@ -7,6 +7,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
interface Bullet {
x: number;
@@ -68,6 +69,8 @@ interface Enemy {
],
})
export class SpaceInvadersComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
private ctx!: CanvasRenderingContext2D;
@@ -88,6 +91,9 @@ export class SpaceInvadersComponent implements OnInit, AfterViewInit, OnDestroy
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
const canvas = this.canvasRef.nativeElement;
this.ctx = canvas.getContext('2d')!;
this.resizeCanvas();
diff --git a/src/app/shared/components/three-geometric/three-geometric.component.ts b/src/app/shared/components/three-geometric/three-geometric.component.ts
index 5f45869..c4d10e4 100644
--- a/src/app/shared/components/three-geometric/three-geometric.component.ts
+++ b/src/app/shared/components/three-geometric/three-geometric.component.ts
@@ -8,6 +8,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
import * as THREE from 'three';
@Component({
@@ -28,6 +29,8 @@ import * as THREE from 'three';
],
})
export class ThreeGeometricComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
@Input() type: 'floating' | 'rings' | 'particles' = 'floating';
@Input() color: string = '#3b82f6';
@@ -41,6 +44,9 @@ export class ThreeGeometricComponent implements OnInit, AfterViewInit, OnDestroy
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.initThree();
this.createGeometry();
this.animate();
diff --git a/src/app/shared/components/three-trophy/three-trophy.component.ts b/src/app/shared/components/three-trophy/three-trophy.component.ts
index 51d1ff3..b87ac82 100644
--- a/src/app/shared/components/three-trophy/three-trophy.component.ts
+++ b/src/app/shared/components/three-trophy/three-trophy.component.ts
@@ -8,6 +8,7 @@ import {
AfterViewInit,
} from '@angular/core';
import { CommonModule } from '@angular/common';
+import { injectIsBrowser } from '../../utils/is-browser';
import * as THREE from 'three';
@Component({
@@ -35,6 +36,8 @@ import * as THREE from 'three';
],
})
export class ThreeTrophyComponent implements OnInit, AfterViewInit, OnDestroy {
+ private readonly isBrowser = injectIsBrowser();
+
@ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
@Input() progress: number | null = null;
@Input() size: number = 200;
@@ -50,6 +53,9 @@ export class ThreeTrophyComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit(): void {}
ngAfterViewInit(): void {
+ if (!this.isBrowser) {
+ return;
+ }
this.initThree();
this.createTrophy();
this.animate();
diff --git a/src/app/shared/utils/is-browser.ts b/src/app/shared/utils/is-browser.ts
new file mode 100644
index 0000000..e8d6290
--- /dev/null
+++ b/src/app/shared/utils/is-browser.ts
@@ -0,0 +1,7 @@
+import { inject, PLATFORM_ID } from '@angular/core';
+import { isPlatformBrowser } from '@angular/common';
+
+/** True when running in a real browser (not during SSR). */
+export function injectIsBrowser(): boolean {
+ return isPlatformBrowser(inject(PLATFORM_ID));
+}