From eb1dd36195f950e196826e8ad1009ecdc8e8c995 Mon Sep 17 00:00:00 2001 From: Tim Kainz Date: Thu, 16 Apr 2026 17:02:27 +0200 Subject: [PATCH] add icons to technologies --- public/tech-logos/angular.svg | 1 + public/tech-logos/csharp.svg | 1 + public/tech-logos/docker.svg | 1 + public/tech-logos/dotnet.svg | 1 + public/tech-logos/flutter.svg | 1 + public/tech-logos/ionic.svg | 1 + public/tech-logos/java.svg | 1 + public/tech-logos/javascript.svg | 1 + public/tech-logos/react.svg | 1 + public/tech-logos/spring.svg | 1 + public/tech-logos/typescript.svg | 1 + public/tech-logos/wpf.svg | 1 + src/pages/home/home.css | 129 +++++++++++++++++++++++++++++++ src/pages/home/home.html | 37 +++++++-- src/pages/home/home.spec.ts | 14 ++++ src/pages/home/home.ts | 46 +++++++---- 16 files changed, 218 insertions(+), 20 deletions(-) create mode 100644 public/tech-logos/angular.svg create mode 100644 public/tech-logos/csharp.svg create mode 100644 public/tech-logos/docker.svg create mode 100644 public/tech-logos/dotnet.svg create mode 100644 public/tech-logos/flutter.svg create mode 100644 public/tech-logos/ionic.svg create mode 100644 public/tech-logos/java.svg create mode 100644 public/tech-logos/javascript.svg create mode 100644 public/tech-logos/react.svg create mode 100644 public/tech-logos/spring.svg create mode 100644 public/tech-logos/typescript.svg create mode 100644 public/tech-logos/wpf.svg diff --git a/public/tech-logos/angular.svg b/public/tech-logos/angular.svg new file mode 100644 index 0000000..efab186 --- /dev/null +++ b/public/tech-logos/angular.svg @@ -0,0 +1 @@ +Angular \ No newline at end of file diff --git a/public/tech-logos/csharp.svg b/public/tech-logos/csharp.svg new file mode 100644 index 0000000..567a363 --- /dev/null +++ b/public/tech-logos/csharp.svg @@ -0,0 +1 @@ +sharp \ No newline at end of file diff --git a/public/tech-logos/docker.svg b/public/tech-logos/docker.svg new file mode 100644 index 0000000..3722516 --- /dev/null +++ b/public/tech-logos/docker.svg @@ -0,0 +1 @@ +Docker \ No newline at end of file diff --git a/public/tech-logos/dotnet.svg b/public/tech-logos/dotnet.svg new file mode 100644 index 0000000..9899d75 --- /dev/null +++ b/public/tech-logos/dotnet.svg @@ -0,0 +1 @@ +.NET \ No newline at end of file diff --git a/public/tech-logos/flutter.svg b/public/tech-logos/flutter.svg new file mode 100644 index 0000000..da4bcc7 --- /dev/null +++ b/public/tech-logos/flutter.svg @@ -0,0 +1 @@ +Flutter \ No newline at end of file diff --git a/public/tech-logos/ionic.svg b/public/tech-logos/ionic.svg new file mode 100644 index 0000000..bb440a1 --- /dev/null +++ b/public/tech-logos/ionic.svg @@ -0,0 +1 @@ +Ionic \ No newline at end of file diff --git a/public/tech-logos/java.svg b/public/tech-logos/java.svg new file mode 100644 index 0000000..7f83254 --- /dev/null +++ b/public/tech-logos/java.svg @@ -0,0 +1 @@ +OpenJDK \ No newline at end of file diff --git a/public/tech-logos/javascript.svg b/public/tech-logos/javascript.svg new file mode 100644 index 0000000..6b62449 --- /dev/null +++ b/public/tech-logos/javascript.svg @@ -0,0 +1 @@ +JavaScript \ No newline at end of file diff --git a/public/tech-logos/react.svg b/public/tech-logos/react.svg new file mode 100644 index 0000000..fe27542 --- /dev/null +++ b/public/tech-logos/react.svg @@ -0,0 +1 @@ +React \ No newline at end of file diff --git a/public/tech-logos/spring.svg b/public/tech-logos/spring.svg new file mode 100644 index 0000000..28ac4cb --- /dev/null +++ b/public/tech-logos/spring.svg @@ -0,0 +1 @@ +Spring \ No newline at end of file diff --git a/public/tech-logos/typescript.svg b/public/tech-logos/typescript.svg new file mode 100644 index 0000000..3ce7315 --- /dev/null +++ b/public/tech-logos/typescript.svg @@ -0,0 +1 @@ +TypeScript \ No newline at end of file diff --git a/public/tech-logos/wpf.svg b/public/tech-logos/wpf.svg new file mode 100644 index 0000000..614e072 --- /dev/null +++ b/public/tech-logos/wpf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/home/home.css b/src/pages/home/home.css index 181ec4d..c14b749 100644 --- a/src/pages/home/home.css +++ b/src/pages/home/home.css @@ -140,6 +140,117 @@ h2 { outline: none; } +.stack-carousel-shell { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.75rem; + align-items: center; +} + +.stack-carousel { + display: flex; + gap: 1rem; + overflow-x: auto; + padding: 0.2rem; + scroll-snap-type: x mandatory; + scrollbar-width: thin; + scrollbar-color: rgba(88, 166, 255, 0.45) rgba(33, 38, 45, 0.65); +} + +.stack-slide { + flex: 0 0 min(320px, calc(100% - 0.2rem)); + scroll-snap-align: start; +} + +.skill-heading { + display: flex; + align-items: center; + gap: 0.65rem; +} + +.skill-icon { + display: inline-flex; + justify-content: center; + align-items: center; + width: 2rem; + height: 2rem; + border-radius: 10px; + border: 1px solid rgba(88, 166, 255, 0.4); + background: radial-gradient(circle at 30% 20%, rgba(88, 166, 255, 0.2), rgba(22, 27, 34, 0.95)); + box-shadow: inset 0 1px 0 rgba(240, 246, 252, 0.08); + overflow: hidden; + transition: border-color 140ms ease, box-shadow 140ms ease, background-color 140ms ease; +} + +.skill-logo { + width: 1.2rem; + height: 1.2rem; + object-fit: contain; + opacity: 0.92; + filter: grayscale(1) brightness(1.25) contrast(0.92); + transition: transform 140ms ease, opacity 140ms ease, filter 140ms ease; +} + +.skill-fallback { + display: none; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.04em; + color: #9ecbff; +} + +.skill-icon.is-fallback .skill-logo { + display: none; +} + +.skill-icon.is-fallback .skill-fallback { + display: inline; +} + +.skill-card .meta { + margin-top: 0.9rem; +} + +.skill-card { + transition: border-color 140ms ease, box-shadow 140ms ease; +} + +.stack-slide:hover .skill-icon, +.stack-slide:focus-within .skill-icon { + border-color: rgba(121, 192, 255, 0.75); + box-shadow: 0 0 0 3px rgba(56, 139, 253, 0.18), inset 0 1px 0 rgba(240, 246, 252, 0.14); + background: radial-gradient(circle at 30% 20%, rgba(121, 192, 255, 0.28), rgba(22, 27, 34, 0.95)); +} + +.stack-slide:hover .skill-logo, +.stack-slide:focus-within .skill-logo { + opacity: 1; + transform: scale(1.04); + filter: grayscale(1) brightness(1.4) contrast(1); +} + +.carousel-btn { + width: 2.5rem; + height: 2.5rem; + border: 1px solid #3d444d; + border-radius: 999px; + background: rgba(33, 38, 45, 0.8); + color: #e6edf3; + cursor: pointer; + transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease; +} + +.carousel-btn:hover { + border-color: #58a6ff; + background: rgba(56, 139, 253, 0.2); + transform: translateY(-1px); +} + +.carousel-btn:focus-visible { + outline: 2px solid #58a6ff; + outline-offset: 2px; +} + .card h3 { margin: 0; font-size: 1.08rem; @@ -167,4 +278,22 @@ h2 { .hero { padding: 2rem 1.2rem; } + + .stack-carousel-shell { + grid-template-columns: 1fr; + } + + .carousel-btn { + display: none; + } + + .stack-slide { + flex-basis: min(280px, 85%); + } +} + +@media (min-width: 1000px) { + .stack-slide { + flex-basis: calc((100% - 2rem) / 3); + } } diff --git a/src/pages/home/home.html b/src/pages/home/home.html index 195c311..eac0311 100644 --- a/src/pages/home/home.html +++ b/src/pages/home/home.html @@ -34,13 +34,36 @@

Tech Stack

Primary technologies I use to deliver production-ready software.

-
- @for (skill of skills; track skill.name) { - -

{{ skill.name }}

-

{{ skill.category }} - {{ skill.level }}

-
- } + diff --git a/src/pages/home/home.spec.ts b/src/pages/home/home.spec.ts index 9d17aed..c95018c 100644 --- a/src/pages/home/home.spec.ts +++ b/src/pages/home/home.spec.ts @@ -37,4 +37,18 @@ describe('Home', () => { expect(links.length).toBe(component.projects.length); expect(links[0]?.getAttribute('href')).toContain('/projects/tasktimer'); }); + + it('should render stack carousel with icon badges', () => { + const compiled = fixture.nativeElement as HTMLElement; + const carousel = compiled.querySelector('#stack .stack-carousel'); + const icons = compiled.querySelectorAll('#stack .skill-icon'); + const logos = compiled.querySelectorAll('#stack .skill-logo'); + const controls = compiled.querySelectorAll('#stack .carousel-btn'); + + expect(carousel).toBeTruthy(); + expect(icons.length).toBe(component.skills.length); + expect(logos.length).toBe(component.skills.length); + expect(logos[0]?.getAttribute('src')).toContain('/tech-logos/'); + expect(controls.length).toBe(2); + }); }); diff --git a/src/pages/home/home.ts b/src/pages/home/home.ts index 5ceb9db..ee1bdb1 100644 --- a/src/pages/home/home.ts +++ b/src/pages/home/home.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; import { RouterLink } from '@angular/router'; import { CardComponent } from '../../components/card/card'; import { PROJECTS, Project } from '../projects/project-data'; @@ -7,6 +7,8 @@ interface Skill { name: string; category: 'Frontend' | 'Backend' | 'Mobile' | 'Cloud' | 'Frontend / Backend' | 'Deployment'; level: string; + logoUrl: string; + fallbackLabel: string; } @Component({ @@ -16,6 +18,8 @@ interface Skill { styleUrl: './home.css', }) export class Home { + @ViewChild('stackCarousel') private readonly stackCarousel?: ElementRef; + readonly hero = { name: 'Tim Kainz', role: 'Fullstack Developer from Austria', @@ -24,19 +28,35 @@ export class Home { }; readonly skills: Skill[] = [ - { name: 'Angular', category: 'Frontend', level: 'Advanced' }, - { name: 'React', category: 'Frontend', level: 'Advanced' }, - { name: 'Ionic', category: 'Frontend', level: 'Advanced' }, - { name: 'TypeScript', category: 'Frontend / Backend', level: 'Advanced' }, - { name: 'Javascript', category: 'Frontend / Backend', level: 'Advanced' }, - { name: 'Docker', category: 'Deployment', level: 'Advanced' }, - { name: 'C# / .NET', category: 'Frontend / Backend', level: 'Advanced' }, - { name: 'ASP.NET Core', category: 'Backend', level: 'Advanced' }, - { name: 'WPF', category: 'Frontend', level: 'Advanced' }, - { name: 'Java', category: 'Backend', level: 'Advanced' }, - { name: 'Spring Boot', category: 'Backend', level: 'Advanced' }, - { name: 'Flutter', category: 'Mobile', level: 'Advanced' }, + { 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' }, ]; readonly projects: Project[] = PROJECTS; + + scrollStack(direction: -1 | 1): void { + const carousel = this.stackCarousel?.nativeElement; + if (!carousel) { + return; + } + + const cardWidth = Math.max(carousel.clientWidth * 0.82, 260); + carousel.scrollBy({ left: cardWidth * 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'); + } }