Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker

This commit is contained in:
IZZY 2025-05-06 16:24:43 +01:00
commit 615a5e7a37
47 changed files with 3326 additions and 1576 deletions

62
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,62 @@
{
"editor.insertSpaces": true,
// Ignores the warning when Git is missing
"git.ignoreMissingGitWarning": true,
// Disable crash reports being sent to Microsoft.
"telemetry.enableCrashReporter": false,
// Disable usage data and errors being sent to Microsoft.
"telemetry.enableTelemetry": false,
"editor.formatOnSave": true,
"cmake.configureOnOpen": false,
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8",
"editor.formatOnSave": true
},
"autopep8.args": ["--max-line-length=200"],
"editor.detectIndentation": false,
"git.confirmSync": false,
"editor.rulers": [
{
"column": 120
}
],
"git.autofetch": true,
"files.autoSave": "onFocusChange",
"cSpell.language": "en-GB",
"C_Cpp.formatting": "disabled",
"cmake.showOptionsMovedNotification": false,
"diffEditor.renderSideBySide": false,
"terminal.integrated.commandsToSkipShell": ["matlab.interrupt"],
"[matlab]": {
"editor.defaultFormatter": "AffenWiesel.matlab-formatter"
},
"workbench.editorAssociations": {
"*.pdf": "latex-workshop-pdf-hook"
},
"notebook.output.textLineLimit": 50,
"prettier.printWidth": 130,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.useTabs": true,
"editor.tabSize": 4,
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[cpp]": {
"editor.defaultFormatter": "ms-vscode.cpptools"
},
"C_Cpp.default.cppStandard": "gnu++23",
"C_Cpp.default.cStandard": "gnu23",
"diffEditor.hideUnchangedRegions.enabled": true,
"python.createEnvironment.trigger": "off",
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"importSorter.generalConfiguration.sortOnBeforeSave": true,
"cSpell.words": [
"vars"
]
}

View File

@ -9,8 +9,9 @@ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
}); });
const eslintConfig = [ export default tseslint.config([...compat.extends("next/core-web-vitals", "next/typescript")], {
...compat.extends("next/core-web-vitals", "next/typescript"), rules: {
]; "no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
export default eslintConfig; },
});

759
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,13 +13,17 @@
"dependencies": { "dependencies": {
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@types/mapbox-gl": "^3.4.1", "@types/mapbox-gl": "^3.4.1",
"axios": "^1.9.0",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"easy-peasy": "^6.1.0",
"express": "^5.1.0", "express": "^5.1.0",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash": "^4.17.21",
"mapbox-gl": "^3.10.0", "mapbox-gl": "^3.10.0",
"next": "15.1.7", "next": "15.1.7",
"path": "^0.12.7", "path": "^0.12.7",
@ -28,10 +32,12 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-node": "^1.0.2" "react-node": "^1.0.2",
"swr": "^2.3.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",

BIN
public/destruction.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

BIN
public/education.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
public/facebook.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/insta.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
public/linkedIn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
public/research.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
public/road.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
public/tech.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/tsunamiWaves.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

BIN
public/x_logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,65 @@
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
export async function GET(request: Request) {
try {
const events = [
{
id: "1234",
title: "Earthquake in Germany",
text1: "Magnitude 8.5",
text2: "30 minutes ago",
magnitude: 8.5,
longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52,
},
{
id: "2134",
title: "Earthquake in California",
text1: "Magnitude 5.3",
text2: "2 hours ago",
magnitude: 5.3,
longitude: -122.4194, // Near San Francisco, California, USA
latitude: 37.7749,
},
{
id: "2314",
title: "Tremor in Japan",
text1: "Magnitude 4.7",
text2: "5 hours ago",
magnitude: 4.7,
longitude: 139.6917, // Near Tokyo, Japan
latitude: 35.6762,
},
{
id: "2341",
title: "Tremor in Spain",
text1: "Magnitude 2.1",
text2: "10 hours ago",
magnitude: 2.1,
longitude: -3.7038, // Near Madrid, Spain
latitude: 40.4168,
},
];
let earthquakes;
if (usingPrisma) earthquakes = await prisma.earthquakes.findMany();
if (earthquakes) {
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 });
} else {
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: events }, { status: 200 });
// return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 });
}
} catch (error) {
console.error("Error in earthquakes endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
}
}

View File

@ -1,11 +1,12 @@
import path from "path";
import fs from "fs";
import csv from "csv-parser"; import csv from "csv-parser";
import fs from "fs";
import path from "path";
export type User = { export type User = {
name: string; name: string;
email: string; email: string;
password: string; password: string;
accessLevel: string;
}; };
/** /**
@ -82,8 +83,8 @@ export async function writeUserCsv(users: User[]): Promise<void> {
csvPath = path.join(csvPath, "src", "databases", "Users.csv"); csvPath = path.join(csvPath, "src", "databases", "Users.csv");
// Prepare CSV data as a string // Prepare CSV data as a string
const headers = "name,email,password"; // CSV headers const headers = "name,email,password,level"; // CSV headers
const rows = users.map((user) => `${user.name},${user.email},${user.password}`); const rows = users.map((user) => `${user.name},${user.email},${user.password},${user.accessLevel}`);
const csvData = `${headers}\n${rows.join("\n")}`; const csvData = `${headers}\n${rows.join("\n")}`;
// Write data to the file // Write data to the file

View File

@ -1,30 +1,46 @@
import bcrypt from "bcrypt";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import {User, readUserCsv, findUserByEmail} from "../functions/csvReadWrite"
import { PrismaClient } from "@prisma/client";
import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json(); // Parse incoming JSON data const body = await request.json(); // Parse incoming JSON data
const { email, password } = body; const { email, password } = body;
console.log("Login API received data");
const userData = await readUserCsv(); const userData = await readUserCsv();
console.log(userData);
console.log(userData)
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove
console.log("Password:", password); // ! remove console.log("Password:", password); // ! remove
const foundUser = findUserByEmail(userData,email) let foundUser;
if (foundUser && foundUser.password === password) {
console.log("User Details Correct") if (usingPrisma) {
return NextResponse.json({ message: "Login successful!" }, { status: 200 }); foundUser = await prisma.user.findUnique({
where: {
email: email, // use the email to uniquely identify the user
},
});
} else { } else {
console.log("User email or password is invalid") foundUser = findUserByEmail(userData, email);
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
} }
if (foundUser && (await bcrypt.compare(password, usingPrisma ? foundUser.hashedPassword : foundUser.password))) {
// todo remove password from returned user
return NextResponse.json({ message: "Login successful!", user: foundUser }, { status: 200 });
} else {
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
}
} catch (error) { } catch (error) {
console.error("Error in signup endpoint:", error); console.error("Error in signup endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
} }
} }

View File

@ -0,0 +1,54 @@
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
export async function GET(request: Request) {
try {
const events = [
{
id: "1234",
title: "Earthquake - Berlin Observatory",
text1: "Logged by ",
text2: "30 minutes ago",
longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52,
},
{
id: "2134",
title: "New Observatory - Phuket, Thailand",
text1: "Dr. Neil Armstrong",
text2: "2 weeks ago",
longitude: -122.4194,
latitude: 37.7749,
},
{
id: "2314",
title: "Observatory Scientist Change",
text1: "Dr. Samantha Green new lead scientist",
text2: "1 month ago",
longitude: 139.6917,
latitude: 35.6762,
},
];
// todo get earthquakes associated with observatories
let observatories;
if (usingPrisma) observatories = await prisma.observatories.findMany();
if (observatories) {
return NextResponse.json({ message: "Got observatories successfully", observatories }, { status: 200 });
} else {
return NextResponse.json({ message: "Got observatories successfully", observatories: events }, { status: 200 });
// return NextResponse.json({ message: "Failed to get observatories" }, { status: 401 });
}
} catch (error) {
console.error("Error in observatories endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
}
}

View File

@ -1,27 +1,44 @@
import bcrypt from "bcrypt";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import {User, readUserCsv, writeUserCsv, findUserByEmail, passwordStrengthCheck} from "../functions/csvReadWrite"
import { PrismaClient } from "@prisma/client";
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json(); // Parse incoming JSON data const body = await request.json(); // Parse incoming JSON data
const {name, email, password } = body; let { email, password, name } = body;
console.log("Signin API received data"); const accessLevel = "basic";
const userData = await readUserCsv(); const userData = await readUserCsv();
console.log(userData) console.log(userData);
console.log("Name:", name); // ! remove console.log("Name:", name); // ! remove
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove
console.log("Password:", password); // ! remove console.log("Password:", password); // ! remove
const foundUser = findUserByEmail(userData,email) let foundUser;
if (usingPrisma) {
foundUser = await prisma.user.findUnique({
where: {
email: email, // use the email to uniquely identify the user
},
});
} else {
foundUser = findUserByEmail(userData, email);
}
if (foundUser) { if (foundUser) {
console.log("Email already in the system")
return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 }); return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 });
} }
const passwordCheckResult = await passwordStrengthCheck(password) const passwordCheckResult = await passwordStrengthCheck(password);
if (passwordCheckResult === "short") { if (passwordCheckResult === "short") {
return NextResponse.json({ message: "Your password is shorter than 8 characters" }, { status: 400 }); return NextResponse.json({ message: "Your password is shorter than 8 characters" }, { status: 400 });
@ -39,16 +56,26 @@ export async function POST(request: Request) {
return NextResponse.json({ message: "Password check script failure" }, { status: 500 }); return NextResponse.json({ message: "Password check script failure" }, { status: 500 });
} else { } else {
try { try {
userData.push(body) const passwordHash = await bcrypt.hash(password, 10);
await writeUserCsv(userData) if (usingPrisma) {
// todo add sending back newUser
const newUser = await prisma.user.create({
data: {
name,
email,
passwordHash,
},
});
} else {
userData.push({ name, email, password: passwordHash, accessLevel });
}
await writeUserCsv(userData);
return NextResponse.json({ message: "Account Created" }, { status: 201 }); return NextResponse.json({ message: "Account Created" }, { status: 201 });
} catch (error) { } catch (error) {
console.error("Error in writting :", error); console.error("Error in writting :", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} }
} }
} catch (error) { } catch (error) {
console.error("Error in signup endpoint:", error); console.error("Error in signup endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }); return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });

View File

@ -1,4 +1,5 @@
"use client"; "use client";
import Image from "next/image";
import React, { useState } from "react"; import React, { useState } from "react";
const ContactUs = () => { const ContactUs = () => {
@ -8,43 +9,45 @@ const ContactUs = () => {
message: "", message: "",
}); });
const handleChange = (e) => { const handleChange = (e: { target: { name: any; value: any } }) => {
setFormData({ ...formData, [e.target.name]: e.target.value }); setFormData({ ...formData, [e.target.name]: e.target.value });
}; };
const handleSubmit = (e) => { const handleSubmit = (e: { preventDefault: () => void }) => {
e.preventDefault(); e.preventDefault();
console.log("Form submitted with data:", formData); console.log("Form submitted with data:", formData);
// Handle form submission, like sending data to a server
alert("Thank you for reaching out! We will get back to you soon."); alert("Thank you for reaching out! We will get back to you soon.");
setFormData({ name: "", email: "", message: "" }); // Clear form setFormData({ name: "", email: "", message: "" });
}; };
return ( return (
<div className="min-h-screen bg-gray-100 flex flex-col items-center py-10"> <div className="h-full relative text-white border border-black overflow-hidden">
<div className="max-w-4xl mx-auto p-5 bg-white shadow-md rounded-lg"> <Image
<h1 className="text-3xl font-bold text-center text-gray-800 mb-6"> height={5000}
Contact Us width={5000}
</h1> alt="Logo"
<p className="text-lg text-gray-600 text-center mb-6"> className="border border-neutral-300 absolute z-10 -top-20"
Have questions or concerns about earthquake preparedness? Contact us src="/tsunamiWaves.jpg"
using the form below or through the provided contact details. />
{/* Overlay for readability */}
<div className="absolute overflow-hidden w-full h-full bg-gradient-to-b from-black/70 via-black/40 to-transparent flex flex-col items-center z-20">
{/* Container */}
<div className="max-w-4xl mx-auto p-5 mt-20">
{/* Header */}
<h1 className="text-4xl font-bold text-center text-white mb-6">Contact Us</h1>
<p className="text-lg text-center text-neutral-200 mb-6">
Have questions or concerns about earthquake preparedness? Contact us using the form below or through the provided
contact details.
</p> </p>
{/* Flexbox Layout for Form and Contact Details */} {/* Content Section */}
<div className="flex flex-col md:flex-row"> <div className="flex flex-col md:flex-row gap-6">
{/* Contact Form Section */}
{/* Contact Form */} <div className="flex-1 bg-white bg-opacity-90 text-neutral-800 rounded-lg shadow-lg p-6">
<div className="flex-1 mb-6 md:mb-0 md:mr-4"> <form onSubmit={handleSubmit}>
<form
onSubmit={handleSubmit}
className="bg-gray-50 p-6 rounded-lg shadow-lg"
>
<div className="mb-4"> <div className="mb-4">
<label <label htmlFor="name" className="block text-neutral-700 font-medium mb-2">
htmlFor="name"
className="block text-gray-700 font-medium mb-2"
>
Name Name
</label> </label>
<input <input
@ -54,16 +57,13 @@ const ContactUs = () => {
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleChange}
placeholder="Your Name" placeholder="Your Name"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required required
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
<label <label htmlFor="email" className="block text-neutral-700 font-medium mb-2">
htmlFor="email"
className="block text-gray-700 font-medium mb-2"
>
Email Email
</label> </label>
<input <input
@ -73,16 +73,13 @@ const ContactUs = () => {
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
placeholder="Your Email" placeholder="Your Email"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required required
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
<label <label htmlFor="message" className="block text-neutral-700 font-medium mb-2">
htmlFor="message"
className="block text-gray-700 font-medium mb-2"
>
Message Message
</label> </label>
<textarea <textarea
@ -90,10 +87,11 @@ const ContactUs = () => {
id="message" id="message"
value={formData.message} value={formData.message}
onChange={handleChange} onChange={handleChange}
rows="5" rows={5}
placeholder="Your Message" placeholder="Your Message"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required required
style={{ resize: "none" }}
/> />
</div> </div>
@ -106,77 +104,47 @@ const ContactUs = () => {
</form> </form>
</div> </div>
{/* Contact Details */} {/* Contact Details Section */}
<div className="flex-1 bg-gray-50 p-6 rounded-lg shadow-lg"> <div className="w-[45%] bg-white bg-opacity-90 text-neutral-800 rounded-lg shadow-lg p-6">
<h2 className="text-xl font-bold text-gray-800 mb-4"> <h2 className="text-xl font-bold text-neutral-800 mb-4">Get in Touch</h2>
Get in Touch
</h2>
<div className="mb-4"> <div className="mb-4">
<h3 className="text-gray-700 font-medium">Email</h3> <h3 className="text-neutral-700 font-medium">Email</h3>
<p className="text-gray-600">support@earthquakesafety.org</p> <a href="mailto:getintouch@tremortracker.com" style={{ color: "initial" }}>
</div> getintouch@tremortracker.com
<div className="mb-4">
<h3 className="text-gray-700 font-medium">Phone</h3>
<p className="text-gray-600">+1 800 123 4567</p>
</div>
<div className="mb-4">
<h3 className="text-gray-700 font-medium">Address</h3>
<p className="text-gray-600">
123 Earthquake Ave, Prepared City, CA 98765
</p>
</div>
<h2 className="text-xl font-bold text-gray-800 mb-4 mt-6">
Follow Us
</h2>
<div className="flex space-x-4">
<a
href="#"
className="text-blue-600 hover:text-blue-800 transition duration-200"
>
<span className="sr-only">Twitter</span>
<svg
className="h-6 w-6"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M24 4.56a9.83 9.83..." />
</svg>
</a> </a>
<a </div>
href="#" <div className="mb-4">
className="text-blue-600 hover:text-blue-800 transition duration-200" <h3 className="text-neutral-700 font-medium">Phone</h3>
> <p className="text-neutral-600">+44 7538 359022</p>
</div>
<div className="mb-4">
<h3 className="text-neutral-700 font-medium">Address</h3>
<p className="text-neutral-600">1 Swentown Row, Greenwich, London, SE3 0FQ</p>
</div>
<h2 className="text-xl font-bold text-neutral-800 mb-4 mt-6">Follow Us</h2>
<div className="flex justify-around items-center">
<a href="#" className="w-20 h-20 text-blue-600 hover:text-blue-800 transition duration-200">
<span className="sr-only">Instagram</span>
<Image height={200} width={200} alt="Logo" className="z-10" src="/insta.webp" />
</a>
<a href="#" className="w-20 h-20 text-blue-600 hover:text-blue-800 transition duration-200">
<span className="sr-only">Facebook</span> <span className="sr-only">Facebook</span>
<svg <Image height={200} width={200} alt="Logo" className="z-10" src="/facebook.webp" />
className="h-6 w-6"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M22.675 0h-21.35..." />
</svg>
</a> </a>
<a <a href="#" className="w-20 h-20 p-4 text-blue-600 hover:text-blue-800 transition duration-200">
href="#" <span className="sr-only">X</span>
className="text-blue-600 hover:text-blue-800 transition duration-200" <Image height={200} width={200} alt="Logo" className="z-10 rounded-lg" src="/x_logo.jpg" />
> </a>
<a href="#" className="w-20 h-20 flex items-center text-blue-600 hover:text-blue-800 transition duration-200">
<span className="sr-only">LinkedIn</span> <span className="sr-only">LinkedIn</span>
<svg <Image height={200} width={200} alt="Logo" className="z-10" src="/linkedIn.png" />
className="h-6 w-6"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M22.23 0H1.77C0.8..." />
</svg>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -1,60 +1,23 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar"; import { useMemo, useState } from "react";
import Map from "@/components/map"; import useSWR from "swr";
import { useState, useMemo } from "react";
import Map from "@components/Map";
import Sidebar from "@components/Sidebar";
import { fetcher } from "@utils/fetcher";
export default function Earthquakes() { export default function Earthquakes() {
const [selectedEventId, setSelectedEventId] = useState(""); const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState(""); const [hoveredEventId, setHoveredEventId] = useState("");
// todo properly integrate loading
const events = useMemo( const { data, error, isLoading } = useSWR("/api/earthquakes", fetcher);
() => [
{
id: "1234",
title: "Earthquake in Germany",
text1: "Magnitude 8.5",
text2: "30 minutes ago",
magnitude: 8.5,
longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52,
},
{
id: "2134",
title: "Earthquake in California",
text1: "Magnitude 5.3",
text2: "2 hours ago",
magnitude: 5.3,
longitude: -122.4194, // Near San Francisco, California, USA
latitude: 37.7749,
},
{
id: "2314",
title: "Tremor in Japan",
text1: "Magnitude 4.7",
text2: "5 hours ago",
magnitude: 4.7,
longitude: 139.6917, // Near Tokyo, Japan
latitude: 35.6762,
},
{
id: "2341",
title: "Tremor in Spain",
text1: "Magnitude 2.1",
text2: "10 hours ago",
magnitude: 2.1,
longitude: -3.7038, // Near Madrid, Spain
latitude: 40.4168,
},
],
[]
);
return ( return (
<div className="h-full flex overflow-hidden"> <div className="h-full flex overflow-hidden">
<div className="flex-grow"> <div className="flex-grow">
<Map <Map
events={events} events={data ? data.earthquakes : []}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
@ -66,7 +29,7 @@ export default function Earthquakes() {
logTitle="Log an Earthquake" logTitle="Log an Earthquake"
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists" logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists"
recentsTitle="Recent Earthquakes" recentsTitle="Recent Earthquakes"
events={events} events={data ? data.earthquakes : []}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}

View File

@ -1,17 +1,29 @@
"use client";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Navbar from "@/components/navbar";
import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { action, createStore, StoreProvider } from "easy-peasy";
import { Inter } from "next/font/google";
import { StoreModel } from "@appTypes/StoreModel";
import Navbar from "@components/Navbar";
const inter = Inter({ const inter = Inter({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-inter", variable: "--font-inter",
}); });
export const metadata: Metadata = { const store = createStore<StoreModel>({
title: "Tremor Tracker", currency: {
description: "Generated by tim", selectedCurrency: "GBP",
}; setSelectedCurrency: action((state, payload) => {
state.selectedCurrency = payload;
}),
currencies: ["GBP", "USD", "EUR"],
conversionRates: { GBP: 1, USD: 1.33, EUR: 1.17 },
tickers: { GBP: "£", USD: "$", EUR: "€" },
},
});
export default function RootLayout({ export default function RootLayout({
children, children,
@ -20,10 +32,12 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<StoreProvider store={store}>
<body className={`${inter.variable} h-[calc(100vh-3.5rem)] flex flex-col min-h-screen antialiased`}> <body className={`${inter.variable} h-[calc(100vh-3.5rem)] flex flex-col min-h-screen antialiased`}>
<Navbar></Navbar> <Navbar></Navbar>
<div className="flex-1 overflow-y-auto">{children}</div> <div className="flex-1 overflow-y-auto">{children}</div>
</body> </body>
</StoreProvider>
</html> </html>
); );
} }

View File

@ -1,48 +1,23 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar"; import { useMemo, useState } from "react";
import Map from "@/components/map"; import useSWR from "swr";
import { useState, useMemo } from "react";
import Sidebar from "@/components/Sidebar";
import Map from "@components/Map";
import { fetcher } from "@utils/fetcher";
export default function Observatories() { export default function Observatories() {
const [selectedEventId, setSelectedEventId] = useState(""); const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState(""); const [hoveredEventId, setHoveredEventId] = useState("");
// todo properly integrate loading
const events = useMemo( const { data, error, isLoading } = useSWR("/api/earthquakes", fetcher);
() => [
{
id: "1234",
title: "Earthquake - Berlin Observatory",
text1: "Logged by ",
text2: "30 minutes ago",
longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52,
},
{
id: "2134",
title: "New Observatory - Phuket, Thailand",
text1: "Dr. Neil Armstrong",
text2: "2 weeks ago",
longitude: -122.4194,
latitude: 37.7749,
},
{
id: "2314",
title: "Observatory Scientist Change",
text1: "Dr. Samantha Green new lead scientist",
text2: "1 month ago",
longitude: 139.6917,
latitude: 35.6762,
},
],
[]
);
return ( return (
<div className="h-full flex overflow-hidden"> <div className="h-full flex overflow-hidden">
<div className="flex-grow"> <div className="flex-grow">
<Map <Map
events={events} events={data ? data.observatories : []}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
@ -54,7 +29,7 @@ export default function Observatories() {
logTitle="Observatory Mapping" logTitle="Observatory Mapping"
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes" logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
recentsTitle="Observatory Events" recentsTitle="Observatory Events"
events={events} events={data ? data.observatories : []}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}

View File

@ -1,46 +1,44 @@
"use client"; "use client";
//export default function Page()
const OurMission = () => { const OurMission = () => {
return ( return (
<div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center py-10"> <div
<div className="max-w-4xl mx-auto p-5 bg-white shadow-lg rounded-lg"> className="h-screen relative bg-fixed bg-cover bg-center text-white py-10"
style={{ backgroundImage: "url('destruction.jpg')" }}
>
{/* Overlay to Improve Text Readability */}
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
{/* Content Area */}
<div className="relative z-10 max-w-4xl mx-auto p-5 bg-white bg-opacity-90 shadow-lg rounded-lg">
<h1 className="text-3xl font-bold text-center text-gray-800 mb-6">Our Mission</h1> <h1 className="text-3xl font-bold text-center text-gray-800 mb-6">Our Mission</h1>
<p className="text-lg text-gray-600 leading-relaxed mb-4"> <p className="text-lg text-gray-600 leading-relaxed mb-4">
At <span className="font-semibold text-blue-600">Earthquake Awareness Initiative</span>, our mission is to help people worldwide prepare for and recover from earthquakes. Through education, research, and innovative technology, we work tirelessly to empower communities with the knowledge they need to stay safe before, during, and after seismic events. At <span className="font-semibold text-blue-600">Earthquake Awareness Initiative</span>, our mission is to help people
worldwide prepare for and recover from earthquakes. Through education, research, and innovative technology, we work
tirelessly to empower communities with the knowledge they need to stay safe before, during, and after seismic events.
</p> </p>
<p className="text-lg text-gray-600 leading-relaxed mb-4"> <p className="text-lg text-gray-600 leading-relaxed mb-4">
We aim to bridge the gap between scientific research and community awareness by providing resources, tools, and real-time updates for earthquake preparedness. Together, we aspire to save lives, mitigate impacts, and foster resilience against nature's powerful forces. We aim to bridge the gap between scientific research and community awareness by providing resources, tools, and
real-time updates for earthquake preparedness. Together, we aspire to save lives, mitigate impacts, and foster
resilience against nature's powerful forces.
</p> </p>
<div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6"> <div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6">
<div className="flex flex-col items-center p-4"> <div className="flex flex-col items-center p-10">
<img <img src="education.png" alt="Education Icon" className="h-20 w-20 mb-8" />
src="/images/education-icon.png"
alt="Education Icon"
className="h-16 w-16 mb-4"
/>
<h3 className="text-xl font-bold text-gray-700 mb-2">Education</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">Education</h3>
<p className="text-sm text-gray-500 text-center"> <p className="text-sm text-gray-500 text-center">
Providing accessible resources to educate people about earthquake preparedness. Providing accessible resources to educate people about earthquake preparedness.
</p> </p>
</div> </div>
<div className="flex flex-col items-center p-4"> <div className="flex flex-col items-center p-10">
<img <img src="research.jpg" alt="Research Icon" className="h-20 w-20 mb-4" />
src="/images/research-icon.png"
alt="Research Icon"
className="h-16 w-16 mb-4"
/>
<h3 className="text-xl font-bold text-gray-700 mb-2">Research</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">Research</h3>
<p className="text-sm text-gray-500 text-center"> <p className="text-sm text-gray-500 text-center">
Supporting scientific studies to enhance understanding of seismic activity. Supporting scientific studies to enhance understanding of seismic activity.
</p> </p>
</div> </div>
<div className="flex flex-col items-center p-4"> <div className="flex flex-col items-center p-10">
<img <img src="tech.jpg" alt="Technology Icon" className="h-20 w-20 mb-8" />
src="/images/technology-icon.png"
alt="Technology Icon"
className="h-16 w-16 mb-4"
/>
<h3 className="text-xl font-bold text-gray-700 mb-2">Technology</h3> <h3 className="text-xl font-bold text-gray-700 mb-2">Technology</h3>
<p className="text-sm text-gray-500 text-center"> <p className="text-sm text-gray-500 text-center">
Leveraging innovation to deliver real-time alerts and safety tools. Leveraging innovation to deliver real-time alerts and safety tools.
@ -50,6 +48,46 @@ const OurMission = () => {
</div> </div>
</div> </div>
); );
return (
<div className="min-h-screen bg-neutral-100 flex flex-col items-center justify-center py-10">
<div className="max-w-4xl mx-auto p-5 bg-white shadow-lg rounded-lg">
<h1 className="text-3xl font-bold text-center text-neutral-800 mb-6">Our Mission</h1>
<p className="text-lg text-neutral-600 leading-relaxed mb-4">
At <span className="font-semibold text-blue-600">Earthquake Awareness Initiative</span>, our mission is to help people
worldwide prepare for and recover from earthquakes. Through education, research, and innovative technology, we work
tirelessly to empower communities with the knowledge they need to stay safe before, during, and after seismic events.
</p>
<p className="text-lg text-neutral-600 leading-relaxed mb-4">
We aim to bridge the gap between scientific research and community awareness by providing resources, tools, and
real-time updates for earthquake preparedness. Together, we aspire to save lives, mitigate impacts, and foster
resilience against nature's powerful forces.
</p>
<div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6">
<div className="flex flex-col items-center p-4">
<img src="/images/education-icon.png" alt="Education Icon" className="h-16 w-16 mb-4" />
<h3 className="text-xl font-bold text-neutral-700 mb-2">Education</h3>
<p className="text-sm text-neutral-500 text-center">
Providing accessible resources to educate people about earthquake preparedness.
</p>
</div>
<div className="flex flex-col items-center p-4">
<img src="/images/research-icon.png" alt="Research Icon" className="h-16 w-16 mb-4" />
<h3 className="text-xl font-bold text-neutral-700 mb-2">Research</h3>
<p className="text-sm text-neutral-500 text-center">
Supporting scientific studies to enhance understanding of seismic activity.
</p>
</div>
<div className="flex flex-col items-center p-4">
<img src="/images/technology-icon.png" alt="Technology Icon" className="h-16 w-16 mb-4" />
<h3 className="text-xl font-bold text-neutral-700 mb-2">Technology</h3>
<p className="text-sm text-neutral-500 text-center">
Leveraging innovation to deliver real-time alerts and safety tools.
</p>
</div>
</div>
</div>
</div>
);
}; };
export default OurMission; export default OurMission;

View File

@ -13,8 +13,6 @@ export default function Home() {
</div> </div>
</div> </div>
<div className="mx-auto mt-20 w-4/6 px-2 border border-black divide-y"> <div className="mx-auto mt-20 w-4/6 px-2 border border-black divide-y">
{["Earthquake 1", "Earthquake 2", "Earthquake 3"].map((name) => ( {["Earthquake 1", "Earthquake 2", "Earthquake 3"].map((name) => (
<div className="px-5 py-5" key={name}> <div className="px-5 py-5" key={name}>

View File

@ -1,115 +1,151 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar"; import Image from "next/image";
import { useState } from "react"; import { Dispatch, SetStateAction, useCallback, useState } from "react";
import Artifact from "@appTypes/Artifact";
import { Currency } from "@appTypes/StoreModel";
import Sidebar from "@components/Sidebar";
import { useStoreState } from "@hooks/store";
// Artifacts Data // Artifacts Data
const artifacts = [ const artifacts: Artifact[] = [
{ id: 1, name: "Golden Scarab", description: "An ancient Egyptian artifact symbolizing rebirth.", location: "Cairo, Egypt", image: "/images/artifact1.jpg", price: 150 }, {
{ id: 2, name: "Aztec Sunstone", description: "A replica of the Aztec calendar (inscriptions intact).", location: "Peru", image: "/images/artifact2.jpg", price: 200 }, id: 1,
{ id: 3, name: "Medieval Chalice", description: "Used by royalty in medieval ceremonies.", location: "Cambridge, England", image: "/images/artifact3.jpg", price: 120 }, name: "Golden Scarab",
{ id: 4, name: "Roman Coin", description: "An authentic Roman coin from the 2nd century CE.", location: "Rome, Italy", image: "/images/artifact4.jpg", price: 80 }, description: "An ancient Egyptian artifact symbolizing rebirth.",
{ id: 5, name: "Samurai Mask", description: "Replica of Japanese Samurai battle masks.", location: "Tokyo, Japan", image: "/images/artifact5.jpg", price: 300 }, location: "Cairo, Egypt",
{ id: 6, name: "Ancient Greek Vase", description: "Depicts Greek mythology, found in the Acropolis.", location: "Athens, Greece", image: "/images/artifact6.jpg", price: 250 }, image: "/artifact1.jpg",
{ id: 7, name: "Incan Pendant", description: "Represents the Sun God Inti.", location: "India", image: "/images/artifact7.jpg", price: 175 }, price: 150,
{ id: 8, name: "Persian Carpet Fragment", description: "Ancient Persian artistry.", location: "Petra, Jordan", image: "/images/artifact8.jpg", price: 400 }, },
{ id: 9, name: "Stone Buddha", description: "Authentic stone Buddha carving.", location: "India", image: "/images/artifact9.jpg", price: 220 }, {
{ id: 10, name: "Victorian Brooch", description: "A beautiful Victorian-era brooch with a ruby centre.", location: "Oxford, England", image: "/images/artifact10.jpg", price: 150 }, id: 2,
{ id: 11, name: "Ancient Scroll", description: "A mysterious scroll from ancient times.", location: "Madrid, Spain", image: "/images/artifact11.jpg", price: 500 }, name: "Aztec Sunstone",
{ id: 12, name: "Ming Dynasty Porcelain", description: "Porcelain from China's Ming Dynasty.", location: "Beijing, China", image: "/images/artifact12.jpg", price: 300 }, description: "A replica of the Aztec calendar (inscriptions intact).",
{ id: 13, name: "African Tribal Mask", description: "A unique tribal mask from Africa.", location: "Nigeria", image: "/images/artifact13.jpg", price: 250 }, location: "Peru",
{ id: 14, name: "Crystal Skull", description: "A mystical pre-Columbian artifact.", location: "Colombia", image: "/images/artifact14.jpg", price: 1000 }, image: "/artifact2.jpg",
{ id: 15, name: "Medieval Armor Fragment", description: "A fragment of medieval armor.", location: "Normandy, France", image: "/images/artifact15.jpg", price: 400 }, price: 200,
},
{
id: 3,
name: "Medieval Chalice",
description: "Used by royalty in medieval ceremonies.",
location: "Cambridge, England",
image: "/artifact3.jpg",
price: 120,
},
{
id: 4,
name: "Roman Coin",
description: "An authentic Roman coin from the 2nd century CE.",
location: "Rome, Italy",
image: "/artifact4.jpg",
price: 80,
},
{
id: 5,
name: "Samurai Mask",
description: "Replica of Japanese Samurai battle masks.",
location: "Tokyo, Japan",
image: "/artifact5.jpg",
price: 300,
},
{
id: 6,
name: "Ancient Greek Vase",
description: "Depicts Greek mythology, found in the Acropolis.",
location: "Athens, Greece",
image: "/artifact6.jpg",
price: 250,
},
{
id: 7,
name: "Incan Pendant",
description: "Represents the Sun God Inti.",
location: "India",
image: "/artifact7.jpg",
price: 175,
},
{
id: 8,
name: "Persian Carpet Fragment",
description: "Ancient Persian artistry.",
location: "Petra, Jordan",
image: "/artifact8.jpg",
price: 400,
},
{
id: 9,
name: "Stone Buddha",
description: "Authentic stone Buddha carving.",
location: "India",
image: "/artifact9.jpg",
price: 220,
},
{
id: 10,
name: "Victorian Brooch",
description: "A beautiful Victorian-era brooch with a ruby centre.",
location: "Oxford, England",
image: "/artifact10.jpg",
price: 150,
},
{
id: 11,
name: "Ancient Scroll",
description: "A mysterious scroll from ancient times.",
location: "Madrid, Spain",
image: "/artifact11.jpg",
price: 500,
},
{
id: 12,
name: "Ming Dynasty Porcelain",
description: "Porcelain from China's Ming Dynasty.",
location: "Beijing, China",
image: "/artifact12.jpg",
price: 300,
},
{
id: 13,
name: "African Tribal Mask",
description: "A unique tribal mask from Africa.",
location: "Nigeria",
image: "/artifact13.jpg",
price: 250,
},
{
id: 14,
name: "Crystal Skull",
description: "A mystical pre-Columbian artifact.",
location: "Colombia",
image: "/artifact14.jpg",
price: 1000,
},
{
id: 15,
name: "Medieval Armor Fragment",
description: "A fragment of medieval armor.",
location: "Normandy, France",
image: "/artifact15.jpg",
price: 400,
},
]; ];
// Modal Component // Modal Component
const Modal = ({ artifact, onClose }) => {
if (!artifact) return null;
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div
className="fixed inset-0 bg-gray-900 bg-opacity-50 flex justify-center items-center z-50"
onClick={handleOverlayClick}>
<div className="bg-white rounded-md shadow-lg max-w-lg w-full p-6">
<h3 className="text-xl font-bold mb-4">{artifact.name}</h3>
<img src={artifact.image} alt={artifact.name} className="w-full h-64 object-cover rounded-md mb-4" />
<p className="text-gray-600 mb-2">{artifact.description}</p>
<p className="text-gray-500 font-bold mb-4">Location: {artifact.location}</p>
<p className="text-red-600 font-bold">Price: ${artifact.price}</p>
<div className="flex justify-end gap-4 mt-6">
<button
onClick={() => alert("Reserved Successfully!")}
className="px-4 py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-400">
Reserve
</button>
<button
onClick={() => alert("Purchased Successfully!")}
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-500">
Buy
</button>
<button
onClick={onClose}
className="px-4 py-2 bg-gray-300 rounded-md hover:bg-gray-400">
Close
</button>
</div>
</div>
</div>
);
};
// ArtifactCard Component
const ArtifactCard = ({ artifact, onSelect }) => {
const [selectedCurrency, setSelectedCurrency] = useState("USD");
const conversionRates = { USD: 1, EUR: 0.94, GBP: 0.81 };
const convertPrice = (price, currency) => (price * conversionRates[currency]).toFixed(2);
const handleCurrencyChange = (e) => {
setSelectedCurrency(e.target.value); // Update selected currency
e.stopPropagation(); // Prevent the modal from opening on dropdown click
};
return (
<div
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
onClick={() => onSelect(artifact)} // Opens modal
>
<img src={artifact.image} alt={artifact.name} className="w-full h-56 object-cover" />
<div className="p-4">
<h3 className="text-lg font-bold">{artifact.name}</h3>
<p className="text-gray-700 mb-2">{artifact.description}</p>
<p className="text-gray-500 mb-2">{artifact.location}</p>
<p className="text-red-600 font-bold text-md mt-4">
{selectedCurrency}: {convertPrice(artifact.price, selectedCurrency)}
</p>
<select
value={selectedCurrency}
onChange={handleCurrencyChange} // Handles currency change
className="border border-gray-300 rounded-lg px-3 py-1 text-sm items-left"
onClick={(e) => e.stopPropagation()} // Prevents triggering the modal
>
<option value="USD">USD ($)</option>
<option value="EUR">EUR ()</option>
<option value="GBP">GBP (£)</option>
</select>
</div>
</div>
);
};
// Shop Component // Shop Component
export default function Shop() { export default function Shop() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [selectedArtifact, setSelectedArtifact] = useState(null); // Track selected artifact for modal const [selectedArtifact, setSelectedArtifact] = useState<Artifact | null>(null); // Track selected artifact for modal
const artifactsPerPage = 9; // Number of artifacts per page const artifactsPerPage = 9; // Number of artifacts per page
const indexOfLastArtifact = currentPage * artifactsPerPage; const indexOfLastArtifact = currentPage * artifactsPerPage;
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage; const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
const currentArtifacts = artifacts.slice(indexOfFirstArtifact, indexOfLastArtifact); const currentArtifacts = artifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
const conversionRates = useStoreState((state) => state.currency.conversionRates);
const currencyTickers = useStoreState((state) => state.currency.tickers);
const convertPrice = useCallback((price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2), []);
const handleNextPage = () => { const handleNextPage = () => {
if (indexOfLastArtifact < artifacts.length) { if (indexOfLastArtifact < artifacts.length) {
setCurrentPage((prev) => prev + 1); setCurrentPage((prev) => prev + 1);
@ -122,64 +158,105 @@ export default function Shop() {
} }
}; };
const handleSelectArtifact = (artifact) => { function Modal({ artifact }: { artifact: Artifact }) {
setSelectedArtifact(artifact); // Open modal with selected artifact if (!artifact) return null;
};
const handleCloseModal = () => { const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
setSelectedArtifact(null); // Close modal if (e.target === e.currentTarget) {
setSelectedArtifact(null);
}
}; };
return ( return (
<div className="flex flex-col min-h-screen bg-gray-100"> <div
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
onClick={handleOverlayClick}
>
<div className="bg-white rounded-xl shadow-2xl max-w-lg w-full p-6">
<h3 className="text-2xl font-bold mb-4">{artifact.name}</h3>
<Image
height={5000}
width={5000}
src={artifact.image}
alt={artifact.name}
className="w-full h-64 object-cover rounded-md"
></Image>
<p className="text-xl font-bold">
{currencyTickers[selectedCurrency]}
{convertPrice(artifact.price, selectedCurrency)}
</p>
<p className="text-neutral-600 mt-2">{artifact.description}</p>
<p className="text-neutral-500 font-bold mt-1">Location: {artifact.location}</p>
<div className="flex justify-end gap-4 mt-4 mr-2">
<button
onClick={() => alert("Purchased Successfully!")}
className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Buy
</button>
</div>
</div>
</div>
);
}
// ArtifactCard Component
function ArtifactCard({ artifact }: { artifact: Artifact }) {
return (
<div
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
onClick={() => setSelectedArtifact(artifact)} // Opens modal
>
<img src={artifact.image} alt={artifact.name} className="w-full h-56 object-cover" />
<div className="p-4">
<h3 className="text-lg font-semibold">{artifact.name}</h3>
<p className="text-neutral-500 mb-2">{artifact.location}</p>
<p className="text-black font-bold text-md mt-2">
{currencyTickers[selectedCurrency]}
{convertPrice(artifact.price, selectedCurrency)}
</p>
</div>
</div>
);
}
return (
<div className="flex flex-col min-h-screen bg-neutral-100">
{/* Main Content */} {/* Main Content */}
<div className="flex flex-1"> <div className="flex flex-1 overflow-y-auto">
{/* Artifact Grid */} {/* Artifact Grid */}
<div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 p-6 pr-72"> <div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 p-6">
{currentArtifacts.map((artifact) => ( {currentArtifacts.map((artifact) => (
<ArtifactCard key={artifact.id} artifact={artifact} onSelect={handleSelectArtifact} /> <ArtifactCard key={artifact.id} artifact={artifact} />
))} ))}
</div> </div>
{/* Sidebar */}
<aside className="w-72 bg-white shadow-lg p-4 h-screen fixed top-10 right-0 overflow-auto">
<Sidebar
logTitle="Artifact Collection"
logSubtitle="Record new artifacts - name, description, image, location and price"
recentsTitle="Recent Updates"
events={[/* example events if needed */]}
selectedEventId=""
setSelectedEventId={() => {}}
hoveredEventId=""
setHoveredEventId={() => {}}
button1Name="Add New Artifact"
button2Name="Search Artifacts"
/>
</aside>
</div> </div>
{/* Pagination Footer */} {/* Pagination Footer */}
<footer className="bg-white border-t border-gray-300 py-2 text-center w-full flex justify-center items-centre"> <footer className="mt-10 bg-white border-t border-neutral-300 py-3 text-center flex justify-center items-center">
<button <button
onClick={handlePreviousPage} onClick={handlePreviousPage}
disabled={currentPage === 1} disabled={currentPage === 1}
className={`mx-2 px-4 py-1 bg-blue-700 text-white rounded-md font-bold shadow-md ${ className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
currentPage === 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600" currentPage === 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
}`} > }`}
>
&larr; Previous &larr; Previous
</button> </button>
<p className="mx-3 text-lg font-bold">{currentPage}</p>
<button <button
onClick={handleNextPage} onClick={handleNextPage}
disabled={indexOfLastArtifact >= artifacts.length} disabled={indexOfLastArtifact >= artifacts.length}
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${ className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
indexOfLastArtifact >= artifacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600" indexOfLastArtifact >= artifacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
}`} > }`}
>
Next &rarr; Next &rarr;
</button> </button>
</footer> </footer>
{/* Modal */} {/* Modal */}
<Modal artifact={selectedArtifact} onClose={handleCloseModal} /> {selectedArtifact && <Modal artifact={selectedArtifact} />}
</div> </div>
); );
} }

View File

@ -1,75 +0,0 @@
/* Container styling */
.container {
font-family: Arial, sans-serif;
padding: 20px;
text-align: center;
color: #333;
background-color: #f9f9f9;
}
/* Header styling */
.header {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 10px;
}
.subheader {
font-size: 1.2rem;
color: #555;
margin-bottom: 30px;
}
/* Team styling */
.team {
display: flex;
flex-direction: column;
gap: 20px;
align-items: center; /* Centers team member cards */
}
/* Individual team member card styling */
.card {
display: flex;
align-items: center;
border: 1px solid #ccc;
border-radius: 8px;
width: 90%; /* Width relative to the viewport */
max-width: 800px; /* Prevent it from getting too large */
padding: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
/* Team member image styling */
.image {
width: 110px; /* Both width and height are equal */
height: 110px;
border-radius: 50%; /* Makes the image circular */
object-fit: cover; /* Ensures the image fits perfectly without distortion */
}
/* Card text styling */
.text {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.name {
font-size: 1.5rem;
font-weight: bold;
color: #111;
margin-bottom: 5px;
}
.title {
font-size: 1.2rem;
color: #555;
margin-bottom: 10px;
}
.description {
font-size: 1rem;
color: #777;
}

View File

@ -1,57 +1,843 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { FaCalendarPlus, FaWarehouse, FaCartShopping } from "react-icons/fa6";
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5";
import { FaTimes } from "react-icons/fa";
import { SetStateAction, Dispatch } from "react";
export default function Warehouse() { // Artifact type
const [selectedEventId, setSelectedEventId] = useState(""); interface Artifact {
const [hoveredEventId, setHoveredEventId] = useState(""); id: number;
const events = useMemo( name: string;
() => [ description: string;
{ location: string;
id: "1234", earthquakeId: string;
title: "Artifact found - Germany Earthquake", isRequired: boolean;
text1: "Mortar and pestle", isSold: boolean;
text2: "10 minutes ago", isCollected: boolean;
longitude: 10.4515, // Near Berlin, Germany dateAdded: string;
latitude: 52.52, }
},
{
id: "2134",
title: "All artifacts from California earthquake transported",
text1: "India to London",
text2: "15 hours ago",
longitude: -122.4194, // Near San Francisco, California, USA
latitude: 37.7749,
},
{
id: "2341",
title: "7 artifacts destroyed - Spain earthquake",
text1: "edVases and pots from Madrid",
text2: "3 weeks ago",
longitude: -3.7038, // Near Madrid, Spain
latitude: 40.4168,
},
],
[]
);
// Warehouse Artifacts Data
const warehouseArtifacts: Artifact[] = [
{
id: 1,
name: "Solidified Lava Chunk",
description: "A chunk of solidified lava from the 2023 Iceland eruption.",
location: "Reykjanes, Iceland",
earthquakeId: "EQ2023ICL",
isRequired: true,
isSold: false,
isCollected: false,
dateAdded: "2025-05-04",
},
{
id: 2,
name: "Tephra Sample",
description: "Foreign debris from the 2022 Tonga volcanic eruption.",
location: "Tonga",
earthquakeId: "EQ2022TGA",
isRequired: false,
isSold: true,
isCollected: true,
dateAdded: "2025-05-03",
},
{
id: 3,
name: "Ash Sample",
description: "Volcanic ash from the 2021 La Palma eruption.",
location: "La Palma, Spain",
earthquakeId: "EQ2021LPA",
isRequired: false,
isSold: false,
isCollected: false,
dateAdded: "2025-05-04",
},
{
id: 4,
name: "Ground Soil",
description: "Soil sample from the 2020 Croatia earthquake site.",
location: "Zagreb, Croatia",
earthquakeId: "EQ2020CRO",
isRequired: true,
isSold: false,
isCollected: false,
dateAdded: "2025-05-02",
},
{
id: 5,
name: "Basalt Fragment",
description: "Basalt rock from the 2019 New Zealand eruption.",
location: "White Island, New Zealand",
earthquakeId: "EQ2019NZL",
isRequired: false,
isSold: true,
isCollected: false,
dateAdded: "2025-05-04",
},
];
// Filter Component
function FilterInput({
value,
onChange,
type = "text",
options,
}: {
value: string;
onChange: (value: string) => void;
type?: string;
options?: string[];
}) {
const showSelectedFilter = type === "text" && !["true", "false"].includes(options?.at(-1)!);
return ( return (
<div className="w-full h-full"> <div className="flex h-full pl-0.5 pr-1 items-center group">
<p> <div className="relative">
warehouse image pasted in here <div
</p> className={`p-1 group-hover:bg-blue-100 rounded transition-colors duration-200 ${
<Sidebar !showSelectedFilter && value && "bg-blue-100"
logTitle="Artifact Retrieval and Tracking" }`}
logSubtitle="Record new artifacts collected - name, description, time/date found, location found, scientist and collection stage" >
recentsTitle="Artifact News" <IoFilter
events={events} className={`cursor-pointer text-neutral-500 font-bold group-hover:text-blue-600
selectedEventId={selectedEventId} ${!showSelectedFilter && value && "text-blue-600"}
setSelectedEventId={setSelectedEventId} `}
hoveredEventId={hoveredEventId} />
setHoveredEventId={setHoveredEventId} </div>
button1Name="Log New Artifacts" <div
button2Name="Search Artifacts" className={`absolute z-50 mt-2 w-48 bg-white border border-neutral-300 rounded-md shadow-lg p-2 opacity-0 group-hover:opacity-100 group-hover:visible transition-opacity duration-200 pointer-events-none group-hover:pointer-events-auto
></Sidebar> ${type === "date" ? "-right-1/2" : "-left-1/2"}
`}
>
{options ? (
<div className="max-h-32 overflow-y-auto">
{options.map((opt) => (
<div key={opt} className="p-1 hover:bg-blue-100 cursor-pointer text-sm" onClick={() => onChange(opt)}>
{opt ? (opt === "true" ? "Yes" : "No") : "All"}
</div>
))}
</div>
) : (
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full p-1 border border-neutral-300 rounded-md text-sm"
placeholder="Filter..."
aria-label="Filter input"
/>
)}
</div>
</div>
{value && showSelectedFilter && (
<div className="inline-flex items-center bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-md">
{value === "true" ? "Yes" : value === "false" ? "No" : value}
<FaTimes className="ml-1 cursor-pointer text-blue-600 hover:text-blue-800" onClick={() => onChange("")} />
</div>
)}
</div>
);
}
// Table Component
function ArtifactTable({
artifacts,
filters,
setFilters,
setEditArtifact,
clearSort,
}: {
artifacts: Artifact[];
filters: Record<string, string>;
setFilters: Dispatch<
SetStateAction<{
id: string;
name: string;
earthquakeId: string;
location: string;
description: string;
isRequired: string;
isSold: string;
isCollected: string;
dateAdded: string;
}>
>;
setEditArtifact: (artifact: Artifact) => void;
clearSort: () => void;
}) {
const [sortConfig, setSortConfig] = useState<{
key: keyof Artifact;
direction: "asc" | "desc";
} | null>(null);
const handleSort = (key: keyof Artifact) => {
setSortConfig((prev) => {
if (!prev || prev.key !== key) {
return { key, direction: "asc" };
} else if (prev.direction === "asc") {
return { key, direction: "desc" };
}
return null;
});
};
const clearSortConfig = () => {
setSortConfig(null);
clearSort();
};
const sortedArtifacts = useMemo(() => {
if (!sortConfig) return artifacts;
const sorted = [...artifacts].sort((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1;
return 0;
});
return sorted;
}, [artifacts, sortConfig]);
const columns: { label: string; key: keyof Artifact; width: string }[] = [
{ label: "ID", key: "id", width: "5%" },
{ label: "Name", key: "name", width: "12%" },
{ label: "Earthquake ID", key: "earthquakeId", width: "10%" },
{ label: "Location", key: "location", width: "12%" },
{ label: "Description", key: "description", width: "25%" },
{ label: "Required", key: "isRequired", width: "6%" },
{ label: "Sold", key: "isSold", width: "5%" },
{ label: "Collected", key: "isCollected", width: "7%" },
{ label: "Date Added", key: "dateAdded", width: "8%" },
];
return (
<table className="w-full table-fixed text-left">
<thead className="sticky top-0 bg-neutral-100 border-b border-neutral-200 z-10">
<tr>
{columns.map(({ label, key, width }) => (
<th key={key} className="text-sm px-5 font-semibold text-neutral-800 cursor-pointer" style={{ width }}>
<div className="flex h-11 items-center">
<div className="flex h-full items-center" onClick={() => handleSort(key as keyof Artifact)}>
<div className="select-none">{label}</div>
</div>
<div className="h-full relative">
<FilterInput
value={filters[key]}
onChange={(value) => {
setFilters({ ...filters, [key]: value } as {
id: string;
name: string;
earthquakeId: string;
location: string;
description: string;
isRequired: string;
isSold: string;
isCollected: string;
dateAdded: string;
});
if (value === "") clearSortConfig();
}}
type={key === "dateAdded" ? "date" : "text"}
options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined}
/>
{sortConfig?.key === key && (
<div className="absolute -right-2 top-3">{sortConfig.direction === "asc" ? "↑" : "↓"}</div>
)}
</div>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedArtifacts.map((artifact) => (
<tr
key={artifact.id}
className="border-b border-neutral-200 hover:bg-neutral-100 cursor-pointer"
onClick={() => setEditArtifact(artifact)}
>
{columns.map(({ key, width }) => (
<td
key={key}
className={`py-3 px-5 text-sm text-neutral-600 truncate ${key === "name" && "font-medium text-neutral-800"}`}
style={{ width }}
>
{key === "isRequired"
? artifact.isRequired
? "Yes"
: "No"
: key === "isSold"
? artifact.isSold
? "Yes"
: "No"
: key === "isCollected"
? artifact.isCollected
? "Yes"
: "No"
: artifact[key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Modal Component for Logging Artifact
function LogModal({ onClose }: { onClose: () => void }) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [location, setLocation] = useState("");
const [earthquakeId, setEarthquakeId] = useState("");
const [storageLocation, setStorageLocation] = useState("");
const [isRequired, setIsRequired] = useState(true);
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) {
onClose();
}
};
const handleLog = async () => {
if (!name || !description || !location || !earthquakeId || !storageLocation) {
setError("All fields are required.");
return;
}
setIsSubmitting(true);
try {
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
alert(`Logged ${name} to storage: ${storageLocation}`);
onClose();
} catch {
setError("Failed to log artifact. Please try again.");
} finally {
setIsSubmitting(false);
}
};
return (
<div
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
onClick={handleOverlayClick}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log New Artifact</h3>
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
<div className="space-y-2">
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Artifact Name"
disabled={isSubmitting}
/>
<textarea
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-16"
aria-label="Artifact Description"
disabled={isSubmitting}
/>
<input
type="text"
placeholder="Location"
value={location}
onChange={(e) => setLocation(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Artifact Location"
disabled={isSubmitting}
/>
<input
type="text"
placeholder="Earthquake ID"
value={earthquakeId}
onChange={(e) => setEarthquakeId(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Earthquake ID"
disabled={isSubmitting}
/>
<input
type="text"
placeholder="Storage Location (e.g., A-12)"
value={storageLocation}
onChange={(e) => setStorageLocation(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Storage Location"
disabled={isSubmitting}
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={isRequired}
onChange={(e) => setIsRequired(e.target.checked)}
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
aria-label="Required Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
</div>
</div>
<div className="flex justify-end gap-3 mt-4">
<button
onClick={onClose}
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
disabled={isSubmitting}
>
Cancel
</button>
<button
onClick={handleLog}
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
}`}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<svg
className="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Logging...
</>
) : (
"Log Artifact"
)}
</button>
</div>
</div>
</div>
);
}
// Modal Component for Bulk Logging
function BulkLogModal({ onClose }: { onClose: () => void }) {
const [palletNote, setPalletNote] = useState("");
const [storageLocation, setStorageLocation] = useState("");
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) {
onClose();
}
};
const handleLog = async () => {
if (!palletNote || !storageLocation) {
setError("All fields are required.");
return;
}
setIsSubmitting(true);
try {
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
alert(`Logged bulk pallet to storage: ${storageLocation}`);
onClose();
} catch {
setError("Failed to log pallet. Please try again.");
} finally {
setIsSubmitting(false);
}
};
return (
<div
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
onClick={handleOverlayClick}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log Bulk Pallet</h3>
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
<div className="space-y-2">
<textarea
placeholder="Pallet Delivery Note (e.g., 10 lava chunks, 5 ash samples)"
value={palletNote}
onChange={(e) => setPalletNote(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-24"
aria-label="Pallet Delivery Note"
disabled={isSubmitting}
/>
<input
type="text"
placeholder="Storage Location (e.g., B-05)"
value={storageLocation}
onChange={(e) => setStorageLocation(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Storage Location"
disabled={isSubmitting}
/>
</div>
<div className="flex justify-end gap-3 mt-4">
<button
onClick={onClose}
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
disabled={isSubmitting}
>
Cancel
</button>
<button
onClick={handleLog}
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
}`}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<svg
className="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Logging...
</>
) : (
"Log Pallet"
)}
</button>
</div>
</div>
</div>
);
}
// Modal Component for Editing Artifact
function EditModal({ artifact, onClose }: { artifact: Artifact; onClose: () => void }) {
const [name, setName] = useState(artifact.name);
const [description, setDescription] = useState(artifact.description);
const [location, setLocation] = useState(artifact.location);
const [earthquakeId, setEarthquakeId] = useState(artifact.earthquakeId);
const [isRequired, setIsRequired] = useState(artifact.isRequired);
const [isSold, setIsSold] = useState(artifact.isSold);
const [isCollected, setIsCollected] = useState(artifact.isCollected);
const [dateAdded, setDateAdded] = useState(artifact.dateAdded);
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) {
onClose();
}
};
const handleSave = async () => {
if (!name || !description || !location || !earthquakeId || !dateAdded) {
setError("All fields are required.");
return;
}
setIsSubmitting(true);
try {
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
alert(`Updated artifact ${name}`);
onClose();
} catch {
setError("Failed to update artifact. Please try again.");
} finally {
setIsSubmitting(false);
}
};
return (
<div
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
onClick={handleOverlayClick}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Edit Artifact</h3>
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
<div className="space-y-2">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Artifact Name"
disabled={isSubmitting}
/>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-16"
aria-label="Artifact Description"
disabled={isSubmitting}
/>
<input
type="text"
value={location}
onChange={(e) => setLocation(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Artifact Location"
disabled={isSubmitting}
/>
<input
type="text"
value={earthquakeId}
onChange={(e) => setEarthquakeId(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
aria-label="Earthquake ID"
disabled={isSubmitting}
/>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={isRequired}
onChange={(e) => setIsRequired(e.target.checked)}
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
aria-label="Required Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={isSold}
onChange={(e) => setIsSold(e.target.checked)}
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
aria-label="Sold Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Sold</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={isCollected}
onChange={(e) => setIsCollected(e.target.checked)}
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
aria-label="Collected Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Collected</label>
</div>
<input
type="date"
value={dateAdded}
onChange={(e) => setDateAdded(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md focus:ring-2 focus:ring-blue-500"
aria-label="Date Added"
disabled={isSubmitting}
/>
</div>
<div className="flex justify-end gap-3 mt-4">
<button
onClick={onClose}
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
disabled={isSubmitting}
>
Cancel
</button>
<button
onClick={handleSave}
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
}`}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<svg
className="animate-spin h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Saving...
</>
) : (
"Save"
)}
</button>
</div>
</div>
</div>
);
}
// Filter Logic
const applyFilters = (artifacts: Artifact[], filters: Record<string, string>): Artifact[] => {
return artifacts.filter((artifact) => {
return (
(filters.id === "" || artifact.id.toString().includes(filters.id)) &&
(filters.name === "" || artifact.name.toLowerCase().includes(filters.name.toLowerCase())) &&
(filters.earthquakeId === "" || artifact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) &&
(filters.location === "" || artifact.location.toLowerCase().includes(filters.location.toLowerCase())) &&
(filters.description === "" || artifact.description.toLowerCase().includes(filters.description.toLowerCase())) &&
(filters.isRequired === "" || (filters.isRequired === "true" ? artifact.isRequired : !artifact.isRequired)) &&
(filters.isSold === "" || (filters.isSold === "true" ? artifact.isSold : !artifact.isSold)) &&
(filters.isCollected === "" || (filters.isCollected === "true" ? artifact.isCollected : !artifact.isCollected)) &&
(filters.dateAdded === "" || artifact.dateAdded === filters.dateAdded)
);
});
};
// Warehouse Component
export default function Warehouse() {
const [currentPage, setCurrentPage] = useState(1);
const [showLogModal, setShowLogModal] = useState(false);
const [showBulkLogModal, setShowBulkLogModal] = useState(false);
const [editArtifact, setEditArtifact] = useState<Artifact | null>(null);
const [filters, setFilters] = useState({
id: "",
name: "",
earthquakeId: "",
location: "",
description: "",
isRequired: "",
isSold: "",
isCollected: "",
dateAdded: "",
});
const [isFiltering, setIsFiltering] = useState(false);
const [sortConfig, setSortConfig] = useState<{
key: keyof Artifact;
direction: "asc" | "desc";
} | null>(null);
const artifactsPerPage = 10;
const indexOfLastArtifact = currentPage * artifactsPerPage;
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
// Apply filters with loading state
const filteredArtifacts = useMemo(() => {
setIsFiltering(true);
const result = applyFilters(warehouseArtifacts, filters);
setIsFiltering(false);
return result;
}, [filters]);
const currentArtifacts = filteredArtifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
// Overview stats
const totalArtifacts = warehouseArtifacts.length;
const today = "2025-05-04";
const artifactsAddedToday = warehouseArtifacts.filter((a) => a.dateAdded === today).length;
const artifactsSoldToday = warehouseArtifacts.filter((a) => a.isSold && a.dateAdded === today).length;
const clearFilters = () => {
setFilters({
id: "",
name: "",
earthquakeId: "",
location: "",
description: "",
isRequired: "",
isSold: "",
isCollected: "",
dateAdded: "",
});
setSortConfig(null); // Clear sorting
};
return (
<div className="flex flex-col h-full bg-neutral-50">
{/* Main Content */}
<div className="flex flex-1 p-5">
<div className="flex-grow flex flex-col">
{/* Overview Stats */}
<div className="flex gap-8 ml-5 mt-1">
<div className="flex items-center text-md text-neutral-600">
<FaWarehouse className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
Total Artifacts: <span className="font-semibold ml-1">{totalArtifacts}</span>
</div>
<div className="flex items-center text-md text-neutral-600">
<IoToday className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
Added Today: <span className="font-semibold ml-1">{artifactsAddedToday}</span>
</div>
<div className="flex items-center text-md text-neutral-600">
<FaCartShopping className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
Sold Today: <span className="font-semibold ml-1">{artifactsSoldToday}</span>
</div>
</div>
{/* Logging Buttons */}
<div className="flex justify-end gap-3 mb-4">
<button
onClick={clearFilters}
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
>
Clear All Filters
</button>
<button
onClick={() => setShowLogModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log Single Artifact
</button>
<button
onClick={() => setShowBulkLogModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log Bulk Pallet
</button>
</div>
{/* Table Card */}
<div className="flex-grow bg-white rounded-lg shadow-md border border-neutral-200 overflow-hidden relative">
{isFiltering && (
<div className="absolute inset-0 bg-white bg-opacity-50 flex items-center justify-center z-20">
<svg
className="animate-spin h-8 w-8 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
)}
<div className="h-full overflow-y-none">
<ArtifactTable
artifacts={currentArtifacts}
filters={filters}
setFilters={setFilters}
setEditArtifact={setEditArtifact}
clearSort={() => setSortConfig(null)}
/>
</div>
</div>
</div>
</div>
{/* Modals */}
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
{editArtifact && <EditModal artifact={editArtifact} onClose={() => setEditArtifact(null)} />}
</div> </div>
); );
} }

View File

@ -1,105 +1,53 @@
// tells React if you're using its "server-side rendering" features. this component runs only on the client-side (browser)
"use client"; "use client";
// importing React hooks or utilities that enhance functionality inside the component import axios from "axios";
import { useState, FormEvent, useRef, MouseEvent, useEffect } from "react"; import { FormEvent, MouseEvent, useEffect, useRef, useState } from "react";
/*
useState: Used to manage state (data that changes over time).
FormEvent: TypeScript definition for form-related events like submission.
useRef: Used to access the DOM (the web page elements) directly.
MouseEvent: TypeScript definition for mouse-related events like clicks.
useEffect: Hook for running side effects (e.g., code that runs when something changes)
*/
// defining a type for the props (inputs) that AuthModal expects
interface AuthModalProps { interface AuthModalProps {
isOpen: boolean; // bool for if the modal should be visible isOpen: boolean; // bool for if the modal should be visible
onClose: () => void; //A function that will be executed to close the modal onClose: () => void; //A function that will be executed to close the modal
} }
// creates a React functional component
export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthModalProps ensures TypeScript validates the props to match the type definition above export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
const [isLogin, setIsLogin] = useState<boolean>(true); const [isLogin, setIsLogin] = useState<boolean>(true);
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
const [isFailed, setIsFailed] = useState<boolean>(false); const [isFailed, setIsFailed] = useState<boolean>(false);
const [failMessage, setFailMessage] = useState<boolean>(false); const [failMessage, setFailMessage] = useState<boolean>(false);
/*
useState is a React Hook that declares state variables in a functional component. It returns a two-element array
The state variable (isLogin) : Represents the current state value (e.g., true initially). This is the value you can use in your component.
The state updater function (setIsLogin) : A function that allows you to update the state variable. React takes care of re-rendering the component when the state is updated
*/
/*
modalRef allows direct access to the modal DOM element (the container div for the modal). This is useful for detecting if the user clicks outside of the modal
*/
// useEffect runs code after the component renders or when a dependency changes (in this case, isOpen as seen in the end [])
useEffect(() => { useEffect(() => {
if (isOpen) setIsLogin(true); // runs when isOpen changes, if it is true, the login is shown if (isOpen) setIsLogin(true); // runs when isOpen changes, if it is true, the login is shown
}, [isOpen]); }, [isOpen]);
if (!isOpen) return null; // if is open is false, the model isnt shown if (!isOpen) return null; // if is open is false, the model isnt shown
// this is an arrow function. e: is used to specify that an event object is expected
const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => { const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)) { if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
onClose(); onClose();
} }
}; };
/*
.current gives a reference to the actual DOM element (the inner modal container)
e.target refers to the specific element the mouse click event occurred on
.contains(e.target) checks if the clicked element (e.target) is inside the modal (modalRef.current)
as Node specifies e.target is a Node type to TypeScript
? what is a node
/!... means we get true if the click is outside the modal
Logic : if modalRef.current exists and the click target (e.target) is not inside the modal , then the condition is true
This means the user clicked outside the modal
*/
// LS - The following bit contains the more important code for what I'm focused on
/*
Note : handleSubmit is typically used as a event handler for submitting a form in react.
For example:
<form onSubmit={handleSubmit}>
<input type="text" name="example" />
<button type="submit">Submit</button>
</form>
*/
/*
e is the parameter passsed into the function
async indicates the function runs asyncronously, meaning it performs tasks which take time.
an example of this would be API calls
*/
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // stops page from refreshing e.preventDefault(); // stops page from refreshing
setIsFailed(false) setIsFailed(false);
const formData = new FormData(e.currentTarget); // new variable of class FormData is created. This is part of the standard Web API included in modern web browsers const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string; // gets email from form response const email = formData.get("email") as string;
const password = formData.get("password") as string; // gets password from form response const password = formData.get("password") as string;
const name = isLogin ? undefined : (formData.get("name") as string);// if the form is in login mode, the name is undefine, otherwise the name is a value obtained from the response const name = isLogin ? undefined : (formData.get("name") as string);
let endpoint = isLogin ? "/api/login" : "/api/signup"; // sets endpoint for backend code (either sign up or login)
const body = isLogin ? { email, password } : { name: name!, email, password };// creates a json body for the backend
try { try {
console.log("Sending data to API"); const res = await axios.post(`/api/${isLogin ? "login" : "signup"}`, {
const res = await fetch(endpoint, { // sends a request to the server at the end point headers: { "Content-Type": "application/json" },
method: "POST", // Post is used since the form submission modifies the server side state body: isLogin ? { email, password } : { name: name!, email, password },
headers: { "Content-Type": "application/json" }, //indicates it expects a json object returned
body: JSON.stringify(body), // converts the body to a JSON string to be sent
}); });
if (res.ok) { //res.ok checks if the response is between 200-299 if (res.status) {
console.log("Success!"); onClose();
onClose(); // closes UI
} else if (res.status >= 400 && res.status < 500) { } else if (res.status >= 400 && res.status < 500) {
const responseBody = await res.json() console.log("4xx error:", res.data);
console.log("4xx error:", responseBody.message) setFailMessage(res.data.message);
setFailMessage(responseBody.message) setIsFailed(true);
setIsFailed(true)
} else { } else {
console.error("Error:", await res.text()); // logs error with error message sent to console console.error("Error:", await res.data.message);
} }
} catch (error) {// catches any errors (e.g. Not connected to network) } catch (error) {
console.error("Request failed:", error instanceof Error ? error.message : String(error)); console.error("Request failed:", error instanceof Error ? error.message : String(error));
} }
}; };
@ -107,7 +55,14 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthM
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50" onClick={handleOverlayClick}> <div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50" onClick={handleOverlayClick}>
<div ref={modalRef} className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative"> <div ref={modalRef} className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative">
<button onClick={() => {setIsFailed(false);onClose();}} className="absolute text-xl top-0 right-2 text-gray-500 hover:text-gray-700" aria-label="Close modal"> <button
onClick={() => {
setIsFailed(false);
onClose();
}}
className="absolute text-xl top-0 right-2 text-neutral-500 hover:text-neutral-700"
aria-label="Close modal"
>
× ×
</button> </button>
<h2 className="text-2xl font-bold text-center mb-4">{isLogin ? "Login" : "Sign Up"}</h2> <h2 className="text-2xl font-bold text-center mb-4">{isLogin ? "Login" : "Sign Up"}</h2>
@ -116,7 +71,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthM
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{!isLogin && ( {!isLogin && (
<div> <div>
<label className="block text-sm font-medium text-gray-700">Full Name</label> <label className="block text-sm font-medium text-neutral-700">Full Name</label>
<input <input
type="text" type="text"
name="name" name="name"
@ -126,7 +81,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthM
</div> </div>
)} )}
<div> <div>
<label className="block text-sm font-medium text-gray-700">Email</label> <label className="block text-sm font-medium text-neutral-700">Email</label>
<input <input
type="email" type="email"
name="email" name="email"
@ -135,7 +90,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthM
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700">Password</label> <label className="block text-sm font-medium text-neutral-700">Password</label>
<input <input
type="password" type="password"
name="password" name="password"
@ -148,8 +103,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthM
<div> <div>
<label className="block text-sm font-medium text-red-700">{failMessage}</label> <label className="block text-sm font-medium text-red-700">{failMessage}</label>
</div> </div>
) )}
}
</div> </div>
<button type="submit" className="w-full bg-blue-600 text-white p-2 rounded-md hover:bg-blue-700 transition"> <button type="submit" className="w-full bg-blue-600 text-white p-2 rounded-md hover:bg-blue-700 transition">
{isLogin ? "Login" : "Sign Up"} {isLogin ? "Login" : "Sign Up"}

View File

@ -1,7 +1,7 @@
import React from "react";
import { Dispatch, SetStateAction } from "react";
import Link from "next/link"; import Link from "next/link";
import React, { Dispatch, SetStateAction } from "react";
import { TbHexagon } from "react-icons/tb"; import { TbHexagon } from "react-icons/tb";
import Event from "@appTypes/Event"; import Event from "@appTypes/Event";
import getMagnitudeColor from "@utils/getMagnitudeColour"; import getMagnitudeColor from "@utils/getMagnitudeColour";
@ -51,10 +51,12 @@ export default function Sidebar({
button2Name, button2Name,
}: SidebarProps) { }: SidebarProps) {
return ( return (
<div className="flex flex-col py-6 h-full w-full max-w-xs bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg"> <div className={`flex flex-col h-full w-80 relative bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg`}>
<div className="py-6">
<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="/"> <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"> <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">
{button1Name} {button1Name}
@ -88,13 +90,12 @@ export default function Sidebar({
{/* <p className="text-xs text-neutral-600">{event.text1}</p> */} {/* <p className="text-xs text-neutral-600">{event.text1}</p> */}
<p className="text-xs text-neutral-500 mt-1">{event.text2}</p> <p className="text-xs text-neutral-500 mt-1">{event.text2}</p>
</div> </div>
{ {event.magnitude ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>}
(event.magnitude) ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>
}
</button> </button>
))} ))}
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }

View File

@ -1,13 +1,14 @@
import { useCallback, useState } from "react";
import React, { useRef, useEffect, Dispatch, SetStateAction } from "react";
import mapboxgl, { LngLatBounds } from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css"; import "mapbox-gl/dist/mapbox-gl.css";
import { GiObservatory } from "react-icons/gi";
import Event from "@appTypes/Event"; import mapboxgl, { LngLatBounds } from "mapbox-gl";
import getMagnitudeColor from "@utils/getMagnitudeColour";
import { userAgentFromString } from "next/server"; import { userAgentFromString } from "next/server";
import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { GiObservatory } from "react-icons/gi";
import Event from "@appTypes/Event";
import getMagnitudeColor from "@utils/getMagnitudeColour";
interface MapComponentProps { interface MapComponentProps {
events: Event[]; events: Event[];
@ -19,7 +20,14 @@ interface MapComponentProps {
} }
// Map component with location-style pulsing dots, animations, and tooltips // Map component with location-style pulsing dots, animations, and tooltips
function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEventId, setHoveredEventId, mapType }: MapComponentProps) { function MapComponent({
events,
selectedEventId,
setSelectedEventId,
hoveredEventId,
setHoveredEventId,
mapType,
}: MapComponentProps) {
const map = useRef<mapboxgl.Map | null>(null); const map = useRef<mapboxgl.Map | null>(null);
const markers = useRef<{ [key: string]: mapboxgl.Marker }>({}); const markers = useRef<{ [key: string]: mapboxgl.Marker }>({});
const [mapBounds, setMapBounds] = useState<LngLatBounds>(); const [mapBounds, setMapBounds] = useState<LngLatBounds>();
@ -78,7 +86,6 @@ function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEven
// Add markers with location pulse // Add markers with location pulse
events.forEach((event) => { events.forEach((event) => {
const quakeElement = document.createElement("div"); const quakeElement = document.createElement("div");
const dotElement = document.createElement("div"); const dotElement = document.createElement("div");
const pulseElement = document.createElement("div"); const pulseElement = document.createElement("div");
@ -116,14 +123,14 @@ function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEven
//<GiObservatory /> //<GiObservatory />
const observatoryElement = document.createElement("div"); const observatoryElement = document.createElement("div");
const root = createRoot(observatoryElement); // `createRoot` is now the standard API const root = createRoot(observatoryElement); // `createRoot` is now the standard API
root.render( root.render(<GiObservatory className="text-blue-600 text-2xl drop-shadow-lg" />);
<GiObservatory className="text-blue-600 text-2xl drop-shadow-lg" />
);
quakeElement.appendChild(pulseElement); quakeElement.appendChild(pulseElement);
quakeElement.appendChild(dotElement); quakeElement.appendChild(dotElement);
const marker = new mapboxgl.Marker({ element: mapType === "observatories" ? observatoryElement : quakeElement }).setLngLat([event.longitude, event.latitude]).addTo(map.current!); const marker = new mapboxgl.Marker({ element: mapType === "observatories" ? observatoryElement : quakeElement })
.setLngLat([event.longitude, event.latitude])
.addTo(map.current!);
const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(` const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(`
<div> <div>
@ -142,7 +149,9 @@ function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEven
markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id)); markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id));
markerDomElement.addEventListener("mouseleave", () => setHoveredEventId("")); markerDomElement.addEventListener("mouseleave", () => setHoveredEventId(""));
markerDomElement.addEventListener("click", () => setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id))); markerDomElement.addEventListener("click", () =>
setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id))
);
// Cleanup event listeners on unmount // Cleanup event listeners on unmount
markerDomElement.dataset.listenersAdded = "true"; // Mark for cleanup markerDomElement.dataset.listenersAdded = "true"; // Mark for cleanup

View File

@ -1,23 +1,51 @@
"use client"; "use client";
import { useState } from "react";
import AuthModal from "@components/AuthModal";
import { usePathname } from "next/navigation";
import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useMemo } from "react"; import Link from "next/link";
import { usePathname } from "next/navigation";
import { Dispatch, SetStateAction, useMemo, useState } from "react";
import { FaRegUserCircle } from "react-icons/fa"; import { FaRegUserCircle } from "react-icons/fa";
function NavbarButton({ name, href, dropdownItems }: { name: string; href: string; dropdownItems?: string[] }) { import { Currency } from "@appTypes/StoreModel";
import AuthModal from "@components/AuthModal";
import { useStoreActions, useStoreState } from "@hooks/store";
export default function Navbar({}: // currencySelector,
{
// currencySelector?: { selectedCurrency: string; setSelectedCurrency: Dispatch<SetStateAction<"GBP" | "USD" | "EUR">> };
}) {
const pathname = usePathname(); const pathname = usePathname();
const isActive = dropdownItems ? dropdownItems.some((item) => pathname === `/${item.toLowerCase().replace(" ", "-")}`) : pathname === href; const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
const setSelectedCurrency = useStoreActions((actions) => actions.currency.setSelectedCurrency);
const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Warehouse", "Shop"], []);
// const navOptions = useMemo(() => ["Earthquakes"], []);
const aboutDropdown = ["Contact Us", "Our Mission", "The Team"];
// { label: "Our Mission", path: "/our-mission" },
// { label: "The Team", path: "/the-team" },
// { label: "Contact Us", path: "/contact-us" }]
const [isModalOpen, setIsModalOpen] = useState(false);
const currencies = useStoreState((state) => state.currency.currencies);
const currencyTickers = useStoreState((state) => state.currency.tickers);
function NavbarButton({ name, href, dropdownItems }: { name: string; href: string; dropdownItems?: string[] }) {
const isActive = dropdownItems
? dropdownItems.some((item) => pathname === `/${item.toLowerCase().replace(" ", "-")}`)
: pathname === href;
return ( return (
<button className="flex items-center justify-center px-2 py-4 relative group"> <button className="flex items-center justify-center px-2 py-4 relative group">
{dropdownItems ? ( {dropdownItems ? (
<span className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "group-hover:bg-neutral-200"}`}>{name}</span> <span
className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "group-hover:bg-neutral-200"}`}
>
{name}
</span>
) : ( ) : (
<Link href={href} className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "hover:bg-neutral-200"}`}> <Link
href={href}
className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "hover:bg-neutral-200"}`}
>
{name} {name}
</Link> </Link>
)} )}
@ -29,7 +57,10 @@ function NavbarButton({ name, href, dropdownItems }: { name: string; href: strin
const isDropdownActive = pathname === itemHref; const isDropdownActive = pathname === itemHref;
return ( return (
<li key={item}> <li key={item}>
<Link href={itemHref} className={`block px-4 py-2 hover:bg-neutral-100 ${isDropdownActive ? "bg-neutral-100" : ""}`}> <Link
href={itemHref}
className={`block px-4 py-2 hover:bg-neutral-100 ${isDropdownActive ? "bg-neutral-100" : ""}`}
>
{item} {item}
</Link> </Link>
</li> </li>
@ -42,16 +73,6 @@ function NavbarButton({ name, href, dropdownItems }: { name: string; href: strin
); );
} }
export default function Navbar() {
const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Warehouse", "Shop"], []);
// const navOptions = useMemo(() => ["Earthquakes"], []);
const aboutDropdown = ["Contact Us", "Our Mission", "The Team"];
// { label: "Our Mission", path: "/our-mission" },
// { label: "The Team", path: "/the-team" },
// { label: "Contact Us", path: "/contact-us" }]
const [isModalOpen, setIsModalOpen] = useState(false);
return ( return (
<div className="flex sticky top-0 w-full h-14 z-40 font-medium bg-white border border-b-neutral-200"> <div className="flex sticky top-0 w-full h-14 z-40 font-medium bg-white border border-b-neutral-200">
<div className="my-1 flex aspect-square ml-3 mr-3"> <div className="my-1 flex aspect-square ml-3 mr-3">
@ -66,6 +87,28 @@ export default function Navbar() {
<NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} /> <NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} />
</div> </div>
<div className="flex-grow" /> <div className="flex-grow" />
{pathname.includes("shop") && (
<button className="flex items-center justify-center mr-3 py-4 relative group">
<span className={`px-4 py-1.5 rounded-md transition-colors`}>{selectedCurrency}</span>
<div className="absolute hidden group-hover:block top-full left-1/2 -translate-x-1/2 w-24 bg-white border border-neutral-300 rounded-lg overflow-hidden shadow-lg z-40">
<ul>
{currencies.map((item) => {
let ticker = currencyTickers[item];
return (
<li key={item} onClick={() => setSelectedCurrency(item)}>
<div className={`block px-2 py-2 hover:bg-neutral-100 text-md`}>
{item} ({ticker})
</div>
</li>
);
})}
</ul>
</div>
</button>
)}
<button className="my-auto mr-4" onClick={() => setIsModalOpen(true)}> <button className="my-auto mr-4" onClick={() => setIsModalOpen(true)}>
<FaRegUserCircle size={22} /> <FaRegUserCircle size={22} />
</button> </button>

View File

@ -1,18 +1,16 @@
import React from 'react';
import Link from "next/link"; import Link from "next/link";
import React from "react";
const Sidebar = () => { const Sidebar = () => {
return ( return (
<div className="flex flex-col h-screen w-64 bg-gray-400 text-white border-l border-gray-700"> <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-gray-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> <h2 className="text-xl font-semibold mb-2">Log an Earthquake</h2>
<p className="text-sm text-gray-700"> <p className="text-sm text-neutral-700">
Record new earthquakes - time/date, location, magnitude, observatory and scientists Record new earthquakes - time/date, location, magnitude, observatory and scientists
</p> </p>
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"> <button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
<Link href="/"> <Link href="/">Log Event</Link>
Log Event
</Link>
</button> </button>
</div> </div>
@ -20,20 +18,20 @@ const Sidebar = () => {
<div className="flex-1 p-4"> <div className="flex-1 p-4">
<h2 className="text-xl font-semibold mb-2">Recent Events</h2> <h2 className="text-xl font-semibold mb-2">Recent Events</h2>
<ul className="space-y-2"> <ul className="space-y-2">
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600"> <li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Earthquake in California</p> <p className="text-sm">Earthquake in California</p>
<p className="text-xs text-gray-300">Magnitude 5.3</p> <p className="text-xs text-neutral-300">Magnitude 5.3</p>
<p className="text-xs text-gray-400">2 hours ago</p> <p className="text-xs text-neutral-400">2 hours ago</p>
</li> </li>
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600"> <li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Japan</p> <p className="text-sm">Tremor in Japan</p>
<p className="text-xs text-gray-300">Magnitude 4.7</p> <p className="text-xs text-neutral-300">Magnitude 4.7</p>
<p className="text-xs text-gray-400">5 hours ago</p> <p className="text-xs text-neutral-400">5 hours ago</p>
</li> </li>
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600"> <li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Spain</p> <p className="text-sm">Tremor in Spain</p>
<p className="text-xs text-gray-300">Magnitude 2.1</p> <p className="text-xs text-neutral-300">Magnitude 2.1</p>
<p className="text-xs text-gray-400">10 hours ago</p> <p className="text-xs text-neutral-400">10 hours ago</p>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,18 +1,14 @@
import React from 'react';
import Link from "next/link"; import Link from "next/link";
import React from "react";
const Sidebar = () => { const Sidebar = () => {
return ( return (
<div className="flex flex-col h-screen w-64 bg-gray-400 text-white border-l border-gray-700"> <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-gray-700"> <div className="flex flex-col p-4 border-b border-neutral-700">
<h2 className="text-xl font-semibold mb-2">Observatories</h2> <h2 className="text-xl font-semibold mb-2">Observatories</h2>
<p className="text-sm text-gray-700"> <p className="text-sm text-neutral-700">Observatory events - location, scientists, recent earthquakes</p>
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"> <button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
<Link href="/"> <Link href="/">Observatory News</Link>
Observatory News
</Link>
</button> </button>
</div> </div>
@ -20,20 +16,20 @@ const Sidebar = () => {
<div className="flex-1 p-4"> <div className="flex-1 p-4">
<h2 className="text-xl font-semibold mb-2">Recent Observatory Events</h2> <h2 className="text-xl font-semibold mb-2">Recent Observatory Events</h2>
<ul className="space-y-2"> <ul className="space-y-2">
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600"> <li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Earthquake in California</p> <p className="text-sm">Earthquake in California</p>
<p className="text-xs text-gray-300">Magnitude 5.3</p> <p className="text-xs text-neutral-300">Magnitude 5.3</p>
<p className="text-xs text-gray-400">Cali Observatory</p> <p className="text-xs text-neutral-400">Cali Observatory</p>
</li> </li>
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600"> <li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Japan</p> <p className="text-sm">Tremor in Japan</p>
<p className="text-xs text-gray-300">Magnitude 4.7</p> <p className="text-xs text-neutral-300">Magnitude 4.7</p>
<p className="text-xs text-gray-400">Kyoto Observatory</p> <p className="text-xs text-neutral-400">Kyoto Observatory</p>
</li> </li>
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600"> <li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
<p className="text-sm">Tremor in Spain</p> <p className="text-sm">Tremor in Spain</p>
<p className="text-xs text-gray-300">Magnitude 2.1</p> <p className="text-xs text-neutral-300">Magnitude 2.1</p>
<p className="text-xs text-gray-400">Madrid Observatory</p> <p className="text-xs text-neutral-400">Madrid Observatory</p>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,5 +1,2 @@
name,email,password name,email,password,level
Lukeshan Thananchayan,lukeshan@mail.com,TaylorSwift123 Lukeshan Thananchayan,lukeshan@mail.com,$2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge,basic
Emily Neighbour Carter,emily@mail.com,HarryStyles000
Izzy Patteron,izzy@mail.com,OliviaRodrigez420
Tim Howitz,tim@mail.com,TravisBarker182
1 name email password level
2 Lukeshan Thananchayan lukeshan@mail.com TaylorSwift123 $2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge basic
Emily Neighbour Carter emily@mail.com HarryStyles000
Izzy Patteron izzy@mail.com OliviaRodrigez420
Tim Howitz tim@mail.com TravisBarker182

View File

@ -10,8 +10,7 @@ model User {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
email String @unique email String @unique
passwordHash String passwordHash String
firstname String name String
surname String
role String @default("USER") @db.VarChar(10) role String @default("USER") @db.VarChar(10)
scientist Scientist? @relation // Optional relation to Scientist scientist Scientist? @relation // Optional relation to Scientist
} }

10
src/hooks/store.ts Normal file
View File

@ -0,0 +1,10 @@
"use client";
import { createTypedHooks } from "easy-peasy";
import { StoreModel } from "@appTypes/StoreModel";
export const typedHooks = createTypedHooks<StoreModel>();
export const useStoreActions = typedHooks.useStoreActions;
export const useStoreDispatch = typedHooks.useStoreDispatch;
export const useStoreState = typedHooks.useStoreState;

11
src/types/Artifact.ts Normal file
View File

@ -0,0 +1,11 @@
interface Artifact {
// todo change to string
id: number;
name: string;
description: string;
location: string;
image: string;
price: number;
}
export default Artifact;

17
src/types/StoreModel.ts Normal file
View File

@ -0,0 +1,17 @@
import { Action } from "easy-peasy";
type Currency = "GBP" | "USD" | "EUR";
interface CurrencyModel {
selectedCurrency: Currency;
setSelectedCurrency: Action<CurrencyModel, Currency>;
currencies: Currency[];
conversionRates: Record<Currency, number>;
tickers: Record<Currency, string>;
}
interface StoreModel {
currency: CurrencyModel;
}
export type { StoreModel, Currency };

3
src/utils/fetcher.ts Normal file
View File

@ -0,0 +1,3 @@
import axios from "axios";
export const fetcher = (url: string) => axios.get(url).then((res) => res.data);

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import resolveConfig from "tailwindcss/resolveConfig"; import resolveConfig from "tailwindcss/resolveConfig";
import tailwindConfig from "../../tailwind.config"; import tailwindConfig from "../../tailwind.config";
function getMagnitudeColour(magnitude: number) { function getMagnitudeColour(magnitude: number) {

View File

@ -20,11 +20,11 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"], "@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"], "@hooks/*": ["./src/hooks/*"],
"@utils/*": ["./src/utils/*"], "@utils/*": ["./src/utils/*"],
"@appTypes/*": ["./src/types/*"] "@appTypes/*": ["./src/types/*"],
"@/*": ["./src/*"],
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],