Skip to content

Commit 36b0423

Browse files
committed
Show TideConditions in map popup
1 parent e0116d6 commit 36b0423

9 files changed

Lines changed: 217 additions & 104 deletions

File tree

packages/react/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@visx/tooltip": "^4.0.1-alpha.0",
6565
"astronomy-engine": "^2.1.19",
6666
"coordinate-format": "^1.0.0",
67+
"culori": "^4.0.2",
6768
"d3-array": "^3.2.1",
6869
"date-fns": "^3.6.0"
6970
},
@@ -73,6 +74,7 @@
7374
"@tailwindcss/vite": "^4.2.0",
7475
"@testing-library/react": "^16.0.0",
7576
"@testing-library/user-event": "^14.6.1",
77+
"@types/culori": "^4.0.1",
7678
"@types/react": "^19.0.0",
7779
"@types/react-dom": "^19.0.0",
7880
"@vitest/browser-playwright": "^4.0.18",

packages/react/src/components/StationsMap.stories.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { StationsMap } from "./StationsMap.js";
55
const meta: Meta<typeof StationsMap> = {
66
title: "Components/StationsMap",
77
component: StationsMap,
8+
parameters: {
9+
layout: "fullscreen",
10+
},
811
decorators: [
912
(Story) => (
10-
<div style={{ width: "100%", height: 500 }}>
13+
<div style={{ width: "100vw", height: "100vh" }}>
1114
<Story />
1215
</div>
1316
),

packages/react/src/components/StationsMap.tsx

Lines changed: 32 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ import { keepPreviousData } from "@tanstack/react-query";
2121
import { useStation } from "../hooks/use-station.js";
2222
import { useStations } from "../hooks/use-stations.js";
2323
import { useDebouncedCallback } from "../hooks/use-debounced-callback.js";
24-
import { useExtremes } from "../hooks/use-extremes.js";
2524
import { useNeapsConfig } from "../provider.js";
2625
import { useDarkMode } from "../hooks/use-dark-mode.js";
2726
import { useThemeColors } from "../hooks/use-theme-colors.js";
28-
import { formatLevel, formatTime } from "../utils/format.js";
29-
import type { StationSummary, Extreme } from "../types.js";
27+
import { TideConditions } from "./TideConditions.js";
28+
import type { StationSummary } from "../types.js";
3029

3130
// Props that StationsMap manages internally and cannot be overridden
3231
type ManagedMapProps = "onMove" | "onClick" | "interactiveLayerIds" | "style" | "cursor";
@@ -61,51 +60,19 @@ export interface StationsMapProps extends Omit<ComponentProps<typeof Map>, Manag
6160
function stationsToGeoJSON(stations: StationSummary[]): GeoJSON.FeatureCollection {
6261
return {
6362
type: "FeatureCollection",
64-
features: stations.map((s) => ({
65-
type: "Feature" as const,
66-
geometry: { type: "Point" as const, coordinates: [s.longitude, s.latitude] },
67-
properties: { id: s.id, name: s.name, region: s.region, country: s.country, type: s.type },
63+
features: stations.map(({ longitude, latitude, ...properties }) => ({
64+
type: "Feature",
65+
geometry: {
66+
type: "Point",
67+
coordinates: [longitude, latitude],
68+
},
69+
properties,
6870
})),
6971
};
7072
}
7173

72-
function getNextExtreme(extremes: Extreme[]): Extreme | null {
73-
const now = new Date();
74-
return extremes.find((e) => e.time > now) ?? null;
75-
}
76-
7774
function StationPreviewCard({ stationId }: { stationId: string }) {
78-
const config = useNeapsConfig();
79-
const now = new Date();
80-
const end = new Date(now.getTime() + 24 * 60 * 60 * 1000);
81-
const { data, isLoading } = useExtremes({
82-
id: stationId,
83-
start: now.toISOString(),
84-
end: end.toISOString(),
85-
});
86-
87-
if (isLoading) {
88-
return <span className="text-xs text-(--neaps-text-muted)">Loading...</span>;
89-
}
90-
91-
if (!data) return null;
92-
93-
const next = getNextExtreme(data.extremes);
94-
return (
95-
<div className="text-xs">
96-
{next && (
97-
<div className="mt-1">
98-
<span className="text-(--neaps-text-muted)">Next {next.high ? "High" : "Low"}: </span>
99-
<span className="font-semibold text-(--neaps-text)">
100-
{formatLevel(next.level, data.units ?? config.units)}{" "}
101-
<span className="text-(--neaps-text-muted)">
102-
at {formatTime(next.time, data.station?.timezone ?? "UTC", config.locale)}
103-
</span>
104-
</span>
105-
</div>
106-
)}
107-
</div>
108-
);
75+
return <TideConditions id={stationId} showDate={false} />;
10976
}
11077

11178
export const StationsMap = forwardRef<MapRef, StationsMapProps>(function StationsMap(
@@ -117,7 +84,7 @@ export const StationsMap = forwardRef<MapRef, StationsMapProps>(function Station
11784
focusStation,
11885
showGeolocation = true,
11986
clustering = true,
120-
clusterMaxZoom: clusterMaxZoomProp = 14,
87+
clusterMaxZoom: clusterMaxZoomProp = 7,
12188
clusterRadius: clusterRadiusProp = 50,
12289
popupContent = "preview",
12390
children,
@@ -198,26 +165,20 @@ export const StationsMap = forwardRef<MapRef, StationsMapProps>(function Station
198165
// Station point click
199166
if (props?.id) {
200167
const coords = (feature.geometry as GeoJSON.Point).coordinates;
201-
const station: StationSummary = {
202-
id: props.id,
203-
name: props.name,
168+
const station = {
169+
...props,
204170
latitude: coords[1],
205171
longitude: coords[0],
206-
region: props.region ?? "",
207-
country: props.country ?? "",
208-
continent: "",
209-
timezone: "",
210-
type: props.type ?? "reference",
211-
};
172+
} as StationSummary;
212173

213-
if (popupContent === "preview" ? (viewState.zoom ?? 0) >= 10 : popupContent !== false) {
174+
if (popupContent !== false) {
214175
setSelectedStation(station);
215176
}
216177

217178
onStationSelect?.(station);
218179
}
219180
},
220-
[onStationSelect, viewState.zoom, popupContent],
181+
[onStationSelect, popupContent],
221182
);
222183

223184
const handleLocateMe = useCallback(() => {
@@ -372,18 +333,29 @@ export const StationsMap = forwardRef<MapRef, StationsMapProps>(function Station
372333
<Popup
373334
longitude={selectedStation.longitude}
374335
latitude={selectedStation.latitude}
375-
anchor="bottom"
336+
maxWidth="none"
337+
offset={10}
376338
onClose={() => setSelectedStation(null)}
377339
closeOnClick={false}
378-
className="neaps-popup"
340+
closeButton={false}
379341
>
380-
<div className="p-2 min-w-40">
342+
<div className="relative">
381343
{typeof popupContent === "function" ? (
382344
popupContent(selectedStation)
383345
) : (
384346
<>
385-
<div className="font-semibold text-sm text-(--neaps-text)">
386-
{selectedStation.name}
347+
<div className="flex gap-2 justify-between items-start mb-2">
348+
<div className="text-base font-semibold text-(--neaps-text)">
349+
{selectedStation.name}
350+
</div>
351+
<button
352+
type="button"
353+
onClick={() => setSelectedStation(null)}
354+
className="text-(--neaps-text-muted) hover:text-(--neaps-text) cursor-pointer leading-4 text-lg"
355+
aria-label="Close popup"
356+
>
357+
×
358+
</button>
387359
</div>
388360
{popupContent === "simple" ? (
389361
<div className="text-xs text-(--neaps-text-muted)">
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { TideConditions } from "./TideConditions.js";
3+
4+
const STATION_ID = "noaa/8443970";
5+
6+
const meta: Meta<typeof TideConditions> = {
7+
title: "Components/TideConditions",
8+
component: TideConditions,
9+
decorators: [
10+
(Story) => (
11+
<div style={{ maxWidth: 400 }}>
12+
<Story />
13+
</div>
14+
),
15+
],
16+
};
17+
18+
export default meta;
19+
type Story = StoryObj<typeof TideConditions>;
20+
21+
export const Default: Story = {
22+
args: { id: STATION_ID },
23+
};
24+
25+
export const NoDate: Story = {
26+
args: { id: STATION_ID, showDate: false },
27+
};
28+
29+
export const NoData: Story = {
30+
args: {
31+
timeline: [],
32+
extremes: [],
33+
units: "feet",
34+
timezone: "UTC",
35+
},
36+
};

0 commit comments

Comments
 (0)