Added close earthquake log and search modals on click outside

This commit is contained in:
Tim Howitz 2025-06-01 23:02:51 +01:00
parent 128517b388
commit dd650c6ba6
4 changed files with 448 additions and 459 deletions

View File

@ -229,6 +229,7 @@ export default function Profile() {
} }
setIsDeleting(true); setIsDeleting(true);
try { try {
// todo add delete user route
const res = await axios.post( const res = await axios.post(
"/api/delete-user", "/api/delete-user",
{ userId: user!.id }, { userId: user!.id },

View File

@ -95,6 +95,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// todo!! add log api route
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
alert(`Logged ${name} to storage: ${storageLocation}`); alert(`Logged ${name} to storage: ${storageLocation}`);
onClose(); onClose();

View File

@ -1,5 +1,5 @@
"use client"; "use client";
import { useState } from "react"; import { FormEvent, useState, useRef } from "react";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
@ -7,10 +7,18 @@ const typeOptions = [
{ value: "volcanic", label: "Volcanic" }, { value: "volcanic", label: "Volcanic" },
{ value: "tectonic", label: "Tectonic" }, { value: "tectonic", label: "Tectonic" },
{ value: "collapse", label: "Collapse" }, { value: "collapse", label: "Collapse" },
{ value: "explosion", label: "Explosion" } { value: "explosion", label: "Explosion" },
]; ];
export default function EarthquakeLogModal({ open, onClose, onSuccess }) { export default function EarthquakeLogModal({
open,
onClose,
onSuccess,
}: {
open: boolean;
onClose: () => void;
onSuccess: () => void;
}) {
const [date, setDate] = useState<Date | null>(new Date()); const [date, setDate] = useState<Date | null>(new Date());
const [magnitude, setMagnitude] = useState(""); const [magnitude, setMagnitude] = useState("");
const [type, setType] = useState(typeOptions[0].value); const [type, setType] = useState(typeOptions[0].value);
@ -21,15 +29,14 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
const [depth, setDepth] = useState(""); const [depth, setDepth] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [successCode, setSuccessCode] = useState<string | null>(null); const [successCode, setSuccessCode] = useState<string | null>(null);
const modalRef = useRef<HTMLDivElement>(null);
async function handleLatLonChange(lat: string, lon: string) { async function handleLatLonChange(lat: string, lon: string) {
setLatitude(lat); setLatitude(lat);
setLongitude(lon); setLongitude(lon);
if (/^-?\d+(\.\d+)?$/.test(lat) && /^-?\d+(\.\d+)?$/.test(lon)) { if (/^-?\d+(\.\d+)?$/.test(lat) && /^-?\d+(\.\d+)?$/.test(lon)) {
try { try {
const resp = await fetch( const resp = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10`);
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=10`
);
if (resp.ok) { if (resp.ok) {
const data = await resp.json(); const data = await resp.json();
setCity( setCity(
@ -49,7 +56,7 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
} }
} }
async function handleSubmit(e) { async function handleSubmit(e: FormEvent) {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
if (!date || !magnitude || !type || !city || !country || !latitude || !longitude || !depth) { if (!date || !magnitude || !type || !city || !country || !latitude || !longitude || !depth) {
@ -69,8 +76,8 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
country: country.trim(), country: country.trim(),
latitude: parseFloat(latitude), latitude: parseFloat(latitude),
longitude: parseFloat(longitude), longitude: parseFloat(longitude),
depth depth,
}) }),
}); });
if (res.ok) { if (res.ok) {
const result = await res.json(); const result = await res.json();
@ -88,13 +95,19 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
} }
} }
function handleOutsideClick(e: React.MouseEvent<HTMLDivElement>) {
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
onClose();
}
}
if (!open) return null; if (!open) return null;
// Success popup overlay // Success popup overlay
if (successCode) { if (successCode) {
return ( return (
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center"> <div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center" onClick={handleOutsideClick}>
<div className="bg-white rounded-lg px-8 py-8 max-w-md w-full relative shadow-lg"> <div className="bg-white rounded-lg px-8 py-8 max-w-md w-full relative shadow-lg" ref={modalRef}>
<button <button
onClick={() => { onClick={() => {
setSuccessCode(null); setSuccessCode(null);
@ -103,12 +116,10 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
className="absolute right-4 top-4 text-xl text-gray-400 hover:text-gray-700" className="absolute right-4 top-4 text-xl text-gray-400 hover:text-gray-700"
aria-label="Close" aria-label="Close"
> >
&times; ×
</button> </button>
<div className="text-center"> <div className="text-center">
<h2 className="text-xl font-semibold mb-3"> <h2 className="text-xl font-semibold mb-3">Thank you for logging an earthquake!</h2>
Thank you for logging an earthquake!
</h2>
<div className="mb-0">The Earthquake Identifier is</div> <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 className="font-mono text-lg mb-4 bg-slate-100 rounded px-2 py-1 inline-block text-blue-800">{successCode}</div>
</div> </div>
@ -118,13 +129,10 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
} }
return ( return (
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center"> <div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center" onClick={handleOutsideClick}>
<div className="bg-white rounded-lg shadow-lg p-6 max-w-lg w-full relative"> <div className="bg-white rounded-lg shadow-lg p-6 max-w-lg w-full relative" ref={modalRef}>
<button <button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg">
onClick={onClose} ×
className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg"
>
&times;
</button> </button>
<h2 className="font-bold text-xl mb-4">Log Earthquake</h2> <h2 className="font-bold text-xl mb-4">Log Earthquake</h2>
<form onSubmit={handleSubmit} className="space-y-3"> <form onSubmit={handleSubmit} className="space-y-3">
@ -132,7 +140,7 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
<label className="block text-sm font-medium">Date</label> <label className="block text-sm font-medium">Date</label>
<DatePicker <DatePicker
selected={date} selected={date}
onChange={date => setDate(date)} onChange={(date) => setDate(date)}
className="border rounded px-3 py-2 w-full" className="border rounded px-3 py-2 w-full"
dateFormat="yyyy-MM-dd" dateFormat="yyyy-MM-dd"
maxDate={new Date()} maxDate={new Date()}
@ -151,7 +159,7 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
max="10" max="10"
step="0.1" step="0.1"
value={magnitude} value={magnitude}
onChange={e => { onChange={(e) => {
const val = e.target.value; const val = e.target.value;
if (parseFloat(val) > 10) return; if (parseFloat(val) > 10) return;
setMagnitude(val); setMagnitude(val);
@ -161,13 +169,8 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
</div> </div>
<div> <div>
<label className="block text-sm font-medium">Type</label> <label className="block text-sm font-medium">Type</label>
<select <select className="border rounded px-3 py-2 w-full" value={type} onChange={(e) => setType(e.target.value)} required>
className="border rounded px-3 py-2 w-full" {typeOptions.map((opt) => (
value={type}
onChange={e => setType(e.target.value)}
required
>
{typeOptions.map(opt => (
<option key={opt.value} value={opt.value}> <option key={opt.value} value={opt.value}>
{opt.label} {opt.label}
</option> </option>
@ -176,14 +179,12 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
</div> </div>
<div> <div>
<label className="block text-sm font-medium">City/Area</label> <label className="block text-sm font-medium">City/Area</label>
<span className="block text-xs text-gray-400"> <span className="block text-xs text-gray-400">(Use Lat/Lon then press Enter for reverse lookup)</span>
(Use Lat/Lon then press Enter for reverse lookup)
</span>
<input <input
type="text" type="text"
className="border rounded px-3 py-2 w-full" className="border rounded px-3 py-2 w-full"
value={city} value={city}
onChange={e => setCity(e.target.value)} onChange={(e) => setCity(e.target.value)}
required required
/> />
</div> </div>
@ -193,7 +194,7 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
type="text" type="text"
className="border rounded px-3 py-2 w-full" className="border rounded px-3 py-2 w-full"
value={country} value={country}
onChange={e => setCountry(e.target.value)} onChange={(e) => setCountry(e.target.value)}
required required
/> />
</div> </div>
@ -204,7 +205,7 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
type="number" type="number"
className="border rounded px-3 py-2 w-full" className="border rounded px-3 py-2 w-full"
value={latitude} value={latitude}
onChange={e => handleLatLonChange(e.target.value, longitude)} onChange={(e) => handleLatLonChange(e.target.value, longitude)}
placeholder="e.g. 36.12" placeholder="e.g. 36.12"
step="any" step="any"
required required
@ -216,7 +217,7 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
type="number" type="number"
className="border rounded px-3 py-2 w-full" className="border rounded px-3 py-2 w-full"
value={longitude} value={longitude}
onChange={e => handleLatLonChange(latitude, e.target.value)} onChange={(e) => handleLatLonChange(latitude, e.target.value)}
placeholder="e.g. -115.17" placeholder="e.g. -115.17"
step="any" step="any"
required required
@ -229,16 +230,12 @@ export default function EarthquakeLogModal({ open, onClose, onSuccess }) {
type="text" type="text"
className="border rounded px-3 py-2 w-full" className="border rounded px-3 py-2 w-full"
value={depth} value={depth}
onChange={e => setDepth(e.target.value)} onChange={(e) => setDepth(e.target.value)}
placeholder="e.g. 10 km" placeholder="e.g. 10 km"
required required
/> />
</div> </div>
<button <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full" disabled={loading}>
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"} {loading ? "Logging..." : "Log Earthquake"}
</button> </button>
</form> </form>

View File

@ -23,6 +23,8 @@ const COLUMNS = [
{ label: "Date", key: "date" }, { label: "Date", key: "date" },
]; ];
// todo modify slightly
export default function EarthquakeSearchModal({ export default function EarthquakeSearchModal({
open, open,
onClose, onClose,
@ -72,15 +74,12 @@ export default function EarthquakeSearchModal({
// Filter logic // Filter logic
const filteredRows = useMemo(() => { const filteredRows = useMemo(() => {
return results.filter((row) => return results.filter(
(!filters.code || (row) =>
row.code.toLowerCase().includes(filters.code.toLowerCase())) && (!filters.code || row.code.toLowerCase().includes(filters.code.toLowerCase())) &&
(!filters.location || (!filters.location || (row.location || "").toLowerCase().includes(filters.location.toLowerCase())) &&
(row.location || "").toLowerCase().includes(filters.location.toLowerCase())) && (!filters.magnitude || String(row.magnitude).startsWith(filters.magnitude)) &&
(!filters.magnitude || (!filters.date || row.date.slice(0, 10) === filters.date)
String(row.magnitude).startsWith(filters.magnitude)) &&
(!filters.date ||
row.date.slice(0, 10) === filters.date)
); );
}, [results, filters]); }, [results, filters]);
@ -111,10 +110,9 @@ export default function EarthquakeSearchModal({
return ( return (
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center animate-fadein"> <div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center animate-fadein">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-2xl relative"> <div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-2xl relative">
<button <button onClick={onClose} className="absolute right-4 top-4 text-2xl text-gray-400 hover:text-red-500 font-bold">
onClick={onClose} &times;
className="absolute right-4 top-4 text-2xl text-gray-400 hover:text-red-500 font-bold" </button>
>&times;</button>
<h2 className="font-bold text-xl mb-4">Search Earthquakes</h2> <h2 className="font-bold text-xl mb-4">Search Earthquakes</h2>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
@ -160,9 +158,7 @@ export default function EarthquakeSearchModal({
Clear Clear
</button> </button>
</form> </form>
{error && ( {error && <div className="text-red-600 font-medium mb-2">{error}</div>}
<div className="text-red-600 font-medium mb-2">{error}</div>
)}
{/* Filter Row */} {/* Filter Row */}
<div className="mb-2"> <div className="mb-2">
<div className="flex gap-3"> <div className="flex gap-3">
@ -171,17 +167,10 @@ export default function EarthquakeSearchModal({
key={col.key} key={col.key}
type={col.key === "magnitude" ? "number" : col.key === "date" ? "date" : "text"} type={col.key === "magnitude" ? "number" : col.key === "date" ? "date" : "text"}
value={filters[col.key] || ""} value={filters[col.key] || ""}
onChange={e => onChange={(e) => setFilters((f) => ({ ...f, [col.key]: e.target.value }))}
setFilters(f => ({ ...f, [col.key]: e.target.value }))
}
className="border border-neutral-200 rounded px-2 py-1 text-xs" className="border border-neutral-200 rounded px-2 py-1 text-xs"
style={{ style={{
width: width: col.key === "magnitude" ? 70 : col.key === "date" ? 130 : 120,
col.key === "magnitude"
? 70
: col.key === "date"
? 130
: 120,
}} }}
placeholder={`Filter ${col.label}`} placeholder={`Filter ${col.label}`}
aria-label={`Filter ${col.label}`} aria-label={`Filter ${col.label}`}
@ -200,14 +189,15 @@ export default function EarthquakeSearchModal({
key={col.key} key={col.key}
className={`font-semibold px-3 py-2 cursor-pointer select-none text-left`} className={`font-semibold px-3 py-2 cursor-pointer select-none text-left`}
onClick={() => onClick={() =>
setSort(sort && sort.key === col.key 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: sort.dir === "asc" ? "desc" : "asc" }
: { key: col.key as keyof Earthquake, dir: "asc" }) : { key: col.key as keyof Earthquake, dir: "asc" }
)
} }
> >
{col.label} {col.label}
{sort?.key === col.key && {sort?.key === col.key && (sort.dir === "asc" ? " ↑" : " ↓")}
(sort.dir === "asc" ? " ↑" : " ↓")}
</th> </th>
))} ))}
</tr> </tr>
@ -220,7 +210,7 @@ export default function EarthquakeSearchModal({
</td> </td>
</tr> </tr>
)} )}
{sortedRows.map(eq => ( {sortedRows.map((eq) => (
<tr <tr
key={eq.id} key={eq.id}
className="hover:bg-blue-50 cursor-pointer border-b" className="hover:bg-blue-50 cursor-pointer border-b"