55 changed files with 841 additions and 1235 deletions
Binary file not shown.
Binary file not shown.
@ -1,17 +0,0 @@ |
|||
@font-face { |
|||
font-family: 'Inter var'; |
|||
font-weight: 100 900; |
|||
font-display: swap; |
|||
font-style: normal; |
|||
font-named-instance: 'Regular'; |
|||
src: url("Inter-roman.var.woff2?v=3.18") format("woff2"); |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Inter var'; |
|||
font-weight: 100 900; |
|||
font-display: swap; |
|||
font-style: italic; |
|||
font-named-instance: 'Italic'; |
|||
src: url("Inter-italic.var.woff2?v=3.18") format("woff2"); |
|||
} |
|||
@ -1,63 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import type { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { ChatMessage } from './components/ChatMessage'; |
|||
import { MessageBox } from './components/MessageBox'; |
|||
import { Sidebar } from './components/Sidebar'; |
|||
import { connection } from './connection'; |
|||
|
|||
export const Main = (): JSX.Element => { |
|||
const [messages, setMessages] = React.useState< |
|||
{ message: Types.TextPacket; ack: boolean }[] |
|||
>([]); |
|||
const { t } = useTranslation(); |
|||
|
|||
React.useEffect(() => { |
|||
connection.onTextPacket.subscribe((message) => { |
|||
setMessages((messages) => [ |
|||
...messages, |
|||
{ message: message, ack: false }, |
|||
]); |
|||
}); |
|||
}, []); |
|||
|
|||
React.useEffect(() => { |
|||
connection.onRoutingPacket.subscribe((routingPacket) => { |
|||
setMessages( |
|||
messages.map((message) => { |
|||
return routingPacket.packet.payloadVariant.oneofKind === 'decoded' && |
|||
message.message.packet.id === |
|||
routingPacket.packet.payloadVariant.decoded.requestId |
|||
? { |
|||
ack: true, |
|||
message: message.message, |
|||
} |
|||
: message; |
|||
}), |
|||
); |
|||
}); |
|||
}, [messages]); |
|||
|
|||
return ( |
|||
<div className="flex flex-col md:flex-row flex-grow m-3 space-y-2 md:space-y-0 space-x-0 md:space-x-2"> |
|||
<div className="flex flex-col flex-grow container mx-auto"> |
|||
<div className="flex flex-col flex-grow py-6 space-y-2"> |
|||
{messages.length ? ( |
|||
messages.map((message, Main) => ( |
|||
<ChatMessage key={Main} message={message} /> |
|||
)) |
|||
) : ( |
|||
<div className="m-auto text-2xl text-gray-500"> |
|||
{t('placeholder.no_messages')} |
|||
</div> |
|||
)} |
|||
</div> |
|||
<MessageBox /> |
|||
</div> |
|||
<Sidebar /> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,73 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import Avatar from 'boring-avatars'; |
|||
|
|||
import { |
|||
CheckCircleIcon, |
|||
DotsCircleHorizontalIcon, |
|||
} from '@heroicons/react/outline'; |
|||
import type { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { useAppSelector } from '../hooks/redux'; |
|||
|
|||
interface ChatMessageProps { |
|||
message: { message: Types.TextPacket; ack: boolean }; |
|||
} |
|||
|
|||
export const ChatMessage = (props: ChatMessageProps): JSX.Element => { |
|||
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo); |
|||
const nodes = useAppSelector((state) => state.meshtastic.nodes); |
|||
|
|||
const node = nodes.find((node) => { |
|||
return node.num === props.message.message.packet.from; |
|||
}); |
|||
|
|||
return ( |
|||
<div className="flex items-end"> |
|||
<Avatar |
|||
size={40} |
|||
name={node?.user?.longName ?? 'UNK'} |
|||
variant="beam" |
|||
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']} |
|||
/> |
|||
<div className="flex flex-col container px-2 items-start"> |
|||
<React.Suspense |
|||
fallback={ |
|||
<div className="flex border-b border-gray-300"> |
|||
<div className="m-auto p-3 text-gray-500">Loading</div> |
|||
</div> |
|||
} |
|||
> |
|||
<div |
|||
className={`px-4 py-2 rounded-3xl shadow-md ${ |
|||
props.message.message.packet.from !== myNodeInfo.myNodeNum |
|||
? 'bg-gray-300' |
|||
: 'bg-green-200' |
|||
}`}
|
|||
> |
|||
<div className="flex text-xs text-gray-500 space-x-1"> |
|||
<div className="font-medium">{node?.user?.longName ?? 'UNK'}</div> |
|||
<p>-</p> |
|||
<div className="underline"> |
|||
{new Date( |
|||
props.message.message.packet.rxTime > 0 |
|||
? props.message.message.packet.rxTime |
|||
: Date.now(), |
|||
).toLocaleString()} |
|||
</div> |
|||
</div> |
|||
<div className="flex justify-between text-gray-600"> |
|||
<span className="inline-block">{props.message.message.data}</span> |
|||
{node?.num === myNodeInfo.myNodeNum && |
|||
(props.message.ack ? ( |
|||
<CheckCircleIcon className="my-auto w-5 h-5" /> |
|||
) : ( |
|||
<DotsCircleHorizontalIcon className="my-auto animate-pulse w-5 h-5" /> |
|||
))} |
|||
</div> |
|||
</div> |
|||
</React.Suspense> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,64 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { |
|||
DeviceMobileIcon, |
|||
StatusOfflineIcon, |
|||
StatusOnlineIcon, |
|||
} from '@heroicons/react/outline'; |
|||
import { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { useAppSelector } from '../hooks/redux'; |
|||
import { Logo } from './Logo'; |
|||
|
|||
export const Header = (): JSX.Element => { |
|||
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus); |
|||
const ready = useAppSelector((state) => state.meshtastic.ready); |
|||
const lastMeshInterraction = useAppSelector( |
|||
(state) => state.meshtastic.lastMeshInterraction, |
|||
); |
|||
|
|||
return ( |
|||
<nav className="select-none w-full shadow-md"> |
|||
<div className="flex w-full container mx-auto justify-between px-6 py-4"> |
|||
<Logo /> |
|||
<div></div> |
|||
|
|||
<div className="flex space-x-2 items-center"> |
|||
<div className="flex"> |
|||
<div |
|||
className={`w-5 h-5 rounded-full ${ |
|||
new Date(lastMeshInterraction) < new Date(Date.now() - 40000) |
|||
? 'bg-red-400 animate-pulse' |
|||
: new Date(lastMeshInterraction) < |
|||
new Date(Date.now() - 20000) |
|||
? 'bg-yellow-400 animate-pulse' |
|||
: 'bg-green-400' |
|||
}`}
|
|||
></div> |
|||
{new Date(lastMeshInterraction) > new Date(Date.now() - 40000) ? ( |
|||
<StatusOnlineIcon className="m-auto ml-1 h-5 w-5" /> |
|||
) : ( |
|||
<StatusOfflineIcon className="m-auto ml-1 h-5 w-5" /> |
|||
)} |
|||
</div> |
|||
|
|||
<div className="flex"> |
|||
<div |
|||
className={`w-5 h-5 rounded-full ${ |
|||
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> |
|||
<DeviceMobileIcon className="m-auto ml-1 w-5 h-5" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
); |
|||
}; |
|||
@ -1,84 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
export const Logo = (): JSX.Element => { |
|||
return ( |
|||
<svg |
|||
height="30" |
|||
width="200" |
|||
viewBox="0 0 1115 116" |
|||
version="1.1" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
style={{ |
|||
fillRule: 'evenodd', |
|||
clipRule: 'evenodd', |
|||
strokeLinejoin: 'round', |
|||
strokeMiterlimit: 1.5, |
|||
}} |
|||
> |
|||
<g transform="matrix(1.05081,0,0,0.602459,-74.3378,-345.171)"> |
|||
<g> |
|||
<g transform="matrix(0.973838,0,0,1.69858,-1.56777,-229.112)"> |
|||
<path |
|||
d="M81.582,577.266L148.536,478.804" |
|||
style={{ fill: 'none', stroke: 'black', strokeWidth: '14.11px' }} |
|||
/> |
|||
</g> |
|||
<g transform="matrix(0.961342,0,0,1.67678,882.45,-216.54)"> |
|||
<path |
|||
d="M81.582,577.266L148.536,478.804" |
|||
style={{ fill: 'none', stroke: 'black', strokeWidth: '14.86px' }} |
|||
/> |
|||
</g> |
|||
<g transform="matrix(12.0448,0,0,21.0595,-7445.39,-7644.88)"> |
|||
<text |
|||
x="640.988px" |
|||
y="399.072px" |
|||
style={{ |
|||
fontFamily: 'ArialMT, Arial, sans-serif', |
|||
fontSize: '12px', |
|||
}} |
|||
> |
|||
ESHT |
|||
</text> |
|||
</g> |
|||
<g transform="matrix(0.977299,0,0,1.70462,-43.6432,50.5292)"> |
|||
<path |
|||
d="M187.032,410.85L250.896,317.192L314.907,410.702" |
|||
style={{ fill: 'none', stroke: 'black', strokeWidth: '13.85px' }} |
|||
/> |
|||
</g> |
|||
<g transform="matrix(0.977299,0,0,1.70462,468.182,53.0697)"> |
|||
<path |
|||
d="M187.032,410.85L250.896,317.192L314.907,410.702" |
|||
style={{ fill: 'none', stroke: 'black', strokeWidth: '13.85px' }} |
|||
/> |
|||
</g> |
|||
<g transform="matrix(0.571939,0,0,1,784.482,759.924)"> |
|||
<text |
|||
x="0px" |
|||
y="0px" |
|||
style={{ |
|||
fontFamily: 'ArialMT, Arial, sans-serif', |
|||
fontSize: '252.715px', |
|||
}} |
|||
> |
|||
ST |
|||
</text> |
|||
</g> |
|||
<g transform="matrix(0.571939,0,0,1,1030.51,760.498)"> |
|||
<text |
|||
x="0px" |
|||
y="0px" |
|||
style={{ |
|||
fontFamily: 'ArialMT, Arial, sans-serif', |
|||
fontSize: '252.715px', |
|||
}} |
|||
> |
|||
C |
|||
</text> |
|||
</g> |
|||
</g> |
|||
</g> |
|||
</svg> |
|||
); |
|||
}; |
|||
@ -1,69 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { motion } from 'framer-motion'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { connection } from '../connection'; |
|||
import { useAppDispatch, useAppSelector } from '../hooks/redux'; |
|||
import { toggleSidebar } from '../slices/appSlice'; |
|||
|
|||
export const MessageBox = (): JSX.Element => { |
|||
const ready = useAppSelector((state) => state.meshtastic.ready); |
|||
const [currentMessage, setCurrentMessage] = React.useState(''); |
|||
const sendMessage = () => { |
|||
if (ready) { |
|||
connection.sendText(currentMessage, undefined, true); |
|||
setCurrentMessage(''); |
|||
} |
|||
}; |
|||
const { t } = useTranslation(); |
|||
const dispatch = useAppDispatch(); |
|||
|
|||
return ( |
|||
<div className="flex text-lg font-medium space-x-2 md:space-x-0 w-full"> |
|||
<motion.button |
|||
initial={{}} |
|||
whileHover={{ |
|||
backgroundColor: 'rgba(229, 231, 235)', |
|||
}} |
|||
className="flex h-14 w-14 text-xl hover:text-gray-500 text-gray-400 rounded-full border shadow-md focus:outline-none cursor-pointer md:hidden" |
|||
onClick={() => { |
|||
dispatch(toggleSidebar()); |
|||
}} |
|||
> |
|||
<MenuIcon className="m-auto h-6 w-6" /> |
|||
</motion.button> |
|||
<form |
|||
className="flex flex-wrap relative w-full" |
|||
onSubmit={(e) => { |
|||
e.preventDefault(); |
|||
sendMessage(); |
|||
}} |
|||
> |
|||
{ready} |
|||
<input |
|||
type="text" |
|||
placeholder={`${t('placeholder.no_messages')}...`} |
|||
disabled={!ready} |
|||
value={currentMessage} |
|||
onChange={(e) => { |
|||
setCurrentMessage(e.target.value); |
|||
}} |
|||
className={`p-3 placeholder-gray-400 text-gray-700 relative rounded-3xl border shadow-md focus:outline-none w-full pr-10 ${ |
|||
ready ? 'cursor-text' : 'cursor-not-allowed' |
|||
}`}
|
|||
/> |
|||
<span className="flex z-10 h-full text-gray-400 absolute w-8 right-1"> |
|||
<PaperAirplaneIcon |
|||
onClick={sendMessage} |
|||
className={`text-xl hover:text-gray-500 h-6 w-6 my-auto ${ |
|||
ready ? 'cursor-pointer' : 'cursor-not-allowed' |
|||
}`}
|
|||
/> |
|||
</span> |
|||
</form> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,38 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { AnimatePresence, motion } from 'framer-motion'; |
|||
|
|||
import { useAppSelector } from '../hooks/redux'; |
|||
import { Channels } from './Sidebar/Channels/Index'; |
|||
import { Device } from './Sidebar/Device/Index'; |
|||
import { Nodes } from './Sidebar/Nodes/Index'; |
|||
import { UI } from './Sidebar/UI/Index'; |
|||
|
|||
export const Sidebar = (): JSX.Element => { |
|||
const sidebarOpen = useAppSelector((state) => state.app.sidebarOpen); |
|||
|
|||
return ( |
|||
<AnimatePresence> |
|||
{sidebarOpen && ( |
|||
<motion.div |
|||
initial={{ |
|||
height: 0, |
|||
}} |
|||
animate={{ |
|||
height: 'auto', |
|||
}} |
|||
exit={{ |
|||
height: 0, |
|||
}} |
|||
className="flex flex-col rounded-3xl md:ml-0 shadow-md border w-full md:max-w-sm" |
|||
> |
|||
<Nodes /> |
|||
<Device /> |
|||
<Channels /> |
|||
<div className="flex-grow border-b"></div> |
|||
<UI /> |
|||
</motion.div> |
|||
)} |
|||
</AnimatePresence> |
|||
); |
|||
}; |
|||
@ -1,114 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Disclosure } from '@headlessui/react'; |
|||
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/outline'; |
|||
import { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
export interface ChannelProps { |
|||
channel: Protobuf.Channel; |
|||
} |
|||
|
|||
export const Channel = (props: ChannelProps): JSX.Element => { |
|||
return ( |
|||
<Disclosure> |
|||
{({ open }) => ( |
|||
<> |
|||
<Disclosure.Button className="flex bg-gray-50 w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer"> |
|||
<div className="flex ml-4"> |
|||
{open ? ( |
|||
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" /> |
|||
) : ( |
|||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" /> |
|||
)} |
|||
{props.channel.index} -{' '} |
|||
{Protobuf.Channel_Role[props.channel.role]} |
|||
</div> |
|||
</Disclosure.Button> |
|||
<Disclosure.Panel> |
|||
<div className="w-full bg-gray-100 px-2"> |
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Bandwidth:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.bandwidth} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Channel Number:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.channelNum} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Coding Rate:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.codingRate} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>ID:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.id} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Modem Config:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.modemConfig |
|||
? Protobuf.ChannelSettings_ModemConfig[ |
|||
props.channel.settings.modemConfig |
|||
] |
|||
: null} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Name:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.name} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>PSK:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.psk.toLocaleString()} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Spread Factor:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.spreadFactor} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Tx Power:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.txPower} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Uplink:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.uplinkEnabled ? 'true' : 'false'} |
|||
</code> |
|||
</div> |
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Downlink:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{props.channel.settings?.downlinkEnabled ? 'true' : 'false'} |
|||
</code> |
|||
</div> |
|||
</div> |
|||
</Disclosure.Panel> |
|||
</> |
|||
)} |
|||
</Disclosure> |
|||
); |
|||
}; |
|||
@ -1,19 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { useAppSelector } from '../../../hooks/redux'; |
|||
import { Channel } from './Channel'; |
|||
|
|||
export const ChannelList = (): JSX.Element => { |
|||
const channels = useAppSelector((state) => state.meshtastic.channels); |
|||
|
|||
return ( |
|||
<> |
|||
{channels.map((channel, index) => { |
|||
if (channel.role !== Protobuf.Channel_Role.DISABLED) |
|||
return <Channel key={index} channel={channel} />; |
|||
})} |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,20 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { HashtagIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { Dropdown } from '../../basic/Dropdown'; |
|||
import { ChannelList } from './ChannelList'; |
|||
|
|||
export const Channels = (): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
return ( |
|||
<Dropdown |
|||
icon={<HashtagIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />} |
|||
title={t('settings.channel')} |
|||
content={<ChannelList />} |
|||
fallbackMessage={'Loading...'} |
|||
/> |
|||
); |
|||
}; |
|||
@ -1,20 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { AdjustmentsIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { Dropdown } from '../../basic/Dropdown'; |
|||
import { Settings } from './Settings'; |
|||
|
|||
export const Device = (): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
return ( |
|||
<Dropdown |
|||
icon={<AdjustmentsIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />} |
|||
title={t('settings.device')} |
|||
content={<Settings />} |
|||
fallbackMessage={'Loading...'} |
|||
/> |
|||
); |
|||
}; |
|||
@ -1,20 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { UsersIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { Dropdown } from '../../basic/Dropdown'; |
|||
import { NodeList } from './NodeList'; |
|||
|
|||
export const Nodes = (): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
return ( |
|||
<Dropdown |
|||
icon={<UsersIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />} |
|||
title={t('strings.nodes')} |
|||
content={<NodeList />} |
|||
fallbackMessage={t('placeholder.no_messages')} |
|||
/> |
|||
); |
|||
}; |
|||
@ -1,94 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import Avatar from 'boring-avatars'; |
|||
|
|||
import { Disclosure } from '@headlessui/react'; |
|||
import { |
|||
ChevronDownIcon, |
|||
ChevronRightIcon, |
|||
ClockIcon, |
|||
FlagIcon, |
|||
GlobeIcon, |
|||
LightningBoltIcon, |
|||
} from '@heroicons/react/outline'; |
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { useAppSelector } from '../../../hooks/redux'; |
|||
|
|||
export interface NodeProps { |
|||
node: Protobuf.NodeInfo; |
|||
} |
|||
|
|||
export const Node = (props: NodeProps): JSX.Element => { |
|||
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo); |
|||
|
|||
return ( |
|||
<Disclosure> |
|||
{({ open }) => ( |
|||
<> |
|||
<Disclosure.Button className="flex bg-gray-50 w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer"> |
|||
<div className="flex ml-4"> |
|||
{open ? ( |
|||
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" /> |
|||
) : ( |
|||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" /> |
|||
)} |
|||
<div className="relative"> |
|||
{props.node.num === myNodeInfo.myNodeNum ? ( |
|||
<FlagIcon className="absolute -right-1 -top-2 text-yellow-500 my-auto w-4 h-4" /> |
|||
) : null} |
|||
<Avatar |
|||
size={30} |
|||
name={props.node.user?.longName ?? 'Unknown'} |
|||
variant="beam" |
|||
colors={[ |
|||
'#213435', |
|||
'#46685B', |
|||
'#648A64', |
|||
'#A6B985', |
|||
'#E1E3AC', |
|||
]} |
|||
/> |
|||
</div> |
|||
{props.node.user?.longName} |
|||
</div> |
|||
</Disclosure.Button> |
|||
<Disclosure.Panel> |
|||
<div className="border-b bg-gray-100 px-2"> |
|||
<p>{props.node.snr}</p> |
|||
<p> |
|||
{`Last heard: ${ |
|||
props.node?.lastHeard |
|||
? new Date(props.node.lastHeard).toLocaleString() |
|||
: 'Unknown' |
|||
}`}{' '}
|
|||
{} |
|||
</p> |
|||
<div className="flex"> |
|||
<GlobeIcon className="my-auto mr-2 w-5 h-5" /> |
|||
<p> |
|||
{props.node.position?.latitudeI && |
|||
props.node.position?.longitudeI |
|||
? `${props.node.position.latitudeI / 1e7},
|
|||
${props.node.position.longitudeI / 1e7}` |
|||
: 'Unknown'} |
|||
, El: |
|||
{props.node.position?.altitude} |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="flex"> |
|||
<ClockIcon className="my-auto mr-2 w-5 h-5" /> |
|||
<p>{props.node.position?.time}</p> |
|||
</div> |
|||
<div className="flex"> |
|||
<LightningBoltIcon className="my-auto mr-2 w-5 h-5" /> |
|||
<p>{props.node.position?.batteryLevel}</p> |
|||
</div> |
|||
</div> |
|||
</Disclosure.Panel> |
|||
</> |
|||
)} |
|||
</Disclosure> |
|||
); |
|||
}; |
|||
@ -1,16 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useAppSelector } from '../../../hooks/redux'; |
|||
import { Node } from './Node'; |
|||
|
|||
export const NodeList = (): JSX.Element => { |
|||
const nodes = useAppSelector((state) => state.meshtastic.nodes); |
|||
|
|||
return ( |
|||
<> |
|||
{nodes.map((node, index) => ( |
|||
<Node key={index} node={node} /> |
|||
))} |
|||
</> |
|||
); |
|||
}; |
|||
@ -1,21 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { CogIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { Dropdown } from '../../basic/Dropdown'; |
|||
import { Translations } from './Translations'; |
|||
|
|||
export const UI = (): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<Dropdown |
|||
icon={<CogIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />} |
|||
title={t('settings.ui')} |
|||
content={<Translations />} |
|||
fallbackMessage={'Loading...'} |
|||
/> |
|||
); |
|||
}; |
|||
@ -1,65 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Br, Jp, Us } from 'react-flags-select'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { Disclosure } from '@headlessui/react'; |
|||
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/outline'; |
|||
|
|||
export const Translations = (): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<Disclosure> |
|||
{({ open }) => ( |
|||
<> |
|||
<Disclosure.Button className="flex bg-gray-50 w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer"> |
|||
<div className="flex ml-4"> |
|||
{open ? ( |
|||
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" /> |
|||
) : ( |
|||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" /> |
|||
)} |
|||
{t('strings.language')} |
|||
{/* <div className="my-auto"> |
|||
{language === LanguageEnum.ENGLISH ? ( |
|||
<Us className="ml-2 w-8" /> |
|||
) : language === LanguageEnum.JAPANESE ? ( |
|||
<Jp className="ml-2 w-8" /> |
|||
) : language === LanguageEnum.PORTUGUESE ? ( |
|||
<Br className="ml-2 w-8" /> |
|||
) : null} |
|||
</div> */} |
|||
</div> |
|||
</Disclosure.Button> |
|||
<Disclosure.Panel> |
|||
<div |
|||
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2" |
|||
onClick={() => { |
|||
// setLanguage(LanguageEnum.ENGLISH);
|
|||
}} |
|||
> |
|||
English <Us className="w-8 my-auto" /> |
|||
</div> |
|||
<div |
|||
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2" |
|||
onClick={() => { |
|||
// setLanguage(LanguageEnum.PORTUGUESE);
|
|||
}} |
|||
> |
|||
Português <Br className="w-8 my-auto" /> |
|||
</div> |
|||
<div |
|||
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2" |
|||
onClick={() => { |
|||
// setLanguage(LanguageEnum.JAPANESE);
|
|||
}} |
|||
> |
|||
日本語 <Jp className="w-8 my-auto" /> |
|||
</div> |
|||
</Disclosure.Panel> |
|||
</> |
|||
)} |
|||
</Disclosure> |
|||
); |
|||
}; |
|||
@ -1,123 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { HomeIcon, MenuIcon } from '@heroicons/react/outline'; |
|||
|
|||
export const Tmp = () => { |
|||
return ( |
|||
<div className="h-screen flex flex-col flex-auto items-center w-full min-w-0 bg-gray-200 "> |
|||
<div className="relative flex justify-center w-full overflow-hidden z-50 bg-primary"> |
|||
<div className="max-w-360 w-full sm:py-3 sm:m-8 sm:mb-0 md:mt-12 md:mx-8 md:pt-4 md:pb-3 sm:rounded-t-xl border-b sm:shadow-2xl overflow-hidden bg-white"> |
|||
<div className="relative flex flex-auto flex-0 items-center h-16 px-4 md:px-6"> |
|||
{/* NORMAL NAV ICON */} |
|||
<div className="hidden md:flex items-center mx-2"> |
|||
<img |
|||
className="w-16 dark:hidden" |
|||
src="Mesh_Logo_Black.svg" |
|||
alt="Logo image" |
|||
/> |
|||
<img |
|||
className="hidden dark:flexw-16" |
|||
src="Mesh_Logo_White.svg" |
|||
alt="Logo image" |
|||
/> |
|||
</div> |
|||
{/* END NORMAL NAV ICON */} |
|||
{/* MOBILE NAV BUTTON */} |
|||
<button className="md:hidden w-10 h-10 rounded-full hover:bg-gray-200 hover:shadow-inner text-gray-500"> |
|||
<span className="flex justify-center "> |
|||
<MenuIcon className="h-6 w-6" /> |
|||
</span> |
|||
</button> |
|||
{/* END MOBILE NAV BUTTON */} |
|||
<div className="flex items-center pl-2 ml-auto space-x-1 sm:space-x-2"> |
|||
{/* HEADER BUTTON */} |
|||
<button className="w-10 h-10 rounded-full hover:bg-gray-200 hover:shadow-inner"> |
|||
<span className="flex justify-center "> |
|||
<span className="w-6 shadow rounded-sm"> |
|||
<img |
|||
className="w-full" |
|||
src="assets/images/flags/US.svg" |
|||
alt="Flag image for en" |
|||
/> |
|||
</span> |
|||
</span> |
|||
</button> |
|||
{/* END HEADER BUTTON */} |
|||
</div> |
|||
</div> |
|||
<div className="hidden md:flex flex-auto flex-0 relative items-center h-16 px-4 "> |
|||
<div className="flex items-center"> |
|||
{/* NAV ITEM */} |
|||
<div className="flex h-12 items-center hover:bg-gray-100 rounded-md cursor-pointer px-3 select-none"> |
|||
<HomeIcon className="h-5 w-5 mr-3" /> |
|||
<span>Dashboard</span> |
|||
</div> |
|||
{/* END NAV ITEM */} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="flex flex-auto justify-center w-full sm:px-8 sm:mb-8"> |
|||
<div className="flex flex-col flex-auto w-full sm:max-w-360 sm:shadow-xl sm:overflow-hidden bg-gray-100 sm:rounded-b-xl"> |
|||
<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 bg-white"> |
|||
<div className="flex-1 min-w-0"> |
|||
<div className="flex flex-wrap items-center font-medium"> |
|||
<div> |
|||
<a className="whitespace-nowrap text-purple-500"> |
|||
User Interface |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div className="mt-2"> |
|||
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight leading-7 sm:leading-10 truncate"> |
|||
{' '} |
|||
Confirmation Dialog{' '} |
|||
</h2> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div className="flex-auto p-6 sm:p-10 "> |
|||
<div className="max-w-3xl"> |
|||
<div className="max-w-3xl prose prose-sm"> |
|||
<p> |
|||
{' '} |
|||
One of the repetitive and tedious jobs in Angular is to |
|||
create confirmation dialogs. Since dialogs require their own |
|||
component you have to either create a separate component |
|||
every time you need a confirmation dialog or you have to |
|||
create your own confirmation dialog system that can be |
|||
configured.{' '} |
|||
</p> |
|||
<p> |
|||
{' '} |
|||
In order for you to save time while developing with Fuse, we |
|||
have created a simple yet powerful{' '} |
|||
<code>FuseConfirmationService</code> to create customized |
|||
confirmation dialogs on-the-fly.{' '} |
|||
</p> |
|||
<p> |
|||
{' '} |
|||
Below you can configure and preview the confirmation dialog. |
|||
You can use the generated configuration object within your |
|||
code to have the exact same dialog.{' '} |
|||
</p> |
|||
<p> |
|||
{' '} |
|||
For more detailed information and API documentation, check |
|||
the{' '} |
|||
<a href="/ui/fuse-components/services/confirmation"> |
|||
documentation |
|||
</a>{' '} |
|||
page.{' '} |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -1,73 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { AnimatePresence, motion } from 'framer-motion'; |
|||
|
|||
import { Disclosure } from '@headlessui/react'; |
|||
import { ChevronDownIcon } from '@heroicons/react/outline'; |
|||
|
|||
interface DropdownProps { |
|||
icon: JSX.Element; |
|||
title: string; |
|||
content: JSX.Element; |
|||
fallbackMessage: string; |
|||
} |
|||
|
|||
export const Dropdown = (props: DropdownProps): JSX.Element => { |
|||
return ( |
|||
<Disclosure> |
|||
{({ open }) => ( |
|||
<> |
|||
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer first:rounded-t-3xl last:rounded-b-3xl"> |
|||
<div className="flex"> |
|||
<motion.div |
|||
className="my-auto mr-2" |
|||
variants={{ |
|||
initial: { rotate: -90 }, |
|||
animate: { |
|||
rotate: 0, |
|||
}, |
|||
}} |
|||
initial="initial" |
|||
animate={open ? 'animate' : 'initial'} |
|||
> |
|||
<ChevronDownIcon className="w-5 h-5" /> |
|||
</motion.div> |
|||
{props.icon} |
|||
{props.title} |
|||
</div> |
|||
</Disclosure.Button> |
|||
|
|||
<AnimatePresence> |
|||
{open && ( |
|||
<Disclosure.Panel |
|||
as={motion.div} |
|||
initial={{ |
|||
height: 0, |
|||
}} |
|||
animate={{ |
|||
height: 'auto', |
|||
}} |
|||
exit={{ |
|||
height: 0, |
|||
}} |
|||
className="shadow-inner" |
|||
> |
|||
<React.Suspense |
|||
fallback={ |
|||
<div className="flex border-b border-gray-300"> |
|||
<div className="m-auto p-3 text-gray-500"> |
|||
{props.fallbackMessage} |
|||
</div> |
|||
</div> |
|||
} |
|||
> |
|||
{props.content} |
|||
</React.Suspense> |
|||
</Disclosure.Panel> |
|||
)} |
|||
</AnimatePresence> |
|||
</> |
|||
)} |
|||
</Disclosure> |
|||
); |
|||
}; |
|||
@ -1,31 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Switch } from '@headlessui/react'; |
|||
|
|||
interface ToggleSwitchProps { |
|||
active: boolean; |
|||
} |
|||
|
|||
export const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => { |
|||
const [active, setActive] = React.useState(false); |
|||
|
|||
React.useEffect(() => { |
|||
setActive(props.active); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Switch |
|||
checked={active} |
|||
onChange={setActive} |
|||
className={`w-12 h-6 flex items-center bg-gray-300 rounded-full p-1 duration-300 ease-in-out my-auto ${ |
|||
active ? 'bg-green-400' : null |
|||
}`}
|
|||
> |
|||
<span |
|||
className={`bg-white w-4 h-4 rounded-full shadow-md transform duration-300 ease-in-out ${ |
|||
active ? 'translate-x-6' : null |
|||
}`}
|
|||
></span> |
|||
</Switch> |
|||
); |
|||
}; |
|||
@ -0,0 +1,44 @@ |
|||
import React from 'react'; |
|||
|
|||
import Avatar from 'boring-avatars'; |
|||
|
|||
export interface MessageProps { |
|||
message: string; |
|||
ack: boolean; |
|||
isSender: boolean; |
|||
rxTime: Date; |
|||
} |
|||
|
|||
export const Message = ({ |
|||
message, |
|||
ack, |
|||
isSender, |
|||
rxTime, |
|||
}: MessageProps): JSX.Element => { |
|||
return ( |
|||
<div |
|||
className={`flex space-x-2 ${!isSender && 'ml-auto flex-row-reverse'}`} |
|||
> |
|||
<div className={`shadow-md rounded-full mt-auto ${!isSender && 'ml-2'}`}> |
|||
<Avatar |
|||
size={30} |
|||
name={'UNK'} |
|||
variant="beam" |
|||
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']} |
|||
/> |
|||
</div> |
|||
<div |
|||
className={`relative max-w-3/4 px-3 py-2 rounded-t-lg mb-2 ${ |
|||
isSender |
|||
? 'bg-gray-500 text-gray-50 rounded-br-lg' |
|||
: 'bg-primary text-blue-50 rounded-bl-lg' |
|||
} ${ack ? 'animate-none' : 'animate-pulse'}`}
|
|||
> |
|||
<div className="min-w-4 leading-5">{message}</div> |
|||
</div> |
|||
<div className="mt-auto mb-2 text-xs font-medium text-secondary mr-3 dark:text-gray-200"> |
|||
{rxTime.getHours()}:{rxTime.getMinutes()} |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,60 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { |
|||
EmojiHappyIcon, |
|||
PaperAirplaneIcon, |
|||
PaperClipIcon, |
|||
} from '@heroicons/react/outline'; |
|||
|
|||
import { connection } from '../../connection'; |
|||
import { useAppSelector } from '../../hooks/redux'; |
|||
import { Button } from '../generic/Button'; |
|||
|
|||
export const MessageBar = (): JSX.Element => { |
|||
const ready = useAppSelector((state) => state.meshtastic.ready); |
|||
const [currentMessage, setCurrentMessage] = React.useState(''); |
|||
const sendMessage = () => { |
|||
if (ready) { |
|||
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"> |
|||
<Button> |
|||
<EmojiHappyIcon className="w-6 h-6" /> |
|||
</Button> |
|||
|
|||
<Button> |
|||
<PaperClipIcon className="w-6 h-6" /> |
|||
</Button> |
|||
</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); |
|||
}} |
|||
className="focus:outline-none h-10 w-full resize-none rounded-full border border-gray-300 dark:bg-gray-900 px-4" |
|||
/> |
|||
<Button type="submit"> |
|||
<PaperAirplaneIcon className="w-6 h-6 rotate-90" /> |
|||
</Button> |
|||
</form> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,54 @@ |
|||
import React from 'react'; |
|||
|
|||
export interface InputProps { |
|||
valid?: boolean; |
|||
placeholder?: string; |
|||
validationMessage?: string; |
|||
icon?: JSX.Element; |
|||
type: string; |
|||
name: string; |
|||
value?: string; |
|||
} |
|||
|
|||
export const Input = ({ |
|||
valid, |
|||
placeholder, |
|||
validationMessage, |
|||
icon, |
|||
type, |
|||
name, |
|||
value, |
|||
}: InputProps): JSX.Element => { |
|||
return ( |
|||
<div className="space-y-1"> |
|||
<label |
|||
htmlFor={name} |
|||
className="block text-sm font-medium dark:text-white" |
|||
> |
|||
{name} |
|||
</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 |
|||
type={type} |
|||
name={name} |
|||
id={name} |
|||
value={value} |
|||
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' |
|||
}`}
|
|||
placeholder={placeholder} |
|||
/> |
|||
</div> |
|||
{!valid && ( |
|||
<div className="text-sm text-gray-600">{validationMessage}</div> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,31 @@ |
|||
import React from 'react'; |
|||
|
|||
export interface ButtonProps { |
|||
children: React.ReactNode; |
|||
className?: string; |
|||
clickAction?: () => void; |
|||
type?: 'button' | 'submit' | 'reset' | undefined; |
|||
} |
|||
|
|||
export const Button = ({ |
|||
children, |
|||
className, |
|||
clickAction, |
|||
type, |
|||
}: ButtonProps): JSX.Element => { |
|||
return ( |
|||
<button |
|||
className={`w-10 h-10 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 active:bg-gray-300 dark:active:bg-gray-800 hover:shadow-inner text-gray-500 dark:text-gray-400 ${ |
|||
className ?? '' |
|||
}`}
|
|||
onClick={() => { |
|||
if (clickAction) { |
|||
clickAction(); |
|||
} |
|||
}} |
|||
type={type} |
|||
> |
|||
<span className="flex justify-center">{children}</span> |
|||
</button> |
|||
); |
|||
}; |
|||
@ -0,0 +1,10 @@ |
|||
import React from 'react'; |
|||
|
|||
export const Logo = (): JSX.Element => { |
|||
return ( |
|||
<div className="hidden md:flex"> |
|||
<img className="w-16 dark:hidden" src="Mesh_Logo_Black.svg" /> |
|||
<img className="hidden dark:flex w-16" src="Mesh_Logo_White.svg" /> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,37 @@ |
|||
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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,77 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Dialog } from '@headlessui/react'; |
|||
import { |
|||
AnnotationIcon, |
|||
CogIcon, |
|||
InformationCircleIcon, |
|||
ViewGridIcon, |
|||
XCircleIcon, |
|||
} from '@heroicons/react/outline'; |
|||
|
|||
import { useAppDispatch, useAppSelector } from '../../hooks/redux'; |
|||
import { routes } from '../../router'; |
|||
import { closeMobileNav } from '../../slices/appSlice'; |
|||
import { Button } from '../generic/Button'; |
|||
import { MenuButton } from './MenuButton'; |
|||
|
|||
export const MobileNav = (): JSX.Element => { |
|||
const dispatch = useAppDispatch(); |
|||
|
|||
const mobileNavOpen = useAppSelector((state) => state.app.mobileNavOpen); |
|||
|
|||
return ( |
|||
<Dialog |
|||
open={mobileNavOpen} |
|||
onClose={() => dispatch(closeMobileNav())} |
|||
className="flex fixed inset-0 z-10 overflow-y-auto" |
|||
> |
|||
<Dialog.Overlay className="fixed inset-0 backdrop-filter backdrop-blur" /> |
|||
|
|||
<div className="mx-auto w-full max-w-sm m-6 p-6 transform bg-white dark:bg-secondaryDark border dark:border-gray-600 rounded-3xl"> |
|||
<Button |
|||
className="float-right" |
|||
clickAction={() => { |
|||
dispatch(closeMobileNav()); |
|||
}} |
|||
> |
|||
<XCircleIcon className="w-6 h-6" /> |
|||
</Button> |
|||
<div> |
|||
<MenuButton |
|||
icon={<AnnotationIcon />} |
|||
text={'Messages'} |
|||
link={routes.messages().link} |
|||
clickAction={() => { |
|||
dispatch(closeMobileNav()); |
|||
}} |
|||
/> |
|||
<MenuButton |
|||
icon={<ViewGridIcon />} |
|||
text={'Nodes'} |
|||
link={routes.nodes().link} |
|||
clickAction={() => { |
|||
dispatch(closeMobileNav()); |
|||
}} |
|||
/> |
|||
<MenuButton |
|||
icon={<CogIcon />} |
|||
text={'Settings'} |
|||
link={routes.settings().link} |
|||
clickAction={() => { |
|||
dispatch(closeMobileNav()); |
|||
}} |
|||
/> |
|||
<MenuButton |
|||
icon={<InformationCircleIcon />} |
|||
text={'About'} |
|||
link={routes.about().link} |
|||
clickAction={() => { |
|||
dispatch(closeMobileNav()); |
|||
}} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</Dialog> |
|||
); |
|||
}; |
|||
@ -0,0 +1,40 @@ |
|||
import React from 'react'; |
|||
|
|||
import { |
|||
AnnotationIcon, |
|||
CogIcon, |
|||
InformationCircleIcon, |
|||
ViewGridIcon, |
|||
} from '@heroicons/react/outline'; |
|||
|
|||
import { routes } from '../../router'; |
|||
import { MenuButton } from './MenuButton'; |
|||
|
|||
export const Navigation = (): JSX.Element => { |
|||
return ( |
|||
<div className="hidden md:flex flex-auto flex-0 relative items-center h-16 px-4 "> |
|||
<div className="flex items-center"> |
|||
<MenuButton |
|||
icon={<AnnotationIcon />} |
|||
text={'Messages'} |
|||
link={routes.messages().link} |
|||
/> |
|||
<MenuButton |
|||
icon={<ViewGridIcon />} |
|||
text={'Nodes'} |
|||
link={routes.nodes().link} |
|||
/> |
|||
<MenuButton |
|||
icon={<CogIcon />} |
|||
text={'Settings'} |
|||
link={routes.settings().link} |
|||
/> |
|||
<MenuButton |
|||
icon={<InformationCircleIcon />} |
|||
text={'About'} |
|||
link={routes.about().link} |
|||
/> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,28 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import { useAppSelector } from '../../../hooks/redux'; |
|||
import { Button } from '../../generic/Button'; |
|||
|
|||
export const DeviceStatusDropdown = (): JSX.Element => { |
|||
const ready = useAppSelector((state) => state.meshtastic.ready); |
|||
const deviceStatus = useAppSelector((state) => state.meshtastic.deviceStatus); |
|||
|
|||
return ( |
|||
<Button> |
|||
<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> |
|||
</Button> |
|||
); |
|||
}; |
|||
@ -0,0 +1,66 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Jp, Pt, Us } from 'react-flags-select'; |
|||
|
|||
import { Menu } from '@headlessui/react'; |
|||
|
|||
import { useAppDispatch } from '../../../hooks/redux'; |
|||
import i18n from '../../../translation'; |
|||
import { Button } from '../../generic/Button'; |
|||
|
|||
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"> |
|||
<Button> |
|||
<Menu.Button as="div"> |
|||
<Us className="w-6 shadow rounded-sm" /> |
|||
</Menu.Button> |
|||
</Button> |
|||
|
|||
<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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,22 @@ |
|||
import React from 'react'; |
|||
|
|||
import { MenuIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { useAppDispatch } from '../../../hooks/redux'; |
|||
import { openMobileNav } from '../../../slices/appSlice'; |
|||
import { Button } from '../../generic/Button'; |
|||
|
|||
export const MobileNavToggle = (): JSX.Element => { |
|||
const dispatch = useAppDispatch(); |
|||
|
|||
return ( |
|||
<Button |
|||
clickAction={() => { |
|||
dispatch(openMobileNav()); |
|||
}} |
|||
className="md:hidden" |
|||
> |
|||
<MenuIcon className="h-6 w-6" /> |
|||
</Button> |
|||
); |
|||
}; |
|||
@ -0,0 +1,26 @@ |
|||
import React from 'react'; |
|||
|
|||
import { MoonIcon, SunIcon } from '@heroicons/react/outline'; |
|||
|
|||
import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; |
|||
import { setDarkModeEnabled } from '../../../slices/appSlice'; |
|||
import { Button } from '../../generic/Button'; |
|||
|
|||
export const ThemeToggle = (): JSX.Element => { |
|||
const dispatch = useAppDispatch(); |
|||
const darkMode = useAppSelector((state) => state.app.darkMode); |
|||
|
|||
return ( |
|||
<Button |
|||
clickAction={() => { |
|||
dispatch(setDarkModeEnabled(!darkMode)); |
|||
}} |
|||
> |
|||
{darkMode ? ( |
|||
<SunIcon className="h-6 w-6" /> |
|||
) : ( |
|||
<MoonIcon className="h-6 w-6" /> |
|||
)} |
|||
</Button> |
|||
); |
|||
}; |
|||
@ -1,15 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
interface NavItemProps { |
|||
icon: JSX.Element; |
|||
text: string; |
|||
} |
|||
|
|||
export const NavItem = ({ icon, text }: NavItemProps) => { |
|||
return ( |
|||
<div className="flex h-12 items-center hover:bg-gray-100 rounded-md cursor-pointer px-3 select-none"> |
|||
{icon} |
|||
<span className="">{text}</span> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,37 @@ |
|||
import React from 'react'; |
|||
|
|||
export interface PrimaryTemplateProps { |
|||
children: React.ReactNode; |
|||
title: string; |
|||
tagline: string; |
|||
} |
|||
|
|||
export const PrimaryTemplate = ({ |
|||
children, |
|||
title, |
|||
tagline, |
|||
}: 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-1 min-w-0"> |
|||
<div className="flex flex-wrap items-center font-medium"> |
|||
<div> |
|||
<a className="whitespace-nowrap text-primary">{tagline}</a> |
|||
</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"> |
|||
{title} |
|||
</h2> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div className="flex-auto p-6 sm:p-10 "> |
|||
<div className="max-w-3xl"> |
|||
<div className="max-w-3xl">{children}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,11 @@ |
|||
import React from 'react'; |
|||
|
|||
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; |
|||
|
|||
export const About = (): JSX.Element => { |
|||
return ( |
|||
<PrimaryTemplate title="meshtastic-web" tagline="About"> |
|||
<p>Content</p> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -0,0 +1,26 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Message } from '../components/chat/Message'; |
|||
import { MessageBar } from '../components/chat/MessageBar'; |
|||
import { useAppSelector } from '../hooks/redux'; |
|||
|
|||
export const Messages = (): JSX.Element => { |
|||
const messages = useAppSelector((state) => state.meshtastic.messages); |
|||
|
|||
return ( |
|||
<div className="flex flex-col w-full"> |
|||
<div className="flex flex-col p-6 sm:py-8 sm:px-10 border-b dark:border-gray-600 bg-white dark:bg-secondaryDark flex-grow overflow-y-auto space-y-2"> |
|||
{messages.map((message, index) => ( |
|||
<Message |
|||
key={index} |
|||
isSender={message.isSender} |
|||
message={message.message.data} |
|||
ack={message.ack} |
|||
rxTime={new Date()} |
|||
/> |
|||
))} |
|||
</div> |
|||
<MessageBar /> |
|||
</div> |
|||
); |
|||
}; |
|||
@ -0,0 +1,11 @@ |
|||
import React from 'react'; |
|||
|
|||
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; |
|||
|
|||
export const Nodes = (): JSX.Element => { |
|||
return ( |
|||
<PrimaryTemplate title="Administration" tagline="Node"> |
|||
<p>Nodes</p> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -0,0 +1,36 @@ |
|||
import React from 'react'; |
|||
|
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
import { Input } from '../components/form/Input'; |
|||
import { PrimaryTemplate } from '../components/templates/PrimaryTemplate'; |
|||
import { useAppSelector } from '../hooks/redux'; |
|||
|
|||
export const Settings = (): JSX.Element => { |
|||
const { t } = useTranslation(); |
|||
const radioConfig = useAppSelector((state) => state.meshtastic.preferences); |
|||
|
|||
console.log(radioConfig); |
|||
|
|||
return ( |
|||
<PrimaryTemplate title="Settings" tagline="Device"> |
|||
<div className="flex"> |
|||
<div className="w-1/3 text-lg">WiFi</div> |
|||
<div className="space-y-2"> |
|||
<Input |
|||
name={t('strings.wifi_ssid')} |
|||
value={radioConfig.wifiSsid} |
|||
type="text" |
|||
valid={true} |
|||
/> |
|||
<Input |
|||
name={t('strings.wifi_psk')} |
|||
value={radioConfig.wifiPassword} |
|||
type="text" |
|||
valid={true} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</PrimaryTemplate> |
|||
); |
|||
}; |
|||
@ -0,0 +1,8 @@ |
|||
import { createRouter, defineRoute } from 'type-route'; |
|||
|
|||
export const { RouteProvider, useRoute, routes } = createRouter({ |
|||
messages: defineRoute('/'), |
|||
nodes: defineRoute('/nodes'), |
|||
settings: defineRoute('/settings'), |
|||
about: defineRoute('/about'), |
|||
}); |
|||
@ -1,31 +1,44 @@ |
|||
import type { PayloadAction } from '@reduxjs/toolkit'; |
|||
import { createSlice } from '@reduxjs/toolkit'; |
|||
|
|||
export type currentPageName = 'messages' | 'settings'; |
|||
|
|||
interface AppState { |
|||
sidebarOpen: boolean; |
|||
mobileNavOpen: boolean; |
|||
darkMode: boolean; |
|||
currentPage: currentPageName; |
|||
} |
|||
|
|||
const initialState: AppState = { |
|||
sidebarOpen: true, |
|||
mobileNavOpen: false, |
|||
darkMode: false, |
|||
currentPage: 'messages', |
|||
}; |
|||
|
|||
export const appSlice = createSlice({ |
|||
name: 'app', |
|||
initialState, |
|||
reducers: { |
|||
openSidebar(state) { |
|||
state.sidebarOpen = true; |
|||
openMobileNav(state) { |
|||
state.mobileNavOpen = true; |
|||
}, |
|||
closeMobileNav(state) { |
|||
state.mobileNavOpen = false; |
|||
}, |
|||
closeSidebar(state) { |
|||
state.sidebarOpen = false; |
|||
setDarkModeEnabled(state, action: PayloadAction<boolean>) { |
|||
state.darkMode = action.payload; |
|||
}, |
|||
toggleSidebar(state) { |
|||
state.sidebarOpen = !state.sidebarOpen; |
|||
setCurrentPage(state, action: PayloadAction<currentPageName>) { |
|||
state.currentPage = action.payload; |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
export const { openSidebar, closeSidebar, toggleSidebar } = appSlice.actions; |
|||
export const { |
|||
openMobileNav, |
|||
closeMobileNav, |
|||
setDarkModeEnabled, |
|||
setCurrentPage, |
|||
} = appSlice.actions; |
|||
|
|||
export default appSlice.reducer; |
|||
|
|||
@ -1,22 +0,0 @@ |
|||
{ |
|||
"errors": {}, |
|||
"placeholder": { |
|||
"message": "Enter Message", |
|||
"no_messages": "No messages yet", |
|||
"no_nodes": "No nodes found" |
|||
}, |
|||
"strings": { |
|||
"nodes": "Nodes", |
|||
"color_scheme": "Color scheme", |
|||
"language": "Language", |
|||
"device_region": "Device region", |
|||
"wifi_ssid": "WiFi SSID", |
|||
"wifi_psk": "WiFi PSK", |
|||
"save_changes": "Save changes" |
|||
}, |
|||
"settings": { |
|||
"ui": "UI Settings", |
|||
"device": "Device Settings", |
|||
"channel": "Channels" |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
export const en = { |
|||
errors: {}, |
|||
placeholder: { |
|||
message: 'Enter Message', |
|||
no_messages: 'No messages yet', |
|||
no_nodes: 'No nodes found', |
|||
}, |
|||
strings: { |
|||
nodes: 'Nodes', |
|||
color_scheme: 'Color scheme', |
|||
language: 'Language', |
|||
device_region: 'Device region', |
|||
wifi_ssid: 'WiFi SSID', |
|||
wifi_psk: 'WiFi PSK', |
|||
save_changes: 'Save changes', |
|||
}, |
|||
settings: { |
|||
ui: 'UI Settings', |
|||
device: 'Device Settings', |
|||
channel: 'Channels', |
|||
}, |
|||
}; |
|||
@ -1,22 +0,0 @@ |
|||
{ |
|||
"errors": {}, |
|||
"placeholder": { |
|||
"message": "メッセージを入力してください", |
|||
"no_messages": "まだメッセージはありません", |
|||
"no_nodes": "ノードが見つかりません" |
|||
}, |
|||
"strings": { |
|||
"nodes": "ノード", |
|||
"color_scheme": "カラースキーム", |
|||
"language": "言語", |
|||
"device_region": "デバイスリージョン", |
|||
"wifi_ssid": "WiFi名", |
|||
"wifi_psk": "WiFiパスワード", |
|||
"save_changes": "変更内容を保存" |
|||
}, |
|||
"settings": { |
|||
"ui": "UI設定", |
|||
"device": "デバイスの設定", |
|||
"channel": "#################" |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
export const jp = { |
|||
errors: {}, |
|||
placeholder: { |
|||
message: 'メッセージを入力してください', |
|||
no_messages: 'まだメッセージはありません', |
|||
no_nodes: 'ノードが見つかりません', |
|||
}, |
|||
strings: { |
|||
nodes: 'ノード', |
|||
color_scheme: 'カラースキーム', |
|||
language: '言語', |
|||
device_region: 'デバイスリージョン', |
|||
wifi_ssid: 'WiFi名', |
|||
wifi_psk: 'WiFiパスワード', |
|||
save_changes: '変更内容を保存', |
|||
}, |
|||
settings: { |
|||
ui: 'UI設定', |
|||
device: 'デバイスの設定', |
|||
channel: '#################', |
|||
}, |
|||
}; |
|||
@ -1,22 +0,0 @@ |
|||
{ |
|||
"errors": {}, |
|||
"placeholder": { |
|||
"message": "Entre mensagem", |
|||
"no_messages": "Não a mensagens ainda", |
|||
"no_nodes": "Nenhum nó foi encontrado" |
|||
}, |
|||
"strings": { |
|||
"nodes": "Nós", |
|||
"color_scheme": "Esquema de cores", |
|||
"language": "Idioma", |
|||
"device_region": "Região do dispositivo", |
|||
"wifi_ssid": "Nome do WiFi", |
|||
"wifi_psk": "Senha do WiFi", |
|||
"save_changes": "Salvar alterações" |
|||
}, |
|||
"settings": { |
|||
"ui": "Configurações da Interface", |
|||
"device": "Configurações do dispositivo", |
|||
"channel": "Canais" |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
export const pt = { |
|||
errors: {}, |
|||
placeholder: { |
|||
message: 'Entre mensagem', |
|||
no_messages: 'Não a mensagens ainda', |
|||
no_nodes: 'Nenhum nó foi encontrado', |
|||
}, |
|||
strings: { |
|||
nodes: 'Nós', |
|||
color_scheme: 'Esquema de cores', |
|||
language: 'Idioma', |
|||
device_region: 'Região do dispositivo', |
|||
wifi_ssid: 'Nome do WiFi', |
|||
wifi_psk: 'Senha do WiFi', |
|||
save_changes: 'Salvar alterações', |
|||
}, |
|||
settings: { |
|||
ui: 'Configurações da Interface', |
|||
device: 'Configurações do dispositivo', |
|||
channel: 'Canais', |
|||
}, |
|||
}; |
|||
Loading…
Reference in new issue