feat(home): estado vacío amable y vibración háptica al ganar monedas
Un día sin tareas muestra una tarjeta con la mascota del niño en vez de una pantalla en blanco; la columna de tarde vacía tiene su propio mensaje. El feedback de recompensa suma una vibración corta (Vibration API) que se dispara junto al sonido sobre el mismo evento.
This commit is contained in:
@@ -21,6 +21,9 @@ export class I18nService {
|
||||
afternoon: { ES: 'TARDE', CA: 'TARDA' },
|
||||
ready: { ES: 'listo', CA: 'fet' },
|
||||
noSchool: { ES: 'HOY NO HAY COLE', CA: 'AVUI NO HI HA COLE' },
|
||||
freeAfternoon: { ES: 'TARDE LIBRE', CA: 'TARDA LLIURE' },
|
||||
emptyDayTitle: { ES: '¡Hoy no hay tareas!', CA: 'Avui no hi ha tasques!' },
|
||||
emptyDaySub: { ES: 'Disfruta del día', CA: 'Gaudeix del dia' },
|
||||
allDone: { ES: '¡TODO LISTO!', CA: 'TOT FET!' },
|
||||
great: { ES: '¡GENIAL!', CA: 'GENIAL!' },
|
||||
done: { ES: '¡HECHO!', CA: 'FET!' },
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Sonido de recompensa sintetizado con WebAudio (sin ficheros de audio). Un par
|
||||
* de notas alegres al ganar monedas. Silencioso si WebAudio no está disponible.
|
||||
* Feedback de recompensa: sonido sintetizado con WebAudio (sin ficheros de
|
||||
* audio) y vibración háptica sutil en dispositivos que la soportan (tablets).
|
||||
* Ambos se disparan sobre el MISMO evento (causalidad) para que se perciban
|
||||
* como una sola respuesta. Silencioso/inerte si el navegador no lo soporta.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SoundService {
|
||||
@@ -47,4 +49,11 @@ export class SoundService {
|
||||
osc.stop(start + 0.28);
|
||||
});
|
||||
}
|
||||
|
||||
/** Vibración corta al ganar monedas (no-op si el dispositivo no vibra). */
|
||||
buzz(): void {
|
||||
if (typeof navigator !== 'undefined' && 'vibrate' in navigator) {
|
||||
navigator.vibrate(15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,13 @@ import { TaskCardComponent, ToggleEvent } from './task-card.component';
|
||||
|
||||
<section class="col">
|
||||
<h2 class="col__head">🌙 {{ i18n.t('afternoon') }}</h2>
|
||||
@if (afternoon.length) {
|
||||
@for (task of afternoon; track task.id) {
|
||||
<app-task-card [task]="task" [ttsEnabled]="ttsEnabled" (toggle)="toggle.emit($event)" />
|
||||
}
|
||||
} @else {
|
||||
<div class="empty">🌈 {{ i18n.t('freeAfternoon') }}</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Eventos del día -->
|
||||
<app-event-banner [events]="t.specialEvents" [ttsEnabled]="t.child.ttsEnabled" />
|
||||
|
||||
@if (t.progress.total > 0) {
|
||||
<!-- Conmutador de modo -->
|
||||
<div class="modes">
|
||||
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'BOARD'" (click)="setMode('BOARD')">
|
||||
@@ -27,9 +31,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Eventos del día -->
|
||||
<app-event-banner [events]="t.specialEvents" [ttsEnabled]="t.child.ttsEnabled" />
|
||||
|
||||
<!-- Progreso global -->
|
||||
<app-progress-bar [done]="t.progress.totalDone" [total]="t.progress.total" />
|
||||
|
||||
@@ -44,6 +45,14 @@
|
||||
} @else {
|
||||
<app-focus-view [tasksInput]="allTasks()" [ttsEnabled]="t.child.ttsEnabled" (toggle)="onToggle($event)" />
|
||||
}
|
||||
} @else {
|
||||
<!-- Día sin tareas: mensaje amable, nunca una pantalla en blanco. -->
|
||||
<div class="freeday">
|
||||
<span class="freeday__mascot">{{ t.child.mascot }}</span>
|
||||
<p class="freeday__title">{{ i18n.t('emptyDayTitle') }}</p>
|
||||
<p class="freeday__sub">{{ i18n.t('emptyDaySub') }} 🎉</p>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
|
||||
<!-- Monedas voladoras (capa superpuesta) -->
|
||||
|
||||
@@ -89,6 +89,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Día sin tareas: tarjeta amable con la mascota, nunca una pantalla vacía.
|
||||
.freeday {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-6) var(--space-4);
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
text-align: center;
|
||||
animation: slideUp 0.4s ease both;
|
||||
|
||||
&__mascot {
|
||||
font-size: 96px;
|
||||
line-height: 1;
|
||||
animation: floatY 3s ease-in-out infinite;
|
||||
}
|
||||
&__title {
|
||||
margin: var(--space-3) 0 0;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: 1.8rem;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
&__sub {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Moneda voladora: parte de la tarea y viaja al monedero.
|
||||
.flycoin {
|
||||
position: fixed;
|
||||
|
||||
@@ -127,12 +127,14 @@ export class HomeComponent implements OnInit {
|
||||
};
|
||||
this.today.set(updated);
|
||||
|
||||
// Feedback positivo solo al ganar monedas (marcar, no desmarcar).
|
||||
// Feedback positivo solo al ganar monedas (marcar, no desmarcar). Sonido y
|
||||
// vibración se disparan juntos sobre el mismo evento (armonía multimodal).
|
||||
if (result.coinsEarned > 0) {
|
||||
this.flyCoins(ev.x, ev.y, result.coinsEarned);
|
||||
this.wallet?.bump();
|
||||
if (current.child.soundEnabled) {
|
||||
this.sound.playReward();
|
||||
this.sound.buzz();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user