extract carousel to component
All checks were successful
publish.yml / publish (push) Successful in 1m3s
All checks were successful
publish.yml / publish (push) Successful in 1m3s
This commit is contained in:
118
src/components/tech-stack-carousel/tech-stack-carousel.ts
Normal file
118
src/components/tech-stack-carousel/tech-stack-carousel.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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 autoScrollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
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 {
|
||||
const prefersReducedMotion =
|
||||
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resumeAutoScroll();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.pauseAutoScroll();
|
||||
}
|
||||
|
||||
scrollStack(direction: -1 | 1): void {
|
||||
const carousel = this.stackCarousel?.nativeElement;
|
||||
if (!carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
carousel.scrollBy({ left: this.getScrollStep(carousel) * direction, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
pauseAutoScroll(): void {
|
||||
if (!this.autoScrollTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(this.autoScrollTimer);
|
||||
this.autoScrollTimer = null;
|
||||
}
|
||||
|
||||
resumeAutoScroll(): void {
|
||||
if (this.autoScrollTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.autoScrollTimer = setInterval(() => {
|
||||
this.autoAdvanceStack();
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
onCarouselFocusOut(event: FocusEvent): void {
|
||||
const next = event.relatedTarget as Node | null;
|
||||
const shell = event.currentTarget as HTMLElement | null;
|
||||
|
||||
if (!shell?.contains(next)) {
|
||||
this.resumeAutoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
onLogoError(event: Event): void {
|
||||
const image = event.target as HTMLImageElement | null;
|
||||
const wrapper = image?.closest('.skill-icon');
|
||||
wrapper?.classList.add('is-fallback');
|
||||
}
|
||||
|
||||
private autoAdvanceStack(): void {
|
||||
const carousel = this.stackCarousel?.nativeElement;
|
||||
if (!carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxScroll = carousel.scrollWidth - carousel.clientWidth;
|
||||
if (maxScroll <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (carousel.scrollLeft >= maxScroll - 4) {
|
||||
carousel.scrollTo({ left: 0, behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
|
||||
carousel.scrollBy({ left: this.getScrollStep(carousel), behavior: 'smooth' });
|
||||
}
|
||||
|
||||
private getScrollStep(carousel: HTMLElement): number {
|
||||
return Math.max(carousel.clientWidth * 0.82, 260);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user