+15
-15
@@ -4,9 +4,9 @@ name: meshtastic-web build
|
|||||||
on:
|
on:
|
||||||
# Triggers the workflow on push or pull request events but only for the master branch
|
# Triggers the workflow on push or pull request events but only for the master branch
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -19,31 +19,31 @@ jobs:
|
|||||||
# Checks-out repository
|
# Checks-out repository
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- uses: pnpm/action-setup@v2.0.1
|
||||||
# Build project
|
with:
|
||||||
|
version: 6.14.3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '16'
|
||||||
cache: 'yarn'
|
cache: 'pnpm'
|
||||||
- run: yarn install --ignore-optional
|
- run: pnpm lint
|
||||||
- run: yarn lint
|
- run: pnpm build
|
||||||
- run: yarn build
|
- run: pnpm package
|
||||||
- run: yarn package
|
|
||||||
- run: tree build/output
|
- run: tree build/output
|
||||||
|
|
||||||
# Create a zip file from the output folder
|
# Create a zip file from the output folder
|
||||||
- name: Create output zip file
|
- name: Create output zip file
|
||||||
uses: papeloto/action-zip@v1
|
uses: papeloto/action-zip@v1
|
||||||
with:
|
with:
|
||||||
files: build/output/
|
files: build/output/
|
||||||
dest: output.zip
|
dest: output.zip
|
||||||
|
|
||||||
# Upload Artifact
|
# Upload Artifact
|
||||||
- name: Upload a Build Artifact
|
- name: Upload a Build Artifact
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
automatic_release_tag: "latest"
|
automatic_release_tag: 'latest'
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: |
|
files: |
|
||||||
output.zip
|
output.zip
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ Official [Meshtastic](https://meshtastic.org) web interface, that can be run ind
|
|||||||
Build the project:
|
Build the project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
GZip the output:
|
GZip the output:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn package
|
pnpm package
|
||||||
```
|
```
|
||||||
|
|||||||
+8
-6
@@ -6,27 +6,29 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "NODE_ENV=development snowpack dev",
|
"start": "NODE_ENV=development snowpack dev",
|
||||||
"build": "snowpack build",
|
"build": "snowpack build",
|
||||||
"package": "yarn gzipper c -i html,js,css build build/output",
|
"package": "pnpm gzipper c -i html,js,css build build/output",
|
||||||
"format": "prettier --write 'src/**/*.{ts,tsx}'",
|
"format": "prettier --write 'src/**/*.{ts,tsx}'",
|
||||||
"lint": "eslint 'src/**/*.{ts,tsx}'"
|
"lint": "eslint 'src/**/*.{ts,tsx}'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.4.0",
|
"@headlessui/react": "^1.4.0",
|
||||||
"@heroicons/react": "^1.0.1",
|
"@heroicons/react": "^1.0.1",
|
||||||
"@meshtastic/meshtasticjs": "^0.6.16",
|
"@meshtastic/meshtasticjs": "^0.6.17",
|
||||||
"@reduxjs/toolkit": "^1.6.0",
|
"@reduxjs/toolkit": "^1.6.0",
|
||||||
|
"apexcharts": "^3.27.3",
|
||||||
"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",
|
||||||
|
"moment": "^2.29.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-apexcharts": "^1.3.9",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-flags-select": "^2.1.2",
|
"react-flags-select": "^2.1.2",
|
||||||
"react-hook-form": "^7.9.0",
|
"react-hook-form": "^7.13.0-next.5",
|
||||||
"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",
|
"type-route": "^0.6.0",
|
||||||
"use-breakpoint": "^2.0.1",
|
"use-breakpoint": "^2.0.1"
|
||||||
"yarn": "^1.22.11"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snowpack/plugin-dotenv": "^2.0.5",
|
"@snowpack/plugin-dotenv": "^2.0.5",
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-import-resolver-babel-module": "^5.3.1",
|
"eslint-import-resolver-babel-module": "^5.3.1",
|
||||||
"eslint-import-resolver-typescript": "^2.4.0",
|
"eslint-import-resolver-typescript": "^2.4.0",
|
||||||
"eslint-plugin-import": "^2.24.0",
|
"eslint-plugin-import": "^2.24.1",
|
||||||
"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",
|
||||||
|
|||||||
Generated
+5254
File diff suppressed because it is too large
Load Diff
@@ -34,15 +34,5 @@
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<script type="module" src="/static/index.js"></script>
|
<script type="module" src="/static/index.js"></script>
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-5
@@ -137,26 +137,24 @@ const App = (): JSX.Element => {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col h-full bg-gray-200 dark:bg-primaryDark">
|
<div className="flex flex-col h-full bg-gray-200 dark:bg-primaryDark">
|
||||||
<div className="flex flex-shrink-0 overflow-hidden bg-primary dark:bg-primary">
|
<div className="flex flex-shrink-0 overflow-hidden bg-primary dark:bg-primary">
|
||||||
<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="w-full overflow-hidden bg-white border-b md:mt-8 md:mx-8 md:pt-4 md:pb-3 md:rounded-t-3xl dark:border-gray-600 md:shadow-md dark:bg-primaryDark">
|
||||||
<div className="flex items-center justify-between h-16 px-4 md:px-6">
|
<div className="flex items-center justify-between h-16 px-4 md:px-6">
|
||||||
<div className="hidden md:flex">
|
<div className="hidden md:flex">
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
|
<Navigation className="hidden md:flex" />
|
||||||
<MobileNavToggle />
|
<MobileNavToggle />
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<DeviceStatusDropdown />
|
<DeviceStatusDropdown />
|
||||||
{/* <LanguageDropdown /> */}
|
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Navigation className="hidden md:flex" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
|
|
||||||
<div className="flex flex-grow w-full min-h-0 md:px-8 md:mb-8">
|
<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 w-full bg-gray-100 md:shadow-xl md:overflow-hidden dark:bg-secondaryDark md:rounded-b-3xl">
|
||||||
{route.name === 'messages' && <Messages />}
|
{route.name === 'messages' && <Messages />}
|
||||||
{route.name === 'nodes' && <Nodes />}
|
{route.name === 'nodes' && <Nodes />}
|
||||||
{route.name === 'settings' && <Settings />}
|
{route.name === 'settings' && <Settings />}
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import React from 'react';
|
|||||||
|
|
||||||
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
|
|
||||||
interface LocalBlurProps {
|
interface BlurProps extends DefaultDivProps {
|
||||||
disableOnMd?: boolean;
|
disableOnMd?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BlurProps = LocalBlurProps & DefaultDivProps;
|
|
||||||
|
|
||||||
export const Blur = ({
|
export const Blur = ({
|
||||||
disableOnMd,
|
disableOnMd,
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import React from 'react';
|
|||||||
|
|
||||||
type DefaultButtonProps = JSX.IntrinsicElements['button'];
|
type DefaultButtonProps = JSX.IntrinsicElements['button'];
|
||||||
|
|
||||||
interface LocalButtonProps {
|
interface ButtonProps extends DefaultButtonProps {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
circle?: boolean;
|
circle?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
border?: boolean;
|
border?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ButtonProps = LocalButtonProps & DefaultButtonProps;
|
|
||||||
|
|
||||||
export const Button = ({
|
export const Button = ({
|
||||||
icon,
|
icon,
|
||||||
circle,
|
circle,
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ApexChart from 'react-apexcharts';
|
||||||
|
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
|
|
||||||
|
interface ISeries {
|
||||||
|
name: string;
|
||||||
|
data: {
|
||||||
|
x: string | Date;
|
||||||
|
y: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartProps extends DefaultDivProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
hasMultipleSeries: boolean;
|
||||||
|
series: ISeries[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Chart = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
hasMultipleSeries,
|
||||||
|
series,
|
||||||
|
...props
|
||||||
|
}: ChartProps): JSX.Element => {
|
||||||
|
const [activeSeries, setActiveSeries] = React.useState<ISeries>(series[0]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-col flex-auto text-white shadow-md dark bg-primaryDark rounded-3xl"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mx-10 mt-10">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mr-4 text-2xl font-semibold leading-7 tracking-tight md:text-3xl">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="font-medium text-gray-400">{description}</div>
|
||||||
|
</div>
|
||||||
|
{hasMultipleSeries && (
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{series.map((data, index) => (
|
||||||
|
<Button
|
||||||
|
active={data.name === activeSeries.name}
|
||||||
|
key={index}
|
||||||
|
className="font-medium"
|
||||||
|
onClick={(): void => {
|
||||||
|
setActiveSeries(series[index]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-80">
|
||||||
|
<ApexChart
|
||||||
|
height="96%"
|
||||||
|
type="area"
|
||||||
|
options={{
|
||||||
|
chart: {
|
||||||
|
animations: {
|
||||||
|
speed: 400,
|
||||||
|
animateGradually: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolbar: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: ['#818CF8'],
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
colors: ['#312E81'],
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
padding: {
|
||||||
|
top: 10,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
xaxis: {
|
||||||
|
lines: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
lines: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
stroke: {
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
followCursor: true,
|
||||||
|
theme: 'dark',
|
||||||
|
x: {
|
||||||
|
format: 'MMM dd, yyyy',
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
formatter: (value: number): string => `${value}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
axisBorder: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTicks: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
crosshairs: {
|
||||||
|
stroke: {
|
||||||
|
color: '#475569',
|
||||||
|
dashArray: 0,
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
style: {
|
||||||
|
colors: '#CBD5E1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
type: 'datetime',
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
axisTicks: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisBorder: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
series={[activeSeries]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,12 +4,11 @@ import { Blur } from '@components/generic/Blur';
|
|||||||
|
|
||||||
type DefaultAsideProps = JSX.IntrinsicElements['aside'];
|
type DefaultAsideProps = JSX.IntrinsicElements['aside'];
|
||||||
|
|
||||||
interface LocalDrawerProps {
|
interface DrawerProps extends DefaultAsideProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
permenant?: boolean;
|
permenant?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
export type DrawerProps = LocalDrawerProps & DefaultAsideProps;
|
|
||||||
|
|
||||||
export const Drawer = ({
|
export const Drawer = ({
|
||||||
open,
|
open,
|
||||||
|
|||||||
@@ -2,18 +2,24 @@ import React from 'react';
|
|||||||
|
|
||||||
type DefaultInputProps = JSX.IntrinsicElements['input'];
|
type DefaultInputProps = JSX.IntrinsicElements['input'];
|
||||||
|
|
||||||
interface LocalInputProps {
|
interface InputProps extends DefaultInputProps {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
label?: string;
|
label?: string;
|
||||||
valid?: boolean;
|
valid?: boolean;
|
||||||
validationMessage?: string;
|
validationMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InputProps = LocalInputProps & DefaultInputProps;
|
|
||||||
|
|
||||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
function Input(
|
function Input(
|
||||||
{ icon, label, valid, validationMessage, id, ...props }: InputProps,
|
{
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
valid,
|
||||||
|
validationMessage,
|
||||||
|
id,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
}: InputProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -35,9 +41,14 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
disabled={disabled}
|
||||||
{...props}
|
{...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 ${
|
className={`block w-full h-11 rounded-md border shadow-sm focus:outline-none focus:border-primary dark:focus:border-primary dark:border-gray-600 dark:text-white ${
|
||||||
icon ? 'pl-9' : 'pl-2'
|
icon ? 'pl-9' : 'pl-2'
|
||||||
|
} ${
|
||||||
|
disabled
|
||||||
|
? 'bg-gray-200 dark:bg-primaryDark cursor-not-allowed'
|
||||||
|
: 'bg-white dark:bg-secondaryDark'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ export interface SelectProps {
|
|||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
}[];
|
}[];
|
||||||
id: string;
|
id: string;
|
||||||
value: string;
|
active: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
};
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +23,7 @@ export const Select = ({
|
|||||||
label,
|
label,
|
||||||
options,
|
options,
|
||||||
id,
|
id,
|
||||||
value,
|
active,
|
||||||
onChange,
|
onChange,
|
||||||
}: SelectProps): JSX.Element => {
|
}: SelectProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
@@ -28,10 +32,11 @@ export const Select = ({
|
|||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Listbox value={value} onChange={onChange}>
|
<Listbox value={active.value} onChange={onChange}>
|
||||||
<div className="relative mt-1">
|
<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">
|
<Listbox.Button className="flex 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>
|
<div className="">{active.icon}</div>
|
||||||
|
<span className="block truncate">{active.name}</span>
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon
|
<SelectorIcon
|
||||||
className="w-5 h-5 text-gray-400"
|
className="w-5 h-5 text-gray-400"
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ import React from 'react';
|
|||||||
|
|
||||||
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
|
|
||||||
interface LocalSidebarItemProps {
|
interface SidebarItemProps extends DefaultDivProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SidebarItemProps = LocalSidebarItemProps & DefaultDivProps;
|
|
||||||
|
|
||||||
export const SidebarItem = ({
|
export const SidebarItem = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
selected,
|
selected,
|
||||||
icon,
|
icon,
|
||||||
|
...props
|
||||||
}: SidebarItemProps): JSX.Element => {
|
}: SidebarItemProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${
|
className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${
|
||||||
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark'
|
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark'
|
||||||
}`}
|
}`}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="text-gray-500 dark:text-gray-400">{icon}</div>
|
<div className="text-gray-500 dark:text-gray-400">{icon}</div>
|
||||||
<div className="ml-3 text-left">
|
<div className="ml-3 text-left">
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Tab } from '@headlessui/react';
|
||||||
|
|
||||||
|
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||||
|
|
||||||
|
interface TabProps extends DefaultDivProps {
|
||||||
|
tabs: {
|
||||||
|
name: string;
|
||||||
|
body: JSX.Element;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tabs = ({ tabs, className, ...props }: TabProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Tab.Group as="div" className={className}>
|
||||||
|
<Tab.List className="flex border-l border-r border-t shadow-md rounded-t-3xl dark:border-gray-600">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.name}
|
||||||
|
className={({ selected }): string =>
|
||||||
|
`w-full text-lg font-medium p-2 border-b-2 ${
|
||||||
|
selected
|
||||||
|
? 'dark:border-gray-200 border-gray-600'
|
||||||
|
: 'border-transparent dark:border-transparent'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels className="h-full">
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<Tab.Panel
|
||||||
|
key={index}
|
||||||
|
className={
|
||||||
|
'border dark:border-gray-600 rounded-b-3xl p-4 h-full shadow-md'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tab.body}
|
||||||
|
</Tab.Panel>
|
||||||
|
))}
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Switch } from '@headlessui/react';
|
||||||
|
|
||||||
|
type DefaultButtonProps = JSX.IntrinsicElements['button'];
|
||||||
|
|
||||||
|
interface ToggleProps extends DefaultButtonProps {
|
||||||
|
label?: string;
|
||||||
|
valid?: boolean;
|
||||||
|
validationMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toggle = ({
|
||||||
|
label,
|
||||||
|
valid,
|
||||||
|
validationMessage,
|
||||||
|
id,
|
||||||
|
...props
|
||||||
|
}: ToggleProps): JSX.Element => {
|
||||||
|
const [enabled, setEnabled] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<label htmlFor={id} className="block text-sm font-medium dark:text-white">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="float-right">
|
||||||
|
<Switch
|
||||||
|
id={id}
|
||||||
|
{...props}
|
||||||
|
checked={enabled}
|
||||||
|
onChange={setEnabled}
|
||||||
|
className={`${
|
||||||
|
enabled ? 'bg-primary' : 'bg-gray-200 dark:bg-primaryDark'
|
||||||
|
}
|
||||||
|
relative inline-flex flex-shrink-0 h-[38px] w-[74px] border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Use setting</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`${enabled ? 'translate-x-9' : 'translate-x-0'}
|
||||||
|
pointer-events-none inline-block h-[34px] w-[34px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
{!valid && (
|
||||||
|
<div className="text-sm text-gray-600">{validationMessage}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,7 @@ export const MobileNav = (): JSX.Element => {
|
|||||||
dispatch(closeMobileNav());
|
dispatch(closeMobileNav());
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col w-64">
|
<div className="flex flex-col">
|
||||||
<div className="m-auto my-6">
|
<div className="m-auto my-6">
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const Navigation = ({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`h-16 px-4 md:space-x-2 space-y-2 md:space-y-0 ${className}`}
|
className={`px-4 md:space-x-2 space-y-2 md:space-y-0 ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div onClick={onClick}>
|
<div onClick={onClick}>
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Avatar from 'boring-avatars';
|
|
||||||
|
|
||||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
|
||||||
|
|
||||||
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
|
||||||
|
|
||||||
export interface NodeProps {
|
|
||||||
node: Protobuf.NodeInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Node = ({
|
|
||||||
node,
|
|
||||||
...props
|
|
||||||
}: NodeProps & DefaultDivProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className="flex space-x-4 items-center w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
size={30}
|
|
||||||
name={node.user?.longName ?? 'UNK'}
|
|
||||||
variant="beam"
|
|
||||||
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
|
|
||||||
/>
|
|
||||||
<div>{node.user?.longName}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -17,7 +17,7 @@ export const PrimaryTemplate = ({
|
|||||||
}: 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 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">
|
<div className="flex p-4 bg-white border-b md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
|
||||||
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
|
{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">
|
||||||
@@ -32,11 +32,11 @@ export const PrimaryTemplate = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-auto flex-grow p-6 bg-white md:p-10 dark:bg-secondaryDark">
|
<div className="flex-auto flex-grow p-6 bg-white md:p-10 dark:bg-secondaryDark overflow-y-auto">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{footer && (
|
{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">
|
<div className="flex p-4 bg-white border-t md:flex-row flex-0 md:items-center md:justify-between md:px-10 dark:border-gray-600 dark:bg-secondaryDark">
|
||||||
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
|
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
|
||||||
<div className="flex-1 min-w-0">{footer}</div>
|
<div className="flex-1 min-w-0">{footer}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
import { IHTTPConnection } from '@meshtastic/meshtasticjs';
|
import {
|
||||||
|
IBLEConnection,
|
||||||
|
IHTTPConnection,
|
||||||
|
ISerialConnection,
|
||||||
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
export const connection = new IHTTPConnection();
|
export const connection = new IHTTPConnection();
|
||||||
|
export const bleConnection = new IBLEConnection();
|
||||||
|
export const serialConnection = new ISerialConnection();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import useBreakpointHook from 'use-breakpoint';
|
import useBreakpointHook from 'use-breakpoint';
|
||||||
|
|
||||||
const BREAKPOINTS = {
|
const BREAKPOINTS = {
|
||||||
sm: 640,
|
sm: 0,
|
||||||
// => @media (min-width: 640px) { ... }
|
// => @media (min-width: 640px) { ... }
|
||||||
|
|
||||||
md: 768,
|
md: 768,
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export const Nodes = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!nodes.length && (
|
||||||
|
<span className="p-4 text-sm text-gray-400 dark:text-gray-600">
|
||||||
|
No nodes discovered yet...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{nodes.map((node) => (
|
{nodes.map((node) => (
|
||||||
<Tab
|
<Tab
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
|
|||||||
+118
-1
@@ -1,5 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { Chart } from '@app/components/generic/Chart';
|
||||||
import { Button } from '@components/generic/Button';
|
import { Button } from '@components/generic/Button';
|
||||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
import { MenuIcon } from '@heroicons/react/outline';
|
import { MenuIcon } from '@heroicons/react/outline';
|
||||||
@@ -26,7 +29,121 @@ export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">Content</div>
|
<div className="w-full space-y-2">
|
||||||
|
<Chart
|
||||||
|
title="Visitors Overview"
|
||||||
|
description="Number of unique visitors"
|
||||||
|
hasMultipleSeries={true}
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
name: 'Series 1',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(1).toDate(),
|
||||||
|
y: 4884,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(4).toDate(),
|
||||||
|
y: 5351,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(7).toDate(),
|
||||||
|
y: 5293,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(10).toDate(),
|
||||||
|
y: 4908,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(13).toDate(),
|
||||||
|
y: 5027,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(16).toDate(),
|
||||||
|
y: 4837,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(19).toDate(),
|
||||||
|
y: 4484,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(22).toDate(),
|
||||||
|
y: 4071,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(25).toDate(),
|
||||||
|
y: 4124,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(28).toDate(),
|
||||||
|
y: 4563,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(11, 'months').day(1).toDate(),
|
||||||
|
y: 3820,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(11, 'months').day(4).toDate(),
|
||||||
|
y: 3968,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Series 2',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(1).toDate(),
|
||||||
|
y: 4332,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(4).toDate(),
|
||||||
|
y: 6642,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(7).toDate(),
|
||||||
|
y: 5531,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(10).toDate(),
|
||||||
|
y: 2231,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(13).toDate(),
|
||||||
|
y: 5532,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(16).toDate(),
|
||||||
|
y: 3352,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(19).toDate(),
|
||||||
|
y: 6633,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(22).toDate(),
|
||||||
|
y: 1442,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(25).toDate(),
|
||||||
|
y: 4332,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(12, 'months').day(28).toDate(),
|
||||||
|
y: 6332,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(11, 'months').day(1).toDate(),
|
||||||
|
y: 5334,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: moment().subtract(11, 'months').day(4).toDate(),
|
||||||
|
y: 5253,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</PrimaryTemplate>
|
</PrimaryTemplate>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Input } from '@app/components/generic/Input';
|
||||||
|
import { Tabs } from '@app/components/generic/Tabs';
|
||||||
|
import { Toggle } from '@app/components/generic/Toggle';
|
||||||
|
import {
|
||||||
|
bleConnection,
|
||||||
|
connection,
|
||||||
|
serialConnection,
|
||||||
|
} from '@app/core/connection';
|
||||||
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
|
import { Button } from '@components/generic/Button';
|
||||||
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
|
import { LinkIcon, MenuIcon, SaveIcon } from '@heroicons/react/outline';
|
||||||
|
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
export interface ConnectionProps {
|
||||||
|
navOpen: boolean;
|
||||||
|
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Connection = ({
|
||||||
|
navOpen,
|
||||||
|
setNavOpen,
|
||||||
|
}: ConnectionProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const user = useAppSelector((state) => state.meshtastic.user);
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } = useForm<Protobuf.User>({
|
||||||
|
defaultValues: user,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
void connection.setOwner(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrimaryTemplate
|
||||||
|
title="Connection"
|
||||||
|
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 md:max-w-xl">
|
||||||
|
<div className="mb-2 flex w-full border dark:border-gray-600 rounded-3xl p-2">
|
||||||
|
Current connection method:
|
||||||
|
<div className="ml-2 rounded-full bg-gray-400 dark:bg-primaryDark text-sm px-1 my-auto">
|
||||||
|
BLE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form className="space-y-2" onSubmit={onSubmit}>
|
||||||
|
<Tabs
|
||||||
|
className="h-60"
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
name: 'HTTP',
|
||||||
|
body: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input label={'Device URL'} />
|
||||||
|
<Toggle label="Use TLS?" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Bluetooth',
|
||||||
|
body: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
Devices:
|
||||||
|
<Button
|
||||||
|
onClick={async (): Promise<void> => {
|
||||||
|
console.log(await bleConnection.getDevices());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Get Devices
|
||||||
|
</Button>
|
||||||
|
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
|
||||||
|
Device Name
|
||||||
|
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
|
||||||
|
Device Name
|
||||||
|
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Serial',
|
||||||
|
body: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
Devices:
|
||||||
|
<Button
|
||||||
|
onClick={async (): Promise<void> => {
|
||||||
|
console.log(await serialConnection.getPorts());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Get Devices
|
||||||
|
</Button>
|
||||||
|
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
|
||||||
|
Device Name
|
||||||
|
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between rounded-3xl border dark:border-600 p-2">
|
||||||
|
Device Name
|
||||||
|
<LinkIcon className="my-auto mr-2 w-5 h-5 text-gray-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</PrimaryTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,13 +3,14 @@ import React from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Toggle } from '@app/components/generic/Toggle';
|
||||||
import { connection } from '@app/core/connection';
|
import { connection } from '@app/core/connection';
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
import { Button } from '@components/generic/Button';
|
import { Button } from '@components/generic/Button';
|
||||||
import { Input } from '@components/generic/Input';
|
import { Input } from '@components/generic/Input';
|
||||||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
|
||||||
import { MenuIcon, SaveIcon } from '@heroicons/react/outline';
|
import { MenuIcon, SaveIcon } from '@heroicons/react/outline';
|
||||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
export interface DeviceProps {
|
export interface DeviceProps {
|
||||||
navOpen: boolean;
|
navOpen: boolean;
|
||||||
@@ -19,13 +20,21 @@ export interface DeviceProps {
|
|||||||
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const user = useAppSelector((state) => state.meshtastic.user);
|
const user = useAppSelector((state) => state.meshtastic.user);
|
||||||
|
const { register, handleSubmit, formState } = useForm<{
|
||||||
const { register, handleSubmit, formState } = useForm<Protobuf.User>({
|
isLicensed: boolean;
|
||||||
defaultValues: user,
|
shortName: string;
|
||||||
|
longName: string;
|
||||||
|
}>({
|
||||||
|
defaultValues: {
|
||||||
|
isLicensed: user.isLicensed,
|
||||||
|
shortName: user.shortName,
|
||||||
|
longName: user.longName,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
void connection.setOwner(data);
|
Protobuf.User.mergePartial(user, data);
|
||||||
|
void connection.setOwner(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,6 +65,12 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
|
|||||||
<div className="w-full max-w-3xl md:max-w-xl">
|
<div className="w-full max-w-3xl md:max-w-xl">
|
||||||
<form className="space-y-2" onSubmit={onSubmit}>
|
<form className="space-y-2" onSubmit={onSubmit}>
|
||||||
<Input label={'Device Name'} {...register('longName')} />
|
<Input label={'Device Name'} {...register('longName')} />
|
||||||
|
<Input
|
||||||
|
label={'Short Name'}
|
||||||
|
maxLength={3}
|
||||||
|
{...register('shortName')}
|
||||||
|
/>
|
||||||
|
<Toggle label="Licenced Operator?" {...register('isLicensed')} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</PrimaryTemplate>
|
</PrimaryTemplate>
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import { Tab } from '@headlessui/react';
|
|||||||
import {
|
import {
|
||||||
CollectionIcon,
|
CollectionIcon,
|
||||||
DeviceMobileIcon,
|
DeviceMobileIcon,
|
||||||
|
LinkIcon,
|
||||||
WifiIcon,
|
WifiIcon,
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
|
|
||||||
|
import { Connection } from './Connection';
|
||||||
import { Device } from './Device';
|
import { Device } from './Device';
|
||||||
import { Interface } from './Interface';
|
import { Interface } from './Interface';
|
||||||
import { Radio } from './Radio';
|
import { Radio } from './Radio';
|
||||||
@@ -46,6 +48,20 @@ export const Settings = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Tab
|
||||||
|
onClick={(): void => {
|
||||||
|
setNavOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ selected }): JSX.Element => (
|
||||||
|
<SidebarItem
|
||||||
|
title="Connection"
|
||||||
|
description="Method and peramaters for connecting to the device"
|
||||||
|
selected={selected}
|
||||||
|
icon={<LinkIcon className="flex-shrink-0 w-6 h-6" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setNavOpen(false);
|
setNavOpen(false);
|
||||||
@@ -84,6 +100,9 @@ export const Settings = (): JSX.Element => {
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<Tab.Panels className="flex w-full">
|
<Tab.Panels className="flex w-full">
|
||||||
|
<Tab.Panel className="flex w-full">
|
||||||
|
<Connection navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
|
</Tab.Panel>
|
||||||
<Tab.Panel className="flex w-full">
|
<Tab.Panel className="flex w-full">
|
||||||
<Device navOpen={navOpen} setNavOpen={setNavOpen} />
|
<Device navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ export const Interface = ({
|
|||||||
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">
|
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">
|
||||||
<Select
|
<Select
|
||||||
label="Language"
|
label="Language"
|
||||||
value={i18n.language}
|
active={{
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
icon: <Us />,
|
||||||
|
}}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
void i18n.changeLanguage(value);
|
void i18n.changeLanguage(value);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
|
|||||||
label={t('strings.wifi_psk')}
|
label={t('strings.wifi_psk')}
|
||||||
{...register('wifiPassword')}
|
{...register('wifiPassword')}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
label={'Charge current'}
|
||||||
|
disabled
|
||||||
|
{...register('chargeCurrent')}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={'Last GPS Attempt'}
|
||||||
|
disabled
|
||||||
|
{...register('gpsAttemptTime')}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</PrimaryTemplate>
|
</PrimaryTemplate>
|
||||||
|
|||||||
Reference in New Issue
Block a user