import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; import { CardComponent } from '../card/card'; interface Skill { name: string; category: 'Frontend' | 'Backend' | 'Mobile' | 'Cloud' | 'Frontend / Backend' | 'Deployment'; level: string; logoUrl: string; fallbackLabel: string; } @Component({ selector: 'app-tech-stack-carousel', imports: [CardComponent], templateUrl: './tech-stack-carousel.html', styleUrl: './tech-stack-carousel.css', }) export class TechStackCarousel implements AfterViewInit, OnDestroy { @ViewChild('stackCarousel') private readonly stackCarousel?: ElementRef; private autoScrollIntervalId: ReturnType | null = null; private readonly autoScrollIntervalMs = 5000; readonly skills: Skill[] = [ { name: 'Angular', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/angular.svg', fallbackLabel: 'NG' }, { name: 'React', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/react.svg', fallbackLabel: 'RE' }, { name: 'Ionic', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/ionic.svg', fallbackLabel: 'IO' }, { name: 'TypeScript', category: 'Frontend / Backend', level: 'Advanced', logoUrl: '/tech-logos/typescript.svg', fallbackLabel: 'TS' }, { name: 'Javascript', category: 'Frontend / Backend', level: 'Advanced', logoUrl: '/tech-logos/javascript.svg', fallbackLabel: 'JS' }, { name: 'Docker', category: 'Deployment', level: 'Advanced', logoUrl: '/tech-logos/docker.svg', fallbackLabel: 'DK' }, { name: 'C# / .NET', category: 'Frontend / Backend', level: 'Advanced', logoUrl: '/tech-logos/csharp.svg', fallbackLabel: 'CS' }, { name: 'ASP.NET Core', category: 'Backend', level: 'Advanced', logoUrl: '/tech-logos/dotnet.svg', fallbackLabel: 'AS' }, { name: 'WPF', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/wpf.svg', fallbackLabel: 'WP' }, { name: 'Java', category: 'Backend', level: 'Advanced', logoUrl: '/tech-logos/java.svg', fallbackLabel: 'JV' }, { name: 'Spring Boot', category: 'Backend', level: 'Advanced', logoUrl: '/tech-logos/spring.svg', fallbackLabel: 'SB' }, { name: 'Flutter', category: 'Mobile', level: 'Advanced', logoUrl: '/tech-logos/flutter.svg', fallbackLabel: 'FL' }, ]; ngAfterViewInit(): void { this.startAutoScroll(); } ngOnDestroy(): void { this.stopAutoScroll(); } scrollStack(direction: -1 | 1): void { const carousel = this.stackCarousel?.nativeElement; if (!carousel) { return; } const maxScrollLeft = Math.max(carousel.scrollWidth - carousel.clientWidth, 0); if (maxScrollLeft <= 0) { return; } if (direction === 1 && carousel.scrollLeft >= maxScrollLeft) { carousel.scrollTo({ left: 0, behavior: 'auto' }); return; } carousel.scrollBy({ left: this.getScrollStep(carousel) * direction, behavior: 'smooth' }); } onLogoError(event: Event): void { const image = event.target as HTMLImageElement | null; const wrapper = image?.closest('.skill-icon'); wrapper?.classList.add('is-fallback'); } private getScrollStep(carousel: HTMLElement): number { return Math.max(carousel.clientWidth * 0.82, 260); } private startAutoScroll(): void { if (this.autoScrollIntervalId !== null) { return; } this.autoScrollIntervalId = setInterval(() => { this.scrollStack(1); }, this.autoScrollIntervalMs); } private stopAutoScroll(): void { if (this.autoScrollIntervalId === null) { return; } clearInterval(this.autoScrollIntervalId); this.autoScrollIntervalId = null; } }