diff --git a/package-lock.json b/package-lock.json
index ee0daed..54bc2b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 5c8e561..2e39b73 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app/api/earthquakes/log/route.ts b/src/app/api/earthquakes/log/route.ts
new file mode 100644
index 0000000..f6404ca
--- /dev/null
+++ b/src/app/api/earthquakes/log/route.ts
@@ -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 });
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/observatories/log/route.ts b/src/app/api/observatories/log/route.ts
new file mode 100644
index 0000000..0651cd1
--- /dev/null
+++ b/src/app/api/observatories/log/route.ts
@@ -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 });
+ }
+}
\ No newline at end of file
diff --git a/src/app/earthquakes/page.tsx b/src/app/earthquakes/page.tsx
index 5fbd35a..9b26801 100644
--- a/src/app/earthquakes/page.tsx
+++ b/src/app/earthquakes/page.tsx
@@ -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 (
-
-
-
- ×
-
-
Search Earthquakes
-
-
- {results.length === 0 && !loading && search !== "" &&
No results found.
}
-
-
-
-
- );
+ 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 (
+
+
+
+ ×
+
+
Search Earthquakes
+
+
+ {results.length === 0 && !loading && search !== "" &&
No results found.
}
+
+
+
+
+ );
}
// --- 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 (
-
-
-
-
-
setSearchModalOpen(true)}
- />
- setSearchModalOpen(false)}
- onSelect={(eq) => {
- setSelectedEventId(eq.code); // select on map/sidebar
- // setSelectedSearchResult(eq); // you can use this if you want to show detail modal
- }}
- />
-
- );
-}
+ 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 (
+
+
+
+
+
setLogModalOpen(true)} // Correct!
+ onButton2Click={() => setSearchModalOpen(true)}
+ />
+ setSearchModalOpen(false)}
+ onSelect={(eq) => {
+ setSelectedEventId(eq.code);
+ // setSelectedSearchResult(eq); // optional
+ }}
+ />
+ setLogModalOpen(false)}
+ onSuccess={() => mutate()} // To refresh
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/observatories/page.tsx b/src/app/observatories/page.tsx
index b496a09..6409fac 100644
--- a/src/app/observatories/page.tsx
+++ b/src/app/observatories/page.tsx
@@ -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 (
-
- );
+ 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 (
+
+
+
+
+
setLogModalOpen(true)}
+ />
+ setLogModalOpen(false)}
+ onSuccess={() => mutate()}
+ />
+
+ );
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index d1fad16..a114388 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -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 (
+
+
+
+
+ {whole}
+ .
+ {decimal}
+
+
+
+ );
+}
+
export default function Home() {
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 6 }));
@@ -131,34 +152,21 @@ export default function Home() {
{recents.map((eq) => (
-
-
- Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}
-
-
- {getRelativeDate(eq.date)}
-
+ key={eq.code}
+ className="flex items-center justify-between p-4 bg-white rounded-xl shadow border"
+ >
+
+
+ Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}
-
= 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}
-
+
+ {getRelativeDate(eq.date)}
+
+
+
))}
+
diff --git a/src/components/EarthquakeLogModal.tsx b/src/components/EarthquakeLogModal.tsx
new file mode 100644
index 0000000..ce9d30f
--- /dev/null
+++ b/src/components/EarthquakeLogModal.tsx
@@ -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(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(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 (
+
+
+
{
+ setSuccessCode(null);
+ onClose();
+ }}
+ className="absolute right-4 top-4 text-xl text-gray-400 hover:text-gray-700"
+ aria-label="Close"
+ >
+ ×
+
+
+
+ Thank you for logging an earthquake!
+
+
The Earthquake Identifier is
+
{successCode}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ ×
+
+
Log Earthquake
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/LogObservatoryModal.tsx b/src/components/LogObservatoryModal.tsx
new file mode 100644
index 0000000..fdb8d44
--- /dev/null
+++ b/src/components/LogObservatoryModal.tsx
@@ -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(new Date());
+ const [dateClosed, setDateClosed] = useState(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 (
+
+
+
{ setSuccess(null); onClose(); }}
+ className="absolute right-4 top-4 text-xl text-gray-400 hover:text-gray-700"
+ aria-label="Close"
+ >×
+
+
Thank you for logging an observatory!
+
The Observatory is now being shown as {success.name}
+
+
+
+ );
+ }
+
+ return (
+
+
+
×
+
Log Observatory
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 98bfa62..b88ec84 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -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(null);
@@ -73,12 +75,25 @@ export default function Sidebar({
{logTitle}
{logSubtitle}
-
-
- {button1Name}
-
-
- {/* "Search Earthquakes" should NOT be wrapped in a Link! */}
+ {onButton1Click ? (
+
+ {button1Name}
+
+ ) : (
+
+
+ {button1Name}
+
+
+ )}
+