fix(journey): a dead translation key, and two cards that spoke differently

`common.errorOccurred` is in no locale. It sat in the reorder failure toast
already and I copied it into three more places, so a failed dismiss, restore or
reorder printed the key at the reader. They all say `common.errorTitle` now.

The suggestion mark on a phone card was the word spelled out, which took a third
of a 136px card and pushed the date off it; it is the same plus the desktop
suggestion card offers, and the card's italic dimmed title carries the rest.
Dismissing a suggestion from the entry sheet has its own full-width row above
the buttons rather than a two-line label wedged between Delete and Cancel.

And two cards that should have been one design: the dashboard's spotlight trip
wrote its facts in capitals on a panel of dark glass, the journey hero wrote
them in sentence case on a gradient with its badge loose in a corner. The
journey card is built the way the trip card is now, and the trip card's three
pills are set like the DAY x OF y badge beside them, a size down and on the
same ground.
This commit is contained in:
Maurice
2026-09-13 20:32:49 +02:00
parent 15aefa449e
commit cbc28fad88
11 changed files with 95 additions and 55 deletions
@@ -61,8 +61,10 @@ describe('JourneyEntryCover', () => {
/>,
)
// Once as the badge, once as the fallback title.
expect(screen.getAllByText('Suggestion').length).toBe(2)
// The corner mark says it to a screen reader; the title says it in words,
// since a suggestion pulled from a trip place is the one card with no story.
expect(screen.getByLabelText('Suggestion')).toBeInTheDocument()
expect(screen.getByText('Suggestion')).toBeInTheDocument()
})
it('FE-JRN-COVER-005: mood and weather appear only while the journey keeps those fields', () => {
@@ -1,4 +1,4 @@
import { MapPin } from 'lucide-react'
import { MapPin, Plus } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { moodMeta, weatherMeta } from '../../mobile/screens/journey/mobileJourneyMeta'
import CountryFlag from '../shared/CountryFlag'
@@ -130,9 +130,17 @@ export default function JourneyEntryCover({
<CountryFlag code={entry.country_code} size={14} />
</span>
)}
{/* A mark rather than the word: "Suggestion" spelled out took a third of a
136px card and pushed the date off it. The plus is the same promise the
desktop suggestion card makes with its Add Entry button, and the card's
italic, dimmed title carries the rest. */}
{isSuggestion && (
<span className={`absolute top-2 ${entry.country_code ? 'left-[30px]' : 'left-2'} ${badge} uppercase tracking-[0.06em]`}>
{t('journey.entry.suggestion')}
<span
className={`absolute top-2 ${entry.country_code ? 'left-[30px]' : 'left-2'} ${badgeDot} text-white`}
title={t('journey.entry.suggestion')}
aria-label={t('journey.entry.suggestion')}
>
<Plus size={12} strokeWidth={2.8} />
</span>
)}
@@ -48,6 +48,9 @@ vi.mock('leaflet', () => {
divIcon: vi.fn(() => ({})),
latLngBounds: vi.fn(() => ({})),
layerGroup: vi.fn(() => ({ addLayer: vi.fn(), addTo: vi.fn(), remove: vi.fn() })),
// The map builds its own attribution control so it can collapse to the credit
// alone (see the GL twin: the phone lays a carousel over the bottom edge).
control: { attribution: vi.fn(() => ({ addTo: vi.fn() })) },
},
map: vi.fn(() => mockMap),
tileLayer: vi.fn(() => ({ addTo: vi.fn(), setUrl: vi.fn() })),
@@ -55,6 +58,7 @@ vi.mock('leaflet', () => {
polyline: vi.fn(() => { const line: any = { addTo: vi.fn(() => line), bindTooltip: vi.fn(() => line) }; return line }),
divIcon: vi.fn(() => ({})),
latLngBounds: vi.fn(() => ({})),
control: { attribution: vi.fn(() => ({ addTo: vi.fn() })) },
};
});
@@ -161,10 +165,11 @@ describe('JourneyMap', () => {
render(
<JourneyMap checkins={[]} entries={entriesWithCoords} />
);
// Each marker calls bindTooltip with the entry label
// The tooltip is the same card the GL renderer draws now (#2299), so the title
// is inside its markup rather than being the whole label.
const mockMarkerInstance = (L.marker as any).mock.results[0].value;
expect(mockMarkerInstance.bindTooltip).toHaveBeenCalledWith(
'Paris',
expect.stringContaining('Paris'),
expect.objectContaining({ direction: 'top' }),
);
});
@@ -205,12 +210,12 @@ describe('JourneyMap', () => {
// Tooltips use the entry titles
const mockMarker1 = (L.marker as any).mock.results[0].value;
expect(mockMarker1.bindTooltip).toHaveBeenCalledWith(
'Happy Paris',
expect.stringContaining('Happy Paris'),
expect.objectContaining({ direction: 'top' }),
);
const mockMarker2 = (L.marker as any).mock.results[1].value;
expect(mockMarker2.bindTooltip).toHaveBeenCalledWith(
'Sad Berlin',
expect.stringContaining('Sad Berlin'),
expect.objectContaining({ direction: 'top' }),
);
});
@@ -551,7 +556,9 @@ describe('JourneyMap', () => {
]}
/>
);
const tooltipTitles = vi.mocked(mockedMarker().bindTooltip).mock.calls.map(c => c[0]);
const titleOf = (html: unknown) =>
String(html).match(/trek-journey-popup-title">([^<]*)</)?.[1] ?? '';
const tooltipTitles = vi.mocked(mockedMarker().bindTooltip).mock.calls.map(c => titleOf(c[0]));
expect(tooltipTitles).toEqual(['Paris', 'Berlin']);
});
@@ -562,7 +569,10 @@ describe('JourneyMap', () => {
entries={[{ id: 'e0', lat: 1, lng: 2, title: null, mood: null, entry_date: '2025-06-01' }]}
/>
);
expect(mockedMarker().bindTooltip).toHaveBeenCalledWith('Entry', expect.objectContaining({ direction: 'top' }));
expect(mockedMarker().bindTooltip).toHaveBeenCalledWith(
expect.stringContaining('Entry'),
expect.objectContaining({ direction: 'top' }),
);
});
it('FE-COMP-JOURNEYMAP-037: unmounting tears the leaflet map down', () => {
@@ -690,7 +700,7 @@ describe('JourneyMap', () => {
const labels = vi.mocked(mockedMarker().bindTooltip).mock.calls.map(c => c[0]);
expect(labels.some(l => typeof l === 'string' && l.includes('<img'))).toBe(false);
expect(labels).toContain('&lt;img src=x onerror=&quot;alert(1)&quot;&gt;');
expect(labels.some(l => typeof l === 'string' && l.includes('&lt;img src=x onerror=&quot;alert(1)&quot;&gt;'))).toBe(true);
});
// #1614 — photos placed by their own capture coordinates, collapsed by proximity.
it('FE-COMP-JOURNEYMAP-046: draws one thumbnail per cluster and counts the rest', () => {
@@ -195,7 +195,8 @@ describe('JourneyMapGL', () => {
render(<JourneyMapGL checkins={[]} entries={entries} />)
expect(mapboxgl.accessToken).toBe('pk.test_token')
expect(mapboxgl.Map).toHaveBeenCalledTimes(1)
expect(mapOptions()).toMatchObject({ style: 'mapbox://styles/mapbox/standard', attributionControl: true })
// Collapsed to its ⓘ rather than a strip of text across the bottom (#2299).
expect(mapOptions()).toMatchObject({ style: 'mapbox://styles/mapbox/standard', attributionControl: { compact: true } })
})
it('FE-COMP-JMAPGL-003: maplibre needs no token and opts out of the around-center rotate', () => {
@@ -331,7 +332,7 @@ describe('JourneyMapGL', () => {
const html = popupHtml()
expect(html).toContain('>Louvre<')
expect(html).toContain('trek-journey-popup-place')
expect(html).toContain('trek-journey-popup-sep')
expect(html).toContain('trek-journey-popup-chip')
})
it('FE-COMP-JMAPGL-014: the popup falls back to the location name when the entry has no title', () => {
@@ -349,9 +350,9 @@ describe('JourneyMapGL', () => {
const html = popupHtml()
expect(html).toContain('>Reykjavík<')
// the location moved to the title line, so there is no place chip left below
// the location moved to the title line, so there is no place chip left below.
// The date keeps its own chip, which is why only the place one is checked.
expect(html).not.toContain('trek-journey-popup-place')
expect(html).not.toContain('trek-journey-popup-sep')
})
it('FE-COMP-JMAPGL-015: an entry with neither title nor place still gets a heading', () => {