feat: app completa recordaLexia (fases 1-5)

App web familiar de rutinas visuales para niños con TDAH: muestra cada día el
material del cole y las rutinas de tarde, con gamificación por monedas y tienda
de recompensas. Multi-niño y bilingüe ES/CA. Uso doméstico/homelab.

Backend (Spring Boot 3.5 / Java 21 / Gradle):
- Dominio por capas, PostgreSQL + Liquibase, datos semilla.
- API REST con DTOs: /today, toggle con monedas y bonos de bloque/día, monedero,
  tienda/canje, ajustes y CRUD del panel de padres.
- Seguridad ligera por PIN (BCrypt + sesion en memoria), sin Keycloak.
- Tests JUnit: generacion del dia, monedas/bonos con reversion, canje, seguridad.

Frontend (Angular 19, standalone + signals):
- Perfiles, Home (Tablero y Foco), Tienda y panel de padres (5 pestañas).
- Tipografia OpenDyslexic conmutable (accesibilidad), i18n ES/CA, TTS y sonido.
- Tokens de diseño fieles al handoff (paleta, animaciones, monedas voladoras).

Empaquetado:
- Docker multi-stage + docker-compose (PostgreSQL + backend + Nginx).
- Decisiones de arquitectura documentadas en docs/adr.
This commit is contained in:
Jaume Garriga Maestre
2026-06-21 10:48:57 +02:00
commit 52e559a159
160 changed files with 29022 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
import { Component, ElementRef, Input, inject, signal } from '@angular/core';
/** Monedero: pill con 🪙 y el saldo. Hace walletBump cuando se le pide (al ganar). */
@Component({
selector: 'app-wallet',
imports: [],
template: `
<span class="wallet" [class.wallet--bump]="bumping()">
<span class="wallet__coin">🪙</span>
<span class="wallet__amount">{{ coins }}</span>
</span>
`,
styles: [
`
.wallet {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 9px 18px;
border-radius: var(--radius-pill);
background: var(--coin-bg);
color: var(--coin-text);
font-family: var(--font-display);
font-weight: 700;
font-size: 1.4rem;
}
.wallet--bump { animation: walletBump 0.45s ease; }
`,
],
})
export class WalletComponent {
@Input({ required: true }) coins = 0;
protected readonly bumping = signal(false);
private readonly el = inject(ElementRef<HTMLElement>);
/** Rectángulo del monedero: destino de las monedas voladoras. */
getRect(): DOMRect {
return (this.el.nativeElement as HTMLElement).getBoundingClientRect();
}
/** Dispara la animación de "rebote" del monedero. */
bump(): void {
this.bumping.set(false);
// Reinicia la animación en el siguiente frame.
requestAnimationFrame(() => this.bumping.set(true));
setTimeout(() => this.bumping.set(false), 500);
}
}