WIP
This commit is contained in:
@@ -1,22 +1,3 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"extends": ["@verypossible/eslint-config/react"]
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:import/recommended",
|
|
||||||
"plugin:import/typescript",
|
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/consistent-type-imports": "error"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Meshtastic.js
|
# Meshtastic Web
|
||||||
|
|
||||||
[](https://open.vscode.dev/meshtastic/meshtastic-web)
|
[](https://open.vscode.dev/meshtastic/meshtastic-web)
|
||||||
|
|
||||||
|
|||||||
+10
-13
@@ -11,14 +11,10 @@
|
|||||||
"lint": "eslint 'src/**/*.{ts,tsx}'"
|
"lint": "eslint 'src/**/*.{ts,tsx}'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@headlessui/react": "^1.4.0",
|
||||||
"@emotion/styled": "^11.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",
|
||||||
"i18next": "^20.3.5",
|
"i18next": "^20.3.5",
|
||||||
"i18next-browser-languagedetector": "^6.1.2",
|
"i18next-browser-languagedetector": "^6.1.2",
|
||||||
@@ -28,8 +24,8 @@
|
|||||||
"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",
|
||||||
"react-select": "^5.0.0-beta.0",
|
|
||||||
"type-route": "^0.6.0",
|
"type-route": "^0.6.0",
|
||||||
|
"use-breakpoint": "^2.0.1",
|
||||||
"yarn": "^1.22.11"
|
"yarn": "^1.22.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -37,23 +33,24 @@
|
|||||||
"@snowpack/plugin-postcss": "^1.4.3",
|
"@snowpack/plugin-postcss": "^1.4.3",
|
||||||
"@snowpack/plugin-react-refresh": "^2.5.0",
|
"@snowpack/plugin-react-refresh": "^2.5.0",
|
||||||
"@snowpack/plugin-typescript": "^1.2.0",
|
"@snowpack/plugin-typescript": "^1.2.0",
|
||||||
"@types/eslint": "^7.2.13",
|
|
||||||
"@types/react": "^17.0.13",
|
"@types/react": "^17.0.13",
|
||||||
"@types/react-dom": "^17.0.8",
|
"@types/react-dom": "^17.0.8",
|
||||||
"@types/react-redux": "^7.1.16",
|
"@types/react-redux": "^7.1.16",
|
||||||
"@types/snowpack-env": "^2.3.3",
|
"@types/snowpack-env": "^2.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
"@typescript-eslint/eslint-plugin": "^4.29.1",
|
||||||
"@typescript-eslint/parser": "^4.28.1",
|
"@typescript-eslint/parser": "^4.29.1",
|
||||||
|
"@verypossible/eslint-config": "^1.6.0",
|
||||||
"autoprefixer": "^10.2.6",
|
"autoprefixer": "^10.2.6",
|
||||||
"eslint": "^7.29.0",
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-import": "^2.23.4",
|
"eslint-import-resolver-babel-module": "^5.3.1",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-import-resolver-typescript": "^2.4.0",
|
||||||
|
"eslint-plugin-import": "^2.24.0",
|
||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"gzipper": "^5.0.0",
|
"gzipper": "^5.0.0",
|
||||||
"postcss": "^8.3.5",
|
"postcss": "^8.3.5",
|
||||||
"postcss-cli": "^8.3.1",
|
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"snowpack": "^3.7.1",
|
"snowpack": "^3.7.1",
|
||||||
"tailwindcss": "^2.2.4",
|
"tailwindcss": "^2.2.4",
|
||||||
|
|||||||
+1
-1
@@ -3,4 +3,4 @@ module.exports = {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
alias: {
|
||||||
|
// Type 1: Package Import Alias
|
||||||
|
// "lodash": "lodash-es",
|
||||||
|
// Type 2: Local Directory Import Alias (relative to cwd)
|
||||||
|
'@app': './src',
|
||||||
|
'@pages': './src/pages',
|
||||||
|
'@components': './src/components',
|
||||||
|
'@core': './src/core',
|
||||||
|
'@static': './src/static',
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
/* Enable an SPA Fallback in development: */
|
/* Enable an SPA Fallback in development: */
|
||||||
// {"match": "routes", "src": ".*", "dest": "/index.html"},
|
// {"match": "routes", "src": ".*", "dest": "/index.html"},
|
||||||
|
|||||||
+46
-49
@@ -1,17 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ThemeProvider } from '@material-ui/system';
|
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||||
import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs';
|
import { DeviceStatusDropdown } from '@components/menu/buttons/DeviceStatusDropdown';
|
||||||
|
import { MobileNavToggle } from '@components/menu/buttons/MobileNavToggle';
|
||||||
import { DeviceStatusDropdown } from './components/menu/buttons/DeviceStatusDropdown';
|
import { ThemeToggle } from '@components/menu/buttons/ThemeToggle';
|
||||||
import { LanguageDropdown } from './components/menu/buttons/LanguageDropdown';
|
import { Logo } from '@components/menu/Logo';
|
||||||
import { MobileNavToggle } from './components/menu/buttons/MobileNavToggle';
|
import { MobileNav } from '@components/menu/MobileNav';
|
||||||
import { ThemeToggle } from './components/menu/buttons/ThemeToggle';
|
import { Navigation } from '@components/menu/Navigation';
|
||||||
import { Logo } from './components/menu/Logo';
|
import { connection } from '@core/connection';
|
||||||
import { MobileNav } from './components/menu/MobileNav';
|
import { useRoute } from '@core/router';
|
||||||
import { Navigation } from './components/menu/Navigation';
|
|
||||||
import { connection } from './core/connection';
|
|
||||||
import { useRoute } from './core/router';
|
|
||||||
import {
|
import {
|
||||||
ackMessage,
|
ackMessage,
|
||||||
addChannel,
|
addChannel,
|
||||||
@@ -22,13 +19,12 @@ import {
|
|||||||
setMyNodeInfo,
|
setMyNodeInfo,
|
||||||
setPreferences,
|
setPreferences,
|
||||||
setReady,
|
setReady,
|
||||||
} from './core/slices/meshtasticSlice';
|
} from '@core/slices/meshtasticSlice';
|
||||||
import { theme } from './core/theme';
|
import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs';
|
||||||
import { useAppDispatch, useAppSelector } from './hooks/redux';
|
import { About } from '@pages/About';
|
||||||
import { About } from './pages/About';
|
import { Messages } from '@pages/Messages';
|
||||||
import { Messages } from './pages/Messages';
|
import { Nodes } from '@pages/Nodes/Index';
|
||||||
import { Nodes } from './pages/Nodes';
|
import { Settings } from '@pages/settings/Index';
|
||||||
import { Settings } from './pages/Settings';
|
|
||||||
|
|
||||||
const App = (): JSX.Element => {
|
const App = (): JSX.Element => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -44,12 +40,14 @@ const App = (): JSX.Element => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
|
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
|
||||||
|
|
||||||
connection.connect({
|
void connection.connect({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
address: hostOverrideEnabled
|
address: hostOverrideEnabled
|
||||||
? hostOverride
|
? hostOverride
|
||||||
: import.meta.env.NODE_ENV === 'production'
|
: import.meta.env.NODE_ENV === 'production'
|
||||||
? window.location.hostname
|
? window.location.hostname
|
||||||
: import.meta.env.SNOWPACK_PUBLIC_DEVICE_IP,
|
: import.meta.env.SNOWPACK_PUBLIC_DEVICE_IP ??
|
||||||
|
'http://meshtastic.local',
|
||||||
receiveBatchRequests: false,
|
receiveBatchRequests: false,
|
||||||
tls: false,
|
tls: false,
|
||||||
fetchInterval: 2000,
|
fetchInterval: 2000,
|
||||||
@@ -116,7 +114,7 @@ const App = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return (): void => {
|
||||||
connection.onDeviceStatus.cancelAll();
|
connection.onDeviceStatus.cancelAll();
|
||||||
connection.onMyNodeInfo.cancelAll();
|
connection.onMyNodeInfo.cancelAll();
|
||||||
connection.onNodeInfoPacket.cancelAll();
|
connection.onNodeInfoPacket.cancelAll();
|
||||||
@@ -128,41 +126,40 @@ const App = (): JSX.Element => {
|
|||||||
}, [dispatch, myNodeInfo.myNodeNum]);
|
}, [dispatch, myNodeInfo.myNodeNum]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme(darkMode)}>
|
<div
|
||||||
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
|
className={`h-screen w-screen ${darkMode ? 'dark rs-theme-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="flex flex-col h-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 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 overflow-hidden bg-white border-b md:mt-12 md:mx-8 md:pt-4 md:pb-3 md:rounded-t-xl dark:border-gray-600 md:shadow-md 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 className="hidden md:flex" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<MobileNav />
|
||||||
|
|
||||||
<MobileNav />
|
<div className="flex flex-grow w-full min-h-0 md:px-8 md:mb-8">
|
||||||
|
<div className="flex w-full bg-gray-100 md:shadow-xl md:overflow-hidden dark:bg-secondaryDark md:rounded-b-xl">
|
||||||
<div className="flex flex-grow min-h-0 w-full sm:px-8 sm:mb-8">
|
{route.name === 'messages' && <Messages />}
|
||||||
<div className="flex w-full sm:shadow-xl sm:overflow-hidden bg-gray-100 dark:bg-secondaryDark sm:rounded-b-xl">
|
{route.name === 'nodes' && <Nodes />}
|
||||||
{route.name === 'messages' && <Messages />}
|
{route.name === 'settings' && <Settings />}
|
||||||
{route.name === 'nodes' && <Nodes />}
|
{route.name === 'about' && <About />}
|
||||||
{route.name === 'settings' && <Settings />}
|
{route.name === false && 'Not Found'}
|
||||||
{route.name === 'about' && <About />}
|
|
||||||
{route.name === false && 'Not Found'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -19,9 +19,13 @@ export const Message = ({
|
|||||||
}: MessageProps): JSX.Element => {
|
}: MessageProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex space-x-2 ${!isSender && 'ml-auto flex-row-reverse'}`}
|
className={`flex space-x-2 ${
|
||||||
|
!isSender ? 'ml-auto flex-row-reverse' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`shadow-md rounded-full mt-auto ${!isSender && 'ml-2'}`}>
|
<div
|
||||||
|
className={`shadow-md rounded-full mt-auto ${!isSender ? 'ml-2' : ''}`}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={30}
|
size={30}
|
||||||
name={senderName ?? 'UNK'}
|
name={senderName ?? 'UNK'}
|
||||||
@@ -37,11 +41,11 @@ export const Message = ({
|
|||||||
: 'bg-primary text-blue-50 rounded-bl-lg'
|
: 'bg-primary text-blue-50 rounded-bl-lg'
|
||||||
} ${ack ? 'animate-none' : 'animate-pulse'}`}
|
} ${ack ? 'animate-none' : 'animate-pulse'}`}
|
||||||
>
|
>
|
||||||
<div className="min-w-4 leading-5">{message}</div>
|
<div className="leading-5 min-w-4">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600">{senderName}</div>
|
<div className="text-xs text-gray-600">{senderName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto mb-4 text-xs font-medium text-secondary mr-3 dark:text-gray-200">
|
<div className="mt-auto mb-4 mr-3 text-xs font-medium text-secondary dark:text-gray-200">
|
||||||
{rxTime.toLocaleTimeString(undefined, {
|
{rxTime.toLocaleTimeString(undefined, {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
|
|||||||
@@ -2,59 +2,57 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { Input } from '@components/generic/Input';
|
||||||
|
import { connection } from '@core/connection';
|
||||||
import {
|
import {
|
||||||
EmojiHappyIcon,
|
EmojiHappyIcon,
|
||||||
PaperAirplaneIcon,
|
PaperAirplaneIcon,
|
||||||
PaperClipIcon,
|
PaperClipIcon,
|
||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
|
|
||||||
import { connection } from '../../core/connection';
|
|
||||||
import { useAppSelector } from '../../hooks/redux';
|
|
||||||
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);
|
||||||
const [currentMessage, setCurrentMessage] = React.useState('');
|
const [currentMessage, setCurrentMessage] = React.useState('');
|
||||||
const sendMessage = () => {
|
const sendMessage = (): void => {
|
||||||
if (ready) {
|
if (ready) {
|
||||||
connection.sendText(currentMessage, undefined, true);
|
void connection.sendText(currentMessage, undefined, true);
|
||||||
setCurrentMessage('');
|
setCurrentMessage('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
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 w-full p-4 mx-auto space-x-2 text-gray-500 bg-gray-50 dark:bg-transparent dark:text-gray-400">
|
||||||
<div className="flex">
|
<div className="flex w-full max-w-4xl">
|
||||||
<IconButton>
|
<div className="flex">
|
||||||
<EmojiHappyIcon className="w-6 h-6" />
|
<Button icon={<EmojiHappyIcon className="w-5 h-5" />} circle />
|
||||||
</IconButton>
|
<Button icon={<PaperClipIcon className="w-5 h-5" />} circle />
|
||||||
|
</div>
|
||||||
<IconButton>
|
<form
|
||||||
<PaperClipIcon className="w-6 h-6" />
|
className="flex w-full space-x-2"
|
||||||
</IconButton>
|
onSubmit={(e): void => {
|
||||||
</div>
|
e.preventDefault();
|
||||||
<form
|
sendMessage();
|
||||||
className="flex w-full space-x-2"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendMessage();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
minLength={2}
|
|
||||||
placeholder={`${t('placeholder.message')}...`}
|
|
||||||
disabled={!ready}
|
|
||||||
value={currentMessage}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCurrentMessage(e.target.value);
|
|
||||||
}}
|
}}
|
||||||
className="focus:outline-none h-10 w-full resize-none rounded-full border border-gray-300 dark:bg-gray-900 px-4"
|
>
|
||||||
/>
|
<Input
|
||||||
<IconButton type="submit">
|
type="text"
|
||||||
<PaperAirplaneIcon className="w-6 h-6 rotate-90" />
|
minLength={2}
|
||||||
</IconButton>
|
placeholder={`${t('placeholder.message')}...`}
|
||||||
</form>
|
disabled={!ready}
|
||||||
|
value={currentMessage}
|
||||||
|
onChange={(e): void => {
|
||||||
|
setCurrentMessage(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<PaperAirplaneIcon className="w-5 h-5" />}
|
||||||
|
type="submit"
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
type DefaultInputProps = JSX.IntrinsicElements['input'];
|
|
||||||
|
|
||||||
export interface InputProps {
|
|
||||||
valid?: boolean;
|
|
||||||
validationMessage?: string;
|
|
||||||
icon?: JSX.Element;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Input = React.forwardRef<
|
|
||||||
HTMLInputElement,
|
|
||||||
InputProps & DefaultInputProps
|
|
||||||
>(function Input(
|
|
||||||
{
|
|
||||||
valid,
|
|
||||||
validationMessage,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
id,
|
|
||||||
...props
|
|
||||||
}: InputProps & DefaultInputProps,
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label htmlFor={id} className="block text-sm font-medium dark:text-white">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
{icon && (
|
|
||||||
<div className="flex absolute inset-y-0 left-0 px-3 items-center pointer-events-none">
|
|
||||||
{React.cloneElement(icon, {
|
|
||||||
className: 'w-5 h-5 text-gray-500 dark:text-gray-600',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
className={`block w-full h-11 rounded-md border shadow-sm focus:outline-none focus:border-primary dark:focus:border-primary dark:bg-secondaryDark dark:border-gray-600 dark:text-white ${
|
|
||||||
icon ? 'pl-9' : 'pl-2'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!valid && (
|
|
||||||
<div className="text-sm text-gray-600">{validationMessage}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { FieldValues, UseControllerProps } from 'react-hook-form';
|
|
||||||
import { Controller } from 'react-hook-form';
|
|
||||||
import ReactSelect from 'react-select';
|
|
||||||
|
|
||||||
interface SelectProps<T> extends UseControllerProps<T> {
|
|
||||||
label: string;
|
|
||||||
options: {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Select = <T extends FieldValues>({
|
|
||||||
name,
|
|
||||||
control,
|
|
||||||
label,
|
|
||||||
options,
|
|
||||||
}: SelectProps<T>): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
|
|
||||||
|
interface LocalBlurProps {
|
||||||
|
disableOnMd?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlurProps = LocalBlurProps & DefaultDivProps;
|
||||||
|
|
||||||
|
export const Blur = ({
|
||||||
|
disableOnMd,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: BlurProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 z-10 w-full h-full transition-opacity ${
|
||||||
|
disableOnMd ? 'md:hidden' : 'test'
|
||||||
|
} ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
className={`absolute inset-0 w-full h-full backdrop-filter backdrop-blur-sm ${
|
||||||
|
disableOnMd ? 'md:hidden' : 'test'
|
||||||
|
}`}
|
||||||
|
tabIndex={0}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,28 +1,50 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import MaterialButton from '@material-ui/core/Button';
|
type DefaultButtonProps = JSX.IntrinsicElements['button'];
|
||||||
import type { ButtonProps as MaterialButtonProps } from '@material-ui/core/Button/Button';
|
|
||||||
|
|
||||||
interface LocalButtonProps {
|
interface LocalButtonProps {
|
||||||
text: string;
|
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
|
circle?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
border?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonProps = MaterialButtonProps & LocalButtonProps;
|
export type ButtonProps = LocalButtonProps & DefaultButtonProps;
|
||||||
|
|
||||||
export const Button = ({ text, icon, ...props }: ButtonProps): JSX.Element => {
|
export const Button = ({
|
||||||
|
icon,
|
||||||
|
circle,
|
||||||
|
className,
|
||||||
|
active,
|
||||||
|
border,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ButtonProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<MaterialButton
|
<button
|
||||||
|
className={`items-center select-none flex dark:text-white ${
|
||||||
|
active && !disabled ? 'bg-gray-100 dark:bg-gray-700' : ''
|
||||||
|
} ${
|
||||||
|
circle ? 'rounded-full h-10 w-10' : 'rounded-md p-3 space-x-3 text-sm'
|
||||||
|
} ${
|
||||||
|
disabled
|
||||||
|
? 'cursor-not-allowed dark:bg-primaryDark bg-white'
|
||||||
|
: 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 hover:shadow-md'
|
||||||
|
} ${border ? 'border dark:border-gray-600' : ''} ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
className="dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
||||||
>
|
>
|
||||||
<div className="flex p-3">
|
{icon && (
|
||||||
{icon &&
|
<div
|
||||||
React.cloneElement(icon, {
|
className={`text-gray-500 dark:text-gray-400 ${
|
||||||
className: 'h-6 w-6 mr-3 text-gray-500 dark:text-gray-400',
|
circle ? 'mx-auto' : ''
|
||||||
})}
|
}`}
|
||||||
<span>{text}</span>
|
>
|
||||||
</div>
|
{icon}
|
||||||
</MaterialButton>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span>{children}</span>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Blur } from '@components/generic/Blur';
|
||||||
|
|
||||||
|
type DefaultAsideProps = JSX.IntrinsicElements['aside'];
|
||||||
|
|
||||||
|
interface LocalDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
permenant?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
export type DrawerProps = LocalDrawerProps & DefaultAsideProps;
|
||||||
|
|
||||||
|
export const Drawer = ({
|
||||||
|
open,
|
||||||
|
permenant,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: DrawerProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{open && <Blur disableOnMd={true} onClick={onClose} />}
|
||||||
|
|
||||||
|
<aside
|
||||||
|
className={`transform top-0 left-0 bg-white dark:bg-secondaryDark shadow-md max-w-xs w-full border-r dark:border-gray-600 h-full overflow-auto ease-in-out transition-all duration-300 z-30 ${
|
||||||
|
permenant ? '' : 'absolute'
|
||||||
|
} ${open ? 'translate-x-0' : '-translate-x-full'}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type DefaultInputProps = JSX.IntrinsicElements['input'];
|
||||||
|
|
||||||
|
interface LocalInputProps {
|
||||||
|
icon?: JSX.Element;
|
||||||
|
label?: string;
|
||||||
|
valid?: boolean;
|
||||||
|
validationMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputProps = LocalInputProps & DefaultInputProps;
|
||||||
|
|
||||||
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
function Input(
|
||||||
|
{ icon, label, valid, validationMessage, id, ...props }: InputProps,
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="block text-sm font-medium dark:text-white"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
{icon && (
|
||||||
|
<div className="absolute inset-y-0 left-0 flex items-center px-3 pointer-events-none">
|
||||||
|
{React.cloneElement(icon, {
|
||||||
|
className: 'w-5 h-5 text-gray-500 dark:text-gray-600',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={`block w-full h-11 rounded-md border shadow-sm focus:outline-none focus:border-primary dark:focus:border-primary bg-white dark:bg-secondaryDark dark:border-gray-600 dark:text-white ${
|
||||||
|
icon ? 'pl-9' : 'pl-2'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!valid && (
|
||||||
|
<div className="text-sm text-gray-600">{validationMessage}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Listbox } from '@headlessui/react';
|
||||||
|
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
|
||||||
|
|
||||||
|
export interface SelectProps {
|
||||||
|
label: string;
|
||||||
|
options: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
}[];
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select = ({
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: SelectProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<label htmlFor={id} className="block text-sm font-medium dark:text-white">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Listbox value={value} onChange={onChange}>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<Listbox.Button className="relative w-full text-left bg-white border rounded-md shadow-sm h-11 focus:outline-none focus:border-primary dark:focus:border-primary dark:bg-secondaryDark dark:border-gray-600 dark:text-white">
|
||||||
|
<span className="block truncate">{value}</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<SelectorIcon
|
||||||
|
className="w-5 h-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Listbox.Options className="absolute w-full bg-white border rounded-md shadow-sm focus:outline-none dark:bg-secondaryDark dark:border-gray-600 dark:text-white">
|
||||||
|
{options.map((option) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={option.value}
|
||||||
|
className={({ active }): string =>
|
||||||
|
`cursor-default select-none relative py-2 pl-10 pr-4 first:rounded-t-md last:rounded-b-md dark:text-white ${
|
||||||
|
active ? 'bg-gray-200 dark:bg-primaryDark' : 'text-gray-900'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{({ selected, active }): JSX.Element => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
selected ? 'font-medium' : 'font-normal'
|
||||||
|
} block truncate`}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</span>
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
active ? 'text-amber-600' : 'text-amber-600'
|
||||||
|
}
|
||||||
|
absolute inset-y-0 left-0 flex items-center pl-3`}
|
||||||
|
>
|
||||||
|
<CheckIcon className="w-5 h-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
|
|
||||||
|
interface LocalSidebarItemProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
selected: boolean;
|
||||||
|
icon: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SidebarItemProps = LocalSidebarItemProps & DefaultDivProps;
|
||||||
|
|
||||||
|
export const SidebarItem = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
selected,
|
||||||
|
icon,
|
||||||
|
}: SidebarItemProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${
|
||||||
|
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-gray-500 dark:text-gray-400">{icon}</div>
|
||||||
|
<div className="ml-3 text-left">
|
||||||
|
<div className="font-medium text-left">{title}</div>
|
||||||
|
<div className="mt-0.5 text-gray-400 text-sm">{description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,8 +3,16 @@ import React from 'react';
|
|||||||
export const Logo = (): JSX.Element => {
|
export const Logo = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<img className="w-16 dark:hidden" src="Mesh_Logo_Black.svg" />
|
<img
|
||||||
<img className="hidden dark:flex w-16" src="Mesh_Logo_White.svg" />
|
title="Logo"
|
||||||
|
className="w-16 dark:hidden"
|
||||||
|
src="Mesh_Logo_Black.svg"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
title="Logo"
|
||||||
|
className="hidden w-16 dark:flex"
|
||||||
|
src="Mesh_Logo_White.svg"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { Link } from 'type-route';
|
|
||||||
|
|
||||||
interface MenuButtonProps {
|
|
||||||
icon: JSX.Element;
|
|
||||||
text: string;
|
|
||||||
link: Link;
|
|
||||||
clickAction?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MenuButton = ({
|
|
||||||
icon,
|
|
||||||
text,
|
|
||||||
link,
|
|
||||||
clickAction,
|
|
||||||
}: MenuButtonProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
if (clickAction) {
|
|
||||||
clickAction();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
{...link}
|
|
||||||
className="flex text-sm h-12 items-center dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md cursor-pointer px-3 select-none"
|
|
||||||
>
|
|
||||||
{React.cloneElement(icon, {
|
|
||||||
className: 'h-6 w-6 mr-3 text-gray-500 dark:text-gray-400',
|
|
||||||
})}
|
|
||||||
<span className="">{text}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||||
AnnotationIcon,
|
import { Drawer } from '@components/generic/Drawer';
|
||||||
CogIcon,
|
import { closeMobileNav } from '@core/slices/appSlice';
|
||||||
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 { Button } from '../generic/Button';
|
|
||||||
import { Logo } from './Logo';
|
import { Logo } from './Logo';
|
||||||
|
import { Navigation } from './Navigation';
|
||||||
|
|
||||||
export const MobileNav = (): JSX.Element => {
|
export const MobileNav = (): JSX.Element => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -20,41 +13,22 @@ export const MobileNav = (): JSX.Element => {
|
|||||||
const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen);
|
const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SwipeableDrawer
|
<Drawer
|
||||||
open={mobileNavOpen}
|
open={mobileNavOpen}
|
||||||
anchor="left"
|
onClose={(): void => {
|
||||||
onClose={() => {
|
|
||||||
dispatch(closeMobileNav());
|
dispatch(closeMobileNav());
|
||||||
}}
|
}}
|
||||||
onOpen={() => {
|
|
||||||
dispatch(openMobileNav());
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col dark:bg-secondaryDark h-full">
|
<div className="flex flex-col w-64">
|
||||||
<div className="m-auto my-6">
|
<div className="m-auto my-6">
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Navigation
|
||||||
icon={<AnnotationIcon />}
|
onClick={(): void => {
|
||||||
text={'Messages'}
|
dispatch(closeMobileNav());
|
||||||
{...routes.messages().link}
|
}}
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<ViewGridIcon />}
|
|
||||||
text={'Nodes'}
|
|
||||||
{...routes.nodes().link}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<CogIcon />}
|
|
||||||
text={'Settings'}
|
|
||||||
{...routes.settings().link}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
icon={<InformationCircleIcon />}
|
|
||||||
text={'About'}
|
|
||||||
{...routes.about().link}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SwipeableDrawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { routes, useRoute } from '@core/router';
|
||||||
import {
|
import {
|
||||||
AnnotationIcon,
|
AnnotationIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
@@ -7,33 +9,60 @@ import {
|
|||||||
ViewGridIcon,
|
ViewGridIcon,
|
||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
|
|
||||||
import { routes } from '../../core/router';
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
import { Button } from '../generic/Button';
|
|
||||||
|
|
||||||
export const Navigation = (): JSX.Element => {
|
export type NavigationProps = DefaultDivProps;
|
||||||
|
|
||||||
|
export const Navigation = ({
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: NavigationProps): JSX.Element => {
|
||||||
|
const route = useRoute();
|
||||||
return (
|
return (
|
||||||
<div className="hidden md:flex flex-auto flex-0 relative items-center h-16 px-4 ">
|
<div
|
||||||
<div className="flex items-center">
|
className={`h-16 px-4 md:space-x-2 space-y-2 md:space-y-0 ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div onClick={onClick}>
|
||||||
<Button
|
<Button
|
||||||
icon={<AnnotationIcon />}
|
icon={<AnnotationIcon className="w-6 h-6" />}
|
||||||
text={'Messages'}
|
active={route.name === 'messages'}
|
||||||
|
className="w-full md:w-auto"
|
||||||
{...routes.messages().link}
|
{...routes.messages().link}
|
||||||
/>
|
>
|
||||||
|
Messages
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div onClick={onClick}>
|
||||||
<Button
|
<Button
|
||||||
icon={<ViewGridIcon />}
|
icon={<ViewGridIcon className="w-6 h-6" />}
|
||||||
text={'Nodes'}
|
className="w-full md:w-auto"
|
||||||
|
active={route.name === 'nodes'}
|
||||||
{...routes.nodes().link}
|
{...routes.nodes().link}
|
||||||
/>
|
>
|
||||||
|
Nodes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div onClick={onClick}>
|
||||||
<Button
|
<Button
|
||||||
icon={<CogIcon />}
|
icon={<CogIcon className="w-6 h-6" />}
|
||||||
text={'Settings'}
|
className="w-full md:w-auto"
|
||||||
|
active={route.name === 'settings'}
|
||||||
{...routes.settings().link}
|
{...routes.settings().link}
|
||||||
/>
|
>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div onClick={onClick}>
|
||||||
<Button
|
<Button
|
||||||
icon={<InformationCircleIcon />}
|
icon={<InformationCircleIcon className="w-6 h-6" />}
|
||||||
text={'About'}
|
className="w-full md:w-auto"
|
||||||
|
active={route.name === 'about'}
|
||||||
{...routes.about().link}
|
{...routes.about().link}
|
||||||
/>
|
>
|
||||||
|
About
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
import { SwitchVerticalIcon } from '@heroicons/react/outline';
|
import { SwitchVerticalIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
import { useAppSelector } from '../../../hooks/redux';
|
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton>
|
<Button
|
||||||
<SwitchVerticalIcon className={`h-6 w-6 ${!ready && 'animate-pulse'}`} />
|
icon={
|
||||||
{/* <div
|
<SwitchVerticalIcon
|
||||||
className={`flex w-6 h-6 rounded-full animate-pulse shadow-md ${
|
className={`h-6 w-6 ${!ready ? 'animate-pulse' : ''}`}
|
||||||
deviceStatus <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
|
/>
|
||||||
? 'bg-red-400 animate-pulse'
|
}
|
||||||
: deviceStatus <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
|
circle
|
||||||
!ready
|
/>
|
||||||
? 'bg-yellow-400 animate-pulse'
|
|
||||||
: ready
|
|
||||||
? 'bg-green-400'
|
|
||||||
: 'bg-gray-400'
|
|
||||||
}`}
|
|
||||||
></div> */}
|
|
||||||
</IconButton>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Jp, Pt, Us } from 'react-flags-select';
|
|
||||||
|
|
||||||
import { Menu } from '@headlessui/react';
|
|
||||||
|
|
||||||
import i18n from '../../../core/translation';
|
|
||||||
import { useAppDispatch } from '../../../hooks/redux';
|
|
||||||
import { IconButton } from '../../generic/IconButton';
|
|
||||||
|
|
||||||
export const LanguageDropdown = (): JSX.Element => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const languages = [
|
|
||||||
{
|
|
||||||
name: 'English',
|
|
||||||
value: 'en',
|
|
||||||
flag: <Us className="w-6" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Português',
|
|
||||||
value: 'pt',
|
|
||||||
flag: <Pt className="w-6" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Japanese',
|
|
||||||
value: 'jp',
|
|
||||||
flag: <Jp className="w-6" />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu as="div" className="w-10 h-10">
|
|
||||||
<div className="absolute">
|
|
||||||
<IconButton>
|
|
||||||
<Menu.Button as="div">
|
|
||||||
<Us className="w-6 shadow rounded-sm" />
|
|
||||||
</Menu.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) => (
|
|
||||||
<Menu.Item
|
|
||||||
key={index}
|
|
||||||
onClick={() => {
|
|
||||||
i18n.changeLanguage(language.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ active }) => (
|
|
||||||
<button
|
|
||||||
className={`dark:text-white first:rounded-t-md last:rounded-b-md space-x-2 group flex items-center w-full px-2 py-2 text-sm ${
|
|
||||||
active && 'bg-gray-200 dark:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{language.flag}
|
|
||||||
<p className="font-medium">{language.name}</p>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
{/* ... */}
|
|
||||||
</Menu.Items>
|
|
||||||
</div>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { openMobileNav } from '@core/slices/appSlice';
|
||||||
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 { IconButton } from '../../generic/IconButton';
|
|
||||||
|
|
||||||
export const MobileNavToggle = (): JSX.Element => {
|
export const MobileNavToggle = (): JSX.Element => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<IconButton
|
<Button
|
||||||
onClick={() => {
|
icon={<MenuIcon className="w-5 h-5" />}
|
||||||
|
onClick={(): void => {
|
||||||
dispatch(openMobileNav());
|
dispatch(openMobileNav());
|
||||||
}}
|
}}
|
||||||
>
|
circle
|
||||||
<MenuIcon className="h-6 w-6" />
|
/>
|
||||||
</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { setDarkModeEnabled } from '@core/slices/appSlice';
|
||||||
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 { IconButton } from '../../generic/IconButton';
|
|
||||||
|
|
||||||
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 (
|
||||||
<IconButton
|
<Button
|
||||||
onClick={() => {
|
icon={
|
||||||
|
darkMode ? (
|
||||||
|
<SunIcon className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<MoonIcon className="w-5 h-5" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
circle
|
||||||
|
onClick={(): void => {
|
||||||
dispatch(setDarkModeEnabled(!darkMode));
|
dispatch(setDarkModeEnabled(!darkMode));
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{darkMode ? (
|
|
||||||
<SunIcon className="h-6 w-6" />
|
|
||||||
) : (
|
|
||||||
<MoonIcon className="h-6 w-6" />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -4,16 +4,21 @@ export interface PrimaryTemplateProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
tagline: string;
|
tagline: string;
|
||||||
|
button?: JSX.Element;
|
||||||
|
footer?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrimaryTemplate = ({
|
export const PrimaryTemplate = ({
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
tagline,
|
tagline,
|
||||||
|
button,
|
||||||
|
footer,
|
||||||
}: PrimaryTemplateProps): JSX.Element => {
|
}: PrimaryTemplateProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-auto min-w-0">
|
<div className="flex flex-col flex-auto min-w-0">
|
||||||
<div className="flex flex-col sm:flex-row flex-0 sm:items-center sm:justify-between p-6 sm:py-8 sm:px-10 border-b dark:border-gray-600 bg-white dark:bg-secondaryDark">
|
<div className="flex p-6 bg-white border-b md:flex-row flex-0 md:items-center md:justify-between md:py-8 md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
|
||||||
|
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex flex-wrap items-center font-medium">
|
<div className="flex flex-wrap items-center font-medium">
|
||||||
<div>
|
<div>
|
||||||
@@ -21,13 +26,20 @@ export const PrimaryTemplate = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight leading-7 sm:leading-10 truncate dark:text-white">
|
<h2 className="text-3xl font-extrabold leading-7 tracking-tight truncate md:text-4xl md:leading-10 dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-auto p-6 sm:p-10 ">{children}</div>
|
<div className="flex-auto flex-grow p-6 md:p-10">{children}</div>
|
||||||
|
|
||||||
|
{footer && (
|
||||||
|
<div className="flex p-6 bg-white border-t md:flex-row flex-0 md:items-center md:justify-between md:py-8 md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
|
||||||
|
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
|
||||||
|
<div className="flex-1 min-w-0">{footer}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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,7 +6,7 @@ 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
|
void i18n
|
||||||
.use(detector)
|
.use(detector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import useBreakpointHook from 'use-breakpoint';
|
||||||
|
|
||||||
|
const BREAKPOINTS = {
|
||||||
|
sm: 640,
|
||||||
|
// => @media (min-width: 640px) { ... }
|
||||||
|
|
||||||
|
md: 768,
|
||||||
|
// => @media (min-width: 768px) { ... }
|
||||||
|
|
||||||
|
lg: 1024,
|
||||||
|
// => @media (min-width: 1024px) { ... }
|
||||||
|
|
||||||
|
xl: 1280,
|
||||||
|
// => @media (min-width: 1280px) { ... }
|
||||||
|
|
||||||
|
'2xl': 1536,
|
||||||
|
// => @media (min-width: 1536px) { ... }
|
||||||
|
};
|
||||||
|
export const useBreakpoint = () => useBreakpointHook(BREAKPOINTS);
|
||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
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 '../core/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;
|
||||||
|
|||||||
+5
-4
@@ -1,14 +1,15 @@
|
|||||||
import './index.css';
|
import '@app/index.css';
|
||||||
import './core/translation';
|
import '@core/translation';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { RouteProvider } from '@core/router';
|
||||||
|
import { store } from '@core/store';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { RouteProvider } from './core/router';
|
|
||||||
import { store } from './core/store';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
+1
-3
@@ -1,13 +1,11 @@
|
|||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-16
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Message } from '@components/chat/Message';
|
||||||
|
import { MessageBar } from '@components/chat/MessageBar';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
import { HashtagIcon, MapIcon, UsersIcon } from '@heroicons/react/outline';
|
import { HashtagIcon, MapIcon, UsersIcon } from '@heroicons/react/outline';
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { Message } from '../components/chat/Message';
|
|
||||||
import { MessageBar } from '../components/chat/MessageBar';
|
|
||||||
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 => {
|
||||||
@@ -13,7 +13,7 @@ export const Messages = (): JSX.Element => {
|
|||||||
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
||||||
const channels = useAppSelector((state) => state.meshtastic.channels);
|
const channels = useAppSelector((state) => state.meshtastic.channels);
|
||||||
|
|
||||||
const channelName = () => {
|
const channelName = (): string => {
|
||||||
const name =
|
const name =
|
||||||
channels.find((channel) => channel.role === Protobuf.Channel_Role.PRIMARY)
|
channels.find((channel) => channel.role === Protobuf.Channel_Role.PRIMARY)
|
||||||
?.settings?.name ?? 'Unknown';
|
?.settings?.name ?? 'Unknown';
|
||||||
@@ -23,21 +23,18 @@ export const Messages = (): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full">
|
||||||
<div className="flex justify-between w-full border-b dark:border-gray-600 dark:text-gray-300 px-2">
|
<div className="flex justify-between w-full px-2 border-b dark:border-gray-600 dark:text-gray-300">
|
||||||
<div className="flex my-auto text-sm">
|
<div className="flex my-auto text-sm">
|
||||||
<HashtagIcon className="h-4 w-4 my-auto" />
|
<HashtagIcon className="w-4 h-4 my-auto" />
|
||||||
{channelName()}
|
{channelName()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<IconButton>
|
<Button icon={<MapIcon className="w-5 h-5" />} circle />
|
||||||
<MapIcon className="w-6 h-6" />
|
|
||||||
</IconButton>
|
<Button icon={<UsersIcon className="w-5 h-5" />} circle />
|
||||||
<IconButton>
|
|
||||||
<UsersIcon className="w-6 h-6" />
|
|
||||||
</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 flex-grow p-6 space-y-2 overflow-y-auto bg-white border-b md:py-8 md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
<Message
|
<Message
|
||||||
key={index}
|
key={index}
|
||||||
@@ -56,6 +53,3 @@ export const Messages = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
<IconButton>
|
|
||||||
<UsersIcon className="w-6 h-6" />
|
|
||||||
</IconButton>;
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
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">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Avatar from 'boring-avatars';
|
||||||
|
|
||||||
|
import { useBreakpoint } from '@app/hooks/breakpoint';
|
||||||
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { Drawer } from '@components/generic/Drawer';
|
||||||
|
import { SidebarItem } from '@components/generic/SidebarItem';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import { XCircleIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export const Nodes = (): JSX.Element => {
|
||||||
|
const [navOpen, setNavOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const { breakpoint } = useBreakpoint();
|
||||||
|
|
||||||
|
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Group>
|
||||||
|
<div className="relative flex w-full dark:text-white">
|
||||||
|
<Drawer
|
||||||
|
open={breakpoint === 'sm' ? navOpen : true}
|
||||||
|
permenant={breakpoint !== 'sm'}
|
||||||
|
onClose={(): void => {
|
||||||
|
setNavOpen(!navOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab.List className="flex flex-col border-b divide-y divide-gray-300 dark:divide-gray-600 dark:border-gray-600">
|
||||||
|
<div className="flex items-center justify-between m-8 mr-6 md:my-10">
|
||||||
|
<div className="text-4xl font-extrabold leading-none tracking-tight">
|
||||||
|
Nodes
|
||||||
|
</div>
|
||||||
|
<div className="md:hidden">
|
||||||
|
<Button
|
||||||
|
icon={<XCircleIcon className="w-5 h-5" />}
|
||||||
|
circle
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<Tab
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(false);
|
||||||
|
}}
|
||||||
|
key={node.num}
|
||||||
|
>
|
||||||
|
{({ selected }): JSX.Element => (
|
||||||
|
<SidebarItem
|
||||||
|
title={node.user?.longName ?? node.num.toString()}
|
||||||
|
description="Node info"
|
||||||
|
selected={selected}
|
||||||
|
icon={
|
||||||
|
<Avatar
|
||||||
|
size={30}
|
||||||
|
name={node.user?.longName ?? node.num.toString()}
|
||||||
|
variant="beam"
|
||||||
|
colors={[
|
||||||
|
'#213435',
|
||||||
|
'#46685B',
|
||||||
|
'#648A64',
|
||||||
|
'#A6B985',
|
||||||
|
'#E1E3AC',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
</Drawer>
|
||||||
|
<div className="w-full">
|
||||||
|
<Tab.Panels>
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<Tab.Panel key={node.num}>
|
||||||
|
<Node navOpen={navOpen} setNavOpen={setNavOpen} node={node} />
|
||||||
|
</Tab.Panel>
|
||||||
|
))}
|
||||||
|
</Tab.Panels>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
|
import { MenuIcon } from '@heroicons/react/outline';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export interface NodeProps {
|
||||||
|
navOpen: boolean;
|
||||||
|
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
node: Protobuf.NodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<PrimaryTemplate
|
||||||
|
title={node.user?.longName ?? node.num.toString()}
|
||||||
|
tagline="Node"
|
||||||
|
button={
|
||||||
|
<Button
|
||||||
|
icon={<MenuIcon className="w-5 h-5" />}
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(!navOpen);
|
||||||
|
}}
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">Content</div>
|
||||||
|
</PrimaryTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
|
||||||
|
|
||||||
import { Input } from '../components/form/Input';
|
|
||||||
import { Select } from '../components/form/Select';
|
|
||||||
import { Switch } from '../components/form/Switch';
|
|
||||||
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate';
|
|
||||||
import { connection } from '../core/connection';
|
|
||||||
import {
|
|
||||||
setHostOverride,
|
|
||||||
setHostOverrideEnabled,
|
|
||||||
} from '../core/slices/meshtasticSlice';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks/redux';
|
|
||||||
|
|
||||||
export const Settings = (): JSX.Element => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
|
|
||||||
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride);
|
|
||||||
const hostOverrideEnabled = useAppSelector(
|
|
||||||
(state) => state.meshtastic.hostOverrideEnabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { register, handleSubmit, control } =
|
|
||||||
useForm<Protobuf.RadioConfig_UserPreferences>({
|
|
||||||
defaultValues: radioConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
|
||||||
console.log(data);
|
|
||||||
connection.setPreferences(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [localHostOverride, setLocalHostOverride] =
|
|
||||||
React.useState(hostOverride);
|
|
||||||
const [localHostOverrideEnabled, setLocalHostOverrideEnabled] =
|
|
||||||
React.useState(hostOverrideEnabled);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PrimaryTemplate title="Settings" tagline="Device">
|
|
||||||
<form onSubmit={onSubmit}>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex pb-2 dark:text-white border-b dark:border-gray-600">
|
|
||||||
<div className="w-1/3 text-lg">WiFi</div>
|
|
||||||
<div className="space-y-2 w-full max-w-xs">
|
|
||||||
<Input
|
|
||||||
label={t('strings.wifi_ssid')}
|
|
||||||
{...register('wifiSsid', {})}
|
|
||||||
type="text"
|
|
||||||
valid={true}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={t('strings.wifi_psk')}
|
|
||||||
{...register('wifiPassword', {})}
|
|
||||||
type="password"
|
|
||||||
valid={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
<Switch
|
|
||||||
name={'isRouter'}
|
|
||||||
control={control}
|
|
||||||
label={'Is Router Node?'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
name={'isLowPower'}
|
|
||||||
control={control}
|
|
||||||
label={'Is Low Power?'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
name={'fixedPosition'}
|
|
||||||
control={control}
|
|
||||||
label={'Has Fixed Position?'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
name={'serialDisabled'}
|
|
||||||
control={control}
|
|
||||||
label={'Is Serial Disabled?'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
name={'mqttDisabled'}
|
|
||||||
control={control}
|
|
||||||
label={'Is MQTT Disabled?'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
name={'debugLogEnabled'}
|
|
||||||
control={control}
|
|
||||||
label={'Is Debug Log Enabled?'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
name={'region'}
|
|
||||||
control={control}
|
|
||||||
label="Region"
|
|
||||||
options={(() => {
|
|
||||||
return Object.keys(Protobuf.RegionCode)
|
|
||||||
.filter((value) => isNaN(Number(value)) === false)
|
|
||||||
.map((key) => {
|
|
||||||
return {
|
|
||||||
value: key,
|
|
||||||
label: Protobuf.RegionCode[parseInt(key)],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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
|
|
||||||
label={'Enable host override'}
|
|
||||||
checked={localHostOverrideEnabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
console.log(event.target.checked);
|
|
||||||
setLocalHostOverrideEnabled(event.target.checked);
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
<Input
|
|
||||||
label={'Host override'}
|
|
||||||
placeholder={'meshtastic.local'}
|
|
||||||
value={localHostOverride}
|
|
||||||
onChange={(event) => {
|
|
||||||
setLocalHostOverride(event.target.value);
|
|
||||||
}}
|
|
||||||
type="text"
|
|
||||||
valid={true}
|
|
||||||
disabled={!localHostOverrideEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(setHostOverride(localHostOverride));
|
|
||||||
dispatch(setHostOverrideEnabled(localHostOverrideEnabled));
|
|
||||||
}}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{t('strings.save_changes')}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</PrimaryTemplate>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { connection } from '@app/core/connection';
|
||||||
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { Input } from '@components/generic/Input';
|
||||||
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
|
import { MenuIcon, SaveIcon } from '@heroicons/react/outline';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export interface DeviceProps {
|
||||||
|
navOpen: boolean;
|
||||||
|
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const radioConfig = useAppSelector((state) => state.meshtastic.preferences);
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } =
|
||||||
|
useForm<Protobuf.RadioConfig_UserPreferences>({
|
||||||
|
defaultValues: radioConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
void connection.setPreferences(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrimaryTemplate
|
||||||
|
title="Device"
|
||||||
|
tagline="Settings"
|
||||||
|
button={
|
||||||
|
<Button
|
||||||
|
icon={<MenuIcon className="w-5 h-5" />}
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(!navOpen);
|
||||||
|
}}
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<Button
|
||||||
|
className="px-10 ml-auto"
|
||||||
|
icon={<SaveIcon className="w-5 h-5" />}
|
||||||
|
disabled={!formState.isDirty}
|
||||||
|
active
|
||||||
|
border
|
||||||
|
>
|
||||||
|
{t('strings.save_changes')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Input label={t('strings.wifi_ssid')} {...register('wifiSsid')} />
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label={t('strings.wifi_psk')}
|
||||||
|
{...register('wifiPassword')}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</PrimaryTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useBreakpoint } from '@app/hooks/breakpoint';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { Drawer } from '@components/generic/Drawer';
|
||||||
|
import { SidebarItem } from '@components/generic/SidebarItem';
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
import {
|
||||||
|
CollectionIcon,
|
||||||
|
DeviceMobileIcon,
|
||||||
|
WifiIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from '@heroicons/react/outline';
|
||||||
|
|
||||||
|
import { Device } from './Device';
|
||||||
|
import { Interface } from './Interface';
|
||||||
|
import { Radio } from './Radio';
|
||||||
|
|
||||||
|
export const Settings = (): JSX.Element => {
|
||||||
|
const [navOpen, setNavOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const { breakpoint } = useBreakpoint();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Group>
|
||||||
|
<div className="relative flex w-full dark:text-white">
|
||||||
|
<Drawer
|
||||||
|
open={breakpoint === 'sm' ? navOpen : true}
|
||||||
|
permenant={breakpoint !== 'sm'}
|
||||||
|
onClose={(): void => {
|
||||||
|
setNavOpen(!navOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab.List className="flex flex-col border-b divide-y divide-gray-300 dark:divide-gray-600 dark:border-gray-600">
|
||||||
|
<div className="flex items-center justify-between m-8 mr-6 md:my-10">
|
||||||
|
<div className="text-4xl font-extrabold leading-none tracking-tight">
|
||||||
|
Settings
|
||||||
|
</div>
|
||||||
|
<div className="md:hidden">
|
||||||
|
<Button
|
||||||
|
icon={<XCircleIcon className="w-5 h-5" />}
|
||||||
|
circle
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tab
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ selected }): JSX.Element => (
|
||||||
|
<SidebarItem
|
||||||
|
title="Device"
|
||||||
|
description="Device settings, such as device name and wifi settings"
|
||||||
|
selected={selected}
|
||||||
|
icon={<DeviceMobileIcon className="flex-shrink-0 w-6 h-6" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
{({ selected }): JSX.Element => (
|
||||||
|
<SidebarItem
|
||||||
|
title="Radio"
|
||||||
|
description="Adjust radio power and frequency settings"
|
||||||
|
selected={selected}
|
||||||
|
icon={<WifiIcon className="flex-shrink-0 w-6 h-6" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
{({ selected }): JSX.Element => (
|
||||||
|
<SidebarItem
|
||||||
|
title="Interface"
|
||||||
|
description="Change language and other UI settings"
|
||||||
|
selected={selected}
|
||||||
|
icon={<CollectionIcon className="flex-shrink-0 w-6 h-6" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
</Tab.List>
|
||||||
|
</Drawer>
|
||||||
|
<div className="flex w-full">
|
||||||
|
<Tab.Panels className="flex w-full">
|
||||||
|
<Tab.Panel className="flex w-full">
|
||||||
|
<Device navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="flex w-full">
|
||||||
|
<Radio navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel className="flex w-full">
|
||||||
|
<Interface navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Jp, Pt, Us } from 'react-flags-select';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Select } from '@app/components/generic/Select';
|
||||||
|
import i18n from '@app/core/translation';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
|
import { MenuIcon, SaveIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
|
export interface InterfaceProps {
|
||||||
|
navOpen: boolean;
|
||||||
|
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Interface = ({
|
||||||
|
navOpen,
|
||||||
|
setNavOpen,
|
||||||
|
}: InterfaceProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrimaryTemplate
|
||||||
|
title="Interface"
|
||||||
|
tagline="Settings"
|
||||||
|
button={
|
||||||
|
<Button
|
||||||
|
icon={<MenuIcon className="w-5 h-5" />}
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(!navOpen);
|
||||||
|
}}
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<Button
|
||||||
|
className="px-10 ml-auto"
|
||||||
|
icon={<SaveIcon className="w-5 h-5" />}
|
||||||
|
active
|
||||||
|
border
|
||||||
|
>
|
||||||
|
{t('strings.save_changes')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">
|
||||||
|
<Select
|
||||||
|
label="Language"
|
||||||
|
value={i18n.language}
|
||||||
|
onChange={(value): void => {
|
||||||
|
void i18n.changeLanguage(value);
|
||||||
|
}}
|
||||||
|
id="aaa"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
name: 'English',
|
||||||
|
value: 'en',
|
||||||
|
icon: <Us className="w-6" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Português',
|
||||||
|
value: 'pt',
|
||||||
|
icon: <Pt className="w-6" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Japanese',
|
||||||
|
value: 'jp',
|
||||||
|
icon: <Jp className="w-6" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PrimaryTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { Input } from '@components/generic/Input';
|
||||||
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
|
import { MenuIcon, SaveIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
|
export interface RadioProps {
|
||||||
|
navOpen: boolean;
|
||||||
|
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrimaryTemplate
|
||||||
|
title="Radio"
|
||||||
|
tagline="Settings"
|
||||||
|
button={
|
||||||
|
<Button
|
||||||
|
icon={<MenuIcon className="w-5 h-5" />}
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(!navOpen);
|
||||||
|
}}
|
||||||
|
circle
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<Button
|
||||||
|
className="px-10 ml-auto"
|
||||||
|
icon={<SaveIcon className="w-5 h-5" />}
|
||||||
|
active
|
||||||
|
border
|
||||||
|
>
|
||||||
|
{t('strings.save_changes')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">
|
||||||
|
<Input label="test" />
|
||||||
|
</div>
|
||||||
|
</PrimaryTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,10 +12,17 @@
|
|||||||
// Add this line to get types for streaming imports (packageOptions.source="remote"):
|
// Add this line to get types for streaming imports (packageOptions.source="remote"):
|
||||||
// "*": [".snowpack/types/*"]
|
// "*": [".snowpack/types/*"]
|
||||||
// More info: https://www.snowpack.dev/guides/streaming-imports
|
// More info: https://www.snowpack.dev/guides/streaming-imports
|
||||||
|
"@app/*": ["./src/*"],
|
||||||
|
"@pages/*": ["./src/pages/*"],
|
||||||
|
"@components/*": ["./src/components/*"],
|
||||||
|
"@core/*": ["./src/core/*"],
|
||||||
|
"@static/*": ["./src/static/*"]
|
||||||
},
|
},
|
||||||
/* noEmit - Snowpack builds (emits) files, not tsc. */
|
/* noEmit - Snowpack builds (emits) files, not tsc. */
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
/* Additional Options */
|
/* Additional Options */
|
||||||
|
"importHelpers": true,
|
||||||
|
"removeComments": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user