mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 10:41:49 +00:00
feat: add distance unit (metric/imperial) display setting
Mirrors the existing temperature_unit pattern. Adds distance_unit to Settings, a Display settings control, admin default, and a formatDistance helper applied at distance render sites. Backward compatible (default metric). Closes #1300.
This commit is contained in:
@@ -7,7 +7,7 @@ import Section from '../Settings/Section'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import { MapView } from '../Map/MapView'
|
||||
import { CURRENCIES, SYMBOLS } from '../Budget/BudgetPanel.constants'
|
||||
import type { Place } from '../../types'
|
||||
import type { DistanceUnit, Place } from '../../types'
|
||||
|
||||
const MAP_PRESETS = [
|
||||
{ name: 'OpenStreetMap', url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
|
||||
@@ -19,6 +19,7 @@ const MAP_PRESETS = [
|
||||
|
||||
type Defaults = {
|
||||
temperature_unit?: string
|
||||
distance_unit?: DistanceUnit
|
||||
dark_mode?: string | boolean
|
||||
time_format?: string
|
||||
default_currency?: string
|
||||
@@ -212,6 +213,22 @@ export default function DefaultUserSettingsTab(): React.ReactElement {
|
||||
))}
|
||||
</OptionRow>
|
||||
|
||||
{/* Distance */}
|
||||
<OptionRow label={<>Distance <ResetButton field="distance_unit" /></>}>
|
||||
{([
|
||||
{ value: 'metric', label: 'km Metric' },
|
||||
{ value: 'imperial', label: 'mi Imperial' },
|
||||
] as const).map(opt => (
|
||||
<OptionButton
|
||||
key={opt.value}
|
||||
active={defaults.distance_unit === opt.value}
|
||||
onClick={() => save({ distance_unit: opt.value })}
|
||||
>
|
||||
{opt.label}
|
||||
</OptionButton>
|
||||
))}
|
||||
</OptionRow>
|
||||
|
||||
{/* Time Format */}
|
||||
<OptionRow label={<>{t('settings.timeFormat')} <ResetButton field="time_format" /></>}>
|
||||
{([
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { RouteResult, RouteSegment, RouteWithLegs, Waypoint, RouteAnchors } from '../../types'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import type { DistanceUnit, RouteResult, RouteSegment, RouteWithLegs, Waypoint, RouteAnchors } from '../../types'
|
||||
import { formatDistance } from '../../utils/units'
|
||||
|
||||
const OSRM_BASE = 'https://router.project-osrm.org/route/v1'
|
||||
|
||||
@@ -60,7 +62,7 @@ export async function calculateRoute(
|
||||
coordinates,
|
||||
distance,
|
||||
duration,
|
||||
distanceText: formatDistance(distance),
|
||||
distanceText: formatRouteDistance(distance),
|
||||
durationText: formatDuration(duration),
|
||||
walkingText: formatDuration(walkingDuration),
|
||||
drivingText: formatDuration(drivingDuration),
|
||||
@@ -218,7 +220,7 @@ export async function calculateSegments(
|
||||
duration: leg.duration,
|
||||
walkingText: formatDuration(walkingDuration),
|
||||
drivingText: formatDuration(leg.duration),
|
||||
distanceText: formatDistance(leg.distance),
|
||||
distanceText: formatRouteDistance(leg.distance),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -265,7 +267,7 @@ export async function calculateRouteWithLegs(
|
||||
duration: leg.duration,
|
||||
walkingText: formatDuration(walkingDuration),
|
||||
drivingText: formatDuration(leg.duration),
|
||||
distanceText: formatDistance(leg.distance),
|
||||
distanceText: formatRouteDistance(leg.distance),
|
||||
durationText: formatDuration(leg.duration),
|
||||
}
|
||||
}
|
||||
@@ -280,11 +282,16 @@ export async function calculateRouteWithLegs(
|
||||
return result
|
||||
}
|
||||
|
||||
function formatDistance(meters: number): string {
|
||||
if (meters < 1000) {
|
||||
function getDistanceUnit(): DistanceUnit {
|
||||
return useSettingsStore.getState().settings.distance_unit === 'imperial' ? 'imperial' : 'metric'
|
||||
}
|
||||
|
||||
function formatRouteDistance(meters: number): string {
|
||||
const unit = getDistanceUnit()
|
||||
if (unit === 'metric' && meters < 1000) {
|
||||
return `${Math.round(meters)} m`
|
||||
}
|
||||
return `${(meters / 1000).toFixed(1)} km`
|
||||
return formatDistance(meters / 1000, unit)
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
|
||||
@@ -402,9 +402,9 @@ describe('DayDetailPanel', () => {
|
||||
await screen.findByText('20:15');
|
||||
});
|
||||
|
||||
it('FE-PLANNER-DAYDETAIL-027: weather chips show Fahrenheit wind speed', async () => {
|
||||
it('FE-PLANNER-DAYDETAIL-027: weather chips show imperial wind speed', async () => {
|
||||
seedStore(useSettingsStore, {
|
||||
settings: { time_format: '24h', temperature_unit: 'fahrenheit', blur_booking_codes: false },
|
||||
settings: { time_format: '24h', temperature_unit: 'celsius', distance_unit: 'imperial', blur_booking_codes: false },
|
||||
});
|
||||
server.use(
|
||||
http.get('/api/weather/detailed', () =>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getLocaleForLanguage, useTranslation } from '../../i18n'
|
||||
import type { Day, Place, Category, Reservation, AssignmentsMap } from '../../types'
|
||||
import { isDayInAccommodationRange } from '../../utils/dayOrder'
|
||||
import { splitReservationDateTime } from '../../utils/formatters'
|
||||
import { convertDistance } from '../../utils/units'
|
||||
import { useDayDetail } from './useDayDetail'
|
||||
|
||||
const WEATHER_ICON_MAP = {
|
||||
@@ -68,6 +69,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
const tripObj = useTripStore((s) => s.trip)
|
||||
const canEditDays = can('day_edit', tripObj)
|
||||
const isFahrenheit = useSettingsStore(s => s.settings.temperature_unit) === 'fahrenheit'
|
||||
const distanceUnit = useSettingsStore(s => s.settings.distance_unit) || 'metric'
|
||||
const is12h = useSettingsStore(s => s.settings.time_format) === '12h'
|
||||
const blurCodes = useSettingsStore(s => s.settings.blur_booking_codes)
|
||||
const fmtTime = (v) => {
|
||||
@@ -76,6 +78,8 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
return formatTime12(v, is12h)
|
||||
}
|
||||
const unit = isFahrenheit ? '°F' : '°C'
|
||||
const formatWindSpeed = (kmh: number) =>
|
||||
distanceUnit === 'imperial' ? `${Math.round(convertDistance(kmh, distanceUnit))} mph` : `${Math.round(kmh)} km/h`
|
||||
const collapsed = collapsedProp
|
||||
const toggleCollapse = () => onToggleCollapse?.()
|
||||
const {
|
||||
@@ -172,7 +176,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
<Chip icon={CloudRain} value={`${weather.precipitation_sum.toFixed(1)} mm`} />
|
||||
)}
|
||||
{weather.wind_max != null && (
|
||||
<Chip icon={Wind} value={isFahrenheit ? `${Math.round(weather.wind_max * 0.621371)} mph` : `${Math.round(weather.wind_max)} km/h`} />
|
||||
<Chip icon={Wind} value={formatWindSpeed(weather.wind_max)} />
|
||||
)}
|
||||
{weather.sunrise && <Chip icon={Sunrise} value={weather.sunrise} />}
|
||||
{weather.sunset && <Chip icon={Sunset} value={weather.sunset} />}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useToast } from '../shared/Toast'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import type { Place, Category, Day, Assignment, Reservation, TripFile, AssignmentsMap } from '../../types'
|
||||
import { splitReservationDateTime, formatTime } from '../../utils/formatters'
|
||||
import { formatDistance } from '../../utils/units'
|
||||
|
||||
const detailsCache = new Map()
|
||||
|
||||
@@ -122,6 +123,7 @@ export default function PlaceInspector({
|
||||
const { t, locale, language } = useTranslation()
|
||||
const toast = useToast()
|
||||
const timeFormat = useSettingsStore(s => s.settings.time_format) || '24h'
|
||||
const distanceUnit = useSettingsStore(s => s.settings.distance_unit) || 'metric'
|
||||
const [hoursExpanded, setHoursExpanded] = useState(false)
|
||||
const [filesExpanded, setFilesExpanded] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
@@ -274,7 +276,8 @@ export default function PlaceInspector({
|
||||
<PlaceExtras openingHours={openingHours} weekdayIndex={weekdayIndex} hoursExpanded={hoursExpanded}
|
||||
setHoursExpanded={setHoursExpanded} timeFormat={timeFormat} t={t} place={place} placeFiles={placeFiles}
|
||||
onFileUpload={onFileUpload} filesExpanded={filesExpanded} setFilesExpanded={setFilesExpanded}
|
||||
fileInputRef={fileInputRef} handleFileUpload={handleFileUpload} isUploading={isUploading} />
|
||||
fileInputRef={fileInputRef} handleFileUpload={handleFileUpload} isUploading={isUploading}
|
||||
distanceUnit={distanceUnit} />
|
||||
|
||||
</div>
|
||||
|
||||
@@ -682,7 +685,7 @@ function PlaceReservationParticipants({ selectedAssignmentId, reservations, assi
|
||||
}
|
||||
|
||||
function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpanded, timeFormat, t, place,
|
||||
placeFiles, onFileUpload, filesExpanded, setFilesExpanded, fileInputRef, handleFileUpload, isUploading }: any) {
|
||||
placeFiles, onFileUpload, filesExpanded, setFilesExpanded, fileInputRef, handleFileUpload, isUploading, distanceUnit }: any) {
|
||||
return (
|
||||
<div className={`grid grid-cols-1 ${openingHours?.length > 0 ? 'sm:grid-cols-2' : ''} gap-2`}>
|
||||
{openingHours && openingHours.length > 0 && (
|
||||
@@ -775,7 +778,7 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
<div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, fontWeight: 600 }}>
|
||||
<MapPin size={12} color="#3b82f6" />
|
||||
{distKm < 1 ? `${Math.round(totalDist)} m` : `${distKm.toFixed(1)} km`}
|
||||
{formatDistance(distKm, distanceUnit)}
|
||||
</div>
|
||||
{hasEle && (
|
||||
<>
|
||||
|
||||
@@ -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(<DisplaySettingsTab />);
|
||||
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(<DisplaySettingsTab />);
|
||||
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);
|
||||
|
||||
@@ -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<string>(settings.temperature_unit || 'celsius')
|
||||
const [distanceUnit, setDistanceUnit] = useState<DistanceUnit>(settings.distance_unit || 'metric')
|
||||
const [langOpen, setLangOpen] = useState(false)
|
||||
const langDropdownRef = useRef<HTMLDivElement | null>(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 (
|
||||
<Section title={t('settings.display')} icon={Palette}>
|
||||
{/* Display currency */}
|
||||
@@ -200,6 +206,37 @@ export default function DisplaySettingsTab(): React.ReactElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Distance */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-content-secondary">Distance</label>
|
||||
<div className="flex gap-3">
|
||||
{([
|
||||
{ value: 'metric', label: 'km Metric' },
|
||||
{ value: 'imperial', label: 'mi Imperial' },
|
||||
] as const).map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={async () => {
|
||||
setDistanceUnit(opt.value)
|
||||
try { await updateSetting('distance_unit', opt.value) }
|
||||
catch (e: unknown) { toast.error(e instanceof Error ? e.message : t('common.error')) }
|
||||
}}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 20px', borderRadius: 10, cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
|
||||
border: distanceUnit === opt.value ? '2px solid var(--text-primary)' : '2px solid var(--border-primary)',
|
||||
background: distanceUnit === opt.value ? 'var(--bg-hover)' : 'var(--bg-card)',
|
||||
color: 'var(--text-primary)',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time Format */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-content-secondary">{t('settings.timeFormat')}</label>
|
||||
|
||||
@@ -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(<DashboardPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(distanceValue('10 km')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders imperial atlas distance as miles', async () => {
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ distance_unit: 'imperial' }) });
|
||||
|
||||
render(<DashboardPage />);
|
||||
|
||||
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,
|
||||
|
||||
@@ -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 (
|
||||
<section className="atlas">
|
||||
@@ -401,7 +416,7 @@ function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElemen
|
||||
|
||||
<div className="atlas-card">
|
||||
<div className="label">{t('dashboard.atlas.distanceFlown')}</div>
|
||||
<div className="value mono">{distanceText} <span className="unit">{t('dashboard.atlas.kmUnit')}</span></div>
|
||||
<div className="value mono">{distanceText} <span className="unit">{distanceLabel}</span></div>
|
||||
<div className="delta">{t('dashboard.atlas.aroundEquator', { count: equatorTimes })}</div>
|
||||
<svg className="spark" width="80" height="36" viewBox="0 0 80 36">
|
||||
<circle cx="40" cy="18" r="14" fill="none" stroke="oklch(0.88 0.01 70)" strokeWidth="2" />
|
||||
|
||||
@@ -30,6 +30,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
default_currency: 'USD',
|
||||
language: localStorage.getItem('app_language') || 'en',
|
||||
temperature_unit: 'fahrenheit',
|
||||
distance_unit: 'metric',
|
||||
time_format: '12h',
|
||||
show_place_description: false,
|
||||
optimize_from_accommodation: true,
|
||||
|
||||
@@ -100,6 +100,8 @@ export interface TripFile {
|
||||
url: string
|
||||
}
|
||||
|
||||
export type DistanceUnit = 'metric' | 'imperial'
|
||||
|
||||
export interface Settings {
|
||||
map_tile_url: string
|
||||
default_lat: number
|
||||
@@ -109,6 +111,7 @@ export interface Settings {
|
||||
default_currency: string
|
||||
language: string
|
||||
temperature_unit: string
|
||||
distance_unit?: DistanceUnit
|
||||
time_format: string
|
||||
show_place_description: boolean
|
||||
blur_booking_codes?: boolean
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { DistanceUnit } from '../types'
|
||||
|
||||
const KM_TO_MI = 0.621371
|
||||
|
||||
export function getDistanceUnitLabel(unit: DistanceUnit): 'km' | 'mi' {
|
||||
return unit === 'imperial' ? 'mi' : 'km'
|
||||
}
|
||||
|
||||
export function convertDistance(km: number, unit: DistanceUnit): number {
|
||||
const safeKm = Number.isFinite(km) ? Math.max(0, km) : 0
|
||||
return unit === 'imperial' ? safeKm * KM_TO_MI : safeKm
|
||||
}
|
||||
|
||||
export function formatDistance(km: number, unit: DistanceUnit): string {
|
||||
const value = convertDistance(km, unit)
|
||||
const label = getDistanceUnitLabel(unit)
|
||||
const rounded = Math.round(value * 10) / 10
|
||||
const text = value > 0 && rounded === 0
|
||||
? '<0.1'
|
||||
: rounded.toLocaleString(undefined, { maximumFractionDigits: 1 })
|
||||
return `${text} ${label}`
|
||||
}
|
||||
Reference in New Issue
Block a user