Compare commits

...

2 Commits

Author SHA1 Message Date
6cd95fa0e4 Improved Sidebar styling 2025-06-01 14:18:44 +01:00
f88f783de9 Made some small improvements 2025-06-01 14:18:32 +01:00
7 changed files with 429 additions and 455 deletions

View File

@ -6,134 +6,117 @@ import Sidebar from "@components/Sidebar";
import { createPoster } from "@utils/axiosHelpers"; import { createPoster } from "@utils/axiosHelpers";
import { Earthquake } from "@prismaclient"; import { Earthquake } from "@prismaclient";
import { getRelativeDate } from "@utils/formatters"; import { getRelativeDate } from "@utils/formatters";
import GeologicalEvent from "@appTypes/Event"; import GeologicalEvent from "@appTypes/GeologicalEvent";
import EarthquakeSearchModal from "@components/EarthquakeSearchModal"; import EarthquakeSearchModal from "@components/EarthquakeSearchModal";
import EarthquakeLogModal from "@components/EarthquakeLogModal"; // If you use a separate log modal import EarthquakeLogModal from "@components/EarthquakeLogModal"; // If you use a separate log modal
import { useStoreState } from "@hooks/store"; import { useStoreState } from "@hooks/store";
// Optional: "No Access Modal" - as in your original // Optional: "No Access Modal" - as in your original
function NoAccessModal({ open, onClose }) { function NoAccessModal({ open, onClose }) {
if (!open) return null; if (!open) return null;
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">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-xs w-full text-center relative"> <div className="bg-white rounded-lg shadow-lg p-8 max-w-xs w-full text-center relative">
<button <button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" aria-label="Close">
onClick={onClose} &times;
className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" </button>
aria-label="Close" <h2 className="font-bold text-xl mb-4">Access Denied</h2>
> <p className="text-gray-600 mb-3">
&times; Sorry, you do not have access rights to Log an Earthquake. Please Log in here, or contact an Admin if you believe this
</button> is a mistake
<h2 className="font-bold text-xl mb-4">Access Denied</h2> </p>
<p className="text-gray-600 mb-3"> <button onClick={onClose} className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2">
Sorry, you do not have access rights to Log an Earthquake. Please Log in here, or contact an Admin if you believe this is a mistake OK
</p> </button>
<button </div>
onClick={onClose} </div>
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2" );
>OK</button>
</div>
</div>
);
} }
export default function Earthquakes() { export default function Earthquakes() {
const [selectedEventId, setSelectedEventId] = useState(""); const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState(""); const [hoveredEventId, setHoveredEventId] = useState("");
const [searchModalOpen, setSearchModalOpen] = useState(false); const [searchModalOpen, setSearchModalOpen] = useState(false);
const [logModalOpen, setLogModalOpen] = useState(false); const [logModalOpen, setLogModalOpen] = useState(false);
const [noAccessModalOpen, setNoAccessModalOpen] = useState(false); const [noAccessModalOpen, setNoAccessModalOpen] = useState(false);
// Your user/role logic // Your user/role logic
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 canLogEarthquake = role === "SCIENTIST" || role === "ADMIN"; const canLogEarthquake = role === "SCIENTIST" || role === "ADMIN";
// Fetch earthquakes (10 days recent) // Fetch earthquakes (10 days recent)
const { data, error, isLoading, mutate } = useSWR( const { data, error, isLoading, mutate } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 10 }));
"/api/earthquakes",
createPoster({ rangeDaysPrev: 10 })
);
// Shape for Map/Sidebar // Shape for Map/Sidebar
const earthquakeEvents = useMemo( const earthquakeEvents = useMemo(
() => () =>
data && data.earthquakes data && data.earthquakes
? data.earthquakes ? data.earthquakes
.map( .map(
(x: Earthquake): GeologicalEvent => ({ (x: Earthquake): GeologicalEvent => ({
id: x.code, id: x.code,
title: `Earthquake in ${x.location || (x.code && x.code.split("-")[2])}`, title: `Earthquake in ${x.location || (x.code && x.code.split("-")[2])}`,
magnitude: x.magnitude, magnitude: x.magnitude,
longitude: x.longitude, longitude: x.longitude,
latitude: x.latitude, latitude: x.latitude,
text1: "", text1: "",
text2: getRelativeDate(x.date), text2: getRelativeDate(x.date),
date: x.date, date: x.date,
}) code: x.code,
) })
.sort( )
(a: GeologicalEvent, b: GeologicalEvent) => .sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime())
new Date(b.date).getTime() - new Date(a.date).getTime() : [],
) [data]
: [], );
[data]
);
// Handler for log // Handler for log
const handleLogClick = () => { const handleLogClick = () => {
if (canLogEarthquake) { if (canLogEarthquake) {
setLogModalOpen(true); setLogModalOpen(true);
} else { } else {
setNoAccessModalOpen(true); setNoAccessModalOpen(true);
} }
}; };
return ( return (
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden"> <div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
<div className="flex-grow h-full"> <div className="flex-grow h-full">
<Map <Map
events={earthquakeEvents} events={earthquakeEvents}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
mapType="Earthquakes" mapType="Earthquakes"
/> />
</div> </div>
<Sidebar <Sidebar
logTitle="Log an Earthquake" logTitle="Log an Earthquake"
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists" logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists"
recentsTitle="Recent Earthquakes" recentsTitle="Recent Earthquakes"
events={earthquakeEvents} events={earthquakeEvents}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
button1Name="Log an Earthquake" button1Name="Log an Earthquake"
button2Name="Search Earthquakes" button2Name="Search Earthquakes"
onButton1Click={handleLogClick} onButton1Click={handleLogClick}
onButton2Click={() => setSearchModalOpen(true)} onButton2Click={() => setSearchModalOpen(true)}
button1Disabled={!canLogEarthquake} button1Disabled={!canLogEarthquake}
/> />
{/* ---- SEARCH MODAL ---- */} {/* ---- SEARCH MODAL ---- */}
<EarthquakeSearchModal <EarthquakeSearchModal
open={searchModalOpen} open={searchModalOpen}
onClose={() => setSearchModalOpen(false)} onClose={() => setSearchModalOpen(false)}
onSelect={(eq) => setSelectedEventId(eq.code)} onSelect={(eq) => setSelectedEventId(eq.code)}
/> />
{/* ---- LOGGING MODAL ---- */} {/* ---- LOGGING MODAL ---- */}
<EarthquakeLogModal <EarthquakeLogModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} />
open={logModalOpen} {/* ---- NO ACCESS ---- */}
onClose={() => setLogModalOpen(false)} <NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} />
onSuccess={() => mutate()} </div>
/> );
{/* ---- NO ACCESS ---- */}
<NoAccessModal
open={noAccessModalOpen}
onClose={() => setNoAccessModalOpen(false)}
/>
</div>
);
} }

View File

@ -3,8 +3,8 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
--background: #ffffff; --background: #ffffff;
--foreground: #171717; --foreground: #171717;
} }
/* @media (prefers-color-scheme: dark) { /* @media (prefers-color-scheme: dark) {
@ -15,216 +15,216 @@
} */ } */
body { body {
color: var(--foreground); color: var(--foreground);
background: var(--background); background: var(--background);
} }
/* Increase specificity and use !important where necessary */ /* Increase specificity and use !important where necessary */
.mapboxgl-popup .mapboxgl-popup-content { .mapboxgl-popup .mapboxgl-popup-content {
@apply rounded-xl p-4 px-5 drop-shadow-lg border border-neutral-300 max-w-xs !important; @apply rounded-xl p-4 px-5 drop-shadow-lg border border-neutral-300 max-w-xs !important;
} }
/* Hide the popup tip */ /* Hide the popup tip */
.mapboxgl-popup .mapboxgl-popup-tip { .mapboxgl-popup .mapboxgl-popup-tip {
display: none !important; display: none !important;
} }
/* Child elements */ /* Child elements */
.mapboxgl-popup-content h3 { .mapboxgl-popup-content h3 {
@apply text-sm font-medium text-neutral-800 !important; @apply text-sm font-medium text-neutral-800 !important;
} }
.mapboxgl-popup-content p { .mapboxgl-popup-content p {
@apply text-xs text-neutral-600 !important; @apply text-xs text-neutral-600 !important;
} }
.mapboxgl-popup-content p+p { .mapboxgl-popup-content p + p {
@apply text-neutral-500 !important; @apply text-neutral-500 !important;
} }
.icon-link { .icon-link {
/* default styles if needed */ /* default styles if needed */
} }
.icon-link:hover, .icon-link:hover,
.icon-link:focus { .icon-link:focus {
background-color: #16424b; background-color: #16424b;
} }
.icon-link:hover h3, .icon-link:hover h3,
.icon-link:focus h3, .icon-link:focus h3,
.icon-link:hover p, .icon-link:hover p,
.icon-link:focus p { .icon-link:focus p {
color: #fff !important; color: #fff !important;
} }
.icon-link:hover h3, .icon-link:hover h3,
.icon-link:hover p, .icon-link:hover p,
.icon-link:focus h3, .icon-link:focus h3,
.icon-link:focus p { .icon-link:focus p {
color: #111; color: #111;
/* or black */ /* or black */
} }
/* ---- LAVA FLOOD OVERLAY ---- */ /* ---- LAVA FLOOD OVERLAY ---- */
.lava-flood-overlay { .lava-flood-overlay {
pointer-events: none; pointer-events: none;
position: fixed; position: fixed;
top: -100vh; top: -100vh;
left: 0; left: 0;
right: 0; right: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: 9999; z-index: 9999;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
transition: top 0.9s cubic-bezier(.6, 0, .2, 1); transition: top 0.9s cubic-bezier(0.6, 0, 0.2, 1);
} }
.lava-flood-overlay.lava-active { .lava-flood-overlay.lava-active {
top: 0; top: 0;
transition: top 0.33s cubic-bezier(.6, 0, .2, 1); transition: top 0.33s cubic-bezier(0.6, 0, 0.2, 1);
} }
.lava-flood-overlay img, .lava-flood-overlay img,
.lava-gradient { .lava-gradient {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
object-fit: cover; object-fit: cover;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
filter: brightness(1.15) saturate(1.8) drop-shadow(0 0 80px #ff5500); filter: brightness(1.15) saturate(1.8) drop-shadow(0 0 80px #ff5500);
} }
/* ---- INSANE SCREEN SHAKE (LinkedIn) ---- */ /* ---- INSANE SCREEN SHAKE (LinkedIn) ---- */
@keyframes supershake { @keyframes supershake {
0% { 0% {
transform: translate(0, 0) rotate(0); transform: translate(0, 0) rotate(0);
} }
5% { 5% {
transform: translate(-20px, 5px) rotate(-2deg); transform: translate(-20px, 5px) rotate(-2deg);
} }
10% { 10% {
transform: translate(18px, -8px) rotate(2deg); transform: translate(18px, -8px) rotate(2deg);
} }
15% { 15% {
transform: translate(-22px, 8px) rotate(-4deg); transform: translate(-22px, 8px) rotate(-4deg);
} }
20% { 20% {
transform: translate(22px, -2px) rotate(4deg); transform: translate(22px, -2px) rotate(4deg);
} }
25% { 25% {
transform: translate(-18px, 12px) rotate(-2deg); transform: translate(-18px, 12px) rotate(-2deg);
} }
30% { 30% {
transform: translate(18px, -10px) rotate(2deg); transform: translate(18px, -10px) rotate(2deg);
} }
35% { 35% {
transform: translate(-22px, 14px) rotate(-4deg); transform: translate(-22px, 14px) rotate(-4deg);
} }
40% { 40% {
transform: translate(22px, -12px) rotate(4deg); transform: translate(22px, -12px) rotate(4deg);
} }
45% { 45% {
transform: translate(-18px, 8px) rotate(-2deg); transform: translate(-18px, 8px) rotate(-2deg);
} }
50% { 50% {
transform: translate(18px, -14px) rotate(4deg); transform: translate(18px, -14px) rotate(4deg);
} }
55% { 55% {
transform: translate(-22px, 12px) rotate(-4deg); transform: translate(-22px, 12px) rotate(-4deg);
} }
60% { 60% {
transform: translate(22px, -8px) rotate(2deg); transform: translate(22px, -8px) rotate(2deg);
} }
65% { 65% {
transform: translate(-18px, 10px) rotate(-2deg); transform: translate(-18px, 10px) rotate(-2deg);
} }
70% { 70% {
transform: translate(18px, -12px) rotate(2deg); transform: translate(18px, -12px) rotate(2deg);
} }
75% { 75% {
transform: translate(-22px, 14px) rotate(-4deg); transform: translate(-22px, 14px) rotate(-4deg);
} }
80% { 80% {
transform: translate(22px, -10px) rotate(4deg); transform: translate(22px, -10px) rotate(4deg);
} }
85% { 85% {
transform: translate(-18px, 8px) rotate(-2deg); transform: translate(-18px, 8px) rotate(-2deg);
} }
90% { 90% {
transform: translate(18px, -14px) rotate(2deg); transform: translate(18px, -14px) rotate(2deg);
} }
95% { 95% {
transform: translate(-20px, 5px) rotate(-2deg); transform: translate(-20px, 5px) rotate(-2deg);
} }
100% { 100% {
transform: translate(0, 0) rotate(0); transform: translate(0, 0) rotate(0);
} }
} }
.shake-screen { .shake-screen {
animation: supershake 1s cubic-bezier(.36, .07, .19, .97) both; animation: supershake 1s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
} }
/* ---- CRACK + COLLAPSE OVERLAY (X icon) ---- */ /* ---- CRACK + COLLAPSE OVERLAY (X icon) ---- */
.crack-overlay { .crack-overlay {
pointer-events: none; pointer-events: none;
position: fixed; position: fixed;
inset: 0; inset: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: 9999; z-index: 9999;
transition: transform 1.1s cubic-bezier(.65, .05, .45, 1), opacity 0.5s; transition: transform 1.1s cubic-bezier(0.65, 0.05, 0.45, 1), opacity 0.5s;
will-change: transform, opacity; will-change: transform, opacity;
} }
.crack { .crack {
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
} }
.crack1 { .crack1 {
width: 35vw; width: 35vw;
left: 10vw; left: 10vw;
top: 22vh; top: 22vh;
opacity: 0.8; opacity: 0.8;
} }
.crack2 { .crack2 {
width: 32vw; width: 32vw;
right: 12vw; right: 12vw;
top: 42vh; top: 42vh;
opacity: 0.7; opacity: 0.7;
transform: rotate(-8deg); transform: rotate(-8deg);
} }
/* Add more .crackN classes if using more cracks */ /* Add more .crackN classes if using more cracks */
/* Collapse falling effect */ /* Collapse falling effect */
.crack-collapse { .crack-collapse {
transform: perspective(900px) rotateX(75deg) translateY(80vh) scale(0.9); transform: perspective(900px) rotateX(75deg) translateY(80vh) scale(0.9);
opacity: 0; opacity: 0;
transition: transform 1.1s cubic-bezier(.65, .05, .45, 1), opacity 0.6s; transition: transform 1.1s cubic-bezier(0.65, 0.05, 0.45, 1), opacity 0.6s;
} }

View File

@ -7,110 +7,94 @@ import LogObservatoryModal from "@/components/LogObservatoryModal"; // Adjust if
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";
import GeologicalEvent from "@appTypes/Event"; import GeologicalEvent from "@appTypes/GeologicalEvent";
import { useStoreState } from "@hooks/store"; import { useStoreState } from "@hooks/store";
function NoAccessModal({ open, onClose }) { function NoAccessModal({ open, onClose }) {
if (!open) return null; if (!open) return null;
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">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-xs w-full text-center relative"> <div className="bg-white rounded-lg shadow-lg p-8 max-w-xs w-full text-center relative">
<button <button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" aria-label="Close">
onClick={onClose} &times;
className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" </button>
aria-label="Close" <h2 className="font-bold text-xl mb-4">No Access</h2>
>&times;</button> <p className="text-gray-600 mb-3">Sorry, You do not have access rights, please log in or contact an Admin.</p>
<h2 className="font-bold text-xl mb-4">No Access</h2> <button onClick={onClose} className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2">
<p className="text-gray-600 mb-3">Sorry, You do not have access rights, please log in or contact an Admin.</p> OK
<button </button>
onClick={onClose} </div>
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2" </div>
>OK</button> );
</div>
</div>
);
} }
export default function Observatories() { export default function Observatories() {
const [selectedEventId, setSelectedEventId] = useState(""); const [selectedEventId, setSelectedEventId] = useState("");
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 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( const { data, error, isLoading, mutate } = useSWR("/api/observatories", fetcher);
"/api/observatories",
fetcher
);
const observatoryEvents = useMemo( const observatoryEvents = useMemo(
() => () =>
data && data.observatories data && data.observatories
? data.observatories ? data.observatories
.map((x: Observatory): GeologicalEvent & { isFunctional: boolean } => ({ .map((x: Observatory): GeologicalEvent & { isFunctional: boolean } => ({
id: x.id.toString(), id: x.id.toString(),
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, // <-- include this!
text1: "", text1: "",
text2: getRelativeDate(x.dateEstablished), text2: getRelativeDate(x.dateEstablished),
date: x.dateEstablished, date: x.dateEstablished,
})) }))
.sort( .sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime())
(a: GeologicalEvent, b: GeologicalEvent) => : [],
new Date(b.date).getTime() - new Date(a.date).getTime() [data]
) );
: [],
[data]
);
const handleLogClick = () => { const handleLogClick = () => {
if (canLogObservatory) { if (canLogObservatory) {
setLogModalOpen(true); setLogModalOpen(true);
} else { } else {
setNoAccessModalOpen(true); setNoAccessModalOpen(true);
} }
}; };
return ( return (
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden"> <div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
<div className="flex-grow h-full"> <div className="flex-grow h-full">
<Map <Map
events={observatoryEvents} events={observatoryEvents}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
mapType="observatories" mapType="observatories"
/> />
</div> </div>
<Sidebar <Sidebar
logTitle="Observatory Mapping" logTitle="Observatory Mapping"
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes" logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
recentsTitle="New Observatories" recentsTitle="New Observatories"
events={observatoryEvents} events={observatoryEvents}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
button1Name="Log a New Observatory" button1Name="Log a New Observatory"
button2Name="Search Observatories" button2Name="Search Observatories"
onButton1Click={handleLogClick} onButton1Click={handleLogClick}
button1Disabled={!canLogObservatory} button1Disabled={!canLogObservatory}
/> />
<LogObservatoryModal <LogObservatoryModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} />
open={logModalOpen} <NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} />
onClose={() => setLogModalOpen(false)} </div>
onSuccess={() => mutate()} );
/>
<NoAccessModal
open={noAccessModalOpen}
onClose={() => setNoAccessModalOpen(false)}
/>
</div>
);
} }

View File

@ -2,7 +2,7 @@ import Link from "next/link";
import React, { Dispatch, SetStateAction, useState } from "react"; import React, { Dispatch, SetStateAction, useState } from "react";
import { TbHexagon } from "react-icons/tb"; import { TbHexagon } from "react-icons/tb";
import Event from "@appTypes/Event"; import Event from "@appTypes/GeologicalEvent";
import getMagnitudeColor from "@utils/getMagnitudeColour"; import getMagnitudeColor from "@utils/getMagnitudeColour";
function setButton1(button1Name) { function setButton1(button1Name) {

View File

@ -5,7 +5,7 @@ import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useSta
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { GiObservatory } from "react-icons/gi"; import { GiObservatory } from "react-icons/gi";
import GeologicalEvent from "@appTypes/Event"; import GeologicalEvent from "@appTypes/GeologicalEvent";
import getMagnitudeColor from "@utils/getMagnitudeColour"; import getMagnitudeColor from "@utils/getMagnitudeColour";
interface MapComponentProps { interface MapComponentProps {
@ -111,12 +111,12 @@ function MapComponent({
} }
const observatoryElement = document.createElement("div"); const observatoryElement = document.createElement("div");
const root = createRoot(observatoryElement); const root = createRoot(observatoryElement);
root.render( root.render(
<GiObservatory <GiObservatory
className={`text-2xl drop-shadow-lg ${event.isFunctional === false ? "text-gray-400" : "text-blue-600"}`} className={`text-2xl drop-shadow-lg ${event.isFunctional === false ? "text-gray-400" : "text-blue-600"}`}
/> />
); );
quakeElement.appendChild(pulseElement); quakeElement.appendChild(pulseElement);
quakeElement.appendChild(dotElement); quakeElement.appendChild(dotElement);
@ -128,6 +128,7 @@ function MapComponent({
const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(` const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(`
<div> <div>
<h3>${event.title}</h3> <h3>${event.title}</h3>
${mapType !== "observatories" ? `<p style="margin-bottom:3px;">${event.code}` : null}
${mapType === "observatories" ? `<p>${event.text1}</p>` : `<p>Magnitude: ${event.magnitude}</p>`} ${mapType === "observatories" ? `<p>${event.text1}</p>` : `<p>Magnitude: ${event.magnitude}</p>`}
<p>${event.text2}</p> <p>${event.text2}</p>
</div> </div>

View File

@ -1,124 +1,128 @@
import React, { Dispatch, SetStateAction, useEffect, useRef } from "react"; import React, { Dispatch, SetStateAction, useEffect, useRef } from "react";
import { TbHexagon } from "react-icons/tb"; import { TbHexagon } from "react-icons/tb";
import GeologicalEvent from "@appTypes/Event"; import GeologicalEvent from "@appTypes/GeologicalEvent";
import getMagnitudeColor from "@utils/getMagnitudeColour"; import getMagnitudeColor from "@utils/getMagnitudeColour";
interface SidebarProps { interface SidebarProps {
logTitle: string; logTitle: string;
logSubtitle: string; logSubtitle: string;
recentsTitle: string; recentsTitle: string;
events: GeologicalEvent[]; events: GeologicalEvent[];
selectedEventId: GeologicalEvent["id"]; selectedEventId: GeologicalEvent["id"];
setSelectedEventId: Dispatch<SetStateAction<string>>; setSelectedEventId: Dispatch<SetStateAction<string>>;
hoveredEventId: GeologicalEvent["id"]; hoveredEventId: GeologicalEvent["id"];
setHoveredEventId: Dispatch<SetStateAction<string>>; setHoveredEventId: Dispatch<SetStateAction<string>>;
button1Name: string; button1Name: string;
button2Name: string; button2Name: string;
onButton2Click?: () => void; onButton2Click?: () => void;
onButton1Click?: () => void; onButton1Click?: () => void;
button1Disabled?: boolean; button1Disabled?: boolean;
} }
function MagnitudeNumber({ magnitude }: { magnitude: number }) { function MagnitudeNumber({ magnitude }: { magnitude: number }) {
const magnitudeStr = magnitude.toFixed(1); const magnitudeStr = magnitude.toFixed(1);
const [whole, decimal] = magnitudeStr.split("."); const [whole, decimal] = magnitudeStr.split(".");
return ( return (
<div className="relative" style={{ color: getMagnitudeColor(magnitude) }}> <div className="relative" style={{ color: getMagnitudeColor(magnitude) }}>
<TbHexagon size={40} className="drop-shadow-sm" /> <TbHexagon size={40} className="drop-shadow-sm" />
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<div className="flex items-baseline font-mono font-bold tracking-tight"> <div className="flex items-baseline font-mono font-bold tracking-tight">
<span className="text-xl -mr-1">{whole}</span> <span className="text-xl -mr-1">{whole}</span>
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span> <span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
<span className="text-xs -mr-[1px]">{decimal}</span> <span className="text-xs -mr-[1px]">{decimal}</span>
</div> </div>
</div> </div>
</div> </div>
); );
} }
export default function Sidebar({ export default function Sidebar({
logTitle, logTitle,
logSubtitle, logSubtitle,
recentsTitle, recentsTitle,
events, events,
selectedEventId, selectedEventId,
setSelectedEventId, setSelectedEventId,
hoveredEventId, hoveredEventId,
setHoveredEventId, setHoveredEventId,
button1Name, button1Name,
button2Name, button2Name,
onButton2Click, onButton2Click,
onButton1Click, onButton1Click,
button1Disabled = false, button1Disabled = false,
}: SidebarProps) { }: SidebarProps) {
const eventsContainerRef = useRef<HTMLDivElement>(null); const eventsContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (selectedEventId && eventsContainerRef.current) { if (selectedEventId && eventsContainerRef.current) {
const selectedEventElement = eventsContainerRef.current.querySelector(`[data-event-id="${selectedEventId}"]`); const selectedEventElement = eventsContainerRef.current.querySelector(`[data-event-id="${selectedEventId}"]`);
if (selectedEventElement) { if (selectedEventElement) {
selectedEventElement.scrollIntoView({ selectedEventElement.scrollIntoView({
block: "center", block: "center",
behavior: "smooth", behavior: "smooth",
}); });
} }
} }
}, [selectedEventId]); }, [selectedEventId]);
return ( return (
<div className="flex flex-col h-full w-80 bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg"> <div className="flex flex-col h-full w-80 bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg">
<div className="py-6 flex flex-col h-full"> <div className="py-6 flex flex-col h-full">
<div className="px-6 pb-8 border-b border-neutral-200"> <div className="px-6 pb-8 border-b border-neutral-200">
<h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2> <h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2>
<p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p> <p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p>
<button <button
className={`mt-4 w-full py-2 px-4 rounded-lg transition-colors duration-200 font-medium className={`mt-4 w-full py-2 px-4 rounded-lg transition-colors duration-200 font-medium
${button1Disabled ${
? "bg-gray-300 text-gray-500 cursor-not-allowed" button1Disabled
: "bg-blue-600 hover:bg-blue-700 text-white" ? "bg-gray-300 text-gray-500 cursor-not-allowed"
}`} : "bg-blue-600 hover:bg-blue-700 text-white"
onClick={onButton1Click} }`}
type="button" onClick={onButton1Click}
type="button"
tabIndex={button1Disabled ? -1 : 0} tabIndex={button1Disabled ? -1 : 0}
aria-disabled={button1Disabled ? "true" : "false"} aria-disabled={button1Disabled ? "true" : "false"}
> >
{button1Name} {button1Name}
</button> </button>
<button <button
className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium" className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium"
onClick={onButton2Click} onClick={onButton2Click}
type="button" type="button"
> >
{button2Name} {button2Name}
</button> </button>
</div> </div>
<div className="px-6 pt-6"> <div className="px-6 pt-6">
<h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2> <h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2>
</div> </div>
<div className="flex-1 px-6 overflow-y-auto" ref={eventsContainerRef}> <div className="flex-1 px-6 overflow-y-auto" ref={eventsContainerRef}>
<div className="space-y-3"> <div className="space-y-3">
{events.map((event) => ( {events.map((event) => (
<button <button
key={event.id} key={event.id}
data-event-id={event.id} data-event-id={event.id}
className={`w-full border ${hoveredEventId === event.id ? "bg-neutral-100" : "bg-white"} ${ className={`w-full border ${
selectedEventId === event.id ? "border-neutral-800" : "border-neutral-200" selectedEventId === event.id
} rounded-lg p-4 flex items-center gap-4 hover:bg-neutral-100 transition-colors duration-150 shadow-sm text-left`} ? "border-neutral-500 border-2 shadow-md"
onClick={() => { : hoveredEventId === event.id
setSelectedEventId((prevEventId) => (prevEventId !== event.id ? event.id : "")); ? "bg-neutral-100 shadow-sm"
}} : "bg-white border-neutral-200 shadow-sm"
onMouseEnter={() => setHoveredEventId(event.id)} } rounded-lg p-4 flex items-center gap-4 hover:bg-neutral-100 transition-colors duration-150 text-left`}
onMouseLeave={() => setHoveredEventId("")} onClick={() => {
> setSelectedEventId((prevEventId) => (prevEventId !== event.id ? event.id : ""));
<div className="flex-1"> }}
<p className="text-sm font-medium text-neutral-800 line-clamp-1">{event.title}</p> onMouseEnter={() => setHoveredEventId(event.id)}
<p className="text-xs text-neutral-500 mt-1 line-clamp-1">{event.text2}</p> onMouseLeave={() => setHoveredEventId("")}
</div> >
{event.magnitude ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>} <div className="flex-1">
</button> <p className="text-sm font-medium text-neutral-800 line-clamp-1">{event.title}</p>
))} <p className="text-xs text-neutral-500 mt-1 line-clamp-1">{event.text2}</p>
</div> </div>
</div> {event.magnitude ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>}
</div> </button>
</div> ))}
); </div>
</div>
</div>
</div>
);
} }

View File

@ -3,8 +3,10 @@ interface GeologicalEvent {
date: Date; date: Date;
title: string; title: string;
magnitude?: number; magnitude?: number;
code?: string;
longitude: number; longitude: number;
latitude: number; latitude: number;
isFunctional?: boolean;
text1: string; text1: string;
text2: string; text2: string;
} }