Map logic rework
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||
import { BLE } from '@components/connection/BLE';
|
||||
import { HTTP } from '@components/connection/HTTP';
|
||||
import { Serial } from '@components/connection/Serial';
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
setConnectionParams,
|
||||
setConnType,
|
||||
} from '@core/slices/appSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Button, Card, Select } from '@meshtastic/components';
|
||||
import { Types } from '@meshtastic/meshtasticjs';
|
||||
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa';
|
||||
import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md';
|
||||
|
||||
import { useMapbox } from '@app/hooks/mapbox';
|
||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||
import {
|
||||
setBearing,
|
||||
setExaggeration,
|
||||
setHillShade,
|
||||
setLatLng,
|
||||
setMapStyle,
|
||||
setPitch,
|
||||
setZoom,
|
||||
} from '@core/slices/mapSlice';
|
||||
import { Card, IconButton } from '@meshtastic/components';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { useMapbox } from '@hooks/useMapbox';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
import type { MapStyle } from './styles';
|
||||
import { MapStyles } from './styles';
|
||||
@@ -24,63 +19,10 @@ import { MapStyles } from './styles';
|
||||
export const Map = (): JSX.Element => {
|
||||
const dispatch = useAppDispatch();
|
||||
const darkMode = useAppSelector((state) => state.app.darkMode);
|
||||
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
||||
|
||||
const mapState = useAppSelector((state) => state.map);
|
||||
const [markers, setMarkers] = React.useState<
|
||||
{ id: number; marker: mapboxgl.Marker }[]
|
||||
>([]);
|
||||
const mapRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const map = useMapbox(mapRef, mapState.accessToken, {
|
||||
center: mapState.latLng,
|
||||
zoom: mapState.zoom,
|
||||
bearing: mapState.bearing,
|
||||
pitch: mapState.pitch,
|
||||
style: mapState.style.url,
|
||||
});
|
||||
|
||||
const updateNodes = React.useCallback(() => {
|
||||
nodes.map((node) => {
|
||||
if (map?.loaded() && node.currentPosition) {
|
||||
const existingMarker = markers.find(
|
||||
(marker) => marker.id === node.number,
|
||||
)?.marker;
|
||||
const marker =
|
||||
existingMarker ??
|
||||
new mapboxgl.Marker({}).setLngLat([0, 0]).addTo(map);
|
||||
|
||||
marker
|
||||
.setLngLat([
|
||||
node.currentPosition.longitudeI / 1e7,
|
||||
node.currentPosition.latitudeI / 1e7,
|
||||
])
|
||||
.setPopup(
|
||||
new mapboxgl.Popup().setHTML(
|
||||
renderToString(
|
||||
<Card>
|
||||
<div className="text-xl font-medium">
|
||||
{node.user?.longName}
|
||||
</div>
|
||||
<ul>
|
||||
<li>ID: {node.number}</li>
|
||||
</ul>
|
||||
</Card>,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!existingMarker) {
|
||||
setMarkers((markers) => [
|
||||
...markers,
|
||||
{
|
||||
id: node.number,
|
||||
marker,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [markers, map, nodes]);
|
||||
const { ref } = useMapbox();
|
||||
|
||||
const ChangeMapStyle = React.useCallback(
|
||||
(styleName: string, style: MapStyle) => {
|
||||
@@ -97,95 +39,6 @@ export const Map = (): JSX.Element => {
|
||||
[dispatch, darkMode, mapState.style.title],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
map?.on('load', () => {
|
||||
updateNodes();
|
||||
});
|
||||
map?.on('styledata', () => {
|
||||
if (!map.getSource('mapbox-dem')) {
|
||||
map.addSource('mapbox-dem', {
|
||||
type: 'raster-dem',
|
||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||
tileSize: 512,
|
||||
maxzoom: 14,
|
||||
});
|
||||
}
|
||||
map.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: mapState.exaggeration ? 1.5 : 0,
|
||||
});
|
||||
});
|
||||
map?.on('dragend', (e) => {
|
||||
dispatch(setLatLng(e.target.getCenter()));
|
||||
});
|
||||
map?.on('zoomend', (e) => {
|
||||
dispatch(setZoom(e.target.getZoom()));
|
||||
});
|
||||
map?.on('rotate', (e) => {
|
||||
dispatch(setBearing(e.target.getBearing()));
|
||||
});
|
||||
map?.on('pitch', (e) => {
|
||||
dispatch(setPitch(e.target.getPitch()));
|
||||
});
|
||||
}, [dispatch, map, updateNodes, mapState.exaggeration]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const center = map?.getCenter();
|
||||
if (center !== mapState.latLng) {
|
||||
map?.setCenter(mapState.latLng);
|
||||
}
|
||||
}, [map, mapState.latLng]);
|
||||
|
||||
/**
|
||||
* Hill Shading
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
if (mapState.hillShade) {
|
||||
map.addLayer(
|
||||
{
|
||||
id: 'hillshading',
|
||||
source: 'mapbox-dem',
|
||||
type: 'hillshade',
|
||||
// insert below waterway-river-canal-shadow;
|
||||
// where hillshading sits in the Mapbox Outdoors style
|
||||
},
|
||||
'waterway-river-canal-shadow',
|
||||
);
|
||||
} else {
|
||||
map.removeLayer('hillshading');
|
||||
}
|
||||
}
|
||||
}, [map, mapState.hillShade]);
|
||||
|
||||
/**
|
||||
* Exaggeration
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
map.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: mapState.exaggeration ? 1.5 : 0,
|
||||
});
|
||||
}
|
||||
}, [map, mapState.exaggeration]);
|
||||
|
||||
/**
|
||||
* Map Style
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
map.setStyle(mapState.style.url);
|
||||
}
|
||||
}, [map, mapState.style]);
|
||||
|
||||
/**
|
||||
* Markers
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
updateNodes();
|
||||
}, [nodes, updateNodes]);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full h-full">
|
||||
<div className="fixed right-0 z-20 p-2 m-4 space-y-2 bg-white rounded-md shadow-md md:mx-10 dark:bg-primaryDark">
|
||||
@@ -232,7 +85,7 @@ export const Map = (): JSX.Element => {
|
||||
<IconButton icon={<MdRadar />} />
|
||||
</div>
|
||||
<div className="flex w-full h-full">
|
||||
<div className="flex-grow w-full h-full" ref={mapRef} />
|
||||
<div className="flex-grow w-full h-full" ref={ref} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import React from 'react';
|
||||
|
||||
import mapbox from 'mapbox-gl';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { FiAirplay } from 'react-icons/fi';
|
||||
|
||||
import {
|
||||
setBearing,
|
||||
setLatLng,
|
||||
setMapStyle,
|
||||
setPitch,
|
||||
setZoom,
|
||||
} from '@core/slices/mapSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { useCreateMapbox } from '@hooks/useCreateMapbox';
|
||||
import { Card } from '@meshtastic/components';
|
||||
|
||||
import { MapStyles } from '../Map/styles';
|
||||
import { MapboxContext } from './mapboxContext';
|
||||
|
||||
export type MapboxProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const MapboxProvider = ({
|
||||
children,
|
||||
}: MapboxProviderProps): JSX.Element => {
|
||||
const darkMode = useAppSelector((state) => state.app.darkMode);
|
||||
const mapState = useAppSelector((state) => state.map);
|
||||
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const [markers, setMarkers] = React.useState<
|
||||
{ id: number; marker: mapbox.Marker }[]
|
||||
>([]);
|
||||
const [markerElements, setMarkerElements] = React.useState<
|
||||
{ id: number; element: JSX.Element; ref: React.RefObject<HTMLDivElement> }[]
|
||||
>([]);
|
||||
|
||||
const map = useCreateMapbox({
|
||||
ref,
|
||||
accessToken:
|
||||
'pk.eyJ1Ijoic2FjaGF3IiwiYSI6ImNrNW9meXozZjBsdW0zbHBjM2FnNnV6cmsifQ.3E4n8eFGD9ZOFo-XDVeZnQ',
|
||||
options: {
|
||||
center: mapState.latLng,
|
||||
zoom: mapState.zoom,
|
||||
bearing: mapState.bearing,
|
||||
pitch: mapState.pitch,
|
||||
style: mapState.style.url,
|
||||
},
|
||||
});
|
||||
|
||||
const updateNodes = React.useCallback(() => {
|
||||
nodes.map((node) => {
|
||||
if (map?.loaded() && node.currentPosition) {
|
||||
const existingMarker = markers.find(
|
||||
(marker) => marker.id === node.number,
|
||||
)?.marker;
|
||||
|
||||
const tmpRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
const markerElement = markerElements.find(
|
||||
(element) => element.id === node.number,
|
||||
) ?? {
|
||||
element: (
|
||||
<div ref={tmpRef}>
|
||||
Test
|
||||
<FiAirplay />
|
||||
</div>
|
||||
),
|
||||
id: node.number,
|
||||
ref: tmpRef,
|
||||
};
|
||||
|
||||
console.log(markerElement);
|
||||
|
||||
const marker =
|
||||
existingMarker ??
|
||||
new mapbox.Marker(markerElement.ref.current ?? undefined, {})
|
||||
.setLngLat([0, 0])
|
||||
.addTo(map);
|
||||
|
||||
marker
|
||||
.setLngLat([
|
||||
node.currentPosition.longitudeI / 1e7,
|
||||
node.currentPosition.latitudeI / 1e7,
|
||||
])
|
||||
.setPopup(
|
||||
new mapbox.Popup().setHTML(
|
||||
renderToString(
|
||||
<Card>
|
||||
<div className="p-2">
|
||||
<div className="text-xl font-medium">
|
||||
{node.user?.longName}
|
||||
</div>
|
||||
<ul>
|
||||
<li>ID: {node.number}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!existingMarker) {
|
||||
setMarkers((markers) => [
|
||||
...markers,
|
||||
{
|
||||
id: node.number,
|
||||
marker,
|
||||
},
|
||||
]);
|
||||
setMarkerElements((markerElements) => [
|
||||
...markerElements,
|
||||
markerElement,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [markers, markerElements, map, nodes]);
|
||||
|
||||
React.useEffect(() => {
|
||||
map?.on('load', () => {
|
||||
updateNodes();
|
||||
});
|
||||
map?.on('styledata', () => {
|
||||
if (!map.getSource('mapbox-dem')) {
|
||||
map.addSource('mapbox-dem', {
|
||||
type: 'raster-dem',
|
||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||
tileSize: 512,
|
||||
maxzoom: 14,
|
||||
});
|
||||
}
|
||||
map.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: mapState.exaggeration ? 1.5 : 0,
|
||||
});
|
||||
});
|
||||
map?.on('dragend', (e) => {
|
||||
dispatch(setLatLng(e.target.getCenter()));
|
||||
});
|
||||
map?.on('zoomend', (e) => {
|
||||
dispatch(setZoom(e.target.getZoom()));
|
||||
});
|
||||
map?.on('rotate', (e) => {
|
||||
dispatch(setBearing(e.target.getBearing()));
|
||||
});
|
||||
map?.on('pitch', (e) => {
|
||||
dispatch(setPitch(e.target.getPitch()));
|
||||
});
|
||||
}, [dispatch, map, updateNodes, mapState.exaggeration]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const center = map?.getCenter();
|
||||
if (center !== mapState.latLng) {
|
||||
map?.setCenter(mapState.latLng);
|
||||
}
|
||||
}, [map, mapState.latLng]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (['Light', 'Dark'].includes(mapState.style.title)) {
|
||||
dispatch(setMapStyle(darkMode ? MapStyles.Dark : MapStyles.Light));
|
||||
}
|
||||
}, [dispatch, darkMode, mapState.style.title]);
|
||||
|
||||
/**
|
||||
* Hill Shading
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
if (mapState.hillShade) {
|
||||
map.addLayer(
|
||||
{
|
||||
id: 'hillshading',
|
||||
source: 'mapbox-dem',
|
||||
type: 'hillshade',
|
||||
// insert below waterway-river-canal-shadow;
|
||||
// where hillshading sits in the Mapbox Outdoors style
|
||||
},
|
||||
'waterway-river-canal-shadow',
|
||||
);
|
||||
} else {
|
||||
map.removeLayer('hillshading');
|
||||
}
|
||||
}
|
||||
}, [map, mapState.hillShade]);
|
||||
|
||||
/**
|
||||
* Exaggeration
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
map.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: mapState.exaggeration ? 1.5 : 0,
|
||||
});
|
||||
}
|
||||
}, [map, mapState.exaggeration]);
|
||||
|
||||
/**
|
||||
* Map Style
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
map.setStyle(mapState.style.url);
|
||||
}
|
||||
}, [map, mapState.style]);
|
||||
|
||||
/**
|
||||
* Markers
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
updateNodes();
|
||||
}, [nodes, updateNodes]);
|
||||
|
||||
return (
|
||||
<MapboxContext.Provider value={{ map, ref }}>
|
||||
{children}
|
||||
</MapboxContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import type mapbox from 'mapbox-gl';
|
||||
|
||||
export interface MapboxContextValue {
|
||||
ref: React.Ref<HTMLDivElement>;
|
||||
map?: mapbox.Map;
|
||||
}
|
||||
|
||||
export const MapboxContext = React.createContext<MapboxContextValue>(
|
||||
{} as MapboxContextValue
|
||||
);
|
||||
@@ -3,9 +3,10 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FiSend } from 'react-icons/fi';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||
import { connection } from '@core/connection';
|
||||
import { ackMessage } from '@core/slices/meshtasticSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { IconButton, Input, Select } from '@meshtastic/components';
|
||||
|
||||
export interface MessageBarProps {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, IconButton } from '@meshtastic/components';
|
||||
export const BLE = (): JSX.Element => {
|
||||
const [bleDevices, setBleDevices] = React.useState<BluetoothDevice[]>([]);
|
||||
|
||||
const { register, handleSubmit, control } = useForm<{
|
||||
const { handleSubmit } = useForm<{
|
||||
device?: BluetoothDevice;
|
||||
}>();
|
||||
|
||||
@@ -23,7 +23,7 @@ export const BLE = (): JSX.Element => {
|
||||
void updateBleDeviceList();
|
||||
}, [updateBleDeviceList]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const onSubmit = handleSubmit(async () => {
|
||||
await setConnection(connType.BLE);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import type React from 'react';
|
||||
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { useAppDispatch } from '@app/hooks/redux.js';
|
||||
import { connectionUrl, setConnection } from '@core/connection';
|
||||
import { connType, setConnectionParams } from '@core/slices/appSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { Button, Checkbox, Input, Select } from '@meshtastic/components';
|
||||
|
||||
export const HTTP = (): JSX.Element => {
|
||||
|
||||
@@ -3,16 +3,16 @@ import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FiCheck } from 'react-icons/fi';
|
||||
|
||||
import { useAppDispatch } from '@app/hooks/redux';
|
||||
import { serial, setConnection } from '@core/connection';
|
||||
import { connType, setConnectionParams } from '@core/slices/appSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { Button, IconButton } from '@meshtastic/components';
|
||||
|
||||
export const Serial = (): JSX.Element => {
|
||||
const [serialDevices, setSerialDevices] = React.useState<SerialPort[]>([]);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { register, handleSubmit, control } = useForm<{
|
||||
const { handleSubmit } = useForm<{
|
||||
device?: SerialPort;
|
||||
}>();
|
||||
|
||||
@@ -25,7 +25,7 @@ export const Serial = (): JSX.Element => {
|
||||
void updateSerialDeviceList();
|
||||
}, [updateSerialDeviceList]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const onSubmit = handleSubmit(async () => {
|
||||
await setConnection(connType.SERIAL);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/redux';
|
||||
import { Dialog } from '@headlessui/react';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
type DefaultDivProps = JSX.IntrinsicElements['div'];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Theme } from 'react-select';
|
||||
import ReactSelect from 'react-select';
|
||||
|
||||
import { bitwiseDecode, bitwiseEncode } from '@app/core/utils/bitwise';
|
||||
import { useAppSelector } from '@app/hooks/redux.js';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
import { Label } from './Label';
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||
import { Drawer } from '@components/generic/Drawer';
|
||||
import { closeMobileNav } from '@core/slices/appSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
|
||||
import { Logo } from './Logo';
|
||||
import { Navigation } from './Navigation';
|
||||
|
||||
@@ -2,8 +2,9 @@ import type React from 'react';
|
||||
|
||||
import { FiBluetooth, FiCpu, FiWifi } from 'react-icons/fi';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||
import { connType, openConnectionModal } from '@core/slices/appSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Button } from '@meshtastic/components';
|
||||
import { Types } from '@meshtastic/meshtasticjs';
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@ import type React from 'react';
|
||||
import { FiMenu } from 'react-icons/fi';
|
||||
|
||||
import { openMobileNav } from '@core/slices/appSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
import { useAppDispatch } from '../../../hooks/redux';
|
||||
|
||||
export const MobileNavToggle = (): JSX.Element => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
|
||||
import { FiBell, FiX } from 'react-icons/fi';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/redux';
|
||||
import { shift, useFloating } from '@floating-ui/react-dom';
|
||||
import { Popover } from '@headlessui/react';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { Button, IconButton } from '@meshtastic/components';
|
||||
|
||||
export const Notifications = (): JSX.Element => {
|
||||
|
||||
@@ -2,8 +2,9 @@ import type React from 'react';
|
||||
|
||||
import { FiMoon, FiSun } from 'react-icons/fi';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||
import { setDarkModeEnabled } from '@core/slices/appSlice';
|
||||
import { useAppDispatch } from '@hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@hooks/useAppSelector';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
export const ThemeToggle = (): JSX.Element => {
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from 'react';
|
||||
|
||||
import { FiXCircle } from 'react-icons/fi';
|
||||
|
||||
import { useBreakpoint } from '@app/hooks/breakpoint';
|
||||
import { Drawer } from '@components/generic/Drawer';
|
||||
import type { SidebarItemProps } from '@components/generic/SidebarItem';
|
||||
import { SidebarItem } from '@components/generic/SidebarItem';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import { useBreakpoint } from '@hooks/useBreakpoint';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
|
||||
export interface PageLayoutProps {
|
||||
|
||||
Reference in New Issue
Block a user