Compare commits

..

11 Commits

Author SHA1 Message Date
06e215a209 Merge branch 'master' of gitea.thowitz.com:thowitz-work/tremor-tracker 2025-05-30 12:26:25 +01:00
Emily Neighbour
04307562a4 removing unnecessary images 2025-05-28 22:42:11 +01:00
Emily Neighbour
d6f7d73622 removed sidebar bits 2025-05-28 22:37:28 +01:00
Emily Neighbour
803253b96e Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-28 22:31:57 +01:00
Emily Neighbour
2c033028cf bottom footer fix 2025-05-28 22:31:45 +01:00
Emily Neighbour
3cb2033046 learn page format alignment 2025-05-28 22:27:58 +01:00
IZZY
1f005295b4 we have a search modal it just doesnt work yet 2025-05-28 22:11:39 +01:00
c1d686b012 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-28 15:45:14 +01:00
Emily Neighbour
c3ecca40f2 new the team image 2025-05-28 08:52:30 +01:00
IZZY
d5da39d812 Added a pretty background 2025-05-27 16:43:22 +01:00
IZZY
977c35bd57 Working Shop and footer 2025-05-27 13:48:32 +01:00
23 changed files with 468 additions and 481 deletions

View File

@ -10,18 +10,18 @@ generator client {
// User model // User model
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
name String name String
email String @unique email String @unique
passwordHash String passwordHash String
role String @default("GUEST") @db.VarChar(10) // Allowed: ADMIN, SCIENTIST, GUEST role String @default("GUEST") @db.VarChar(10) // Allowed: ADMIN, SCIENTIST, GUEST
scientist Scientist? @relation scientist Scientist? @relation
purchasedOrders Order[] @relation("UserOrders") purchasedOrders Order[] @relation("UserOrders")
requests Request[] @relation("UserRequests") requests Request[] @relation("UserRequests")
earthquakes Earthquake[] @relation("UserEarthquakeCreator") earthquakes Earthquake[] @relation("UserEarthquakeCreator")
observatories Observatory[] @relation("UserObservatoryCreator") observatories Observatory[] @relation("UserObservatoryCreator")
artefacts Artefact[] @relation("UserArtefactCreator") artefacts Artefact[] @relation("UserArtefactCreator")
} }
model Request { model Request {
@ -35,15 +35,15 @@ model Request {
// Scientist model // Scientist model
model Scientist { model Scientist {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
name String name String
level String @db.VarChar(10) // JUNIOR, SENIOR level String @db.VarChar(10) // JUNIOR, SENIOR
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId Int @unique userId Int @unique
superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction) superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
superiorId Int? superiorId Int?
subordinates Scientist[] @relation("SuperiorRelation") subordinates Scientist[] @relation("SuperiorRelation")
} }
model Earthquake { model Earthquake {
@ -93,7 +93,7 @@ model Artefact {
earthquake Earthquake @relation(fields: [earthquakeId], references: [id]) earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
creatorId Int? creatorId Int?
creator User? @relation("UserArtefactCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) creator User? @relation("UserArtefactCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
isRequired Boolean @default(true) isRequired Boolean @default(true) @map("isRequired")
dateAddedToShop DateTime? dateAddedToShop DateTime?
shopPrice Float? shopPrice Float?
isSold Boolean @default(false) isSold Boolean @default(false)

BIN
public/BlueBackground.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/EarthHighRes.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 KiB

BIN
public/earthquakeRelief.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

BIN
public/tectonicPlates.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { prisma } from "@utils/prisma";
export async function GET(request: Request) {
try {
const artefact = await prisma.artefact.findMany();
return NextResponse.json({
message: "Got artefacts successfully",
artefact
}, { status: 200 });
} catch (error) {
console.error("Error in artefacts endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
}
}

View File

@ -2,65 +2,157 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import Map from "@components/Map"; import Map from "@components/Map";
import Sidebar from "@components/Sidebar"; import Sidebar from "@components/Sidebar";
import { createPoster } from "@utils/axiosHelpers"; import { createPoster } from "@utils/axiosHelpers";
import { Earthquake } from "@prismaclient"; import { Earthquake } from "@prismaclient";
import { getRelativeDate } from "@utils/formatters"; import { getRelativeDate } from "@utils/formatters";
import GeologicalEvent from "@appTypes/Event"; import GeologicalEvent from "@appTypes/Event";
import axios from "axios";
export default function Earthquakes() { // --- SEARCH MODAL COMPONENT ---
const [selectedEventId, setSelectedEventId] = useState(""); function EarthquakeSearchModal({ open, onClose, onSelect }) {
const [hoveredEventId, setHoveredEventId] = useState(""); const [search, setSearch] = useState("");
// todo properly integrate loading const [results, setResults] = useState([]);
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 5 })); const [loading, setLoading] = useState(false);
const earthquakeEvents = useMemo( const handleSearch = async (e) => {
() => e.preventDefault();
data && data.earthquakes setLoading(true);
? data.earthquakes setResults([]);
.map( try {
(x: Earthquake): GeologicalEvent => ({ const res = await axios.post("/api/earthquakes/search", { query: search });
id: x.code, setResults(res.data.earthquakes || []);
title: `Earthquake in ${x.code.split("-")[2]}`, } catch (e) {
magnitude: x.magnitude, alert("Failed to search.");
longitude: x.longitude, }
latitude: x.latitude, setLoading(false);
text1: "", };
text2: getRelativeDate(x.date),
date: x.date,
})
)
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime()) // Remove Date conversion
: [],
[data]
);
return ( if (!open) return null;
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden"> return (
<div className="flex-grow h-full"> <div className="fixed z-50 inset-0 bg-black/40 flex items-center justify-center">
<Map <div className="bg-white rounded-lg shadow-lg p-6 max-w-lg w-full relative">
events={earthquakeEvents} <button
selectedEventId={selectedEventId} onClick={onClose}
setSelectedEventId={setSelectedEventId} className="absolute right-4 top-4 text-gray-500 hover:text-black text-lg"
hoveredEventId={hoveredEventId} >
setHoveredEventId={setHoveredEventId} &times;
mapType="Earthquakes" </button>
/> <h2 className="font-bold text-xl mb-4">Search Earthquakes</h2>
</div> <form onSubmit={handleSearch} className="flex gap-2 mb-4">
<Sidebar <input
logTitle="Log an Earthquake" type="text"
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists" placeholder="e.g. Mexico, EV-7.4-Mexico-00035"
recentsTitle="Recent Earthquakes" value={search}
events={earthquakeEvents} onChange={e => setSearch(e.target.value)}
selectedEventId={selectedEventId} className="flex-grow px-3 py-2 border rounded"
setSelectedEventId={setSelectedEventId} required
hoveredEventId={hoveredEventId} />
setHoveredEventId={setHoveredEventId} <button
button1Name="Log an Earthquake" type="submit"
button2Name="Search Earthquakes" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
/> >
</div> {loading ? "Searching..." : "Search"}
); </button>
</form>
<div>
{results.length === 0 && !loading && search !== "" && (
<p className="text-gray-400 text-sm">No results found.</p>
)}
<ul>
{results.map(eq => (
<li
key={eq.id}
className="border-b py-2 cursor-pointer hover:bg-gray-50 flex items-center justify-between"
onClick={() => { onSelect(eq); onClose(); }}
tabIndex={0}
>
<div>
<strong>{eq.code}</strong> {" "}
<span>{eq.location}</span> <span className="text-xs text-gray-500">{new Date(eq.date).toLocaleDateString()}</span>
</div>
<div className={`rounded-full px-2 py-1 ml-2 text-white font-semibold ${eq.magnitude >= 7 ? "bg-red-500" : eq.magnitude >= 6 ? "bg-orange-400" : "bg-yellow-400"}`}>
{eq.magnitude}
</div>
</li>
))}
</ul>
</div>
</div>
</div>
);
} }
// --- MAIN PAGE COMPONENT ---
export default function Earthquakes() {
const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState("");
// Search modal state
const [searchModalOpen, setSearchModalOpen] = useState(false);
// Fetch recent earthquakes as before
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 5 }));
// Prepare events for maps/sidebar
const earthquakeEvents = useMemo(
() =>
data && data.earthquakes
? data.earthquakes
.map(
(x: Earthquake): GeologicalEvent => ({
id: x.code,
title: `Earthquake in ${x.code.split("-")[2]}`,
magnitude: x.magnitude,
longitude: x.longitude,
latitude: x.latitude,
text1: "",
text2: getRelativeDate(x.date),
date: x.date,
})
)
.sort((a: GeologicalEvent, b: GeologicalEvent) => new Date(b.date).getTime() - new Date(a.date).getTime())
: [],
[data]
);
// Optional: show details of selected search result (not implemented here)
// const [selectedSearchResult, setSelectedSearchResult] = useState(null);
return (
<div className="flex h-[calc(100vh-3.5rem)] w-full overflow-hidden">
<div className="flex-grow h-full">
<Map
events={earthquakeEvents}
selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId}
mapType="Earthquakes"
/>
</div>
<Sidebar
logTitle="Log an Earthquake"
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists"
recentsTitle="Recent Earthquakes"
events={earthquakeEvents}
selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId}
button1Name="Log an Earthquake"
button2Name="Search Earthquakes"
onButton2Click={() => setSearchModalOpen(true)} // <-- important!
/>
<EarthquakeSearchModal
open={searchModalOpen}
onClose={() => setSearchModalOpen(false)}
onSelect={eq => {
setSelectedEventId(eq.code); // select on map/sidebar
// setSelectedSearchResult(eq); // you can use this if you want to show detail modal
}}
/>
</div>
);
}

View File

@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { prisma } from "@utils/prisma";
export async function POST(req: Request) {
try {
const { query } = await req.json();
// Find earthquakes where either code or location matches (case-insensitive)
const earthquakes = await prisma.earthquake.findMany({
where: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ location: { contains: query, mode: "insensitive" } }
],
},
orderBy: { date: "desc" },
take: 20, // limit results
});
return NextResponse.json({ earthquakes, message: "Success" });
} catch (error) {
console.error("Error in earthquake search", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
}
}

View File

@ -1,48 +1,53 @@
// app/learn/page.tsx
"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 className="min-h-screen bg-blue-50 flex flex-col"> <div
{/* (Optional) Navbar */} className="min-h-screen bg-fixed bg-cover bg-center flex flex-col relative"
{/* <Navbar /> */} style={{
backgroundImage: "url('/earthquakeRelief.jpg')", // adjust path as needed
}}
>
{/* Overlay for readability */}
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
<main className="flex-1 flex flex-col items-center justify-start pt-12 px-4"> <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-blue-800 text-center">Earthquakes</h1> {/* Title & subtitle OVER the background (not in the content box) */}
<p className="max-w-2xl text-lg text-gray-700 mb-6 text-center"> <h1 className="text-4xl font-bold mb-4 text-white text-center drop-shadow-lg">Earthquakes</h1>
<span className="font-bold">In this page, you can learn all about what earthquakes are, and how to keep safe!</span> <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> </p>
<div className="max-w-3xl text-base text-gray-600 text-left space-y-8">
{/* 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 1 */}
<section> <section className="mb-8">
<p> <p>
<span className="font-semibold text-blue-800">What are earthquakes?</span> <span className="font-bold text-black md:text-xl">What are earthquakes?</span>
<br /> <br />
Earthquakes are a shaking of the earth's surface caused by a sudden release of energy underground. They can range in 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 size, from tiny trembles to large quakes, which can cause destruction and even tsunamis. Hundreds of earthquakes
happen every daybut most are too small to feel. happen every daybut most are too small to feel.
</p> </p>
</section> </section>
{/* Section 2 */} {/* Section 2 */}
<section> <section className="mb-8">
<p> <p>
<span className="font-semibold text-blue-800">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. Regions near plate boundaries, such as around the Pacific Ocean ("The Ring of Fire"), experience the most activity.
</p> </p>
</section> </section>
{/* Section 3 */} {/* Section 3 */}
<section> <section>
<p> <p>
<span className="font-semibold text-blue-800">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 */} {/* 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 */} {/* SUB BULLETS */}

View File

@ -1,5 +1,6 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import BottomFooter from "@components/BottomFooter"; import BottomFooter from "@components/BottomFooter";
export default function Home() { export default function Home() {
@ -16,10 +17,7 @@ export default function Home() {
</div> </div>
<p className="mt-2"></p> <p className="mt-2"></p>
<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 <Link href="/earthquakes" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
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" />
<h3 className="text-xl font-bold text-black mb-4">Earthquakes</h3> <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"> <p className="text-md text-black text-center max-w-xs opacity-90">
@ -36,10 +34,7 @@ export default function Home() {
Find recently active observatories, and newly opened/closed sites Find recently active observatories, and newly opened/closed sites
</p> </p>
</Link> </Link>
<Link <Link href="/shop" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
href="/shop"
className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"
>
<Image height={100} width={100} src="/artifactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" /> <Image height={100} width={100} src="/artifactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" />
<h3 className="text-xl font-bold text-black mb-4">Artefacts</h3> <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"> <p className="text-md text-black text-center max-w-xs opacity-90">
@ -59,11 +54,16 @@ export default function Home() {
Welcome to Tremor Tracker Welcome to Tremor Tracker
</h1> </h1>
<p className="text-lg md:text-xl font-sans text-white w-4/6 mx-auto drop-shadow-md z-10"> <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 true, reliable data. Our mission is seismic education and preparation for all TremorTracker is a non-profit website and research company, that aims to provide true, reliable data. Our mission
is seismic education and preparation for all
</p> </p>
<p className="mt-20"></p> <p className="mt-20"></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-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">info</p> <p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
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 daybut most are too small to feel.
</p>
<p className="mt-20"></p> <p className="mt-20"></p>
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10"> <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? How do we log earthquakes?
@ -107,30 +107,21 @@ export default function Home() {
</section> </section>
<p className="mt-2"></p> <p className="mt-2"></p>
<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 <Link href="/contact-us" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
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="Education Icon" className="h-20 w-20 mb-4" /> <Image height={100} width={100} src="/contactUs.jpg" alt="Education Icon" className="h-20 w-20 mb-4" />
<h3 className="text-xl font-bold text-black mb-4">Contact us directly</h3> <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"> <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. Visit our socials or leave us a message via phone or email.
</p> </p>
</Link> </Link>
<Link <Link href="/our-mission" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
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="Research Icon" className="h-20 w-20 mb-4" /> <Image height={100} width={100} src="/mission.jpg" alt="Research Icon" className="h-20 w-20 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 <Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
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="Technology Icon" className="h-20 w-20 mb-4" /> <Image height={100} width={100} src="/team.jpg" alt="Technology Icon" className="h-20 w-20 mb-4" />
<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">
@ -139,12 +130,12 @@ export default function Home() {
</Link> </Link>
</div> </div>
<p className="mt-10"></p> <p className="mt-10"></p>
<section style={{ height: 500}} className="text-black"> <section style={{ height: 500 }} className="text-black">
<div className="w-full relative overflow-hidden z=10"> <div className="w-full relative overflow-hidden z=10">
<div className=""> <div className="">
<Image height={1000} width={2000} alt="Background Image" src="/scientists.png"></Image> <Image height={1000} width={2000} alt="Background Image" src="/scientists.png"></Image>
</div> </div>
<BottomFooter /> <BottomFooter />
</div> </div>
</section> </section>
</main> </main>

View File

@ -1,191 +1,46 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
import { Dispatch, SetStateAction, useCallback, useState } from "react"; import { Dispatch, SetStateAction, useCallback, useState, useEffect } from "react";
import BottomFooter from "@components/BottomFooter";
import { ExtendedArtefact } from "@appTypes/ApiTypes"; import { ExtendedArtefact } from "@appTypes/ApiTypes";
import { Currency } from "@appTypes/StoreModel"; import { Currency } from "@appTypes/StoreModel";
import { useStoreState } from "@hooks/store"; import { useStoreState } from "@hooks/store";
const artefacts: ExtendedArtefact[] = [
{
id: 1,
name: "Golden Scarab",
description: "An ancient Egyptian artefact symbolizing rebirth.",
location: "Cairo, Egypt",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact1.jpg",
price: 150,
},
{
id: 2,
name: "Aztec Sunstone",
description: "A replica of the Aztec calendar (inscriptions intact).",
location: "Peru",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact2.jpg",
price: 200,
},
{
id: 3,
name: "Medieval Chalice",
description: "Used by royalty in medieval ceremonies.",
location: "Cambridge, England",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact3.jpg",
price: 120,
},
{
id: 4,
name: "Roman Coin",
description: "An authentic Roman coin from the 2nd century CE.",
location: "Rome, Italy",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact4.jpg",
price: 80,
},
{
id: 5,
name: "Samurai Mask",
description: "Replica of Japanese Samurai battle masks.",
location: "Tokyo, Japan",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact5.jpg",
price: 300,
},
{
id: 6,
name: "Ancient Greek Vase",
description: "Depicts Greek mythology, found in the Acropolis.",
location: "Athens, Greece",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact6.jpg",
price: 250,
},
{
id: 7,
name: "Incan Pendant",
description: "Represents the Sun God Inti.",
location: "India",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact7.jpg",
price: 175,
},
{
id: 8,
name: "Persian Carpet Fragment",
description: "Ancient Persian artistry.",
location: "Petra, Jordan",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact8.jpg",
price: 400,
},
{
id: 9,
name: "Stone Buddha",
description: "Authentic stone Buddha carving.",
location: "India",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact9.jpg",
price: 220,
},
{
id: 10,
name: "Victorian Brooch",
description: "A beautiful Victorian-era brooch with a ruby centre.",
location: "Oxford, England",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact10.jpg",
price: 150,
},
{
id: 11,
name: "Ancient Scroll",
description: "A mysterious scroll from ancient times.",
location: "Madrid, Spain",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact11.jpg",
price: 500,
},
{
id: 12,
name: "Ming Dynasty Porcelain",
description: "Porcelain from China's Ming Dynasty.",
location: "Beijing, China",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact12.jpg",
price: 300,
},
{
id: 13,
name: "African Tribal Mask",
description: "A unique tribal mask from Africa.",
location: "Nigeria",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact13.jpg",
price: 250,
},
{
id: 14,
name: "Crystal Skull",
description: "A mystical pre-Columbian artefact.",
location: "Colombia",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact14.jpg",
price: 1000,
},
{
id: 15,
name: "Medieval Armor Fragment",
description: "A fragment of medieval armor.",
location: "Normandy, France",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact15.jpg",
price: 400,
},
{
id: 16,
name: "Medieval Helmet Fragment",
description: "A fragment of a medieval helmet.",
location: "Normandy, France",
earthquakeID: "h",
observatory: "jhd",
dateReleased: "12/02/2025",
image: "/artefact16.jpg",
price: 500,
},
];
export default function Shop() { export default function Shop() {
const [artefacts, setArtefacts] = useState<ExtendedArtefact[]>([]);
const [loading, setLoading] = useState(true);
// 3. Fetch from your API route and map data to fit your existing fields
useEffect(() => {
async function fetchArtefacts() {
setLoading(true);
try {
const res = await fetch("/api/artefacts");
const data = await res.json();
const transformed = data.artefact.map((a: any) => ({
id: a.id,
name: a.name,
description: a.description,
location: a.warehouseArea, // your database
earthquakeID: a.earthquakeId?.toString() ?? "",
observatory: a.type ?? "", // if you want to display type
dateReleased: a.createdAt ? new Date(a.createdAt).toLocaleDateString() : "",
image: "/artefactImages/" + (a.imageName || "NoImageFound.PNG"),
price: a.shopPrice ?? 100, // fallback price if not in DB
}));
setArtefacts(transformed);
} catch (e) {
// Optionally handle error
console.error("Failed to fetch artefacts", e);
} finally {
setLoading(false);
}
}
fetchArtefacts();
}, []);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null); const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false); const [showPaymentModal, setShowPaymentModal] = useState(false);
@ -433,20 +288,20 @@ export default function Shop() {
} }
return ( return (
<div <div
className="min-h-screen relative flex flex-col" className="min-h-screen bg-blue-50 relative flex flex-col"
style={{ style={{
backgroundImage: "url('/artefacts.jpg')", backgroundImage: "url('/EarthHighRes.jpg')",
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
}} }}
> >
<div className="absolute inset-0 bg-black bg-opacity-50 z-0"></div> <div className="absolute inset-0 bg-black bg-opacity-0 z-0"></div>
<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-white 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
</h1> </h1>
<p className="text-lg md:text-xl text-center text-white mb-10 drop-shadow-md max-w-2xl"> <p className="text-lg md:text-xl text-center text-white mb-10 drop-shadow-md max-w-2xl">
Discover extraordinary historical artefacts and collectibles from major seismic events from around the world - now Discover extraordinary artefacts and collectibles from major seismic events from around the world - Previously studied by our scientists, now
available for purchase. available for purchase.
</p> </p>
<div className="w-full max-w-7xl grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 p-2"> <div className="w-full max-w-7xl grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 p-2">
@ -489,6 +344,10 @@ export default function Shop() {
{showThankYouModal && orderNumber && ( {showThankYouModal && orderNumber && (
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} /> <ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
)} )}
</div> {!selectedArtefact && !showPaymentModal && !showThankYouModal && (
); <div className="relative z-50">
} <BottomFooter />
</div>
)}
</div>
);

View File

@ -33,7 +33,7 @@ export default function Page() {
return ( return (
<div <div
className="min-h-screen relative flex flex-col items-center justify-center px-4 py-30" className="min-h-screen relative flex flex-col items-center justify-center px-4 py-30"
style={{ backgroundImage: "url('tectonicPlate.jpg')", backgroundSize: "cover", backgroundPosition: "center" }} style={{ backgroundImage: "url('tectonicPlates.png')", backgroundSize: "cover", backgroundPosition: "center" }}
> >
{/* Overlay */} {/* Overlay */}
<div className="absolute inset-0 bg-black bg-opacity-50 pointer-events-none"></div> <div className="absolute inset-0 bg-black bg-opacity-50 pointer-events-none"></div>

View File

@ -1,88 +1,74 @@
// components/Footer.tsx // components/Footer.tsx
import Link from "next/link"; import Link from "next/link";
import { FaTwitter, FaFacebook, FaYoutube, FaLinkedin } from "react-icons/fa"; import { FaFacebook, FaLinkedin, FaTwitter, FaYoutube } from "react-icons/fa";
export default function Footer() { export default function Footer() {
return ( return (
<footer className="bg-[#16424b] text-white pt-12 pb-4 px-6 mt-12"> <footer className="bg-[#16424b] text-white pt-12 pb-4 px-6 mt-12 z-0">
<div className="max-w-6xl mx-auto flex flex-col md:flex-row justify-between gap-8"> <div className="max-w-6xl mx-auto flex flex-col md:flex-row justify-between gap-8">
{/* Useful Links */} {/* Useful Links */}
<div className="min-w-[200px] mb-8 md:mb-0 flex-1"> <div className="min-w-[200px] mb-8 md:mb-0 flex-1">
<h3 className="font-bold underline text-lg mb-3">Useful links</h3> <h3 className="font-bold underline text-lg mb-3">Useful links</h3>
<ul className="space-y-2"> <ul className="space-y-2">
<li> <li>
<Link <Link
href="https://www.gov.uk/guidance/extreme-weather-and-natural-hazards" href="https://www.gov.uk/guidance/extreme-weather-and-natural-hazards"
className="hover:underline" className="hover:underline"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Gov.UK guidance Gov.UK guidance
</Link> </Link>
</li> </li>
<li> <li>
<Link href="https://www.dysoninstitute.ac.uk/about-us/governance/privacy-notices/" <Link href="https://www.dysoninstitute.ac.uk/about-us/governance/privacy-notices/" className="hover:underline">
className="hover:underline"> Privacy policy
Privacy policy </Link>
</Link> </li>
</li> <li>
<li> <Link href="https://privacy.dyson.com/en/globalcookiepolicy.aspx" className="hover:underline">
<Link href="https://privacy.dyson.com/en/globalcookiepolicy.aspx" Cookies policy
className="hover:underline"> </Link>
Cookies policy </li>
</Link> </ul>
</li> </div>
</ul> {/* Donate Section */}
</div> <div className="min-w-[220px] mb-8 md:mb-0 flex-1">
{/* Donate Section */} <h3 className="font-bold underline text-lg mb-3">Donate</h3>
<div className="min-w-[220px] mb-8 md:mb-0 flex-1"> <p className="mb-4">
<h3 className="font-bold underline text-lg mb-3">Donate</h3> We are a nonprofit entirely funded by your donations, every penny helps provide life saving information.
<p className="mb-4"> </p>
We are a nonprofit entirely funded by your donations, every penny helps provide life saving information. <Link
</p> href="#"
<Link className="bg-gray-200 hover:bg-blue-600 hover:text-white text-black font-bold rounded-full px-8 py-2 shadow transition-colors duration-200 inline-block text-center"
href="#" >
className="bg-gray-200 hover:bg-blue-600 hover:text-white text-black font-bold rounded-full px-8 py-2 shadow transition-colors duration-200 inline-block text-center" Donate Now
> </Link>
Donate Now </div>
</Link> </div>
</div> {/* Bottom bar */}
</div> <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">
{/* Bottom bar */} {/* Bottom left: Copyright */}
<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"> <span className="text-sm flex items-center">
{/* Bottom left: Copyright */} <span className="mr-2">&#169;</span> TremorTracker 2025
<span className="text-sm flex items-center"> </span>
<span className="mr-2">&#169;</span> TremorTracker 2025 {/* Bottom right: Social icons */}
</span> <div className="flex flex-col items-end">
{/* Bottom right: Social icons */} <span className="text-sm mb-2">Follow us on</span>
<div className="flex flex-col items-end"> <div className="flex space-x-3">
<span className="text-sm mb-2">Follow us on</span> {/* Replace src with your icon URLs, or use next/image if preferred */}
<div className="flex space-x-3"> <a href="#" target="_blank" rel="noopener noreferrer">
{/* Replace src with your icon URLs, or use next/image if preferred */} <img src="instagram.png" alt="instagram" className="h-7 w-7 rounded-full shadow" />
<a href="#" target="_blank" rel="noopener noreferrer"> </a>
<img <a href="#" target="_blank" rel="noopener noreferrer">
src="instagram.png" <img src="linkedin.png" alt="linkedin" className="h-7 w-7 rounded-full shadow" />
alt="instagram" </a>
className="h-7 w-7 rounded-full shadow" <a href="#" target="_blank" rel="noopener noreferrer">
/> <img src="x_logo.jpg" alt="X" className="h-7 w-7 rounded-full shadow" />
</a> </a>
<a href="#" target="_blank" rel="noopener noreferrer"> </div>
<img </div>
src="linkedin.png" </div>
alt="linkedin" </footer>
className="h-7 w-7 rounded-full shadow" );
/> }
</a>
<a href="#" target="_blank" rel="noopener noreferrer">
<img
src="x_logo.jpg"
alt="X"
className="h-7 w-7 rounded-full shadow"
/>
</a>
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,87 @@
import Link from "next/link";
import React, { Dispatch, SetStateAction, useState } from "react";
import { TbHexagon } from "react-icons/tb";
import Event from "@appTypes/Event";
import getMagnitudeColor from "@utils/getMagnitudeColour";
function setButton1(button1Name) {
const [modalOpen, setModalOpen] = useState(false);
return (
modalOpen && (
<div className="fixed z-50 inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-lg max-w-6xl w-full p-10">
<div className="flex items-center justify-between mb-3">
<h2 className="text-2xl font-extrabold">Log New Earthquake</h2>
<button
onClick={() => setModalOpen(false)}
className="text-2xl font-bold text-gray-500 hover:text-gray-900"
aria-label="Close modal"
>
&times;
</button>
</div>
{/* Blank Table */}
<table className="w-full border mb-4">
<thead>
<tr>
<th className="border px-2 py-1">Date</th>
<th className="border px-2 py-1">Code</th>
<th className="border px-2 py-1">Magnitude</th>
<th className="border px-2 py-1">Observatory</th>
<th className="border px-2 py-1">Type</th>
<th className="border px-2 py-1">Latitude</th>
<th className="border px-2 py-1">Longitude</th>
<th className="border px-2 py-1">Location</th>
<th className="border px-2 py-1">Observatory</th>
<th className="border px-2 py-1">Depth</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border px-2 py-1">
<input type="date" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="number" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="text" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="text" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="text" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="date" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="number" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="text" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="text" className="border p-1 w-full" disabled />
</td>
<td className="border px-2 py-1">
<input type="text" className="border p-1 w-full" disabled />
</td>
</tr>
</tbody>
</table>
<div className="flex justify-end gap-4">
<button onClick={() => setModalOpen(true)} className="bg-blue-600 text-white px-10 py-2 rounded">
Add
</button>
<button onClick={() => setModalOpen(false)} className="bg-blue-600 text-white px-4 py-2 rounded">
Close
</button>
</div>
</div>
</div>
)
);
}

View File

@ -16,6 +16,7 @@ interface SidebarProps {
setHoveredEventId: Dispatch<SetStateAction<string>>; setHoveredEventId: Dispatch<SetStateAction<string>>;
button1Name: string; button1Name: string;
button2Name: string; button2Name: string;
onButton2Click?: () => void;
} }
function MagnitudeNumber({ magnitude }: { magnitude: number }) { function MagnitudeNumber({ magnitude }: { magnitude: number }) {
@ -47,6 +48,7 @@ export default function Sidebar({
setHoveredEventId, setHoveredEventId,
button1Name, button1Name,
button2Name, button2Name,
onButton2Click,
}: SidebarProps) { }: SidebarProps) {
const eventsContainerRef = useRef<HTMLDivElement>(null); const eventsContainerRef = useRef<HTMLDivElement>(null);
@ -68,17 +70,21 @@ export default function Sidebar({
<div className="px-6 pb-8 border-b border-neutral-200"> <div className="px-6 pb-8 border-b border-neutral-200">
<h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2> <h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2>
<p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p> <p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p>
<Link href="/">
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium"> <Link href="/">
{button1Name} <button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium">
</button> {button1Name}
</Link> </button>
<Link href="/"> </Link>
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium"> {/* "Search Earthquakes" should NOT be wrapped in a Link! */}
{button2Name} <button
</button> className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium"
</Link> onClick={onButton2Click}
</div> type="button"
>
{button2Name}
</button>
</div>
<div className="px-6 pt-6"> <div className="px-6 pt-6">
<h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2> <h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2>
</div> </div>

View File

@ -1,42 +0,0 @@
import Link from "next/link";
import React from "react";
const Sidebar = () => {
return (
<div className="flex flex-col h-screen w-64 bg-neutral-400 text-white border-l border-neutral-700">
<div className="flex flex-col p-4 border-b border-neutral-700">
<h2 className="text-xl font-semibold mb-2">Log an Earthquake</h2>
<p className="text-sm text-neutral-700">
Record new earthquakes - time/date, location, magnitude, observatory and scientists
</p>
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
<Link href="/">Log Event</Link>
</button>
</div>
{/* Section: Recent Events - Will need to be replaced with a link to the database*/}
<div className="flex-1 p-4">
<h2 className="text-xl font-semibold mb-2">Recent Events</h2>
<ul className="space-y-2">
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Earthquake in California</p>
<p className="text-xs text-neutral-300">Magnitude 5.3</p>
<p className="text-xs text-neutral-400">2 hours ago</p>
</li>
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Japan</p>
<p className="text-xs text-neutral-300">Magnitude 4.7</p>
<p className="text-xs text-neutral-400">5 hours ago</p>
</li>
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Spain</p>
<p className="text-xs text-neutral-300">Magnitude 2.1</p>
<p className="text-xs text-neutral-400">10 hours ago</p>
</li>
</ul>
</div>
</div>
);
};
export default Sidebar;

View File

@ -1,40 +0,0 @@
import Link from "next/link";
import React from "react";
const Sidebar = () => {
return (
<div className="flex flex-col h-screen w-64 bg-neutral-400 text-white border-l border-neutral-700">
<div className="flex flex-col p-4 border-b border-neutral-700">
<h2 className="text-xl font-semibold mb-2">Observatories</h2>
<p className="text-sm text-neutral-700">Observatory events - location, scientists, recent earthquakes</p>
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
<Link href="/">Observatory News</Link>
</button>
</div>
{/* Section: Recent Events - Will need to be replaced with a link to the database*/}
<div className="flex-1 p-4">
<h2 className="text-xl font-semibold mb-2">Recent Observatory Events</h2>
<ul className="space-y-2">
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Earthquake in California</p>
<p className="text-xs text-neutral-300">Magnitude 5.3</p>
<p className="text-xs text-neutral-400">Cali Observatory</p>
</li>
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Japan</p>
<p className="text-xs text-neutral-300">Magnitude 4.7</p>
<p className="text-xs text-neutral-400">Kyoto Observatory</p>
</li>
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Spain</p>
<p className="text-xs text-neutral-300">Magnitude 2.1</p>
<p className="text-xs text-neutral-400">Madrid Observatory</p>
</li>
</ul>
</div>
</div>
);
};
export default Sidebar;