WIP
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -19,9 +19,13 @@ export const Message = ({
|
||||
}: MessageProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={`flex space-x-2 ${!isSender && 'ml-auto flex-row-reverse'}`}
|
||||
className={`flex space-x-2 ${
|
||||
!isSender ? 'ml-auto flex-row-reverse' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={`shadow-md rounded-full mt-auto ${!isSender && 'ml-2'}`}>
|
||||
<div
|
||||
className={`shadow-md rounded-full mt-auto ${!isSender ? 'ml-2' : ''}`}
|
||||
>
|
||||
<Avatar
|
||||
size={30}
|
||||
name={senderName ?? 'UNK'}
|
||||
@@ -37,11 +41,11 @@ export const Message = ({
|
||||
: 'bg-primary text-blue-50 rounded-bl-lg'
|
||||
} ${ack ? 'animate-none' : 'animate-pulse'}`}
|
||||
>
|
||||
<div className="min-w-4 leading-5">{message}</div>
|
||||
<div className="leading-5 min-w-4">{message}</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">{senderName}</div>
|
||||
</div>
|
||||
<div className="mt-auto mb-4 text-xs font-medium text-secondary mr-3 dark:text-gray-200">
|
||||
<div className="mt-auto mb-4 mr-3 text-xs font-medium text-secondary dark:text-gray-200">
|
||||
{rxTime.toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
|
||||
@@ -2,59 +2,57 @@ import React from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/redux';
|
||||
import { Button } from '@components/generic/Button';
|
||||
import { Input } from '@components/generic/Input';
|
||||
import { connection } from '@core/connection';
|
||||
import {
|
||||
EmojiHappyIcon,
|
||||
PaperAirplaneIcon,
|
||||
PaperClipIcon,
|
||||
} from '@heroicons/react/outline';
|
||||
|
||||
import { connection } from '../../core/connection';
|
||||
import { useAppSelector } from '../../hooks/redux';
|
||||
import { IconButton } from '../generic/IconButton';
|
||||
|
||||
export const MessageBar = (): JSX.Element => {
|
||||
const ready = useAppSelector((state) => state.meshtastic.ready);
|
||||
const [currentMessage, setCurrentMessage] = React.useState('');
|
||||
const sendMessage = () => {
|
||||
const sendMessage = (): void => {
|
||||
if (ready) {
|
||||
connection.sendText(currentMessage, undefined, true);
|
||||
void connection.sendText(currentMessage, undefined, true);
|
||||
setCurrentMessage('');
|
||||
}
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
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">
|
||||
<IconButton>
|
||||
<EmojiHappyIcon className="w-6 h-6" />
|
||||
</IconButton>
|
||||
|
||||
<IconButton>
|
||||
<PaperClipIcon className="w-6 h-6" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<form
|
||||
className="flex w-full space-x-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
minLength={2}
|
||||
placeholder={`${t('placeholder.message')}...`}
|
||||
disabled={!ready}
|
||||
value={currentMessage}
|
||||
onChange={(e) => {
|
||||
setCurrentMessage(e.target.value);
|
||||
<div className="flex w-full p-4 mx-auto space-x-2 text-gray-500 bg-gray-50 dark:bg-transparent dark:text-gray-400">
|
||||
<div className="flex w-full max-w-4xl">
|
||||
<div className="flex">
|
||||
<Button icon={<EmojiHappyIcon className="w-5 h-5" />} circle />
|
||||
<Button icon={<PaperClipIcon className="w-5 h-5" />} circle />
|
||||
</div>
|
||||
<form
|
||||
className="flex w-full space-x-2"
|
||||
onSubmit={(e): void => {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}}
|
||||
className="focus:outline-none h-10 w-full resize-none rounded-full border border-gray-300 dark:bg-gray-900 px-4"
|
||||
/>
|
||||
<IconButton type="submit">
|
||||
<PaperAirplaneIcon className="w-6 h-6 rotate-90" />
|
||||
</IconButton>
|
||||
</form>
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
minLength={2}
|
||||
placeholder={`${t('placeholder.message')}...`}
|
||||
disabled={!ready}
|
||||
value={currentMessage}
|
||||
onChange={(e): void => {
|
||||
setCurrentMessage(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<PaperAirplaneIcon className="w-5 h-5" />}
|
||||
type="submit"
|
||||
circle
|
||||
/>
|
||||
</form>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -3,8 +3,16 @@ import React from 'react';
|
||||
export const Logo = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<img className="w-16 dark:hidden" src="Mesh_Logo_Black.svg" />
|
||||
<img className="hidden dark:flex w-16" src="Mesh_Logo_White.svg" />
|
||||
<img
|
||||
title="Logo"
|
||||
className="w-16 dark:hidden"
|
||||
src="Mesh_Logo_Black.svg"
|
||||
/>
|
||||
<img
|
||||
title="Logo"
|
||||
className="hidden w-16 dark:flex"
|
||||
src="Mesh_Logo_White.svg"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,18 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
AnnotationIcon,
|
||||
CogIcon,
|
||||
InformationCircleIcon,
|
||||
ViewGridIcon,
|
||||
} from '@heroicons/react/outline';
|
||||
import SwipeableDrawer from '@material-ui/core/SwipeableDrawer/SwipeableDrawer';
|
||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||
import { Drawer } from '@components/generic/Drawer';
|
||||
import { closeMobileNav } from '@core/slices/appSlice';
|
||||
|
||||
import { routes } from '../../core/router';
|
||||
import { closeMobileNav, openMobileNav } from '../../core/slices/appSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks/redux';
|
||||
import { Button } from '../generic/Button';
|
||||
import { Logo } from './Logo';
|
||||
import { Navigation } from './Navigation';
|
||||
|
||||
export const MobileNav = (): JSX.Element => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -20,41 +13,22 @@ export const MobileNav = (): JSX.Element => {
|
||||
const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen);
|
||||
|
||||
return (
|
||||
<SwipeableDrawer
|
||||
<Drawer
|
||||
open={mobileNavOpen}
|
||||
anchor="left"
|
||||
onClose={() => {
|
||||
onClose={(): void => {
|
||||
dispatch(closeMobileNav());
|
||||
}}
|
||||
onOpen={() => {
|
||||
dispatch(openMobileNav());
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col dark:bg-secondaryDark h-full">
|
||||
<div className="flex flex-col w-64">
|
||||
<div className="m-auto my-6">
|
||||
<Logo />
|
||||
</div>
|
||||
<Button
|
||||
icon={<AnnotationIcon />}
|
||||
text={'Messages'}
|
||||
{...routes.messages().link}
|
||||
/>
|
||||
<Button
|
||||
icon={<ViewGridIcon />}
|
||||
text={'Nodes'}
|
||||
{...routes.nodes().link}
|
||||
/>
|
||||
<Button
|
||||
icon={<CogIcon />}
|
||||
text={'Settings'}
|
||||
{...routes.settings().link}
|
||||
/>
|
||||
<Button
|
||||
icon={<InformationCircleIcon />}
|
||||
text={'About'}
|
||||
{...routes.about().link}
|
||||
<Navigation
|
||||
onClick={(): void => {
|
||||
dispatch(closeMobileNav());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SwipeableDrawer>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@components/generic/Button';
|
||||
import { routes, useRoute } from '@core/router';
|
||||
import {
|
||||
AnnotationIcon,
|
||||
CogIcon,
|
||||
@@ -7,33 +9,60 @@ import {
|
||||
ViewGridIcon,
|
||||
} from '@heroicons/react/outline';
|
||||
|
||||
import { routes } from '../../core/router';
|
||||
import { Button } from '../generic/Button';
|
||||
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||
|
||||
export const Navigation = (): JSX.Element => {
|
||||
export type NavigationProps = DefaultDivProps;
|
||||
|
||||
export const Navigation = ({
|
||||
onClick,
|
||||
className,
|
||||
...props
|
||||
}: NavigationProps): JSX.Element => {
|
||||
const route = useRoute();
|
||||
return (
|
||||
<div className="hidden md:flex flex-auto flex-0 relative items-center h-16 px-4 ">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`h-16 px-4 md:space-x-2 space-y-2 md:space-y-0 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
<div onClick={onClick}>
|
||||
<Button
|
||||
icon={<AnnotationIcon />}
|
||||
text={'Messages'}
|
||||
icon={<AnnotationIcon className="w-6 h-6" />}
|
||||
active={route.name === 'messages'}
|
||||
className="w-full md:w-auto"
|
||||
{...routes.messages().link}
|
||||
/>
|
||||
>
|
||||
Messages
|
||||
</Button>
|
||||
</div>
|
||||
<div onClick={onClick}>
|
||||
<Button
|
||||
icon={<ViewGridIcon />}
|
||||
text={'Nodes'}
|
||||
icon={<ViewGridIcon className="w-6 h-6" />}
|
||||
className="w-full md:w-auto"
|
||||
active={route.name === 'nodes'}
|
||||
{...routes.nodes().link}
|
||||
/>
|
||||
>
|
||||
Nodes
|
||||
</Button>
|
||||
</div>
|
||||
<div onClick={onClick}>
|
||||
<Button
|
||||
icon={<CogIcon />}
|
||||
text={'Settings'}
|
||||
icon={<CogIcon className="w-6 h-6" />}
|
||||
className="w-full md:w-auto"
|
||||
active={route.name === 'settings'}
|
||||
{...routes.settings().link}
|
||||
/>
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
<div onClick={onClick}>
|
||||
<Button
|
||||
icon={<InformationCircleIcon />}
|
||||
text={'About'}
|
||||
icon={<InformationCircleIcon className="w-6 h-6" />}
|
||||
className="w-full md:w-auto"
|
||||
active={route.name === 'about'}
|
||||
{...routes.about().link}
|
||||
/>
|
||||
>
|
||||
About
|
||||
</Button>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -4,16 +4,21 @@ export interface PrimaryTemplateProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
tagline: string;
|
||||
button?: JSX.Element;
|
||||
footer?: JSX.Element;
|
||||
}
|
||||
|
||||
export const PrimaryTemplate = ({
|
||||
children,
|
||||
title,
|
||||
tagline,
|
||||
button,
|
||||
footer,
|
||||
}: PrimaryTemplateProps): JSX.Element => {
|
||||
return (
|
||||
<div className="flex flex-col flex-auto min-w-0">
|
||||
<div className="flex flex-col sm:flex-row flex-0 sm:items-center sm:justify-between p-6 sm:py-8 sm:px-10 border-b dark:border-gray-600 bg-white dark:bg-secondaryDark">
|
||||
<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">
|
||||
{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">
|
||||
<div>
|
||||
@@ -21,13 +26,20 @@ export const PrimaryTemplate = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight leading-7 sm:leading-10 truncate dark:text-white">
|
||||
<h2 className="text-3xl font-extrabold leading-7 tracking-tight truncate md:text-4xl md:leading-10 dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-auto p-6 sm:p-10 ">{children}</div>
|
||||
<div className="flex-auto flex-grow p-6 md:p-10">{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">
|
||||
{button && <div className="pr-2 m-auto md:hidden">{button}</div>}
|
||||
<div className="flex-1 min-w-0">{footer}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user