Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker
BIN
public/earthquake.mp3
Normal file
BIN
public/lava.jpg
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 72 KiB |
BIN
public/learn1.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 2.4 KiB |
1
public/target.png
Normal 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
|
After Width: | Height: | Size: 18 KiB |
BIN
public/team1.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
@ -248,7 +248,7 @@ const ContactUs = () => {
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<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>
|
||||
{/* Facebook: Pulsating Map */}
|
||||
<a
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
@ -15,216 +15,319 @@
|
||||
} */
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
/* Increase specificity and use !important where necessary */
|
||||
.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 */
|
||||
.mapboxgl-popup .mapboxgl-popup-tip {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Child elements */
|
||||
.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 {
|
||||
@apply text-xs text-neutral-600 !important;
|
||||
@apply text-xs text-neutral-600 !important;
|
||||
}
|
||||
|
||||
.mapboxgl-popup-content p + p {
|
||||
@apply text-neutral-500 !important;
|
||||
.mapboxgl-popup-content p+p {
|
||||
@apply text-neutral-500 !important;
|
||||
}
|
||||
|
||||
.icon-link {
|
||||
/* default styles if needed */
|
||||
/* default styles if needed */
|
||||
}
|
||||
|
||||
.icon-link:hover,
|
||||
.icon-link:focus {
|
||||
background-color: #16424b;
|
||||
background-color: #16424b;
|
||||
}
|
||||
|
||||
.icon-link:hover h3,
|
||||
.icon-link:focus h3,
|
||||
.icon-link:hover p,
|
||||
.icon-link:focus p {
|
||||
color: #fff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.icon-link:hover h3,
|
||||
.icon-link:hover p,
|
||||
.icon-link:focus h3,
|
||||
.icon-link:focus p {
|
||||
color: #111;
|
||||
/* or black */
|
||||
color: #111;
|
||||
/* or black */
|
||||
}
|
||||
|
||||
/* ---- LAVA FLOOD OVERLAY ---- */
|
||||
.lava-flood-overlay {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: -100vh;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
transition: top 0.9s cubic-bezier(0.6, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: -100vh;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
transition: top 0.9s cubic-bezier(0.6, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.lava-flood-overlay.lava-active {
|
||||
top: 0;
|
||||
transition: top 0.33s cubic-bezier(0.6, 0, 0.2, 1);
|
||||
top: 0;
|
||||
transition: top 0.33s cubic-bezier(0.6, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.lava-flood-overlay img,
|
||||
.lava-gradient {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
filter: brightness(1.15) saturate(1.8) drop-shadow(0 0 80px #ff5500);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
filter: brightness(1.15) saturate(1.8) drop-shadow(0 0 80px #ff5500);
|
||||
}
|
||||
|
||||
/* ---- INSANE SCREEN SHAKE (LinkedIn) ---- */
|
||||
@keyframes supershake {
|
||||
0% {
|
||||
transform: translate(0, 0) rotate(0);
|
||||
}
|
||||
0% {
|
||||
transform: translate(0, 0) rotate(0);
|
||||
}
|
||||
|
||||
5% {
|
||||
transform: translate(-20px, 5px) rotate(-2deg);
|
||||
}
|
||||
5% {
|
||||
transform: translate(-20px, 5px) rotate(-2deg);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: translate(18px, -8px) rotate(2deg);
|
||||
}
|
||||
10% {
|
||||
transform: translate(18px, -8px) rotate(2deg);
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: translate(-22px, 8px) rotate(-4deg);
|
||||
}
|
||||
15% {
|
||||
transform: translate(-22px, 8px) rotate(-4deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(22px, -2px) rotate(4deg);
|
||||
}
|
||||
20% {
|
||||
transform: translate(22px, -2px) rotate(4deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translate(-18px, 12px) rotate(-2deg);
|
||||
}
|
||||
25% {
|
||||
transform: translate(-18px, 12px) rotate(-2deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translate(18px, -10px) rotate(2deg);
|
||||
}
|
||||
30% {
|
||||
transform: translate(18px, -10px) rotate(2deg);
|
||||
}
|
||||
|
||||
35% {
|
||||
transform: translate(-22px, 14px) rotate(-4deg);
|
||||
}
|
||||
35% {
|
||||
transform: translate(-22px, 14px) rotate(-4deg);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translate(22px, -12px) rotate(4deg);
|
||||
}
|
||||
40% {
|
||||
transform: translate(22px, -12px) rotate(4deg);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: translate(-18px, 8px) rotate(-2deg);
|
||||
}
|
||||
45% {
|
||||
transform: translate(-18px, 8px) rotate(-2deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(18px, -14px) rotate(4deg);
|
||||
}
|
||||
50% {
|
||||
transform: translate(18px, -14px) rotate(4deg);
|
||||
}
|
||||
|
||||
55% {
|
||||
transform: translate(-22px, 12px) rotate(-4deg);
|
||||
}
|
||||
55% {
|
||||
transform: translate(-22px, 12px) rotate(-4deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translate(22px, -8px) rotate(2deg);
|
||||
}
|
||||
60% {
|
||||
transform: translate(22px, -8px) rotate(2deg);
|
||||
}
|
||||
|
||||
65% {
|
||||
transform: translate(-18px, 10px) rotate(-2deg);
|
||||
}
|
||||
65% {
|
||||
transform: translate(-18px, 10px) rotate(-2deg);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translate(18px, -12px) rotate(2deg);
|
||||
}
|
||||
70% {
|
||||
transform: translate(18px, -12px) rotate(2deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translate(-22px, 14px) rotate(-4deg);
|
||||
}
|
||||
75% {
|
||||
transform: translate(-22px, 14px) rotate(-4deg);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translate(22px, -10px) rotate(4deg);
|
||||
}
|
||||
80% {
|
||||
transform: translate(22px, -10px) rotate(4deg);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: translate(-18px, 8px) rotate(-2deg);
|
||||
}
|
||||
85% {
|
||||
transform: translate(-18px, 8px) rotate(-2deg);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: translate(18px, -14px) rotate(2deg);
|
||||
}
|
||||
90% {
|
||||
transform: translate(18px, -14px) rotate(2deg);
|
||||
}
|
||||
|
||||
95% {
|
||||
transform: translate(-20px, 5px) rotate(-2deg);
|
||||
}
|
||||
95% {
|
||||
transform: translate(-20px, 5px) rotate(-2deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0, 0) rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0) rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.shake-screen {
|
||||
animation: supershake 1s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
animation: supershake 1s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
|
||||
/* ---- CRACK + COLLAPSE OVERLAY (X icon) ---- */
|
||||
.crack-overlay {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
transition: transform 1.1s cubic-bezier(0.65, 0.05, 0.45, 1), opacity 0.5s;
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
transition: transform 1.1s cubic-bezier(0.65, 0.05, 0.45, 1), opacity 0.5s;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.crack {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.crack1 {
|
||||
width: 35vw;
|
||||
left: 10vw;
|
||||
top: 22vh;
|
||||
opacity: 0.8;
|
||||
width: 35vw;
|
||||
left: 10vw;
|
||||
top: 22vh;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.crack2 {
|
||||
width: 32vw;
|
||||
right: 12vw;
|
||||
top: 42vh;
|
||||
opacity: 0.7;
|
||||
transform: rotate(-8deg);
|
||||
width: 32vw;
|
||||
right: 12vw;
|
||||
top: 42vh;
|
||||
opacity: 0.7;
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
|
||||
/* Add more .crackN classes if using more cracks */
|
||||
|
||||
/* Collapse falling effect */
|
||||
.crack-collapse {
|
||||
transform: perspective(900px) rotateX(75deg) translateY(80vh) scale(0.9);
|
||||
opacity: 0;
|
||||
transition: transform 1.1s cubic-bezier(0.65, 0.05, 0.45, 1), opacity 0.6s;
|
||||
transform: perspective(900px) rotateX(75deg) translateY(80vh) scale(0.9);
|
||||
opacity: 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,95 +1,121 @@
|
||||
"use client";
|
||||
import BottomFooter from "@components/BottomFooter";
|
||||
|
||||
export default function LearnPage() {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-fixed bg-cover bg-center flex flex-col relative"
|
||||
style={{
|
||||
backgroundImage: "url('/earthquakeRelief.jpg')", // adjust path as needed
|
||||
}}
|
||||
>
|
||||
{/* Overlay for readability */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-fixed bg-cover bg-center flex flex-col relative"
|
||||
style={{
|
||||
backgroundImage: "url('/earthquakeRelief.jpg')", // adjust path as needed
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<h1 className="text-4xl font-bold mb-4 text-red-700 text-center drop-shadow-lg">Earthquakes</h1>
|
||||
<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!
|
||||
</p>
|
||||
<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 className="mb-8">
|
||||
<p>
|
||||
<span className="font-bold text-black md:text-xl">What are earthquakes?</span>
|
||||
<br />
|
||||
Earthquakes are a shaking of the earth's surface caused by a sudden release of energy underground. They can range in
|
||||
size, from tiny trembles to large quakes, which can cause destruction and even tsunamis. Hundreds of earthquakes
|
||||
happen every day—but most are too small to feel.
|
||||
</p>
|
||||
</section>
|
||||
{/* Section 2 */}
|
||||
<section className="mb-8">
|
||||
<p>
|
||||
<span className="font-bold text-black md:text-xl">What are the types of earthquakes?</span>
|
||||
<br />
|
||||
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 Earth’s 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 doesn’t 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>What’s the difference?</strong><br/>
|
||||
<b>Tectonic</b> and <b>volcanic</b> earthquakes both come from natural Earth movements—tectonic 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>
|
||||
</section>
|
||||
{/* Section 3 */}
|
||||
<section>
|
||||
<p>
|
||||
<span className="font-bold text-black md:text-xl">How can I be prepared?</span>
|
||||
</p>
|
||||
<ul className="list-disc list-inside pl-6 space-y-2">
|
||||
<li>
|
||||
<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
|
||||
electricity or water supplies.
|
||||
<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>Food (non-perishable)</li>
|
||||
<li>Bottled water</li>
|
||||
<li>Torch</li>
|
||||
<li>Satellite phone</li>
|
||||
<li>Warm clothing and blankets</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<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.
|
||||
<div className="mt-2 flex justify-center">
|
||||
<iframe
|
||||
width="350"
|
||||
height="200"
|
||||
className="rounded shadow"
|
||||
src="https://www.youtube.com/embed/-MKMiFWK6Xk"
|
||||
title="Drop, Cover, and Hold On - Official Earthquake Drill Video"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold text-gray-800">Identify a safe zone:</span>
|
||||
This should be a sturdy place where all members of your household can shelter, such as under a strong table, in a
|
||||
structurally sound room, or your local community’s shared space.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold text-gray-800">Discuss what to do:</span>
|
||||
Share this information with your family and friends! Talk about what each person would do in an emergency.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<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-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">
|
||||
In this page, you can learn all about what earthquakes are, and how best to keep safe!
|
||||
</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">
|
||||
{/* Section 1 */}
|
||||
<section className="mb-8">
|
||||
<p>
|
||||
<span className="font-bold text-black md:text-xl">What are earthquakes?</span>
|
||||
<br />
|
||||
Earthquakes are a shaking of the earth's surface caused by a sudden release of energy underground. They can range in
|
||||
size, from tiny trembles to large quakes, which can cause destruction and even tsunamis. Hundreds of earthquakes
|
||||
happen every day—but most are too small to feel.
|
||||
</p>
|
||||
</section>
|
||||
{/* Section 2 */}
|
||||
<section className="mb-8">
|
||||
<p>
|
||||
<span className="font-bold text-black md:text-xl">What are the types of earthquakes?</span>
|
||||
<br />
|
||||
Regions near plate boundaries, such as around the Pacific Ocean ("The Ring of Fire"), experience the most activity.
|
||||
</p>
|
||||
</section>
|
||||
{/* Section 3 */}
|
||||
<section>
|
||||
<p>
|
||||
<span className="font-bold text-black md:text-xl">How can I be prepared?</span>
|
||||
</p>
|
||||
{/* MAIN BULLET POINTS */}
|
||||
<ul className="list-disc list-inside pl-6 space-y-2">
|
||||
<li>
|
||||
<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
|
||||
electricity or water supplies.
|
||||
{/* SUB BULLETS */}
|
||||
<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>Food (non-perishable)</li>
|
||||
<li>Bottled water</li>
|
||||
<li>Torch</li>
|
||||
<li>Satellite phone</li>
|
||||
<li>Warm clothing and blankets</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<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.
|
||||
{/* Embed YouTube video */}
|
||||
<div className="mt-2 flex justify-center">
|
||||
<iframe
|
||||
width="350"
|
||||
height="200"
|
||||
className="rounded shadow"
|
||||
src="https://www.youtube.com/embed/-MKMiFWK6Xk"
|
||||
title="Drop, Cover, and Hold On - Official Earthquake Drill Video"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold text-gray-800">Identify a safe zone:</span>
|
||||
This should be a sturdy place where all members of your household can shelter, such as under a strong table, in a
|
||||
structurally sound room, or your local community’s shared space.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold text-gray-800">Discuss what to do:</span>
|
||||
Share this information with your family and friends! Talk about what each person would do in an emergency.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<BottomFooter />
|
||||
</div>
|
||||
);
|
||||
{/* 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 earthquake’s 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>
|
||||
</main>
|
||||
<BottomFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,7 +3,8 @@ import { useState, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import Map from "@components/Map";
|
||||
import LogObservatoryModal from "@/components/LogObservatoryModal"; // Adjust if your path is different
|
||||
import LogObservatoryModal from "@/components/LogObservatoryModal";
|
||||
import SearchObservatoriesModal from "@/components/SearchObservatoriesModal"; // <-- add this import
|
||||
import { fetcher } from "@utils/axiosHelpers";
|
||||
import { Observatory } from "@prismaclient";
|
||||
import { getRelativeDate } from "@utils/formatters";
|
||||
@ -11,90 +12,96 @@ import GeologicalEvent from "@appTypes/GeologicalEvent";
|
||||
import { useStoreState } from "@hooks/store";
|
||||
|
||||
function NoAccessModal({ open, onClose }) {
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-xs w-full text-center relative">
|
||||
<button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
<h2 className="font-bold text-xl mb-4">No Access</h2>
|
||||
<p className="text-gray-600 mb-3">Sorry, You do not have access rights, please log in or contact an Admin.</p>
|
||||
<button onClick={onClose} className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-xs w-full text-center relative">
|
||||
<button onClick={onClose} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg" aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
<h2 className="font-bold text-xl mb-4">No Access</h2>
|
||||
<p className="text-gray-600 mb-3">Sorry, You do not have access rights, please log in or contact an Admin.</p>
|
||||
<button onClick={onClose} className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 mt-2">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Observatories() {
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
const [logModalOpen, setLogModalOpen] = useState(false);
|
||||
const [noAccessModalOpen, setNoAccessModalOpen] = useState(false);
|
||||
const [selectedEventId, setSelectedEventId] = useState("");
|
||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||
const [logModalOpen, setLogModalOpen] = useState(false);
|
||||
const [noAccessModalOpen, setNoAccessModalOpen] = useState(false);
|
||||
const [searchModalOpen, setSearchModalOpen] = useState(false); // <-- NEW STATE
|
||||
|
||||
const user = useStoreState((state) => state.user);
|
||||
const role: "GUEST" | "SCIENTIST" | "ADMIN" = user?.role ?? "GUEST";
|
||||
const canLogObservatory = role === "SCIENTIST" || role === "ADMIN";
|
||||
const user = useStoreState((state) => state.user);
|
||||
const role: "GUEST" | "SCIENTIST" | "ADMIN" = user?.role ?? "GUEST";
|
||||
const canLogObservatory = role === "SCIENTIST" || role === "ADMIN";
|
||||
const { data, error, isLoading, mutate } = useSWR("/api/observatories", fetcher);
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR("/api/observatories", fetcher);
|
||||
const observatoryEvents = useMemo(
|
||||
() =>
|
||||
data && data.observatories
|
||||
? data.observatories
|
||||
.map((x: Observatory): GeologicalEvent & { isFunctional: boolean } => ({
|
||||
id: x.id.toString(),
|
||||
title: ` ${x.name}`,
|
||||
longitude: x.longitude,
|
||||
latitude: x.latitude,
|
||||
isFunctional: !!x.isFunctional, // if isFunctional = 1/0, coerce to boolean
|
||||
text1: "",
|
||||
text2: getRelativeDate(x.dateEstablished),
|
||||
date: x.dateEstablished,
|
||||
}))
|
||||
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
: [],
|
||||
[data]
|
||||
);
|
||||
|
||||
const observatoryEvents = useMemo(
|
||||
() =>
|
||||
data && data.observatories
|
||||
? data.observatories
|
||||
.map((x: Observatory): GeologicalEvent & { isFunctional: boolean } => ({
|
||||
id: x.id.toString(),
|
||||
title: ` ${x.name}`,
|
||||
longitude: x.longitude,
|
||||
latitude: x.latitude,
|
||||
isFunctional: x.isFunctional, // <-- include this!
|
||||
text1: "",
|
||||
text2: getRelativeDate(x.dateEstablished),
|
||||
date: x.dateEstablished,
|
||||
}))
|
||||
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
: [],
|
||||
[data]
|
||||
);
|
||||
const handleLogClick = () => {
|
||||
if (canLogObservatory) {
|
||||
setLogModalOpen(true);
|
||||
} else {
|
||||
setNoAccessModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogClick = () => {
|
||||
if (canLogObservatory) {
|
||||
setLogModalOpen(true);
|
||||
} else {
|
||||
setNoAccessModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
|
||||
<div className="flex-grow h-full">
|
||||
<Map
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
mapType="observatories"
|
||||
/>
|
||||
</div>
|
||||
<Sidebar
|
||||
logTitle="Observatory Mapping"
|
||||
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
|
||||
recentsTitle="New Observatories"
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
button1Name="Log a New Observatory"
|
||||
button2Name="Search Observatories"
|
||||
onButton1Click={handleLogClick}
|
||||
button1Disabled={!canLogObservatory}
|
||||
/>
|
||||
<LogObservatoryModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} />
|
||||
<NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
|
||||
<div className="flex-grow h-full">
|
||||
<Map
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
mapType="observatories"
|
||||
/>
|
||||
</div>
|
||||
<Sidebar
|
||||
logTitle="Observatory Mapping"
|
||||
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
|
||||
recentsTitle="New Observatories"
|
||||
events={observatoryEvents}
|
||||
selectedEventId={selectedEventId}
|
||||
setSelectedEventId={setSelectedEventId}
|
||||
hoveredEventId={hoveredEventId}
|
||||
setHoveredEventId={setHoveredEventId}
|
||||
button1Name="Log a New Observatory"
|
||||
button2Name="Search Observatories"
|
||||
onButton1Click={handleLogClick}
|
||||
button1Disabled={!canLogObservatory}
|
||||
onButton2Click={() => setSearchModalOpen(true)} // <-- This line enables the search modal
|
||||
/>
|
||||
<LogObservatoryModal open={logModalOpen} onClose={() => setLogModalOpen(false)} onSuccess={() => mutate()} />
|
||||
<NoAccessModal open={noAccessModalOpen} onClose={() => setNoAccessModalOpen(false)} />
|
||||
<SearchObservatoriesModal
|
||||
open={searchModalOpen}
|
||||
onClose={() => setSearchModalOpen(false)}
|
||||
observatories={data?.observatories ?? []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
367
src/app/page.tsx
@ -3,220 +3,177 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { TbHexagon } from "react-icons/tb";
|
||||
import useSWR from "swr";
|
||||
|
||||
import BottomFooter from "@components/BottomFooter";
|
||||
import { createPoster } from "@utils/axiosHelpers";
|
||||
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
||||
|
||||
// formats the date
|
||||
function getRelativeDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
if (diffDays === 0) return "today";
|
||||
if (diffDays === 1) return "yesterday";
|
||||
return date.toLocaleDateString();
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
if (diffDays === 0) return "today";
|
||||
if (diffDays === 1) return "yesterday";
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
// copied from sidebar
|
||||
function MagnitudeNumber({ magnitude }: { magnitude: number }) {
|
||||
const magnitudeStr = magnitude.toFixed(1);
|
||||
const [whole, decimal] = magnitudeStr.split(".");
|
||||
return (
|
||||
<div className="relative" style={{ color: getMagnitudeColor(magnitude) }}>
|
||||
<TbHexagon size={40} className="drop-shadow-sm" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex items-baseline font-mono font-bold tracking-tight">
|
||||
<span className="text-xl -mr-1">{whole}</span>
|
||||
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
|
||||
<span className="text-xs -mr-[1px]">{decimal}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const magnitudeStr = magnitude.toFixed(1);
|
||||
const [whole, decimal] = magnitudeStr.split(".");
|
||||
return (
|
||||
<div className="relative" style={{ color: getMagnitudeColor(magnitude) }}>
|
||||
<TbHexagon size={40} className="drop-shadow-sm" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex items-baseline font-mono font-bold tracking-tight">
|
||||
<span className="text-xl -mr-1">{whole}</span>
|
||||
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
|
||||
<span className="text-xs -mr-[1px]">{decimal}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 6 }));
|
||||
// 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 { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 6 }));
|
||||
// Take 5 most recent
|
||||
const recents = (data?.earthquakes ?? []).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 5);
|
||||
return (
|
||||
<main className="min-h-screen text-black">
|
||||
<div className="w-full relative">
|
||||
<div>
|
||||
<Image height={2000} width={2000} alt="Background Image" src="/lava_flows.jpg" />
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40"></div>
|
||||
<div className="absolute inset-0 top-[30%]">
|
||||
<Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<main className="min-h-screen text-black">
|
||||
<div className="w-full relative">
|
||||
<div>
|
||||
<Image height={2000} width={2000} alt="Background Image" src="/lava_flows.jpg" />
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40"></div>
|
||||
<div className="absolute inset-0 top-[30%]">
|
||||
<Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2"></p>
|
||||
<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">
|
||||
<Image height={100} width={100} src="/earthquake.png" alt="Education Icon" className="h-40 w-40 mb-4" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Earthquakes</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Log new earthquakes with their required details or search past seismic events
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/observatories"
|
||||
className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"
|
||||
>
|
||||
<Image height={100} width={100} src="/observe.png" alt="Research Icon" className="h-40 w-40 mb-4" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Observatories</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Find recently active observatories, and newly opened/closed sites
|
||||
</p>
|
||||
</Link>
|
||||
<Link href="/shop" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||
<Image height={100} width={100} src="/artefact.png" alt="Aftefacts Icon" className="h-40 w-40 mb-4" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Artefacts</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
View or purchase recently discovered artefacts from seismic events
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-18"></p>
|
||||
<section className="min-h-screen text-black">
|
||||
<div className="w-full relative z-40">
|
||||
<div>
|
||||
<Image height={2500} width={2000} alt="Background Image" src="/earthquakesMap.jpg" />
|
||||
</div>
|
||||
<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 Earth’s surface, triggered by a rapid release of energy deep underground.
|
||||
This usually happens because the Earth’s 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 shake—an earthquake. Earthquakes can vary greatly in size—from
|
||||
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 earthquake’s 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>
|
||||
<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>
|
||||
<p className="text-lg md:text-xl text-black drop-shadow-md">
|
||||
Learn about the most recent earthquake events from around the world:
|
||||
</p>
|
||||
</section>
|
||||
<p className="mt-6"></p>
|
||||
<div className="mx-auto w-5/6 px-2">
|
||||
{error && (
|
||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||
<p>Failed to load earthquakes.</p>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && recents.length === 0 && (
|
||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||
<p>No earthquakes found.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
{recents.map((eq) => (
|
||||
<div key={eq.code} className="flex items-center justify-between p-4 bg-white rounded-xl shadow border">
|
||||
<div>
|
||||
<div className="font-semibold">Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}</div>
|
||||
<div className="text-sm text-gray-500">{getRelativeDate(eq.date)}</div>
|
||||
</div>
|
||||
<MagnitudeNumber magnitude={eq.magnitude} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">Find Out More!</h1>
|
||||
<p className="text-lg md:text-xl text-black drop-shadow-md">Explore more of our website...</p>
|
||||
</section>
|
||||
<p className="mt-2"></p>
|
||||
<div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2">
|
||||
<Link href="/contact-us" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||
<Image height={100} width={100} src="/contactUs.jpg" alt="Contact Us Icon" className="h-20 w-20 mb-4" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Contact us directly</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Visit our socials or leave us a message via phone or email.
|
||||
</p>
|
||||
</Link>
|
||||
<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" />
|
||||
<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">
|
||||
Find out more about our purpose and the features we offer.
|
||||
</p>
|
||||
</Link>
|
||||
<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" />
|
||||
<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">
|
||||
Learn about our team leads and their responsibilities.
|
||||
</p>
|
||||
</Link>
|
||||
<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" />
|
||||
<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">
|
||||
Find out more about earthquakes, what causes them and how to prepare.
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-10"></p>
|
||||
<section style={{ height: 500 }} className="text-black">
|
||||
<div className="w-full relative overflow-hidden z=10">
|
||||
<div className="flex justify-center">
|
||||
<Image height={400} width={800} alt="Background Image" src="/team.PNG" />
|
||||
</div>
|
||||
<BottomFooter />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
{/* ===== 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">
|
||||
<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" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Earthquakes</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Log new earthquakes with their required details or search past seismic events
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/observatories"
|
||||
className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"
|
||||
>
|
||||
<Image height={100} width={100} src="/observe.png" alt="Research Icon" className="h-40 w-40 mb-4" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Observatories</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Find recently active observatories, and newly opened/closed sites
|
||||
</p>
|
||||
</Link>
|
||||
<Link href="/shop" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||
<Image height={100} width={100} src="/artefact.png" alt="Aftefacts Icon" className="h-40 w-40 mb-4" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Artefacts</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
View or purchase recently discovered artefacts from seismic events
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-18"></p>
|
||||
|
||||
{/* === REMOVED Middle Info/Image Section === */}
|
||||
{/* <section className="min-h-screen text-black">
|
||||
...entire earthquakesMap section removed...
|
||||
</section> */}
|
||||
|
||||
<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">
|
||||
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">Recent Earthquake Events</h1>
|
||||
<p className="text-lg md:text-xl text-black drop-shadow-md">
|
||||
Learn about the most recent earthquake events from around the world:
|
||||
</p>
|
||||
</section>
|
||||
<p className="mt-6"></p>
|
||||
<div className="mx-auto w-5/6 px-2">
|
||||
{error && (
|
||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||
<p>Failed to load earthquakes.</p>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && recents.length === 0 && (
|
||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||
<p>No earthquakes found.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
{recents.map((eq) => (
|
||||
<div key={eq.code} className="flex items-center justify-between p-4 bg-white rounded-xl shadow border">
|
||||
<div>
|
||||
<div className="font-semibold">Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}</div>
|
||||
<div className="text-sm text-gray-500">{getRelativeDate(eq.date)}</div>
|
||||
</div>
|
||||
<MagnitudeNumber magnitude={eq.magnitude} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">Find Out More!</h1>
|
||||
<p className="text-lg md:text-xl text-black drop-shadow-md">Explore more of our website...</p>
|
||||
</section>
|
||||
<p className="mt-2"></p>
|
||||
<div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2">
|
||||
<Link href="/contact-us" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||
<Image height={100} width={100} src="/contactUs.jpg" alt="Contact Us Icon" className="h-20 w-20 mb-4" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Contact us directly</h3>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Visit our socials or leave us a message via phone or email.
|
||||
</p>
|
||||
</Link>
|
||||
<Link href="/our-mission" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||
<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>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Find out more about our purpose and the features we offer.
|
||||
</p>
|
||||
</Link>
|
||||
<Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||
<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>
|
||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||
Learn about our team leads and their responsibilities.
|
||||
</p>
|
||||
</Link>
|
||||
<Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||
<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>
|
||||
<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.
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-10"></p>
|
||||
<section style={{ height: 500 }} className="text-black">
|
||||
<div className="w-full relative overflow-hidden z=10">
|
||||
<div className="flex justify-center">
|
||||
<Image height={400} width={800} alt="Background Image" src="/team.PNG" />
|
||||
</div>
|
||||
<BottomFooter />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
|
||||
import Image from 'next/image';
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
||||
import { Currency } from "@appTypes/StoreModel";
|
||||
import BottomFooter from "@components/BottomFooter";
|
||||
import { useStoreState } from "@hooks/store";
|
||||
import { ExtendedArtefact } from '@appTypes/ApiTypes';
|
||||
import { Currency } from '@appTypes/StoreModel';
|
||||
import BottomFooter from '@components/BottomFooter';
|
||||
import { useStoreState } from '@hooks/store';
|
||||
|
||||
interface SuperExtendedArtefact extends ExtendedArtefact {
|
||||
location: string;
|
||||
@ -310,6 +310,27 @@ export default function Shop() {
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<h1 className="text-4xl md:text-4xl font-bold text-center text-blue-300 mb-2 tracking-tight drop-shadow-lg">
|
||||
Artefact Shop
|
||||
|
||||
@ -2,79 +2,100 @@ import React, { useCallback, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function BottomFooter() {
|
||||
// ig easter egg
|
||||
const [lavaActive, setLavaActive] = useState(false);
|
||||
// Instagram Lava Flood Easter Egg
|
||||
const [lavaActive, setLavaActive] = useState(false);
|
||||
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 shakeTimeout = useRef<any>(null);
|
||||
|
||||
// x easter egg
|
||||
// X Logo Full-Page Crack & Spin Easter Egg
|
||||
const [showCracks, setShowCracks] = useState(false);
|
||||
const [collapse, setCollapse] = useState(false);
|
||||
const crackTimeout = useRef<any>(null);
|
||||
const [crackOverlayActive, setCrackOverlayActive] = useState(false);
|
||||
|
||||
// Lava flood handler (top-down flood)
|
||||
// Lava flood & sound handler
|
||||
const handleInstagramClick = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setLavaActive(true);
|
||||
// Play earthquake sound
|
||||
if (earthquakeAudio.current) {
|
||||
earthquakeAudio.current.currentTime = 0;
|
||||
earthquakeAudio.current.play();
|
||||
}
|
||||
clearTimeout(lavaTimeout.current);
|
||||
lavaTimeout.current = setTimeout(() => setLavaActive(false), 2000);
|
||||
}, []);
|
||||
|
||||
// LinkedIn shake handler
|
||||
const handleLinkedInClick = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (shaking) return; // prevent stacking
|
||||
setShaking(true);
|
||||
const body = document.body;
|
||||
body.classList.remove("shake-screen");
|
||||
void body.offsetWidth;
|
||||
body.classList.add("shake-screen");
|
||||
shakeTimeout.current = setTimeout(() => {
|
||||
setShaking(false);
|
||||
const handleLinkedInClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (shaking) return; // prevent stacking
|
||||
setShaking(true);
|
||||
const body = document.body;
|
||||
body.classList.remove("shake-screen");
|
||||
}, 1000);
|
||||
}, [shaking]);
|
||||
void body.offsetWidth;
|
||||
body.classList.add("shake-screen");
|
||||
shakeTimeout.current = setTimeout(() => {
|
||||
setShaking(false);
|
||||
body.classList.remove("shake-screen");
|
||||
}, 1000);
|
||||
},
|
||||
[shaking]
|
||||
);
|
||||
|
||||
// X (crack and collapse) handler
|
||||
const handleXClick = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setShowCracks(true);
|
||||
crackTimeout.current = setTimeout(() => {
|
||||
setCollapse(true);
|
||||
// X click = crack, spin page, then reset after 2s
|
||||
const handleXClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (crackOverlayActive) return;
|
||||
setShowCracks(true);
|
||||
setCrackOverlayActive(true);
|
||||
document.body.classList.remove("body-spin-back");
|
||||
document.body.classList.add("body-cracked");
|
||||
setTimeout(() => {
|
||||
setShowCracks(false);
|
||||
setCollapse(false);
|
||||
}, 1500);
|
||||
}, 1000);
|
||||
}, []);
|
||||
document.body.classList.remove("body-cracked");
|
||||
document.body.classList.add("body-spin-back");
|
||||
setTimeout(() => {
|
||||
setShowCracks(false);
|
||||
setCrackOverlayActive(false);
|
||||
document.body.classList.remove("body-spin-back");
|
||||
}, 200);
|
||||
}, 2000);
|
||||
},
|
||||
[crackOverlayActive]
|
||||
);
|
||||
|
||||
// Clean up classes and timeouts on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(lavaTimeout.current);
|
||||
clearTimeout(shakeTimeout.current);
|
||||
clearTimeout(crackTimeout.current);
|
||||
document.body.classList.remove("shake-screen");
|
||||
document.body.classList.remove("body-cracked");
|
||||
document.body.classList.remove("body-spin-back");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hidden audio element */}
|
||||
<audio ref={earthquakeAudio} src="/earthquake.mp3" preload="auto" />
|
||||
{/* Lava Flood Overlay */}
|
||||
{lavaActive && (
|
||||
<div className="lava-flood-overlay lava-active">
|
||||
<img src="/lava.jpg" alt="Lava flood" draggable={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Crack & Collapse Overlay */}
|
||||
{(showCracks || collapse) && (
|
||||
<div className={`crack-overlay${collapse ? " crack-collapse" : ""}`}>
|
||||
{/* Crack overlay for the spinning effect */}
|
||||
{showCracks && (
|
||||
<div className="crack-overlay" style={{ pointerEvents: "none" }}>
|
||||
<img className="crack crack1" src="/crack1.png" alt="" />
|
||||
<img className="crack crack2" src="/crack2.png" alt="" />
|
||||
{/* Add more cracks for extra effect if you wish */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -113,7 +134,6 @@ export default function BottomFooter() {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Donate Section */}
|
||||
<div className="min-w-[220px] mb-8 md:mb-0 flex-1">
|
||||
<h3 className="font-bold underline text-lg mb-3">Donate</h3>
|
||||
@ -128,17 +148,16 @@ export default function BottomFooter() {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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="flex flex-row items-center w-full md:w-auto">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="TremorTracker logo"
|
||||
className="h-16 w-auto mr-4 object-contain"
|
||||
style={{ maxHeight: 75 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center w-full md:w-auto">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="TremorTracker logo"
|
||||
className="h-16 w-auto mr-4 object-contain"
|
||||
style={{ maxHeight: 75 }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm flex items-center">
|
||||
<span className="mr-2">©</span> TremorTracker 2025
|
||||
</span>
|
||||
@ -167,7 +186,14 @@ export default function BottomFooter() {
|
||||
style={{ cursor: "pointer" }}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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;
|
||||