From 0315926857648252d535d2b9f171fb5d9744ba1b Mon Sep 17 00:00:00 2001 From: santidev21 Date: Mon, 5 Oct 2026 17:09:54 -0500 Subject: [PATCH] feat(landing): public marketing page at the root (5.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guests hitting / were bounced straight to the login form, with no pitch. Add a landing at the root path (value proposition, value props, "cómo funciona", trust, CTAs) and change rootRedirectGuard so only authenticated users are redirected (to /dashboard); guests now see the landing. CTAs link to /register and /login, plus a demo button when demoEnabled. Screenshots are intentionally pending (a marked spot + docs note) — real captures only, no invented ones. --- bikontrol-web/src/app/app.routes.ts | 2 +- .../marketing/landing/landing.component.html | 136 ++++++++++++++++++ .../marketing/landing/landing.component.scss | 3 + .../landing/landing.component.spec.ts | 49 +++++++ .../marketing/landing/landing.component.ts | 71 +++++++++ .../shared/guards/root-redirect.guard.spec.ts | 6 +- .../app/shared/guards/root-redirect.guard.ts | 3 +- docs/specs/README.md | 1 + docs/specs/landing.md | 14 ++ e2e/tests/landing.spec.ts | 29 ++++ 10 files changed, 309 insertions(+), 5 deletions(-) create mode 100644 bikontrol-web/src/app/modules/marketing/landing/landing.component.html create mode 100644 bikontrol-web/src/app/modules/marketing/landing/landing.component.scss create mode 100644 bikontrol-web/src/app/modules/marketing/landing/landing.component.spec.ts create mode 100644 bikontrol-web/src/app/modules/marketing/landing/landing.component.ts create mode 100644 docs/specs/landing.md create mode 100644 e2e/tests/landing.spec.ts diff --git a/bikontrol-web/src/app/app.routes.ts b/bikontrol-web/src/app/app.routes.ts index fa98504..c11155b 100644 --- a/bikontrol-web/src/app/app.routes.ts +++ b/bikontrol-web/src/app/app.routes.ts @@ -9,7 +9,7 @@ export const routes: Routes = [ pathMatch: 'full', canActivate: [rootRedirectGuard], loadComponent: () => - import('./modules/auth/pages/login/login.component').then((c) => c.LoginComponent), + import('./modules/marketing/landing/landing.component').then((c) => c.LandingComponent), }, // Auth diff --git a/bikontrol-web/src/app/modules/marketing/landing/landing.component.html b/bikontrol-web/src/app/modules/marketing/landing/landing.component.html new file mode 100644 index 0000000..f19b7ad --- /dev/null +++ b/bikontrol-web/src/app/modules/marketing/landing/landing.component.html @@ -0,0 +1,136 @@ +
+ +
+
+ Bikontrol logo + Bikontrol +
+ +
+ + +
+

+ El mantenimiento de tu moto, bajo control. +

+

+ Registrá tus motos, seguí cada service y recibí un aviso antes de que venza. Simple, privado y + gratis para dueños de motos. +

+ +
+ Empezar gratis + Ya tengo cuenta + @if (demoEnabled) { + + } +
+ @if (errorMessage()) { +

{{ errorMessage() }}

+ } +
+ + +
+
+ @for (feature of features; track feature.title) { +
+
+ ✓ +
+

{{ feature.title }}

+

{{ feature.description }}

+
+ } +
+
+ + +
+
+

Cómo funciona

+
    + @for (step of steps; track step.title; let i = $index) { +
  1. +
    + {{ i + 1 }} +
    +

    {{ step.title }}

    +

    {{ step.description }}

    +
  2. + } +
+
+
+ + +
+
+

Pensado para que confíes tus datos

+
    +
  • Backups verificados de la base de datos.
  • +
  • Cuenta y datos tuyos: los descargás o los borrás cuando quieras.
  • +
  • Sin publicidad ni venta de tus datos.
  • +
+
+
+ + + + +
+

Empezá en 2 minutos

+

Cargá tu primer moto y tu plan de mantenimiento hoy mismo.

+ Crear mi cuenta +
+ + + +
diff --git a/bikontrol-web/src/app/modules/marketing/landing/landing.component.scss b/bikontrol-web/src/app/modules/marketing/landing/landing.component.scss new file mode 100644 index 0000000..5d4e87f --- /dev/null +++ b/bikontrol-web/src/app/modules/marketing/landing/landing.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/bikontrol-web/src/app/modules/marketing/landing/landing.component.spec.ts b/bikontrol-web/src/app/modules/marketing/landing/landing.component.spec.ts new file mode 100644 index 0000000..bd68916 --- /dev/null +++ b/bikontrol-web/src/app/modules/marketing/landing/landing.component.spec.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { AuthService } from '../../auth/services/auth.service'; +import { HttpErrorService } from '../../../shared/services/http-error.service'; +import { LandingComponent } from './landing.component'; + +@Component({ selector: 'app-stub', standalone: true, template: '' }) +class StubComponent {} + +describe('LandingComponent', () => { + let authServiceMock: any; + + beforeEach(() => { + authServiceMock = { demoLogin: vi.fn(() => of({ token: 't' })) }; + TestBed.configureTestingModule({ + imports: [LandingComponent], + providers: [ + // Wildcard route so the demo login's navigate(['/dashboard']) resolves. + provideRouter([{ path: '**', component: StubComponent }]), + { provide: AuthService, useValue: authServiceMock }, + { + provide: HttpErrorService, + useValue: { message: (e: any, f: string) => e?.message ?? f }, + }, + ], + }); + }); + + it('renders the value proposition and primary CTA', () => { + const fixture = TestBed.createComponent(LandingComponent); + fixture.detectChanges(); + + const text = (fixture.nativeElement as HTMLElement).textContent ?? ''; + expect(text).toContain('El mantenimiento de tu moto, bajo control.'); + expect(text).toContain('Empezar gratis'); + expect(text).toContain('Cómo funciona'); + }); + + it('starts a demo session when the demo is enabled', () => { + const component = TestBed.createComponent(LandingComponent).componentInstance; + + component.onDemoLogin(); + + // environment.demoEnabled is true in the dev/test environment. + expect(authServiceMock.demoLogin).toHaveBeenCalled(); + }); +}); diff --git a/bikontrol-web/src/app/modules/marketing/landing/landing.component.ts b/bikontrol-web/src/app/modules/marketing/landing/landing.component.ts new file mode 100644 index 0000000..58402d8 --- /dev/null +++ b/bikontrol-web/src/app/modules/marketing/landing/landing.component.ts @@ -0,0 +1,71 @@ +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; +import { environment } from '@env/environment'; +import { AuthService } from '../../auth/services/auth.service'; +import { HttpErrorService } from '../../../shared/services/http-error.service'; + +/** + * Public marketing landing (root path for guests). Value proposition, how it + * works and CTAs; authenticated users are sent to the dashboard by + * `rootRedirectGuard`. Real screenshots are pending (see docs/specs/landing.md). + */ +@Component({ + selector: 'app-landing', + imports: [RouterLink], + templateUrl: './landing.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrl: './landing.component.scss', +}) +export class LandingComponent { + private readonly authService = inject(AuthService); + private readonly router = inject(Router); + private readonly httpError = inject(HttpErrorService); + + readonly demoEnabled = environment.demoEnabled; + readonly demoLoading = signal(false); + readonly errorMessage = signal(null); + + readonly features = [ + { + title: 'No te olvides más del service', + description: 'Recordatorios por kilometraje o por tiempo, antes de que venza.', + }, + { + title: 'Todo tu historial en un lugar', + description: 'Qué le hiciste, cuándo y a qué kilometraje, siempre a mano.', + }, + { + title: 'Sabés cuánto gastás', + description: 'Costo por moto, por año y por kilómetro.', + }, + { + title: 'Tus datos, tuyos', + description: 'Descargá todo cuando quieras y borrá tu cuenta en un clic.', + }, + ]; + + readonly steps = [ + { title: 'Creá tu cuenta', description: 'En un minuto, con tu correo o con Google.' }, + { + title: 'Cargá tu moto y tu plan', + description: 'Elegí tus mantenimientos; te sugerimos los esenciales.', + }, + { + title: 'Recibí los avisos', + description: 'Por email y notificaciones, antes de que toque el service.', + }, + ]; + + onDemoLogin(): void { + if (!this.demoEnabled || this.demoLoading()) return; + this.demoLoading.set(true); + this.errorMessage.set(null); + this.authService.demoLogin().subscribe({ + next: () => this.router.navigate(['/dashboard']), + error: (error) => { + this.demoLoading.set(false); + this.errorMessage.set(this.httpError.message(error, 'No se pudo iniciar la demo.')); + }, + }); + } +} diff --git a/bikontrol-web/src/app/shared/guards/root-redirect.guard.spec.ts b/bikontrol-web/src/app/shared/guards/root-redirect.guard.spec.ts index 7ab56a7..8292337 100644 --- a/bikontrol-web/src/app/shared/guards/root-redirect.guard.spec.ts +++ b/bikontrol-web/src/app/shared/guards/root-redirect.guard.spec.ts @@ -32,12 +32,12 @@ describe('rootRedirectGuard', () => { expect(result).toEqual({ redirectedTo: '/dashboard' }); }); - it('should redirect to /login for guests', () => { + it('should allow guests through to the landing page', () => { authService.isAuthenticated.mockReturnValue(false); const result = TestBed.runInInjectionContext(() => rootRedirectGuard({} as any, {} as any)); - expect(router.parseUrl).toHaveBeenCalledWith('/login'); - expect(result).toEqual({ redirectedTo: '/login' }); + expect(result).toBe(true); + expect(router.parseUrl).not.toHaveBeenCalled(); }); }); diff --git a/bikontrol-web/src/app/shared/guards/root-redirect.guard.ts b/bikontrol-web/src/app/shared/guards/root-redirect.guard.ts index a5eb99c..2d244d5 100644 --- a/bikontrol-web/src/app/shared/guards/root-redirect.guard.ts +++ b/bikontrol-web/src/app/shared/guards/root-redirect.guard.ts @@ -6,5 +6,6 @@ export const rootRedirectGuard: CanActivateFn = () => { const authService = inject(AuthService); const router = inject(Router); - return router.parseUrl(authService.isAuthenticated() ? '/dashboard' : '/login'); + // Already signed in → straight to the dashboard. Guests get the landing page. + return authService.isAuthenticated() ? router.parseUrl('/dashboard') : true; }; diff --git a/docs/specs/README.md b/docs/specs/README.md index e391b01..35aa3ed 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -9,3 +9,4 @@ Detail specs for Bikontrol. Start with [AGENTS.md](../../AGENTS.md) for the proj - [reminders.md](reminders.md) — maintenance reminder engine (due rules, daily job, dedupe, preferences) - [legal.md](legal.md) — public Terms/Privacy pages (templates pending legal review) - [help.md](help.md) — public Help/FAQ page (how-to + FAQ) +- [landing.md](landing.md) — public root landing page (value prop + CTAs) diff --git a/docs/specs/landing.md b/docs/specs/landing.md new file mode 100644 index 0000000..e639547 --- /dev/null +++ b/docs/specs/landing.md @@ -0,0 +1,14 @@ +# Landing + +Public marketing page at the **root path** (`modules/marketing/landing`). The +`rootRedirectGuard` sends authenticated users straight to `/dashboard` and lets +guests see the landing; CTAs link to `/register`, `/login` (and a demo button +when `environment.demoEnabled` is on). + +Sections: hero + value proposition, value props, "cómo funciona", trust, final +CTA and footer (Ayuda / Términos / Privacidad). + +**Screenshots are pending.** The plan asks for real captures (see +[docs/screenshots/](../screenshots/)); add them to +`bikontrol-web/public/screenshots/` and render them in the marked spot in the +template. Do not ship invented/mocked screenshots. diff --git a/e2e/tests/landing.spec.ts b/e2e/tests/landing.spec.ts new file mode 100644 index 0000000..0b2a5ae --- /dev/null +++ b/e2e/tests/landing.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +/** + * Landing (plan 5.1): guests see the marketing page at `/`, while authenticated + * users are sent straight to the dashboard. + */ + +test('guests see the landing and authenticated users are redirected to the dashboard', async ({ + page, +}) => { + // --- Guest: the landing renders ------------------------------------------- + await page.goto('/'); + await expect(page.getByRole('heading', { name: /El mantenimiento de tu moto/ })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Empezar gratis' })).toBeVisible(); + + // --- Authenticated: root goes to the dashboard ----------------------------- + const email = `landing+${Date.now()}@bikontrol.test`; + const password = 'E2ePassword123!'; + await page.goto('/register'); + await page.getByLabel('Nombre completo').fill('Landing Rider'); + await page.getByLabel('Correo').fill(email); + await page.getByLabel('Contraseña', { exact: true }).fill(password); + await page.getByLabel('Confirmar contraseña').fill(password); + await page.getByRole('button', { name: 'Registrarse' }).click(); + await expect(page).toHaveURL(/\/dashboard\/home/); + + await page.goto('/'); + await expect(page).toHaveURL(/\/dashboard\/home/); +});