Rebuilt Transition adn chart select
This commit is contained in:
parent
f593cbd29a
commit
287ad8859b
6
TODO.md
6
TODO.md
@ -1,6 +0,0 @@
|
|||||||
Revise list of charts to only show those applicable to the selected procedure (so terminal itself and all transitions accompanied)
|
|
||||||
Revise image overlay
|
|
||||||
- Find center of full page
|
|
||||||
- Find center of georeferenced area
|
|
||||||
- Calculate skew parameters
|
|
||||||
- Skew geobounds
|
|
||||||
@ -3,7 +3,8 @@ import { QRCodeSVG } from 'qrcode.react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ProcedureSelect } from './components//ProcedureSelect';
|
import { ProcedureSelect } from './components//ProcedureSelect';
|
||||||
import { Map } from './components/Map';
|
import { Map } from './components/Map';
|
||||||
import { useNavigraphAuth } from './hooks/useNavigraphAuth';
|
import { Sidebar } from './components/Sidebar';
|
||||||
|
import { useNavigraphAuth } from './contexts/NavigraphAuth/NavigraphAuthContext';
|
||||||
import Parser from './parser/parser';
|
import Parser from './parser/parser';
|
||||||
|
|
||||||
const parser = await Parser.instance();
|
const parser = await Parser.instance();
|
||||||
@ -12,8 +13,10 @@ function App() {
|
|||||||
const [selectedAirport, setSelectedAirport] = useState<Airport>();
|
const [selectedAirport, setSelectedAirport] = useState<Airport>();
|
||||||
const [selectedRunway, setSelectedRunway] = useState<Runway>();
|
const [selectedRunway, setSelectedRunway] = useState<Runway>();
|
||||||
const [selectedTerminal, setSelectedTerminal] = useState<Terminal>();
|
const [selectedTerminal, setSelectedTerminal] = useState<Terminal>();
|
||||||
const [procedures, setProcedures] = useState<{ name: string; data: object }[]>([]);
|
const [transitions, setTransitions] = useState<Procedure[]>([]);
|
||||||
const [params, setParams] = useState<DeviceFlowParams | null>(null);
|
const [params, setParams] = useState<DeviceFlowParams | null>(null);
|
||||||
|
const [selectedTransition, setSelectedTransition] = useState<Procedure>();
|
||||||
|
const [selectedChart, setSelectedChart] = useState<Chart>();
|
||||||
|
|
||||||
const { user, signIn, initialized } = useNavigraphAuth();
|
const { user, signIn, initialized } = useNavigraphAuth();
|
||||||
|
|
||||||
@ -21,7 +24,7 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{procedures.length === 0 ? (
|
{transitions.length === 0 ? (
|
||||||
<div className="flex min-h-dvh w-full">
|
<div className="flex min-h-dvh w-full">
|
||||||
{!initialized && <div>Loading...</div>}
|
{!initialized && <div>Loading...</div>}
|
||||||
|
|
||||||
@ -44,7 +47,7 @@ function App() {
|
|||||||
setSelectedRunway={setSelectedRunway}
|
setSelectedRunway={setSelectedRunway}
|
||||||
setSelectedTerminal={setSelectedTerminal}
|
setSelectedTerminal={setSelectedTerminal}
|
||||||
handleSelection={(selectedTransitions) =>
|
handleSelection={(selectedTransitions) =>
|
||||||
setProcedures(
|
setTransitions(
|
||||||
selectedTransitions.map((transition) => ({
|
selectedTransitions.map((transition) => ({
|
||||||
name: transition,
|
name: transition,
|
||||||
data: parser.parse(selectedRunway!, transition),
|
data: parser.parse(selectedRunway!, transition),
|
||||||
@ -56,16 +59,23 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-dvh w-dvw">
|
<div className="flex h-dvh w-dvw">
|
||||||
{procedures.length > 0 && selectedAirport && selectedTerminal ? (
|
{transitions.length > 0 && selectedAirport && selectedTerminal ? (
|
||||||
<Map
|
<>
|
||||||
airport={selectedAirport}
|
<Sidebar
|
||||||
terminal={selectedTerminal}
|
airport={selectedAirport}
|
||||||
procedures={procedures}
|
terminal={selectedTerminal}
|
||||||
backAction={() => {
|
transitions={transitions}
|
||||||
setSelectedTerminal(undefined);
|
transition={selectedTransition}
|
||||||
setProcedures([]);
|
chart={selectedChart}
|
||||||
}}
|
setTransition={setSelectedTransition}
|
||||||
/>
|
setChart={setSelectedChart}
|
||||||
|
backAction={() => {
|
||||||
|
setSelectedTerminal(undefined);
|
||||||
|
setTransitions([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Map airport={selectedAirport} chart={selectedChart} procedure={selectedTransition} />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<h1 className="text-center text-3xl">Error</h1>
|
<h1 className="text-center text-3xl">Error</h1>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,197 +1,105 @@
|
|||||||
import BrowserImageManipulation from 'browser-image-manipulation';
|
import { default as L } from 'leaflet';
|
||||||
import { default as L, type LatLngBoundsExpression } from 'leaflet';
|
|
||||||
import 'leaflet-svg-shape-markers';
|
import 'leaflet-svg-shape-markers';
|
||||||
import { type Chart } from 'navigraph/charts';
|
import { createRef, type FC } from 'react';
|
||||||
import { createRef, useEffect, useState, type FC } from 'react';
|
|
||||||
import { GeoJSON, ImageOverlay, MapContainer, TileLayer } from 'react-leaflet';
|
import { GeoJSON, ImageOverlay, MapContainer, TileLayer } from 'react-leaflet';
|
||||||
import { charts } from '../lib/navigraph';
|
|
||||||
|
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
airport: Airport;
|
airport: Airport;
|
||||||
terminal: Terminal;
|
chart: Chart | undefined;
|
||||||
procedures: { name: string; data: object }[];
|
procedure: Procedure | undefined;
|
||||||
backAction: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Map: FC<MapProps> = ({ airport, terminal, procedures, backAction }) => {
|
export const Map: FC<MapProps> = ({ airport, chart, procedure }) => {
|
||||||
const [selectedProcedure, setSelectedProcedure] = useState(procedures[0]);
|
|
||||||
const [chartIndex, setChartIndex] = useState<Chart[]>([]);
|
|
||||||
const [selectedChart, setSelectedChart] = useState<{
|
|
||||||
data: string;
|
|
||||||
index_number: string;
|
|
||||||
bounds: LatLngBoundsExpression;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const mapRef = createRef<L.Map>();
|
const mapRef = createRef<L.Map>();
|
||||||
const imageRef = createRef<L.ImageOverlay>();
|
const imageRef = createRef<L.ImageOverlay>();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
setChartIndex((await charts.getChartsIndex({ icao: airport.ICAO, version: 'STD' })) ?? []);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MapContainer
|
||||||
<button
|
center={[airport.Latitude, airport.Longitude]}
|
||||||
className="fixed top-2 left-2 z-[5000] cursor-pointer rounded border border-red-500 bg-red-500 px-2 py-1 font-semibold text-stone-50 focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black"
|
zoom={13}
|
||||||
onClick={backAction}
|
zoomSnap={0}
|
||||||
>
|
className="h-full w-full"
|
||||||
Go back
|
ref={(_mapRef) => {
|
||||||
</button>
|
_mapRef?.attributionControl.setPosition('topright');
|
||||||
|
_mapRef?.zoomControl.setPosition('topright');
|
||||||
|
mapRef.current = _mapRef;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
|
||||||
<MapContainer
|
{chart && chart.bounds && (
|
||||||
center={[airport.Latitude, airport.Longitude]}
|
<ImageOverlay
|
||||||
zoom={13}
|
url={chart.data}
|
||||||
zoomSnap={0}
|
bounds={chart.bounds}
|
||||||
className="h-full w-full"
|
opacity={0.75}
|
||||||
ref={(_mapRef) => {
|
ref={(_imageRef) => {
|
||||||
_mapRef?.attributionControl.setPosition('topright');
|
if (_imageRef) {
|
||||||
_mapRef?.zoomControl.setPosition('topright');
|
mapRef.current?.fitBounds(_imageRef.getBounds(), {
|
||||||
mapRef.current = _mapRef;
|
padding: [-50, -50],
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedChart && selectedChart.bounds && (
|
|
||||||
<ImageOverlay
|
|
||||||
url={selectedChart.data}
|
|
||||||
bounds={selectedChart.bounds}
|
|
||||||
opacity={0.75}
|
|
||||||
ref={(_imageRef) => {
|
|
||||||
if (_imageRef) {
|
|
||||||
mapRef.current?.fitBounds(_imageRef.getBounds(), {
|
|
||||||
padding: [-50, -50],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
imageRef.current = _imageRef;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<GeoJSON
|
|
||||||
key={`${selectedProcedure.name}-lines`}
|
|
||||||
data={selectedProcedure.data}
|
|
||||||
style={({ properties }) => ({
|
|
||||||
color: '#ff00ff',
|
|
||||||
stroke: true,
|
|
||||||
weight: 5,
|
|
||||||
opacity: 1,
|
|
||||||
dashArray: properties.isManual ? '20, 20' : undefined,
|
|
||||||
})}
|
|
||||||
filter={(feature) => feature.geometry.type !== 'Point'}
|
|
||||||
/>
|
|
||||||
<GeoJSON
|
|
||||||
key={`${selectedProcedure.name}-points`}
|
|
||||||
data={selectedProcedure.data}
|
|
||||||
style={{
|
|
||||||
color: 'black',
|
|
||||||
fill: true,
|
|
||||||
fillColor: 'transparent',
|
|
||||||
stroke: true,
|
|
||||||
weight: 3,
|
|
||||||
}}
|
|
||||||
pointToLayer={({ properties }, latlng) => {
|
|
||||||
if (properties.isFlyOver)
|
|
||||||
return L.shapeMarker(latlng, {
|
|
||||||
shape: 'triangle',
|
|
||||||
radius: 6,
|
|
||||||
});
|
});
|
||||||
if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 });
|
}
|
||||||
|
imageRef.current = _imageRef;
|
||||||
return L.shapeMarker(latlng, {
|
|
||||||
shape: 'star-4',
|
|
||||||
radius: 10,
|
|
||||||
rotation: 45,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onEachFeature={({ geometry, properties }, layer) => {
|
/>
|
||||||
if (geometry.type === 'Point') {
|
)}
|
||||||
layer.bindPopup(
|
|
||||||
`${properties.name}<br>
|
{procedure && (
|
||||||
|
<>
|
||||||
|
<GeoJSON
|
||||||
|
key={`${procedure.name}-lines`}
|
||||||
|
data={procedure.data}
|
||||||
|
style={({ properties }) => ({
|
||||||
|
color: '#ff00ff',
|
||||||
|
stroke: true,
|
||||||
|
weight: 5,
|
||||||
|
opacity: 1,
|
||||||
|
dashArray: properties.isManual ? '20, 20' : undefined,
|
||||||
|
})}
|
||||||
|
filter={(feature) => feature.geometry.type !== 'Point'}
|
||||||
|
/>
|
||||||
|
<GeoJSON
|
||||||
|
key={`${procedure.name}-points`}
|
||||||
|
data={procedure.data}
|
||||||
|
style={{
|
||||||
|
color: 'black',
|
||||||
|
fill: true,
|
||||||
|
fillColor: 'transparent',
|
||||||
|
stroke: true,
|
||||||
|
weight: 3,
|
||||||
|
}}
|
||||||
|
pointToLayer={({ properties }, latlng) => {
|
||||||
|
if (properties.isFlyOver)
|
||||||
|
return L.shapeMarker(latlng, {
|
||||||
|
shape: 'triangle',
|
||||||
|
radius: 6,
|
||||||
|
});
|
||||||
|
if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 });
|
||||||
|
|
||||||
|
return L.shapeMarker(latlng, {
|
||||||
|
shape: 'star-4',
|
||||||
|
radius: 10,
|
||||||
|
rotation: 45,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onEachFeature={({ geometry, properties }, layer) => {
|
||||||
|
if (geometry.type === 'Point') {
|
||||||
|
layer.bindPopup(
|
||||||
|
`${properties.name}<br>
|
||||||
${properties.altitude} ft<br>
|
${properties.altitude} ft<br>
|
||||||
${properties.speed} kts<br>
|
${properties.speed} kts<br>
|
||||||
CNSTR:
|
CNSTR:
|
||||||
${properties.altitudeConstraint ?? ''}
|
${properties.altitudeConstraint ?? ''}
|
||||||
${properties.speedConstraint ?? ''}<br>`
|
${properties.speedConstraint ?? ''}<br>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
filter={(feature) => feature.geometry.type === 'Point'}
|
filter={(feature) => feature.geometry.type === 'Point'}
|
||||||
/>
|
/>
|
||||||
</MapContainer>
|
</>
|
||||||
|
)}
|
||||||
<div className="absolute right-0 bottom-0 left-0 z-[5000] bg-[#ffffff77] bg-blend-color backdrop-blur-xs">
|
</MapContainer>
|
||||||
<div className="flex items-center gap-2 overflow-x-auto p-2">
|
|
||||||
<span className="text-lg font-semibold">Procedures:</span>
|
|
||||||
{procedures.map((procedure) => (
|
|
||||||
<button
|
|
||||||
key={procedure.name}
|
|
||||||
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${selectedProcedure.name === procedure.name ? 'outline-2' : ''}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedProcedure.name === procedure.name) return;
|
|
||||||
|
|
||||||
setSelectedProcedure(procedure);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{procedure.name ? procedure.name : 'ZZZZ'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-lg font-semibold">Charts:</span>
|
|
||||||
<div className="flex items-center gap-2 overflow-x-auto p-2">
|
|
||||||
{chartIndex
|
|
||||||
.filter((chart) => chart.is_georeferenced)
|
|
||||||
.map((chart) => (
|
|
||||||
<button
|
|
||||||
key={chart.index_number}
|
|
||||||
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold whitespace-nowrap focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${selectedChart?.index_number === chart.index_number ? 'outline-2' : ''}`}
|
|
||||||
onClick={async () => {
|
|
||||||
if (selectedChart?.index_number === chart.index_number) return;
|
|
||||||
|
|
||||||
if (!mapRef.current) return;
|
|
||||||
|
|
||||||
if (!chart.bounding_boxes) return;
|
|
||||||
|
|
||||||
const planView = chart.bounding_boxes.planview;
|
|
||||||
|
|
||||||
const chartImage = await charts.getChartImage({ chart, theme: 'light' });
|
|
||||||
if (!chartImage) return;
|
|
||||||
// Crop
|
|
||||||
const dataURL = await new BrowserImageManipulation()
|
|
||||||
.loadBlob(chartImage)
|
|
||||||
.crop(
|
|
||||||
planView.pixels.x2 - planView.pixels.x1,
|
|
||||||
planView.pixels.y1 - planView.pixels.y2,
|
|
||||||
planView.pixels.x1,
|
|
||||||
planView.pixels.y2
|
|
||||||
)
|
|
||||||
.saveAsImage();
|
|
||||||
|
|
||||||
const bounds = new L.LatLngBounds(
|
|
||||||
[planView.latlng.lat1, planView.latlng.lng1],
|
|
||||||
[planView.latlng.lat2, planView.latlng.lng2]
|
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedChart({ data: dataURL, index_number: chart.index_number, bounds });
|
|
||||||
|
|
||||||
if (imageRef.current) {
|
|
||||||
mapRef.current?.fitBounds(imageRef.current.getBounds(), {
|
|
||||||
padding: [-50, -50],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{chart.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
109
browser/src/components/Sidebar.tsx
Normal file
109
browser/src/components/Sidebar.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import BrowserImageManipulation from 'browser-image-manipulation';
|
||||||
|
import L from 'leaflet';
|
||||||
|
import type { Chart as NGChart } from 'navigraph/charts';
|
||||||
|
import { useEffect, useState, type Dispatch, type FC, type SetStateAction } from 'react';
|
||||||
|
import { charts } from '../lib/navigraph';
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
airport: Airport;
|
||||||
|
terminal: Terminal;
|
||||||
|
transitions: Procedure[];
|
||||||
|
transition: Procedure | undefined;
|
||||||
|
chart: Chart | undefined;
|
||||||
|
setTransition: Dispatch<SetStateAction<SidebarProps['transition']>>;
|
||||||
|
setChart: Dispatch<SetStateAction<SidebarProps['chart']>>;
|
||||||
|
backAction: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: FC<SidebarProps> = ({
|
||||||
|
airport,
|
||||||
|
terminal,
|
||||||
|
transitions,
|
||||||
|
transition,
|
||||||
|
chart,
|
||||||
|
setTransition,
|
||||||
|
setChart,
|
||||||
|
backAction,
|
||||||
|
}) => {
|
||||||
|
const [chartIndex, setChartIndex] = useState<NGChart[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setChartIndex((await charts.getChartsIndex({ icao: airport.ICAO, version: 'STD' })) ?? []);
|
||||||
|
})();
|
||||||
|
}, [airport.ICAO]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-[300px] shrink-0 flex-col overflow-hidden">
|
||||||
|
<button
|
||||||
|
className="sticky top-0 m-2 cursor-pointer rounded border border-red-500 bg-red-500 px-2 py-1 font-semibold text-stone-50 focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black"
|
||||||
|
onClick={backAction}
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex h-[calc(100%-50px)] flex-col gap-2 overflow-y-auto px-2 pb-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="sticky top-0 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">
|
||||||
|
Transitions for <span className="font-bold">{terminal.FullName}</span>
|
||||||
|
</div>
|
||||||
|
{transitions.map((_procedure) => (
|
||||||
|
<button
|
||||||
|
key={_procedure.name}
|
||||||
|
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 font-semibold focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${_procedure.name === transition?.name ? 'outline-2' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (_procedure.name === transition?.name) return;
|
||||||
|
|
||||||
|
setTransition(_procedure);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{_procedure.name ? _procedure.name : 'ZZZZ'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="sticky top-0 -mx-2 bg-gray-500 px-2 text-lg font-semibold text-white">Charts</div>
|
||||||
|
{chartIndex
|
||||||
|
.filter((_chart) => _chart.is_georeferenced)
|
||||||
|
.map((_chart) => (
|
||||||
|
<button
|
||||||
|
key={_chart.index_number}
|
||||||
|
className={`cursor-pointer rounded border border-gray-300 bg-gray-300 px-2 py-1 text-left font-semibold focus:outline-2 focus:outline-black focus-visible:outline-2 focus-visible:outline-black ${chart?.index_number === _chart.index_number ? 'outline-2' : ''}`}
|
||||||
|
onClick={async () => {
|
||||||
|
if (chart?.index_number === _chart.index_number) return;
|
||||||
|
if (!_chart.bounding_boxes) return;
|
||||||
|
|
||||||
|
const planView = _chart.bounding_boxes.planview;
|
||||||
|
|
||||||
|
const chartImage = await charts.getChartImage({ chart: _chart, theme: 'light' });
|
||||||
|
if (!chartImage) return;
|
||||||
|
// Crop
|
||||||
|
const dataURL = await new BrowserImageManipulation()
|
||||||
|
.loadBlob(chartImage)
|
||||||
|
.crop(
|
||||||
|
planView.pixels.x2 - planView.pixels.x1,
|
||||||
|
planView.pixels.y1 - planView.pixels.y2,
|
||||||
|
planView.pixels.x1,
|
||||||
|
planView.pixels.y2
|
||||||
|
)
|
||||||
|
.saveAsImage();
|
||||||
|
|
||||||
|
const bounds = new L.LatLngBounds(
|
||||||
|
[planView.latlng.lat1, planView.latlng.lng1],
|
||||||
|
[planView.latlng.lat2, planView.latlng.lng2]
|
||||||
|
);
|
||||||
|
|
||||||
|
setChart({ data: dataURL, index_number: _chart.index_number, bounds });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="font-bold">{_chart.index_number}</span>
|
||||||
|
<br />
|
||||||
|
{_chart.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
19
browser/src/contexts/NavigraphAuth/NavigraphAuthContext.ts
Normal file
19
browser/src/contexts/NavigraphAuth/NavigraphAuthContext.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { type User } from 'navigraph/auth';
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { auth } from '../../lib/navigraph';
|
||||||
|
|
||||||
|
interface NavigraphAuthContext {
|
||||||
|
initialized: boolean;
|
||||||
|
user: User | null;
|
||||||
|
signIn: typeof auth.signInWithDeviceFlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authContext = createContext<NavigraphAuthContext>({
|
||||||
|
initialized: false,
|
||||||
|
user: null,
|
||||||
|
signIn: () => Promise.reject('Not initialized'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useNavigraphAuth = () => {
|
||||||
|
return useContext(authContext);
|
||||||
|
};
|
||||||
@ -1,18 +1,7 @@
|
|||||||
import { type User } from 'navigraph/auth';
|
import type { User } from 'navigraph/auth';
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { auth } from '../lib/navigraph';
|
import { auth } from '../../lib/navigraph';
|
||||||
|
import { authContext } from './NavigraphAuthContext';
|
||||||
interface NavigraphAuthContext {
|
|
||||||
initialized: boolean;
|
|
||||||
user: User | null;
|
|
||||||
signIn: typeof auth.signInWithDeviceFlow;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authContext = createContext<NavigraphAuthContext>({
|
|
||||||
initialized: false,
|
|
||||||
user: null,
|
|
||||||
signIn: () => Promise.reject('Not initialized'),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Provider hook that creates auth object and handles state
|
// Provider hook that creates auth object and handles state
|
||||||
function useProvideAuth() {
|
function useProvideAuth() {
|
||||||
@ -42,12 +31,7 @@ function useProvideAuth() {
|
|||||||
// Provider component that wraps your app and makes auth object
|
// Provider component that wraps your app and makes auth object
|
||||||
// available to any child component that calls useAuth().
|
// available to any child component that calls useAuth().
|
||||||
export function NavigraphAuthProvider({ children }: { children: React.ReactNode }) {
|
export function NavigraphAuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const auth = useProvideAuth();
|
const _auth = useProvideAuth();
|
||||||
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
|
return <authContext.Provider value={_auth}>{children}</authContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hook for child components to get the auth object
|
|
||||||
// and re-render when it changes.
|
|
||||||
export const useNavigraphAuth = () => {
|
|
||||||
return useContext(authContext);
|
|
||||||
};
|
|
||||||
9
browser/src/types/index.d.ts
vendored
Normal file
9
browser/src/types/index.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export declare global {
|
||||||
|
type Chart = {
|
||||||
|
data: string;
|
||||||
|
index_number: string;
|
||||||
|
bounds: LatLngBoundsExpression;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Procedure = { name: string; data: object };
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user