Browse Source

chore: fixes from deno linting/formatting

pull/477/head
Dan Ditomaso 1 year ago
parent
commit
50eb2a827f
  1. 36
      deno.json
  2. 14
      package.json
  3. 37
      src/App.tsx
  4. 14
      src/components/CommandPalette.tsx
  5. 21
      src/components/DeviceSelector.tsx
  6. 7
      src/components/DeviceSelectorButton.tsx
  7. 8
      src/components/Dialog/DialogManager.tsx
  8. 27
      src/components/Dialog/ImportDialog.tsx
  9. 13
      src/components/Dialog/LocationResponseDialog.tsx
  10. 30
      src/components/Dialog/NewDeviceDialog.tsx
  11. 287
      src/components/Dialog/NodeDetailsDialog.tsx
  12. 24
      src/components/Dialog/NodeOptionsDialog.tsx
  13. 10
      src/components/Dialog/PKIBackupDialog.tsx
  14. 2
      src/components/Dialog/PkiRegenerateDialog.tsx
  15. 12
      src/components/Dialog/QRDialog.tsx
  16. 2
      src/components/Dialog/RebootDialog.tsx
  17. 4
      src/components/Dialog/RemoveNodeDialog.tsx
  18. 2
      src/components/Dialog/ShutdownDialog.tsx
  19. 16
      src/components/Dialog/TracerouteResponseDialog.tsx
  20. 17
      src/components/Form/DynamicForm.tsx
  21. 14
      src/components/Form/DynamicFormField.tsx
  22. 20
      src/components/Form/FormInput.tsx
  23. 6
      src/components/Form/FormMultiSelect.tsx
  24. 18
      src/components/Form/FormPasswordGenerator.tsx
  25. 16
      src/components/Form/FormSelect.tsx
  26. 7
      src/components/KeyBackupReminder.tsx
  27. 71
      src/components/PageComponents/Channel.tsx
  28. 7
      src/components/PageComponents/Config/Bluetooth.tsx
  29. 2
      src/components/PageComponents/Config/Device.tsx
  30. 2
      src/components/PageComponents/Config/Display.tsx
  31. 2
      src/components/PageComponents/Config/LoRa.tsx
  32. 2
      src/components/PageComponents/Config/Network.tsx
  33. 2
      src/components/PageComponents/Config/Position.tsx
  34. 2
      src/components/PageComponents/Config/Power.tsx
  35. 11
      src/components/PageComponents/Config/Security/Security.tsx
  36. 2
      src/components/PageComponents/Config/Security/securityReducer.tsx
  37. 6
      src/components/PageComponents/Config/Security/types.ts
  38. 4
      src/components/PageComponents/Connect/BLE.tsx
  39. 23
      src/components/PageComponents/Connect/HTTP.tsx
  40. 4
      src/components/PageComponents/Connect/Serial.tsx
  41. 54
      src/components/PageComponents/Map/NodeDetail.tsx
  42. 11
      src/components/PageComponents/Messages/ChannelChat.tsx
  43. 28
      src/components/PageComponents/Messages/Message.tsx
  44. 22
      src/components/PageComponents/Messages/MessageInput.tsx
  45. 39
      src/components/PageComponents/Messages/TraceRoute.tsx
  46. 2
      src/components/PageComponents/ModuleConfig/AmbientLighting.tsx
  47. 2
      src/components/PageComponents/ModuleConfig/Audio.tsx
  48. 17
      src/components/PageComponents/ModuleConfig/CannedMessage.tsx
  49. 2
      src/components/PageComponents/ModuleConfig/DetectionSensor.tsx
  50. 2
      src/components/PageComponents/ModuleConfig/ExternalNotification.tsx
  51. 53
      src/components/PageComponents/ModuleConfig/MQTT.tsx
  52. 2
      src/components/PageComponents/ModuleConfig/NeighborInfo.tsx
  53. 2
      src/components/PageComponents/ModuleConfig/Paxcounter.tsx
  54. 2
      src/components/PageComponents/ModuleConfig/RangeTest.tsx
  55. 2
      src/components/PageComponents/ModuleConfig/Serial.tsx
  56. 2
      src/components/PageComponents/ModuleConfig/StoreForward.tsx
  57. 2
      src/components/PageComponents/ModuleConfig/Telemetry.tsx
  58. 8
      src/components/PageLayout.tsx
  59. 128
      src/components/Sidebar.tsx
  60. 12
      src/components/ThemeSwitcher.tsx
  61. 4
      src/components/Toaster.tsx
  62. 2
      src/components/UI/Avatar.tsx
  63. 8
      src/components/UI/Button.tsx
  64. 6
      src/components/UI/Command.tsx
  65. 8
      src/components/UI/Dialog.tsx
  66. 12
      src/components/UI/DropdownMenu.tsx
  67. 9
      src/components/UI/Footer.tsx
  68. 1
      src/components/UI/Generator.tsx
  69. 5
      src/components/UI/Input.tsx
  70. 16
      src/components/UI/Menubar.tsx
  71. 25
      src/components/UI/Modal.tsx
  72. 7
      src/components/UI/MultiSelect.tsx
  73. 2
      src/components/UI/Popover.tsx
  74. 8
      src/components/UI/Select.tsx
  75. 5
      src/components/UI/Sidebar/sidebarButton.tsx
  76. 2
      src/components/UI/Spinner.tsx
  77. 2
      src/components/UI/Tabs.tsx
  78. 20
      src/components/UI/Toast.tsx
  79. 4
      src/components/UI/Tooltip.tsx
  80. 2
      src/components/UI/Typography/Blockquote.tsx
  81. 2
      src/components/UI/Typography/Code.tsx
  82. 2
      src/components/UI/Typography/H1.tsx
  83. 2
      src/components/UI/Typography/H2.tsx
  84. 2
      src/components/UI/Typography/H3.tsx
  85. 2
      src/components/UI/Typography/H4.tsx
  86. 2
      src/components/UI/Typography/H5.tsx
  87. 6
      src/components/UI/Typography/Link.tsx
  88. 2
      src/components/UI/Typography/P.tsx
  89. 2
      src/components/UI/Typography/Subtle.tsx
  90. 2
      src/components/generic/Blur.tsx
  91. 2
      src/components/generic/Mono.tsx
  92. 14
      src/components/generic/Table/index.tsx
  93. 67
      src/components/generic/ThemeProvider.tsx
  94. 3
      src/components/generic/TimeAgo.tsx
  95. 4
      src/components/generic/Uptime.tsx
  96. 4
      src/core/hooks/useBrowserFeatureDetection.ts
  97. 13
      src/core/hooks/useKeyBackupReminder.tsx
  98. 6
      src/core/hooks/useTheme.ts
  99. 34
      src/core/hooks/useToast.ts
  100. 4
      src/core/stores/appStore.ts

36
deno.json

@ -0,0 +1,36 @@
{
"imports": {
"@meshtastic/core": "jsr:@meshtastic/core@^2.6.0",
"@meshtastic/js": "jsr:@meshtastic/js@^2.3.4",
"@meshtastic/transport-http": "jsr:@meshtastic/transport-http@^0.2.1",
"@meshtastic/transport-web-serial": "jsr:@meshtastic/transport-web-serial@^0.2.1",
"@app/": "./src/",
"@pages/": "./src/pages/",
"@components/": "./src/components/",
"@core/": "./src/core/",
"@layouts/": "./src/layouts/"
},
"compilerOptions": {
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"strictNullChecks": true,
"types": [
"vite/client",
"node",
"@types/web-bluetooth",
"@types/w3c-web-serial"
],
"strictPropertyInitialization": false
},
"unstable": [
"sloppy-imports"
]
}

14
package.json

@ -8,8 +8,8 @@
"build": "deno run -A npm:vite build",
"build:analyze": "BUNDLE_ANALYZE=true deno task build",
"lint": "deno lint src/",
"lint:fix": "deno lint --write src/",
"format": "deno fmt --write src/",
"lint:fix": "deno lint --fix src/",
"format": "deno fmt src/",
"dev": "deno task dev:ui",
"dev:ui": "deno run -A npm:vite dev",
"dev:scan": "VITE_DEBUG_SCAN=true deno task dev:ui",
@ -18,7 +18,6 @@
"preview": "deno run --allow-net npm:vite preview",
"package": "deno run -A npm:gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && deno run -A --allow-run tar -cvf dist/build.tar -C ./dist/output/ ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/meshtastic/web.git"
@ -26,6 +25,15 @@
"bugs": {
"url": "https://github.com/meshtastic/web/issues"
},
"simple-git-hooks": {
"pre-commit": "deno task lint:fix && deno task format"
},
"lint-staged": {
"*.{ts,tsx}": [
"deno task lint:fix",
"deno task format"
]
},
"homepage": "https://meshtastic.org",
"dependencies": {
"@bufbuild/protobuf": "^2.2.3",

37
src/App.tsx

@ -1,20 +1,19 @@
import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
import { PageRouter } from "@app/PageRouter.tsx";
import { ThemeProvider } from "@app/components/generic/ThemeProvider";
import { CommandPalette } from "@components/CommandPalette.tsx";
import { DeviceSelector } from "@components/DeviceSelector.tsx";
import { DialogManager } from "@components/Dialog/DialogManager";
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
import { KeyBackupReminder } from "@components/KeyBackupReminder";
import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx";
import { Toaster } from "@components/Toaster.tsx";
import Footer from "@components/UI/Footer.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { Dashboard } from "@pages/Dashboard/index.tsx";
import type { JSX } from "react";
import { MapProvider } from "react-map-gl/maplibre";
export const App = (): JSX.Element => {
export const App = () => {
const { getDevice } = useDeviceStore();
const { selectedDevice, setConnectDialogOpen, connectDialogOpen } =
useAppStore();
@ -36,19 +35,21 @@ export const App = (): JSX.Element => {
<div className="flex grow">
<DeviceSelector />
<div className="flex grow flex-col">
{device ? (
<div className="flex h-screen">
<DialogManager />
<KeyBackupReminder />
<CommandPalette />
<PageRouter />
</div>
) : (
<>
<Dashboard />
<Footer />
</>
)}
{device
? (
<div className="flex h-screen">
<DialogManager />
<KeyBackupReminder />
<CommandPalette />
<PageRouter />
</div>
)
: (
<>
<Dashboard />
<Footer />
</>
)}
</div>
</div>
</div>

14
src/components/CommandPalette.tsx

@ -1,4 +1,4 @@
import { Avatar } from "@components/UI/Avatar";
import { Avatar } from "./UI/Avatar.tsx";
import {
CommandDialog,
CommandEmpty,
@ -117,13 +117,11 @@ export const CommandPalette = () => {
return {
label:
device.nodes.get(device.hardware.myNodeNum)?.user?.longName ??
device.hardware.myNodeNum.toString(),
device.hardware.myNodeNum.toString(),
icon: (
<Avatar
text={
device.nodes.get(device.hardware.myNodeNum)?.user
?.shortName ?? device.hardware.myNodeNum.toString()
}
text={device.nodes.get(device.hardware.myNodeNum)?.user
?.shortName ?? device.hardware.myNodeNum.toString()}
/>
),
action() {
@ -241,8 +239,8 @@ export const CommandPalette = () => {
}
};
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
globalThis.addEventListener("keydown", handleKeydown);
return () => globalThis.removeEventListener("keydown", handleKeydown);
}, [setCommandPaletteOpen]);
return (

21
src/components/DeviceSelector.tsx

@ -5,10 +5,11 @@ import { Code } from "@components/UI/Typography/Code.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { HomeIcon, PlusIcon, SearchIcon } from "lucide-react";
import type { JSX } from "react";
import { Avatar } from "./UI/Avatar";
export const DeviceSelector = (): JSX.Element => {
import { Avatar } from "./UI/Avatar.tsx";
import process from "node:process";
export const DeviceSelector = () => {
const { getDevices } = useDeviceStore();
const {
selectedDevice,
@ -38,11 +39,9 @@ export const DeviceSelector = (): JSX.Element => {
active={selectedDevice === device.id}
>
<Avatar
text={
device.nodes
.get(device.hardware.myNodeNum)
?.user?.shortName.toString() ?? "UNK"
}
text={device.nodes
.get(device.hardware.myNodeNum)
?.user?.shortName.toString() ?? "UNK"}
/>
</DeviceSelectorButton>
))}
@ -66,9 +65,11 @@ export const DeviceSelector = (): JSX.Element => {
<SearchIcon />
</button>
{/* TODO: This is being commented out until its fixed */}
{/* <button type="button" className="transition-all hover:text-accent">
{
/* <button type="button" className="transition-all hover:text-accent">
<LanguagesIcon />
</button> */}
</button> */
}
<Separator />
<Code>{process.env.COMMIT_HASH}</Code>
</div>

7
src/components/DeviceSelectorButton.tsx

@ -5,7 +5,6 @@ export interface DeviceSelectorButtonProps {
}
export const DeviceSelectorButton = ({
active,
onClick,
children,
}: DeviceSelectorButtonProps) => (
@ -14,9 +13,11 @@ export const DeviceSelectorButton = ({
onClick={onClick}
onKeyDown={onClick}
>
{/* {active && (
{
/* {active && (
<div className="absolute -left-2 h-10 w-1.5 rounded-full bg-accent" />
)} */}
)} */
}
<div className="flex aspect-square cursor-pointer flex-col items-center justify-center">
{children}
</div>

8
src/components/Dialog/DialogManager.tsx

@ -1,15 +1,15 @@
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.tsx";
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog";
import { PkiBackupDialog } from "./PKIBackupDialog.tsx";
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { JSX } from "react";
import { NodeDetailsDialog } from "./NodeDetailsDialog";
export const DialogManager = (): JSX.Element => {
import { NodeDetailsDialog } from "./NodeDetailsDialog.tsx";
export const DialogManager = () => {
const { channels, config, dialog, setDialogOpen } = useDevice();
return (
<>

27
src/components/Dialog/ImportDialog.tsx

@ -15,7 +15,7 @@ import { Switch } from "@components/UI/Switch.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { toByteArray } from "base64-js";
import { type JSX, useEffect, useState } from "react";
import { useEffect, useState } from "react";
export interface ImportDialogProps {
open: boolean;
@ -26,7 +26,7 @@ export interface ImportDialogProps {
export const ImportDialog = ({
open,
onOpenChange,
}: ImportDialogProps): JSX.Element => {
}: ImportDialogProps) => {
const [importDialogInput, setImportDialogInput] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
const [validUrl, setValidUrl] = useState<boolean>(false);
@ -62,7 +62,7 @@ export const ImportDialog = ({
),
);
setValidUrl(true);
} catch (error) {
} catch (_error) {
setValidUrl(false);
setChannelSet(undefined);
}
@ -73,10 +73,9 @@ export const ImportDialog = ({
connection?.setChannel(
create(Protobuf.Channel.ChannelSchema, {
index,
role:
index === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
role: index === 0
? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY,
settings: ch,
}),
);
@ -119,25 +118,29 @@ export const ImportDialog = ({
<div className="w-36">
<Label>Use Preset?</Label>
<Switch
disabled={true}
disabled
checked={channelSet?.loraConfig?.usePreset ?? true}
/>
</div>
{/* <Select
{
/* <Select
label="Modem Preset"
disabled
value={channelSet?.loraConfig?.modemPreset}
>
{renderOptions(Protobuf.Config_LoRaConfig_ModemPreset)}
</Select> */}
</Select> */
}
</div>
{/* <Select
{
/* <Select
label="Region"
disabled
value={channelSet?.loraConfig?.region}
>
{renderOptions(Protobuf.Config_LoRaConfig_RegionCode)}
</Select> */}
</Select> */
}
<span className="text-md block font-medium text-text-primary">
Channels:

13
src/components/Dialog/LocationResponseDialog.tsx

@ -1,14 +1,13 @@
import { useDevice } from "@app/core/stores/deviceStore";
import { useDevice } from "../../core/stores/deviceStore.ts";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
} from "../UI/Dialog.tsx";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
export interface LocationResponseDialogProps {
location: Types.PacketMetadata<Protobuf.Mesh.location> | undefined;
@ -20,15 +19,13 @@ export const LocationResponseDialog = ({
location,
open,
onOpenChange,
}: LocationResponseDialogProps): JSX.Element => {
}: LocationResponseDialogProps) => {
const { nodes } = useDevice();
const from = nodes.get(location?.from ?? 0);
const longName =
from?.user?.longName ??
const longName = from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
const shortName =
from?.user?.shortName ??
const shortName = from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
return (

30
src/components/Dialog/NewDeviceDialog.tsx

@ -1,7 +1,7 @@
import {
type BrowserFeature,
useBrowserFeatureDetection,
} from "@app/core/hooks/useBrowserFeatureDetection";
} from "../../core/hooks/useBrowserFeatureDetection.ts";
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
@ -18,9 +18,9 @@ import {
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { AlertCircle, InfoIcon } from "lucide-react";
import { Fragment, type JSX } from "react/jsx-runtime";
import { Link } from "../UI/Typography/Link";
import { AlertCircle } from "lucide-react";
import { Link } from "../UI/Typography/Link.tsx";
import { Fragment } from "react/jsx-runtime";
export interface TabElementProps {
closeDialog: () => void;
@ -85,14 +85,16 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
<p className="text-sm">
{browserFeatures.length > 0 && (
<>
This application requires {formatFeatureList(browserFeatures)}.
Please use a Chromium-based browser like Chrome or Edge.
This application requires{" "}
{formatFeatureList(browserFeatures)}. Please use a
Chromium-based browser like Chrome or Edge.
</>
)}
{needsSecureContext && (
<>
{browserFeatures.length > 0 && " Additionally, it"}
{browserFeatures.length === 0 && "This application"} requires a{" "}
{browserFeatures.length === 0 && "This application"} requires a
{" "}
<Link href={links["Secure Context"]}>secure context</Link>.
Please connect using HTTPS or localhost.
</>
@ -107,7 +109,7 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
export const NewDeviceDialog = ({
open,
onOpenChange,
}: NewDeviceProps): JSX.Element => {
}: NewDeviceProps) => {
const { unsupported } = useBrowserFeatureDetection();
const tabs: TabManifest[] = [
@ -119,15 +121,13 @@ export const NewDeviceDialog = ({
{
label: "Bluetooth",
element: BLE,
isDisabled:
unsupported.includes("Web Bluetooth") ||
isDisabled: unsupported.includes("Web Bluetooth") ||
unsupported.includes("Secure Context"),
},
{
label: "Serial",
element: Serial,
isDisabled:
unsupported.includes("Web Serial") ||
isDisabled: unsupported.includes("Web Serial") ||
unsupported.includes("Secure Context"),
},
];
@ -149,9 +149,9 @@ export const NewDeviceDialog = ({
{tabs.map((tab) => (
<TabsContent key={tab.label} value={tab.label}>
<fieldset disabled={tab.isDisabled}>
{tab.isDisabled ? (
<ErrorMessage missingFeatures={unsupported} />
) : null}
{tab.isDisabled
? <ErrorMessage missingFeatures={unsupported} />
: null}
<tab.element closeDialog={() => onOpenChange(false)} />
</fieldset>
</TabsContent>

287
src/components/Dialog/NodeDetailsDialog.tsx

@ -1,23 +1,23 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore";
import { useAppStore } from "../../core/stores/appStore.ts";
import { useDevice } from "../../core/stores/deviceStore.ts";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@components/UI/Accordion";
} from "../UI/Accordion.tsx";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
} from "../UI/Dialog.tsx";
import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { DeviceImage } from "../generic/DeviceImage";
import { TimeAgo } from "../generic/TimeAgo";
import { Uptime } from "../generic/Uptime";
import { DeviceImage } from "../generic/DeviceImage.tsx";
import { TimeAgo } from "../generic/TimeAgo.tsx";
import { Uptime } from "../generic/Uptime.tsx";
export interface NodeDetailsDialogProps {
open: boolean;
@ -32,134 +32,159 @@ export const NodeDetailsDialog = ({
const { nodeNumDetails } = useAppStore();
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
return device ? (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Node Details for {device.user?.longName ?? "UNKNOWN"} (
{device.user?.shortName ?? "UNK"})
</DialogTitle>
</DialogHeader>
<DialogFooter>
<div className="w-full">
<DeviceImage
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
deviceType={
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
}
/>
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Details:
</p>
<p>
Hardware:{" "}
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
</p>
<p>Node Number: {device.num}</p>
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
<p>
Role:{" "}
{
Protobuf.Config.Config_DeviceConfig_Role[
device.user?.role ?? 0
]
}
</p>
<p>
Last Heard:{" "}
{device.lastHeard === 0 ? (
"Never"
) : (
<TimeAgo timestamp={device.lastHeard * 1000} />
)}
</p>
</div>
{device.position ? (
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
return device
? (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Node Details for {device.user?.longName ?? "UNKNOWN"} (
{device.user?.shortName ?? "UNK"})
</DialogTitle>
</DialogHeader>
<DialogFooter>
<div className="w-full">
<DeviceImage
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
deviceType={Protobuf.Mesh
.HardwareModel[device.user?.hwModel ?? 0]}
/>
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Position:
Details:
</p>
{device.position.latitudeI && device.position.longitudeI ? (
<p>
Coordinates:{" "}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
device.position.latitudeI / 1e7
}&mlon=${device.position.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{device.position.latitudeI / 1e7},{" "}
{device.position.longitudeI / 1e7}
</a>
</p>
) : null}
{device.position.altitude ? (
<p>Altitude: {device.position.altitude}m</p>
) : null}
</div>
) : null}
{device.deviceMetrics ? (
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Device Metrics:
<p>
Hardware:{" "}
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
</p>
<p>Node Number: {device.num}</p>
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
<p>
Role: {Protobuf.Config.Config_DeviceConfig_Role[
device.user?.role ?? 0
]}
</p>
<p>
Last Heard: {device.lastHeard === 0
? (
"Never"
)
: <TimeAgo timestamp={device.lastHeard * 1000} />}
</p>
{device.deviceMetrics.airUtilTx ? (
<p>
Air TX utilization:{" "}
{device.deviceMetrics.airUtilTx.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.channelUtilization ? (
<p>
Channel utilization:{" "}
{device.deviceMetrics.channelUtilization.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.batteryLevel ? (
<p>
Battery level:{" "}
{device.deviceMetrics.batteryLevel.toFixed(2)}%
</p>
) : null}
{device.deviceMetrics.voltage ? (
<p>Voltage: {device.deviceMetrics.voltage.toFixed(2)}V</p>
) : null}
{device.deviceMetrics.uptimeSeconds ? (
<p>
Uptime:{" "}
<Uptime seconds={device.deviceMetrics.uptimeSeconds} />
</p>
) : null}
</div>
) : null}
{device ? (
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<Accordion className="AccordionRoot" type="single" collapsible>
<AccordionItem className="AccordionItem" value="item-1">
<AccordionTrigger>
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
All Raw Metrics:
</p>
</AccordionTrigger>
<AccordionContent className="overflow-x-scroll">
<pre className="text-xs w-full">
{device.position
? (
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Position:
</p>
{device.position.latitudeI && device.position.longitudeI
? (
<p>
Coordinates:{" "}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
device.position.latitudeI / 1e7
}&mlon=${
device.position.longitudeI / 1e7
}&layers=N`}
target="_blank"
rel="noreferrer"
>
{device.position.latitudeI / 1e7},{" "}
{device.position.longitudeI / 1e7}
</a>
</p>
)
: null}
{device.position.altitude
? <p>Altitude: {device.position.altitude}m</p>
: null}
</div>
)
: null}
{device.deviceMetrics
? (
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
Device Metrics:
</p>
{device.deviceMetrics.airUtilTx
? (
<p>
Air TX utilization:{" "}
{device.deviceMetrics.airUtilTx.toFixed(2)}%
</p>
)
: null}
{device.deviceMetrics.channelUtilization
? (
<p>
Channel utilization:{" "}
{device.deviceMetrics.channelUtilization.toFixed(2)}%
</p>
)
: null}
{device.deviceMetrics.batteryLevel
? (
<p>
Battery level:{" "}
{device.deviceMetrics.batteryLevel.toFixed(2)}%
</p>
)
: null}
{device.deviceMetrics.voltage
? (
<p>
Voltage: {device.deviceMetrics.voltage.toFixed(2)}V
</p>
)
: null}
{device.deviceMetrics.uptimeSeconds
? (
<p>
Uptime:{" "}
<Uptime
seconds={device.deviceMetrics.uptimeSeconds}
/>
</p>
)
: null}
</div>
)
: null}
{device
? (
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<Accordion
className="AccordionRoot"
type="single"
collapsible
>
<AccordionItem className="AccordionItem" value="item-1">
<AccordionTrigger>
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
All Raw Metrics:
</p>
</AccordionTrigger>
<AccordionContent className="overflow-x-scroll">
<pre className="text-xs w-full">
{JSON.stringify(device, null, 2)}
</pre>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
) : null}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
) : null;
</pre>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
)
: null}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
: null;
};

24
src/components/Dialog/NodeOptionsDialog.tsx

@ -1,17 +1,17 @@
import { toast } from "@app/core/hooks/useToast";
import { useAppStore } from "@app/core/stores/appStore";
import { useDevice } from "@app/core/stores/deviceStore";
import { toast } from "../../core/hooks/useToast.ts";
import { useAppStore } from "../../core/stores/appStore.ts";
import { useDevice } from "../../core/stores/deviceStore.ts";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
} from "../UI/Dialog.tsx";
import type { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { TrashIcon } from "lucide-react";
import type { JSX } from "react";
import { Button } from "../UI/Button";
import { Button } from "../UI/Button.tsx";
export interface NodeOptionsDialogProps {
node: Protobuf.Mesh.NodeInfo | undefined;
@ -23,7 +23,7 @@ export const NodeOptionsDialog = ({
node,
open,
onOpenChange,
}: NodeOptionsDialogProps): JSX.Element => {
}: NodeOptionsDialogProps) => {
const { setDialogOpen, connection, setActivePage } = useDevice();
const {
setNodeNumToBeRemoved,
@ -31,11 +31,9 @@ export const NodeOptionsDialog = ({
setChatType,
setActiveChat,
} = useAppStore();
const longName =
node?.user?.longName ??
const longName = node?.user?.longName ??
(node ? `!${numberToHexUnpadded(node?.num)}` : "Unknown");
const shortName =
node?.user?.shortName ??
const shortName = node?.user?.shortName ??
(node ? `${numberToHexUnpadded(node?.num).substring(0, 4)}` : "UNK");
function handleDirectMessage() {
@ -53,7 +51,7 @@ export const NodeOptionsDialog = ({
connection?.requestPosition(node.num).then(() =>
toast({
title: "Position request sent.",
}),
})
);
onOpenChange();
}
@ -66,7 +64,7 @@ export const NodeOptionsDialog = ({
connection?.traceRoute(node.num).then(() =>
toast({
title: "Traceroute sent.",
}),
})
);
onOpenChange();
}

10
src/components/Dialog/PKIBackupDialog.tsx

@ -1,5 +1,5 @@
import { useDevice } from "@app/core/stores/deviceStore";
import { Button } from "@components/UI/Button";
import { useDevice } from "../../core/stores/deviceStore.ts";
import { Button } from "../UI/Button.tsx";
import {
Dialog,
DialogContent,
@ -40,7 +40,7 @@ export const PkiBackupDialog = ({
const renderPrintWindow = React.useCallback(() => {
if (!privateKey || !publicKey) return;
const printWindow = window.open("", "_blank");
const printWindow = globalThis.open("", "_blank");
if (printWindow) {
printWindow.document.write(`
<html>
@ -116,14 +116,14 @@ export const PkiBackupDialog = ({
</DialogHeader>
<DialogFooter className="mt-6">
<Button
variant={"default"}
variant="default"
onClick={() => createDownloadKeyFile()}
className=""
>
<DownloadIcon size={20} className="mr-2" />
Download
</Button>
<Button variant={"default"} onClick={() => renderPrintWindow()}>
<Button variant="default" onClick={() => renderPrintWindow()}>
<PrinterIcon size={20} className="mr-2" />
Print
</Button>

2
src/components/Dialog/PkiRegenerateDialog.tsx

@ -18,7 +18,7 @@ export const PkiRegenerateDialog = ({
open,
onOpenChange,
onSubmit,
}: PkiRegenerateDialogProps): JSX.Element => {
}: PkiRegenerateDialogProps) => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>

12
src/components/Dialog/QRDialog.tsx

@ -28,7 +28,7 @@ export const QRDialog = ({
onOpenChange,
loraConfig,
channels,
}: QRDialogProps): JSX.Element => {
}: QRDialogProps) => {
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
@ -77,8 +77,8 @@ export const QRDialog = ({
{channel.settings?.name.length
? channel.settings.name
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
? "Primary"
: `Channel: ${channel.index}`}
? "Primary"
: `Channel: ${channel.index}`}
</Label>
<Checkbox
key={channel.index}
@ -86,7 +86,9 @@ export const QRDialog = ({
onCheckedChange={() => {
if (selectedChannels.includes(channel.index)) {
setSelectedChannels(
selectedChannels.filter((c) => c !== channel.index),
selectedChannels.filter((c) =>
c !== channel.index
),
);
} else {
setSelectedChannels([
@ -130,7 +132,7 @@ export const QRDialog = ({
<Label>Sharable URL</Label>
<Input
value={qrCodeUrl}
disabled={true}
disabled
className="dark:text-slate-900"
action={{
icon: ClipboardIcon,

2
src/components/Dialog/RebootDialog.tsx

@ -19,7 +19,7 @@ export interface RebootDialogProps {
export const RebootDialog = ({
open,
onOpenChange,
}: RebootDialogProps): JSX.Element => {
}: RebootDialogProps) => {
const { connection } = useDevice();
const [time, setTime] = useState<number>(5);

4
src/components/Dialog/RemoveNodeDialog.tsx

@ -1,4 +1,4 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useAppStore } from "../../core/stores/appStore.ts";
import { useDevice } from "@app/core/stores/deviceStore.ts";
import { Button } from "@components/UI/Button.tsx";
import {
@ -19,7 +19,7 @@ export interface RemoveNodeDialogProps {
export const RemoveNodeDialog = ({
open,
onOpenChange,
}: RemoveNodeDialogProps): JSX.Element => {
}: RemoveNodeDialogProps) => {
const { connection, nodes, removeNode } = useDevice();
const { nodeNumToBeRemoved } = useAppStore();

2
src/components/Dialog/ShutdownDialog.tsx

@ -19,7 +19,7 @@ export interface ShutdownDialogProps {
export const ShutdownDialog = ({
open,
onOpenChange,
}: ShutdownDialogProps): JSX.Element => {
}: ShutdownDialogProps) => {
const { connection } = useDevice();
const [time, setTime] = useState<number>(5);

16
src/components/Dialog/TracerouteResponseDialog.tsx

@ -1,15 +1,15 @@
import { useDevice } from "@app/core/stores/deviceStore";
import { useDevice } from "../../core/stores/deviceStore.ts";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog";
} from "../UI/Dialog.tsx";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
export interface TracerouteResponseDialogProps {
traceroute: Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery> | undefined;
@ -21,18 +21,16 @@ export const TracerouteResponseDialog = ({
traceroute,
open,
onOpenChange,
}: TracerouteResponseDialogProps): JSX.Element => {
}: TracerouteResponseDialogProps) => {
const { nodes } = useDevice();
const route: number[] = traceroute?.data.route ?? [];
const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = traceroute?.data.snrTowards ?? [];
const snrBack = traceroute?.data.snrBack ?? [];
const from = nodes.get(traceroute?.from ?? 0);
const longName =
from?.user?.longName ??
const longName = from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : "Unknown");
const shortName =
from?.user?.shortName ??
const shortName = from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : "UNK");
const to = nodes.get(traceroute?.to ?? 0);
return (

17
src/components/Form/DynamicForm.tsx

@ -74,10 +74,11 @@ export function DynamicForm<T extends FieldValues>({
const value = getValues(field.fieldName);
if (value === "always") return true;
if (typeof value === "boolean") return field.invert ? value : !value;
if (typeof value === "number")
if (typeof value === "number") {
return field.invert
? field.selector !== value
: field.selector === value;
}
return false;
});
};
@ -85,11 +86,9 @@ export function DynamicForm<T extends FieldValues>({
return (
<form
className="space-y-8"
{...(submitType === "onSubmit"
? { onSubmit: handleSubmit(onSubmit) }
: {
onChange: handleSubmit(onSubmit),
})}
{...(submitType === "onSubmit" ? { onSubmit: handleSubmit(onSubmit) } : {
onChange: handleSubmit(onSubmit),
})}
>
{fieldGroups.map((fieldGroup) => (
<div key={fieldGroup.label} className="space-y-8 sm:space-y-5">
@ -105,10 +104,8 @@ export function DynamicForm<T extends FieldValues>({
label={field.label}
fieldName={field.name}
description={field.description}
valid={
field.validationText === undefined ||
field.validationText === ""
}
valid={field.validationText === undefined ||
field.validationText === ""}
validationText={field.validationText}
>
<DynamicFormField

14
src/components/Form/DynamicFormField.tsx

@ -1,7 +1,7 @@
import {
type MultiSelectFieldProps,
MultiSelectInput,
} from "@app/components/Form/FormMultiSelect";
} from "./FormMultiSelect.tsx";
import {
GenericInput,
type InputFieldProps,
@ -48,11 +48,19 @@ export function DynamicFormField<T extends FieldValues>({
case "toggle":
return (
<ToggleInput field={field} control={control} disabled={disabled} />
<ToggleInput
field={field}
control={control}
disabled={disabled}
/>
);
case "select":
return (
<SelectInput field={field} control={control} disabled={disabled} />
<SelectInput
field={field}
control={control}
disabled={disabled}
/>
);
case "passwordGenerator":
return (

20
src/components/Form/FormInput.tsx

@ -40,17 +40,15 @@ export function GenericInput<T extends FieldValues>({
control={control}
render={({ field: { value, onChange, ...rest } }) => (
<Input
type={
field.type === "password" && passwordShown ? "text" : field.type
}
action={
field.type === "password"
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined
}
type={field.type === "password" && passwordShown
? "text"
: field.type}
action={field.type === "password"
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined}
step={field.properties?.step}
value={field.type === "number" ? Number.parseFloat(value) : value}
id={field.name}

6
src/components/Form/FormMultiSelect.tsx

@ -3,7 +3,7 @@ import type {
GenericFormElementProps,
} from "@components/Form/DynamicForm.tsx";
import type { FieldValues } from "react-hook-form";
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect";
import { MultiSelect, MultiSelectItem } from "../UI/MultiSelect.tsx";
export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "multiSelect";
@ -28,8 +28,8 @@ export function MultiSelectInput<T extends FieldValues>({
// Make sure to filter out the UNSET value, as it shouldn't be shown in the UI
const optionsEnumValues = enumValue
? Object.entries(enumValue)
.filter((value) => typeof value[1] === "number")
.filter((value) => value[0] !== "UNSET")
.filter((value) => typeof value[1] === "number")
.filter((value) => value[0] !== "UNSET")
: [];
const formatName = (name: string) => {

18
src/components/Form/FormPasswordGenerator.tsx

@ -2,10 +2,10 @@ import type {
BaseFormBuilderProps,
GenericFormElementProps,
} from "@components/Form/DynamicForm.tsx";
import type { ButtonVariant } from "@components/UI/Button";
import type { ButtonVariant } from "../UI/Button.tsx";
import { Generator } from "@components/UI/Generator.tsx";
import { Eye, EyeOff } from "lucide-react";
import type { ChangeEventHandler, MouseEventHandler } from "react";
import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { Controller, type FieldValues } from "react-hook-form";
@ -43,14 +43,12 @@ export function PasswordGenerator<T extends FieldValues>({
<Generator
type={field.hide && !passwordShown ? "password" : "text"}
id={field.id}
action={
field.hide
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined
}
action={field.hide
? {
icon: passwordShown ? EyeOff : Eye,
onClick: togglePasswordVisiblity,
}
: undefined}
devicePSKBitCount={field.devicePSKBitCount}
bits={field.bits}
inputChange={field.inputChange}

16
src/components/Form/FormSelect.tsx

@ -36,8 +36,8 @@ export function SelectInput<T extends FieldValues>({
field.properties;
const optionsEnumValues = enumValue
? Object.entries(enumValue).filter(
(value) => typeof value[1] === "number",
)
(value) => typeof value[1] === "number",
)
: [];
return (
<Select
@ -58,11 +58,13 @@ export function SelectInput<T extends FieldValues>({
<SelectItem key={name} value={value.toString()}>
{formatEnumName
? name
.replace(/_/g, " ")
.toLowerCase()
.split(" ")
.map((s) => s.charAt(0).toUpperCase() + s.substring(1))
.join(" ")
.replace(/_/g, " ")
.toLowerCase()
.split(" ")
.map((s) =>
s.charAt(0).toUpperCase() + s.substring(1)
)
.join(" ")
: name}
</SelectItem>
))}

7
src/components/KeyBackupReminder.tsx

@ -1,7 +1,7 @@
import { useBackupReminder } from "@app/core/hooks/useKeyBackupReminder";
import { useDevice } from "@app/core/stores/deviceStore";
import { useBackupReminder } from "@core/hooks/useKeyBackupReminder.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
export const KeyBackupReminder = (): JSX.Element => {
export const KeyBackupReminder = () => {
const { setDialogOpen } = useDevice();
useBackupReminder({
@ -15,5 +15,6 @@ export const KeyBackupReminder = (): JSX.Element => {
sameSite: "strict",
},
});
// deno-lint-ignore jsx-no-useless-fragment
return <></>;
};

71
src/components/PageComponents/Channel.tsx

@ -7,13 +7,13 @@ import { Protobuf } from "@meshtastic/core";
import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string";
import { useState } from "react";
import { PkiRegenerateDialog } from "../Dialog/PkiRegenerateDialog";
import { PkiRegenerateDialog } from "../Dialog/PkiRegenerateDialog.tsx";
export interface SettingsPanelProps {
channel: Protobuf.Channel.Channel;
}
export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
export const Channel = ({ channel }: SettingsPanelProps) => {
const { config, connection, addChannel } = useDevice();
const { toast } = useToast();
@ -24,8 +24,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
channel?.settings?.psk.length ?? 16,
);
const [validationText, setValidationText] = useState<string>();
const [preSharedDialogOpen, setPreSharedDialogOpen] =
useState<boolean>(false);
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(
false,
);
const onSubmit = (data: ChannelValidation) => {
const channel = create(Protobuf.Channel.ChannelSchema, {
@ -92,7 +93,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
<DynamicForm<ChannelValidation>
onSubmit={onSubmit}
submitType="onSubmit"
hasSubmitButton={true}
hasSubmitButton
defaultValues={{
...channel,
...{
@ -107,7 +108,7 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
channel?.settings?.moduleSettings?.positionPrecision === 32,
positionPrecision:
channel?.settings?.moduleSettings?.positionPrecision ===
undefined
undefined
? 10
: channel?.settings?.moduleSettings?.positionPrecision,
},
@ -126,10 +127,9 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
description:
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
properties: {
enumValue:
channel.index === 0
? { PRIMARY: 1 }
: { DISABLED: 0, SECONDARY: 2 },
enumValue: channel.index === 0
? { PRIMARY: 1 }
: { DISABLED: 0, SECONDARY: 2 },
},
},
{
@ -192,32 +192,31 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
description:
"If not sharing precise location, position shared on channel will be accurate within this distance",
properties: {
enumValue:
config.display?.units === 0
? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
enumValue: config.display?.units === 0
? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
},
},
],

7
src/components/PageComponents/Config/Bluetooth.tsx

@ -1,4 +1,4 @@
import { useAppStore } from "@app/core/stores/appStore";
import { useAppStore } from "../../../core/stores/appStore.ts";
import type { BluetoothValidation } from "@app/validation/config/bluetooth.tsx";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
@ -111,9 +111,8 @@ export const Bluetooth = () => {
disabledBy: [
{
fieldName: "mode",
selector:
Protobuf.Config.Config_BluetoothConfig_PairingMode
.FIXED_PIN,
selector: Protobuf.Config.Config_BluetoothConfig_PairingMode
.FIXED_PIN,
invert: true,
},
{

2
src/components/PageComponents/Config/Device.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const Device = (): JSX.Element => {
export const Device = () => {
const { config, setWorkingConfig } = useDevice();
const onSubmit = (data: DeviceValidation) => {

2
src/components/PageComponents/Config/Display.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const Display = (): JSX.Element => {
export const Display = () => {
const { config, setWorkingConfig } = useDevice();
const onSubmit = (data: DisplayValidation) => {

2
src/components/PageComponents/Config/LoRa.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const LoRa = (): JSX.Element => {
export const LoRa = () => {
const { config, setWorkingConfig } = useDevice();
const onSubmit = (data: LoRaValidation) => {

2
src/components/PageComponents/Config/Network.tsx

@ -8,7 +8,7 @@ import {
} from "@core/utils/ip.ts";
import { Protobuf } from "@meshtastic/core";
export const Network = (): JSX.Element => {
export const Network = () => {
const { config, setWorkingConfig } = useDevice();
const onSubmit = (data: NetworkValidation) => {

2
src/components/PageComponents/Config/Position.tsx

@ -1,7 +1,7 @@
import {
type FlagName,
usePositionFlags,
} from "@app/core/hooks/usePositionFlags";
} from "../../../core/hooks/usePositionFlags.ts";
import type { PositionValidation } from "@app/validation/config/position.tsx";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";

2
src/components/PageComponents/Config/Power.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const Power = (): JSX.Element => {
export const Power = () => {
const { config, setWorkingConfig } = useDevice();
const onSubmit = (data: PowerValidation) => {

11
src/components/PageComponents/Config/Security/Security.tsx

@ -1,10 +1,10 @@
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
import { PkiRegenerateDialog } from "../../../Dialog/PkiRegenerateDialog.tsx";
import { DynamicForm } from "@app/components/Form/DynamicForm.tsx";
import { useAppStore } from "@app/core/stores/appStore";
import { useAppStore } from "../../../../core/stores/appStore.ts";
import {
getX25519PrivateKey,
getX25519PublicKey,
} from "@app/core/utils/x25519";
} from "../../../../core/utils/x25519.ts";
import type { SecurityValidation } from "@app/validation/config/security.tsx";
import { create } from "@bufbuild/protobuf";
import { useDevice } from "@core/stores/deviceStore.ts";
@ -12,7 +12,7 @@ import { Protobuf } from "@meshtastic/core";
import { fromByteArray, toByteArray } from "base64-js";
import { Eye, EyeOff } from "lucide-react";
import { useReducer } from "react";
import { securityReducer } from "./securityReducer";
import { securityReducer } from "./securityReducer.tsx";
export const Security = () => {
const { config, setWorkingConfig, setDialogOpen } = useDevice();
@ -308,8 +308,7 @@ export const Security = () => {
<PkiRegenerateDialog
open={state.privateKeyDialogOpen}
onOpenChange={() =>
dispatch({ type: "SHOW_PRIVATE_KEY_DIALOG", payload: false })
}
dispatch({ type: "SHOW_PRIVATE_KEY_DIALOG", payload: false })}
onSubmit={pkiRegenerate}
/>
</>

2
src/components/PageComponents/Config/Security/securityReducer.tsx

@ -1,4 +1,4 @@
import type { SecurityAction, SecurityState } from "./types";
import type { SecurityAction, SecurityState } from "./types.ts";
export function securityReducer(
state: SecurityState,

6
src/components/PageComponents/Config/Security/types.ts

@ -18,7 +18,7 @@ export type SecurityAction =
| { type: "SET_ADMIN_KEY"; payload: string }
| { type: "SHOW_PRIVATE_KEY_DIALOG"; payload: boolean }
| {
type: "REGENERATE_PRIV_PUB_KEY";
payload: { privateKey: string; publicKey: string };
}
type: "REGENERATE_PRIV_PUB_KEY";
payload: { privateKey: string; publicKey: string };
}
| { type: "REGENERATE_ADMIN_KEY"; payload: { adminKey: string } };

4
src/components/PageComponents/Connect/BLE.tsx

@ -1,4 +1,4 @@
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
@ -8,7 +8,7 @@ import { randId } from "@core/utils/randId.ts";
import { BleConnection, Constants } from "@meshtastic/js";
import { useCallback, useEffect, useState } from "react";
export const BLE = ({ closeDialog }: TabElementProps): JSX.Element => {
export const BLE = ({ closeDialog }: TabElementProps) => {
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
const { addDevice } = useDeviceStore();
const { setSelectedDevice } = useAppStore();

23
src/components/PageComponents/Connect/HTTP.tsx

@ -1,4 +1,4 @@
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
@ -9,23 +9,23 @@ import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core";
import { TransportHTTP } from "@meshtastic/transport-http";
import { type JSX, useState } from "react";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
export const HTTP = ({ closeDialog }: TabElementProps) => {
const [https, setHTTPS] = useState(false);
const { addDevice } = useDeviceStore();
const { setSelectedDevice } = useAppStore();
const { register, handleSubmit, control, watch } = useForm<{
const { register, handleSubmit, control } = useForm<{
ip: string;
tls: boolean;
}>({
defaultValues: {
ip: ["client.meshtastic.org", "localhost"].includes(
window.location.hostname,
)
globalThis.location.hostname,
)
? "meshtastic.local"
: window.location.host,
: globalThis.location.host,
tls: location.protocol === "https:",
},
});
@ -61,16 +61,15 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
<Controller
name="tls"
control={control}
render={({ field: { value, onChange, ...rest } }) => (
render={({ field: { ...rest } }) => (
<>
<Label>Use HTTPS</Label>
<Switch
onCheckedChange={(checked) => {
onCheckedChange={(checked: boolean) => {
checked ? setHTTPS(true) : setHTTPS(false);
}}
disabled={
location.protocol === "https:" || connectionInProgress
}
disabled={location.protocol === "https:" ||
connectionInProgress}
checked={https}
{...rest}
/>

4
src/components/PageComponents/Connect/Serial.tsx

@ -1,4 +1,4 @@
import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { useAppStore } from "@core/stores/appStore.ts";
@ -9,7 +9,7 @@ import { MeshDevice } from "@meshtastic/core";
import { TransportWebSerial } from "@meshtastic/transport-web-serial";
import { useCallback, useEffect, useState } from "react";
export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
export const Serial = ({ closeDialog }: TabElementProps) => {
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
const { addDevice } = useDeviceStore();
const { setSelectedDevice } = useAppStore();

54
src/components/PageComponents/Map/NodeDetail.tsx

@ -1,8 +1,8 @@
import { Separator } from "@app/components/UI/Seperator";
import { Separator } from "../../UI/Seperator.tsx";
import { H5 } from "@app/components/UI/Typography/H5.tsx";
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
import { formatQuantity } from "@app/core/utils/string";
import { Avatar } from "@components/UI/Avatar";
import { formatQuantity } from "../../../core/utils/string.ts";
import { Avatar } from "../../UI/Avatar.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Protobuf } from "@meshtastic/core";
@ -37,21 +37,23 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<Avatar text={node.user?.shortName} />
<div>
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
<LockIcon
className="text-green-600"
size={12}
strokeWidth={3}
aria-label="Public Key Enabled"
/>
) : (
<LockOpenIcon
className="text-yellow-500"
size={12}
strokeWidth={3}
aria-label="No Public Key"
/>
)}
{node.user?.publicKey && node.user?.publicKey.length > 0
? (
<LockIcon
className="text-green-600"
size={12}
strokeWidth={3}
aria-label="Public Key Enabled"
/>
)
: (
<LockOpenIcon
className="text-yellow-500"
size={12}
strokeWidth={3}
aria-label="No Public Key"
/>
)}
</div>
<Star
@ -73,15 +75,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
node.deviceMetrics?.voltage?.toPrecision(3) ?? "Unknown"
} volts`}
>
{node.deviceMetrics?.batteryLevel > 100 ? (
<BatteryChargingIcon size={22} />
) : node.deviceMetrics?.batteryLevel > 80 ? (
<BatteryFullIcon size={22} />
) : node.deviceMetrics?.batteryLevel > 20 ? (
<BatteryMediumIcon size={22} />
) : (
<BatteryLowIcon size={22} />
)}
{node.deviceMetrics?.batteryLevel > 100
? <BatteryChargingIcon size={22} />
: node.deviceMetrics?.batteryLevel > 80
? <BatteryFullIcon size={22} />
: node.deviceMetrics?.batteryLevel > 20
? <BatteryMediumIcon size={22} />
: <BatteryLowIcon size={22} />}
<Subtle aria-label="Battery">
{node.deviceMetrics?.batteryLevel > 100
? "Charging"

11
src/components/PageComponents/Messages/ChannelChat.tsx

@ -7,7 +7,6 @@ import { MessageInput } from "@components/PageComponents/Messages/MessageInput.t
import type { Types } from "@meshtastic/core";
import { InboxIcon } from "lucide-react";
import { useCallback, useEffect, useRef } from "react";
import type { JSX } from "react";
export interface ChannelChatProps {
messages?: MessageWithState[];
@ -26,7 +25,7 @@ export const ChannelChat = ({
messages,
channel,
to,
}: ChannelChatProps): JSX.Element => {
}: ChannelChatProps) => {
const { nodes } = useDevice();
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -34,8 +33,7 @@ export const ChannelChat = ({
const scrollToBottom = useCallback(() => {
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
const isNearBottom =
scrollContainer.scrollHeight -
const isNearBottom = scrollContainer.scrollHeight -
scrollContainer.scrollTop -
scrollContainer.clientHeight <
100;
@ -72,9 +70,8 @@ export const ChannelChat = ({
key={message.id}
message={message}
sender={nodes.get(message.from)}
lastMsgSameUser={
index > 0 && messages[index - 1].from === message.from
}
lastMsgSameUser={index > 0 &&
messages[index - 1].from === message.from}
/>
);
})}

28
src/components/PageComponents/Messages/Message.tsx

@ -4,14 +4,13 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@app/components/UI/Tooltip";
import { useAppStore } from "@app/core/stores/appStore";
} from "@components/UI/Tooltip.tsx";
import {
type MessageWithState,
useDeviceStore,
} from "@app/core/stores/deviceStore.ts";
import { cn } from "@app/core/utils/cn";
import { Avatar } from "@components/UI/Avatar";
import { cn } from "@core/utils/cn.ts";
import { Avatar } from "@components/UI/Avatar.tsx";
import type { Protobuf } from "@meshtastic/core";
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
import type { LucideIcon } from "lucide-react";
@ -94,7 +93,6 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
const getMessageTextStyles = (state: MessageState) => {
const isAcknowledged = state === MESSAGE_STATES.ACK;
const isFailed = state === MESSAGE_STATES.FAILED;
const isWaiting = state === MESSAGE_STATES.WAITING;
return cn(
"break-words overflow-hidden",
@ -145,16 +143,18 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
)}
>
<div className="flex items-center gap-2 mb-2">
{!lastMsgSameUser ? (
<div className="flex place-items-center gap-2 mb-1">
<Avatar text={messageUser?.shortName} />
<div className="flex flex-col">
<span className="font-medium text-slate-900 dark:text-white truncate">
{messageUser?.longName}
</span>
{!lastMsgSameUser
? (
<div className="flex place-items-center gap-2 mb-1">
<Avatar text={messageUser?.shortName} />
<div className="flex flex-col">
<span className="font-medium text-slate-900 dark:text-white truncate">
{messageUser?.longName}
</span>
</div>
</div>
</div>
) : null}
)
: null}
</div>
<TimeDisplay date={message.rxTime} />
<div className="flex place-items-center gap-2 pb-2">

22
src/components/PageComponents/Messages/MessageInput.tsx

@ -1,16 +1,10 @@
import { debounce } from "@app/core/utils/debounce";
import { debounce } from "../../../core/utils/debounce.ts";
import { Button } from "@components/UI/Button.tsx";
import { Input } from "@components/UI/Input.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import type { Types } from "@meshtastic/core";
import { SendIcon } from "lucide-react";
import {
type JSX,
startTransition,
useCallback,
useMemo,
useState,
} from "react";
import { startTransition, useCallback, useMemo, useState } from "react";
export interface MessageInputProps {
to: Types.Destination;
@ -22,7 +16,7 @@ export const MessageInput = ({
to,
channel,
maxBytes,
}: MessageInputProps): JSX.Element => {
}: MessageInputProps) => {
const {
connection,
setMessageState,
@ -43,7 +37,7 @@ export const MessageInput = ({
async (message: string) => {
await connection
?.sendText(message, to, true, channel)
.then((id) =>
.then((id: number) =>
setMessageState(
to === "broadcast" ? "broadcast" : "direct",
channel,
@ -51,7 +45,7 @@ export const MessageInput = ({
myNodeNum,
id,
"ack",
),
)
)
.catch((e: Types.PacketError) =>
setMessageState(
@ -61,7 +55,7 @@ export const MessageInput = ({
myNodeNum,
e.id,
e.error,
),
)
);
},
[channel, connection, myNodeNum, setMessageState, to],
@ -82,7 +76,7 @@ export const MessageInput = ({
<div className="flex gap-2">
<form
className="w-full"
action={async (formData: FormData) => {
action={(formData: FormData) => {
// prevent user from sending blank/empty message
if (localDraft === "") return;
const message = formData.get("messageInput") as string;
@ -97,7 +91,7 @@ export const MessageInput = ({
<div className="flex grow gap-2">
<span className="w-full">
<Input
autoFocus={true}
autoFocus
minLength={1}
name="messageInput"
placeholder="Enter Message"

39
src/components/PageComponents/Messages/TraceRoute.tsx

@ -1,7 +1,6 @@
import { useDevice } from "@app/core/stores/deviceStore.ts";
import type { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import type { JSX } from "react";
export interface TraceRouteProps {
from?: Protobuf.Mesh.NodeInfo;
@ -19,7 +18,7 @@ export const TraceRoute = ({
routeBack,
snrTowards,
snrBack,
}: TraceRouteProps): JSX.Element => {
}: TraceRouteProps) => {
const { nodes } = useDevice();
return (
@ -38,23 +37,25 @@ export const TraceRoute = ({
))}
{from?.user?.longName}
</span>
{routeBack ? (
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
<p className="font-semibold">Route back:</p>
<p>{from?.user?.longName}</p>
<p> {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
{routeBack.map((hop, i) => (
<span key={nodes.get(hop)?.num}>
<p>
{nodes.get(hop)?.user?.longName ??
`!${numberToHexUnpadded(hop)}`}
</p>
<p> {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
</span>
))}
{to?.user?.longName}
</span>
) : null}
{routeBack
? (
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
<p className="font-semibold">Route back:</p>
<p>{from?.user?.longName}</p>
<p> {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
{routeBack.map((hop, i) => (
<span key={nodes.get(hop)?.num}>
<p>
{nodes.get(hop)?.user?.longName ??
`!${numberToHexUnpadded(hop)}`}
</p>
<p> {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
</span>
))}
{to?.user?.longName}
</span>
)
: null}
</div>
);
};

2
src/components/PageComponents/ModuleConfig/AmbientLighting.tsx

@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
export const AmbientLighting = (): JSX.Element => {
export const AmbientLighting = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: AmbientLightingValidation) => {

2
src/components/PageComponents/ModuleConfig/Audio.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const Audio = (): JSX.Element => {
export const Audio = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: AudioValidation) => {

17
src/components/PageComponents/ModuleConfig/CannedMessage.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const CannedMessage = (): JSX.Element => {
export const CannedMessage = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: CannedMessageValidation) => {
@ -63,9 +63,8 @@ export const CannedMessage = (): JSX.Element => {
label: "Clockwise event",
description: "Select input event.",
properties: {
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@ -74,9 +73,8 @@ export const CannedMessage = (): JSX.Element => {
label: "Counter Clockwise event",
description: "Select input event.",
properties: {
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{
@ -85,9 +83,8 @@ export const CannedMessage = (): JSX.Element => {
label: "Press event",
description: "Select input event",
properties: {
enumValue:
Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
},
},
{

2
src/components/PageComponents/ModuleConfig/DetectionSensor.tsx

@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
export const DetectionSensor = (): JSX.Element => {
export const DetectionSensor = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: DetectionSensorValidation) => {

2
src/components/PageComponents/ModuleConfig/ExternalNotification.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const ExternalNotification = (): JSX.Element => {
export const ExternalNotification = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: ExternalNotificationValidation) => {

53
src/components/PageComponents/ModuleConfig/MQTT.tsx

@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
export const MQTT = (): JSX.Element => {
export const MQTT = () => {
const { config, moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: MqttValidation) => {
@ -165,32 +165,31 @@ export const MQTT = (): JSX.Element => {
description:
"Position shared will be accurate within this distance",
properties: {
enumValue:
config.display?.units === 0
? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
enumValue: config.display?.units === 0
? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
},
disabledBy: [
{

2
src/components/PageComponents/ModuleConfig/NeighborInfo.tsx

@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
export const NeighborInfo = (): JSX.Element => {
export const NeighborInfo = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: NeighborInfoValidation) => {

2
src/components/PageComponents/ModuleConfig/Paxcounter.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const Paxcounter = (): JSX.Element => {
export const Paxcounter = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: PaxcounterValidation) => {

2
src/components/PageComponents/ModuleConfig/RangeTest.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const RangeTest = (): JSX.Element => {
export const RangeTest = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: RangeTestValidation) => {

2
src/components/PageComponents/ModuleConfig/Serial.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const Serial = (): JSX.Element => {
export const Serial = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: SerialValidation) => {

2
src/components/PageComponents/ModuleConfig/StoreForward.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const StoreForward = (): JSX.Element => {
export const StoreForward = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: StoreForwardValidation) => {

2
src/components/PageComponents/ModuleConfig/Telemetry.tsx

@ -4,7 +4,7 @@ import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
export const Telemetry = (): JSX.Element => {
export const Telemetry = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const onSubmit = (data: TelemetryValidation) => {

8
src/components/PageLayout.tsx

@ -1,7 +1,7 @@
import { cn } from "@app/core/utils/cn.ts";
import { AlignLeftIcon, type LucideIcon } from "lucide-react";
import Footer from "./UI/Footer";
import { Spinner } from "./UI/Spinner";
import Footer from "./UI/Footer.tsx";
import { Spinner } from "./UI/Spinner.tsx";
export interface PageLayoutProps {
label: string;
@ -44,9 +44,7 @@ export const PageLayout = ({
className="transition-all hover:text-accent"
onClick={action.onClick}
>
{action?.isLoading ? (
<Spinner />
) : (
{action?.isLoading ? <Spinner /> : (
<action.icon
className={action.iconClasses}
aria-disabled={action.disabled}

128
src/components/Sidebar.tsx

@ -23,7 +23,7 @@ export interface SidebarProps {
children?: React.ReactNode;
}
export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
export const Sidebar = ({ children }: SidebarProps) => {
const { hardware, nodes, metadata } = useDevice();
const myNode = nodes.get(hardware.myNodeNum);
const myMetadata = metadata.get(0);
@ -64,69 +64,71 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
},
];
return showSidebar ? (
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700">
<div className="flex justify-between px-8 pt-6">
<div>
<span className="text-lg font-medium">
{myNode?.user?.shortName ?? "UNK"}
</span>
<Subtle>{myNode?.user?.longName ?? "UNK"}</Subtle>
return showSidebar
? (
<div className="min-w-[280px] max-w-min flex-col overflow-y-auto border-r-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700">
<div className="flex justify-between px-8 pt-6">
<div>
<span className="text-lg font-medium">
{myNode?.user?.shortName ?? "UNK"}
</span>
<Subtle>{myNode?.user?.longName ?? "UNK"}</Subtle>
</div>
<button
type="button"
className="transition-all hover:text-accent"
onClick={() => setDialogOpen("deviceName", true)}
>
<EditIcon size={16} />
</button>
<button type="button" onClick={() => setShowSidebar(false)}>
<SidebarCloseIcon size={24} />
</button>
</div>
<button
type="button"
className="transition-all hover:text-accent"
onClick={() => setDialogOpen("deviceName", true)}
>
<EditIcon size={16} />
</button>
<button type="button" onClick={() => setShowSidebar(false)}>
<SidebarCloseIcon size={24} />
</button>
</div>
<div className="px-8 pb-6">
<div className="flex items-center">
<BatteryMediumIcon size={24} viewBox={"0 0 28 24"} />
<Subtle>
{myNode?.deviceMetrics?.batteryLevel
? myNode?.deviceMetrics?.batteryLevel > 100
? "Charging"
: `${myNode?.deviceMetrics?.batteryLevel}%`
: "UNK"}
</Subtle>
</div>
<div className="flex items-center">
<ZapIcon size={24} viewBox={"0 0 36 24"} />
<Subtle>
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
</Subtle>
</div>
<div className="flex items-center">
<CpuIcon size={24} viewBox={"0 0 36 24"} />
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
<div className="px-8 pb-6">
<div className="flex items-center">
<BatteryMediumIcon size={24} viewBox="0 0 28 24" />
<Subtle>
{myNode?.deviceMetrics?.batteryLevel
? myNode?.deviceMetrics?.batteryLevel > 100
? "Charging"
: `${myNode?.deviceMetrics?.batteryLevel}%`
: "UNK"}
</Subtle>
</div>
<div className="flex items-center">
<ZapIcon size={24} viewBox="0 0 36 24" />
<Subtle>
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
</Subtle>
</div>
<div className="flex items-center">
<CpuIcon size={24} viewBox="0 0 36 24" />
<Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
</div>
</div>
</div>
<SidebarSection label="Navigation">
{pages.map((link) => (
<SidebarButton
key={link.name}
label={link.name}
Icon={link.icon}
onClick={() => {
setActivePage(link.page);
}}
active={link.page === activePage}
/>
))}
</SidebarSection>
{children}
</div>
) : (
<div className="px-1 pt-8 border-r-[0.5px] border-slate-700">
<button type="button" onClick={() => setShowSidebar(true)}>
<SidebarOpenIcon size={24} />
</button>
</div>
);
<SidebarSection label="Navigation">
{pages.map((link) => (
<SidebarButton
key={link.name}
label={link.name}
Icon={link.icon}
onClick={() => {
setActivePage(link.page);
}}
active={link.page === activePage}
/>
))}
</SidebarSection>
{children}
</div>
)
: (
<div className="px-1 pt-8 border-r-[0.5px] border-slate-700">
<button type="button" onClick={() => setShowSidebar(true)}>
<SidebarOpenIcon size={24} />
</button>
</div>
);
};

12
src/components/ThemeSwitcher.tsx

@ -1,5 +1,5 @@
import { useTheme } from "@app/core/hooks/useTheme";
import { cn } from "@app/core/utils/cn";
import { useTheme } from "../core/hooks/useTheme.ts";
import { cn } from "../core/utils/cn.ts";
import { Monitor, Moon, Sun } from "lucide-react";
type ThemePreference = "light" | "dark" | "system";
@ -32,11 +32,9 @@ export default function ThemeSwitcher({
className,
)}
onClick={toggleTheme}
aria-label={
preference === "system"
? `System theme (currently ${theme}). Click to change theme.`
: `Current theme: ${theme}. Click to change theme.`
}
aria-label={preference === "system"
? `System theme (currently ${theme}). Click to change theme.`
: `Current theme: ${theme}. Click to change theme.`}
>
{themeIcons[preference]}
</button>

4
src/components/Toaster.tsx

@ -5,8 +5,8 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
} from "@components/UI/Toast";
import { useToast } from "@core/hooks/useToast";
} from "./UI/Toast.tsx";
import { useToast } from "../core/hooks/useToast.ts";
export function Toaster() {
const { toasts } = useToast();

2
src/components/UI/Avatar.tsx

@ -1,4 +1,4 @@
import { cn } from "@app/core/utils/cn";
import { cn } from "../../core/utils/cn.ts";
import type React from "react";
type RGBColor = {

8
src/components/UI/Button.tsx

@ -1,4 +1,4 @@
import { type VariantProps, cva } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@core/utils/cn.ts";
@ -20,7 +20,8 @@ const buttonVariants = cva(
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
ghost:
"bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
link: "bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
link:
"bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
},
size: {
default: "h-10 py-2 px-4",
@ -38,7 +39,8 @@ const buttonVariants = cva(
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(

6
src/components/UI/Command.tsx

@ -144,11 +144,11 @@ CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandShortcut,
CommandList,
CommandSeparator,
CommandShortcut,
};

8
src/components/UI/Dialog.tsx

@ -23,7 +23,7 @@ DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs transition-opacity animate-in fade-in",
@ -113,10 +113,10 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogDescription,
DialogTrigger,
};

12
src/components/UI/DropdownMenu.tsx

@ -184,18 +184,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
DropdownMenuTrigger,
};

9
src/components/UI/Footer.tsx

@ -1,12 +1,15 @@
import { cn } from "@app/core/utils/cn";
import { cn } from "../../core/utils/cn.ts";
import React from "react";
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {}
const Footer = React.forwardRef<HTMLElement, FooterProps>(
({ className, ...props }, ref) => {
({ className, ...props }) => {
return (
<footer className={cn("flex mt-auto justify-center p-2", className)}>
<footer
className={cn("flex mt-auto justify-center p-2", className)}
{...props}
>
<p>
<a
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"

1
src/components/UI/Generator.tsx

@ -54,7 +54,6 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
disabled,
...props
},
ref,
) => {
const inputRef = React.useRef<HTMLInputElement>(null);

5
src/components/UI/Input.tsx

@ -1,7 +1,7 @@
import * as React from "react";
import { cn } from "@core/utils/cn.ts";
import { type VariantProps, cva } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority";
import type { LucideIcon } from "lucide-react";
const inputVariants = cva(
@ -20,7 +20,8 @@ const inputVariants = cva(
);
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement>,
extends
React.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {
prefix?: string;
suffix?: string;

16
src/components/UI/Menubar.tsx

@ -216,19 +216,19 @@ MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarCheckboxItem,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarMenu,
MenubarPortal,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
MenubarTrigger,
};

25
src/components/UI/Modal.tsx

@ -1,25 +0,0 @@
export interface ModalProps {
title: string;
actions?: JSX.Element[];
children: React.ReactNode;
}
export const Modal = ({
title,
actions,
children,
}: ModalProps): JSX.Element => {
return (
<div className="rounded-md overflow-hidden w-full">
<div className="flex h-12 px-3 bg-slate-200 dark:bg-slate-700 justify-between">
<h2 className="my-auto font-semibold text-lg">{title}</h2>
{actions && (
<div className="my-auto">{actions.map((action) => action)}</div>
)}
</div>
<div className="h-full border border-slate-200 dark:border-slate-700">
{children}
</div>
</div>
);
};

7
src/components/UI/MultiSelect.tsx

@ -1,4 +1,4 @@
import { cn } from "@app/core/utils/cn";
import { cn } from "../../core/utils/cn.ts";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
@ -8,9 +8,8 @@ interface MultiSelectProps {
}
const MultiSelect = ({ children, className = "" }: MultiSelectProps) => {
return (
<div className={cn("flex flex-wrap gap-2", className)}>{children}</div>
);
return <div className={cn("flex flex-wrap gap-2", className)}>{children}
</div>;
};
interface MultiSelectItemProps {

2
src/components/UI/Popover.tsx

@ -26,4 +26,4 @@ const PopoverContent = React.forwardRef<
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
export { Popover, PopoverContent, PopoverTrigger };

8
src/components/UI/Select.tsx

@ -101,11 +101,11 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
};

5
src/components/UI/Sidebar/sidebarButton.tsx

@ -1,12 +1,11 @@
import { Button } from "@components/UI/Button.tsx";
import type { LucideIcon } from "lucide-react";
import type { JSX } from "react";
export interface SidebarButtonProps {
label: string;
active?: boolean;
Icon?: LucideIcon;
element?: JSX.Element;
element?;
onClick?: () => void;
}
@ -16,7 +15,7 @@ export const SidebarButton = ({
Icon,
element,
onClick,
}: SidebarButtonProps): JSX.Element => (
}: SidebarButtonProps) => (
<Button
onClick={onClick}
variant={active ? "subtle" : "ghost"}

2
src/components/UI/Spinner.tsx

@ -1,4 +1,4 @@
import { cn } from "@app/core/utils/cn";
import { cn } from "../../core/utils/cn.ts";
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
size?: "sm" | "md" | "lg";

2
src/components/UI/Tabs.tsx

@ -50,4 +50,4 @@ const TabsContent = React.forwardRef<
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
export { Tabs, TabsContent, TabsList, TabsTrigger };

20
src/components/UI/Toast.tsx

@ -1,9 +1,9 @@
import * as ToastPrimitives from "@radix-ui/react-toast";
import { type VariantProps, cva } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@core/utils/cn";
import { cn } from "../../core/utils/cn.ts";
const ToastProvider = ToastPrimitives.Provider;
@ -41,8 +41,8 @@ const toastVariants = cva(
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
& React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root>
& VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
@ -116,13 +116,13 @@ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
Toast,
ToastAction,
type ToastActionElement,
ToastClose,
ToastDescription,
type ToastProps,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
ToastViewport,
};

4
src/components/UI/Tooltip.tsx

@ -29,8 +29,8 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export {
Tooltip,
TooltipTrigger,
TooltipArrow,
TooltipContent,
TooltipProvider,
TooltipArrow,
TooltipTrigger,
};

2
src/components/UI/Typography/Blockquote.tsx

@ -2,7 +2,7 @@ export interface BlockquoteProps {
children: React.ReactNode;
}
export const BlockQuote = ({ children }: BlockquoteProps): JSX.Element => (
export const BlockQuote = ({ children }: BlockquoteProps) => (
<blockquote className="mt-6 border-l-2 border-slate-300 pl-6 italic text-slate-800 dark:border-slate-600 dark:text-slate-200">
{children}
</blockquote>

2
src/components/UI/Typography/Code.tsx

@ -2,7 +2,7 @@ export interface CodeProps {
children: React.ReactNode;
}
export const Code = ({ children }: CodeProps): JSX.Element => (
export const Code = ({ children }: CodeProps) => (
<code className="relative rounded-sm bg-slate-100 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-slate-900 dark:bg-slate-800 dark:text-slate-400">
{children}
</code>

2
src/components/UI/Typography/H1.tsx

@ -2,7 +2,7 @@ export interface H1Props {
children: React.ReactNode;
}
export const H1 = ({ children }: H1Props): JSX.Element => (
export const H1 = ({ children }: H1Props) => (
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
{children}
</h1>

2
src/components/UI/Typography/H2.tsx

@ -2,7 +2,7 @@ export interface H2Props {
children: React.ReactNode;
}
export const H2 = ({ children }: H2Props): JSX.Element => (
export const H2 = ({ children }: H2Props) => (
<h2 className="scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700">
{children}
</h2>

2
src/components/UI/Typography/H3.tsx

@ -2,7 +2,7 @@ export interface H3Props {
children: React.ReactNode;
}
export const H3 = ({ children }: H3Props): JSX.Element => (
export const H3 = ({ children }: H3Props) => (
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight">
{children}
</h3>

2
src/components/UI/Typography/H4.tsx

@ -5,7 +5,7 @@ export interface H4Props {
children: React.ReactNode;
}
export const H4 = ({ className, children }: H4Props): JSX.Element => (
export const H4 = ({ className, children }: H4Props) => (
<h4
className={cn(
"scroll-m-20 text-xl font-semibold tracking-tight",

2
src/components/UI/Typography/H5.tsx

@ -5,7 +5,7 @@ export interface H5Props {
children: React.ReactNode;
}
export const H5 = ({ className, children }: H5Props): JSX.Element => (
export const H5 = ({ className, children }: H5Props) => (
<h5
className={cn("scroll-m-20 text-lg font-medium tracking-tight", className)}
>

6
src/components/UI/Typography/Link.tsx

@ -1,4 +1,4 @@
import { cn } from "@app/core/utils/cn";
import { cn } from "../../../core/utils/cn.ts";
export interface LinkProps {
href: string;
@ -6,10 +6,10 @@ export interface LinkProps {
className?: string;
}
export const Link = ({ href, children, className }: LinkProps): JSX.Element => (
export const Link = ({ href, children, className }: LinkProps) => (
<a
href={href}
target={"_blank"}
target="_blank"
rel="noopener noreferrer"
className={cn(
"font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50",

2
src/components/UI/Typography/P.tsx

@ -2,6 +2,6 @@ export interface PProps {
children: React.ReactNode;
}
export const P = ({ children }: PProps): JSX.Element => (
export const P = ({ children }: PProps) => (
<p className="leading-7 not-first:mt-6">{children}</p>
);

2
src/components/UI/Typography/Subtle.tsx

@ -5,7 +5,7 @@ export interface SubtleProps {
children: React.ReactNode;
}
export const Subtle = ({ className, children }: SubtleProps): JSX.Element => (
export const Subtle = ({ className, children }: SubtleProps) => (
<p className={cn("text-sm text-slate-500 dark:text-slate-400", className)}>
{children}
</p>

2
src/components/generic/Blur.tsx

@ -1,4 +1,4 @@
export const Blur = (): JSX.Element => {
export const Blur = () => {
return (
<div
className="fixed inset-0 backdrop-blur-md backdrop-brightness-press"

2
src/components/generic/Mono.tsx

@ -2,7 +2,7 @@ export const Mono = ({
children,
className,
...rest
}: JSX.IntrinsicElements["span"]): JSX.Element => {
}: JSX.IntrinsicElements["span"]) => {
return (
<span
className={`font-mono text-sm text-text-secondary ${className ?? ""}`}

14
src/components/generic/Table/index.tsx

@ -1,9 +1,9 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import React, { useState } from "react";
import { useState } from "react";
export interface TableProps {
headings: Heading[];
rows: JSX.Element[][];
rows: [][];
}
export interface Heading {
@ -12,7 +12,7 @@ export interface Heading {
sortable: boolean;
}
export const Table = ({ headings, rows }: TableProps): JSX.Element => {
export const Table = ({ headings, rows }: TableProps) => {
const [sortColumn, setSortColumn] = useState<string | null>("Last Heard");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
@ -75,11 +75,9 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
<div className="flex gap-2">
{heading.title}
{sortColumn === heading.title &&
(sortOrder === "asc" ? (
<ChevronUpIcon size={16} />
) : (
<ChevronDownIcon size={16} />
))}
(sortOrder === "asc"
? <ChevronUpIcon size={16} />
: <ChevronDownIcon size={16} />)}
</div>
</th>
))}

67
src/components/generic/ThemeProvider.tsx

@ -1,67 +0,0 @@
import type React from "react";
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "light" | "dark" | "system";
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window !== "undefined") {
const savedTheme = localStorage.getItem("theme") as Theme;
return savedTheme || "system";
}
return "system";
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
} else {
root.classList.add(theme);
}
localStorage.setItem("theme", theme);
}, [theme]);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
if (theme === "system") {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(mediaQuery.matches ? "dark" : "light");
}
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}

3
src/components/generic/TimeAgo.tsx

@ -5,7 +5,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import type { JSX } from "react";
export interface TimeAgoProps {
timestamp: number;
@ -43,7 +42,7 @@ const getTimeAgo = (
return rtf.format(Math.floor(0 - diff), "second");
};
export const TimeAgo = ({ timestamp }: TimeAgoProps): JSX.Element => {
export const TimeAgo = ({ timestamp }: TimeAgoProps) => {
return (
<TooltipProvider>
<Tooltip>

4
src/components/generic/Uptime.tsx

@ -1,5 +1,3 @@
import type { JSX } from "react";
export interface UptimeProps {
seconds: number;
}
@ -12,6 +10,6 @@ const getUptime = (seconds: number): string => {
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
};
export const Uptime = ({ seconds }: UptimeProps): JSX.Element => {
export const Uptime = ({ seconds }: UptimeProps) => {
return <span>{getUptime(seconds)}</span>;
};

4
src/core/hooks/useBrowserFeatureDetection.ts

@ -14,8 +14,8 @@ export function useBrowserFeatureDetection(): BrowserSupport {
["Web Serial", !!navigator?.serial],
[
"Secure Context",
window.location.protocol === "https:" ||
window.location.hostname === "localhost",
globalThis.location.protocol === "https:" ||
globalThis.location.hostname === "localhost",
],
];

13
src/core/hooks/useKeyBackupReminder.tsx

@ -1,8 +1,8 @@
import { Button } from "@app/components/UI/Button";
import { Button } from "../../components/UI/Button.tsx";
import type { CookieAttributes } from "js-cookie";
import { useCallback, useEffect, useRef } from "react";
import useCookie from "./useCookie";
import { useToast } from "./useToast";
import useCookie from "./useCookie.ts";
import { useToast } from "./useToast.ts";
interface UseBackupReminderOptions {
reminderInDays?: number;
@ -26,8 +26,8 @@ const ON_ACCEPT_REMINDER_DAYS = 365;
function isReminderExpired(lastShown: string): boolean {
const lastShownDate = new Date(lastShown);
const now = new Date();
const daysSinceLastShown =
(now.getTime() - lastShownDate.getTime()) / (1000 * 60 * 60 * 24);
const daysSinceLastShown = (now.getTime() - lastShownDate.getTime()) /
(1000 * 60 * 60 * 24);
return daysSinceLastShown >= 7;
}
@ -63,8 +63,7 @@ export function useBackupReminder({
useEffect(() => {
if (!enabled || toastShownRef.current) return;
const shouldShowReminder =
!reminderCookie?.suppressed ||
const shouldShowReminder = !reminderCookie?.suppressed ||
isReminderExpired(reminderCookie.lastShown);
if (!shouldShowReminder) return;

6
src/core/hooks/useTheme.ts

@ -4,7 +4,7 @@ type Theme = "light" | "dark" | "system";
export function useTheme() {
const getSystemTheme = () =>
window.matchMedia("(prefers-color-scheme: dark)").matches
globalThis.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
@ -14,7 +14,7 @@ export function useTheme() {
);
const [preference, setPreference] = useState<Theme>(() =>
typeof window !== "undefined" ? getStoredPreference() : "light",
typeof window !== "undefined" ? getStoredPreference() : "light"
);
const theme = preference === "system" ? getSystemTheme() : preference;
@ -26,7 +26,7 @@ export function useTheme() {
useEffect(() => {
if (preference !== "system") return;
const media = window.matchMedia("(prefers-color-scheme: dark)");
const media = globalThis.matchMedia("(prefers-color-scheme: dark)");
const updateTheme = () => setPreference(getStoredPreference());
media.addEventListener("change", updateTheme);

34
src/core/hooks/useToast.ts

@ -31,21 +31,21 @@ type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
@ -81,7 +81,7 @@ export const reducer = (state: State, action: Action): State => {
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
};
@ -103,10 +103,10 @@ export const reducer = (state: State, action: Action): State => {
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
...t,
open: false,
}
: t
),
};
}

4
src/core/stores/appStore.ts

@ -112,7 +112,7 @@ export const useAppStore = create<AppState>()((set, get) => ({
);
},
setNodeNumToBeRemoved: (nodeNum) =>
set((state) => ({
set(() => ({
nodeNumToBeRemoved: nodeNum,
})),
setConnectDialogOpen: (open: boolean) => {
@ -124,7 +124,7 @@ export const useAppStore = create<AppState>()((set, get) => ({
},
setNodeNumDetails: (nodeNum) =>
set((state) => ({
set(() => ({
nodeNumDetails: nodeNum,
})),
setActiveChat: (chat) =>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save