Did a lot of cleaning up and improving

This commit is contained in:
Tim Howitz 2025-04-28 19:03:29 +01:00
parent c6ebf4d50a
commit ab72dd88cb
18 changed files with 1178 additions and 926 deletions

102
package-lock.json generated
View File

@ -14,6 +14,7 @@
"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",
@ -55,6 +56,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
@ -1637,7 +1650,7 @@
"version": "19.0.10", "version": "19.0.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -1647,7 +1660,7 @@
"version": "19.0.4", "version": "19.0.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz",
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
@ -2803,7 +2816,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/csv-parser": { "node_modules/csv-parser": {
@ -3048,6 +3061,42 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/easy-peasy": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/easy-peasy/-/easy-peasy-6.1.0.tgz",
"integrity": "sha512-zW7mUATJRohNWWSrihmeFhS85jjsIHa1ptTDn6bSXa2DXh8Zeawfpmm1LtKa+Y6jNNz5aQlMjWZ8uuuVk5bdVQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.22.6",
"fast-deep-equal": "^3.1.3",
"immer": "^9.0.21",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"ts-toolbelt": "^9.6.0",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.0 || ^19.0",
"@types/react-dom": "^18.0 || ^19.0",
"react": "^18.0 || ^19.0",
"react-dom": "^18.0 || ^19.0",
"react-native": ">=0.59"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -3844,7 +3893,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
@ -4561,6 +4609,16 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -6605,6 +6663,21 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -6628,6 +6701,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@ -7770,6 +7849,12 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/ts-toolbelt": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
"license": "Apache-2.0"
},
"node_modules/tsconfig-paths": { "node_modules/tsconfig-paths": {
"version": "3.15.0", "version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@ -7953,6 +8038,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util": { "node_modules/util": {
"version": "0.10.4", "version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",

View File

@ -17,6 +17,7 @@
"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",

View File

@ -1,173 +1,143 @@
"use client"; "use client";
import React, { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import React, { useState } from "react";
const ContactUs = () => { const ContactUs = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: "", name: "",
email: "", email: "",
message: "", message: "",
}); });
const handleChange = (e) => { const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value }); setFormData({ ...formData, [e.target.name]: e.target.value });
}; };
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
console.log("Form submitted with data:", formData); console.log("Form submitted with data:", formData);
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: "" }); setFormData({ name: "", email: "", message: "" });
}; };
return ( return (
<div <div className="h-screen relative text-white py-10 border border-black">
className="h-screen relative text-white py-10 border border-black"> <Image height={5000} width={5000} alt="Logo" className="border border-neutral-300 absolute z-10" src="/tsunamiWaves.jpg" />
<Image height={5000} width={5000} alt="Logo" className="border border-neutral-300 absolute z-10" src="/tsunamiWaves.jpg"/>
{/* Overlay for readability */} {/* Overlay for readability */}
<div className="absolute overflow-hidden w-full h-full bg-black bg-opacity-40 flex flex-col items-center z-20"> <div className="absolute overflow-hidden w-full h-full bg-black bg-opacity-40 flex flex-col items-center z-20">
{/* Container */} {/* Container */}
<div className="max-w-4xl mx-auto p-5"> <div className="max-w-4xl mx-auto p-5">
{/* Header */} {/* Header */}
<h1 className="text-4xl font-bold text-center text-white mb-6"> <h1 className="text-4xl font-bold text-center text-white mb-6">Contact Us</h1>
Contact Us <p className="text-lg text-center text-neutral-300 mb-6">
</h1> Have questions or concerns about earthquake preparedness? Contact us using the form below or through the provided
<p className="text-lg text-center text-gray-300 mb-6"> contact details.
Have questions or concerns about earthquake preparedness? Contact us </p>
using the form below or through the provided contact details.
</p>
{/* Content Section */} {/* Content Section */}
<div className="flex flex-col md:flex-row gap-6"> <div className="flex flex-col md:flex-row gap-6">
{/* Contact Form Section */} {/* Contact Form Section */}
<div className="flex-1 bg-white bg-opacity-90 text-gray-800 rounded-lg shadow-lg p-6"> <div className="flex-1 bg-white bg-opacity-90 text-neutral-800 rounded-lg shadow-lg p-6">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mb-4"> <div className="mb-4">
<label <label htmlFor="name" className="block text-neutral-700 font-medium mb-2">
htmlFor="name" Name
className="block text-gray-700 font-medium mb-2" </label>
> <input
Name type="text"
</label> name="name"
<input id="name"
type="text" value={formData.name}
name="name" onChange={handleChange}
id="name" placeholder="Your Name"
value={formData.name} className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
onChange={handleChange} required
placeholder="Your Name" />
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" </div>
required
/>
</div>
<div className="mb-4"> <div className="mb-4">
<label <label htmlFor="email" className="block text-neutral-700 font-medium mb-2">
htmlFor="email" Email
className="block text-gray-700 font-medium mb-2" </label>
> <input
Email type="email"
</label> name="email"
<input id="email"
type="email" value={formData.email}
name="email" onChange={handleChange}
id="email" placeholder="Your Email"
value={formData.email} className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
onChange={handleChange} required
placeholder="Your Email" />
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" </div>
required
/>
</div>
<div className="mb-4"> <div className="mb-4">
<label <label htmlFor="message" className="block text-neutral-700 font-medium mb-2">
htmlFor="message" Message
className="block text-gray-700 font-medium mb-2" </label>
> <textarea
Message name="message"
</label> id="message"
<textarea value={formData.message}
name="message" onChange={handleChange}
id="message" rows="5"
value={formData.message} placeholder="Your Message"
onChange={handleChange} className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
rows="5" required
placeholder="Your Message" />
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" </div>
required
/>
</div>
<button <button
type="submit" type="submit"
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition duration-200" className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition duration-200"
> >
Send Message Send Message
</button> </button>
</form> </form>
</div> </div>
{/* Contact Details Section */} {/* Contact Details Section */}
<div className="flex-1 bg-white bg-opacity-90 text-gray-800 rounded-lg shadow-lg p-6"> <div className="flex-1 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 <div className="mb-4">
</h2> <h3 className="text-neutral-700 font-medium">Email</h3>
<div className="mb-4"> <p className="text-neutral-600">support@earthquakesafety.org</p>
<h3 className="text-gray-700 font-medium">Email</h3> </div>
<p className="text-gray-600">support@earthquakesafety.org</p> <div className="mb-4">
</div> <h3 className="text-neutral-700 font-medium">Phone</h3>
<div className="mb-4"> <p className="text-neutral-600">+1 800 123 4567</p>
<h3 className="text-gray-700 font-medium">Phone</h3> </div>
<p className="text-gray-600">+1 800 123 4567</p> <div className="mb-4">
</div> <h3 className="text-neutral-700 font-medium">Address</h3>
<div className="mb-4"> <p className="text-neutral-600">123 Earthquake Ave, Prepared City, CA 98765</p>
<h3 className="text-gray-700 font-medium">Address</h3> </div>
<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"> <h2 className="text-xl font-bold text-neutral-800 mb-4 mt-6">Follow Us</h2>
Follow Us <div className="flex justify-around items-center">
</h2> <a href="#" className="w-20 h-20 text-blue-600 hover:text-blue-800 transition duration-200">
<div className="flex justify-around items-center"> <span className="sr-only">Instagram</span>
<a <Image height={200} width={200} alt="Logo" className="z-10" src="/insta.webp" />
href="#" </a>
className="w-20 h-20 text-blue-600 hover:text-blue-800 transition duration-200" <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">Instagram</span> <Image height={200} width={200} alt="Logo" className="z-10" src="/facebook.webp" />
<Image height={200} width={200} alt="Logo" className="z-10" src="/insta.webp"/> </a>
</a> <a href="#" className="w-20 h-20 p-4 text-blue-600 hover:text-blue-800 transition duration-200">
<a <span className="sr-only">X</span>
href="#" <Image height={200} width={200} alt="Logo" className="z-10 rounded-lg" src="/x_logo.jpg" />
className="w-20 h-20 text-blue-600 hover:text-blue-800 transition duration-200" </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">Facebook</span> <span className="sr-only">LinkedIn</span>
<Image height={200} width={200} alt="Logo" className="z-10" src="/facebook.webp"/> <Image height={200} width={200} alt="Logo" className="z-10" src="/linkedIn.png" />
</a> </a>
<a </div>
href="#" </div>
className="w-20 h-20 p-4 text-blue-600 hover:text-blue-800 transition duration-200" </div>
> </div>
<span className="sr-only">X</span> </div>
<Image height={200} width={200} alt="Logo" className="z-10 rounded-lg" src="/x_logo.jpg"/> </div>
</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>
<Image height={200} width={200} alt="Logo" className="z-10" src="/linkedIn.png"/>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}; };
export default ContactUs; export default ContactUs;

View File

@ -1,80 +1,81 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar"; import { useMemo, useState } from 'react';
import Map from "@/components/map";
import { useState, useMemo } from "react"; import Map from '@components/Map';
import Sidebar from '@components/Sidebar';
export default function Earthquakes() { export default function Earthquakes() {
const [selectedEventId, setSelectedEventId] = useState(""); const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState(""); const [hoveredEventId, setHoveredEventId] = useState("");
const events = useMemo( const events = useMemo(
() => [ () => [
{ {
id: "1234", id: "1234",
title: "Earthquake in Germany", title: "Earthquake in Germany",
text1: "Magnitude 8.5", text1: "Magnitude 8.5",
text2: "30 minutes ago", text2: "30 minutes ago",
magnitude: 8.5, magnitude: 8.5,
longitude: 10.4515, // Near Berlin, Germany longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52, latitude: 52.52,
}, },
{ {
id: "2134", id: "2134",
title: "Earthquake in California", title: "Earthquake in California",
text1: "Magnitude 5.3", text1: "Magnitude 5.3",
text2: "2 hours ago", text2: "2 hours ago",
magnitude: 5.3, magnitude: 5.3,
longitude: -122.4194, // Near San Francisco, California, USA longitude: -122.4194, // Near San Francisco, California, USA
latitude: 37.7749, latitude: 37.7749,
}, },
{ {
id: "2314", id: "2314",
title: "Tremor in Japan", title: "Tremor in Japan",
text1: "Magnitude 4.7", text1: "Magnitude 4.7",
text2: "5 hours ago", text2: "5 hours ago",
magnitude: 4.7, magnitude: 4.7,
longitude: 139.6917, // Near Tokyo, Japan longitude: 139.6917, // Near Tokyo, Japan
latitude: 35.6762, latitude: 35.6762,
}, },
{ {
id: "2341", id: "2341",
title: "Tremor in Spain", title: "Tremor in Spain",
text1: "Magnitude 2.1", text1: "Magnitude 2.1",
text2: "10 hours ago", text2: "10 hours ago",
magnitude: 2.1, magnitude: 2.1,
longitude: -3.7038, // Near Madrid, Spain longitude: -3.7038, // Near Madrid, Spain
latitude: 40.4168, 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={events}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
mapType="Earthquakes" mapType="Earthquakes"
></Map> ></Map>
</div> </div>
<Sidebar <Sidebar
logTitle="Log an Earthquake" logTitle="Log an Earthquake"
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists" logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists"
recentsTitle="Recent Earthquakes" recentsTitle="Recent Earthquakes"
events={events} events={events}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
button1Name="Log an Earthquake" button1Name="Log an Earthquake"
button2Name="Search Earthquakes" button2Name="Search Earthquakes"
></Sidebar> ></Sidebar>
{/* <SidebarTest></SidebarTest> */} {/* <SidebarTest></SidebarTest> */}
</div> </div>
); );
} }

View File

@ -1,29 +1,43 @@
"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,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body className={`${inter.variable} h-[calc(100vh-3.5rem)] flex flex-col min-h-screen antialiased`}> <StoreProvider store={store}>
<Navbar></Navbar> <body className={`${inter.variable} h-[calc(100vh-3.5rem)] flex flex-col min-h-screen antialiased`}>
<div className="flex-1 overflow-y-auto">{children}</div> <Navbar></Navbar>
</body> <div className="flex-1 overflow-y-auto">{children}</div>
</html> </body>
); </StoreProvider>
</html>
);
} }

View File

@ -1,68 +1,68 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar"; import Sidebar from "@/components/Sidebar";
import Map from "@/components/map"; import Map from "@/components/Map";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
export default function Observatories() { export default function Observatories() {
const [selectedEventId, setSelectedEventId] = useState(""); const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState(""); const [hoveredEventId, setHoveredEventId] = useState("");
const events = useMemo( const events = useMemo(
() => [ () => [
{ {
id: "1234", id: "1234",
title: "Earthquake - Berlin Observatory", title: "Earthquake - Berlin Observatory",
text1: "Logged by ", text1: "Logged by ",
text2: "30 minutes ago", text2: "30 minutes ago",
longitude: 10.4515, // Near Berlin, Germany longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52, latitude: 52.52,
}, },
{ {
id: "2134", id: "2134",
title: "New Observatory - Phuket, Thailand", title: "New Observatory - Phuket, Thailand",
text1: "Dr. Neil Armstrong", text1: "Dr. Neil Armstrong",
text2: "2 weeks ago", text2: "2 weeks ago",
longitude: -122.4194, longitude: -122.4194,
latitude: 37.7749, latitude: 37.7749,
}, },
{ {
id: "2314", id: "2314",
title: "Observatory Scientist Change", title: "Observatory Scientist Change",
text1: "Dr. Samantha Green new lead scientist", text1: "Dr. Samantha Green new lead scientist",
text2: "1 month ago", text2: "1 month ago",
longitude: 139.6917, longitude: 139.6917,
latitude: 35.6762, 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={events}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
mapType="observatories" mapType="observatories"
></Map> ></Map>
</div> </div>
<Sidebar <Sidebar
logTitle="Observatory Mapping" logTitle="Observatory Mapping"
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes" logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
recentsTitle="Observatory Events" recentsTitle="Observatory Events"
events={events} events={events}
selectedEventId={selectedEventId} selectedEventId={selectedEventId}
setSelectedEventId={setSelectedEventId} setSelectedEventId={setSelectedEventId}
hoveredEventId={hoveredEventId} hoveredEventId={hoveredEventId}
setHoveredEventId={setHoveredEventId} setHoveredEventId={setHoveredEventId}
button1Name="Log a New Observatory" button1Name="Log a New Observatory"
button2Name="Search Observatories" button2Name="Search Observatories"
></Sidebar> ></Sidebar>
{/* <SidebarTest></SidebarTest> */} {/* <SidebarTest></SidebarTest> */}
</div> </div>
); );
} }

View File

@ -2,54 +2,46 @@
//export default function Page() //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 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"> <div className="max-w-4xl mx-auto p-5 bg-white 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-neutral-800 mb-6">Our Mission</h1>
<p className="text-lg text-gray-600 leading-relaxed mb-4"> <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. At <span className="font-semibold text-blue-600">Earthquake Awareness Initiative</span>, our mission is to help people
</p> worldwide prepare for and recover from earthquakes. Through education, research, and innovative technology, we work
<p className="text-lg text-gray-600 leading-relaxed mb-4"> tirelessly to empower communities with the knowledge they need to stay safe before, during, and after seismic events.
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> <p className="text-lg text-neutral-600 leading-relaxed mb-4">
<div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6"> We aim to bridge the gap between scientific research and community awareness by providing resources, tools, and
<div className="flex flex-col items-center p-4"> real-time updates for earthquake preparedness. Together, we aspire to save lives, mitigate impacts, and foster
<img resilience against nature's powerful forces.
src="/images/education-icon.png" </p>
alt="Education Icon" <div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6">
className="h-16 w-16 mb-4" <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-gray-700 mb-2">Education</h3> <h3 className="text-xl font-bold text-neutral-700 mb-2">Education</h3>
<p className="text-sm text-gray-500 text-center"> <p className="text-sm text-neutral-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-4">
<img <img src="/images/research-icon.png" alt="Research Icon" className="h-16 w-16 mb-4" />
src="/images/research-icon.png" <h3 className="text-xl font-bold text-neutral-700 mb-2">Research</h3>
alt="Research Icon" <p className="text-sm text-neutral-500 text-center">
className="h-16 w-16 mb-4" Supporting scientific studies to enhance understanding of seismic activity.
/> </p>
<h3 className="text-xl font-bold text-gray-700 mb-2">Research</h3> </div>
<p className="text-sm text-gray-500 text-center"> <div className="flex flex-col items-center p-4">
Supporting scientific studies to enhance understanding of seismic activity. <img src="/images/technology-icon.png" alt="Technology Icon" className="h-16 w-16 mb-4" />
</p> <h3 className="text-xl font-bold text-neutral-700 mb-2">Technology</h3>
</div> <p className="text-sm text-neutral-500 text-center">
<div className="flex flex-col items-center p-4"> Leveraging innovation to deliver real-time alerts and safety tools.
<img </p>
src="/images/technology-icon.png" </div>
alt="Technology Icon" </div>
className="h-16 w-16 mb-4" </div>
/> </div>
<h3 className="text-xl font-bold text-gray-700 mb-2">Technology</h3> );
<p className="text-sm text-gray-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

@ -1,185 +1,282 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar"; import { Dispatch, SetStateAction, useCallback, useState } from "react";
import { 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: "/images/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: "/images/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: "/images/artifact3.jpg",
price: 120,
},
{
id: 4,
name: "Roman Coin",
description: "An authentic Roman coin from the 2nd century CE.",
location: "Rome, Italy",
image: "/images/artifact4.jpg",
price: 80,
},
{
id: 5,
name: "Samurai Mask",
description: "Replica of Japanese Samurai battle masks.",
location: "Tokyo, Japan",
image: "/images/artifact5.jpg",
price: 300,
},
{
id: 6,
name: "Ancient Greek Vase",
description: "Depicts Greek mythology, found in the Acropolis.",
location: "Athens, Greece",
image: "/images/artifact6.jpg",
price: 250,
},
{
id: 7,
name: "Incan Pendant",
description: "Represents the Sun God Inti.",
location: "India",
image: "/images/artifact7.jpg",
price: 175,
},
{
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: 11,
name: "Ancient Scroll",
description: "A mysterious scroll from ancient times.",
location: "Madrid, Spain",
image: "/images/artifact11.jpg",
price: 500,
},
{
id: 12,
name: "Ming Dynasty Porcelain",
description: "Porcelain from China's Ming Dynasty.",
location: "Beijing, China",
image: "/images/artifact12.jpg",
price: 300,
},
{
id: 13,
name: "African Tribal Mask",
description: "A unique tribal mask from Africa.",
location: "Nigeria",
image: "/images/artifact13.jpg",
price: 250,
},
{
id: 14,
name: "Crystal Skull",
description: "A mystical pre-Columbian artifact.",
location: "Colombia",
image: "/images/artifact14.jpg",
price: 1000,
},
{
id: 15,
name: "Medieval Armor Fragment",
description: "A fragment of medieval armor.",
location: "Normandy, France",
image: "/images/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 handleNextPage = () => { const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
if (indexOfLastArtifact < artifacts.length) { const conversionRates = useStoreState((state) => state.currency.conversionRates);
setCurrentPage((prev) => prev + 1); const currencyTickers = useStoreState((state) => state.currency.tickers);
} const convertPrice = useCallback((price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2), []);
};
const handlePreviousPage = () => { const handleNextPage = () => {
if (currentPage > 1) { if (indexOfLastArtifact < artifacts.length) {
setCurrentPage((prev) => prev - 1); setCurrentPage((prev) => prev + 1);
} }
}; };
const handleSelectArtifact = (artifact) => { const handlePreviousPage = () => {
setSelectedArtifact(artifact); // Open modal with selected artifact if (currentPage > 1) {
}; setCurrentPage((prev) => prev - 1);
}
};
const handleCloseModal = () => { function Modal({ artifact }: { artifact: Artifact }) {
setSelectedArtifact(null); // Close modal if (!artifact) return null;
};
return ( const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
<div className="flex flex-col min-h-screen bg-gray-100"> if (e.target === e.currentTarget) {
{/* Main Content */} setSelectedArtifact(null);
<div className="flex flex-1"> }
{/* Artifact Grid */} };
<div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 p-6 pr-72">
{currentArtifacts.map((artifact) => (
<ArtifactCard key={artifact.id} artifact={artifact} onSelect={handleSelectArtifact} />
))}
</div>
{/* Sidebar */} return (
<aside className="w-72 bg-white shadow-lg p-4 h-screen fixed top-10 right-0 overflow-auto"> <div
<Sidebar className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
logTitle="Artifact Collection" onClick={handleOverlayClick}
logSubtitle="Record new artifacts - name, description, image, location and price" >
recentsTitle="Recent Updates" <div className="bg-white rounded-md shadow-lg max-w-lg w-full p-6">
events={[/* example events if needed */]} <h3 className="text-xl font-bold mb-4">{artifact.name}</h3>
selectedEventId="" <img src={artifact.image} alt={artifact.name} className="w-full h-64 object-cover rounded-md mb-4" />
setSelectedEventId={() => {}} <p className="text-neutral-600 mb-2">{artifact.description}</p>
hoveredEventId="" <p className="text-neutral-500 font-bold mb-4">Location: {artifact.location}</p>
setHoveredEventId={() => {}} <p className="text-red-600 font-bold">Price: ${artifact.price}</p>
button1Name="Add New Artifact" <div className="flex justify-end gap-4 mt-6">
button2Name="Search Artifacts" <button
/> onClick={() => alert("Reserved Successfully!")}
</aside> className="px-4 py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-400"
</div> >
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={() => setSelectedArtifact(null)}
className="px-4 py-2 bg-neutral-300 rounded-md hover:bg-neutral-400"
>
Close
</button>
</div>
</div>
</div>
);
}
{/* Pagination Footer */} // ArtifactCard Component
<footer className="bg-white border-t border-gray-300 py-2 text-center w-full flex justify-center items-centre"> function ArtifactCard({ artifact }: { artifact: Artifact }) {
<button return (
onClick={handlePreviousPage} <div
disabled={currentPage === 1} className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
className={`mx-2 px-4 py-1 bg-blue-700 text-white rounded-md font-bold shadow-md ${ onClick={() => setSelectedArtifact(artifact)} // Opens modal
currentPage === 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600" >
}`} > <img src={artifact.image} alt={artifact.name} className="w-full h-56 object-cover" />
&larr; Previous <div className="p-4">
</button> <h3 className="text-lg font-bold">{artifact.name}</h3>
<button <p className="text-neutral-700 mb-2">{artifact.description}</p>
onClick={handleNextPage} <p className="text-neutral-500 mb-2">{artifact.location}</p>
disabled={indexOfLastArtifact >= artifacts.length} <p className="text-blue-600 font-bold text-md mt-4">
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${ {currencyTickers[selectedCurrency]}
indexOfLastArtifact >= artifacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600" {convertPrice(artifact.price, selectedCurrency)}
}`} > </p>
Next &rarr; </div>
</button> </div>
</footer> );
}
{/* Modal */} return (
<Modal artifact={selectedArtifact} onClose={handleCloseModal} /> <div className="flex flex-col min-h-screen bg-neutral-100">
</div> {/* Main Content */}
); <div className="flex flex-1 overflow-y-auto mr-[19rem]">
{/* Artifact Grid */}
<div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
{currentArtifacts.map((artifact) => (
<ArtifactCard key={artifact.id} artifact={artifact} />
))}
</div>
</div>
{/* Sidebar */}
<div className="bg-white shadow-lg h-screen fixed right-0 overflow-y-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"
/>
</div>
{/* Pagination Footer */}
<footer className="bg-white border-t border-neutral-300 py-2 text-center w-full flex justify-center items-center mr-80">
<button
onClick={handlePreviousPage}
disabled={currentPage === 1}
className={`mx-2 px-4 py-1 bg-blue-700 text-white rounded-md font-bold shadow-md ${
currentPage === 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
}`}
>
&larr; Previous
</button>
<button
onClick={handleNextPage}
disabled={indexOfLastArtifact >= artifacts.length}
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"
}`}
>
Next &rarr;
</button>
</footer>
{/* Modal */}
{selectedArtifact && <Modal artifact={selectedArtifact} />}
</div>
);
} }

View File

@ -1,57 +1,55 @@
"use client"; "use client";
import Sidebar from "@/components/sidebar"; import Sidebar from "@/components/Sidebar";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
export default function Warehouse() { export default function Warehouse() {
const [selectedEventId, setSelectedEventId] = useState(""); const [selectedEventId, setSelectedEventId] = useState("");
const [hoveredEventId, setHoveredEventId] = useState(""); const [hoveredEventId, setHoveredEventId] = useState("");
const events = useMemo( const events = useMemo(
() => [ () => [
{ {
id: "1234", id: "1234",
title: "Artifact found - Germany Earthquake", title: "Artifact found - Germany Earthquake",
text1: "Mortar and pestle", text1: "Mortar and pestle",
text2: "10 minutes ago", text2: "10 minutes ago",
longitude: 10.4515, // Near Berlin, Germany longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52, latitude: 52.52,
}, },
{ {
id: "2134", id: "2134",
title: "All artifacts from California earthquake transported", title: "All artifacts from California earthquake transported",
text1: "India to London", text1: "India to London",
text2: "15 hours ago", text2: "15 hours ago",
longitude: -122.4194, // Near San Francisco, California, USA longitude: -122.4194, // Near San Francisco, California, USA
latitude: 37.7749, latitude: 37.7749,
}, },
{ {
id: "2341", id: "2341",
title: "7 artifacts destroyed - Spain earthquake", title: "7 artifacts destroyed - Spain earthquake",
text1: "edVases and pots from Madrid", text1: "edVases and pots from Madrid",
text2: "3 weeks ago", text2: "3 weeks ago",
longitude: -3.7038, // Near Madrid, Spain longitude: -3.7038, // Near Madrid, Spain
latitude: 40.4168, latitude: 40.4168,
}, },
], ],
[] []
); );
return ( return (
<div className="w-full h-full"> <div className="w-full h-full">
<p> <p>warehouse image pasted in here</p>
warehouse image pasted in here <Sidebar
</p> logTitle="Artifact Retrieval and Tracking"
<Sidebar logSubtitle="Record new artifacts collected - name, description, time/date found, location found, scientist and collection stage"
logTitle="Artifact Retrieval and Tracking" recentsTitle="Artifact News"
logSubtitle="Record new artifacts collected - name, description, time/date found, location found, scientist and collection stage" events={events}
recentsTitle="Artifact News" selectedEventId={selectedEventId}
events={events} setSelectedEventId={setSelectedEventId}
selectedEventId={selectedEventId} hoveredEventId={hoveredEventId}
setSelectedEventId={setSelectedEventId} setHoveredEventId={setHoveredEventId}
hoveredEventId={hoveredEventId} button1Name="Log New Artifacts"
setHoveredEventId={setHoveredEventId} button2Name="Search Artifacts"
button1Name="Log New Artifacts" ></Sidebar>
button2Name="Search Artifacts" </div>
></Sidebar> );
</div>
);
} }

View File

@ -1,7 +1,8 @@
// tells React if you're using its "server-side rendering" features. this component runs only on the client-side (browser) // 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 // importing React hooks or utilities that enhance functionality inside the component
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). useState: Used to manage state (data that changes over time).
FormEvent: TypeScript definition for form-related events like submission. FormEvent: TypeScript definition for form-related events like submission.
@ -12,39 +13,40 @@ import { useState, FormEvent, useRef, MouseEvent, useEffect } from "react";
// defining a type for the props (inputs) that AuthModal expects // 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 // 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); //AuthModalProps ensures TypeScript validates the props to match the type definition above
const modalRef = useRef<HTMLDivElement>(null); const [isLogin, setIsLogin] = useState<boolean>(true);
const [isFailed, setIsFailed] = useState<boolean>(false); const modalRef = useRef<HTMLDivElement>(null);
const [failMessage, setFailMessage] = useState<boolean>(false); const [isFailed, setIsFailed] = 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 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 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 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 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 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 // 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) .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 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) .contains(e.target) checks if the clicked element (e.target) is inside the modal (modalRef.current)
@ -55,8 +57,8 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthM
This means the user clicked outside the modal This means the user clicked outside the modal
*/ */
// LS - The following bit contains the more important code for what I'm focused on // 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. Note : handleSubmit is typically used as a event handler for submitting a form in react.
For example: For example:
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -64,105 +66,113 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthM
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
*/ */
/* /*
e is the parameter passsed into the function e is the parameter passsed into the function
async indicates the function runs asyncronously, meaning it performs tasks which take time. async indicates the function runs asyncronously, meaning it performs tasks which take time.
an example of this would be API calls 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); // new variable of class FormData is created. This is part of the standard Web API included in modern web browsers
const email = formData.get("email") as string; // gets email from form response const email = formData.get("email") as string; // gets email from form response
const password = formData.get("password") as string; // gets password from form response const password = formData.get("password") as string; // gets password from form response
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); // if the form is in login mode, the name is undefine, otherwise the name is a value obtained from the response
let endpoint = isLogin ? "/api/login" : "/api/signup"; // sets endpoint for backend code (either sign up or login) 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 const body = isLogin ? { email, password } : { name: name!, email, password }; // creates a json body for the backend
try {
console.log("Sending data to API");
const res = await fetch(endpoint, {
// sends a request to the server at the end point
method: "POST", // Post is used since the form submission modifies the server side state
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
console.log("Success!");
onClose(); // closes UI
} else if (res.status >= 400 && res.status < 500) {
const responseBody = await res.json();
console.log("4xx error:", responseBody.message);
setFailMessage(responseBody.message);
setIsFailed(true);
} else {
console.error("Error:", await res.text()); // logs error with error message sent to console
}
} catch (error) {
// catches any errors (e.g. Not connected to network)
console.error("Request failed:", error instanceof Error ? error.message : String(error));
}
};
try { return (
console.log("Sending data to API"); <div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50" onClick={handleOverlayClick}>
const res = await fetch(endpoint, { // sends a request to the server at the end point <div ref={modalRef} className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative">
method: "POST", // Post is used since the form submission modifies the server side state <button
headers: { "Content-Type": "application/json" }, //indicates it expects a json object returned onClick={() => {
body: JSON.stringify(body), // converts the body to a JSON string to be sent setIsFailed(false);
}); onClose();
if (res.ok) { //res.ok checks if the response is between 200-299 }}
console.log("Success!"); className="absolute text-xl top-0 right-2 text-neutral-500 hover:text-neutral-700"
onClose(); // closes UI aria-label="Close modal"
} else if (res.status >= 400 && res.status <500){ >
const responseBody = await res.json() ×
console.log("4xx error:", responseBody.message) </button>
setFailMessage(responseBody.message) <h2 className="text-2xl font-bold text-center mb-4">{isLogin ? "Login" : "Sign Up"}</h2>
setIsFailed(true)
} else{
console.error("Error:", await res.text()); // logs error with error message sent to console
}
} catch (error) {// catches any errors (e.g. Not connected to network)
console.error("Request failed:", error instanceof Error ? error.message : String(error));
}
};
return ( {/* Form */}
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50" onClick={handleOverlayClick}> <form onSubmit={handleSubmit} className="space-y-4">
<div ref={modalRef} className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative"> {!isLogin && (
<button onClick={() => {setIsFailed(false);onClose();}} className="absolute text-xl top-0 right-2 text-gray-500 hover:text-gray-700" aria-label="Close modal"> <div>
× <label className="block text-sm font-medium text-neutral-700">Full Name</label>
</button> <input
<h2 className="text-2xl font-bold text-center mb-4">{isLogin ? "Login" : "Sign Up"}</h2> type="text"
name="name"
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required={!isLogin}
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-neutral-700">Email</label>
<input
type="email"
name="email"
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700">Password</label>
<input
type="password"
name="password"
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
{isFailed && (
<div>
<label className="block text-sm font-medium text-red-700">{failMessage}</label>
</div>
)}
</div>
<button type="submit" className="w-full bg-blue-600 text-white p-2 rounded-md hover:bg-blue-700 transition">
{isLogin ? "Login" : "Sign Up"}
</button>
</form>
{/* Form */} <p className="mt-4 text-center text-sm">
<form onSubmit={handleSubmit} className="space-y-4"> {isLogin ? "Need an account?" : "Already have an account?"}{" "}
{!isLogin && ( <button onClick={() => setIsLogin(!isLogin)} className="text-blue-600 hover:underline">
<div> {isLogin ? "Sign Up" : "Login"}
<label className="block text-sm font-medium text-gray-700">Full Name</label> </button>
<input </p>
type="text" </div>
name="name" </div>
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" );
required={!isLogin}
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
name="email"
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Password</label>
<input
type="password"
name="password"
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
{ isFailed && (
<div>
<label className="block text-sm font-medium text-red-700">{failMessage}</label>
</div>
)
}
</div>
<button type="submit" className="w-full bg-blue-600 text-white p-2 rounded-md hover:bg-blue-700 transition">
{isLogin ? "Login" : "Sign Up"}
</button>
</form>
<p className="mt-4 text-center text-sm">
{isLogin ? "Need an account?" : "Already have an account?"}{" "}
<button onClick={() => setIsLogin(!isLogin)} className="text-blue-600 hover:underline">
{isLogin ? "Sign Up" : "Login"}
</button>
</p>
</div>
</div>
);
} }

View File

@ -1,100 +1,101 @@
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";
interface SidebarProps { interface SidebarProps {
logTitle: string; logTitle: string;
logSubtitle: string; logSubtitle: string;
recentsTitle: string; recentsTitle: string;
events: Event[]; events: Event[];
selectedEventId: Event["id"]; selectedEventId: Event["id"];
setSelectedEventId: Dispatch<SetStateAction<string>>; setSelectedEventId: Dispatch<SetStateAction<string>>;
hoveredEventId: Event["id"]; hoveredEventId: Event["id"];
setHoveredEventId: Dispatch<SetStateAction<string>>; setHoveredEventId: Dispatch<SetStateAction<string>>;
button1Name: string; button1Name: string;
button2Name: string; button2Name: string;
} }
function MagnitudeNumber({ magnitude }: { magnitude: number }) { function MagnitudeNumber({ magnitude }: { magnitude: number }) {
// Convert magnitude to string with one decimal place // Convert magnitude to string with one decimal place
const magnitudeStr = magnitude.toFixed(1); const magnitudeStr = magnitude.toFixed(1);
const [whole, decimal] = magnitudeStr.split("."); const [whole, decimal] = magnitudeStr.split(".");
// Define color based on magnitude (0-10 scale) // Define color based on magnitude (0-10 scale)
return ( return (
<div className={`relative`} style={{ color: getMagnitudeColor(magnitude) }}> <div className={`relative`} style={{ color: getMagnitudeColor(magnitude) }}>
<TbHexagon size={40} className="drop-shadow-sm" /> <TbHexagon size={40} className="drop-shadow-sm" />
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<div className="flex items-baseline font-mono font-bold tracking-tight"> <div className="flex items-baseline font-mono font-bold tracking-tight">
<span className="text-xl -mr-1">{whole}</span> <span className="text-xl -mr-1">{whole}</span>
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span> <span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
<span className="text-xs -mr-[1px]">{decimal}</span> <span className="text-xs -mr-[1px]">{decimal}</span>
</div> </div>
</div> </div>
</div> </div>
); );
} }
export default function Sidebar({ export default function Sidebar({
logTitle, logTitle,
logSubtitle, logSubtitle,
recentsTitle, recentsTitle,
events, events,
selectedEventId, selectedEventId,
setSelectedEventId, setSelectedEventId,
hoveredEventId, hoveredEventId,
setHoveredEventId, setHoveredEventId,
button1Name, button1Name,
button2Name, button2Name,
}: 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="px-6 pb-8 border-b border-neutral-200"> <div className="py-6">
<h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2> <div className="px-6 pb-8 border-b border-neutral-200">
<p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p> <h2 className={`text-2xl font-bold text-neutral-800 mb-2`}>{logTitle}</h2>
<Link href="/"> <p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p>
<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}
</button>
</Link>
<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">
{button2Name}
</button>
</Link>
</div>
<div className="flex-1 px-6 pt-6 overflow-y-auto"> <Link href="/">
<h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2> <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">
<div className="space-y-3"> {button1Name}
{events.map((event) => ( </button>
<button </Link>
key={event.title} <Link href="/">
className={`w-full border ${hoveredEventId === event.id ? "bg-neutral-100" : "bg-white"} ${ <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">
selectedEventId === event.id ? "border-neutral-800" : "border-neutral-200" {button2Name}
} rounded-lg p-4 flex items-center gap-4 hover:bg-neutral-100 transition-colors duration-150 shadow-sm text-left`} </button>
onClick={() => { </Link>
setSelectedEventId((prevEventId) => (prevEventId !== event.id ? event.id : "")); </div>
}}
onMouseEnter={() => setHoveredEventId(event.id)} <div className="flex-1 px-6 pt-6 overflow-y-auto">
onMouseLeave={() => setHoveredEventId("")} <h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2>
> <div className="space-y-3">
<div className="flex-1"> {events.map((event) => (
<p className="text-sm font-medium text-neutral-800">{event.title}</p> <button
{/* Uncomment if you want to use text1 */} key={event.title}
{/* <p className="text-xs text-neutral-600">{event.text1}</p> */} className={`w-full border ${hoveredEventId === event.id ? "bg-neutral-100" : "bg-white"} ${
<p className="text-xs text-neutral-500 mt-1">{event.text2}</p> selectedEventId === event.id ? "border-neutral-800" : "border-neutral-200"
</div> } rounded-lg p-4 flex items-center gap-4 hover:bg-neutral-100 transition-colors duration-150 shadow-sm text-left`}
{ onClick={() => {
(event.magnitude) ? <MagnitudeNumber magnitude={event.magnitude} /> : <></> setSelectedEventId((prevEventId) => (prevEventId !== event.id ? event.id : ""));
} }}
</button> onMouseEnter={() => setHoveredEventId(event.id)}
))} onMouseLeave={() => setHoveredEventId("")}
</div> >
</div> <div className="flex-1">
</div> <p className="text-sm font-medium text-neutral-800">{event.title}</p>
); {/* Uncomment if you want to use text1 */}
{/* <p className="text-xs text-neutral-600">{event.text1}</p> */}
<p className="text-xs text-neutral-500 mt-1">{event.text2}</p>
</div>
{event.magnitude ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>}
</button>
))}
</div>
</div>
</div>
</div>
);
} }

View File

@ -1,75 +1,117 @@
"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";
import { Currency } from "@appTypes/StoreModel";
import AuthModal from "@components/AuthModal";
import { useStoreActions, useStoreState } from "@hooks/store";
function NavbarButton({ name, href, dropdownItems }: { name: string; href: string; dropdownItems?: string[] }) { function NavbarButton({ name, href, dropdownItems }: { name: string; href: string; dropdownItems?: string[] }) {
const pathname = usePathname(); const pathname = usePathname();
const isActive = dropdownItems ? dropdownItems.some((item) => pathname === `/${item.toLowerCase().replace(" ", "-")}`) : pathname === href; 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"}`}
<Link href={href} className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "hover:bg-neutral-200"}`}> >
{name} {name}
</Link> </span>
)} ) : (
{dropdownItems && ( <Link
<div className="absolute hidden group-hover:block top-full left-1/2 -translate-x-1/2 w-40 bg-white border border-neutral-300 rounded-lg overflow-hidden shadow-lg z-40"> href={href}
<ul> className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "hover:bg-neutral-200"}`}
{dropdownItems.map((item) => { >
const itemHref = `/${item.toLowerCase().replace(" ", "-")}`; {name}
const isDropdownActive = pathname === itemHref; </Link>
return ( )}
<li key={item}> {dropdownItems && (
<Link href={itemHref} className={`block px-4 py-2 hover:bg-neutral-100 ${isDropdownActive ? "bg-neutral-100" : ""}`}> <div className="absolute hidden group-hover:block top-full left-1/2 -translate-x-1/2 w-40 bg-white border border-neutral-300 rounded-lg overflow-hidden shadow-lg z-40">
{item} <ul>
</Link> {dropdownItems.map((item) => {
</li> const itemHref = `/${item.toLowerCase().replace(" ", "-")}`;
); const isDropdownActive = pathname === itemHref;
})} return (
</ul> <li key={item}>
</div> <Link
)} href={itemHref}
</button> className={`block px-4 py-2 hover:bg-neutral-100 ${isDropdownActive ? "bg-neutral-100" : ""}`}
); >
{item}
</Link>
</li>
);
})}
</ul>
</div>
)}
</button>
);
} }
export default function Navbar() { export default function Navbar({}: // currencySelector,
const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Warehouse", "Shop"], []); {
// const navOptions = useMemo(() => ["Earthquakes"], []); // currencySelector?: { selectedCurrency: string; setSelectedCurrency: Dispatch<SetStateAction<"GBP" | "USD" | "EUR">> };
const aboutDropdown = ["Contact Us", "Our Mission", "The Team"]; }) {
// { label: "Our Mission", path: "/our-mission" }, const pathname = usePathname();
// { label: "The Team", path: "/the-team" }, const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
// { label: "Contact Us", path: "/contact-us" }] 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 [isModalOpen, setIsModalOpen] = useState(false);
return ( const currencies = useStoreState((state) => state.currency.currencies);
<div className="flex sticky top-0 w-full h-14 z-40 font-medium bg-white border border-b-neutral-200"> const currencyTickers = useStoreState((state) => state.currency.tickers);
<div className="my-1 flex aspect-square ml-3 mr-3">
<Link href="/" className="rounded-full"> return (
<Image height={50} width={50} alt="Logo" className="border border-neutral-300 rounded-full" src="/logo.png" /> <div className="flex sticky top-0 w-full h-14 z-40 font-medium bg-white border border-b-neutral-200">
</Link> <div className="my-1 flex aspect-square ml-3 mr-3">
</div> <Link href="/" className="rounded-full">
<div className="flex"> <Image height={50} width={50} alt="Logo" className="border border-neutral-300 rounded-full" src="/logo.png" />
{navOptions.map((name) => ( </Link>
<NavbarButton name={name} href={`/${name.toLowerCase()}`} key={name} /> </div>
))} <div className="flex">
<NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} /> {navOptions.map((name) => (
</div> <NavbarButton name={name} href={`/${name.toLowerCase()}`} key={name} />
<div className="flex-grow" /> ))}
<button className="my-auto mr-4" onClick={() => setIsModalOpen(true)}> <NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} />
<FaRegUserCircle size={22} /> </div>
</button> <div className="flex-grow" />
<AuthModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div> {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)}>
<FaRegUserCircle size={22} />
</button>
<AuthModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div>
);
} }

View File

@ -1,44 +1,42 @@
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 </button>
</Link> </div>
</button>
</div>
{/* Section: Recent Events - Will need to be replaced with a link to the database*/} {/* Section: Recent Events - Will need to be replaced with a link to the database*/}
<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>
</div> </div>
); );
}; };
export default Sidebar; export default Sidebar;

View File

@ -1,44 +1,40 @@
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 <button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
</p> <Link href="/">Observatory News</Link>
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"> </button>
<Link href="/"> </div>
Observatory News
</Link>
</button>
</div>
{/* Section: Recent Events - Will need to be replaced with a link to the database*/} {/* Section: Recent Events - Will need to be replaced with a link to the database*/}
<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>
</div> </div>
); );
}; };
export default Sidebar; export default Sidebar;

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 };

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"],