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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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