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
+5 -2
View File
@@ -13,6 +13,7 @@ import {
addChannel,
addMessage,
addNode,
addPosition,
addUser,
setDeviceStatus,
setLastMeshInterraction,
@@ -82,9 +83,11 @@ const App = (): JSX.Element => {
});
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(
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
import Avatar from 'boring-avatars';
+5 -1
View File
@@ -15,6 +15,7 @@ export const MessageBar = (): JSX.Element => {
const dispatch = useAppDispatch();
const ready = useAppSelector((state) => state.meshtastic.ready);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo);
const [currentMessage, setCurrentMessage] = React.useState('');
const [destinationNode, setDestinationNode] =
@@ -52,8 +53,11 @@ export const MessageBar = (): JSX.Element => {
...nodes
.filter((node) => node.num !== myNodeInfo.myNodeNum)
.map((node) => {
const user = users.filter(
(user) => user.packet.from === node.num,
)[0]?.data;
return {
name: node.user?.shortName ?? node.num,
name: user ? user.shortName : 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'];
-46
View File
@@ -67,49 +67,3 @@ export const 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'];
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
export interface CoverProps {
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';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
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'];
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
export interface StatCardProps {
title: string;
+1 -1
View File
@@ -34,7 +34,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
? 'border-red-500'
: props.disabled
? '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}
/>
+2 -2
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
export interface LabelProps {
error?: string;
@@ -21,7 +21,7 @@ export const InputWrapper = ({
? 'border-red-500'
: disabled
? '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}
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
export interface LabelProps {
label: string;
+2 -2
View File
@@ -29,8 +29,8 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
<InputWrapper>
<select
ref={ref}
className={`w-full bg-white dark:bg-transparent focus:outline-none focus:border-primary ${
small ? 'py-1 mx-1' : 'h-10 mx-2'
className={`w-full rounded-md bg-white dark:bg-transparent focus:outline-none focus:border-primary ${
small ? 'p-1' : 'h-10 px-2'
}`}
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 => {
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 { Drawer } from '@components/generic/Drawer';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
import {
FiGrid,
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
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';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
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 { Drawer } from '@components/generic/Drawer';
import { IconButton } from '@components/generic/IconButton';
import type { SidebarItemProps } from '@components/generic/SidebarItem';
import { SidebarItem } from '@components/generic/SidebarItem';
import { SidebarItem, SidebarItemProps } from '@components/generic/SidebarItem';
import { Tab } from '@headlessui/react';
export interface PageLayoutProps {
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
export interface PrimaryTemplateProps {
children: React.ReactNode;
+10 -11
View File
@@ -22,8 +22,7 @@ interface MeshtasticState {
ready: boolean;
myNodeInfo: Protobuf.MyNodeInfo;
myNode: Protobuf.NodeInfo;
users: Protobuf.User[];
myUser: Protobuf.User;
users: Types.UserPacket[];
positionPackets: Types.PositionPacket[];
nodes: Protobuf.NodeInfo[];
channels: Protobuf.Channel[];
@@ -41,7 +40,6 @@ const initialState: MeshtasticState = {
myNodeInfo: Protobuf.MyNodeInfo.create(),
myNode: Protobuf.NodeInfo.create(),
users: [],
myUser: Protobuf.User.create(),
positionPackets: [],
nodes: [],
channels: [],
@@ -69,21 +67,22 @@ export const meshtasticSlice = createSlice({
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
state.myNodeInfo = action.payload;
},
addUser: (state, action: PayloadAction<Protobuf.User>) => {
if (action.payload.id === state.myNode.user?.id) {
state.myUser = action.payload;
}
addUser: (state, action: PayloadAction<Types.UserPacket>) => {
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) => {
return user.id === action.payload.id ? action.payload : user;
return user.data.id === action.payload.data.id
? action.payload
: user;
});
} else {
state.users.push(action.payload);
}
},
addPositionPacket: (state, action: PayloadAction<Types.PositionPacket>) => {
addPosition: (state, action: PayloadAction<Types.PositionPacket>) => {
state.positionPackets.push(action.payload);
},
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
@@ -161,7 +160,7 @@ export const {
setReady,
setMyNodeInfo,
addUser,
addPositionPacket,
addPosition,
addNode,
addChannel,
setPreferences,
+2 -6
View File
@@ -10,20 +10,16 @@ import { RouteProvider } from '@core/router';
import { store } from '@core/store';
import App from './App';
import ReloadPrompt from './components/pwa/ReloadPrompt';
ReactDOM.render(
<React.StrictMode>
<RouteProvider>
<Provider store={store}>
<App />
<ReloadPrompt />
</Provider>
</RouteProvider>
</React.StrictMode>,
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 { 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';
@@ -12,6 +12,7 @@ import { useAppSelector } from '../hooks/redux';
export const Messages = (): JSX.Element => {
const messages = useAppSelector((state) => state.meshtastic.messages);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
const channels = useAppSelector((state) => state.meshtastic.channels);
return (
@@ -49,8 +50,9 @@ export const Messages = (): JSX.Element => {
ack={message.ack}
rxTime={new Date()}
senderName={
nodes.find((node) => node.num === message.message.packet.from)
?.user?.longName ?? 'UNK'
users.find(
(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';
@@ -10,21 +10,22 @@ import { Node } from './Node';
export const Nodes = (): JSX.Element => {
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const users = useAppSelector((state) => state.meshtastic.users);
return (
<PageLayout
title="Nodes"
emptyMessage="No nodes discovered yet..."
sidebarItems={nodes.map((node) => {
const user = users.find((user) => user.packet.from === node.num)?.data;
return {
title: node.user?.longName ?? node.num.toString(),
description: node.user?.hwModel
? Protobuf.HardwareModel[node.user.hwModel]
title: user ? user.longName : node.num.toString(),
description: user
? Protobuf.HardwareModel[user.hwModel]
: 'Unknown Hardware',
icon: (
<Avatar
size={30}
name={node.user?.longName ?? node.num.toString()}
name={user ? user.longName : node.num.toString()}
variant="beam"
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
/>
+10 -3
View File
@@ -1,11 +1,12 @@
import 'react-json-pretty/themes/acai.css';
import React from 'react';
import type React from 'react';
import { FiMenu, FiTerminal } from 'react-icons/fi';
import JSONPretty from 'react-json-pretty';
import TimeAgo from 'react-timeago';
import { useAppSelector } from '@app/hooks/redux';
import { Card } from '@components/generic/Card';
import { Checkbox } from '@components/generic/form/Checkbox';
import { Input } from '@components/generic/form/Input';
@@ -21,9 +22,15 @@ export interface NodeProps {
}
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 (
<PrimaryTemplate
title={node.user?.longName ?? node.num.toString()}
title={user ? user.longName : node.num.toString()}
tagline="Node"
button={
<IconButton
@@ -47,7 +54,7 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
description={new Date(node.lastHeard * 1000).toLocaleString()}
>
<div className="p-10">
<JSONPretty data={node.position} />
<JSONPretty data={position} />
</div>
</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 { 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 { 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';
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
import { useForm } from 'react-hook-form';
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 [debug, setDebug] = React.useState(false);
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<{
longName: string;
shortName: string;
@@ -35,17 +38,19 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
team: Protobuf.Team;
}>({
defaultValues: {
longName: user.longName,
shortName: user.shortName,
isLicensed: user.isLicensed,
team: user.team,
longName: user?.data.longName,
shortName: user?.data.shortName,
isLicensed: user?.data.isLicensed,
team: user?.data.team,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setOwner({ ...user, ...data });
// 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 (
@@ -92,10 +97,14 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
<Cover enabled={debug} content={<JSONPretty data={user} />} />
<div className="w-full max-w-3xl p-10 md:max-w-xl">
<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
label={'Hardware'}
value={Protobuf.HardwareModel[user.hwModel]}
value={
Protobuf.HardwareModel[
user?.data.hwModel ?? Protobuf.HardwareModel.UNSET
]
}
disabled
/>
<Input label={'Device Name'} {...register('longName')} />
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
import {
FiLayers,
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import type React from 'react';
import { useTranslation } from 'react-i18next';
import { FiMenu, FiSave } from 'react-icons/fi';