From 545eb650ca30f592de5a00ea3cb2b6ed28037314 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 7 Nov 2026 00:15:14 +0100 Subject: [PATCH] fix(demo): the welcome is a sheet on a phone, not the desktop dialog made narrow On a phone the head took half the screen, the list scrolled in a small window and the start button broke over two lines next to the links. The phone now gets a bottom sheet of the phone shell: a small head with the countdown, the same content as a list, a full-width start button with the two links under it. The photo credits open in its place and back, as on the desktop. --- .../src/components/Demo/DemoWelcome.test.tsx | 21 ++- client/src/components/Demo/DemoWelcome.tsx | 16 ++ client/src/components/Demo/MDemoWelcome.tsx | 172 ++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 client/src/components/Demo/MDemoWelcome.tsx diff --git a/client/src/components/Demo/DemoWelcome.test.tsx b/client/src/components/Demo/DemoWelcome.test.tsx index 25b192ed6..a11179b1a 100644 --- a/client/src/components/Demo/DemoWelcome.test.tsx +++ b/client/src/components/Demo/DemoWelcome.test.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, within } from '@testing-library/react'; +import { act, fireEvent, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { render, screen } from '../../../tests/helpers/render'; @@ -6,6 +6,10 @@ import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { useAuthStore } from '../../store/authStore'; import DemoWelcome from './DemoWelcome'; +// useIsPhone follows matchMedia, which the global stub always answers with false. +const phone = vi.hoisted(() => ({ value: false })); +vi.mock('../../mobile/useIsPhone', () => ({ useIsPhone: () => phone.value })); + beforeEach(() => { resetAllStores(); seedStore(useAuthStore, { serverTimezone: 'UTC' }); @@ -13,6 +17,7 @@ beforeEach(() => { afterEach(() => { vi.useRealTimers(); + phone.value = false; }); describe('DemoWelcome', () => { @@ -146,4 +151,18 @@ describe('DemoWelcome', () => { 'https://creativecommons.org/licenses/by-sa/3.0/' ); }); + + it('FE-COMP-DEMOWELCOME-030: on a phone it is a sheet with the same content and a full-width start', async () => { + phone.value = true; + render(); + const sheet = screen.getByRole('dialog', { name: 'Live demo' }); + expect(sheet).toHaveTextContent('Welcome to TREK'); + expect(within(sheet).getByTestId('demo-countdown')).toBeInTheDocument(); + expect(within(sheet).getByText('Everything resets every hour')).toBeInTheDocument(); + expect(within(sheet).getByText('Trip planner')).toBeInTheDocument(); + expect(within(sheet).getByText('Automatic backups')).toBeInTheDocument(); + expect(within(sheet).getByRole('link', { name: /source code/i })).toHaveAttribute('target', '_blank'); + await userEvent.click(within(sheet).getByRole('button', { name: /start exploring/i })); + await waitFor(() => expect(screen.queryByText('Trip planner')).not.toBeInTheDocument()); + }); }); diff --git a/client/src/components/Demo/DemoWelcome.tsx b/client/src/components/Demo/DemoWelcome.tsx index d8774e8c3..00b101219 100644 --- a/client/src/components/Demo/DemoWelcome.tsx +++ b/client/src/components/Demo/DemoWelcome.tsx @@ -1,6 +1,7 @@ import { ArrowLeft, ArrowRight, Clock, ExternalLink, Images, RotateCcw } from 'lucide-react'; import { lazy, Suspense, useId } from 'react'; import { useTranslation } from '../../i18n'; +import { useIsPhone } from '../../mobile/useIsPhone'; import { DialogButton, DialogFooter, @@ -16,6 +17,7 @@ import { import ErrorBoundary from '../shared/ErrorBoundary'; import TrekIcon from '../shared/TrekIcon'; import { DEMO_FEATURES, DEMO_FULL_VERSION, DEMO_LIMITS, TREK_REPO_URL, type DemoLine } from './demoWelcomeModel'; +import MDemoWelcome from './MDemoWelcome'; import { useDemoWelcome } from './useDemoWelcome'; // The credits and their photo list load when a visitor opens them, not with the dashboard. @@ -138,6 +140,20 @@ export default function DemoWelcome() { const { t } = useTranslation(); const labelId = useId(); const { open, close, countdown, creditsOpen, backFromCredits, openCredits, closeCredits } = useDemoWelcome(); + const phone = useIsPhone(); + + // On a phone the welcome is a sheet of the phone shell, not the desktop dialog made narrow. + if (phone) + return ( + + ); return ( import('./DemoCredits')); + +type Translate = ReturnType['t']; + +interface MDemoWelcomeProps { + open: boolean; + close: () => void; + countdown: string; + creditsOpen: boolean; + openCredits: () => void; + closeCredits: () => void; +} + +const CARD = 'rounded-[18px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)]'; + +function Lines({ lines, accent, t }: { lines: readonly DemoLine[]; accent?: boolean; t: Translate }) { + return ( +
    + {lines.map(({ icon: Icon, labelKey }) => ( +
  • + + {t(labelKey)} +
  • + ))} +
+ ); +} + +function Welcome({ t }: { t: Translate }) { + return ( +
+

{t('demo.welcome.tagline')}

+ +
+ +
+
{t('demo.welcome.resetTitle')}
+

{t('demo.welcome.resetBody')}

+
+
+ +
+ {t('demo.welcome.tryTitle')} +
    + {DEMO_FEATURES.map(({ id, icon: Icon, titleKey, bodyKey }) => ( +
  • + + + +
    +
    {t(titleKey)}
    +

    {t(bodyKey)}

    +
    +
  • + ))} +
+
+ +
+ {t('demo.welcome.limitsTitle')} + +
+ +
+ {t('demo.welcome.fullTitle')} + +
+
+ ); +} + +/** + * The demo welcome on a phone: a sheet from the bottom like every other form + * in the phone shell, instead of the desktop dialog squeezed onto a narrow + * screen. The head stays small so the list gets the room, the start button + * runs the full width, and the two links sit under it. + */ +export default function MDemoWelcome({ + open, + close, + countdown, + creditsOpen, + openCredits, + closeCredits, +}: MDemoWelcomeProps) { + const { t } = useTranslation(); + const title = t(creditsOpen ? 'demo.credits.title' : 'demo.welcome.title'); + + return ( + +
+ + + + } + title={title} + subtitle={ + + + {t('demo.welcome.resetIn')} + + {countdown} + + + } + onClose={close} + closeLabel={t('common.close')} + /> + +
+ {creditsOpen ? ( + + + + + + ) : ( + + )} +
+ +
+ {creditsOpen ? ( + + ) : ( + <> + +
+ + + {t('demo.welcome.repo')} + + +
+ + )} +
+
+
+ ); +}