Refactor code structure for improved readability and maintainability
|
|
@ -77,38 +77,59 @@ export const EN_TRANSLATIONS: TranslationDictionary = {
|
||||||
// Featured Projects
|
// Featured Projects
|
||||||
fpHeaderTag: '01 // PROJECTS',
|
fpHeaderTag: '01 // PROJECTS',
|
||||||
fpAriaLabel: "Bryan Baño's featured projects",
|
fpAriaLabel: "Bryan Baño's featured projects",
|
||||||
fpPlaceholderImage: 'Image',
|
fpPlaceholderImage: 'No screenshots',
|
||||||
fpAltPrefix: 'Screenshot of ',
|
fpAltPrefix: 'Screenshot of ',
|
||||||
|
fpCarouselPrev: 'Previous project',
|
||||||
|
fpCarouselNext: 'Next project',
|
||||||
|
fpInDevelopment: 'In Development',
|
||||||
|
fpNoImagesTechnical: 'Architecture & CACES Audit Spec',
|
||||||
projectDetailChallenge: 'Challenges',
|
projectDetailChallenge: 'Challenges',
|
||||||
projectDetailSolution: 'Solution',
|
projectDetailSolution: 'Solution',
|
||||||
projectDetailResults: 'Results',
|
projectDetailResults: 'Results',
|
||||||
projectDetailCloseAria: 'Close project details',
|
projectDetailCloseAria: 'Close project details',
|
||||||
prj1Subtitle: 'CACES Institutional Accreditation',
|
|
||||||
prj1Desc: 'Led platform development centralizing evidence upload and validation with advanced security via JWT and SharePoint.',
|
// PRJ-01: SIPLECE (No images, technical / accreditation focus)
|
||||||
prj1Impact: 'Automated indicator tracking, eliminating physical paper management with complete traceability for government audits.',
|
prj1Subtitle: 'CACES Accreditation & Document Governance',
|
||||||
prj1Challenge: 'The accreditation process was fragmented across spreadsheets, folders, and multiple validation stages, which made evidence control slow and vulnerable to omissions during audits.',
|
prj1Desc: 'Centralized institutional platform to upload, audit, and validate technical-academic evidence required by CACES, integrating SharePoint, JWT authentication, and role-based access control.',
|
||||||
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.',
|
prj1Impact: 'Automated institutional indicator tracking, eliminating manual paperwork and guaranteeing complete traceability for governmental audits.',
|
||||||
prj1Result1: 'Reduced manual evidence review time and eliminated duplicate records across departments.',
|
prj1Challenge: 'The accreditation process was fragmented across desynchronized spreadsheets, physical binders, and informal approvals, creating severe delays and risking critical evidence omissions.',
|
||||||
prj1Result2: 'Improved audit readiness with full traceability, timestamps, and version control for every document.',
|
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.',
|
||||||
prj1Result3: 'Strengthened institutional governance by standardizing the approval flow for each accreditation requirement.',
|
prj1Result1: 'Drastically reduced review and validation turnaround times between academic and administrative departments.',
|
||||||
prj2Subtitle: 'Comprehensive Workshop & Work Order Management',
|
prj1Result2: 'Achieved 100% document version control and traceability for official CACES evaluation committees.',
|
||||||
prj2Institution: 'Automotive Sector',
|
prj1Result3: 'Standardized institutional governance for 100+ evaluation indicators and quality metrics.',
|
||||||
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.',
|
// PRJ-02: Ammi-Online (Full academic management with images)
|
||||||
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.',
|
prj2Subtitle: 'Academic Management, Admissions, Payments & Grades',
|
||||||
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.',
|
prj2Institution: 'Higher Education / Academic Platform',
|
||||||
prj2Result1: 'Minimized inventory shortages by improving stock visibility and replenishment planning.',
|
prj2Desc: 'Comprehensive web ecosystem for full student lifecycle management: online applications, automated enrollments, payment processing, and real-time grade records.',
|
||||||
prj2Result2: 'Reduced operational friction with digital work orders and complete vehicle histories.',
|
prj2Impact: 'Unified academic and financial workflows into a single high-concurrency system, cutting enrollment wait times and guaranteeing instant payment reconciliation.',
|
||||||
prj2Result3: 'Improved delivery time and service planning through cleaner operational traceability.',
|
prj2Challenge: 'Fragmented admission requests, manual bank receipt reconciliations, and spreadsheet grade records that were vulnerable to human errors and discrepancies.',
|
||||||
prj3Subtitle: 'Accelerated Virtual Education',
|
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.',
|
||||||
prj3Institution: 'Academic Platform',
|
prj2Result1: '100% automated enrollment and digital payment reconciliation without manual intervention.',
|
||||||
prj3Desc: 'Led academic management platform development based on RESTful API architecture for high availability in remote learning.',
|
prj2Result2: 'Centralized real-time grade records with comprehensive teacher modification audit trails.',
|
||||||
prj3Impact: 'Centralizes academic administration and automates student tracking, eliminating manual enrollment and attendance processes.',
|
prj2Result3: 'Ultra-fast and reactive user experience for thousands of concurrent students via Angular Signals and OnPush.',
|
||||||
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.',
|
// PRJ-03: HR Vacations & Leaves Module (With images)
|
||||||
prj3Result1: 'Automated academic records and reduced the time spent on repetitive administrative tasks.',
|
prj3Subtitle: 'Vacation Control, FIFO Algorithm & Real-Time Legal Deeds',
|
||||||
prj3Result2: 'Improved data consistency for access, course enrollment, and attendance follow-up.',
|
prj3Institution: 'Corporate / Human Resources',
|
||||||
prj3Result3: 'Created a more resilient and scalable foundation for the digital education experience.',
|
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
|
// Terminal & Workstation
|
||||||
terminalWorkstationTag: '02 // INTERACTIVE WORKSTATION',
|
terminalWorkstationTag: '02 // INTERACTIVE WORKSTATION',
|
||||||
|
|
|
||||||
|
|
@ -77,38 +77,59 @@ export const ES_TRANSLATIONS: TranslationDictionary = {
|
||||||
// Featured Projects
|
// Featured Projects
|
||||||
fpHeaderTag: '01 // PROYECTOS',
|
fpHeaderTag: '01 // PROYECTOS',
|
||||||
fpAriaLabel: 'Proyectos reales de Bryan Baño',
|
fpAriaLabel: 'Proyectos reales de Bryan Baño',
|
||||||
fpPlaceholderImage: 'Imagen',
|
fpPlaceholderImage: 'Sin capturas',
|
||||||
fpAltPrefix: 'Captura de pantalla de ',
|
fpAltPrefix: 'Captura de pantalla de ',
|
||||||
|
fpCarouselPrev: 'Proyecto anterior',
|
||||||
|
fpCarouselNext: 'Siguiente proyecto',
|
||||||
|
fpInDevelopment: 'En Desarrollo',
|
||||||
|
fpNoImagesTechnical: 'Arquitectura & Auditoría CACES',
|
||||||
projectDetailChallenge: 'Complicaciones',
|
projectDetailChallenge: 'Complicaciones',
|
||||||
projectDetailSolution: 'Solución',
|
projectDetailSolution: 'Solución',
|
||||||
projectDetailResults: 'Resultados',
|
projectDetailResults: 'Resultados',
|
||||||
projectDetailCloseAria: 'Cerrar detalle del proyecto',
|
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.',
|
// PRJ-01: SIPLECE (Sin imágenes, enfoque técnico / acreditación)
|
||||||
prj1Impact: 'Automatizó el seguimiento de indicadores, eliminando la gestión física de documentos con trazabilidad total para auditorías gubernamentales.',
|
prj1Subtitle: 'Acreditación CACES & Gestión Documental',
|
||||||
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.',
|
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.',
|
||||||
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.',
|
prj1Impact: 'Automatizó el seguimiento de indicadores institucionales, erradicando la manipulación física de documentos y asegurando trazabilidad completa para auditorías gubernamentales.',
|
||||||
prj1Result1: 'Reduje el tiempo de revisión manual de evidencias y eliminé registros duplicados entre departamentos.',
|
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.',
|
||||||
prj1Result2: 'Mejoré la preparación para auditorías con trazabilidad completa, marcas de tiempo y control de versiones para cada documento.',
|
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.',
|
||||||
prj1Result3: 'Fortalecí la gobernanza institucional al estandarizar el flujo de aprobación de cada requisito de acreditación.',
|
prj1Result1: 'Reducción drástica del tiempo de revisión y validación de evidencias entre departamentos académicos y administrativos.',
|
||||||
prj2Subtitle: 'Gestión Integral de Talleres & Órdenes',
|
prj1Result2: 'Trazabilidad y versionado al 100% de cada documento para auditorías oficiales del CACES.',
|
||||||
prj2Institution: 'Sector Automotriz',
|
prj1Result3: 'Gobernanza institucional estandarizada para más de 100 indicadores y criterios de evaluación.',
|
||||||
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.',
|
// PRJ-02: Ammi-Online (Gestión académica completa con imágenes)
|
||||||
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.',
|
prj2Subtitle: 'Gestión Académica, Solicitudes, Pagos & Calificaciones',
|
||||||
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.',
|
prj2Institution: 'Educación Superior / Academia',
|
||||||
prj2Result1: 'Disminuí los desabastecimientos mediante mejor visibilidad del stock y planificación de reposición.',
|
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.',
|
||||||
prj2Result2: 'Reducí la fricción operativa con órdenes digitales y un historial completo de cada vehículo.',
|
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.',
|
||||||
prj2Result3: 'Mejoré tiempos de entrega y planificación del servicio gracias a una trazabilidad más clara del trabajo.',
|
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.',
|
||||||
prj3Subtitle: 'Educación Virtual Acelerada',
|
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.',
|
||||||
prj3Institution: 'Plataforma Académica',
|
prj2Result1: 'Automatización del 100% del flujo de matrícula y validación digital de pagos sin intervención manual.',
|
||||||
prj3Desc: 'Lideré el desarrollo de la plataforma de gestión académica basada en arquitectura de APIs RESTful para alta disponibilidad en entornos remotos.',
|
prj2Result2: 'Centralización de actas de notas con auditoría rigurosa de modificaciones por docente.',
|
||||||
prj3Impact: 'Centraliza la administración académica y automatiza el seguimiento estudiantil, eliminando procesos manuales de inscripción y control de asistencia.',
|
prj2Result3: 'Navegación reactiva instantánea para miles de estudiantes simultáneos mediante ChangeDetection OnPush y Signals.',
|
||||||
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.',
|
// PRJ-03: Módulo Vacaciones & Permisos RRHH (Con imágenes)
|
||||||
prj3Result1: 'Automatizé registros académicos y reduje el tiempo invertido en tareas administrativas repetitivas.',
|
prj3Subtitle: 'Control de Vacaciones, Algoritmo FIFO & Actas en Tiempo Real',
|
||||||
prj3Result2: 'Mejoré la consistencia de datos para acceso, matriculación y seguimiento de asistencia.',
|
prj3Institution: 'Corporativo / Talento Humano',
|
||||||
prj3Result3: 'Construí una base más resiliente y escalable para la experiencia de educación digital.',
|
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
|
// Terminal & Workstation
|
||||||
terminalWorkstationTag: '02 // INTERACTIVE WORKSTATION',
|
terminalWorkstationTag: '02 // INTERACTIVE WORKSTATION',
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,10 @@ export interface TranslationDictionary {
|
||||||
readonly fpAriaLabel: string;
|
readonly fpAriaLabel: string;
|
||||||
readonly fpPlaceholderImage: string;
|
readonly fpPlaceholderImage: string;
|
||||||
readonly fpAltPrefix: string;
|
readonly fpAltPrefix: string;
|
||||||
|
readonly fpCarouselPrev: string;
|
||||||
|
readonly fpCarouselNext: string;
|
||||||
|
readonly fpInDevelopment: string;
|
||||||
|
readonly fpNoImagesTechnical: string;
|
||||||
readonly projectDetailChallenge: string;
|
readonly projectDetailChallenge: string;
|
||||||
readonly projectDetailSolution: string;
|
readonly projectDetailSolution: string;
|
||||||
readonly projectDetailResults: string;
|
readonly projectDetailResults: string;
|
||||||
|
|
@ -109,6 +113,15 @@ export interface TranslationDictionary {
|
||||||
readonly prj3Result1: string;
|
readonly prj3Result1: string;
|
||||||
readonly prj3Result2: string;
|
readonly prj3Result2: string;
|
||||||
readonly prj3Result3: 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
|
// Terminal & Workstation
|
||||||
readonly terminalWorkstationTag: string;
|
readonly terminalWorkstationTag: string;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* Featured Projects Component Styles */
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -44,17 +45,64 @@
|
||||||
letter-spacing: 0.08em;
|
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;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 768px) {
|
||||||
.projects-stack {
|
.projects-grid {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: var(--space-5);
|
|
||||||
align-items: start;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +112,6 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
min-height: 100%;
|
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(16, 20, 25, 0.92), rgba(12, 16, 20, 0.88)),
|
linear-gradient(180deg, rgba(16, 20, 25, 0.92), rgba(12, 16, 20, 0.88)),
|
||||||
rgba(11, 15, 18, 0.85);
|
rgba(11, 15, 18, 0.85);
|
||||||
|
|
@ -74,11 +121,19 @@
|
||||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition:
|
transition: transform var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
transform var(--transition-fast),
|
animation: cardFadeIn 0.28s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
border-color var(--transition-fast),
|
}
|
||||||
box-shadow var(--transition-fast),
|
|
||||||
background var(--transition-fast);
|
@keyframes cardFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0.55;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card:hover,
|
.project-card:hover,
|
||||||
|
|
@ -107,6 +162,108 @@
|
||||||
justify-content: center;
|
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 {
|
.project-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
@ -115,40 +272,6 @@
|
||||||
filter: saturate(1.08) contrast(1.04);
|
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 {
|
.project-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -267,6 +390,47 @@
|
||||||
border-radius: 1.4rem;
|
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) {
|
@media (max-width: 540px) {
|
||||||
.project-card {
|
.project-card {
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
|
|
@ -281,4 +445,3 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,39 @@
|
||||||
<div class="featured-projects-wrapper" role="region" [attr.aria-label]="i18n.t().fpAriaLabel">
|
<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">
|
<header class="section-micro-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="pulse-indicator" aria-hidden="true"></span>
|
<span class="pulse-indicator" aria-hidden="true"></span>
|
||||||
<span class="header-tag">{{ i18n.t().fpHeaderTag }}</span>
|
<span class="header-tag">{{ i18n.t().fpHeaderTag }}</span>
|
||||||
</div>
|
</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>
|
</header>
|
||||||
|
|
||||||
<div class="projects-stack">
|
<!-- Cuadrícula de Proyectos (Grid estático sin scroll horizontal) -->
|
||||||
@for (prj of projects(); track prj.id) {
|
<div class="projects-grid">
|
||||||
|
@for (prj of visibleProjects(); track prj.id) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="project-card"
|
class="project-card"
|
||||||
|
|
@ -16,20 +42,32 @@
|
||||||
[attr.aria-pressed]="selectedProjectId() === prj.id"
|
[attr.aria-pressed]="selectedProjectId() === prj.id"
|
||||||
(click)="selectProject(prj.id)"
|
(click)="selectProject(prj.id)"
|
||||||
>
|
>
|
||||||
<div class="project-image-slot">
|
<div class="project-image-slot" [class.no-image]="!prj.imageUrl">
|
||||||
@if (!imageErrors()[prj.id] && prj.imageUrl) {
|
@if (prj.imageUrl && !imageErrors()[prj.imageUrl]) {
|
||||||
<img
|
<img
|
||||||
[src]="prj.imageUrl"
|
[src]="prj.imageUrl"
|
||||||
[alt]="i18n.t().fpAltPrefix + prj.title"
|
[alt]="i18n.t().fpAltPrefix + prj.title"
|
||||||
class="project-img"
|
class="project-img"
|
||||||
(error)="onImageError(prj.id)"
|
(error)="onImageError(prj.imageUrl)"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="image-placeholder">
|
<!-- Diseño Blueprint / Ficha Técnica para SIPLECE o proyectos sin capturas -->
|
||||||
<span class="placeholder-icon">◧</span>
|
<div class="tech-blueprint-slot">
|
||||||
<span class="placeholder-code">{{ prj.code }}</span>
|
<div class="blueprint-grid-overlay" aria-hidden="true"></div>
|
||||||
<span class="placeholder-label">{{ i18n.t().fpPlaceholderImage }}</span>
|
<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>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -52,12 +90,33 @@
|
||||||
}
|
}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</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) {
|
@if (selectedProject(); as selected) {
|
||||||
<div class="project-detail-overlay" (click)="clearSelection()">
|
<div class="project-detail-overlay" (click)="clearSelection()">
|
||||||
<div class="project-detail-shell" (click)="$event.stopPropagation()">
|
<div class="project-detail-shell" (click)="$event.stopPropagation()">
|
||||||
|
|
|
||||||
|
|
@ -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 { I18nService } from '../../../../application/services/i18n.service';
|
||||||
import { ProjectDetailComponent } from '../project-detail/project-detail.component';
|
import { ProjectDetailComponent } from '../project-detail/project-detail.component';
|
||||||
|
|
||||||
|
|
@ -15,7 +22,9 @@ export interface ProjectCardData {
|
||||||
readonly results: readonly string[];
|
readonly results: readonly string[];
|
||||||
readonly tags: readonly string[];
|
readonly tags: readonly string[];
|
||||||
readonly imageUrl?: string;
|
readonly imageUrl?: string;
|
||||||
|
readonly images?: readonly string[];
|
||||||
readonly accentColor: string;
|
readonly accentColor: string;
|
||||||
|
readonly inDevelopment?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -29,13 +38,91 @@ export interface ProjectCardData {
|
||||||
export class FeaturedProjectsComponent {
|
export class FeaturedProjectsComponent {
|
||||||
protected readonly i18n = inject(I18nService);
|
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[]>(() => {
|
protected readonly projects = computed<ProjectCardData[]>(() => {
|
||||||
const t = this.i18n.t();
|
const t = this.i18n.t();
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'siplece',
|
id: 'ammi',
|
||||||
code: 'PRJ-01',
|
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',
|
title: 'SIPLECE',
|
||||||
subtitle: t.prj1Subtitle,
|
subtitle: t.prj1Subtitle,
|
||||||
institution: 'IST Mayor Pedro Traversari',
|
institution: 'IST Mayor Pedro Traversari',
|
||||||
|
|
@ -44,55 +131,69 @@ export class FeaturedProjectsComponent {
|
||||||
challenge: t.prj1Challenge,
|
challenge: t.prj1Challenge,
|
||||||
solution: t.prj1Solution,
|
solution: t.prj1Solution,
|
||||||
results: [t.prj1Result1, t.prj1Result2, t.prj1Result3],
|
results: [t.prj1Result1, t.prj1Result2, t.prj1Result3],
|
||||||
tags: ['Angular', '.NET 8', 'MySQL', 'JWT', 'SharePoint'],
|
tags: ['Angular', '.NET 8', 'Clean Arch', 'JWT & RBAC', 'SharePoint'],
|
||||||
imageUrl: 'assets/images/projects/siplece.webp',
|
accentColor: '#7dcfff',
|
||||||
accentColor: '#7dcfff'
|
images: [] // Sin imágenes, diseño de ficha de arquitectura
|
||||||
},
|
|
||||||
{
|
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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 selectedProjectId = signal<string | null>(null);
|
||||||
|
|
||||||
protected readonly selectedProject = computed(() => {
|
protected readonly selectedProject = computed(() => {
|
||||||
const selectedId = this.selectedProjectId();
|
const selectedId = this.selectedProjectId();
|
||||||
if (!selectedId) {
|
if (!selectedId) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.projects().find(project => project.id === selectedId) ?? 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 readonly imageErrors = signal<Record<string, boolean>>({});
|
||||||
|
|
||||||
protected selectProject(projectId: string): void {
|
protected selectProject(projectId: string): void {
|
||||||
|
|
@ -103,7 +204,7 @@ export class FeaturedProjectsComponent {
|
||||||
this.selectedProjectId.set(null);
|
this.selectedProjectId.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
onImageError(projectId: string): void {
|
onImageError(imageKey: string): void {
|
||||||
this.imageErrors.update(curr => ({ ...curr, [projectId]: true }));
|
this.imageErrors.update(curr => ({ ...curr, [imageKey]: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-kicker {
|
.detail-kicker {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
margin-bottom: 0.35rem;
|
margin-bottom: 0.35rem;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
@ -35,6 +37,18 @@
|
||||||
text-transform: uppercase;
|
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 {
|
.project-detail-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(1.5rem, 2.2vw, 2.2rem);
|
font-size: clamp(1.5rem, 2.2vw, 2.2rem);
|
||||||
|
|
@ -69,34 +83,180 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-detail-visual {
|
.project-detail-visual {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
min-height: 260px;
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-image-viewport {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 220px;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
background: #0a0d12;
|
background: #0a0d12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-detail-visual img {
|
.main-image-viewport img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 260px;
|
font-family: var(--font-mono);
|
||||||
font-size: 1.25rem;
|
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;
|
font-weight: 800;
|
||||||
color: var(--zen-text-secondary);
|
color: var(--zen-text-secondary);
|
||||||
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.03), transparent 70%);
|
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.03), transparent 70%);
|
||||||
border: 1px dashed rgba(255, 255, 255, 0.12);
|
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 {
|
.project-detail-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
<section class="project-detail-panel" [style.--prj-accent]="project.accentColor" aria-live="polite">
|
<section class="project-detail-panel" [style.--prj-accent]="project.accentColor" aria-live="polite">
|
||||||
<div class="project-detail-header">
|
<div class="project-detail-header">
|
||||||
<div>
|
<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>
|
<h3>{{ project.title }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="close-button" (click)="onClose()" [attr.aria-label]="i18n.t().projectDetailCloseAria">
|
<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-layout">
|
||||||
<div class="project-detail-visual">
|
<div class="project-detail-visual">
|
||||||
@if (project.imageUrl) {
|
@if (project.images && project.images.length > 0) {
|
||||||
<img [src]="project.imageUrl" [alt]="project.title" loading="lazy" />
|
<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 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 {
|
} @else {
|
||||||
<div class="detail-placeholder">
|
<!-- Ficha de Arquitectura Técnica para proyectos sin imágenes (SIPLECE) -->
|
||||||
<span>{{ project.code }}</span>
|
<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>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 { I18nService } from '../../../../application/services/i18n.service';
|
||||||
import { ProjectCardData } from '../featured-projects/featured-projects.component';
|
import { ProjectCardData } from '../featured-projects/featured-projects.component';
|
||||||
|
|
||||||
|
|
@ -16,7 +16,27 @@ export class ProjectDetailComponent {
|
||||||
|
|
||||||
@Output() close = new EventEmitter<void>();
|
@Output() close = new EventEmitter<void>();
|
||||||
|
|
||||||
|
protected readonly activeImageIndex = signal<number>(0);
|
||||||
|
protected readonly imageErrors = signal<Record<string, boolean>>({});
|
||||||
|
|
||||||
protected onClose(): void {
|
protected onClose(): void {
|
||||||
this.close.emit();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 113 KiB |