feat(projects): add decisions and result to project dialog
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:
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]) =>
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Reference in new issue
Block a user