Browse Source

WIP (#1)

* WIP

* WIP
pull/2/head
Sacha Weatherstone 5 years ago
committed by GitHub
parent
commit
eacdf11a6d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      .github/workflows/main.yml
  2. 4
      README.md
  3. 14
      package.json
  4. 5254
      pnpm-lock.yaml
  5. 10
      public/index.html
  6. 8
      src/App.tsx
  7. 4
      src/components/generic/Blur.tsx
  8. 4
      src/components/generic/Button.tsx
  9. 158
      src/components/generic/Chart.tsx
  10. 3
      src/components/generic/Drawer.tsx
  11. 21
      src/components/generic/Input.tsx
  12. 15
      src/components/generic/Select.tsx
  13. 6
      src/components/generic/SidebarItem.tsx
  14. 47
      src/components/generic/Tabs.tsx
  15. 51
      src/components/generic/Toggle.tsx
  16. 2
      src/components/menu/MobileNav.tsx
  17. 2
      src/components/menu/Navigation.tsx
  18. 31
      src/components/nodes/Node.tsx
  19. 6
      src/components/templates/PrimaryTemplate.tsx
  20. 8
      src/core/connection.ts
  21. 2
      src/hooks/breakpoint.ts
  22. 6
      src/pages/Nodes/Index.tsx
  23. 119
      src/pages/Nodes/Node.tsx
  24. 137
      src/pages/settings/Connection.tsx
  25. 25
      src/pages/settings/Device.tsx
  26. 19
      src/pages/settings/Index.tsx
  27. 6
      src/pages/settings/Interface.tsx
  28. 10
      src/pages/settings/Radio.tsx
  29. 5133
      yarn.lock

30
.github/workflows/main.yml

@ -4,9 +4,9 @@ name: meshtastic-web build
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@ -19,31 +19,31 @@ jobs:
# Checks-out repository
- name: Checkout
uses: actions/checkout@v2
# Build project
- uses: pnpm/[email protected]
with:
version: 6.14.3
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- run: yarn install --ignore-optional
- run: yarn lint
- run: yarn build
- run: yarn package
node-version: '16'
cache: 'pnpm'
- run: pnpm lint
- run: pnpm build
- run: pnpm package
- run: tree build/output
# Create a zip file from the output folder
- name: Create output zip file
uses: papeloto/action-zip@v1
with:
files: build/output/
dest: output.zip
# Upload Artifact
- name: Upload a Build Artifact
uses: "marvinpinto/action-automatic-releases@latest"
uses: 'marvinpinto/action-automatic-releases@latest'
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
repo_token: '${{ secrets.GITHUB_TOKEN }}'
automatic_release_tag: 'latest'
prerelease: false
files: |
output.zip

4
README.md

@ -11,11 +11,11 @@ Official [Meshtastic](https://meshtastic.org) web interface, that can be run ind
Build the project:
```bash
yarn build
pnpm build
```
GZip the output:
```bash
yarn package
pnpm package
```

14
package.json

@ -6,27 +6,29 @@
"scripts": {
"start": "NODE_ENV=development snowpack dev",
"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}'",
"lint": "eslint 'src/**/*.{ts,tsx}'"
},
"dependencies": {
"@headlessui/react": "^1.4.0",
"@heroicons/react": "^1.0.1",
"@meshtastic/meshtasticjs": "^0.6.16",
"@meshtastic/meshtasticjs": "^0.6.17",
"@reduxjs/toolkit": "^1.6.0",
"apexcharts": "^3.27.3",
"boring-avatars": "^1.5.8",
"i18next": "^20.3.5",
"i18next-browser-languagedetector": "^6.1.2",
"moment": "^2.29.1",
"react": "^17.0.2",
"react-apexcharts": "^1.3.9",
"react-dom": "^17.0.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-redux": "^7.2.4",
"type-route": "^0.6.0",
"use-breakpoint": "^2.0.1",
"yarn": "^1.22.11"
"use-breakpoint": "^2.0.1"
},
"devDependencies": {
"@snowpack/plugin-dotenv": "^2.0.5",
@ -46,7 +48,7 @@
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-babel-module": "^5.3.1",
"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-hooks": "^4.2.0",
"gzipper": "^5.0.0",

5254
pnpm-lock.yaml

File diff suppressed because it is too large

10
public/index.html

@ -34,15 +34,5 @@
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<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>
</html>

8
src/App.tsx

@ -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-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="hidden md:flex">
<Logo />
</div>
<Navigation className="hidden md:flex" />
<MobileNavToggle />
<div className="flex items-center space-x-2">
<DeviceStatusDropdown />
{/* <LanguageDropdown /> */}
<ThemeToggle />
</div>
</div>
<Navigation className="hidden md:flex" />
</div>
</div>
<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 w-full bg-gray-100 md:shadow-xl md:overflow-hidden dark:bg-secondaryDark md:rounded-b-3xl">
{route.name === 'messages' && <Messages />}
{route.name === 'nodes' && <Nodes />}
{route.name === 'settings' && <Settings />}

4
src/components/generic/Blur.tsx

@ -2,12 +2,10 @@ import React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div'];
interface LocalBlurProps {
interface BlurProps extends DefaultDivProps {
disableOnMd?: boolean;
}
export type BlurProps = LocalBlurProps & DefaultDivProps;
export const Blur = ({
disableOnMd,
className,

4
src/components/generic/Button.tsx

@ -2,15 +2,13 @@ import React from 'react';
type DefaultButtonProps = JSX.IntrinsicElements['button'];
interface LocalButtonProps {
interface ButtonProps extends DefaultButtonProps {
icon?: JSX.Element;
circle?: boolean;
active?: boolean;
border?: boolean;
}
export type ButtonProps = LocalButtonProps & DefaultButtonProps;
export const Button = ({
icon,
circle,

158
src/components/generic/Chart.tsx

@ -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>
);
};

3
src/components/generic/Drawer.tsx

@ -4,12 +4,11 @@ import { Blur } from '@components/generic/Blur';
type DefaultAsideProps = JSX.IntrinsicElements['aside'];
interface LocalDrawerProps {
interface DrawerProps extends DefaultAsideProps {
open: boolean;
permenant?: boolean;
onClose: () => void;
}
export type DrawerProps = LocalDrawerProps & DefaultAsideProps;
export const Drawer = ({
open,

21
src/components/generic/Input.tsx

@ -2,18 +2,24 @@ import React from 'react';
type DefaultInputProps = JSX.IntrinsicElements['input'];
interface LocalInputProps {
interface InputProps extends DefaultInputProps {
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,
{
icon,
label,
valid,
validationMessage,
id,
disabled,
...props
}: InputProps,
ref,
) {
return (
@ -35,9 +41,14 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
id={id}
ref={ref}
disabled={disabled}
{...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'
} ${
disabled
? 'bg-gray-200 dark:bg-primaryDark cursor-not-allowed'
: 'bg-white dark:bg-secondaryDark'
}`}
/>
</div>

15
src/components/generic/Select.tsx

@ -11,7 +11,11 @@ export interface SelectProps {
icon: JSX.Element;
}[];
id: string;
value: string;
active: {
name: string;
value: string;
icon: JSX.Element;
};
onChange: (value: string) => void;
}
@ -19,7 +23,7 @@ export const Select = ({
label,
options,
id,
value,
active,
onChange,
}: SelectProps): JSX.Element => {
return (
@ -28,10 +32,11 @@ export const Select = ({
{label}
</label>
<Listbox value={value} onChange={onChange}>
<Listbox value={active.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>
<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">
<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">
<SelectorIcon
className="w-5 h-5 text-gray-400"

6
src/components/generic/SidebarItem.tsx

@ -2,26 +2,26 @@ import React from 'react';
type DefaultDivProps = JSX.IntrinsicElements['div'];
interface LocalSidebarItemProps {
interface SidebarItemProps extends DefaultDivProps {
title: string;
description: string;
selected: boolean;
icon: JSX.Element;
}
export type SidebarItemProps = LocalSidebarItemProps & DefaultDivProps;
export const SidebarItem = ({
title,
description,
selected,
icon,
...props
}: 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'
}`}
{...props}
>
<div className="text-gray-500 dark:text-gray-400">{icon}</div>
<div className="ml-3 text-left">

47
src/components/generic/Tabs.tsx

@ -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>
);
};

51
src/components/generic/Toggle.tsx

@ -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>
);
};

2
src/components/menu/MobileNav.tsx

@ -19,7 +19,7 @@ export const MobileNav = (): JSX.Element => {
dispatch(closeMobileNav());
}}
>
<div className="flex flex-col w-64">
<div className="flex flex-col">
<div className="m-auto my-6">
<Logo />
</div>

2
src/components/menu/Navigation.tsx

@ -21,7 +21,7 @@ export const Navigation = ({
const route = useRoute();
return (
<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}
>
<div onClick={onClick}>

31
src/components/nodes/Node.tsx

@ -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>
);
};

6
src/components/templates/PrimaryTemplate.tsx

@ -17,7 +17,7 @@ export const PrimaryTemplate = ({
}: PrimaryTemplateProps): JSX.Element => {
return (
<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>}
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center font-medium">
@ -32,11 +32,11 @@ export const PrimaryTemplate = ({
</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}
</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">
<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>}
<div className="flex-1 min-w-0">{footer}</div>
</div>

8
src/core/connection.ts

@ -1,3 +1,9 @@
import { IHTTPConnection } from '@meshtastic/meshtasticjs';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
} from '@meshtastic/meshtasticjs';
export const connection = new IHTTPConnection();
export const bleConnection = new IBLEConnection();
export const serialConnection = new ISerialConnection();

2
src/hooks/breakpoint.ts

@ -3,7 +3,7 @@
import useBreakpointHook from 'use-breakpoint';
const BREAKPOINTS = {
sm: 640,
sm: 0,
// => @media (min-width: 640px) { ... }
md: 768,

6
src/pages/Nodes/Index.tsx

@ -46,6 +46,12 @@ export const Nodes = (): JSX.Element => {
</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) => (
<Tab
onClick={(): void => {

119
src/pages/Nodes/Node.tsx

@ -1,5 +1,8 @@
import React from 'react';
import moment from 'moment';
import { Chart } from '@app/components/generic/Chart';
import { Button } from '@components/generic/Button';
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate';
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>
);
};

137
src/pages/settings/Connection.tsx

@ -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>
);
};

25
src/pages/settings/Device.tsx

@ -3,13 +3,14 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Toggle } from '@app/components/generic/Toggle';
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';
import { Protobuf } from '@meshtastic/meshtasticjs';
export interface DeviceProps {
navOpen: boolean;
@ -19,13 +20,21 @@ export interface DeviceProps {
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
const { t } = useTranslation();
const user = useAppSelector((state) => state.meshtastic.user);
const { register, handleSubmit, formState } = useForm<Protobuf.User>({
defaultValues: user,
const { register, handleSubmit, formState } = useForm<{
isLicensed: boolean;
shortName: string;
longName: string;
}>({
defaultValues: {
isLicensed: user.isLicensed,
shortName: user.shortName,
longName: user.longName,
},
});
const onSubmit = handleSubmit((data) => {
void connection.setOwner(data);
Protobuf.User.mergePartial(user, data);
void connection.setOwner(user);
});
return (
@ -56,6 +65,12 @@ export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => {
<div className="w-full max-w-3xl md:max-w-xl">
<form className="space-y-2" onSubmit={onSubmit}>
<Input label={'Device Name'} {...register('longName')} />
<Input
label={'Short Name'}
maxLength={3}
{...register('shortName')}
/>
<Toggle label="Licenced Operator?" {...register('isLicensed')} />
</form>
</div>
</PrimaryTemplate>

19
src/pages/settings/Index.tsx

@ -8,10 +8,12 @@ import { Tab } from '@headlessui/react';
import {
CollectionIcon,
DeviceMobileIcon,
LinkIcon,
WifiIcon,
XCircleIcon,
} from '@heroicons/react/outline';
import { Connection } from './Connection';
import { Device } from './Device';
import { Interface } from './Interface';
import { Radio } from './Radio';
@ -46,6 +48,20 @@ export const Settings = (): JSX.Element => {
/>
</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
onClick={(): void => {
setNavOpen(false);
@ -84,6 +100,9 @@ export const Settings = (): JSX.Element => {
</Drawer>
<div 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">
<Device navOpen={navOpen} setNavOpen={setNavOpen} />
</Tab.Panel>

6
src/pages/settings/Interface.tsx

@ -47,7 +47,11 @@ export const Interface = ({
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">
<Select
label="Language"
value={i18n.language}
active={{
name: '',
value: '',
icon: <Us />,
}}
onChange={(value): void => {
void i18n.changeLanguage(value);
}}

10
src/pages/settings/Radio.tsx

@ -61,6 +61,16 @@ export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => {
label={t('strings.wifi_psk')}
{...register('wifiPassword')}
/>
<Input
label={'Charge current'}
disabled
{...register('chargeCurrent')}
/>
<Input
label={'Last GPS Attempt'}
disabled
{...register('gpsAttemptTime')}
/>
</form>
</div>
</PrimaryTemplate>

5133
yarn.lock

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