Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/web/src/features/dashboard/application-identity.ts
Original file line number Diff line number Diff line change
@@ -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<ApplicationIdentity | null>(null);

export function useApplicationIdentity() {
const identity = useContext(ApplicationIdentityContext);

if (!identity) {
throw new Error(
'useApplicationIdentity must be used within ApplicationIdentityContext',
);
}

return identity;
}
4 changes: 3 additions & 1 deletion apps/web/src/features/dashboard/application-shell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ describe('application shell', () => {
</QueryClientProvider>,
);

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();

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/dashboard/application-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/features/dashboard/dashboard-landing.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<ApplicationIdentityContext.Provider value={{ kind: 'guest' }}>
<DashboardLanding />
</ApplicationIdentityContext.Provider>,
);

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(
<ApplicationIdentityContext.Provider value={{ kind: 'user', user }}>
<DashboardLanding />
</ApplicationIdentityContext.Provider>,
);

expect(
screen.getByRole('heading', { name: 'Welcome, Alex Rivera' }),
).toBeVisible();
});
});
23 changes: 20 additions & 3 deletions apps/web/src/features/dashboard/dashboard-landing.tsx
Original file line number Diff line number Diff line change
@@ -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 <SectionLanding title="Dashboard" />;
const identity = useApplicationIdentity();

const identityLabel =
identity.kind === 'guest'
? 'Guest'
: (identity.user.displayName ?? identity.user.email);

return (
<header>
<p className="text-sm font-medium text-primary">Dashboard</p>
<h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">
Welcome, {identityLabel}
</h1>
<p className="mt-2 text-muted-foreground">
Let&apos;s get stronger today.
</p>
</header>
);
}
15 changes: 9 additions & 6 deletions apps/web/src/routes/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -20,12 +21,14 @@ function ProtectedApplicationLayout() {
return (
<DashboardAccessBoundary>
{(identity) => (
<ApplicationShell
identity={identity}
onExitGuest={identity.kind === 'guest' ? exitGuest : undefined}
>
<Outlet />
</ApplicationShell>
<ApplicationIdentityContext.Provider value={identity}>
<ApplicationShell
identity={identity}
onExitGuest={identity.kind === 'guest' ? exitGuest : undefined}
>
<Outlet />
</ApplicationShell>
</ApplicationIdentityContext.Provider>
)}
</DashboardAccessBoundary>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/routes/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down