Files
website/src/components/tech-stack-carousel/tech-stack-carousel.ts
Tim Kainz 7108f22e8b
All checks were successful
publish.yml / publish (push) Successful in 1m4s
simplified carousel
2026-04-24 00:08:08 +02:00

95 lines
3.6 KiB
TypeScript

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<HTMLElement>;
private autoScrollIntervalId: ReturnType<typeof setInterval> | 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;
}
}