import { Component, computed, inject, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from "rxjs"; import { MatCardModule } from "@angular/material/card"; import { MatChipsModule } from "@angular/material/chips"; import { MatIcon } from "@angular/material/icon"; import { TranslatePipe } from "@ngx-translate/core"; import { MatButtonModule } from "@angular/material/button"; import { MatDialog } from "@angular/material/dialog"; import { ProjectDialogComponent } from "./dialog/project-dialog.component"; import { AssetsConstants } from "../../constants/AssetsConstants"; export interface Projects { identifier: string; title: string, shortDescription: string, introduction: string, images: { url: string, source: string }[], diagram?: string, icon: string, assets: string, links: { name: string, url: string }[], bulletPoints: string[], challenges: string[], learnings: string[], isFeatured: boolean, technologies: string[] } @Component({ selector: 'app-projects', imports: [ MatCardModule, MatChipsModule, MatIcon, TranslatePipe, MatButtonModule ], templateUrl: './projects.component.html', styleUrl: './projects.component.scss', schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class ProjectsComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); private readonly dialog = inject(MatDialog); private readonly router = inject(Router); 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: '' })), diagram: AssetsConstants.TBP_DIAGRAM, 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', 'PROJECTS.TBP.BULLET_5', ], challenges: [ 'PROJECTS.TBP.CHALLENGE_1', 'PROJECTS.TBP.CHALLENGE_2', 'PROJECTS.TBP.CHALLENGE_3', 'PROJECTS.TBP.CHALLENGE_4', ], learnings: [ 'PROJECTS.TBP.LEARNING_1', 'PROJECTS.TBP.LEARNING_2', ], isFeatured: true, technologies: ['Java', 'Spring Boot', 'PostgreSQL', 'React', 'Tailwind CSS', 'Docker'] }, { identifier: "claude-in-a-box", title: 'PROJECTS.CLAUDE_BOX.TITLE', shortDescription: 'PROJECTS.CLAUDE_BOX.SHORT_DESCRIPTION', introduction: 'PROJECTS.CLAUDE_BOX.INTRODUCTION', images: [], diagram: AssetsConstants.CLAUDE_BOX_DIAGRAM, icon: 'smart_toy', assets: '', links: [], bulletPoints: [ 'PROJECTS.CLAUDE_BOX.BULLET_1', 'PROJECTS.CLAUDE_BOX.BULLET_2', 'PROJECTS.CLAUDE_BOX.BULLET_3', 'PROJECTS.CLAUDE_BOX.BULLET_4', 'PROJECTS.CLAUDE_BOX.BULLET_5', 'PROJECTS.CLAUDE_BOX.BULLET_6', ], challenges: [ 'PROJECTS.CLAUDE_BOX.CHALLENGE_1', 'PROJECTS.CLAUDE_BOX.CHALLENGE_2', 'PROJECTS.CLAUDE_BOX.CHALLENGE_3', 'PROJECTS.CLAUDE_BOX.CHALLENGE_4', ], learnings: [ 'PROJECTS.CLAUDE_BOX.LEARNING_1', 'PROJECTS.CLAUDE_BOX.LEARNING_2', 'PROJECTS.CLAUDE_BOX.LEARNING_3', ], isFeatured: false, technologies: ['Claude Code', 'MCP', 'Docker', 'Bash', 'Node.js', 'Playwright'] }, { identifier: "playground", title: 'PROJECTS.PLAYGROUND.TITLE', shortDescription: 'PROJECTS.PLAYGROUND.SHORT_DESCRIPTION', introduction: 'PROJECTS.PLAYGROUND.INTRODUCTION', images: AssetsConstants.PLAYGROUND_IMAGES.map(url => ({ url, source: '' })), diagram: AssetsConstants.PLAYGROUND_DIAGRAM, icon: 'web', assets: '', links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://andreas-dahm.eu' }], bulletPoints: [ 'PROJECTS.PLAYGROUND.BULLET_1', 'PROJECTS.PLAYGROUND.BULLET_2', 'PROJECTS.PLAYGROUND.BULLET_3', 'PROJECTS.PLAYGROUND.BULLET_4', 'PROJECTS.PLAYGROUND.BULLET_5', 'PROJECTS.PLAYGROUND.BULLET_6', ], challenges: [ 'PROJECTS.PLAYGROUND.CHALLENGE_1', 'PROJECTS.PLAYGROUND.CHALLENGE_2', 'PROJECTS.PLAYGROUND.CHALLENGE_3', ], learnings: [ 'PROJECTS.PLAYGROUND.LEARNING_1', 'PROJECTS.PLAYGROUND.LEARNING_2', 'PROJECTS.PLAYGROUND.LEARNING_3', ], isFeatured: false, technologies: ['Angular', 'TypeScript', 'Tailwind CSS', 'WebGPU', 'GLSL', 'Babylon.js', 'Gitea Actions', 'Docker', 'Traefik', 'nginx', 'Tailscale'] }, { identifier: "elmucho", title: 'PROJECTS.EL_MUCHO.TITLE', shortDescription: 'PROJECTS.EL_MUCHO.SHORT_DESCRIPTION', introduction: 'PROJECTS.EL_MUCHO.INTRODUCTION', images: AssetsConstants.EL_MUCHO_IMAGES.map(url => ({ url, source: '' })), icon: 'sports_esports', assets: '', links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://store.steampowered.com/app/1532640/El_Mucho/' }], bulletPoints: [ 'PROJECTS.EL_MUCHO.BULLET_1', 'PROJECTS.EL_MUCHO.BULLET_2', 'PROJECTS.EL_MUCHO.BULLET_3', 'PROJECTS.EL_MUCHO.BULLET_4', ], 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: false, technologies: ['Unity', 'C#', 'Steamworks'] }, { identifier: "gamejams", title: 'PROJECTS.GAME_JAMS.TITLE', shortDescription: 'PROJECTS.GAME_JAMS.SHORT_DESCRIPTION', introduction: 'PROJECTS.GAME_JAMS.INTRODUCTION', images: AssetsConstants.GAME_JAMS_IMAGES.map(url => ({ url, source: '' })), icon: 'videogame_asset', assets: '', links: [ { name: 'Peter Blue and the carnival of death', url: 'https://lobothedark.itch.io/beansjam' }, { name: 'Arkonauts', url: 'https://lobothedark.itch.io/arkonauts' }, { name: 'Sloth Bros', url: 'https://lobothedark.itch.io/sloth-bros' }, { name: 'PROJECTS.GAME_JAMS.LINK_COLLECTION', url: 'https://itch.io/c/6628860/lobos-collection' } ], bulletPoints: [ 'PROJECTS.GAME_JAMS.BULLET_1', 'PROJECTS.GAME_JAMS.BULLET_2', 'PROJECTS.GAME_JAMS.BULLET_3', 'PROJECTS.GAME_JAMS.BULLET_4', ], challenges: [ 'PROJECTS.GAME_JAMS.CHALLENGE_1', 'PROJECTS.GAME_JAMS.CHALLENGE_2', ], learnings: [ 'PROJECTS.GAME_JAMS.LEARNING_1', 'PROJECTS.GAME_JAMS.LEARNING_2', ], isFeatured: false, technologies: ['Unity', 'C#'] }, { identifier: "diploma", title: 'PROJECTS.DIPLOMA.TITLE', shortDescription: 'PROJECTS.DIPLOMA.SHORT_DESCRIPTION', introduction: 'PROJECTS.DIPLOMA.INTRODUCTION', images: AssetsConstants.DIPLOMA_IMAGES.map(url => ({ url, source: '' })), icon: 'history_edu', assets: AssetsConstants.DIPLOMA, links: [{ name: 'PROJECTS.LINK_TO_PROJECT', url: 'https://www.th-bingen.de' }], bulletPoints: [ 'PROJECTS.DIPLOMA.BULLET_1', 'PROJECTS.DIPLOMA.BULLET_2', 'PROJECTS.DIPLOMA.BULLET_3', 'PROJECTS.DIPLOMA.BULLET_4', 'PROJECTS.DIPLOMA.BULLET_5', ], challenges: [ 'PROJECTS.DIPLOMA.CHALLENGE_1', 'PROJECTS.DIPLOMA.CHALLENGE_2', 'PROJECTS.DIPLOMA.CHALLENGE_3', ], learnings: [ 'PROJECTS.DIPLOMA.LEARNING_1', 'PROJECTS.DIPLOMA.LEARNING_2', ], isFeatured: false, technologies: ['Java', 'BVH / k-DOP', 'Collision detection', 'Cloth simulation'] }, { identifier: "tribble-the-homeserver", title: 'PROJECTS.TRIBBLE.TITLE', shortDescription: 'PROJECTS.TRIBBLE.SHORT_DESCRIPTION', introduction: 'PROJECTS.TRIBBLE.INTRODUCTION', images: [ { 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/' }, { name: 'AdGuard Home', url: 'https://adguard.com/en/adguard-home/overview.html' }, { name: 'Paperless-ngx', url: 'https://paperless-ngx.com/' }, { name: 'Tailscale', url: 'https://tailscale.com/' }, { name: 'Watchtower', url: 'https://containrrr.dev/watchtower/' } ], bulletPoints: [ 'PROJECTS.TRIBBLE.BULLET_1', 'PROJECTS.TRIBBLE.BULLET_2', 'PROJECTS.TRIBBLE.BULLET_3', 'PROJECTS.TRIBBLE.BULLET_4', 'PROJECTS.TRIBBLE.BULLET_5', ], challenges: [ 'PROJECTS.TRIBBLE.CHALLENGE_1', 'PROJECTS.TRIBBLE.CHALLENGE_2', ], learnings: [ 'PROJECTS.TRIBBLE.LEARNING_1', 'PROJECTS.TRIBBLE.LEARNING_2', ], isFeatured: false, technologies: ['Ubuntu Server', 'Docker', 'Portainer', 'Traefik', 'Gitea', 'Jellyfin', 'AdGuard Home', 'Paperless-ngx', 'Tailscale', 'Watchtower'] } ] 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 { if (this.isBrowser) { window.scrollTo({ top: 0, behavior: 'smooth' }); } setTimeout(() => { this.dialogOpenFunction(); }, 10); } ngOnDestroy(): void { if (this.queryParamSub) { this.queryParamSub.unsubscribe(); } } private dialogOpenFunction(): void { this.queryParamSub = this.route.queryParamMap.subscribe(params => { const projectIdentifier = params.get('project'); if (projectIdentifier) { const project = this.allProjects.find(p => p.identifier === projectIdentifier); if (project) { this.openProjectDialog(project); } } }); } openProjectDialog(project: Projects) { this.dialog.open(ProjectDialogComponent, { data: project, width: '90vw', maxWidth: '900px', autoFocus: false, restoreFocus: false }); } }