build(angular): upgrade to angular 22, add to-be-played project
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:
1 parent
6121fa553a
commit
775845b801
39 files changed
+3425
-3676
No files matched your search
@@ -3,7 +3,7 @@ import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
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 {provideTranslateService} from '@ngx-translate/core';
|
||||
import {provideTranslateHttpLoader} from '@ngx-translate/http-loader';
|
||||
@@ -25,7 +25,7 @@ export const appConfig: ApplicationConfig = {
|
||||
anchorScrolling: 'enabled'
|
||||
})),
|
||||
provideAnimations(),
|
||||
provideHttpClient(),
|
||||
provideHttpClient(withXhr()),
|
||||
provideTranslateService({
|
||||
lang: INITIAL_LANG,
|
||||
fallbackLang: 'en',
|
||||
|
||||
@@ -51,4 +51,11 @@ export class AssetsConstants {
|
||||
'/assets/projects/tribble-the-homeserver/3.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'
|
||||
];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { RouterLink, RouterOutlet } from '@angular/router';
|
||||
import {TopbarComponent} from '../topbar/topbar.component';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
@@ -10,6 +10,7 @@ import {SeoService} from '../../service/seo.service';
|
||||
selector: 'app-root',
|
||||
imports: [RouterLink, RouterOutlet, TopbarComponent, TranslatePipe, ParticleBackgroundComponent],
|
||||
templateUrl: './app.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
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 { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -19,6 +19,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styles: [`
|
||||
.dialog {
|
||||
display: flex;
|
||||
|
||||
@@ -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 { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -20,6 +20,7 @@ import {RouterConstants} from '../../constants/RouterConstants';
|
||||
TranslatePipe, MatDivider
|
||||
],
|
||||
templateUrl: './topbar.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './topbar.component.scss'
|
||||
})
|
||||
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', () => {
|
||||
it('shows only the post-school entries until the toggle button is clicked', () => {
|
||||
expect(textOf('[data-testid="education-where"]')).toContain('ABOUT.EDUCATION.E6.WHERE');
|
||||
|
||||
@@ -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 { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
@@ -37,6 +37,7 @@ interface SkillGroup {
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './about.component.scss'
|
||||
})
|
||||
export class AboutComponent {
|
||||
@@ -112,6 +113,12 @@ export class AboutComponent {
|
||||
];
|
||||
|
||||
readonly projectKeys = [
|
||||
{
|
||||
key: 'ABOUT.PROJECT.TBP',
|
||||
externalLink: 'https://tbp.andreas-dahm.eu',
|
||||
internalLink: 'projects',
|
||||
identifier: 'to-be-played',
|
||||
},
|
||||
{
|
||||
key: 'ABOUT.PROJECT.CLAUDE_BOX',
|
||||
externalLink: '',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { AlgorithmsService } from './algorithms.service';
|
||||
import { AlgorithmCategory } from './algorithm-category';
|
||||
import { RouterLink } from '@angular/router';
|
||||
@@ -10,6 +10,7 @@ import {TranslatePipe} from '@ngx-translate/core';
|
||||
selector: 'app-algorithms',
|
||||
templateUrl: './algorithms.component.html',
|
||||
styleUrl: './algorithms.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
imports: [RouterLink, MatCardModule, MatIconModule, TranslatePipe],
|
||||
})
|
||||
export class AlgorithmsComponent {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {Component, ChangeDetectionStrategy} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
@@ -28,6 +28,7 @@ import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
|
||||
Information
|
||||
],
|
||||
templateUrl: './cloth.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './cloth.component.scss',
|
||||
})
|
||||
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 {TranslatePipe} from "@ngx-translate/core";
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
@@ -29,6 +29,7 @@ import {GenericGridComponent, GridPos} from '../../../shared/components/generic-
|
||||
FormsModule,
|
||||
GenericGridComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './conway-gol.component.html',
|
||||
})
|
||||
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 {FormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
@@ -29,6 +29,7 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
Information
|
||||
],
|
||||
templateUrl: './four-color.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './four-color.component.scss'
|
||||
})
|
||||
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 {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
@@ -35,6 +35,7 @@ import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||
NgxSliderModule
|
||||
],
|
||||
templateUrl: './fractal.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './fractal.component.scss',
|
||||
})
|
||||
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 {MANDELBULB_FRAGMENT, MANDELBULB_VERTEX} from './fractal3d.shader';
|
||||
import {Information} from '../information/information';
|
||||
@@ -22,6 +22,7 @@ import {BabylonCanvas, RenderCallback, RenderConfig} from '../../../shared/compo
|
||||
BabylonCanvas
|
||||
],
|
||||
templateUrl: './fractal3d.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './fractal3d.component.scss',
|
||||
})
|
||||
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 {AlgorithmInformation} from './information.models';
|
||||
|
||||
@@ -8,6 +8,7 @@ import {AlgorithmInformation} from './information.models';
|
||||
TranslatePipe
|
||||
],
|
||||
templateUrl: './information.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './information.scss',
|
||||
})
|
||||
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 {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
@@ -25,6 +25,7 @@ import {PathfindingService} from '../service/pathfinding.service';
|
||||
DecimalPipe
|
||||
],
|
||||
templateUrl: './labyrinth.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './labyrinth.component.scss',
|
||||
})
|
||||
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 {FormsModule} from '@angular/forms';
|
||||
|
||||
@@ -43,6 +43,7 @@ enum NodeType {
|
||||
Information,
|
||||
GenericGridComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './pathfinding.component.html',
|
||||
})
|
||||
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 {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
|
||||
@@ -29,6 +29,7 @@ import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
|
||||
Information,
|
||||
],
|
||||
templateUrl: './pendulum.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './pendulum.component.scss',
|
||||
})
|
||||
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 {MatCardModule} from "@angular/material/card";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
@@ -18,6 +18,7 @@ import {Information} from '../information/information';
|
||||
selector: 'app-sorting',
|
||||
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslatePipe, FormsModule, MatInput, Information],
|
||||
templateUrl: './sorting.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './sorting.component.scss'
|
||||
})
|
||||
export class SortingComponent implements OnInit {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import {MatCard} from '@angular/material/card';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
import {SharedFunctions} from '../../shared/SharedFunctions';
|
||||
@@ -10,6 +10,7 @@ import {SharedFunctions} from '../../shared/SharedFunctions';
|
||||
TranslatePipe
|
||||
],
|
||||
templateUrl: './imprint.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './imprint.component.scss',
|
||||
})
|
||||
export class ImprintComponent {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatCard } from '@angular/material/card';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
@@ -8,6 +8,7 @@ import { RouterConstants } from '../../constants/RouterConstants';
|
||||
@Component({
|
||||
selector: 'app-not-found',
|
||||
imports: [RouterLink, MatCard, MatButton, TranslatePipe],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './not-found.component.html',
|
||||
})
|
||||
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 {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogActions,
|
||||
@@ -27,6 +27,7 @@ import {MatButton} from '@angular/material/button';
|
||||
MatButton,
|
||||
MatDialogClose
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -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 { Subscription } from "rxjs";
|
||||
import { MatCardModule } from "@angular/material/card";
|
||||
@@ -43,6 +43,7 @@ export interface Projects {
|
||||
],
|
||||
templateUrl: './projects.component.html',
|
||||
styleUrl: './projects.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
@@ -53,6 +54,38 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
private queryParamSub: Subscription | undefined;
|
||||
|
||||
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",
|
||||
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 {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
@@ -30,6 +30,7 @@ const SECOND_INTERVAL_FREQUENCY_HZ = 587;
|
||||
MatCheckboxModule,
|
||||
TranslatePipe
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
templateUrl: './stopwatch.component.html'
|
||||
})
|
||||
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';
|
||||
|
||||
export interface GridPos { row: number; col: number }
|
||||
@@ -7,6 +7,7 @@ export interface GridPos { row: number; col: number }
|
||||
selector: 'app-generic-grid',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './generic-grid.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './generic-grid.scss',
|
||||
})
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particles-background',
|
||||
imports: [],
|
||||
templateUrl: './particles-background.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './particles-background.component.scss',
|
||||
})
|
||||
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 {TranslateService} from '@ngx-translate/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',
|
||||
imports: [],
|
||||
templateUrl: './babylon-canvas.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './babylon-canvas.component.scss',
|
||||
})
|
||||
export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
|
||||
@@ -185,6 +185,17 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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.",
|
||||
@@ -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.",
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -185,6 +185,17 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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.",
|
||||
@@ -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.",
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
|
||||
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 |
Reference in new issue
Block a user