we have a search modal it just doesnt work yet
This commit is contained in:
parent
c1d686b012
commit
1f005295b4
@ -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}
|
×
|
||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/app/earthquakes/search/route.ts
Normal file
25
src/app/earthquakes/search/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
Loading…
x
Reference in New Issue
Block a user