diff --git a/src/frontend/angular.json b/src/frontend/angular.json index a7788ee..e3d5a58 100644 --- a/src/frontend/angular.json +++ b/src/frontend/angular.json @@ -44,8 +44,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "8kB", - "maximumError": "16kB" + "maximumWarning": "15kB", + "maximumError": "30kB" } ], "outputHashing": "all" diff --git a/src/frontend/public/apple-touch-icon.png b/src/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..530d3c9 Binary files /dev/null and b/src/frontend/public/apple-touch-icon.png differ diff --git a/src/frontend/public/favicon-16x16.png b/src/frontend/public/favicon-16x16.png new file mode 100644 index 0000000..c205491 Binary files /dev/null and b/src/frontend/public/favicon-16x16.png differ diff --git a/src/frontend/public/favicon-192.png b/src/frontend/public/favicon-192.png new file mode 100644 index 0000000..d4d79a3 Binary files /dev/null and b/src/frontend/public/favicon-192.png differ diff --git a/src/frontend/public/favicon-32x32.png b/src/frontend/public/favicon-32x32.png new file mode 100644 index 0000000..3ebacf7 Binary files /dev/null and b/src/frontend/public/favicon-32x32.png differ diff --git a/src/frontend/public/favicon-512.png b/src/frontend/public/favicon-512.png new file mode 100644 index 0000000..2f629de Binary files /dev/null and b/src/frontend/public/favicon-512.png differ diff --git a/src/frontend/public/favicon.ico b/src/frontend/public/favicon.ico index 57614f9..3811cf4 100644 Binary files a/src/frontend/public/favicon.ico and b/src/frontend/public/favicon.ico differ diff --git a/src/frontend/src/app/application/i18n/translations/en.ts b/src/frontend/src/app/application/i18n/translations/en.ts index 23dbf2e..a427930 100644 --- a/src/frontend/src/app/application/i18n/translations/en.ts +++ b/src/frontend/src/app/application/i18n/translations/en.ts @@ -14,6 +14,8 @@ export const EN_TRANSLATIONS: TranslationDictionary = { navTerminal: 'Terminal', navContact: 'Contact', navAll: 'View All', + navPrevSection: 'Previous section', + navNextSection: 'Next section', // Hero 3D preloading: 'PRELOADING', @@ -63,6 +65,9 @@ export const EN_TRANSLATIONS: TranslationDictionary = { tpAboutName: 'Bryan Baño', tpAboutDescription: 'I am a software engineer focused on building thoughtful, accessible, and technically solid digital experiences. I combine clean architecture, modern frontend development, and scalable backend solutions. I work with .NET, Angular, Signals, REST APIs, and relational databases.\n\nMy approach combines engineering and design. First I understand the problem, then simplify the experience, and finally build a maintainable solution. I prioritize performance, accessibility, separation of concerns, and code quality.\n\nI also explore interfaces inspired by Unix terminals, retro systems, Cyber-Zen aesthetics, and new ways to present technical information without sacrificing readability. Every project must be functional, expressive, and easy to maintain.\n\nI currently develop solutions that connect frontend, backend, visual design, and digital interaction. I am interested in creating products that not only work reliably, but also communicate a clear and memorable identity.', tpAboutAria: 'About Bryan Baño', + tpAboutLink: 'About me', + tpAsciiError: 'Could not load portrait.', + tpAsciiLoading: 'Loading portrait...', tpEyebrow: 'INGENIERÍA EN COMPUTACIÓN & EXPERTISE', tpTitle: 'Technical Skills & Areas of Interest', tpLeadStudent: '9th semester Computer Engineering student at', @@ -287,9 +292,22 @@ Approach: Zero over-engineering (Ponytail), high throughput, and Zen aesthetic d simTag: '03 // CLOUD ARCHITECTURE & SERVERLESS SECURITY', simHeading: 'Real-Time Incident Response Simulator', simSubheading: 'Interactive visualization of an automated active-defense architecture in AWS. Experience how an unauthorized IAM access event is detected, mitigated by a Lambda function, and dispatched via SES in under 180 ms without manual intervention.', - simBtnSimulate: 'Simulate Unauthorized IAM Access', + simModeStory: 'Explained View (Business)', + simModeTech: 'SecOps Telemetry View (Dev)', + simEmailLabel: 'Email to receive the forensic report:', + simEmailPlaceholder: 'your-email@company.com', + simEmailHint: 'Enter your email to receive cryptographic audit details of the contained incident.', + simBtnSimulate: 'Simulate Attack & Defense', simBtnSimulating: 'Containing Threat in Real-Time...', simBtnReset: 'Reset Simulation', + simStep1Title: 'Intrusion Attempt', + simStep1Desc: 'An attacker attempts to steal administrative credentials from an untrusted IP.', + simStep2Title: 'Instant Detection', + simStep2Desc: 'The security perimeter flags the anomaly and triggers active defense in under 50 milliseconds.', + simStep3Title: 'Automated Lockdown', + simStep3Desc: 'A serverless function revokes active sessions and seals the perimeter without human intervention.', + simStep4Title: 'Forensic Notification', + simStep4Desc: 'Forensic evidence is compiled and the security certificate is dispatched by email.', simThreatNodeTitle: 'Threat Vector', simThreatNodeSub: 'Anomalous iam:CreateAccessKey', simCloudTrailNodeTitle: 'AWS CloudTrail & EventBridge', @@ -304,5 +322,13 @@ Approach: Zero over-engineering (Ponytail), high throughput, and Zen aesthetic d simMetricArchitecture: 'Serverless Architecture', simStatusIdle: 'SYSTEM NOMINAL // LISTENING FOR EVENTS', simStatusActive: 'THREAT DETECTED // EXECUTING REMEDIATION', - simStatusContained: 'THREAT CONTAINED // SECURE STATE RESTORED' + simStatusContained: 'THREAT CONTAINED // SECURE STATE RESTORED', + simReportTitle: 'FORENSIC AUDIT CERTIFICATE // INCIDENT MITIGATED', + simReportTargetEmail: 'Audit Recipient:', + simReportStatusSecure: 'STATUS: 100% SECURE // ZERO LEAKS', + simReportTimeVsAvg: '184 ms (Vs. 4.2 hours industry average)', + simReportCredentialsCompromised: '0 credentials compromised', + simReportFinancialLoss: '$0.00 USD (Financial damage avoided)', + simReportDownloadBtn: 'Download Forensic Report (Demo PDF)', + simReportDemoBadge: 'LIVE AUTOGENERATED CERTIFICATE' }; diff --git a/src/frontend/src/app/application/i18n/translations/es.ts b/src/frontend/src/app/application/i18n/translations/es.ts index c355fee..2ded779 100644 --- a/src/frontend/src/app/application/i18n/translations/es.ts +++ b/src/frontend/src/app/application/i18n/translations/es.ts @@ -14,6 +14,8 @@ export const ES_TRANSLATIONS: TranslationDictionary = { navTerminal: 'Terminal', navContact: 'Contacto', navAll: 'Ver Todo', + navPrevSection: 'Sección anterior', + navNextSection: 'Siguiente sección', // Hero 3D preloading: 'PRECARGANDO', @@ -63,6 +65,9 @@ export const ES_TRANSLATIONS: TranslationDictionary = { tpAboutName: 'Bryan Baño', tpAboutDescription: 'Egresado de Ingeniería en Computación (EPN) | Desarrollador Full Stack apasionado por crear arquitecturas eficientes. Me enfoco en construir experiencias digitales claras, accesibles y técnicamente sólidas. Me interesa combinar arquitectura limpia, desarrollo frontend moderno y soluciones backend escalables. Trabajo con .NET, Angular, Signals, APIs REST y bases de datos relacionales.\n\nMi proceso combina ingeniería y diseño. Primero entiendo el problema, después simplifico la experiencia y finalmente construyo una solución mantenible. Priorizo el rendimiento, la accesibilidad, la separación de responsabilidades y la calidad del código.\n\nTambién exploro interfaces inspiradas en terminales Unix, sistemas retro, estética Cyber-Zen y nuevas formas de presentar información técnica sin sacrificar legibilidad. Cada proyecto debe ser funcional, expresivo y fácil de mantener.\n\nActualmente desarrollo soluciones que conectan frontend, backend, diseño visual e interacción digital. Me interesa crear productos que no solo funcionen correctamente, sino que también comuniquen una identidad clara y memorable.', tpAboutAria: 'Sobre Bryan Baño', + tpAboutLink: 'Acerca de mí', + tpAsciiError: 'No se pudo cargar el retrato.', + tpAsciiLoading: 'Cargando retrato...', tpEyebrow: 'INGENIERÍA EN COMPUTACIÓN & EXPERTISE', tpTitle: 'Habilidades Técnicas & Áreas de Interés', tpLeadStudent: 'Estudiante de 9no semestre de Ingeniería en Computación en la', @@ -287,9 +292,22 @@ Enfoque: Cero sobreingeniería (Ponytail), alta concurrencia y diseño estético simTag: 'ARQUITECTURA CLOUD & SEGURIDAD SERVERLESS', simHeading: 'Simulador de Respuesta a Incidentes en Tiempo Real', simSubheading: 'Visualización interactiva de una arquitectura de contención reactiva en AWS. Experimenta cómo un evento de acceso IAM no autorizado es detectado, mitigado por una función Lambda y reportado vía SES en menos de 180 ms sin intervención manual.', - simBtnSimulate: 'Simular Acceso IAM No Autorizado', + simModeStory: 'Vista Explicada (Negocio)', + simModeTech: 'Vista Telemetría SecOps (Dev)', + simEmailLabel: 'Correo para recibir el reporte forense:', + simEmailPlaceholder: 'tu-correo@empresa.com', + simEmailHint: 'Ingresa tu correo para recibir la auditoría criptográfica del incidente mitigado.', + simBtnSimulate: 'Simular Ataque y Defensa', simBtnSimulating: 'Conteniendo Amenaza en Tiempo Real...', simBtnReset: 'Reiniciar Simulación', + simStep1Title: 'Intento de Intrusión', + simStep1Desc: 'Un atacante intenta robar credenciales de administración desde una IP no autorizada.', + simStep2Title: 'Detección Instantánea', + simStep2Desc: 'El sistema monitorea anomalías y dispara la regla de defensa en menos de 50 milisegundos.', + simStep3Title: 'Bloqueo Automático', + simStep3Desc: 'Una función serverless revoca las sesiones y sella el perímetro sin intervención humana.', + simStep4Title: 'Notificación y Reporte', + simStep4Desc: 'Se compila la evidencia forense y se despacha el certificado de seguridad por correo.', simThreatNodeTitle: 'Vector de Amenaza', simThreatNodeSub: 'iam:CreateAccessKey anómalo', simCloudTrailNodeTitle: 'AWS CloudTrail & EventBridge', @@ -304,5 +322,13 @@ Enfoque: Cero sobreingeniería (Ponytail), alta concurrencia y diseño estético simMetricArchitecture: 'Arquitectura Serverless', simStatusIdle: 'SISTEMA NOMINAL // ESCUCHANDO EVENTOS', simStatusActive: 'AMENAZA DETECTADA // EJECUTANDO MITIGACIÓN', - simStatusContained: 'AMENAZA NEUTRALIZADA // ESTADO SEGURO RESTAURADO' + simStatusContained: 'AMENAZA NEUTRALIZADA // ESTADO SEGURO RESTAURADO', + simReportTitle: 'CERTIFICADO FORENSE DE INCIDENTE MITIGADO', + simReportTargetEmail: 'Destinatario de Auditoría:', + simReportStatusSecure: 'ESTADO: 100% SEGURO // SIN FILTRACIONES', + simReportTimeVsAvg: '184 ms (Vs. 4.2 horas promedio de la industria)', + simReportCredentialsCompromised: '0 credenciales comprometidas', + simReportFinancialLoss: '$0.00 USD (Daño financiero evitado)', + simReportDownloadBtn: 'Descargar Reporte Forense (Demo PDF)', + simReportDemoBadge: 'CERTIFICADO AUTOGENERADO EN VIVO' }; diff --git a/src/frontend/src/app/domain/models/i18n.model.ts b/src/frontend/src/app/domain/models/i18n.model.ts index 87b073e..15da0ea 100644 --- a/src/frontend/src/app/domain/models/i18n.model.ts +++ b/src/frontend/src/app/domain/models/i18n.model.ts @@ -14,6 +14,8 @@ export interface TranslationDictionary { readonly navTerminal: string; readonly navContact: string; readonly navAll: string; + readonly navPrevSection: string; + readonly navNextSection: string; // Hero 3D readonly preloading: string; @@ -63,6 +65,9 @@ export interface TranslationDictionary { readonly tpAboutName: string; readonly tpAboutDescription: string; readonly tpAboutAria: string; + readonly tpAboutLink: string; + readonly tpAsciiError: string; + readonly tpAsciiLoading: string; readonly tpEyebrow: string; readonly tpTitle: string; readonly tpLeadStudent: string; @@ -255,9 +260,22 @@ export interface TranslationDictionary { readonly simTag: string; readonly simHeading: string; readonly simSubheading: string; + readonly simModeStory: string; + readonly simModeTech: string; + readonly simEmailLabel: string; + readonly simEmailPlaceholder: string; + readonly simEmailHint: string; readonly simBtnSimulate: string; readonly simBtnSimulating: string; readonly simBtnReset: string; + readonly simStep1Title: string; + readonly simStep1Desc: string; + readonly simStep2Title: string; + readonly simStep2Desc: string; + readonly simStep3Title: string; + readonly simStep3Desc: string; + readonly simStep4Title: string; + readonly simStep4Desc: string; readonly simThreatNodeTitle: string; readonly simThreatNodeSub: string; readonly simCloudTrailNodeTitle: string; @@ -273,4 +291,12 @@ export interface TranslationDictionary { readonly simStatusIdle: string; readonly simStatusActive: string; readonly simStatusContained: string; + readonly simReportTitle: string; + readonly simReportTargetEmail: string; + readonly simReportStatusSecure: string; + readonly simReportTimeVsAvg: string; + readonly simReportCredentialsCompromised: string; + readonly simReportFinancialLoss: string; + readonly simReportDownloadBtn: string; + readonly simReportDemoBadge: string; } diff --git a/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.css b/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.css index 49ae00d..8436543 100644 --- a/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.css +++ b/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.css @@ -221,161 +221,146 @@ background: rgba(78, 155, 111, 0.12); } +.mobile-only { + display: none; +} + @media (max-width: 900px) { - .lateral-panel { - position: absolute; - z-index: 10; - width: calc(100% - 24px) !important; - max-width: 440px !important; - left: 50% !important; - right: auto !important; - transform: translateX(-50%) translateY(12px); - background: rgba(14, 17, 23, 0.88); - backdrop-filter: blur(14px); - -webkit-backdrop-filter: blur(14px); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: var(--zen-radius-sm, 6px); - padding: 12px 14px; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.75); - box-sizing: border-box; - } - - .lateral-panel.active { - transform: translateX(-50%) translateY(0); - } - - /* FASE 1: Lead arriba, Filosofía abajo */ - .pos-f1-left { - top: 56px !important; - bottom: auto !important; - } - - .pos-f1-left .lead-desc, - .pos-f1-left .lead-footer { + .desktop-only { display: none !important; } - .pos-f1-right { - top: auto !important; - bottom: max(16px, env(safe-area-inset-bottom, 16px)) !important; + .mobile-only { + display: flex; + flex-direction: column; + width: 100%; + gap: var(--space-4); + padding: var(--space-2) 0 var(--space-8); + box-sizing: border-box; } - /* FASE 2: Frontend arriba, Backend abajo */ - .pos-f2-right { - top: 56px !important; - bottom: auto !important; - padding: 10px 14px; + /* Tarjeta Principal de Perfil Móvil */ + .mobile-lead-card { + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-5); + background: + linear-gradient(180deg, rgba(18, 22, 30, 0.96), rgba(13, 16, 22, 0.92)), + #0d1117; + border: 1px solid var(--zen-border-subtle); + border-radius: var(--zen-radius-md); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35); + box-sizing: border-box; + width: 100%; } - .pos-f2-left { - top: auto !important; - bottom: max(16px, env(safe-area-inset-bottom, 16px)) !important; - } - - /* FASE 3: Laboratorio arriba, Clean Arch abajo */ - .pos-f3-right { - top: 56px !important; - bottom: auto !important; - padding: 10px 14px; - } - - .pos-f3-left { - top: auto !important; - bottom: max(16px, env(safe-area-inset-bottom, 16px)) !important; - } - - /* Tipografía y espaciado compacto para móvil */ - .lead-header { - gap: 4px; - margin-bottom: 8px; - flex-direction: row; + .mobile-lead-header { + display: flex; + align-items: center; + justify-content: space-between; flex-wrap: wrap; - align-items: center; - justify-content: space-between; - } - - .lead-status { - font-size: 0.62rem; - line-height: 1.2; - } - - .lead-tag { - font-size: 0.58rem; - line-height: 1.2; - } - - .lead-name { - font-size: 1.5rem; - margin: 0 0 4px 0; - line-height: 1.15; - } - - .lead-role { - font-size: 0.64rem; - margin-bottom: 6px; - line-height: 1.3; - } - - .lead-desc { - font-size: 0.74rem; - line-height: 1.45; - margin: 0 0 10px 0; - max-width: 100%; - } - - .lead-badges, .panel-tags { - gap: 6px; - } - - .lead-footer { - margin-top: 10px; - padding-top: 8px; - flex-direction: row; - justify-content: space-between; - align-items: center; gap: 8px; } - .lead-metric { - font-size: 0.62rem; - line-height: 1.3; + .mobile-lead-name { + font-family: var(--font-sans); + font-size: clamp(2rem, 7vw, 2.6rem); + font-weight: 800; + color: var(--zen-text-primary); + line-height: 1.12; + letter-spacing: -0.03em; + margin: 0; } - .lead-social-links { - flex-direction: row; - gap: 8px; - } - - .social-link { - width: 32px; - height: 32px; - } - - .social-link svg { - width: 16px; - height: 16px; - } - - .panel-title { - font-size: 0.95rem; - margin-bottom: 4px; - } - - .panel-desc { + .mobile-lead-role { + font-family: var(--font-sans); font-size: 0.72rem; - line-height: 1.35; - margin-bottom: 6px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--zen-accent-kintsugi); + margin: 0; } - .panel-eyebrow { - font-size: 0.6rem; - margin-bottom: 4px; + .mobile-lead-desc { + font-family: var(--font-sans); + font-size: 0.875rem; + color: var(--zen-text-secondary); + line-height: 1.65; + margin: 0; } - .zen-loader-pill { - top: 60px; - right: 12px; - font-size: 0.7rem; - padding: 3px 8px; + .mobile-lead-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 4px 0; + } + + .mobile-lead-footer { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + padding-top: var(--space-3); + border-top: 1px solid var(--zen-border-subtle); + margin-top: var(--space-1); + } + + .mobile-lead-metric { + font-family: var(--font-sans); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.08em; + color: var(--zen-text-muted); + } + + .mobile-social-links { + display: flex; + align-items: center; + gap: 10px; + } + + /* Cuadrícula de Pilares Técnicos Móvil */ + .mobile-pillars-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-3); + width: 100%; + } + + @media (min-width: 600px) { + .mobile-pillars-grid { + grid-template-columns: repeat(2, 1fr); + } + } + + .mobile-pillar-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: var(--space-4); + background: rgba(18, 22, 28, 0.7); + border: 1px solid var(--zen-border-subtle); + border-radius: var(--zen-radius-sm); + box-sizing: border-box; + backdrop-filter: blur(8px); + } + + .mobile-pillar-card .panel-title { + font-size: 1.05rem; + margin: 0; + } + + .mobile-pillar-card .panel-desc { + font-size: 0.78rem; + line-height: 1.5; + margin: 0; + } + + .mobile-pillar-card .panel-tags { + margin-top: 4px; } } diff --git a/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.html b/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.html index 72628d8..fa5df04 100644 --- a/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.html +++ b/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.html @@ -1,4 +1,5 @@ -
+ +
@@ -131,3 +132,122 @@
+ + +
+ +
+
+ {{ i18n.t().p1LeadStatus }} + {{ i18n.t().p1LeadTag }} +
+ +

{{ i18n.t().p1LeadName }}

+ {{ i18n.t().p1LeadRole }} +

{{ i18n.t().p1LeadDesc }}

+ +
+ {{ i18n.t().p1LeadBadge1 }} + {{ i18n.t().p1LeadBadge2 }} + {{ i18n.t().p1LeadBadge3 }} +
+ + +
+ + +
+ +
+ {{ i18n.t().p2LeftTag }} +

{{ i18n.t().p2LeftTitle }}

+

{{ i18n.t().p2LeftDesc }}

+
+ {{ i18n.t().p2LeftBadge1 }} + {{ i18n.t().p2LeftBadge2 }} +
+
+ + +
+ {{ i18n.t().p2RightTag }} +

{{ i18n.t().p2RightTitle }}

+

{{ i18n.t().p2RightDesc }}

+
+ {{ i18n.t().p2RightBadge1 }} + {{ i18n.t().p2RightBadge2 }} +
+
+ + +
+ {{ i18n.t().p1RightTag }} +

{{ i18n.t().p1RightTitle }}

+

{{ i18n.t().p1RightDesc }}

+
+ {{ i18n.t().p1RightBadge }} +
+
+ + +
+ {{ i18n.t().p3LeftTag }} +

{{ i18n.t().p3LeftTitle }}

+

{{ i18n.t().p3LeftDesc }}

+
+ {{ i18n.t().p3LeftBadge1 }} + {{ i18n.t().p3LeftBadge2 }} +
+
+ + +
+ {{ i18n.t().p3RightTag }} +

{{ i18n.t().p3RightTitle }}

+

{{ i18n.t().p3RightDesc }}

+
+ {{ i18n.t().p3RightBadge1 }} + {{ i18n.t().p3RightBadge2 }} +
+
+
+
diff --git a/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.ts b/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.ts index f6813ce..cb47329 100644 --- a/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.ts +++ b/src/frontend/src/app/presentation/features/hero-3d/hero-3d.component.ts @@ -8,7 +8,8 @@ import { ChangeDetectionStrategy, signal, inject, - NgZone + NgZone, + input } from '@angular/core'; import { I18nService } from '../../../application/services/i18n.service'; @@ -23,6 +24,8 @@ const LERP_FACTOR = 0.09; changeDetection: ChangeDetectionStrategy.OnPush }) export class Hero3dComponent implements OnInit, AfterViewInit, OnDestroy { + readonly initialPosition = input<'top' | 'bottom'>('top'); + @ViewChild('scrollContainer') private containerRef!: ElementRef; @ViewChild('canvas') private canvasRef!: ElementRef; @@ -47,17 +50,33 @@ export class Hero3dComponent implements OnInit, AfterViewInit, OnDestroy { private mediaQueryList?: MediaQueryList; ngOnInit(): void { + if (this.initialPosition() === 'bottom') { + this.targetFrame = TOTAL_FRAMES; + this.currentInterpolatedFrame = TOTAL_FRAMES; + this.lastRenderedFrame = TOTAL_FRAMES; + this.activePhase.set(3); + } + this.checkReducedMotion(); - this.preloadImages(); + if (typeof window !== 'undefined' && window.innerWidth <= 900) { + this.isLoaded.set(true); + } else { + this.preloadImages(); + } } ngAfterViewInit(): void { + if (!this.canvasRef || !this.containerRef) { + this.isLoaded.set(true); + return; + } + const canvas = this.canvasRef.nativeElement; this.ctx = canvas.getContext('2d', { alpha: false }); this.intersectionObserver = new IntersectionObserver(([entry]) => { this.isVisible = entry.isIntersecting; - if (this.isVisible && !this.isReducedMotion) { + if (this.isVisible && !this.isReducedMotion && typeof window !== 'undefined' && window.innerWidth > 900) { this.startAnimationLoop(); } else if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId); @@ -72,8 +91,27 @@ export class Hero3dComponent implements OnInit, AfterViewInit, OnDestroy { window.addEventListener('resize', this.onResize, { passive: true }); }); - this.onResize(); - this.onScroll(); + if (typeof window !== 'undefined' && window.innerWidth > 900) { + const container = this.containerRef.nativeElement; + const scrollableDistance = container.offsetHeight - window.innerHeight; + + if (this.initialPosition() === 'bottom' && scrollableDistance > 0) { + window.scrollTo({ top: scrollableDistance, behavior: 'instant' }); + this.targetFrame = TOTAL_FRAMES; + this.currentInterpolatedFrame = TOTAL_FRAMES; + this.lastRenderedFrame = TOTAL_FRAMES; + this.activePhase.set(3); + } else if (this.initialPosition() === 'top') { + window.scrollTo({ top: 0, behavior: 'instant' }); + this.targetFrame = 1; + this.currentInterpolatedFrame = 1; + this.lastRenderedFrame = 1; + this.activePhase.set(1); + } + + this.onResize(); + this.renderFrame(Math.round(this.currentInterpolatedFrame)); + } } private checkReducedMotion(): void { @@ -88,7 +126,7 @@ export class Hero3dComponent implements OnInit, AfterViewInit, OnDestroy { this.isLoopRunning = false; } this.renderFrame(1); - } else { + } else if (typeof window !== 'undefined' && window.innerWidth > 900) { this.startAnimationLoop(); } }); @@ -96,15 +134,17 @@ export class Hero3dComponent implements OnInit, AfterViewInit, OnDestroy { private preloadImages(): void { let loaded = 0; + const startFrame = this.initialPosition() === 'bottom' ? TOTAL_FRAMES : 1; const firstImg = new Image(); - firstImg.src = this.getFramePath(1); + firstImg.src = this.getFramePath(startFrame); firstImg.onload = () => { - this.images[1] = firstImg; + this.images[startFrame] = firstImg; loaded++; - this.renderFrame(1); + this.renderFrame(startFrame); - for (let i = 2; i <= TOTAL_FRAMES; i++) { + for (let i = 1; i <= TOTAL_FRAMES; i++) { + if (i === startFrame) continue; const img = new Image(); img.src = this.getFramePath(i); img.onload = () => { @@ -131,6 +171,7 @@ export class Hero3dComponent implements OnInit, AfterViewInit, OnDestroy { } private onScroll = (): void => { + if (typeof window !== 'undefined' && window.innerWidth <= 900) return; if (!this.isVisible || !this.containerRef || this.isReducedMotion) return; const container = this.containerRef.nativeElement; @@ -143,10 +184,26 @@ export class Hero3dComponent implements OnInit, AfterViewInit, OnDestroy { const progress = Math.min(Math.max(rawProgress, 0), 1); this.targetFrame = Math.min(Math.max(1, 1 + progress * (TOTAL_FRAMES - 1)), TOTAL_FRAMES); + // Si ocurre un salto grande de posición inicial, sincronizar frame directamente sin animación forzada + const jumpDiff = Math.abs(this.targetFrame - this.currentInterpolatedFrame); + if (jumpDiff > 40 && !this.isLoopRunning) { + this.currentInterpolatedFrame = this.targetFrame; + this.renderFrame(Math.round(this.targetFrame)); + this.updateActivePhase(this.targetFrame); + return; + } + this.startAnimationLoop(); }; private onResize = (): void => { + if (typeof window !== 'undefined' && window.innerWidth <= 900) { + if (this.animationFrameId !== null) { + cancelAnimationFrame(this.animationFrameId); + this.isLoopRunning = false; + } + return; + } this.resizeCanvas(); this.renderFrame(Math.round(this.currentInterpolatedFrame)); }; diff --git a/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.css b/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.css index a96dbcc..10e4765 100644 --- a/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.css +++ b/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.css @@ -1,344 +1,126 @@ -:host { display: block; width: 100%; } - -.zen-sim-container { - max-width: 1540px; - margin: 0 auto; - padding: 0 24px 40px; - display: flex; - flex-direction: column; - gap: var(--space-4, 16px); - box-sizing: border-box; - width: 100%; -} - -/* Cabecera Normalizada de Sección */ -.sim-section-header { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding-bottom: var(--space-2, 8px); - border-bottom: 1px solid var(--zen-border-subtle, #1e2533); - margin-bottom: var(--space-1, 4px); - flex-wrap: wrap; - gap: var(--space-3, 12px); -} - -.header-left { - display: flex; - align-items: center; - gap: var(--space-3, 12px); -} - -.title-with-badge { - display: flex; - align-items: center; - gap: var(--space-3, 12px); -} - -.pulse-indicator { - width: 10px; - height: 10px; - border-radius: 50%; - background-color: var(--zen-accent-kintsugi, #e0af68); - box-shadow: 0 0 12px var(--zen-accent-kintsugi, #e0af68); - flex-shrink: 0; -} - -.section-title { - margin: 0; - font-family: var(--font-sans); - font-size: clamp(1.6rem, 3vw, 2.25rem); - font-weight: 800; - letter-spacing: -0.01em; - color: var(--zen-text-primary, #f0f3f8); - line-height: 1.15; - text-transform: uppercase; -} - -.status-pill { - display: inline-flex; - align-items: center; - gap: 8px; - background: rgba(30, 41, 59, 0.6); - border: 1px solid rgba(255, 255, 255, 0.1); - padding: 6px 14px; - border-radius: 999px; - font-family: var(--font-mono, monospace); - font-size: 0.75rem; - font-weight: 600; - color: #9aa5b8; -} - -.status-beacon { - width: 8px; - height: 8px; - border-radius: 50%; - background: #38bdf8; - box-shadow: 0 0 8px #38bdf8; - animation: pulseBeacon 2s infinite ease-in-out; -} - -.zen-sim-board { - background: #0a0d14; - border: 1px solid #1e2533; - border-radius: 6px; - padding: 20px; - display: flex; - flex-direction: column; - gap: 18px; -} - -.diagram-canvas { - display: grid; - grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; - align-items: center; - gap: 8px; -} - -.arch-node { - background: #11151f; - border: 1px solid #1e293b; - border-radius: 4px; - padding: 12px 10px; - display: flex; - flex-direction: column; - gap: 6px; - position: relative; - min-height: 125px; - box-sizing: border-box; -} - -.node-badge-tag { - font-family: var(--font-mono, monospace); - font-size: 0.625rem; - font-weight: 800; - letter-spacing: 1px; - padding: 2px 6px; - border-radius: 2px; - width: fit-content; -} -.node-badge-tag.threat { background: rgba(239, 68, 68, 0.2); color: #f87171; } -.node-badge-tag.detection { background: rgba(56, 189, 248, 0.2); color: #38bdf8; } -.node-badge-tag.remediation { background: rgba(34, 197, 94, 0.2); color: #4ade80; } -.node-badge-tag.notification { background: rgba(197, 155, 39, 0.2); color: #e2b94a; } - -.node-icon-frame { - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - color: #9aa5b8; -} - -.node-svg { width: 20px; height: 20px; } - -.node-info { display: flex; flex-direction: column; gap: 2px; } - -.node-title { - font-family: var(--font-sans, sans-serif); - font-size: 0.8125rem; - font-weight: 700; - color: #f0f3f8; - margin: 0; -} - -.node-detail, .node-meta-ip, .node-meta-svc { - font-family: var(--font-mono, monospace); - font-size: 0.6875rem; - color: #9aa5b8; -} -.node-meta-ip, .node-meta-svc { font-size: 0.625rem; color: #64748b; } - -.node-status-indicator { - position: absolute; - top: 8px; - right: 8px; - width: 6px; - height: 6px; - border-radius: 50%; - background: #334155; -} - -.threat-node.active { border-color: #ef4444; background: #1c1316; } -.threat-node.active .node-icon-frame, .threat-node.active .node-status-indicator.ping { color: #ef4444; background: #ef4444; } - -.detection-node.active { border-color: #38bdf8; background: #0f1c29; } -.detection-node.active .node-icon-frame, .detection-node.active .node-status-indicator.ping { color: #38bdf8; background: #38bdf8; } - -.remediation-node.active { border-color: #22c55e; background: #0f2316; } -.remediation-node.active .node-icon-frame, .remediation-node.active .node-status-indicator.ping { color: #22c55e; background: #22c55e; } - -.notification-node.active { border-color: #c59b27; background: #231c0e; } -.notification-node.active .node-icon-frame, .notification-node.active .node-status-indicator.ping { color: #c59b27; background: #c59b27; } - -.arch-node.past { border-color: rgba(34, 197, 94, 0.4); } -.arch-node.past .node-status-indicator { background: #22c55e; } - -.flow-connector { - display: flex; - align-items: center; - justify-content: center; - position: relative; - height: 20px; - color: #334155; -} - -.beam-line { - position: absolute; - top: 50%; - left: 0; - right: 0; - height: 2px; - background: #1e293b; - transform: translateY(-50%); -} - -.flow-arrow { - position: relative; - z-index: 2; - font-size: 0.625rem; - background: #0a0d14; - padding: 0 2px; -} - -.flow-connector.flowing { color: #38bdf8; } -.flow-connector.flowing .beam-line { - background: linear-gradient(90deg, #ef4444, #38bdf8, #22c55e); -} - -.sim-action-deck { - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - flex-wrap: wrap; -} - -.sim-trigger-btn { - display: inline-flex; - align-items: center; - gap: 8px; - background: #c59b27; - color: #07080b; - border: 1px solid #e2b94a; - border-radius: 4px; - padding: 10px 18px; - font-family: var(--font-sans, sans-serif); - font-size: 0.875rem; - font-weight: 700; - cursor: pointer; - transition: opacity 0.2s; -} - -.sim-trigger-btn:disabled { opacity: 0.65; cursor: not-allowed; } - -.sim-reset-btn { - background: transparent; - color: #9aa5b8; - border: 1px solid #2d3748; - border-radius: 4px; - padding: 9px 16px; - font-family: var(--font-sans, sans-serif); - font-size: 0.8125rem; - font-weight: 600; - cursor: pointer; -} - -.secops-console-frame { - background: #06080c; - border: 1px solid #1e2533; - border-radius: 4px; - overflow: hidden; - font-family: var(--font-mono, monospace); - display: flex; - flex-direction: column; -} - -.console-titlebar { - background: #0f131a; - padding: 6px 10px; - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid #1e2533; -} - -.console-dots { display: flex; gap: 5px; } -.dot { width: 7px; height: 7px; border-radius: 50%; } -.dot.red { background: #ef4444; } -.dot.yellow { background: #eab308; } -.dot.green { background: #22c55e; } - -.console-title { font-size: 0.6875rem; color: #94a3b8; } - -.console-pill { - font-size: 0.5625rem; - color: #38bdf8; - background: rgba(56, 189, 248, 0.12); - padding: 2px 5px; - border-radius: 2px; -} - -.console-body { - padding: 8px 10px; - min-height: 100px; - max-height: 160px; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 4px; - font-size: 0.75rem; - line-height: 1.4; -} - -.log-row { display: flex; gap: 8px; color: #94a3b8; } -.log-time { color: #475569; flex-shrink: 0; } -.log-tag { font-weight: 700; flex-shrink: 0; } -.log-row.warn .log-tag, .log-row.warn .log-msg { color: #f87171; } -.log-row.action .log-tag, .log-row.action .log-msg { color: #fde047; } -.log-row.success .log-tag, .log-row.success .log-msg { color: #4ade80; } - -.console-cursor-row { display: flex; align-items: center; gap: 6px; margin-top: 3px; } -.console-prompt { color: #34d399; font-size: 0.6875rem; } -.cursor-block { width: 6px; height: 11px; background: #38bdf8; } - -.arch-metrics-bar { - display: flex; - align-items: center; - justify-content: space-around; - padding-top: 10px; - border-top: 1px solid #1a2230; - flex-wrap: wrap; - gap: 12px; -} - -.metric-card { display: flex; flex-direction: column; gap: 2px; text-align: center; } - -.metric-label { - font-family: var(--font-mono, monospace); - font-size: 0.6875rem; - color: #5e697d; - text-transform: uppercase; -} - -.metric-val { - font-family: var(--font-mono, monospace); - font-size: 0.8125rem; - font-weight: 700; - color: #f0f3f8; -} -.metric-val.highlight { color: #38bdf8; } -.metric-val.success { color: #4ade80; } - -.metric-divider { width: 1px; height: 20px; background: #1e293b; } - -@keyframes pulseBeacon { 0%, 100% { opacity: 0.6; transform: scale(1); } 50% { opacity: 1; transform: scale(1.3); } } - -@media (max-width: 900px) { - .diagram-canvas { grid-template-columns: 1fr; gap: 10px; } - .flow-connector { height: 16px; width: 100%; transform: rotate(90deg); } - .metric-divider { display: none; } - .arch-metrics-bar { flex-direction: column; gap: 10px; } -} +:host{display:block;width:100%} +.zen-sim-container{max-width:1540px;margin:0 auto;padding:0 24px 40px;display:flex;flex-direction:column;gap:16px;width:100%;box-sizing:border-box} +.sim-section-header,.report-header,.console-titlebar,.arch-metrics-bar{display:flex;align-items:center;justify-content:space-between} +.sim-section-header{width:100%;padding-bottom:8px;border-bottom:1px solid #1e2533;margin-bottom:4px;flex-wrap:wrap;gap:12px} +.header-left,.report-header-left,.email-input-wrapper,.story-step-card,.arch-node,.node-info,.secops-console-frame,.forensic-report-card,.report-metric-box,.metric-card{display:flex;flex-direction:column} +.header-left{gap:4px} +.title-with-badge,.email-label,.action-buttons,.sim-trigger-btn,.mode-btn,.console-cursor-row,.download-report-btn{display:flex;align-items:center} +.title-with-badge{gap:12px} +.section-tag,.step-badge,.node-badge-tag,.report-badge,.box-label,.metric-label,.console-title,.console-pill,.log-row,.status-pill,.step-meta,.node-detail,.node-meta-ip,.node-meta-svc,.email-field,.box-val,.metric-val,.console-prompt{font-family:var(--font-mono,monospace)} +.section-tag{font-size:.6875rem;color:#5e697d;letter-spacing:.08em} +.pulse-indicator{width:10px;height:10px;border-radius:50%;background:#c59b27;box-shadow:0 0 12px #c59b27;flex-shrink:0} +.section-title{margin:0;font-size:clamp(1.6rem,3vw,2.25rem);font-weight:800;color:#f0f3f8;line-height:1.15;text-transform:uppercase} +.header-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap} +.view-mode-toggle{display:inline-flex;background:#0d1117;border:1px solid #1e2533;border-radius:4px;padding:2px;gap:2px} +.mode-btn{gap:6px;padding:5px 10px;background:0 0;border:none;border-radius:2px;color:#9aa5b8;font-size:.75rem;font-weight:600;cursor:pointer} +.mode-btn.active{background:#131720;color:#c59b27;box-shadow:0 1px 4px rgba(0,0,0,.4)} +.mode-icon{width:13px;height:13px} +.status-pill{gap:8px;background:rgba(30,41,59,.6);border:1px solid rgba(255,255,255,.1);padding:6px 14px;border-radius:999px;font-size:.75rem;font-weight:600;color:#9aa5b8} +.status-beacon{width:8px;height:8px;border-radius:50%;background:#38bdf8;box-shadow:0 0 8px #38bdf8;animation:pulseBeacon 2s infinite ease-in-out} +.status-pill.danger .status-beacon,.dot.red{background:#ef4444} +.status-pill.active .status-beacon,.dot.yellow{background:#eab308} +.status-pill.contained .status-beacon,.dot.green{background:#22c55e} +.zen-sim-board{background:#0a0d14;border:1px solid #1e2533;border-radius:6px;padding:24px;display:flex;flex-direction:column;gap:20px} +.email-control-deck{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;background:#10141d;border:1px solid #1c2331;border-radius:4px;padding:14px 18px;flex-wrap:wrap} +.email-input-wrapper{gap:6px;flex:1;min-width:260px} +.email-label{gap:6px;font-size:.78rem;font-weight:600;color:#9aa5b8} +.email-icon{width:14px;height:14px;color:#c59b27} +.email-field{background:#07090e;border:1px solid #1e2533;border-radius:3px;color:#f0f3f8;font-size:.8125rem;padding:8px 12px;outline:0} +.email-field:focus{border-color:#c59b27;box-shadow:0 0 6px rgba(197,155,39,.25)} +.action-buttons{gap:10px} +.sim-trigger-btn{gap:8px;background:#c59b27;color:#07080b;border:1px solid #e2b94a;border-radius:3px;padding:9px 18px;font-size:.84rem;font-weight:700;cursor:pointer} +.sim-trigger-btn:hover:not(:disabled){background:#d4af37;box-shadow:0 0 12px rgba(197,155,39,.4)} +.sim-trigger-btn:disabled{opacity:.6;cursor:not-allowed} +.sim-reset-btn{background:0 0;color:#9aa5b8;border:1px solid #2d3748;border-radius:3px;padding:8px 14px;font-size:.8125rem;font-weight:600;cursor:pointer} +.story-grid-flow{display:flex;align-items:stretch;justify-content:space-between;gap:8px;width:100%} +.story-step-card{flex:1;background:#11151f;border:1px solid #1e293b;border-radius:4px;padding:16px 14px;gap:8px;transition:all .3s} +.step-badge,.node-badge-tag{font-size:.625rem;font-weight:700;letter-spacing:.08em} +.step-badge{color:#64748b} +.step-icon-wrap{width:36px;height:36px;border-radius:4px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);color:#9aa5b8} +.step-icon-wrap svg{width:20px;height:20px} +.node-badge-tag.threat,.step-icon-wrap.danger{color:#f87171;background:rgba(239,68,68,.1)} +.node-badge-tag.detection,.step-icon-wrap.warning{color:#38bdf8;background:rgba(56,189,248,.1)} +.node-badge-tag.remediation,.step-icon-wrap.action{color:#4ade80;background:rgba(34,197,94,.1)} +.node-badge-tag.notification,.step-icon-wrap.success{color:#e2b94a;background:rgba(197,155,39,.1)} +.step-title,.node-title{font-size:.8125rem;font-weight:700;color:#f0f3f8;margin:0} +.step-title{font-size:.875rem} +.step-desc{font-size:.75rem;color:#9aa5b8;line-height:1.45;margin:0;flex-grow:1} +.step-meta{font-size:.6875rem;color:#64748b;border-top:1px solid #1c2331;padding-top:6px} +.story-step-card.active{border-color:#c59b27;background:#171b26;box-shadow:0 0 16px rgba(197,155,39,.2);transform:translateY(-2px)} +.story-step-card.completed,.arch-node.past{border-color:rgba(34,197,94,.4)} +.story-arrow{display:flex;align-items:center;justify-content:center;color:#2d3748;font-size:1.1rem;font-weight:700;padding:0 4px} +.story-arrow.active{color:#c59b27} +.diagram-canvas{display:grid;grid-template-columns:1fr auto 1fr auto 1fr auto 1fr;align-items:center;gap:8px;width:100%} +.arch-node{background:#11151f;border:1px solid #1e293b;border-radius:4px;padding:12px 10px;gap:6px;position:relative;min-height:125px;box-sizing:border-box} +.node-badge-tag{padding:2px 6px;border-radius:2px;width:fit-content} +.node-icon-frame{width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:#9aa5b8;flex-shrink:0} +.node-svg{width:22px;height:22px;max-width:22px;max-height:22px;display:block} +.node-info{gap:2px} +.node-detail,.node-meta-ip,.node-meta-svc{font-size:.6875rem;color:#9aa5b8} +.node-meta-ip,.node-meta-svc{font-size:.625rem;color:#64748b} +.node-status-indicator{position:absolute;top:8px;right:8px;width:6px;height:6px;border-radius:50%;background:#334155} +.threat-node.active{border-color:#ef4444;background:#1c1316} +.threat-node.active .node-icon-frame,.threat-node.active .node-status-indicator.ping{color:#ef4444;background:#ef4444} +.detection-node.active{border-color:#38bdf8;background:#0f1c29} +.detection-node.active .node-icon-frame,.detection-node.active .node-status-indicator.ping{color:#38bdf8;background:#38bdf8} +.remediation-node.active{border-color:#22c55e;background:#0f2316} +.remediation-node.active .node-icon-frame,.remediation-node.active .node-status-indicator.ping{color:#22c55e;background:#22c55e} +.notification-node.active{border-color:#c59b27;background:#231c0e} +.notification-node.active .node-icon-frame,.notification-node.active .node-status-indicator.ping{color:#c59b27;background:#c59b27} +.arch-node.past .node-status-indicator{background:#22c55e} +.flow-connector{display:flex;align-items:center;justify-content:center;position:relative;height:20px;color:#334155} +.beam-line{position:absolute;top:50%;left:0;right:0;height:2px;background:#1e293b;transform:translateY(-50%)} +.flow-arrow{position:relative;z-index:2;font-size:.625rem;background:#0a0d14;padding:0 2px} +.flow-connector.flowing{color:#38bdf8} +.flow-connector.flowing .beam-line{background:linear-gradient(90deg,#ef4444,#38bdf8,#22c55e)} +.secops-console-frame{background:#06080c;border:1px solid #1e2533;border-radius:4px;overflow:hidden} +.console-titlebar{background:#0f131a;padding:6px 10px;border-bottom:1px solid #1e2533} +.console-dots{display:flex;gap:5px} +.dot{width:7px;height:7px;border-radius:50%} +.console-title{font-size:.6875rem;color:#94a3b8} +.console-pill{font-size:.5625rem;color:#38bdf8;background:rgba(56,189,248,.12);padding:2px 5px;border-radius:2px} +.console-body{padding:8px 10px;min-height:100px;max-height:160px;overflow-y:auto;display:flex;flex-direction:column;gap:4px;font-size:.75rem;line-height:1.4} +.log-row{display:flex;gap:8px;color:#94a3b8} +.log-time{color:#475569;flex-shrink:0} +.log-tag{font-weight:700;flex-shrink:0} +.log-row.warn .log-msg,.log-row.warn .log-tag{color:#f87171} +.log-row.action .log-msg,.log-row.action .log-tag{color:#fde047} +.log-row.success .log-msg,.log-row.success .log-tag{color:#4ade80} +.console-cursor-row{gap:6px;margin-top:3px} +.console-prompt{color:#34d399;font-size:.6875rem} +.cursor-block{width:6px;height:11px;background:#38bdf8} +.forensic-report-card{background:linear-gradient(180deg,#10151f 0%,#0c1017 100%);border:1px solid rgba(197,155,39,.4);border-radius:6px;padding:20px;gap:16px;box-shadow:0 4px 20px rgba(0,0,0,.4);animation:fadeInReport .4s ease-out} +@keyframes fadeInReport{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} +.report-header{flex-wrap:wrap;gap:12px;border-bottom:1px solid #1e293b;padding-bottom:12px} +.report-header-left{gap:4px} +.report-badge{font-size:.625rem;color:#c59b27;letter-spacing:.1em;font-weight:700} +.report-title{margin:0;font-size:1.05rem;font-weight:800;color:#f0f3f8} +.report-status-pill{background:rgba(34,197,94,.15);border:1px solid rgba(34,197,94,.4);color:#4ade80;font-size:.6875rem;font-weight:700;padding:4px 10px;border-radius:999px} +.report-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px} +.report-metric-box{background:#080b10;border:1px solid #1a2230;border-radius:4px;padding:10px 12px;gap:4px} +.box-label{font-size:.625rem;color:#64748b;text-transform:uppercase} +.box-val{font-size:.8125rem;font-weight:700;color:#f0f3f8} +.box-val.highlight-mail{color:#e2b94a} +.box-val.highlight-time{color:#38bdf8} +.box-val.success{color:#4ade80} +.report-footer{display:flex;justify-content:flex-end} +.download-report-btn{gap:8px;background:0 0;border:1px solid rgba(197,155,39,.5);color:#c59b27;padding:8px 16px;border-radius:3px;font-size:.8125rem;font-weight:600;cursor:pointer} +.download-report-btn:hover{background:rgba(197,155,39,.1);border-color:#c59b27} +.dl-icon{width:15px;height:15px} +.arch-metrics-bar{padding:16px 12px 6px;border-top:1px solid #1a2230;flex-wrap:nowrap;gap:16px;width:100%;box-sizing:border-box} +.metric-card{align-items:center;justify-content:center;gap:4px;text-align:center;flex:1} +.metric-label{font-size:.6875rem;color:#5e697d;text-transform:uppercase;letter-spacing:.04em} +.metric-val{font-size:.875rem;font-weight:700;color:#f0f3f8} +.metric-val.highlight{color:#38bdf8} +.metric-val.success{color:#4ade80} +.metric-divider{width:1px;height:28px;background:#1e293b;flex-shrink:0} +@keyframes pulseBeacon{0%,100%{opacity:.6;transform:scale(1)}50%{opacity:1;transform:scale(1.3)}} +@media (max-width:1024px){ +.story-grid-flow{flex-direction:column} +.story-arrow{transform:rotate(90deg);margin:-4px 0} +.report-grid{grid-template-columns:repeat(2,1fr)} +.diagram-canvas{grid-template-columns:1fr;gap:10px} +.flow-connector{height:16px;width:100%;transform:rotate(90deg)} +.metric-divider{display:none} +.arch-metrics-bar{flex-direction:column;gap:12px} +} \ No newline at end of file diff --git a/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.html b/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.html index cdd3fde..9e34cf7 100644 --- a/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.html +++ b/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.html @@ -1,173 +1,350 @@
- +

{{ i18n.t().navSimulator }}

+
-
- - - @if (stage() === 'idle') { - {{ i18n.t().simStatusIdle }} - } @else if (isContained()) { - {{ i18n.t().simStatusContained }} - } @else { - {{ i18n.t().simStatusActive }} - } - +
+ +
+ + + +
+ + +
+ + + @if (stage() === 'idle') { + {{ i18n.t().simStatusIdle }} + } @else if (isContained()) { + {{ i18n.t().simStatusContained }} + } @else { + {{ i18n.t().simStatusActive }} + } + +
- +
- -
- -
-
VECTOR // 01
-
- - - - -
-
-

{{ i18n.t().simThreatNodeTitle }}

- {{ i18n.t().simThreatNodeSub }} - IP: {{ threatIp() }} -
-
-
- - -
-
- ► -
- - -
-
AWS // 02
-
- - - -
-
-

{{ i18n.t().simCloudTrailNodeTitle }}

- {{ i18n.t().simCloudTrailNodeSub }} - us-east-1 // EventBridge Rule -
-
-
- - -
-
- ► -
- - -
-
SERVERLESS // 03
-
- - - -
-
-

{{ i18n.t().simLambdaNodeTitle }}

- {{ i18n.t().simLambdaNodeSub }} - STS Invalidation & DenyPolicy -
-
-
- - -
-
- ► -
- - -
-
SECOPS // 04
-
- + + + +
+ + +
- -
- - - -
- - -
-
-
- - - -
- {{ i18n.t().simConsoleTitle }} - JSON LOGSTREAM -
- -
- @for (log of logs(); track $index) { -
- [{{ log.timestamp }}] - [{{ log.tag }}] - {{ log.message }} + + @if (viewMode() === 'story') { +
+ +
+
01 // ATAQUE
+
+ + + +
- } -
- secops@aws-perimeter:~$ - +

{{ i18n.t().simStep1Title }}

+

{{ i18n.t().simStep1Desc }}

+
IP: {{ threatIp() }}
+
+ +
→
+ + +
+
02 // DETECCIÓN
+
+ + + + + +
+

{{ i18n.t().simStep2Title }}

+

{{ i18n.t().simStep2Desc }}

+
< 50ms // Monitoreo Activo
+
+ +
→
+ + +
+
03 // BLOQUEO
+
+ + + + +
+

{{ i18n.t().simStep3Title }}

+

{{ i18n.t().simStep3Desc }}

+
Revocación Inmediata STS
+
+ +
→
+ + +
+
04 // INFORME
+
+ + + + + + + +
+

{{ i18n.t().simStep4Title }}

+

{{ i18n.t().simStep4Desc }}

+
{{ targetRecipient() }}
-
+ } + + + @if (viewMode() === 'tech') { +
+ +
+
VECTOR // 01
+
+ + + + +
+
+

{{ i18n.t().simThreatNodeTitle }}

+ {{ i18n.t().simThreatNodeSub }} + IP: {{ threatIp() }} +
+
+
+ + +
+
+ ► +
+ + +
+
AWS // 02
+
+ + + +
+
+

{{ i18n.t().simCloudTrailNodeTitle }}

+ {{ i18n.t().simCloudTrailNodeSub }} + us-east-1 // EventBridge Rule +
+
+
+ + +
+
+ ► +
+ + +
+
SERVERLESS // 03
+
+ + + +
+
+

{{ i18n.t().simLambdaNodeTitle }}

+ {{ i18n.t().simLambdaNodeSub }} + STS Invalidation & DenyPolicy +
+
+
+ + +
+
+ ► +
+ + +
+
SECOPS // 04
+
+ + + + +
+
+

{{ i18n.t().simSesNodeTitle }}

+ {{ i18n.t().simSesNodeSub }} + {{ targetRecipient() }} +
+
+
+
+ + +
+
+
+ + + +
+ {{ i18n.t().simConsoleTitle }} + JSON LOGSTREAM +
+ +
+ @for (log of logs(); track $index) { +
+ [{{ log.timestamp }}] + [{{ log.tag }}] + {{ log.message }} +
+ } +
+ secops@aws-perimeter:~$ + +
+
+
+ } + + + @if (showReport() || isContained()) { +
+
+
+ {{ i18n.t().simReportDemoBadge }} +

{{ i18n.t().simReportTitle }}

+
+ {{ i18n.t().simReportStatusSecure }} +
+ +
+
+ {{ i18n.t().simReportTargetEmail }} + {{ targetRecipient() }} +
+ +
+ {{ i18n.t().simMetricLatency }} + {{ i18n.t().simReportTimeVsAvg }} +
+ +
+ Integridad de Credenciales + {{ i18n.t().simReportCredentialsCompromised }} +
+ +
+ Impacto Financiero Evitado + {{ i18n.t().simReportFinancialLoss }} +
+
+ +
+ +
+
+ }
diff --git a/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.ts b/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.ts index 7838166..02f855a 100644 --- a/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.ts +++ b/src/frontend/src/app/presentation/features/incident-simulator/incident-simulator.component.ts @@ -3,6 +3,7 @@ import { I18nService } from '../../../application/services/i18n.service'; import { RetroAudioService } from '../../../application/services/retro-audio.service'; export type SimulationStage = 'idle' | 'breach' | 'detection' | 'remediation' | 'notification' | 'contained'; +export type SimulatorViewMode = 'story' | 'tech'; export interface SecOpsLog { readonly timestamp: string; @@ -23,9 +24,13 @@ export class IncidentSimulatorComponent implements OnInit, OnDestroy { private readonly audio = inject(RetroAudioService); protected readonly stage = signal('idle'); + protected readonly viewMode = signal('story'); + protected readonly userEmail = signal(''); protected readonly logs = signal([]); protected readonly threatIp = signal('198.51.100.42'); protected readonly executionTimeMs = signal(184); + protected readonly showReport = signal(false); + protected readonly downloadFeedback = signal(false); protected readonly isSimulating = computed(() => { const s = this.stage(); @@ -34,12 +39,112 @@ export class IncidentSimulatorComponent implements OnInit, OnDestroy { protected readonly isContained = computed(() => this.stage() === 'contained'); + protected readonly targetRecipient = computed(() => { + const email = this.userEmail().trim(); + return email.length > 3 && email.includes('@') ? email : 'secops-team@bryan.dev'; + }); + private timeoutIds: ReturnType[] = []; ngOnInit(): void { this.initDefaultLogs(); } + setViewMode(mode: SimulatorViewMode): void { + if (this.viewMode() === mode) return; + this.audio.playClick(); + this.viewMode.set(mode); + } + + onEmailInput(event: Event): void { + const val = (event.target as HTMLInputElement).value; + this.userEmail.set(val); + } + + startSimulation(): void { + if (this.isSimulating()) return; + + this.clearTimeouts(); + this.audio.playClick(); + this.logs.set([]); + this.showReport.set(false); + this.downloadFeedback.set(false); + + // Paso 1: Intento de Intrusión (Vector de Amenaza) + this.stage.set('breach'); + this.appendLog({ + level: 'WARN', + tag: 'IAM-ALERT', + message: `Anomalous API request: iam:CreateAccessKey from untrusted IP ${this.threatIp()} for principal "svc-deployer".` + }); + + // Paso 2: Detección Inmediata en EventBridge & CloudTrail (después de 700ms) + const t1 = setTimeout(() => { + this.stage.set('detection'); + this.audio.playCellReveal(); + this.appendLog({ + level: 'INFO', + tag: 'CLOUDTRAIL', + message: 'Management event ingested in us-east-1. EventBridge rule "SecOps-AutoRemediate" triggered synchronously.' + }); + }, 700); + this.timeoutIds.push(t1); + + // Paso 3: Remediación Automatizada Serverless (después de 1500ms) + const t2 = setTimeout(() => { + this.stage.set('remediation'); + this.audio.playFlagToggle(); + this.appendLog({ + level: 'ACTION', + tag: 'LAMBDA', + message: 'Executing fn-iam-incident-response: Revoking active STS sessions & attaching inline DenyAllSecurityPolicy.' + }); + }, 1500); + this.timeoutIds.push(t2); + + // Paso 4: Despacho de Reporte Forense (después de 2300ms) + const t3 = setTimeout(() => { + this.stage.set('notification'); + this.audio.playCellReveal(); + this.appendLog({ + level: 'INFO', + tag: 'SES-ALERT', + message: `Dispatching cryptographic audit report to ${this.targetRecipient()} via Amazon SES.` + }); + }, 2300); + this.timeoutIds.push(t3); + + // Paso 5: Amenaza Contenida & Generación de Reporte (después de 3100ms) + const t4 = setTimeout(() => { + this.stage.set('contained'); + this.audio.playVictory(); + this.showReport.set(true); + this.appendLog({ + level: 'SUCCESS', + tag: 'CONTAINED', + message: `Threat neutralized successfully in ${this.executionTimeMs()}ms. Zero credentials compromised. State: SECURE.` + }); + }, 3100); + this.timeoutIds.push(t4); + } + + resetSimulation(): void { + this.clearTimeouts(); + this.audio.playClick(); + this.stage.set('idle'); + this.showReport.set(false); + this.downloadFeedback.set(false); + this.initDefaultLogs(); + } + + simulateDownload(): void { + this.audio.playClick(); + this.downloadFeedback.set(true); + setTimeout(() => { + this.downloadFeedback.set(false); + }, 3000); + } + private initDefaultLogs(): void { const time = this.getCurrentTimestamp(); this.logs.set([ @@ -58,77 +163,6 @@ export class IncidentSimulatorComponent implements OnInit, OnDestroy { ]); } - startSimulation(): void { - if (this.isSimulating()) return; - - this.clearTimeouts(); - this.audio.playClick(); - this.logs.set([]); - - // Fase 1: Intento de Acceso No Autorizado (Vector de Amenaza) - this.stage.set('breach'); - this.appendLog({ - level: 'WARN', - tag: 'IAM-ALERT', - message: `Anomalous API request: iam:CreateAccessKey from untrusted IP ${this.threatIp()} for principal "svc-deployer".` - }); - - // Fase 2: Captura en CloudTrail & Enrutamiento en EventBridge (después de 700ms) - const t1 = setTimeout(() => { - this.stage.set('detection'); - this.audio.playCellReveal(); - this.appendLog({ - level: 'INFO', - tag: 'CLOUDTRAIL', - message: 'Management event ingested in us-east-1. EventBridge rule "SecOps-AutoRemediate" triggered synchronously.' - }); - }, 700); - this.timeoutIds.push(t1); - - // Fase 3: Remediación Automatizada en AWS Lambda (después de 1500ms) - const t2 = setTimeout(() => { - this.stage.set('remediation'); - this.audio.playFlagToggle(); - this.appendLog({ - level: 'ACTION', - tag: 'LAMBDA', - message: 'Executing fn-iam-incident-response: Revoking active STS sessions & attaching inline DenyAllSecurityPolicy.' - }); - }, 1500); - this.timeoutIds.push(t2); - - // Fase 4: Despacho de Notificación Amazon SES (después de 2300ms) - const t3 = setTimeout(() => { - this.stage.set('notification'); - this.audio.playCellReveal(); - this.appendLog({ - level: 'INFO', - tag: 'SES-ALERT', - message: 'Dispatching cryptographic audit report to secops-team@bryan.dev via Amazon SES.' - }); - }, 2300); - this.timeoutIds.push(t3); - - // Fase 5: Amenaza Contenida con Éxito (después de 3100ms) - const t4 = setTimeout(() => { - this.stage.set('contained'); - this.audio.playVictory(); - this.appendLog({ - level: 'SUCCESS', - tag: 'CONTAINED', - message: `Threat neutralized successfully in ${this.executionTimeMs()}ms. Zero credentials compromised. State: SECURE.` - }); - }, 3100); - this.timeoutIds.push(t4); - } - - resetSimulation(): void { - this.clearTimeouts(); - this.audio.playClick(); - this.stage.set('idle'); - this.initDefaultLogs(); - } - private appendLog(entry: Omit): void { const timestamp = this.getCurrentTimestamp(); this.logs.update(prev => [...prev, { ...entry, timestamp }]); diff --git a/src/frontend/src/app/presentation/features/landing/landing.component.css b/src/frontend/src/app/presentation/features/landing/landing.component.css index a7774df..45801fd 100644 --- a/src/frontend/src/app/presentation/features/landing/landing.component.css +++ b/src/frontend/src/app/presentation/features/landing/landing.component.css @@ -153,11 +153,24 @@ box-sizing: border-box; background-color: #121517; flex: 1 0 auto; + animation: zenViewEnter 0.32s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +@keyframes zenViewEnter { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } .hero-container.isolated-view { padding-top: 0; background-color: #000000; + animation: none; } .interactive-stage.isolated-view { @@ -209,6 +222,15 @@ } @media (max-width: 960px) { + .hero-container.isolated-view { + padding: calc(56px + var(--space-4)) clamp(0.75rem, 3vw, 1.5rem) var(--space-8); + background-color: #121517; + min-height: calc(100vh - 56px); + height: auto; + overflow-y: visible; + display: block; + } + .interactive-stage.isolated-view { height: auto; min-height: calc(100vh - 56px); @@ -438,6 +460,194 @@ background-color: #121517; } +/* ============================================================================== + FLECHAS SUTILES FLOTANTES DE NAVEGACIÓN (DEBAJO DEL NAVBAR) + ============================================================================== */ + +.zen-floating-nav-controls { + position: fixed; + top: 68px; + left: 0; + right: 0; + width: 100%; + pointer-events: none; + z-index: 950; + display: flex; + justify-content: space-between; + padding: 0 clamp(0.75rem, 2.5vw, 2.5rem); + box-sizing: border-box; +} + +.zen-nav-arrow-btn { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 8px; + background: rgba(10, 13, 20, 0.75); + backdrop-filter: blur(12px) saturate(160%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 999px; + padding: 6px 14px; + color: #9aa5b8; + font-family: var(--font-mono, monospace); + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + opacity: 1; +} + +.zen-nav-arrow-btn:hover:not(:disabled) { + color: #f0f3f8; + border-color: rgba(197, 155, 39, 0.5); + background: rgba(18, 22, 32, 0.9); + transform: translateY(-1px); + box-shadow: 0 0 14px rgba(197, 155, 39, 0.25); +} + +.zen-nav-arrow-btn.disabled, +.zen-nav-arrow-btn:disabled { + opacity: 0; + pointer-events: none; + transform: scale(0.92); +} + +.nav-arrow-svg { + width: 14px; + height: 14px; + stroke: #c59b27; + flex-shrink: 0; + transition: transform 0.2s ease; +} + +.nav-arrow-left:hover:not(:disabled) .nav-arrow-svg { + transform: translateX(-3px); +} + +.nav-arrow-right:hover:not(:disabled) .nav-arrow-svg { + transform: translateX(3px); +} + +.nav-arrow-label { + letter-spacing: 0.02em; + white-space: nowrap; +} + +/* ============================================================================== + TELÓN DE TRANSICIÓN SUAVE Y SCANLINE ZEN + ============================================================================== */ + +.zen-transition-curtain { + position: fixed; + inset: 0; + z-index: 9999; + background: rgba(8, 10, 14, 0.88); + backdrop-filter: blur(8px); + pointer-events: none; + opacity: 0; + transition: opacity 0.16s cubic-bezier(0.16, 1, 0.3, 1); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.zen-transition-curtain.visible { + opacity: 1; + pointer-events: auto; +} + +.curtain-scanline { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, #c59b27, #38bdf8, transparent); + box-shadow: 0 0 10px #c59b27; + animation: scanlinePass 0.8s linear infinite; +} + +.curtain-logo-wrapper { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + animation: logoWrapperEnter 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.logo-glow-ring { + position: absolute; + top: 10px; + width: 90px; + height: 90px; + border-radius: 50%; + background: radial-gradient(circle, rgba(197, 155, 39, 0.45) 0%, rgba(56, 189, 248, 0.18) 50%, transparent 70%); + filter: blur(14px); + animation: logoPulseGlow 1.2s ease-in-out infinite alternate; + pointer-events: none; +} + +.logo-image-container { + position: relative; + width: 68px; + height: 68px; + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(197, 155, 39, 0.45); + box-shadow: 0 0 24px rgba(197, 155, 39, 0.3), 0 8px 32px rgba(0, 0, 0, 0.85); + background: #000000; + display: flex; + align-items: center; + justify-content: center; +} + +.curtain-logo-img { + width: 100%; + height: 100%; + object-fit: cover; + transform: scale(0.92); + animation: logoBreath 1.3s ease-in-out infinite alternate; +} + +.logo-shimmer-sweep { + position: absolute; + inset: 0; + background: linear-gradient(110deg, transparent 25%, rgba(255, 255, 255, 0.4) 50%, transparent 75%); + transform: translateX(-100%); + animation: logoShimmer 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite; + pointer-events: none; +} + +@keyframes logoWrapperEnter { + from { opacity: 0; transform: scale(0.92); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes logoPulseGlow { + 0% { transform: scale(0.85); opacity: 0.4; } + 100% { transform: scale(1.3); opacity: 0.95; } +} + +@keyframes logoBreath { + 0% { transform: scale(0.88); filter: drop-shadow(0 0 4px rgba(197, 155, 39, 0.3)); } + 100% { transform: scale(0.96); filter: drop-shadow(0 0 12px rgba(197, 155, 39, 0.7)); } +} + +@keyframes logoShimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +@keyframes scanlinePass { + 0% { transform: translateY(0); opacity: 0; } + 50% { opacity: 1; } + 100% { transform: translateY(100vh); opacity: 0; } +} + @media (max-width: 768px) { .zen-header { padding: 0 8px; @@ -471,4 +681,17 @@ padding: 3px 6px; font-size: 0.6875rem; } + + .zen-floating-nav-controls { + top: 58px; + padding: 0 10px; + } + + .nav-arrow-label { + display: none; + } + + .zen-nav-arrow-btn { + padding: 6px 10px; + } } diff --git a/src/frontend/src/app/presentation/features/landing/landing.component.html b/src/frontend/src/app/presentation/features/landing/landing.component.html index 2208c1b..f182bd1 100644 --- a/src/frontend/src/app/presentation/features/landing/landing.component.html +++ b/src/frontend/src/app/presentation/features/landing/landing.component.html @@ -81,10 +81,59 @@
- + + + + + + + @if (activeSection() === 'hero') {
- +
} diff --git a/src/frontend/src/app/presentation/features/landing/landing.component.ts b/src/frontend/src/app/presentation/features/landing/landing.component.ts index c127491..f9169c7 100644 --- a/src/frontend/src/app/presentation/features/landing/landing.component.ts +++ b/src/frontend/src/app/presentation/features/landing/landing.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, signal, inject, ViewChild, ElementRef } from '@angular/core'; +import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, signal, computed, inject, ViewChild, ElementRef } from '@angular/core'; import { Hero3dComponent } from '../hero-3d/hero-3d.component'; import { RetroOsComponent } from '../retro-os/retro-os.component'; import { TerminalComponent } from '../terminal/terminal.component'; @@ -40,6 +40,31 @@ export class LandingComponent implements OnInit, OnDestroy { protected readonly currentYear = signal(new Date().getFullYear()); protected readonly currentTime = signal(''); protected readonly activeSection = signal('hero'); + protected readonly heroEntryPosition = signal<'top' | 'bottom'>('top'); + protected readonly isCurtainVisible = signal(false); + protected readonly transitionTargetSection = signal('hero'); + + protected readonly hasPrevSection = computed(() => { + return LANDING_SECTIONS.indexOf(this.activeSection()) > 0; + }); + + protected readonly hasNextSection = computed(() => { + return LANDING_SECTIONS.indexOf(this.activeSection()) < LANDING_SECTIONS.length - 1; + }); + + protected readonly prevSectionName = computed(() => { + const idx = LANDING_SECTIONS.indexOf(this.activeSection()); + return idx > 0 ? this.getSectionTitle(LANDING_SECTIONS[idx - 1]) : ''; + }); + + protected readonly nextSectionName = computed(() => { + const idx = LANDING_SECTIONS.indexOf(this.activeSection()); + return idx < LANDING_SECTIONS.length - 1 ? this.getSectionTitle(LANDING_SECTIONS[idx + 1]) : ''; + }); + + protected readonly transitionTargetTitle = computed(() => { + return this.getSectionTitle(this.transitionTargetSection()); + }); private clockInterval: ReturnType | null = null; private readonly timeFormatter = new Intl.DateTimeFormat('en-GB', { @@ -57,6 +82,7 @@ export class LandingComponent implements OnInit, OnDestroy { private touchStartX = 0; private wheelDeltaAccum = 0; private wheelResetTimer: ReturnType | null = null; + private transitionTimer: ReturnType | null = null; ngOnInit(): void { this.updateClock(); @@ -65,31 +91,60 @@ export class LandingComponent implements OnInit, OnDestroy { this.setupScrollTransitions(); } - setSection(section: LandingSection, entryPosition: 'top' | 'bottom' = 'top'): void { - if (this.activeSection() === section && this.isTransitioning) return; - this.isTransitioning = true; - setTimeout(() => { - this.isTransitioning = false; - }, 750); - - this.activeSection.set(section); - if (typeof window !== 'undefined') { - const hash = section === 'hero' ? '' : `#${section}`; - const url = window.location.pathname + window.location.search + hash; - window.history.replaceState(null, '', url); - this.scrollActiveNavIntoView(section); - - setTimeout(() => { - if (entryPosition === 'bottom') { - const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); - window.scrollTo({ top: maxScroll, behavior: 'instant' }); - } else { - window.scrollTo({ top: 0, behavior: 'instant' }); - } - }, 30); + getSectionTitle(section: LandingSection): string { + const dict = this.i18n.t(); + switch (section) { + case 'hero': return dict.navHome; + case 'retro': return dict.navRetro; + case 'projects': return dict.navProjects; + case 'simulator': return dict.navSimulator; + case 'terminal': return dict.navTerminal; + case 'contact': return dict.navContact; } } + setSection(section: LandingSection, entryPosition: 'top' | 'bottom' = 'top'): void { + if (this.activeSection() === section || this.isTransitioning) return; + this.isTransitioning = true; + this.transitionTargetSection.set(section); + this.isCurtainVisible.set(true); + + if (this.transitionTimer) clearTimeout(this.transitionTimer); + + // Brief fade-curtain duration before switching section component + this.transitionTimer = setTimeout(() => { + if (section === 'hero') { + this.heroEntryPosition.set(entryPosition); + } + + this.activeSection.set(section); + + if (typeof window !== 'undefined') { + const hash = section === 'hero' ? '' : `#${section}`; + const url = window.location.pathname + window.location.search + hash; + window.history.replaceState(null, '', url); + this.scrollActiveNavIntoView(section); + + if (section !== 'hero') { + if (entryPosition === 'bottom') { + const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); + window.scrollTo({ top: maxScroll, behavior: 'instant' }); + } else { + window.scrollTo({ top: 0, behavior: 'instant' }); + } + } + } + + // Smoothly hide curtain after DOM mount and scroll placement + setTimeout(() => { + this.isCurtainVisible.set(false); + setTimeout(() => { + this.isTransitioning = false; + }, 220); + }, 120); + }, 150); + } + private setupScrollTransitions(): void { if (typeof window === 'undefined') return; window.addEventListener('wheel', this.onWheel, { passive: false }); @@ -167,7 +222,7 @@ export class LandingComponent implements OnInit, OnDestroy { } }; - private goToNextSection(): void { + protected goToNextSection(): void { const currentIndex = LANDING_SECTIONS.indexOf(this.activeSection()); if (currentIndex >= 0 && currentIndex < LANDING_SECTIONS.length - 1) { const next = LANDING_SECTIONS[currentIndex + 1]; @@ -175,7 +230,7 @@ export class LandingComponent implements OnInit, OnDestroy { } } - private goToPrevSection(): void { + protected goToPrevSection(): void { const currentIndex = LANDING_SECTIONS.indexOf(this.activeSection()); if (currentIndex > 0) { const prev = LANDING_SECTIONS[currentIndex - 1]; @@ -224,6 +279,9 @@ export class LandingComponent implements OnInit, OnDestroy { if (this.wheelResetTimer !== null) { clearTimeout(this.wheelResetTimer); } + if (this.transitionTimer !== null) { + clearTimeout(this.transitionTimer); + } if (typeof window !== 'undefined') { window.removeEventListener('wheel', this.onWheel); window.removeEventListener('touchstart', this.onTouchStart); diff --git a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html index 4d6308a..294ef03 100644 --- a/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html +++ b/src/frontend/src/app/presentation/features/technical-profile/technical-profile.component.html @@ -6,11 +6,11 @@
@if (asciiArt()) { -
{{ asciiArt() }}
+
{{ asciiArt() }}
} @else if (asciiLoadError()) { -
No se pudo cargar el retrato.
+
{{ i18n.t().tpAsciiError }}
} @else { -
Cargando retrato...
+
{{ i18n.t().tpAsciiLoading }}
}
@@ -18,7 +18,7 @@ @for (paragraph of aboutParagraphs(); track $index) {

{{ paragraph }}

} - About me + {{ i18n.t().tpAboutLink }}
diff --git a/src/frontend/src/assets/favicon.ico b/src/frontend/src/assets/favicon.ico new file mode 100644 index 0000000..3811cf4 Binary files /dev/null and b/src/frontend/src/assets/favicon.ico differ diff --git a/src/frontend/src/assets/images/apple-touch-icon.png b/src/frontend/src/assets/images/apple-touch-icon.png new file mode 100644 index 0000000..530d3c9 Binary files /dev/null and b/src/frontend/src/assets/images/apple-touch-icon.png differ diff --git a/src/frontend/src/assets/images/favicon-192.png b/src/frontend/src/assets/images/favicon-192.png new file mode 100644 index 0000000..d4d79a3 Binary files /dev/null and b/src/frontend/src/assets/images/favicon-192.png differ diff --git a/src/frontend/src/assets/images/favicon-32x32.png b/src/frontend/src/assets/images/favicon-32x32.png new file mode 100644 index 0000000..3ebacf7 Binary files /dev/null and b/src/frontend/src/assets/images/favicon-32x32.png differ diff --git a/src/frontend/src/index.html b/src/frontend/src/index.html index 498c1b7..bbff681 100644 --- a/src/frontend/src/index.html +++ b/src/frontend/src/index.html @@ -2,10 +2,13 @@ - Bryan // Portafolio Profesional Interactivo + Bryan Baño - + + + +