feat(projects): add decisions and result to project dialog
Build, Test & Push Frontend / quality-check (push) Successful in 2m18s
Build, Test & Push Frontend / docker (push) Successful in 50s

Add optional Decisions and Result blocks to the project dialog.
Fill them for ToBePlayed, Tribble, Claude-in-a-Box and Playground.
Remove Portainer and the Google Drive backup claims from Tribble.
This commit is contained in:
lobo committed 2026-10-21 10:39:30 +02:00
1 parent 1cff9e6eb2
commit 0857ce482b
6 files changed
+132 -21

No files matched your search

@@ -20,6 +20,20 @@
</div>
}
@if (project.decisions?.length) {
<div class="p-5 rounded-xl bg-black/3 border border-black/5 dark:bg-white/5 dark:border-white/10 mb-4">
<div class="flex items-center gap-3 mb-3 text-link">
<mat-icon class="text-[24px]! w-6! h-6!">alt_route</mat-icon>
<h3 class="m-0 text-sm uppercase tracking-wider font-semibold">{{ 'PROJECTS.SECTION.DECISIONS' | translate }}</h3>
</div>
<ul class="m-0 pl-5 text-[0.95rem] leading-relaxed opacity-85">
@for(decision of project.decisions; track decision) {
<li class="mb-1">{{ decision | translate }}</li>
}
</ul>
</div>
}
<div class="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] sm-dialog:grid-cols-1 gap-4 mb-4">
<div class="p-5 rounded-xl bg-black/3 border border-black/5 dark:bg-white/5 dark:border-white/10">
<div class="flex items-center gap-3 mb-3 text-link">
@@ -45,6 +59,16 @@
</ul>
</div>
</div>
@if (project.result) {
<div class="p-5 rounded-xl bg-black/3 border border-black/5 dark:bg-white/5 dark:border-white/10 mb-4">
<div class="flex items-center gap-3 mb-3 text-link">
<mat-icon class="text-[24px]! w-6! h-6!">flag</mat-icon>
<h3 class="m-0 text-sm uppercase tracking-wider font-semibold">{{ 'PROJECTS.SECTION.RESULT' | translate }}</h3>
</div>
<p class="m-0 text-[0.95rem] leading-relaxed opacity-85">{{ project.result | translate }}</p>
</div>
}
</div>
@if (project.images.length > 0)
@@ -69,4 +69,26 @@ describe('ProjectDialogComponent', () => {
expect(fixture.nativeElement.querySelector('img[alt="PROJECTS.DIAGRAM_ALT"]')).toBeNull();
});
it('renders decisions and result when the project has them', async () => {
const fixture = await createDialog({
title: 'T', technologies: [], images: [], decisions: ['D1', 'D2'], result: 'R',
});
const text: string = fixture.nativeElement.textContent;
for (const expected of ['PROJECTS.SECTION.DECISIONS', 'D1', 'D2', 'PROJECTS.SECTION.RESULT', 'R']) {
expect(text).toContain(expected);
}
const decisions = Array.from<HTMLElement>(fixture.nativeElement.querySelectorAll('h3'))
.find(h => h.textContent?.includes('PROJECTS.SECTION.DECISIONS'));
expect(decisions?.closest('div.p-5')?.querySelectorAll('li').length).toBe(2);
});
it('renders no decisions or result blocks without them', async () => {
const fixture = await createDialog({ title: 'T', technologies: [], images: [] });
const text: string = fixture.nativeElement.textContent;
expect(text).not.toContain('PROJECTS.SECTION.DECISIONS');
expect(text).not.toContain('PROJECTS.SECTION.RESULT');
});
});
@@ -52,6 +52,13 @@ describe('ProjectsComponent', () => {
...project.bulletPoints,
...project.challenges,
...project.learnings,
...(project.decisions ?? []),
...(project.result ? [project.result] : []),
]).concat([
'PROJECTS.SECTION.TECHNICAL',
'PROJECTS.SECTION.LEARNINGS',
'PROJECTS.SECTION.DECISIONS',
'PROJECTS.SECTION.RESULT',
]);
const missing = Object.entries({ en, de }).flatMap(([language, translations]) =>
+27 -3
View File
@@ -30,6 +30,8 @@ export interface Projects {
bulletPoints: string[],
challenges: string[],
learnings: string[],
decisions?: string[],
result?: string,
isFeatured: boolean,
technologies: string[]
}
@@ -88,6 +90,12 @@ export class ProjectsComponent implements OnInit, OnDestroy {
'PROJECTS.TBP.LEARNING_1',
'PROJECTS.TBP.LEARNING_2',
],
decisions: [
'PROJECTS.TBP.DECISION_1',
'PROJECTS.TBP.DECISION_2',
'PROJECTS.TBP.DECISION_3',
],
result: 'PROJECTS.TBP.RESULT',
isFeatured: true,
technologies: ['Java', 'Spring Boot', 'PostgreSQL', 'React', 'Tailwind CSS', 'Docker']
},
@@ -120,6 +128,12 @@ export class ProjectsComponent implements OnInit, OnDestroy {
'PROJECTS.CLAUDE_BOX.LEARNING_2',
'PROJECTS.CLAUDE_BOX.LEARNING_3',
],
decisions: [
'PROJECTS.CLAUDE_BOX.DECISION_1',
'PROJECTS.CLAUDE_BOX.DECISION_2',
'PROJECTS.CLAUDE_BOX.DECISION_3',
],
result: 'PROJECTS.CLAUDE_BOX.RESULT',
isFeatured: false,
technologies: ['Claude Code', 'MCP', 'Docker', 'Bash', 'Node.js', 'Playwright']
},
@@ -151,6 +165,12 @@ export class ProjectsComponent implements OnInit, OnDestroy {
'PROJECTS.PLAYGROUND.LEARNING_2',
'PROJECTS.PLAYGROUND.LEARNING_3',
],
decisions: [
'PROJECTS.PLAYGROUND.DECISION_1',
'PROJECTS.PLAYGROUND.DECISION_2',
'PROJECTS.PLAYGROUND.DECISION_3',
],
result: 'PROJECTS.PLAYGROUND.RESULT',
isFeatured: false,
technologies: ['Angular', 'TypeScript', 'Tailwind CSS', 'WebGPU', 'GLSL', 'Babylon.js', 'Gitea Actions', 'Docker', 'Traefik', 'nginx', 'Tailscale']
},
@@ -256,7 +276,6 @@ export class ProjectsComponent implements OnInit, OnDestroy {
links: [
{ name: 'Ubuntu Server', url: 'https://ubuntu.com/server' },
{ name: 'Docker', url: 'https://www.docker.com/' },
{ name: 'Portainer', url: 'https://www.portainer.io/' },
{ name: 'Traefik', url: 'https://traefik.io/' },
{ name: 'Gitea', url: 'https://gitea.io/' },
{ name: 'Jellyfin', url: 'https://jellyfin.org/' },
@@ -270,7 +289,6 @@ export class ProjectsComponent implements OnInit, OnDestroy {
'PROJECTS.TRIBBLE.BULLET_2',
'PROJECTS.TRIBBLE.BULLET_3',
'PROJECTS.TRIBBLE.BULLET_4',
'PROJECTS.TRIBBLE.BULLET_5',
],
challenges: [
'PROJECTS.TRIBBLE.CHALLENGE_1',
@@ -280,8 +298,14 @@ export class ProjectsComponent implements OnInit, OnDestroy {
'PROJECTS.TRIBBLE.LEARNING_1',
'PROJECTS.TRIBBLE.LEARNING_2',
],
decisions: [
'PROJECTS.TRIBBLE.DECISION_1',
'PROJECTS.TRIBBLE.DECISION_2',
'PROJECTS.TRIBBLE.DECISION_3',
],
result: 'PROJECTS.TRIBBLE.RESULT',
isFeatured: false,
technologies: ['Ubuntu Server', 'Docker', 'Portainer', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale', 'Watchtower']
technologies: ['Ubuntu Server', 'Docker', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale', 'Watchtower']
}
]