* WIP

* WIP
This commit is contained in:
Sacha Weatherstone
2021-08-25 13:34:24 +10:00
committed by GitHub
parent a797cf7be1
commit eacdf11a6d
29 changed files with 5898 additions and 5237 deletions

View File

@@ -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/action-setup@v2.0.1
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

View File

@@ -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
```

View File

@@ -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 generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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 />}

View File

@@ -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,

View File

@@ -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,

View File

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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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"

View File

@@ -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">

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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 => {

View File

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

View File

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

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff