Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker

This commit is contained in:
Emily Neighbour 2025-05-28 22:31:57 +01:00
commit 803253b96e
4 changed files with 253 additions and 99 deletions

View File

@ -2,65 +2,157 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import Map from "@components/Map"; import Map from "@components/Map";
import Sidebar from "@components/Sidebar"; import Sidebar from "@components/Sidebar";
import { createPoster } from "@utils/axiosHelpers"; import { createPoster } from "@utils/axiosHelpers";
import { Earthquake } from "@prismaclient"; import { Earthquake } from "@prismaclient";
import { getRelativeDate } from "@utils/formatters"; import { getRelativeDate } from "@utils/formatters";
import GeologicalEvent from "@appTypes/Event"; import GeologicalEvent from "@appTypes/Event";
import axios from "axios";
export default function Earthquakes() { // --- SEARCH MODAL COMPONENT ---
const [selectedEventId, setSelectedEventId] = useState(""); function EarthquakeSearchModal({ open, onClose, onSelect }) {
const [hoveredEventId, setHoveredEventId] = useState(""); const [search, setSearch] = useState("");
// todo properly integrate loading const [results, setResults] = useState([]);
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 5 })); const [loading, setLoading] = useState(false);
const earthquakeEvents = useMemo( const handleSearch = async (e) => {
() => e.preventDefault();
data && data.earthquakes setLoading(true);
? data.earthquakes setResults([]);
.map( try {
(x: Earthquake): GeologicalEvent => ({ const res = await axios.post("/api/earthquakes/search", { query: search });
id: x.code, setResults(res.data.earthquakes || []);
title: `Earthquake in ${x.code.split("-")[2]}`, } catch (e) {
magnitude: x.magnitude, alert("Failed to search.");
longitude: x.longitude, }
latitude: x.latitude, setLoading(false);
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]
);
return ( if (!open) return null;
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden"> return (
<div className="flex-grow h-full"> <div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
<Map <div className="bg-white rounded-lg shadow-lg p-6 max-w-lg w-full relative">
events={earthquakeEvents} <button
selectedEventId={selectedEventId} onClick={onClose}
setSelectedEventId={setSelectedEventId} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg"
hoveredEventId={hoveredEventId} >
setHoveredEventId={setHoveredEventId} &times;
mapType="Earthquakes" </button>
/> <h2 className="font-bold text-xl mb-4">Search Earthquakes</h2>
</div> <form onSubmit={handleSearch} className="flex gap-2 mb-4">
<Sidebar <input
logTitle="Log an Earthquake" type="text"
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists" placeholder="e.g. Mexico, EV-7.4-Mexico-00035"
recentsTitle="Recent Earthquakes" value={search}
events={earthquakeEvents} onChange={e => setSearch(e.target.value)}
selectedEventId={selectedEventId} className="flex-grow px-3 py-2 border rounded"
setSelectedEventId={setSelectedEventId} required
hoveredEventId={hoveredEventId} />
setHoveredEventId={setHoveredEventId} <button
button1Name="Log an Earthquake" type="submit"
button2Name="Search Earthquakes" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
/> >
</div> {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>
);
} }
// --- MAIN PAGE COMPONENT ---
export default function Earthquakes() {
const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState("");
// Search modal state
const [searchModalOpen, setSearchModalOpen] = useState(false);
// Fetch recent earthquakes as before
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 5 }));
// Prepare events for maps/sidebar
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]
);
// Optional: show details of selected search result (not implemented here)
// const [selectedSearchResult, setSelectedSearchResult] = useState(null);
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"
onButton2Click={() => setSearchModalOpen(true)} // <-- important!
/>
<EarthquakeSearchModal
open={searchModalOpen}
onClose={() => setSearchModalOpen(false)}
onSelect={eq => {
setSelectedEventId(eq.code); // select on map/sidebar
// setSelectedSearchResult(eq); // you can use this if you want to show detail modal
}}
/>
</div>
);
}

View File

@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { prisma } from "@utils/prisma";
export async function POST(req: Request) {
try {
const { query } = await req.json();
// Find earthquakes where either code or location matches (case-insensitive)
const earthquakes = await prisma.earthquake.findMany({
where: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ location: { contains: query, mode: "insensitive" } }
],
},
orderBy: { date: "desc" },
take: 20, // limit results
});
return NextResponse.json({ earthquakes, message: "Success" });
} catch (error) {
console.error("Error in earthquake search", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
}
}

View File

@ -16,6 +16,7 @@ interface SidebarProps {
setHoveredEventId: Dispatch<SetStateAction<string>>; setHoveredEventId: Dispatch<SetStateAction<string>>;
button1Name: string; button1Name: string;
button2Name: string; button2Name: string;
onButton2Click?: () => void;
} }
function MagnitudeNumber({ magnitude }: { magnitude: number }) { function MagnitudeNumber({ magnitude }: { magnitude: number }) {
@ -47,6 +48,7 @@ export default function Sidebar({
setHoveredEventId, setHoveredEventId,
button1Name, button1Name,
button2Name, button2Name,
onButton2Click,
}: SidebarProps) { }: SidebarProps) {
const eventsContainerRef = useRef<HTMLDivElement>(null); const eventsContainerRef = useRef<HTMLDivElement>(null);
@ -68,17 +70,21 @@ export default function Sidebar({
<div className="px-6 pb-8 border-b border-neutral-200"> <div className="px-6 pb-8 border-b border-neutral-200">
<h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2> <h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2>
<p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p> <p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p>
<Link href="/">
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium"> <Link href="/">
{button1Name} <button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium">
</button> {button1Name}
</Link> </button>
<Link href="/"> </Link>
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium"> {/* "Search Earthquakes" should NOT be wrapped in a Link! */}
{button2Name} <button
</button> className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium"
</Link> onClick={onButton2Click}
</div> type="button"
>
{button2Name}
</button>
</div>
<div className="px-6 pt-6"> <div className="px-6 pt-6">
<h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2> <h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2>
</div> </div>

View File

@ -1,42 +1,73 @@
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
const Sidebar = () => { type SidebarProps = {
return ( logTitle: string;
<div className="flex flex-col h-screen w-64 bg-neutral-400 text-white border-l border-neutral-700"> logSubtitle: string;
<div className="flex flex-col p-4 border-b border-neutral-700"> recentsTitle: string;
<h2 className="text-xl font-semibold mb-2">Log an Earthquake</h2> events: any[]; // Or type this better if desired
<p className="text-sm text-neutral-700"> button1Name: string;
Record new earthquakes - time/date, location, magnitude, observatory and scientists button2Name: string;
</p> onButton1Click?: () => void;
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"> onButton2Click?: () => void;
<Link href="/">Log Event</Link> }
</button>
</div>
{/* Section: Recent Events - Will need to be replaced with a link to the database*/} const Sidebar: React.FC<SidebarProps> = ({
<div className="flex-1 p-4"> logTitle,
<h2 className="text-xl font-semibold mb-2">Recent Events</h2> logSubtitle,
<ul className="space-y-2"> recentsTitle,
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600"> events,
<p className="text-sm">Earthquake in California</p> button1Name,
<p className="text-xs text-neutral-300">Magnitude 5.3</p> button2Name,
<p className="text-xs text-neutral-400">2 hours ago</p> onButton1Click,
</li> onButton2Click,
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600"> }) => {
<p className="text-sm">Tremor in Japan</p> return (
<p className="text-xs text-neutral-300">Magnitude 4.7</p> <div className="flex flex-col h-full w-80 bg-white border-l border-neutral-200 shadow-md">
<p className="text-xs text-neutral-400">5 hours ago</p> <div className="flex flex-col p-4 border-b border-neutral-200">
</li> <h2 className="text-xl font-semibold mb-2">{logTitle}</h2>
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600"> <p className="text-sm text-neutral-600">{logSubtitle}</p>
<p className="text-sm">Tremor in Spain</p> <button
<p className="text-xs text-neutral-300">Magnitude 2.1</p> className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"
<p className="text-xs text-neutral-400">10 hours ago</p> onClick={onButton1Click}
</li> >
</ul> {button1Name}
</div> </button>
</div> <button
); className="mt-2 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"
onClick={onButton2Click}
>
{button2Name}
</button>
</div>
<div className="flex-1 p-4 overflow-y-auto">
<h2 className="text-xl font-semibold mb-2">{recentsTitle}</h2>
<ul className="space-y-2">
{events.map((item, idx) => (
<li
className="bg-neutral-100 p-3 rounded hover:bg-neutral-200 flex items-center justify-between"
key={item.id || idx}
>
<div>
<p className="font-medium">{item.title}</p>
<p className="text-xs text-neutral-600">
{item.text2}
</p>
</div>
<div className={`ml-3 rounded-full font-semibold px-2 py-1 ${item.magnitude >= 7
? "bg-red-500 text-white"
: item.magnitude >= 6
? "bg-orange-400 text-white"
: "bg-yellow-400 text-black"
}`}>
{item.magnitude}
</div>
</li>
))}
</ul>
</div>
</div>
);
}; };
export default Sidebar; export default Sidebar;