Switch to Vite + improvements

This commit is contained in:
Sacha Weatherstone
2021-11-12 00:03:54 +11:00
parent c684f6ad5f
commit 02c0e22fe4
48 changed files with 1848 additions and 264 deletions
+1 -1
View File
@@ -1 +1 @@
SNOWPACK_PUBLIC_DEVICE_IP= VITE_PUBLIC_DEVICE_IP=
+2 -2
View File
@@ -33,7 +33,7 @@ cp ./.env.example ./.env
And define the device IP address in the `.env` file. And define the device IP address in the `.env` file.
``` ```
SNOWPACK_PUBLIC_DEVICE_IP=xxx.xxx.xxx.xxx VITE_PUBLIC_DEVICE_IP=xxx.xxx.xxx.xxx
``` ```
Install the dependencies. Install the dependencies.
@@ -45,5 +45,5 @@ pnpm i
Start the developtment server: Start the developtment server:
```bash ```bash
pnpm start pnpm dev
``` ```
+8 -5
View File
@@ -2,7 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" type="image/svg+xml" />
<link
rel="alternate icon"
href="/safari-tab.svg"
type="image/png"
sizes="16x16"
/>
<link rel="apple-touch-icon" sizes="180x180" href="/touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
@@ -14,10 +20,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
/> />
<meta <meta name="description" content="Meshtastic Web App" />
name="description"
content="Web site created using create-snowpack-app"
/>
<title>Meshtastic Web</title> <title>Meshtastic Web</title>
</head> </head>
<body> <body>
+9 -8
View File
@@ -7,13 +7,13 @@
"dev": "NODE_ENV=development vite", "dev": "NODE_ENV=development vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,woff,woff2 dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)", "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}'", "format": "prettier --write 'src/**/*.{ts,tsx}'",
"lint": "eslint 'src/**/*.{ts,tsx}'" "lint": "eslint 'src/**/*.{ts,tsx}'"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.4.2", "@headlessui/react": "^1.4.2",
"@meshtastic/meshtasticjs": "^0.6.25", "@meshtastic/meshtasticjs": "^0.6.26",
"@reduxjs/toolkit": "^1.6.2", "@reduxjs/toolkit": "^1.6.2",
"boring-avatars": "^1.5.8", "boring-avatars": "^1.5.8",
"i18next": "^21.4.2", "i18next": "^21.4.2",
@@ -22,8 +22,8 @@
"react-apexcharts": "^1.3.9", "react-apexcharts": "^1.3.9",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-file-icon": "^1.1.0", "react-file-icon": "^1.1.0",
"react-hook-form": "^7.19.1", "react-hook-form": "^7.19.4",
"react-i18next": "^11.14.0", "react-i18next": "^11.14.1",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-json-pretty": "^2.2.0", "react-json-pretty": "^2.2.0",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
@@ -38,7 +38,6 @@
"@types/react-file-icon": "^1.0.1", "@types/react-file-icon": "^1.0.1",
"@types/react-redux": "^7.1.20", "@types/react-redux": "^7.1.20",
"@types/react-timeago": "^4.1.3", "@types/react-timeago": "^4.1.3",
"@types/snowpack-env": "^2.3.4",
"@types/w3c-web-serial": "^1.0.2", "@types/w3c-web-serial": "^1.0.2",
"@types/web-bluetooth": "^0.0.11", "@types/web-bluetooth": "^0.0.11",
"@typescript-eslint/eslint-plugin": "^5.3.1", "@typescript-eslint/eslint-plugin": "^5.3.1",
@@ -51,8 +50,8 @@
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-babel-module": "^5.3.1", "eslint-import-resolver-babel-module": "^5.3.1",
"eslint-import-resolver-typescript": "^2.5.0", "eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.2", "eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.26.1", "eslint-plugin-react": "^7.27.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"gzipper": "^6.0.0", "gzipper": "^6.0.0",
"postcss": "^8.3.11", "postcss": "^8.3.11",
@@ -60,6 +59,8 @@
"tailwindcss": "^3.0.0-alpha.2", "tailwindcss": "^3.0.0-alpha.2",
"tar": "^6.1.11", "tar": "^6.1.11",
"typescript": "^4.4.4", "typescript": "^4.4.4",
"vite": "^2.6.14" "vite": "^2.6.14",
"vite-plugin-pwa": "^0.11.3",
"workbox-window": "^6.3.0"
} }
} }
+1578 -74
View File
File diff suppressed because it is too large Load Diff
-38
View File
@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-tab.svg" color="#67ea94" />
<link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
<link href="https://fonts.gstatic.com" rel="preconnect" />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<meta name="theme-color" content="#67ea94" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
/>
<meta
name="description"
content="Web site created using create-snowpack-app"
/>
<title>Meshtastic Web</title>
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/index.js"></script>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
+5 -2
View File
@@ -13,6 +13,7 @@ import {
addChannel, addChannel,
addMessage, addMessage,
addNode, addNode,
addPosition,
addUser, addUser,
setDeviceStatus, setDeviceStatus,
setLastMeshInterraction, setLastMeshInterraction,
@@ -82,9 +83,11 @@ const App = (): JSX.Element => {
}); });
connection.onUserPacket.subscribe((user) => { connection.onUserPacket.subscribe((user) => {
console.log('got user packet'); dispatch(addUser(user));
});
dispatch(addUser(user.data)); connection.onPositionPacket.subscribe((position) => {
dispatch(addPosition(position));
}); });
connection.onNodeInfoPacket.subscribe( connection.onNodeInfoPacket.subscribe(
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import Avatar from 'boring-avatars'; import Avatar from 'boring-avatars';
+5 -1
View File
@@ -15,6 +15,7 @@ export const MessageBar = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const ready = useAppSelector((state) => state.meshtastic.ready); const ready = useAppSelector((state) => state.meshtastic.ready);
const nodes = useAppSelector((state) => state.meshtastic.nodes); const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo); const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo);
const [currentMessage, setCurrentMessage] = React.useState(''); const [currentMessage, setCurrentMessage] = React.useState('');
const [destinationNode, setDestinationNode] = const [destinationNode, setDestinationNode] =
@@ -52,8 +53,11 @@ export const MessageBar = (): JSX.Element => {
...nodes ...nodes
.filter((node) => node.num !== myNodeInfo.myNodeNum) .filter((node) => node.num !== myNodeInfo.myNodeNum)
.map((node) => { .map((node) => {
const user = users.filter(
(user) => user.packet.from === node.num,
)[0]?.data;
return { return {
name: node.user?.shortName ?? node.num, name: user ? user.shortName : node.num,
value: node.num, value: node.num,
}; };
}), }),
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div']; type DefaultDivProps = JSX.IntrinsicElements['div'];
-46
View File
@@ -67,49 +67,3 @@ export const Button = ({
</button> </button>
); );
}; };
// import React from 'react';
// type DefaultButtonProps = JSX.IntrinsicElements['button'];
// export interface ButtonProps extends DefaultButtonProps {
// icon?: JSX.Element;
// circle?: boolean;
// active?: boolean;
// border?: boolean;
// confirmAction?: () => void;
// rightIcon?: React.ReactNode;
// leftIcon?: React.ReactNode;
// nested?: boolean;
// }
// export const Button = ({
// rightIcon,
// leftIcon,
// children,
// nested,
// ...props
// }: ButtonProps): JSX.Element => {
// return (
// <button
// className={`select-none flex hover:bg-gray-300 dark:hover:bg-gray-500 rounded-md cursor-pointer active:scale-95 dark:text-white ${
// nested
// ? 'hover:bg-gray-300 dark:hover:bg-gray-500 dark:bg-gray-600 bg-gray-200'
// : 'dark:bg-gray-700 bg-gray-200'
// }`}
// {...props}
// >
// {leftIcon && (
// <div className="flex py-1 bg-gray-00 rounded-l-md">
// <div className="mx-2 my-auto">{leftIcon}</div>
// </div>
// )}
// <div className="flex px-4 py-2 space-x-2 leading-4">{children}</div>
// {rightIcon && (
// <div className="flex py-2 bg-gray-200 rounded-r-md">
// <div className="mx-2 my-auto">{rightIcon}</div>
// </div>
// )}
// </button>
// );
// };
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div']; type DefaultDivProps = JSX.IntrinsicElements['div'];
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
export interface CoverProps { export interface CoverProps {
content: JSX.Element; content: JSX.Element;
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { Blur } from '@components/generic/Blur'; import { Blur } from '@components/generic/Blur';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
type DefaulButtonProps = JSX.IntrinsicElements['button']; type DefaulButtonProps = JSX.IntrinsicElements['button'];
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div']; type DefaultDivProps = JSX.IntrinsicElements['div'];
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
export interface StatCardProps { export interface StatCardProps {
title: string; title: string;
+1 -1
View File
@@ -34,7 +34,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
? 'border-red-500' ? 'border-red-500'
: props.disabled : props.disabled
? 'border-gray-200' ? 'border-gray-200'
: ' focus-within:border-primary hover:border-primary' : 'focus-within:border-primary dark:focus-within:border-primary hover:border-primary dark:hover:border-primary'
}`} }`}
{...props} {...props}
/> />
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
export interface LabelProps { export interface LabelProps {
error?: string; error?: string;
@@ -21,7 +21,7 @@ export const InputWrapper = ({
? 'border-red-500' ? 'border-red-500'
: disabled : disabled
? 'border-gray-200' ? 'border-gray-200'
: ' focus-within:border-primary hover:border-primary' : ' focus-within:border-primary dark:focus-within:border-primary hover:border-primary dark:hover:border-primary'
}`} }`}
> >
{children} {children}
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
export interface LabelProps { export interface LabelProps {
label: string; label: string;
+2 -2
View File
@@ -29,8 +29,8 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
<InputWrapper> <InputWrapper>
<select <select
ref={ref} ref={ref}
className={`w-full bg-white dark:bg-transparent focus:outline-none focus:border-primary ${ className={`w-full rounded-md bg-white dark:bg-transparent focus:outline-none focus:border-primary ${
small ? 'py-1 mx-1' : 'h-10 mx-2' small ? 'p-1' : 'h-10 px-2'
}`} }`}
disabled={ disabled={
props.disabled props.disabled
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
export const Logo = (): JSX.Element => { export const Logo = (): JSX.Element => {
return ( return (
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { useAppDispatch, useAppSelector } from '@app/hooks/redux'; import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
import { Drawer } from '@components/generic/Drawer'; import { Drawer } from '@components/generic/Drawer';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { import {
FiGrid, FiGrid,
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { FiWifi, FiWifiOff } from 'react-icons/fi'; import { FiWifi, FiWifiOff } from 'react-icons/fi';
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { FiMenu } from 'react-icons/fi'; import { FiMenu } from 'react-icons/fi';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { FiMoon, FiSun } from 'react-icons/fi'; import { FiMoon, FiSun } from 'react-icons/fi';
+29
View File
@@ -0,0 +1,29 @@
.ReloadPrompt-container {
padding: 0;
margin: 0;
width: 0;
height: 0;
}
.ReloadPrompt-toast {
position: fixed;
right: 0;
bottom: 0;
margin: 16px;
padding: 12px;
border: 1px solid #8885;
border-radius: 4px;
z-index: 1;
text-align: left;
box-shadow: 3px 4px 5px 0 #8885;
background-color: white;
}
.ReloadPrompt-toast-message {
margin-bottom: 8px;
}
.ReloadPrompt-toast-button {
border: 1px solid #8885;
outline: none;
margin-right: 5px;
border-radius: 2px;
padding: 3px 10px;
}
+61
View File
@@ -0,0 +1,61 @@
import './ReloadPrompt.css';
// eslint-disable-next-line no-use-before-define
import type React from 'react';
import { useRegisterSW } from 'virtual:pwa-register/react';
const ReloadPrompt = (): JSX.Element => {
const {
offlineReady: [offlineReady, setOfflineReady],
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegistered(r) {
// eslint-disable-next-line prefer-template
console.log(`SW Registered:`, r);
},
onRegisterError(error) {
console.log('SW registration error', error);
},
});
const close = (): void => {
setOfflineReady(false);
setNeedRefresh(false);
};
return (
<div className="ReloadPrompt-container">
{(offlineReady || needRefresh) && (
<div className="ReloadPrompt-toast">
<div className="ReloadPrompt-message">
{offlineReady ? (
<span>App ready to work offline</span>
) : (
<span>
New content available, click on reload button to update.
</span>
)}
</div>
{needRefresh && (
<button
className="ReloadPrompt-toast-button"
onClick={(): Promise<void> => updateServiceWorker(true)}
>
Reload
</button>
)}
<button
className="ReloadPrompt-toast-button"
onClick={(): void => close()}
>
Close
</button>
</div>
)}
</div>
);
};
export default ReloadPrompt;
+1 -2
View File
@@ -5,8 +5,7 @@ import { FiXCircle } from 'react-icons/fi';
import { useBreakpoint } from '@app/hooks/breakpoint'; import { useBreakpoint } from '@app/hooks/breakpoint';
import { Drawer } from '@components/generic/Drawer'; import { Drawer } from '@components/generic/Drawer';
import { IconButton } from '@components/generic/IconButton'; import { IconButton } from '@components/generic/IconButton';
import type { SidebarItemProps } from '@components/generic/SidebarItem'; import { SidebarItem, SidebarItemProps } from '@components/generic/SidebarItem';
import { SidebarItem } from '@components/generic/SidebarItem';
import { Tab } from '@headlessui/react'; import { Tab } from '@headlessui/react';
export interface PageLayoutProps { export interface PageLayoutProps {
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
export interface PrimaryTemplateProps { export interface PrimaryTemplateProps {
children: React.ReactNode; children: React.ReactNode;
+10 -11
View File
@@ -22,8 +22,7 @@ interface MeshtasticState {
ready: boolean; ready: boolean;
myNodeInfo: Protobuf.MyNodeInfo; myNodeInfo: Protobuf.MyNodeInfo;
myNode: Protobuf.NodeInfo; myNode: Protobuf.NodeInfo;
users: Protobuf.User[]; users: Types.UserPacket[];
myUser: Protobuf.User;
positionPackets: Types.PositionPacket[]; positionPackets: Types.PositionPacket[];
nodes: Protobuf.NodeInfo[]; nodes: Protobuf.NodeInfo[];
channels: Protobuf.Channel[]; channels: Protobuf.Channel[];
@@ -41,7 +40,6 @@ const initialState: MeshtasticState = {
myNodeInfo: Protobuf.MyNodeInfo.create(), myNodeInfo: Protobuf.MyNodeInfo.create(),
myNode: Protobuf.NodeInfo.create(), myNode: Protobuf.NodeInfo.create(),
users: [], users: [],
myUser: Protobuf.User.create(),
positionPackets: [], positionPackets: [],
nodes: [], nodes: [],
channels: [], channels: [],
@@ -69,21 +67,22 @@ export const meshtasticSlice = createSlice({
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => { setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
state.myNodeInfo = action.payload; state.myNodeInfo = action.payload;
}, },
addUser: (state, action: PayloadAction<Protobuf.User>) => { addUser: (state, action: PayloadAction<Types.UserPacket>) => {
if (action.payload.id === state.myNode.user?.id) {
state.myUser = action.payload;
}
if ( if (
state.users.findIndex((user) => user.id === action.payload.id) !== -1 state.users.findIndex(
(user) => user.data.id === action.payload.data.id,
) !== -1
) { ) {
state.users = state.users.map((user) => { state.users = state.users.map((user) => {
return user.id === action.payload.id ? action.payload : user; return user.data.id === action.payload.data.id
? action.payload
: user;
}); });
} else { } else {
state.users.push(action.payload); state.users.push(action.payload);
} }
}, },
addPositionPacket: (state, action: PayloadAction<Types.PositionPacket>) => { addPosition: (state, action: PayloadAction<Types.PositionPacket>) => {
state.positionPackets.push(action.payload); state.positionPackets.push(action.payload);
}, },
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => { addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
@@ -161,7 +160,7 @@ export const {
setReady, setReady,
setMyNodeInfo, setMyNodeInfo,
addUser, addUser,
addPositionPacket, addPosition,
addNode, addNode,
addChannel, addChannel,
setPreferences, setPreferences,
+2 -6
View File
@@ -10,20 +10,16 @@ import { RouteProvider } from '@core/router';
import { store } from '@core/store'; import { store } from '@core/store';
import App from './App'; import App from './App';
import ReloadPrompt from './components/pwa/ReloadPrompt';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<RouteProvider> <RouteProvider>
<Provider store={store}> <Provider store={store}>
<App /> <App />
<ReloadPrompt />
</Provider> </Provider>
</RouteProvider> </RouteProvider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root'), document.getElementById('root'),
); );
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://www.snowpack.dev/#hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept();
}
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
+5 -3
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { FiHash } from 'react-icons/fi'; import { FiHash } from 'react-icons/fi';
@@ -12,6 +12,7 @@ import { useAppSelector } from '../hooks/redux';
export const Messages = (): JSX.Element => { export const Messages = (): JSX.Element => {
const messages = useAppSelector((state) => state.meshtastic.messages); const messages = useAppSelector((state) => state.meshtastic.messages);
const nodes = useAppSelector((state) => state.meshtastic.nodes); const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
const channels = useAppSelector((state) => state.meshtastic.channels); const channels = useAppSelector((state) => state.meshtastic.channels);
return ( return (
@@ -49,8 +50,9 @@ export const Messages = (): JSX.Element => {
ack={message.ack} ack={message.ack}
rxTime={new Date()} rxTime={new Date()}
senderName={ senderName={
nodes.find((node) => node.num === message.message.packet.from) users.find(
?.user?.longName ?? 'UNK' (user) => user.packet.from === message.message.packet.from,
)?.data.longName ?? 'UNK'
} }
/> />
))} ))}
+7 -6
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import Avatar from 'boring-avatars'; import Avatar from 'boring-avatars';
@@ -10,21 +10,22 @@ import { Node } from './Node';
export const Nodes = (): JSX.Element => { export const Nodes = (): JSX.Element => {
const nodes = useAppSelector((state) => state.meshtastic.nodes); const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
return ( return (
<PageLayout <PageLayout
title="Nodes" title="Nodes"
emptyMessage="No nodes discovered yet..." emptyMessage="No nodes discovered yet..."
sidebarItems={nodes.map((node) => { sidebarItems={nodes.map((node) => {
const user = users.find((user) => user.packet.from === node.num)?.data;
return { return {
title: node.user?.longName ?? node.num.toString(), title: user ? user.longName : node.num.toString(),
description: node.user?.hwModel description: user
? Protobuf.HardwareModel[node.user.hwModel] ? Protobuf.HardwareModel[user.hwModel]
: 'Unknown Hardware', : 'Unknown Hardware',
icon: ( icon: (
<Avatar <Avatar
size={30} size={30}
name={node.user?.longName ?? node.num.toString()} name={user ? user.longName : node.num.toString()}
variant="beam" variant="beam"
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']} colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
/> />
+10 -3
View File
@@ -1,11 +1,12 @@
import 'react-json-pretty/themes/acai.css'; import 'react-json-pretty/themes/acai.css';
import React from 'react'; import type React from 'react';
import { FiMenu, FiTerminal } from 'react-icons/fi'; import { FiMenu, FiTerminal } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty'; import JSONPretty from 'react-json-pretty';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox'; import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input'; import { Input } from '@components/generic/form/Input';
@@ -21,9 +22,15 @@ export interface NodeProps {
} }
export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => { export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
const user = useAppSelector((state) => state.meshtastic.users).find(
(user) => user.packet.from === node.num,
)?.data;
const position = useAppSelector(
(state) => state.meshtastic.positionPackets,
).find((position) => position.packet.from === node.num)?.data;
return ( return (
<PrimaryTemplate <PrimaryTemplate
title={node.user?.longName ?? node.num.toString()} title={user ? user.longName : node.num.toString()}
tagline="Node" tagline="Node"
button={ button={
<IconButton <IconButton
@@ -47,7 +54,7 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
description={new Date(node.lastHeard * 1000).toLocaleString()} description={new Date(node.lastHeard * 1000).toLocaleString()}
> >
<div className="p-10"> <div className="p-10">
<JSONPretty data={node.position} /> <JSONPretty data={position} />
</div> </div>
</Card> </Card>
<Card <Card
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { Card } from '@components/generic/Card'; import { Card } from '@components/generic/Card';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
// import { DefaultExtensionType, defaultStyles, FileIcon } from 'react-file-icon'; // import { DefaultExtensionType, defaultStyles, FileIcon } from 'react-file-icon';
import { FiMenu, FiTrash, FiUploadCloud } from 'react-icons/fi'; import { FiMenu, FiTrash, FiUploadCloud } from 'react-icons/fi';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { FiFileText, FiRss } from 'react-icons/fi'; import { FiFileText, FiRss } from 'react-icons/fi';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
+18 -9
View File
@@ -27,7 +27,10 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const [debug, setDebug] = React.useState(false); const [debug, setDebug] = React.useState(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.meshtastic.myUser); const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo);
const user = useAppSelector((state) => state.meshtastic.users).find(
(user) => user.packet.from === myNodeInfo.myNodeNum,
);
const { register, handleSubmit, formState } = useForm<{ const { register, handleSubmit, formState } = useForm<{
longName: string; longName: string;
shortName: string; shortName: string;
@@ -35,17 +38,19 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
team: Protobuf.Team; team: Protobuf.Team;
}>({ }>({
defaultValues: { defaultValues: {
longName: user.longName, longName: user?.data.longName,
shortName: user.shortName, shortName: user?.data.shortName,
isLicensed: user.isLicensed, isLicensed: user?.data.isLicensed,
team: user.team, team: user?.data.team,
}, },
}); });
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
void connection.setOwner({ ...user, ...data });
// TODO: can be remove once getUser is implemented // TODO: can be remove once getUser is implemented
dispatch(addUser({ ...user, ...data })); if (user) {
void connection.setOwner({ ...user.data, ...data });
dispatch(addUser({ ...user, ...{ data: { ...user.data, ...data } } }));
}
}); });
return ( return (
@@ -92,10 +97,14 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
<Cover enabled={debug} content={<JSONPretty data={user} />} /> <Cover enabled={debug} content={<JSONPretty data={user} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl"> <div className="w-full max-w-3xl p-10 md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}> <form className="space-y-2" onSubmit={onSubmit}>
<Input label={'Device ID'} value={user.id} disabled /> <Input label={'Device ID'} value={user?.data.id} disabled />
<Input <Input
label={'Hardware'} label={'Hardware'}
value={Protobuf.HardwareModel[user.hwModel]} value={
Protobuf.HardwareModel[
user?.data.hwModel ?? Protobuf.HardwareModel.UNSET
]
}
disabled disabled
/> />
<Input label={'Device Name'} {...register('longName')} /> <Input label={'Device Name'} {...register('longName')} />
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { import {
FiLayers, FiLayers,
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react'; import type React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FiMenu, FiSave } from 'react-icons/fi'; import { FiMenu, FiSave } from 'react-icons/fi';
+9
View File
@@ -0,0 +1,9 @@
Add desctiptions to form elements (below on mobile, to the right on desktop)
full width form elements on channel manager, don't use deprecated `modemConfig`
add default value to undefined protobufs, (omit if default to keep them small (only for ota packets))
add input validation min,max etc
change ch type select to disable, make primary and delete buttons
maybe make channel editor acordion?
meshtastic.js
- either extrapolate user and position out of nodeInfo, or fire off events for all position and user packets even when contained in a nodeInfo packet, decide how to fire events for nodeInfo, wether we merge it or do something else
+16 -18
View File
@@ -1,34 +1,32 @@
{ {
"include": ["src", "types"], "include": ["src", "types"],
"compilerOptions": { "compilerOptions": {
"module": "esnext", "target": "ESNext",
"target": "esnext", "useDefineForClassFields": true,
"moduleResolution": "node", "lib": ["DOM", "DOM.Iterable", "ESNext"],
"jsx": "preserve", "allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./", "baseUrl": "./",
/* paths - import rewriting/resolving */
"paths": { "paths": {
// If you configured any Snowpack aliases, add them here.
// Add this line to get types for streaming imports (packageOptions.source="remote"):
// "*": [".snowpack/types/*"]
// More info: https://www.snowpack.dev/guides/streaming-imports
"@app/*": ["./src/*"], "@app/*": ["./src/*"],
"@pages/*": ["./src/pages/*"], "@pages/*": ["./src/pages/*"],
"@components/*": ["./src/components/*"], "@components/*": ["./src/components/*"],
"@core/*": ["./src/core/*"] "@core/*": ["./src/core/*"]
}, },
/* noEmit - Snowpack builds (emits) files, not tsc. */
"noEmit": true,
/* Additional Options */
"importHelpers": true, "importHelpers": true,
"removeComments": true, "removeComments": true,
"strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"skipLibCheck": true, "types": ["vite/client", "vite-plugin-pwa/client"],
"types": ["snowpack-env"],
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"importsNotUsedAsValues": "error" "importsNotUsedAsValues": "error"
} }
} }
+42 -1
View File
@@ -1,11 +1,52 @@
import path from 'path'; import path from 'path';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
react(),
VitePWA({
mode: 'production',
includeAssets: [
'favicon.svg',
'favicon.ico',
'robots.txt',
'touch-icon.png',
],
manifest: {
name: 'Meshtastic Web',
short_name: 'Meshtastic',
description: 'Meshtastic Web App',
// theme_color: '#2C2D3C',
theme_color: '#67ea94',
icons: [
{
src: 'android-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'android-512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'android-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable',
},
],
},
workbox: {
sourcemap: true,
},
}),
],
build: { build: {
target: 'esnext', target: 'esnext',
assetsDir: '', assetsDir: '',