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
17 changes: 14 additions & 3 deletions apps/web-e2e/src/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function RequireUser({ admin = false }: { admin?: boolean }) {

function Splash() {
return (
<div className="grid min-h-dvh place-items-center text-stone-500">
<div className="grid min-h-svh place-items-center text-stone-500">
Loading…
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/auth-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function AuthCard({
children: ReactNode;
}) {
return (
<div className="grid min-h-dvh place-items-center px-4 py-10">
<div className="grid min-h-svh place-items-center px-4 py-10">
<div className="w-full max-w-sm space-y-6">
<div className="flex flex-col items-center gap-3 text-center">
<img src="/favicon.svg" alt="" className="h-14 w-14" />
Expand Down
35 changes: 35 additions & 0 deletions apps/web/src/components/back-link.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
to={to}
className="inline-flex max-w-full items-center gap-1.5 text-sm text-brand-600"
>
<svg
viewBox="0 0 16 16"
className="size-4 shrink-0"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M13 8H3m4-4L3 8l4 4" />
</svg>
{children}
</Link>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function Layout() {
const { data: me } = useMe();
const { data: instance } = useInstance();
return (
<div className="mx-auto flex min-h-dvh max-w-2xl flex-col">
<div className="mx-auto flex min-h-svh max-w-2xl flex-col">
<header className="sticky top-0 z-30 flex items-center gap-2 border-b border-stone-200 bg-stone-50/90 px-4 pb-3 pt-[max(0.75rem,env(safe-area-inset-top))] backdrop-blur dark:border-stone-800 dark:bg-stone-950/90">
<img src="/favicon.svg" alt="" className="h-7 w-7" />
<span className="text-lg font-semibold">openRun</span>
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/pages/activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down Expand Up @@ -59,9 +60,7 @@ export function ActivityPage() {
return (
<>
<div>
<Link to="/" className="text-sm text-brand-600">
← Runs
</Link>
<BackLink to="/">Runs</BackLink>
{editing ? (
<EditActivity activity={a} onDone={() => setEditing(false)} />
) : (
Expand Down
8 changes: 2 additions & 6 deletions apps/web/src/pages/club-coffee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -51,12 +52,7 @@ export function ClubCoffeePage() {
return (
<>
<div>
<Link
to={`/clubs/${id}/runs/${runId}`}
className="text-sm text-brand-600"
>
← {s.runTitle}
</Link>
<BackLink to={`/clubs/${id}/runs/${runId}`}>{s.runTitle}</BackLink>
<h1 className="mt-1 text-2xl font-semibold">Coffee order</h1>
<p className="text-stone-600 dark:text-stone-300">
{s.clubName} · {formatRunTime(s.startsAt, s.timezone)}
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/pages/club-manage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -43,9 +44,7 @@ export function ClubManagePage() {
return (
<>
<div>
<Link to={`/clubs/${club.id}`} className="text-sm text-brand-600">
← {club.name}
</Link>
<BackLink to={`/clubs/${club.id}`}>{club.name}</BackLink>
<h1 className="mt-1 text-2xl font-semibold">Manage club</h1>
</div>
<Requests club={club} />
Expand Down
9 changes: 3 additions & 6 deletions apps/web/src/pages/club-run.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -38,17 +39,13 @@ export function ClubRunPage() {
return (
<>
<div>
<Link
to={`/clubs/${r.clubId}`}
className="inline-flex items-center gap-1.5 text-sm text-brand-600"
>
←{' '}
<BackLink to={`/clubs/${r.clubId}`}>
<ClubLogo
club={{ id: r.clubId, name: r.clubName, logoUrl: r.clubLogoUrl }}
size="sm"
/>
{r.clubName}
</Link>
</BackLink>
<h1
className={`mt-1 text-2xl font-semibold ${cancelled ? 'line-through' : ''}`}
>
Expand Down
27 changes: 13 additions & 14 deletions apps/web/src/pages/club.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ShareLevel, string> = {
attendance: 'Attendance only',
Expand Down Expand Up @@ -52,28 +53,26 @@ export function ClubPage() {
return (
<>
<div>
<Link to="/clubs" className="text-sm text-brand-600">
← Clubs
</Link>
<div className="mt-1 flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<ClubLogo club={c} size="lg" />
<div className="min-w-0">
<h1 className="text-2xl font-semibold">{c.name}</h1>
<p className="text-sm text-stone-500">
{c.memberCount} {c.memberCount === 1 ? 'member' : 'members'}
</p>
</div>
</div>
<div className="flex min-h-9 items-center justify-between gap-3">
<BackLink to="/clubs">Clubs</BackLink>
{manage && (
<Link
to={`/clubs/${c.id}/manage`}
className="btn-secondary shrink-0"
className="btn-secondary px-3 py-1.5 text-sm"
>
Manage
</Link>
)}
</div>
<div className="mt-2 flex items-center gap-3">
<ClubLogo club={c} size="lg" />
<div className="min-w-0">
<h1 className="text-2xl leading-tight font-semibold">{c.name}</h1>
<p className="mt-0.5 text-sm text-stone-500">
{c.memberCount} {c.memberCount === 1 ? 'member' : 'members'}
</p>
</div>
</div>
{c.description && (
<p className="mt-2 whitespace-pre-wrap text-stone-600 dark:text-stone-300">
{c.description}
Expand Down
22 changes: 19 additions & 3 deletions apps/web/src/pages/clubs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function ClubsPage() {
</section>
) : null}

<JoinWithCode />
<JoinWithCode open={clubs?.length === 0} />
</>
);
}
Expand Down Expand Up @@ -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<HTMLFormElement>) => {
e.preventDefault();
const code = String(new FormData(e.currentTarget).get('code'));
Expand All @@ -178,18 +183,29 @@ function JoinWithCode() {
},
});
};
if (!open && !startOpen) {
return (
<button
className="block w-full text-center text-sm font-medium text-brand-600"
onClick={() => setOpen(true)}
>
Have a code? Join a club
</button>
);
}
return (
<form onSubmit={onSubmit} className="card space-y-2">
<label className="font-semibold" htmlFor="code">
Join with a code
</label>
<div className="flex gap-2">
<input
className="input font-mono uppercase"
className="input font-mono uppercase placeholder:normal-case"
id="code"
name="code"
placeholder="e.g. K7MPQ2RX"
autoCapitalize="characters"
autoFocus={open}
required
/>
<button className="btn-primary shrink-0" disabled={join.isPending}>
Expand Down
9 changes: 2 additions & 7 deletions apps/web/src/pages/privacy.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link } from 'react-router-dom';
import { useInstance, useMe } from '../lib/queries';
import { BackLink } from '../components/back-link';

/** "Who can see what": readable before signing up, so it's outside the app layout. */
export function PrivacyPage() {
Expand All @@ -9,12 +9,7 @@ export function PrivacyPage() {

return (
<div className="mx-auto max-w-2xl space-y-4 px-4 pb-16 pt-[max(1.5rem,env(safe-area-inset-top))]">
<Link
to={me ? '/settings' : '/register'}
className="text-sm text-brand-600"
>
← Back
</Link>
<BackLink to={me ? '/settings' : '/register'}>Back</BackLink>
<h1 className="text-2xl font-semibold">Who can see what</h1>

<Section title="Your runs">
Expand Down
28 changes: 25 additions & 3 deletions apps/web/src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,25 @@ export function SettingsPage() {
});
};

const [signingOut, setSigningOut] = useState(false);
const [signOutError, setSignOutError] = useState<string | null>(null);
const signOut = async () => {
await authClient.signOut();
setSigningOut(true);
setSignOutError(null);
const fallback = "Couldn't sign out. Check your connection and try again.";
try {
// Better Auth reports failures in the result rather than throwing.
const { error } = await authClient.signOut();
if (error) {
setSignOutError(authErrorMessage(error, fallback));
setSigningOut(false);
return;
}
} catch {
setSignOutError(fallback);
setSigningOut(false);
return;
}
queryClient.clear();
navigate('/login', { replace: true });
};
Expand Down Expand Up @@ -135,9 +152,14 @@ export function SettingsPage() {
<h2 className="font-semibold">This device</h2>
<Appearance />
<InstallButton />
<button className="btn-secondary w-full" onClick={signOut}>
Sign out
<button
className="btn-secondary w-full"
onClick={signOut}
disabled={signingOut}
>
{signingOut ? 'Signing out…' : 'Sign out'}
</button>
<FormError message={signOutError} />
</section>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
Loading