diff --git a/web/src/components/SettingsMenu.tsx b/web/src/components/SettingsMenu.tsx index a855508c..603eb9d8 100644 --- a/web/src/components/SettingsMenu.tsx +++ b/web/src/components/SettingsMenu.tsx @@ -4,9 +4,10 @@ */ import React, { useState } from "react"; -import { Cog6ToothIcon, BellIcon, ClockIcon } from "@heroicons/react/24/outline"; +import { Cog6ToothIcon, BellIcon, ClockIcon, MapPinIcon } from "@heroicons/react/24/outline"; import { NotificationSettings } from "./settings/NotificationSettings"; import { TimeFormatSettings } from "./settings/TimeFormatSettings"; +import { DistanceSettings } from "./settings/DistanceSettings"; import { Button } from "@/components/ui/Button"; import { Dialog, @@ -41,6 +42,12 @@ const settingsSections: SettingsSection[] = [ icon: , component: TimeFormatSettings, }, + { + id: "distance", + label: "Distance Units", + icon: , + component: DistanceSettings, + }, ]; export const SettingsMenu: React.FC = () => { diff --git a/web/src/components/settings/DistanceSettings.tsx b/web/src/components/settings/DistanceSettings.tsx new file mode 100644 index 00000000..e6e8a248 --- /dev/null +++ b/web/src/components/settings/DistanceSettings.tsx @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024-2025, s0up and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import React, { useState } from "react"; +import { MapPinIcon, CheckIcon } from "@heroicons/react/24/outline"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/Button"; +import { + getDistanceSettings, + saveDistanceSettings, + formatDistance, + type DistanceSettings as DistanceSettingsType, + type DistanceUnit, +} from "@/utils/distanceSettings"; +import { showToast } from "@/components/common/Toast"; + +export const DistanceSettings: React.FC = () => { + const [settings, setSettings] = useState(getDistanceSettings); + const [hasChanges, setHasChanges] = useState(false); + + const updateUnit = (unit: DistanceUnit) => { + setSettings({ unit }); + setHasChanges(true); + }; + + const saveSettings = () => { + saveDistanceSettings(settings); + setHasChanges(false); + showToast("Distance settings saved", "success", { + description: "Changes will be applied across the application", + }); + }; + + const resetToDefaults = () => { + setSettings({ unit: "km" }); + setHasChanges(true); + }; + + return ( + + + + + + Distance Settings + + + Choose between metric and imperial units for distance display + + + + {hasChanges && ( + + + + Save Changes + + { + setSettings(getDistanceSettings()); + setHasChanges(false); + }} + variant="secondary" + > + Cancel + + + )} + + + + + + + + Distance Unit + + + + + updateUnit("km")} + className={`flex-1 p-4 rounded-lg border transition-colors text-left ${ + settings.unit === "km" + ? "bg-blue-500/10 border-blue-400/50" + : "bg-gray-200/50 dark:bg-gray-800/50 border-gray-300 dark:border-gray-800 hover:bg-gray-300/50 dark:hover:bg-gray-800" + }`} + > + Metric + Kilometers (km) + + updateUnit("mi")} + className={`flex-1 p-4 rounded-lg border transition-colors text-left ${ + settings.unit === "mi" + ? "bg-blue-500/10 border-blue-400/50" + : "bg-gray-200/50 dark:bg-gray-800/50 border-gray-300 dark:border-gray-800 hover:bg-gray-300/50 dark:hover:bg-gray-800" + }`} + > + Imperial + Miles (mi) + + + + + + + + + Preview + + + 100 km = {formatDistance(100, settings)} + + + + + + + + + + + + + + Quick Actions + + + Reset settings or apply common configurations + + + + + Reset to Defaults + + + + + + + ); +}; diff --git a/web/src/components/speedtest/ServerList.tsx b/web/src/components/speedtest/ServerList.tsx index eef95984..8357958f 100644 --- a/web/src/components/speedtest/ServerList.tsx +++ b/web/src/components/speedtest/ServerList.tsx @@ -18,6 +18,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; +import { formatDistance, useDistanceSettings } from "@/utils/distanceSettings"; import { Select, SelectContent, @@ -92,6 +93,7 @@ export const ServerList: React.FC = ({ const saved = localStorage.getItem("server-list-open"); return saved === null ? true : saved === "true"; }); + const { settings: distanceSettings } = useDistanceSettings(); // Persist server list open state to localStorage useEffect(() => { @@ -560,9 +562,7 @@ export const ServerList: React.FC = ({ {filteredServersWithSelect .slice(0, displayCount) - .map((server) => { - const distance = server.distance; - return ( + .map((server) => ( = ({ {server.country} -{" "} - {Math.floor(distance)} km + {formatDistance(server.distance, distanceSettings)} - ); - })} + ))} )} > diff --git a/web/src/components/speedtest/traceroute/components/ServerCard.tsx b/web/src/components/speedtest/traceroute/components/ServerCard.tsx index 87178074..292d88b9 100644 --- a/web/src/components/speedtest/traceroute/components/ServerCard.tsx +++ b/web/src/components/speedtest/traceroute/components/ServerCard.tsx @@ -11,6 +11,7 @@ import { getServerTypeLabel, getServerTypeColorClass, } from "../utils/serverUtils"; +import { formatDistance, useDistanceSettings } from "@/utils/distanceSettings"; interface ServerCardProps { server: Server; @@ -25,6 +26,8 @@ export const ServerCard: React.FC = ({ onSelect, disabled = false, }) => { + const { settings: distanceSettings } = useDistanceSettings(); + return ( = ({ {server.isIperf ? "Custom Server" - : `${server.country} - ${Math.floor(server.distance)} km`} + : `${server.country} - ${formatDistance(server.distance, distanceSettings)}`} {getServerTypeLabel(server)} diff --git a/web/src/utils/distanceSettings.ts b/web/src/utils/distanceSettings.ts new file mode 100644 index 00000000..bfe0e02e --- /dev/null +++ b/web/src/utils/distanceSettings.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024-2025, s0up and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import React from "react"; + +export type DistanceUnit = "km" | "mi"; + +export interface DistanceSettings { + unit: DistanceUnit; +} + +const DISTANCE_SETTINGS_KEY = "netronome-distance-settings"; +const DEFAULT_SETTINGS: DistanceSettings = { + unit: "km", +}; + +const KM_TO_MI = 0.621371; + +export const getDistanceSettings = (): DistanceSettings => { + try { + const saved = localStorage.getItem(DISTANCE_SETTINGS_KEY); + if (saved) { + const settings = JSON.parse(saved); + return { + unit: settings.unit === "mi" ? "mi" : "km", + }; + } + } catch (error) { + console.warn("Failed to load distance settings from localStorage:", error); + } + return DEFAULT_SETTINGS; +}; + +export const saveDistanceSettings = (settings: DistanceSettings): void => { + try { + localStorage.setItem(DISTANCE_SETTINGS_KEY, JSON.stringify(settings)); + window.dispatchEvent(new CustomEvent("distanceSettingsChanged", { detail: settings })); + } catch (error) { + console.warn("Failed to save distance settings to localStorage:", error); + } +}; + +export const formatDistance = (distanceKm: number, settings?: DistanceSettings): string => { + const currentSettings = settings || getDistanceSettings(); + if (currentSettings.unit === "mi") { + return `${Math.round(distanceKm * KM_TO_MI)} mi`; + } + return `${Math.round(distanceKm)} km`; +}; + +export const useDistanceSettings = (): { settings: DistanceSettings; updateSettings: (newSettings: DistanceSettings) => void } => { + const [settings, setSettings] = React.useState(getDistanceSettings); + + React.useEffect(() => { + const handleSettingsChange = (event: CustomEvent) => { + setSettings(event.detail); + }; + + window.addEventListener("distanceSettingsChanged", handleSettingsChange as EventListener); + + return () => { + window.removeEventListener("distanceSettingsChanged", handleSettingsChange as EventListener); + }; + }, []); + + const updateSettings = (newSettings: DistanceSettings) => { + saveDistanceSettings(newSettings); + setSettings(newSettings); + }; + + return { settings, updateSettings }; +};
+ Choose between metric and imperial units for distance display +
+ Reset settings or apply common configurations +