Browse Source

Internal cleanup

pull/21/head
Sacha Weatherstone 4 years ago
parent
commit
e5d75eff19
  1. 23
      package.json
  2. 1561
      pnpm-lock.yaml
  3. 13
      src/App.tsx
  4. 100
      src/components/Connection.tsx
  5. 2
      src/components/MapBox/MapboxProvider.tsx
  6. 28
      src/components/connection/BLE.tsx
  7. 32
      src/components/connection/HTTP.tsx
  8. 28
      src/components/connection/Serial.tsx
  9. 21
      src/components/generic/Card.tsx
  10. 2
      src/components/generic/ContextMenu.tsx
  11. 10
      src/components/generic/Cover.tsx
  12. 46
      src/components/generic/ListItem.tsx
  13. 9
      src/components/generic/Loading.tsx
  14. 0
      src/components/generic/Sidebar/CollapsibleSection.tsx
  15. 0
      src/components/generic/Sidebar/ExternalSection.tsx
  16. 2
      src/components/generic/Sidebar/SidebarOverlay.tsx
  17. 19
      src/components/generic/Tooltip.tsx
  18. 73
      src/components/generic/button/Button.tsx
  19. 35
      src/components/generic/button/IconButton.tsx
  20. 48
      src/components/generic/form/Checkbox.tsx
  21. 2
      src/components/generic/form/Form.tsx
  22. 37
      src/components/generic/form/Input.tsx
  23. 29
      src/components/generic/form/InputWrapper.tsx
  24. 68
      src/components/generic/form/Select.tsx
  25. 7
      src/components/layout/Sidebar/ButtonNav.tsx
  26. 5
      src/components/layout/Sidebar/Settings/Channels.tsx
  27. 31
      src/components/layout/Sidebar/Settings/Index.tsx
  28. 2
      src/components/layout/Sidebar/Settings/Interface.tsx
  29. 7
      src/components/layout/Sidebar/Settings/Position.tsx
  30. 4
      src/components/layout/Sidebar/Settings/Power.tsx
  31. 4
      src/components/layout/Sidebar/Settings/Radio.tsx
  32. 5
      src/components/layout/Sidebar/Settings/User.tsx
  33. 4
      src/components/layout/Sidebar/Settings/WiFi.tsx
  34. 9
      src/components/layout/Sidebar/Settings/channels/Channels.tsx
  35. 9
      src/components/layout/Sidebar/Settings/channels/ChannelsGroup.tsx
  36. 10
      src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx
  37. 10
      src/components/layout/Sidebar/Settings/plugins/RangeTest.tsx
  38. 10
      src/components/layout/Sidebar/Settings/plugins/Serial.tsx
  39. 10
      src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx
  40. 31
      src/components/layout/Sidebar/Settings/plugins/panels/ExternalNotifications/DebugPanel.tsx
  41. 27
      src/components/layout/Sidebar/Settings/plugins/panels/RangeTest/DebugPanel.tsx
  42. 30
      src/components/layout/Sidebar/Settings/plugins/panels/Serial/DebugPanel.tsx
  43. 31
      src/components/layout/Sidebar/Settings/plugins/panels/StoreForward/DebugPanel.tsx
  44. 21
      src/components/layout/Sidebar/Settings/radio/channels/panels/DebugPanel.tsx
  45. 20
      src/components/layout/Sidebar/Settings/radio/channels/panels/QRCodePanel.tsx
  46. 7
      src/components/layout/Sidebar/index.tsx
  47. 4
      src/components/layout/index.tsx
  48. 4
      src/components/menu/BottomNav.tsx
  49. 4
      src/components/menu/buttons/CopyButton.tsx
  50. 56
      src/components/menu/buttons/DeviceStatus.tsx
  51. 5
      src/components/modals/VersionInfo.tsx
  52. 2
      src/core/slices/mapSlice.ts
  53. 15
      src/core/slices/meshtasticSlice.ts
  54. 7
      src/core/store.ts
  55. 6
      src/index.tsx
  56. 11
      src/pages/Extensions/FileBrowser.tsx
  57. 11
      src/pages/Extensions/Index.tsx
  58. 19
      src/pages/Extensions/Info.tsx
  59. 199
      src/pages/Extensions/Logs.tsx
  60. 7
      src/pages/Map/MapContainer.tsx
  61. 15
      src/pages/Map/index.tsx
  62. 3
      src/pages/Messages/ChannelChat.tsx
  63. 2
      src/pages/Messages/DmChat.tsx
  64. 4
      src/pages/Messages/Message.tsx
  65. 2
      src/pages/Messages/MessageBar.tsx
  66. 21
      src/pages/Messages/index.tsx
  67. 30
      src/pages/Nodes/NodeCard.tsx
  68. 8
      src/pages/Nodes/index.tsx
  69. 21
      src/pages/Nodes/panels/DebugPanel.tsx
  70. 32
      src/pages/Nodes/panels/PositionPanel.tsx
  71. 9
      src/pages/NotFound.tsx

23
package.json

@ -9,15 +9,16 @@
"preview": "vite preview",
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)",
"format": "prettier --write 'src/**/*.{ts,tsx}'",
"check": "unimported",
"lint": "eslint 'src/**/*.{ts,tsx}'"
},
"dependencies": {
"@emeraldpay/hashicon-react": "^0.5.2",
"@floating-ui/react-dom": "^0.4.3",
"@meshtastic/components": "^1.0.23",
"@meshtastic/meshtasticjs": "^0.6.45",
"@reduxjs/toolkit": "^1.7.2",
"@tippyjs/react": "^4.2.6",
"base64-js": "^1.5.1",
"cuid": "^2.1.8",
"framer-motion": "^6.2.6",
"graphql-request": "^4.0.0",
"mapbox-gl": "^2.7.0",
@ -28,25 +29,25 @@
"react-hook-form": "^7.27.0",
"react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0",
"react-multi-select-component": "^4.2.2",
"react-qr-code": "^2.0.3",
"react-multi-select-component": "^4.2.3",
"react-redux": "^7.2.6",
"react-use-clipboard": "^1.0.7",
"rfc4648": "^1.5.1",
"rollup-plugin-visualizer": "^5.5.4",
"swr": "^1.2.1",
"swr": "^1.2.2",
"timeago-react": "^3.0.4",
"tippy.js": "^6.3.7",
"type-route": "^0.6.0",
"vite-plugin-environment": "^1.1.0"
},
"devDependencies": {
"@hookform/devtools": "^4.0.2",
"@types/mapbox-gl": "^2.6.1",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.12",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"@verypossible/eslint-config": "^1.6.1",
"@vitejs/plugin-react": "^1.2.0",
"autoprefixer": "^10.4.2",
@ -63,10 +64,12 @@
"postcss": "^8.4.6",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.7",
"tailwindcss": "^3.0.22",
"rollup-plugin-visualizer": "^5.5.4",
"tailwindcss": "^3.0.23",
"tar": "^6.1.11",
"typescript": "^4.5.5",
"vite": "^2.8.1",
"unimported": "^1.19.1",
"vite": "^2.8.4",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-pwa": "^0.11.13",
"workbox-window": "^6.4.2"

1561
pnpm-lock.yaml

File diff suppressed because it is too large

13
src/App.tsx

@ -2,15 +2,14 @@ import React from 'react';
import { Map } from '@app/pages/Map';
import { Connection } from '@components/Connection';
import { ContextMenu } from '@components/generic/ContextMenu';
import { BottomNav } from '@components/menu/BottomNav';
import { useRoute } from '@core/router';
import { useAppSelector } from '@hooks/useAppSelector';
import { ContextMenu } from './components/generic/ContextMenu';
import { BottomNav } from './components/menu/BottomNav';
import { Extensions } from './pages/Extensions/Index';
import { Messages } from './pages/Messages';
import { Nodes } from './pages/Nodes';
import { NotFound } from './pages/NotFound';
import { Extensions } from '@pages/Extensions/Index';
import { Messages } from '@pages/Messages';
import { Nodes } from '@pages/Nodes';
import { NotFound } from '@pages/NotFound';
export const App = (): JSX.Element => {
const route = useRoute();

100
src/components/Connection.tsx

@ -2,11 +2,13 @@ import React from 'react';
import { AnimatePresence } from 'framer-motion';
import { Card } from '@app/components/generic/Card';
import { BLE } from '@components/connection/BLE';
import { HTTP } from '@components/connection/HTTP';
import { Serial } from '@components/connection/Serial';
import { Select } from '@components/generic/form/Select';
import { Modal } from '@components/generic/Modal';
import { connection, connectionUrl, setConnection } from '@core/connection';
import { connectionUrl, setConnection } from '@core/connection';
import {
closeConnectionModal,
connType,
@ -15,7 +17,6 @@ import {
} from '@core/slices/appSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { Button, Card, Select } from '@meshtastic/components';
import { Types } from '@meshtastic/meshtasticjs';
export const Connection = (): JSX.Element => {
@ -56,50 +57,65 @@ export const Connection = (): JSX.Element => {
dispatch(closeConnectionModal());
}}
>
<Card>
<div className="flex w-full max-w-3xl gap-2 p-2">
<Card className="relative">
<div className="flex max-w-3xl flex-grow gap-4 p-2">
<div className="w-1/2">
{state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_DISCONNECTED ? (
<div className="space-y-2">
<Select
label="Connection Method"
optionsEnum={connType}
value={appState.connType}
onChange={(e): void => {
dispatch(setConnType(parseInt(e.target.value)));
}}
<div className="space-y-2">
<Select
label="Connection Method"
optionsEnum={connType}
value={appState.connType}
onChange={(e): void => {
dispatch(setConnType(parseInt(e.target.value)));
}}
disabled={
state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED
}
/>
{appState.connType === connType.HTTP && (
<HTTP
connecting={
state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED
}
/>
{appState.connType === connType.HTTP && <HTTP />}
{appState.connType === connType.BLE && <BLE />}
{appState.connType === connType.SERIAL && <Serial />}
</div>
) : (
<div>
<span>Connecting...</span>
{state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED && (
<Button
border
onClick={async (): Promise<void> => {
await connection.disconnect();
}}
>
Disconnect
</Button>
)}
</div>
)}
)}
{appState.connType === connType.BLE && (
<BLE
connecting={
state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED
}
/>
)}
{appState.connType === connType.SERIAL && (
<Serial
connecting={
state.deviceStatus ===
Types.DeviceStatusEnum.DEVICE_CONNECTED
}
/>
)}
</div>
</div>
<div className="w-1/2 ">
<div className="h-96 overflow-y-auto rounded-md bg-secondaryDark p-2">
{state.logs.map((log, index) => (
<div key={index} className="flex">
<div className="truncate font-mono text-sm">
{log.message}
<div className="w-1/2">
<div className="h-96 overflow-y-auto rounded-md bg-gray-200 p-2 dark:bg-secondaryDark dark:text-gray-400">
{state.logs
.filter((log) => {
return ![
Types.Emitter.handleFromRadio,
Types.Emitter.handleMeshPacket,
Types.Emitter.sendPacket,
].includes(log.emitter);
})
.map((log, index) => (
<div key={index} className="flex">
<div className="truncate font-mono text-sm">
{log.message}
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>

2
src/components/MapBox/MapboxProvider.tsx

@ -2,6 +2,7 @@ import React from 'react';
import mapboxgl from 'mapbox-gl';
import { MapboxContext } from '@components/MapBox/mapboxContext';
import {
setBearing,
setLatLng,
@ -14,7 +15,6 @@ import { useAppSelector } from '@hooks/useAppSelector';
import { useCreateMapbox } from '@hooks/useCreateMapbox';
import { MapStyles } from '../../pages/Map/styles';
import { MapboxContext } from './mapboxContext';
export type MapboxProviderProps = {
children: React.ReactNode;

28
src/components/connection/BLE.tsx

@ -3,12 +3,17 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { FiCheck } from 'react-icons/fi';
import { connType } from '@app/core/slices/appSlice';
import { setConnection } from '@core/connection';
import { Button, IconButton } from '@meshtastic/components';
import { Button } from '@components/generic/button/Button';
import { IconButton } from '@components/generic/button/IconButton';
import { connection, setConnection } from '@core/connection';
import { connType } from '@core/slices/appSlice';
import { IBLEConnection } from '@meshtastic/meshtasticjs';
export const BLE = (): JSX.Element => {
export interface BLEProps {
connecting: boolean;
}
export const BLE = ({ connecting }: BLEProps): JSX.Element => {
const [bleDevices, setBleDevices] = React.useState<BluetoothDevice[]>([]);
const { handleSubmit } = useForm<{
@ -45,11 +50,22 @@ export const BLE = (): JSX.Element => {
await setConnection(connType.BLE);
}}
icon={<FiCheck />}
disabled={connecting}
/>
</div>
))}
<Button type="submit" className="mt-2 ml-auto" border>
Connect
<Button
className="mt-2 ml-auto"
onClick={async (): Promise<void> => {
if (connecting) {
await connection.disconnect();
} else {
await onSubmit();
}
}}
border
>
{connecting ? 'Disconnect' : 'Connect'}
</Button>
</form>
);

32
src/components/connection/HTTP.tsx

@ -2,12 +2,19 @@ import type React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { connectionUrl, setConnection } from '@core/connection';
import { Button } from '@components/generic/button/Button';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection, connectionUrl, setConnection } from '@core/connection';
import { connType, setConnectionParams } from '@core/slices/appSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { Button, Checkbox, Input, Select } from '@meshtastic/components';
export const HTTP = (): JSX.Element => {
export interface HTTPProps {
connecting: boolean;
}
export const HTTP = ({ connecting }: HTTPProps): JSX.Element => {
const dispatch = useAppDispatch();
const { register, handleSubmit, control } = useForm<{
@ -56,16 +63,27 @@ export const HTTP = (): JSX.Element => {
value: 'remote',
},
]}
disabled={connecting}
{...register('ipSource')}
/>
{watchIpSource === 'local' ? (
<Input label="Host" value={connectionUrl} disabled />
) : (
<Input label="Host" {...register('ip')} />
<Input label="Host" disabled={connecting} {...register('ip')} />
)}
<Checkbox label="Use TLS?" {...register('tls')} />
<Button type="submit" className="mt-2 ml-auto" border>
Connect
<Checkbox label="Use TLS?" disabled={connecting} {...register('tls')} />
<Button
className="mt-2 ml-auto"
onClick={async (): Promise<void> => {
if (connecting) {
await connection.disconnect();
} else {
await onSubmit();
}
}}
border
>
{connecting ? 'Disconnect' : 'Connect'}
</Button>
</form>
);

28
src/components/connection/Serial.tsx

@ -3,13 +3,18 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { FiCheck } from 'react-icons/fi';
import { setConnection } from '@core/connection';
import { Button } from '@components/generic/button/Button';
import { IconButton } from '@components/generic/button/IconButton';
import { connection, setConnection } from '@core/connection';
import { connType, setConnectionParams } from '@core/slices/appSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { Button, IconButton } from '@meshtastic/components';
import { ISerialConnection } from '@meshtastic/meshtasticjs';
export const Serial = (): JSX.Element => {
export interface SerialProps {
connecting: boolean;
}
export const Serial = ({ connecting }: SerialProps): JSX.Element => {
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]);
const dispatch = useAppDispatch();
@ -36,7 +41,7 @@ export const Serial = (): JSX.Element => {
{serialDevices.length > 0 ? (
serialDevices.map((device, index) => (
<div
className="flex justify-between rounded-md bg-gray-700 p-2"
className="flex justify-between rounded-md bg-secondaryDark p-2"
key={index}
>
<div className="my-auto flex gap-4">
@ -59,6 +64,7 @@ export const Serial = (): JSX.Element => {
);
await setConnection(connType.SERIAL);
}}
disabled={connecting}
icon={<FiCheck />}
/>
</div>
@ -68,8 +74,18 @@ export const Serial = (): JSX.Element => {
<p>No previously connected devices found</p>
</div>
)}
<Button type="submit" className="mt-2 ml-auto" border>
Connect
<Button
className="mt-2 ml-auto"
onClick={async (): Promise<void> => {
if (connecting) {
await connection.disconnect();
} else {
await onSubmit();
}
}}
border
>
{connecting ? 'Disconnect' : 'Connect'}
</Button>
</form>
);

21
src/components/generic/Card.tsx

@ -0,0 +1,21 @@
import type React from 'react';
import { m } from 'framer-motion';
export interface CardProps {
className?: string;
children: React.ReactNode;
}
export const Card = ({ className, children }: CardProps): JSX.Element => {
return (
<m.div
className={`flex select-none rounded-md bg-white p-4 shadow-md dark:bg-primaryDark ${className}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{children}
</m.div>
);
};

2
src/components/generic/ContextMenu.tsx

@ -2,7 +2,7 @@ import React from 'react';
import { FiActivity, FiAperture, FiTag } from 'react-icons/fi';
import { ContextItem } from './ContextItem';
import { ContextItem } from '@components/generic/ContextItem';
export interface ContextMenuProps {
items?: JSX.Element;

10
src/components/generic/Cover.tsx

@ -1,10 +0,0 @@
import type React from 'react';
export interface CoverProps {
content: JSX.Element;
enabled: boolean;
}
export const Cover = ({ content, enabled }: CoverProps): JSX.Element => {
return enabled ? <div className="m-4 ">{content}</div> : <></>;
};

46
src/components/generic/ListItem.tsx

@ -1,46 +0,0 @@
import type React from 'react';
import { IconButton } from '@meshtastic/components';
export interface ListItemProps {
selected: boolean;
selectedIcon: JSX.Element;
actions?: JSX.Element;
status: JSX.Element;
onClick?: () => void;
children: React.ReactNode;
}
export const ListItem = ({
selected,
selectedIcon,
actions,
status,
onClick,
children,
}: ListItemProps): JSX.Element => {
return (
<div
onClick={(): void => {
onClick && onClick();
}}
className={`flex select-none rounded-md border bg-gray-100 shadow-md dark:bg-primaryDark ${
selected
? 'border-primary dark:border-primary'
: 'border-gray-100 dark:border-primaryDark'
}`}
>
<div className="w-3 rounded-l-md bg-green-500" />
<div className="flex justify-between p-2">
<div className="my-auto flex space-x-2">
{status}
<div className="flex gap-2">{children}</div>
</div>
<div className="flex gap-2">
{actions}
<IconButton active={selected} icon={selectedIcon} />
</div>
</div>
</div>
);
};

9
src/components/generic/Loading.tsx

@ -0,0 +1,9 @@
import type React from 'react';
export const Loading = (): JSX.Element => {
return (
<div className="absolute top-0 bottom-0 left-0 right-0 z-10 flex rounded-md backdrop-blur-sm backdrop-filter">
<div className="m-auto text-lg font-medium text-gray-400">Loading</div>
</div>
);
};

0
src/components/layout/Sidebar/sections/CollapsibleSection.tsx → src/components/generic/Sidebar/CollapsibleSection.tsx

0
src/components/layout/Sidebar/sections/ExternalSection.tsx → src/components/generic/Sidebar/ExternalSection.tsx

2
src/components/layout/Sidebar/sections/SidebarOverlay.tsx → src/components/generic/Sidebar/SidebarOverlay.tsx

@ -3,7 +3,7 @@ import type React from 'react';
import { AnimatePresence, AnimateSharedLayout, m } from 'framer-motion';
import { FiArrowLeft } from 'react-icons/fi';
import { IconButton } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
export interface SidebarOverlayProps {
title: string;

19
src/components/generic/Tooltip.tsx

@ -0,0 +1,19 @@
import 'tippy.js/dist/tippy.css';
import type React from 'react';
import cuid from 'cuid';
import Tippy, { TippyProps } from '@tippyjs/react';
export const Tooltip = ({
children,
content,
...props
}: TippyProps): JSX.Element => {
return (
<Tippy content={content} {...props}>
<div key={cuid()}>{children}</div>
</Tippy>
);
};

73
src/components/generic/button/Button.tsx

@ -0,0 +1,73 @@
import React from 'react';
import { FiCheck } from 'react-icons/fi';
type DefaultButtonProps = JSX.IntrinsicElements['button'];
export enum ButtonSize {
Small = 'small',
Medium = 'medium',
Large = 'large',
}
export interface ButtonProps extends DefaultButtonProps {
icon?: JSX.Element;
active?: boolean;
border?: boolean;
size?: ButtonSize;
confirmAction?: () => void;
}
export const Button = ({
icon,
className,
active,
border,
size = ButtonSize.Medium,
confirmAction,
disabled,
children,
...props
}: ButtonProps): JSX.Element => {
const [hasConfirmed, setHasConfirmed] = React.useState(false);
const handleConfirm = (): void => {
if (typeof confirmAction == 'function') {
if (hasConfirmed) {
void confirmAction();
}
setHasConfirmed(true);
setTimeout(() => {
setHasConfirmed(false);
}, 3000);
}
};
return (
<button
onClick={handleConfirm}
className={`flex select-none items-center space-x-3 rounded-md border border-transparent text-sm transition duration-200 ease-in-out focus-within:border-primary focus-within:shadow-border active:scale-95 dark:text-white dark:focus-within:border-primary
${
size === ButtonSize.Small
? 'p-0'
: size === ButtonSize.Medium
? 'p-2'
: 'p-4'
}
${
disabled
? 'cursor-not-allowed bg-white dark:bg-primaryDark'
: 'cursor-pointer hover:bg-gray-100 hover:shadow-md dark:hover:bg-secondaryDark'
} ${border ? 'border-gray-400 dark:border-gray-200' : ''} ${className}`}
{...props}
>
{icon && (
<div className="text-gray-500 dark:text-gray-400">
{hasConfirmed ? <FiCheck /> : icon}
</div>
)}
<span>{children}</span>
</button>
);
};

35
src/components/generic/button/IconButton.tsx

@ -0,0 +1,35 @@
import type React from 'react';
type DefaulButtonProps = JSX.IntrinsicElements['button'];
export interface IconButtonProps extends DefaulButtonProps {
icon: React.ReactNode;
active?: boolean;
}
export const IconButton = ({
icon,
active,
disabled,
...props
}: IconButtonProps): JSX.Element => {
return (
<div className="my-auto text-gray-500 dark:text-gray-400">
<button
type="button"
disabled={disabled}
className={`rounded-md p-2 transition duration-200 ease-in-out active:scale-95 ${
active
? 'bg-gray-200 dark:bg-gray-600'
: 'hover:bg-gray-200 dark:hover:bg-gray-600'
} ${
disabled ? 'cursor-not-allowed text-gray-400 dark:text-gray-700' : ''
}`}
{...props}
>
{icon}
<span className="sr-only">Refresh</span>
</button>
</div>
);
};

48
src/components/generic/form/Checkbox.tsx

@ -0,0 +1,48 @@
import React from 'react';
import { Label } from '@components/generic/form/Label';
type DefaultInputProps = JSX.IntrinsicElements['input'];
export interface CheckboxProps extends DefaultInputProps {
action?: (enabled: boolean) => void;
label: string;
valid?: boolean;
validationMessage?: string;
error?: boolean;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
function Input(
{ label, valid, validationMessage, id, error, ...props }: CheckboxProps,
ref,
) {
return (
<div className="flex w-full flex-col">
<Label label={label} />
<div className="ml-auto">
<input
ref={ref}
type="checkbox"
id={id}
className={`h-8 w-8 appearance-none rounded-md border border-gray-400 transition duration-200 ease-in-out checked:border-transparent checked:bg-primary focus-within:shadow-border focus:outline-none dark:border-gray-200 ${
props.disabled
? 'border-gray-400 bg-gray-300 text-gray-500 dark:border-gray-700 dark:bg-secondaryDark dark:text-gray-400'
: ''
} ${
error
? 'border-red-500'
: props.disabled
? 'border-gray-200'
: 'focus-within:border-primary hover:border-primary dark:focus-within:border-primary dark:hover:border-primary'
}`}
{...props}
/>
</div>
{!valid && (
<div className="text-sm text-gray-600">{validationMessage}</div>
)}
</div>
);
},
);

2
src/components/generic/form/Form.tsx

@ -1,6 +1,6 @@
import type React from 'react';
import { Loading } from '@meshtastic/components';
import { Loading } from '@components/generic/Loading';
export interface FormProps {
loading?: boolean;

37
src/components/generic/form/Input.tsx

@ -0,0 +1,37 @@
import React from 'react';
import { InputWrapper } from '@components/generic/form/InputWrapper';
import { Label } from '@components/generic/form/Label';
type DefaultInputProps = JSX.IntrinsicElements['input'];
export interface InputProps extends DefaultInputProps {
label?: string;
error?: string;
action?: JSX.Element;
prefix?: string;
suffix?: string;
}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
function Input({ label, error, action, suffix, ...props }: InputProps, ref) {
return (
<div className="w-full">
{label && <Label label={label} error={error} />}
<InputWrapper error={error} disabled={props.disabled}>
<input
ref={ref}
className="h-10 w-full bg-transparent px-3 py-2 focus:outline-none disabled:cursor-not-allowed dark:text-white"
{...props}
/>
{suffix && (
<span className="my-auto mr-3 text-sm font-medium text-gray-500 dark:text-gray-400">
{suffix}
</span>
)}
{action && <div className="mr-1 flex">{action}</div>}
</InputWrapper>
</div>
);
},
);

29
src/components/generic/form/InputWrapper.tsx

@ -0,0 +1,29 @@
import type React from 'react';
export interface LabelProps {
error?: string;
disabled?: boolean;
children: React.ReactNode;
}
export const InputWrapper = ({
error,
disabled,
children,
}: LabelProps): JSX.Element => (
<div
className={`flex w-full rounded-md border border-gray-400 transition duration-200 ease-in-out dark:border-gray-200 ${
disabled
? 'border-gray-400 bg-gray-300 text-gray-500 dark:border-gray-700 dark:bg-secondaryDark dark:text-gray-400'
: ''
} ${
error
? 'border-red-500 dark:border-red-500'
: disabled
? ''
: ' focus-within:border-primary focus-within:shadow-border hover:border-primary dark:focus-within:border-primary dark:hover:border-primary'
}`}
>
{children}
</div>
);

68
src/components/generic/form/Select.tsx

@ -0,0 +1,68 @@
import React from 'react';
import { InputWrapper } from '@components/generic/form/InputWrapper';
import { Label } from '@components/generic/form/Label';
type DefaultSelectProps = JSX.IntrinsicElements['select'];
export interface SelectProps extends DefaultSelectProps {
options?: {
name: string | number;
value: string | number;
}[];
optionsEnum?: { [s: string]: string | number };
label?: string;
error?: string;
small?: boolean;
}
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ options, optionsEnum, label, error, small, ...props }, ref) => {
const optionsEnumValues = optionsEnum
? Object.entries(optionsEnum).filter(
(value) => typeof value[1] === 'number',
)
: [];
return (
<div>
{label && <Label label={label} error={error} />}
<InputWrapper error={error} disabled={props.disabled}>
<select
ref={ref}
className={`w-full rounded-md bg-transparent focus:border-primary focus:outline-none disabled:cursor-not-allowed dark:text-white ${
small ? 'm-1' : 'mx-2 h-10'
}`}
disabled={
props.disabled
? true
: !(optionsEnumValues.length || options?.length)
}
{...props}
>
{!(optionsEnumValues.length || options?.length) && (
<option key="loading" className="dark:bg-gray-700">
Loading
</option>
)}
{optionsEnumValues.length &&
optionsEnumValues.map(([name, value], index) => (
<option key={index} className="dark:bg-gray-700" value={value}>
{name}
</option>
))}
{options &&
options.map((option, index) => (
<option
key={index}
className="dark:bg-gray-700"
value={option.value}
>
{option.name}
</option>
))}
</select>
</InputWrapper>
</div>
);
},
);

7
src/components/layout/Sidebar/ButtonNav.tsx

@ -4,11 +4,10 @@ import { FiMessageCircle, FiSettings } from 'react-icons/fi';
import { RiMindMap, RiRoadMapLine } from 'react-icons/ri';
import { VscExtensions } from 'react-icons/vsc';
import { toggleMobileNav } from '@app/core/slices/appSlice';
import { useAppDispatch } from '@app/hooks/useAppDispatch';
import { NavLinkButton } from '@components/layout/Sidebar/NavLinkButton';
import { routes, useRoute } from '@core/router';
import { NavLinkButton } from './NavLinkButton';
import { toggleMobileNav } from '@core/slices/appSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
export interface ButtonNavProps {
toggleSettingsOpen: () => void;

5
src/components/layout/Sidebar/Settings/Channels.tsx

@ -3,9 +3,12 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Channels = (): JSX.Element => {

31
src/components/layout/Sidebar/Settings/Index.tsx

@ -15,22 +15,21 @@ import {
FiZap,
} from 'react-icons/fi';
import { CollapsibleSection } from '@app/components/layout/Sidebar/sections/CollapsibleSection';
import { ExternalSection } from '@app/components/layout/Sidebar/sections/ExternalSection';
import { SidebarOverlay } from '@app/components/layout/Sidebar/sections/SidebarOverlay';
import { Channels } from '@app/components/layout/Sidebar/Settings/Channels';
import { ExternalNotificationsSettingsPlanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/ExternalNotifications/SettingsPlanel';
import { RangeTestSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/RangeTest/SettingsPanel';
import { SerialSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/Serial/SettingsPanel';
import { StoreForwardSettingsPanel } from '@app/components/layout/Sidebar/Settings/plugins/panels/StoreForward/SettingsPanel';
import { Position } from '@app/components/layout/Sidebar/Settings/Position';
import { Power } from '@app/components/layout/Sidebar/Settings/Power';
import { Radio } from '@app/components/layout/Sidebar/Settings/Radio';
import { User } from '@app/components/layout/Sidebar/Settings/User';
import { WiFi } from '@app/components/layout/Sidebar/Settings/WiFi';
import { Interface } from './Interface';
import { ChannelsGroup } from './radio/channels/panels/ChannelsGroup';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
import { Channels } from '@components/layout/Sidebar/Settings/Channels';
import { ChannelsGroup } from '@components/layout/Sidebar/Settings/channels/ChannelsGroup';
import { Interface } from '@components/layout/Sidebar/Settings/Interface';
import { ExternalNotificationsSettingsPlanel } from '@components/layout/Sidebar/Settings/plugins/ExternalNotifications';
import { RangeTestSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/RangeTest';
import { SerialSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/Serial';
import { StoreForwardSettingsPanel } from '@components/layout/Sidebar/Settings/plugins/StoreForward';
import { Position } from '@components/layout/Sidebar/Settings/Position';
import { Power } from '@components/layout/Sidebar/Settings/Power';
import { Radio } from '@components/layout/Sidebar/Settings/Radio';
import { User } from '@components/layout/Sidebar/Settings/User';
import { WiFi } from '@components/layout/Sidebar/Settings/WiFi';
export interface SettingsProps {
open: boolean;

2
src/components/layout/Sidebar/Settings/Interface.tsx

@ -1,6 +1,6 @@
import type React from 'react';
import { Select } from '@meshtastic/components';
import { Select } from '@components/generic/form/Select';
export const Interface = (): JSX.Element => {
return (

7
src/components/layout/Sidebar/Settings/Position.tsx

@ -4,11 +4,14 @@ import { Controller, useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { MultiSelect } from 'react-multi-select-component';
import { bitwiseEncode } from '@app/core/utils/bitwise';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Label } from '@components/generic/form/Label';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { bitwiseEncode } from '@core/utils/bitwise';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Position = (): JSX.Element => {

4
src/components/layout/Sidebar/Settings/Power.tsx

@ -3,9 +3,11 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, IconButton, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Power = (): JSX.Element => {

4
src/components/layout/Sidebar/Settings/Radio.tsx

@ -3,9 +3,11 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, IconButton, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const Radio = (): JSX.Element => {

5
src/components/layout/Sidebar/Settings/User.tsx

@ -4,9 +4,12 @@ import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { base16 } from 'rfc4648';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export const User = (): JSX.Element => {

4
src/components/layout/Sidebar/Settings/WiFi.tsx

@ -3,9 +3,11 @@ import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const WiFi = (): JSX.Element => {

9
src/components/layout/Sidebar/Settings/radio/channels/panels/SettingsPanel.tsx → src/components/layout/Sidebar/Settings/channels/Channels.tsx

@ -5,9 +5,12 @@ import { useForm } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { MdRefresh, MdVisibility, MdVisibilityOff } from 'react-icons/md';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { Checkbox, IconButton, Input, Select } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { Select } from '@components/generic/form/Select';
import { connection } from '@core/connection';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface SettingsPanelProps {

9
src/components/layout/Sidebar/Settings/radio/channels/panels/ChannelsGroup.tsx → src/components/layout/Sidebar/Settings/channels/ChannelsGroup.tsx

@ -3,13 +3,12 @@ import type React from 'react';
import { FaQrcode } from 'react-icons/fa';
import { FiCode, FiSave } from 'react-icons/fi';
import { CollapsibleSection } from '@app/components/layout/Sidebar/sections/CollapsibleSection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { IconButton } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { SettingsPanel } from '@components/layout/Sidebar/Settings/channels/Channels';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { SettingsPanel } from './SettingsPanel';
export const ChannelsGroup = (): JSX.Element => {
const channels = useAppSelector((state) => state.meshtastic.radio.channels);

10
src/components/layout/Sidebar/Settings/plugins/panels/ExternalNotifications/SettingsPlanel.tsx → src/components/layout/Sidebar/Settings/plugins/ExternalNotifications.tsx

@ -3,10 +3,12 @@ import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const ExternalNotificationsSettingsPlanel = (): JSX.Element => {

10
src/components/layout/Sidebar/Settings/plugins/panels/RangeTest/SettingsPanel.tsx → src/components/layout/Sidebar/Settings/plugins/RangeTest.tsx

@ -3,10 +3,12 @@ import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const RangeTestSettingsPanel = (): JSX.Element => {

10
src/components/layout/Sidebar/Settings/plugins/panels/Serial/SettingsPanel.tsx → src/components/layout/Sidebar/Settings/plugins/Serial.tsx

@ -3,10 +3,12 @@ import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const SerialSettingsPanel = (): JSX.Element => {

10
src/components/layout/Sidebar/Settings/plugins/panels/StoreForward/SettingsPanel.tsx → src/components/layout/Sidebar/Settings/plugins/StoreForward.tsx

@ -3,10 +3,12 @@ import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { FiSave } from 'react-icons/fi';
import { Form } from '@app/components/generic/form/Form';
import { connection } from '@app/core/connection';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Checkbox, IconButton, Input } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Form } from '@components/generic/form/Form';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { useAppSelector } from '@hooks/useAppSelector';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export const StoreForwardSettingsPanel = (): JSX.Element => {

31
src/components/layout/Sidebar/Settings/plugins/panels/ExternalNotifications/DebugPanel.tsx

@ -1,31 +0,0 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const ExternalNotificationsDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
extNotificationPluginActive: preferences.extNotificationPluginActive,
extNotificationPluginAlertBell: preferences.extNotificationPluginAlertBell,
extNotificationPluginAlertMessage:
preferences.extNotificationPluginAlertMessage,
extNotificationPluginEnabled: preferences.extNotificationPluginEnabled,
extNotificationPluginOutput: preferences.extNotificationPluginOutput,
extNotificationPluginOutputMs: preferences.extNotificationPluginOutputMs,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};

27
src/components/layout/Sidebar/Settings/plugins/panels/RangeTest/DebugPanel.tsx

@ -1,27 +0,0 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const RangeTestDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
rangeTestPluginEnabled: preferences.rangeTestPluginEnabled,
rangeTestPluginSave: preferences.rangeTestPluginSave,
rangeTestPluginSender: preferences.rangeTestPluginSender,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};

30
src/components/layout/Sidebar/Settings/plugins/panels/Serial/DebugPanel.tsx

@ -1,30 +0,0 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const SerialDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
serialpluginEnabled: preferences.serialpluginEnabled,
serialpluginEcho: preferences.serialpluginEcho,
serialpluginMode: preferences.serialpluginMode,
serialpluginRxd: preferences.serialpluginRxd,
serialpluginTxd: preferences.serialpluginTxd,
serialpluginTimeout: preferences.serialpluginTimeout,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};

31
src/components/layout/Sidebar/Settings/plugins/panels/StoreForward/DebugPanel.tsx

@ -1,31 +0,0 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { CopyButton } from '@components/menu/buttons/CopyButton';
export const StoreForwardDebugPanel = (): JSX.Element => {
const preferences = useAppSelector(
(state) => state.meshtastic.radio.preferences,
);
const debugData = {
storeForwardPluginEnabled: preferences.storeForwardPluginEnabled,
storeForwardPluginHeartbeat: preferences.storeForwardPluginHeartbeat,
storeForwardPluginRecords: preferences.storeForwardPluginRecords,
storeForwardPluginHistoryReturnMax:
preferences.storeForwardPluginHistoryReturnMax,
storeForwardPluginHistoryReturnWindow:
preferences.storeForwardPluginHistoryReturnWindow,
};
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(debugData)} />
</div>
<JSONPretty className="max-w-sm" data={debugData} />
</>
);
};

21
src/components/layout/Sidebar/Settings/radio/channels/panels/DebugPanel.tsx

@ -1,21 +0,0 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface DebugPanelProps {
channel: Protobuf.Channel;
}
export const DebugPanel = ({ channel }: DebugPanelProps): JSX.Element => {
return (
<>
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(channel)} />
</div>
<JSONPretty className="max-w-sm" data={channel} />
</>
);
};

20
src/components/layout/Sidebar/Settings/radio/channels/panels/QRCodePanel.tsx

@ -1,20 +0,0 @@
import type React from 'react';
import QRCode from 'react-qr-code';
import type { Protobuf } from '@meshtastic/meshtasticjs';
export interface QRCodePanelProps {
channel: Protobuf.Channel;
}
export const QRCodePanel = ({ channel }: QRCodePanelProps): JSX.Element => {
return (
<div className="m-auto">
<QRCode
className="rounded-md"
value={`https://www.meshtastic.org/d/#${channel.index}`}
/>
</div>
);
};

7
src/components/layout/Sidebar/index.tsx

@ -1,9 +1,8 @@
import React from 'react';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { ButtonNav } from './ButtonNav';
import { Settings } from './Settings/Index';
import { ButtonNav } from '@components/layout/Sidebar/ButtonNav';
import { Settings } from '@components/layout/Sidebar/Settings/Index';
import { useAppSelector } from '@hooks/useAppSelector';
export interface SidebarProps {
children: React.ReactNode;

4
src/components/layout/index.tsx

@ -2,10 +2,10 @@ import type React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { IconButton } from '@meshtastic/components';
import { IconButton } from '@components/generic/button/IconButton';
import { Sidebar } from '@components/layout/Sidebar';
import { ErrorFallback } from '../ErrorFallback';
import { Sidebar } from './Sidebar';
export interface LayoutProps {
title: string;

4
src/components/menu/BottomNav.tsx

@ -16,15 +16,15 @@ import {
RiArrowUpLine,
} from 'react-icons/ri';
import { Tooltip } from '@components/generic/Tooltip';
import {
connType,
openConnectionModal,
setDarkModeEnabled,
toggleMobileNav,
} from '@app/core/slices/appSlice';
} from '@core/slices/appSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { Tooltip } from '@meshtastic/components';
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import { VersionInfo } from '../modals/VersionInfo';

4
src/components/menu/buttons/CopyButton.tsx

@ -3,8 +3,8 @@ import type React from 'react';
import { FiCheck, FiClipboard } from 'react-icons/fi';
import useCopyClipboard from 'react-use-clipboard';
import type { ButtonProps } from '@meshtastic/components';
import { IconButton } from '@meshtastic/components';
import type { ButtonProps } from '@components/generic/button/Button';
import { IconButton } from '@components/generic/button/IconButton';
export interface CopyButtonProps extends ButtonProps {
data: string;

56
src/components/menu/buttons/DeviceStatus.tsx

@ -1,56 +0,0 @@
import type React from 'react';
import { FiBluetooth, FiCpu, FiWifi } from 'react-icons/fi';
import { connType, openConnectionModal } from '@core/slices/appSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { Button } from '@meshtastic/components';
import { Types } from '@meshtastic/meshtasticjs';
export const DeviceStatus = (): JSX.Element => {
const dispatch = useAppDispatch();
const appState = useAppSelector((state) => state.app);
const state = useAppSelector((state) => state.meshtastic);
return (
<Button
active
onClick={(): void => {
dispatch(dispatch(openConnectionModal()));
}}
>
<div className="flex gap-2 px-2">
<div
className={`
my-auto h-2 w-2 min-w-[2] rounded-full ${
[
Types.DeviceStatusEnum.DEVICE_CONNECTED,
Types.DeviceStatusEnum.DEVICE_CONFIGURED,
].includes(state.deviceStatus)
? 'bg-green-400'
: [
Types.DeviceStatusEnum.DEVICE_CONNECTING,
Types.DeviceStatusEnum.DEVICE_RECONNECTING,
Types.DeviceStatusEnum.DEVICE_CONFIGURING,
].includes(state.deviceStatus)
? 'bg-yellow-400'
: 'bg-gray-400'
}`}
></div>
<div className="my-auto">
{state.nodes.find(
(node) => node.number === state.radio.hardware.myNodeNum,
)?.user?.longName ?? 'Disconnected'}
</div>
{appState.connType === connType.BLE ? (
<FiBluetooth className="h-5 w-5" />
) : appState.connType === connType.SERIAL ? (
<FiCpu className="h-5 w-5" />
) : (
<FiWifi className="h-5 w-5" />
)}
</div>
</Button>
);
};

5
src/components/modals/VersionInfo.tsx

@ -3,7 +3,8 @@ import React from 'react';
import { AnimatePresence } from 'framer-motion';
import { Modal } from '@components/generic/Modal';
import { Card } from '@meshtastic/components';
import { Card } from '../generic/Card';
export interface VersionInfoProps {
visible: boolean;
@ -49,7 +50,7 @@ export const VersionInfo = ({
onclose();
}}
>
<Card>
<Card className="relative">
<div className="w-full max-w-3xl p-10">Version Info</div>
{/* {data?.sha} */}
</Card>

2
src/core/slices/mapSlice.ts

@ -1,6 +1,6 @@
import mapboxgl from 'mapbox-gl';
import type { MapStyleName } from '@app/pages/Map/styles';
import type { MapStyleName } from '@pages/Map/styles';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';

15
src/core/slices/meshtasticSlice.ts

@ -85,8 +85,6 @@ export const meshtasticSlice = createSlice({
initialState,
reducers: {
addLogEvent: (state, action: PayloadAction<Types.LogEventPacket>) => {
console.log(action.payload.packet);
state.logs.push(action.payload);
},
setDeviceStatus: (state, action: PayloadAction<Types.DeviceStatusEnum>) => {
@ -155,8 +153,6 @@ export const meshtasticSlice = createSlice({
);
if (node) {
console.log('node exists');
node.lastHeard = new Date(action.payload.lastHeard * 1000);
node.snr.push(action.payload.snr);
} else {
@ -204,23 +200,14 @@ export const meshtasticSlice = createSlice({
state.radio.preferences = action.payload;
},
addMessage: (state, action: PayloadAction<MessageWithAck>) => {
console.log(action.payload);
console.log(
`${action.payload.message.packet.from} -> ${action.payload.message.packet.to}`,
);
state.chats[action.payload.message.packet.channel].lastInterraction =
new Date();
if (action.payload.message.packet.to === 0xffffffff) {
console.log('boradcast');
state.chats[action.payload.message.packet.channel].messages.push(
action.payload,
);
} else {
console.log('dm');
const dmIndex =
action.payload.message.packet.from === state.radio.hardware.myNodeNum
? action.payload.message.packet.to
@ -233,8 +220,6 @@ export const meshtasticSlice = createSlice({
state,
action: PayloadAction<{ chatIndex: number; messageId: number }>,
) => {
console.log(action.payload);
state.chats[action.payload.chatIndex].messages.map((message) => {
if (message.message.packet.id === action.payload.messageId) {
message.ack = true;

7
src/core/store.ts

@ -1,9 +1,8 @@
import appReducer from '@core/slices/appSlice';
import mapReducer from '@core/slices/mapSlice';
import meshtasticReducer from '@core/slices/meshtasticSlice';
import { configureStore } from '@reduxjs/toolkit';
import appReducer from './slices/appSlice';
import mapReducer from './slices/mapSlice';
import meshtasticReducer from './slices/meshtasticSlice';
export const store = configureStore({
reducer: {
app: appReducer,

6
src/index.tsx

@ -1,4 +1,3 @@
import '@meshtastic/components/dist/style.css';
import '@app/index.css';
import React from 'react';
@ -9,12 +8,11 @@ import { ErrorBoundary } from 'react-error-boundary';
import { Provider } from 'react-redux';
import { App } from '@app/App';
import { ErrorFallback } from '@components/ErrorFallback';
import { ReloadPrompt } from '@components/pwa/ReloadPrompt';
import { RouteProvider } from '@core/router';
import { store } from '@core/store';
import { ErrorFallback } from './components/ErrorFallback';
import { RouteProvider } from './core/router';
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary FallbackComponent={ErrorFallback}>

11
src/pages/Extensions/FileBrowser.tsx

@ -3,8 +3,9 @@ import React from 'react';
import { AnimatePresence, m } from 'framer-motion';
import useSWR from 'swr';
import fetcher from '@app/core/utils/fetcher';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Card } from '@app/components/generic/Card';
import fetcher from '@core/utils/fetcher';
import { useAppSelector } from '@hooks/useAppSelector';
export interface File {
nameModified: string;
@ -38,8 +39,8 @@ export const FileBrowser = (): JSX.Element => {
);
return (
<div className="flex h-full w-full select-none flex-col gap-4 p-4">
<div className="w-full flex-grow rounded-md bg-white dark:bg-primaryDark">
<div className="flex h-full p-4">
<Card className="flex-grow flex-col">
<div className="flex h-10 w-full rounded-t-md border-b border-gray-300 px-4 text-lg font-semibold shadow-md dark:border-gray-600 dark:bg-zinc-700 dark:text-white">
<div className="my-auto w-1/3">FileName</div>
<div className="my-auto w-1/3">Actions</div>
@ -80,7 +81,7 @@ export const FileBrowser = (): JSX.Element => {
</div>
))}
</div>
</div>
</Card>
</div>
);
};

11
src/pages/Extensions/Index.tsx

@ -5,12 +5,11 @@ import { MdSubject } from 'react-icons/md';
import { RiPinDistanceFill } from 'react-icons/ri';
import { VscExtensions } from 'react-icons/vsc';
import { Layout } from '@app/components/layout';
import { ExternalSection } from '@app/components/layout/Sidebar/sections/ExternalSection';
import { FileBrowser } from './FileBrowser';
import { Info } from './Info';
import { Logs } from './Logs';
import { ExternalSection } from '@components/generic/Sidebar/ExternalSection';
import { Layout } from '@components/layout';
import { FileBrowser } from '@pages/Extensions/FileBrowser';
import { Info } from '@pages/Extensions/Info';
import { Logs } from '@pages/Extensions/Logs';
export const Extensions = (): JSX.Element => {
const [selectedExtension, setSelectedExtension] = React.useState<

19
src/pages/Extensions/Info.tsx

@ -2,8 +2,9 @@ import React from 'react';
import JSONPretty from 'react-json-pretty';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Card } from '@app/components/generic/Card';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { useAppSelector } from '@hooks/useAppSelector';
export const Info = (): JSX.Element => {
const hardwareInfo = useAppSelector(
@ -17,18 +18,18 @@ export const Info = (): JSX.Element => {
return (
<div className="flex h-full flex-col gap-4 p-4 md:flex-row">
<div className="flex w-full flex-col gap-4 rounded-md bg-white p-8 shadow-md dark:bg-primaryDark md:w-1/4">
<div className="m-auto">
<Card className="md:w-1/4">
<div className="m-auto flex flex-col gap-2">
<Hashicon value={hardwareInfo.myNodeNum.toString()} size={180} />
<div className="text-center text-lg font-medium dark:text-white">
{node?.user?.longName || 'Unknown'}
</div>
</div>
<div className="text-center text-lg font-medium dark:text-white">
{node?.user?.longName || 'Unknown'}
</div>
</div>
</Card>
<div className="flex-grow rounded-md bg-white p-8 shadow-md dark:bg-primaryDark">
<Card className="flex-grow">
<JSONPretty className="overflow-y-auto" data={hardwareInfo} />
</div>
</Card>
</div>
);
};

199
src/pages/Extensions/Logs.tsx

@ -3,7 +3,8 @@ import type React from 'react';
import { AnimatePresence, m } from 'framer-motion';
import { FiArrowRight, FiPaperclip } from 'react-icons/fi';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { Card } from '@app/components/generic/Card';
import { useAppSelector } from '@hooks/useAppSelector';
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
export const Logs = (): JSX.Element => {
@ -13,28 +14,27 @@ export const Logs = (): JSX.Element => {
type lookupType = { [key: number]: string };
const emitterLookup: lookupType = {
[Types.Emitter.sendPacket]: 'text-blue-500',
[Types.Emitter.sendText]: 'text-blue-500',
[Types.Emitter.sendPacket]: 'text-blue-500',
[Types.Emitter.sendRaw]: 'text-blue-500',
[Types.Emitter.setPreferences]: 'text-blue-500',
[Types.Emitter.confirmSetPreferences]: 'text-blue-500',
[Types.Emitter.setOwner]: 'text-blue-500',
[Types.Emitter.sendText]: 'text-rose-500',
[Types.Emitter.sendPacket]: 'text-pink-500',
[Types.Emitter.sendRaw]: 'text-fuchsia-500',
[Types.Emitter.setPreferences]: 'text-purple-500',
[Types.Emitter.confirmSetPreferences]: 'text-violet-500',
[Types.Emitter.setOwner]: 'text-indigo-500',
[Types.Emitter.setChannel]: 'text-blue-500',
[Types.Emitter.confirmSetChannel]: 'text-blue-500',
[Types.Emitter.deleteChannel]: 'text-blue-500',
[Types.Emitter.getChannel]: 'text-blue-500',
[Types.Emitter.getAllChannels]: 'text-blue-500',
[Types.Emitter.getPreferences]: 'text-blue-500',
[Types.Emitter.getOwner]: 'text-blue-500',
[Types.Emitter.configure]: 'text-blue-500',
[Types.Emitter.handleFromRadio]: 'text-blue-500',
[Types.Emitter.handleMeshPacket]: 'text-blue-500',
[Types.Emitter.connect]: 'text-blue-500',
[Types.Emitter.ping]: 'text-blue-500',
[Types.Emitter.readFromRadio]: 'text-blue-500',
[Types.Emitter.writeToRadio]: 'text-blue-500',
[Types.Emitter.setDebugMode]: 'text-blue-500',
[Types.Emitter.confirmSetChannel]: 'text-sky-500',
[Types.Emitter.deleteChannel]: 'text-cyan-500',
[Types.Emitter.getChannel]: 'text-teal-500',
[Types.Emitter.getAllChannels]: 'text-emerald-500',
[Types.Emitter.getPreferences]: 'text-green-500',
[Types.Emitter.getOwner]: 'text-lime-500',
[Types.Emitter.configure]: 'text-yellow-500',
[Types.Emitter.handleFromRadio]: 'text-amber-500',
[Types.Emitter.handleMeshPacket]: 'text-orange-500',
[Types.Emitter.connect]: 'text-red-500',
[Types.Emitter.ping]: 'text-stone-500',
[Types.Emitter.readFromRadio]: 'text-zinc-500',
[Types.Emitter.writeToRadio]: 'text-gray-500',
[Types.Emitter.setDebugMode]: 'text-slate-500',
};
const levelLookup: lookupType = {
@ -48,84 +48,87 @@ export const Logs = (): JSX.Element => {
};
return (
<div className="flex h-full p-4">
<table className="table-cell h-full w-full select-none rounded-md bg-white dark:bg-primaryDark">
<tbody
className="
block h-full flex-col overflow-y-auto py-4 px-2 font-mono text-xs dark:text-gray-400"
>
<AnimatePresence>
{logs.length === 0 && (
<div className="flex h-full w-full">
<m.img
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="m-auto h-64 w-64 text-green-500"
src={`/placeholders/${
darkMode ? 'View Code Dark.svg' : 'View Code.svg'
}`}
/>
</div>
)}
</AnimatePresence>
{logs.map((log, index) => (
// <ContextMenu
// key={index}
// items={
// <>
// <ContextItem title="Test" icon={<FiGitBranch />} />
// </>
// }
// >
<tr
key={index}
className="group hover:bg-gray-200 dark:hover:bg-secondaryDark"
>
<m.td
className="w-6 cursor-pointer"
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
>
<div className="m-auto pl-2 text-white group-hover:text-black dark:text-primaryDark dark:group-hover:text-gray-400">
<FiArrowRight />
</div>
</m.td>
<Wrapper>
{log.date
.toLocaleString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
.replaceAll('/', '-')
.replace(',', '')}
</Wrapper>
<Wrapper>
<div className={emitterLookup[log.emitter]}>
[{Types.EmitterScope[log.scope]}.{Types.Emitter[log.emitter]}]
<div className="flex h-full flex-col gap-4 p-4">
<Card className="flex-grow">
<table className="table-cell flex-grow">
<tbody
className="
block h-full flex-col overflow-y-auto font-mono text-xs dark:text-gray-400"
>
<AnimatePresence>
{logs.length === 0 && (
<div className="flex h-full w-full">
<m.img
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="m-auto h-64 w-64 text-green-500"
src={`/placeholders/${
darkMode ? 'View Code Dark.svg' : 'View Code.svg'
}`}
/>
</div>
</Wrapper>
<Wrapper className={levelLookup[log.level]}>
[{Protobuf.LogRecord_Level[log.level]}]{/* </div> */}
</Wrapper>
<td
className={`m-auto ${
log.packet ? '' : 'dark:text-secondaryDark'
}`}
)}
</AnimatePresence>
{logs.map((log, index) => (
// <ContextMenu
// key={index}
// items={
// <>
// <ContextItem title="Test" icon={<FiGitBranch />} />
// </>
// }
// >
<tr
key={index}
className="group hover:bg-gray-200 dark:hover:bg-secondaryDark"
>
<FiPaperclip />
</td>
<td className="truncate pl-1">{log.message}</td>
</tr>
// </ContextMenu>
))}
</tbody>
</table>
<m.td
className="w-6 cursor-pointer"
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
>
<div className="m-auto pl-2 text-white group-hover:text-black dark:text-primaryDark dark:group-hover:text-gray-400">
<FiArrowRight />
</div>
</m.td>
<Wrapper>
{log.date
.toLocaleString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
.replaceAll('/', '-')
.replace(',', '')}
</Wrapper>
<Wrapper>
<div className={emitterLookup[log.emitter]}>
[{Types.EmitterScope[log.scope]}.
{Types.Emitter[log.emitter]}]
</div>
</Wrapper>
<Wrapper className={levelLookup[log.level]}>
[{Protobuf.LogRecord_Level[log.level]}]{/* </div> */}
</Wrapper>
<td
className={`m-auto ${
log.packet ? '' : 'dark:text-secondaryDark'
}`}
>
<FiPaperclip />
</td>
<td className="w-full truncate pl-1">{log.message}</td>
</tr>
// </ContextMenu>
))}
</tbody>
</table>
</Card>
</div>
);
};

7
src/pages/Map/MapContainer.tsx

@ -3,6 +3,7 @@ import React from 'react';
import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa';
import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md';
import { IconButton } from '@components/generic/button/IconButton';
import {
setExaggeration,
setHillShade,
@ -11,10 +12,8 @@ import {
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { useMapbox } from '@hooks/useMapbox';
import { IconButton } from '@meshtastic/components';
import type { MapStyle } from './styles';
import { MapStyles } from './styles';
import type { MapStyle } from '@pages/Map/styles';
import { MapStyles } from '@pages/Map/styles';
export const MapContainer = (): JSX.Element => {
const dispatch = useAppDispatch();

15
src/pages/Map/index.tsx

@ -4,14 +4,13 @@ import mapboxgl from 'mapbox-gl';
import { FiMapPin } from 'react-icons/fi';
import { RiRoadMapLine } from 'react-icons/ri';
import { Layout } from '@app/components/layout';
import { MapboxProvider } from '@app/components/MapBox/MapboxProvider';
import type { Node } from '@app/core/slices/meshtasticSlice';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { NodeCard } from '../Nodes/NodeCard';
import { MapContainer } from './MapContainer';
import { Marker } from './Marker';
import { Layout } from '@components/layout';
import { MapboxProvider } from '@components/MapBox/MapboxProvider';
import type { Node } from '@core/slices/meshtasticSlice';
import { useAppSelector } from '@hooks/useAppSelector';
import { MapContainer } from '@pages/Map/MapContainer';
import { Marker } from '@pages/Map/Marker';
import { NodeCard } from '@pages/Nodes/NodeCard';
export const Map = (): JSX.Element => {
const [selectedNode, setSelectedNode] = React.useState<Node>();

3
src/pages/Messages/ChannelChat.tsx

@ -5,10 +5,11 @@ import { FiSettings } from 'react-icons/fi';
import { MdPublic } from 'react-icons/md';
import TimeAgo from 'timeago-react';
import { IconButton } from '@components/generic/button/IconButton';
import { Tooltip } from '@components/generic/Tooltip';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { useAppSelector } from '@hooks/useAppSelector';
import { IconButton, Tooltip } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface ChannelChatProps {

2
src/pages/Messages/DmChat.tsx

@ -2,10 +2,10 @@ import React from 'react';
import { FiSettings } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import type { Node } from '@core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { IconButton } from '@meshtastic/components';
export interface DmChatProps {
node: Node;

4
src/pages/Messages/Message.tsx

@ -2,9 +2,9 @@ import type React from 'react';
import { FiClock } from 'react-icons/fi';
import type { Node } from '@app/core/slices/meshtasticSlice';
import { Tooltip } from '@components/generic/Tooltip';
import type { Node } from '@core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { Tooltip } from '@meshtastic/components';
export interface MessageProps {
lastMsgSameUser: boolean;

2
src/pages/Messages/MessageBar.tsx

@ -1,10 +1,10 @@
import React from 'react';
import { Input } from '@components/generic/form/Input';
import { connection } from '@core/connection';
import { ackMessage } from '@core/slices/meshtasticSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { Input } from '@meshtastic/components';
export interface MessageBarProps {
chatIndex: number;

21
src/pages/Messages/index.tsx

@ -2,15 +2,14 @@ import React from 'react';
import { FiHash, FiMessageCircle } from 'react-icons/fi';
import { Layout } from '@app/components/layout';
import { IconButton } from '@components/generic/button/IconButton';
import { Layout } from '@components/layout';
import { useAppSelector } from '@hooks/useAppSelector';
import { IconButton } from '@meshtastic/components';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { ChannelChat } from './ChannelChat';
import { DmChat } from './DmChat';
import { Message } from './Message';
import { MessageBar } from './MessageBar';
import { ChannelChat } from '@pages/Messages/ChannelChat';
import { DmChat } from '@pages/Messages/DmChat';
import { Message } from '@pages/Messages/Message';
import { MessageBar } from '@pages/Messages/MessageBar';
export const Messages = (): JSX.Element => {
const [selectedChatIndex, setSelectedChatIndex] = React.useState<number>(0);
@ -105,11 +104,9 @@ export const Messages = (): JSX.Element => {
: chats[selectedChatIndex].messages[index - 1].message
.packet.from === message.message.packet.from
}
sender={nodes.find((node) => {
console.log(message);
return node.number === message.message.packet.from;
})}
sender={nodes.find(
(node) => node.number === message.message.packet.from,
)}
/>
))}
</div>

30
src/pages/Nodes/NodeCard.tsx

@ -12,14 +12,14 @@ import { IoTelescope } from 'react-icons/io5';
import { MdGpsFixed, MdGpsNotFixed, MdGpsOff } from 'react-icons/md';
import JSONPretty from 'react-json-pretty';
import { CollapsibleSection } from '@app/components/layout/Sidebar/sections/CollapsibleSection';
import { SidebarOverlay } from '@app/components/layout/Sidebar/sections/SidebarOverlay';
import { SidebarItem } from '@app/components/layout/Sidebar/SidebarItem';
import { CopyButton } from '@app/components/menu/buttons/CopyButton';
import { IconButton } from '@components/generic/button/IconButton';
import { CollapsibleSection } from '@components/generic/Sidebar/CollapsibleSection';
import { SidebarOverlay } from '@components/generic/Sidebar/SidebarOverlay';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import type { Node } from '@core/slices/meshtasticSlice';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { useMapbox } from '@hooks/useMapbox';
import { IconButton } from '@meshtastic/components';
type PositionConfidence = 'high' | 'low' | 'none';
@ -126,7 +126,25 @@ export const NodeCard = ({
</div>
</CollapsibleSection>
<CollapsibleSection title="Location" icon={<FiMapPin />}>
<div>Info</div>
<>
{node.currentPosition && (
<div className="flex h-10 select-none justify-between rounded-md border border-gray-300 bg-transparent bg-gray-200 px-1 text-gray-500 dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 ">
<div className="my-auto px-1">
{(node.currentPosition.latitudeI / 1e7).toPrecision(6)},&nbsp;
{(node.currentPosition?.longitudeI / 1e7).toPrecision(6)}
</div>
<CopyButton
data={
node.currentPosition
? `${node.currentPosition.latitudeI / 1e7},${
node.currentPosition.longitudeI / 1e7
}`
: ''
}
/>
</div>
)}
</>
</CollapsibleSection>
<CollapsibleSection title="Line of Sight" icon={<IoTelescope />}>
<div>Info</div>

8
src/pages/Nodes/index.tsx

@ -5,11 +5,11 @@ import ReactFlow, { Background, Controls, MiniMap } from 'react-flow-renderer';
import { FiSettings } from 'react-icons/fi';
import { RiMindMap } from 'react-icons/ri';
import { Layout } from '@app/components/layout';
import { SidebarItem } from '@app/components/layout/Sidebar/SidebarItem';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { IconButton } from '@components/generic/button/IconButton';
import { Layout } from '@components/layout';
import { SidebarItem } from '@components/layout/Sidebar/SidebarItem';
import { Hashicon } from '@emeraldpay/hashicon-react';
import { IconButton } from '@meshtastic/components';
import { useAppSelector } from '@hooks/useAppSelector';
export const Nodes = (): JSX.Element => {
const [graphNodes, setGraphNodes] = React.useState<Node[]>([]);

21
src/pages/Nodes/panels/DebugPanel.tsx

@ -1,21 +0,0 @@
import type React from 'react';
import JSONPretty from 'react-json-pretty';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import type { Node } from '@core/slices/meshtasticSlice';
export interface DebugPanelProps {
node: Node;
}
export const DebugPanel = ({ node }: DebugPanelProps): JSX.Element => {
return (
<div className="relative">
<div className="fixed right-0 m-2">
<CopyButton data={JSON.stringify(node)} />
</div>
<JSONPretty className="max-w-sm" data={node} />
</div>
);
};

32
src/pages/Nodes/panels/PositionPanel.tsx

@ -1,32 +0,0 @@
import type React from 'react';
import { CopyButton } from '@components/menu/buttons/CopyButton';
import type { Node } from '@core/slices/meshtasticSlice';
export interface PositionPanelProps {
node: Node;
}
export const PositionPanel = ({ node }: PositionPanelProps): JSX.Element => {
return (
<div className="p-2">
{node.currentPosition && (
<div className="flex h-10 select-none justify-between rounded-md border border-gray-300 bg-transparent bg-gray-200 px-1 text-gray-500 dark:border-gray-600 dark:bg-secondaryDark dark:text-gray-400 ">
<div className="my-auto px-1">
{(node.currentPosition.latitudeI / 1e7).toPrecision(6)},&nbsp;
{(node.currentPosition?.longitudeI / 1e7).toPrecision(6)}
</div>
<CopyButton
data={
node.currentPosition
? `${node.currentPosition.latitudeI / 1e7},${
node.currentPosition.longitudeI / 1e7
}`
: ''
}
/>
</div>
)}
</div>
);
};

9
src/pages/NotFound.tsx

@ -1,13 +1,12 @@
import type React from 'react';
import { Card } from '@meshtastic/components';
import { Card } from '@app/components/generic/Card';
export const NotFound = (): JSX.Element => {
return (
<Card
title="The requested file or directory could not be found"
description="Better luck next time"
>
<Card>
<h3>The requested file or directory could not be found</h3>
<h4>Better luck next time</h4>
<br />
<br />
</Card>

Loading…
Cancel
Save