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}'"
},
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@headlessui/react": "^1.3.0",
"@heroicons/react": "^1.0.1",
"@material-ui/core": "^5.0.0-beta.3",
"@meshtastic/meshtasticjs": "^0.6.16",
"@reduxjs/toolkit": "^1.6.0",
"add": "^2.0.6",
"boring-avatars": "^1.5.8",
"framer-motion": "^4.1.17",
"i18next": "^20.3.5",
"i18next-browser-languagedetector": "^6.1.2",
"react": "^17.0.2",
@ -25,7 +28,9 @@
"react-hook-form": "^7.9.0",
"react-i18next": "^11.11.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": {
"@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"
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"

70
src/App.tsx

@ -1,5 +1,6 @@
import React from 'react';
import { ThemeProvider } from '@material-ui/system';
import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs';
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 { MobileNav } from './components/menu/MobileNav';
import { Navigation } from './components/menu/Navigation';
import { connection } from './connection';
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';
import { useRoute } from './router';
import { connection } from './core/connection';
import { useRoute } from './core/router';
import {
ackMessage,
addChannel,
@ -26,7 +22,13 @@ import {
setMyNodeInfo,
setPreferences,
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 dispatch = useAppDispatch();
@ -126,39 +128,41 @@ const App = (): JSX.Element => {
}, [dispatch, myNodeInfo.myNodeNum]);
return (
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
<div className="flex flex-col h-full w-full bg-gray-200 dark:bg-primaryDark">
<div className="flex flex-shrink-0 w-full overflow-hidden bg-primary dark:bg-primary">
<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 items-center justify-between h-16 px-4 md:px-6">
<div className="hidden md:flex">
<Logo />
</div>
<MobileNavToggle />
<div className="flex items-center space-x-2">
<DeviceStatusDropdown />
<LanguageDropdown />
<ThemeToggle />
<ThemeProvider theme={theme(darkMode)}>
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
<div className="flex flex-col h-full w-full bg-gray-200 dark:bg-primaryDark">
<div className="flex flex-shrink-0 w-full overflow-hidden bg-primary dark:bg-primary">
<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 items-center justify-between h-16 px-4 md:px-6">
<div className="hidden md:flex">
<Logo />
</div>
<MobileNavToggle />
<div className="flex items-center space-x-2">
<DeviceStatusDropdown />
<LanguageDropdown />
<ThemeToggle />
</div>
</div>
<Navigation />
</div>
<Navigation />
</div>
</div>
<MobileNav />
<MobileNav />
<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">
{route.name === 'messages' && <Messages />}
{route.name === 'nodes' && <Nodes />}
{route.name === 'settings' && <Settings />}
{route.name === 'about' && <About />}
{route.name === false && 'Not Found'}
<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">
{route.name === 'messages' && <Messages />}
{route.name === 'nodes' && <Nodes />}
{route.name === 'settings' && <Settings />}
{route.name === 'about' && <About />}
{route.name === false && 'Not Found'}
</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,
} from '@heroicons/react/outline';
import { connection } from '../../connection';
import { connection } from '../../core/connection';
import { useAppSelector } from '../../hooks/redux';
import { Button } from '../generic/Button';
import { IconButton } from '../generic/IconButton';
export const MessageBar = (): JSX.Element => {
const ready = useAppSelector((state) => state.meshtastic.ready);
@ -25,13 +25,13 @@ export const MessageBar = (): JSX.Element => {
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">
<Button>
<IconButton>
<EmojiHappyIcon className="w-6 h-6" />
</Button>
</IconButton>
<Button>
<IconButton>
<PaperClipIcon className="w-6 h-6" />
</Button>
</IconButton>
</div>
<form
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"
/>
<Button type="submit">
<IconButton type="submit">
<PaperAirplaneIcon className="w-6 h-6 rotate-90" />
</Button>
</IconButton>
</form>
</div>
);

54
src/components/form/Select.tsx

@ -1,38 +1,38 @@
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: {
value: string;
label: string;
}[];
label: string;
}
export const Select = React.forwardRef<
HTMLSelectElement,
SelectProps & DefaultSelectProps
>(function Select(
{ options, label, id, ...props }: SelectProps & DefaultSelectProps,
ref,
) {
export const Select = <T extends FieldValues>({
name,
control,
label,
options,
}: SelectProps<T>): JSX.Element => {
return (
<div className="space-y-1">
<label htmlFor={id} className="block text-sm font-medium dark:text-white">
{label}
</label>
<select
ref={ref}
{...props}
className="block w-full p-2 border dark:border-gray-600 rounded-md shadow-sm dark:bg-secondaryDark"
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<Controller
name={name}
control={control}
rules={{
required: 'This is required',
}}
render={({ field: { onChange, value, name } }) => (
<div className="space-y-1">
<span className="block text-sm font-medium dark:text-white">
{label}
</span>
<ReactSelect options={options} />
</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';
export interface ButtonProps {
children: React.ReactNode;
className?: string;
clickAction?: () => void;
type?: 'button' | 'submit' | 'reset' | undefined;
import MaterialButton from '@material-ui/core/Button';
import type { ButtonProps as MaterialButtonProps } from '@material-ui/core/Button/Button';
interface LocalButtonProps {
text: string;
icon?: JSX.Element;
}
export const Button = ({
children,
className,
clickAction,
type,
}: ButtonProps): JSX.Element => {
export type ButtonProps = MaterialButtonProps & LocalButtonProps;
export const Button = ({ text, icon, ...props }: ButtonProps): JSX.Element => {
return (
<button
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 ${
className ?? ''
}`}
onClick={() => {
if (clickAction) {
clickAction();
}
}}
type={type}
<MaterialButton
{...props}
className="dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
>
<span className="flex justify-center">{children}</span>
</button>
<div className="flex p-3">
{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,
ViewGridIcon,
} 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 { routes } from '../../router';
import { closeMobileNav } from '../../slices/appSlice';
import { Drawer } from '../generic/Drawer';
import { Button } from '../generic/Button';
import { Logo } from './Logo';
import { MenuButton } from './MenuButton';
export const MobileNav = (): JSX.Element => {
const dispatch = useAppDispatch();
@ -20,49 +20,41 @@ export const MobileNav = (): JSX.Element => {
const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen);
return (
<Drawer
<SwipeableDrawer
open={mobileNavOpen}
anchor="left"
onClose={() => {
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">
<Logo />
</div>
<MenuButton
<Button
icon={<AnnotationIcon />}
text={'Messages'}
link={routes.messages().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.messages().link}
/>
<MenuButton
<Button
icon={<ViewGridIcon />}
text={'Nodes'}
link={routes.nodes().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.nodes().link}
/>
<MenuButton
<Button
icon={<CogIcon />}
text={'Settings'}
link={routes.settings().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.settings().link}
/>
<MenuButton
<Button
icon={<InformationCircleIcon />}
text={'About'}
link={routes.about().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.about().link}
/>
</div>
</Drawer>
</SwipeableDrawer>
);
};

20
src/components/menu/Navigation.tsx

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

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

@ -3,14 +3,14 @@ import React from 'react';
import { SwitchVerticalIcon } from '@heroicons/react/outline';
import { useAppSelector } from '../../../hooks/redux';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const DeviceStatusDropdown = (): JSX.Element => {
const ready = useAppSelector((state) => state.meshtastic.ready);
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus);
return (
<Button>
<IconButton>
<SwitchVerticalIcon className={`h-6 w-6 ${!ready && 'animate-pulse'}`} />
{/* <div
className={`flex w-6 h-6 rounded-full animate-pulse shadow-md ${
@ -24,6 +24,6 @@ export const DeviceStatusDropdown = (): JSX.Element => {
: 'bg-gray-400'
}`}
></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 i18n from '../../../core/translation';
import { useAppDispatch } from '../../../hooks/redux';
import i18n from '../../../translation';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const LanguageDropdown = (): JSX.Element => {
const dispatch = useAppDispatch();
@ -32,11 +32,11 @@ export const LanguageDropdown = (): JSX.Element => {
return (
<Menu as="div" className="w-10 h-10">
<div className="absolute">
<Button>
<IconButton>
<Menu.Button as="div">
<Us className="w-6 shadow rounded-sm" />
</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">
{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 { openMobileNav } from '../../../core/slices/appSlice';
import { useAppDispatch } from '../../../hooks/redux';
import { openMobileNav } from '../../../slices/appSlice';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const MobileNavToggle = (): JSX.Element => {
const dispatch = useAppDispatch();
return (
<Button
clickAction={() => {
dispatch(openMobileNav());
}}
className="md:hidden"
>
<MenuIcon className="h-6 w-6" />
</Button>
<div className="md:hidden">
<IconButton
onClick={() => {
dispatch(openMobileNav());
}}
>
<MenuIcon className="h-6 w-6" />
</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 { setDarkModeEnabled } from '../../../core/slices/appSlice';
import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
import { setDarkModeEnabled } from '../../../slices/appSlice';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const ThemeToggle = (): JSX.Element => {
const dispatch = useAppDispatch();
const darkMode = useAppSelector((state) => state.app.darkMode);
return (
<Button
clickAction={() => {
<IconButton
onClick={() => {
dispatch(setDarkModeEnabled(!darkMode));
}}
>
@ -21,6 +21,6 @@ export const ThemeToggle = (): JSX.Element => {
) : (
<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';
type DefaultDivProps = JSX.IntrinsicElements['div'];
export interface NodeProps {
node: Protobuf.NodeInfo;
}
export const Node = ({ node }: NodeProps): JSX.Element => {
export const Node = ({
node,
...props
}: NodeProps & DefaultDivProps): JSX.Element => {
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
size={30}
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 className="flex-auto p-6 sm:p-10 ">
<div className="max-w-3xl">
<div className="max-w-3xl">{children}</div>
</div>
</div>
<div className="flex-auto p-6 sm:p-10 ">{children}</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 { initReactI18next } from 'react-i18next';
import { en } from './translations/en';
import { jp } from './translations/jp';
import { pt } from './translations/pt';
import { en } from '../translations/en';
import { jp } from '../translations/jp';
import { pt } from '../translations/pt';
i18n
.use(detector)

2
src/hooks/redux.ts

@ -1,7 +1,7 @@
import type { TypedUseSelectorHook } 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 useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

6
src/index.tsx

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

2
src/pages/About.tsx

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

14
src/pages/Messages.tsx

@ -5,7 +5,7 @@ import { Protobuf } from '@meshtastic/meshtasticjs';
import { Message } from '../components/chat/Message';
import { MessageBar } from '../components/chat/MessageBar';
import { Button } from '../components/generic/Button';
import { IconButton } from '../components/generic/IconButton';
import { useAppSelector } from '../hooks/redux';
export const Messages = (): JSX.Element => {
@ -29,12 +29,12 @@ export const Messages = (): JSX.Element => {
{channelName()}
</div>
<div className="flex">
<Button>
<IconButton>
<MapIcon className="w-6 h-6" />
</Button>
<Button>
</IconButton>
<IconButton>
<UsersIcon className="w-6 h-6" />
</Button>
</IconButton>
</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">
@ -56,6 +56,6 @@ export const Messages = (): JSX.Element => {
</div>
);
};
<Button>
<IconButton>
<UsersIcon className="w-6 h-6" />
</Button>;
</IconButton>;

34
src/pages/Nodes.tsx

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

68
src/pages/Settings.tsx

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

3
tailwind.config.js

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

775
yarn.lock

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