diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css index 921b51d..31ca9d0 100644 --- a/frontend/src/app/app.component.css +++ b/frontend/src/app/app.component.css @@ -95,3 +95,45 @@ html, body { flex: 1; min-height: 0; /* Importante para que funcione el scroll interno */ } + +/* ---- FAB Tutorial ---- */ +.tutorial-fab { + position: fixed; + bottom: 28px; + right: 28px; + z-index: 900; + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + background: rgba(6, 182, 212, 0.12); + border: 1px solid rgba(6, 182, 212, 0.35); + border-radius: 50%; + color: #67e8f9; + cursor: pointer; + backdrop-filter: blur(8px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(6, 182, 212, 0.1); + transition: all 0.25s ease; +} + +.tutorial-fab svg { + width: 20px; + height: 20px; +} + +.tutorial-fab:hover { + background: rgba(6, 182, 212, 0.22); + border-color: rgba(6, 182, 212, 0.6); + box-shadow: 0 6px 24px rgba(6, 182, 212, 0.25), 0 0 0 1px rgba(6, 182, 212, 0.2); + transform: translateY(-2px); +} + +@media (max-width: 540px) { + .tutorial-fab { + bottom: 20px; + right: 16px; + width: 40px; + height: 40px; + } +} diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index f786ff6..7caa59d 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -8,4 +8,20 @@
- \ No newline at end of file + + + @if (showNavbar) { + + } + + + @if (showOnboarding) { + + } + diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 02bda6c..21e7a14 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,11 +1,12 @@ import { Component, OnInit } from '@angular/core'; -import { RouterOutlet, RouterLink, Router, NavigationEnd } from '@angular/router'; +import { RouterOutlet, RouterLink, Router, NavigationEnd, ActivatedRoute } from '@angular/router'; import { TransactionListComponent } from './components/transaction-list/transaction-list.component'; import { LoginRegisterComponent } from './components/auth-components/login-register/login-register.component'; import { NavBarComponent } from './components/nav-bar/nav-bar.component'; import { CommonModule } from '@angular/common'; import { filter } from 'rxjs/operators'; import { ThemeService } from './services/theme/theme.service'; +import { OnboardingComponent } from './components/onboarding/onboarding.component'; @Component({ selector: 'app-root', @@ -13,7 +14,8 @@ import { ThemeService } from './services/theme/theme.service'; imports: [ RouterOutlet, NavBarComponent, - CommonModule + CommonModule, + OnboardingComponent ], templateUrl: './app.component.html', styleUrl: './app.component.css' @@ -24,7 +26,8 @@ import { ThemeService } from './services/theme/theme.service'; export class AppComponent implements OnInit { title = 'smartspend-frontend'; - showNavbar = false; // Iniciar en false para evitar flash del navbar + showNavbar = false; + showOnboarding = false; constructor(private router: Router, private themeService: ThemeService) { // Aplicar tema guardado lo antes posible para evitar flash @@ -35,17 +38,29 @@ export class AppComponent implements OnInit { } ngOnInit() { - // 👈 Detectar la ruta inicial inmediatamente this.checkCurrentRoute(); - // Escucha los cambios de ruta posteriores this.router.events .pipe(filter(event => event instanceof NavigationEnd)) .subscribe((event: any) => { this.updateNavbarVisibility(event.url); + if (event.url.includes('tutorial=true')) { + this.showOnboarding = true; + this.router.navigate([], { queryParams: {}, replaceUrl: true }); + } else if (event.url.startsWith('/dashboard') && !OnboardingComponent.isCompleted()) { + this.showOnboarding = true; + } }); } + openTutorial(): void { + this.showOnboarding = true; + } + + onOnboardingClosed(): void { + this.showOnboarding = false; + } + private checkCurrentRoute() { const currentUrl = this.router.url; this.updateNavbarVisibility(currentUrl); diff --git a/frontend/src/app/components/auth-components/login-register/login-register.component.css b/frontend/src/app/components/auth-components/login-register/login-register.component.css index 102e664..39390e7 100644 --- a/frontend/src/app/components/auth-components/login-register/login-register.component.css +++ b/frontend/src/app/components/auth-components/login-register/login-register.component.css @@ -179,6 +179,12 @@ margin-right: auto; margin-bottom: 0.5rem; overflow: hidden; + border-radius: 8px; +} + +/* El div que inyecta la librería GSI de Google tiene fondo blanco por defecto */ +.register-card #google-signin-btn > div { + background: transparent !important; } .register-card #google-signin-btn { diff --git a/frontend/src/app/components/create-transaction-modal/create-transaction-modal.component.html b/frontend/src/app/components/create-transaction-modal/create-transaction-modal.component.html index fd51257..818e66a 100644 --- a/frontend/src/app/components/create-transaction-modal/create-transaction-modal.component.html +++ b/frontend/src/app/components/create-transaction-modal/create-transaction-modal.component.html @@ -316,10 +316,10 @@

[ngModelOptions]="{ standalone: true }" placeholder="Nombre" maxlength="30" - class="flex-1 rounded-lg bg-slate-800 border border-slate-600 px-3 py-2 + class="min-w-0 flex-1 rounded-lg bg-slate-800 border border-slate-600 px-3 py-2 text-slate-200 focus:outline-none focus:ring-2 focus:ring-cyan-400 text-sm" /> -
+
€ +
+ + +
+
+ + SmartSpend +
+ +
+ + +
+
+
+ + +
+ @for (step of steps; track $index) { + + } +
+ + +
+
{{ steps[currentStep].icon }}
+

{{ steps[currentStep].title }}

+

{{ steps[currentStep].description }}

+ +
    + @for (detail of steps[currentStep].details; track $index) { +
  • + ✓ + {{ detail }} +
  • + } +
+ + @if (steps[currentStep].tip) { +
+ 💡 Consejo: + {{ steps[currentStep].tip }} +
+ } +
+ + +
+ {{ currentStep + 1 }} / {{ steps.length }} + +
+ @if (currentStep > 0) { + + } + @if (!isLastStep) { + + } @else { + + } +
+
+ +
+
diff --git a/frontend/src/app/components/onboarding/onboarding.component.ts b/frontend/src/app/components/onboarding/onboarding.component.ts new file mode 100644 index 0000000..9e47f23 --- /dev/null +++ b/frontend/src/app/components/onboarding/onboarding.component.ts @@ -0,0 +1,140 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +interface OnboardingStep { + icon: string; + title: string; + description: string; + details: string[]; + tip?: string; +} + +const ONBOARDING_KEY = 'smartspend_onboarding_done'; + +@Component({ + selector: 'app-onboarding', + standalone: true, + imports: [CommonModule], + templateUrl: './onboarding.component.html', + styleUrl: './onboarding.component.css' +}) +export class OnboardingComponent { + @Output() closed = new EventEmitter(); + + currentStep = 0; + + readonly steps: OnboardingStep[] = [ + { + icon: '👋', + title: '¡Bienvenido a SmartSpend!', + description: 'Tu gestor de finanzas personales inteligente. En menos de 2 minutos conocerás todo lo que puedes hacer.', + details: [ + 'Controla tus gastos e ingresos en un solo lugar', + 'Divide gastos compartidos y salda deudas fácilmente', + 'Automatiza transacciones que se repiten cada mes', + 'Visualiza tu salud financiera con gráficos y previsiones' + ] + }, + { + icon: '🏦', + title: 'Cuentas bancarias', + description: 'Todo gira en torno a tus cuentas. Crea una cuenta por cada banco o propósito que quieras controlar.', + details: [ + 'Crea varias cuentas: cuenta corriente, ahorro...', + 'Cada cuenta tiene su propio saldo e historial de movimientos', + 'Cambia de cuenta activa desde el panel principal', + 'El saldo se actualiza automáticamente con cada transacción', + 'Además podrás hacer movimientos entre tus cuentas sin romper tus estadísticas' + ], + tip: 'Empieza con una sola cuenta y añade más cuando lo necesites.' + }, + { + icon: '💸', + title: 'Transacciones', + description: 'Registra cada gasto o ingreso con todos los detalles que necesites.', + details: [ + 'Elige tipo: Gasto 🔴, Ingreso 🟢 o Traspaso 🔄', + 'Asigna una categoría (alimentación, transporte, ocio…)', + 'Añade una descripción y la fecha exacta', + 'Filtra y busca por cualquier campo en el historial' + ], + tip: 'Pulsa el botón "+" del dashboard para crear una transacción rápida.' + }, + { + icon: '🔄', + title: 'Transacciones recurrentes', + description: 'Para gastos o ingresos que se repiten: alquiler, sueldo, suscripciones…', + details: [ + 'Define la frecuencia: diaria, semanal, mensual o anual', + 'La aplicación genera automáticamente cada nueva ocurrencia el día que le toca sin tener que hacer nada', + 'Puedes activar o pausar cualquier movimiento recurrente cuando quieras', + 'Accede a ellas desde la sección "Recurrentes" del menú' + ], + tip: 'Una transacción recurrente activa no elimina sus transacciones ya generadas si la pausas.' + }, + { + icon: '👥', + title: 'Gastos compartidos', + description: 'Cuando un gasto lo pagas tú pero es compartido con varias personas, marca quién te debe parte.', + details: [ + 'En el formulario de transacción, marca la opción "Compartido"', + 'Indica el nombre de la persona que comparte el gasto contigo', + 'SmartSpend calcula automáticamente cuánto te debe cada uno o puedes introducir la cantidad exacta que te debe cada persona', + 'La deuda queda registrada, pudiendo saldarla cuando se pague en efectivo o por transferencia', + 'Estos gastos que no te corresponden a ti, no afectan a tus estadísticas de gasto ni a tu saldo, solo te afectará tu parte del gasto' + ], + }, + { + icon: '⚖️', + title: 'Deudas y compensaciones', + description: 'Gestiona quién le debe a quién y compensa automáticamente deudas cruzadas.', + details: [ + 'En el dashboard verás un resumen de deudas pendientes', + 'Cuando saldes una deuda, se generará automáticamente una transacción de compensación en tu cuenta que lo refleje', + ], + tip: '¡La compensación automática evita tener que hacer múltiples transferencias!' + } + ]; + + get isLastStep(): boolean { + return this.currentStep === this.steps.length - 1; + } + + get progressPercent(): number { + return Math.round(((this.currentStep + 1) / this.steps.length) * 100); + } + + next(): void { + if (!this.isLastStep) { + this.currentStep++; + } + } + + prev(): void { + if (this.currentStep > 0) { + this.currentStep--; + } + } + + goToStep(index: number): void { + this.currentStep = index; + } + + finish(): void { + localStorage.setItem(ONBOARDING_KEY, 'true'); + this.closed.emit(); + } + + skip(): void { + localStorage.setItem(ONBOARDING_KEY, 'true'); + this.closed.emit(); + } + + static isCompleted(): boolean { + return localStorage.getItem(ONBOARDING_KEY) === 'true'; + } + + static reset(): void { + localStorage.removeItem(ONBOARDING_KEY); + } +} diff --git a/frontend/src/app/components/register/register.component.css b/frontend/src/app/components/register/register.component.css index dff8b06..41c90c9 100644 --- a/frontend/src/app/components/register/register.component.css +++ b/frontend/src/app/components/register/register.component.css @@ -149,6 +149,12 @@ margin-right: auto; margin-bottom: 0.5rem; overflow: hidden; + border-radius: 8px; +} + +/* El div que inyecta la librería GSI de Google tiene fondo blanco por defecto */ +.register-card #google-register-btn > div { + background: transparent !important; } .register-card #google-register-btn {