@@ -613,7 +627,7 @@ export default function JourneyDetailPage() {
.catch(() => toast.error(t('common.errorOccurred')))
}
return (
-
+
{ setActiveEntryId(String(entry.id)); mapRef.current?.highlightMarker(String(entry.id)) }} style={String(entry.id) === activeEntryId ? { outline: `2px solid ${DAY_COLORS[dayIdx % DAY_COLORS.length]}`, outlineOffset: '3px', borderRadius: '12px' } : undefined}>
{canReorder && (
- {e.location_name}{e.entry_time ? ` · ${e.entry_time}` : ''}
+ {formatLocationName(e.location_name)}{e.entry_time ? ` · ${e.entry_time}` : ''}
@@ -1360,7 +1375,7 @@ function EntryCard({ entry, readOnly, onEdit, onDelete, onPhotoClick }: {
{entry.location_name && (
- {entry.location_name}
+ {formatLocationName(entry.location_name)}
)}
{entry.entry_time && (
@@ -1403,7 +1418,7 @@ function EntryCard({ entry, readOnly, onEdit, onDelete, onPhotoClick }: {
{entry.location_name && (
- {entry.location_name}
+ {formatLocationName(entry.location_name)}
)}
{entry.entry_time && (
@@ -1482,7 +1497,7 @@ function SkeletonCard({ entry, onClick }: { entry: JourneyEntry; onClick?: () =>
{entry.title || t('journey.detail.newEntry')}
- {entry.location_name || ''}{entry.entry_time ? ` · ${entry.entry_time}` : ''}
+ {formatLocationName(entry.location_name)}{entry.entry_time ? ` · ${entry.entry_time}` : ''}
@@ -2962,11 +2977,12 @@ function JourneyShareSection({ journeyId }: { journeyId: number }) {
)
}
-function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
+function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, onRefresh }: {
journey: JourneyDetail
onClose: () => void
onSaved: () => void
onOpenInvite: () => void
+ onRefresh: () => void
}) {
const { t } = useTranslation()
const [title, setTitle] = useState(journey.title)
@@ -3133,7 +3149,7 @@ function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite }: {
try {
await journeyApi.removeContributor(journey.id, c.user_id)
toast.success(t('journey.contributors.removed'))
- onSaved()
+ onRefresh()
} catch {
toast.error(t('journey.contributors.removeFailed'))
}
diff --git a/client/src/pages/JourneyPublicPage.tsx b/client/src/pages/JourneyPublicPage.tsx
index aa10be43..816ac6ec 100644
--- a/client/src/pages/JourneyPublicPage.tsx
+++ b/client/src/pages/JourneyPublicPage.tsx
@@ -216,6 +216,28 @@ export default function JourneyPublicPage() {
)}
+ {/* Floating view toggle — visible above the fullscreen map on mobile */}
+ {isMobile && view === 'timeline' && perms.share_timeline && perms.share_map && availableViews.length > 1 && (
+
+
+ {availableViews.map(v => (
+
+ ))}
+
+
+ )}
+
{/* Mobile combined map+timeline (public, read-only) */}
{isMobile && view === 'timeline' && perms.share_timeline && perms.share_map && (
{}}
publicPhotoUrl={(photoId) => `/api/public/journey/${token}/photos/${photoId}/original`}
+ carouselBottom="calc(env(safe-area-inset-bottom, 16px) + 8px)"
/>
)}
diff --git a/client/src/utils/formatters.ts b/client/src/utils/formatters.ts
index 980f85ac..7a6c01a2 100644
--- a/client/src/utils/formatters.ts
+++ b/client/src/utils/formatters.ts
@@ -1,5 +1,38 @@
import type { AssignmentsMap } from '../types'
+// Collapses verbose Nominatim display_name strings (e.g. "Place, 1, Road, Neighbourhood,
+// City, County, State, Country, Postcode, Country") into "Place, Postcode, Country".
+// Clean short names (≤3 parts) pass through untouched.
+export function formatLocationName(raw: string | null | undefined): string {
+ if (!raw) return ''
+ const parts = raw.split(',').map(p => p.trim()).filter(Boolean)
+ if (parts.length <= 3) return raw.trim()
+
+ // Dedup preserving insertion order
+ const seen = new Set()
+ const unique: string[] = []
+ for (const p of parts) {
+ if (!seen.has(p.toLowerCase())) { seen.add(p.toLowerCase()); unique.push(p) }
+ }
+ if (unique.length <= 3) return unique.join(', ')
+
+ const name = unique[0]
+ const last = unique[unique.length - 1]
+ const secondLast = unique.length >= 2 ? unique[unique.length - 2] : null
+
+ // Detect postcode at tail: short alphanumeric with at least one digit, ≤10 chars
+ const postalRe = /^[A-Z0-9][A-Z0-9\s\-]{1,8}$/i
+ const isLastPostal = postalRe.test(last) && /\d/.test(last) && last.length <= 10
+ const postcode = isLastPostal ? last : null
+ const country = isLastPostal ? secondLast : last
+
+ const result: string[] = [name]
+ if (postcode && postcode !== name) result.push(postcode)
+ if (country && country !== name && country !== postcode) result.push(country)
+
+ return result.join(', ')
+}
+
const ZERO_DECIMAL_CURRENCIES = new Set(['JPY', 'KRW', 'VND', 'CLP', 'ISK', 'HUF'])
export function currencyDecimals(currency: string): number {