PORTFOLIO/.agents/skills/portfolio-frontend/SKILL.md

666 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: portfolio-frontend
description: Parámetros, reglas y estándares de arquitectura frontend con Clean Architecture en Angular (Signals reactivos, Domain Ports & Adapters, Application Stores, Presentation OnPush, Terminal interactiva, Retro OS Canvas y 3D Scroll Hero).
---
# Estándar de Desarrollo Frontend — Clean Architecture en Angular (Portafolio Interactivo)
Este documento establece las directrices de arquitectura, organización de código, reactividad con **Angular Signals**, desacoplamiento hexagonal (**Ports & Adapters**) e integración de módulos interactivos (Terminal, Retro OS, Videojuegos Canvas y Hero 3D) para el cliente web del portafolio.
---
## 1. Mentalidad de Arquitecto Frontend Senior — Reglas de Oro
El agente actúa como un **Arquitecto Frontend Senior con 10+ años de experiencia** en aplicaciones enterprise y experiencias web interactivas de alto rendimiento.
1. **Clean Architecture / Inversión de Dependencias (DIP)**:
- Las capas internas nunca conocen a las externas. El código fuente solo apunta hacia el centro:
`Presentation` ──► `Application` ──► `Domain` ◄── `Infrastructure / Data`
- El **Dominio** es TypeScript puro, agnóstico del framework (cero imports de `@angular/*`, cero `HttpClient`).
- Los cambios de endpoints en el backend solo afectan a `infrastructure/`, jamás a la UI ni al Dominio.
2. **Reactividad Primitiva con Signals & OnPush**:
- Todo componente en la capa de presentación debe usar `changeDetection: ChangeDetectionStrategy.OnPush`.
- El estado se administra exclusivamente con **Angular Signals** (`signal`, `computed`, `effect`).
- Prohibido abusar de `BehaviorSubject` para estados locales o UI cuando `signal()` es nativo, más limpio y de menor overhead.
- RxJS se reserva exclusivamente para flujos asíncronos y cancelables en adaptadores de infraestructura (`HttpClient`).
3. **Smart vs. Dumb Components**:
- **Smart Components (Pages/Containers)**: Inyectan Application Stores o Facades, coordinan la vista y pasan señales a los componentes hijos.
- **Dumb / Presentational Components (UI)**: Reciben datos estrictamente por `input()` / `input.required()` y notifican eventos mediante `output()`. Cero inyecciones de repositorios o servicios HTTP.
4. **Sintaxis Moderna de Control de Flujo**:
- Uso obligatorio de la nueva sintaxis `@if`, `@for` (con `track`), `@switch` y `@defer`.
- Prohibido el uso de directivas estructurales legadas (`*ngIf`, `*ngFor`).
5. **Filosofía Ponytail (Cero Sobreingeniería)**:
- Usar las herramientas nativas de Angular y la Web Platform antes de añadir paquetes `npm`.
- Mini-juego retro: Usar la **API nativa Canvas 2D** en lugar de motores de juego pesados de 500KB.
- Scroll 3D: Implementar con Three.js optimizado o Canvas/CSS 3D ligero, pausando el render loop cuando no esté visible con `IntersectionObserver`.
---
## 2. Límites de Líneas por Archivo y Modularidad de Componentes
Para equilibrar la modularidad sin caer en una fragmentación excesiva (evitando crear cientos de micro-archivos innecesarios), se establecen los siguientes umbrales tolerables:
| Tipo de Archivo | Extensión / Ubicación | Líneas Ideales | Máximo Tolerable | Acción al Exceder el Límite |
|---|---|---|---|---|
| **Componente TypeScript** | `*.component.ts` | **80 – 180** | **280** | Extraer lógica pesada al Store o delegar bloques de vista en subcomponentes Dumb. |
| **Plantilla HTML** | `*.component.html` | **50 – 130** | **200** | Descomponer bloques visuales complejos en subcomponentes presentacionales hijos. |
| **Hojas de Estilo** | `*.component.css` | **40 – 100** | **160** | Consumir tokens globales de diseño y utilidades para no duplicar reglas CSS. |
| **Application Stores** | `*.store.ts` | **80 – 160** | **240** | Dividir por dominio funcional o sub-estado específico si acumula demasiados flujos. |
| **Servicios / Casos de Uso** | `*.service.ts` / `*.usecase.ts` | **50 – 120** | **180** | Aplicar Single Responsibility (SRP): mantener el servicio acotado a su función. |
| **Modelos y Puertos** | `domain/models/`, `ports/` | **30 – 70** | **120** | Separar modelos o agrupar contratos relacionados de forma coherente. |
| **Adaptadores y Repositorios**| `infrastructure/adapters/` | **60 – 120** | **180** | Mantener solo las llamadas HTTP del recurso específico. |
| **Mappers y DTOs** | `infrastructure/mappers/` | **30 – 80** | **130** | Dividir funciones de mapeo si el DTO tiene muchas transformaciones complejas. |
| **Motor de Juego / Canvas** | `retro-game/` | **100 – 200** | **320** | Separar en módulos si el juego crece: `game-engine.ts`, `renderer.ts`, `input-handler.ts`. |
### Criterio de Descomposición Inteligente:
- **No forzar la micro-fragmentación**: No es necesario crear un subcomponente por cada botón o etiqueta. Se crea un nuevo subcomponente cuando una sección visual o interactiva tiene identidad propia (ej. `terminal-prompt`, `retro-window-header`, `game-scoreboard`).
- **Control Flow Anidado**: Si una plantilla HTML supera **3 a 4 niveles de anidamiento** de bloques `@if` o `@for`, es momento de extraer ese bloque interno a un subcomponente Dumb dedicado para mantener la legibilidad.
---
## 3. Diagrama de Capas de Arquitectura Limpia
```
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER (Angular) │
│ - Pages (Hero, Terminal, Retro OS, Projects, Admin) │
│ - Dumb Components (WindowFrame, TerminalView, GameCanvas) │
│ - UI Directives & Pipes │
└──────────────────────────────┬──────────────────────────────┘
│ consume
┌──────────────────────────────▼──────────────────────────────┐
│ APPLICATION LAYER (Stores & Use Cases) │
│ - ProjectStore (Signals) - TerminalEngine (Signals) │
│ - RetroOsStore (Signals) - AuthStore (RBAC Signals) │
│ - InteractiveApiStore │
└──────────────────────────────┬──────────────────────────────┘
│ orquesta usando puertos
┌──────────────────────────────▼──────────────────────────────┐
│ DOMAIN LAYER (Core Puro) │
│ - Models (Project, Skill, Command, RetroWindow, AuthUser) │
│ - Ports (Abstract Classes / Injection Tokens) │
│ - Domain Value Objects & Business Rules │
└──────────────────────────────▲──────────────────────────────┘
│ implementa contratos de
┌──────────────────────────────┴──────────────────────────────┐
│ INFRASTRUCTURE / DATA LAYER (Adapters) │
│ - HttpProjectRepository - HttpInteractiveRepository │
│ - HttpAuthRepository - LocalStorageAdapter │
│ - DTOs & Mappers puros (DTO <-> Model) │
└─────────────────────────────────────────────────────────────┘
```
---
## 4. Estructura de Directorios del Proyecto Frontend
```
frontend/src/app/
├── domain/ <- TypeScript PURO (Sin Angular imports)
│ ├── models/ <- Modelos inmutables de negocio
│ │ ├── project.model.ts
│ │ ├── skill.model.ts
│ │ ├── terminal-command.model.ts
│ │ ├── retro-os.model.ts
│ │ ├── interactive-demo.model.ts
│ │ └── auth.model.ts
│ └── ports/ <- Contratos abstractos (Interfaces / Abstract Classes)
│ ├── project.port.ts
│ ├── interactive-demo.port.ts
│ └── auth.port.ts
│
├── infrastructure/ <- Implementaciones técnicas y adaptadores
│ ├── dtos/ <- Contratos directos de la API .NET
│ │ ├── project.dto.ts
│ │ ├── interactive-demo.dto.ts
│ │ └── auth.dto.ts
│ ├── mappers/ <- Transformadores puros (DTO <-> Model)
│ │ ├── project.mapper.ts
│ │ ├── interactive-demo.mapper.ts
│ │ └── auth.mapper.ts
│ └── adapters/ <- Implementación HTTP con HttpClient
│ ├── http-project.adapter.ts
│ ├── http-interactive-demo.adapter.ts
│ └── http-auth.adapter.ts
│
├── application/ <- Orquestación de casos de uso y estado reactivo
│ ├── stores/ <- Signals Stores (State Management)
│ │ ├── project.store.ts
│ │ ├── terminal.store.ts
│ │ ├── retro-os.store.ts
│ │ ├── interactive-api.store.ts
│ │ └── auth.store.ts
│ └── services/ <- Lógica de negocio de aplicación
│ ├── terminal-executor.service.ts <- Parser y ejecutor de comandos
│ └── retro-audio.service.ts <- Efectos de sonido retro con Web Audio API
│
├── presentation/ <- UI Angular (Componentes Standalone OnPush)
│ ├── features/
│ │ ├── hero-3d/ <- Laptop desarmable en scroll 3D
│ │ │ ├── hero-3d.component.ts
│ │ │ └── hero-3d.component.html
│ │ ├── terminal/ <- Consola Linux interactiva
│ │ │ ├── terminal.component.ts
│ │ │ ├── terminal.component.html
│ │ │ └── components/terminal-line.component.ts
│ │ ├── retro-os/ <- Sistema operativo vintage & mini-juego
│ │ │ ├── retro-os.component.ts
│ │ │ ├── retro-os.component.html
│ │ │ └── components/
│ │ │ ├── retro-window.component.ts
│ │ │ └── retro-game-canvas.component.ts
│ │ ├── interactive-api/ <- Probador de API en vivo (.NET Playground)
│ │ │ ├── interactive-api.component.ts
│ │ │ └── interactive-api.component.html
│ │ ├── projects/ <- Galería de proyectos con filtros
│ │ │ ├── project-list.component.ts
│ │ │ └── project-card.component.ts
│ │ └── admin/ <- Backoffice protegido por RBAC
│ │ ├── login/login.component.ts
│ │ └── dashboard/admin-dashboard.component.ts
│ └── shared/ <- Componentes UI reutilizables
│ ├── components/
│ │ ├── zen-button/zen-button.component.ts
│ │ └── zen-badge/zen-badge.component.ts
│ └── pipes/
│ └── json-pretty.pipe.ts
│
├── core/ <- Servicios transversales
│ ├── guards/
│ │ └── auth.guard.ts <- Verificación de token y rol admin
│ ├── interceptors/
│ │ ├── auth.interceptor.ts <- Inyección de Bearer Token JWT
│ │ └── error.interceptor.ts <- Mapeo de ProblemDetails RFC 7807
│ └── tokens/
│ └── api-config.token.ts
│
├── app.config.ts <- Inyección de dependencias (providers)
└── app.routes.ts <- Rutas principales con Lazy Loading
```
---
## 5. Implementación Práctica por Capas (Ejemplos Estándar)
### 5.1 Capa de Dominio (`domain/`)
Modelos inmutables en TypeScript puro. Cero decoradores `@Injectable` o librerías externas.
```typescript
// domain/models/interactive-demo.model.ts
export interface ServerTelemetry {
readonly status: string;
readonly framework: string;
readonly databaseEngine: string;
readonly uptimeSeconds: number;
readonly serverTimeUtc: string;
readonly latencyMs?: number;
}
export interface ApiDemoResult {
readonly endpoint: string;
readonly httpStatus: number;
readonly executionTimeMs: number;
readonly payload: unknown;
readonly isSuccess: boolean;
}
```
```typescript
// domain/ports/interactive-demo.port.ts
import { Observable } from 'rxjs';
import { ApiDemoResult, ServerTelemetry } from '../models/interactive-demo.model';
export abstract class InteractiveDemoPort {
abstract getTelemetry(): Observable<ServerTelemetry>;
abstract pingServer(): Observable<ApiDemoResult>;
abstract executeCustomDemo(demoId: string): Observable<ApiDemoResult>;
}
```
---
### 5.2 Capa de Infraestructura (`infrastructure/`)
Maneja la comunicación HTTP, DTOs y Mappers hacia el backend .NET.
```typescript
// infrastructure/dtos/interactive-demo.dto.ts
export interface ServerTelemetryDto {
status: string;
framework: string;
database_engine: string;
uptime_seconds: number;
server_time_utc: string;
}
export interface ApiExecutionResponseDto<T = unknown> {
endpoint: string;
status_code: number;
duration_ms: number;
data: T;
}
```
```typescript
// infrastructure/mappers/interactive-demo.mapper.ts
import { ApiDemoResult, ServerTelemetry } from '../../domain/models/interactive-demo.model';
import { ApiExecutionResponseDto, ServerTelemetryDto } from '../dtos/interactive-demo.dto';
export class InteractiveDemoMapper {
static toDomainTelemetry(dto: ServerTelemetryDto, latencyMs: number): ServerTelemetry {
return {
status: dto.status,
framework: dto.framework,
databaseEngine: dto.database_engine,
uptimeSeconds: dto.uptime_seconds,
serverTimeUtc: dto.server_time_utc,
latencyMs
};
}
static toDomainDemoResult(dto: ApiExecutionResponseDto): ApiDemoResult {
return {
endpoint: dto.endpoint,
httpStatus: dto.status_code,
executionTimeMs: dto.duration_ms,
payload: dto.data,
isSuccess: dto.status_code >= 200 && dto.status_code < 300
};
}
}
```
```typescript
// infrastructure/adapters/http-interactive-demo.adapter.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, map } from 'rxjs';
import { InteractiveDemoPort } from '../../domain/ports/interactive-demo.port';
import { ApiDemoResult, ServerTelemetry } from '../../domain/models/interactive-demo.model';
import { ApiExecutionResponseDto, ServerTelemetryDto } from '../dtos/interactive-demo.dto';
import { InteractiveDemoMapper } from '../mappers/interactive-demo.mapper';
@Injectable({ providedIn: 'root' })
export class HttpInteractiveDemoAdapter implements InteractiveDemoPort {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api/v1/interactive';
getTelemetry(): Observable<ServerTelemetry> {
const startTime = performance.now();
return this.http.get<ServerTelemetryDto>(`${this.baseUrl}/telemetry`).pipe(
map(dto => {
const latency = Math.round(performance.now() - startTime);
return InteractiveDemoMapper.toDomainTelemetry(dto, latency);
})
);
}
pingServer(): Observable<ApiDemoResult> {
return this.http.get<ApiExecutionResponseDto>(`${this.baseUrl}/ping`).pipe(
map(InteractiveDemoMapper.toDomainDemoResult)
);
}
executeCustomDemo(demoId: string): Observable<ApiDemoResult> {
return this.http.post<ApiExecutionResponseDto>(`${this.baseUrl}/execute`, { demo_id: demoId }).pipe(
map(InteractiveDemoMapper.toDomainDemoResult)
);
}
}
```
---
### 5.3 Capa de Aplicación (`application/stores/`)
Administración de estado reactivo mediante **Angular Signals**.
```typescript
// application/stores/interactive-api.store.ts
import { Injectable, computed, inject, signal } from '@angular/core';
import { InteractiveDemoPort } from '../../domain/ports/interactive-demo.port';
import { ApiDemoResult, ServerTelemetry } from '../../domain/models/interactive-demo.model';
@Injectable({ providedIn: 'root' })
export class InteractiveApiStore {
private readonly demoPort = inject(InteractiveDemoPort);
// Signals privadas de estado
private readonly _telemetry = signal<ServerTelemetry | null>(null);
private readonly _lastResult = signal<ApiDemoResult | null>(null);
private readonly _isLoading = signal<boolean>(false);
private readonly _errorMessage = signal<string | null>(null);
// Signals públicas de solo lectura
readonly telemetry = this._telemetry.asReadonly();
readonly lastResult = this._lastResult.asReadonly();
readonly isLoading = this._isLoading.asReadonly();
readonly errorMessage = this._errorMessage.asReadonly();
readonly isHealthy = computed(() => this._telemetry()?.status === 'Healthy');
loadTelemetry(): void {
this._isLoading.set(true);
this._errorMessage.set(null);
this.demoPort.getTelemetry().subscribe({
next: (telemetry) => {
this._telemetry.set(telemetry);
this._isLoading.set(false);
},
error: (err) => {
this._errorMessage.set(err.message ?? 'No se pudo conectar con el backend .NET');
this._isLoading.set(false);
}
});
}
executePing(): void {
this._isLoading.set(true);
this._errorMessage.set(null);
this.demoPort.pingServer().subscribe({
next: (result) => {
this._lastResult.set(result);
this._isLoading.set(false);
},
error: (err) => {
this._errorMessage.set(err.message ?? 'Error al ejecutar ping');
this._isLoading.set(false);
}
});
}
}
```
---
### 5.4 Capa de Presentación (`presentation/`)
#### A. Smart Page Component
```typescript
// presentation/features/interactive-api/interactive-api.component.ts
import { Component, OnInit, ChangeDetectionStrategy, inject } from '@angular/core';
import { InteractiveApiStore } from '../../../application/stores/interactive-api.store';
import { JsonPrettyPipe } from '../../../shared/pipes/json-pretty.pipe';
@Component({
selector: 'app-interactive-api',
standalone: true,
imports: [JsonPrettyPipe],
templateUrl: './interactive-api.component.html',
styleUrl: './interactive-api.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InteractiveApiComponent implements OnInit {
protected readonly store = inject(InteractiveApiStore);
ngOnInit(): void {
this.store.loadTelemetry();
}
onTestPing(): void {
this.store.executePing();
}
}
```
#### B. Template con Control Flow Moderno y Estilo Zen
```html
<!-- presentation/features/interactive-api/interactive-api.component.html -->
<section class="zen-card api-tester-container">
<header class="card-header">
<div class="header-badge">
<span class="status-dot" [class.online]="store.isHealthy()"></span>
<span class="mono-label">.NET 8 BACKEND // LIVE TESTER</span>
</div>
<h2 class="section-title">Interacción en Tiempo Real con la API</h2>
</header>
<div class="control-actions">
<button
type="button"
class="zen-btn-primary"
[disabled]="store.isLoading()"
(click)="onTestPing()"
>
@if (store.isLoading()) {
<span>Consultando...</span>
} @else {
<span>Ejecutar Ping Controlado</span>
}
</button>
</div>
@if (store.errorMessage(); as error) {
<div class="zen-alert-error" role="alert">
<span>{{ error }}</span>
</div>
}
@if (store.lastResult(); as result) {
<div class="api-response-viewer">
<div class="response-meta">
<span class="method-tag">GET</span>
<span class="url-tag">{{ result.endpoint }}</span>
<span class="status-tag" [class.success]="result.isSuccess">
{{ result.httpStatus }} OK
</span>
<span class="latency-tag">{{ result.executionTimeMs }} ms</span>
</div>
<pre class="json-code"><code>{{ result.payload | jsonPretty }}</code></pre>
</div>
}
</section>
```
---
## 6. Arquitectura de Módulos Específicos
### 6.1 Motor de la Terminal Interactiva (`application/services/terminal-executor.service.ts`)
Parser de comandos extensible con mapeo declarativo y llamadas HTTP reales al backend:
```typescript
import { Injectable, inject } from '@angular/core';
import { Observable, of, isObservable, map } from 'rxjs';
import { InteractiveDemoPort } from '../../domain/ports/interactive-demo.port';
@Injectable({ providedIn: 'root' })
export class TerminalExecutorService {
private readonly demoPort = inject(InteractiveDemoPort);
private readonly commands: Record<string, (args: string[]) => Observable<string> | string> = {
help: () => `Comandos disponibles:
about - Conoce sobre mi perfil y enfoque arquitectónico
skills - Stack técnico (.NET, Angular, MySQL, Clean Arch)
projects - Listado de proyectos destacados
curl <url> - Ejecuta una petición HTTP en vivo al backend .NET
retro - Abre el sistema operativo vintage y videojuegos
clear - Limpia el buffer de la consola`,
about: () => 'Bryan // Fullstack Developer especializado en .NET & Angular Clean Architecture.',
skills: () => 'Backend: .NET 8/9, EF Core, MySQL, MediatR, CQRS.\nFrontend: Angular, Signals, OnPush, Canvas 2D.\nPrácticas: Clean Architecture, Ponytail (YAGNI/KISS), TDD.',
clear: () => '__CLEAR__',
retro: () => 'Abriendo entorno ZenOS...',
curl: (args) => {
if (args[0] === '/api/v1/interactive/ping' || args[0] === 'ping') {
return this.demoPort.pingServer().pipe(
map(res => `HTTP ${res.httpStatus} OK (${res.executionTimeMs}ms)\n${JSON.stringify(res.payload, null, 2)}`)
);
}
return 'Endpoint no reconocido. Prueba con: curl /api/v1/interactive/ping';
}
};
execute(rawInput: string): Observable<string> {
const parts = rawInput.trim().split(/\s+/);
const commandName = parts[0]?.toLowerCase();
const args = parts.slice(1);
if (!commandName) return of('');
const handler = this.commands[commandName];
if (!handler) return of(`comando no encontrado: ${commandName}. Escribe "help" para ver la lista.`);
const result = handler(args);
return isObservable(result) ? result : of(result);
}
}
```
### 6.2 Motor del Videojuego Canvas Retro (`retro-game-canvas.component.ts`)
Bucle de renderizado nativo en 2D sin librerías pesadas, con limpieza estricta de memoria:
```typescript
import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectionStrategy, signal } from '@angular/core';
@Component({
selector: 'app-retro-game-canvas',
standalone: true,
template: `<canvas #gameCanvas width="320" height="240" class="pixel-canvas"></canvas>`,
styleUrl: './retro-game-canvas.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RetroGameCanvasComponent implements AfterViewInit, OnDestroy {
@ViewChild('gameCanvas') private canvasRef!: ElementRef<HTMLCanvasElement>;
private ctx!: CanvasRenderingContext2D;
private animationFrameId: number | null = null;
private isRunning = false;
protected readonly score = signal<number>(0);
protected readonly isGameOver = signal<boolean>(false);
ngAfterViewInit(): void {
const context = this.canvasRef.nativeElement.getContext('2d');
if (!context) return;
this.ctx = context;
this.ctx.imageSmoothingEnabled = false; // Pixel-art nítido
this.isRunning = true;
this.gameLoop();
}
private gameLoop = (): void => {
if (!this.isRunning) return;
this.update();
this.render();
this.animationFrameId = requestAnimationFrame(this.gameLoop);
};
private update(): void {
// Actualización de entidades y colisiones del juego
}
private render(): void {
this.ctx.fillStyle = '#0a0c10';
this.ctx.fillRect(0, 0, 320, 240);
}
ngOnDestroy(): void {
this.isRunning = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId); // Cero memory leaks
}
}
}
```
### 6.3 Hero 3D con Scroll Desarmable (`hero-3d.component.ts`)
Lectura de scroll pasiva y pausa del render loop mediante `IntersectionObserver`:
```typescript
import { Component, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-hero-3d',
standalone: true,
templateUrl: './hero-3d.component.html',
styleUrl: './hero-3d.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Hero3dComponent implements AfterViewInit, OnDestroy {
@ViewChild('canvasContainer') private containerRef!: ElementRef<HTMLDivElement>;
private isVisible = true;
private intersectionObserver?: IntersectionObserver;
protected scrollProgress = 0;
ngAfterViewInit(): void {
this.intersectionObserver = new IntersectionObserver(([entry]) => {
this.isVisible = entry.isIntersecting;
}, { threshold: 0.05 });
this.intersectionObserver.observe(this.containerRef.nativeElement);
window.addEventListener('scroll', this.onScroll, { passive: true });
}
private onScroll = (): void => {
if (!this.isVisible) return;
const el = this.containerRef.nativeElement;
const rect = el.getBoundingClientRect();
this.scrollProgress = Math.min(Math.max(-rect.top / window.innerHeight, 0), 1);
// Aplicar transformación a las piezas 3D según scrollProgress (0.0 a 1.0)
};
ngOnDestroy(): void {
window.removeEventListener('scroll', this.onScroll);
this.intersectionObserver?.disconnect();
}
}
```
---
## 7. Interceptores y Configuración Global (`app.config.ts`)
```typescript
// core/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('access_token');
if (token && !req.url.includes('/auth/login')) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
}
return next(req);
};
```
```typescript
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './core/interceptors/auth.interceptor';
import { errorInterceptor } from './core/interceptors/error.interceptor';
import { InteractiveDemoPort } from './domain/ports/interactive-demo.port';
import { HttpInteractiveDemoAdapter } from './infrastructure/adapters/http-interactive-demo.adapter';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
// Inversión de dependencias de puertos
{ provide: InteractiveDemoPort, useClass: HttpInteractiveDemoAdapter }
]
};
```
---
## 8. Tabla de Anti-Patrones Prohibidos en Frontend
| Anti-patrón Prohibido | Solución Obligatoria |
|---|---|
| Inyectar `HttpClient` directamente en componentes | Inyectar Store/Facade que consuma Ports implementados en `infrastructure/` |
| Directivas legadas `*ngIf`, `*ngFor` | Sintaxis moderna `@if`, `@for (item of items; track item.id)` |
| `ChangeDetectionStrategy.Default` en componentes | Siempre `ChangeDetectionStrategy.OnPush` |
| Bucle infinito en canvas del juego al cambiar de ruta | Limpiar bucle con `cancelAnimationFrame` en `ngOnDestroy()` |
| Instalar librerías de 1MB para efectos sencillos | Usar Web APIs nativas: Canvas 2D, Web Audio, IntersectionObserver |
| Uso de `any` en respuestas HTTP | Definir DTOs tipados en `infrastructure/dtos/` y mapear a modelos |
| Estado global mutable o variables sueltas | Angular Signals con interfaces inmutables (`readonly`) |