0 ? 'sm:grid-cols-2' : ''} gap-2`}>
{openingHours && openingHours.length > 0 && (
@@ -775,7 +778,7 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
- {distKm < 1 ? `${Math.round(totalDist)} m` : `${distKm.toFixed(1)} km`}
+ {formatDistance(distKm, distanceUnit)}
{hasEle && (
<>
diff --git a/client/src/components/Settings/DisplaySettingsTab.test.tsx b/client/src/components/Settings/DisplaySettingsTab.test.tsx
index 51f1b80b..235d14ad 100644
--- a/client/src/components/Settings/DisplaySettingsTab.test.tsx
+++ b/client/src/components/Settings/DisplaySettingsTab.test.tsx
@@ -150,6 +150,22 @@ describe('DisplaySettingsTab', () => {
expect(updateSetting).toHaveBeenCalledWith('temperature_unit', 'fahrenheit');
});
+ it('FE-COMP-DISPLAY-028: metric distance button is active by default', () => {
+ seedStore(useSettingsStore, { settings: { temperature_unit: 'celsius' } });
+ render(
);
+ const metricBtn = screen.getByText('km Metric').closest('button')!;
+ expect(metricBtn.style.border).toContain('var(--text-primary)');
+ });
+
+ it('FE-COMP-DISPLAY-029: clicking imperial distance calls updateSetting with imperial', async () => {
+ const user = userEvent.setup();
+ const updateSetting = vi.fn().mockResolvedValue(undefined);
+ seedStore(useSettingsStore, { settings: buildSettings({ distance_unit: 'metric' }), updateSetting });
+ render(
);
+ await user.click(screen.getByText('mi Imperial'));
+ expect(updateSetting).toHaveBeenCalledWith('distance_unit', 'imperial');
+ });
+
it('FE-COMP-DISPLAY-020: clicking 24h time format calls updateSetting with 24h', async () => {
const user = userEvent.setup();
const updateSetting = vi.fn().mockResolvedValue(undefined);
diff --git a/client/src/components/Settings/DisplaySettingsTab.tsx b/client/src/components/Settings/DisplaySettingsTab.tsx
index d5cb40ed..c777366c 100644
--- a/client/src/components/Settings/DisplaySettingsTab.tsx
+++ b/client/src/components/Settings/DisplaySettingsTab.tsx
@@ -6,12 +6,14 @@ import { useToast } from '../shared/Toast'
import CustomSelect from '../shared/CustomSelect'
import { CURRENCIES, SYMBOLS } from '../Budget/BudgetPanel.constants'
import Section from './Section'
+import type { DistanceUnit } from '../../types'
export default function DisplaySettingsTab(): React.ReactElement {
const { settings, updateSetting } = useSettingsStore()
const { t } = useTranslation()
const toast = useToast()
const [tempUnit, setTempUnit] = useState
(settings.temperature_unit || 'celsius')
+ const [distanceUnit, setDistanceUnit] = useState(settings.distance_unit || 'metric')
const [langOpen, setLangOpen] = useState(false)
const langDropdownRef = useRef(null)
@@ -28,6 +30,10 @@ export default function DisplaySettingsTab(): React.ReactElement {
setTempUnit(settings.temperature_unit || 'celsius')
}, [settings.temperature_unit])
+ useEffect(() => {
+ setDistanceUnit(settings.distance_unit || 'metric')
+ }, [settings.distance_unit])
+
return (
{/* Display currency */}
@@ -200,6 +206,37 @@ export default function DisplaySettingsTab(): React.ReactElement {
+ {/* Distance */}
+
+
+
+ {([
+ { value: 'metric', label: 'km Metric' },
+ { value: 'imperial', label: 'mi Imperial' },
+ ] as const).map(opt => (
+
+ ))}
+
+
+
{/* Time Format */}
diff --git a/client/src/pages/DashboardPage.test.tsx b/client/src/pages/DashboardPage.test.tsx
index 73a5e36f..74f278c3 100644
--- a/client/src/pages/DashboardPage.test.tsx
+++ b/client/src/pages/DashboardPage.test.tsx
@@ -4,9 +4,10 @@ import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../../tests/helpers/msw/server';
import { resetAllStores, seedStore } from '../../tests/helpers/store';
-import { buildUser, buildAdmin, buildTrip } from '../../tests/helpers/factories';
+import { buildUser, buildAdmin, buildTrip, buildSettings } from '../../tests/helpers/factories';
import { useAuthStore } from '../store/authStore';
import { usePermissionsStore } from '../store/permissionsStore';
+import { useSettingsStore } from '../store/settingsStore';
import DashboardPage from './DashboardPage';
beforeEach(() => {
@@ -798,10 +799,51 @@ describe('DashboardPage', () => {
});
});
+ describe('FE-PAGE-DASH-033: Atlas distance respects distance unit setting', () => {
+ const distanceValue = (text: string) =>
+ screen.getByText((_, element) =>
+ element?.classList.contains('value') === true &&
+ element.textContent?.replace(/\s+/g, ' ').trim() === text
+ );
+
+ beforeEach(() => {
+ server.use(
+ http.get('/api/auth/travel-stats', () =>
+ HttpResponse.json({
+ totalTrips: 1,
+ totalDays: 1,
+ totalPlaces: 1,
+ totalDistanceKm: 10,
+ countries: [],
+ })
+ ),
+ );
+ });
+
+ it('renders metric atlas distance as kilometers', async () => {
+ seedStore(useSettingsStore, { settings: buildSettings({ distance_unit: 'metric' }) });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(distanceValue('10 km')).toBeInTheDocument();
+ });
+ });
+
+ it('renders imperial atlas distance as miles', async () => {
+ seedStore(useSettingsStore, { settings: buildSettings({ distance_unit: 'imperial' }) });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(distanceValue('6.2 mi')).toBeInTheDocument();
+ });
+ });
+ });
+
describe('FE-PAGE-DASH-032: Dark mode detection uses window.matchMedia', () => {
it('renders without error when dark_mode is set to auto', async () => {
// Seed settings with dark_mode = 'auto' to exercise the matchMedia branch
- const { useSettingsStore } = await import('../store/settingsStore');
seedStore(useSettingsStore, {
settings: {
map_tile_url: '',
@@ -812,6 +854,7 @@ describe('DashboardPage', () => {
default_currency: 'USD',
language: 'en',
temperature_unit: 'fahrenheit',
+ distance_unit: 'metric',
time_format: '12h',
show_place_description: false,
blur_booking_codes: false,
diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx
index 110ee9a9..c9cb0d53 100644
--- a/client/src/pages/DashboardPage.tsx
+++ b/client/src/pages/DashboardPage.tsx
@@ -19,6 +19,7 @@ import {
LayoutGrid, List, Ticket, X,
} from 'lucide-react'
import { formatTime, splitReservationDateTime } from '../utils/formatters'
+import { convertDistance, getDistanceUnitLabel } from '../utils/units'
import { useSettingsStore } from '../store/settingsStore'
import '../styles/dashboard.css'
@@ -358,12 +359,26 @@ function BoardingPassHero({ trip, bundle, locale, onOpen, onEdit, onCopy, onArch
}
// ── Atlas / stats row ────────────────────────────────────────────────────────
+function formatCompactDistance(value: number): string {
+ const safeValue = Number.isFinite(value) ? Math.max(0, value) : 0
+ if (safeValue >= 1000) {
+ return `${(safeValue / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 })}k`
+ }
+ const rounded = Math.round(safeValue * 10) / 10
+ if (safeValue > 0 && rounded === 0) return '<0.1'
+ return rounded.toLocaleString(undefined, { maximumFractionDigits: 1 })
+}
+
function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElement {
const { t } = useTranslation()
+ const distanceUnit = useSettingsStore(s => s.settings.distance_unit) || 'metric'
const countries = stats?.countries || []
const distanceKm = stats?.totalDistanceKm || 0
- const distanceText = distanceKm >= 1000 ? `${(distanceKm / 1000).toFixed(1)}k` : String(distanceKm)
- const equatorTimes = (distanceKm / 40075).toFixed(2)
+ const distance = convertDistance(distanceKm, distanceUnit)
+ const distanceText = formatCompactDistance(distance)
+ const equatorDistance = convertDistance(40075, distanceUnit)
+ const equatorTimes = (distance / equatorDistance).toFixed(2)
+ const distanceLabel = getDistanceUnitLabel(distanceUnit)
return (
@@ -401,7 +416,7 @@ function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElemen
{t('dashboard.atlas.distanceFlown')}
-
{distanceText} {t('dashboard.atlas.kmUnit')}
+
{distanceText} {distanceLabel}
{t('dashboard.atlas.aroundEquator', { count: equatorTimes })}