diff --git a/src/components/Channel.tsx b/src/components/Channel.tsx new file mode 100644 index 00000000..7e4ec634 --- /dev/null +++ b/src/components/Channel.tsx @@ -0,0 +1,127 @@ +import React from 'react'; + +import { useForm } from 'react-hook-form'; +import { FiEdit3, FiSave } from 'react-icons/fi'; + +import { Protobuf } from '@meshtastic/meshtasticjs'; + +import { connection } from '../core/connection.js'; +import { EnumSelect } from './generic/form/EnumSelect.jsx'; +import { Input } from './generic/form/Input.jsx'; +import { IconButton } from './generic/IconButton.jsx'; + +export interface ChannelProps { + channel: Protobuf.Channel; +} +interface DotProps { + role: Protobuf.Channel_Role; + admin: boolean; +} +const Dot = ({ role, admin }: DotProps): JSX.Element => ( +
+); + +export const Channel = ({ channel }: ChannelProps): JSX.Element => { + const [edit, setEdit] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + const { register, handleSubmit, formState } = useForm<{ + role: Protobuf.Channel_Role; + settings: { + name: string; + }; + }>({ + defaultValues: { + role: channel.role, + settings: { + name: channel.settings?.name, + }, + }, + }); + + const onSubmit = handleSubmit(async (data) => { + setLoading(true); + const adminChannel = Protobuf.Channel.create({ + role: data.role, + index: channel.index, + settings: data.settings, + }); + + await connection.setChannel(adminChannel, (): Promise => { + setLoading(false); + return Promise.resolve(); + }); + }); + + return ( +
+ {edit ? ( + <> + {loading && ( +
+
+ Loading +
+
+ )} +
+
+
+ + +
+ +
+
+ => { + await onSubmit(); + + setEdit(false); + }} + icon={} + /> + + ) : ( + <> +
+ +
+ {channel.settings?.name.length + ? channel.settings.name + : channel.role === Protobuf.Channel_Role.PRIMARY + ? 'Primary' + : `Channel: ${channel.index}`} +
+
+ { + setEdit(true); + }} + icon={} + /> + + )} +
+ ); +}; diff --git a/src/components/chat/MessageBar.tsx b/src/components/chat/MessageBar.tsx index be28adfe..6d4e515f 100644 --- a/src/components/chat/MessageBar.tsx +++ b/src/components/chat/MessageBar.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { FiPaperclip, FiSend, FiSmile } from 'react-icons/fi'; +import { FiSend } from 'react-icons/fi'; import { ackMessage } from '@app/core/slices/meshtasticSlice.js'; import { useAppDispatch, useAppSelector } from '@app/hooks/redux'; -import { Button } from '@components/generic/Button'; -import { Input } from '@components/generic/Input'; +import { Input } from '@components/generic/form/Input'; import { connection } from '@core/connection'; +import { IconButton } from '../generic/IconButton.jsx'; + export const MessageBar = (): JSX.Element => { const dispatch = useAppDispatch(); const ready = useAppSelector((state) => state.meshtastic.ready); @@ -27,10 +28,6 @@ export const MessageBar = (): JSX.Element => { return (
-
-
{ @@ -48,7 +45,7 @@ export const MessageBar = (): JSX.Element => { setCurrentMessage(e.target.value); }} /> -
diff --git a/src/components/generic/Blur.tsx b/src/components/generic/Blur.tsx index 0d2d7427..9e654473 100644 --- a/src/components/generic/Blur.tsx +++ b/src/components/generic/Blur.tsx @@ -14,7 +14,7 @@ export const Blur = ({ }: BlurProps): JSX.Element => { return (
); }; + +// import React from 'react'; + +// type DefaultButtonProps = JSX.IntrinsicElements['button']; + +// export interface ButtonProps extends DefaultButtonProps { +// icon?: JSX.Element; +// circle?: boolean; +// active?: boolean; +// border?: boolean; +// confirmAction?: () => void; +// rightIcon?: React.ReactNode; +// leftIcon?: React.ReactNode; +// nested?: boolean; +// } + +// export const Button = ({ +// rightIcon, +// leftIcon, +// children, +// nested, +// ...props +// }: ButtonProps): JSX.Element => { +// return ( +// +// ); +// }; diff --git a/src/components/generic/Card.tsx b/src/components/generic/Card.tsx index cab5b4ff..acb8dc4b 100644 --- a/src/components/generic/Card.tsx +++ b/src/components/generic/Card.tsx @@ -4,7 +4,7 @@ type DefaultDivProps = JSX.IntrinsicElements['div']; interface CardProps extends DefaultDivProps { title: string; - description: string; + description: string | JSX.Element; buttons?: JSX.Element; lgPlaceholder?: JSX.Element; } @@ -20,7 +20,7 @@ export const Card = ({ }: CardProps): JSX.Element => { return (
diff --git a/src/components/generic/IconButton.tsx b/src/components/generic/IconButton.tsx new file mode 100644 index 00000000..f747b067 --- /dev/null +++ b/src/components/generic/IconButton.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { FiCheck } from 'react-icons/fi'; + +type DefaulButtonProps = JSX.IntrinsicElements['button']; + +export interface IconButtonProps extends DefaulButtonProps { + icon: React.ReactNode; + confirmAction?: () => void; +} + +export const IconButton = ({ + icon, + confirmAction, + ...props +}: IconButtonProps): JSX.Element => { + const [hasConfirmed, setHasConfirmed] = React.useState(false); + + const handleConfirm = (): void => { + if (confirmAction) { + if (hasConfirmed) { + void confirmAction(); + } + setHasConfirmed(true); + setTimeout(() => { + setHasConfirmed(false); + }, 3000); + } + }; + return ( +
+ +
+ ); +}; diff --git a/src/components/generic/Input.tsx b/src/components/generic/Input.tsx deleted file mode 100644 index d2ad6c07..00000000 --- a/src/components/generic/Input.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; - -type DefaultInputProps = JSX.IntrinsicElements['input']; - -interface InputProps extends DefaultInputProps { - icon?: JSX.Element; - label?: string; - valid?: boolean; - validationMessage?: string; -} - -export const Input = React.forwardRef( - function Input( - { - icon, - label, - valid, - validationMessage, - id, - disabled, - ...props - }: InputProps, - ref, - ) { - return ( -
- -
- {icon && ( -
- {React.cloneElement(icon, { - className: 'w-5 h-5 text-gray-500 dark:text-gray-600', - })} -
- )} - -
- {!valid && ( -
{validationMessage}
- )} -
- ); - }, -); diff --git a/src/components/generic/Toggle.tsx b/src/components/generic/Toggle.tsx index fef5645d..240585a6 100644 --- a/src/components/generic/Toggle.tsx +++ b/src/components/generic/Toggle.tsx @@ -2,11 +2,13 @@ import React from 'react'; import { Switch } from '@headlessui/react'; +import { Label } from './form/Label.jsx'; + type DefaultButtonProps = JSX.IntrinsicElements['button']; interface ToggleProps extends DefaultButtonProps { action?: (enabled: boolean) => void; - label?: string; + label: string; valid?: boolean; validationMessage?: string; checked?: boolean; @@ -37,12 +39,13 @@ export const Toggle = ({ return (
- */}
( + ({ options, optionsEnum, label, error, small, ...props }, ref) => { + const optionsEnumValues = optionsEnum + ? Object.entries(optionsEnum).filter( + (value) => typeof value[1] === 'number', + ) + : []; + return ( +
+ {label &&
+ ); + }, +); diff --git a/src/components/generic/form/Input.tsx b/src/components/generic/form/Input.tsx new file mode 100644 index 00000000..5152a924 --- /dev/null +++ b/src/components/generic/form/Input.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { InputWrapper } from './InputWrapper.jsx'; +import { Label } from './Label.jsx'; + +type DefaultInputProps = JSX.IntrinsicElements['input']; + +interface InputProps extends DefaultInputProps { + label?: string; + error?: string; + action?: JSX.Element; +} + +export const Input = React.forwardRef( + function Input({ label, error, action, ...props }: InputProps, ref) { + return ( +
+ {label &&
+ ); + }, +); diff --git a/src/components/generic/form/InputWrapper.tsx b/src/components/generic/form/InputWrapper.tsx new file mode 100644 index 00000000..b43c3ba0 --- /dev/null +++ b/src/components/generic/form/InputWrapper.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +export interface LabelProps { + error?: string; + disabled?: boolean; + children: React.ReactNode; +} + +export const InputWrapper = ({ + error, + disabled, + children, +}: LabelProps): JSX.Element => ( +
+ {children} +
+); diff --git a/src/components/generic/form/Label.tsx b/src/components/generic/form/Label.tsx new file mode 100644 index 00000000..dd0ef22d --- /dev/null +++ b/src/components/generic/form/Label.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export interface LabelProps { + label: string; + error?: string; +} + +export const Label = ({ label, error }: LabelProps): JSX.Element => ( + +); diff --git a/src/components/menu/buttons/DeviceStatusDropdown.tsx b/src/components/menu/buttons/DeviceStatusDropdown.tsx index 6ce10f32..e24d0fea 100644 --- a/src/components/menu/buttons/DeviceStatusDropdown.tsx +++ b/src/components/menu/buttons/DeviceStatusDropdown.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { FiWifi, FiWifiOff } from 'react-icons/fi'; +import { IconButton } from '@app/components/generic/IconButton.jsx'; import { useAppSelector } from '@app/hooks/redux'; -import { Button } from '@components/generic/Button'; import { Types } from '@meshtastic/meshtasticjs'; export const DeviceStatusDropdown = (): JSX.Element => { @@ -11,8 +11,8 @@ export const DeviceStatusDropdown = (): JSX.Element => { const ready = useAppSelector((state) => state.meshtastic.ready); return ( -
-
+
+
{ }`} >
{Types.DeviceStatusEnum[deviceStatus]}
-
diff --git a/src/components/menu/buttons/MobileNavToggle.tsx b/src/components/menu/buttons/MobileNavToggle.tsx index 19f5fe9d..6b63a71c 100644 --- a/src/components/menu/buttons/MobileNavToggle.tsx +++ b/src/components/menu/buttons/MobileNavToggle.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FiMenu } from 'react-icons/fi'; -import { Button } from '@components/generic/Button'; +import { IconButton } from '@app/components/generic/IconButton.jsx'; import { openMobileNav } from '@core/slices/appSlice'; import { useAppDispatch } from '../../../hooks/redux'; @@ -12,12 +12,11 @@ export const MobileNavToggle = (): JSX.Element => { return (
-
); diff --git a/src/components/menu/buttons/ThemeToggle.tsx b/src/components/menu/buttons/ThemeToggle.tsx index 299617ad..164c7060 100644 --- a/src/components/menu/buttons/ThemeToggle.tsx +++ b/src/components/menu/buttons/ThemeToggle.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { FiMoon, FiSun } from 'react-icons/fi'; +import { IconButton } from '@app/components/generic/IconButton.jsx'; import { useAppDispatch, useAppSelector } from '@app/hooks/redux'; -import { Button } from '@components/generic/Button'; import { setDarkModeEnabled } from '@core/slices/appSlice'; export const ThemeToggle = (): JSX.Element => { @@ -11,7 +11,7 @@ export const ThemeToggle = (): JSX.Element => { const darkMode = useAppSelector((state) => state.app.darkMode); return ( -
diff --git a/src/pages/Nodes/Index.tsx b/src/pages/Nodes/Index.tsx index b1b950d1..e5d3916e 100644 --- a/src/pages/Nodes/Index.tsx +++ b/src/pages/Nodes/Index.tsx @@ -3,9 +3,9 @@ import React from 'react'; import Avatar from 'boring-avatars'; import { FiXCircle } from 'react-icons/fi'; +import { IconButton } from '@app/components/generic/IconButton.jsx'; 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'; @@ -36,9 +36,8 @@ export const Nodes = (): JSX.Element => { Nodes
- - } + buttons={} />} className="md:w-1/3" > {data ? ( @@ -92,34 +87,37 @@ export const Files = ({ navOpen, setNavOpen }: RangeTestProps): JSX.Element => { {data.data.files.map((file: IFile) => (
-
- + - - {file.nameModified ?? file.name} - -
))} diff --git a/src/pages/Plugins/Index.tsx b/src/pages/Plugins/Index.tsx index f9519fb1..fefb35cf 100644 --- a/src/pages/Plugins/Index.tsx +++ b/src/pages/Plugins/Index.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { FiFileText, FiRss, FiXCircle } from 'react-icons/fi'; +import { IconButton } from '@app/components/generic/IconButton.jsx'; 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'; @@ -32,7 +32,7 @@ export const Plugins = (): JSX.Element => { Plugins
- } >
+
+  - Primary +
+  - Secondary +
+  - Disabled +
+  - Admin +
+ } > -
-
- - -
- - -
-
-
- - -
+
{channels.map((channel) => ( -
-
- {Protobuf.Channel_Role[channel.role]} - {channel.settings?.name} -
-
+ ))} + +
+
+ Please ensure any changes are working before confirming +
+ +
diff --git a/src/pages/settings/Connection.tsx b/src/pages/settings/Connection.tsx index 632c66f3..bb739eed 100644 --- a/src/pages/settings/Connection.tsx +++ b/src/pages/settings/Connection.tsx @@ -5,7 +5,8 @@ import { useTranslation } from 'react-i18next'; import { FiLink2, FiMenu, FiSave } from 'react-icons/fi'; import { Card } from '@app/components/generic/Card'; -import { Input } from '@app/components/generic/Input'; +import { Input } from '@app/components/generic/form/Input'; +import { IconButton } from '@app/components/generic/IconButton.jsx'; import { Tabs } from '@app/components/generic/Tabs'; import { Toggle } from '@app/components/generic/Toggle'; import { bleConnection, serialConnection } from '@app/core/connection'; @@ -39,12 +40,11 @@ export const Connection = ({ title="Connection" tagline="Settings" button={ -
- +
+ } + disabled={formState.isDirty} + onClick={(): void => { + reset(); + }} + /> + +
} > { >
+
WiFi
{ +
Position
+ + + + + + +
Other
- {Protobuf.PositionFlags[radioConfig.positionFlags]}