diff --git a/client/src/components/Journey/JourneyEntryCover.test.tsx b/client/src/components/Journey/JourneyEntryCover.test.tsx index bd33f52fa..994b66580 100644 --- a/client/src/components/Journey/JourneyEntryCover.test.tsx +++ b/client/src/components/Journey/JourneyEntryCover.test.tsx @@ -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', () => { diff --git a/client/src/components/Journey/JourneyEntryCover.tsx b/client/src/components/Journey/JourneyEntryCover.tsx index d09accdf8..8a4069b7c 100644 --- a/client/src/components/Journey/JourneyEntryCover.tsx +++ b/client/src/components/Journey/JourneyEntryCover.tsx @@ -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({ )} + {/* 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 && ( - - {t('journey.entry.suggestion')} + + )} diff --git a/client/src/components/Journey/JourneyMap.test.tsx b/client/src/components/Journey/JourneyMap.test.tsx index 80662d186..08c118e6c 100644 --- a/client/src/components/Journey/JourneyMap.test.tsx +++ b/client/src/components/Journey/JourneyMap.test.tsx @@ -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( ); - // 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">([^<]*) 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(' typeof l === 'string' && l.includes('<img src=x onerror="alert(1)">'))).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', () => { diff --git a/client/src/components/Journey/JourneyMapGL.test.tsx b/client/src/components/Journey/JourneyMapGL.test.tsx index d9e2ea776..c9b99ee58 100644 --- a/client/src/components/Journey/JourneyMapGL.test.tsx +++ b/client/src/components/Journey/JourneyMapGL.test.tsx @@ -195,7 +195,8 @@ describe('JourneyMapGL', () => { render() 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', () => { diff --git a/client/src/mobile/screens/dashboard/MDashboard.tsx b/client/src/mobile/screens/dashboard/MDashboard.tsx index 3f5067e1d..e52a5b4de 100644 --- a/client/src/mobile/screens/dashboard/MDashboard.tsx +++ b/client/src/mobile/screens/dashboard/MDashboard.tsx @@ -412,9 +412,16 @@ function MSpotlightCard({ trip, t, onOpen, actions }: { ) } +/** + * One fact about the spotlight trip: how long, how many places, how many people. + * + * Set like the DAY x OF y badge above it, a size down. The three used to be + * sentence case while the two badges over them were capitals, so the card had two + * voices in the same corner. + */ function SpotlightPill({ icon, label }: { icon: React.ReactElement; label: string }): React.ReactElement { return ( - + {icon} {label} diff --git a/client/src/mobile/screens/journey/MJourney.tsx b/client/src/mobile/screens/journey/MJourney.tsx index b98786dac..16622509b 100644 --- a/client/src/mobile/screens/journey/MJourney.tsx +++ b/client/src/mobile/screens/journey/MJourney.tsx @@ -103,41 +103,47 @@ function HeroCard({ journey, onOpen }: { journey: JourneyListItem; onOpen: () => const src = journeyCoverSrc(journey.cover_image) return ( + // Built like the dashboard's spotlight trip: the photo carries the card and + // everything written sits on one panel of dark glass at its foot, rather than + // a badge floating in one corner and text bleeding into a gradient at the + // other. Two hero cards, two different languages, was the complaint. ) } +/** One fact about the journey, set like the spotlight trip's pills on the dashboard. */ +function HeroPill({ icon, label }: { icon: React.ReactElement; label: string }) { + return ( + + {icon} + {label} + + ) +} + function GridCard({ journey, onOpen }: { journey: JourneyListItem; onOpen: () => void }) { const entries = journey.entry_count ?? 0 const photos = journey.photo_count ?? 0 diff --git a/client/src/mobile/screens/journey/MJourneyDetail.tsx b/client/src/mobile/screens/journey/MJourneyDetail.tsx index 51e68317a..3ca4485a8 100644 --- a/client/src/mobile/screens/journey/MJourneyDetail.tsx +++ b/client/src/mobile/screens/journey/MJourneyDetail.tsx @@ -158,7 +158,7 @@ export default function MJourneyDetail() { await reorderEntries(current.id, reordered.map(e => e.id)) await loadJourney(current.id) } catch { - toast.error(t('common.errorOccurred')) + toast.error(t('common.errorTitle')) } }, [current, entries, reorderEntries, loadJourney, toast, t]) diff --git a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx index 5d2462b27..440f464a9 100644 --- a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx +++ b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx @@ -963,6 +963,24 @@ export default function MJourneyEntrySheet({ )} + {/* A suggestion is not deleted, it is put down: the row survives so the trip + sync does not offer the same place again (discussion #2299). + + Its own row above the buttons rather than among them: the sentence is + longer than a button label and wrapped into two lines between Delete and + Cancel. Full width, it reads as what it is, an alternative to saving this + suggestion rather than a fourth thing competing with them. */} + {!readOnly && onDismiss && ( + + )} +
{!readOnly && onDelete && ( - )} {!readOnly && captureOnly && (