diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 02b4240..a7a60e1 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -229,6 +229,7 @@ export default function Profile() { } setIsDeleting(true); try { + // todo add delete user route const res = await axios.post( "/api/delete-user", { userId: user!.id }, diff --git a/src/app/warehouse/page.tsx b/src/app/warehouse/page.tsx index 9d696f3..b0a8421 100644 --- a/src/app/warehouse/page.tsx +++ b/src/app/warehouse/page.tsx @@ -95,6 +95,7 @@ function LogModal({ onClose }: { onClose: () => void }) { } setIsSubmitting(true); try { + // todo!! add log api route await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call alert(`Logged ${name} to storage: ${storageLocation}`); onClose(); diff --git a/src/components/EarthquakeLogModal.tsx b/src/components/EarthquakeLogModal.tsx index ce9d30f..8f754f0 100644 --- a/src/components/EarthquakeLogModal.tsx +++ b/src/components/EarthquakeLogModal.tsx @@ -1,248 +1,245 @@ "use client"; -import { useState } from "react"; +import { FormEvent, useState, useRef } 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" } + { 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); +export default function EarthquakeLogModal({ + open, + onClose, + onSuccess, +}: { + open: boolean; + onClose: () => void; + onSuccess: () => void; +}) { + 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); + const modalRef = useRef(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 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); - } - } + async function handleSubmit(e: FormEvent) { + 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; + function handleOutsideClick(e: React.MouseEvent) { + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + onClose(); + } + } - // Success popup overlay - if (successCode) { - return ( -
-
- -
-

- Thank you for logging an earthquake! -

-
The Earthquake Identifier is
-
{successCode}
-
-
-
- ); - } + if (!open) return null; - return ( -
-
- -

Log Earthquake

-
-
- - setDate(date)} - className="border rounded px-3 py-2 w-full" - dateFormat="yyyy-MM-dd" - maxDate={new Date()} - showMonthDropdown - showYearDropdown - dropdownMode="select" - required - /> -
-
- - { - const val = e.target.value; - if (parseFloat(val) > 10) return; - setMagnitude(val); - }} - required - /> -
-
- - -
-
- - - (Use Lat/Lon then press Enter for reverse lookup) - - setCity(e.target.value)} - required - /> -
-
- - setCountry(e.target.value)} - required - /> -
-
-
- - handleLatLonChange(e.target.value, longitude)} - placeholder="e.g. 36.12" - step="any" - required - /> -
-
- - handleLatLonChange(latitude, e.target.value)} - placeholder="e.g. -115.17" - step="any" - required - /> -
-
-
- - setDepth(e.target.value)} - placeholder="e.g. 10 km" - required - /> -
- -
-
-
- ); -} \ No newline at end of file + // Success popup overlay + if (successCode) { + return ( +
+
+ +
+

Thank you for logging an earthquake!

+
The Earthquake Identifier is
+
{successCode}
+
+
+
+ ); + } + + return ( +
+
+ +

Log Earthquake

+
+
+ + setDate(date)} + className="border rounded px-3 py-2 w-full" + dateFormat="yyyy-MM-dd" + maxDate={new Date()} + showMonthDropdown + showYearDropdown + dropdownMode="select" + required + /> +
+
+ + { + const val = e.target.value; + if (parseFloat(val) > 10) return; + setMagnitude(val); + }} + required + /> +
+
+ + +
+
+ + (Use Lat/Lon then press Enter for reverse lookup) + setCity(e.target.value)} + required + /> +
+
+ + setCountry(e.target.value)} + required + /> +
+
+
+ + handleLatLonChange(e.target.value, longitude)} + placeholder="e.g. 36.12" + step="any" + required + /> +
+
+ + handleLatLonChange(latitude, e.target.value)} + placeholder="e.g. -115.17" + step="any" + required + /> +
+
+
+ + setDepth(e.target.value)} + placeholder="e.g. 10 km" + required + /> +
+ +
+
+
+ ); +} diff --git a/src/components/EarthquakeSearchModal.tsx b/src/components/EarthquakeSearchModal.tsx index d433bd0..4c0b2e0 100644 --- a/src/components/EarthquakeSearchModal.tsx +++ b/src/components/EarthquakeSearchModal.tsx @@ -3,243 +3,233 @@ import { useState, useEffect, useMemo } from "react"; import axios from "axios"; export type Earthquake = { - id: string; - code: string; - magnitude: number; - location: string; - date: string; - longitude: number; - latitude: number; + id: string; + code: string; + magnitude: number; + location: string; + date: string; + longitude: number; + latitude: number; }; function formatDate(iso: string) { - return new Date(iso).toLocaleDateString(); + return new Date(iso).toLocaleDateString(); } const COLUMNS = [ - { label: "Code", key: "code", className: "font-mono font-bold" }, - { label: "Location", key: "location" }, - { label: "Magnitude", key: "magnitude", numeric: true }, - { label: "Date", key: "date" }, + { label: "Code", key: "code", className: "font-mono font-bold" }, + { label: "Location", key: "location" }, + { label: "Magnitude", key: "magnitude", numeric: true }, + { label: "Date", key: "date" }, ]; +// todo modify slightly + export default function EarthquakeSearchModal({ - open, - onClose, - onSelect, + open, + onClose, + onSelect, }: { - open: boolean; - onClose: () => void; - onSelect: (eq: Earthquake) => void; + open: boolean; + onClose: () => void; + onSelect: (eq: Earthquake) => void; }) { - const [search, setSearch] = useState(""); - const [results, setResults] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); - // Filters per column - const [filters, setFilters] = useState<{ [k: string]: string }>({ - code: "", - location: "", - magnitude: "", - date: "", - }); - // Sort state - const [sort, setSort] = useState<{ key: keyof Earthquake; dir: "asc" | "desc" } | null>(null); + // Filters per column + const [filters, setFilters] = useState<{ [k: string]: string }>({ + code: "", + location: "", + magnitude: "", + date: "", + }); + // Sort state + const [sort, setSort] = useState<{ key: keyof Earthquake; dir: "asc" | "desc" } | null>(null); - useEffect(() => { - if (!open) { - setSearch(""); - setResults([]); - setFilters({ code: "", location: "", magnitude: "", date: "" }); - setError(""); - setSort(null); - } - }, [open]); + useEffect(() => { + if (!open) { + setSearch(""); + setResults([]); + setFilters({ code: "", location: "", magnitude: "", date: "" }); + setError(""); + setSort(null); + } + }, [open]); - const doSearch = async (q = search) => { - setLoading(true); - setResults([]); - setError(""); - try { - const resp = await axios.post("/api/earthquakes/search", { query: q }); - setResults(resp.data.earthquakes || []); - } catch (e: any) { - setError("Failed to search earthquakes."); - } - setLoading(false); - }; + const doSearch = async (q = search) => { + setLoading(true); + setResults([]); + setError(""); + try { + const resp = await axios.post("/api/earthquakes/search", { query: q }); + setResults(resp.data.earthquakes || []); + } catch (e: any) { + setError("Failed to search earthquakes."); + } + setLoading(false); + }; - // Filter logic - const filteredRows = useMemo(() => { - return results.filter((row) => - (!filters.code || - row.code.toLowerCase().includes(filters.code.toLowerCase())) && - (!filters.location || - (row.location || "").toLowerCase().includes(filters.location.toLowerCase())) && - (!filters.magnitude || - String(row.magnitude).startsWith(filters.magnitude)) && - (!filters.date || - row.date.slice(0, 10) === filters.date) - ); - }, [results, filters]); + // Filter logic + const filteredRows = useMemo(() => { + return results.filter( + (row) => + (!filters.code || row.code.toLowerCase().includes(filters.code.toLowerCase())) && + (!filters.location || (row.location || "").toLowerCase().includes(filters.location.toLowerCase())) && + (!filters.magnitude || String(row.magnitude).startsWith(filters.magnitude)) && + (!filters.date || row.date.slice(0, 10) === filters.date) + ); + }, [results, filters]); - // Sort logic - const sortedRows = useMemo(() => { - if (!sort) return filteredRows; - const sorted = [...filteredRows].sort((a, b) => { - let valA = a[sort.key]; - let valB = b[sort.key]; - if (sort.key === "magnitude") { - valA = Number(valA); - valB = Number(valB); - } else if (sort.key === "date") { - valA = a.date; - valB = b.date; - } else { - valA = String(valA || ""); - valB = String(valB || ""); - } - if (valA < valB) return sort.dir === "asc" ? -1 : 1; - if (valA > valB) return sort.dir === "asc" ? 1 : -1; - return 0; - }); - return sorted; - }, [filteredRows, sort]); + // Sort logic + const sortedRows = useMemo(() => { + if (!sort) return filteredRows; + const sorted = [...filteredRows].sort((a, b) => { + let valA = a[sort.key]; + let valB = b[sort.key]; + if (sort.key === "magnitude") { + valA = Number(valA); + valB = Number(valB); + } else if (sort.key === "date") { + valA = a.date; + valB = b.date; + } else { + valA = String(valA || ""); + valB = String(valB || ""); + } + if (valA < valB) return sort.dir === "asc" ? -1 : 1; + if (valA > valB) return sort.dir === "asc" ? 1 : -1; + return 0; + }); + return sorted; + }, [filteredRows, sort]); - if (!open) return null; - return ( -
-
- -

Search Earthquakes

-
{ - e.preventDefault(); - doSearch(); - }} - className="flex gap-2 mb-3" - > - setSearch(e.target.value)} - className="flex-grow px-3 py-2 border rounded" - disabled={loading} - /> - - -
- {error && ( -
{error}
- )} - {/* Filter Row */} -
-
- {COLUMNS.map((col) => ( - - setFilters(f => ({ ...f, [col.key]: e.target.value })) - } - className="border border-neutral-200 rounded px-2 py-1 text-xs" - style={{ - width: - col.key === "magnitude" - ? 70 - : col.key === "date" - ? 130 - : 120, - }} - placeholder={`Filter ${col.label}`} - aria-label={`Filter ${col.label}`} - disabled={loading || results.length === 0} - /> - ))} -
-
- {/* Results Table */} -
- - - - {COLUMNS.map((col) => ( - - ))} - - - - {sortedRows.length === 0 && !loading && ( - - - - )} - {sortedRows.map(eq => ( - { - onSelect(eq); - onClose(); - }} - tabIndex={0} - > - - - - - - ))} - -
- setSort(sort && sort.key === col.key - ? { key: col.key as keyof Earthquake, dir: sort.dir === "asc" ? "desc" : "asc" } - : { key: col.key as keyof Earthquake, dir: "asc" }) - } - > - {col.label} - {sort?.key === col.key && - (sort.dir === "asc" ? " ↑" : " ↓")} -
- No results found. -
{eq.code}{eq.location}{eq.magnitude}{formatDate(eq.date)}
-
-
-
- ); -} \ No newline at end of file + if (!open) return null; + return ( +
+
+ +

Search Earthquakes

+
{ + e.preventDefault(); + doSearch(); + }} + className="flex gap-2 mb-3" + > + setSearch(e.target.value)} + className="flex-grow px-3 py-2 border rounded" + disabled={loading} + /> + + +
+ {error &&
{error}
} + {/* Filter Row */} +
+
+ {COLUMNS.map((col) => ( + setFilters((f) => ({ ...f, [col.key]: e.target.value }))} + className="border border-neutral-200 rounded px-2 py-1 text-xs" + style={{ + width: col.key === "magnitude" ? 70 : col.key === "date" ? 130 : 120, + }} + placeholder={`Filter ${col.label}`} + aria-label={`Filter ${col.label}`} + disabled={loading || results.length === 0} + /> + ))} +
+
+ {/* Results Table */} +
+ + + + {COLUMNS.map((col) => ( + + ))} + + + + {sortedRows.length === 0 && !loading && ( + + + + )} + {sortedRows.map((eq) => ( + { + onSelect(eq); + onClose(); + }} + tabIndex={0} + > + + + + + + ))} + +
+ setSort( + sort && sort.key === col.key + ? { key: col.key as keyof Earthquake, dir: sort.dir === "asc" ? "desc" : "asc" } + : { key: col.key as keyof Earthquake, dir: "asc" } + ) + } + > + {col.label} + {sort?.key === col.key && (sort.dir === "asc" ? " ↑" : " ↓")} +
+ No results found. +
{eq.code}{eq.location}{eq.magnitude}{formatDate(eq.date)}
+
+
+
+ ); +}