46 changed files with 1145 additions and 1533 deletions
@ -1,22 +1,3 @@ |
|||
{ |
|||
"root": true, |
|||
"parser": "@typescript-eslint/parser", |
|||
"plugins": ["@typescript-eslint"], |
|||
"extends": [ |
|||
"eslint:recommended", |
|||
"plugin:@typescript-eslint/recommended", |
|||
"plugin:react-hooks/recommended", |
|||
"plugin:react/recommended", |
|||
"plugin:import/recommended", |
|||
"plugin:import/typescript", |
|||
"plugin:prettier/recommended" |
|||
], |
|||
"rules": { |
|||
"@typescript-eslint/consistent-type-imports": "error" |
|||
}, |
|||
"settings": { |
|||
"react": { |
|||
"version": "detect" |
|||
} |
|||
} |
|||
"extends": ["@verypossible/eslint-config/react"] |
|||
} |
|||
|
|||
@ -1,204 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Controller, useForm } from 'react-hook-form'; |
|||
|
|||
import Button from '@material-ui/core/Button'; |
|||
import Checkbox from '@material-ui/core/Checkbox'; |
|||
import FormControlLabel from '@material-ui/core/FormControlLabel'; |
|||
import Radio from '@material-ui/core/Radio'; |
|||
import RadioGroup from '@material-ui/core/RadioGroup'; |
|||
import Switch from '@material-ui/core/Switch'; |
|||
import TextField from '@material-ui/core/TextField'; |
|||
import Typography from '@material-ui/core/Typography'; |
|||
|
|||
let renderCount = 0; |
|||
|
|||
const options = [ |
|||
{ |
|||
value: 'chocolate', |
|||
label: 'Chocolate', |
|||
}, |
|||
{ |
|||
value: 'strawberry', |
|||
label: 'Strawberry', |
|||
}, |
|||
{ |
|||
value: 'vanilla', |
|||
label: 'Vanilla', |
|||
}, |
|||
]; |
|||
|
|||
const defaultValues = { |
|||
Native: '', |
|||
TextField: '', |
|||
Select: '', |
|||
ReactSelect: '', |
|||
Checkbox: false, |
|||
switch: false, |
|||
RadioGroup: '', |
|||
}; |
|||
|
|||
export const TestForm = () => { |
|||
const { handleSubmit, register, reset, control, watch } = useForm({ |
|||
defaultValues, |
|||
mode: 'onChange', |
|||
}); |
|||
renderCount++; |
|||
|
|||
const data = watch(); |
|||
|
|||
return ( |
|||
<div className="flex w-full max-w-screen-md justify-start items-start"> |
|||
<form |
|||
className="w-1/2" |
|||
onSubmit={handleSubmit((data) => console.info(data))} |
|||
> |
|||
<div className="mt-48 mb-16"> |
|||
<Typography className="mb-24 font-medium text-14"> |
|||
Native Input: |
|||
</Typography> |
|||
<input |
|||
className="border-1 outline-none rounded-8 p-8" |
|||
{...register('Native')} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="mt-48 mb-16"> |
|||
<Typography className="mb-24 font-medium text-14"> |
|||
MUI Checkbox |
|||
</Typography> |
|||
<Controller |
|||
name="Checkbox" |
|||
control={control} |
|||
defaultValue={false} |
|||
render={({ field: { onChange, value } }) => ( |
|||
<Checkbox |
|||
checked={value} |
|||
onChange={(ev) => onChange(ev.target.checked)} |
|||
/> |
|||
)} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="mt-48 mb-16"> |
|||
<Typography className="mb-24 font-medium text-14"> |
|||
Radio Group |
|||
</Typography> |
|||
<Controller |
|||
render={({ field }) => ( |
|||
<RadioGroup {...field} aria-label="gender" name="gender1"> |
|||
<FormControlLabel |
|||
value="female" |
|||
control={<Radio />} |
|||
label="Female" |
|||
/> |
|||
<FormControlLabel |
|||
value="male" |
|||
control={<Radio />} |
|||
label="Male" |
|||
/> |
|||
</RadioGroup> |
|||
)} |
|||
name="RadioGroup" |
|||
control={control} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="mt-48 mb-16"> |
|||
<Typography className="mb-24 font-medium text-14"> |
|||
MUI TextField |
|||
</Typography> |
|||
<Controller |
|||
render={({ field }) => <TextField {...field} variant="outlined" />} |
|||
name="TextField" |
|||
control={control} |
|||
/> |
|||
</div> |
|||
|
|||
{/* <div className="mt-48 mb-16"> |
|||
<Typography className="mb-24 font-medium text-14"> |
|||
MUI Select |
|||
</Typography> |
|||
<Controller |
|||
render={({ field }) => ( |
|||
<Select {...field} variant="outlined"> |
|||
<MenuItem value={10}>Ten</MenuItem> |
|||
<MenuItem value={20}>Twenty</MenuItem> |
|||
<MenuItem value={30}>Thirty</MenuItem> |
|||
</Select> |
|||
)} |
|||
name="Select" |
|||
control={control} |
|||
/> |
|||
</div> */} |
|||
|
|||
<div className="mt-48 mb-16"> |
|||
<Typography className="mb-24 font-medium text-14"> |
|||
MUI Switch |
|||
</Typography> |
|||
<Controller |
|||
name="switch" |
|||
control={control} |
|||
defaultValue={false} |
|||
render={({ field: { onChange, value } }) => ( |
|||
<Switch |
|||
checked={value} |
|||
onChange={(ev) => onChange(ev.target.checked)} |
|||
/> |
|||
)} |
|||
/> |
|||
</div> |
|||
|
|||
<div className="mt-48 mb-16"> |
|||
<Typography className="mb-24 font-medium text-14"> |
|||
React Select |
|||
</Typography> |
|||
{/* <Controller |
|||
render={({ field }) => <ReactSelect {...field} />} |
|||
options={options} |
|||
name="ReactSelect" |
|||
isClearable |
|||
control={control} |
|||
onChange={([selected]) => { |
|||
return { value: selected }; |
|||
}} |
|||
/> */} |
|||
</div> |
|||
|
|||
<div className="flex my-48 items-center"> |
|||
<Button |
|||
className="mx-8" |
|||
variant="contained" |
|||
color="secondary" |
|||
type="submit" |
|||
> |
|||
Submit |
|||
</Button> |
|||
|
|||
<Button |
|||
className="mx-8" |
|||
type="button" |
|||
onClick={() => { |
|||
reset(defaultValues); |
|||
}} |
|||
> |
|||
Reset Form |
|||
</Button> |
|||
</div> |
|||
</form> |
|||
|
|||
<div className="w-1/2 my-48 p-24"> |
|||
<pre className="language-js p-24 w-400"> |
|||
{JSON.stringify(data, null, 2)} |
|||
</pre> |
|||
|
|||
<Typography |
|||
className="mt-16 font-medium text-12 italic" |
|||
color="textSecondary" |
|||
> |
|||
Render Count: {renderCount} |
|||
</Typography> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,52 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
type DefaultInputProps = JSX.IntrinsicElements['input']; |
|||
|
|||
export interface InputProps { |
|||
valid?: boolean; |
|||
validationMessage?: string; |
|||
icon?: JSX.Element; |
|||
label: string; |
|||
} |
|||
|
|||
export const Input = React.forwardRef< |
|||
HTMLInputElement, |
|||
InputProps & DefaultInputProps |
|||
>(function Input( |
|||
{ |
|||
valid, |
|||
validationMessage, |
|||
icon, |
|||
label, |
|||
id, |
|||
...props |
|||
}: InputProps & DefaultInputProps, |
|||
ref, |
|||
) { |
|||
return ( |
|||
<div className="space-y-1"> |
|||
<label htmlFor={id} className="block text-sm font-medium dark:text-white"> |
|||
{label} |
|||
</label> |
|||
<div className="relative"> |
|||
{icon && ( |
|||
<div className="flex absolute inset-y-0 left-0 px-3 items-center pointer-events-none"> |
|||
{React.cloneElement(icon, { |
|||
className: 'w-5 h-5 text-gray-500 dark:text-gray-600', |
|||
})} |
|||
</div> |
|||
)} |
|||
<input |
|||
ref={ref} |
|||
{...props} |
|||
className={`block w-full h-11 rounded-md border shadow-sm focus:outline-none focus:border-primary dark:focus:border-primary dark:bg-secondaryDark dark:border-gray-600 dark:text-white ${ |
|||
icon ? 'pl-9' : 'pl-2' |
|||
}`}
|
|||
/> |
|||
</div> |
|||
{!valid && ( |
|||
<div className="text-sm text-gray-600">{validationMessage}</div> |
|||
)} |
|||
</div> |
|||
); |
|||
}); |
|||
@ -1,38 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import type { FieldValues, UseControllerProps } from 'react-hook-form'; |
|||
import { Controller } from 'react-hook-form'; |
|||
import ReactSelect from 'react-select'; |
|||
|
|||
interface SelectProps<T> extends UseControllerProps<T> { |
|||
label: string; |
|||
options: { |
|||
value: string; |
|||
label: string; |
|||
}[]; |
|||
} |
|||
|
|||
export const Select = <T extends FieldValues>({ |
|||
name, |
|||
control, |
|||
label, |
|||
options, |
|||
}: SelectProps<T>): JSX.Element => { |
|||
return ( |
|||
<Controller |
|||
name={name} |
|||
control={control} |
|||
rules={{ |
|||
required: 'This is required', |
|||
}} |
|||
render={({ field: { onChange, value, name } }) => ( |
|||
<div className="space-y-1"> |
|||
<span className="block text-sm font-medium dark:text-white"> |
|||
{label} |
|||
</span> |
|||
<ReactSelect options={options} /> |
|||
</div> |
|||
)} |
|||
/> |
|||
); |
|||
}; |
|||
@ -1,40 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import type { FieldValues, UseControllerProps } from 'react-hook-form'; |
|||
import { Controller } from 'react-hook-form'; |
|||
|
|||
import MaterialSwitch from '@material-ui/core/Switch'; |
|||
|
|||
interface SwitchProps<T> extends UseControllerProps<T> { |
|||
label: string; |
|||
} |
|||
|
|||
export const Switch = <T extends FieldValues>({ |
|||
name, |
|||
control, |
|||
label, |
|||
}: SwitchProps<T>): JSX.Element => { |
|||
return ( |
|||
<Controller |
|||
name={name} |
|||
control={control} |
|||
rules={{ |
|||
required: 'This is required', |
|||
}} |
|||
render={({ field: { onChange, value, name } }) => ( |
|||
<div className="flex flex-col"> |
|||
<span className="block text-sm font-medium dark:text-white"> |
|||
{label} |
|||
</span> |
|||
<div className="relative w-14 mr-2 ml-auto select-none"> |
|||
<MaterialSwitch |
|||
id={name} |
|||
checked={value} |
|||
onChange={(ev) => onChange(ev.target.checked)} |
|||
/> |
|||
</div> |
|||
</div> |
|||
)} |
|||
/> |
|||
); |
|||
}; |
|||
@ -0,0 +1,33 @@ |
|||
import React from 'react'; |
|||
|
|||
type DefaultDivProps = JSX.IntrinsicElements['div']; |
|||
|
|||
interface LocalBlurProps { |
|||
disableOnMd?: boolean; |
|||
} |
|||
|
|||
export type BlurProps = LocalBlurProps & DefaultDivProps; |
|||
|
|||
export const Blur = ({ |
|||
disableOnMd, |
|||
className, |
|||
onClick, |
|||
...props |
|||
}: BlurProps): JSX.Element => { |
|||
return ( |
|||
<div |
|||
className={`absolute inset-0 z-10 w-full h-full transition-opacity ${ |
|||
disableOnMd ? 'md:hidden' : 'test' |
|||
} ${className}`}
|
|||
{...props} |
|||
> |
|||
<div |
|||
onClick={onClick} |
|||
className={`absolute inset-0 w-full h-full backdrop-filter backdrop-blur-sm ${ |
|||
disableOnMd ? 'md:hidden' : 'test' |
|||
}`}
|
|||
tabIndex={0} |
|||
></div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,28 +1,50 @@ |
|||
import React from 'react'; |
|||
|
|||
import MaterialButton from '@material-ui/core/Button'; |
|||
import type { ButtonProps as MaterialButtonProps } from '@material-ui/core/Button/Button'; |
|||
type DefaultButtonProps = JSX.IntrinsicElements['button']; |
|||
|
|||
interface LocalButtonProps { |
|||
text: string; |
|||
icon?: JSX.Element; |
|||
circle?: boolean; |
|||
active?: boolean; |
|||
border?: boolean; |
|||
} |
|||
|
|||
export type ButtonProps = MaterialButtonProps & LocalButtonProps; |
|||
export type ButtonProps = LocalButtonProps & DefaultButtonProps; |
|||
|
|||
export const Button = ({ text, icon, ...props }: ButtonProps): JSX.Element => { |
|||
export const Button = ({ |
|||
icon, |
|||
circle, |
|||
className, |
|||
active, |
|||
border, |
|||
disabled, |
|||
children, |
|||
...props |
|||
}: ButtonProps): JSX.Element => { |
|||
return ( |
|||
<MaterialButton |
|||
<button |
|||
className={`items-center select-none flex dark:text-white ${ |
|||
active && !disabled ? 'bg-gray-100 dark:bg-gray-700' : '' |
|||
} ${ |
|||
circle ? 'rounded-full h-10 w-10' : 'rounded-md p-3 space-x-3 text-sm' |
|||
} ${ |
|||
disabled |
|||
? 'cursor-not-allowed dark:bg-primaryDark bg-white' |
|||
: 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 hover:shadow-md' |
|||
} ${border ? 'border dark:border-gray-600' : ''} ${className}`}
|
|||
{...props} |
|||
className="dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" |
|||
> |
|||
<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> |
|||
{icon && ( |
|||
<div |
|||
className={`text-gray-500 dark:text-gray-400 ${ |
|||
circle ? 'mx-auto' : '' |
|||
}`}
|
|||
> |
|||
{icon} |
|||
</div> |
|||
)} |
|||
|
|||
<span>{children}</span> |
|||
</button> |
|||
); |
|||
}; |
|||
|
|||
@ -0,0 +1,35 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Blur } from '@components/generic/Blur'; |
|||
|
|||
type DefaultAsideProps = JSX.IntrinsicElements['aside']; |
|||
|
|||
interface LocalDrawerProps { |
|||
open: boolean; |
|||
permenant?: boolean; |
|||
onClose: () => void; |
|||
} |
|||
export type DrawerProps = LocalDrawerProps & DefaultAsideProps; |
|||
|
|||
export const Drawer = ({ |
|||
open, |
|||
permenant, |
|||
onClose, |
|||
children, |
|||
...props |
|||
}: DrawerProps): JSX.Element => { |
|||
return ( |
|||
<> |
|||
{open && <Blur disableOnMd={true} onClick={onClose} />} |
|||
|
|||
<aside |
|||
className={`transform top-0 left-0 bg-white dark:bg-secondaryDark shadow-md max-w-xs w-full border-r dark:border-gray-600 h-full overflow-auto ease-in-out transition-all duration-300 z-30 ${ |
|||
permenant ? '' : 'absolute' |
|||
} ${open ? 'translate-x-0' : '-translate-x-full'}`}
|
|||
{...props} |
|||
> |
|||
{children} |
|||
</aside> |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,15 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import MaterialIconButton from '@material-ui/core/IconButton'; |
|||
import type { IconButtonProps } from '@material-ui/core/IconButton/IconButton'; |
|||
|
|||
export const IconButton = ({ |
|||
children, |
|||
...props |
|||
}: IconButtonProps): JSX.Element => { |
|||
return ( |
|||
<MaterialIconButton {...props} className="text-gray-500 dark:text-gray-400"> |
|||
{children} |
|||
</MaterialIconButton> |
|||
); |
|||
}; |
|||
@ -0,0 +1,50 @@ |
|||
import React from 'react'; |
|||
|
|||
type DefaultInputProps = JSX.IntrinsicElements['input']; |
|||
|
|||
interface LocalInputProps { |
|||
icon?: JSX.Element; |
|||
label?: string; |
|||
valid?: boolean; |
|||
validationMessage?: string; |
|||
} |
|||
|
|||
export type InputProps = LocalInputProps & DefaultInputProps; |
|||
|
|||
export const Input = React.forwardRef<HTMLInputElement, InputProps>( |
|||
function Input( |
|||
{ icon, label, valid, validationMessage, id, ...props }: InputProps, |
|||
ref, |
|||
) { |
|||
return ( |
|||
<div className="w-full"> |
|||
<label |
|||
htmlFor={id} |
|||
className="block text-sm font-medium dark:text-white" |
|||
> |
|||
{label} |
|||
</label> |
|||
<div className="relative"> |
|||
{icon && ( |
|||
<div className="absolute inset-y-0 left-0 flex items-center px-3 pointer-events-none"> |
|||
{React.cloneElement(icon, { |
|||
className: 'w-5 h-5 text-gray-500 dark:text-gray-600', |
|||
})} |
|||
</div> |
|||
)} |
|||
<input |
|||
id={id} |
|||
ref={ref} |
|||
{...props} |
|||
className={`block w-full h-11 rounded-md border shadow-sm focus:outline-none focus:border-primary dark:focus:border-primary bg-white dark:bg-secondaryDark dark:border-gray-600 dark:text-white ${ |
|||
icon ? 'pl-9' : 'pl-2' |
|||
}`}
|
|||
/> |
|||
</div> |
|||
{!valid && ( |
|||
<div className="text-sm text-gray-600">{validationMessage}</div> |
|||
)} |
|||
</div> |
|||
); |
|||
}, |
|||
); |
|||
@ -0,0 +1,82 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Listbox } from '@headlessui/react'; |
|||
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid'; |
|||
|
|||
export interface SelectProps { |
|||
label: string; |
|||
options: { |
|||
name: string; |
|||
value: string; |
|||
icon: JSX.Element; |
|||
}[]; |
|||
id: string; |
|||
value: string; |
|||
onChange: (value: string) => void; |
|||
} |
|||
|
|||
export const Select = ({ |
|||
label, |
|||
options, |
|||
id, |
|||
value, |
|||
onChange, |
|||
}: SelectProps): JSX.Element => { |
|||
return ( |
|||
<div className="w-full"> |
|||
<label htmlFor={id} className="block text-sm font-medium dark:text-white"> |
|||
{label} |
|||
</label> |
|||
|
|||
<Listbox value={value} onChange={onChange}> |
|||
<div className="relative mt-1"> |
|||
<Listbox.Button className="relative w-full text-left bg-white border rounded-md shadow-sm h-11 focus:outline-none focus:border-primary dark:focus:border-primary dark:bg-secondaryDark dark:border-gray-600 dark:text-white"> |
|||
<span className="block truncate">{value}</span> |
|||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> |
|||
<SelectorIcon |
|||
className="w-5 h-5 text-gray-400" |
|||
aria-hidden="true" |
|||
/> |
|||
</span> |
|||
</Listbox.Button> |
|||
|
|||
<Listbox.Options className="absolute w-full bg-white border rounded-md shadow-sm focus:outline-none dark:bg-secondaryDark dark:border-gray-600 dark:text-white"> |
|||
{options.map((option) => ( |
|||
<Listbox.Option |
|||
key={option.value} |
|||
className={({ active }): string => |
|||
`cursor-default select-none relative py-2 pl-10 pr-4 first:rounded-t-md last:rounded-b-md dark:text-white ${ |
|||
active ? 'bg-gray-200 dark:bg-primaryDark' : 'text-gray-900' |
|||
}` |
|||
} |
|||
value={option.value} |
|||
> |
|||
{({ selected, active }): JSX.Element => ( |
|||
<> |
|||
<span |
|||
className={`${ |
|||
selected ? 'font-medium' : 'font-normal' |
|||
} block truncate`}
|
|||
> |
|||
{option.name} |
|||
</span> |
|||
{selected ? ( |
|||
<span |
|||
className={`${ |
|||
active ? 'text-amber-600' : 'text-amber-600' |
|||
} |
|||
absolute inset-y-0 left-0 flex items-center pl-3`}
|
|||
> |
|||
<CheckIcon className="w-5 h-5" aria-hidden="true" /> |
|||
</span> |
|||
) : null} |
|||
</> |
|||
)} |
|||
</Listbox.Option> |
|||
))} |
|||
</Listbox.Options> |
|||
</div> |
|||
</Listbox> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,33 @@ |
|||
import React from 'react'; |
|||
|
|||
type DefaultDivProps = JSX.IntrinsicElements['div']; |
|||
|
|||
interface LocalSidebarItemProps { |
|||
title: string; |
|||
description: string; |
|||
selected: boolean; |
|||
icon: JSX.Element; |
|||
} |
|||
|
|||
export type SidebarItemProps = LocalSidebarItemProps & DefaultDivProps; |
|||
|
|||
export const SidebarItem = ({ |
|||
title, |
|||
description, |
|||
selected, |
|||
icon, |
|||
}: SidebarItemProps): JSX.Element => { |
|||
return ( |
|||
<div |
|||
className={`flex p-5 cursor-pointer select-none dark:hover:bg-primaryDark ${ |
|||
selected ? 'bg-gray-200 dark:bg-primaryDark' : 'dark:bg-secondaryDark' |
|||
}`}
|
|||
> |
|||
<div className="text-gray-500 dark:text-gray-400">{icon}</div> |
|||
<div className="ml-3 text-left"> |
|||
<div className="font-medium text-left">{title}</div> |
|||
<div className="mt-0.5 text-gray-400 text-sm">{description}</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,37 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import type { Link } from 'type-route'; |
|||
|
|||
interface MenuButtonProps { |
|||
icon: JSX.Element; |
|||
text: string; |
|||
link: Link; |
|||
clickAction?: () => void; |
|||
} |
|||
|
|||
export const MenuButton = ({ |
|||
icon, |
|||
text, |
|||
link, |
|||
clickAction, |
|||
}: MenuButtonProps): JSX.Element => { |
|||
return ( |
|||
<div |
|||
onClick={() => { |
|||
if (clickAction) { |
|||
clickAction(); |
|||
} |
|||
}} |
|||
> |
|||
<a |
|||
{...link} |
|||
className="flex text-sm h-12 items-center dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md cursor-pointer px-3 select-none" |
|||
> |
|||
{React.cloneElement(icon, { |
|||
className: 'h-6 w-6 mr-3 text-gray-500 dark:text-gray-400', |
|||
})} |
|||
<span className="">{text}</span> |
|||
</a> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,29 +1,20 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useAppSelector } from '@app/hooks/redux'; |
|||
import { Button } from '@components/generic/Button'; |
|||
import { SwitchVerticalIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { useAppSelector } from '../../../hooks/redux'; |
|||
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 ( |
|||
<IconButton> |
|||
<SwitchVerticalIcon className={`h-6 w-6 ${!ready && 'animate-pulse'}`} /> |
|||
{/* <div |
|||
className={`flex w-6 h-6 rounded-full animate-pulse shadow-md ${ |
|||
deviceStatus <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED |
|||
? 'bg-red-400 animate-pulse' |
|||
: deviceStatus <= Types.DeviceStatusEnum.DEVICE_CONFIGURING && |
|||
!ready |
|||
? 'bg-yellow-400 animate-pulse' |
|||
: ready |
|||
? 'bg-green-400' |
|||
: 'bg-gray-400' |
|||
}`}
|
|||
></div> */} |
|||
</IconButton> |
|||
<Button |
|||
icon={ |
|||
<SwitchVerticalIcon |
|||
className={`h-6 w-6 ${!ready ? 'animate-pulse' : ''}`} |
|||
/> |
|||
} |
|||
circle |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
@ -1,66 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Jp, Pt, Us } from 'react-flags-select'; |
|||
|
|||
import { Menu } from '@headlessui/react'; |
|||
|
|||
import i18n from '../../../core/translation'; |
|||
import { useAppDispatch } from '../../../hooks/redux'; |
|||
import { IconButton } from '../../generic/IconButton'; |
|||
|
|||
export const LanguageDropdown = (): JSX.Element => { |
|||
const dispatch = useAppDispatch(); |
|||
|
|||
const languages = [ |
|||
{ |
|||
name: 'English', |
|||
value: 'en', |
|||
flag: <Us className="w-6" />, |
|||
}, |
|||
{ |
|||
name: 'Português', |
|||
value: 'pt', |
|||
flag: <Pt className="w-6" />, |
|||
}, |
|||
{ |
|||
name: 'Japanese', |
|||
value: 'jp', |
|||
flag: <Jp className="w-6" />, |
|||
}, |
|||
]; |
|||
|
|||
return ( |
|||
<Menu as="div" className="w-10 h-10"> |
|||
<div className="absolute"> |
|||
<IconButton> |
|||
<Menu.Button as="div"> |
|||
<Us className="w-6 shadow rounded-sm" /> |
|||
</Menu.Button> |
|||
</IconButton> |
|||
|
|||
<Menu.Items className="z-20 absolute right-0 bg-white dark:bg-secondaryDark border dark:border-gray-600 divide-y divide-gray-200 dark:divide-gray-600 rounded-md shadow-md focus:outline-none"> |
|||
{languages.map((language, index) => ( |
|||
<Menu.Item |
|||
key={index} |
|||
onClick={() => { |
|||
i18n.changeLanguage(language.value); |
|||
}} |
|||
> |
|||
{({ active }) => ( |
|||
<button |
|||
className={`dark:text-white first:rounded-t-md last:rounded-b-md space-x-2 group flex items-center w-full px-2 py-2 text-sm ${ |
|||
active && 'bg-gray-200 dark:bg-gray-800' |
|||
}`}
|
|||
> |
|||
{language.flag} |
|||
<p className="font-medium">{language.name}</p> |
|||
</button> |
|||
)} |
|||
</Menu.Item> |
|||
))} |
|||
{/* ... */} |
|||
</Menu.Items> |
|||
</div> |
|||
</Menu> |
|||
); |
|||
}; |
|||
@ -1,23 +1,23 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Button } from '@components/generic/Button'; |
|||
import { openMobileNav } from '@core/slices/appSlice'; |
|||
import { MenuIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { openMobileNav } from '../../../core/slices/appSlice'; |
|||
import { useAppDispatch } from '../../../hooks/redux'; |
|||
import { IconButton } from '../../generic/IconButton'; |
|||
|
|||
export const MobileNavToggle = (): JSX.Element => { |
|||
const dispatch = useAppDispatch(); |
|||
|
|||
return ( |
|||
<div className="md:hidden"> |
|||
<IconButton |
|||
onClick={() => { |
|||
<Button |
|||
icon={<MenuIcon className="w-5 h-5" />} |
|||
onClick={(): void => { |
|||
dispatch(openMobileNav()); |
|||
}} |
|||
> |
|||
<MenuIcon className="h-6 w-6" /> |
|||
</IconButton> |
|||
circle |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
@ -1,26 +1,27 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux'; |
|||
import { Button } from '@components/generic/Button'; |
|||
import { setDarkModeEnabled } from '@core/slices/appSlice'; |
|||
import { MoonIcon, SunIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { setDarkModeEnabled } from '../../../core/slices/appSlice'; |
|||
import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; |
|||
import { IconButton } from '../../generic/IconButton'; |
|||
|
|||
export const ThemeToggle = (): JSX.Element => { |
|||
const dispatch = useAppDispatch(); |
|||
const darkMode = useAppSelector((state) => state.app.darkMode); |
|||
|
|||
return ( |
|||
<IconButton |
|||
onClick={() => { |
|||
<Button |
|||
icon={ |
|||
darkMode ? ( |
|||
<SunIcon className="w-5 h-5" /> |
|||
) : ( |
|||
<MoonIcon className="w-5 h-5" /> |
|||
) |
|||
} |
|||
circle |
|||
onClick={(): void => { |
|||
dispatch(setDarkModeEnabled(!darkMode)); |
|||
}} |
|||
> |
|||
{darkMode ? ( |
|||
<SunIcon className="h-6 w-6" /> |
|||
) : ( |
|||
<MoonIcon className="h-6 w-6" /> |
|||
)} |
|||
</IconButton> |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
@ -1,44 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useForm } from 'react-hook-form'; |
|||
|
|||
import { XCircleIcon } from '@heroicons/react/outline'; |
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { connection } from '../../core/connection'; |
|||
|
|||
export interface NodeProps { |
|||
node: Protobuf.NodeInfo; |
|||
onClose: () => void; |
|||
} |
|||
|
|||
export const NodeDetails = ({ node }: NodeProps): JSX.Element => { |
|||
const { register, handleSubmit } = useForm<Protobuf.User>({ |
|||
defaultValues: node.user, |
|||
}); |
|||
|
|||
const onSubmit = handleSubmit((data) => { |
|||
console.log(data); |
|||
connection.setOwner(data); |
|||
}); |
|||
|
|||
return ( |
|||
<div> |
|||
<div className="flex dark:bg-primaryDark p-2 rounded-t-md justify-between border-b dark:border-gray-600 dark:text-white"> |
|||
<div>{node.user?.longName ?? node.num}</div> |
|||
<XCircleIcon className="h-5 w-5 dark:text-white my-auto" /> |
|||
</div> |
|||
<div> |
|||
<form onSubmit={onSubmit}> |
|||
{/* <Input label="Node Name" {...register('longName', {})} /> */} |
|||
<button |
|||
type="submit" |
|||
className="w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900" |
|||
> |
|||
Save |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,28 +0,0 @@ |
|||
import type { Theme } from '@material-ui/core'; |
|||
import { createTheme } from '@material-ui/core/styles'; |
|||
|
|||
export const theme = (darkMode: boolean): Theme => { |
|||
return createTheme( |
|||
darkMode |
|||
? { |
|||
palette: { |
|||
mode: 'dark', |
|||
primary: { |
|||
main: '#67ea94', |
|||
}, |
|||
background: { |
|||
default: '#0F172A', |
|||
paper: '#0F172A', |
|||
}, |
|||
}, |
|||
} |
|||
: { |
|||
palette: { |
|||
mode: 'light', |
|||
primary: { |
|||
main: '#67ea94', |
|||
}, |
|||
}, |
|||
}, |
|||
); |
|||
}; |
|||
@ -0,0 +1,21 @@ |
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */ |
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ |
|||
import useBreakpointHook from 'use-breakpoint'; |
|||
|
|||
const BREAKPOINTS = { |
|||
sm: 640, |
|||
// => @media (min-width: 640px) { ... }
|
|||
|
|||
md: 768, |
|||
// => @media (min-width: 768px) { ... }
|
|||
|
|||
lg: 1024, |
|||
// => @media (min-width: 1024px) { ... }
|
|||
|
|||
xl: 1280, |
|||
// => @media (min-width: 1280px) { ... }
|
|||
|
|||
'2xl': 1536, |
|||
// => @media (min-width: 1536px) { ... }
|
|||
}; |
|||
export const useBreakpoint = () => useBreakpointHook(BREAKPOINTS); |
|||
@ -1,7 +1,9 @@ |
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */ |
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ |
|||
import type { TypedUseSelectorHook } from 'react-redux'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
|
|||
import type { AppDispatch, RootState } from '../core/store'; |
|||
import type { AppDispatch, RootState } from '@core/store'; |
|||
|
|||
export const useAppDispatch = () => useDispatch<AppDispatch>(); |
|||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; |
|||
|
|||
@ -1,13 +1,11 @@ |
|||
import React from 'react'; |
|||
|
|||
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; |
|||
import { TestForm } from '../components/TestForm'; |
|||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; |
|||
|
|||
export const About = (): JSX.Element => { |
|||
return ( |
|||
<PrimaryTemplate title="meshtastic-web" tagline="About"> |
|||
<p>Content</p> |
|||
<TestForm /> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
|
|||
@ -1,45 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { Node } from '../components/nodes/Node'; |
|||
import { NodeDetails } from '../components/nodes/NodeDetails'; |
|||
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; |
|||
import { useAppSelector } from '../hooks/redux'; |
|||
|
|||
export const Nodes = (): JSX.Element => { |
|||
const nodes = useAppSelector((state) => state.meshtastic.nodes); |
|||
const [currentNode, setCurrentNode] = React.useState< |
|||
Protobuf.NodeInfo | undefined |
|||
>(); |
|||
|
|||
return ( |
|||
<PrimaryTemplate title="Administration" tagline="Node"> |
|||
<div className="flex w-full space-x-5"> |
|||
<div className="w-1/3"> |
|||
{nodes.map((node) => ( |
|||
<Node |
|||
key={node.num} |
|||
node={node} |
|||
onClick={() => { |
|||
setCurrentNode(node); |
|||
}} |
|||
/> |
|||
))} |
|||
</div> |
|||
<div className="w-2/3"> |
|||
{currentNode ? ( |
|||
<NodeDetails |
|||
onClose={() => { |
|||
setCurrentNode(undefined); |
|||
}} |
|||
node={currentNode} |
|||
/> |
|||
) : ( |
|||
<div>Node not selected</div> |
|||
)} |
|||
</div> |
|||
</div> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -0,0 +1,92 @@ |
|||
import React from 'react'; |
|||
|
|||
import Avatar from 'boring-avatars'; |
|||
|
|||
import { useBreakpoint } from '@app/hooks/breakpoint'; |
|||
import { useAppSelector } from '@app/hooks/redux'; |
|||
import { Button } from '@components/generic/Button'; |
|||
import { Drawer } from '@components/generic/Drawer'; |
|||
import { SidebarItem } from '@components/generic/SidebarItem'; |
|||
import { Tab } from '@headlessui/react'; |
|||
import { XCircleIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { Node } from './Node'; |
|||
|
|||
export const Nodes = (): JSX.Element => { |
|||
const [navOpen, setNavOpen] = React.useState(false); |
|||
|
|||
const { breakpoint } = useBreakpoint(); |
|||
|
|||
const nodes = useAppSelector((state) => state.meshtastic.nodes); |
|||
|
|||
return ( |
|||
<Tab.Group> |
|||
<div className="relative flex w-full dark:text-white"> |
|||
<Drawer |
|||
open={breakpoint === 'sm' ? navOpen : true} |
|||
permenant={breakpoint !== 'sm'} |
|||
onClose={(): void => { |
|||
setNavOpen(!navOpen); |
|||
}} |
|||
> |
|||
<Tab.List className="flex flex-col border-b divide-y divide-gray-300 dark:divide-gray-600 dark:border-gray-600"> |
|||
<div className="flex items-center justify-between m-8 mr-6 md:my-10"> |
|||
<div className="text-4xl font-extrabold leading-none tracking-tight"> |
|||
Nodes |
|||
</div> |
|||
<div className="md:hidden"> |
|||
<Button |
|||
icon={<XCircleIcon className="w-5 h-5" />} |
|||
circle |
|||
onClick={(): void => { |
|||
setNavOpen(false); |
|||
}} |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
{nodes.map((node) => ( |
|||
<Tab |
|||
onClick={(): void => { |
|||
setNavOpen(false); |
|||
}} |
|||
key={node.num} |
|||
> |
|||
{({ selected }): JSX.Element => ( |
|||
<SidebarItem |
|||
title={node.user?.longName ?? node.num.toString()} |
|||
description="Node info" |
|||
selected={selected} |
|||
icon={ |
|||
<Avatar |
|||
size={30} |
|||
name={node.user?.longName ?? node.num.toString()} |
|||
variant="beam" |
|||
colors={[ |
|||
'#213435', |
|||
'#46685B', |
|||
'#648A64', |
|||
'#A6B985', |
|||
'#E1E3AC', |
|||
]} |
|||
/> |
|||
} |
|||
/> |
|||
)} |
|||
</Tab> |
|||
))} |
|||
</Tab.List> |
|||
</Drawer> |
|||
<div className="w-full"> |
|||
<Tab.Panels> |
|||
{nodes.map((node) => ( |
|||
<Tab.Panel key={node.num}> |
|||
<Node navOpen={navOpen} setNavOpen={setNavOpen} node={node} /> |
|||
</Tab.Panel> |
|||
))} |
|||
</Tab.Panels> |
|||
</div> |
|||
</div> |
|||
</Tab.Group> |
|||
); |
|||
}; |
|||
@ -0,0 +1,32 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Button } from '@components/generic/Button'; |
|||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; |
|||
import { MenuIcon } from '@heroicons/react/outline'; |
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
export interface NodeProps { |
|||
navOpen: boolean; |
|||
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>; |
|||
node: Protobuf.NodeInfo; |
|||
} |
|||
|
|||
export const Node = ({ navOpen, setNavOpen, node }: NodeProps): JSX.Element => { |
|||
return ( |
|||
<PrimaryTemplate |
|||
title={node.user?.longName ?? node.num.toString()} |
|||
tagline="Node" |
|||
button={ |
|||
<Button |
|||
icon={<MenuIcon className="w-5 h-5" />} |
|||
onClick={(): void => { |
|||
setNavOpen(!navOpen); |
|||
}} |
|||
circle |
|||
/> |
|||
} |
|||
> |
|||
<div className="w-full max-w-3xl space-y-2 md:max-w-xl">Content</div> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -1,160 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useForm } from 'react-hook-form'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { Input } from '../components/form/Input'; |
|||
import { Select } from '../components/form/Select'; |
|||
import { Switch } from '../components/form/Switch'; |
|||
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; |
|||
import { connection } from '../core/connection'; |
|||
import { |
|||
setHostOverride, |
|||
setHostOverrideEnabled, |
|||
} from '../core/slices/meshtasticSlice'; |
|||
import { useAppDispatch, useAppSelector } from '../hooks/redux'; |
|||
|
|||
export const Settings = (): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
const dispatch = useAppDispatch(); |
|||
|
|||
const radioConfig = useAppSelector((state) => state.meshtastic.preferences); |
|||
const hostOverride = useAppSelector((state) => state.meshtastic.hostOverride); |
|||
const hostOverrideEnabled = useAppSelector( |
|||
(state) => state.meshtastic.hostOverrideEnabled, |
|||
); |
|||
|
|||
const { register, handleSubmit, control } = |
|||
useForm<Protobuf.RadioConfig_UserPreferences>({ |
|||
defaultValues: radioConfig, |
|||
}); |
|||
|
|||
const onSubmit = handleSubmit((data) => { |
|||
console.log(data); |
|||
connection.setPreferences(data); |
|||
}); |
|||
|
|||
const [localHostOverride, setLocalHostOverride] = |
|||
React.useState(hostOverride); |
|||
const [localHostOverrideEnabled, setLocalHostOverrideEnabled] = |
|||
React.useState(hostOverrideEnabled); |
|||
|
|||
return ( |
|||
<PrimaryTemplate title="Settings" tagline="Device"> |
|||
<form onSubmit={onSubmit}> |
|||
<div className="space-y-4"> |
|||
<div className="flex pb-2 dark:text-white border-b dark:border-gray-600"> |
|||
<div className="w-1/3 text-lg">WiFi</div> |
|||
<div className="space-y-2 w-full max-w-xs"> |
|||
<Input |
|||
label={t('strings.wifi_ssid')} |
|||
{...register('wifiSsid', {})} |
|||
type="text" |
|||
valid={true} |
|||
/> |
|||
<Input |
|||
label={t('strings.wifi_psk')} |
|||
{...register('wifiPassword', {})} |
|||
type="password" |
|||
valid={true} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div className="flex pb-2 dark:text-white border-b dark:border-gray-600"> |
|||
<div className="w-1/3 text-lg">Node</div> |
|||
<div className="space-y-2 w-full max-w-xs"> |
|||
<Switch |
|||
name={'isRouter'} |
|||
control={control} |
|||
label={'Is Router Node?'} |
|||
/> |
|||
|
|||
<Switch |
|||
name={'isLowPower'} |
|||
control={control} |
|||
label={'Is Low Power?'} |
|||
/> |
|||
|
|||
<Switch |
|||
name={'fixedPosition'} |
|||
control={control} |
|||
label={'Has Fixed Position?'} |
|||
/> |
|||
|
|||
<Switch |
|||
name={'serialDisabled'} |
|||
control={control} |
|||
label={'Is Serial Disabled?'} |
|||
/> |
|||
|
|||
<Switch |
|||
name={'mqttDisabled'} |
|||
control={control} |
|||
label={'Is MQTT Disabled?'} |
|||
/> |
|||
|
|||
<Switch |
|||
name={'debugLogEnabled'} |
|||
control={control} |
|||
label={'Is Debug Log Enabled?'} |
|||
/> |
|||
|
|||
<Select |
|||
name={'region'} |
|||
control={control} |
|||
label="Region" |
|||
options={(() => { |
|||
return Object.keys(Protobuf.RegionCode) |
|||
.filter((value) => isNaN(Number(value)) === false) |
|||
.map((key) => { |
|||
return { |
|||
value: key, |
|||
label: Protobuf.RegionCode[parseInt(key)], |
|||
}; |
|||
}); |
|||
})()} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div className="flex pb-2 dark:text-white border-b dark:border-gray-600"> |
|||
<div className="w-1/3 text-lg">Client</div> |
|||
<div className="space-y-2 w-full max-w-xs"> |
|||
{/* <Toggle |
|||
label={'Enable host override'} |
|||
checked={localHostOverrideEnabled} |
|||
onChange={(event) => { |
|||
console.log(event.target.checked); |
|||
setLocalHostOverrideEnabled(event.target.checked); |
|||
}} |
|||
/> */} |
|||
<Input |
|||
label={'Host override'} |
|||
placeholder={'meshtastic.local'} |
|||
value={localHostOverride} |
|||
onChange={(event) => { |
|||
setLocalHostOverride(event.target.value); |
|||
}} |
|||
type="text" |
|||
valid={true} |
|||
disabled={!localHostOverrideEnabled} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<button |
|||
type="submit" |
|||
onClick={() => { |
|||
dispatch(setHostOverride(localHostOverride)); |
|||
dispatch(setHostOverrideEnabled(localHostOverrideEnabled)); |
|||
}} |
|||
className="w-full rounded-md dark:bg-primaryDark shadow-md border dark:border-gray-600 p-2 mt-6 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900" |
|||
> |
|||
{t('strings.save_changes')} |
|||
</button> |
|||
</form> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -0,0 +1,69 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useForm } from 'react-hook-form'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { connection } from '@app/core/connection'; |
|||
import { useAppSelector } from '@app/hooks/redux'; |
|||
import { Button } from '@components/generic/Button'; |
|||
import { Input } from '@components/generic/Input'; |
|||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; |
|||
import { MenuIcon, SaveIcon } from '@heroicons/react/outline'; |
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
export interface DeviceProps { |
|||
navOpen: boolean; |
|||
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>; |
|||
} |
|||
|
|||
export const Device = ({ navOpen, setNavOpen }: DeviceProps): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
const radioConfig = useAppSelector((state) => state.meshtastic.preferences); |
|||
|
|||
const { register, handleSubmit, formState } = |
|||
useForm<Protobuf.RadioConfig_UserPreferences>({ |
|||
defaultValues: radioConfig, |
|||
}); |
|||
|
|||
const onSubmit = handleSubmit((data) => { |
|||
void connection.setPreferences(data); |
|||
}); |
|||
|
|||
return ( |
|||
<PrimaryTemplate |
|||
title="Device" |
|||
tagline="Settings" |
|||
button={ |
|||
<Button |
|||
icon={<MenuIcon className="w-5 h-5" />} |
|||
onClick={(): void => { |
|||
setNavOpen(!navOpen); |
|||
}} |
|||
circle |
|||
/> |
|||
} |
|||
footer={ |
|||
<Button |
|||
className="px-10 ml-auto" |
|||
icon={<SaveIcon className="w-5 h-5" />} |
|||
disabled={!formState.isDirty} |
|||
active |
|||
border |
|||
> |
|||
{t('strings.save_changes')} |
|||
</Button> |
|||
} |
|||
> |
|||
<div className="w-full max-w-3xl space-y-2 md:max-w-xl"> |
|||
<form onSubmit={onSubmit}> |
|||
<Input label={t('strings.wifi_ssid')} {...register('wifiSsid')} /> |
|||
<Input |
|||
type="password" |
|||
label={t('strings.wifi_psk')} |
|||
{...register('wifiPassword')} |
|||
/> |
|||
</form> |
|||
</div> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -0,0 +1,101 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useBreakpoint } from '@app/hooks/breakpoint'; |
|||
import { Button } from '@components/generic/Button'; |
|||
import { Drawer } from '@components/generic/Drawer'; |
|||
import { SidebarItem } from '@components/generic/SidebarItem'; |
|||
import { Tab } from '@headlessui/react'; |
|||
import { |
|||
CollectionIcon, |
|||
DeviceMobileIcon, |
|||
WifiIcon, |
|||
XCircleIcon, |
|||
} from '@heroicons/react/outline'; |
|||
|
|||
import { Device } from './Device'; |
|||
import { Interface } from './Interface'; |
|||
import { Radio } from './Radio'; |
|||
|
|||
export const Settings = (): JSX.Element => { |
|||
const [navOpen, setNavOpen] = React.useState(false); |
|||
|
|||
const { breakpoint } = useBreakpoint(); |
|||
|
|||
return ( |
|||
<Tab.Group> |
|||
<div className="relative flex w-full dark:text-white"> |
|||
<Drawer |
|||
open={breakpoint === 'sm' ? navOpen : true} |
|||
permenant={breakpoint !== 'sm'} |
|||
onClose={(): void => { |
|||
setNavOpen(!navOpen); |
|||
}} |
|||
> |
|||
<Tab.List className="flex flex-col border-b divide-y divide-gray-300 dark:divide-gray-600 dark:border-gray-600"> |
|||
<div className="flex items-center justify-between m-8 mr-6 md:my-10"> |
|||
<div className="text-4xl font-extrabold leading-none tracking-tight"> |
|||
Settings |
|||
</div> |
|||
<div className="md:hidden"> |
|||
<Button |
|||
icon={<XCircleIcon className="w-5 h-5" />} |
|||
circle |
|||
onClick={(): void => { |
|||
setNavOpen(false); |
|||
}} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<Tab |
|||
onClick={(): void => { |
|||
setNavOpen(false); |
|||
}} |
|||
> |
|||
{({ selected }): JSX.Element => ( |
|||
<SidebarItem |
|||
title="Device" |
|||
description="Device settings, such as device name and wifi settings" |
|||
selected={selected} |
|||
icon={<DeviceMobileIcon className="flex-shrink-0 w-6 h-6" />} |
|||
/> |
|||
)} |
|||
</Tab> |
|||
<Tab> |
|||
{({ selected }): JSX.Element => ( |
|||
<SidebarItem |
|||
title="Radio" |
|||
description="Adjust radio power and frequency settings" |
|||
selected={selected} |
|||
icon={<WifiIcon className="flex-shrink-0 w-6 h-6" />} |
|||
/> |
|||
)} |
|||
</Tab> |
|||
<Tab> |
|||
{({ selected }): JSX.Element => ( |
|||
<SidebarItem |
|||
title="Interface" |
|||
description="Change language and other UI settings" |
|||
selected={selected} |
|||
icon={<CollectionIcon className="flex-shrink-0 w-6 h-6" />} |
|||
/> |
|||
)} |
|||
</Tab> |
|||
</Tab.List> |
|||
</Drawer> |
|||
<div className="flex w-full"> |
|||
<Tab.Panels className="flex w-full"> |
|||
<Tab.Panel className="flex w-full"> |
|||
<Device navOpen={navOpen} setNavOpen={setNavOpen} /> |
|||
</Tab.Panel> |
|||
<Tab.Panel className="flex w-full"> |
|||
<Radio navOpen={navOpen} setNavOpen={setNavOpen} /> |
|||
</Tab.Panel> |
|||
<Tab.Panel className="flex w-full"> |
|||
<Interface navOpen={navOpen} setNavOpen={setNavOpen} /> |
|||
</Tab.Panel> |
|||
</Tab.Panels> |
|||
</div> |
|||
</div> |
|||
</Tab.Group> |
|||
); |
|||
}; |
|||
@ -0,0 +1,76 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Jp, Pt, Us } from 'react-flags-select'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { Select } from '@app/components/generic/Select'; |
|||
import i18n from '@app/core/translation'; |
|||
import { Button } from '@components/generic/Button'; |
|||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; |
|||
import { MenuIcon, SaveIcon } from '@heroicons/react/outline'; |
|||
|
|||
export interface InterfaceProps { |
|||
navOpen: boolean; |
|||
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>; |
|||
} |
|||
|
|||
export const Interface = ({ |
|||
navOpen, |
|||
setNavOpen, |
|||
}: InterfaceProps): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<PrimaryTemplate |
|||
title="Interface" |
|||
tagline="Settings" |
|||
button={ |
|||
<Button |
|||
icon={<MenuIcon className="w-5 h-5" />} |
|||
onClick={(): void => { |
|||
setNavOpen(!navOpen); |
|||
}} |
|||
circle |
|||
/> |
|||
} |
|||
footer={ |
|||
<Button |
|||
className="px-10 ml-auto" |
|||
icon={<SaveIcon className="w-5 h-5" />} |
|||
active |
|||
border |
|||
> |
|||
{t('strings.save_changes')} |
|||
</Button> |
|||
} |
|||
> |
|||
<div className="w-full max-w-3xl space-y-2 md:max-w-xl"> |
|||
<Select |
|||
label="Language" |
|||
value={i18n.language} |
|||
onChange={(value): void => { |
|||
void i18n.changeLanguage(value); |
|||
}} |
|||
id="aaa" |
|||
options={[ |
|||
{ |
|||
name: 'English', |
|||
value: 'en', |
|||
icon: <Us className="w-6" />, |
|||
}, |
|||
{ |
|||
name: 'Português', |
|||
value: 'pt', |
|||
icon: <Pt className="w-6" />, |
|||
}, |
|||
{ |
|||
name: 'Japanese', |
|||
value: 'jp', |
|||
icon: <Jp className="w-6" />, |
|||
}, |
|||
]} |
|||
/> |
|||
</div> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -0,0 +1,47 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { Button } from '@components/generic/Button'; |
|||
import { Input } from '@components/generic/Input'; |
|||
import { PrimaryTemplate } from '@components/templates/PrimaryTemplate'; |
|||
import { MenuIcon, SaveIcon } from '@heroicons/react/outline'; |
|||
|
|||
export interface RadioProps { |
|||
navOpen: boolean; |
|||
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>; |
|||
} |
|||
|
|||
export const Radio = ({ navOpen, setNavOpen }: RadioProps): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<PrimaryTemplate |
|||
title="Radio" |
|||
tagline="Settings" |
|||
button={ |
|||
<Button |
|||
icon={<MenuIcon className="w-5 h-5" />} |
|||
onClick={(): void => { |
|||
setNavOpen(!navOpen); |
|||
}} |
|||
circle |
|||
/> |
|||
} |
|||
footer={ |
|||
<Button |
|||
className="px-10 ml-auto" |
|||
icon={<SaveIcon className="w-5 h-5" />} |
|||
active |
|||
border |
|||
> |
|||
{t('strings.save_changes')} |
|||
</Button> |
|||
} |
|||
> |
|||
<div className="w-full max-w-3xl space-y-2 md:max-w-xl"> |
|||
<Input label="test" /> |
|||
</div> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue