Lots of changes to Log earthquake/observatory
This commit is contained in:
parent
b73114afd1
commit
587b6b7f01
95
package-lock.json
generated
95
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"body-parser": "^2.2.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"easy-peasy": "^6.1.0",
|
||||
"express": "^5.1.0",
|
||||
@ -30,6 +31,7 @@
|
||||
"path": "^0.12.7",
|
||||
"prisma": "^6.4.1",
|
||||
"react": "^19.1.0",
|
||||
"react-datepicker": "^8.4.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
@ -246,6 +248,59 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
||||
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz",
|
||||
"integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.9.tgz",
|
||||
"integrity": "sha512-Y0aCJBNtfVF6ikI1kVzA0WzSAhVBz79vFWOhvb5MLCRNODZ1ylGSLTuncchR7JsLyn9QzV6JD44DyZhhOtvpRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.9",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -2493,6 +2548,15 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
@ -2783,6 +2847,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@ -6324,6 +6398,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-datepicker": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.4.0.tgz",
|
||||
"integrity": "sha512-6nPDnj8vektWCIOy9ArS3avus9Ndsyz5XgFCJ7nBxXASSpBdSL6lG9jzNNmViPOAOPh6T5oJyGaXuMirBLECag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.3",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
@ -7344,6 +7433,12 @@
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"body-parser": "^2.2.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"easy-peasy": "^6.1.0",
|
||||
"express": "^5.1.0",
|
||||
@ -33,6 +34,7 @@
|
||||
"path": "^0.12.7",
|
||||
"prisma": "^6.4.1",
|
||||
"react": "^19.1.0",
|
||||
"react-datepicker": "^8.4.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
|
||||
52
src/app/api/earthquakes/log/route.ts
Normal file
52
src/app/api/earthquakes/log/route.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@utils/prisma";
|
||||
|
||||
// Generates code using only the country, and highest id in DB for numbering
|
||||
async function generateEarthquakeCode(type: string, country: string) {
|
||||
const typeLetter = type.trim().charAt(0).toUpperCase();
|
||||
// Remove non-alphanumeric for the country part
|
||||
const countrySlug = (country || "Unknown").replace(/[^\w]/gi, "");
|
||||
// Use highest DB id to find the latest added earthquake's code number
|
||||
const last = await prisma.earthquake.findFirst({
|
||||
orderBy: { id: "desc" },
|
||||
select: { code: true }
|
||||
});
|
||||
let num = 10000;
|
||||
if (last?.code) {
|
||||
const parts = last.code.split("-");
|
||||
const lastNum = parseInt(parts[parts.length - 1], 10);
|
||||
if (!isNaN(lastNum)) num = lastNum + 1;
|
||||
}
|
||||
return `E${typeLetter}-${countrySlug}-${num.toString().padStart(5, "0")}`;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { date, magnitude, type, location, latitude, longitude, depth, country } = body;
|
||||
const creatorId = 1;
|
||||
if (!date || !magnitude || !type || !location || !latitude || !longitude || !depth || !country) {
|
||||
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
||||
}
|
||||
if (+magnitude > 10) {
|
||||
return NextResponse.json({ error: "Magnitude cannot exceed 10" }, { status: 400 });
|
||||
}
|
||||
const code = await generateEarthquakeCode(type, country);
|
||||
const eq = await prisma.earthquake.create({
|
||||
data: {
|
||||
date: new Date(date),
|
||||
code,
|
||||
magnitude: +magnitude,
|
||||
type,
|
||||
location, // "city, country"
|
||||
latitude: +latitude,
|
||||
longitude: +longitude,
|
||||
depth,
|
||||
creatorId,
|
||||
}
|
||||
});
|
||||
return NextResponse.json({ id: eq.id, code }, { status: 201 });
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
34
src/app/api/observatories/log/route.ts
Normal file
34
src/app/api/observatories/log/route.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@utils/prisma";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const {
|
||||
name, location, latitude, longitude, dateEstablished,
|
||||
dateClosed, isFunctional
|
||||
} = body;
|
||||
const creatorId = 1; // (Set per logged-in user if desired)
|
||||
if (
|
||||
!name || !location || latitude == null || longitude == null ||
|
||||
!dateEstablished || isFunctional == null
|
||||
) {
|
||||
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
||||
}
|
||||
const created = await prisma.observatory.create({
|
||||
data: {
|
||||
name,
|
||||
location,
|
||||
latitude: +latitude,
|
||||
longitude: +longitude,
|
||||
dateEstablished: new Date(dateEstablished),
|
||||
isFunctional,
|
||||
seismicSensorOnline: false, // You could add this to modal if you want
|
||||
creatorId
|
||||
}
|
||||
});
|
||||
return NextResponse.json({ id: created.id, name: created.name }, { status: 201 });
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import Map from "@components/Map";
|
||||
@ -9,151 +8,147 @@ import { Earthquake } from "@prismaclient";
|
||||
import { getRelativeDate } from "@utils/formatters";
|
||||
import GeologicalEvent from "@appTypes/Event";
|
||||
import axios from "axios";
|
||||
|
||||
// todo (optional) add in filtering of map earthquakes
|
||||
import EarthquakeLogModal from "@components/EarthquakeLogModal";
|
||||
|
||||
// --- SEARCH MODAL COMPONENT ---
|
||||
function EarthquakeSearchModal({ open, onClose, onSelect }) {
|
||||
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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// --- 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: 10 }));
|
||||
|
||||
// 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)}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
const [searchModalOpen, setSearchModalOpen] = useState(false);
|
||||
const [logModalOpen, setLogModalOpen] = useState(false); // <-- Move here!
|
||||
// Fetch recent earthquakes as before
|
||||
const { data, error, isLoading, mutate } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 10 }));
|
||||
// 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]
|
||||
);
|
||||
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"
|
||||
onButton1Click={() => setLogModalOpen(true)} // Correct!
|
||||
onButton2Click={() => setSearchModalOpen(true)}
|
||||
/>
|
||||
<EarthquakeSearchModal
|
||||
open={searchModalOpen}
|
||||
onClose={() => setSearchModalOpen(false)}
|
||||
onSelect={(eq) => {
|
||||
setSelectedEventId(eq.code);
|
||||
// setSelectedSearchResult(eq); // optional
|
||||
}}
|
||||
/>
|
||||
<EarthquakeLogModal
|
||||
open={logModalOpen}
|
||||
onClose={() => setLogModalOpen(false)}
|
||||
onSuccess={() => mutate()} // To refresh
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,70 +1,77 @@
|
||||
"use client";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import Map from "@components/Map";
|
||||
import LogObservatoryModal from "@/components/LogObservatoryModal"; // Adjust if your path is different
|
||||
import { fetcher } from "@utils/axiosHelpers";
|
||||
import { Observatory } from "@prismaclient";
|
||||
import { getRelativeDate } from "@utils/formatters";
|
||||
import GeologicalEvent from "@appTypes/Event";
|
||||
|
||||
// todo add in showing of observatory stats when searching
|
||||
// todo add in deleting observatory when searching
|
||||
// todo add in changing colour of observatory icons if non-functional
|
||||
|
||||
export default function Observatories() {
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
const { data, error, isLoading } = useSWR("/api/observatories", fetcher);
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
const [logModalOpen, setLogModalOpen] = useState(false);
|
||||
|
||||
// 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]
|
||||
);
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
"/api/observatories",
|
||||
fetcher
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
|
||||
<div className="flex-grow h-full">
|
||||
<Map
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
mapType="observatories"
|
||||
/>
|
||||
</div>
|
||||
<Sidebar
|
||||
logTitle="Observatory Mapping"
|
||||
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
|
||||
recentsTitle="Observatory Events"
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
button1Name="Log a New Observatory"
|
||||
button2Name="Search Observatories"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
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()
|
||||
)
|
||||
: [],
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
|
||||
<div className="flex-grow h-full">
|
||||
<Map
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
mapType="observatories"
|
||||
/>
|
||||
</div>
|
||||
<Sidebar
|
||||
logTitle="Observatory Mapping"
|
||||
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
|
||||
recentsTitle="New Observatories"
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
button1Name="Log a New Observatory"
|
||||
button2Name="Search Observatories"
|
||||
onButton1Click={() => setLogModalOpen(true)}
|
||||
/>
|
||||
<LogObservatoryModal
|
||||
open={logModalOpen}
|
||||
onClose={() => setLogModalOpen(false)}
|
||||
onSuccess={() => mutate()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import BottomFooter from "@components/BottomFooter";
|
||||
import { createPoster } from "@utils/axiosHelpers";
|
||||
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
||||
import { TbHexagon } from "react-icons/tb";
|
||||
|
||||
// formats the date
|
||||
function getRelativeDate(dateString: string): string {
|
||||
@ -16,6 +18,25 @@ function getRelativeDate(dateString: string): string {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
// copied from sidebar
|
||||
function MagnitudeNumber({ magnitude }: { magnitude: number }) {
|
||||
const magnitudeStr = magnitude.toFixed(1);
|
||||
const [whole, decimal] = magnitudeStr.split(".");
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ color: getMagnitudeColor(magnitude) }}>
|
||||
<TbHexagon size={40} className="drop-shadow-sm" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex items-baseline font-mono font-bold tracking-tight">
|
||||
<span className="text-xl -mr-1">{whole}</span>
|
||||
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
|
||||
<span className="text-xs -mr-[1px]">{decimal}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 6 }));
|
||||
|
||||
@ -131,34 +152,21 @@ export default function Home() {
|
||||
<div className="flex flex-col gap-4">
|
||||
{recents.map((eq) => (
|
||||
<div
|
||||
key={eq.code}
|
||||
className="flex items-center justify-between p-4 bg-white rounded-xl shadow border"
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{getRelativeDate(eq.date)}
|
||||
</div>
|
||||
key={eq.code}
|
||||
className="flex items-center justify-between p-4 bg-white rounded-xl shadow border"
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}
|
||||
</div>
|
||||
<span
|
||||
className={`flex items-center justify-center font-bold text-md ml-2 rounded-full border-2 border-current
|
||||
${
|
||||
eq.magnitude >= 7
|
||||
? "text-red-600 border-red-600"
|
||||
: eq.magnitude >= 6
|
||||
? "text-orange-500 border-orange-500"
|
||||
: "text-yellow-500 border-yellow-500"
|
||||
}
|
||||
min-w-[2.8rem] min-h-[2.8rem] max-h-12 max-w-12`}
|
||||
style={{ aspectRatio: "1 / 1" }}
|
||||
title={`Magnitude ${eq.magnitude}`}
|
||||
>
|
||||
{eq.magnitude}
|
||||
</span>
|
||||
<div className="text-sm text-gray-500">
|
||||
{getRelativeDate(eq.date)}
|
||||
</div>
|
||||
</div>
|
||||
<MagnitudeNumber magnitude={eq.magnitude} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-20"></p>
|
||||
|
||||
248
src/components/EarthquakeLogModal.tsx
Normal file
248
src/components/EarthquakeLogModal.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
const typeOptions = [
|
||||
{ value: "volcanic", label: "Volcanic" },
|
||||
{ value: "tectonic", label: "Tectonic" },
|
||||
{ value: "collapse", label: "Collapse" },
|
||||
{ value: "explosion", label: "Explosion" }
|
||||
];
|
||||
|
||||
export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
|
||||
const [date, setDate] = useState<Date | null>(new Date());
|
||||
const [magnitude, setMagnitude] = useState("");
|
||||
const [type, setType] = useState(typeOptions[0].value);
|
||||
const [city, setCity] = useState("");
|
||||
const [country, setCountry] = useState("");
|
||||
const [latitude, setLatitude] = useState("");
|
||||
const [longitude, setLongitude] = useState("");
|
||||
const [depth, setDepth] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [successCode, setSuccessCode] = useState<string | null>(null);
|
||||
|
||||
async function handleLatLonChange(lat: string, lon: string) {
|
||||
setLatitude(lat);
|
||||
setLongitude(lon);
|
||||
if (/^-?\d+(\.\d+)?$/.test(lat) && /^-?\d+(\.\d+)?$/.test(lon)) {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10`
|
||||
);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
setCity(
|
||||
data.address.city ||
|
||||
data.address.town ||
|
||||
data.address.village ||
|
||||
data.address.hamlet ||
|
||||
data.address.county ||
|
||||
data.address.state ||
|
||||
""
|
||||
);
|
||||
setCountry(data.address.country || "");
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
if (!date || !magnitude || !type || !city || !country || !latitude || !longitude || !depth) {
|
||||
alert("Please complete all fields.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch("/api/earthquakes/log", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
date,
|
||||
magnitude: parseFloat(magnitude),
|
||||
type,
|
||||
location: `${city.trim()}, ${country.trim()}`,
|
||||
country: country.trim(),
|
||||
latitude: parseFloat(latitude),
|
||||
longitude: parseFloat(longitude),
|
||||
depth
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
setSuccessCode(result.code);
|
||||
setLoading(false);
|
||||
if (onSuccess) onSuccess();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert("Failed to log earthquake! " + (err.error || ""));
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert("Failed to log. " + e.message);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
// Success popup overlay
|
||||
if (successCode) {
|
||||
return (
|
||||
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg px-8 py-8 max-w-md w-full relative shadow-lg">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSuccessCode(null);
|
||||
onClose();
|
||||
}}
|
||||
className="absolute right-4 top-4 text-xl text-gray-400 hover:text-gray-700"
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold mb-3">
|
||||
Thank you for logging an earthquake!
|
||||
</h2>
|
||||
<div className="mb-0">The Earthquake Identifier is</div>
|
||||
<div className="font-mono text-lg mb-4 bg-slate-100 rounded px-2 py-1 inline-block text-blue-800">{successCode}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">Log Earthquake</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Date</label>
|
||||
<DatePicker
|
||||
selected={date}
|
||||
onChange={date => setDate(date)}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
dateFormat="yyyy-MM-dd"
|
||||
maxDate={new Date()}
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
dropdownMode="select"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Magnitude</label>
|
||||
<input
|
||||
type="number"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
min="0"
|
||||
max="10"
|
||||
step="0.1"
|
||||
value={magnitude}
|
||||
onChange={e => {
|
||||
const val = e.target.value;
|
||||
if (parseFloat(val) > 10) return;
|
||||
setMagnitude(val);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Type</label>
|
||||
<select
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={type}
|
||||
onChange={e => setType(e.target.value)}
|
||||
required
|
||||
>
|
||||
{typeOptions.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">City/Area</label>
|
||||
<span className="block text-xs text-gray-400">
|
||||
(Use Lat/Lon then press Enter for reverse lookup)
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={city}
|
||||
onChange={e => setCity(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Country</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={country}
|
||||
onChange={e => setCountry(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium">Latitude</label>
|
||||
<input
|
||||
type="number"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={latitude}
|
||||
onChange={e => handleLatLonChange(e.target.value, longitude)}
|
||||
placeholder="e.g. 36.12"
|
||||
step="any"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium">Longitude</label>
|
||||
<input
|
||||
type="number"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={longitude}
|
||||
onChange={e => handleLatLonChange(latitude, e.target.value)}
|
||||
placeholder="e.g. -115.17"
|
||||
step="any"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Depth</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={depth}
|
||||
onChange={e => setDepth(e.target.value)}
|
||||
placeholder="e.g. 10 km"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Logging..." : "Log Earthquake"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
223
src/components/LogObservatoryModal.tsx
Normal file
223
src/components/LogObservatoryModal.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
const yesNo = [
|
||||
{ value: true, label: "Yes" },
|
||||
{ value: false, label: "No" }
|
||||
];
|
||||
|
||||
export default function LogObservatoryModal({ open, onClose, onSuccess }) {
|
||||
const [name, setName] = useState("");
|
||||
const [isOpen, setIsOpen] = useState("true");
|
||||
const [dateOpened, setDateOpened] = useState<Date | null>(new Date());
|
||||
const [dateClosed, setDateClosed] = useState<Date | null>(null);
|
||||
const [city, setCity] = useState("");
|
||||
const [country, setCountry] = useState("");
|
||||
const [latitude, setLatitude] = useState("");
|
||||
const [longitude, setLongitude] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState<{ name: string } | null>(null);
|
||||
|
||||
// Reverse Geo-code
|
||||
async function handleLatLonChange(lat: string, lon: string) {
|
||||
setLatitude(lat);
|
||||
setLongitude(lon);
|
||||
if (/^-?\d+(\.\d+)?$/.test(lat) && /^-?\d+(\.\d+)?$/.test(lon)) {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10`
|
||||
);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
setCity(
|
||||
data.address.city ||
|
||||
data.address.town ||
|
||||
data.address.village ||
|
||||
data.address.hamlet ||
|
||||
data.address.county ||
|
||||
data.address.state ||
|
||||
""
|
||||
);
|
||||
setCountry(data.address.country || "");
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
if (!name || !dateOpened || !latitude || !longitude || !city || !country) {
|
||||
alert("Please complete all fields.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (isOpen === "false" && !dateClosed) {
|
||||
alert("Please enter the date this observatory closed.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch("/api/observatories/log", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
isFunctional: isOpen === "true" ? true : false,
|
||||
location: `${city.trim()}, ${country.trim()}`,
|
||||
latitude: parseFloat(latitude),
|
||||
longitude: parseFloat(longitude),
|
||||
dateEstablished: dateOpened,
|
||||
dateClosed: isOpen === "false" ? dateClosed : null,
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
setSuccess({ name });
|
||||
setLoading(false);
|
||||
if (onSuccess) onSuccess();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert("Failed to log observatory! " + (err.error || ""));
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert("Failed to log. " + e.message);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg px-8 py-8 max-w-md w-full relative shadow-lg">
|
||||
<button
|
||||
onClick={() => { setSuccess(null); onClose(); }}
|
||||
className="absolute right-4 top-4 text-xl text-gray-400 hover:text-gray-700"
|
||||
aria-label="Close"
|
||||
>×</button>
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold mb-3">Thank you for logging an observatory!</h2>
|
||||
<div>The Observatory is now being shown as <b>{success.name}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">Log Observatory</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Observatory Name</label>
|
||||
<input
|
||||
type="text" className="border rounded px-3 py-2 w-full"
|
||||
value={name} onChange={e => setName(e.target.value)} required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Is this observatory still open?</label>
|
||||
<select
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={isOpen}
|
||||
onChange={e => setIsOpen(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Date Opened</label>
|
||||
<DatePicker
|
||||
selected={dateOpened}
|
||||
onChange={date => setDateOpened(date)}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
dateFormat="yyyy-MM-dd"
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
dropdownMode="select"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{isOpen === "false" && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Date Closed</label>
|
||||
<DatePicker
|
||||
selected={dateClosed}
|
||||
onChange={date => setDateClosed(date)}
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
dateFormat="yyyy-MM-dd"
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
dropdownMode="select"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium">City/Area</label>
|
||||
<span className="block text-xs text-gray-400">
|
||||
(Use Lat/Lon then press Enter for reverse lookup)
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={city}
|
||||
onChange={e => setCity(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium">Country</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={country}
|
||||
onChange={e => setCountry(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium">Latitude</label>
|
||||
<input type="number"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={latitude}
|
||||
onChange={e => handleLatLonChange(e.target.value, longitude)}
|
||||
placeholder="e.g. 36.12"
|
||||
step="any"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium">Longitude</label>
|
||||
<input type="number"
|
||||
className="border rounded px-3 py-2 w-full"
|
||||
value={longitude}
|
||||
onChange={e => handleLatLonChange(latitude, e.target.value)}
|
||||
placeholder="e.g. -115.17"
|
||||
step="any"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Logging..." : "Log Observatory"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -17,6 +17,7 @@ interface SidebarProps {
|
||||
button1Name: string;
|
||||
button2Name: string;
|
||||
onButton2Click?: () => void;
|
||||
onButton1Click?: () => void;
|
||||
}
|
||||
|
||||
function MagnitudeNumber({ magnitude }: { magnitude: number }) {
|
||||
@ -51,6 +52,7 @@ export default function Sidebar({
|
||||
button1Name,
|
||||
button2Name,
|
||||
onButton2Click,
|
||||
onButton1Click,
|
||||
}: SidebarProps) {
|
||||
const eventsContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -73,12 +75,25 @@ export default function Sidebar({
|
||||
<h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2>
|
||||
<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">
|
||||
{button1Name}
|
||||
</button>
|
||||
</Link>
|
||||
{/* "Search Earthquakes" should NOT be wrapped in a Link! */}
|
||||
{onButton1Click ? (
|
||||
<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"
|
||||
onClick={onButton1Click}
|
||||
type="button"
|
||||
>
|
||||
{button1Name}
|
||||
</button>
|
||||
) : (
|
||||
<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"
|
||||
type="button"
|
||||
>
|
||||
{button1Name}
|
||||
</button>
|
||||
</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"
|
||||
onClick={onButton2Click}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user