Refactoring tech stack into a reusable component

This commit is contained in:
AdamBtech
2025-06-02 19:42:38 +02:00
parent e6b6fe4eac
commit c1d05c005e
10 changed files with 106 additions and 179 deletions

View File

@@ -125,65 +125,6 @@
text-shadow: 0 1px 2px rgba(0,0,0,0.1); text-shadow: 0 1px 2px rgba(0,0,0,0.1);
} }
/* Tech Stack Section */
.tech-section {
margin-top: var(--card-gap);
padding-top: var(--card-gap);
border-top: 1px solid var(--color-nier-border);
position: relative;
}
.tech-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0;
height: 1px;
background: var(--color-nier-accent);
animation: expandLine 0.6s ease-out calc(var(--animation-delay-base) * 12) forwards;
}
.tech-grid {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.5rem;
}
/* Tech Items */
.tech-item {
background: linear-gradient(135deg, var(--color-nier-accent) 0%, var(--color-nier-highlight) 100%);
color: var(--color-nier-text-light);
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
font-family: var(--font-terminal);
text-transform: uppercase;
letter-spacing: 0.05em;
border-radius: 3px;
white-space: nowrap;
transition: var(--transition-smooth);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
/* Initial state for staggered animation */
opacity: 0;
transform: scale(0.8) translateY(10px);
animation: techItemFadeIn 0.4s ease-out forwards;
}
/* Staggered animation delays for tech items */
.tech-item:nth-child(1) { animation-delay: calc(var(--animation-delay-base) * 13); }
.tech-item:nth-child(2) { animation-delay: calc(var(--animation-delay-base) * 14); }
.tech-item:nth-child(3) { animation-delay: calc(var(--animation-delay-base) * 15); }
.tech-item:nth-child(4) { animation-delay: calc(var(--animation-delay-base) * 16); }
.tech-item:nth-child(5) { animation-delay: calc(var(--animation-delay-base) * 17); }
.tech-item:nth-child(n+6) { animation-delay: calc(var(--animation-delay-base) * 18); }
.tech-item:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
}
/* Status Badges */ /* Status Badges */
.current-status { .current-status {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
@@ -392,38 +333,14 @@
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.detail-block {
margin-bottom: 1rem;
padding: 0.5rem 0.25rem;
}
.detail-label {
font-size: 0.7rem;
margin-bottom: 0.375rem;
}
.detail-value { .detail-value {
font-size: 0.8rem; font-size: 0.8rem;
line-height: 1.4; line-height: 1.4;
} }
.tech-section {
margin-top: 1.25rem;
padding-top: 1rem;
}
.tech-item {
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
margin-bottom: 0.25rem;
/* Simplified tech item animation */
animation-name: techItemFadeIn;
animation-duration: 0.2s;
animation-timing-function: ease-out;
animation-delay: 0.6s;
animation-fill-mode: forwards;
}
.current-status { .current-status {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;

View File

@@ -15,15 +15,7 @@
<div class="experience-body"> <div class="experience-body">
<app-detail-block label="PROGRAM:" [value]="training.classification" /> <app-detail-block label="PROGRAM:" [value]="training.classification" />
<app-detail-block label="INSTITUTION:" [value]="training.timeline" /> <app-detail-block label="INSTITUTION:" [value]="training.timeline" />
<app-tech-stack label="CURRICULUM:" [techStack]="training.techStack"/>
<div class="tech-section">
<div class="detail-label">CURRICULUM:</div>
<div class="tech-grid">
@for (tech of training.techStack; track tech) {
<span class="tech-item">{{ tech }}</span>
}
</div>
</div>
</div> </div>
</article> </article>
} }

View File

@@ -2,15 +2,16 @@ import { Component, signal } from '@angular/core';
import { type Experience } from '../../../../shared/models/experience.model'; import { type Experience } from '../../../../shared/models/experience.model';
import { DetailBlockComponent } from '../shared/detail-block/detail-block.component'; import { DetailBlockComponent } from '../shared/detail-block/detail-block.component';
import { InfoCardComponent } from '../shared/info-card/info-card.component'; import { InfoCardComponent } from '../shared/info-card/info-card.component';
import { TechStackComponent } from '../tech-stack/tech-stack.component';
@Component({ @Component({
selector: 'app-column-education-info', selector: 'app-column-education-info',
imports: [DetailBlockComponent, InfoCardComponent], imports: [DetailBlockComponent, InfoCardComponent, TechStackComponent],
templateUrl: './column-education-info.component.html', templateUrl: './column-education-info.component.html',
styleUrl: './column-education-info.component.css', styleUrl: './column-education-info.component.css',
}) })
export class ColumnEducationInfoComponent { export class ColumnEducationInfoComponent {
private readonly _currentTraining = signal<Experience>({ private _currentTraining = signal<Experience>({
id: 'developer-training', id: 'developer-training',
title: 'DEVELOPER_TRAINING.EXE', title: 'DEVELOPER_TRAINING.EXE',
classification: "Concepteur Développeur d'Applications (RNCP Level BAC+4)", classification: "Concepteur Développeur d'Applications (RNCP Level BAC+4)",
@@ -28,5 +29,5 @@ export class ColumnEducationInfoComponent {
isCurrent: true, isCurrent: true,
} as const); } as const);
readonly currentTraining = this._currentTraining.asReadonly(); currentTraining = this._currentTraining.asReadonly();
} }

View File

@@ -150,64 +150,6 @@
color: var(--color-nier-text-light); color: var(--color-nier-text-light);
} }
/* Tech Stack Section */
.tech-section {
margin-top: var(--card-gap);
padding-top: var(--card-gap);
border-top: 1px solid var(--color-nier-border);
position: relative;
}
.tech-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0;
height: 1px;
background: var(--color-nier-accent);
animation: expandLine 0.6s ease-out calc(var(--animation-delay-base) * 12) forwards;
}
.tech-grid {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.5rem;
}
/* Tech Items */
.tech-item {
background: linear-gradient(135deg, var(--color-nier-accent) 0%, var(--color-nier-highlight) 100%);
color: var(--color-nier-text-light);
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
font-family: var(--font-terminal);
text-transform: uppercase;
letter-spacing: 0.05em;
border-radius: 3px;
white-space: nowrap;
transition: var(--transition-smooth);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
/* Initial state for staggered animation */
opacity: 0;
transform: scale(0.8) translateY(10px);
animation: techItemFadeIn 0.4s ease-out forwards;
}
/* Staggered animation delays for tech items */
.tech-item:nth-child(1) { animation-delay: calc(var(--animation-delay-base) * 13); }
.tech-item:nth-child(2) { animation-delay: calc(var(--animation-delay-base) * 14); }
.tech-item:nth-child(3) { animation-delay: calc(var(--animation-delay-base) * 15); }
.tech-item:nth-child(4) { animation-delay: calc(var(--animation-delay-base) * 16); }
.tech-item:nth-child(5) { animation-delay: calc(var(--animation-delay-base) * 17); }
.tech-item:nth-child(n+6) { animation-delay: calc(var(--animation-delay-base) * 18); }
.tech-item:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
}
/* Visual Effects */ /* Visual Effects */
.corner-accent { .corner-accent {

View File

@@ -24,15 +24,7 @@
<div class="experience-body"> <div class="experience-body">
<app-detail-block label="CLASSIFICATION:" [value]="experience.classification"/> <app-detail-block label="CLASSIFICATION:" [value]="experience.classification"/>
<app-detail-block label="TIMELINE:" [value]="experience.timeline"/> <app-detail-block label="TIMELINE:" [value]="experience.timeline"/>
<app-tech-stack label="TECH_STACK:" [techStack]="experience.techStack"/>
<div class="tech-section">
<div class="detail-label">TECH_STACK:</div>
<div class="tech-grid">
@for (tech of experience.techStack; track tech) {
<span class="tech-item">{{ tech }}</span>
}
</div>
</div>
</div> </div>
</article> </article>
} }

View File

@@ -1,15 +1,16 @@
import { Component, signal } from '@angular/core'; import { Component, signal } from '@angular/core';
import { Experience } from '../../../../shared/models/experience.model'; import { Experience } from '../../../../shared/models/experience.model';
import { DetailBlockComponent } from '../shared/detail-block/detail-block.component'; import { DetailBlockComponent } from '../shared/detail-block/detail-block.component';
import { TechStackComponent } from '../tech-stack/tech-stack.component';
@Component({ @Component({
selector: 'app-column-professional-info', selector: 'app-column-professional-info',
imports: [DetailBlockComponent], imports: [DetailBlockComponent, TechStackComponent],
templateUrl: './column-professional-info.component.html', templateUrl: './column-professional-info.component.html',
styleUrl: './column-professional-info.component.css', styleUrl: './column-professional-info.component.css',
}) })
export class ColumnProfessionalInfoComponent { export class ColumnProfessionalInfoComponent {
private readonly _experiences = signal<readonly Experience[]>([ private _experiences = signal<Experience[]>([
{ {
id: 'senior-specialist', id: 'senior-specialist',
title: 'PYTHON_API_SPECIALIST.EXE', title: 'PYTHON_API_SPECIALIST.EXE',
@@ -17,7 +18,7 @@ export class ColumnProfessionalInfoComponent {
objective: objective:
'Specialized in Python/Django development for API automation and RPA processes. Led technical interventions for complex enterprise HR system integrations, developing robust solutions that bridge business requirements with technical implementation.', 'Specialized in Python/Django development for API automation and RPA processes. Led technical interventions for complex enterprise HR system integrations, developing robust solutions that bridge business requirements with technical implementation.',
timeline: 'UKG | January 2024 - January 2025', timeline: 'UKG | January 2024 - January 2025',
techStack: ['PYTHON', 'DJANGO', 'API', 'RPA'] as const, techStack: ['PYTHON', 'DJANGO', 'API', 'RPA'],
status: 'completed', status: 'completed',
statusLabel: 'ARCHIVED | SME_SPECIALIST', statusLabel: 'ARCHIVED | SME_SPECIALIST',
}, },
@@ -28,7 +29,7 @@ export class ColumnProfessionalInfoComponent {
objective: objective:
'Subject Matter Expert (SME) for enterprise HRSD connectors and system integrations. Specialized in complex technical challenges involving API development, RPA automation, and SFTP protocols. Provided BI support and data visualization solutions.', 'Subject Matter Expert (SME) for enterprise HRSD connectors and system integrations. Specialized in complex technical challenges involving API development, RPA automation, and SFTP protocols. Provided BI support and data visualization solutions.',
timeline: 'UKG | January 2023 - January 2024', timeline: 'UKG | January 2023 - January 2024',
techStack: ['PYTHON', 'DJANGO', 'API', 'RPA'] as const, techStack: ['PYTHON', 'DJANGO', 'API', 'RPA'],
status: 'completed', status: 'completed',
statusLabel: 'ARCHIVED | SME_SPECIALIST', statusLabel: 'ARCHIVED | SME_SPECIALIST',
}, },
@@ -39,7 +40,7 @@ export class ColumnProfessionalInfoComponent {
objective: objective:
'Delivered high-level technical support for VIP business clients using HRSD solutions. Developed expertise in SaaS platforms and enterprise decision-making processes while managing critical client relationships.', 'Delivered high-level technical support for VIP business clients using HRSD solutions. Developed expertise in SaaS platforms and enterprise decision-making processes while managing critical client relationships.',
timeline: 'UKG | October 2021 - January 2023', timeline: 'UKG | October 2021 - January 2023',
techStack: ['SAAS', 'HRSD', 'CLIENT_MGMT'] as const, techStack: ['SAAS', 'HRSD', 'CLIENT_MGMT'],
status: 'completed', status: 'completed',
statusLabel: 'ARCHIVED | VIP_TIER', statusLabel: 'ARCHIVED | VIP_TIER',
}, },
@@ -50,11 +51,11 @@ export class ColumnProfessionalInfoComponent {
objective: objective:
'Provided VIP end-user support, managed hardware/software deployments, and implemented networking and information security practices in a mission-critical environment.', 'Provided VIP end-user support, managed hardware/software deployments, and implemented networking and information security practices in a mission-critical environment.',
timeline: 'Armée de Terre | July 2018 - December 2018', timeline: 'Armée de Terre | July 2018 - December 2018',
techStack: ['NETWORKING', 'SECURITY', 'DEPLOYMENT'] as const, techStack: ['NETWORKING', 'SECURITY', 'DEPLOYMENT'],
status: 'completed', status: 'completed',
statusLabel: 'ARCHIVED | MILITARY', statusLabel: 'ARCHIVED | MILITARY',
}, },
] as const); ]);
readonly experiences = this._experiences.asReadonly(); experiences = this._experiences;
} }

View File

@@ -0,0 +1,61 @@
css/* tech-stack.component.css */
.tech-section {
margin-top: 1.25rem;
padding-top: 1rem;
}
.tech-grid {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.5rem;
}
/* Tech Items with fallbacks */
.tech-item {
background: linear-gradient(135deg, var(--color-nier-accent, #d4af37) 0%, var(--color-nier-highlight, #f4d03f) 100%);
color: var(--color-nier-text-light, #ffffff);
padding: 0.25rem 0.5rem;
font-size: 0.7rem;
font-family: var(--font-terminal, 'Courier New', monospace);
text-transform: uppercase;
letter-spacing: 0.05em;
border-radius: 3px;
white-space: nowrap;
transition: var(--transition-smooth, all 0.3s ease);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
/* Initial state for staggered animation */
opacity: 0;
transform: scale(0.8) translateY(10px);
animation: techItemFadeIn 0.4s ease-out forwards;
}
/* Staggered animation delays for tech items */
.tech-item:nth-child(1) { animation-delay: calc(var(--animation-delay-base, 0.1s) * 13); }
.tech-item:nth-child(2) { animation-delay: calc(var(--animation-delay-base, 0.1s) * 14); }
.tech-item:nth-child(3) { animation-delay: calc(var(--animation-delay-base, 0.1s) * 15); }
.tech-item:nth-child(4) { animation-delay: calc(var(--animation-delay-base, 0.1s) * 16); }
.tech-item:nth-child(5) { animation-delay: calc(var(--animation-delay-base, 0.1s) * 17); }
.tech-item:nth-child(n+6) { animation-delay: calc(var(--animation-delay-base, 0.1s) * 18); }
.tech-item:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
}
.detail-label {
font-size: 0.7rem;
margin-bottom: 0.375rem;
}
@keyframes techItemFadeIn {
from {
opacity: 0;
transform: scale(0.8) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}

View File

@@ -0,0 +1,9 @@
<div class="tech-section">
<div class="detail-label">{{label()}}
</div>
<div class="tech-grid">
@for (tech of techStack(); track tech) {
<span class="tech-item">{{ tech }}</span>
}
</div>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, input } from '@angular/core';
@Component({
selector: 'app-tech-stack',
imports: [],
templateUrl: './tech-stack.component.html',
styleUrl: './tech-stack.component.css',
})
export class TechStackComponent {
label = input.required<string>();
techStack = input.required<string[]>();
}

View File

@@ -1,13 +1,13 @@
type Experience = { type Experience = {
readonly id: string; id: string;
readonly title: string; title: string;
readonly classification: string; classification: string;
readonly objective: string; objective: string;
readonly timeline: string; timeline: string;
readonly techStack: readonly string[]; techStack: string[];
readonly status: 'current' | 'completed'; status: 'current' | 'completed';
readonly statusLabel: string; statusLabel: string;
readonly isCurrent?: boolean; isCurrent?: boolean;
}; };
export { type Experience }; export { type Experience };