WIP
This commit is contained in:
+7
-2
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+36
-32
@@ -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 />
|
<MobileNavToggle />
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<DeviceStatusDropdown />
|
<DeviceStatusDropdown />
|
||||||
<LanguageDropdown />
|
<LanguageDropdown />
|
||||||
<ThemeToggle />
|
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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,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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
+1
-1
@@ -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;
|
||||||
|
|||||||
+3
-3
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
+31
-3
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+39
-29
@@ -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'}
|
||||||
|
|||||||
+1
-2
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user