Observatory search page!

This commit is contained in:
IZZY 2025-06-01 17:17:25 +01:00
parent 19253672dc
commit fd77ceae88
2 changed files with 258 additions and 81 deletions

View File

@ -3,7 +3,8 @@ import { useState, useMemo } from "react";
import useSWR from "swr"; import useSWR from "swr";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
import Map from "@components/Map"; 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 { fetcher } from "@utils/axiosHelpers";
import { Observatory } from "@prismaclient"; import { Observatory } from "@prismaclient";
import { getRelativeDate } from "@utils/formatters"; import { getRelativeDate } from "@utils/formatters";
@ -33,11 +34,11 @@ export default function Observatories() {
const [hoveredEventId, setHoveredEventId] = useState(""); const [hoveredEventId, setHoveredEventId] = useState("");
const [logModalOpen, setLogModalOpen] = useState(false); const [logModalOpen, setLogModalOpen] = useState(false);
const [noAccessModalOpen, setNoAccessModalOpen] = useState(false); const [noAccessModalOpen, setNoAccessModalOpen] = useState(false);
const [searchModalOpen, setSearchModalOpen] = useState(false); // <-- NEW STATE
const user = useStoreState((state) => state.user); const user = useStoreState((state) => state.user);
const role: "GUEST" | "SCIENTIST" | "ADMIN" = user?.role ?? "GUEST"; const role: "GUEST" | "SCIENTIST" | "ADMIN" = user?.role ?? "GUEST";
const canLogObservatory = role === "SCIENTIST" || role === "ADMIN"; 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( const observatoryEvents = useMemo(
@ -49,7 +50,7 @@ export default function Observatories() {
title: ` ${x.name}`, title: ` ${x.name}`,
longitude: x.longitude, longitude: x.longitude,
latitude: x.latitude, latitude: x.latitude,
isFunctional: x.isFunctional, // <-- include this! isFunctional: !!x.isFunctional, // if isFunctional = 1/0, coerce to boolean
text1: "", text1: "",
text2: getRelativeDate(x.dateEstablished), text2: getRelativeDate(x.dateEstablished),
date: x.dateEstablished, date: x.dateEstablished,
@ -92,9 +93,15 @@ export default function Observatories() {
button2Name="Search Observatories" button2Name="Search Observatories"
onButton1Click={handleLogClick} onButton1Click={handleLogClick}
button1Disabled={!canLogObservatory} button1Disabled={!canLogObservatory}
onButton2Click={() => setSearchModalOpen(true)} // <-- This line enables the search modal
/> />
<LogObservatoryModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} /> <LogObservatoryModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} />
<NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} /> <NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} />
<SearchObservatoriesModal
open={searchModalOpen}
onClose={() => setSearchModalOpen(false)}
observatories={data?.observatories ?? []}
/>
</div> </div>
); );
} }

View 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"
>
&times;
</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;