Skip to content

Commit 77e63e8

Browse files
feat: Charts pane
1 parent 716dabf commit 77e63e8

File tree

10 files changed

+224
-18
lines changed

10 files changed

+224
-18
lines changed

examples/playground/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
"dependencies": {
1313
"@navigraph/app": "*",
1414
"@navigraph/auth": "*",
15+
"@navigraph/charts": "*",
1516
"@navigraph/leaflet": "*",
17+
"@tanstack/react-query": "^5.52.2",
1618
"clsx": "^2.1.1",
1719
"leaflet": "^1.9.4",
1820
"react": "^18.3.1",
1921
"react-dom": "^18.3.1",
2022
"react-icons": "^5.3.0",
2123
"react-leaflet": "^4.2.1",
24+
"react-loading-icons": "^1.1.0",
2225
"react-router-dom": "^6.26.1",
2326
"recoil": "^0.7.7"
2427
},
@@ -38,4 +41,4 @@
3841
"typescript-eslint": "^8.0.1",
3942
"vite": "^5.4.1"
4043
}
41-
}
44+
}

examples/playground/src/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import useAppConfigLoader from "./hooks/useAppConfigLoader"
66
import App from "./pages/App"
77
import Auth from "./pages/Auth"
88
import Tiles from "./pages/Tiles"
9+
import Charts from "./pages/Charts"
910

1011
function Root() {
1112
useAppConfigLoader();
@@ -18,6 +19,7 @@ function Root() {
1819
<Route path="/app" element={<App />} />
1920
<Route path="/auth" element={<Auth />} />
2021
<Route path="/tiles" element={<Tiles />} />
22+
<Route path="/charts" element={<Charts />} />
2123
</Routes>
2224
<Outlet />
2325
<MainWindow />
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import clsx from "clsx"
2+
3+
interface Props {
4+
segments: (string | { label: string, disabled?: boolean })[]
5+
index: number,
6+
onChange: (index: number) => void,
7+
inactiveTextColor?: (index: number) => string,
8+
activeBackgroundColor?: (index: number) => string,
9+
}
10+
11+
export default function SegmentControl({ segments, index, onChange, inactiveTextColor, activeBackgroundColor }: Props) {
12+
return (
13+
<div className="flex flex-row bg-ng-background-500 rounded-md border-2 border-ng-background-600">
14+
{segments.map((segment, i) => {
15+
const [disabled, label] = typeof segment === 'object' ? [segment.disabled ?? false, segment.label] : [false, segment];
16+
17+
return (
18+
<button
19+
onClick={() => onChange(i)}
20+
disabled={disabled}
21+
className={clsx(
22+
"flex-1 rounded-md font-semibold text-xs enabled:hover:shadow-lg p-1 disabled:text-gray-300",
23+
index === i ? activeBackgroundColor?.(i) ?? 'bg-blue-50' : 'enabled:hover:bg-white enabled:hover:bg-opacity-5',
24+
index === i || !inactiveTextColor ? 'text-white' : inactiveTextColor?.(i)
25+
)}>
26+
{label}
27+
</button>
28+
)
29+
})}
30+
</div>
31+
);
32+
}

examples/playground/src/components/SideBar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import clsx from "clsx";
22
import { IconType } from "react-icons";
3-
import { FaMap, FaUser } from "react-icons/fa";
3+
import { FaGlobe, FaMap, FaUser } from "react-icons/fa";
44
import { MdOutlineSettings } from "react-icons/md";
55
import { NavLink } from "react-router-dom";
66
import { appState } from "../state/app";
@@ -37,7 +37,8 @@ export default function SideBar() {
3737
<div className="flex flex-col w-20 p-3 gap-5">
3838
<SideBarLink path="/app" icon={MdOutlineSettings}>App</SideBarLink>
3939
<SideBarLink path="/auth" icon={FaUser} disabled={!app}>Auth</SideBarLink>
40-
<SideBarLink path="/tiles" icon={FaMap} disabled={!user?.scope.includes(Scope.TILES)}>Tiles</SideBarLink>
40+
<SideBarLink path="/tiles" icon={FaGlobe} disabled={!user?.scope.includes(Scope.TILES)}>Tiles</SideBarLink>
41+
<SideBarLink path="/charts" icon={FaMap} disabled={!user?.scope.includes(Scope.CHARTS)}>Charts</SideBarLink>
4142
</div >
4243
)
4344
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useRecoilValue } from "recoil";
2+
import { appState } from "../state/app";
3+
import { userState } from "../state/user";
4+
import { useNavigate } from "react-router-dom";
5+
import { NavigraphAuth, User } from "@navigraph/auth";
6+
import { Scope } from "@navigraph/app";
7+
import { ReactNode } from "react";
8+
import { getChartsAPI } from "@navigraph/charts";
9+
10+
interface PropsStruct {
11+
[Scope.CHARTS]: ReturnType<typeof getChartsAPI>
12+
}
13+
14+
export function protectedPage<P extends {}, S extends Scope[]>(Component: (props: P & { auth: NavigraphAuth, user: User } & Pick<PropsStruct, Extract<S[number], keyof PropsStruct>>) => ReactNode, requiredScopes: S) {
15+
return (props: P) => {
16+
const navigate = useNavigate();
17+
18+
const app = useRecoilValue(appState);
19+
20+
const user = useRecoilValue(userState);
21+
22+
if (!app || !user || !requiredScopes.every((scope) => user.scope.includes(scope))) {
23+
navigate('/');
24+
return null
25+
}
26+
27+
const charts = requiredScopes.includes(Scope.CHARTS) ? getChartsAPI() : undefined;
28+
29+
return Component({ user, auth: app.auth, charts, ...props } as unknown as any);
30+
};
31+
}

examples/playground/src/index.css

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
@tailwind utilities;
44

55
@layer {
6+
::-webkit-scrollbar {
7+
@apply w-2
8+
}
9+
10+
::-webkit-scrollbar-track {
11+
@apply bg-ng-background-300 rounded-lg
12+
}
13+
14+
::-webkit-scrollbar-thumb {
15+
@apply bg-blue-100 rounded-lg
16+
}
17+
618
:root {
719
@apply bg-ng-background-600
820
}
@@ -15,7 +27,7 @@
1527
}
1628

1729
.page-container {
18-
@apply w-96 bg-ng-background-100 text-xl pt-3
30+
@apply w-96 bg-ng-background-400 text-xl pt-3
1931
}
2032

2133
.no-scrollbar::-webkit-scrollbar {
@@ -28,6 +40,6 @@
2840
}
2941

3042
.pane {
31-
@apply bg-ng-background-400 p-2 rounded-lg shadow-md
43+
@apply bg-ng-background-500 p-2 rounded-lg shadow-md border-2 border-ng-background-800
3244
}
3345
}

examples/playground/src/pages/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default function App() {
6060
<div className="page-container flex flex-col items-center gap-3">
6161
<h1>App Configuration</h1>
6262

63-
<div className="flex flex-col gap-2 bg-ng-background-400 p-2 rounded-lg shadow-md">
63+
<div className="flex flex-col gap-2 pane">
6464
<TextField value={clientId ?? ''} onChange={setClientId} label="Client ID" className="w-64" disabled={!editUnlocked} />
6565
<TextField value={clientSecret ?? ''} onChange={setClientSecret} label="Client Secret" className="w-64" disabled={!editUnlocked} />
6666
<span className="text-sm">Scopes: </span>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { FaMagnifyingGlass } from "react-icons/fa6";
2+
import { TextField } from "../components/TextField";
3+
import { useState } from "react";
4+
import { useQuery } from "@tanstack/react-query";
5+
import { Scope } from "@navigraph/app";
6+
import { protectedPage } from "../components/protectedPage";
7+
import { AirportInfo, Chart, getChartsAPI } from "@navigraph/charts";
8+
import SpinningCircles from "react-loading-icons/dist/esm/components/spinning-circles";
9+
import JsonView from "../components/JsonView";
10+
import SegmentControl from "../components/SegmentControl";
11+
import clsx from "clsx";
12+
import Button from "../components/Button";
13+
import { IoLayers } from "react-icons/io5";
14+
import { LuTableProperties } from "react-icons/lu";
15+
16+
enum ChartCategory {
17+
STAR,
18+
APP,
19+
TAXI,
20+
SID,
21+
REF
22+
}
23+
24+
const categoryKeys = [
25+
'ARR',
26+
'APP',
27+
'APT',
28+
'DEP',
29+
'REF'
30+
]
31+
32+
function ChartRow({ chart }: { chart: Chart }) {
33+
const [open, setOpen] = useState(false);
34+
35+
return (
36+
<div className="max-h-96 flex flex-col gap-1">
37+
<div className="py-1 px-4 border-y-[0.5px] border-ng-background-500 flex gap-2 items-center justify-between hover:bg-ng-background-300 cursor-pointer">
38+
<span className="text-sm font-semibold">{chart.name}<br />{chart.index_number}</span>
39+
<div className="flex gap-2">
40+
<Button selected={open} onClick={() => setOpen((x) => !x)}><LuTableProperties className="text-gray-25" size={20} /></Button>
41+
<Button onClick={() => null}><IoLayers className="text-gray-25" size={20} /></Button>
42+
</div>
43+
</div>
44+
{open && <JsonView content={chart} />}
45+
</div>
46+
)
47+
}
48+
49+
function ChartsPage({ airport, charts }: { airport: AirportInfo, charts: ReturnType<typeof getChartsAPI> }) {
50+
const { data: index, isLoading } = useQuery({
51+
queryKey: ['charts-index', airport.icao_airport_identifier],
52+
queryFn: () => charts.getChartsIndex({ icao: airport.icao_airport_identifier }),
53+
})
54+
55+
const [category, setCategory] = useState(ChartCategory.STAR);
56+
57+
if (isLoading) return <SpinningCircles />
58+
59+
if (!index) return null;
60+
61+
return (
62+
<>
63+
<SegmentControl
64+
segments={Object.keys(ChartCategory).filter((x) => x !== '0' && !parseInt(x))}
65+
index={category}
66+
onChange={setCategory}
67+
activeBackgroundColor={(i) => ['bg-star-700', 'bg-app-600', 'bg-rwy-700', 'bg-sid-500', 'bg-[#A264D8]'][i]}
68+
inactiveTextColor={(i) => ['text-star-700', 'text-app-600', 'text-rwy-700', 'text-sid-500', 'text-[#A264D8]'][i]}
69+
/>
70+
<div className="flex flex-col overflow-auto flex-1 border-[1px] border-ng-background-500 w-full">
71+
{index.filter((chart) => chart.category === categoryKeys[category]).map((chart) => <ChartRow key={chart.id} chart={chart} />)}
72+
</div>
73+
</>
74+
)
75+
}
76+
77+
enum AirportPage {
78+
Info,
79+
Charts
80+
}
81+
82+
function AirportPane({ airport, charts }: { airport: AirportInfo, charts: ReturnType<typeof getChartsAPI> }) {
83+
const [page, setPage] = useState(AirportPage.Info)
84+
85+
return (
86+
<>
87+
<SegmentControl index={page} onChange={setPage} segments={['Info', 'Charts']} />
88+
{page === AirportPage.Info ? <JsonView content={airport} /> : <ChartsPage airport={airport} charts={charts} />}
89+
</>
90+
)
91+
}
92+
93+
const Charts = protectedPage(({ charts }) => {
94+
const [icao, setIcao] = useState('');
95+
96+
const { data: airport, isLoading } = useQuery({
97+
queryKey: ['charts-airport', icao],
98+
queryFn: () => charts.getAirportInfo({ icao }),
99+
enabled: icao.length === 4
100+
})
101+
102+
return (
103+
<div className="page-container flex flex-col items-center gap-3">
104+
<h1>Charts</h1>
105+
106+
<TextField label="Airport ICAO" icon={FaMagnifyingGlass} value={icao} onChange={setIcao} />
107+
108+
{icao.length === 4 && (isLoading ? <SpinningCircles /> : airport ? <AirportPane airport={airport} charts={charts} /> : <span>{icao} has no Charts</span>)}
109+
</div>
110+
)
111+
}, [Scope.CHARTS]);
112+
113+
export default Charts;

examples/playground/src/pages/Tiles.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
1-
import { useRecoilState, useRecoilValue } from "recoil";
2-
import { appState } from "../state/app";
3-
import { redirect } from "react-router-dom";
1+
import { useRecoilState } from "recoil";
42
import Button from "../components/Button";
53
import { mapFaaState, mapSourceState, mapTacState, mapThemeState } from "../state/mapStyle";
64
import { FaMoon, FaSun } from "react-icons/fa";
75
import JsonView from "../components/JsonView";
86
import { createPreset } from "../components/Map";
7+
import { protectedPage } from "../components/protectedPage";
8+
import { Scope } from "@navigraph/app";
99

10-
export default function Tiles() {
11-
const app = useRecoilValue(appState);
12-
10+
const Tiles = protectedPage(() => {
1311
const [source, setSource] = useRecoilState(mapSourceState);
1412
const [theme, setTheme] = useRecoilState(mapThemeState);
1513
const [faa, setFaa] = useRecoilState(mapFaaState);
1614
const [tac, setTac] = useRecoilState(mapTacState);
1715

18-
if (!app) {
19-
redirect('/');
20-
return null;
21-
}
22-
2316
return (
2417
<div className="page-container flex flex-col items-center gap-3 px-3">
2518
<h1>Tiles</h1>
@@ -43,4 +36,6 @@ export default function Tiles() {
4336
<JsonView content={createPreset(source, theme, faa, tac)} />
4437
</div>
4538
)
46-
}
39+
}, [Scope.TILES]);
40+
41+
export default Tiles;

yarn.lock

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,18 @@
16851685
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a"
16861686
integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==
16871687

1688+
"@tanstack/[email protected]":
1689+
version "5.52.2"
1690+
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.52.2.tgz#a023864a892fda9858b724d667eb19cd84ce054a"
1691+
integrity sha512-9vvbFecK4A0nDnrc/ks41e3UHONF1DAnGz8Tgbxkl59QcvKWmc0ewhYuIKRh8NC4ja5LTHT9EH16KHbn2AIYWA==
1692+
1693+
"@tanstack/react-query@^5.52.2":
1694+
version "5.52.2"
1695+
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.52.2.tgz#3fffbc86351edcaeec335bc8958bcab4204bd169"
1696+
integrity sha512-d4OwmobpP+6+SvuAxW1RzAY95Pv87Gu+0GjtErzFOUXo+n0FGcwxKvzhswCsXKxsgnAr3bU2eJ2u+GXQAutkCQ==
1697+
dependencies:
1698+
"@tanstack/query-core" "5.52.2"
1699+
16881700
"@tootallnate/once@2":
16891701
version "2.0.0"
16901702
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -5592,6 +5604,11 @@ react-leaflet@^4.2.1:
55925604
dependencies:
55935605
"@react-leaflet/core" "^2.1.0"
55945606

5607+
react-loading-icons@^1.1.0:
5608+
version "1.1.0"
5609+
resolved "https://registry.yarnpkg.com/react-loading-icons/-/react-loading-icons-1.1.0.tgz#c37f2472936ab93c6a7f43c0a2c2fe8efc3ff7c8"
5610+
integrity sha512-Y9eZ6HAufmUd8DIQd6rFrx5Bt/oDlTM9Nsjvf8YpajTa3dI8cLNU8jUN5z7KTANU+Yd6/KJuBjxVlrU2dMw33g==
5611+
55955612
react-refresh@^0.14.0:
55965613
version "0.14.0"
55975614
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"

0 commit comments

Comments
 (0)