diff --git a/apps/web-e2e/src/smoke.spec.ts b/apps/web-e2e/src/smoke.spec.ts index b249096..27252b8 100644 --- a/apps/web-e2e/src/smoke.spec.ts +++ b/apps/web-e2e/src/smoke.spec.ts @@ -94,7 +94,10 @@ test.describe('signed in', () => { ).toBeVisible(); await expect(page.getByRole('heading', { name: 'Splits' })).toBeVisible(); - await page.getByRole('link', { name: '← Runs' }).click(); + await page + .getByRole('main') + .getByRole('link', { name: 'Runs', exact: true }) + .click(); await expect( page.getByRole('link', { name: /Riverside loop/ }), ).toBeVisible(); @@ -362,7 +365,11 @@ test.describe('clubs', () => { 256, ); - await page.getByRole('link', { name: '← Parkside Runners' }).click(); + await page + .getByRole('main') + .getByRole('link', { name: 'Parkside Runners', exact: true }) + .first() + .click(); await expect(page.getByTestId('club-logo')).toBeVisible(); const code = ( await page @@ -412,7 +419,11 @@ test.describe('clubs', () => { await expect(page.getByText('Bea', { exact: true })).toBeVisible(); // Coffee for tomorrow's run (today's may have closed already). - await page.getByRole('link', { name: '← Parkside Runners' }).click(); + await page + .getByRole('main') + .getByRole('link', { name: 'Parkside Runners', exact: true }) + .first() + .click(); const nextRun = page.getByRole('article').nth(1); await nextRun.getByRole('button', { name: 'Going' }).click(); await expect(nextRun.getByText('1 going')).toBeVisible(); diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 38e373b..b598837 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -27,7 +27,7 @@ function RequireUser({ admin = false }: { admin?: boolean }) { function Splash() { return ( -
+
Loading…
); diff --git a/apps/web/src/components/auth-card.tsx b/apps/web/src/components/auth-card.tsx index bbb2447..4c2321b 100644 --- a/apps/web/src/components/auth-card.tsx +++ b/apps/web/src/components/auth-card.tsx @@ -8,7 +8,7 @@ export function AuthCard({ children: ReactNode; }) { return ( -
+
diff --git a/apps/web/src/components/back-link.tsx b/apps/web/src/components/back-link.tsx new file mode 100644 index 0000000..9b6b390 --- /dev/null +++ b/apps/web/src/components/back-link.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; + +/** + * "← Somewhere" at the top of a page. The arrow is drawn rather than typed, + * because the text arrow sits low against the words in most fonts. + */ +export function BackLink({ + to, + children, +}: { + to: string; + children: ReactNode; +}) { + return ( + + + + + {children} + + ); +} diff --git a/apps/web/src/components/layout.tsx b/apps/web/src/components/layout.tsx index 8373bed..54cc63b 100644 --- a/apps/web/src/components/layout.tsx +++ b/apps/web/src/components/layout.tsx @@ -10,7 +10,7 @@ export function Layout() { const { data: me } = useMe(); const { data: instance } = useInstance(); return ( -
+
openRun diff --git a/apps/web/src/pages/activity.tsx b/apps/web/src/pages/activity.tsx index 275cd59..39309a3 100644 --- a/apps/web/src/pages/activity.tsx +++ b/apps/web/src/pages/activity.tsx @@ -31,6 +31,7 @@ import { SPORT_LABELS, formatDateTime, ordinal, pbLabel } from '../lib/format'; import { useMe } from '../lib/queries'; import { VISIBILITY_LABELS } from '../lib/privacy'; import { useShoes } from '../lib/stats'; +import { BackLink } from '../components/back-link'; const RouteMap = lazy(() => import('../components/route-map')); @@ -59,9 +60,7 @@ export function ActivityPage() { return ( <>
- - ← Runs - + Runs {editing ? ( setEditing(false)} /> ) : ( diff --git a/apps/web/src/pages/club-coffee.tsx b/apps/web/src/pages/club-coffee.tsx index 1d1a53e..bac491e 100644 --- a/apps/web/src/pages/club-coffee.tsx +++ b/apps/web/src/pages/club-coffee.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { Link, useParams } from 'react-router-dom'; import { useOrderSummary } from '../lib/clubs'; import { formatRunTime } from '../lib/format'; +import { BackLink } from '../components/back-link'; /** Keeps the screen on while the order list is open, where supported. */ function useWakeLock() { @@ -51,12 +52,7 @@ export function ClubCoffeePage() { return ( <>
- - ← {s.runTitle} - + {s.runTitle}

Coffee order

{s.clubName} · {formatRunTime(s.startsAt, s.timezone)} diff --git a/apps/web/src/pages/club-manage.tsx b/apps/web/src/pages/club-manage.tsx index c88e264..e229adc 100644 --- a/apps/web/src/pages/club-manage.tsx +++ b/apps/web/src/pages/club-manage.tsx @@ -12,6 +12,7 @@ import { ClubLogo } from '../components/club-logo'; import { clubApi, useClub, useClubMutation } from '../lib/clubs'; import { usePlaceSearch } from '../lib/places'; import { useInstance, useMe } from '../lib/queries'; +import { BackLink } from '../components/back-link'; const PointMap = lazy(() => import('../components/zone-map')); const LogoPicker = lazy(() => import('../components/logo-picker')); @@ -43,9 +44,7 @@ export function ClubManagePage() { return ( <>

- - ← {club.name} - + {club.name}

Manage club

diff --git a/apps/web/src/pages/club-run.tsx b/apps/web/src/pages/club-run.tsx index 3d14acb..eb45c98 100644 --- a/apps/web/src/pages/club-run.tsx +++ b/apps/web/src/pages/club-run.tsx @@ -16,6 +16,7 @@ import { } from '../lib/clubs'; import { ApiError } from '../lib/api'; import { formatRunTime, mapLink } from '../lib/format'; +import { BackLink } from '../components/back-link'; export function ClubRunPage() { const { id = '', runId = '' } = useParams(); @@ -38,17 +39,13 @@ export function ClubRunPage() { return ( <>
- - ←{' '} + {r.clubName} - +

diff --git a/apps/web/src/pages/club.tsx b/apps/web/src/pages/club.tsx index 2cbab5e..bc890df 100644 --- a/apps/web/src/pages/club.tsx +++ b/apps/web/src/pages/club.tsx @@ -13,6 +13,7 @@ import { ClubLogo } from '../components/club-logo'; import { CoffeePicker, describeOrder } from '../components/coffee'; import { clubApi, useClub, useClubMutation, useClubRuns } from '../lib/clubs'; import { ApiError } from '../lib/api'; +import { BackLink } from '../components/back-link'; export const SHARE_LEVEL_LABELS: Record = { attendance: 'Attendance only', @@ -52,28 +53,26 @@ export function ClubPage() { return ( <>
- - ← Clubs - -
-
- -
-

{c.name}

-

- {c.memberCount} {c.memberCount === 1 ? 'member' : 'members'} -

-
-
+
+ Clubs {manage && ( Manage )}
+
+ +
+

{c.name}

+

+ {c.memberCount} {c.memberCount === 1 ? 'member' : 'members'} +

+
+
{c.description && (

{c.description} diff --git a/apps/web/src/pages/clubs.tsx b/apps/web/src/pages/clubs.tsx index a816044..a8aeb4a 100644 --- a/apps/web/src/pages/clubs.tsx +++ b/apps/web/src/pages/clubs.tsx @@ -45,7 +45,7 @@ export function ClubsPage() { ) : null} - + ); } @@ -166,9 +166,14 @@ function CreateClub({ onCancel }: { onCancel: () => void }) { ); } -function JoinWithCode() { +/** + * Most people arrive by invite link, so the code form stays folded away unless + * they have no clubs yet. Codes are for reading out at the start of a run. + */ +function JoinWithCode({ open: startOpen }: { open: boolean }) { const join = useJoinClub(); const navigate = useNavigate(); + const [open, setOpen] = useState(false); const onSubmit = (e: FormEvent) => { e.preventDefault(); const code = String(new FormData(e.currentTarget).get('code')); @@ -178,6 +183,16 @@ function JoinWithCode() { }, }); }; + if (!open && !startOpen) { + return ( + + ); + } return (

+ ); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index e667ee8..9f4cb97 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -24,7 +24,7 @@ color-scheme: dark; } body { - @apply min-h-dvh bg-stone-50 text-stone-900 antialiased dark:bg-stone-950 dark:text-stone-100; + @apply min-h-svh bg-stone-50 text-stone-900 antialiased dark:bg-stone-950 dark:text-stone-100; } }