Sorted events in date order and improved Map fitting
This commit is contained in:
parent
3d2b71c768
commit
9124274603
@ -8,6 +8,7 @@ import Sidebar from "@components/Sidebar";
|
||||
import { createPoster } from "@utils/axiosHelpers";
|
||||
import { Earthquake } from "@prismaclient";
|
||||
import { getRelativeDate } from "@utils/formatters";
|
||||
import GeologicalEvent from "@appTypes/Event";
|
||||
|
||||
export default function Earthquakes() {
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
@ -18,14 +19,20 @@ export default function Earthquakes() {
|
||||
const earthquakeEvents = useMemo(
|
||||
() =>
|
||||
data && data.earthquakes
|
||||
? data.earthquakes.map((x: Earthquake) => ({
|
||||
id: x.id,
|
||||
? data.earthquakes
|
||||
.map(
|
||||
(x: Earthquake): GeologicalEvent => ({
|
||||
id: x.code,
|
||||
title: `Earthquake in ${x.code.split("-")[2]}`,
|
||||
magnitude: x.magnitude,
|
||||
longitude: x.longitude,
|
||||
latitude: x.latitude,
|
||||
text1: "",
|
||||
text2: getRelativeDate(x.date),
|
||||
}))
|
||||
date: x.date,
|
||||
})
|
||||
)
|
||||
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime()) // Remove Date conversion
|
||||
: [],
|
||||
[data]
|
||||
);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
@ -6,18 +7,41 @@ import useSWR from "swr";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import Map from "@components/Map";
|
||||
import { fetcher } from "@utils/axiosHelpers";
|
||||
import { Observatory } from "@prismaclient";
|
||||
import { getRelativeDate } from "@utils/formatters";
|
||||
import GeologicalEvent from "@appTypes/Event";
|
||||
|
||||
export default function Observatories() {
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
// todo properly integrate loading
|
||||
const { data, error, isLoading } = useSWR("/api/observatories", fetcher);
|
||||
|
||||
// todo add in earthquake events
|
||||
const observatoryEvents = useMemo(
|
||||
() =>
|
||||
data && data.observatories
|
||||
? data.observatories
|
||||
.map(
|
||||
(x: Observatory): GeologicalEvent => ({
|
||||
id: x.id.toString(),
|
||||
title: `New Observatory - ${x.name}`,
|
||||
longitude: x.longitude,
|
||||
latitude: x.latitude,
|
||||
text1: "",
|
||||
text2: getRelativeDate(x.dateEstablished),
|
||||
date: x.dateEstablished,
|
||||
})
|
||||
)
|
||||
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime()) // Remove Date conversion
|
||||
: [],
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
|
||||
<div className="flex-grow h-full">
|
||||
<Map
|
||||
events={data ? data.observatories : []}
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
@ -29,7 +53,7 @@ export default function Observatories() {
|
||||
logTitle="Observatory Mapping"
|
||||
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
|
||||
recentsTitle="Observatory Events"
|
||||
events={data ? data.observatories : []}
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
|
||||
@ -1,25 +1,22 @@
|
||||
import "mapbox-gl/dist/mapbox-gl.css";
|
||||
|
||||
import mapboxgl, { LngLatBounds } from "mapbox-gl";
|
||||
import { userAgentFromString } from "next/server";
|
||||
import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { GiObservatory } from "react-icons/gi";
|
||||
|
||||
import Event from "@appTypes/Event";
|
||||
import GeologicalEvent from "@appTypes/Event";
|
||||
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
||||
|
||||
interface MapComponentProps {
|
||||
events: Event[];
|
||||
selectedEventId: Event["id"];
|
||||
events: GeologicalEvent[];
|
||||
selectedEventId: GeologicalEvent["id"];
|
||||
setSelectedEventId: Dispatch<SetStateAction<string>>;
|
||||
hoveredEventId: Event["id"];
|
||||
hoveredEventId: GeologicalEvent["id"];
|
||||
setHoveredEventId: Dispatch<SetStateAction<string>>;
|
||||
mapType: String;
|
||||
}
|
||||
|
||||
// Map component with location-style pulsing dots, animations, and tooltips
|
||||
function MapComponent({
|
||||
events,
|
||||
selectedEventId,
|
||||
@ -32,9 +29,9 @@ function MapComponent({
|
||||
const markers = useRef<{ [key: string]: mapboxgl.Marker }>({});
|
||||
const [mapBounds, setMapBounds] = useState<LngLatBounds>();
|
||||
|
||||
const fitToBounds = useCallback((bounds: LngLatBounds) => {
|
||||
const fitToBounds = useCallback((bounds: LngLatBounds, padding: number = 150) => {
|
||||
if (map.current && bounds) {
|
||||
map.current!.fitBounds(bounds, { padding: 150, maxZoom: 10 });
|
||||
map.current!.fitBounds(bounds, { padding, maxZoom: 10, zoom: 1 });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -49,7 +46,7 @@ function MapComponent({
|
||||
Object.values(markers.current).forEach((marker) => marker.getPopup()?.remove());
|
||||
}, []);
|
||||
|
||||
const flyToEvent = useCallback((event: Event) => {
|
||||
const flyToEvent = useCallback((event: GeologicalEvent) => {
|
||||
if (map.current) {
|
||||
map.current.flyTo({
|
||||
center: [event.longitude, event.latitude],
|
||||
@ -76,15 +73,13 @@ function MapComponent({
|
||||
}
|
||||
|
||||
map.current.on("load", () => {
|
||||
// Fit map to bounds
|
||||
const bounds = new mapboxgl.LngLatBounds();
|
||||
events.forEach((event) => {
|
||||
bounds.extend([event.longitude, event.latitude]);
|
||||
});
|
||||
fitToBounds(bounds);
|
||||
fitToBounds(bounds, 0);
|
||||
setMapBounds(bounds);
|
||||
|
||||
// Add markers with location pulse
|
||||
events.forEach((event) => {
|
||||
const quakeElement = document.createElement("div");
|
||||
const dotElement = document.createElement("div");
|
||||
@ -92,37 +87,31 @@ function MapComponent({
|
||||
|
||||
if (event.magnitude) {
|
||||
const color = getMagnitudeColor(event.magnitude);
|
||||
|
||||
// Create marker container
|
||||
quakeElement.style.width = "50px"; // Increased size to accommodate pulse
|
||||
quakeElement.style.width = "50px";
|
||||
quakeElement.style.height = "50px";
|
||||
quakeElement.style.position = "absolute";
|
||||
quakeElement.style.display = "flex";
|
||||
quakeElement.style.alignItems = "center";
|
||||
quakeElement.style.justifyContent = "center";
|
||||
|
||||
// Central dot
|
||||
dotElement.style.width = "10px";
|
||||
dotElement.style.height = "10px";
|
||||
dotElement.style.backgroundColor = color;
|
||||
dotElement.style.borderRadius = "50%";
|
||||
dotElement.style.position = "absolute";
|
||||
dotElement.style.zIndex = "2"; // Ensure dot is above pulse
|
||||
dotElement.style.zIndex = "2";
|
||||
|
||||
// Pulsing ring
|
||||
pulseElement.className = "location-pulse";
|
||||
pulseElement.style.width = "20px"; // Initial size
|
||||
pulseElement.style.width = "20px";
|
||||
pulseElement.style.height = "20px";
|
||||
pulseElement.style.backgroundColor = `${color}80`; // Color with 50% opacity (hex alpha)
|
||||
pulseElement.style.backgroundColor = `${color}80`;
|
||||
pulseElement.style.borderRadius = "50%";
|
||||
pulseElement.style.position = "absolute";
|
||||
pulseElement.style.zIndex = "1";
|
||||
}
|
||||
|
||||
// Observatory marker
|
||||
//<GiObservatory />
|
||||
const observatoryElement = document.createElement("div");
|
||||
const root = createRoot(observatoryElement); // `createRoot` is now the standard API
|
||||
const root = createRoot(observatoryElement);
|
||||
root.render(<GiObservatory className="text-blue-600 text-2xl drop-shadow-lg" />);
|
||||
|
||||
quakeElement.appendChild(pulseElement);
|
||||
@ -143,9 +132,8 @@ function MapComponent({
|
||||
marker.setPopup(popup);
|
||||
markers.current[event.id] = marker;
|
||||
|
||||
// Add hover events
|
||||
const markerDomElement = marker.getElement();
|
||||
markerDomElement.style.cursor = "pointer"; // Optional: indicate interactivity
|
||||
markerDomElement.style.cursor = "pointer";
|
||||
|
||||
markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id));
|
||||
markerDomElement.addEventListener("mouseleave", () => setHoveredEventId(""));
|
||||
@ -153,11 +141,19 @@ function MapComponent({
|
||||
setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id))
|
||||
);
|
||||
|
||||
// Cleanup event listeners on unmount
|
||||
markerDomElement.dataset.listenersAdded = "true"; // Mark for cleanup
|
||||
markerDomElement.dataset.listenersAdded = "true";
|
||||
});
|
||||
});
|
||||
|
||||
// Add map click handler to deselect when clicking outside markers
|
||||
map.current.on("click", (e) => {
|
||||
const target = e.originalEvent.target as HTMLElement;
|
||||
// Check if the click target is not a marker element
|
||||
if (!target.closest(".mapboxgl-marker")) {
|
||||
setSelectedEventId("");
|
||||
}
|
||||
});
|
||||
|
||||
map.current.on("error", (e) => {
|
||||
console.error("Mapbox error:", e);
|
||||
});
|
||||
@ -170,22 +166,17 @@ function MapComponent({
|
||||
useEffect(() => {
|
||||
const event = events.find((x) => x.id === selectedEventId);
|
||||
if (event) flyToEvent(event);
|
||||
else if (!selectedEventId) {
|
||||
if (mapBounds) fitToBounds(mapBounds);
|
||||
else if (!selectedEventId && mapBounds) {
|
||||
fitToBounds(mapBounds, 0);
|
||||
}
|
||||
}, [events, selectedEventId, mapBounds, fitToBounds, clearAllPopups, flyToEvent]);
|
||||
}, [events, selectedEventId, mapBounds, fitToBounds, flyToEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear all popups first
|
||||
clearAllPopups();
|
||||
|
||||
// Handle both events if they exist and are different
|
||||
if (hoveredEventId && selectedEventId && hoveredEventId !== selectedEventId) {
|
||||
showPopup(hoveredEventId);
|
||||
showPopup(selectedEventId);
|
||||
}
|
||||
// Handle single event case (either hovered or selected)
|
||||
else if (hoveredEventId || selectedEventId) {
|
||||
} else if (hoveredEventId || selectedEventId) {
|
||||
showPopup(hoveredEventId || selectedEventId);
|
||||
}
|
||||
}, [hoveredEventId, selectedEventId, clearAllPopups, showPopup]);
|
||||
@ -197,7 +188,6 @@ function MapComponent({
|
||||
);
|
||||
}
|
||||
|
||||
// CSS for location-style pulsing animation
|
||||
const pulseStyles = `
|
||||
.location-pulse {
|
||||
animation: locationPulse 2s infinite;
|
||||
@ -1,18 +1,18 @@
|
||||
import Link from "next/link";
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
import React, { Dispatch, SetStateAction, useEffect, useRef } from "react";
|
||||
import { TbHexagon } from "react-icons/tb";
|
||||
|
||||
import Event from "@appTypes/Event";
|
||||
import GeologicalEvent from "@appTypes/Event";
|
||||
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
||||
|
||||
interface SidebarProps {
|
||||
logTitle: string;
|
||||
logSubtitle: string;
|
||||
recentsTitle: string;
|
||||
events: Event[];
|
||||
selectedEventId: Event["id"];
|
||||
events: GeologicalEvent[];
|
||||
selectedEventId: GeologicalEvent["id"];
|
||||
setSelectedEventId: Dispatch<SetStateAction<string>>;
|
||||
hoveredEventId: Event["id"];
|
||||
hoveredEventId: GeologicalEvent["id"];
|
||||
setHoveredEventId: Dispatch<SetStateAction<string>>;
|
||||
button1Name: string;
|
||||
button2Name: string;
|
||||
@ -48,6 +48,20 @@ export default function Sidebar({
|
||||
button1Name,
|
||||
button2Name,
|
||||
}: SidebarProps) {
|
||||
const eventsContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEventId && eventsContainerRef.current) {
|
||||
const selectedEventElement = eventsContainerRef.current.querySelector(`[data-event-id="${selectedEventId}"]`);
|
||||
if (selectedEventElement) {
|
||||
selectedEventElement.scrollIntoView({
|
||||
block: "center",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedEventId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-80 bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg">
|
||||
<div className="py-6 flex flex-col h-full">
|
||||
@ -66,13 +80,14 @@ export default function Sidebar({
|
||||
</Link>
|
||||
</div>
|
||||
<div className="px-6 pt-6">
|
||||
<h2 className="text-xl font-bold text-neutral-800 mb-2">{recentsTitle}</h2>
|
||||
<h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2>
|
||||
</div>
|
||||
<div className="flex-1 px-6 overflow-y-auto pt-2">
|
||||
<div className="flex-1 px-6 overflow-y-auto" ref={eventsContainerRef}>
|
||||
<div className="space-y-3">
|
||||
{events.map((event) => (
|
||||
<button
|
||||
key={event.title}
|
||||
key={event.id}
|
||||
data-event-id={event.id}
|
||||
className={`w-full border ${hoveredEventId === event.id ? "bg-neutral-100" : "bg-white"} ${
|
||||
selectedEventId === event.id ? "border-neutral-800" : "border-neutral-200"
|
||||
} rounded-lg p-4 flex items-center gap-4 hover:bg-neutral-100 transition-colors duration-150 shadow-sm text-left`}
|
||||
@ -83,8 +98,8 @@ export default function Sidebar({
|
||||
onMouseLeave={() => setHoveredEventId("")}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-neutral-800 truncate">{event.title}</p>
|
||||
<p className="text-xs text-neutral-500 mt-1 truncate">{event.text2}</p>
|
||||
<p className="text-sm font-medium text-neutral-800 line-clamp-1">{event.title}</p>
|
||||
<p className="text-xs text-neutral-500 mt-1 line-clamp-1">{event.text2}</p>
|
||||
</div>
|
||||
{event.magnitude ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>}
|
||||
</button>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
interface Event {
|
||||
interface GeologicalEvent {
|
||||
id: string;
|
||||
date: Date;
|
||||
title: string;
|
||||
magnitude?: number;
|
||||
longitude: number;
|
||||
@ -8,4 +9,4 @@ interface Event {
|
||||
text2: string;
|
||||
}
|
||||
|
||||
export default Event;
|
||||
export default GeologicalEvent;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user