feat(projects): add architecture diagrams, align project texts
Add diagrams to three projects. A tap opens them full size. Align en/de texts: no versions, Gitea not GitHub, Portainer, home server + VPS. Make To Be Played the featured project. Remove the Hostinger logo image.
This commit is contained in:
1 parent
449e2d430a
commit
93e9c59436
11 files changed
+247
-50
No files matched your search
@@ -41,12 +41,11 @@ export class AssetsConstants {
|
||||
'/assets/projects/diploma/6.webp'
|
||||
];
|
||||
|
||||
static readonly CLAUDE_BOX_IMAGES = [
|
||||
'/assets/projects/claude-in-a-box/1.svg'
|
||||
];
|
||||
static readonly CLAUDE_BOX_DIAGRAM = '/assets/projects/claude-in-a-box/1.svg';
|
||||
static readonly TBP_DIAGRAM = '/assets/projects/to-be-played/architecture.svg';
|
||||
static readonly TRIBBLE_DIAGRAM = '/assets/projects/tribble-the-homeserver/architecture.svg';
|
||||
|
||||
static readonly TRIBBLE_IMAGES = [
|
||||
'/assets/projects/tribble-the-homeserver/1.webp',
|
||||
'/assets/projects/tribble-the-homeserver/2.webp',
|
||||
'/assets/projects/tribble-the-homeserver/3.webp',
|
||||
'/assets/projects/tribble-the-homeserver/4.webp'
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@if (project.diagram) {
|
||||
<div class="mb-4 rounded-xl overflow-hidden bg-[#1e2530]">
|
||||
<a [href]="project.diagram" target="_blank" rel="noopener" [attr.aria-label]="'PROJECTS.DIAGRAM_ALT' | translate">
|
||||
<img class="w-full h-auto block" [src]="project.diagram" [alt]="'PROJECTS.DIAGRAM_ALT' | translate">
|
||||
</a>
|
||||
</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">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { provideTranslateService } from '@ngx-translate/core';
|
||||
@@ -27,27 +27,46 @@ describe('ProjectDialogComponent', () => {
|
||||
expect(() => register()).not.toThrow();
|
||||
});
|
||||
|
||||
it('renders an upgraded swiper-container for project images', async () => {
|
||||
async function createDialog(data: object): Promise<ComponentFixture<ProjectDialogComponent>> {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProjectDialogComponent],
|
||||
providers: [
|
||||
provideZonelessChangeDetection(),
|
||||
provideTranslateService({ lang: 'en', fallbackLang: 'en' }),
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
title: 'T',
|
||||
technologies: [],
|
||||
images: [{ url: 'a.png' }, { url: 'b.png' }],
|
||||
},
|
||||
},
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
],
|
||||
}).compileComponents();
|
||||
const fixture = TestBed.createComponent(ProjectDialogComponent);
|
||||
fixture.detectChanges();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
it('renders an upgraded swiper-container for project images', async () => {
|
||||
const fixture = await createDialog({
|
||||
title: 'T',
|
||||
technologies: [],
|
||||
images: [{ url: 'a.png' }, { url: 'b.png' }],
|
||||
});
|
||||
|
||||
const el: HTMLElement = fixture.nativeElement.querySelector('swiper-container');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el instanceof customElements.get('swiper-container')!).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the diagram above the sections when the project has one', async () => {
|
||||
const fixture = await createDialog({ title: 'T', technologies: [], images: [], diagram: 'd.svg' });
|
||||
|
||||
const img: HTMLImageElement | null = fixture.nativeElement.querySelector('img[alt="PROJECTS.DIAGRAM_ALT"]');
|
||||
expect(img?.getAttribute('src')).toBe('d.svg');
|
||||
const link: HTMLAnchorElement | null = img?.closest('a') ?? null;
|
||||
expect(link?.getAttribute('href')).toBe('d.svg');
|
||||
expect(link?.getAttribute('target')).toBe('_blank');
|
||||
expect(fixture.nativeElement.querySelector('swiper-container')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders no diagram without one', async () => {
|
||||
const fixture = await createDialog({ title: 'T', technologies: [], images: [] });
|
||||
|
||||
expect(fixture.nativeElement.querySelector('img[alt="PROJECTS.DIAGRAM_ALT"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,8 @@
|
||||
<mat-card-title>{{ project.title | translate }}</mat-card-title>
|
||||
<mat-card-subtitle>{{ project.shortDescription | translate }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
@if(project.images.length > 0) {
|
||||
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="project.images[0].url" [alt]="project.title | translate">
|
||||
@if (cardImage(project); as imageUrl) {
|
||||
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="imageUrl" [alt]="project.title | translate">
|
||||
} @else {
|
||||
<div class="flex justify-center items-center h-[clamp(150px,20vw,200px)] bg-[#f0f0f0]">
|
||||
<mat-icon class="text-[clamp(3rem,8vw,4rem)] w-[clamp(3rem,8vw,4rem)] h-[clamp(3rem,8vw,4rem)] text-[#666]">{{ project.icon }}</mat-icon>
|
||||
@@ -31,8 +31,8 @@
|
||||
<mat-card-header class="pb-4">
|
||||
<mat-card-title>{{ project.title | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
@if(project.images.length > 0) {
|
||||
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="project.images[0].url" [alt]="project.title | translate">
|
||||
@if (cardImage(project); as imageUrl) {
|
||||
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="imageUrl" [alt]="project.title | translate">
|
||||
} @else {
|
||||
<div class="flex justify-center items-center h-[clamp(150px,20vw,200px)] bg-[#f0f0f0]">
|
||||
<mat-icon class="text-[clamp(3rem,8vw,4rem)] w-[clamp(3rem,8vw,4rem)] h-[clamp(3rem,8vw,4rem)] text-[#666]">{{ project.icon }}</mat-icon>
|
||||
|
||||
@@ -36,6 +36,14 @@ describe('ProjectsComponent', () => {
|
||||
expect(titles).toContain('PROJECTS.TBP.TITLE');
|
||||
});
|
||||
|
||||
it('features To Be Played', () => {
|
||||
expect(fixture.componentInstance.featuredProject()?.identifier).toBe('to-be-played');
|
||||
});
|
||||
|
||||
it('card falls back to the diagram when a project has no images', () => {
|
||||
expect(fixture.nativeElement.querySelector('img[src="/assets/projects/claude-in-a-box/1.svg"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('every project translation key exists in en and de', () => {
|
||||
const keys = fixture.componentInstance.allProjects.flatMap(project => [
|
||||
project.title,
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface Projects {
|
||||
url: string,
|
||||
source: string
|
||||
}[],
|
||||
diagram?: string,
|
||||
icon: string,
|
||||
assets: string,
|
||||
links: {
|
||||
@@ -59,6 +60,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
shortDescription: 'PROJECTS.TBP.SHORT_DESCRIPTION',
|
||||
introduction: 'PROJECTS.TBP.INTRODUCTION',
|
||||
images: AssetsConstants.TBP_IMAGES.map(url => ({ url, source: '' })),
|
||||
diagram: AssetsConstants.TBP_DIAGRAM,
|
||||
icon: 'playlist_add_check',
|
||||
assets: '',
|
||||
links: [
|
||||
@@ -82,7 +84,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
'PROJECTS.TBP.LEARNING_1',
|
||||
'PROJECTS.TBP.LEARNING_2',
|
||||
],
|
||||
isFeatured: false,
|
||||
isFeatured: true,
|
||||
technologies: ['Java', 'Spring Boot', 'PostgreSQL', 'React', 'Tailwind CSS', 'Docker']
|
||||
},
|
||||
{
|
||||
@@ -90,7 +92,8 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
title: 'PROJECTS.CLAUDE_BOX.TITLE',
|
||||
shortDescription: 'PROJECTS.CLAUDE_BOX.SHORT_DESCRIPTION',
|
||||
introduction: 'PROJECTS.CLAUDE_BOX.INTRODUCTION',
|
||||
images: AssetsConstants.CLAUDE_BOX_IMAGES.map(url => ({ url, source: '' })),
|
||||
images: [],
|
||||
diagram: AssetsConstants.CLAUDE_BOX_DIAGRAM,
|
||||
icon: 'smart_toy',
|
||||
assets: '',
|
||||
links: [],
|
||||
@@ -136,7 +139,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
'PROJECTS.PLAYGROUND.LEARNING_2',
|
||||
],
|
||||
isFeatured: false,
|
||||
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'GitHub Actions', 'Docker']
|
||||
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'Gitea Actions', 'Docker']
|
||||
},
|
||||
{
|
||||
identifier: "elmucho",
|
||||
@@ -156,13 +159,12 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
challenges: [
|
||||
'PROJECTS.EL_MUCHO.CHALLENGE_1',
|
||||
'PROJECTS.EL_MUCHO.CHALLENGE_2',
|
||||
'PROJECTS.EL_MUCHO.CHALLENGE_3',
|
||||
],
|
||||
learnings: [
|
||||
'PROJECTS.EL_MUCHO.LEARNING_1',
|
||||
'PROJECTS.EL_MUCHO.LEARNING_2',
|
||||
],
|
||||
isFeatured: true,
|
||||
isFeatured: false,
|
||||
technologies: ['Unity', 'C#', 'Steamworks', 'Git']
|
||||
},
|
||||
{
|
||||
@@ -223,16 +225,17 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
shortDescription: 'PROJECTS.TRIBBLE.SHORT_DESCRIPTION',
|
||||
introduction: 'PROJECTS.TRIBBLE.INTRODUCTION',
|
||||
images: [
|
||||
{ url: AssetsConstants.TRIBBLE_IMAGES[0], source: 'https://upload.wikimedia.org/wikipedia/commons/0/03/Hostinger_Logo.png' },
|
||||
{ url: AssetsConstants.TRIBBLE_IMAGES[1], source: 'https://dashboardicons.com/icons/docker-engine' },
|
||||
{ url: AssetsConstants.TRIBBLE_IMAGES[2], source: 'https://dashboardicons.com/icons/gitea' },
|
||||
{ url: AssetsConstants.TRIBBLE_IMAGES[3], source: 'https://dashboardicons.com/icons/traefik' }
|
||||
{ url: AssetsConstants.TRIBBLE_IMAGES[0], source: 'https://dashboardicons.com/icons/docker-engine' },
|
||||
{ url: AssetsConstants.TRIBBLE_IMAGES[1], source: 'https://dashboardicons.com/icons/gitea' },
|
||||
{ url: AssetsConstants.TRIBBLE_IMAGES[2], source: 'https://dashboardicons.com/icons/traefik' }
|
||||
],
|
||||
diagram: AssetsConstants.TRIBBLE_DIAGRAM,
|
||||
icon: 'dns',
|
||||
assets: '',
|
||||
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/' },
|
||||
@@ -255,13 +258,17 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
||||
'PROJECTS.TRIBBLE.LEARNING_2',
|
||||
],
|
||||
isFeatured: false,
|
||||
technologies: ['Ubuntu Server', 'Docker', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale']
|
||||
technologies: ['Ubuntu Server', 'Docker', 'Portainer', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale']
|
||||
}
|
||||
]
|
||||
|
||||
featuredProject = computed(() => this.allProjects.find(p => p.isFeatured));
|
||||
otherProjects = computed(() => this.allProjects.filter(p => !p.isFeatured));
|
||||
|
||||
cardImage(project: Projects): string | undefined {
|
||||
return project.images[0]?.url ?? project.diagram;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
setTimeout(() => { this.dialogOpenFunction(); }, 10);
|
||||
|
||||
Reference in new issue
Block a user