From 01a874b69ba944f8ef244832cceede09fe8acccd Mon Sep 17 00:00:00 2001 From: rd-code-23 Date: Mon, 24 Aug 2026 13:24:14 -0700 Subject: [PATCH] feat(web): add dashboard introduction --- .../dashboard/application-identity.ts | 21 +++++++++++ .../dashboard/application-shell.test.tsx | 4 ++- .../features/dashboard/application-shell.tsx | 2 +- .../dashboard/dashboard-access-boundary.tsx | 5 +-- .../dashboard/dashboard-landing.test.tsx | 36 +++++++++++++++++++ .../features/dashboard/dashboard-landing.tsx | 23 ++++++++++-- apps/web/src/routes/_app.tsx | 15 ++++---- apps/web/src/routes/index.test.tsx | 2 +- 8 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/features/dashboard/application-identity.ts create mode 100644 apps/web/src/features/dashboard/dashboard-landing.test.tsx diff --git a/apps/web/src/features/dashboard/application-identity.ts b/apps/web/src/features/dashboard/application-identity.ts new file mode 100644 index 0000000..13f7442 --- /dev/null +++ b/apps/web/src/features/dashboard/application-identity.ts @@ -0,0 +1,21 @@ +// Defines and exposes the application identity shared by protected pages. +import type { PublicUser } from '@goforlift/contracts'; +import { createContext, useContext } from 'react'; + +export type ApplicationIdentity = + { kind: 'guest' } | { kind: 'user'; user: PublicUser }; + +export const ApplicationIdentityContext = + createContext(null); + +export function useApplicationIdentity() { + const identity = useContext(ApplicationIdentityContext); + + if (!identity) { + throw new Error( + 'useApplicationIdentity must be used within ApplicationIdentityContext', + ); + } + + return identity; +} diff --git a/apps/web/src/features/dashboard/application-shell.test.tsx b/apps/web/src/features/dashboard/application-shell.test.tsx index 965a57a..cabb60a 100644 --- a/apps/web/src/features/dashboard/application-shell.test.tsx +++ b/apps/web/src/features/dashboard/application-shell.test.tsx @@ -25,7 +25,9 @@ describe('application shell', () => { , ); - expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); + expect( + screen.getByRole('heading', { name: 'Welcome, Guest' }), + ).toBeVisible(); expect(screen.getAllByText('Guest')).toHaveLength(2); expect(screen.getByText('Progress is temporary')).toBeVisible(); diff --git a/apps/web/src/features/dashboard/application-shell.tsx b/apps/web/src/features/dashboard/application-shell.tsx index 5ff1a54..46ca980 100644 --- a/apps/web/src/features/dashboard/application-shell.tsx +++ b/apps/web/src/features/dashboard/application-shell.tsx @@ -13,7 +13,7 @@ import { useState, type ReactNode } from 'react'; import wordmarkUrl from '../../assets/goforlift-wordmark.png'; import { cn } from '../../lib/utils'; -import type { ApplicationIdentity } from './dashboard-access-boundary'; +import type { ApplicationIdentity } from './application-identity'; const navigationItems = [ { href: '/dashboard', icon: LayoutDashboard, label: 'Dashboard' }, diff --git a/apps/web/src/features/dashboard/dashboard-access-boundary.tsx b/apps/web/src/features/dashboard/dashboard-access-boundary.tsx index d694f7e..b0c534d 100644 --- a/apps/web/src/features/dashboard/dashboard-access-boundary.tsx +++ b/apps/web/src/features/dashboard/dashboard-access-boundary.tsx @@ -1,14 +1,11 @@ // Allows authenticated and guest visitors into application routes while handling unresolved access. -import type { PublicUser } from '@goforlift/contracts'; import { Navigate } from '@tanstack/react-router'; import type { ReactNode } from 'react'; import { Button } from '../../components/ui/button'; import { useCurrentUser } from '../auth/current-user.query'; import { isGuestSession } from '../auth/guest-session'; - -export type ApplicationIdentity = - { kind: 'guest' } | { kind: 'user'; user: PublicUser }; +import type { ApplicationIdentity } from './application-identity'; type DashboardAccessBoundaryProps = { children: (identity: ApplicationIdentity) => ReactNode; diff --git a/apps/web/src/features/dashboard/dashboard-landing.test.tsx b/apps/web/src/features/dashboard/dashboard-landing.test.tsx new file mode 100644 index 0000000..83b7994 --- /dev/null +++ b/apps/web/src/features/dashboard/dashboard-landing.test.tsx @@ -0,0 +1,36 @@ +// Verifies the dashboard introduction uses wording appropriate to the current identity. +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { createPublicUser } from '../../test/fixtures/public-user.fixture'; +import { ApplicationIdentityContext } from './application-identity'; +import { DashboardLanding } from './dashboard-landing'; + +describe('dashboard landing', () => { + it('welcomes a guest without implying saved account data', () => { + render( + + + , + ); + + expect( + screen.getByRole('heading', { name: 'Welcome, Guest' }), + ).toBeVisible(); + expect(screen.getByText("Let's get stronger today.")).toBeVisible(); + }); + + it('welcomes a registered user by display name', () => { + const user = createPublicUser({ displayName: 'Alex Rivera' }); + + render( + + + , + ); + + expect( + screen.getByRole('heading', { name: 'Welcome, Alex Rivera' }), + ).toBeVisible(); + }); +}); diff --git a/apps/web/src/features/dashboard/dashboard-landing.tsx b/apps/web/src/features/dashboard/dashboard-landing.tsx index f946dcf..06969bd 100644 --- a/apps/web/src/features/dashboard/dashboard-landing.tsx +++ b/apps/web/src/features/dashboard/dashboard-landing.tsx @@ -1,6 +1,23 @@ -// Presents the dashboard heading inside the shared responsive application shell. -import { SectionLanding } from './section-landing'; +// Introduces new dashboard visitors with identity-aware onboarding copy. +import { useApplicationIdentity } from './application-identity'; export function DashboardLanding() { - return ; + const identity = useApplicationIdentity(); + + const identityLabel = + identity.kind === 'guest' + ? 'Guest' + : (identity.user.displayName ?? identity.user.email); + + return ( +
+

Dashboard

+

+ Welcome, {identityLabel} +

+

+ Let's get stronger today. +

+
+ ); } diff --git a/apps/web/src/routes/_app.tsx b/apps/web/src/routes/_app.tsx index 988962a..f575427 100644 --- a/apps/web/src/routes/_app.tsx +++ b/apps/web/src/routes/_app.tsx @@ -2,6 +2,7 @@ import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'; import { clearGuestSession } from '../features/auth/guest-session'; +import { ApplicationIdentityContext } from '../features/dashboard/application-identity'; import { ApplicationShell } from '../features/dashboard/application-shell'; import { DashboardAccessBoundary } from '../features/dashboard/dashboard-access-boundary'; @@ -20,12 +21,14 @@ function ProtectedApplicationLayout() { return ( {(identity) => ( - - - + + + + + )} ); diff --git a/apps/web/src/routes/index.test.tsx b/apps/web/src/routes/index.test.tsx index 8f325c3..7a9d8e6 100644 --- a/apps/web/src/routes/index.test.tsx +++ b/apps/web/src/routes/index.test.tsx @@ -43,7 +43,7 @@ describe('welcome page', () => { fireEvent.click(screen.getByRole('button', { name: /continue as guest/i })); expect( - await screen.findByRole('heading', { name: 'Dashboard' }), + await screen.findByRole('heading', { name: 'Welcome, Guest' }), ).toBeVisible(); expect(sessionStorage.getItem('goforlift.guest')).toBe('true'); expect(router.state.location.pathname).toBe('/dashboard');