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'
|
'/assets/projects/diploma/6.webp'
|
||||||
];
|
];
|
||||||
|
|
||||||
static readonly CLAUDE_BOX_IMAGES = [
|
static readonly CLAUDE_BOX_DIAGRAM = '/assets/projects/claude-in-a-box/1.svg';
|
||||||
'/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 = [
|
static readonly TRIBBLE_IMAGES = [
|
||||||
'/assets/projects/tribble-the-homeserver/1.webp',
|
|
||||||
'/assets/projects/tribble-the-homeserver/2.webp',
|
'/assets/projects/tribble-the-homeserver/2.webp',
|
||||||
'/assets/projects/tribble-the-homeserver/3.webp',
|
'/assets/projects/tribble-the-homeserver/3.webp',
|
||||||
'/assets/projects/tribble-the-homeserver/4.webp'
|
'/assets/projects/tribble-the-homeserver/4.webp'
|
||||||
|
|||||||
@@ -12,6 +12,14 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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="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="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">
|
<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 { provideZonelessChangeDetection } from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { provideTranslateService } from '@ngx-translate/core';
|
import { provideTranslateService } from '@ngx-translate/core';
|
||||||
@@ -27,27 +27,46 @@ describe('ProjectDialogComponent', () => {
|
|||||||
expect(() => register()).not.toThrow();
|
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({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ProjectDialogComponent],
|
imports: [ProjectDialogComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideZonelessChangeDetection(),
|
provideZonelessChangeDetection(),
|
||||||
provideTranslateService({ lang: 'en', fallbackLang: 'en' }),
|
provideTranslateService({ lang: 'en', fallbackLang: 'en' }),
|
||||||
{
|
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||||
provide: MAT_DIALOG_DATA,
|
|
||||||
useValue: {
|
|
||||||
title: 'T',
|
|
||||||
technologies: [],
|
|
||||||
images: [{ url: 'a.png' }, { url: 'b.png' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
const fixture = TestBed.createComponent(ProjectDialogComponent);
|
const fixture = TestBed.createComponent(ProjectDialogComponent);
|
||||||
fixture.detectChanges();
|
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');
|
const el: HTMLElement = fixture.nativeElement.querySelector('swiper-container');
|
||||||
expect(el).not.toBeNull();
|
expect(el).not.toBeNull();
|
||||||
expect(el instanceof customElements.get('swiper-container')!).toBe(true);
|
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-title>{{ project.title | translate }}</mat-card-title>
|
||||||
<mat-card-subtitle>{{ project.shortDescription | translate }}</mat-card-subtitle>
|
<mat-card-subtitle>{{ project.shortDescription | translate }}</mat-card-subtitle>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
@if(project.images.length > 0) {
|
@if (cardImage(project); as imageUrl) {
|
||||||
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="project.images[0].url" [alt]="project.title | translate">
|
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="imageUrl" [alt]="project.title | translate">
|
||||||
} @else {
|
} @else {
|
||||||
<div class="flex justify-center items-center h-[clamp(150px,20vw,200px)] bg-[#f0f0f0]">
|
<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>
|
<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-header class="pb-4">
|
||||||
<mat-card-title>{{ project.title | translate }}</mat-card-title>
|
<mat-card-title>{{ project.title | translate }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
@if(project.images.length > 0) {
|
@if (cardImage(project); as imageUrl) {
|
||||||
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="project.images[0].url" [alt]="project.title | translate">
|
<img mat-card-image class="w-full h-[clamp(150px,25vw,250px)] object-cover" [src]="imageUrl" [alt]="project.title | translate">
|
||||||
} @else {
|
} @else {
|
||||||
<div class="flex justify-center items-center h-[clamp(150px,20vw,200px)] bg-[#f0f0f0]">
|
<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>
|
<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');
|
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', () => {
|
it('every project translation key exists in en and de', () => {
|
||||||
const keys = fixture.componentInstance.allProjects.flatMap(project => [
|
const keys = fixture.componentInstance.allProjects.flatMap(project => [
|
||||||
project.title,
|
project.title,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface Projects {
|
|||||||
url: string,
|
url: string,
|
||||||
source: string
|
source: string
|
||||||
}[],
|
}[],
|
||||||
|
diagram?: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
assets: string,
|
assets: string,
|
||||||
links: {
|
links: {
|
||||||
@@ -59,6 +60,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
shortDescription: 'PROJECTS.TBP.SHORT_DESCRIPTION',
|
shortDescription: 'PROJECTS.TBP.SHORT_DESCRIPTION',
|
||||||
introduction: 'PROJECTS.TBP.INTRODUCTION',
|
introduction: 'PROJECTS.TBP.INTRODUCTION',
|
||||||
images: AssetsConstants.TBP_IMAGES.map(url => ({ url, source: '' })),
|
images: AssetsConstants.TBP_IMAGES.map(url => ({ url, source: '' })),
|
||||||
|
diagram: AssetsConstants.TBP_DIAGRAM,
|
||||||
icon: 'playlist_add_check',
|
icon: 'playlist_add_check',
|
||||||
assets: '',
|
assets: '',
|
||||||
links: [
|
links: [
|
||||||
@@ -82,7 +84,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
'PROJECTS.TBP.LEARNING_1',
|
'PROJECTS.TBP.LEARNING_1',
|
||||||
'PROJECTS.TBP.LEARNING_2',
|
'PROJECTS.TBP.LEARNING_2',
|
||||||
],
|
],
|
||||||
isFeatured: false,
|
isFeatured: true,
|
||||||
technologies: ['Java', 'Spring Boot', 'PostgreSQL', 'React', 'Tailwind CSS', 'Docker']
|
technologies: ['Java', 'Spring Boot', 'PostgreSQL', 'React', 'Tailwind CSS', 'Docker']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -90,7 +92,8 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
title: 'PROJECTS.CLAUDE_BOX.TITLE',
|
title: 'PROJECTS.CLAUDE_BOX.TITLE',
|
||||||
shortDescription: 'PROJECTS.CLAUDE_BOX.SHORT_DESCRIPTION',
|
shortDescription: 'PROJECTS.CLAUDE_BOX.SHORT_DESCRIPTION',
|
||||||
introduction: 'PROJECTS.CLAUDE_BOX.INTRODUCTION',
|
introduction: 'PROJECTS.CLAUDE_BOX.INTRODUCTION',
|
||||||
images: AssetsConstants.CLAUDE_BOX_IMAGES.map(url => ({ url, source: '' })),
|
images: [],
|
||||||
|
diagram: AssetsConstants.CLAUDE_BOX_DIAGRAM,
|
||||||
icon: 'smart_toy',
|
icon: 'smart_toy',
|
||||||
assets: '',
|
assets: '',
|
||||||
links: [],
|
links: [],
|
||||||
@@ -136,7 +139,7 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
'PROJECTS.PLAYGROUND.LEARNING_2',
|
'PROJECTS.PLAYGROUND.LEARNING_2',
|
||||||
],
|
],
|
||||||
isFeatured: false,
|
isFeatured: false,
|
||||||
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'GitHub Actions', 'Docker']
|
technologies: ['Angular', 'TypeScript', 'SCSS', 'HTML', 'Gitea Actions', 'Docker']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: "elmucho",
|
identifier: "elmucho",
|
||||||
@@ -156,13 +159,12 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
challenges: [
|
challenges: [
|
||||||
'PROJECTS.EL_MUCHO.CHALLENGE_1',
|
'PROJECTS.EL_MUCHO.CHALLENGE_1',
|
||||||
'PROJECTS.EL_MUCHO.CHALLENGE_2',
|
'PROJECTS.EL_MUCHO.CHALLENGE_2',
|
||||||
'PROJECTS.EL_MUCHO.CHALLENGE_3',
|
|
||||||
],
|
],
|
||||||
learnings: [
|
learnings: [
|
||||||
'PROJECTS.EL_MUCHO.LEARNING_1',
|
'PROJECTS.EL_MUCHO.LEARNING_1',
|
||||||
'PROJECTS.EL_MUCHO.LEARNING_2',
|
'PROJECTS.EL_MUCHO.LEARNING_2',
|
||||||
],
|
],
|
||||||
isFeatured: true,
|
isFeatured: false,
|
||||||
technologies: ['Unity', 'C#', 'Steamworks', 'Git']
|
technologies: ['Unity', 'C#', 'Steamworks', 'Git']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -223,16 +225,17 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
shortDescription: 'PROJECTS.TRIBBLE.SHORT_DESCRIPTION',
|
shortDescription: 'PROJECTS.TRIBBLE.SHORT_DESCRIPTION',
|
||||||
introduction: 'PROJECTS.TRIBBLE.INTRODUCTION',
|
introduction: 'PROJECTS.TRIBBLE.INTRODUCTION',
|
||||||
images: [
|
images: [
|
||||||
{ url: AssetsConstants.TRIBBLE_IMAGES[0], source: 'https://upload.wikimedia.org/wikipedia/commons/0/03/Hostinger_Logo.png' },
|
{ url: AssetsConstants.TRIBBLE_IMAGES[0], source: 'https://dashboardicons.com/icons/docker-engine' },
|
||||||
{ url: AssetsConstants.TRIBBLE_IMAGES[1], 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/gitea' },
|
{ url: AssetsConstants.TRIBBLE_IMAGES[2], source: 'https://dashboardicons.com/icons/traefik' }
|
||||||
{ url: AssetsConstants.TRIBBLE_IMAGES[3], source: 'https://dashboardicons.com/icons/traefik' }
|
|
||||||
],
|
],
|
||||||
|
diagram: AssetsConstants.TRIBBLE_DIAGRAM,
|
||||||
icon: 'dns',
|
icon: 'dns',
|
||||||
assets: '',
|
assets: '',
|
||||||
links: [
|
links: [
|
||||||
{ name: 'Ubuntu Server', url: 'https://ubuntu.com/server' },
|
{ name: 'Ubuntu Server', url: 'https://ubuntu.com/server' },
|
||||||
{ name: 'Docker', url: 'https://www.docker.com/' },
|
{ name: 'Docker', url: 'https://www.docker.com/' },
|
||||||
|
{ name: 'Portainer', url: 'https://www.portainer.io/' },
|
||||||
{ name: 'Traefik', url: 'https://traefik.io/' },
|
{ name: 'Traefik', url: 'https://traefik.io/' },
|
||||||
{ name: 'Gitea', url: 'https://gitea.io/' },
|
{ name: 'Gitea', url: 'https://gitea.io/' },
|
||||||
{ name: 'Jellyfin', url: 'https://jellyfin.org/' },
|
{ name: 'Jellyfin', url: 'https://jellyfin.org/' },
|
||||||
@@ -255,13 +258,17 @@ export class ProjectsComponent implements OnInit, OnDestroy {
|
|||||||
'PROJECTS.TRIBBLE.LEARNING_2',
|
'PROJECTS.TRIBBLE.LEARNING_2',
|
||||||
],
|
],
|
||||||
isFeatured: false,
|
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));
|
featuredProject = computed(() => this.allProjects.find(p => p.isFeatured));
|
||||||
otherProjects = computed(() => this.allProjects.filter(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 {
|
ngOnInit(): void {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
setTimeout(() => { this.dialogOpenFunction(); }, 10);
|
setTimeout(() => { this.dialogOpenFunction(); }, 10);
|
||||||
|
|||||||
+11
-11
@@ -212,9 +212,9 @@
|
|||||||
"LINK_EXTERNAL": "andreas-dahm.eu",
|
"LINK_EXTERNAL": "andreas-dahm.eu",
|
||||||
"LINK_INTERNAL": "Projektdetails",
|
"LINK_INTERNAL": "Projektdetails",
|
||||||
"HIGHLIGHTS": {
|
"HIGHLIGHTS": {
|
||||||
"P1": "Einsatz moderner Technologien und CI/CD-Pipelines (Angular 20+, Spring Boot 4, GitHub).",
|
"P1": "Moderner Frontend-Stack und eine CI/CD-Pipeline mit Gitea Actions (Angular, Docker).",
|
||||||
"P2": "Präsentation persönlicher Projekte und kontinuierliche Verbesserung algorithmischer Fähigkeiten.",
|
"P2": "Präsentation persönlicher Projekte und kontinuierliche Verbesserung algorithmischer Fähigkeiten.",
|
||||||
"P3": "Vertiefung von JavaScript/TypeScript-, Angular- und Spring-Boot-Kenntnissen durch praktisches Arbeiten."
|
"P3": "Vertiefung von JavaScript/TypeScript- und Angular-Kenntnissen durch praktisches Arbeiten."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"P1": {
|
"P1": {
|
||||||
@@ -301,6 +301,7 @@
|
|||||||
"PROJECTS": {
|
"PROJECTS": {
|
||||||
"DOWNLOAD": "Herunterladen",
|
"DOWNLOAD": "Herunterladen",
|
||||||
"READ_MORE": "Mehr erfahren",
|
"READ_MORE": "Mehr erfahren",
|
||||||
|
"DIAGRAM_ALT": "Architekturdiagramm",
|
||||||
"LINK_TO_PROJECT": "Zum Projekt",
|
"LINK_TO_PROJECT": "Zum Projekt",
|
||||||
"CLOSE": "Schließen",
|
"CLOSE": "Schließen",
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
@@ -309,11 +310,11 @@
|
|||||||
},
|
},
|
||||||
"PLAYGROUND": {
|
"PLAYGROUND": {
|
||||||
"TITLE": "Playground Portfolio",
|
"TITLE": "Playground Portfolio",
|
||||||
"SHORT_DESCRIPTION": "Full-Stack Portfolio mit interaktiven Algorithmus-Visualisierungen.",
|
"SHORT_DESCRIPTION": "Frontend-Portfolio mit interaktiven Algorithmus-Visualisierungen.",
|
||||||
"INTRODUCTION": "Diese Website dient als lebendiges Portfolio und Testumgebung für moderne Webtechnologien. Ziel ist es, komplexe Algorithmen und mathematische Konzepte (wie WebGPU-Simulationen oder Raymarching) anschaulich im Browser darzustellen.",
|
"INTRODUCTION": "Diese Website dient als lebendiges Portfolio und Testumgebung für moderne Webtechnologien. Ziel ist es, komplexe Algorithmen und mathematische Konzepte (wie WebGPU-Simulationen oder Raymarching) anschaulich im Browser darzustellen.",
|
||||||
"BULLET_1": "Entwicklung mit Angular 19+ und Material Design.",
|
"BULLET_1": "Entwicklung mit Angular und Material Design.",
|
||||||
"BULLET_2": "Implementierung performanter Visualisierungen (WebGPU, Shader, Canvas).",
|
"BULLET_2": "Implementierung performanter Visualisierungen (WebGPU, Shader, Canvas).",
|
||||||
"BULLET_3": "Automatisierte CI/CD-Pipelines und Containerisierung mit Docker.",
|
"BULLET_3": "Automatisierte CI/CD mit Gitea Actions und Containerisierung mit Docker.",
|
||||||
"BULLET_4": "Internationalization (i18n) für globale Reichweite.",
|
"BULLET_4": "Internationalization (i18n) für globale Reichweite.",
|
||||||
"CHALLENGE_1": "Optimierung der Render-Performance bei komplexen 3D-Fraktalen in Echtzeit.",
|
"CHALLENGE_1": "Optimierung der Render-Performance bei komplexen 3D-Fraktalen in Echtzeit.",
|
||||||
"CHALLENGE_2": "Architektur einer skalierbaren und wartbaren Frontend-Struktur für diverse Sub-Projekte.",
|
"CHALLENGE_2": "Architektur einer skalierbaren und wartbaren Frontend-Struktur für diverse Sub-Projekte.",
|
||||||
@@ -322,11 +323,11 @@
|
|||||||
},
|
},
|
||||||
"TRIBBLE": {
|
"TRIBBLE": {
|
||||||
"TITLE": "Self-Hosted Infrastructure",
|
"TITLE": "Self-Hosted Infrastructure",
|
||||||
"SHORT_DESCRIPTION": "Home-Infrastruktur mit Docker, Traefik und sicherer VPN-Anbindung.",
|
"SHORT_DESCRIPTION": "Home-Server für CI und private Dienste, dazu ein VPS für die öffentlichen Seiten.",
|
||||||
"INTRODUCTION": "Dokumentation und Aufbau einer privaten Cloud-Infrastruktur. Fokus liegt auf Datensouveränität, Automatisierung und Sicherheit.",
|
"INTRODUCTION": "Zwei Maschinen: Der Home-Server „Tribble“ betreibt Gitea mit Actions-Runner und die privaten Dienste, ein Hostinger-VPS liefert die öffentlichen Seiten aus. Die CI baut die Images zu Hause, der VPS zieht neue Images mit Watchtower über Tailscale, und Traefik leitet die öffentlichen Seiten weiter.",
|
||||||
"BULLET_1": "Zentrale Verwaltung via Docker-Compose und Portainer.",
|
"BULLET_1": "Zentrale Verwaltung via Docker-Compose und Portainer.",
|
||||||
"BULLET_2": "Automatisches SSL-Management und Reverse-Proxy mit Traefik.",
|
"BULLET_2": "Automatisches SSL-Management und Reverse-Proxy mit Traefik.",
|
||||||
"BULLET_3": "Private Versionskontrolle (Gitea) und Medien-Streaming (Jellyfin).",
|
"BULLET_3": "Private Versionskontrolle und CI mit Gitea, Medien-Streaming mit Jellyfin.",
|
||||||
"BULLET_4": "Netzwerkweite Ad-Blocking und DNS-Kontrolle via AdGuard Home.",
|
"BULLET_4": "Netzwerkweite Ad-Blocking und DNS-Kontrolle via AdGuard Home.",
|
||||||
"CHALLENGE_1": "Konfiguration sicherer Netzwerkschichten und Firewall-Regeln für Remote-Zugriff.",
|
"CHALLENGE_1": "Konfiguration sicherer Netzwerkschichten und Firewall-Regeln für Remote-Zugriff.",
|
||||||
"CHALLENGE_2": "Automatisierung von Backups und Recovery-Strategien für containerisierte Daten.",
|
"CHALLENGE_2": "Automatisierung von Backups und Recovery-Strategien für containerisierte Daten.",
|
||||||
@@ -336,14 +337,13 @@
|
|||||||
"EL_MUCHO": {
|
"EL_MUCHO": {
|
||||||
"TITLE": "El Mucho (Steam Release)",
|
"TITLE": "El Mucho (Steam Release)",
|
||||||
"SHORT_DESCRIPTION": "Rundenbasiertes Taktik-RPG, veröffentlicht auf Steam.",
|
"SHORT_DESCRIPTION": "Rundenbasiertes Taktik-RPG, veröffentlicht auf Steam.",
|
||||||
"INTRODUCTION": "Ein kommerzielles Spieleprojekt, das von der ersten Idee bis zum weltweiten Release auf Steam eigenverantwortlich umgesetzt wurde. Ein taktisches RPG, das klassische Gameplay-Elemente mit modernen Systemen verbindet.",
|
"INTRODUCTION": "Ein kommerzielles Spiel, das mein Bruder und ich von der ersten Idee bis zum Release auf Steam gebracht haben: geplant, gebaut, verkauft. Ein taktisches RPG, das klassische Gameplay-Elemente mit modernen Systemen verbindet.",
|
||||||
"BULLET_1": "Komplette Engine-Entwicklung in Unity (C#).",
|
"BULLET_1": "Spielentwicklung in Unity (C#).",
|
||||||
"BULLET_2": "Integration von Steamworks-Funktionen (Achievements, Cloud Saves).",
|
"BULLET_2": "Integration von Steamworks-Funktionen (Achievements, Cloud Saves).",
|
||||||
"BULLET_3": "Entwicklung einer eigenen taktischen KI und Pfadfindungs-Logik.",
|
"BULLET_3": "Entwicklung einer eigenen taktischen KI und Pfadfindungs-Logik.",
|
||||||
"BULLET_4": "Management des gesamten Asset-Pipelines und Sound-Designs.",
|
"BULLET_4": "Management des gesamten Asset-Pipelines und Sound-Designs.",
|
||||||
"CHALLENGE_1": "Implementierung eines robusten rundenbasierten Systems mit komplexen Abhängigkeiten.",
|
"CHALLENGE_1": "Implementierung eines robusten rundenbasierten Systems mit komplexen Abhängigkeiten.",
|
||||||
"CHALLENGE_2": "Performance-Optimierung für eine flüssige Darstellung auf verschiedenen Hardware-Profilen.",
|
"CHALLENGE_2": "Performance-Optimierung für eine flüssige Darstellung auf verschiedenen Hardware-Profilen.",
|
||||||
"CHALLENGE_3": "Umgang mit den strengen Zertifizierungs-Anforderungen von Steam.",
|
|
||||||
"LEARNING_1": "Durchhaltevermögen und Fokus über einen mehrjährigen Entwicklungszyklus.",
|
"LEARNING_1": "Durchhaltevermögen und Fokus über einen mehrjährigen Entwicklungszyklus.",
|
||||||
"LEARNING_2": "Vermarktung und Community-Management für ein digitales Produkt."
|
"LEARNING_2": "Vermarktung und Community-Management für ein digitales Produkt."
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-11
@@ -212,9 +212,9 @@
|
|||||||
"LINK_EXTERNAL": "andreas-dahm.eu",
|
"LINK_EXTERNAL": "andreas-dahm.eu",
|
||||||
"LINK_INTERNAL": "Project details",
|
"LINK_INTERNAL": "Project details",
|
||||||
"HIGHLIGHTS": {
|
"HIGHLIGHTS": {
|
||||||
"P1": "Using modern technologies and CI/CD pipelines (Angular 20+, Spring Boot 4, GitHub).",
|
"P1": "Modern frontend stack and a CI/CD pipeline on Gitea Actions (Angular, Docker).",
|
||||||
"P2": "Showcasing personal projects and improving algorithmic skills over time.",
|
"P2": "Showcasing personal projects and improving algorithmic skills over time.",
|
||||||
"P3": "Deepening knowledge in JavaScript/TypeScript, Angular, Spring Boot and related technologies through hands-on practice."
|
"P3": "Deepening knowledge in JavaScript/TypeScript, Angular and related technologies through hands-on practice."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"P1": {
|
"P1": {
|
||||||
@@ -301,6 +301,7 @@
|
|||||||
"PROJECTS": {
|
"PROJECTS": {
|
||||||
"DOWNLOAD": "Download",
|
"DOWNLOAD": "Download",
|
||||||
"READ_MORE": "Read More",
|
"READ_MORE": "Read More",
|
||||||
|
"DIAGRAM_ALT": "Architecture diagram",
|
||||||
"LINK_TO_PROJECT": "To the project",
|
"LINK_TO_PROJECT": "To the project",
|
||||||
"CLOSE": "Close",
|
"CLOSE": "Close",
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
@@ -309,11 +310,11 @@
|
|||||||
},
|
},
|
||||||
"PLAYGROUND": {
|
"PLAYGROUND": {
|
||||||
"TITLE": "Playground Portfolio",
|
"TITLE": "Playground Portfolio",
|
||||||
"SHORT_DESCRIPTION": "Full-stack portfolio with interactive algorithm visualizations.",
|
"SHORT_DESCRIPTION": "Frontend portfolio with interactive algorithm visualizations.",
|
||||||
"INTRODUCTION": "This website serves as a living portfolio and testing ground for modern web technologies. The goal is to clearly represent complex algorithms and mathematical concepts (such as WebGPU simulations or Raymarching) directly in the browser.",
|
"INTRODUCTION": "This website serves as a living portfolio and testing ground for modern web technologies. The goal is to clearly represent complex algorithms and mathematical concepts (such as WebGPU simulations or Raymarching) directly in the browser.",
|
||||||
"BULLET_1": "Development with Angular 19+ and Material Design.",
|
"BULLET_1": "Development with Angular and Material Design.",
|
||||||
"BULLET_2": "Implementation of performant visualizations (WebGPU, Shader, Canvas).",
|
"BULLET_2": "Implementation of performant visualizations (WebGPU, Shader, Canvas).",
|
||||||
"BULLET_3": "Automated CI/CD pipelines and containerization with Docker.",
|
"BULLET_3": "Automated CI/CD with Gitea Actions and containerization with Docker.",
|
||||||
"BULLET_4": "Internationalization (i18n) for global reach.",
|
"BULLET_4": "Internationalization (i18n) for global reach.",
|
||||||
"CHALLENGE_1": "Optimizing render performance for complex 3D fractals in real-time.",
|
"CHALLENGE_1": "Optimizing render performance for complex 3D fractals in real-time.",
|
||||||
"CHALLENGE_2": "Architecting a scalable and maintainable frontend structure for diverse sub-projects.",
|
"CHALLENGE_2": "Architecting a scalable and maintainable frontend structure for diverse sub-projects.",
|
||||||
@@ -322,11 +323,11 @@
|
|||||||
},
|
},
|
||||||
"TRIBBLE": {
|
"TRIBBLE": {
|
||||||
"TITLE": "Self-Hosted Infrastructure",
|
"TITLE": "Self-Hosted Infrastructure",
|
||||||
"SHORT_DESCRIPTION": "Home infrastructure with Docker, Traefik, and secure VPN connectivity.",
|
"SHORT_DESCRIPTION": "Home server for CI and private services, plus a VPS for the public sites.",
|
||||||
"INTRODUCTION": "Documentation and construction of a private cloud infrastructure. Focus is on data sovereignty, automation, and security.",
|
"INTRODUCTION": "Two machines: the home server \"Tribble\" runs Gitea with its Actions runner and the private services, and a Hostinger VPS serves the public sites. CI builds the images at home, the VPS pulls new images over Tailscale with Watchtower, and Traefik routes the public sites.",
|
||||||
"BULLET_1": "Central management via Docker-Compose and Portainer.",
|
"BULLET_1": "Central management via Docker-Compose and Portainer.",
|
||||||
"BULLET_2": "Automatic SSL management and reverse proxy with Traefik.",
|
"BULLET_2": "Automatic SSL management and reverse proxy with Traefik.",
|
||||||
"BULLET_3": "Private version control (Gitea) and media streaming (Jellyfin).",
|
"BULLET_3": "Private version control and CI with Gitea, media streaming with Jellyfin.",
|
||||||
"BULLET_4": "Network-wide ad-blocking and DNS control via AdGuard Home.",
|
"BULLET_4": "Network-wide ad-blocking and DNS control via AdGuard Home.",
|
||||||
"CHALLENGE_1": "Configuring secure network layers and firewall rules for remote access.",
|
"CHALLENGE_1": "Configuring secure network layers and firewall rules for remote access.",
|
||||||
"CHALLENGE_2": "Automating backups and recovery strategies for containerized data.",
|
"CHALLENGE_2": "Automating backups and recovery strategies for containerized data.",
|
||||||
@@ -336,14 +337,13 @@
|
|||||||
"EL_MUCHO": {
|
"EL_MUCHO": {
|
||||||
"TITLE": "El Mucho (Steam Release)",
|
"TITLE": "El Mucho (Steam Release)",
|
||||||
"SHORT_DESCRIPTION": "Turn-based tactical RPG, published on Steam.",
|
"SHORT_DESCRIPTION": "Turn-based tactical RPG, published on Steam.",
|
||||||
"INTRODUCTION": "A commercial game project that was independently implemented from the initial idea to the worldwide release on Steam. A tactical RPG that combines classic gameplay elements with modern systems.",
|
"INTRODUCTION": "A commercial game that my brother and I took from the first idea to the release on Steam: planned, built and sold. A tactical RPG that combines classic gameplay elements with modern systems.",
|
||||||
"BULLET_1": "Complete engine development in Unity (C#).",
|
"BULLET_1": "Game development in Unity (C#).",
|
||||||
"BULLET_2": "Integration of Steamworks features (Achievements, Cloud Saves).",
|
"BULLET_2": "Integration of Steamworks features (Achievements, Cloud Saves).",
|
||||||
"BULLET_3": "Development of a custom tactical AI and pathfinding logic.",
|
"BULLET_3": "Development of a custom tactical AI and pathfinding logic.",
|
||||||
"BULLET_4": "Management of the entire asset pipeline and sound design.",
|
"BULLET_4": "Management of the entire asset pipeline and sound design.",
|
||||||
"CHALLENGE_1": "Implementing a robust turn-based system with complex dependencies.",
|
"CHALLENGE_1": "Implementing a robust turn-based system with complex dependencies.",
|
||||||
"CHALLENGE_2": "Performance optimization for a smooth experience across various hardware profiles.",
|
"CHALLENGE_2": "Performance optimization for a smooth experience across various hardware profiles.",
|
||||||
"CHALLENGE_3": "Handling Steam's strict certification requirements.",
|
|
||||||
"LEARNING_1": "Perseverance and focus over a multi-year development cycle.",
|
"LEARNING_1": "Perseverance and focus over a multi-year development cycle.",
|
||||||
"LEARNING_2": "Marketing and community management for a digital product."
|
"LEARNING_2": "Marketing and community management for a digital product."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">To Be Played</title>
|
||||||
|
<desc id="desc">Architecture of To Be Played: a React frontend talks to an Auth service that signs tokens and a Core service that verifies them. Both use PostgreSQL, Core also calls the Steam API and IGDB.</desc>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#1e2530"/>
|
||||||
|
<stop offset="100%" stop-color="#15181d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#26c6da"/>
|
||||||
|
<stop offset="100%" stop-color="#ffa726"/>
|
||||||
|
</linearGradient>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#4a5563"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrowCyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#26c6da"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
||||||
|
.sans { font-family: Roboto, Arial, sans-serif; }
|
||||||
|
.lbl { fill: #8b97a6; font-size: 17px; letter-spacing: .04em; }
|
||||||
|
.chip { fill: #cdd6e0; font-size: 19px; }
|
||||||
|
.cap { fill: #6c7887; font-size: 15px; letter-spacing: .18em; }
|
||||||
|
.name { fill: #cdd6e0; font-size: 22px; font-weight: 600; }
|
||||||
|
.sub { fill: #8b97a6; font-size: 16px; }
|
||||||
|
.arr { fill: #8b97a6; font-size: 15px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||||
|
<rect x="0" y="0" width="1200" height="4" fill="url(#accent)"/>
|
||||||
|
|
||||||
|
<text x="64" y="72" class="mono" fill="#26c6da" font-size="26" font-weight="600">to-be-played</text>
|
||||||
|
<text x="64" y="102" class="sans lbl">three services, one login</text>
|
||||||
|
<rect x="48" y="240" width="196" height="110" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="68" y="274" class="sans name">React Frontend</text>
|
||||||
|
<text x="68" y="300" class="sans sub">browser</text>
|
||||||
|
<rect x="350" y="150" width="310" height="120" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="370" y="184" class="sans name">Auth service</text>
|
||||||
|
<text x="370" y="210" class="sans sub">signs tokens, Ed25519 private key</text>
|
||||||
|
<text x="370" y="232" class="sans sub">HttpOnly cookies</text>
|
||||||
|
<rect x="350" y="290" width="310" height="220" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="370" y="324" class="sans name">Core service</text>
|
||||||
|
<text x="370" y="350" class="sans sub">library, stats, suggestions</text>
|
||||||
|
<text x="370" y="372" class="sans sub">verifies tokens, public key only</text>
|
||||||
|
<rect x="740" y="150" width="180" height="230" rx="14" fill="#1a222c" stroke="#2f3a47"/>
|
||||||
|
<text x="760" y="178" class="sans cap" text-anchor="start">POSTGRESQL</text>
|
||||||
|
<rect x="760" y="192" width="140" height="56" rx="10" fill="#243040" stroke="#33475c"/>
|
||||||
|
<text x="830" y="227" class="mono chip" text-anchor="middle">auth db</text>
|
||||||
|
<rect x="760" y="302" width="140" height="56" rx="10" fill="#243040" stroke="#33475c"/>
|
||||||
|
<text x="830" y="337" class="mono chip" text-anchor="middle">core db</text>
|
||||||
|
<text x="1000" y="372" class="sans cap" text-anchor="start">EXTERNAL</text>
|
||||||
|
<rect x="1000" y="395" width="152" height="50" rx="10" fill="#212a35" stroke="#2f3a47" stroke-dasharray="4 4"/>
|
||||||
|
<text x="1076" y="427" class="sans chip" text-anchor="middle">Steam API</text>
|
||||||
|
<rect x="1000" y="460" width="152" height="50" rx="10" fill="#212a35" stroke="#2f3a47" stroke-dasharray="4 4"/>
|
||||||
|
<text x="1076" y="492" class="sans chip" text-anchor="middle">IGDB</text>
|
||||||
|
<path d="M 252 258 H 342" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<text x="297" y="248" class="sans arr" text-anchor="middle">login</text>
|
||||||
|
<path d="M 252 330 H 342" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<text x="297" y="320" class="sans arr" text-anchor="middle">API, cookie</text>
|
||||||
|
<path d="M 668 220 H 752" stroke="#26c6da" stroke-width="2" fill="none" marker-end="url(#arrowCyan)"/>
|
||||||
|
<path d="M 668 330 H 752" stroke="#26c6da" stroke-width="2" fill="none" marker-end="url(#arrowCyan)"/>
|
||||||
|
<path d="M 668 420 H 992" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<text x="830" y="410" class="sans arr" text-anchor="middle">sync</text>
|
||||||
|
<path d="M 668 485 H 992" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<text x="830" y="475" class="sans arr" text-anchor="middle">time to beat</text>
|
||||||
|
<line x1="64" y1="566" x2="1136" y2="566" stroke="#2a333e"/>
|
||||||
|
<text x="64" y="598" class="mono" fill="#5b6673" font-size="15">react · spring boot · postgresql · docker</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,84 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630" role="img" aria-labelledby="title desc">
|
||||||
|
<title id="title">Self-hosted infrastructure</title>
|
||||||
|
<desc id="desc">Architecture of the self-hosted infrastructure: a push to Gitea on the home server builds an image at home, Watchtower on the Hostinger VPS pulls it over Tailscale, and Traefik routes the public sites.</desc>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#1e2530"/>
|
||||||
|
<stop offset="100%" stop-color="#15181d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#26c6da"/>
|
||||||
|
<stop offset="100%" stop-color="#ffa726"/>
|
||||||
|
</linearGradient>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#4a5563"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrowCyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#26c6da"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
||||||
|
.sans { font-family: Roboto, Arial, sans-serif; }
|
||||||
|
.lbl { fill: #8b97a6; font-size: 17px; letter-spacing: .04em; }
|
||||||
|
.chip { fill: #cdd6e0; font-size: 19px; }
|
||||||
|
.cap { fill: #6c7887; font-size: 15px; letter-spacing: .18em; }
|
||||||
|
.name { fill: #cdd6e0; font-size: 22px; font-weight: 600; }
|
||||||
|
.sub { fill: #8b97a6; font-size: 16px; }
|
||||||
|
.arr { fill: #8b97a6; font-size: 15px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||||
|
<rect x="0" y="0" width="1200" height="4" fill="url(#accent)"/>
|
||||||
|
|
||||||
|
<text x="64" y="72" class="mono" fill="#26c6da" font-size="26" font-weight="600">Self-hosted infrastructure</text>
|
||||||
|
<text x="64" y="102" class="sans lbl">build at home, serve from the VPS</text>
|
||||||
|
<rect x="130" y="140" width="460" height="390" rx="18" fill="#1a222c" stroke="#26c6da" stroke-width="2"/>
|
||||||
|
<text x="154" y="168" class="sans cap" text-anchor="start">HOME SERVER "TRIBBLE"</text>
|
||||||
|
<rect x="154" y="186" width="412" height="130" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="174" y="220" class="sans name">Gitea</text>
|
||||||
|
<text x="174" y="246" class="sans sub">git, Actions runner, container registry</text>
|
||||||
|
<rect x="154" y="346" width="128" height="58" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="218" y="382" class="sans chip" style="font-size:16px" text-anchor="middle">Traefik</text>
|
||||||
|
<rect x="296" y="346" width="128" height="58" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="360" y="382" class="sans chip" style="font-size:16px" text-anchor="middle">Portainer</text>
|
||||||
|
<rect x="438" y="346" width="128" height="58" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="502" y="382" class="sans chip" style="font-size:16px" text-anchor="middle">Jellyfin</text>
|
||||||
|
<rect x="154" y="418" width="128" height="58" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="218" y="454" class="sans chip" style="font-size:16px" text-anchor="middle">AdGuard Home</text>
|
||||||
|
<rect x="296" y="418" width="128" height="58" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="360" y="454" class="sans chip" style="font-size:16px" text-anchor="middle">Paperless-ngx</text>
|
||||||
|
<rect x="438" y="418" width="128" height="58" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="502" y="454" class="sans chip" style="font-size:16px" text-anchor="middle">Tailscale</text>
|
||||||
|
<rect x="700" y="140" width="452" height="390" rx="18" fill="#1a222c" stroke="#ffa726" stroke-width="2"/>
|
||||||
|
<text x="724" y="168" class="sans cap" text-anchor="start">HOSTINGER VPS</text>
|
||||||
|
<rect x="724" y="206" width="404" height="90" rx="10" fill="#243040" stroke="#33475c"/>
|
||||||
|
<text x="744" y="240" class="sans name">Watchtower</text>
|
||||||
|
<text x="744" y="266" class="sans sub">pulls new images automatically</text>
|
||||||
|
<path d="M 736 296 V 484" stroke="#4a5563" stroke-width="2"/>
|
||||||
|
<text x="750" y="322" class="sans arr" text-anchor="start">deploy</text>
|
||||||
|
<rect x="752" y="330" width="216" height="52" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="770" y="362" class="sans chip" style="font-size:17px" text-anchor="start">Playground (this site)</text>
|
||||||
|
<path d="M 736 356 H 750" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M 1000 356 H 976" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="752" y="394" width="216" height="52" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="770" y="426" class="sans chip" style="font-size:17px" text-anchor="start">Pepper Pepperoni</text>
|
||||||
|
<path d="M 736 420 H 750" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M 1000 420 H 976" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="752" y="458" width="216" height="52" rx="10" fill="#212a35" stroke="#2f3a47"/>
|
||||||
|
<text x="770" y="490" class="sans chip" style="font-size:17px" text-anchor="start">To Be Played</text>
|
||||||
|
<path d="M 736 484 H 750" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M 1000 484 H 976" stroke="#4a5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="1000" y="330" width="128" height="180" rx="10" fill="#2a2118" stroke="#5c4326"/>
|
||||||
|
<text x="1016" y="368" class="sans name" text-anchor="start">Traefik</text>
|
||||||
|
<text x="1016" y="394" class="sans sub" text-anchor="start">TLS, routing</text>
|
||||||
|
<path d="M 40 251 H 150" stroke="#26c6da" stroke-width="2" fill="none" marker-end="url(#arrowCyan)"/>
|
||||||
|
<text x="44" y="240" class="mono arr" text-anchor="start">git push</text>
|
||||||
|
<path d="M 574 251 H 716" stroke="#26c6da" stroke-width="2" fill="none" marker-end="url(#arrowCyan)"/>
|
||||||
|
<text x="645" y="214" class="sans arr" text-anchor="middle">new image</text>
|
||||||
|
<text x="645" y="234" class="sans arr" text-anchor="middle">via Tailscale</text>
|
||||||
|
<line x1="64" y1="566" x2="1136" y2="566" stroke="#2a333e"/>
|
||||||
|
<text x="64" y="598" class="mono" fill="#5b6673" font-size="15">gitea · docker · traefik · tailscale · watchtower</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
Reference in new issue
Block a user