Browse Source

WIP

pull/1/head
Sacha Weatherstone 5 years ago
parent
commit
2940791b9e
  1. 9
      package.json
  2. 4
      public/index.html
  3. 70
      src/App.tsx
  4. 85
      src/components/Sidebar/Device/Settings.tsx
  5. 204
      src/components/TestForm.tsx
  6. 16
      src/components/chat/MessageBar.tsx
  7. 54
      src/components/form/Select.tsx
  8. 40
      src/components/form/Switch.tsx
  9. 33
      src/components/form/Toggle.tsx
  10. 43
      src/components/generic/Button.tsx
  11. 37
      src/components/generic/Drawer.tsx
  12. 15
      src/components/generic/IconButton.tsx
  13. 46
      src/components/menu/MobileNav.tsx
  14. 20
      src/components/menu/Navigation.tsx
  15. 6
      src/components/menu/buttons/DeviceStatusDropdown.tsx
  16. 8
      src/components/menu/buttons/LanguageDropdown.tsx
  17. 21
      src/components/menu/buttons/MobileNavToggle.tsx
  18. 10
      src/components/menu/buttons/ThemeToggle.tsx
  19. 12
      src/components/nodes/Node.tsx
  20. 44
      src/components/nodes/NodeDetails.tsx
  21. 6
      src/components/templates/PrimaryTemplate.tsx
  22. 0
      src/core/connection.ts
  23. 0
      src/core/router.ts
  24. 0
      src/core/slices/appSlice.ts
  25. 0
      src/core/slices/meshtasticSlice.ts
  26. 0
      src/core/store.ts
  27. 28
      src/core/theme.ts
  28. 6
      src/core/translation.ts
  29. 2
      src/hooks/redux.ts
  30. 6
      src/index.tsx
  31. 2
      src/pages/About.tsx
  32. 14
      src/pages/Messages.tsx
  33. 34
      src/pages/Nodes.tsx
  34. 68
      src/pages/Settings.tsx
  35. 3
      tailwind.config.js
  36. 775
      yarn.lock

9
package.json

@ -11,12 +11,15 @@
"lint": "eslint 'src/**/*.{ts,tsx}'" "lint": "eslint 'src/**/*.{ts,tsx}'"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@headlessui/react": "^1.3.0", "@headlessui/react": "^1.3.0",
"@heroicons/react": "^1.0.1", "@heroicons/react": "^1.0.1",
"@material-ui/core": "^5.0.0-beta.3",
"@meshtastic/meshtasticjs": "^0.6.16", "@meshtastic/meshtasticjs": "^0.6.16",
"@reduxjs/toolkit": "^1.6.0", "@reduxjs/toolkit": "^1.6.0",
"add": "^2.0.6",
"boring-avatars": "^1.5.8", "boring-avatars": "^1.5.8",
"framer-motion": "^4.1.17",
"i18next": "^20.3.5", "i18next": "^20.3.5",
"i18next-browser-languagedetector": "^6.1.2", "i18next-browser-languagedetector": "^6.1.2",
"react": "^17.0.2", "react": "^17.0.2",
@ -25,7 +28,9 @@
"react-hook-form": "^7.9.0", "react-hook-form": "^7.9.0",
"react-i18next": "^11.11.4", "react-i18next": "^11.11.4",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"type-route": "^0.6.0" "react-select": "^5.0.0-beta.0",
"type-route": "^0.6.0",
"yarn": "^1.22.11"
}, },
"devDependencies": { "devDependencies": {
"@snowpack/plugin-dotenv": "^2.0.5", "@snowpack/plugin-dotenv": "^2.0.5",

4
public/index.html

@ -15,6 +15,10 @@
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap"
rel="stylesheet" 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="theme-color" content="#67ea94" />
<meta <meta
name="viewport" name="viewport"

70
src/App.tsx

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { ThemeProvider } from '@material-ui/system';
import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs'; import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs';
import { DeviceStatusDropdown } from './components/menu/buttons/DeviceStatusDropdown'; import { DeviceStatusDropdown } from './components/menu/buttons/DeviceStatusDropdown';
@ -9,13 +10,8 @@ import { ThemeToggle } from './components/menu/buttons/ThemeToggle';
import { Logo } from './components/menu/Logo'; import { Logo } from './components/menu/Logo';
import { MobileNav } from './components/menu/MobileNav'; import { MobileNav } from './components/menu/MobileNav';
import { Navigation } from './components/menu/Navigation'; import { Navigation } from './components/menu/Navigation';
import { connection } from './connection'; import { connection } from './core/connection';
import { useAppDispatch, useAppSelector } from './hooks/redux'; import { useRoute } from './core/router';
import { About } from './pages/About';
import { Messages } from './pages/Messages';
import { Nodes } from './pages/Nodes';
import { Settings } from './pages/Settings';
import { useRoute } from './router';
import { import {
ackMessage, ackMessage,
addChannel, addChannel,
@ -26,7 +22,13 @@ import {
setMyNodeInfo, setMyNodeInfo,
setPreferences, setPreferences,
setReady, setReady,
} from './slices/meshtasticSlice'; } from './core/slices/meshtasticSlice';
import { theme } from './core/theme';
import { useAppDispatch, useAppSelector } from './hooks/redux';
import { About } from './pages/About';
import { Messages } from './pages/Messages';
import { Nodes } from './pages/Nodes';
import { Settings } from './pages/Settings';
const App = (): JSX.Element => { const App = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -126,39 +128,41 @@ const App = (): JSX.Element => {
}, [dispatch, myNodeInfo.myNodeNum]); }, [dispatch, myNodeInfo.myNodeNum]);
return ( return (
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}> <ThemeProvider theme={theme(darkMode)}>
<div className="flex flex-col h-full w-full bg-gray-200 dark:bg-primaryDark"> <div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
<div className="flex flex-shrink-0 w-full overflow-hidden bg-primary dark:bg-primary"> <div className="flex flex-col h-full w-full bg-gray-200 dark:bg-primaryDark">
<div className="w-full sm:py-3 sm:m-8 sm:mb-0 md:mt-12 md:mx-8 md:pt-4 md:pb-3 sm:rounded-t-xl border-b dark:border-gray-600 sm:shadow-md overflow-hidden bg-white dark:bg-primaryDark"> <div className="flex flex-shrink-0 w-full overflow-hidden bg-primary dark:bg-primary">
<div className="flex items-center justify-between h-16 px-4 md:px-6"> <div className="w-full sm:py-3 sm:m-8 sm:mb-0 md:mt-12 md:mx-8 md:pt-4 md:pb-3 sm:rounded-t-xl border-b dark:border-gray-600 sm:shadow-md overflow-hidden bg-white dark:bg-primaryDark">
<div className="hidden md:flex"> <div className="flex items-center justify-between h-16 px-4 md:px-6">
<Logo /> <div className="hidden md:flex">
</div> <Logo />
</div>
<MobileNavToggle />
<div className="flex items-center space-x-2"> <MobileNavToggle />
<DeviceStatusDropdown /> <div className="flex items-center space-x-2">
<LanguageDropdown /> <DeviceStatusDropdown />
<ThemeToggle /> <LanguageDropdown />
<ThemeToggle />
</div>
</div> </div>
<Navigation />
</div> </div>
<Navigation />
</div> </div>
</div>
<MobileNav /> <MobileNav />
<div className="flex flex-grow min-h-0 w-full sm:px-8 sm:mb-8"> <div className="flex flex-grow min-h-0 w-full sm:px-8 sm:mb-8">
<div className="flex w-full sm:shadow-xl sm:overflow-hidden bg-gray-100 dark:bg-secondaryDark sm:rounded-b-xl"> <div className="flex w-full sm:shadow-xl sm:overflow-hidden bg-gray-100 dark:bg-secondaryDark sm:rounded-b-xl">
{route.name === 'messages' && <Messages />} {route.name === 'messages' && <Messages />}
{route.name === 'nodes' && <Nodes />} {route.name === 'nodes' && <Nodes />}
{route.name === 'settings' && <Settings />} {route.name === 'settings' && <Settings />}
{route.name === 'about' && <About />} {route.name === 'about' && <About />}
{route.name === false && 'Not Found'} {route.name === false && 'Not Found'}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </ThemeProvider>
); );
}; };

85
src/components/Sidebar/Device/Settings.tsx

@ -1,85 +0,0 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { SaveIcon } from '@heroicons/react/outline';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { connection } from '../../../connection';
import { useAppSelector } from '../../../hooks/redux';
export const Settings = (): JSX.Element => {
const { t } = useTranslation();
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const onSubmit = handleSubmit((data) => connection.setPreferences(data));
return (
<form onSubmit={onSubmit}>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{t('strings.device_region')}</div>
<div className="flex shadow-md rounded-3xl ml-2">
<select
{...register('region', {
valueAsNumber: true,
})}
>
<option value={Protobuf.RegionCode.ANZ}>
{Protobuf.RegionCode[Protobuf.RegionCode.ANZ]}
</option>
<option value={Protobuf.RegionCode.CN}>
{Protobuf.RegionCode[Protobuf.RegionCode.CN]}
</option>
<option value={Protobuf.RegionCode.EU433}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU433]}
</option>
<option value={Protobuf.RegionCode.EU865}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU865]}
</option>
<option value={Protobuf.RegionCode.JP}>
{Protobuf.RegionCode[Protobuf.RegionCode.JP]}
</option>
<option value={Protobuf.RegionCode.KR}>
{Protobuf.RegionCode[Protobuf.RegionCode.KR]}
</option>
<option value={Protobuf.RegionCode.TW}>
{Protobuf.RegionCode[Protobuf.RegionCode.TW]}
</option>
<option value={Protobuf.RegionCode.US}>
{Protobuf.RegionCode[Protobuf.RegionCode.US]}
</option>
<option value={Protobuf.RegionCode.Unset}>
{Protobuf.RegionCode[Protobuf.RegionCode.Unset]}
</option>
</select>
</div>
</div>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{t('strings.wifi_ssid')}</div>
<div className="flex shadow-md rounded-3xl ml-2">
<input {...register('wifiSsid', {})} type="text" />
</div>
</div>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{t('strings.wifi_psk')}</div>
<div className="flex shadow-md rounded-3xl ml-2">
<input {...register('wifiPassword', {})} type="password" />
</div>
</div>
<div className="flex bg-gray-100 group p-1 cursor-pointer hover:bg-gray-200 border-b">
<button
type="submit"
className="flex m-auto font-medium group-hover:text-gray-700"
>
<SaveIcon className="m-auto mr-2 group-hover:text-gray-700 w-5 h-5" />
{t('strings.save_changes')}
</button>
</div>
</form>
);
};

204
src/components/TestForm.tsx

@ -0,0 +1,204 @@
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import Button from '@material-ui/core/Button';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import Switch from '@material-ui/core/Switch';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
let renderCount = 0;
const options = [
{
value: 'chocolate',
label: 'Chocolate',
},
{
value: 'strawberry',
label: 'Strawberry',
},
{
value: 'vanilla',
label: 'Vanilla',
},
];
const defaultValues = {
Native: '',
TextField: '',
Select: '',
ReactSelect: '',
Checkbox: false,
switch: false,
RadioGroup: '',
};
export const TestForm = () => {
const { handleSubmit, register, reset, control, watch } = useForm({
defaultValues,
mode: 'onChange',
});
renderCount++;
const data = watch();
return (
<div className="flex w-full max-w-screen-md justify-start items-start">
<form
className="w-1/2"
onSubmit={handleSubmit((data) => console.info(data))}
>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
Native Input:
</Typography>
<input
className="border-1 outline-none rounded-8 p-8"
{...register('Native')}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI Checkbox
</Typography>
<Controller
name="Checkbox"
control={control}
defaultValue={false}
render={({ field: { onChange, value } }) => (
<Checkbox
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
/>
)}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
Radio Group
</Typography>
<Controller
render={({ field }) => (
<RadioGroup {...field} aria-label="gender" name="gender1">
<FormControlLabel
value="female"
control={<Radio />}
label="Female"
/>
<FormControlLabel
value="male"
control={<Radio />}
label="Male"
/>
</RadioGroup>
)}
name="RadioGroup"
control={control}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI TextField
</Typography>
<Controller
render={({ field }) => <TextField {...field} variant="outlined" />}
name="TextField"
control={control}
/>
</div>
{/* <div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI Select
</Typography>
<Controller
render={({ field }) => (
<Select {...field} variant="outlined">
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
)}
name="Select"
control={control}
/>
</div> */}
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI Switch
</Typography>
<Controller
name="switch"
control={control}
defaultValue={false}
render={({ field: { onChange, value } }) => (
<Switch
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
/>
)}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
React Select
</Typography>
{/* <Controller
render={({ field }) => <ReactSelect {...field} />}
options={options}
name="ReactSelect"
isClearable
control={control}
onChange={([selected]) => {
return { value: selected };
}}
/> */}
</div>
<div className="flex my-48 items-center">
<Button
className="mx-8"
variant="contained"
color="secondary"
type="submit"
>
Submit
</Button>
<Button
className="mx-8"
type="button"
onClick={() => {
reset(defaultValues);
}}
>
Reset Form
</Button>
</div>
</form>
<div className="w-1/2 my-48 p-24">
<pre className="language-js p-24 w-400">
{JSON.stringify(data, null, 2)}
</pre>
<Typography
className="mt-16 font-medium text-12 italic"
color="textSecondary"
>
Render Count: {renderCount}
</Typography>
</div>
</div>
);
};

16
src/components/chat/MessageBar.tsx

@ -8,9 +8,9 @@ import {
PaperClipIcon, PaperClipIcon,
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import { connection } from '../../connection'; import { connection } from '../../core/connection';
import { useAppSelector } from '../../hooks/redux'; import { useAppSelector } from '../../hooks/redux';
import { Button } from '../generic/Button'; import { IconButton } from '../generic/IconButton';
export const MessageBar = (): JSX.Element => { export const MessageBar = (): JSX.Element => {
const ready = useAppSelector((state) => state.meshtastic.ready); const ready = useAppSelector((state) => state.meshtastic.ready);
@ -25,13 +25,13 @@ export const MessageBar = (): JSX.Element => {
return ( return (
<div className="flex p-4 bg-gray-50 dark:bg-transparent space-x-2 text-gray-500 dark:text-gray-400"> <div className="flex p-4 bg-gray-50 dark:bg-transparent space-x-2 text-gray-500 dark:text-gray-400">
<div className="flex"> <div className="flex">
<Button> <IconButton>
<EmojiHappyIcon className="w-6 h-6" /> <EmojiHappyIcon className="w-6 h-6" />
</Button> </IconButton>
<Button> <IconButton>
<PaperClipIcon className="w-6 h-6" /> <PaperClipIcon className="w-6 h-6" />
</Button> </IconButton>
</div> </div>
<form <form
className="flex w-full space-x-2" className="flex w-full space-x-2"
@ -51,9 +51,9 @@ export const MessageBar = (): JSX.Element => {
}} }}
className="focus:outline-none h-10 w-full resize-none rounded-full border border-gray-300 dark:bg-gray-900 px-4" className="focus:outline-none h-10 w-full resize-none rounded-full border border-gray-300 dark:bg-gray-900 px-4"
/> />
<Button type="submit"> <IconButton type="submit">
<PaperAirplaneIcon className="w-6 h-6 rotate-90" /> <PaperAirplaneIcon className="w-6 h-6 rotate-90" />
</Button> </IconButton>
</form> </form>
</div> </div>
); );

54
src/components/form/Select.tsx

@ -1,38 +1,38 @@
import React from 'react'; import React from 'react';
type DefaultSelectProps = JSX.IntrinsicElements['select']; import type { FieldValues, UseControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import ReactSelect from 'react-select';
export interface SelectProps { interface SelectProps<T> extends UseControllerProps<T> {
label: string;
options: { options: {
value: string; value: string;
label: string; label: string;
}[]; }[];
label: string;
} }
export const Select = React.forwardRef< export const Select = <T extends FieldValues>({
HTMLSelectElement, name,
SelectProps & DefaultSelectProps control,
>(function Select( label,
{ options, label, id, ...props }: SelectProps & DefaultSelectProps, options,
ref, }: SelectProps<T>): JSX.Element => {
) {
return ( return (
<div className="space-y-1"> <Controller
<label htmlFor={id} className="block text-sm font-medium dark:text-white"> name={name}
{label} control={control}
</label> rules={{
<select required: 'This is required',
ref={ref} }}
{...props} render={({ field: { onChange, value, name } }) => (
className="block w-full p-2 border dark:border-gray-600 rounded-md shadow-sm dark:bg-secondaryDark" <div className="space-y-1">
> <span className="block text-sm font-medium dark:text-white">
{options.map((option) => ( {label}
<option key={option.value} value={option.value}> </span>
{option.label} <ReactSelect options={options} />
</option> </div>
))} )}
</select> />
</div>
); );
}); };

40
src/components/form/Switch.tsx

@ -0,0 +1,40 @@
import React from 'react';
import type { FieldValues, UseControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import MaterialSwitch from '@material-ui/core/Switch';
interface SwitchProps<T> extends UseControllerProps<T> {
label: string;
}
export const Switch = <T extends FieldValues>({
name,
control,
label,
}: SwitchProps<T>): JSX.Element => {
return (
<Controller
name={name}
control={control}
rules={{
required: 'This is required',
}}
render={({ field: { onChange, value, name } }) => (
<div className="flex flex-col">
<span className="block text-sm font-medium dark:text-white">
{label}
</span>
<div className="relative w-14 mr-2 ml-auto select-none">
<MaterialSwitch
id={name}
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
/>
</div>
</div>
)}
/>
);
};

33
src/components/form/Toggle.tsx

@ -1,33 +0,0 @@
import React from 'react';
type DefaultInputProps = JSX.IntrinsicElements['input'];
export interface ToggleProps {
label: string;
}
export const Toggle = React.forwardRef<
HTMLInputElement,
ToggleProps & DefaultInputProps
>(function Input(
{ label, id, checked, ...props }: ToggleProps & DefaultInputProps,
ref,
) {
return (
<div className="flex flex-col">
<span className="block text-sm font-medium dark:text-white">{label}</span>
<div className="relative w-14 mr-2 ml-auto select-none">
<input
ref={ref}
{...props}
type="checkbox"
className="peer checked:right-0 absolute w-7 h-7 rounded-full bg-white appearance-none cursor-pointer"
/>
<label
htmlFor={id}
className="block overflow-hidden h-7 rounded-full peer-checked:bg-primary shadow-sm bg-gray-300 dark:bg-gray-700 cursor-pointer"
></label>
</div>
</div>
);
});

43
src/components/generic/Button.tsx

@ -1,31 +1,28 @@
import React from 'react'; import React from 'react';
export interface ButtonProps { import MaterialButton from '@material-ui/core/Button';
children: React.ReactNode; import type { ButtonProps as MaterialButtonProps } from '@material-ui/core/Button/Button';
className?: string;
clickAction?: () => void; interface LocalButtonProps {
type?: 'button' | 'submit' | 'reset' | undefined; text: string;
icon?: JSX.Element;
} }
export const Button = ({ export type ButtonProps = MaterialButtonProps & LocalButtonProps;
children,
className, export const Button = ({ text, icon, ...props }: ButtonProps): JSX.Element => {
clickAction,
type,
}: ButtonProps): JSX.Element => {
return ( return (
<button <MaterialButton
className={`w-10 h-10 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 active:bg-gray-300 dark:active:bg-gray-800 hover:shadow-inner text-gray-500 dark:text-gray-400 ${ {...props}
className ?? '' className="dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
}`}
onClick={() => {
if (clickAction) {
clickAction();
}
}}
type={type}
> >
<span className="flex justify-center">{children}</span> <div className="flex p-3">
</button> {icon &&
React.cloneElement(icon, {
className: 'h-6 w-6 mr-3 text-gray-500 dark:text-gray-400',
})}
<span>{text}</span>
</div>
</MaterialButton>
); );
}; };

37
src/components/generic/Drawer.tsx

@ -1,37 +0,0 @@
import React from 'react';
export interface DrawerProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
export const Drawer = ({
open,
onClose,
children,
}: DrawerProps): JSX.Element => {
return (
<>
{open && (
<div
className="z-10 fixed inset-0 transition-opacity"
onClick={onClose}
>
<div
className="absolute inset-0 backdrop-filter backdrop-blur"
tabIndex={0}
></div>
</div>
)}
<aside
className={`transform top-0 left-0 w-64 bg-white dark:bg-secondaryDark shadow-md border-r dark:border-gray-600 fixed h-full overflow-auto ease-in-out transition-all duration-300 z-30 ${
open ? 'translate-x-0' : '-translate-x-full'
}`}
>
{children}
</aside>
</>
);
};

15
src/components/generic/IconButton.tsx

@ -0,0 +1,15 @@
import React from 'react';
import MaterialIconButton from '@material-ui/core/IconButton';
import type { IconButtonProps } from '@material-ui/core/IconButton/IconButton';
export const IconButton = ({
children,
...props
}: IconButtonProps): JSX.Element => {
return (
<MaterialIconButton {...props} className="text-gray-500 dark:text-gray-400">
{children}
</MaterialIconButton>
);
};

46
src/components/menu/MobileNav.tsx

@ -6,13 +6,13 @@ import {
InformationCircleIcon, InformationCircleIcon,
ViewGridIcon, ViewGridIcon,
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer/SwipeableDrawer';
import { routes } from '../../core/router';
import { closeMobileNav, openMobileNav } from '../../core/slices/appSlice';
import { useAppDispatch, useAppSelector } from '../../hooks/redux'; import { useAppDispatch, useAppSelector } from '../../hooks/redux';
import { routes } from '../../router'; import { Button } from '../generic/Button';
import { closeMobileNav } from '../../slices/appSlice';
import { Drawer } from '../generic/Drawer';
import { Logo } from './Logo'; import { Logo } from './Logo';
import { MenuButton } from './MenuButton';
export const MobileNav = (): JSX.Element => { export const MobileNav = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -20,49 +20,41 @@ export const MobileNav = (): JSX.Element => {
const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen); const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen);
return ( return (
<Drawer <SwipeableDrawer
open={mobileNavOpen} open={mobileNavOpen}
anchor="left"
onClose={() => { onClose={() => {
dispatch(closeMobileNav()); dispatch(closeMobileNav());
}} }}
onOpen={() => {
dispatch(openMobileNav());
}}
> >
<div className="flex flex-col"> <div className="flex flex-col dark:bg-secondaryDark h-full">
<div className="m-auto my-6"> <div className="m-auto my-6">
<Logo /> <Logo />
</div> </div>
<MenuButton <Button
icon={<AnnotationIcon />} icon={<AnnotationIcon />}
text={'Messages'} text={'Messages'}
link={routes.messages().link} {...routes.messages().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
/> />
<MenuButton <Button
icon={<ViewGridIcon />} icon={<ViewGridIcon />}
text={'Nodes'} text={'Nodes'}
link={routes.nodes().link} {...routes.nodes().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
/> />
<MenuButton <Button
icon={<CogIcon />} icon={<CogIcon />}
text={'Settings'} text={'Settings'}
link={routes.settings().link} {...routes.settings().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
/> />
<MenuButton <Button
icon={<InformationCircleIcon />} icon={<InformationCircleIcon />}
text={'About'} text={'About'}
link={routes.about().link} {...routes.about().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
/> />
</div> </div>
</Drawer> </SwipeableDrawer>
); );
}; };

20
src/components/menu/Navigation.tsx

@ -7,32 +7,32 @@ import {
ViewGridIcon, ViewGridIcon,
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import { routes } from '../../router'; import { routes } from '../../core/router';
import { MenuButton } from './MenuButton'; import { Button } from '../generic/Button';
export const Navigation = (): JSX.Element => { export const Navigation = (): JSX.Element => {
return ( return (
<div className="hidden md:flex flex-auto flex-0 relative items-center h-16 px-4 "> <div className="hidden md:flex flex-auto flex-0 relative items-center h-16 px-4 ">
<div className="flex items-center"> <div className="flex items-center">
<MenuButton <Button
icon={<AnnotationIcon />} icon={<AnnotationIcon />}
text={'Messages'} text={'Messages'}
link={routes.messages().link} {...routes.messages().link}
/> />
<MenuButton <Button
icon={<ViewGridIcon />} icon={<ViewGridIcon />}
text={'Nodes'} text={'Nodes'}
link={routes.nodes().link} {...routes.nodes().link}
/> />
<MenuButton <Button
icon={<CogIcon />} icon={<CogIcon />}
text={'Settings'} text={'Settings'}
link={routes.settings().link} {...routes.settings().link}
/> />
<MenuButton <Button
icon={<InformationCircleIcon />} icon={<InformationCircleIcon />}
text={'About'} text={'About'}
link={routes.about().link} {...routes.about().link}
/> />
</div> </div>
</div> </div>

6
src/components/menu/buttons/DeviceStatusDropdown.tsx

@ -3,14 +3,14 @@ import React from 'react';
import { SwitchVerticalIcon } from '@heroicons/react/outline'; import { SwitchVerticalIcon } from '@heroicons/react/outline';
import { useAppSelector } from '../../../hooks/redux'; import { useAppSelector } from '../../../hooks/redux';
import { Button } from '../../generic/Button'; import { IconButton } from '../../generic/IconButton';
export const DeviceStatusDropdown = (): JSX.Element => { export const DeviceStatusDropdown = (): JSX.Element => {
const ready = useAppSelector((state) => state.meshtastic.ready); const ready = useAppSelector((state) => state.meshtastic.ready);
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus); const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus);
return ( return (
<Button> <IconButton>
<SwitchVerticalIcon className={`h-6 w-6 ${!ready && 'animate-pulse'}`} /> <SwitchVerticalIcon className={`h-6 w-6 ${!ready && 'animate-pulse'}`} />
{/* <div {/* <div
className={`flex w-6 h-6 rounded-full animate-pulse shadow-md ${ className={`flex w-6 h-6 rounded-full animate-pulse shadow-md ${
@ -24,6 +24,6 @@ export const DeviceStatusDropdown = (): JSX.Element => {
: 'bg-gray-400' : 'bg-gray-400'
}`} }`}
></div> */} ></div> */}
</Button> </IconButton>
); );
}; };

8
src/components/menu/buttons/LanguageDropdown.tsx

@ -4,9 +4,9 @@ import { Jp, Pt, Us } from 'react-flags-select';
import { Menu } from '@headlessui/react'; import { Menu } from '@headlessui/react';
import i18n from '../../../core/translation';
import { useAppDispatch } from '../../../hooks/redux'; import { useAppDispatch } from '../../../hooks/redux';
import i18n from '../../../translation'; import { IconButton } from '../../generic/IconButton';
import { Button } from '../../generic/Button';
export const LanguageDropdown = (): JSX.Element => { export const LanguageDropdown = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -32,11 +32,11 @@ export const LanguageDropdown = (): JSX.Element => {
return ( return (
<Menu as="div" className="w-10 h-10"> <Menu as="div" className="w-10 h-10">
<div className="absolute"> <div className="absolute">
<Button> <IconButton>
<Menu.Button as="div"> <Menu.Button as="div">
<Us className="w-6 shadow rounded-sm" /> <Us className="w-6 shadow rounded-sm" />
</Menu.Button> </Menu.Button>
</Button> </IconButton>
<Menu.Items className="z-20 absolute right-0 bg-white dark:bg-secondaryDark border dark:border-gray-600 divide-y divide-gray-200 dark:divide-gray-600 rounded-md shadow-md focus:outline-none"> <Menu.Items className="z-20 absolute right-0 bg-white dark:bg-secondaryDark border dark:border-gray-600 divide-y divide-gray-200 dark:divide-gray-600 rounded-md shadow-md focus:outline-none">
{languages.map((language, index) => ( {languages.map((language, index) => (

21
src/components/menu/buttons/MobileNavToggle.tsx

@ -2,21 +2,22 @@ import React from 'react';
import { MenuIcon } from '@heroicons/react/outline'; import { MenuIcon } from '@heroicons/react/outline';
import { openMobileNav } from '../../../core/slices/appSlice';
import { useAppDispatch } from '../../../hooks/redux'; import { useAppDispatch } from '../../../hooks/redux';
import { openMobileNav } from '../../../slices/appSlice'; import { IconButton } from '../../generic/IconButton';
import { Button } from '../../generic/Button';
export const MobileNavToggle = (): JSX.Element => { export const MobileNavToggle = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
return ( return (
<Button <div className="md:hidden">
clickAction={() => { <IconButton
dispatch(openMobileNav()); onClick={() => {
}} dispatch(openMobileNav());
className="md:hidden" }}
> >
<MenuIcon className="h-6 w-6" /> <MenuIcon className="h-6 w-6" />
</Button> </IconButton>
</div>
); );
}; };

10
src/components/menu/buttons/ThemeToggle.tsx

@ -2,17 +2,17 @@ import React from 'react';
import { MoonIcon, SunIcon } from '@heroicons/react/outline'; import { MoonIcon, SunIcon } from '@heroicons/react/outline';
import { setDarkModeEnabled } from '../../../core/slices/appSlice';
import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
import { setDarkModeEnabled } from '../../../slices/appSlice'; import { IconButton } from '../../generic/IconButton';
import { Button } from '../../generic/Button';
export const ThemeToggle = (): JSX.Element => { export const ThemeToggle = (): JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const darkMode = useAppSelector((state) => state.app.darkMode); const darkMode = useAppSelector((state) => state.app.darkMode);
return ( return (
<Button <IconButton
clickAction={() => { onClick={() => {
dispatch(setDarkModeEnabled(!darkMode)); dispatch(setDarkModeEnabled(!darkMode));
}} }}
> >
@ -21,6 +21,6 @@ export const ThemeToggle = (): JSX.Element => {
) : ( ) : (
<MoonIcon className="h-6 w-6" /> <MoonIcon className="h-6 w-6" />
)} )}
</Button> </IconButton>
); );
}; };

12
src/components/nodes/Node.tsx

@ -4,13 +4,21 @@ import Avatar from 'boring-avatars';
import type { Protobuf } from '@meshtastic/meshtasticjs'; import type { Protobuf } from '@meshtastic/meshtasticjs';
type DefaultDivProps = JSX.IntrinsicElements['div'];
export interface NodeProps { export interface NodeProps {
node: Protobuf.NodeInfo; node: Protobuf.NodeInfo;
} }
export const Node = ({ node }: NodeProps): JSX.Element => { export const Node = ({
node,
...props
}: NodeProps & DefaultDivProps): JSX.Element => {
return ( return (
<div className="flex space-x-4 items-center w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"> <div
{...props}
className="flex space-x-4 items-center w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
>
<Avatar <Avatar
size={30} size={30}
name={node.user?.longName ?? 'UNK'} name={node.user?.longName ?? 'UNK'}

44
src/components/nodes/NodeDetails.tsx

@ -0,0 +1,44 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { XCircleIcon } from '@heroicons/react/outline';
import type { Protobuf } from '@meshtastic/meshtasticjs';
import { connection } from '../../core/connection';
export interface NodeProps {
node: Protobuf.NodeInfo;
onClose: () => void;
}
export const NodeDetails = ({ node }: NodeProps): JSX.Element => {
const { register, handleSubmit } = useForm<Protobuf.User>({
defaultValues: node.user,
});
const onSubmit = handleSubmit((data) => {
console.log(data);
connection.setOwner(data);
});
return (
<div>
<div className="flex dark:bg-primaryDark p-2 rounded-t-md justify-between border-b dark:border-gray-600 dark:text-white">
<div>{node.user?.longName ?? node.num}</div>
<XCircleIcon className="h-5 w-5 dark:text-white my-auto" />
</div>
<div>
<form onSubmit={onSubmit}>
{/* <Input label="Node Name" {...register('longName', {})} /> */}
<button
type="submit"
className="w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
>
Save
</button>
</form>
</div>
</div>
);
};

6
src/components/templates/PrimaryTemplate.tsx

@ -27,11 +27,7 @@ export const PrimaryTemplate = ({
</div> </div>
</div> </div>
</div> </div>
<div className="flex-auto p-6 sm:p-10 "> <div className="flex-auto p-6 sm:p-10 ">{children}</div>
<div className="max-w-3xl">
<div className="max-w-3xl">{children}</div>
</div>
</div>
</div> </div>
); );
}; };

0
src/connection.ts → src/core/connection.ts

0
src/router.ts → src/core/router.ts

0
src/slices/appSlice.ts → src/core/slices/appSlice.ts

0
src/slices/meshtasticSlice.ts → src/core/slices/meshtasticSlice.ts

0
src/store.ts → src/core/store.ts

28
src/core/theme.ts

@ -0,0 +1,28 @@
import type { Theme } from '@material-ui/core';
import { createTheme } from '@material-ui/core/styles';
export const theme = (darkMode: boolean): Theme => {
return createTheme(
darkMode
? {
palette: {
mode: 'dark',
primary: {
main: '#67ea94',
},
background: {
default: '#0F172A',
paper: '#0F172A',
},
},
}
: {
palette: {
mode: 'light',
primary: {
main: '#67ea94',
},
},
},
);
};

6
src/translation.ts → src/core/translation.ts

@ -2,9 +2,9 @@ import i18n from 'i18next';
import detector from 'i18next-browser-languagedetector'; import detector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import { en } from './translations/en'; import { en } from '../translations/en';
import { jp } from './translations/jp'; import { jp } from '../translations/jp';
import { pt } from './translations/pt'; import { pt } from '../translations/pt';
i18n i18n
.use(detector) .use(detector)

2
src/hooks/redux.ts

@ -1,7 +1,7 @@
import type { TypedUseSelectorHook } from 'react-redux'; import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from '../store'; import type { AppDispatch, RootState } from '../core/store';
export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

6
src/index.tsx

@ -1,5 +1,5 @@
import './index.css'; import './index.css';
import './translation'; import './core/translation';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@ -7,8 +7,8 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import App from './App'; import App from './App';
import { RouteProvider } from './router'; import { RouteProvider } from './core/router';
import { store } from './store'; import { store } from './core/store';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>

2
src/pages/About.tsx

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '../components/templates/PrimaryTemplate';
import { TestForm } from '../components/TestForm';
export const About = (): JSX.Element => { export const About = (): JSX.Element => {
return ( return (
<PrimaryTemplate title="meshtastic-web" tagline="About"> <PrimaryTemplate title="meshtastic-web" tagline="About">
<p>Content</p> <p>Content</p>
<TestForm />
</PrimaryTemplate> </PrimaryTemplate>
); );
}; };

14
src/pages/Messages.tsx

@ -5,7 +5,7 @@ import { Protobuf } from '@meshtastic/meshtasticjs';
import { Message } from '../components/chat/Message'; import { Message } from '../components/chat/Message';
import { MessageBar } from '../components/chat/MessageBar'; import { MessageBar } from '../components/chat/MessageBar';
import { Button } from '../components/generic/Button'; import { IconButton } from '../components/generic/IconButton';
import { useAppSelector } from '../hooks/redux'; import { useAppSelector } from '../hooks/redux';
export const Messages = (): JSX.Element => { export const Messages = (): JSX.Element => {
@ -29,12 +29,12 @@ export const Messages = (): JSX.Element => {
{channelName()} {channelName()}
</div> </div>
<div className="flex"> <div className="flex">
<Button> <IconButton>
<MapIcon className="w-6 h-6" /> <MapIcon className="w-6 h-6" />
</Button> </IconButton>
<Button> <IconButton>
<UsersIcon className="w-6 h-6" /> <UsersIcon className="w-6 h-6" />
</Button> </IconButton>
</div> </div>
</div> </div>
<div className="flex flex-col p-6 sm:py-8 sm:px-10 border-b dark:border-gray-600 bg-white dark:bg-secondaryDark flex-grow overflow-y-auto space-y-2"> <div className="flex flex-col p-6 sm:py-8 sm:px-10 border-b dark:border-gray-600 bg-white dark:bg-secondaryDark flex-grow overflow-y-auto space-y-2">
@ -56,6 +56,6 @@ export const Messages = (): JSX.Element => {
</div> </div>
); );
}; };
<Button> <IconButton>
<UsersIcon className="w-6 h-6" /> <UsersIcon className="w-6 h-6" />
</Button>; </IconButton>;

34
src/pages/Nodes.tsx

@ -1,17 +1,45 @@
import React from 'react'; import React from 'react';
import type { Protobuf } from '@meshtastic/meshtasticjs';
import { Node } from '../components/nodes/Node'; import { Node } from '../components/nodes/Node';
import { NodeDetails } from '../components/nodes/NodeDetails';
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '../components/templates/PrimaryTemplate';
import { useAppSelector } from '../hooks/redux'; import { useAppSelector } from '../hooks/redux';
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 [currentNode, setCurrentNode] = React.useState<
Protobuf.NodeInfo | undefined
>();
return ( return (
<PrimaryTemplate title="Administration" tagline="Node"> <PrimaryTemplate title="Administration" tagline="Node">
{nodes.map((node) => ( <div className="flex w-full space-x-5">
<Node key={node.num} node={node} /> <div className="w-1/3">
))} {nodes.map((node) => (
<Node
key={node.num}
node={node}
onClick={() => {
setCurrentNode(node);
}}
/>
))}
</div>
<div className="w-2/3">
{currentNode ? (
<NodeDetails
onClose={() => {
setCurrentNode(undefined);
}}
node={currentNode}
/>
) : (
<div>Node not selected</div>
)}
</div>
</div>
</PrimaryTemplate> </PrimaryTemplate>
); );
}; };

68
src/pages/Settings.tsx

@ -7,14 +7,14 @@ import { Protobuf } from '@meshtastic/meshtasticjs';
import { Input } from '../components/form/Input'; import { Input } from '../components/form/Input';
import { Select } from '../components/form/Select'; import { Select } from '../components/form/Select';
import { Toggle } from '../components/form/Toggle'; import { Switch } from '../components/form/Switch';
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; import { PrimaryTemplate } from '../components/templates/PrimaryTemplate';
import { connection } from '../connection'; import { connection } from '../core/connection';
import { useAppDispatch, useAppSelector } from '../hooks/redux';
import { import {
setHostOverride, setHostOverride,
setHostOverrideEnabled, setHostOverrideEnabled,
} from '../slices/meshtasticSlice'; } from '../core/slices/meshtasticSlice';
import { useAppDispatch, useAppSelector } from '../hooks/redux';
export const Settings = (): JSX.Element => { export const Settings = (): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -26,7 +26,7 @@ export const Settings = (): JSX.Element => {
(state) => state.meshtastic.hostOverrideEnabled, (state) => state.meshtastic.hostOverrideEnabled,
); );
const { register, handleSubmit } = const { register, handleSubmit, control } =
useForm<Protobuf.RadioConfig_UserPreferences>({ useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: radioConfig, defaultValues: radioConfig,
}); });
@ -65,36 +65,46 @@ export const Settings = (): JSX.Element => {
<div className="flex pb-2 dark:text-white border-b dark:border-gray-600"> <div className="flex pb-2 dark:text-white border-b dark:border-gray-600">
<div className="w-1/3 text-lg">Node</div> <div className="w-1/3 text-lg">Node</div>
<div className="space-y-2 w-full max-w-xs"> <div className="space-y-2 w-full max-w-xs">
<Toggle label={'Is router node'} {...register('isRouter', {})} /> <Switch
<Toggle name={'isRouter'}
label={'Is router node'} control={control}
{...register('isLowPower', {})} label={'Is Router Node?'}
/> />
<Toggle
label={'Is router node'} <Switch
{...register('fixedPosition', {})} name={'isLowPower'}
control={control}
label={'Is Low Power?'}
/> />
<Toggle
label={'Is serial disabled'} <Switch
{...register('serialDisabled', {})} name={'fixedPosition'}
control={control}
label={'Has Fixed Position?'}
/> />
<Toggle
label={'Is router low power'} <Switch
{...register('isLowPower', {})} name={'serialDisabled'}
control={control}
label={'Is Serial Disabled?'}
/> />
<Toggle
label={'Is MQTT disabled'} <Switch
{...register('mqttDisabled', {})} name={'mqttDisabled'}
control={control}
label={'Is MQTT Disabled?'}
/> />
<Toggle
label={'Debug log enabled'} <Switch
{...register('debugLogEnabled', {})} name={'debugLogEnabled'}
control={control}
label={'Is Debug Log Enabled?'}
/> />
<Select <Select
name={'region'}
control={control}
label="Region" label="Region"
{...register('region', {
valueAsNumber: true,
})}
options={(() => { options={(() => {
return Object.keys(Protobuf.RegionCode) return Object.keys(Protobuf.RegionCode)
.filter((value) => isNaN(Number(value)) === false) .filter((value) => isNaN(Number(value)) === false)
@ -111,14 +121,14 @@ export const Settings = (): JSX.Element => {
<div className="flex pb-2 dark:text-white border-b dark:border-gray-600"> <div className="flex pb-2 dark:text-white border-b dark:border-gray-600">
<div className="w-1/3 text-lg">Client</div> <div className="w-1/3 text-lg">Client</div>
<div className="space-y-2 w-full max-w-xs"> <div className="space-y-2 w-full max-w-xs">
<Toggle {/* <Toggle
label={'Enable host override'} label={'Enable host override'}
checked={localHostOverrideEnabled} checked={localHostOverrideEnabled}
onChange={(event) => { onChange={(event) => {
console.log(event.target.checked); console.log(event.target.checked);
setLocalHostOverrideEnabled(event.target.checked); setLocalHostOverrideEnabled(event.target.checked);
}} }}
/> /> */}
<Input <Input
label={'Host override'} label={'Host override'}
placeholder={'meshtastic.local'} placeholder={'meshtastic.local'}

3
tailwind.config.js

@ -4,8 +4,7 @@ module.exports = {
darkMode: 'class', // or 'media' or 'class' darkMode: 'class', // or 'media' or 'class'
theme: { theme: {
fontFamily: { fontFamily: {
sans: 'Inter var', sans: 'Roboto',
mono: 'IBM Plex Mono',
}, },
extend: { extend: {
colors: { colors: {

775
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save