WIP
This commit is contained in:
+2
-1
@@ -24,7 +24,8 @@
|
|||||||
"react-flags-select": "^2.1.2",
|
"react-flags-select": "^2.1.2",
|
||||||
"react-hook-form": "^7.9.0",
|
"react-hook-form": "^7.9.0",
|
||||||
"react-i18next": "^11.11.4",
|
"react-i18next": "^11.11.4",
|
||||||
"react-redux": "^7.2.4"
|
"react-redux": "^7.2.4",
|
||||||
|
"type-route": "^0.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snowpack/plugin-dotenv": "^2.0.5",
|
"@snowpack/plugin-dotenv": "^2.0.5",
|
||||||
|
|||||||
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");
|
|
||||||
}
|
|
||||||
+2
-2
@@ -8,7 +8,7 @@
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
||||||
<link rel="manifest" href="site.webmanifest" />
|
<link rel="manifest" href="site.webmanifest" />
|
||||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#67ea94" />
|
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#67ea94" />
|
||||||
<link href="fonts/inter/inter.css" rel="stylesheet" />
|
<link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="https://fonts.gstatic.com" rel="preconnect" />
|
<link href="https://fonts.gstatic.com" rel="preconnect" />
|
||||||
<link
|
<link
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-snowpack-app"
|
content="Web site created using create-snowpack-app"
|
||||||
/>
|
/>
|
||||||
<title>Snowpack App</title>
|
<title>Meshtastic Web</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+74
-68
@@ -1,13 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { HomeIcon, MenuIcon, MoonIcon } from '@heroicons/react/outline';
|
|
||||||
import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs';
|
import { Protobuf, SettingsManager, Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { NavItem } from './components/nav/NavItem';
|
import { DeviceStatusDropdown } from './components/menu/buttons/DeviceStatusDropdown';
|
||||||
|
import { LanguageDropdown } from './components/menu/buttons/LanguageDropdown';
|
||||||
|
import { MobileNavToggle } from './components/menu/buttons/MobileNavToggle';
|
||||||
|
import { ThemeToggle } from './components/menu/buttons/ThemeToggle';
|
||||||
|
import { Logo } from './components/menu/Logo';
|
||||||
|
import { MobileNav } from './components/menu/MobileNav';
|
||||||
|
import { Navigation } from './components/menu/Navigation';
|
||||||
import { connection } from './connection';
|
import { connection } from './connection';
|
||||||
import { useAppDispatch } from './hooks/redux';
|
import { useAppDispatch, useAppSelector } from './hooks/redux';
|
||||||
|
import { About } from './pages/About';
|
||||||
|
import { Messages } from './pages/Messages';
|
||||||
|
import { Nodes } from './pages/Nodes';
|
||||||
|
import { Settings } from './pages/Settings';
|
||||||
|
import { useRoute } from './router';
|
||||||
import {
|
import {
|
||||||
|
ackMessage,
|
||||||
addChannel,
|
addChannel,
|
||||||
|
addMessage,
|
||||||
addNode,
|
addNode,
|
||||||
setDeviceStatus,
|
setDeviceStatus,
|
||||||
setLastMeshInterraction,
|
setLastMeshInterraction,
|
||||||
@@ -18,6 +30,10 @@ import {
|
|||||||
|
|
||||||
const App = (): JSX.Element => {
|
const App = (): JSX.Element => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const myNodeInfo = useAppSelector((state) => state.meshtastic.myNodeInfo);
|
||||||
|
const darkMode = useAppSelector((state) => state.app.darkMode);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
|
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
|
||||||
@@ -70,77 +86,67 @@ const App = (): JSX.Element => {
|
|||||||
connection.onMeshHeartbeat.subscribe((date) =>
|
connection.onMeshHeartbeat.subscribe((date) =>
|
||||||
dispatch(setLastMeshInterraction(date.getTime())),
|
dispatch(setLastMeshInterraction(date.getTime())),
|
||||||
);
|
);
|
||||||
}, [dispatch]);
|
|
||||||
|
connection.onTextPacket.subscribe((message) => {
|
||||||
|
console.log(message.packet.from, '===', myNodeInfo.myNodeNum);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addMessage({
|
||||||
|
message: message,
|
||||||
|
ack: message.packet.from !== myNodeInfo.myNodeNum,
|
||||||
|
isSender: message.packet.from === myNodeInfo.myNodeNum,
|
||||||
|
received: new Date(message.packet.rxTime),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.onRoutingPacket.subscribe((routingPacket) => {
|
||||||
|
if (routingPacket.packet.payloadVariant.oneofKind === 'decoded') {
|
||||||
|
dispatch(
|
||||||
|
ackMessage(routingPacket.packet.payloadVariant.decoded.requestId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
connection.onDeviceStatus.cancelAll();
|
||||||
|
connection.onMyNodeInfo.cancelAll();
|
||||||
|
connection.onNodeInfoPacket.cancelAll();
|
||||||
|
connection.onAdminPacket.cancelAll();
|
||||||
|
connection.onMeshHeartbeat.cancelAll();
|
||||||
|
connection.onTextPacket.cancelAll();
|
||||||
|
connection.onRoutingPacket.cancelAll();
|
||||||
|
};
|
||||||
|
}, [dispatch, myNodeInfo.myNodeNum]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <div className="flex flex-col h-screen w-screen">
|
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
|
||||||
// <Header />
|
<div className="flex flex-col h-full w-full bg-gray-200 dark:bg-primaryDark">
|
||||||
// <Main />
|
<div className="flex flex-shrink-0 w-full overflow-hidden bg-primary dark:bg-primary">
|
||||||
// </div>
|
<div className="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 dark:border-gray-600 sm:shadow-md overflow-hidden bg-white dark:bg-primaryDark">
|
||||||
<div className="h-screen flex flex-col flex-auto items-center w-full min-w-0 bg-gray-200 ">
|
<div className="flex items-center justify-between h-16 px-4 md:px-6">
|
||||||
<div className="relative flex justify-center w-full overflow-hidden z-50 bg-primary">
|
<Logo />
|
||||||
<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">
|
<MobileNavToggle />
|
||||||
<div className="relative flex flex-auto flex-0 items-center h-16 px-4 md:px-6">
|
<div className="flex items-center space-x-2">
|
||||||
{/* NORMAL NAV ICON */}
|
<DeviceStatusDropdown />
|
||||||
<div className="hidden md:flex items-center mx-2">
|
<LanguageDropdown />
|
||||||
<img
|
<ThemeToggle />
|
||||||
className="w-16 dark:hidden"
|
</div>
|
||||||
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 */}
|
|
||||||
{/* THEME BUTTON */}
|
|
||||||
<button className="w-10 h-10 rounded-full hover:bg-gray-200 hover:shadow-inner text-gray-500">
|
|
||||||
<span className="flex justify-center ">
|
|
||||||
<MoonIcon className="h-6 w-6" />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/* END THEME 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 */}
|
|
||||||
<NavItem
|
|
||||||
icon={<HomeIcon className="h-6 w-6 mr-3 text-gray-500" />}
|
|
||||||
text={'Dashboard'}
|
|
||||||
/>
|
|
||||||
{/* END NAV ITEM */}
|
|
||||||
</div>
|
</div>
|
||||||
|
<Navigation />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-auto justify-center w-full sm:px-8 sm:mb-8">
|
<MobileNav />
|
||||||
<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 ">content</div>
|
<div className="flex flex-grow min-h-0 w-full sm:px-8 sm:mb-8">
|
||||||
|
<div className="flex w-full sm:shadow-xl sm:overflow-hidden bg-gray-100 dark:bg-secondaryDark sm:rounded-b-xl">
|
||||||
|
{route.name === 'messages' && <Messages />}
|
||||||
|
{route.name === 'nodes' && <Nodes />}
|
||||||
|
{route.name === 'settings' && <Settings />}
|
||||||
|
{route.name === 'about' && <About />}
|
||||||
|
{route.name === false && 'Not Found'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+6
-5
@@ -7,15 +7,16 @@ import ReactDOM from 'react-dom';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
// import { Tmp } from './components/Tmp';
|
import { RouteProvider } from './router';
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<RouteProvider>
|
||||||
<App />
|
<Provider store={store}>
|
||||||
{/* <Tmp /> */}
|
<App />
|
||||||
</Provider>
|
</Provider>
|
||||||
|
</RouteProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
});
|
||||||
+22
-9
@@ -1,31 +1,44 @@
|
|||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export type currentPageName = 'messages' | 'settings';
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
sidebarOpen: boolean;
|
mobileNavOpen: boolean;
|
||||||
darkMode: boolean;
|
darkMode: boolean;
|
||||||
|
currentPage: currentPageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AppState = {
|
const initialState: AppState = {
|
||||||
sidebarOpen: true,
|
mobileNavOpen: false,
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
|
currentPage: 'messages',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appSlice = createSlice({
|
export const appSlice = createSlice({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
openSidebar(state) {
|
openMobileNav(state) {
|
||||||
state.sidebarOpen = true;
|
state.mobileNavOpen = true;
|
||||||
},
|
},
|
||||||
closeSidebar(state) {
|
closeMobileNav(state) {
|
||||||
state.sidebarOpen = false;
|
state.mobileNavOpen = false;
|
||||||
},
|
},
|
||||||
toggleSidebar(state) {
|
setDarkModeEnabled(state, action: PayloadAction<boolean>) {
|
||||||
state.sidebarOpen = !state.sidebarOpen;
|
state.darkMode = action.payload;
|
||||||
|
},
|
||||||
|
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;
|
export default appSlice.reducer;
|
||||||
|
|||||||
@@ -2,40 +2,35 @@ import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export interface MessageWithAck {
|
||||||
|
message: Types.TextPacket;
|
||||||
|
ack: boolean;
|
||||||
|
isSender: boolean;
|
||||||
|
received: Date;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
deviceStatus: Types.DeviceStatusEnum;
|
deviceStatus: Types.DeviceStatusEnum;
|
||||||
lastMeshInterraction: number;
|
lastMeshInterraction: number;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
fromRaioPackets: Protobuf.FromRadio[];
|
|
||||||
meshPackets: Protobuf.MeshPacket[];
|
|
||||||
myNodeInfo: Protobuf.MyNodeInfo;
|
myNodeInfo: Protobuf.MyNodeInfo;
|
||||||
radioConfig: Protobuf.RadioConfig[];
|
|
||||||
routingPackets: Types.RoutingPacket[];
|
|
||||||
positionPackets: Types.PositionPacket[];
|
positionPackets: Types.PositionPacket[];
|
||||||
textPackets: Types.TextPacket[];
|
|
||||||
logRecords: Protobuf.LogRecord[];
|
|
||||||
//
|
|
||||||
nodes: Protobuf.NodeInfo[];
|
nodes: Protobuf.NodeInfo[];
|
||||||
channels: Protobuf.Channel[];
|
channels: Protobuf.Channel[];
|
||||||
preferences: Protobuf.RadioConfig_UserPreferences;
|
preferences: Protobuf.RadioConfig_UserPreferences;
|
||||||
|
messages: MessageWithAck[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AppState = {
|
const initialState: AppState = {
|
||||||
deviceStatus: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
|
deviceStatus: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
|
||||||
lastMeshInterraction: 0,
|
lastMeshInterraction: 0,
|
||||||
ready: false,
|
ready: false,
|
||||||
fromRaioPackets: [],
|
|
||||||
meshPackets: [],
|
|
||||||
myNodeInfo: Protobuf.MyNodeInfo.create(),
|
myNodeInfo: Protobuf.MyNodeInfo.create(),
|
||||||
radioConfig: [],
|
|
||||||
routingPackets: [],
|
|
||||||
positionPackets: [],
|
positionPackets: [],
|
||||||
textPackets: [],
|
|
||||||
logRecords: [],
|
|
||||||
//
|
|
||||||
nodes: [],
|
nodes: [],
|
||||||
channels: [],
|
channels: [],
|
||||||
preferences: Protobuf.RadioConfig_UserPreferences.create(),
|
preferences: Protobuf.RadioConfig_UserPreferences.create(),
|
||||||
|
messages: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const meshtasticSlice = createSlice({
|
export const meshtasticSlice = createSlice({
|
||||||
@@ -51,31 +46,12 @@ export const meshtasticSlice = createSlice({
|
|||||||
setReady: (state, action: PayloadAction<boolean>) => {
|
setReady: (state, action: PayloadAction<boolean>) => {
|
||||||
state.ready = action.payload;
|
state.ready = action.payload;
|
||||||
},
|
},
|
||||||
addFromRadioPacket: (state, action: PayloadAction<Protobuf.FromRadio>) => {
|
|
||||||
state.fromRaioPackets.push(action.payload);
|
|
||||||
},
|
|
||||||
addMeshPacket: (state, action: PayloadAction<Protobuf.MeshPacket>) => {
|
|
||||||
state.meshPackets.push(action.payload);
|
|
||||||
},
|
|
||||||
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
|
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
|
||||||
state.myNodeInfo = action.payload;
|
state.myNodeInfo = action.payload;
|
||||||
},
|
},
|
||||||
addRadioConfig: (state, action: PayloadAction<Protobuf.RadioConfig>) => {
|
|
||||||
state.radioConfig.push(action.payload);
|
|
||||||
},
|
|
||||||
addRoutingPacket: (state, action: PayloadAction<Types.RoutingPacket>) => {
|
|
||||||
state.routingPackets.push(action.payload);
|
|
||||||
},
|
|
||||||
addPositionPacket: (state, action: PayloadAction<Types.PositionPacket>) => {
|
addPositionPacket: (state, action: PayloadAction<Types.PositionPacket>) => {
|
||||||
state.positionPackets.push(action.payload);
|
state.positionPackets.push(action.payload);
|
||||||
},
|
},
|
||||||
addTextPacket: (state, action: PayloadAction<Types.TextPacket>) => {
|
|
||||||
state.textPackets.push(action.payload);
|
|
||||||
},
|
|
||||||
addLogRecord: (state, action: PayloadAction<Protobuf.LogRecord>) => {
|
|
||||||
state.logRecords.push(action.payload);
|
|
||||||
},
|
|
||||||
//
|
|
||||||
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
|
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
|
||||||
if (
|
if (
|
||||||
state.nodes.findIndex((node) => node.num === action.payload.num) !== -1
|
state.nodes.findIndex((node) => node.num === action.payload.num) !== -1
|
||||||
@@ -109,6 +85,16 @@ export const meshtasticSlice = createSlice({
|
|||||||
) => {
|
) => {
|
||||||
state.preferences = action.payload;
|
state.preferences = action.payload;
|
||||||
},
|
},
|
||||||
|
addMessage: (state, action: PayloadAction<MessageWithAck>) => {
|
||||||
|
state.messages.push(action.payload);
|
||||||
|
},
|
||||||
|
ackMessage: (state, messageId: PayloadAction<number>) => {
|
||||||
|
state.messages.map((message) => {
|
||||||
|
if (message.message.packet.id === messageId.payload) {
|
||||||
|
message.ack = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,17 +102,13 @@ export const {
|
|||||||
setDeviceStatus,
|
setDeviceStatus,
|
||||||
setLastMeshInterraction,
|
setLastMeshInterraction,
|
||||||
setReady,
|
setReady,
|
||||||
addFromRadioPacket,
|
|
||||||
addMeshPacket,
|
|
||||||
setMyNodeInfo,
|
setMyNodeInfo,
|
||||||
addRadioConfig,
|
|
||||||
addRoutingPacket,
|
|
||||||
addPositionPacket,
|
addPositionPacket,
|
||||||
addTextPacket,
|
|
||||||
addLogRecord,
|
|
||||||
addNode,
|
addNode,
|
||||||
addChannel,
|
addChannel,
|
||||||
setPreferences,
|
setPreferences,
|
||||||
|
addMessage,
|
||||||
|
ackMessage,
|
||||||
} = meshtasticSlice.actions;
|
} = meshtasticSlice.actions;
|
||||||
|
|
||||||
export default meshtasticSlice.reducer;
|
export default meshtasticSlice.reducer;
|
||||||
|
|||||||
+6
-4
@@ -2,7 +2,9 @@ import i18n from 'i18next';
|
|||||||
import detector from 'i18next-browser-languagedetector';
|
import detector from 'i18next-browser-languagedetector';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
import en from './translations/en.json';
|
import { en } from './translations/en';
|
||||||
|
import { jp } from './translations/jp';
|
||||||
|
import { pt } from './translations/pt';
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(detector)
|
.use(detector)
|
||||||
@@ -10,9 +12,9 @@ i18n
|
|||||||
.init({
|
.init({
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
resources: {
|
resources: {
|
||||||
en: {
|
en: { translation: en },
|
||||||
translation: en,
|
jp: { translation: jp },
|
||||||
},
|
pt: { translation: pt },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
};
|
||||||
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
purge: ['./public/**/*.html', './src/**/*.tsx'],
|
purge: ['./public/**/*.html', './src/**/*.tsx'],
|
||||||
darkMode: false, // or 'media' or 'class'
|
darkMode: 'class', // or 'media' or 'class'
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: 'Inter var',
|
sans: 'Inter var',
|
||||||
@@ -10,6 +10,8 @@ module.exports = {
|
|||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#67ea94',
|
primary: '#67ea94',
|
||||||
|
primaryDark: '#1E293B',
|
||||||
|
secondaryDark: '#0F172A',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -188,7 +188,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.12.13"
|
"@babel/helper-plugin-utils" "^7.12.13"
|
||||||
|
|
||||||
"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.9.2":
|
"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.7.6", "@babel/runtime@^7.9.2":
|
||||||
version "7.14.8"
|
version "7.14.8"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
|
||||||
integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
|
integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
|
||||||
@@ -2448,6 +2448,13 @@ hey-listen@^1.0.8:
|
|||||||
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
|
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
|
||||||
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
|
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
|
||||||
|
|
||||||
|
history@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08"
|
||||||
|
integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.7.6"
|
||||||
|
|
||||||
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
@@ -4965,6 +4972,13 @@ type-fest@^0.20.2:
|
|||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||||
|
|
||||||
|
type-route@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-route/-/type-route-0.6.0.tgz#0762fde72c592f7fc9eea3451f7c538811b7439e"
|
||||||
|
integrity sha512-uh5bxHxHOKNDNTetGwBgtSP5ba3SUtnKcdj3d5AjbIALVbYBwaix4wwfpyxqrE9ia31LknXUc+359FChcC01jw==
|
||||||
|
dependencies:
|
||||||
|
history "^5.0.0"
|
||||||
|
|
||||||
typedarray-to-buffer@^3.1.5:
|
typedarray-to-buffer@^3.1.5:
|
||||||
version "3.1.5"
|
version "3.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||||
|
|||||||
Reference in New Issue
Block a user