diff --git a/AGENTS.md b/AGENTS.md index d90cd5c..d45b95e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,11 @@ Expected high-level structure: `apps/web` is the React frontend. +Within `apps/web/src`, use `app/` for application-wide composition such as the +protected shell, access boundary, and identity shared across routed features. +Keep `features/` reserved for product capabilities such as dashboard, routines, +workouts, and authentication. + `apps/api` is the Express backend. `packages/contracts` contains code that represents contracts shared between the diff --git a/apps/web/src/features/dashboard/dashboard-access-boundary.test.tsx b/apps/web/src/app/application-access-boundary.test.tsx similarity index 89% rename from apps/web/src/features/dashboard/dashboard-access-boundary.test.tsx rename to apps/web/src/app/application-access-boundary.test.tsx index 2dfbcfa..f9c17b5 100644 --- a/apps/web/src/features/dashboard/dashboard-access-boundary.test.tsx +++ b/apps/web/src/app/application-access-boundary.test.tsx @@ -1,4 +1,4 @@ -// Verifies dashboard access states with an isolated router and query cache for each case. +// Verifies application access states with an isolated router and query cache for each case. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createMemoryHistory, @@ -11,10 +11,10 @@ import { import { cleanup, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { createPublicUser } from '../../test/fixtures/public-user.fixture'; -import { startGuestSession } from '../auth/guest-session'; +import { startGuestSession } from '../features/auth/guest-session'; +import { createPublicUser } from '../test/fixtures/public-user.fixture'; +import { ApplicationAccessBoundary } from './application-access-boundary'; import { ApplicationShell } from './application-shell'; -import { DashboardAccessBoundary } from './dashboard-access-boundary'; const publicUser = createPublicUser({ avatarUrl: 'https://images.example.com/avatar.png', @@ -26,7 +26,7 @@ afterEach(() => { vi.unstubAllGlobals(); }); -describe('dashboard access boundary', () => { +describe('application access boundary', () => { it('allows an authenticated visitor into the dashboard', async () => { vi.stubGlobal( 'fetch', @@ -115,13 +115,13 @@ function renderDashboard() { getParentRoute: () => rootRoute, path: '/dashboard', component: () => ( - + {(identity) => (

Dashboard

)} -
+ ), }); const testRouter = createRouter({ diff --git a/apps/web/src/features/dashboard/dashboard-access-boundary.tsx b/apps/web/src/app/application-access-boundary.tsx similarity index 81% rename from apps/web/src/features/dashboard/dashboard-access-boundary.tsx rename to apps/web/src/app/application-access-boundary.tsx index b0c534d..40d6b2e 100644 --- a/apps/web/src/features/dashboard/dashboard-access-boundary.tsx +++ b/apps/web/src/app/application-access-boundary.tsx @@ -2,18 +2,18 @@ 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'; +import { Button } from '../components/ui/button'; +import { useCurrentUser } from '../features/auth/current-user.query'; +import { isGuestSession } from '../features/auth/guest-session'; import type { ApplicationIdentity } from './application-identity'; -type DashboardAccessBoundaryProps = { +type ApplicationAccessBoundaryProps = { children: (identity: ApplicationIdentity) => ReactNode; }; -export function DashboardAccessBoundary({ +export function ApplicationAccessBoundary({ children, -}: DashboardAccessBoundaryProps) { +}: ApplicationAccessBoundaryProps) { const currentUser = useCurrentUser(); if (isGuestSession()) { diff --git a/apps/web/src/features/dashboard/application-identity.ts b/apps/web/src/app/application-identity.ts similarity index 100% rename from apps/web/src/features/dashboard/application-identity.ts rename to apps/web/src/app/application-identity.ts diff --git a/apps/web/src/features/dashboard/application-shell.test.tsx b/apps/web/src/app/application-shell.test.tsx similarity index 95% rename from apps/web/src/features/dashboard/application-shell.test.tsx rename to apps/web/src/app/application-shell.test.tsx index cabb60a..8778db2 100644 --- a/apps/web/src/features/dashboard/application-shell.test.tsx +++ b/apps/web/src/app/application-shell.test.tsx @@ -5,8 +5,11 @@ 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 { isGuestSession, startGuestSession } from '../auth/guest-session'; +import { + isGuestSession, + startGuestSession, +} from '../features/auth/guest-session'; +import { router } from '../router'; afterEach(() => { cleanup(); diff --git a/apps/web/src/features/dashboard/application-shell.tsx b/apps/web/src/app/application-shell.tsx similarity index 98% rename from apps/web/src/features/dashboard/application-shell.tsx rename to apps/web/src/app/application-shell.tsx index 46ca980..045d524 100644 --- a/apps/web/src/features/dashboard/application-shell.tsx +++ b/apps/web/src/app/application-shell.tsx @@ -11,8 +11,8 @@ import { import { Link } from '@tanstack/react-router'; import { useState, type ReactNode } from 'react'; -import wordmarkUrl from '../../assets/goforlift-wordmark.png'; -import { cn } from '../../lib/utils'; +import wordmarkUrl from '../assets/goforlift-wordmark.png'; +import { cn } from '../lib/utils'; import type { ApplicationIdentity } from './application-identity'; const navigationItems = [ diff --git a/apps/web/src/features/dashboard/dashboard-landing.test.tsx b/apps/web/src/features/dashboard/dashboard-landing.test.tsx index 83b7994..8ff7d7f 100644 --- a/apps/web/src/features/dashboard/dashboard-landing.test.tsx +++ b/apps/web/src/features/dashboard/dashboard-landing.test.tsx @@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; +import { ApplicationIdentityContext } from '../../app/application-identity'; import { createPublicUser } from '../../test/fixtures/public-user.fixture'; -import { ApplicationIdentityContext } from './application-identity'; import { DashboardLanding } from './dashboard-landing'; describe('dashboard landing', () => { diff --git a/apps/web/src/features/dashboard/dashboard-landing.tsx b/apps/web/src/features/dashboard/dashboard-landing.tsx index 06969bd..f468908 100644 --- a/apps/web/src/features/dashboard/dashboard-landing.tsx +++ b/apps/web/src/features/dashboard/dashboard-landing.tsx @@ -1,5 +1,5 @@ // Introduces new dashboard visitors with identity-aware onboarding copy. -import { useApplicationIdentity } from './application-identity'; +import { useApplicationIdentity } from '../../app/application-identity'; export function DashboardLanding() { const identity = useApplicationIdentity(); diff --git a/apps/web/src/features/dashboard/section-landing.tsx b/apps/web/src/features/dashboard/section-landing.tsx deleted file mode 100644 index 3b3f94b..0000000 --- a/apps/web/src/features/dashboard/section-landing.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// Presents a minimal heading within the parent application route layout. - -type SectionLandingProps = { - title: string; -}; - -export function SectionLanding({ title }: SectionLandingProps) { - return ( -

- {title} -

- ); -} diff --git a/apps/web/src/routes/_app.history.tsx b/apps/web/src/routes/_app.history.tsx index 3fb92a7..5048a72 100644 --- a/apps/web/src/routes/_app.history.tsx +++ b/apps/web/src/routes/_app.history.tsx @@ -1,8 +1,14 @@ // Maps the history URL into the protected application layout. import { createFileRoute } from '@tanstack/react-router'; -import { SectionLanding } from '../features/dashboard/section-landing'; - export const Route = createFileRoute('/_app/history')({ - component: () => , + component: HistoryLanding, }); + +function HistoryLanding() { + return ( +

+ History +

+ ); +} diff --git a/apps/web/src/routes/_app.routines.tsx b/apps/web/src/routes/_app.routines.tsx index 2a61935..87f2a35 100644 --- a/apps/web/src/routes/_app.routines.tsx +++ b/apps/web/src/routes/_app.routines.tsx @@ -1,8 +1,14 @@ // Maps the routines URL into the protected application layout. import { createFileRoute } from '@tanstack/react-router'; -import { SectionLanding } from '../features/dashboard/section-landing'; - export const Route = createFileRoute('/_app/routines')({ - component: () => , + component: RoutinesLanding, }); + +function RoutinesLanding() { + return ( +

+ Routines +

+ ); +} diff --git a/apps/web/src/routes/_app.settings.tsx b/apps/web/src/routes/_app.settings.tsx index d9ed0b2..e0af1e4 100644 --- a/apps/web/src/routes/_app.settings.tsx +++ b/apps/web/src/routes/_app.settings.tsx @@ -1,8 +1,14 @@ // Maps the settings URL into the protected application layout. import { createFileRoute } from '@tanstack/react-router'; -import { SectionLanding } from '../features/dashboard/section-landing'; - export const Route = createFileRoute('/_app/settings')({ - component: () => , + component: SettingsLanding, }); + +function SettingsLanding() { + return ( +

+ Settings +

+ ); +} diff --git a/apps/web/src/routes/_app.tsx b/apps/web/src/routes/_app.tsx index f575427..7fa9bc4 100644 --- a/apps/web/src/routes/_app.tsx +++ b/apps/web/src/routes/_app.tsx @@ -1,10 +1,10 @@ // Protects and frames every route inside the signed-in or guest application area. import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'; +import { ApplicationAccessBoundary } from '../app/application-access-boundary'; +import { ApplicationIdentityContext } from '../app/application-identity'; +import { ApplicationShell } from '../app/application-shell'; 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'; export const Route = createFileRoute('/_app')({ component: ProtectedApplicationLayout, @@ -19,7 +19,7 @@ function ProtectedApplicationLayout() { } return ( - + {(identity) => ( )} - + ); }