Skip to content

Commit 5987c08

Browse files
Merge pull request #102 from sebastiankrll/enhance/improve-ux
Enhance/improve ux
2 parents 3ef2548 + 251366e commit 5987c08

35 files changed

Lines changed: 401 additions & 135 deletions

apps/ingestion/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async function fetchVatsimData(): Promise<void> {
5757
updateBookingsData();
5858

5959
const init: InitialData = {
60-
pilots: pilotsLong.map((p) => getPilotShort(p)),
60+
pilots: pilotsLong.filter((p) => p.live === "live").map((p) => getPilotShort(p)),
6161
airports: airportsLong.map((a) => getAirportShort(a)),
6262
controllers: controllersMerged,
6363
timestamp: new Date(vatsimData.general.update_timestamp),

apps/ingestion/src/pilot.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createHash } from "node:crypto";
2-
import { rdsGetMultiple, rdsGetSingle } from "@sr24/db/redis";
2+
import { rdsGetMultiple, rdsGetSingle, rdsSetMultiple } from "@sr24/db/redis";
33
import type { StaticAirport } from "@sr24/types/db";
44
import type { PilotDelta, PilotFlightPlan, PilotLong, PilotShort, PilotTimes } from "@sr24/types/interface";
55
import type { VatsimData, VatsimPilot, VatsimPilotFlightPlan, VatsimPrefile } from "@sr24/types/vatsim";
@@ -108,21 +108,27 @@ export async function mapPilots(vatsimData: VatsimData): Promise<PilotLong[]> {
108108
if (cachedPilot) {
109109
pilotLong = { ...cachedPilot, ...updatedFields };
110110
} else {
111-
pilotLong = {
112-
id: id,
113-
cid: String(pilot.cid),
114-
callsign: pilot.callsign,
115-
aircraft: pilot.flight_plan?.aircraft_short || "A320",
116-
name: pilot.name,
117-
server: pilot.server,
118-
pilot_rating: PILOT_RATINGS.find((r) => r.id === pilot.pilot_rating)?.short_name || "NEW",
119-
military_rating: MILITARY_RATINGS.find((r) => r.id === pilot.military_rating)?.short_name || "M0",
120-
flight_plan: await mapPilotFlightPlan(pilot.flight_plan),
121-
logon_time: new Date(pilot.logon_time),
122-
times: null,
123-
live: "live",
124-
...updatedFields,
125-
};
111+
const existing = (await rdsGetSingle(`pilot:${id}`)) as PilotLong | undefined;
112+
113+
if (existing) {
114+
pilotLong = { ...existing, ...updatedFields };
115+
} else {
116+
pilotLong = {
117+
id: id,
118+
cid: String(pilot.cid),
119+
callsign: pilot.callsign,
120+
aircraft: pilot.flight_plan?.aircraft_short || "A320",
121+
name: pilot.name,
122+
server: pilot.server,
123+
pilot_rating: PILOT_RATINGS.find((r) => r.id === pilot.pilot_rating)?.short_name || "NEW",
124+
military_rating: MILITARY_RATINGS.find((r) => r.id === pilot.military_rating)?.short_name || "M0",
125+
flight_plan: await mapPilotFlightPlan(pilot.flight_plan),
126+
logon_time: new Date(pilot.logon_time),
127+
times: null,
128+
live: "live",
129+
...updatedFields,
130+
};
131+
}
126132
}
127133

128134
pilotLong.vertical_speed = calculateVerticalSpeed(pilotLong, cachedPilot);
@@ -194,6 +200,8 @@ export async function mapPilots(vatsimData: VatsimData): Promise<PilotLong[]> {
194200
}
195201
}
196202

203+
await rdsSetMultiple(newPilotsLong, "pilot", (p) => p.id, 12 * 60 * 60);
204+
197205
cached = newCached;
198206
return newPilotsLong;
199207
}
@@ -208,7 +216,7 @@ function getPilotId(pilot: VatsimPilot | VatsimPrefile): string {
208216
.update(`${base}${plan ? `_${variable}` : ""}`)
209217
.digest();
210218
const b64url = digest.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
211-
return b64url.slice(0, 10);
219+
return b64url.slice(0, 16);
212220
}
213221

214222
export function getPilotDelta(): PilotDelta {
@@ -353,7 +361,7 @@ function mapPilotTimes(
353361
touch_down: new Date(sched_off_block.getTime() + TAXI_TIME_MS + enrouteTimeMs),
354362
sched_on_block: roundDateTo5Min(sched_on_block),
355363
on_block: sched_on_block,
356-
state: estimateInitState(current),
364+
state: prefile ? "Boarding" : estimateInitState(current),
357365
stop_counter: 0,
358366
};
359367
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#active-filters {
2+
position: absolute;
3+
left: 1rem;
4+
bottom: 2.5rem;
5+
width: 20rem;
6+
height: 24px;
7+
display: flex;
8+
align-items: center;
9+
gap: 8px;
10+
background: var(--color-bg);
11+
color: var(--color-light-text);
12+
border: 1px solid var(--color-border);
13+
box-shadow: var(--box-shadow);
14+
z-index: 1;
15+
font-size: var(--font-size-normal);
16+
}
17+
18+
#active-filters::after {
19+
content: "";
20+
position: absolute;
21+
top: 0;
22+
right: 0;
23+
height: 100%;
24+
width: 10px;
25+
background: linear-gradient(to left, var(--color-bg), transparent);
26+
}
27+
28+
#active-filters-icon {
29+
background-color: var(--color-green);
30+
width: 24px;
31+
height: 24px;
32+
}
33+
34+
#active-filters-icon use {
35+
color: var(--color-bg);
36+
}
37+
38+
#active-filters-count {
39+
flex-shrink: 0;
40+
}
41+
42+
#active-filters-selected {
43+
overflow: hidden;
44+
display: flex;
45+
align-items: center;
46+
gap: 4px;
47+
}
48+
49+
#active-filters-selected p {
50+
white-space: nowrap;
51+
background-color: var(--color-green);
52+
color: white;
53+
padding: 2px 4px;
54+
border-radius: 2px;
55+
font-size: var(--font-size-small);
56+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useFilterStatsStore, useFiltersStore } from "@/storage/zustand";
2+
import "./ActiveFilters.css";
3+
import Icon from "@/components/Icon/Icon";
4+
import type { FilterState } from "@/types/zustand";
5+
6+
export default function ActiveFilters() {
7+
const filters = useFiltersStore();
8+
const filterStats = useFilterStatsStore();
9+
10+
const activeFilters = getFilterValues(filters);
11+
12+
if (!filters.active) return null;
13+
14+
return (
15+
<div id="active-filters">
16+
<div id="active-filters-icon">
17+
<Icon name="filter" size={24} />
18+
</div>
19+
<div id="active-filters-count">
20+
<span style={{ color: "var(--color-main-text)", fontWeight: "var(--font-weight-bold)" }}>{filterStats.pilotCount[0]}</span> of{" "}
21+
{filterStats.pilotCount[1]} pilots
22+
</div>
23+
<div id="active-filters-selected">
24+
{activeFilters.map((filter) => (
25+
<p key={filter}>{filter}</p>
26+
))}
27+
</div>
28+
</div>
29+
);
30+
}
31+
32+
function getFilterValues(filterState: FilterState): string[] {
33+
return Object.entries(filterState)
34+
.filter(([_key, value]) => Array.isArray(value) && value.length > 0)
35+
.map(([key]) => key);
36+
}

apps/web/app/(map)/components/Map.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { usePathname, useRouter } from "next/navigation";
66
import { useTheme } from "next-themes";
77
import Initializer from "@/components/Initializer/Initializer";
88
import BasePanel from "@/components/Panel/BasePanel";
9-
import { useMapRotationStore, useSettingsStore } from "@/storage/zustand";
9+
import { useFilterStatsStore, useMapRotationStore, useSettingsStore } from "@/storage/zustand";
1010
import { init, mapService } from "../lib";
11+
import ActiveFilters from "./ActiveFilters";
1112
import Controls from "./Controls";
1213

1314
export default function OMap({ children }: { children?: React.ReactNode }) {
@@ -27,6 +28,7 @@ export default function OMap({ children }: { children?: React.ReactNode }) {
2728
firColor,
2829
} = useSettingsStore();
2930
const { setRotation } = useMapRotationStore();
31+
const { setPilotCount } = useFilterStatsStore();
3032

3133
useEffect(() => {
3234
const handleMoveEnd = () => {
@@ -35,15 +37,20 @@ export default function OMap({ children }: { children?: React.ReactNode }) {
3537
};
3638

3739
const map = mapService.init({ onNavigate: (href) => router.push(href), autoTrackPoints: true });
40+
3841
map.on("moveend", handleMoveEnd);
3942
mapService.addEventListeners();
4043

44+
mapService.subscribe((stats) => {
45+
setPilotCount([stats.pilots.rendered, stats.pilots.total]);
46+
});
47+
4148
return () => {
4249
mapService.removeEventListeners();
4350
map.un("moveend", handleMoveEnd);
4451
map.setTarget(undefined);
4552
};
46-
}, [router, setRotation]);
53+
}, [router, setRotation, setPilotCount]);
4754

4855
useEffect(() => {
4956
init(pathname);
@@ -82,6 +89,7 @@ export default function OMap({ children }: { children?: React.ReactNode }) {
8289
<Initializer />
8390
<BasePanel>{children}</BasePanel>
8491
<Controls />
92+
<ActiveFilters />
8593
<div id="map" />
8694
</>
8795
);

apps/web/app/(map)/components/Panels/Airport/AirportTitle.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,13 @@ function formatLocalTime(tz: string): string {
1212
hour12: false,
1313
}).format(now);
1414

15-
const offsetMinutes = new Date(now.toLocaleString("en-US", { timeZone: tz })).getTimezoneOffset() * -1;
16-
17-
const sign = offsetMinutes >= 0 ? "+" : "-";
18-
const abs = Math.abs(offsetMinutes);
19-
const hours = String(Math.floor(abs / 60)).padStart(2, "0");
20-
const minutes = String(abs % 60).padStart(2, "0");
21-
const utcOffset = `UTC ${sign}${hours}:${minutes}`;
22-
2315
const date = new Intl.DateTimeFormat("en-US", {
2416
month: "short",
2517
day: "numeric",
2618
timeZone: tz,
2719
}).format(now);
2820

29-
return `${time} | ${utcOffset} | ${date}`;
21+
return `${time} | ${date}`;
3022
}
3123

3224
export function AirportTitle({ staticAirport }: { staticAirport: StaticAirport | null }) {
Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,49 @@
11
import type { DashboardData } from "@sr24/types/interface";
2+
import { useMemo, useState } from "react";
23
import { Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
4+
import { ChooseSwitch } from "@/components/Input/Input";
35
import { useSettingsStore } from "@/storage/zustand";
46
import { convertTime } from "@/utils/helpers";
57

68
export function DashboardHistory({ history }: { history: DashboardData["history"] }) {
79
const { timeZone, timeFormat } = useSettingsStore();
10+
const [mode, setMode] = useState<"24 hours" | "7 days">("24 hours");
811

9-
const data = history.map((point) => ({
12+
const filteredHistory = useMemo(() => {
13+
const now = Date.now();
14+
const cutoff = mode === "24 hours" ? now - 24 * 60 * 60 * 1000 : now - 7 * 24 * 60 * 60 * 1000;
15+
16+
return history.filter(([timestamp]) => timestamp * 1000 >= cutoff);
17+
}, [history, mode]);
18+
19+
const data = filteredHistory.map((point) => ({
1020
name: `${new Date(point[0] * 1000).toLocaleDateString()} ${convertTime(point[0] * 1000, timeFormat, timeZone)}`,
1121
pilots: point[1],
1222
controllers: point[2],
1323
}));
1424

1525
return (
16-
<ResponsiveContainer width="100%" height={150} maxHeight={500}>
17-
<LineChart data={data} margin={{ top: 10, right: 5, bottom: 10, left: 5 }}>
18-
<YAxis
19-
yAxisId="all"
20-
orientation="left"
21-
stroke="var(--color-main-text)"
22-
fontSize="10px"
23-
width={30}
24-
tickSize={4}
25-
tickLine={false}
26-
axisLine={false}
27-
/>
28-
<XAxis dataKey="name" tick={false} mirror={true} axisLine={false} />
29-
<Line type="monotone" dataKey="controllers" yAxisId="all" stroke="var(--color-red)" dot={false} name="Controllers" />
30-
<Line type="monotone" dataKey="pilots" yAxisId="all" stroke="var(--color-green)" dot={false} name="Pilots" />
31-
<Legend verticalAlign="bottom" height={5} iconSize={10} wrapperStyle={{ fontSize: "10px" }} />
32-
<Tooltip wrapperStyle={{ fontSize: "10px" }} contentStyle={{ background: "var(--color-bg)", borderColor: "var(--color-border)" }} />
33-
</LineChart>
34-
</ResponsiveContainer>
26+
<div className="panel-section-content">
27+
<ChooseSwitch options={["24 hours", "7 days"] as const} value={mode} onChange={setMode} />
28+
<ResponsiveContainer width="100%" height={150} maxHeight={500}>
29+
<LineChart data={data} margin={{ top: 10, right: 5, bottom: 10, left: 5 }}>
30+
<YAxis
31+
yAxisId="all"
32+
orientation="left"
33+
stroke="var(--color-main-text)"
34+
fontSize="10px"
35+
width={30}
36+
tickSize={4}
37+
tickLine={false}
38+
axisLine={false}
39+
/>
40+
<XAxis dataKey="name" tick={false} mirror={true} axisLine={false} />
41+
<Line type="monotone" dataKey="controllers" yAxisId="all" stroke="var(--color-red)" dot={false} name="Controllers" />
42+
<Line type="monotone" dataKey="pilots" yAxisId="all" stroke="var(--color-green)" dot={false} name="Pilots" />
43+
<Legend verticalAlign="bottom" height={5} iconSize={10} wrapperStyle={{ fontSize: "10px" }} />
44+
<Tooltip wrapperStyle={{ fontSize: "10px" }} contentStyle={{ background: "var(--color-bg)", borderColor: "var(--color-border)" }} />
45+
</LineChart>
46+
</ResponsiveContainer>
47+
</div>
3548
);
3649
}

apps/web/app/(map)/components/Panels/Dashboard/DashboardStats.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { DashboardStats as Stats } from "@sr24/types/interface";
22
import { useRouter } from "next/navigation";
33
import { useState } from "react";
44
import { Fragment } from "react/jsx-runtime";
5+
import { ChooseSwitch } from "@/components/Input/Input";
56

67
export function DashboardStats({ stats }: { stats: Stats }) {
78
const router = useRouter();
@@ -29,20 +30,7 @@ export function DashboardStats({ stats }: { stats: Stats }) {
2930
</div>
3031
</div>
3132
<div className="panel-sub-container sep" id="panel-dashboard-busiest">
32-
<div id="dashboard-stats-navigation">
33-
<button className={openTab === "airports" ? "active" : ""} type="button" onClick={() => setOpenTab("airports")}>
34-
Airports
35-
</button>
36-
<button className={openTab === "routes" ? "active" : ""} type="button" onClick={() => setOpenTab("routes")}>
37-
Routes
38-
</button>
39-
<button className={openTab === "aircrafts" ? "active" : ""} type="button" onClick={() => setOpenTab("aircrafts")}>
40-
Aircrafts
41-
</button>
42-
<button className={openTab === "controllers" ? "active" : ""} type="button" onClick={() => setOpenTab("controllers")}>
43-
Controllers
44-
</button>
45-
</div>
33+
<ChooseSwitch options={["airports", "routes", "aircrafts", "controllers"] as const} value={openTab} onChange={setOpenTab} />
4634
{openTab === "airports" && <AirportStats stats={stats} router={router} />}
4735
{openTab === "routes" && <RouteStats stats={stats} />}
4836
{openTab === "aircrafts" && <AircraftStats stats={stats} />}

apps/web/app/(map)/components/Panels/Filters/FiltersPanel.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
.filter-input {
9595
display: flex;
9696
gap: 2px;
97+
align-items: center;
9798
}
9899

99100
#filter-input-route {

apps/web/app/(map)/components/Panels/Filters/FiltersPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const UPPERCASE_FILTERS = ["Aircraft Registration", "Callsign"];
2727
const RANGE_FILTERS = ["Barometric Altitude", "Groundspeed"];
2828

2929
export default function FiltersPanel() {
30-
const { setFilters, setActive, resetAllFilters } = useFiltersStore();
30+
const { setFilters, resetAllFilters, setActive } = useFiltersStore();
3131

3232
const [options, setOptions] = useState<string[]>([]);
3333
const [inputs, setInputs] = useState<string[]>([]);

0 commit comments

Comments
 (0)