2025-03-23 15:24:10 +00:00
|
|
|
"use client";
|
2025-05-27 13:22:42 +01:00
|
|
|
import { useMemo, useState } from "react";
|
2025-05-04 16:04:44 +01:00
|
|
|
import useSWR from "swr";
|
2025-05-12 13:25:57 +01:00
|
|
|
import Map from "@components/Map";
|
2025-05-04 16:04:44 +01:00
|
|
|
import Sidebar from "@components/Sidebar";
|
2025-05-27 13:22:42 +01:00
|
|
|
import { createPoster } from "@utils/axiosHelpers";
|
2025-05-27 13:12:19 +01:00
|
|
|
import { Earthquake } from "@prismaclient";
|
|
|
|
|
import { getRelativeDate } from "@utils/formatters";
|
2025-05-27 14:10:41 +01:00
|
|
|
import GeologicalEvent from "@appTypes/Event";
|
2025-05-28 22:11:39 +01:00
|
|
|
import axios from "axios";
|
2025-05-31 16:43:20 +01:00
|
|
|
import EarthquakeLogModal from "@components/EarthquakeLogModal";
|
2025-05-31 21:25:59 +01:00
|
|
|
import { useStoreState } from "@hooks/store";
|
|
|
|
|
|
|
|
|
|
// --- NO ACCESS MODAL ---
|
|
|
|
|
function NoAccessModal({ open, onClose }) {
|
|
|
|
|
if (!open) return null;
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
|
|
|
|
|
<div className="bg-white rounded-lg shadow-lg p-8 max-w-xs w-full text-center relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg"
|
|
|
|
|
aria-label="Close"
|
|
|
|
|
>×</button>
|
|
|
|
|
<h2 className="font-bold text-xl mb-4">Access Denied</h2>
|
|
|
|
|
<p className="text-gray-600 mb-3">Sorry, you do not have access rights to Log an Earthquake. Please Log in here, or contact an Admin if you believe this is a mistake</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2"
|
|
|
|
|
>OK</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-05-30 12:58:53 +01:00
|
|
|
|
2025-05-28 22:11:39 +01:00
|
|
|
// --- SEARCH MODAL COMPONENT ---
|
|
|
|
|
function EarthquakeSearchModal({ open, onClose, onSelect }) {
|
2025-05-31 16:43:20 +01:00
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
|
const [results, setResults] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const handleSearch = async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setResults([]);
|
|
|
|
|
try {
|
|
|
|
|
const res = await axios.post("/api/earthquakes/search", { query: search });
|
|
|
|
|
setResults(res.data.earthquakes || []);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
alert("Failed to search.");
|
|
|
|
|
}
|
|
|
|
|
setLoading(false);
|
|
|
|
|
};
|
|
|
|
|
if (!open) return null;
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
|
|
|
|
|
<div className="bg-white rounded-lg shadow-lg p-6 max-w-lg w-full relative">
|
|
|
|
|
<button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg">
|
|
|
|
|
×
|
|
|
|
|
</button>
|
|
|
|
|
<h2 className="font-bold text-xl mb-4">Search Earthquakes</h2>
|
|
|
|
|
<form onSubmit={handleSearch} className="flex gap-2 mb-4">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="e.g. Mexico, EV-7.4-Mexico-00035"
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
|
|
|
className="flex-grow px-3 py-2 border rounded"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
|
|
|
|
|
{loading ? "Searching..." : "Search"}
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
<div>
|
|
|
|
|
{results.length === 0 && !loading && search !== "" && <p className="text-gray-400 text-sm">No results found.</p>}
|
|
|
|
|
<ul>
|
|
|
|
|
{results.map((eq) => (
|
|
|
|
|
<li
|
|
|
|
|
key={eq.id}
|
|
|
|
|
className="border-b py-2 cursor-pointer hover:bg-gray-50 flex items-center justify-between"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onSelect(eq);
|
|
|
|
|
onClose();
|
|
|
|
|
}}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<strong>{eq.code}</strong> <span>{eq.location}</span>{" "}
|
|
|
|
|
<span className="text-xs text-gray-500">{new Date(eq.date).toLocaleDateString()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={`rounded-full px-2 py-1 ml-2 text-white font-semibold ${
|
|
|
|
|
eq.magnitude >= 7 ? "bg-red-500" : eq.magnitude >= 6 ? "bg-orange-400" : "bg-yellow-400"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{eq.magnitude}
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-03-19 19:20:18 +00:00
|
|
|
}
|
2025-05-28 22:11:39 +01:00
|
|
|
|
|
|
|
|
// --- MAIN PAGE COMPONENT ---
|
|
|
|
|
export default function Earthquakes() {
|
2025-05-31 16:43:20 +01:00
|
|
|
const [selectedEventId, setSelectedEventId] = useState("");
|
|
|
|
|
const [hoveredEventId, setHoveredEventId] = useState("");
|
|
|
|
|
const [searchModalOpen, setSearchModalOpen] = useState(false);
|
2025-05-31 21:25:59 +01:00
|
|
|
const [logModalOpen, setLogModalOpen] = useState(false);
|
|
|
|
|
const [noAccessModalOpen, setNoAccessModalOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const user = useStoreState((state) => state.user);
|
|
|
|
|
const role: "GUEST" | "SCIENTIST" | "ADMIN" = user?.role ?? "GUEST";
|
|
|
|
|
const canLogEarthquake = role === "SCIENTIST" || role === "ADMIN";
|
|
|
|
|
|
|
|
|
|
// Fetch recent earthquakes
|
2025-05-31 16:43:20 +01:00
|
|
|
const { data, error, isLoading, mutate } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 10 }));
|
2025-05-31 21:25:59 +01:00
|
|
|
|
|
|
|
|
// Prepare events
|
2025-05-31 16:43:20 +01:00
|
|
|
const earthquakeEvents = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
data && data.earthquakes
|
|
|
|
|
? 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())
|
|
|
|
|
: [],
|
|
|
|
|
[data]
|
|
|
|
|
);
|
2025-05-31 21:25:59 +01:00
|
|
|
|
|
|
|
|
// This handler is always called, regardless of button state!
|
|
|
|
|
const handleLogClick = () => {
|
|
|
|
|
if (canLogEarthquake) {
|
|
|
|
|
setLogModalOpen(true);
|
|
|
|
|
} else {
|
|
|
|
|
setNoAccessModalOpen(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-31 16:43:20 +01:00
|
|
|
return (
|
|
|
|
|
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
|
|
|
|
|
<div className="flex-grow h-full">
|
|
|
|
|
<Map
|
|
|
|
|
events={earthquakeEvents}
|
|
|
|
|
selectedEventId={selectedEventId}
|
|
|
|
|
setSelectedEventId={setSelectedEventId}
|
|
|
|
|
hoveredEventId={hoveredEventId}
|
|
|
|
|
setHoveredEventId={setHoveredEventId}
|
|
|
|
|
mapType="Earthquakes"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Sidebar
|
|
|
|
|
logTitle="Log an Earthquake"
|
|
|
|
|
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists"
|
|
|
|
|
recentsTitle="Recent Earthquakes"
|
|
|
|
|
events={earthquakeEvents}
|
|
|
|
|
selectedEventId={selectedEventId}
|
|
|
|
|
setSelectedEventId={setSelectedEventId}
|
|
|
|
|
hoveredEventId={hoveredEventId}
|
|
|
|
|
setHoveredEventId={setHoveredEventId}
|
|
|
|
|
button1Name="Log an Earthquake"
|
|
|
|
|
button2Name="Search Earthquakes"
|
2025-05-31 21:25:59 +01:00
|
|
|
onButton1Click={handleLogClick} // <--- Important!
|
2025-05-31 16:43:20 +01:00
|
|
|
onButton2Click={() => setSearchModalOpen(true)}
|
2025-05-31 21:25:59 +01:00
|
|
|
button1Disabled={!canLogEarthquake} // <--- For style only!
|
2025-05-31 16:43:20 +01:00
|
|
|
/>
|
|
|
|
|
<EarthquakeSearchModal
|
|
|
|
|
open={searchModalOpen}
|
|
|
|
|
onClose={() => setSearchModalOpen(false)}
|
2025-05-31 21:25:59 +01:00
|
|
|
onSelect={(eq) => setSelectedEventId(eq.code)}
|
2025-05-31 16:43:20 +01:00
|
|
|
/>
|
|
|
|
|
<EarthquakeLogModal
|
|
|
|
|
open={logModalOpen}
|
|
|
|
|
onClose={() => setLogModalOpen(false)}
|
|
|
|
|
onSuccess={() => mutate()} // To refresh
|
|
|
|
|
/>
|
2025-05-31 21:25:59 +01:00
|
|
|
<NoAccessModal
|
|
|
|
|
open={noAccessModalOpen}
|
|
|
|
|
onClose={() => setNoAccessModalOpen(false)}
|
|
|
|
|
/>
|
2025-05-31 16:43:20 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|