This commit is contained in:
Sacha Weatherstone
2021-08-11 23:09:45 +10:00
parent aff46494e9
commit 2940791b9e
36 changed files with 1096 additions and 623 deletions
@@ -1,85 +0,0 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { SaveIcon } from '@heroicons/react/outline';
import { Protobuf } from '@meshtastic/meshtasticjs';
import { connection } from '../../../connection';
import { useAppSelector } from '../../../hooks/redux';
export const Settings = (): JSX.Element => {
const { t } = useTranslation();
const preferences = useAppSelector((state) => state.meshtastic.preferences);
const { register, handleSubmit } =
useForm<Protobuf.RadioConfig_UserPreferences>({
defaultValues: preferences,
});
const onSubmit = handleSubmit((data) => connection.setPreferences(data));
return (
<form onSubmit={onSubmit}>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{t('strings.device_region')}</div>
<div className="flex shadow-md rounded-3xl ml-2">
<select
{...register('region', {
valueAsNumber: true,
})}
>
<option value={Protobuf.RegionCode.ANZ}>
{Protobuf.RegionCode[Protobuf.RegionCode.ANZ]}
</option>
<option value={Protobuf.RegionCode.CN}>
{Protobuf.RegionCode[Protobuf.RegionCode.CN]}
</option>
<option value={Protobuf.RegionCode.EU433}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU433]}
</option>
<option value={Protobuf.RegionCode.EU865}>
{Protobuf.RegionCode[Protobuf.RegionCode.EU865]}
</option>
<option value={Protobuf.RegionCode.JP}>
{Protobuf.RegionCode[Protobuf.RegionCode.JP]}
</option>
<option value={Protobuf.RegionCode.KR}>
{Protobuf.RegionCode[Protobuf.RegionCode.KR]}
</option>
<option value={Protobuf.RegionCode.TW}>
{Protobuf.RegionCode[Protobuf.RegionCode.TW]}
</option>
<option value={Protobuf.RegionCode.US}>
{Protobuf.RegionCode[Protobuf.RegionCode.US]}
</option>
<option value={Protobuf.RegionCode.Unset}>
{Protobuf.RegionCode[Protobuf.RegionCode.Unset]}
</option>
</select>
</div>
</div>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{t('strings.wifi_ssid')}</div>
<div className="flex shadow-md rounded-3xl ml-2">
<input {...register('wifiSsid', {})} type="text" />
</div>
</div>
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
<div className="my-auto">{t('strings.wifi_psk')}</div>
<div className="flex shadow-md rounded-3xl ml-2">
<input {...register('wifiPassword', {})} type="password" />
</div>
</div>
<div className="flex bg-gray-100 group p-1 cursor-pointer hover:bg-gray-200 border-b">
<button
type="submit"
className="flex m-auto font-medium group-hover:text-gray-700"
>
<SaveIcon className="m-auto mr-2 group-hover:text-gray-700 w-5 h-5" />
{t('strings.save_changes')}
</button>
</div>
</form>
);
};
+204
View File
@@ -0,0 +1,204 @@
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import Button from '@material-ui/core/Button';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import Switch from '@material-ui/core/Switch';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
let renderCount = 0;
const options = [
{
value: 'chocolate',
label: 'Chocolate',
},
{
value: 'strawberry',
label: 'Strawberry',
},
{
value: 'vanilla',
label: 'Vanilla',
},
];
const defaultValues = {
Native: '',
TextField: '',
Select: '',
ReactSelect: '',
Checkbox: false,
switch: false,
RadioGroup: '',
};
export const TestForm = () => {
const { handleSubmit, register, reset, control, watch } = useForm({
defaultValues,
mode: 'onChange',
});
renderCount++;
const data = watch();
return (
<div className="flex w-full max-w-screen-md justify-start items-start">
<form
className="w-1/2"
onSubmit={handleSubmit((data) => console.info(data))}
>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
Native Input:
</Typography>
<input
className="border-1 outline-none rounded-8 p-8"
{...register('Native')}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI Checkbox
</Typography>
<Controller
name="Checkbox"
control={control}
defaultValue={false}
render={({ field: { onChange, value } }) => (
<Checkbox
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
/>
)}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
Radio Group
</Typography>
<Controller
render={({ field }) => (
<RadioGroup {...field} aria-label="gender" name="gender1">
<FormControlLabel
value="female"
control={<Radio />}
label="Female"
/>
<FormControlLabel
value="male"
control={<Radio />}
label="Male"
/>
</RadioGroup>
)}
name="RadioGroup"
control={control}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI TextField
</Typography>
<Controller
render={({ field }) => <TextField {...field} variant="outlined" />}
name="TextField"
control={control}
/>
</div>
{/* <div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI Select
</Typography>
<Controller
render={({ field }) => (
<Select {...field} variant="outlined">
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
)}
name="Select"
control={control}
/>
</div> */}
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
MUI Switch
</Typography>
<Controller
name="switch"
control={control}
defaultValue={false}
render={({ field: { onChange, value } }) => (
<Switch
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
/>
)}
/>
</div>
<div className="mt-48 mb-16">
<Typography className="mb-24 font-medium text-14">
React Select
</Typography>
{/* <Controller
render={({ field }) => <ReactSelect {...field} />}
options={options}
name="ReactSelect"
isClearable
control={control}
onChange={([selected]) => {
return { value: selected };
}}
/> */}
</div>
<div className="flex my-48 items-center">
<Button
className="mx-8"
variant="contained"
color="secondary"
type="submit"
>
Submit
</Button>
<Button
className="mx-8"
type="button"
onClick={() => {
reset(defaultValues);
}}
>
Reset Form
</Button>
</div>
</form>
<div className="w-1/2 my-48 p-24">
<pre className="language-js p-24 w-400">
{JSON.stringify(data, null, 2)}
</pre>
<Typography
className="mt-16 font-medium text-12 italic"
color="textSecondary"
>
Render Count: {renderCount}
</Typography>
</div>
</div>
);
};
+8 -8
View File
@@ -8,9 +8,9 @@ import {
PaperClipIcon,
} from '@heroicons/react/outline';
import { connection } from '../../connection';
import { connection } from '../../core/connection';
import { useAppSelector } from '../../hooks/redux';
import { Button } from '../generic/Button';
import { IconButton } from '../generic/IconButton';
export const MessageBar = (): JSX.Element => {
const ready = useAppSelector((state) => state.meshtastic.ready);
@@ -25,13 +25,13 @@ export const MessageBar = (): JSX.Element => {
return (
<div className="flex p-4 bg-gray-50 dark:bg-transparent space-x-2 text-gray-500 dark:text-gray-400">
<div className="flex">
<Button>
<IconButton>
<EmojiHappyIcon className="w-6 h-6" />
</Button>
</IconButton>
<Button>
<IconButton>
<PaperClipIcon className="w-6 h-6" />
</Button>
</IconButton>
</div>
<form
className="flex w-full space-x-2"
@@ -51,9 +51,9 @@ export const MessageBar = (): JSX.Element => {
}}
className="focus:outline-none h-10 w-full resize-none rounded-full border border-gray-300 dark:bg-gray-900 px-4"
/>
<Button type="submit">
<IconButton type="submit">
<PaperAirplaneIcon className="w-6 h-6 rotate-90" />
</Button>
</IconButton>
</form>
</div>
);
+27 -27
View File
@@ -1,38 +1,38 @@
import React from 'react';
type DefaultSelectProps = JSX.IntrinsicElements['select'];
import type { FieldValues, UseControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import ReactSelect from 'react-select';
export interface SelectProps {
interface SelectProps<T> extends UseControllerProps<T> {
label: string;
options: {
value: string;
label: string;
}[];
label: string;
}
export const Select = React.forwardRef<
HTMLSelectElement,
SelectProps & DefaultSelectProps
>(function Select(
{ options, label, id, ...props }: SelectProps & DefaultSelectProps,
ref,
) {
export const Select = <T extends FieldValues>({
name,
control,
label,
options,
}: SelectProps<T>): JSX.Element => {
return (
<div className="space-y-1">
<label htmlFor={id} className="block text-sm font-medium dark:text-white">
{label}
</label>
<select
ref={ref}
{...props}
className="block w-full p-2 border dark:border-gray-600 rounded-md shadow-sm dark:bg-secondaryDark"
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<Controller
name={name}
control={control}
rules={{
required: 'This is required',
}}
render={({ field: { onChange, value, name } }) => (
<div className="space-y-1">
<span className="block text-sm font-medium dark:text-white">
{label}
</span>
<ReactSelect options={options} />
</div>
)}
/>
);
});
};
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import type { FieldValues, UseControllerProps } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import MaterialSwitch from '@material-ui/core/Switch';
interface SwitchProps<T> extends UseControllerProps<T> {
label: string;
}
export const Switch = <T extends FieldValues>({
name,
control,
label,
}: SwitchProps<T>): JSX.Element => {
return (
<Controller
name={name}
control={control}
rules={{
required: 'This is required',
}}
render={({ field: { onChange, value, name } }) => (
<div className="flex flex-col">
<span className="block text-sm font-medium dark:text-white">
{label}
</span>
<div className="relative w-14 mr-2 ml-auto select-none">
<MaterialSwitch
id={name}
checked={value}
onChange={(ev) => onChange(ev.target.checked)}
/>
</div>
</div>
)}
/>
);
};
-33
View File
@@ -1,33 +0,0 @@
import React from 'react';
type DefaultInputProps = JSX.IntrinsicElements['input'];
export interface ToggleProps {
label: string;
}
export const Toggle = React.forwardRef<
HTMLInputElement,
ToggleProps & DefaultInputProps
>(function Input(
{ label, id, checked, ...props }: ToggleProps & DefaultInputProps,
ref,
) {
return (
<div className="flex flex-col">
<span className="block text-sm font-medium dark:text-white">{label}</span>
<div className="relative w-14 mr-2 ml-auto select-none">
<input
ref={ref}
{...props}
type="checkbox"
className="peer checked:right-0 absolute w-7 h-7 rounded-full bg-white appearance-none cursor-pointer"
/>
<label
htmlFor={id}
className="block overflow-hidden h-7 rounded-full peer-checked:bg-primary shadow-sm bg-gray-300 dark:bg-gray-700 cursor-pointer"
></label>
</div>
</div>
);
});
+20 -23
View File
@@ -1,31 +1,28 @@
import React from 'react';
export interface ButtonProps {
children: React.ReactNode;
className?: string;
clickAction?: () => void;
type?: 'button' | 'submit' | 'reset' | undefined;
import MaterialButton from '@material-ui/core/Button';
import type { ButtonProps as MaterialButtonProps } from '@material-ui/core/Button/Button';
interface LocalButtonProps {
text: string;
icon?: JSX.Element;
}
export const Button = ({
children,
className,
clickAction,
type,
}: ButtonProps): JSX.Element => {
export type ButtonProps = MaterialButtonProps & LocalButtonProps;
export const Button = ({ text, icon, ...props }: ButtonProps): JSX.Element => {
return (
<button
className={`w-10 h-10 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 active:bg-gray-300 dark:active:bg-gray-800 hover:shadow-inner text-gray-500 dark:text-gray-400 ${
className ?? ''
}`}
onClick={() => {
if (clickAction) {
clickAction();
}
}}
type={type}
<MaterialButton
{...props}
className="dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
>
<span className="flex justify-center">{children}</span>
</button>
<div className="flex p-3">
{icon &&
React.cloneElement(icon, {
className: 'h-6 w-6 mr-3 text-gray-500 dark:text-gray-400',
})}
<span>{text}</span>
</div>
</MaterialButton>
);
};
-37
View File
@@ -1,37 +0,0 @@
import React from 'react';
export interface DrawerProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
export const Drawer = ({
open,
onClose,
children,
}: DrawerProps): JSX.Element => {
return (
<>
{open && (
<div
className="z-10 fixed inset-0 transition-opacity"
onClick={onClose}
>
<div
className="absolute inset-0 backdrop-filter backdrop-blur"
tabIndex={0}
></div>
</div>
)}
<aside
className={`transform top-0 left-0 w-64 bg-white dark:bg-secondaryDark shadow-md border-r dark:border-gray-600 fixed h-full overflow-auto ease-in-out transition-all duration-300 z-30 ${
open ? 'translate-x-0' : '-translate-x-full'
}`}
>
{children}
</aside>
</>
);
};
+15
View File
@@ -0,0 +1,15 @@
import React from 'react';
import MaterialIconButton from '@material-ui/core/IconButton';
import type { IconButtonProps } from '@material-ui/core/IconButton/IconButton';
export const IconButton = ({
children,
...props
}: IconButtonProps): JSX.Element => {
return (
<MaterialIconButton {...props} className="text-gray-500 dark:text-gray-400">
{children}
</MaterialIconButton>
);
};
+19 -27
View File
@@ -6,13 +6,13 @@ import {
InformationCircleIcon,
ViewGridIcon,
} from '@heroicons/react/outline';
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer/SwipeableDrawer';
import { routes } from '../../core/router';
import { closeMobileNav, openMobileNav } from '../../core/slices/appSlice';
import { useAppDispatch, useAppSelector } from '../../hooks/redux';
import { routes } from '../../router';
import { closeMobileNav } from '../../slices/appSlice';
import { Drawer } from '../generic/Drawer';
import { Button } from '../generic/Button';
import { Logo } from './Logo';
import { MenuButton } from './MenuButton';
export const MobileNav = (): JSX.Element => {
const dispatch = useAppDispatch();
@@ -20,49 +20,41 @@ export const MobileNav = (): JSX.Element => {
const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen);
return (
<Drawer
<SwipeableDrawer
open={mobileNavOpen}
anchor="left"
onClose={() => {
dispatch(closeMobileNav());
}}
onOpen={() => {
dispatch(openMobileNav());
}}
>
<div className="flex flex-col">
<div className="flex flex-col dark:bg-secondaryDark h-full">
<div className="m-auto my-6">
<Logo />
</div>
<MenuButton
<Button
icon={<AnnotationIcon />}
text={'Messages'}
link={routes.messages().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.messages().link}
/>
<MenuButton
<Button
icon={<ViewGridIcon />}
text={'Nodes'}
link={routes.nodes().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.nodes().link}
/>
<MenuButton
<Button
icon={<CogIcon />}
text={'Settings'}
link={routes.settings().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.settings().link}
/>
<MenuButton
<Button
icon={<InformationCircleIcon />}
text={'About'}
link={routes.about().link}
clickAction={() => {
dispatch(closeMobileNav());
}}
{...routes.about().link}
/>
</div>
</Drawer>
</SwipeableDrawer>
);
};
+10 -10
View File
@@ -7,32 +7,32 @@ import {
ViewGridIcon,
} from '@heroicons/react/outline';
import { routes } from '../../router';
import { MenuButton } from './MenuButton';
import { routes } from '../../core/router';
import { Button } from '../generic/Button';
export const Navigation = (): JSX.Element => {
return (
<div className="hidden md:flex flex-auto flex-0 relative items-center h-16 px-4 ">
<div className="flex items-center">
<MenuButton
<Button
icon={<AnnotationIcon />}
text={'Messages'}
link={routes.messages().link}
{...routes.messages().link}
/>
<MenuButton
<Button
icon={<ViewGridIcon />}
text={'Nodes'}
link={routes.nodes().link}
{...routes.nodes().link}
/>
<MenuButton
<Button
icon={<CogIcon />}
text={'Settings'}
link={routes.settings().link}
{...routes.settings().link}
/>
<MenuButton
<Button
icon={<InformationCircleIcon />}
text={'About'}
link={routes.about().link}
{...routes.about().link}
/>
</div>
</div>
@@ -3,14 +3,14 @@ import React from 'react';
import { SwitchVerticalIcon } from '@heroicons/react/outline';
import { useAppSelector } from '../../../hooks/redux';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const DeviceStatusDropdown = (): JSX.Element => {
const ready = useAppSelector((state) => state.meshtastic.ready);
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus);
return (
<Button>
<IconButton>
<SwitchVerticalIcon className={`h-6 w-6 ${!ready && 'animate-pulse'}`} />
{/* <div
className={`flex w-6 h-6 rounded-full animate-pulse shadow-md ${
@@ -24,6 +24,6 @@ export const DeviceStatusDropdown = (): JSX.Element => {
: 'bg-gray-400'
}`}
></div> */}
</Button>
</IconButton>
);
};
@@ -4,9 +4,9 @@ import { Jp, Pt, Us } from 'react-flags-select';
import { Menu } from '@headlessui/react';
import i18n from '../../../core/translation';
import { useAppDispatch } from '../../../hooks/redux';
import i18n from '../../../translation';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const LanguageDropdown = (): JSX.Element => {
const dispatch = useAppDispatch();
@@ -32,11 +32,11 @@ export const LanguageDropdown = (): JSX.Element => {
return (
<Menu as="div" className="w-10 h-10">
<div className="absolute">
<Button>
<IconButton>
<Menu.Button as="div">
<Us className="w-6 shadow rounded-sm" />
</Menu.Button>
</Button>
</IconButton>
<Menu.Items className="z-20 absolute right-0 bg-white dark:bg-secondaryDark border dark:border-gray-600 divide-y divide-gray-200 dark:divide-gray-600 rounded-md shadow-md focus:outline-none">
{languages.map((language, index) => (
+11 -10
View File
@@ -2,21 +2,22 @@ import React from 'react';
import { MenuIcon } from '@heroicons/react/outline';
import { openMobileNav } from '../../../core/slices/appSlice';
import { useAppDispatch } from '../../../hooks/redux';
import { openMobileNav } from '../../../slices/appSlice';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const MobileNavToggle = (): JSX.Element => {
const dispatch = useAppDispatch();
return (
<Button
clickAction={() => {
dispatch(openMobileNav());
}}
className="md:hidden"
>
<MenuIcon className="h-6 w-6" />
</Button>
<div className="md:hidden">
<IconButton
onClick={() => {
dispatch(openMobileNav());
}}
>
<MenuIcon className="h-6 w-6" />
</IconButton>
</div>
);
};
+5 -5
View File
@@ -2,17 +2,17 @@ import React from 'react';
import { MoonIcon, SunIcon } from '@heroicons/react/outline';
import { setDarkModeEnabled } from '../../../core/slices/appSlice';
import { useAppDispatch, useAppSelector } from '../../../hooks/redux';
import { setDarkModeEnabled } from '../../../slices/appSlice';
import { Button } from '../../generic/Button';
import { IconButton } from '../../generic/IconButton';
export const ThemeToggle = (): JSX.Element => {
const dispatch = useAppDispatch();
const darkMode = useAppSelector((state) => state.app.darkMode);
return (
<Button
clickAction={() => {
<IconButton
onClick={() => {
dispatch(setDarkModeEnabled(!darkMode));
}}
>
@@ -21,6 +21,6 @@ export const ThemeToggle = (): JSX.Element => {
) : (
<MoonIcon className="h-6 w-6" />
)}
</Button>
</IconButton>
);
};
+10 -2
View File
@@ -4,13 +4,21 @@ 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 }: NodeProps): JSX.Element => {
export const Node = ({
node,
...props
}: NodeProps & DefaultDivProps): JSX.Element => {
return (
<div className="flex space-x-4 items-center w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900">
<div
{...props}
className="flex space-x-4 items-center w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
>
<Avatar
size={30}
name={node.user?.longName ?? 'UNK'}
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { XCircleIcon } from '@heroicons/react/outline';
import type { Protobuf } from '@meshtastic/meshtasticjs';
import { connection } from '../../core/connection';
export interface NodeProps {
node: Protobuf.NodeInfo;
onClose: () => void;
}
export const NodeDetails = ({ node }: NodeProps): JSX.Element => {
const { register, handleSubmit } = useForm<Protobuf.User>({
defaultValues: node.user,
});
const onSubmit = handleSubmit((data) => {
console.log(data);
connection.setOwner(data);
});
return (
<div>
<div className="flex dark:bg-primaryDark p-2 rounded-t-md justify-between border-b dark:border-gray-600 dark:text-white">
<div>{node.user?.longName ?? node.num}</div>
<XCircleIcon className="h-5 w-5 dark:text-white my-auto" />
</div>
<div>
<form onSubmit={onSubmit}>
{/* <Input label="Node Name" {...register('longName', {})} /> */}
<button
type="submit"
className="w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
>
Save
</button>
</form>
</div>
</div>
);
};
+1 -5
View File
@@ -27,11 +27,7 @@ export const PrimaryTemplate = ({
</div>
</div>
</div>
<div className="flex-auto p-6 sm:p-10 ">
<div className="max-w-3xl">
<div className="max-w-3xl">{children}</div>
</div>
</div>
<div className="flex-auto p-6 sm:p-10 ">{children}</div>
</div>
);
};