build(angular): upgrade to angular 22, add to-be-played project
Build, Test & Push Frontend / quality-check (push) Failing after 35s
Build, Test & Push Frontend / docker (push) Skipped

Move to Angular 22 and update all dependencies. TypeScript stays on 6.0
(7 is blocked by Angular 22). Drop the ip-address override, add
fast-uri and basic-ftp overrides for audit fixes.
Set ChangeDetectionStrategy.Eager on components, as the ng update
migration does, and turn off the new OnPush lint rule.
Add the To Be Played project to the projects page and the about page.
This commit is contained in:
lobo committed 2026-10-12 09:48:29 +02:00
1 parent 6121fa553a
commit 775845b801
39 files changed
+3424 -3675

No files matched your search

+2 -2
View File
@@ -2,10 +2,10 @@
## Project Overview ## Project Overview
This is the frontend of the Playground project, built with Angular 21 and Angular Material. It includes features like a light/dark theme toggle and multi-language support via ngx-translate. The application is a static Single Page Application (SPA) served by NGINX. This is the frontend of the Playground project, built with Angular 22 and Angular Material. It includes features like a light/dark theme toggle and multi-language support via ngx-translate. The application is a static Single Page Application (SPA) served by NGINX.
**Key Technologies:** **Key Technologies:**
* **Frontend Framework:** Angular 21 * **Frontend Framework:** Angular 22
* **UI Components & Theming:** Angular Material * **UI Components & Theming:** Angular Material
* **Internationalization:** ngx-translate * **Internationalization:** ngx-translate
* **Server:** NGINX (for serving the SPA) * **Server:** NGINX (for serving the SPA)
+2 -2
View File
@@ -2,10 +2,10 @@
## Project Overview ## Project Overview
This is the frontend of the Playground project, built with Angular 21 and Angular Material. It includes features like a light/dark theme toggle and multi-language support via ngx-translate. The application is a static Single Page Application (SPA) served by NGINX. This is the frontend of the Playground project, built with Angular 22 and Angular Material. It includes features like a light/dark theme toggle and multi-language support via ngx-translate. The application is a static Single Page Application (SPA) served by NGINX.
**Key Technologies:** **Key Technologies:**
* **Frontend Framework:** Angular 21 * **Frontend Framework:** Angular 22
* **UI Components & Theming:** Angular Material * **UI Components & Theming:** Angular Material
* **Internationalization:** ngx-translate * **Internationalization:** ngx-translate
* **Server:** NGINX (for serving the SPA) * **Server:** NGINX (for serving the SPA)
+3 -3
View File
@@ -1,7 +1,7 @@
# 🎨 Playground Frontend # 🎨 Playground Frontend
This is the **frontend** of the Playground project. This is the **frontend** of the Playground project.
Built with **Angular 21** and **Angular Material**, including a light/dark theme toggle and Built with **Angular 22** and **Angular Material**, including a light/dark theme toggle and
multi-language support (EN/DE) via **ngx-translate**. multi-language support (EN/DE) via **ngx-translate**.
The app is built as a static **Single Page Application (SPA)** served by **NGINX**, The app is built as a static **Single Page Application (SPA)** served by **NGINX**,
@@ -14,8 +14,8 @@ deployed at:
| Component | Purpose | | Component | Purpose |
| --------- | ------- | | --------- | ------- |
| Angular 21 | Frontend framework (standalone components, signals, zoneless change detection) | | Angular 22 | Frontend framework (standalone components, signals, zoneless change detection) |
| Angular Material 21 | UI components & theming | | Angular Material 22 | UI components & theming |
| Tailwind CSS 4 | Utility-first styling (CSS-first config, no `tailwind.config.js`) | | Tailwind CSS 4 | Utility-first styling (CSS-first config, no `tailwind.config.js`) |
| ngx-translate 18 | i18n / instant translation | | ngx-translate 18 | i18n / instant translation |
| Babylon.js 9 | WebGL/WebGPU rendering for the algorithm demos | | Babylon.js 9 | WebGL/WebGPU rendering for the algorithm demos |
+2
View File
@@ -18,6 +18,8 @@ module.exports = defineConfig([
"@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/prefer-for-of": "off", "@typescript-eslint/prefer-for-of": "off",
// ng update set ChangeDetectionStrategy.Eager everywhere; OnPush is a separate refactor.
"@angular-eslint/prefer-on-push-component-change-detection": "off",
"@angular-eslint/directive-selector": [ "@angular-eslint/directive-selector": [
"error", "error",
{ {
+3169 -3617
View File
File diff suppressed because it is too large. Load diff
+27 -26
View File
@@ -12,53 +12,54 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular-slider/ngx-slider": "^21.0.0", "@angular-slider/ngx-slider": "^22.0.0",
"@angular/animations": "~21.2.21", "@angular/animations": "~22.2.2",
"@angular/cdk": "~21.2.6", "@angular/cdk": "~22.2.2",
"@angular/common": "~21.2.21", "@angular/common": "~22.2.2",
"@angular/compiler": "~21.2.21", "@angular/compiler": "~22.2.2",
"@angular/core": "~21.2.21", "@angular/core": "~22.2.2",
"@angular/forms": "~21.2.21", "@angular/forms": "~22.2.2",
"@angular/material": "~21.2.6", "@angular/material": "~22.2.2",
"@angular/platform-browser": "~21.2.21", "@angular/platform-browser": "~22.2.2",
"@angular/router": "~21.2.21", "@angular/router": "~22.2.2",
"@angular/service-worker": "~21.2.21", "@angular/service-worker": "~22.2.2",
"@babylonjs/core": "^9.22.0", "@babylonjs/core": "^9.30.0",
"@ngx-translate/core": "^18.0.0", "@ngx-translate/core": "^18.0.0",
"@ngx-translate/http-loader": "^18.0.0", "@ngx-translate/http-loader": "^18.0.0",
"rxjs": "~7.8.2", "rxjs": "~7.8.2",
"swiper": "^14.1.0", "swiper": "^14.3.0",
"tslib": "~2.8.1" "tslib": "~2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "~21.2.21", "@angular/build": "~22.2.2",
"@angular/cli": "~21.2.7", "@angular/cli": "~22.2.2",
"@angular/compiler-cli": "~21.2.21", "@angular/compiler-cli": "~22.2.2",
"@eslint/js": "~10.0.1", "@eslint/js": "~10.0.1",
"@lhci/cli": "^0.15.1", "@lhci/cli": "^0.15.1",
"@puppeteer/browsers": "3.2.1", "@puppeteer/browsers": "3.2.4",
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
"@types/jasmine": "~6.0.0", "@types/jasmine": "~7.0.0",
"@webgpu/types": "^0.1.72", "@webgpu/types": "^0.1.74",
"angular-eslint": "21.3.1", "angular-eslint": "22.6.0",
"eslint": "^10.0.3", "eslint": "^10.12.0",
"jasmine-core": "^7.0.2", "jasmine-core": "^7.0.2",
"karma": "^6.4.4", "karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0", "karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1", "karma-coverage": "^2.2.1",
"karma-jasmine": "^5.1.0", "karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.2.0", "karma-jasmine-html-reporter": "^2.2.0",
"puppeteer": "^25.1.0", "puppeteer": "^25.13.0",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"typescript": "~5.9.3", "typescript": "~6.0.3",
"typescript-eslint": "^8.67.0" "typescript-eslint": "^8.71.1"
}, },
"overrides": { "overrides": {
"tmp": "^0.2.3", "tmp": "^0.2.3",
"ip-address": "^10.1.1",
"@lhci/cli": { "@lhci/cli": {
"lighthouse": "^13.4.1", "lighthouse": "^13.4.1",
"uuid": "^11.1.1" "uuid": "^11.1.1"
} },
"fast-uri": "^3.1.8",
"basic-ftp": "^6.2.3"
} }
} }
+2 -2
View File
@@ -3,7 +3,7 @@ import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import {provideAnimations} from '@angular/platform-browser/animations'; import {provideAnimations} from '@angular/platform-browser/animations';
import {provideHttpClient} from '@angular/common/http'; import {provideHttpClient, withXhr} from '@angular/common/http';
import {provideServiceWorker} from '@angular/service-worker'; import {provideServiceWorker} from '@angular/service-worker';
import {provideTranslateService} from '@ngx-translate/core'; import {provideTranslateService} from '@ngx-translate/core';
import {provideTranslateHttpLoader} from '@ngx-translate/http-loader'; import {provideTranslateHttpLoader} from '@ngx-translate/http-loader';
@@ -25,7 +25,7 @@ export const appConfig: ApplicationConfig = {
anchorScrolling: 'enabled' anchorScrolling: 'enabled'
})), })),
provideAnimations(), provideAnimations(),
provideHttpClient(), provideHttpClient(withXhr()),
provideTranslateService({ provideTranslateService({
lang: INITIAL_LANG, lang: INITIAL_LANG,
fallbackLang: 'en', fallbackLang: 'en',
+7
View File
@@ -51,4 +51,11 @@ export class AssetsConstants {
'/assets/projects/tribble-the-homeserver/3.png', '/assets/projects/tribble-the-homeserver/3.png',
'/assets/projects/tribble-the-homeserver/4.png' '/assets/projects/tribble-the-homeserver/4.png'
]; ];
static readonly TBP_IMAGES = [
'/assets/projects/to-be-played/1.png',
'/assets/projects/to-be-played/2.png',
'/assets/projects/to-be-played/3.png',
'/assets/projects/to-be-played/4.png'
];
} }
+2 -1
View File
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core'; import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router'; import { RouterLink, RouterOutlet } from '@angular/router';
import {TopbarComponent} from '../topbar/topbar.component'; import {TopbarComponent} from '../topbar/topbar.component';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
@@ -10,6 +10,7 @@ import {SeoService} from '../../service/seo.service';
selector: 'app-root', selector: 'app-root',
imports: [RouterLink, RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent], imports: [RouterLink, RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
templateUrl: './app.component.html', templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })
export class AppComponent { export class AppComponent {
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core'; import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -19,6 +19,7 @@ import { MatIconModule } from '@angular/material/icon';
</div> </div>
</div> </div>
`, `,
changeDetection: ChangeDetectionStrategy.Eager,
styles: [` styles: [`
.dialog { .dialog {
display: flex; display: flex;
+2 -1
View File
@@ -1,4 +1,4 @@
import { Component, computed, inject } from '@angular/core'; import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router'; import { RouterLink, RouterLinkActive } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -20,6 +20,7 @@ import {RouterConstants} from '../../constants/RouterConstants';
TranslatePipe, MatDivider TranslatePipe, MatDivider
], ],
templateUrl: './topbar.component.html', templateUrl: './topbar.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './topbar.component.scss' styleUrl: './topbar.component.scss'
}) })
export class TopbarComponent { export class TopbarComponent {
@@ -151,6 +151,19 @@ describe('AboutComponent', () => {
}); });
}); });
describe('projects', () => {
it('lists To Be Played and links to its project dialog', () => {
expect(textOf('mat-card strong')).toContain('ABOUT.PROJECT.TBP.TITLE');
const hrefs = fixture.debugElement
.queryAll(By.css('a[href]'))
.map(anchor => anchor.nativeElement.getAttribute('href'));
expect(hrefs).toContain('/projects?project=to-be-played');
expect(hrefs).toContain('https://tbp.andreas-dahm.eu');
});
});
describe('education', () => { describe('education', () => {
it('shows only the post-school entries until the toggle button is clicked', () => { it('shows only the post-school entries until the toggle button is clicked', () => {
expect(textOf('[data-testid="education-where"]')).toContain('ABOUT.EDUCATION.E6.WHERE'); expect(textOf('[data-testid="education-where"]')).toContain('ABOUT.EDUCATION.E6.WHERE');
+8 -1
View File
@@ -1,4 +1,4 @@
import { Component, computed, signal } from '@angular/core'; import { Component, computed, signal, ChangeDetectionStrategy } from '@angular/core';
import { NgOptimizedImage } from '@angular/common'; import { NgOptimizedImage } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
@@ -37,6 +37,7 @@ interface SkillGroup {
RouterLink RouterLink
], ],
templateUrl: './about.component.html', templateUrl: './about.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './about.component.scss' styleUrl: './about.component.scss'
}) })
export class AboutComponent { export class AboutComponent {
@@ -112,6 +113,12 @@ export class AboutComponent {
]; ];
readonly projectKeys = [ readonly projectKeys = [
{
key: 'ABOUT.PROJECT.TBP',
externalLink: 'https://tbp.andreas-dahm.eu',
internalLink: 'projects',
identifier: 'to-be-played',
},
{ {
key: 'ABOUT.PROJECT.CLAUDE_BOX', key: 'ABOUT.PROJECT.CLAUDE_BOX',
externalLink: '', externalLink: '',
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core'; import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { AlgorithmsService } from './algorithms.service'; import { AlgorithmsService } from './algorithms.service';
import { AlgorithmCategory } from './algorithm-category'; import { AlgorithmCategory } from './algorithm-category';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
@@ -10,6 +10,7 @@ import {TranslatePipe} from '@ngx-translate/core';
selector: 'app-algorithms', selector: 'app-algorithms',
templateUrl: './algorithms.component.html', templateUrl: './algorithms.component.html',
styleUrl: './algorithms.component.scss', styleUrl: './algorithms.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe], imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe],
}) })
export class AlgorithmsComponent { export class AlgorithmsComponent {
@@ -1,4 +1,4 @@
import {Component} from '@angular/core'; import {Component, ChangeDetectionStrategy} from '@angular/core';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {MatSliderModule} from '@angular/material/slider'; import {MatSliderModule} from '@angular/material/slider';
@@ -28,6 +28,7 @@ import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
Information Information
], ],
templateUrl: './cloth.component.html', templateUrl: './cloth.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './cloth.component.scss', styleUrl: './cloth.component.scss',
}) })
export class ClothComponent { export class ClothComponent {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, signal, ViewChild} from '@angular/core'; import {AfterViewInit, Component, signal, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card"; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
import {TranslatePipe} from "@ngx-translate/core"; import {TranslatePipe} from "@ngx-translate/core";
import {UrlConstants} from '../../../constants/UrlConstants'; import {UrlConstants} from '../../../constants/UrlConstants';
@@ -29,6 +29,7 @@ import {GenericGridComponent, GridPos} from '../../../shared/components/generic-
FormsModule, FormsModule,
GenericGridComponent GenericGridComponent
], ],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './conway-gol.component.html', templateUrl: './conway-gol.component.html',
}) })
export class ConwayGolComponent implements AfterViewInit { export class ConwayGolComponent implements AfterViewInit {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, inject, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
@@ -29,6 +29,7 @@ import {UrlConstants} from '../../../constants/UrlConstants';
Information Information
], ],
templateUrl: './four-color.component.html', templateUrl: './four-color.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './four-color.component.scss' styleUrl: './four-color.component.scss'
}) })
export class FourColorComponent implements AfterViewInit { export class FourColorComponent implements AfterViewInit {
@@ -1,4 +1,4 @@
import {Component, OnInit, signal} from '@angular/core'; import {Component, OnInit, signal, ChangeDetectionStrategy} from '@angular/core';
import {Information} from '../information/information'; import {Information} from '../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
@@ -35,6 +35,7 @@ import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
NgxSliderModule NgxSliderModule
], ],
templateUrl: './fractal.component.html', templateUrl: './fractal.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './fractal.component.scss', styleUrl: './fractal.component.scss',
}) })
export class FractalComponent implements OnInit { export class FractalComponent implements OnInit {
@@ -1,4 +1,4 @@
import {Component} from '@angular/core'; import {Component, ChangeDetectionStrategy} from '@angular/core';
import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core'; import {ArcRotateCamera, Camera, ShaderMaterial} from '@babylonjs/core';
import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader'; import {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader';
import {Information} from '../information/information'; import {Information} from '../information/information';
@@ -22,6 +22,7 @@ import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/compo
BabylonCanvas BabylonCanvas
], ],
templateUrl: './fractal3d.component.html', templateUrl: './fractal3d.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './fractal3d.component.scss', styleUrl: './fractal3d.component.scss',
}) })
export class Fractal3dComponent { export class Fractal3dComponent {
@@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core'; import {Component, Input, ChangeDetectionStrategy} from '@angular/core';
import {TranslatePipe} from "@ngx-translate/core"; import {TranslatePipe} from "@ngx-translate/core";
import {AlgorithmInformation} from './information.models'; import {AlgorithmInformation} from './information.models';
@@ -8,6 +8,7 @@ import {AlgorithmInformation} from './information.models';
TranslatePipe TranslatePipe
], ],
templateUrl: './information.html', templateUrl: './information.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './information.scss', styleUrl: './information.scss',
}) })
export class Information { export class Information {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core'; import {AfterViewInit, Component, inject, signal, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {Information} from '../../information/information'; import {Information} from '../../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
@@ -25,6 +25,7 @@ import {PathfindingService} from '../service/pathfinding.service';
DecimalPipe DecimalPipe
], ],
templateUrl: './labyrinth.component.html', templateUrl: './labyrinth.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './labyrinth.component.scss', styleUrl: './labyrinth.component.scss',
}) })
export class LabyrinthComponent implements AfterViewInit { export class LabyrinthComponent implements AfterViewInit {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, inject, ViewChild} from '@angular/core'; import {AfterViewInit, Component, inject, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
@@ -43,6 +43,7 @@ enum NodeType {
Information, Information,
GenericGridComponent GenericGridComponent
], ],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './pathfinding.component.html', templateUrl: './pathfinding.component.html',
}) })
export class PathfindingComponent implements AfterViewInit { export class PathfindingComponent implements AfterViewInit {
@@ -1,4 +1,4 @@
import {Component} from '@angular/core'; import {Component, ChangeDetectionStrategy} from '@angular/core';
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component'; import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
@@ -29,6 +29,7 @@ import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
Information, Information,
], ],
templateUrl: './pendulum.component.html', templateUrl: './pendulum.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './pendulum.component.scss', styleUrl: './pendulum.component.scss',
}) })
class PendulumComponent { class PendulumComponent {
@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {ChangeDetectorRef, Component, inject, OnInit, ChangeDetectionStrategy} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {MatCardModule} from "@angular/material/card"; import {MatCardModule} from "@angular/material/card";
import {MatFormFieldModule} from "@angular/material/form-field"; import {MatFormFieldModule} from "@angular/material/form-field";
@@ -18,6 +18,7 @@ import {Information} from '../information/information';
selector: 'app-sorting', selector: 'app-sorting',
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslatePipe, FormsModule, MatInput, Information], imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslatePipe, FormsModule, MatInput, Information],
templateUrl: './sorting.component.html', templateUrl: './sorting.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './sorting.component.scss' styleUrl: './sorting.component.scss'
}) })
export class SortingComponent implements OnInit { export class SortingComponent implements OnInit {
+2 -1
View File
@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import {MatCard} from '@angular/material/card'; import {MatCard} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core'; import {TranslatePipe} from '@ngx-translate/core';
import {SharedFunctions} from '../../shared/SharedFunctions'; import {SharedFunctions} from '../../shared/SharedFunctions';
@@ -10,6 +10,7 @@ import {SharedFunctions} from '../../shared/SharedFunctions';
TranslatePipe TranslatePipe
], ],
templateUrl: './imprint.component.html', templateUrl: './imprint.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './imprint.component.scss', styleUrl: './imprint.component.scss',
}) })
export class ImprintComponent { export class ImprintComponent {
@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { MatCard } from '@angular/material/card'; import { MatCard } from '@angular/material/card';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
@@ -8,6 +8,7 @@ import { RouterConstants } from '../../constants/RouterConstants';
@Component({ @Component({
selector: 'app-not-found', selector: 'app-not-found',
imports: [RouterLink, MatCard, MatButton, TranslatePipe], imports: [RouterLink, MatCard, MatButton, TranslatePipe],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './not-found.component.html', templateUrl: './not-found.component.html',
}) })
export class NotFoundComponent { export class NotFoundComponent {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, inject, CUSTOM_ELEMENTS_SCHEMA, ViewChild, ElementRef} from '@angular/core'; import {AfterViewInit, Component, inject, CUSTOM_ELEMENTS_SCHEMA, ViewChild, ElementRef, ChangeDetectionStrategy} from '@angular/core';
import { import {
MAT_DIALOG_DATA, MAT_DIALOG_DATA,
MatDialogActions, MatDialogActions,
@@ -27,6 +27,7 @@ import {MatButton} from '@angular/material/button';
MatButton, MatButton,
MatDialogClose MatDialogClose
], ],
changeDetection: ChangeDetectionStrategy.Eager,
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class ProjectDialogComponent implements AfterViewInit { export class ProjectDialogComponent implements AfterViewInit {
@@ -0,0 +1,63 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { By } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideTranslateService } from '@ngx-translate/core';
import { ProjectsComponent } from './projects.component';
import en from '../../../assets/i18n/en.json';
import de from '../../../assets/i18n/de.json';
/**
* No translations are loaded, so the translate pipe echoes the key back.
* Whether the keys exist in the language files is checked separately below.
*/
describe('ProjectsComponent', () => {
let fixture: ComponentFixture<ProjectsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProjectsComponent],
providers: [
provideZonelessChangeDetection(),
provideRouter([]),
provideTranslateService({ lang: 'en', fallbackLang: 'en' }),
],
}).compileComponents();
fixture = TestBed.createComponent(ProjectsComponent);
fixture.detectChanges();
});
it('renders the TBP card', () => {
const titles = fixture.debugElement
.queryAll(By.css('mat-card-title'))
.map(title => title.nativeElement.textContent.trim());
expect(titles).toContain('PROJECTS.TBP.TITLE');
});
it('every project translation key exists in en and de', () => {
const keys = fixture.componentInstance.allProjects.flatMap(project => [
project.title,
project.shortDescription,
project.introduction,
...project.bulletPoints,
...project.challenges,
...project.learnings,
]);
const missing = Object.entries({ en, de }).flatMap(([language, translations]) =>
keys
.filter(key => typeof resolve(translations, key) !== 'string')
.map(key => `${language}: ${key}`)
);
expect(missing).toEqual([]);
});
function resolve(translations: unknown, key: string): unknown {
return key
.split('.')
.reduce<unknown>((node, part) => (node as Record<string, unknown> | undefined)?.[part], translations);
}
});
+34 -1
View File
@@ -1,4 +1,4 @@
import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { MatCardModule } from "@angular/material/card"; import { MatCardModule } from "@angular/material/card";
@@ -43,6 +43,7 @@ export interface Projects {
], ],
templateUrl: './projects.component.html', templateUrl: './projects.component.html',
styleUrl: './projects.component.scss', styleUrl: './projects.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })
export class ProjectsComponent implements OnInit, OnDestroy { export class ProjectsComponent implements OnInit, OnDestroy {
@@ -53,6 +54,38 @@ export class ProjectsComponent implements OnInit, OnDestroy {
private queryParamSub: Subscription | undefined; private queryParamSub: Subscription | undefined;
allProjects: Projects[] = [ allProjects: Projects[] = [
{
identifier: "to-be-played",
title: 'PROJECTS.TBP.TITLE',
shortDescription: 'PROJECTS.TBP.SHORT_DESCRIPTION',
introduction: 'PROJECTS.TBP.INTRODUCTION',
images: AssetsConstants.TBP_IMAGES.map(url => ({ url, source: '' })),
icon: 'playlist_add_check',
assets: '',
links: [
{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://tbp.andreas-dahm.eu' },
{ name: 'ToBePlayed-Frontend', url: 'https://git.andreas-dahm.eu/Lobo_Corp/ToBePlayed-Frontend' },
{ name: 'ToBePlayed-Core', url: 'https://git.andreas-dahm.eu/Lobo_Corp/ToBePlayed-Core' },
{ name: 'ToBePlayed-Auth', url: 'https://git.andreas-dahm.eu/Lobo_Corp/ToBePlayed-Auth' }
],
bulletPoints: [
'PROJECTS.TBP.BULLET_1',
'PROJECTS.TBP.BULLET_2',
'PROJECTS.TBP.BULLET_3',
'PROJECTS.TBP.BULLET_4',
],
challenges: [
'PROJECTS.TBP.CHALLENGE_1',
'PROJECTS.TBP.CHALLENGE_2',
'PROJECTS.TBP.CHALLENGE_3',
],
learnings: [
'PROJECTS.TBP.LEARNING_1',
'PROJECTS.TBP.LEARNING_2',
],
isFeatured: false,
technologies: ['Java', 'Spring Boot', 'PostgreSQL', 'React', 'Tailwind CSS', 'Docker']
},
{ {
identifier: "claude-in-a-box", identifier: "claude-in-a-box",
title: 'PROJECTS.CLAUDE_BOX.TITLE', title: 'PROJECTS.CLAUDE_BOX.TITLE',
@@ -1,4 +1,4 @@
import {Component, computed, effect, inject, OnDestroy, signal, WritableSignal} from '@angular/core'; import {Component, computed, effect, inject, OnDestroy, signal, WritableSignal, ChangeDetectionStrategy} from '@angular/core';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card'; import {MatCardModule} from '@angular/material/card';
@@ -30,6 +30,7 @@ const SECOND_INTERVAL_FREQUENCY_HZ = 587;
MatCheckboxModule, MatCheckboxModule,
TranslatePipe TranslatePipe
], ],
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './stopwatch.component.html' templateUrl: './stopwatch.component.html'
}) })
export class StopwatchComponent implements OnDestroy { export class StopwatchComponent implements OnDestroy {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
export interface GridPos { row: number; col: number } export interface GridPos { row: number; col: number }
@@ -7,6 +7,7 @@ export interface GridPos { row: number; col: number }
selector: 'app-generic-grid', selector: 'app-generic-grid',
imports: [CommonModule], imports: [CommonModule],
templateUrl: './generic-grid.html', templateUrl: './generic-grid.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './generic-grid.scss', styleUrl: './generic-grid.scss',
}) })
export class GenericGridComponent implements AfterViewInit { export class GenericGridComponent implements AfterViewInit {
@@ -1,10 +1,11 @@
import {AfterViewInit, Component, ElementRef, HostListener, inject, NgZone, OnDestroy, PLATFORM_ID, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, HostListener, inject, NgZone, OnDestroy, PLATFORM_ID, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {isPlatformBrowser} from '@angular/common'; import {isPlatformBrowser} from '@angular/common';
@Component({ @Component({
selector: 'app-particles-background', selector: 'app-particles-background',
imports: [], imports: [],
templateUrl: './particles-background.component.html', templateUrl: './particles-background.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './particles-background.component.scss', styleUrl: './particles-background.component.scss',
}) })
export class ParticleBackgroundComponent implements AfterViewInit, OnDestroy { export class ParticleBackgroundComponent implements AfterViewInit, OnDestroy {
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar'; import {MatSnackBar} from '@angular/material/snack-bar';
import {TranslateService} from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core'; import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
@@ -26,6 +26,7 @@ export interface SceneEventData {
selector: 'app-babylon-canvas', selector: 'app-babylon-canvas',
imports: [], imports: [],
templateUrl: './babylon-canvas.component.html', templateUrl: './babylon-canvas.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './babylon-canvas.component.scss', styleUrl: './babylon-canvas.component.scss',
}) })
export class BabylonCanvas implements AfterViewInit, OnDestroy { export class BabylonCanvas implements AfterViewInit, OnDestroy {
+25
View File
@@ -185,6 +185,17 @@
} }
}, },
"PROJECT": { "PROJECT": {
"TBP": {
"TITLE": "To Be Played",
"DESCRIPTION": "Ein Steam-Backlog-Tracker, der meine Bibliothek importiert, Status, Bewertung und Notizen verwaltet und passend zur verfügbaren Zeit vorschlägt, was ich als Nächstes spiele. Drei selbst gehostete Dienste: Auth, Core und ein React-Frontend.",
"LINK_EXTERNAL": "tbp.andreas-dahm.eu",
"LINK_INTERNAL": "Projektdetails",
"HIGHLIGHTS": {
"P1": "Steam-Sync von Spielen, Spielzeit, Erfolgen und Wunschliste, dazu die Spieldauer (Time to Beat) aus IGDB.",
"P2": "Vorschläge für „Was spiele ich als Nächstes?“, gefiltert nach Zeitbudget und Stimmung.",
"P3": "Der Auth-Service signiert die Tokens mit Ed25519, Core kennt nur den öffentlichen Schlüssel."
}
},
"CLAUDE_BOX": { "CLAUDE_BOX": {
"TITLE": "Claude-in-a-Box", "TITLE": "Claude-in-a-Box",
"DESCRIPTION": "Eine Entwicklungsumgebung im Container, in der ein KI-Coding-Agent nach einem festen Regelsatz arbeitet und nicht nach dem, was ich gerade eintippe. Er zieht sich die Regeln für den Stack, den er im Repository vorfindet, und ein Git-Hook stoppt den Commit, wenn Build oder Tests rot sind.", "DESCRIPTION": "Eine Entwicklungsumgebung im Container, in der ein KI-Coding-Agent nach einem festen Regelsatz arbeitet und nicht nach dem, was ich gerade eintippe. Er zieht sich die Regeln für den Stack, den er im Repository vorfindet, und ein Git-Hook stoppt den Commit, wenn Build oder Tests rot sind.",
@@ -375,6 +386,20 @@
"CHALLENGE_3": "Die Toolchains in einem Container ohne Root zum Laufen zu bringen. Builds, Browser und Sprach-Runtimes mussten alle in Volumes landen, die dem Entwickler-User gehören.", "CHALLENGE_3": "Die Toolchains in einem Container ohne Root zum Laufen zu bringen. Builds, Browser und Sprach-Runtimes mussten alle in Volumes landen, die dem Entwickler-User gehören.",
"LEARNING_1": "Die Formulierung der Prompts war deutlich weniger wichtig, als ich dachte. Fast der ganze Unterschied kam vom Drumherum.", "LEARNING_1": "Die Formulierung der Prompts war deutlich weniger wichtig, als ich dachte. Fast der ganze Unterschied kam vom Drumherum.",
"LEARNING_2": "Den Output des Agenten genauso zu reviewen und durch dieselben Gates zu schicken wie den Pull Request eines neuen Teammitglieds hat sich als die nützlichste Regel überhaupt herausgestellt." "LEARNING_2": "Den Output des Agenten genauso zu reviewen und durch dieselben Gates zu schicken wie den Pull Request eines neuen Teammitglieds hat sich als die nützlichste Regel überhaupt herausgestellt."
},
"TBP": {
"TITLE": "To Be Played",
"SHORT_DESCRIPTION": "Ein Steam-Backlog-Tracker, der bei der Entscheidung hilft, was als Nächstes gespielt wird.",
"INTRODUCTION": "Meine Steam-Bibliothek hat weit mehr Spiele, als ich je durchspielen werde. To Be Played importiert die Bibliothek, verwaltet den Backlog mit Status, Bewertung und Notizen und schlägt passend zur verfügbaren Zeit vor, was ich als Nächstes spielen kann. Dahinter stehen drei Dienste: ein Auth-Service, der die Login-Tokens signiert, ein Core-Service für Bibliothek und Statistiken und ein React-Frontend. Alles läuft selbst gehostet auf demselben Server wie diese Seite.",
"BULLET_1": "Steam-Sync von Spielen, Spielzeit, Erfolgen und Wunschliste, von Hand und jede Nacht, dazu die Spieldauer (Time to Beat) aus IGDB.",
"BULLET_2": "„Was spiele ich als Nächstes?“: sieben Reihen mit Vorschlägen, etwa der nächste Teil einer Reihe, dasselbe Studio oder Spiele, die Leute durchgespielt haben, denen man folgt, gefiltert nach Zeitbudget und Stimmung.",
"BULLET_3": "Öffentliche Profile, anderen Spielern folgen mit einer Inbox zu ihrem Fortschritt und Pile Points für jedes Spiel, das vom Stapel kommt.",
"BULLET_4": "Deutsch und Englisch, Light- und Dark-Mode, auch auf dem Handy nutzbar. Auth, Core und Frontend werden jeweils als eigenes Docker-Image ausgeliefert.",
"CHALLENGE_1": "Login über zwei Dienste. Auth signiert die Tokens mit einem Ed25519-Schlüssel, Core kennt nur den öffentlichen Schlüssel und kann einen Login also prüfen, aber nie selbst ausstellen. Die Tokens liegen in HttpOnly-Cookies, kein Skript auf der Seite kann sie lesen.",
"CHALLENGE_2": "Die Limits von Steam. Jeder Steam-Aufruf ist rate-limitiert und wird bei Bedarf wiederholt, und ein Sync reserviert seinen Lauf zuerst in der Datenbank, damit nie zwei Syncs desselben Users gleichzeitig laufen.",
"CHALLENGE_3": "Ein Spiel vorzuschlagen ist schwerer, als es klingt. Jede Vorschlagsreihe braucht eigene Regeln, und die Listen hinter jeder Zahl müssen dieselben Regeln nutzen wie die Zahl selbst, damit ein Spiel überall gleich zählt.",
"LEARNING_1": "Die Aufteilung in Dienste lohnt sich nur mit einem klaren Vertrag. Die Token-Claims stehen in einer Vertragsdatei, gegen die beide Dienste testen, und ein Skript prüft, dass beide Kopien gleich sind.",
"LEARNING_2": "Auch ein Hobbyprojekt braucht die langweiligen Teile: Backups, Migrationen und eine feste Deploy-Reihenfolge. Sie haben mehr darüber entschieden, wie stabil es läuft, als jedes Feature."
} }
}, },
"IMPRINT": { "IMPRINT": {
+25
View File
@@ -185,6 +185,17 @@
} }
}, },
"PROJECT": { "PROJECT": {
"TBP": {
"TITLE": "To Be Played",
"DESCRIPTION": "A Steam backlog tracker that imports my library, keeps status, rating and notes, and suggests what to play next for the time I have. Three self-hosted services: auth, core and a React frontend.",
"LINK_EXTERNAL": "tbp.andreas-dahm.eu",
"LINK_INTERNAL": "Project details",
"HIGHLIGHTS": {
"P1": "Steam sync of games, playtime, achievements and wishlist, plus time to beat from IGDB.",
"P2": "\"What to play next\" suggestions filtered by time budget and mood.",
"P3": "Auth service signs the tokens with Ed25519, Core only holds the public key."
}
},
"CLAUDE_BOX": { "CLAUDE_BOX": {
"TITLE": "Claude-in-a-Box", "TITLE": "Claude-in-a-Box",
"DESCRIPTION": "A development setup in a container where an AI coding agent works under a fixed set of rules rather than whatever I happen to type that day. It picks up the rules for whatever stack it finds in the repository, and a git hook stops the commit if the build or the tests are red.", "DESCRIPTION": "A development setup in a container where an AI coding agent works under a fixed set of rules rather than whatever I happen to type that day. It picks up the rules for whatever stack it finds in the repository, and a git hook stops the commit if the build or the tests are red.",
@@ -375,6 +386,20 @@
"CHALLENGE_3": "Getting the toolchains installed in a container that has no root. Builds, browsers and language runtimes all had to go into volumes owned by the development user.", "CHALLENGE_3": "Getting the toolchains installed in a container that has no root. Builds, browsers and language runtimes all had to go into volumes owned by the development user.",
"LEARNING_1": "Prompt wording mattered far less than I expected. Almost all of the difference came from the scaffolding.", "LEARNING_1": "Prompt wording mattered far less than I expected. Almost all of the difference came from the scaffolding.",
"LEARNING_2": "Reviewing and gating the agent's output the same way you would a new colleague's pull request turned out to be the most useful rule of the lot." "LEARNING_2": "Reviewing and gating the agent's output the same way you would a new colleague's pull request turned out to be the most useful rule of the lot."
},
"TBP": {
"TITLE": "To Be Played",
"SHORT_DESCRIPTION": "A Steam backlog tracker that helps decide what to play next.",
"INTRODUCTION": "My Steam library has far more games than I will ever finish. To Be Played imports the library, keeps the backlog with status, rating and notes, and suggests what to play next for the time I have. Behind it are three services: an auth service that signs the login tokens, a core service for the library and the statistics, and a React frontend. All of it is self-hosted on the same server as this site.",
"BULLET_1": "Steam sync of owned games, playtime, achievements and wishlist, by hand and every night, plus time to beat from IGDB.",
"BULLET_2": "\"What to play next\": seven rows of suggestions, such as the next part of a series, the same studio, or games that people you follow finished, filtered by a time budget and a mood.",
"BULLET_3": "Public profiles, following other players with an inbox of their progress, and Pile Points for every game played off the pile.",
"BULLET_4": "German and English, light and dark mode, and it works on the phone. Auth, Core and Frontend each ship as their own Docker image.",
"CHALLENGE_1": "Login across two services. Auth signs the tokens with an Ed25519 key, and Core only holds the public key, so Core can check a login but never create one. The tokens live in HttpOnly cookies, so no script on the page can read them.",
"CHALLENGE_2": "Steam's limits. Every Steam call is rate limited and retried, and a sync first claims its run in the database, so two syncs of the same user never run at the same time.",
"CHALLENGE_3": "Suggesting a game is harder than it sounds. Each row of suggestions needs its own rules, and the lists behind every number must use the same rules as the number, so a game counts the same way everywhere.",
"LEARNING_1": "Splitting an app into services only pays off with a clear contract. The token claims are pinned in a contract file that both services test against, and a script checks that both copies match.",
"LEARNING_2": "A hobby project still needs the boring parts: backups, migrations and a fixed deploy order. They decided more about how stable it runs than any feature did."
} }
}, },
"IMPRINT": { "IMPRINT": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB