WIP
This commit is contained in:
+15
-11
@@ -16,27 +16,31 @@
|
|||||||
"@meshtastic/meshtasticjs": "^0.6.13",
|
"@meshtastic/meshtasticjs": "^0.6.13",
|
||||||
"@reduxjs/toolkit": "^1.6.0",
|
"@reduxjs/toolkit": "^1.6.0",
|
||||||
"boring-avatars": "^1.5.8",
|
"boring-avatars": "^1.5.8",
|
||||||
|
"framer-motion": "^4.1.17",
|
||||||
|
"i18next": "^20.3.5",
|
||||||
|
"i18next-browser-languagedetector": "^6.1.2",
|
||||||
"observable-hooks": "^4.0.5",
|
"observable-hooks": "^4.0.5",
|
||||||
"react": "^18.0.0-alpha-568dc3532",
|
"react": "^18.0.0-alpha-6bf111772-20210701",
|
||||||
"react-dom": "^18.0.0-alpha-568dc3532",
|
"react-dom": "^18.0.0-alpha-6bf111772-20210701",
|
||||||
"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-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"redux-observable": "2.0.0-rc.2",
|
"redux-observable": "^2.0.0",
|
||||||
"rxjs": "^7.1.0"
|
"rxjs": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snowpack/plugin-dotenv": "^2.0.5",
|
"@snowpack/plugin-dotenv": "^2.0.5",
|
||||||
"@snowpack/plugin-postcss": "^1.4.1",
|
"@snowpack/plugin-postcss": "^1.4.3",
|
||||||
"@snowpack/plugin-react-refresh": "^2.5.0",
|
"@snowpack/plugin-react-refresh": "^2.5.0",
|
||||||
"@snowpack/plugin-typescript": "^1.2.0",
|
"@snowpack/plugin-typescript": "^1.2.0",
|
||||||
"@types/eslint": "^7.2.13",
|
"@types/eslint": "^7.2.13",
|
||||||
"@types/react": "^17.0.11",
|
"@types/react": "^17.0.13",
|
||||||
"@types/react-dom": "^17.0.8",
|
"@types/react-dom": "^17.0.8",
|
||||||
"@types/react-redux": "^7.1.16",
|
"@types/react-redux": "^7.1.16",
|
||||||
"@types/snowpack-env": "^2.3.3",
|
"@types/snowpack-env": "^2.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
"@typescript-eslint/eslint-plugin": "^4.28.1",
|
||||||
"@typescript-eslint/parser": "^4.28.0",
|
"@typescript-eslint/parser": "^4.28.1",
|
||||||
"autoprefixer": "^10.2.6",
|
"autoprefixer": "^10.2.6",
|
||||||
"eslint": "^7.29.0",
|
"eslint": "^7.29.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
@@ -47,9 +51,9 @@
|
|||||||
"gzipper": "^5.0.0",
|
"gzipper": "^5.0.0",
|
||||||
"postcss": "^8.3.5",
|
"postcss": "^8.3.5",
|
||||||
"postcss-cli": "^8.3.1",
|
"postcss-cli": "^8.3.1",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.2",
|
||||||
"snowpack": "^3.6.0",
|
"snowpack": "^3.7.1",
|
||||||
"tailwindcss": "^2.2.2",
|
"tailwindcss": "^2.2.4",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-8
@@ -12,18 +12,19 @@ import {
|
|||||||
Types,
|
Types,
|
||||||
} from '@meshtastic/meshtasticjs';
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import Header from './components/Header';
|
import { Header } from './components/Header';
|
||||||
import Main from './Main';
|
import { useAppDispatch } from './hooks/redux';
|
||||||
|
import { Main } from './Main';
|
||||||
|
import { setMyId } from './slices/meshtasticSlice';
|
||||||
import { channelSubject$, nodeSubject$, preferencesSubject$ } from './streams';
|
import { channelSubject$, nodeSubject$, preferencesSubject$ } from './streams';
|
||||||
|
|
||||||
const App = (): JSX.Element => {
|
const App = (): JSX.Element => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [deviceStatus, setDeviceStatus] =
|
const [deviceStatus, setDeviceStatus] =
|
||||||
React.useState<Types.DeviceStatusEnum>(
|
React.useState<Types.DeviceStatusEnum>(
|
||||||
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
|
Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
|
||||||
);
|
);
|
||||||
const [myNodeInfo, setMyNodeInfo] = React.useState<Protobuf.MyNodeInfo>(
|
|
||||||
Protobuf.MyNodeInfo.create(),
|
|
||||||
);
|
|
||||||
const [connection, setConnection] = React.useState<
|
const [connection, setConnection] = React.useState<
|
||||||
ISerialConnection | IHTTPConnection | IBLEConnection
|
ISerialConnection | IHTTPConnection | IBLEConnection
|
||||||
>(new IHTTPConnection());
|
>(new IHTTPConnection());
|
||||||
@@ -58,8 +59,13 @@ const App = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const myNodeInfoEvent =
|
// const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
|
||||||
connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo);
|
|
||||||
|
const myNodeInfoEvent = connection.onMyNodeInfoEvent.subscribe(
|
||||||
|
(nodeInfo) => {
|
||||||
|
dispatch(setMyId(nodeInfo.myNodeNum));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const nodeInfoPacketEvent = connection.onNodeInfoPacketEvent.subscribe(
|
const nodeInfoPacketEvent = connection.onNodeInfoPacketEvent.subscribe(
|
||||||
(node) => nodeSubject$.next(node),
|
(node) => nodeSubject$.next(node),
|
||||||
@@ -108,7 +114,6 @@ const App = (): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
<Main
|
<Main
|
||||||
isReady={isReady}
|
isReady={isReady}
|
||||||
myNodeInfo={myNodeInfo}
|
|
||||||
connection={connection}
|
connection={connection}
|
||||||
darkmode={darkmode}
|
darkmode={darkmode}
|
||||||
setDarkmode={setDarkmode}
|
setDarkmode={setDarkmode}
|
||||||
|
|||||||
+12
-36
@@ -1,35 +1,30 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IBLEConnection,
|
IBLEConnection,
|
||||||
IHTTPConnection,
|
IHTTPConnection,
|
||||||
ISerialConnection,
|
ISerialConnection,
|
||||||
Protobuf,
|
|
||||||
Types,
|
Types,
|
||||||
} from '@meshtastic/meshtasticjs';
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import ChatMessage from './components/ChatMessage';
|
import { ChatMessage } from './components/ChatMessage';
|
||||||
import MessageBox from './components/MessageBox';
|
import { MessageBox } from './components/MessageBox';
|
||||||
import Sidebar from './components/Sidebar';
|
import { Sidebar } from './components/Sidebar';
|
||||||
import { useTranslationsContextValue } from './hooks/useTranslationsContextValue';
|
|
||||||
import { TranslationsContext } from './translations/TranslationsContext';
|
|
||||||
|
|
||||||
interface MainProps {
|
interface MainProps {
|
||||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||||
myNodeInfo: Protobuf.MyNodeInfo;
|
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
darkmode: boolean;
|
darkmode: boolean;
|
||||||
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
|
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Main = (props: MainProps): JSX.Element => {
|
export const Main = (props: MainProps): JSX.Element => {
|
||||||
const translationsContextValue = useTranslationsContextValue();
|
|
||||||
const { translations } = React.useContext(TranslationsContext);
|
|
||||||
|
|
||||||
const [messages, setMessages] = React.useState<
|
const [messages, setMessages] = React.useState<
|
||||||
{ message: Types.TextPacket; ack: boolean }[]
|
{ message: Types.TextPacket; ack: boolean }[]
|
||||||
>([]);
|
>([]);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState<boolean>(false);
|
const { t } = useTranslation();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const textPacketEvent = props.connection.onTextPacketEvent.subscribe(
|
const textPacketEvent = props.connection.onTextPacketEvent.subscribe(
|
||||||
@@ -65,41 +60,22 @@ const Main = (props: MainProps): JSX.Element => {
|
|||||||
}, [props.connection, messages]);
|
}, [props.connection, messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TranslationsContext.Provider value={translationsContextValue}>
|
|
||||||
<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 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 container mx-auto">
|
||||||
<div className="flex flex-col flex-grow py-6 space-y-2">
|
<div className="flex flex-col flex-grow py-6 space-y-2">
|
||||||
{messages.length ? (
|
{messages.length ? (
|
||||||
messages.map((message, Main) => (
|
messages.map((message, Main) => (
|
||||||
<ChatMessage
|
<ChatMessage key={Main} message={message} />
|
||||||
key={Main}
|
|
||||||
message={message}
|
|
||||||
myId={props.myNodeInfo.myNodeNum}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="m-auto text-2xl text-gray-500">
|
<div className="m-auto text-2xl text-gray-500">
|
||||||
{translations.no_messages_message}
|
{t('placeholder.no_messages')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<MessageBox
|
<MessageBox connection={props.connection} isReady={props.isReady} />
|
||||||
connection={props.connection}
|
|
||||||
isReady={props.isReady}
|
|
||||||
sidebarOpen={sidebarOpen}
|
|
||||||
setSidebarOpen={setSidebarOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Sidebar
|
<Sidebar connection={props.connection} />
|
||||||
myId={props.myNodeInfo.myNodeNum}
|
|
||||||
sidebarOpen={sidebarOpen}
|
|
||||||
darkmode={props.darkmode}
|
|
||||||
setDarkmode={props.setDarkmode}
|
|
||||||
connection={props.connection}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</TranslationsContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Main;
|
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import {
|
|||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
import type { Types } from '@meshtastic/meshtasticjs';
|
import type { Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import { useAppSelector } from '../hooks/redux';
|
||||||
import { nodeResource } from '../streams';
|
import { nodeResource } from '../streams';
|
||||||
|
|
||||||
interface ChatMessageProps {
|
interface ChatMessageProps {
|
||||||
message: { message: Types.TextPacket; ack: boolean };
|
message: { message: Types.TextPacket; ack: boolean };
|
||||||
myId: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatMessage = (props: ChatMessageProps): JSX.Element => {
|
export const ChatMessage = (props: ChatMessageProps): JSX.Element => {
|
||||||
const nodeSource = useObservableSuspense(nodeResource);
|
const nodeSource = useObservableSuspense(nodeResource);
|
||||||
|
const myId = useAppSelector((state) => state.meshtastic.myId);
|
||||||
|
|
||||||
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
|
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
|
||||||
|
|
||||||
@@ -54,9 +55,16 @@ const ChatMessage = (props: ChatMessageProps): JSX.Element => {
|
|||||||
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
|
colors={['#213435', '#46685B', '#648A64', '#A6B985', '#E1E3AC']}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col container px-2 items-start">
|
<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
|
<div
|
||||||
className={`px-4 py-2 rounded-md shadow-md ${
|
className={`px-4 py-2 rounded-md shadow-md ${
|
||||||
props.message.message.packet.from !== props.myId
|
props.message.message.packet.from !== myId
|
||||||
? 'bg-gray-300'
|
? 'bg-gray-300'
|
||||||
: 'bg-green-200'
|
: 'bg-green-200'
|
||||||
}`}
|
}`}
|
||||||
@@ -83,9 +91,8 @@ const ChatMessage = (props: ChatMessageProps): JSX.Element => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatMessage;
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type {
|
|||||||
} from '@meshtastic/meshtasticjs';
|
} from '@meshtastic/meshtasticjs';
|
||||||
import { Types } from '@meshtastic/meshtasticjs';
|
import { Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import Logo from './logo';
|
import { Logo } from './Logo';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
status: Types.DeviceStatusEnum;
|
status: Types.DeviceStatusEnum;
|
||||||
@@ -21,9 +21,9 @@ interface HeaderProps {
|
|||||||
connection: IHTTPConnection | ISerialConnection | IBLEConnection;
|
connection: IHTTPConnection | ISerialConnection | IBLEConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = (props: HeaderProps): JSX.Element => {
|
export const Header = (props: HeaderProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<nav className="w-full shadow-md">
|
<nav className="select-none w-full shadow-md">
|
||||||
<div className="flex w-full container mx-auto justify-between px-6 py-4">
|
<div className="flex w-full container mx-auto justify-between px-6 py-4">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div></div>
|
<div></div>
|
||||||
@@ -34,10 +34,10 @@ const Header = (props: HeaderProps): JSX.Element => {
|
|||||||
className={`w-5 h-5 rounded-full ${
|
className={`w-5 h-5 rounded-full ${
|
||||||
new Date(props.LastMeshInterraction) <
|
new Date(props.LastMeshInterraction) <
|
||||||
new Date(Date.now() - 40000)
|
new Date(Date.now() - 40000)
|
||||||
? 'bg-red-400'
|
? 'bg-red-400 animate-pulse'
|
||||||
: new Date(props.LastMeshInterraction) <
|
: new Date(props.LastMeshInterraction) <
|
||||||
new Date(Date.now() - 20000)
|
new Date(Date.now() - 20000)
|
||||||
? 'bg-yellow-400'
|
? 'bg-yellow-400 animate-pulse'
|
||||||
: 'bg-green-400'
|
: 'bg-green-400'
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
@@ -53,10 +53,10 @@ const Header = (props: HeaderProps): JSX.Element => {
|
|||||||
<div
|
<div
|
||||||
className={`w-5 h-5 rounded-full ${
|
className={`w-5 h-5 rounded-full ${
|
||||||
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
|
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED
|
||||||
? 'bg-red-400'
|
? 'bg-red-400 animate-pulse'
|
||||||
: props.status <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
|
: props.status <= Types.DeviceStatusEnum.DEVICE_CONFIGURING &&
|
||||||
!props.IsReady
|
!props.IsReady
|
||||||
? 'bg-yellow-400'
|
? 'bg-yellow-400 animate-pulse'
|
||||||
: props.IsReady
|
: props.IsReady
|
||||||
? 'bg-green-400'
|
? 'bg-green-400'
|
||||||
: 'bg-gray-400'
|
: 'bg-gray-400'
|
||||||
@@ -69,5 +69,3 @@ const Header = (props: HeaderProps): JSX.Element => {
|
|||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Logo = (): JSX.Element => {
|
export const Logo = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
height="30"
|
height="30"
|
||||||
@@ -82,5 +82,3 @@ const Logo = (): JSX.Element => {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Logo;
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
|
import { MenuIcon, PaperAirplaneIcon } from '@heroicons/react/outline';
|
||||||
import type {
|
import type {
|
||||||
IBLEConnection,
|
IBLEConnection,
|
||||||
@@ -7,17 +9,15 @@ import type {
|
|||||||
ISerialConnection,
|
ISerialConnection,
|
||||||
} from '@meshtastic/meshtasticjs';
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { TranslationsContext } from '../translations/TranslationsContext';
|
import { useAppDispatch } from '../hooks/redux';
|
||||||
|
import { toggleSidebar } from '../slices/appSlice';
|
||||||
|
|
||||||
export interface MessageBoxProps {
|
export interface MessageBoxProps {
|
||||||
sidebarOpen: boolean;
|
|
||||||
setSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageBox = (props: MessageBoxProps): JSX.Element => {
|
export const MessageBox = (props: MessageBoxProps): JSX.Element => {
|
||||||
const { translations } = React.useContext(TranslationsContext);
|
|
||||||
const [currentMessage, setCurrentMessage] = React.useState('');
|
const [currentMessage, setCurrentMessage] = React.useState('');
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (props.isReady) {
|
if (props.isReady) {
|
||||||
@@ -25,12 +25,15 @@ const MessageBox = (props: MessageBoxProps): JSX.Element => {
|
|||||||
setCurrentMessage('');
|
setCurrentMessage('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex text-lg font-medium space-x-2 md:space-x-0 w-full">
|
<div className="flex text-lg font-medium space-x-2 md:space-x-0 w-full">
|
||||||
<div
|
<div
|
||||||
className="flex p-3 text-xl hover:text-gray-500 text-gray-400 rounded-md border shadow-md focus:outline-none cursor-pointer md:hidden"
|
className="flex p-3 text-xl hover:text-gray-500 text-gray-400 rounded-md border shadow-md focus:outline-none cursor-pointer md:hidden"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.setSidebarOpen(!props.sidebarOpen);
|
dispatch(toggleSidebar());
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuIcon className="m-auto h-6 2-6" />
|
<MenuIcon className="m-auto h-6 2-6" />
|
||||||
@@ -45,7 +48,7 @@ const MessageBox = (props: MessageBoxProps): JSX.Element => {
|
|||||||
{props.isReady}
|
{props.isReady}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={`${translations.no_message_placeholder}...`}
|
placeholder={`${t('placeholder.no_messages')}...`}
|
||||||
disabled={!props.isReady}
|
disabled={!props.isReady}
|
||||||
value={currentMessage}
|
value={currentMessage}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -67,5 +70,3 @@ const MessageBox = (props: MessageBoxProps): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MessageBox;
|
|
||||||
|
|||||||
+10
-14
@@ -6,33 +6,29 @@ import type {
|
|||||||
ISerialConnection,
|
ISerialConnection,
|
||||||
} from '@meshtastic/meshtasticjs';
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import Channels from './Sidebar/Channels/Index';
|
import { useAppSelector } from '../hooks/redux';
|
||||||
import Device from './Sidebar/Device/Index';
|
import { Channels } from './Sidebar/Channels/Index';
|
||||||
import Nodes from './Sidebar/Nodes/Index';
|
import { Device } from './Sidebar/Device/Index';
|
||||||
import UI from './Sidebar/UI/Index';
|
import { Nodes } from './Sidebar/Nodes/Index';
|
||||||
|
import { UI } from './Sidebar/UI/Index';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
myId: number;
|
|
||||||
sidebarOpen: boolean;
|
|
||||||
darkmode: boolean;
|
|
||||||
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar = (props: SidebarProps): JSX.Element => {
|
export const Sidebar = (props: SidebarProps): JSX.Element => {
|
||||||
|
const sidebarOpen = useAppSelector((state) => state.app.sidebarOpen);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
props.sidebarOpen ? 'flex' : 'hidden md:flex'
|
sidebarOpen ? 'flex' : 'hidden md:flex'
|
||||||
} flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm`}
|
} flex-col rounded-md md:ml-0 shadow-md border w-full max-w-sm`}
|
||||||
>
|
>
|
||||||
<Nodes myId={props.myId} />
|
<Nodes />
|
||||||
<Device connection={props.connection} />
|
<Device connection={props.connection} />
|
||||||
<Channels />
|
<Channels />
|
||||||
<div className="flex-grow border-b"></div>
|
<div className="flex-grow border-b"></div>
|
||||||
<UI darkmode={props.darkmode} setDarkmode={props.setDarkmode} />
|
<UI />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface ChannelProps {
|
|||||||
channel: Protobuf.Channel;
|
channel: Protobuf.Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Channel = (props: ChannelProps): JSX.Element => {
|
export const Channel = (props: ChannelProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Disclosure>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
@@ -112,5 +112,3 @@ const Channel = (props: ChannelProps): JSX.Element => {
|
|||||||
</Disclosure>
|
</Disclosure>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Channel;
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { useObservableSuspense } from 'observable-hooks';
|
|||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { channelResource } from '../../../streams';
|
import { channelResource } from '../../../streams';
|
||||||
import Channel from './Channel';
|
import { Channel } from './Channel';
|
||||||
|
|
||||||
const ChannelList = (): JSX.Element => {
|
export const ChannelList = (): JSX.Element => {
|
||||||
const channelSource = useObservableSuspense(channelResource);
|
const channelSource = useObservableSuspense(channelResource);
|
||||||
|
|
||||||
const [channels, setChannels] = React.useState<Protobuf.Channel[]>([]);
|
const [channels, setChannels] = React.useState<Protobuf.Channel[]>([]);
|
||||||
@@ -39,5 +39,3 @@ const ChannelList = (): JSX.Element => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChannelList;
|
|
||||||
|
|||||||
@@ -1,47 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Disclosure } from '@headlessui/react';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
HashtagIcon,
|
|
||||||
} from '@heroicons/react/outline';
|
|
||||||
|
|
||||||
import { TranslationsContext } from '../../../translations/TranslationsContext';
|
import { HashtagIcon } from '@heroicons/react/outline';
|
||||||
import ChannelList from './ChannelList';
|
|
||||||
|
|
||||||
const Channels = (): JSX.Element => {
|
import { Dropdown } from '../../basic/Dropdown';
|
||||||
const { translations } = React.useContext(TranslationsContext);
|
import { ChannelList } from './ChannelList';
|
||||||
|
|
||||||
|
export const Channels = (): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Dropdown
|
||||||
{({ open }) => (
|
icon={<HashtagIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
|
||||||
<>
|
title={t('settings.channel')}
|
||||||
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
|
content={<ChannelList />}
|
||||||
<div className="flex">
|
fallbackMessage={'Loading...'}
|
||||||
{open ? (
|
/>
|
||||||
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
)}
|
|
||||||
<HashtagIcon className="my-auto text-gray-600 mr-2 2-5 h-5" />
|
|
||||||
{translations.device_channels_title}
|
|
||||||
</div>
|
|
||||||
</Disclosure.Button>
|
|
||||||
<Disclosure.Panel>
|
|
||||||
<React.Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="flex border-b border-gray-300">
|
|
||||||
<div className="m-auto p-3 text-gray-500">Loading...</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ChannelList />
|
|
||||||
</React.Suspense>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Channels;
|
|
||||||
|
|||||||
@@ -1,58 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Disclosure } from '@headlessui/react';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
|
||||||
AdjustmentsIcon,
|
import { AdjustmentsIcon } from '@heroicons/react/outline';
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
} from '@heroicons/react/outline';
|
|
||||||
import type {
|
import type {
|
||||||
IBLEConnection,
|
IBLEConnection,
|
||||||
IHTTPConnection,
|
IHTTPConnection,
|
||||||
ISerialConnection,
|
ISerialConnection,
|
||||||
} from '@meshtastic/meshtasticjs';
|
} from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { TranslationsContext } from '../../../translations/TranslationsContext';
|
import { Dropdown } from '../../basic/Dropdown';
|
||||||
import Settings from './Settings';
|
import { Settings } from './Settings';
|
||||||
|
|
||||||
interface DeviceProps {
|
interface DeviceProps {
|
||||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Device = (props: DeviceProps): JSX.Element => {
|
export const Device = (props: DeviceProps): JSX.Element => {
|
||||||
const { translations } = React.useContext(TranslationsContext);
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Dropdown
|
||||||
{({ open }) => (
|
icon={<AdjustmentsIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
|
||||||
<>
|
title={t('settings.device')}
|
||||||
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
|
content={<Settings connection={props.connection} />}
|
||||||
<div className="flex">
|
fallbackMessage={'Loading...'}
|
||||||
{open ? (
|
/>
|
||||||
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
)}
|
|
||||||
<AdjustmentsIcon className="text-gray-600 my-auto mr-2 w-5 h-5" />
|
|
||||||
{translations.device_settings_title}
|
|
||||||
</div>
|
|
||||||
</Disclosure.Button>
|
|
||||||
<Disclosure.Panel>
|
|
||||||
<>
|
|
||||||
<React.Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="flex border-b border-gray-300">
|
|
||||||
<div className="m-auto p-3 text-gray-500">Loading...</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Settings connection={props.connection} />
|
|
||||||
</React.Suspense>
|
|
||||||
</>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Device;
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { useObservableSuspense } from 'observable-hooks';
|
import { useObservableSuspense } from 'observable-hooks';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { SaveIcon } from '@heroicons/react/outline';
|
import { SaveIcon } from '@heroicons/react/outline';
|
||||||
import type {
|
import type {
|
||||||
@@ -12,14 +13,13 @@ import type {
|
|||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { preferencesResource } from '../../../streams';
|
import { preferencesResource } from '../../../streams';
|
||||||
import { TranslationsContext } from '../../../translations/TranslationsContext';
|
|
||||||
|
|
||||||
interface SettingsProps {
|
interface SettingsProps {
|
||||||
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
connection: ISerialConnection | IHTTPConnection | IBLEConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Settings = (props: SettingsProps): JSX.Element => {
|
export const Settings = (props: SettingsProps): JSX.Element => {
|
||||||
const { translations } = React.useContext(TranslationsContext);
|
const { t } = useTranslation();
|
||||||
const preferences = useObservableSuspense(preferencesResource);
|
const preferences = useObservableSuspense(preferencesResource);
|
||||||
|
|
||||||
const { register, handleSubmit } =
|
const { register, handleSubmit } =
|
||||||
@@ -33,7 +33,7 @@ const Settings = (props: SettingsProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
|
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
|
||||||
<div className="my-auto">{translations.device_region_title}</div>
|
<div className="my-auto">{t('strings.device_region')}</div>
|
||||||
<div className="flex shadow-md rounded-md ml-2">
|
<div className="flex shadow-md rounded-md ml-2">
|
||||||
<select
|
<select
|
||||||
{...register('region', {
|
{...register('region', {
|
||||||
@@ -71,13 +71,13 @@ const Settings = (props: SettingsProps): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
|
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
|
||||||
<div className="my-auto">{translations.device_wifi_ssid}</div>
|
<div className="my-auto">{t('strings.wifi_ssid')}</div>
|
||||||
<div className="flex shadow-md rounded-md ml-2">
|
<div className="flex shadow-md rounded-md ml-2">
|
||||||
<input {...register('wifiSsid', {})} type="text" />
|
<input {...register('wifiSsid', {})} type="text" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
|
<div className="flex bg-gray-50 whitespace-nowrap p-3 justify-between border-b">
|
||||||
<div className="my-auto">{translations.device_wifi_psk}</div>
|
<div className="my-auto">{t('strings.wifi_psk')}</div>
|
||||||
<div className="flex shadow-md rounded-md ml-2">
|
<div className="flex shadow-md rounded-md ml-2">
|
||||||
<input {...register('wifiPassword', {})} type="password" />
|
<input {...register('wifiPassword', {})} type="password" />
|
||||||
</div>
|
</div>
|
||||||
@@ -88,11 +88,9 @@ const Settings = (props: SettingsProps): JSX.Element => {
|
|||||||
className="flex m-auto font-medium group-hover:text-gray-700"
|
className="flex m-auto font-medium group-hover:text-gray-700"
|
||||||
>
|
>
|
||||||
<SaveIcon className="m-auto mr-2 group-hover:text-gray-700 w-5 h-5" />
|
<SaveIcon className="m-auto mr-2 group-hover:text-gray-700 w-5 h-5" />
|
||||||
{translations.save_changes_button}
|
{t('strings.save_changes')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
|
||||||
|
|||||||
@@ -1,53 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Disclosure } from '@headlessui/react';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from '@heroicons/react/outline';
|
|
||||||
|
|
||||||
import { TranslationsContext } from '../../../translations/TranslationsContext';
|
import { UsersIcon } from '@heroicons/react/outline';
|
||||||
import NodeList from './NodeList';
|
|
||||||
|
|
||||||
interface NodesProps {
|
import { Dropdown } from '../../basic/Dropdown';
|
||||||
myId: number;
|
import { NodeList } from './NodeList';
|
||||||
}
|
|
||||||
|
|
||||||
const Nodes = (props: NodesProps): JSX.Element => {
|
export const Nodes = (): JSX.Element => {
|
||||||
const { translations } = React.useContext(TranslationsContext);
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Dropdown
|
||||||
{({ open }) => (
|
icon={<UsersIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
|
||||||
<>
|
title={t('placeholder.no_nodes')}
|
||||||
<Disclosure.Button className="flex rounded-t-md w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
|
content={<NodeList />}
|
||||||
<div className="flex">
|
fallbackMessage={t('placeholder.no_messages')}
|
||||||
{open ? (
|
/>
|
||||||
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
)}
|
|
||||||
<UsersIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />
|
|
||||||
{translations.nodes_title}
|
|
||||||
</div>
|
|
||||||
</Disclosure.Button>
|
|
||||||
<Disclosure.Panel className="shadow-inner">
|
|
||||||
<React.Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="flex border-b border-gray-300">
|
|
||||||
<div className="m-auto p-3 text-gray-500">
|
|
||||||
{translations.no_nodes_message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<NodeList myId={props.myId} />
|
|
||||||
</React.Suspense>
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nodes;
|
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ import {
|
|||||||
} from '@heroicons/react/outline';
|
} from '@heroicons/react/outline';
|
||||||
import type { Types } from '@meshtastic/meshtasticjs';
|
import type { Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
import { useAppSelector } from '../../../hooks/redux';
|
||||||
|
|
||||||
export interface NodeProps {
|
export interface NodeProps {
|
||||||
node: Types.NodeInfoPacket;
|
node: Types.NodeInfoPacket;
|
||||||
myId: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Node = (props: NodeProps): JSX.Element => {
|
export const Node = (props: NodeProps): JSX.Element => {
|
||||||
|
const myId = useAppSelector((state) => state.meshtastic.myId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Disclosure>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
@@ -31,7 +34,7 @@ const Node = (props: NodeProps): JSX.Element => {
|
|||||||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{props.node.data.num === props.myId ? (
|
{props.node.data.num === myId ? (
|
||||||
<FlagIcon className="absolute -right-1 -top-2 text-yellow-500 my-auto w-4 h-4" />
|
<FlagIcon className="absolute -right-1 -top-2 text-yellow-500 my-auto w-4 h-4" />
|
||||||
) : null}
|
) : null}
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -98,5 +101,3 @@ const Node = (props: NodeProps): JSX.Element => {
|
|||||||
</Disclosure>
|
</Disclosure>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Node;
|
|
||||||
|
|||||||
@@ -5,13 +5,9 @@ import { useObservableSuspense } from 'observable-hooks';
|
|||||||
import type { Types } from '@meshtastic/meshtasticjs';
|
import type { Types } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
import { nodeResource } from '../../../streams';
|
import { nodeResource } from '../../../streams';
|
||||||
import Node from './Node';
|
import { Node } from './Node';
|
||||||
|
|
||||||
export interface NodeListProps {
|
export const NodeList = (): JSX.Element => {
|
||||||
myId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeList = (props: NodeListProps): JSX.Element => {
|
|
||||||
const nodeSource = useObservableSuspense(nodeResource);
|
const nodeSource = useObservableSuspense(nodeResource);
|
||||||
|
|
||||||
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
|
const [nodes, setNodes] = React.useState<Types.NodeInfoPacket[]>([]);
|
||||||
@@ -37,10 +33,8 @@ const NodeList = (props: NodeListProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{nodes.map((node, index) => (
|
{nodes.map((node, index) => (
|
||||||
<Node key={index} node={node} myId={props.myId} />
|
<Node key={index} node={node} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NodeList;
|
|
||||||
|
|||||||
@@ -1,44 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Disclosure } from '@headlessui/react';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
CogIcon,
|
|
||||||
} from '@heroicons/react/outline';
|
|
||||||
|
|
||||||
import { TranslationsContext } from '../../../translations/TranslationsContext';
|
import { CogIcon } from '@heroicons/react/outline';
|
||||||
import Translations from './Translations';
|
|
||||||
|
|
||||||
interface UIProps {
|
import { Dropdown } from '../../basic/Dropdown';
|
||||||
darkmode: boolean;
|
import { Translations } from './Translations';
|
||||||
setDarkmode: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}
|
export const UI = (): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const UI = (props: UIProps): JSX.Element => {
|
|
||||||
const { translations } = React.useContext(TranslationsContext);
|
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Dropdown
|
||||||
{({ open }) => (
|
icon={<CogIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />}
|
||||||
<>
|
title={t('settings.ui')}
|
||||||
<Disclosure.Button className="flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
|
content={<Translations />}
|
||||||
<div className="flex">
|
fallbackMessage={'Loading...'}
|
||||||
{open ? (
|
/>
|
||||||
<ChevronDownIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
|
||||||
)}
|
|
||||||
<CogIcon className="my-auto text-gray-600 mr-2 w-5 h-5" />
|
|
||||||
{translations.ui_settings_title}
|
|
||||||
</div>
|
|
||||||
</Disclosure.Button>
|
|
||||||
<Disclosure.Panel>
|
|
||||||
<Translations />
|
|
||||||
</Disclosure.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Disclosure>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UI;
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Br, Jp, Us } from 'react-flags-select';
|
import { Br, Jp, Us } from 'react-flags-select';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Disclosure } from '@headlessui/react';
|
import { Disclosure } from '@headlessui/react';
|
||||||
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/outline';
|
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/outline';
|
||||||
|
|
||||||
import {
|
export const Translations = (): JSX.Element => {
|
||||||
LanguageEnum,
|
const { t } = useTranslation();
|
||||||
TranslationsContext,
|
|
||||||
} from '../../../translations/TranslationsContext';
|
|
||||||
|
|
||||||
const Translations = (): JSX.Element => {
|
|
||||||
const { translations, language, setLanguage } =
|
|
||||||
React.useContext(TranslationsContext);
|
|
||||||
return (
|
return (
|
||||||
<Disclosure>
|
<Disclosure>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
@@ -24,8 +20,8 @@ const Translations = (): JSX.Element => {
|
|||||||
) : (
|
) : (
|
||||||
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
<ChevronRightIcon className="my-auto w-5 h-5 mr-2" />
|
||||||
)}
|
)}
|
||||||
{translations.language_title}
|
{t('strings.language')}
|
||||||
<div className="my-auto">
|
{/* <div className="my-auto">
|
||||||
{language === LanguageEnum.ENGLISH ? (
|
{language === LanguageEnum.ENGLISH ? (
|
||||||
<Us className="ml-2 w-8" />
|
<Us className="ml-2 w-8" />
|
||||||
) : language === LanguageEnum.JAPANESE ? (
|
) : language === LanguageEnum.JAPANESE ? (
|
||||||
@@ -33,14 +29,14 @@ const Translations = (): JSX.Element => {
|
|||||||
) : language === LanguageEnum.PORTUGUESE ? (
|
) : language === LanguageEnum.PORTUGUESE ? (
|
||||||
<Br className="ml-2 w-8" />
|
<Br className="ml-2 w-8" />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div
|
<div
|
||||||
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
|
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLanguage(LanguageEnum.ENGLISH);
|
// setLanguage(LanguageEnum.ENGLISH);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
English <Us className="w-8 my-auto" />
|
English <Us className="w-8 my-auto" />
|
||||||
@@ -48,7 +44,7 @@ const Translations = (): JSX.Element => {
|
|||||||
<div
|
<div
|
||||||
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
|
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLanguage(LanguageEnum.PORTUGUESE);
|
// setLanguage(LanguageEnum.PORTUGUESE);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Português <Br className="w-8 my-auto" />
|
Português <Br className="w-8 my-auto" />
|
||||||
@@ -56,7 +52,7 @@ const Translations = (): JSX.Element => {
|
|||||||
<div
|
<div
|
||||||
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
|
className="flex bg-gray-100 hover:bg-gray-200 cursor-pointer justify-between p-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLanguage(LanguageEnum.JAPANESE);
|
// setLanguage(LanguageEnum.JAPANESE);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
日本語 <Jp className="w-8 my-auto" />
|
日本語 <Jp className="w-8 my-auto" />
|
||||||
@@ -67,5 +63,3 @@ const Translations = (): JSX.Element => {
|
|||||||
</Disclosure>
|
</Disclosure>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Translations;
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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="bg-white flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer">
|
||||||
|
<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}
|
||||||
|
static
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ interface ToggleSwitchProps {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => {
|
export const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => {
|
||||||
const [active, setActive] = React.useState(false);
|
const [active, setActive] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -29,5 +29,3 @@ const ToggleSwitch = (props: ToggleSwitchProps): JSX.Element => {
|
|||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ToggleSwitch;
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { TypedUseSelectorHook } from 'react-redux';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import type { AppDispatch, RootState } from '../store';
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Translations_EN from '../translations/en';
|
|
||||||
import Translations_JP from '../translations/jp';
|
|
||||||
import Translations_PT from '../translations/pt';
|
|
||||||
import type {
|
|
||||||
languageTemplate,
|
|
||||||
TranslationsContextData,
|
|
||||||
} from '../translations/TranslationsContext';
|
|
||||||
import { LanguageEnum } from '../translations/TranslationsContext';
|
|
||||||
|
|
||||||
export const useTranslationsContextValue = (): TranslationsContextData => {
|
|
||||||
const [currentLanguage, setcurrentLanguage] = React.useState<LanguageEnum>(
|
|
||||||
LanguageEnum.ENGLISH,
|
|
||||||
);
|
|
||||||
const [translation, setTranslation] =
|
|
||||||
React.useState<languageTemplate>(Translations_EN);
|
|
||||||
|
|
||||||
const setLanguage = React.useCallback(
|
|
||||||
(language: LanguageEnum) => {
|
|
||||||
setcurrentLanguage(language);
|
|
||||||
switch (language) {
|
|
||||||
case LanguageEnum.ENGLISH:
|
|
||||||
setTranslation(Translations_EN);
|
|
||||||
break;
|
|
||||||
case LanguageEnum.JAPANESE:
|
|
||||||
setTranslation(Translations_JP);
|
|
||||||
break;
|
|
||||||
case LanguageEnum.PORTUGUESE:
|
|
||||||
setTranslation(Translations_PT);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setcurrentLanguage, setTranslation],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
language: currentLanguage,
|
|
||||||
setLanguage: setLanguage,
|
|
||||||
translations: translation,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
|
import './translation';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
@@ -6,7 +7,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import store from './store';
|
import { store } from './store';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
if (!rootElement) throw new Error('Failed to find the root element');
|
if (!rootElement) throw new Error('Failed to find the root element');
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
sidebarOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AppState = {
|
||||||
|
sidebarOpen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appSlice = createSlice({
|
||||||
|
name: 'auth',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
openSidebar(state) {
|
||||||
|
state.sidebarOpen = true;
|
||||||
|
},
|
||||||
|
closeSidebar(state) {
|
||||||
|
state.sidebarOpen = false;
|
||||||
|
},
|
||||||
|
toggleSidebar(state) {
|
||||||
|
state.sidebarOpen = !state.sidebarOpen;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { openSidebar, closeSidebar, toggleSidebar } = appSlice.actions;
|
||||||
|
|
||||||
|
export default appSlice.reducer;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
myId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AppState = {
|
||||||
|
myId: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const meshtasticSlice = createSlice({
|
||||||
|
name: 'meshtastic',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setMyId: (state, action: PayloadAction<number>) => {
|
||||||
|
state.myId = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setMyId } = meshtasticSlice.actions;
|
||||||
|
|
||||||
|
export default meshtasticSlice.reducer;
|
||||||
+9
-19
@@ -1,24 +1,14 @@
|
|||||||
import type { Protobuf } from '@meshtastic/meshtasticjs';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { configureStore, createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
const nodesSlice = createSlice({
|
import appSlice from './slices/appSlice';
|
||||||
name: 'nodes',
|
import meshtasticSlice from './slices/meshtasticSlice';
|
||||||
initialState: {
|
|
||||||
members: [],
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
addMember: (state: Protobuf.NodeInfo[], node: Protobuf.NodeInfo) => {
|
|
||||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
|
||||||
// doesn't actually mutate the state because it uses the Immer library,
|
|
||||||
// which detects changes to a "draft state" and produces a brand new
|
|
||||||
// immutable state based off those changes
|
|
||||||
state.push(node);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
nodes: nodesSlice.reducer,
|
app: appSlice,
|
||||||
|
meshtastic: meshtasticSlice,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import detector from 'i18next-browser-languagedetector';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import en from './translations/en.json';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(detector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: 'en',
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
translation: en,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import Translations_EN from './en';
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LanguageEnum {
|
|
||||||
ENGLISH,
|
|
||||||
JAPANESE,
|
|
||||||
PORTUGUESE,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TranslationsContextData {
|
|
||||||
language: LanguageEnum;
|
|
||||||
setLanguage: (postId: number) => void;
|
|
||||||
translations: languageTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const translationsContextDefaultValue: TranslationsContextData = {
|
|
||||||
language: LanguageEnum.ENGLISH,
|
|
||||||
setLanguage: () => null,
|
|
||||||
translations: Translations_EN,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TranslationsContext = React.createContext<TranslationsContextData>(
|
|
||||||
translationsContextDefaultValue,
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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,17 +0,0 @@
|
|||||||
import type { languageTemplate } from './TranslationsContext';
|
|
||||||
|
|
||||||
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,22 @@
|
|||||||
|
{
|
||||||
|
"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,16 +0,0 @@
|
|||||||
import type { languageTemplate } from './TranslationsContext';
|
|
||||||
|
|
||||||
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,22 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { languageTemplate } from './TranslationsContext';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
no_messages_message: 'Não a mensagens ainda',
|
|
||||||
ui_settings_title: 'Configurações da Interface',
|
|
||||||
nodes_title: 'Nós',
|
|
||||||
device_settings_title: 'Configurações do dispositivo',
|
|
||||||
device_channels_title: 'Canais',
|
|
||||||
color_scheme_title: 'Esquema de cores',
|
|
||||||
language_title: 'Idioma',
|
|
||||||
device_region_title: 'Região do dispositivo',
|
|
||||||
device_wifi_ssid: 'Nome do WiFi',
|
|
||||||
device_wifi_psk: 'Senha do WiFi',
|
|
||||||
save_changes_button: 'Salvar alterações',
|
|
||||||
no_nodes_message: 'Nenhum nó foi encontrado',
|
|
||||||
no_message_placeholder: 'Entre mensagem',
|
|
||||||
} as languageTemplate;
|
|
||||||
-3263
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user