95 lines
3.6 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
|