Refactor code structure for improved readability and maintainability

This commit is contained in:
bryancito1090 2026-09-03 15:59:13 -05:00
parent f67b796d53
commit 5d45ac064f
18 changed files with 781 additions and 164 deletions

View File

@ -77,38 +77,59 @@ export const EN_TRANSLATIONS: TranslationDictionary = {
// Featured Projects
fpHeaderTag: '01 // PROJECTS',
fpAriaLabel: "Bryan Baño's featured projects",
fpPlaceholderImage: 'Image',
fpPlaceholderImage: 'No screenshots',
fpAltPrefix: 'Screenshot of ',
fpCarouselPrev: 'Previous project',
fpCarouselNext: 'Next project',
fpInDevelopment: 'In Development',
fpNoImagesTechnical: 'Architecture & CACES Audit Spec',
projectDetailChallenge: 'Challenges',
projectDetailSolution: 'Solution',
projectDetailResults: 'Results',
projectDetailCloseAria: 'Close project details',
prj1Subtitle: 'CACES Institutional Accreditation',
prj1Desc: 'Led platform development centralizing evidence upload and validation with advanced security via JWT and SharePoint.',
prj1Impact: 'Automated indicator tracking, eliminating physical paper management with complete traceability for government audits.',
prj1Challenge: 'The accreditation process was fragmented across spreadsheets, folders, and multiple validation stages, which made evidence control slow and vulnerable to omissions during audits.',
prj1Solution: 'I designed a central platform with role-based access, structured evidence workflows, document validation, and secure integrations with SharePoint and JWT-based authentication to protect records and streamline approval processes.',
prj1Result1: 'Reduced manual evidence review time and eliminated duplicate records across departments.',
prj1Result2: 'Improved audit readiness with full traceability, timestamps, and version control for every document.',
prj1Result3: 'Strengthened institutional governance by standardizing the approval flow for each accreditation requirement.',
prj2Subtitle: 'Comprehensive Workshop & Work Order Management',
prj2Institution: 'Automotive Sector',
prj2Desc: 'Comprehensive platform for mechanical supply inventory control, vehicle registration, and real-time work order tracking.',
prj2Impact: 'Optimized workshop operational flow, eliminating shortages of critical supplies and improving turnaround times with digital records.',
prj2Challenge: 'The workshop lacked a single source of truth for inventories, vehicle history, and pending work orders; that led to delays, stockouts, and repetitive manual coordination.',
prj2Solution: 'I built a modular system for inventory control, vehicle master data, and maintenance order lifecycle tracking, connecting operational records with real-time reporting to support faster decisions on the shop floor.',
prj2Result1: 'Minimized inventory shortages by improving stock visibility and replenishment planning.',
prj2Result2: 'Reduced operational friction with digital work orders and complete vehicle histories.',
prj2Result3: 'Improved delivery time and service planning through cleaner operational traceability.',
prj3Subtitle: 'Accelerated Virtual Education',
prj3Institution: 'Academic Platform',
prj3Desc: 'Led academic management platform development based on RESTful API architecture for high availability in remote learning.',
prj3Impact: 'Centralizes academic administration and automates student tracking, eliminating manual enrollment and attendance processes.',
prj3Challenge: 'Academic operations depended on disconnected manual procedures for enrollment, attendance, and course records, which slowed administrative work and limited visibility for management.',
prj3Solution: 'I implemented a scalable academic platform with REST APIs, structured student management, attendance tracking, and administrative dashboards to support remote learning under a reliable architecture.',
prj3Result1: 'Automated academic records and reduced the time spent on repetitive administrative tasks.',
prj3Result2: 'Improved data consistency for access, course enrollment, and attendance follow-up.',
prj3Result3: 'Created a more resilient and scalable foundation for the digital education experience.',
// PRJ-01: SIPLECE (No images, technical / accreditation focus)
prj1Subtitle: 'CACES Accreditation & Document Governance',
prj1Desc: 'Centralized institutional platform to upload, audit, and validate technical-academic evidence required by CACES, integrating SharePoint, JWT authentication, and role-based access control.',
prj1Impact: 'Automated institutional indicator tracking, eliminating manual paperwork and guaranteeing complete traceability for governmental audits.',
prj1Challenge: 'The accreditation process was fragmented across desynchronized spreadsheets, physical binders, and informal approvals, creating severe delays and risking critical evidence omissions.',
prj1Solution: 'Designed a decoupled Clean Architecture platform using C# .NET and Angular, integrating SharePoint for encrypted storage, JWT with RBAC, and structured multi-stage approval workflows with immutable timestamps.',
prj1Result1: 'Drastically reduced review and validation turnaround times between academic and administrative departments.',
prj1Result2: 'Achieved 100% document version control and traceability for official CACES evaluation committees.',
prj1Result3: 'Standardized institutional governance for 100+ evaluation indicators and quality metrics.',
// PRJ-02: Ammi-Online (Full academic management with images)
prj2Subtitle: 'Academic Management, Admissions, Payments & Grades',
prj2Institution: 'Higher Education / Academic Platform',
prj2Desc: 'Comprehensive web ecosystem for full student lifecycle management: online applications, automated enrollments, payment processing, and real-time grade records.',
prj2Impact: 'Unified academic and financial workflows into a single high-concurrency system, cutting enrollment wait times and guaranteeing instant payment reconciliation.',
prj2Challenge: 'Fragmented admission requests, manual bank receipt reconciliations, and spreadsheet grade records that were vulnerable to human errors and discrepancies.',
prj2Solution: 'Implemented Clean Architecture with C# .NET 8 (MediatR CQRS) and Angular Signals. Integrated decoupled modules for applications, payment gateways, accounting validation, and grade books under strict database-driven RBAC and JWT.',
prj2Result1: '100% automated enrollment and digital payment reconciliation without manual intervention.',
prj2Result2: 'Centralized real-time grade records with comprehensive teacher modification audit trails.',
prj2Result3: 'Ultra-fast and reactive user experience for thousands of concurrent students via Angular Signals and OnPush.',
// PRJ-03: HR Vacations & Leaves Module (With images)
prj3Subtitle: 'Vacation Control, FIFO Algorithm & Real-Time Legal Deeds',
prj3Institution: 'Corporate / Human Resources',
prj3Desc: 'Comprehensive, legally compliant human resources module for employee vacation tracking, FIFO period calculations, real-time PDF legal deeds, and analytical executive dashboards.',
prj3Impact: 'Fully automated the vacation and leave lifecycle, eliminating manual labor liability calculations and guaranteeing 100% legal compliance.',
prj3Challenge: 'Manual calculations vulnerable to legal disputes over seniority periods, unworked leave deductions, institutional shutdowns, and delayed legal deed generation.',
prj3Solution: 'Engineered a FIFO vacation settlement engine following Clean Architecture, featuring real-time PDF deed generation, dynamic dashboards, and 100% configurable extra days by department, incentive, and closures. Integrated dynamic leave deductions, DB-backed RBAC, and JWT security.',
prj3Result1: 'Instant generation of signed, legally compliant vacation deeds with real-time auditability.',
prj3Result2: 'Dynamic and transparent leave deductions linked directly to the corresponding vacation balance.',
prj3Result3: 'Executive analytics dashboards monitoring absenteeism and labor liability metrics for leadership.',
// PRJ-04: Academic Degree Completion System (In Development, with images)
prj4Subtitle: 'Calls, Modalities & Degree Matrices (In Development)',
prj4Institution: 'Higher Education',
prj4Desc: 'Comprehensive platform for managing university graduation processes: candidate applications, modality tracking (scientific paper, comprehensive exam, ICT projects), and degree requirement validation.',
prj4Impact: 'Standardizes institutional degree completion through configurable open calls, providing absolute transparency across students, tutors, and evaluation tribunals.',
prj4Challenge: 'Coordinating high candidate volumes with disparate prerequisites across degree modalities, manual validation of internship/community service hours, and lack of a unified evaluation channel.',
prj4Solution: 'Architected a Clean Architecture platform (.NET + Angular Signals) orchestrated around dynamic administrative calls. Features database-driven RBAC with 4 roles (Administrator, Student, Professor, and Evaluador), tracking matrices, and custom business rule validators.',
prj4Result1: 'Structured call lifecycle: opening, student application, tribunal assignment, and final defense.',
prj4Result2: 'Configurable support for scientific publications, comprehensive exams, and technological projects.',
prj4Result3: 'Hardened security via JWT, database-driven role permissions, and prerequisite validation.',
// Terminal & Workstation
terminalWorkstationTag: '02 // INTERACTIVE WORKSTATION',

View File

@ -77,38 +77,59 @@ export const ES_TRANSLATIONS: TranslationDictionary = {
// Featured Projects
fpHeaderTag: '01 // PROYECTOS',
fpAriaLabel: 'Proyectos reales de Bryan Baño',
fpPlaceholderImage: 'Imagen',
fpPlaceholderImage: 'Sin capturas',
fpAltPrefix: 'Captura de pantalla de ',
fpCarouselPrev: 'Proyecto anterior',
fpCarouselNext: 'Siguiente proyecto',
fpInDevelopment: 'En Desarrollo',
fpNoImagesTechnical: 'Arquitectura & Auditoría CACES',
projectDetailChallenge: 'Complicaciones',
projectDetailSolution: 'Solución',
projectDetailResults: 'Resultados',
projectDetailCloseAria: 'Cerrar detalle del proyecto',
prj1Subtitle: 'Acreditación Institucional CACES',
prj1Desc: 'Lideré el desarrollo de la plataforma centralizando la carga y validación de evidencias con seguridad avanzada mediante JWT y SharePoint.',
prj1Impact: 'Automatizó el seguimiento de indicadores, eliminando la gestión física de documentos con trazabilidad total para auditorías gubernamentales.',
prj1Challenge: 'El proceso de acreditación estaba fragmentado entre hojas de cálculo, carpetas físicas y varias etapas de validación, lo que hacía lento el control de evidencias y dejaba margen para omisiones durante auditorías.',
prj1Solution: 'Diseñé una plataforma central con accesos por roles, flujos estructurados de evidencias, validación documental e integración segura con SharePoint y autenticación JWT para proteger los registros y agilizar las aprobaciones.',
prj1Result1: 'Reduje el tiempo de revisión manual de evidencias y eliminé registros duplicados entre departamentos.',
prj1Result2: 'Mejoré la preparación para auditorías con trazabilidad completa, marcas de tiempo y control de versiones para cada documento.',
prj1Result3: 'Fortalecí la gobernanza institucional al estandarizar el flujo de aprobación de cada requisito de acreditación.',
prj2Subtitle: 'Gestión Integral de Talleres & Órdenes',
prj2Institution: 'Sector Automotriz',
prj2Desc: 'Plataforma integral para control de inventarios de insumos mecánicos, registro detallado de vehículos y seguimiento de órdenes de trabajo en tiempo real.',
prj2Impact: 'Optimizó el flujo operativo del taller, eliminando el desabastecimiento de suministros críticos y mejorando los tiempos de entrega con historial digital.',
prj2Challenge: 'El taller no contaba con una fuente única de verdad para inventarios, historial de vehículos y órdenes pendientes; esto generaba retrasos, faltantes y coordinación manual repetitiva.',
prj2Solution: 'Desarrollé un sistema modular para control de inventario, datos maestros de vehículos y ciclo de vida de órdenes de mantenimiento, conectando registros operativos con reportes en tiempo real para decisiones más rápidas en taller.',
prj2Result1: 'Disminuí los desabastecimientos mediante mejor visibilidad del stock y planificación de reposición.',
prj2Result2: 'Reducí la fricción operativa con órdenes digitales y un historial completo de cada vehículo.',
prj2Result3: 'Mejoré tiempos de entrega y planificación del servicio gracias a una trazabilidad más clara del trabajo.',
prj3Subtitle: 'Educación Virtual Acelerada',
prj3Institution: 'Plataforma Académica',
prj3Desc: 'Lideré el desarrollo de la plataforma de gestión académica basada en arquitectura de APIs RESTful para alta disponibilidad en entornos remotos.',
prj3Impact: 'Centraliza la administración académica y automatiza el seguimiento estudiantil, eliminando procesos manuales de inscripción y control de asistencia.',
prj3Challenge: 'Las operaciones académicas dependían de procedimientos manuales no conectados para matrícula, asistencia y registros de cursos, lo que reducía velocidad administrativa y visibilidad para la gestión.',
prj3Solution: 'Implementé una plataforma académica escalable con APIs REST, gestión estructurada de estudiantes, control de asistencia y dashboards administrativos para sostener la educación remota con una arquitectura confiable.',
prj3Result1: 'Automatizé registros académicos y reduje el tiempo invertido en tareas administrativas repetitivas.',
prj3Result2: 'Mejoré la consistencia de datos para acceso, matriculación y seguimiento de asistencia.',
prj3Result3: 'Construí una base más resiliente y escalable para la experiencia de educación digital.',
// PRJ-01: SIPLECE (Sin imágenes, enfoque técnico / acreditación)
prj1Subtitle: 'Acreditación CACES & Gestión Documental',
prj1Desc: 'Plataforma institucional para centralizar, auditar y validar evidencias técnico-académicas requeridas por el CACES, integrando SharePoint y autenticación JWT con control de acceso por roles.',
prj1Impact: 'Automatizó el seguimiento de indicadores institucionales, erradicando la manipulación física de documentos y asegurando trazabilidad completa para auditorías gubernamentales.',
prj1Challenge: 'El proceso de acreditación estaba fragmentado entre múltiples hojas de cálculo, carpetas físicas desincronizadas y aprobaciones informales, lo que ocasionaba demoras críticas y riesgo de omisión de evidencias ante los evaluadores externos.',
prj1Solution: 'Diseñé una plataforma central bajo Clean Architecture con C# .NET y Angular, integrando SharePoint para almacenamiento cifrado, autenticación JWT con RBAC y flujos estructurados de aprobación multi-etapa con marcas de tiempo inmutables.',
prj1Result1: 'Reducción drástica del tiempo de revisión y validación de evidencias entre departamentos académicos y administrativos.',
prj1Result2: 'Trazabilidad y versionado al 100% de cada documento para auditorías oficiales del CACES.',
prj1Result3: 'Gobernanza institucional estandarizada para más de 100 indicadores y criterios de evaluación.',
// PRJ-02: Ammi-Online (Gestión académica completa con imágenes)
prj2Subtitle: 'Gestión Académica, Solicitudes, Pagos & Calificaciones',
prj2Institution: 'Educación Superior / Academia',
prj2Desc: 'Ecosistema web integral para la administración del ciclo estudiantil completo: solicitudes de admisión, matriculación automatizada, procesamiento de pagos y registro de calificaciones en tiempo real.',
prj2Impact: 'Unificó las operaciones académicas y financieras en una sola plataforma de alta concurrencia, reduciendo el tiempo de matriculación y garantizando conciliación de pagos inmediata.',
prj2Challenge: 'Procesos de admisión dispersos, conciliación manual de pagos y comprobantes bancarios, y dispersión de actas de calificaciones en hojas de cálculo vulnerables a inconsistencias.',
prj2Solution: 'Implementé una arquitectura limpia desacoplada con backend en C# .NET 8 (CQRS con MediatR) y frontend Angular con Signals. Módulos integrados para solicitudes, pasarelas de pago, validación contable y actas de calificaciones con RBAC estricto desde base de datos y JWT.',
prj2Result1: 'Automatización del 100% del flujo de matrícula y validación digital de pagos sin intervención manual.',
prj2Result2: 'Centralización de actas de notas con auditoría rigurosa de modificaciones por docente.',
prj2Result3: 'Navegación reactiva instantánea para miles de estudiantes simultáneos mediante ChangeDetection OnPush y Signals.',
// PRJ-03: Módulo Vacaciones & Permisos RRHH (Con imágenes)
prj3Subtitle: 'Control de Vacaciones, Algoritmo FIFO & Actas en Tiempo Real',
prj3Institution: 'Corporativo / Talento Humano',
prj3Desc: 'Módulo integral de recursos humanos apegado a la legislación laboral para la administración de vacaciones de empleados, cálculo por algoritmo FIFO de periodos acumulados, actas en tiempo real y dashboards analíticos.',
prj3Impact: 'Automatizó integralmente el ciclo de vacaciones y permisos, erradicando el cálculo manual de pasivos laborales y garantizando 100% de apego a la normativa legal vigente.',
prj3Challenge: 'Cálculo manual propenso a discrepancias legales en periodos acumulados por antigüedad, deducciones por permisos imprevistos, cierres institucionales y falta de generación inmediata de actas con validez jurídica.',
prj3Solution: 'Desarrollé un motor de liquidación vacacional bajo algoritmo FIFO con Clean Architecture, generación de actas en tiempo real en PDF, dashboards dinámicos y configuración al 100% de días extras por departamento, incentivos y cierres institucionales. Todo con gestión de permisos vinculada a descuentos dinámicos, RBAC en base de datos y JWT.',
prj3Result1: 'Generación instantánea de actas de vacaciones con firma y trazabilidad legal en tiempo real.',
prj3Result2: 'Descuento dinámico y transparente de permisos directamente vinculado al saldo y periodo correspondiente.',
prj3Result3: 'Dashboards analíticos de absentismo y pasivos laborales para la toma de decisiones directivas.',
// PRJ-04: Sistema de Titulación Académica (En desarrollo, con imágenes)
prj4Subtitle: 'Convocatorias, Modalidades & Matriz de Grado',
prj4Institution: 'Educación Superior',
prj4Desc: 'Plataforma para la administración integral de procesos de titulación: postulación de aspirantes, seguimiento por modalidades (artículo científico, examen complexivo, proyectos TIC) y validación de requisitos de grado por convocatorias.',
prj4Impact: 'Estandariza el ciclo de graduación universitaria mediante convocatorias parametrizables, garantizando transparencia absoluta entre estudiantes, tutores y evaluadores.',
prj4Challenge: 'Dificultad para coordinar postulaciones con requisitos dispares por modalidad de titulación, validaciones manuales de horas de vinculación/prácticas y ausencia de un canal unificado para tribunales examinadores.',
prj4Solution: 'Diseñé una plataforma basada en Clean Architecture (.NET + Angular Signals) con motor de convocatorias dinámicas abiertas por el administrador. Incluye RBAC desde base de datos con 4 roles (Administrador, Estudiante, Profesor y Evaluador), matrices de seguimiento y validaciones personalizadas.',
prj4Result1: 'Gestión estructurada por convocatorias con apertura, postulación, dictamen de jurado y defensa final.',
prj4Result2: 'Soporte configurable para modalidades de artículo científico, examen complexivo y proyectos TIC.',
prj4Result3: 'Seguridad robusta con JWT, permisos por rol desde base de datos y control exhaustivo de requisitos previos.',
// Terminal & Workstation
terminalWorkstationTag: '02 // INTERACTIVE WORKSTATION',

View File

@ -79,6 +79,10 @@ export interface TranslationDictionary {
readonly fpAriaLabel: string;
readonly fpPlaceholderImage: string;
readonly fpAltPrefix: string;
readonly fpCarouselPrev: string;
readonly fpCarouselNext: string;
readonly fpInDevelopment: string;
readonly fpNoImagesTechnical: string;
readonly projectDetailChallenge: string;
readonly projectDetailSolution: string;
readonly projectDetailResults: string;
@ -109,6 +113,15 @@ export interface TranslationDictionary {
readonly prj3Result1: string;
readonly prj3Result2: string;
readonly prj3Result3: string;
readonly prj4Subtitle: string;
readonly prj4Institution: string;
readonly prj4Desc: string;
readonly prj4Impact: string;
readonly prj4Challenge: string;
readonly prj4Solution: string;
readonly prj4Result1: string;
readonly prj4Result2: string;
readonly prj4Result3: string;
// Terminal & Workstation
readonly terminalWorkstationTag: string;

View File

@ -1,3 +1,4 @@
/* Featured Projects Component Styles */
:host {
display: block;
width: 100%;
@ -44,17 +45,64 @@
letter-spacing: 0.08em;
}
.projects-stack {
.carousel-nav-controls {
display: flex;
align-items: center;
gap: var(--space-3);
}
.carousel-counter {
font-family: var(--font-mono);
font-size: 0.8125rem;
font-weight: 700;
color: var(--zen-text-secondary);
letter-spacing: 0.1em;
}
.carousel-buttons {
display: flex;
align-items: center;
gap: 6px;
}
.carousel-nav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid var(--zen-border-subtle);
background: rgba(255, 255, 255, 0.04);
color: var(--zen-text-primary);
font-size: 1.15rem;
line-height: 1;
cursor: pointer;
transition: all var(--transition-fast);
}
.carousel-nav-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.12);
border-color: var(--zen-accent-kintsugi);
transform: scale(1.06);
}
.carousel-nav-btn:disabled {
opacity: 0.28;
cursor: not-allowed;
}
/* Cuadrícula de Proyectos (Grid limpio sin scroll horizontal) */
.projects-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
width: 100%;
}
@media (min-width: 960px) {
.projects-stack {
@media (min-width: 768px) {
.projects-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-5);
align-items: start;
}
}
@ -64,7 +112,6 @@
align-items: stretch;
gap: var(--space-3);
padding: var(--space-4);
min-height: 100%;
background:
linear-gradient(180deg, rgba(16, 20, 25, 0.92), rgba(12, 16, 20, 0.88)),
rgba(11, 15, 18, 0.85);
@ -74,11 +121,19 @@
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
cursor: pointer;
text-align: left;
transition:
transform var(--transition-fast),
border-color var(--transition-fast),
box-shadow var(--transition-fast),
background var(--transition-fast);
transition: transform var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
animation: cardFadeIn 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes cardFadeIn {
from {
opacity: 0.55;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.project-card:hover,
@ -107,6 +162,108 @@
justify-content: center;
}
.project-image-slot.no-image {
background: #080c12;
border-color: rgba(125, 207, 255, 0.18);
}
.tech-blueprint-slot {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background:
radial-gradient(circle at center, rgba(125, 207, 255, 0.08) 0%, transparent 70%),
#0a0e16;
user-select: none;
}
.blueprint-grid-overlay {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(125, 207, 255, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(125, 207, 255, 0.08) 1px, transparent 1px);
background-size: 16px 16px;
pointer-events: none;
}
.blueprint-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-align: center;
padding: var(--space-3);
}
.blueprint-icon {
font-size: 1.8rem;
color: var(--prj-accent, var(--zen-accent-kintsugi));
line-height: 1;
}
.blueprint-code {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--prj-accent, var(--zen-accent-kintsugi));
}
.blueprint-title {
font-family: var(--font-sans);
font-size: 1.1rem;
font-weight: 800;
color: var(--zen-text-primary);
}
.blueprint-spec {
font-family: var(--font-sans);
font-size: 0.6875rem;
font-weight: 600;
color: var(--zen-text-secondary);
letter-spacing: 0.04em;
}
.in-dev-badge {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0.25rem 0.6rem;
background: rgba(18, 22, 32, 0.88);
border: 1px solid rgba(187, 154, 247, 0.4);
border-radius: 999px;
font-family: var(--font-sans);
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.06em;
color: #bb9af7;
backdrop-filter: blur(6px);
}
.in-dev-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #bb9af7;
box-shadow: 0 0 6px #bb9af7;
animation: pulse-dev 2s infinite ease-in-out;
}
@keyframes pulse-dev {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.8); }
}
.project-img {
width: 100%;
height: 100%;
@ -115,40 +272,6 @@
filter: saturate(1.08) contrast(1.04);
}
.image-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.03) 0%, transparent 80%);
border: 1px dashed rgba(255, 255, 255, 0.12);
color: var(--zen-text-muted);
user-select: none;
}
.placeholder-icon {
font-size: 1.5rem;
color: var(--prj-accent, var(--zen-accent-kintsugi));
opacity: 0.9;
}
.placeholder-code {
font-family: var(--font-sans);
font-size: 0.75rem;
font-weight: 700;
color: var(--zen-text-secondary);
}
.placeholder-label {
font-family: var(--font-sans);
font-size: 0.6875rem;
color: var(--zen-text-muted);
letter-spacing: 0.05em;
}
.project-content {
flex: 1;
min-width: 0;
@ -267,6 +390,47 @@
border-radius: 1.4rem;
}
.carousel-dots-row {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
margin-top: var(--space-2);
}
.carousel-dot {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 20px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
outline: none;
}
.carousel-dot .dot-inner {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transition: all var(--transition-fast);
}
.carousel-dot:hover .dot-inner {
background: rgba(255, 255, 255, 0.45);
transform: scale(1.2);
}
.carousel-dot.is-active .dot-inner {
width: 24px;
border-radius: 999px;
background: var(--prj-accent, var(--zen-accent-kintsugi));
box-shadow: 0 0 10px var(--prj-accent, var(--zen-accent-kintsugi));
}
@media (max-width: 540px) {
.project-card {
padding: var(--space-3);
@ -281,4 +445,3 @@
align-items: flex-start;
}
}

View File

@ -1,13 +1,39 @@
<div class="featured-projects-wrapper" role="region" [attr.aria-label]="i18n.t().fpAriaLabel">
<!-- Cabecera de la sección con controles del carrusel -->
<header class="section-micro-header">
<div class="header-left">
<span class="pulse-indicator" aria-hidden="true"></span>
<span class="header-tag">{{ i18n.t().fpHeaderTag }}</span>
</div>
<div class="carousel-nav-controls">
<span class="carousel-counter" aria-live="polite">
0{{ currentIndex() + 1 }} / 0{{ totalPages() }}
</span>
<div class="carousel-buttons">
<button
type="button"
class="carousel-nav-btn"
(click)="prev()"
[attr.aria-label]="i18n.t().fpCarouselPrev"
>
‹
</button>
<button
type="button"
class="carousel-nav-btn"
(click)="next()"
[attr.aria-label]="i18n.t().fpCarouselNext"
>
›
</button>
</div>
</div>
</header>
<div class="projects-stack">
@for (prj of projects(); track prj.id) {
<!-- Cuadrícula de Proyectos (Grid estático sin scroll horizontal) -->
<div class="projects-grid">
@for (prj of visibleProjects(); track prj.id) {
<button
type="button"
class="project-card"
@ -16,20 +42,32 @@
[attr.aria-pressed]="selectedProjectId() === prj.id"
(click)="selectProject(prj.id)"
>
<div class="project-image-slot">
@if (!imageErrors()[prj.id] && prj.imageUrl) {
<div class="project-image-slot" [class.no-image]="!prj.imageUrl">
@if (prj.imageUrl && !imageErrors()[prj.imageUrl]) {
<img
[src]="prj.imageUrl"
[alt]="i18n.t().fpAltPrefix + prj.title"
class="project-img"
(error)="onImageError(prj.id)"
(error)="onImageError(prj.imageUrl)"
loading="lazy"
/>
} @else {
<div class="image-placeholder">
<span class="placeholder-icon">◧</span>
<span class="placeholder-code">{{ prj.code }}</span>
<span class="placeholder-label">{{ i18n.t().fpPlaceholderImage }}</span>
<!-- Diseño Blueprint / Ficha Técnica para SIPLECE o proyectos sin capturas -->
<div class="tech-blueprint-slot">
<div class="blueprint-grid-overlay" aria-hidden="true"></div>
<div class="blueprint-content">
<span class="blueprint-icon">◧</span>
<span class="blueprint-code">{{ prj.code }}</span>
<span class="blueprint-title">{{ prj.title }}</span>
<span class="blueprint-spec">{{ i18n.t().fpNoImagesTechnical }}</span>
</div>
</div>
}
@if (prj.inDevelopment) {
<div class="in-dev-badge">
<span class="in-dev-dot"></span>
<span>{{ i18n.t().fpInDevelopment }}</span>
</div>
}
</div>
@ -52,12 +90,33 @@
}
</div>
<span class="project-action">{{ selectedProjectId() === prj.id ? 'Detalle abierto' : 'Ver detalle' }}</span>
<span class="project-action">
{{ selectedProjectId() === prj.id ? 'Detalle abierto' : 'Ver detalle' }}
</span>
</div>
</button>
}
</div>
<!-- Indicadores de página (Dots) -->
@if (totalPages() > 1) {
<div class="carousel-dots-row" role="tablist" aria-label="Selector de proyectos">
@for (pageIdx of pagesArray(); track pageIdx) {
<button
type="button"
class="carousel-dot"
[class.is-active]="currentIndex() === pageIdx"
(click)="goToPage(pageIdx)"
[attr.aria-label]="'Página ' + (pageIdx + 1)"
[attr.aria-selected]="currentIndex() === pageIdx"
>
<span class="dot-inner"></span>
</button>
}
</div>
}
<!-- Modal Detalle del Proyecto Seleccionado -->
@if (selectedProject(); as selected) {
<div class="project-detail-overlay" (click)="clearSelection()">
<div class="project-detail-shell" (click)="$event.stopPropagation()">

View File

@ -1,4 +1,11 @@
import { Component, ChangeDetectionStrategy, signal, computed, inject } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
signal,
computed,
inject,
HostListener
} from '@angular/core';
import { I18nService } from '../../../../application/services/i18n.service';
import { ProjectDetailComponent } from '../project-detail/project-detail.component';
@ -15,7 +22,9 @@ export interface ProjectCardData {
readonly results: readonly string[];
readonly tags: readonly string[];
readonly imageUrl?: string;
readonly images?: readonly string[];
readonly accentColor: string;
readonly inDevelopment?: boolean;
}
@Component({
@ -29,13 +38,91 @@ export interface ProjectCardData {
export class FeaturedProjectsComponent {
protected readonly i18n = inject(I18nService);
// Proyectos extraídos directamente de la hoja de vida (CV) de Bryan Baño con soporte i18n
// Navegación de cuadrícula limpia (sin carrusel ni scroll horizontal)
protected readonly currentIndex = signal<number>(0);
protected readonly isMobile = signal<boolean>(
typeof window !== 'undefined' ? window.innerWidth < 768 : false
);
@HostListener('window:resize')
onResize(): void {
const mobile = window.innerWidth < 768;
if (this.isMobile() !== mobile) {
this.isMobile.set(mobile);
if (this.currentIndex() > this.maxIndex()) {
this.currentIndex.set(this.maxIndex());
}
}
}
// Proyectos reales de Bryan Baño con arquitectura limpia y soporte i18n
protected readonly projects = computed<ProjectCardData[]>(() => {
const t = this.i18n.t();
return [
{
id: 'siplece',
id: 'ammi',
code: 'PRJ-01',
title: 'Ammi-Online',
subtitle: t.prj2Subtitle,
institution: t.prj2Institution,
description: t.prj2Desc,
impact: t.prj2Impact,
challenge: t.prj2Challenge,
solution: t.prj2Solution,
results: [t.prj2Result1, t.prj2Result2, t.prj2Result3],
tags: ['Angular', '.NET 8', 'Clean Arch', 'JWT & RBAC', 'MediatR CQRS'],
imageUrl: 'assets/images/projects/ammi/ammi1.jpg',
images: [
'assets/images/projects/ammi/ammi1.jpg',
'assets/images/projects/ammi/ammi2.jpg',
'assets/images/projects/ammi/ammi3.jpg'
],
accentColor: '#9ece6a'
},
{
id: 'vacaciones-rrhh',
code: 'PRJ-02',
title: 'Módulo Vacaciones RRHH',
subtitle: t.prj3Subtitle,
institution: t.prj3Institution,
description: t.prj3Desc,
impact: t.prj3Impact,
challenge: t.prj3Challenge,
solution: t.prj3Solution,
results: [t.prj3Result1, t.prj3Result2, t.prj3Result3],
tags: ['Angular', '.NET 9', 'Clean Arch', 'JWT & RBAC', 'Algoritmo FIFO', 'Reportes PDF'],
imageUrl: 'assets/images/projects/vacaciones-rrhh/rrhh1.jpg',
images: [
'assets/images/projects/vacaciones-rrhh/rrhh1.jpg',
'assets/images/projects/vacaciones-rrhh/rrhh2.jpg',
'assets/images/projects/vacaciones-rrhh/rrhh3.jpg'
],
accentColor: '#e0af68'
},
{
id: 'titulacion',
code: 'PRJ-03',
title: 'Sistema de Titulación',
subtitle: t.prj4Subtitle,
institution: t.prj4Institution,
description: t.prj4Desc,
impact: t.prj4Impact,
challenge: t.prj4Challenge,
solution: t.prj4Solution,
results: [t.prj4Result1, t.prj4Result2, t.prj4Result3],
tags: ['Angular', '.NET 9', 'Clean Arch', 'JWT & RBAC', 'Gestión de Convocatorias', 'En Desarrollo'],
imageUrl: 'assets/images/projects/titulacion/tit1.jpg',
images: [
'assets/images/projects/titulacion/tit1.jpg',
'assets/images/projects/titulacion/tit2.jpg',
'assets/images/projects/titulacion/tit3.jpg'
],
accentColor: '#bb9af7',
inDevelopment: true
},
{
id: 'siplece',
code: 'PRJ-04',
title: 'SIPLECE',
subtitle: t.prj1Subtitle,
institution: 'IST Mayor Pedro Traversari',
@ -44,55 +131,69 @@ export class FeaturedProjectsComponent {
challenge: t.prj1Challenge,
solution: t.prj1Solution,
results: [t.prj1Result1, t.prj1Result2, t.prj1Result3],
tags: ['Angular', '.NET 8', 'MySQL', 'JWT', 'SharePoint'],
imageUrl: 'assets/images/projects/siplece.webp',
accentColor: '#7dcfff'
},
{
id: 'vita',
code: 'PRJ-02',
title: 'VITA Automotriz',
subtitle: t.prj2Subtitle,
institution: t.prj2Institution,
description: t.prj2Desc,
impact: t.prj2Impact,
challenge: t.prj2Challenge,
solution: t.prj2Solution,
results: [t.prj2Result1, t.prj2Result2, t.prj2Result3],
tags: ['Angular', '.NET 8', 'MySQL', 'REST API'],
imageUrl: 'assets/images/projects/vita.webp',
accentColor: '#9ece6a'
},
{
id: 'ammi',
code: 'PRJ-03',
title: 'Ammi-Online',
subtitle: t.prj3Subtitle,
institution: t.prj3Institution,
description: t.prj3Desc,
impact: t.prj3Impact,
challenge: t.prj3Challenge,
solution: t.prj3Solution,
results: [t.prj3Result1, t.prj3Result2, t.prj3Result3],
tags: ['Angular', '.NET 8', 'MySQL', 'Clean Arch'],
imageUrl: 'assets/images/projects/ammi.webp',
accentColor: '#bb9af7'
tags: ['Angular', '.NET 8', 'Clean Arch', 'JWT & RBAC', 'SharePoint'],
accentColor: '#7dcfff',
images: [] // Sin imágenes, diseño de ficha de arquitectura
}
];
});
// Número de tarjetas visibles según tamaño de pantalla (1 en móvil, 3 en desktop)
protected readonly cardsPerPage = computed(() => (this.isMobile() ? 1 : 3));
// Índice máximo permitido para desplazamiento
protected readonly maxIndex = computed(() => {
const total = this.projects().length;
const perPage = this.cardsPerPage();
return Math.max(0, total - perPage);
});
// Total de páginas / pasos de navegación
protected readonly totalPages = computed(() => this.maxIndex() + 1);
// Array de índices para los dots indicadores
protected readonly pagesArray = computed(() =>
Array.from({ length: this.totalPages() }, (_, i) => i)
);
// Tarjetas visibles en la pantalla en este momento
protected readonly visibleProjects = computed(() => {
const start = this.currentIndex();
const count = this.cardsPerPage();
return this.projects().slice(start, start + count);
});
protected readonly canPrev = computed(() => this.currentIndex() > 0);
protected readonly canNext = computed(() => this.currentIndex() < this.maxIndex());
protected prev(): void {
if (this.canPrev()) {
this.currentIndex.update(i => i - 1);
} else {
this.currentIndex.set(this.maxIndex());
}
}
protected next(): void {
if (this.canNext()) {
this.currentIndex.update(i => i + 1);
} else {
this.currentIndex.set(0);
}
}
protected goToPage(index: number): void {
this.currentIndex.set(Math.max(0, Math.min(index, this.maxIndex())));
}
protected readonly selectedProjectId = signal<string | null>(null);
protected readonly selectedProject = computed(() => {
const selectedId = this.selectedProjectId();
if (!selectedId) {
return null;
}
if (!selectedId) return null;
return this.projects().find(project => project.id === selectedId) ?? null;
});
// Manejo de fallback elegante si el usuario aún no ha colocado los archivos de imagen
protected readonly imageErrors = signal<Record<string, boolean>>({});
protected selectProject(projectId: string): void {
@ -103,7 +204,7 @@ export class FeaturedProjectsComponent {
this.selectedProjectId.set(null);
}
onImageError(projectId: string): void {
this.imageErrors.update(curr => ({ ...curr, [projectId]: true }));
onImageError(imageKey: string): void {
this.imageErrors.update(curr => ({ ...curr, [imageKey]: true }));
}
}

View File

@ -26,7 +26,9 @@
}
.detail-kicker {
display: inline-block;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 0.35rem;
font-size: 0.7rem;
font-weight: 700;
@ -35,6 +37,18 @@
text-transform: uppercase;
}
.detail-dev-tag {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.5rem;
background: rgba(187, 154, 247, 0.12);
border: 1px solid rgba(187, 154, 247, 0.3);
border-radius: 999px;
font-size: 0.625rem;
color: #bb9af7;
letter-spacing: 0.05em;
}
.project-detail-header h3 {
margin: 0;
font-size: clamp(1.5rem, 2.2vw, 2.2rem);
@ -69,34 +83,180 @@
}
.project-detail-visual {
display: flex;
flex-direction: column;
gap: var(--space-3);
min-height: 260px;
}
.main-image-viewport {
flex: 1;
min-height: 220px;
border-radius: 1rem;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
background: #0a0d12;
}
.project-detail-visual img {
.main-image-viewport img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity var(--transition-fast);
}
.detail-placeholder {
.gallery-thumbs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-2);
}
.thumb-btn {
height: 58px;
padding: 0;
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: #0d1117;
cursor: pointer;
opacity: 0.55;
transition: border-color var(--transition-fast), transform var(--transition-fast), opacity var(--transition-fast);
}
.thumb-btn:hover {
opacity: 0.85;
border-color: rgba(255, 255, 255, 0.3);
}
.thumb-btn.is-active {
opacity: 1;
border-color: var(--prj-accent, var(--zen-accent-kintsugi));
box-shadow: 0 0 0 1px var(--prj-accent, var(--zen-accent-kintsugi));
}
.thumb-btn img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.thumb-label {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-height: 260px;
font-size: 1.25rem;
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 700;
color: var(--zen-text-secondary);
}
.detail-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
height: 100%;
min-height: 220px;
font-size: 1.15rem;
font-weight: 800;
color: var(--zen-text-secondary);
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.03), transparent 70%);
border: 1px dashed rgba(255, 255, 255, 0.12);
}
/* Panel de Arquitectura Técnica para proyectos sin capturas (SIPLECE) */
.technical-blueprint-panel {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 280px;
border-radius: 1rem;
overflow: hidden;
border: 1px solid rgba(125, 207, 255, 0.2);
background:
radial-gradient(circle at center, rgba(125, 207, 255, 0.08) 0%, transparent 70%),
#080c14;
padding: var(--space-4);
text-align: center;
}
.blueprint-circuit-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(125, 207, 255, 0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(125, 207, 255, 0.07) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
}
.blueprint-panel-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
max-width: 320px;
}
.blueprint-panel-icon {
font-size: 2.4rem;
color: var(--prj-accent, var(--zen-accent-kintsugi));
line-height: 1;
}
.blueprint-panel-code {
font-family: var(--font-mono);
font-size: 0.8125rem;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--prj-accent, var(--zen-accent-kintsugi));
}
.blueprint-panel-title {
margin: 0;
font-family: var(--font-sans);
font-size: 1.35rem;
font-weight: 800;
color: var(--zen-text-primary);
}
.blueprint-panel-subtitle {
margin: 0;
font-family: var(--font-sans);
font-size: 0.75rem;
font-weight: 600;
color: var(--zen-text-secondary);
}
.blueprint-specs-list {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: var(--space-2);
width: 100%;
text-align: left;
}
.blueprint-specs-list .spec-item {
padding: 0.35rem 0.65rem;
border-radius: 0.45rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--zen-text-secondary);
}
.project-detail-body {
display: flex;
flex-direction: column;

View File

@ -2,7 +2,12 @@
<section class="project-detail-panel" [style.--prj-accent]="project.accentColor" aria-live="polite">
<div class="project-detail-header">
<div>
<span class="detail-kicker">{{ project.code }}</span>
<span class="detail-kicker">
{{ project.code }}
@if (project.inDevelopment) {
<span class="detail-dev-tag">● {{ i18n.t().fpInDevelopment }}</span>
}
</span>
<h3>{{ project.title }}</h3>
</div>
<button type="button" class="close-button" (click)="onClose()" [attr.aria-label]="i18n.t().projectDetailCloseAria">
@ -12,11 +17,65 @@
<div class="project-detail-layout">
<div class="project-detail-visual">
@if (project.imageUrl) {
<img [src]="project.imageUrl" [alt]="project.title" loading="lazy" />
@if (project.images && project.images.length > 0) {
<div class="main-image-viewport">
@if (getCurrentImage(); as currentImg) {
@if (!imageErrors()[currentImg]) {
<img
[src]="currentImg"
[alt]="project.title + ' vista ' + (activeImageIndex() + 1)"
(error)="onImageError(currentImg)"
loading="lazy"
/>
} @else {
<div class="detail-placeholder">
<span>{{ project.code }}</span>
<span class="placeholder-icon">◧</span>
<span class="placeholder-code">{{ project.code }}</span>
</div>
}
} @else {
<div class="detail-placeholder">
<span class="placeholder-icon">◧</span>
<span class="placeholder-code">{{ project.code }}</span>
</div>
}
</div>
@if (project.images.length > 1) {
<div class="gallery-thumbs" role="tablist" aria-label="Miniaturas de capturas">
@for (img of project.images; track img; let i = $index) {
<button
type="button"
class="thumb-btn"
[class.is-active]="activeImageIndex() === i"
[attr.aria-selected]="activeImageIndex() === i"
(click)="selectImage(i)"
>
@if (!imageErrors()[img]) {
<img [src]="img" [alt]="project.title + ' miniatura ' + (i + 1)" (error)="onImageError(img)" loading="lazy" />
} @else {
<span class="thumb-label">{{ i + 1 }}</span>
}
</button>
}
</div>
}
} @else {
<!-- Ficha de Arquitectura Técnica para proyectos sin imágenes (SIPLECE) -->
<div class="technical-blueprint-panel">
<div class="blueprint-circuit-bg" aria-hidden="true"></div>
<div class="blueprint-panel-content">
<span class="blueprint-panel-icon">◧</span>
<span class="blueprint-panel-code">{{ project.code }}</span>
<h4 class="blueprint-panel-title">{{ project.title }}</h4>
<p class="blueprint-panel-subtitle">{{ i18n.t().fpNoImagesTechnical }}</p>
<div class="blueprint-specs-list">
<span class="spec-item">🏛 {{ project.institution }}</span>
<span class="spec-item">🔒 Autenticación JWT & RBAC</span>
<span class="spec-item">⚡ Clean Architecture (.NET + Angular)</span>
<span class="spec-item">📊 Trazabilidad & Auditoría CACES</span>
</div>
</div>
</div>
}
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject, signal } from '@angular/core';
import { I18nService } from '../../../../application/services/i18n.service';
import { ProjectCardData } from '../featured-projects/featured-projects.component';
@ -16,7 +16,27 @@ export class ProjectDetailComponent {
@Output() close = new EventEmitter<void>();
protected readonly activeImageIndex = signal<number>(0);
protected readonly imageErrors = signal<Record<string, boolean>>({});
protected onClose(): void {
this.close.emit();
}
protected selectImage(index: number): void {
this.activeImageIndex.set(index);
}
protected onImageError(imagePath: string): void {
this.imageErrors.update(curr => ({ ...curr, [imagePath]: true }));
}
protected getCurrentImage(): string | null {
if (!this.project) return null;
const images = this.project.images;
if (images && images.length > this.activeImageIndex()) {
return images[this.activeImageIndex()];
}
return this.project.imageUrl ?? null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB