Move to snowpack
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
[submodule "design"]
|
||||||
|
path = design
|
||||||
|
url = https://github.com/meshtastic/meshtastic-design/
|
||||||
|
[submodule "public/design"]
|
||||||
|
path = public/design
|
||||||
|
url = https://github.com/meshtastic/meshtastic-design/
|
||||||
+10
-1
@@ -7,24 +7,33 @@
|
|||||||
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\""
|
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@meshtastic/meshtasticjs": "^0.6.5",
|
||||||
|
"@snowpack/plugin-webpack": "^2.3.1",
|
||||||
|
"country-flag-icons": "^1.2.9",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
"react-dom": "^17.0.0"
|
"react-dom": "^17.0.0",
|
||||||
|
"react-icons": "^4.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snowpack/plugin-dotenv": "^2.0.5",
|
"@snowpack/plugin-dotenv": "^2.0.5",
|
||||||
|
"@snowpack/plugin-postcss": "^1.2.2",
|
||||||
"@snowpack/plugin-react-refresh": "^2.4.0",
|
"@snowpack/plugin-react-refresh": "^2.4.0",
|
||||||
"@snowpack/plugin-typescript": "^1.2.0",
|
"@snowpack/plugin-typescript": "^1.2.0",
|
||||||
"@snowpack/web-test-runner-plugin": "^0.2.0",
|
"@snowpack/web-test-runner-plugin": "^0.2.0",
|
||||||
"@testing-library/react": "^11.0.0",
|
"@testing-library/react": "^11.0.0",
|
||||||
"@types/chai": "^4.2.13",
|
"@types/chai": "^4.2.13",
|
||||||
|
"@types/country-flag-icons": "^1.2.0",
|
||||||
"@types/mocha": "^8.2.0",
|
"@types/mocha": "^8.2.0",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/snowpack-env": "^2.3.2",
|
"@types/snowpack-env": "^2.3.2",
|
||||||
"@web/test-runner": "^0.12.0",
|
"@web/test-runner": "^0.12.0",
|
||||||
|
"autoprefixer": "^10.2.5",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
"postcss": "^8.2.9",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"snowpack": "^3.0.1",
|
"snowpack": "^3.0.1",
|
||||||
|
"tailwindcss": "^2.1.1",
|
||||||
"typescript": "^4.0.0"
|
"typescript": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
Submodule
+1
Submodule public/design added at d0339f0297
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB |
+5
-2
@@ -2,9 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/design/web/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="description" content="Web site created using create-snowpack-app" />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-snowpack-app"
|
||||||
|
/>
|
||||||
<title>Snowpack App</title>
|
<title>Snowpack App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
'@snowpack/plugin-react-refresh',
|
'@snowpack/plugin-react-refresh',
|
||||||
'@snowpack/plugin-dotenv',
|
'@snowpack/plugin-dotenv',
|
||||||
|
'@snowpack/plugin-postcss',
|
||||||
[
|
[
|
||||||
'@snowpack/plugin-typescript',
|
'@snowpack/plugin-typescript',
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,15 @@ module.exports = {
|
|||||||
...(process.versions.pnp ? { tsc: 'yarn pnpify tsc' } : {}),
|
...(process.versions.pnp ? { tsc: 'yarn pnpify tsc' } : {}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// [
|
||||||
|
// '@snowpack/plugin-webpack',
|
||||||
|
// {
|
||||||
|
// outputPattern: {
|
||||||
|
// js: 'index.js',
|
||||||
|
// css: 'index.css',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
],
|
],
|
||||||
routes: [
|
routes: [
|
||||||
/* Enable an SPA Fallback in development: */
|
/* Enable an SPA Fallback in development: */
|
||||||
|
|||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.App code {
|
|
||||||
background: #FFF3;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.App p {
|
|
||||||
margin: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-logo {
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
describe('<App>', () => {
|
|
||||||
it('renders learn react link', () => {
|
|
||||||
const { getByText } = render(<App />);
|
|
||||||
const linkElement = getByText(/learn react/i);
|
|
||||||
expect(document.body.contains(linkElement));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+201
-36
@@ -1,41 +1,206 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import logo from './logo.svg';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
interface AppProps {}
|
import {
|
||||||
|
Client,
|
||||||
|
IHTTPConnection,
|
||||||
|
Protobuf,
|
||||||
|
SettingsManager,
|
||||||
|
Types,
|
||||||
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
function App({}: AppProps) {
|
import Header from './components/Header';
|
||||||
// Create the count state.
|
import Main from './Main';
|
||||||
const [count, setCount] = useState(0);
|
import Translations_English from './translations/en';
|
||||||
// Create the counter (+1 every second).
|
import Translations_Japanese from './translations/jp';
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => setCount(count + 1), 1000);
|
export enum LanguageEnum {
|
||||||
return () => clearTimeout(timer);
|
ENGLISH,
|
||||||
}, [count, setCount]);
|
JAPANESE,
|
||||||
// Return the App component.
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Page has been open for <code>{count}</code> seconds.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
className="App-link"
|
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface languageTemplate {
|
||||||
|
no_messages_message: string;
|
||||||
|
ui_settings_title: string;
|
||||||
|
nodes_title: string;
|
||||||
|
color_scheme_title: string;
|
||||||
|
language_title: string;
|
||||||
|
device_settings_title: string;
|
||||||
|
device_channels_title: string;
|
||||||
|
device_region_title: string;
|
||||||
|
device_wifi_ssid: string;
|
||||||
|
device_wifi_psk: string;
|
||||||
|
save_changes_button: string;
|
||||||
|
no_nodes_message: string;
|
||||||
|
no_message_placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [deviceStatus, setDeviceStatus] = useState(
|
||||||
|
{} as Types.DeviceStatusEnum,
|
||||||
|
);
|
||||||
|
const [myNodeInfo, setMyNodeInfo] = useState({} as Protobuf.MyNodeInfo);
|
||||||
|
const [messages, setMessages] = useState(
|
||||||
|
[] as { message: Types.TextPacket; ack: false }[],
|
||||||
|
);
|
||||||
|
const [channels, setChannels] = useState([] as Protobuf.Channel[]);
|
||||||
|
const [nodes, setNodes] = useState([] as Types.NodeInfoPacket[]);
|
||||||
|
const [connection, setConnection] = useState({} as IHTTPConnection);
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const [lastMeshInterraction, setLastMeshInterraction] = useState(0);
|
||||||
|
const [preferences, setPreferences] = useState(
|
||||||
|
{} as Protobuf.RadioConfig_UserPreferences,
|
||||||
|
);
|
||||||
|
const [language, setLanguage] = useState(LanguageEnum.ENGLISH);
|
||||||
|
const [translations, setTranslations] = useState(Translations_English);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (language) {
|
||||||
|
case LanguageEnum.ENGLISH:
|
||||||
|
setTranslations(Translations_English);
|
||||||
|
break;
|
||||||
|
case LanguageEnum.JAPANESE:
|
||||||
|
setTranslations(Translations_Japanese);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const client = new Client();
|
||||||
|
const connection = client.createHTTPConnection();
|
||||||
|
// connection.connect(window.location.hostname, undefined, true);
|
||||||
|
connection.connect({
|
||||||
|
address: '192.168.105.71',
|
||||||
|
receiveBatchRequests: false,
|
||||||
|
tls: false,
|
||||||
|
fetchInterval: 2000,
|
||||||
|
});
|
||||||
|
setConnection(connection);
|
||||||
|
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
|
||||||
|
connection.onDeviceStatusEvent.subscribe((status) => {
|
||||||
|
setDeviceStatus(status);
|
||||||
|
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
|
||||||
|
setIsReady(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
|
||||||
|
connection.onTextPacketEvent.subscribe((message) => {
|
||||||
|
setMessages((messages) => [
|
||||||
|
...messages,
|
||||||
|
{ message: message, ack: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
connection.onNodeInfoPacketEvent.subscribe((node) => {
|
||||||
|
if (
|
||||||
|
nodes.findIndex(
|
||||||
|
(currentNode) => currentNode.data.num === node.data.num,
|
||||||
|
) >= 0
|
||||||
|
) {
|
||||||
|
setNodes(
|
||||||
|
nodes.map((currentNode) =>
|
||||||
|
currentNode.data.num === node.data.num ? node : currentNode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setNodes((nodes) => [...nodes, node]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.onAdminPacketEvent.subscribe((adminMessage) => {
|
||||||
|
switch (adminMessage.data.variant.oneofKind) {
|
||||||
|
case 'getRadioResponse':
|
||||||
|
if (adminMessage.data.variant.getRadioResponse.preferences) {
|
||||||
|
setPreferences(
|
||||||
|
adminMessage.data.variant.getRadioResponse.preferences,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
// case 'getChannelResponse':
|
||||||
|
// if (adminMessage.data.variant.getChannelResponse) {
|
||||||
|
// setChannels((channels) => [
|
||||||
|
// ...channels,
|
||||||
|
// adminMessage.data.variant.getChannelResponse,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connection.onMeshHeartbeat.subscribe(setLastMeshInterraction);
|
||||||
|
|
||||||
|
connection.onRoutingPacketEvent.subscribe((routingPacket) => {
|
||||||
|
console.log(routingPacket);
|
||||||
|
|
||||||
|
messages.map((message) => {
|
||||||
|
console.log(
|
||||||
|
`${
|
||||||
|
routingPacket.payloadVariant.oneofKind === 'decoded'
|
||||||
|
? routingPacket.payloadVariant.decoded.requestId
|
||||||
|
: null
|
||||||
|
} === ${message.message.packet.id}: ${
|
||||||
|
routingPacket.payloadVariant.oneofKind === 'decoded'
|
||||||
|
? routingPacket.payloadVariant.decoded.requestId
|
||||||
|
: null === message.message.packet.id
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// messages.find((message) => {
|
||||||
|
// message.message.packet.id === routingPacket.decoded.requestId;
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-screen w-screen">
|
||||||
|
{/* <Head>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/design/web/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="/design/web/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="/design/web/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="/design/web/site.webmanifest" />
|
||||||
|
<link
|
||||||
|
rel="mask-icon"
|
||||||
|
href="/design/web/safari-pinned-tab.svg"
|
||||||
|
color="#67ea94"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#67ea94" />
|
||||||
|
</Head> */}
|
||||||
|
<Header
|
||||||
|
status={deviceStatus}
|
||||||
|
IsReady={isReady}
|
||||||
|
LastMeshInterraction={lastMeshInterraction}
|
||||||
|
/>
|
||||||
|
<Main
|
||||||
|
IsReady={isReady}
|
||||||
|
Messages={messages}
|
||||||
|
MyNodeInfo={myNodeInfo}
|
||||||
|
Connection={connection}
|
||||||
|
Nodes={nodes}
|
||||||
|
Channels={channels}
|
||||||
|
Preferences={preferences}
|
||||||
|
Language={language}
|
||||||
|
SetLanguage={setLanguage}
|
||||||
|
Translations={translations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { FaBars, FaPaperPlane } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IHTTPConnection,
|
||||||
|
Protobuf,
|
||||||
|
Types,
|
||||||
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import type { LanguageEnum, languageTemplate } from './App';
|
||||||
|
import ChatMessage from './components/ChatMessage';
|
||||||
|
import Sidebar from './components/Sidebar';
|
||||||
|
|
||||||
|
interface MainProps {
|
||||||
|
Messages: { message: Types.TextPacket; ack: boolean }[];
|
||||||
|
Connection: IHTTPConnection;
|
||||||
|
MyNodeInfo: Protobuf.MyNodeInfo;
|
||||||
|
Nodes: Types.NodeInfoPacket[];
|
||||||
|
Channels: Protobuf.Channel[];
|
||||||
|
IsReady: boolean;
|
||||||
|
Preferences: Protobuf.RadioConfig_UserPreferences;
|
||||||
|
Language: LanguageEnum;
|
||||||
|
SetLanguage: Function;
|
||||||
|
Translations: languageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Main = (props: MainProps) => {
|
||||||
|
const [currentMessage, setCurrentMessage] = useState('');
|
||||||
|
const [mobileNavOpen, setMobileNavOpen] = useState(true);
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
if (props.IsReady) {
|
||||||
|
props.Connection.sendText(currentMessage, undefined, true);
|
||||||
|
setCurrentMessage('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col md:flex-row flex-grow space-2">
|
||||||
|
<div className="flex flex-col flex-grow container mx-auto">
|
||||||
|
<div className="flex flex-col flex-grow py-6 px-3 space-y-2">
|
||||||
|
{props.Messages.length ? (
|
||||||
|
props.Messages.map((message, Main) => (
|
||||||
|
<ChatMessage
|
||||||
|
nodes={props.Nodes}
|
||||||
|
key={Main}
|
||||||
|
message={message}
|
||||||
|
myId={props.MyNodeInfo.myNodeNum}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="m-auto text-2xl text-gray-500">
|
||||||
|
{props.Translations.no_messages_message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 w-full p-3">
|
||||||
|
<form
|
||||||
|
className="flex flex-wrap relative w-full"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.IsReady}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={`${props.Translations.no_message_placeholder}...`}
|
||||||
|
disabled={!props.IsReady}
|
||||||
|
value={currentMessage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setCurrentMessage(e.target.value);
|
||||||
|
}}
|
||||||
|
className={`p-3 placeholder-gray-400 text-gray-700 relative rounded-md shadow-md focus:outline-none w-full pr-10 ${
|
||||||
|
props.IsReady ? 'cursor-text' : 'cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="z-10 h-full text-gray-400 absolute w-8 right-0 py-4">
|
||||||
|
<FaPaperPlane
|
||||||
|
onClick={sendMessage}
|
||||||
|
className={`text-xl hover:text-gray-500 ${
|
||||||
|
props.IsReady ? 'cursor-pointer' : 'cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
className="flex p-3 text-xl hover:text-gray-500 text-gray-400 rounded-md shadow-md focus:outline-none cursor-pointer md:hidden"
|
||||||
|
onClick={() => {
|
||||||
|
setMobileNavOpen(!mobileNavOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaBars className="m-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Sidebar
|
||||||
|
IsReady={props.IsReady}
|
||||||
|
Nodes={props.Nodes}
|
||||||
|
Channels={props.Channels}
|
||||||
|
Preferences={props.Preferences}
|
||||||
|
Connection={props.Connection}
|
||||||
|
MobileNavOpen={mobileNavOpen}
|
||||||
|
Language={props.Language}
|
||||||
|
SetLanguage={props.SetLanguage}
|
||||||
|
Translations={props.Translations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Main;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FaCheckCircle, FaCircle, FaUser } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import type { Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
interface ChatMessageProps {
|
||||||
|
message: { message: Types.TextPacket; ack: boolean };
|
||||||
|
myId: number;
|
||||||
|
nodes: Types.NodeInfoPacket[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatMessage = (props: ChatMessageProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-end">
|
||||||
|
<div
|
||||||
|
className={`flex p-3 rounded-full shadow-md ${
|
||||||
|
props.message.message.packet.from !== props.myId
|
||||||
|
? 'bg-gray-300'
|
||||||
|
: 'bg-green-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FaUser className="m-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col container px-2 items-start">
|
||||||
|
<div
|
||||||
|
className={`px-4 py-2 rounded-md shadow-md ${
|
||||||
|
props.message.message.packet.from !== props.myId
|
||||||
|
? 'bg-gray-300'
|
||||||
|
: 'bg-green-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex text-xs text-gray-500 space-x-1">
|
||||||
|
<div className="font-medium">
|
||||||
|
{/* {
|
||||||
|
props.nodes.find(
|
||||||
|
(node) => node.data.num === props.message.message.packet.from,
|
||||||
|
).data.user.longName
|
||||||
|
} */}
|
||||||
|
</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>
|
||||||
|
{props.message.ack ? (
|
||||||
|
<FaCheckCircle className="my-auto" />
|
||||||
|
) : (
|
||||||
|
<FaCircle className=" text-lg my-auto animate-pulse" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatMessage;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FaBroadcastTower, FaMobileAlt } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
status: Types.DeviceStatusEnum;
|
||||||
|
IsReady: boolean;
|
||||||
|
LastMeshInterraction: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = (props: HeaderProps) => {
|
||||||
|
return (
|
||||||
|
<nav className="w-full shadow-md">
|
||||||
|
<div className="flex w-full container mx-auto justify-between px-6 py-4">
|
||||||
|
<img src="/design/typelogo/typelogo.svg" height="30" width="200" />
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex pl-4">
|
||||||
|
<div
|
||||||
|
className={`w-5 h-5 rounded-full ${
|
||||||
|
new Date(props.LastMeshInterraction) <
|
||||||
|
new Date(Date.now() - 40000)
|
||||||
|
? 'bg-red-400'
|
||||||
|
: new Date(props.LastMeshInterraction) <
|
||||||
|
new Date(Date.now() - 20000)
|
||||||
|
? 'bg-yellow-400'
|
||||||
|
: 'bg-green-400'
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<FaBroadcastTower className="m-auto ml-1 text-xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex pl-4">
|
||||||
|
<div
|
||||||
|
className={`w-5 h-5 rounded-full ${
|
||||||
|
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
|
||||||
|
? 'bg-red-400'
|
||||||
|
: props.status <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
|
||||||
|
!props.IsReady
|
||||||
|
? 'bg-yellow-400'
|
||||||
|
: props.IsReady
|
||||||
|
? 'bg-green-400'
|
||||||
|
: 'bg-gray-400'
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<FaMobileAlt className="m-auto ml-1 text-xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { ReactNode, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { FaCaretDown, FaCaretRight, FaSpinner } from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface NavItemProps {
|
||||||
|
isDropdown: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
isNested: boolean;
|
||||||
|
titleContent: ReactNode;
|
||||||
|
dropdownContent?: ReactNode;
|
||||||
|
onClick?: Function;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavItem = (props: NavItemProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.open) {
|
||||||
|
setNavItemOpen(props.open);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const [navItemOpen, setNavItemOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`flex w-full text-lg font-medium justify-between ${
|
||||||
|
navItemOpen && props.isNested ? 'bg-gray-100' : null
|
||||||
|
} ${props.isNested ? 'border-b px-3 py-1' : 'p-3'} ${
|
||||||
|
props.isDropdown && navItemOpen ? 'shadow-md' : 'border-b'
|
||||||
|
} ${
|
||||||
|
props.isDropdown || props.isNested
|
||||||
|
? 'hover:bg-gray-200 cursor-pointer'
|
||||||
|
: null
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (props.isDropdown) setNavItemOpen(!navItemOpen);
|
||||||
|
if (props.onClick) {
|
||||||
|
props.onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.titleContent}
|
||||||
|
{props.isDropdown && !props.isLoading ? (
|
||||||
|
navItemOpen ? (
|
||||||
|
<FaCaretDown className="my-auto group-hover:text-gray-700" />
|
||||||
|
) : (
|
||||||
|
<FaCaretRight className="my-auto group-hover:text-gray-700" />
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{props.isLoading ? (
|
||||||
|
<FaSpinner className="animate-spin my-auto" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{props.isDropdown ? (
|
||||||
|
<div
|
||||||
|
className={`duration-200 ease-in-out transition-all overflow-hidden max-h-0 border-l-8 ${
|
||||||
|
props.isNested ? 'border-gray-500' : 'border-gray-300'
|
||||||
|
} ${navItemOpen ? 'max-h-full' : null}`}
|
||||||
|
>
|
||||||
|
{props.dropdownContent}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavItem;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IHTTPConnection,
|
||||||
|
Protobuf,
|
||||||
|
Types,
|
||||||
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import type { LanguageEnum, languageTemplate } from '../App';
|
||||||
|
import SidebarChannels from './Sidebar/SidebarChannels';
|
||||||
|
import SidebarDeviceSettings from './Sidebar/SidebarDeviceSettings';
|
||||||
|
import SidebarNodes from './Sidebar/SidebarNodes';
|
||||||
|
import SidebarUISettings from './Sidebar/SidebarUISettings';
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
IsReady: boolean;
|
||||||
|
Nodes: Types.NodeInfoPacket[];
|
||||||
|
Channels: Protobuf.Channel[];
|
||||||
|
Preferences: Protobuf.RadioConfig_UserPreferences;
|
||||||
|
Connection: IHTTPConnection;
|
||||||
|
MobileNavOpen: boolean;
|
||||||
|
Language: LanguageEnum;
|
||||||
|
SetLanguage: Function;
|
||||||
|
Translations: languageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sidebar = (props: SidebarProps) => {
|
||||||
|
const updatePreferences = () => {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col rounded-md m-3 md:ml-0 shadow-md w-full max-w-sm ${
|
||||||
|
!props.MobileNavOpen ? 'hidden' : 'visible'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<SidebarNodes
|
||||||
|
IsReady={props.IsReady}
|
||||||
|
Nodes={props.Nodes}
|
||||||
|
Translations={props.Translations}
|
||||||
|
/>
|
||||||
|
<SidebarDeviceSettings
|
||||||
|
IsReady={props.IsReady}
|
||||||
|
Preferences={props.Preferences}
|
||||||
|
Connection={props.Connection}
|
||||||
|
Translations={props.Translations}
|
||||||
|
/>
|
||||||
|
<SidebarChannels
|
||||||
|
IsReady={props.IsReady}
|
||||||
|
Channels={props.Channels}
|
||||||
|
Translations={props.Translations}
|
||||||
|
/>
|
||||||
|
<div className="flex-grow border-b"></div>
|
||||||
|
<SidebarUISettings
|
||||||
|
Language={props.Language}
|
||||||
|
SetLanguage={props.SetLanguage}
|
||||||
|
Translations={props.Translations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FaStream } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import type { languageTemplate } from '../../App';
|
||||||
|
import NavItem from '../NavItem';
|
||||||
|
|
||||||
|
interface SidebarChannelsProps {
|
||||||
|
IsReady: boolean;
|
||||||
|
Channels: Protobuf.Channel[];
|
||||||
|
Translations: languageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarChannels = (props: SidebarChannelsProps) => {
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
isDropdown={true}
|
||||||
|
open={false}
|
||||||
|
isNested={false}
|
||||||
|
titleContent={
|
||||||
|
<div className="flex">
|
||||||
|
<FaStream className="my-auto mr-2" />
|
||||||
|
{props.Translations.device_channels_title}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
isLoading={!props.IsReady}
|
||||||
|
dropdownContent={
|
||||||
|
<>
|
||||||
|
{props.Channels.map((channel, index) => {
|
||||||
|
if (channel.role !== Protobuf.Channel_Role.DISABLED)
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
key={index}
|
||||||
|
isDropdown={true}
|
||||||
|
isNested={true}
|
||||||
|
titleContent={
|
||||||
|
<div className="flex">
|
||||||
|
{channel.index} - {Protobuf.Channel_Role[channel.role]}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
dropdownContent={
|
||||||
|
<NavItem
|
||||||
|
isDropdown={false}
|
||||||
|
isNested={false}
|
||||||
|
titleContent={
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex justify-between border-b hover:bg-gray-200">
|
||||||
|
<p>Bandwidth:</p>
|
||||||
|
<code className="bg-gray-200 rounded-full px-2">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
{channel.settings?.modemConfig
|
||||||
|
? Protobuf.ChannelSettings_ModemConfig[
|
||||||
|
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">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
{channel.settings?.downlinkEnabled
|
||||||
|
? 'true'
|
||||||
|
: 'false'}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SidebarChannels;
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FaSave, FaUserCog } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { IHTTPConnection, Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import type { languageTemplate } from '../../App';
|
||||||
|
import NavItem from '../NavItem';
|
||||||
|
|
||||||
|
interface SidebarDeviceSettingsProps {
|
||||||
|
IsReady: boolean;
|
||||||
|
Preferences: Protobuf.RadioConfig_UserPreferences;
|
||||||
|
Connection: IHTTPConnection;
|
||||||
|
Translations: languageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarDeviceSettings = (props: SidebarDeviceSettingsProps) => {
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
isDropdown={true}
|
||||||
|
open={false}
|
||||||
|
isNested={false}
|
||||||
|
titleContent={
|
||||||
|
<div className="flex">
|
||||||
|
<FaUserCog className="my-auto mr-2" />
|
||||||
|
{props.Translations.device_settings_title}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
isLoading={!props.IsReady}
|
||||||
|
dropdownContent={
|
||||||
|
<>
|
||||||
|
<div className="flex whitespace-nowrap p-3 justify-between border-b">
|
||||||
|
<div className="my-auto">
|
||||||
|
{props.Translations.device_region_title}
|
||||||
|
</div>
|
||||||
|
<div className="flex shadow-md rounded-md ml-2">
|
||||||
|
<select
|
||||||
|
value={props.Preferences?.region ?? Protobuf.RegionCode.Unset}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.Preferences.region = parseInt(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={Protobuf.RegionCode.ANZ}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.ANZ]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.CN}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.CN]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.EU433}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.EU433]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.EU865}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.EU865]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.JP}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.JP]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.KR}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.KR]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.TW}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.TW]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.US}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.US]}
|
||||||
|
</option>
|
||||||
|
<option value={Protobuf.RegionCode.Unset}>
|
||||||
|
{Protobuf.RegionCode[Protobuf.RegionCode.Unset]}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex whitespace-nowrap p-3 justify-between border-b">
|
||||||
|
<div className="my-auto">{props.Translations.device_wifi_ssid}</div>
|
||||||
|
<div className="flex shadow-md rounded-md ml-2">
|
||||||
|
<input
|
||||||
|
onChange={() => {}}
|
||||||
|
type="text"
|
||||||
|
value={props.Preferences.wifiSsid}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex whitespace-nowrap p-3 justify-between border-b">
|
||||||
|
<div className="my-auto">{props.Translations.device_wifi_psk}</div>
|
||||||
|
<div className="flex shadow-md rounded-md ml-2">
|
||||||
|
<input type="password" value={props.Preferences.wifiPassword} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex group p-1 bg-gray-100 cursor-pointer hover:bg-gray-200 border-b">
|
||||||
|
<div
|
||||||
|
className="flex m-auto font-medium group-hover:text-gray-700"
|
||||||
|
onClick={() => {
|
||||||
|
props.Connection.setPreferences(props.Preferences);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaSave className="m-auto mr-2 group-hover:text-gray-700" />
|
||||||
|
{props.Translations.save_changes_button}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SidebarDeviceSettings;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { FaDesktop, FaUsers } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import type { Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import type { languageTemplate } from '../../App';
|
||||||
|
import NavItem from '../NavItem';
|
||||||
|
|
||||||
|
interface sidebarNodesProps {
|
||||||
|
IsReady: boolean;
|
||||||
|
Nodes: Types.NodeInfoPacket[];
|
||||||
|
Translations: languageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarNodes = (props: sidebarNodesProps) => {
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
isDropdown={true}
|
||||||
|
open={false}
|
||||||
|
isNested={false}
|
||||||
|
titleContent={
|
||||||
|
<div className="flex">
|
||||||
|
<FaUsers className="my-auto mr-2" />
|
||||||
|
{props.Translations.nodes_title}
|
||||||
|
<div className="flex m-auto rounded-full bg-gray-300 w-6 h-6 text-sm ml-2">
|
||||||
|
<div className="m-auto">{props.Nodes.length ?? 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
isLoading={!props.IsReady}
|
||||||
|
dropdownContent={
|
||||||
|
props.Nodes.length ? (
|
||||||
|
props.Nodes.map((node, index) => (
|
||||||
|
<NavItem
|
||||||
|
key={index}
|
||||||
|
isDropdown={true}
|
||||||
|
isNested={true}
|
||||||
|
open={false}
|
||||||
|
titleContent={
|
||||||
|
<div key={index} className="flex">
|
||||||
|
<FaDesktop className="my-auto mr-2" />
|
||||||
|
<div className="m-auto">{node.data.user?.longName}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
dropdownContent={
|
||||||
|
<NavItem
|
||||||
|
isDropdown={false}
|
||||||
|
isNested={true}
|
||||||
|
titleContent={
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
SNR:{' '}
|
||||||
|
{node.packet?.rxSnr ? node.packet.rxSnr : 'Unknown'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
RSSI:{' '}
|
||||||
|
{node.packet?.rxRssi ? node.packet.rxRssi : 'Unknown'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Last heard:{' '}
|
||||||
|
{node.data?.lastHeard ? node.data.lastHeard : 'Unknown'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Loc:{' '}
|
||||||
|
{node.data?.position
|
||||||
|
? `alt: ${node.data?.position.altitude}, lat: ${node.data?.position.latitudeI}, lng: ${node.data?.position.longitudeI}, time: ${node.data?.position.time}, batt: ${node.data?.position.batteryLevel}`
|
||||||
|
: 'Unknown'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex border-b border-gray-300">
|
||||||
|
<div className="m-auto p-3 text-gray-500">
|
||||||
|
{props.Translations.no_nodes_message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SidebarNodes;
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// import Flags from 'country-flag-icons/react/3x2';
|
||||||
|
import { FaCog, FaLaptop, FaMoon, FaSun } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import type { languageTemplate } from '../../App';
|
||||||
|
import { LanguageEnum } from '../../App';
|
||||||
|
import ToggleSwitch from '../basic/ToggleSwitch';
|
||||||
|
import NavItem from '../NavItem';
|
||||||
|
|
||||||
|
interface SidebarUISettingsProps {
|
||||||
|
Language: LanguageEnum;
|
||||||
|
SetLanguage: Function;
|
||||||
|
Translations: languageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarUISettings = (props: SidebarUISettingsProps) => {
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
isDropdown={true}
|
||||||
|
isNested={false}
|
||||||
|
titleContent={
|
||||||
|
<div className="flex">
|
||||||
|
<FaCog className="my-auto mr-2" />
|
||||||
|
{props.Translations.ui_settings_title}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
dropdownContent={
|
||||||
|
<>
|
||||||
|
<NavItem
|
||||||
|
isDropdown={false}
|
||||||
|
isNested={true}
|
||||||
|
titleContent={
|
||||||
|
<>
|
||||||
|
<div className="my-auto">
|
||||||
|
{props.Translations.color_scheme_title}
|
||||||
|
</div>
|
||||||
|
<div className="flex shadow-md rounded-md ml-2">
|
||||||
|
<div className="bg-gray-200 flex group p-2 rounded-l-md border border-gray-300 hover:bg-gray-200 cursor-pointer">
|
||||||
|
<FaSun className="m-auto group-hover:text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div className="flex group p-2 border border-gray-300 hover:bg-gray-200 cursor-pointer">
|
||||||
|
<FaMoon className="m-auto group-hover:text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div className="flex group p-2 rounded-r-md border border-gray-300 hover:bg-gray-200 cursor-pointer">
|
||||||
|
<FaLaptop className="m-auto group-hover:text-gray-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<NavItem
|
||||||
|
isDropdown={true}
|
||||||
|
isNested={true}
|
||||||
|
open={false}
|
||||||
|
titleContent={
|
||||||
|
<div className="flex my-auto">
|
||||||
|
{/* {props.Translations.language_title}
|
||||||
|
{props.Language === LanguageEnum.ENGLISH ? (
|
||||||
|
<Flags.US className="ml-2 w-8 shadow-md" />
|
||||||
|
) : props.Language === LanguageEnum.JAPANESE ? (
|
||||||
|
<Flags.JP className="ml-2 w-8 shadow-md" />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
dropdownContent={
|
||||||
|
<>
|
||||||
|
<NavItem
|
||||||
|
onClick={() => {
|
||||||
|
props.SetLanguage(LanguageEnum.ENGLISH);
|
||||||
|
}}
|
||||||
|
isDropdown={false}
|
||||||
|
isNested={true}
|
||||||
|
titleContent={
|
||||||
|
<>{/* English <Flags.US className="w-8 shadow-md" /> */}</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<NavItem
|
||||||
|
onClick={() => {
|
||||||
|
props.SetLanguage(LanguageEnum.JAPANESE);
|
||||||
|
}}
|
||||||
|
isDropdown={false}
|
||||||
|
isNested={true}
|
||||||
|
titleContent={
|
||||||
|
<>{/* 日本語 <Flags.JP className="w-8 shadow-md" /> */}</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<NavItem
|
||||||
|
isDropdown={false}
|
||||||
|
isNested={true}
|
||||||
|
open={false}
|
||||||
|
titleContent={
|
||||||
|
<>
|
||||||
|
<div className="">Test</div>
|
||||||
|
<ToggleSwitch active={true} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SidebarUISettings;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TimelineProps {}
|
||||||
|
|
||||||
|
const Timeline = (props: TimelineProps) => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Timeline;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TimelineItemProps {
|
||||||
|
time: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimelineItem = (props: TimelineItemProps) => {
|
||||||
|
return <div className={`rounded-full h-6 w-6 bg-${props.color}`}>Test</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimelineItem;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface ToggleSwitchProps {
|
||||||
|
active: boolean;
|
||||||
|
toggle?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleSwitch = (props: ToggleSwitchProps) => {
|
||||||
|
const [active, setActive] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActive(props.active);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setActive(!active);
|
||||||
|
}}
|
||||||
|
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
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`bg-white w-4 h-4 rounded-full shadow-md transform duration-300 ease-in-out ${
|
||||||
|
active ? 'translate-x-6' : null
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToggleSwitch;
|
||||||
+3
-13
@@ -1,13 +1,3 @@
|
|||||||
body {
|
@tailwind base;
|
||||||
margin: 0;
|
@tailwind components;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
@tailwind utilities;
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
|
||||||
<g fill="#61DAFB">
|
|
||||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4a43.8 43.8 0 00-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9a487.8 487.8 0 00-41.6-50c32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9a467 467 0 00-63.6 11c-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4a44 44 0 0022.5 5.6c27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7a450.4 450.4 0 01-13.5 39.5 473.3 473.3 0 00-27.5-47.4c14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5a532.7 532.7 0 01-24.1 38.2 520.3 520.3 0 01-90.2.1 551.2 551.2 0 01-45-77.8 521.5 521.5 0 0144.8-78.1 520.3 520.3 0 0190.2-.1 551.2 551.2 0 0145 77.8 560 560 0 01-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8a448.8 448.8 0 01-41.2 8 552.4 552.4 0 0027.4-47.8zM421.2 430a412.3 412.3 0 01-27.8-32 619 619 0 0055.3 0c-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9a451.2 451.2 0 01-41-7.9c3.7-12.9 8.3-26.2 13.5-39.5a473.3 473.3 0 0027.5 47.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32a619 619 0 00-55.3 0c9-11.7 18.3-22.4 27.5-32zm-74 58.9a552.4 552.4 0 00-27.4 47.7c-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9a473.5 473.5 0 00-22.2 60.6c-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9a487.8 487.8 0 0041.6 50c-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9a467 467 0 0063.6-11 280 280 0 015.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9a473.5 473.5 0 0022.2-60.6c9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
|
||||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
import type { languageTemplate } from '../App';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
no_messages_message: 'No messages yet',
|
||||||
|
ui_settings_title: 'UI Settings',
|
||||||
|
nodes_title: 'Nodes',
|
||||||
|
device_settings_title: 'Device Settings',
|
||||||
|
device_channels_title: 'Channels',
|
||||||
|
color_scheme_title: 'Color scheme',
|
||||||
|
language_title: 'Language',
|
||||||
|
device_region_title: 'Device Region',
|
||||||
|
device_wifi_ssid: 'WiFi SSID',
|
||||||
|
device_wifi_psk: 'WiFi PSK',
|
||||||
|
save_changes_button: 'Save changes',
|
||||||
|
no_nodes_message: 'No nodes found',
|
||||||
|
no_message_placeholder: 'Enter Message',
|
||||||
|
} as languageTemplate;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { languageTemplate } from '../App';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
no_messages_message: 'まだメッセージはありません',
|
||||||
|
ui_settings_title: 'UI設定',
|
||||||
|
nodes_title: 'ノード',
|
||||||
|
device_settings_title: 'デバイスの設定',
|
||||||
|
color_scheme_title: 'カラースキーム',
|
||||||
|
language_title: '言語',
|
||||||
|
device_region_title: 'デバイスリージョン',
|
||||||
|
device_wifi_ssid: 'WiFi名',
|
||||||
|
device_wifi_psk: 'WiFiパスワード',
|
||||||
|
save_changes_button: '変更内容を保存',
|
||||||
|
no_nodes_message: 'ノードが見つかりません',
|
||||||
|
no_message_placeholder: 'メッセージを入力してください',
|
||||||
|
} as languageTemplate;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
purge: ['./src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
darkMode: false, // or 'media' or 'class'
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
Arguments:
|
||||||
|
/usr/bin/node /usr/share/yarn/bin/yarn.js add -D tailwindcss@latest postcss@latest autoprefixer@latest
|
||||||
|
|
||||||
|
PATH:
|
||||||
|
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/mnt/c/Program Files/AdoptOpenJDK/jdk-11.0.10.9-hotspot/bin:/mnt/c/Program Files (x86)/Common Files/Oracle/Java/javapath:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files (x86)/NVIDIA Corporation/PhysX/Common:/mnt/c/Program Files/Git/cmd:/mnt/c/Program Files/nodejs/:/mnt/c/Users/sacha/AppData/Local/Programs/Python/Python39/Scripts/:/mnt/c/Users/sacha/AppData/Local/Programs/Python/Python39/:/mnt/c/Users/sacha/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/sacha/AppData/Local/Programs/Microsoft VS Code/bin:/mnt/c/Users/sacha/AppData/Roaming/npm:/home/sachaw/.yarn/bin
|
||||||
|
|
||||||
|
Yarn version:
|
||||||
|
1.22.5
|
||||||
|
|
||||||
|
Node version:
|
||||||
|
15.12.0
|
||||||
|
|
||||||
|
Platform:
|
||||||
|
linux x64
|
||||||
|
|
||||||
|
Trace:
|
||||||
|
Error: getaddrinfo EAI_AGAIN registry.yarnpkg.com
|
||||||
|
at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:69:26)
|
||||||
|
|
||||||
|
npm manifest:
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"start": "snowpack dev",
|
||||||
|
"build": "snowpack build",
|
||||||
|
"test": "web-test-runner \"src/**/*.test.tsx\"",
|
||||||
|
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||||
|
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^17.0.0",
|
||||||
|
"react-dom": "^17.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@snowpack/plugin-dotenv": "^2.0.5",
|
||||||
|
"@snowpack/plugin-react-refresh": "^2.4.0",
|
||||||
|
"@snowpack/plugin-typescript": "^1.2.0",
|
||||||
|
"@snowpack/web-test-runner-plugin": "^0.2.0",
|
||||||
|
"@testing-library/react": "^11.0.0",
|
||||||
|
"@types/chai": "^4.2.13",
|
||||||
|
"@types/mocha": "^8.2.0",
|
||||||
|
"@types/react": "^17.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0",
|
||||||
|
"@types/snowpack-env": "^2.3.2",
|
||||||
|
"@web/test-runner": "^0.12.0",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
|
"snowpack": "^3.0.1",
|
||||||
|
"typescript": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yarn manifest:
|
||||||
|
No manifest
|
||||||
|
|
||||||
|
Lockfile:
|
||||||
|
No lockfile
|
||||||
Reference in New Issue
Block a user