Context menu and placeholder images
This commit is contained in:
+13
-10
@@ -5,6 +5,7 @@ import { Connection } from '@components/Connection';
|
||||
import { useRoute } from '@core/router';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
import { ContextMenu } from './components/generic/ContextMenu';
|
||||
import { BottomNav } from './components/menu/BottomNav';
|
||||
import { Extensions } from './pages/Extensions/Index';
|
||||
import { Messages } from './pages/Messages';
|
||||
@@ -17,17 +18,19 @@ export const App = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<div className={`h-screen w-screen ${darkMode ? 'dark' : ''}`}>
|
||||
<Connection />
|
||||
<div className="flex h-full flex-col bg-gray-200 dark:bg-secondaryDark">
|
||||
<div className="flex min-h-0 w-full flex-grow">
|
||||
{route.name === 'messages' && <Messages />}
|
||||
{route.name === 'nodes' && <Nodes />}
|
||||
{route.name === 'map' && <Map />}
|
||||
{route.name === 'extensions' && <Extensions />}
|
||||
{route.name === false && <NotFound />}
|
||||
<ContextMenu>
|
||||
<Connection />
|
||||
<div className="flex h-full flex-col bg-gray-200 dark:bg-secondaryDark">
|
||||
<div className="flex min-h-0 w-full flex-grow">
|
||||
{route.name === 'messages' && <Messages />}
|
||||
{route.name === 'nodes' && <Nodes />}
|
||||
{route.name === 'map' && <Map />}
|
||||
{route.name === 'extensions' && <Extensions />}
|
||||
{route.name === false && <NotFound />}
|
||||
</div>
|
||||
<BottomNav />
|
||||
</div>
|
||||
<BottomNav />
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import { FiActivity, FiAperture, FiTag } from 'react-icons/fi';
|
||||
|
||||
export interface ContextMenuProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [position, setPosition] = React.useState({ x: 0, y: 0 });
|
||||
const [selectedValue, setSelectedValue] = React.useState<string>();
|
||||
const doSomething = (selectedValue: string) => {
|
||||
setSelectedValue(selectedValue);
|
||||
};
|
||||
|
||||
const showContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
setVisible(false);
|
||||
const newPosition = {
|
||||
x: event.pageX,
|
||||
y: event.pageY,
|
||||
};
|
||||
|
||||
setPosition(newPosition);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const hideContextMenu = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full"
|
||||
onContextMenu={showContextMenu}
|
||||
onClick={hideContextMenu}
|
||||
>
|
||||
{children}
|
||||
{selectedValue && <h1>{selectedValue} is selected</h1>}
|
||||
|
||||
{visible && (
|
||||
<div
|
||||
style={{ top: position.y, left: position.x }}
|
||||
className="fixed z-50 w-60 gap-2 divide-y divide-gray-300 rounded-md border border-gray-300 font-medium shadow-md backdrop-blur-xl dark:divide-gray-600 dark:border-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<div className="cursor-pointer first:rounded-t-md last:rounded-b-md hover:dark:bg-secondaryDark">
|
||||
<m.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
className="flex gap-2 p-2"
|
||||
>
|
||||
<div className="my-auto">
|
||||
<FiActivity />
|
||||
</div>
|
||||
<div className="truncate">Menu item</div>
|
||||
</m.div>
|
||||
</div>
|
||||
|
||||
<div className="cursor-pointer first:rounded-t-md last:rounded-b-md hover:dark:bg-secondaryDark">
|
||||
<m.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
className="flex gap-2 p-2"
|
||||
>
|
||||
<div className="my-auto">
|
||||
<FiAperture />
|
||||
</div>
|
||||
<div className="truncate">Menu item 2</div>
|
||||
</m.div>
|
||||
</div>
|
||||
|
||||
<div className="cursor-pointer first:rounded-t-md last:rounded-b-md hover:dark:bg-secondaryDark">
|
||||
<m.div
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
className="flex gap-2 p-2"
|
||||
>
|
||||
<div className="my-auto">
|
||||
<FiTag />
|
||||
</div>
|
||||
<div className="truncate">
|
||||
Menu item 3 with a very long name that should wrap
|
||||
</div>
|
||||
</m.div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -85,6 +85,8 @@ export const meshtasticSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
addLogEvent: (state, action: PayloadAction<Types.LogEventPacket>) => {
|
||||
console.log(action.payload.packet);
|
||||
|
||||
state.logs.push(action.payload);
|
||||
},
|
||||
setDeviceStatus: (state, action: PayloadAction<Types.DeviceStatusEnum>) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AnimatePresence, m } from 'framer-motion';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import fetcher from '@app/core/utils/fetcher';
|
||||
@@ -27,6 +28,7 @@ export const FileBrowser = (): JSX.Element => {
|
||||
const connectionParams = useAppSelector(
|
||||
(state) => state.app.connectionParams,
|
||||
);
|
||||
const darkMode = useAppSelector((state) => state.app.darkMode);
|
||||
|
||||
const { data } = useSWR<Files>(
|
||||
`${connectionParams.HTTP.tls ? 'https' : 'http'}://${
|
||||
@@ -42,7 +44,22 @@ export const FileBrowser = (): JSX.Element => {
|
||||
<div className="my-auto w-1/3">FileName</div>
|
||||
<div className="my-auto w-1/3">Actions</div>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<div className="h-full px-4">
|
||||
<AnimatePresence>
|
||||
{(!data || data?.data.files.length === 0) && (
|
||||
<div className="flex h-full w-full">
|
||||
<m.img
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="m-auto h-64 w-64 text-green-500"
|
||||
src={`/placeholders/${
|
||||
darkMode ? 'Files Dark.svg' : 'Files.svg'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{data?.data.files.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
import { FiArrowRight } from 'react-icons/fi';
|
||||
import { AnimatePresence, m } from 'framer-motion';
|
||||
import { FiArrowRight, FiPaperclip } from 'react-icons/fi';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/useAppSelector';
|
||||
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
|
||||
|
||||
export const Logs = (): JSX.Element => {
|
||||
const logs = useAppSelector((state) => state.meshtastic.logs);
|
||||
const darkMode = useAppSelector((state) => state.app.darkMode);
|
||||
|
||||
type lookupType = { [key: number]: string };
|
||||
|
||||
@@ -54,6 +55,21 @@ export const Logs = (): JSX.Element => {
|
||||
className="
|
||||
block h-full flex-col overflow-y-auto py-4 px-2 font-mono text-xs dark:text-gray-400"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{logs.length === 0 && (
|
||||
<div className="flex h-full w-full">
|
||||
<m.img
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="m-auto h-64 w-64 text-green-500"
|
||||
src={`/placeholders/${
|
||||
darkMode ? 'View Code Dark.svg' : 'View Code.svg'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{logs.map((log, index) => (
|
||||
<tr key={index} className="group hover:bg-secondaryDark">
|
||||
<m.td
|
||||
@@ -87,6 +103,13 @@ export const Logs = (): JSX.Element => {
|
||||
<Wrapper className={levelLookup[log.level]}>
|
||||
[{Protobuf.LogRecord_Level[log.level]}]{/* </div> */}
|
||||
</Wrapper>
|
||||
<td
|
||||
className={`m-auto ${
|
||||
log.packet ? '' : 'dark:text-secondaryDark'
|
||||
}`}
|
||||
>
|
||||
<FiPaperclip />
|
||||
</td>
|
||||
<td className="truncate pl-1">{log.message}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user