mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-10-06 18:51:56 -04:00
feat(#105): design Preview circuit-outline unavailable state
Keep the Circuit card when GPS outline is missing: show meeting identity and an honest local-cache explanation instead of a one-line empty section. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -58,35 +58,67 @@ function SectionState({
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
function circuitIdentity(meeting: Meeting): { name: string; place: string } {
|
||||
const name = meeting.circuit_short_name?.trim() || meeting.meeting_name
|
||||
const location = meeting.location?.trim() ?? ''
|
||||
const country = meeting.country_name?.trim() ?? ''
|
||||
const parts: string[] = []
|
||||
if (location) parts.push(location)
|
||||
if (country && country !== location) parts.push(country)
|
||||
return { name, place: parts.join(' · ') }
|
||||
}
|
||||
|
||||
function TrackOutlineCard({
|
||||
meeting,
|
||||
outline,
|
||||
loading,
|
||||
error,
|
||||
accent,
|
||||
}: {
|
||||
meeting: Meeting
|
||||
outline: TrackOutline | null | undefined
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
accent: string
|
||||
}) {
|
||||
const outlinePath = useMemo(() => buildOutlinePath(outline?.points ?? []), [outline])
|
||||
const identity = circuitIdentity(meeting)
|
||||
|
||||
let body: ReactNode
|
||||
if (loading) {
|
||||
body = <div className="preview-section-state">Loading…</div>
|
||||
} else if (error) {
|
||||
body = (
|
||||
<div className="preview-section-state error">
|
||||
{error instanceof Error ? error.message : 'Failed to load'}
|
||||
</div>
|
||||
)
|
||||
} else if (outlinePath) {
|
||||
body = (
|
||||
<div className="preview-track-stage" style={{ ['--preview-accent' as string]: accent }}>
|
||||
<svg className="preview-track-svg" viewBox="0 0 100 100" role="img" aria-label="Circuit outline">
|
||||
<path className="preview-track-shadow" d={outlinePath} />
|
||||
<path className="preview-track-outline" d={outlinePath} />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
body = (
|
||||
<div className="preview-track-fallback" data-testid="preview-track-fallback">
|
||||
<div className="preview-track-fallback-glyph" aria-hidden />
|
||||
<p className="preview-track-fallback-name">{identity.name}</p>
|
||||
{identity.place && <p className="preview-track-fallback-place">{identity.place}</p>}
|
||||
<p className="preview-track-fallback-hint">
|
||||
A GPS track outline is not available in the local cache.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="preview-card" data-testid="preview-track-card">
|
||||
<h2 className="preview-card-title">Circuit</h2>
|
||||
<SectionState
|
||||
loading={loading}
|
||||
error={error}
|
||||
empty={!outline || !outlinePath}
|
||||
emptyMessage="Track outline unavailable for this circuit"
|
||||
>
|
||||
<div className="preview-track-stage" style={{ ['--preview-accent' as string]: accent }}>
|
||||
<svg className="preview-track-svg" viewBox="0 0 100 100" role="img" aria-label="Circuit outline">
|
||||
<path className="preview-track-shadow" d={outlinePath} />
|
||||
<path className="preview-track-outline" d={outlinePath} />
|
||||
</svg>
|
||||
</div>
|
||||
</SectionState>
|
||||
{body}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -408,6 +440,7 @@ export function RacePreviewPage() {
|
||||
|
||||
<div className="preview-grid">
|
||||
<TrackOutlineCard
|
||||
meeting={previewMeeting}
|
||||
outline={trackOutlineQuery.data}
|
||||
loading={trackOutlineQuery.isLoading}
|
||||
error={trackOutlineQuery.isError ? (trackOutlineQuery.error as Error) : null}
|
||||
|
||||
@@ -203,6 +203,47 @@
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.preview-track-fallback {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: var(--s2);
|
||||
min-height: 180px;
|
||||
padding: var(--s5);
|
||||
}
|
||||
|
||||
.preview-track-fallback-glyph {
|
||||
width: 88px;
|
||||
height: 52px;
|
||||
margin-bottom: var(--s2);
|
||||
border: 2px dashed rgba(255, 255, 255, 0.14);
|
||||
border-radius: 42% 50% 46% 54%;
|
||||
}
|
||||
|
||||
.preview-track-fallback-name {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-track-fallback-place {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-track-fallback-hint {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-3);
|
||||
max-width: 22rem;
|
||||
margin: var(--s2) 0 0;
|
||||
}
|
||||
|
||||
.preview-podium {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -223,6 +223,8 @@ describe('RacePreviewPage', () => {
|
||||
expect(screen.getByTestId('preview-header')).toHaveTextContent('Monaco')
|
||||
expect(screen.getByTestId('preview-schedule')).toHaveTextContent('FP1')
|
||||
expect(screen.getByTestId('preview-track-card')).toBeInTheDocument()
|
||||
expect(screen.getByRole('img', { name: 'Circuit outline' })).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('preview-track-fallback')).not.toBeInTheDocument()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-last-year-card')).toHaveTextContent('VER')
|
||||
@@ -273,5 +275,75 @@ describe('RacePreviewPage', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-last-year-card')).toHaveTextContent('First time on the calendar')
|
||||
})
|
||||
|
||||
const fallback = screen.getByTestId('preview-track-fallback')
|
||||
expect(screen.getByTestId('preview-track-card')).toContainElement(fallback)
|
||||
expect(fallback).toHaveTextContent('Monaco')
|
||||
expect(fallback).toHaveTextContent('A GPS track outline is not available in the local cache.')
|
||||
expect(screen.queryByRole('img', { name: 'Circuit outline' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a designed circuit fallback when the track outline is unavailable', async () => {
|
||||
const italianGp: Meeting = {
|
||||
...upcomingMeeting,
|
||||
meeting_name: 'Italian Grand Prix',
|
||||
meeting_official_name: 'Formula 1 Pirelli Gran Premio d\'Italia',
|
||||
location: 'Monza',
|
||||
country_name: 'Italy',
|
||||
country_code: 'ITA',
|
||||
circuit_short_name: 'Monza',
|
||||
}
|
||||
|
||||
mockFetchMeetings.mockImplementation(async (year: number) => {
|
||||
if (year === 2099) return [italianGp]
|
||||
if (year === 2098) return [{ ...italianGp, meeting_key: 90, year: 2098 }]
|
||||
return []
|
||||
})
|
||||
mockFetchSessions.mockImplementation(async (meetingKey: number) => {
|
||||
if (meetingKey === 100) return sessions
|
||||
if (meetingKey === 90) return [priorRaceSession]
|
||||
return []
|
||||
})
|
||||
mockFetchTrackOutline.mockResolvedValue(null)
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-track-fallback')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const card = screen.getByTestId('preview-track-card')
|
||||
const fallback = screen.getByTestId('preview-track-fallback')
|
||||
expect(card).toContainElement(fallback)
|
||||
expect(card).toHaveTextContent('Circuit')
|
||||
expect(fallback).toHaveTextContent('Monza')
|
||||
expect(fallback).toHaveTextContent('Italy')
|
||||
expect(fallback).toHaveTextContent('A GPS track outline is not available in the local cache.')
|
||||
expect(fallback).not.toHaveTextContent(/unknown/i)
|
||||
expect(screen.queryByRole('img', { name: 'Circuit outline' })).not.toBeInTheDocument()
|
||||
expect(card).not.toHaveTextContent('Track outline unavailable for this circuit')
|
||||
})
|
||||
|
||||
it('keeps a thrown outline request distinct from the designed unavailable state', async () => {
|
||||
mockFetchMeetings.mockImplementation(async (year: number) => {
|
||||
if (year === 2099) return [upcomingMeeting]
|
||||
if (year === 2098) return [priorMeeting]
|
||||
return []
|
||||
})
|
||||
mockFetchSessions.mockImplementation(async (meetingKey: number) => {
|
||||
if (meetingKey === 100) return sessions
|
||||
if (meetingKey === 90) return [priorRaceSession]
|
||||
return []
|
||||
})
|
||||
mockFetchTrackOutline.mockRejectedValue(new Error('outline request failed'))
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-track-card')).toHaveTextContent('outline request failed')
|
||||
})
|
||||
|
||||
expect(screen.queryByTestId('preview-track-fallback')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('img', { name: 'Circuit outline' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user