Observatory search page!
This commit is contained in:
parent
19253672dc
commit
fd77ceae88
@ -3,7 +3,8 @@ 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 LogObservatoryModal from "@/components/LogObservatoryModal";
|
||||
import SearchObservatoriesModal from "@/components/SearchObservatoriesModal"; // <-- add this import
|
||||
import { fetcher } from "@utils/axiosHelpers";
|
||||
import { Observatory } from "@prismaclient";
|
||||
import { getRelativeDate } from "@utils/formatters";
|
||||
@ -11,90 +12,96 @@ import GeologicalEvent from "@appTypes/GeologicalEvent";
|
||||
import { useStoreState } from "@hooks/store";
|
||||
|
||||
function NoAccessModal({ open, onClose }) {
|
||||
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-8 max-w-xs w-full text-center relative">
|
||||
<button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
<h2 className="font-bold text-xl mb-4">No Access</h2>
|
||||
<p className="text-gray-600 mb-3">Sorry, You do not have access rights, please log in or contact an Admin.</p>
|
||||
<button onClick={onClose} className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
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-8 max-w-xs w-full text-center relative">
|
||||
<button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
<h2 className="font-bold text-xl mb-4">No Access</h2>
|
||||
<p className="text-gray-600 mb-3">Sorry, You do not have access rights, please log in or contact an Admin.</p>
|
||||
<button onClick={onClose} className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Observatories() {
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
const [logModalOpen, setLogModalOpen] = useState(false);
|
||||
const [noAccessModalOpen, setNoAccessModalOpen] = useState(false);
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
const [logModalOpen, setLogModalOpen] = useState(false);
|
||||
const [noAccessModalOpen, setNoAccessModalOpen] = useState(false);
|
||||
const [searchModalOpen, setSearchModalOpen] = useState(false); // <-- NEW STATE
|
||||
|
||||
const user = useStoreState((state) => state.user);
|
||||
const role: "GUEST" | "SCIENTIST" | "ADMIN" = user?.role ?? "GUEST";
|
||||
const canLogObservatory = role === "SCIENTIST" || role === "ADMIN";
|
||||
const user = useStoreState((state) => state.user);
|
||||
const role: "GUEST" | "SCIENTIST" | "ADMIN" = user?.role ?? "GUEST";
|
||||
const canLogObservatory = role === "SCIENTIST" || role === "ADMIN";
|
||||
const { data, error, isLoading, mutate } = useSWR("/api/observatories", fetcher);
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR("/api/observatories", fetcher);
|
||||
const observatoryEvents = useMemo(
|
||||
() =>
|
||||
data && data.observatories
|
||||
? data.observatories
|
||||
.map((x: Observatory): GeologicalEvent & { isFunctional: boolean } => ({
|
||||
id: x.id.toString(),
|
||||
title: ` ${x.name}`,
|
||||
longitude: x.longitude,
|
||||
latitude: x.latitude,
|
||||
isFunctional: !!x.isFunctional, // if isFunctional = 1/0, coerce to boolean
|
||||
text1: "",
|
||||
text2: getRelativeDate(x.dateEstablished),
|
||||
date: x.dateEstablished,
|
||||
}))
|
||||
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
: [],
|
||||
[data]
|
||||
);
|
||||
|
||||
const observatoryEvents = useMemo(
|
||||
() =>
|
||||
data && data.observatories
|
||||
? data.observatories
|
||||
.map((x: Observatory): GeologicalEvent & { isFunctional: boolean } => ({
|
||||
id: x.id.toString(),
|
||||
title: ` ${x.name}`,
|
||||
longitude: x.longitude,
|
||||
latitude: x.latitude,
|
||||
isFunctional: x.isFunctional, // <-- include this!
|
||||
text1: "",
|
||||
text2: getRelativeDate(x.dateEstablished),
|
||||
date: x.dateEstablished,
|
||||
}))
|
||||
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
: [],
|
||||
[data]
|
||||
);
|
||||
const handleLogClick = () => {
|
||||
if (canLogObservatory) {
|
||||
setLogModalOpen(true);
|
||||
} else {
|
||||
setNoAccessModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogClick = () => {
|
||||
if (canLogObservatory) {
|
||||
setLogModalOpen(true);
|
||||
} else {
|
||||
setNoAccessModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
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={handleLogClick}
|
||||
button1Disabled={!canLogObservatory}
|
||||
/>
|
||||
<LogObservatoryModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} />
|
||||
<NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
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={handleLogClick}
|
||||
button1Disabled={!canLogObservatory}
|
||||
onButton2Click={() => setSearchModalOpen(true)} // <-- This line enables the search modal
|
||||
/>
|
||||
<LogObservatoryModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} />
|
||||
<NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} />
|
||||
<SearchObservatoriesModal
|
||||
open={searchModalOpen}
|
||||
onClose={() => setSearchModalOpen(false)}
|
||||
observatories={data?.observatories ?? []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
170
src/components/SearchObservatoriesModal.tsx
Normal file
170
src/components/SearchObservatoriesModal.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
|
||||
interface Observatory {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
dateEstablished: string;
|
||||
dateClosed?: string | null;
|
||||
isFunctional: number; // 0 or 1!
|
||||
// ...other fields can be ignored
|
||||
}
|
||||
|
||||
interface SearchObservatoriesModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
observatories: Observatory[];
|
||||
}
|
||||
|
||||
function statusColor(isFunctional: number) {
|
||||
return isFunctional === 1 ? "bg-green-500" : "bg-red-500";
|
||||
}
|
||||
|
||||
function formatDate(date: string | null | undefined) {
|
||||
if (!date) return "-";
|
||||
// Handles both ISO and possibly SQL datetime strings.
|
||||
const parsed = new Date(date);
|
||||
if (parsed.getFullYear() < 1900) return "-";
|
||||
return parsed.toLocaleDateString();
|
||||
}
|
||||
|
||||
const SearchObservatoriesModal: React.FC<SearchObservatoriesModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
observatories,
|
||||
}) => {
|
||||
const [tab, setTab] = useState<"name" | "location">("name");
|
||||
const [query, setQuery] = useState("");
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!query) return observatories;
|
||||
const q = query.toLowerCase();
|
||||
if (tab === "name") {
|
||||
return observatories.filter((o) =>
|
||||
o.name?.toLowerCase().includes(q)
|
||||
);
|
||||
} else {
|
||||
return observatories.filter((o) =>
|
||||
o.location?.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
}, [observatories, query, tab]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto relative">
|
||||
<button
|
||||
onClick={() => { setQuery(""); setExpandedId(null); onClose(); }}
|
||||
className="absolute top-3 right-3 text-2xl text-gray-500 hover:text-black"
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="px-6 pt-6 pb-2 border-b flex flex-col md:flex-row items-center gap-2 justify-between">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-xl font-semibold mb-2 md:mb-0">Search Observatories</h2>
|
||||
{/* Open/Closed key */}
|
||||
<div className="flex gap-5 items-center mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-green-500 mr-1"></span>
|
||||
<span className="text-sm text-gray-700">Open</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-red-500 mr-1"></span>
|
||||
<span className="text-sm text-gray-700">Closed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`py-1 px-3 rounded-md text-sm ${tab === "name" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
|
||||
onClick={() => setTab("name")}
|
||||
>
|
||||
By Name
|
||||
</button>
|
||||
<button
|
||||
className={`py-1 px-3 rounded-md text-sm ${tab === "location" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-700"}`}
|
||||
onClick={() => setTab("location")}
|
||||
>
|
||||
By Location
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<input
|
||||
className="w-full border rounded px-4 py-2 mb-4 focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||
type="text"
|
||||
placeholder={tab === "name" ? "Type observatory name..." : "Type location..."}
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<ul>
|
||||
{filtered.length === 0 && (
|
||||
<li className="text-gray-500 py-10 text-center">No observatories found.</li>
|
||||
)}
|
||||
{filtered.map((obs) => (
|
||||
<li
|
||||
key={obs.id}
|
||||
className={`border rounded mb-3 transition-shadow ${expandedId === obs.id ? "shadow-lg" : "hover:shadow"} bg-white`}
|
||||
>
|
||||
<button
|
||||
className="flex w-full items-center justify-between px-4 py-2"
|
||||
onClick={() => setExpandedId(expandedId === obs.id ? null : obs.id)}
|
||||
aria-expanded={expandedId === obs.id}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded-full mr-2 ${statusColor(Number(obs.isFunctional))}`}
|
||||
title={Number(obs.isFunctional) === 1 ? "Operational" : "Not operational"}
|
||||
></span>
|
||||
<span className="font-medium">{obs.name}</span>
|
||||
</span>
|
||||
<span className="text-gray-600 text-sm">{obs.location}</span>
|
||||
<svg
|
||||
className={`ml-4 w-5 h-5 text-gray-400 transition-transform ${
|
||||
expandedId === obs.id ? "rotate-90" : ""
|
||||
}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M9 6l6 6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
{expandedId === obs.id && (
|
||||
<div className="px-8 py-4 bg-gray-50 border-t grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||
<div>
|
||||
<span className="font-semibold">Latitude:</span> {obs.latitude}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Longitude:</span> {obs.longitude}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Date Established:</span> {formatDate(obs.dateEstablished)}
|
||||
</div>
|
||||
{/* Date Closed removed as requested */}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="border-t px-6 py-3 text-right">
|
||||
<button
|
||||
onClick={() => { setQuery(""); setExpandedId(null); onClose(); }}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white rounded py-2 px-6"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchObservatoriesModal;
|
||||
Loading…
x
Reference in New Issue
Block a user