Compare commits

..

5 Commits

Author SHA1 Message Date
bfdb665c41 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-06-01 23:07:59 +01:00
Emily Neighbour
fbf61f56dd cart button moved 2025-06-01 18:57:59 +01:00
IZZY
fd77ceae88 Observatory search page! 2025-06-01 17:17:25 +01:00
IZZY
19253672dc neatening 2025-06-01 16:47:31 +01:00
IZZY
daa50887d6 new and improved easter eggs (with sound effects!) 2025-06-01 15:46:17 +01:00
15 changed files with 862 additions and 551 deletions

BIN
public/earthquake.mp3 Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 72 KiB

BIN
public/learn1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

1
public/target.png Normal file
View File

@ -0,0 +1 @@
<svg width="96" height="96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><g transform="translate(-592 -312)"><path d="M667.6 343C677.844 359.098 673.098 380.453 657 390.697 640.902 400.942 619.547 396.196 609.303 380.097 599.058 363.999 603.804 342.644 619.903 332.4 631.22 325.198 645.683 325.198 657 332.4L657 330.054C639.609 319.813 617.209 325.609 606.968 343 596.727 360.391 602.523 382.791 619.914 393.032 637.305 403.273 659.705 397.477 669.946 380.086 676.685 368.642 676.685 354.444 669.946 343Z"/><path d="M652.035 342.307C641.415 334.811 626.729 337.345 619.233 347.965 611.737 358.585 614.271 373.271 624.891 380.767 635.511 388.263 650.197 385.729 657.693 375.109 663.436 366.972 663.436 356.102 657.693 347.965L656.257 349.4C662.957 359.224 660.424 372.62 650.6 379.321 640.776 386.021 627.38 383.488 620.679 373.664 613.979 363.839 616.512 350.443 626.336 343.743 633.654 338.752 643.282 338.752 650.6 343.743Z"/><path d="M638.5 353C639.338 353 640.171 353.125 640.973 353.369L642.534 351.809C637.176 349.576 631.023 352.108 628.789 357.466 626.556 362.824 629.089 368.977 634.446 371.211 639.804 373.444 645.958 370.911 648.191 365.554 649.27 362.966 649.27 360.054 648.191 357.466L646.631 359.027C647.997 363.518 645.463 368.267 640.972 369.632 636.48 370.998 631.732 368.464 630.366 363.973 629.001 359.482 631.534 354.733 636.026 353.368 636.828 353.124 637.662 353 638.5 353Z"/><path d="M679.924 329.617C679.769 329.243 679.404 329 679 329L671 329 671 321C671 320.448 670.552 320 670 320 669.735 320 669.48 320.106 669.293 320.293L660.293 329.293C660.105 329.48 660 329.735 660 330L660 338.586 637.793 360.793C637.396 361.177 637.385 361.81 637.768 362.207 638.152 362.604 638.785 362.615 639.182 362.232 639.191 362.224 639.199 362.215 639.207 362.207L661.414 340 670 340C670.265 340 670.52 339.895 670.707 339.707L679.707 330.707C679.993 330.421 680.079 329.991 679.924 329.617ZM662 330.414 668.983 323.431C668.992 323.422 669 323.425 669 323.438L669 329.586 662 336.586ZM669.586 338 663.414 338 670.414 331 676.562 331C676.575 331 676.578 331.008 676.569 331.017Z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/target1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/team1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -248,7 +248,7 @@ const ContactUs = () => {
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
<span className="sr-only">Instagram</span> <span className="sr-only">Instagram</span>
<Image height={200} width={200} alt="Logo" className="z-10" src="/insta.webp" /> <Image height={200} width={200} alt="Logo" className="z-10" src="/instagram.png" />
</a> </a>
{/* Facebook: Pulsating Map */} {/* Facebook: Pulsating Map */}
<a <a

View File

@ -38,7 +38,7 @@ body {
@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;
} }
@ -228,3 +228,106 @@ body {
opacity: 0; opacity: 0;
transition: transform 1.1s cubic-bezier(0.65, 0.05, 0.45, 1), opacity 0.6s; transition: transform 1.1s cubic-bezier(0.65, 0.05, 0.45, 1), opacity 0.6s;
} }
/* --- X Logo Crack-and-Spin Easter Egg --- */
.x-logo.cracked {
/* Overlay cracks with a clip-path or filters for effect (simple grayscale/contrast for demo) */
filter: grayscale(1) brightness(0.6) contrast(2);
/* Slight shake + scale for realism */
transform: scale(1.04);
transition: filter 0.2s, transform 0.2s;
}
.x-logo.spin {
animation: xspin180 0.8s cubic-bezier(0.77, 0, 0.18, 1) forwards;
}
@keyframes xspin180 {
0% {
transform: rotate(0deg) scale(1.04);
}
80% {
transform: rotate(200deg) scale(1.04);
}
100% {
transform: rotate(180deg) scale(1.04);
}
}
.x-logo.cracked {
filter: grayscale(1) brightness(0.7) contrast(2.6);
transition: filter 0.2s, transform 0.2s;
}
.x-logo.spin {
animation: xspin180 0.8s cubic-bezier(0.77, 0, 0.18, 1) forwards;
}
.x-logo.nospin {
transform: rotate(0deg) scale(1.0);
filter: none;
transition: none;
}
.x-logo-cracks {
pointer-events: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 2;
}
.x-logo-cracks img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
opacity: 0.55;
pointer-events: none;
}
.footer-x-logo-wrap {
position: relative;
display: inline-block;
width: 28px;
height: 28px;
}
/* === X Logo Full Page Crack-and-Spin Easter Egg === */
.body-cracked .crack-overlay,
.body-cracked .crack1,
.body-cracked .crack2 {
pointer-events: none;
}
.body-cracked {
/* Animate the whole page spin with a transform on html/body! */
animation: bodyspin180 0.8s cubic-bezier(0.77, 0, 0.18, 1) forwards;
will-change: transform;
}
.body-spin-back {
/* Immediately resets the transform, no animation needed */
animation: none !important;
transform: rotate(0deg) !important;
}
@keyframes bodyspin180 {
0% {
transform: rotate(0deg);
}
80% {
transform: rotate(200deg);
}
100% {
transform: rotate(180deg);
}
}

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import BottomFooter from "@components/BottomFooter"; import BottomFooter from "@components/BottomFooter";
export default function LearnPage() { export default function LearnPage() {
return ( return (
<div <div
@ -9,17 +8,12 @@ export default function LearnPage() {
backgroundImage: "url('/earthquakeRelief.jpg')", // adjust path as needed backgroundImage: "url('/earthquakeRelief.jpg')", // adjust path as needed
}} }}
> >
{/* Overlay for readability */}
<div className="absolute inset-0 bg-black bg-opacity-50"></div> <div className="absolute inset-0 bg-black bg-opacity-50"></div>
<main className="flex-1 flex flex-col items-center justify-start pt-16 px-4 relative z-20"> <main className="flex-1 flex flex-col items-center justify-start pt-16 px-4 relative z-20">
{/* Title & subtitle OVER the background (not in the content box) */} <h1 className="text-4xl font-bold mb-4 text-red-700 text-center drop-shadow-lg">Earthquakes</h1>
<h1 className="text-4xl font-bold mb-4 text-white text-center drop-shadow-lg">Earthquakes</h1>
<p className="max-w-2xl text-lg text-white mb-8 text-center font-bold drop-shadow"> <p className="max-w-2xl text-lg text-white mb-8 text-center font-bold drop-shadow">
In this page, you can learn all about what earthquakes are, and how best to keep safe! In this page, you can learn all about what earthquakes are, and how best to keep safe!
</p> </p>
{/* Content box: all following info INSIDE */}
<div className="max-w-4xl w-full bg-white bg-opacity-90 rounded-xl shadow-2xl mx-auto mb-12 p-8 md:p-10"> <div className="max-w-4xl w-full bg-white bg-opacity-90 rounded-xl shadow-2xl mx-auto mb-12 p-8 md:p-10">
{/* Section 1 */} {/* Section 1 */}
<section className="mb-8"> <section className="mb-8">
@ -36,7 +30,25 @@ export default function LearnPage() {
<p> <p>
<span className="font-bold text-black md:text-xl">What are the types of earthquakes?</span> <span className="font-bold text-black md:text-xl">What are the types of earthquakes?</span>
<br /> <br />
Regions near plate boundaries, such as around the Pacific Ocean ("The Ring of Fire"), experience the most activity. There are four main types of earthquakes, each caused by different processes:
</p>
<ul className="list-disc list-inside pl-6 mt-3 space-y-2 text-gray-700">
<li>
<span className="font-bold">Tectonic Earthquakes:</span> These are the most common type and occur when rocks in the Earths crust break or slip along faults, usually due to movement between tectonic plates. The released energy creates seismic waves that shake the ground. Tectonic earthquakes are responsible for the vast majority of damaging quakes.
</li>
<li>
<span className="font-bold">Volcanic Earthquakes:</span> These happen in areas with active volcanoes. They're triggered by magma moving underground, which causes fractured rock and sudden releases of pressure. Volcanic earthquakes often occur before or during eruptions, and are usually smaller than tectonic earthquakes but can signal volcanic hazards.
</li>
<li>
<span className="font-bold">Collapse Earthquakes:</span> These occur when underground caves or mines collapse, causing small, local tremors. The shaking is usually minor and doesnt travel far. Collapse earthquakes are common in regions with extensive mining or karst landscapes (with many caves).
</li>
<li>
<span className="font-bold">Explosion Earthquakes:</span> These are caused by human activities like mining blasts, quarry explosions, or underground nuclear tests. The energy source is artificial (not natural), and the resulting seismic waves can be measured, but these events are usually small and localized.
</li>
</ul>
<p className="mt-4">
<strong>Whats the difference?</strong><br/>
<b>Tectonic</b> and <b>volcanic</b> earthquakes both come from natural Earth movementstectonic are about plate motion, while volcanic are caused by magma activity. <b>Collapse</b> earthquakes arise from ground suddenly falling in on itself, and <b>explosion</b> earthquakes are triggered by human-made blasts. The main difference lies in their cause, size, and frequency: tectonic are most common and strongest, volcanic tend to be local, collapse are small and specific to certain areas, and explosions are human-made.
</p> </p>
</section> </section>
{/* Section 3 */} {/* Section 3 */}
@ -44,13 +56,11 @@ export default function LearnPage() {
<p> <p>
<span className="font-bold text-black md:text-xl">How can I be prepared?</span> <span className="font-bold text-black md:text-xl">How can I be prepared?</span>
</p> </p>
{/* MAIN BULLET POINTS */}
<ul className="list-disc list-inside pl-6 space-y-2"> <ul className="list-disc list-inside pl-6 space-y-2">
<li> <li>
<span className="font-bold text-gray-800 ">Assemble an emergency kit:</span> <span className="font-bold text-gray-800 ">Assemble an emergency kit:</span>
This should be stored in your earthquake emergency zone. It may be useful, as in an earthquake, you may lose This should be stored in your earthquake emergency zone. It may be useful, as in an earthquake, you may lose
electricity or water supplies. electricity or water supplies.
{/* SUB BULLETS */}
<ul className="list-disc list-inside pl-6 mt-1 space-y-1 text-gray-700"> <ul className="list-disc list-inside pl-6 mt-1 space-y-1 text-gray-700">
<li>First aid kit and emergency medication</li> <li>First aid kit and emergency medication</li>
<li>Food (non-perishable)</li> <li>Food (non-perishable)</li>
@ -63,7 +73,6 @@ export default function LearnPage() {
<li> <li>
<span className="font-bold text-gray-800">Practice the Drop, Cover, and Hold On drill!</span> <span className="font-bold text-gray-800">Practice the Drop, Cover, and Hold On drill!</span>
This helps you protect yourself from falling objects during an earthquake. This helps you protect yourself from falling objects during an earthquake.
{/* Embed YouTube video */}
<div className="mt-2 flex justify-center"> <div className="mt-2 flex justify-center">
<iframe <iframe
width="350" width="350"
@ -87,6 +96,23 @@ export default function LearnPage() {
</li> </li>
</ul> </ul>
</section> </section>
{/* Q&A SECTION */}
<section className="mt-12 pt-8 border-t border-gray-300">
<h2 className="font-bold text-black text-xl mb-4">How does Tremor Tracker help?</h2>
<div className="mb-6">
<p className="font-semibold">How do we log earthquakes?</p>
<p>
Our Scientists record earthquakes using instruments called <strong>seismometers</strong>, which detect and measure the vibrations in the ground. When an earthquake occurs, the seismometer produces a trace known as a <strong>seismogram</strong>, showing the strength and duration of the shaking. Information from seismometers around the world is sent to data centers, where experts analyze it to pinpoint the earthquakes location, type, depth, and magnitude. This process is called logging or recording earthquakes, and it helps track seismic activity globally.
</p>
</div>
<div>
<p className="font-semibold">What are observatories?</p>
<p>
An <strong>earthquake observatory</strong> is a specialised facility where scientists monitor and study seismic activity. Observatories collect important data about the strength, location, and timing of each earthquake that can be shared with the general public. Scientists at the observatory use this data to better understand how and why earthquakes occur, track earthquake patterns, and issue warnings if a major quake is detected. The information gathered also helps in designing safer buildings and improving emergency response plans.
</p>
</div>
</section>
</div> </div>
</main> </main>
<BottomFooter /> <BottomFooter />

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

@ -3,11 +3,9 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { TbHexagon } from "react-icons/tb"; import { TbHexagon } from "react-icons/tb";
import useSWR from "swr"; import useSWR from "swr";
import BottomFooter from "@components/BottomFooter"; import BottomFooter from "@components/BottomFooter";
import { createPoster } from "@utils/axiosHelpers"; import { createPoster } from "@utils/axiosHelpers";
import getMagnitudeColor from "@utils/getMagnitudeColour"; import getMagnitudeColor from "@utils/getMagnitudeColour";
// formats the date // formats the date
function getRelativeDate(dateString: string): string { function getRelativeDate(dateString: string): string {
const date = new Date(dateString); const date = new Date(dateString);
@ -18,7 +16,6 @@ function getRelativeDate(dateString: string): string {
if (diffDays === 1) return "yesterday"; if (diffDays === 1) return "yesterday";
return date.toLocaleDateString(); return date.toLocaleDateString();
} }
// copied from sidebar // copied from sidebar
function MagnitudeNumber({ magnitude }: { magnitude: number }) { function MagnitudeNumber({ magnitude }: { magnitude: number }) {
const magnitudeStr = magnitude.toFixed(1); const magnitudeStr = magnitude.toFixed(1);
@ -36,12 +33,10 @@ function MagnitudeNumber({ magnitude }: { magnitude: number }) {
</div> </div>
); );
} }
export default function Home() { export default function Home() {
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 6 })); const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 6 }));
// Take 5 most recent // Take 5 most recent
const recents = (data?.earthquakes ?? []).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 5); const recents = (data?.earthquakes ?? []).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 5);
return ( return (
<main className="min-h-screen text-black"> <main className="min-h-screen text-black">
<div className="w-full relative"> <div className="w-full relative">
@ -53,7 +48,18 @@ export default function Home() {
<Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png" /> <Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png" />
</div> </div>
</div> </div>
<p className="mt-2"></p>
{/* ===== New Welcome Section ===== */}
<section className="w-full flex flex-col items-center mt-8 mb-3">
<h1 className="text-4xl md:text-5xl font-sans font-bold text-black drop-shadow-lg mb-2 tracking-tight">
Welcome to Tremor Tracker
</h1>
<p className="text-lg md:text-xl font-sans text-black w-5/6 md:w-4/6 mx-auto drop-shadow-md text-center">
TremorTracker is a non-profit website and research company, that aims to provide seismic education and aid preparation
</p>
</section>
{/* ===== Icons Section ===== */}
<div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2"> <div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2">
<Link href="/earthquakes" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"> <Link href="/earthquakes" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/earthquake.png" alt="Education Icon" className="h-40 w-40 mb-4" /> <Image height={100} width={100} src="/earthquake.png" alt="Education Icon" className="h-40 w-40 mb-4" />
@ -81,61 +87,12 @@ export default function Home() {
</Link> </Link>
</div> </div>
<p className="mt-18"></p> <p className="mt-18"></p>
<section className="min-h-screen text-black">
<div className="w-full relative z-40"> {/* === REMOVED Middle Info/Image Section === */}
<div> {/* <section className="min-h-screen text-black">
<Image height={2500} width={2000} alt="Background Image" src="/earthquakesMap.jpg" /> ...entire earthquakesMap section removed...
</div> </section> */}
<div className="border absolute top-0 inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40">
<section className="relative z-10 flex flex-col items-center text-center w-full px-4 py-14 mt-6">
<h1 className="text-4xl md:text-5xl font-sans font-bold text-white drop-shadow-lg mb-4 tracking-tight z-10">
Welcome to Tremor Tracker
</h1>
<p className="text-lg md:text-xl font-sans text-white w-4/6 mx-auto drop-shadow-md z-10">
TremorTracker is a non-profit website and research company, that aims to provide seismic education and aid
preparation
</p>
<p className="mt-10"></p>
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">What is an earthquake?</p>
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
An earthquake is a sudden shaking of the Earths surface, triggered by a rapid release of energy deep underground.
This usually happens because the Earths outer shell, called the crust, is made up of large pieces known as
tectonic plates. These plates are always moving, but sometimes they get stuck at their edges; when stress builds
up and is finally released, it causes the ground to shakean earthquake. Earthquakes can vary greatly in sizefrom
barely noticeable tremors to powerful quakes capable of causing widespread destruction. There are several types:
Tectonic, Volcanic and Collapse earthquakes. Understanding why and how earthquakes happen helps scientists predict
where they are most likely to occur and how to lessen their impact.
</p>
<p className="mt-10"></p>
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
How do we log earthquakes?
</p>
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
What information are we interested in?
</p>
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
Scientists record earthquakes using special instruments called seismometers, which detect and measure the
vibrations in the ground. When an earthquake occurs, the seismometer produces a trace known as a seismogram,
showing the strength and duration of the shaking. Information from seismometers around the world is sent to data
centers, where experts analyze it to pinpoint the earthquakes location, type, depth, and magnitude. This process
is called logging or recording earthquakes, and it helps track seismic activity globally.
</p>
<p className="mt-10"></p>
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
What are observatories?
</p>
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
An earthquake observatory is a specialized facility where scientists monitor and study seismic activity. These
observatories are equipped with sensitive instruments that can detect and record even the smallest tremors deep
within the Earth. Observatories collect important data about the strength, location, and timing of each earthquake
that can be shared with the general public. Scientists at the observatory use this data to better understand how
and why earthquakes occur, track earthquake patterns, and issue warnings if a major quake is detected. The
information gathered also helps in designing safer buildings and improving emergency response plans.
</p>
</section>
</div>
</div>
</section>
<p className="mt-20"></p> <p className="mt-20"></p>
<section className="relative z-10 flex flex-col items-start text-left w-5/6 mx-auto px-2 -mt-5 mb-2"> <section className="relative z-10 flex flex-col items-start text-left w-5/6 mx-auto px-2 -mt-5 mb-2">
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">Recent Earthquake Events</h1> <h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">Recent Earthquake Events</h1>
@ -187,21 +144,21 @@ export default function Home() {
</p> </p>
</Link> </Link>
<Link href="/our-mission" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"> <Link href="/our-mission" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/mission.jpg" alt="Our Mission Icon" className="h-20 w-20 mb-4" /> <Image height={150} width={150} src="/target1.png" alt="Our Mission Icon" className="mb-4" />
<h3 className="text-xl font-bold text-black mb-4">Our Mission</h3> <h3 className="text-xl font-bold text-black mb-4">Our Mission</h3>
<p className="text-md text-black text-center max-w-xs opacity-90"> <p className="text-md text-black text-center max-w-xs opacity-90">
Find out more about our purpose and the features we offer. Find out more about our purpose and the features we offer.
</p> </p>
</Link> </Link>
<Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"> <Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/team.jpg" alt="Team Icon" className="h-20 w-20 mb-4" /> <Image height={250} width={250} src="/team1.png" alt="Team Icon" className="h-20 w-20 mb-4 relative" />
<h3 className="text-xl font-bold text-black mb-4">Meet the Team</h3> <h3 className="text-xl font-bold text-black mb-4">Meet the Team</h3>
<p className="text-md text-black text-center max-w-xs opacity-90"> <p className="text-md text-black text-center max-w-xs opacity-90">
Learn about our team leads and their responsibilities. Learn about our team leads and their responsibilities.
</p> </p>
</Link> </Link>
<Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"> <Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/learn.jpg" alt="Learn Icon" className="h-20 w-20 mb-4" /> <Image height={250} width={250} src="/learn1.png" alt="Learn Icon" className="h-20 w-20 mb-4" />
<h3 className="text-xl font-bold text-black mb-4">Learn</h3> <h3 className="text-xl font-bold text-black mb-4">Learn</h3>
<p className="text-md text-black text-center max-w-xs opacity-90"> <p className="text-md text-black text-center max-w-xs opacity-90">
Find out more about earthquakes, what causes them and how to prepare. Find out more about earthquakes, what causes them and how to prepare.

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import Image from "next/image"; import Image from 'next/image';
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { ExtendedArtefact } from "@appTypes/ApiTypes"; import { ExtendedArtefact } from '@appTypes/ApiTypes';
import { Currency } from "@appTypes/StoreModel"; import { Currency } from '@appTypes/StoreModel';
import BottomFooter from "@components/BottomFooter"; import BottomFooter from '@components/BottomFooter';
import { useStoreState } from "@hooks/store"; import { useStoreState } from '@hooks/store';
interface SuperExtendedArtefact extends ExtendedArtefact { interface SuperExtendedArtefact extends ExtendedArtefact {
location: string; location: string;
@ -310,6 +310,27 @@ export default function Shop() {
}} }}
> >
<div className="absolute inset-0 bg-black bg-opacity-0 z-0"></div> <div className="absolute inset-0 bg-black bg-opacity-0 z-0"></div>
<button
className="absolute top-6 left-6 z-40 bg-white border border-blue-500 shadow-lg rounded-full p-3 hover:bg-blue-100 flex flex-row items-center"
onClick={() => setShowCartModal(true)}
aria-label="Open your cart"
>
<span className="mr-2 font-bold">{cart.length || ""}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-7 h-7 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13l-1.35 2.7a1 1 0 00.9 1.45h12.2M7 13l1.2-2.4M3 3l.01 0"
/>
</svg>
</button>
<div className="relative z-10 flex flex-col items-center w-full px-2 py-12"> <div className="relative z-10 flex flex-col items-center w-full px-2 py-12">
<h1 className="text-4xl md:text-4xl font-bold text-center text-blue-300 mb-2 tracking-tight drop-shadow-lg"> <h1 className="text-4xl md:text-4xl font-bold text-center text-blue-300 mb-2 tracking-tight drop-shadow-lg">
Artefact Shop Artefact Shop

View File

@ -2,29 +2,37 @@ import React, { useCallback, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
export default function BottomFooter() { export default function BottomFooter() {
// ig easter egg // Instagram Lava Flood Easter Egg
const [lavaActive, setLavaActive] = useState(false); const [lavaActive, setLavaActive] = useState(false);
const lavaTimeout = useRef<any>(null); const lavaTimeout = useRef<any>(null);
// LinkedIn easter egg // Instagram sound
const earthquakeAudio = useRef<HTMLAudioElement | null>(null);
// LinkedIn Shake Easter Egg
const [shaking, setShaking] = useState(false); const [shaking, setShaking] = useState(false);
const shakeTimeout = useRef<any>(null); const shakeTimeout = useRef<any>(null);
// x easter egg // X Logo Full-Page Crack & Spin Easter Egg
const [showCracks, setShowCracks] = useState(false); const [showCracks, setShowCracks] = useState(false);
const [collapse, setCollapse] = useState(false); const [crackOverlayActive, setCrackOverlayActive] = useState(false);
const crackTimeout = useRef<any>(null);
// Lava flood handler (top-down flood) // Lava flood & sound handler
const handleInstagramClick = useCallback((e: React.MouseEvent) => { const handleInstagramClick = useCallback((e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
setLavaActive(true); setLavaActive(true);
// Play earthquake sound
if (earthquakeAudio.current) {
earthquakeAudio.current.currentTime = 0;
earthquakeAudio.current.play();
}
clearTimeout(lavaTimeout.current); clearTimeout(lavaTimeout.current);
lavaTimeout.current = setTimeout(() => setLavaActive(false), 2000); lavaTimeout.current = setTimeout(() => setLavaActive(false), 2000);
}, []); }, []);
// LinkedIn shake handler // LinkedIn shake handler
const handleLinkedInClick = useCallback((e: React.MouseEvent) => { const handleLinkedInClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
if (shaking) return; // prevent stacking if (shaking) return; // prevent stacking
setShaking(true); setShaking(true);
@ -36,45 +44,58 @@ export default function BottomFooter() {
setShaking(false); setShaking(false);
body.classList.remove("shake-screen"); body.classList.remove("shake-screen");
}, 1000); }, 1000);
}, [shaking]); },
[shaking]
);
// X (crack and collapse) handler // X click = crack, spin page, then reset after 2s
const handleXClick = useCallback((e: React.MouseEvent) => { const handleXClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
if (crackOverlayActive) return;
setShowCracks(true); setShowCracks(true);
crackTimeout.current = setTimeout(() => { setCrackOverlayActive(true);
setCollapse(true); document.body.classList.remove("body-spin-back");
document.body.classList.add("body-cracked");
setTimeout(() => {
document.body.classList.remove("body-cracked");
document.body.classList.add("body-spin-back");
setTimeout(() => { setTimeout(() => {
setShowCracks(false); setShowCracks(false);
setCollapse(false); setCrackOverlayActive(false);
}, 1500); document.body.classList.remove("body-spin-back");
}, 1000); }, 200);
}, []); }, 2000);
},
[crackOverlayActive]
);
// Clean up classes and timeouts on unmount
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
clearTimeout(lavaTimeout.current); clearTimeout(lavaTimeout.current);
clearTimeout(shakeTimeout.current); clearTimeout(shakeTimeout.current);
clearTimeout(crackTimeout.current);
document.body.classList.remove("shake-screen"); document.body.classList.remove("shake-screen");
document.body.classList.remove("body-cracked");
document.body.classList.remove("body-spin-back");
}; };
}, []); }, []);
return ( return (
<> <>
{/* Hidden audio element */}
<audio ref={earthquakeAudio} src="/earthquake.mp3" preload="auto" />
{/* Lava Flood Overlay */} {/* Lava Flood Overlay */}
{lavaActive && ( {lavaActive && (
<div className="lava-flood-overlay lava-active"> <div className="lava-flood-overlay lava-active">
<img src="/lava.jpg" alt="Lava flood" draggable={false} /> <img src="/lava.jpg" alt="Lava flood" draggable={false} />
</div> </div>
)} )}
{/* Crack overlay for the spinning effect */}
{/* Crack & Collapse Overlay */} {showCracks && (
{(showCracks || collapse) && ( <div className="crack-overlay" style={{ pointerEvents: "none" }}>
<div className={`crack-overlay${collapse ? " crack-collapse" : ""}`}>
<img className="crack crack1" src="/crack1.png" alt="" /> <img className="crack crack1" src="/crack1.png" alt="" />
<img className="crack crack2" src="/crack2.png" alt="" /> <img className="crack crack2" src="/crack2.png" alt="" />
{/* Add more cracks for extra effect if you wish */}
</div> </div>
)} )}
@ -113,7 +134,6 @@ export default function BottomFooter() {
</li> </li>
</ul> </ul>
</div> </div>
{/* Donate Section */} {/* Donate Section */}
<div className="min-w-[220px] mb-8 md:mb-0 flex-1"> <div className="min-w-[220px] mb-8 md:mb-0 flex-1">
<h3 className="font-bold underline text-lg mb-3">Donate</h3> <h3 className="font-bold underline text-lg mb-3">Donate</h3>
@ -128,7 +148,6 @@ export default function BottomFooter() {
</Link> </Link>
</div> </div>
</div> </div>
{/* Bottom bar */} {/* Bottom bar */}
<div className="max-w-6xl mx-auto mt-8 pt-6 flex flex-col md:flex-row items-center justify-between border-t border-gray-200/30"> <div className="max-w-6xl mx-auto mt-8 pt-6 flex flex-col md:flex-row items-center justify-between border-t border-gray-200/30">
<div className="flex flex-row items-center w-full md:w-auto"> <div className="flex flex-row items-center w-full md:w-auto">
@ -167,7 +186,14 @@ export default function BottomFooter() {
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
aria-label="X Crack Easter egg" aria-label="X Crack Easter egg"
> >
<img src="x_logo.jpg" alt="X" className="h-7 w-7 rounded-full shadow" /> <span className="footer-x-logo-wrap">
<img
src="x_logo.jpg"
alt="X"
className="h-7 w-7 rounded-full shadow"
style={{ display: "block"}}
/>
</span>
</a> </a>
</div> </div>
</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;