diff --git a/apps/web/src/assets/goforlift-ship.png b/apps/web/src/assets/goforlift-ship.png new file mode 100644 index 0000000..23d6cff Binary files /dev/null and b/apps/web/src/assets/goforlift-ship.png differ diff --git a/apps/web/src/assets/goforlift-wordmark.png b/apps/web/src/assets/goforlift-wordmark.png new file mode 100644 index 0000000..1061474 Binary files /dev/null and b/apps/web/src/assets/goforlift-wordmark.png differ diff --git a/apps/web/src/assets/train-with-purpose.png b/apps/web/src/assets/train-with-purpose.png new file mode 100644 index 0000000..a5b0a17 Binary files /dev/null and b/apps/web/src/assets/train-with-purpose.png differ diff --git a/apps/web/src/assets/welcome-starfield-desktop.webp b/apps/web/src/assets/welcome-starfield-desktop.webp new file mode 100644 index 0000000..fea6a2e Binary files /dev/null and b/apps/web/src/assets/welcome-starfield-desktop.webp differ diff --git a/apps/web/src/assets/welcome-starfield.webp b/apps/web/src/assets/welcome-starfield.webp new file mode 100644 index 0000000..51d975e Binary files /dev/null and b/apps/web/src/assets/welcome-starfield.webp differ diff --git a/apps/web/src/features/auth/welcome-page.tsx b/apps/web/src/features/auth/welcome-page.tsx new file mode 100644 index 0000000..aec0871 --- /dev/null +++ b/apps/web/src/features/auth/welcome-page.tsx @@ -0,0 +1,89 @@ +// Presents the responsive welcome page and entry points for Google sign-in and guest access. +import { UserRound } from 'lucide-react'; + +import shipUrl from '../../assets/goforlift-ship.png'; +import headingUrl from '../../assets/train-with-purpose.png'; +import desktopStarfieldUrl from '../../assets/welcome-starfield-desktop.webp'; +import starfieldUrl from '../../assets/welcome-starfield.webp'; +import wordmarkUrl from '../../assets/goforlift-wordmark.png'; +import { Button } from '../../components/ui/button'; + +export function WelcomePage() { + return ( +
+ +
+ + GoForLift +

+ Train with purpose. +

+

+ Create routines, complete guided +
workouts, and track your progress. +

+
+ + +
+

+ Guest progress is temporary and will not be saved. +

+
+
+ ); +} + +function GoogleMark() { + return ( + + ); +} diff --git a/apps/web/src/routes/index.test.tsx b/apps/web/src/routes/index.test.tsx index 56f0b22..d665d04 100644 --- a/apps/web/src/routes/index.test.tsx +++ b/apps/web/src/routes/index.test.tsx @@ -1,24 +1,14 @@ +// Verifies the welcome page's primary entry points and guest-storage notice. import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import { render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { router } from '../router'; -describe('foundation page', () => { - beforeEach(() => { - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - json: () => Promise.resolve({ status: 'ok', database: 'connected' }), - }), - ); - }); - - it('renders the routed application and API status', async () => { - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, - }); +describe('welcome page', () => { + it('renders the Google and guest entry points', async () => { + const queryClient = new QueryClient(); await router.navigate({ to: '/' }); render( @@ -28,10 +18,14 @@ describe('foundation page', () => { ); expect( - await screen.findByRole('heading', { name: /ready for liftoff/i }), + screen.getByRole('heading', { name: /train with purpose/i }), ).toBeVisible(); expect( - await screen.findByText(/api and postgresql connected/i), - ).toBeVisible(); + screen.getByRole('link', { name: /continue with google/i }), + ).toHaveAttribute('href', '/auth/google'); + expect( + screen.getByRole('button', { name: /continue as guest/i }), + ).toBeEnabled(); + expect(screen.getByText(/guest progress is temporary/i)).toBeVisible(); }); }); diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index c93a5e7..56ddf2b 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -1,104 +1,6 @@ -import { healthResponseSchema } from '@goforlift/contracts'; -import { useQuery } from '@tanstack/react-query'; +// Maps the root route to the public welcome page. import { createFileRoute } from '@tanstack/react-router'; -import { Activity, Orbit } from 'lucide-react'; -import { Button } from '../components/ui/button'; +import { WelcomePage } from '../features/auth/welcome-page'; -export const Route = createFileRoute('/')({ component: HomePage }); - -async function getHealth() { - const response = await fetch('/health'); - const data: unknown = await response.json(); - return healthResponseSchema.parse(data); -} - -function HomePage() { - const health = useQuery({ - queryKey: ['health'], - queryFn: getHealth, - retry: false, - }); - - const isConnected = health.data?.database === 'connected'; - const systemStatus = getSystemStatus(health.isPending, isConnected); - - return ( -
-
-
-
-
-

- Development foundation -

-

- Ready for liftoff. -

-

- The React application, typed routing, server-state client, Tailwind - design system, and Express API foundation are configured. -

-
- - -
-
- - -
-
- ); -} - -function getSystemStatus(isPending: boolean, isConnected: boolean) { - if (isPending) { - return { - dotColor: 'bg-muted-foreground', - message: 'Contacting API…', - }; - } - - if (isConnected) { - return { - dotColor: 'bg-primary', - message: 'API and PostgreSQL connected', - }; - } - - return { - dotColor: 'bg-destructive', - message: 'API or PostgreSQL unavailable', - }; -} +export const Route = createFileRoute('/')({ component: WelcomePage }); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index c181765..0a33600 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -56,3 +56,27 @@ button, a { -webkit-tap-highlight-color: transparent; } + +.welcome-starfield { + position: relative; + background-color: #010307; +} + +.welcome-panel { + border: 1px solid transparent; +} + +@media (min-width: 640px) { + .welcome-panel { + border-radius: 1.5rem; + background: linear-gradient( + 160deg, + rgb(11 25 36 / 50%), + rgb(6 16 24 / 56%) + ); + box-shadow: + 0 24px 80px rgb(0 0 0 / 45%), + inset 0 1px 0 rgb(255 255 255 / 4%); + backdrop-filter: blur(8px); + } +} diff --git a/docs/mockups/welcome-page-a1.png b/docs/mockups/welcome-page-a1.png new file mode 100644 index 0000000..d1fbeb9 Binary files /dev/null and b/docs/mockups/welcome-page-a1.png differ