{label}
- setIsMenuOpen((isOpen) => !isOpen)}
+ type="button"
+ >
+ {avatarUrl ? (
+
+ ) : (
+
+
+
+ )}
+
{label}
+ {!compact && ( +{detail}
+ )} +diff --git a/AGENTS.md b/AGENTS.md index d0e1082..d90cd5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -181,6 +181,15 @@ Do not put all application state into Zustand. Do not duplicate TanStack Query server state inside Zustand without a specific reason. +### JSX Readability + +Keep JSX focused on describing the rendered UI. Move non-trivial conditions, +calculations, and data transformations into clearly named values in the +component body before the `return`. Simple conditions that remain immediately +understandable may stay inline. Use blank lines to separate distinct groups of +component logic, such as state, derived values, event handlers, and render +conditions. + ## UI Components diff --git a/apps/web/src/features/dashboard/application-shell.test.tsx b/apps/web/src/features/dashboard/application-shell.test.tsx index 6247f80..965a57a 100644 --- a/apps/web/src/features/dashboard/application-shell.test.tsx +++ b/apps/web/src/features/dashboard/application-shell.test.tsx @@ -2,10 +2,11 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import { cleanup, render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it } from 'vitest'; import { router } from '../../router'; -import { startGuestSession } from '../auth/guest-session'; +import { isGuestSession, startGuestSession } from '../auth/guest-session'; afterEach(() => { cleanup(); @@ -13,7 +14,8 @@ afterEach(() => { }); describe('application shell', () => { - it('renders desktop and mobile navigation with the current page selected', async () => { + it('renders responsive navigation and exits guest mode', async () => { + const user = userEvent.setup(); await router.navigate({ to: '/dashboard' }); startGuestSession(); const queryClient = new QueryClient(); @@ -25,7 +27,7 @@ describe('application shell', () => { expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); expect(screen.getAllByText('Guest')).toHaveLength(2); - expect(screen.getAllByText('Progress is temporary')).toHaveLength(2); + expect(screen.getByText('Progress is temporary')).toBeVisible(); const navigationRegions = screen.getAllByRole('navigation', { name: /primary navigation/i, @@ -46,5 +48,25 @@ describe('application shell', () => { within(navigation).getByRole('link', { name: 'Settings' }), ).toHaveAttribute('href', '/settings'); } + + expect(isGuestSession()).toBe(true); + const profileMenus = screen.getAllByRole('button', { + name: 'Open profile menu', + }); + expect(profileMenus).toHaveLength(2); + expect( + screen.queryByRole('menuitem', { name: 'Exit guest' }), + ).not.toBeInTheDocument(); + + await user.click(profileMenus[0]!); + + const exitAction = screen.getByRole('menuitem', { name: 'Exit guest' }); + + await user.click(exitAction); + + expect(isGuestSession()).toBe(false); + expect( + await screen.findByRole('heading', { name: 'Train with purpose.' }), + ).toBeVisible(); }); }); diff --git a/apps/web/src/features/dashboard/application-shell.tsx b/apps/web/src/features/dashboard/application-shell.tsx index b3f476a..5ff1a54 100644 --- a/apps/web/src/features/dashboard/application-shell.tsx +++ b/apps/web/src/features/dashboard/application-shell.tsx @@ -3,12 +3,13 @@ import { Dumbbell, History, LayoutDashboard, + LogOut, Settings, UserRound, type LucideIcon, } from 'lucide-react'; import { Link } from '@tanstack/react-router'; -import type { ReactNode } from 'react'; +import { useState, type ReactNode } from 'react'; import wordmarkUrl from '../../assets/goforlift-wordmark.png'; import { cn } from '../../lib/utils'; @@ -24,11 +25,13 @@ const navigationItems = [ type ApplicationShellProps = { children: ReactNode; identity: ApplicationIdentity; + onExitGuest?: () => void; }; export function ApplicationShell({ children, identity, + onExitGuest, }: ApplicationShellProps) { return (
{label}
- setIsMenuOpen((isOpen) => !isOpen)}
+ type="button"
+ >
+ {avatarUrl ? (
+
+ ) : (
+
+
+
+ )}
+
{label}
+ {!compact && ( +{detail}
+ )} +