cleaning shop, team, mission and contact pages

This commit is contained in:
Emily Neighbour 2025-05-10 14:05:56 +01:00
parent 0da8b7072e
commit be60b811a9
15 changed files with 404 additions and 368 deletions

BIN
public/artifacts.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
public/crackedRoad.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 KiB

BIN
public/tectonicPlate.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

View File

@ -1,11 +1,11 @@
import bcrypt from "bcrypt";
import { env } from "@utils/env";
import { NextResponse } from "next/server";
import { SignJWT } from "jose";
import bcrypt from 'bcrypt';
import { SignJWT } from 'jose';
import { NextResponse } from 'next/server';
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from '@prisma/client';
import { env } from '@utils/env';
import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite";
import { findUserByEmail, readUserCsv, User } from '../functions/csvReadWrite';
const usingPrisma = false;
let prisma: PrismaClient;
@ -45,12 +45,12 @@ export async function POST(req: Request) {
include: {
earthquakes: true,
observatories: true,
artefacts: true,
artifacts: true,
superior: true,
subordinates: true,
},
},
purchasedArtefacts: true,
purchasedArtifacts: true,
},
});

View File

@ -1,15 +1,15 @@
import { NextResponse } from "next/server";
import { env } from "@utils/env";
import { NextResponse } from 'next/server';
import { PrismaClient } from "@prisma/client";
import { verifyJwt } from "@utils/verifyJwt";
import { PrismaClient } from '@prisma/client';
import { env } from '@utils/env';
import { verifyJwt } from '@utils/verifyJwt';
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
// Artefact type
interface Artefact {
// Artifact type
interface Artifact {
id: number;
name: string;
description: string;
@ -29,7 +29,7 @@ export async function POST(req: Request) {
if (!token) return NextResponse.json({ message: "Unauthorised" }, { status: 401 });
await verifyJwt({ token, secret: env.JWT_SECRET_KEY });
const warehouseArtefacts: Artefact[] = [
const warehouseArtifacts: Artifact[] = [
{
id: 1,
name: "Solidified Lava Chunk",
@ -87,17 +87,17 @@ export async function POST(req: Request) {
},
];
let artefacts;
if (usingPrisma) artefacts = await prisma.artefacts.findMany();
let artifacts;
if (usingPrisma) artifacts = await prisma.artifacts.findMany();
if (artefacts) {
return NextResponse.json({ message: "Got artefacts successfully", artefacts }, { status: 200 });
if (artifacts) {
return NextResponse.json({ message: "Got artifacts successfully", artifacts }, { status: 200 });
} else {
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: warehouseArtefacts }, { status: 200 });
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: warehouseArtifacts }, { status: 200 });
// return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 });
}
} catch (error) {
console.error("Error in artefacts endpoint:", error);
console.error("Error in artifacts endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();

View File

@ -1,6 +1,6 @@
"use client";
import Image from "next/image";
import React, { useState } from "react";
import Image from 'next/image';
import React, { useState } from 'react';
const ContactUs = () => {
const [formData, setFormData] = useState({
@ -36,8 +36,8 @@ const ContactUs = () => {
<div className="max-w-4xl mx-auto p-5 mt-20">
{/* Header */}
<h1 className="text-4xl font-bold text-center text-neutral-50 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
<p className="text-lg text-center max-w-2xl text-neutral-200 mb-6">
Have questions or concerns about earthquakes, observatories or artifacts? Contact us via phone, email, social media or using the form below with the relevant
contact details.
</p>
@ -106,20 +106,20 @@ const ContactUs = () => {
{/* Contact Details Section */}
<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-neutral-800 mb-4">Get in Touch</h2>
<h2 className="text-2xl font-bold text-neutral-800 mb-4">Get in Touch</h2>
<div className="mb-4">
<h3 className="text-neutral-700 font-medium">Email</h3>
<a href="mailto:getintouch@tremortracker.com" style={{ color: "initial" }}>
<h3 className="text-neutral-700 font-bold font-large">Email</h3>
<a href="Mail to:getintouch@tremortracker.com font-medium" style={{ color: "initial" }}>
getintouch@tremortracker.com
</a>
</div>
<div className="mb-4">
<h3 className="text-neutral-700 font-medium">Phone</h3>
<p className="text-neutral-600">+44 7538 359022</p>
<h3 className="text-neutral-700 font-bold font-large">Phone</h3>
<p className="text-neutral-600 font-medium">+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>
<h3 className="text-neutral-700 font-bold font-large">Address</h3>
<p className="text-neutral-600 font-medium">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">

View File

@ -1,12 +1,12 @@
"use client";
import type { Metadata } from "next";
import "./globals.css";
import './globals.css';
import { action, createStore, StoreProvider } from "easy-peasy";
import { Inter } from "next/font/google";
import { action, createStore, StoreProvider } from 'easy-peasy';
import { Inter } from 'next/font/google';
import { StoreModel } from "@appTypes/StoreModel";
import Navbar from "@components/Navbar";
import { StoreModel } from '@appTypes/StoreModel';
import Navbar from '@components/Navbar';
const inter = Inter({
subsets: ["latin"],
@ -32,7 +32,7 @@ const store = createStore<StoreModel>({
// name: "Tim Howitz",
// role: "ADMIN",
// scientist: undefined,
// purchasedArtefacts: [],
// purchasedArtifacts: [],
// },
});

View File

@ -1,52 +1,61 @@
"use client";
import Image from "next/image";
import Image from 'next/image';
const OurMission = () => {
return (
<div
className="min-h-screen relative bg-fixed bg-cover bg-center text-white py-28 px-4"
className="min-h-screen relative bg-fixed bg-cover bg-center text-white"
style={{ backgroundImage: "url('destruction.jpg')" }}
>
{/* Overlay for Readability */}
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
{/* Content Area */}
<div className="relative z-10 max-w-5xl mx-auto p-8 bg-white bg-opacity-95 shadow-xl rounded-3xl">
<h1 className="text-4xl font-extrabold text-center text-neutral-800 mb-8 tracking-tight">Our Mission</h1>
<p className="text-xl text-neutral-600 leading-relaxed mb-6 max-w-3xl mx-auto">
At <span className="font-semibold text-blue-600">Tremor Tracker</span>, we empower communities worldwide to prepare for
and recover from earthquakes through education, cutting-edge research, and innovative technology.
</p>
<p className="text-xl text-neutral-600 leading-relaxed mb-8 max-w-3xl mx-auto">
We bridge scientific insights with public awareness, delivering resources, tools, and real-time updates to enhance
preparedness, save lives, and build resilience against seismic events.
</p>
<div className="flex flex-col md:flex-row md:justify-evenly gap-8 mt-8">
<div className="flex flex-col items-center p-6 hover:bg-neutral-50 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/education.png" alt="Education Icon" className="h-20 w-20 mb-4" />
<h3 className="text-xl font-bold text-neutral-700 mb-2">Education</h3>
<p className="text-sm text-neutral-500 text-center max-w-xs">
Delivering accessible resources to educate communities on earthquake preparedness.
{/* Centered content */}
<div className="relative z-20 flex flex-col items-center justify-center min-h-screen">
{/* Title & Mission Statement */}
<div className="mb-10 flex flex-col items-center">
<h1 className="text-4xl font-bold text-center tracking-tight mb-2 drop-shadow-lg">
Our Mission
</h1>
<p className="text-lg text-center max-w-2xl text-white drop-shadow-md">
Earthquake awareness accessible for everyone
</p>
</div>
<div className="flex flex-col items-center p-6 hover:bg-neutral-50 rounded-xl transition-colors duration-300">
{/* Content Area */}
<div className="max-w-5xl w-full p-8 bg-white bg-opacity-90 shadow-xl rounded-3xl">
<p className="text-xl text-black leading-relaxed mb-6 max-w-3xl mx-auto">
At <span className="font-bold text-black">Tremor Tracker</span>, we empower communities worldwide to understand where and why earthquakes occur to enable better preparation
and recovery. Education, cutting-edge research, and innovative technology combine together.
</p>
<p className="text-xl text-black leading-relaxed mb-8 max-w-3xl mx-auto">
We combine scientific insights with public awareness, delivering resources, tools, and real-time updates to enhance
preparedness for earthquakes in order to save lives, improve recovery efficiency, and build resilience against seismic events.
</p>
<div className="flex flex-col md:flex-row md:justify-evenly gap-8 mt-8">
<div className="flex flex-col items-center p-6 hover:bg-white hover:bg-opacity-10 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/education.png" alt="Education Icon" className="h-20 w-20 mb-4" />
<h3 className="text-xl font-bold text-black mb-2">Education</h3>
<p className="text-sm text-black text-center max-w-xs opacity-90">
Delivering accessible resources to educate communities on earthquakes.
</p>
</div>
<div className="flex flex-col items-center p-6 hover:bg-white hover:bg-opacity-10 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/research.jpg" alt="Research Icon" className="h-20 w-20 mb-4" />
<h3 className="text-xl font-bold text-neutral-700 mb-2">Research</h3>
<p className="text-sm text-neutral-500 text-center max-w-xs">
<h3 className="text-xl font-bold text-black mb-2">Research</h3>
<p className="text-sm text-black text-center max-w-xs opacity-90">
Advancing scientific studies to deepen understanding of seismic activity.
</p>
</div>
<div className="flex flex-col items-center p-6 hover:bg-neutral-50 rounded-xl transition-colors duration-300">
<div className="flex flex-col items-center p-6 hover:bg-white hover:bg-opacity-10 rounded-xl transition-colors duration-300">
<Image height={100} width={100} src="/tech.jpg" alt="Technology Icon" className="h-20 w-20 mb-4" />
<h3 className="text-xl font-bold text-neutral-700 mb-2">Technology</h3>
<p className="text-sm text-neutral-500 text-center max-w-xs">
<h3 className="text-xl font-bold text-black mb-2">Technology</h3>
<p className="text-sm text-black text-center max-w-xs opacity-90">
Harnessing innovation for real-time alerts and safety solutions.
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default OurMission;

View File

@ -1,20 +1,19 @@
"use client";
import Image from "next/image";
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import Image from 'next/image';
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import Artefact from "@appTypes/Artefact";
import { Currency } from "@appTypes/StoreModel";
import Sidebar from "@components/Sidebar";
import { useStoreState } from "@hooks/store";
import Artifact from '@appTypes/Artifact';
import { Currency } from '@appTypes/StoreModel';
import { useStoreState } from '@hooks/store';
// Artefacts Data
const artefacts: Artefact[] = [
// Artifacts Data
const artifacts: Artifact[] = [
{
id: 1,
name: "Golden Scarab",
description: "An ancient Egyptian artefact symbolizing rebirth.",
description: "An ancient Egyptian artifact symbolizing rebirth.",
location: "Cairo, Egypt",
image: "/artefact1.jpg",
image: "/artifact1.jpg",
price: 150,
},
{
@ -22,7 +21,7 @@ const artefacts: Artefact[] = [
name: "Aztec Sunstone",
description: "A replica of the Aztec calendar (inscriptions intact).",
location: "Peru",
image: "/artefact2.jpg",
image: "/artifact2.jpg",
price: 200,
},
{
@ -30,7 +29,7 @@ const artefacts: Artefact[] = [
name: "Medieval Chalice",
description: "Used by royalty in medieval ceremonies.",
location: "Cambridge, England",
image: "/artefact3.jpg",
image: "/artifact3.jpg",
price: 120,
},
{
@ -38,7 +37,7 @@ const artefacts: Artefact[] = [
name: "Roman Coin",
description: "An authentic Roman coin from the 2nd century CE.",
location: "Rome, Italy",
image: "/artefact4.jpg",
image: "/artifact4.jpg",
price: 80,
},
{
@ -46,7 +45,7 @@ const artefacts: Artefact[] = [
name: "Samurai Mask",
description: "Replica of Japanese Samurai battle masks.",
location: "Tokyo, Japan",
image: "/artefact5.jpg",
image: "/artifact5.jpg",
price: 300,
},
{
@ -54,7 +53,7 @@ const artefacts: Artefact[] = [
name: "Ancient Greek Vase",
description: "Depicts Greek mythology, found in the Acropolis.",
location: "Athens, Greece",
image: "/artefact6.jpg",
image: "/artifact6.jpg",
price: 250,
},
{
@ -62,7 +61,7 @@ const artefacts: Artefact[] = [
name: "Incan Pendant",
description: "Represents the Sun God Inti.",
location: "India",
image: "/artefact7.jpg",
image: "/artifact7.jpg",
price: 175,
},
{
@ -70,7 +69,7 @@ const artefacts: Artefact[] = [
name: "Persian Carpet Fragment",
description: "Ancient Persian artistry.",
location: "Petra, Jordan",
image: "/artefact8.jpg",
image: "/artifact8.jpg",
price: 400,
},
{
@ -78,7 +77,7 @@ const artefacts: Artefact[] = [
name: "Stone Buddha",
description: "Authentic stone Buddha carving.",
location: "India",
image: "/artefact9.jpg",
image: "/artifact9.jpg",
price: 220,
},
{
@ -86,7 +85,7 @@ const artefacts: Artefact[] = [
name: "Victorian Brooch",
description: "A beautiful Victorian-era brooch with a ruby centre.",
location: "Oxford, England",
image: "/artefact10.jpg",
image: "/artifact10.jpg",
price: 150,
},
{
@ -94,7 +93,7 @@ const artefacts: Artefact[] = [
name: "Ancient Scroll",
description: "A mysterious scroll from ancient times.",
location: "Madrid, Spain",
image: "/artefact11.jpg",
image: "/artifact11.jpg",
price: 500,
},
{
@ -102,7 +101,7 @@ const artefacts: Artefact[] = [
name: "Ming Dynasty Porcelain",
description: "Porcelain from China's Ming Dynasty.",
location: "Beijing, China",
image: "/artefact12.jpg",
image: "/artifact12.jpg",
price: 300,
},
{
@ -110,15 +109,15 @@ const artefacts: Artefact[] = [
name: "African Tribal Mask",
description: "A unique tribal mask from Africa.",
location: "Nigeria",
image: "/artefact13.jpg",
image: "/artifact13.jpg",
price: 250,
},
{
id: 14,
name: "Crystal Skull",
description: "A mystical pre-Columbian artefact.",
description: "A mystical pre-Columbian artifact.",
location: "Colombia",
image: "/artefact14.jpg",
image: "/artifact14.jpg",
price: 1000,
},
{
@ -126,68 +125,74 @@ const artefacts: Artefact[] = [
name: "Medieval Armor Fragment",
description: "A fragment of medieval armor.",
location: "Normandy, France",
image: "/artefact15.jpg",
image: "/artifact15.jpg",
price: 400,
},
{
id: 16,
name: "Medieval Helmet Fragment",
description: "A fragment of a medieval helmet.",
location: "Normandy, France",
image: "/artifact16.jpg",
price: 500,
},
];
// Modal Component
// Shop Component
export default function Shop() {
const [currentPage, setCurrentPage] = useState(1);
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null); // Track selected artefact for modal
const artefactsPerPage = 9; // Number of artefacts per page
const indexOfLastArtefact = currentPage * artefactsPerPage;
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
const currentArtefacts = artefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
const [selectedArtifact, setSelectedArtifact] = useState<Artifact | null>(null);
const artifactsPerPage = 12;
const indexOfLastArtifact = currentPage * artifactsPerPage;
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
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 convertPrice = useCallback(
(price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2),
[]
);
const handleNextPage = () => {
if (indexOfLastArtefact < artefacts.length) {
if (indexOfLastArtifact < artifacts.length) {
setCurrentPage((prev) => prev + 1);
}
};
const handlePreviousPage = () => {
if (currentPage > 1) {
setCurrentPage((prev) => prev - 1);
}
};
function Modal({ artefact }: { artefact: Artefact }) {
if (!artefact) return null;
function Modal({ artifact }: { artifact: Artifact }) {
if (!artifact) return null;
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) {
setSelectedArtefact(null);
setSelectedArtifact(null);
}
};
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-xl shadow-2xl max-w-lg w-full p-6">
<h3 className="text-2xl font-bold mb-4">{artefact.name}</h3>
<h3 className="text-2xl font-bold mb-4">{artifact.name}</h3>
<Image
height={5000}
width={5000}
src={artefact.image}
alt={artefact.name}
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(artefact.price, selectedCurrency)}
{convertPrice(artifact.price, selectedCurrency)}
</p>
<p className="text-neutral-600 mt-2">{artefact.description}</p>
<p className="text-neutral-500 font-bold mt-1">Location: {artefact.location}</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!")}
@ -201,20 +206,19 @@ export default function Shop() {
);
}
// ArtefactCard Component
function ArtefactCard({ artefact }: { artefact: Artefact }) {
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={() => setSelectedArtefact(artefact)} // Opens modal
onClick={() => setSelectedArtifact(artifact)}
>
<img src={artefact.image} alt={artefact.name} className="w-full h-56 object-cover" />
<img src={artifact.image} alt={artifact.name} className="w-full h-56 object-cover" />
<div className="p-4">
<h3 className="text-lg font-semibold">{artefact.name}</h3>
<p className="text-neutral-500 mb-2">{artefact.location}</p>
<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(artefact.price, selectedCurrency)}
{convertPrice(artifact.price, selectedCurrency)}
</p>
</div>
</div>
@ -222,19 +226,34 @@ export default function Shop() {
}
return (
<div className="flex flex-col min-h-screen bg-neutral-100">
{/* Main Content */}
<div className="flex flex-1 overflow-y-auto">
{/* Artefact Grid */}
<div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 p-6">
{currentArtefacts.map((artefact) => (
<ArtefactCard key={artefact.id} artefact={artefact} />
<div
className="min-h-screen relative flex flex-col"
style={{
backgroundImage: "url('/artifacts.jpg')",
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{/* Overlay */}
<div className="absolute inset-0 bg-black bg-opacity-50 z-0"></div>
<div className="relative z-10 flex flex-col items-center w-full px-2 py-12">
{/* Title & Subheading */}
<h1 className="text-4xl md:text-4xl font-bold text-center text-white mb-2 tracking-tight drop-shadow-lg">
Artifact Shop
</h1>
<p className="text-lg md:text-xl text-center text-white mb-10 drop-shadow-md max-w-2xl">
Discover extraordinary historical artifacts and collectibles from major seismic events from around the world - now available for purchase.
</p>
{/* Artifact Grid */}
<div className="w-full max-w-7xl grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 p-2"> {/* gap-10 for more spacing */}
{currentArtifacts.map((artifact) => (
<ArtifactCard key={artifact.id} artifact={artifact} />
))}
</div>
</div>
{/* Pagination Footer */}
<footer className="mt-10 bg-white border-t border-neutral-300 py-3 text-center flex justify-center items-center">
<footer className="mt-10 bg-white bg-opacity-90 border-neutral-300 py-3 text-center flex justify-center items-center w-100 max-w-7xl rounded-lg">
<button
onClick={handlePreviousPage}
disabled={currentPage === 1}
@ -247,16 +266,18 @@ export default function Shop() {
<p className="mx-3 text-lg font-bold">{currentPage}</p>
<button
onClick={handleNextPage}
disabled={indexOfLastArtefact >= artefacts.length}
disabled={indexOfLastArtifact >= artifacts.length}
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
indexOfLastArtefact >= artefacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
indexOfLastArtifact >= artifacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
}`}
>
Next &rarr;
</button>
</footer>
</div>
{/* Modal */}
{selectedArtefact && <Modal artefact={selectedArtefact} />}
{selectedArtifact && <Modal artifact={selectedArtifact} />}
</div>
);
}

View File

@ -1,5 +1,4 @@
"use client";
const teamMembers = [
{
name: "Tim Howitz",
@ -30,22 +29,29 @@ const teamMembers = [
image: "/Lukeshanthescientist.PNG",
},
];
export default function Page() {
return (
<div className="h-full text-center bg-gradient-to-b from-neutral-50 to-neutral-100 pt-14 px-4">
<div
className="min-h-screen relative flex flex-col items-center justify-center px-4 py-20"
style={{ backgroundImage: "url('tectonicPlate.jpg')", backgroundSize: "cover", backgroundPosition: "center" }}
>
{/* Overlay */}
<div className="absolute inset-0 bg-black bg-opacity-50 pointer-events-none"></div>
{/* Header */}
<h1 className="text-5xl font-extrabold text-neutral-800 mb-6 tracking-tight">Meet Our Team</h1>
<p className="text-xl text-neutral-600 mb-12 max-w-2xl mx-auto">
Our world-class scientists drive innovation across the globe. Meet our department heads below.
<div className="relative z-10 flex flex-col items-center mb-1">
<h1 className="text-4xl font-bold text-center text-white mb-4 tracking-tight drop-shadow-lg">
Meet Our Team
</h1>
<p className="text-lg text-center max-w-2xl text-white mb-12 drop-shadow-md">
Our world-class scientists and engineers drive innovation across the globe. Meet our four department heads:
</p>
</div>
{/* Team Members Section */}
<div className="flex flex-col items-center gap-6">
<div className="relative z-10 flex flex-col items-center gap-6 w-full max-w-3xl">
{teamMembers.map((member, index) => (
<div
key={index}
className="flex bg-white rounded-3xl border border-neutral-200 w-full max-w-[50%] shadow-md hover:shadow-lg transition-shadow duration-300"
className="flex bg-white bg-opacity-90 rounded-3xl border border-neutral-200 w-full shadow-md hover:shadow-lg transition-shadow duration-300"
>
{/* Image */}
<div className="flex items-center ml-6">
@ -55,7 +61,6 @@ export default function Page() {
</div>
</div>
</div>
{/* Text Content */}
<div className="flex flex-col items-start pl-8 py-4 pr-6">
<h2 className="text-2xl font-bold text-neutral-800">{member.name}</h2>

View File

@ -1,12 +1,12 @@
"use client";
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";
// import type { Artefact } from "@prisma/client";
import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import { FaTimes } from 'react-icons/fa';
import { FaCalendarPlus, FaCartShopping, FaWarehouse } from 'react-icons/fa6';
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from 'react-icons/io5';
interface Artefact {
// import type { Artifact } from "@prisma/client";
interface Artifact {
id: number;
name: string;
description: string;
@ -18,8 +18,8 @@ interface Artefact {
dateAdded: string;
}
// Warehouse Artefacts Data
const warehouseArtefacts: Artefact[] = [
// Warehouse Artifacts Data
const warehouseArtifacts: Artifact[] = [
{
id: 1,
name: "Solidified Lava Chunk",
@ -140,14 +140,14 @@ function FilterInput({
}
// Table Component
function ArtefactTable({
artefacts,
function ArtifactTable({
artifacts,
filters,
setFilters,
setEditArtefact,
setEditArtifact,
clearSort,
}: {
artefacts: Artefact[];
artifacts: Artifact[];
filters: Record<string, string>;
setFilters: Dispatch<
SetStateAction<{
@ -162,15 +162,15 @@ function ArtefactTable({
dateAdded: string;
}>
>;
setEditArtefact: (artefact: Artefact) => void;
setEditArtifact: (artifact: Artifact) => void;
clearSort: () => void;
}) {
const [sortConfig, setSortConfig] = useState<{
key: keyof Artefact;
key: keyof Artifact;
direction: "asc" | "desc";
} | null>(null);
const handleSort = (key: keyof Artefact) => {
const handleSort = (key: keyof Artifact) => {
setSortConfig((prev) => {
if (!prev || prev.key !== key) {
return { key, direction: "asc" };
@ -186,9 +186,9 @@ function ArtefactTable({
clearSort();
};
const sortedArtefacts = useMemo(() => {
if (!sortConfig) return artefacts;
const sorted = [...artefacts].sort((a, b) => {
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;
@ -196,9 +196,9 @@ function ArtefactTable({
return 0;
});
return sorted;
}, [artefacts, sortConfig]);
}, [artifacts, sortConfig]);
const columns: { label: string; key: keyof Artefact; width: string }[] = [
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%" },
@ -217,7 +217,7 @@ function ArtefactTable({
{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 Artefact)}>
<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">
@ -250,11 +250,11 @@ function ArtefactTable({
</tr>
</thead>
<tbody>
{sortedArtefacts.map((artefact) => (
{sortedArtifacts.map((artifact) => (
<tr
key={artefact.id}
key={artifact.id}
className="border-b border-neutral-200 hover:bg-neutral-100 cursor-pointer"
onClick={() => setEditArtefact(artefact)}
onClick={() => setEditArtifact(artifact)}
>
{columns.map(({ key, width }) => (
<td
@ -263,18 +263,18 @@ function ArtefactTable({
style={{ width }}
>
{key === "isRequired"
? artefact.isRequired
? artifact.isRequired
? "Yes"
: "No"
: key === "isSold"
? artefact.isSold
? artifact.isSold
? "Yes"
: "No"
: key === "isCollected"
? artefact.isCollected
? artifact.isCollected
? "Yes"
: "No"
: artefact[key]}
: artifact[key]}
</td>
))}
</tr>
@ -284,7 +284,7 @@ function ArtefactTable({
);
}
// Modal Component for Logging Artefact
// Modal Component for Logging Artifact
function LogModal({ onClose }: { onClose: () => void }) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
@ -312,7 +312,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
alert(`Logged ${name} to storage: ${storageLocation}`);
onClose();
} catch {
setError("Failed to log artefact. Please try again.");
setError("Failed to log artifact. Please try again.");
} finally {
setIsSubmitting(false);
}
@ -324,7 +324,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
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 Artefact</h3>
<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
@ -333,7 +333,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
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="Artefact Name"
aria-label="Artifact Name"
disabled={isSubmitting}
/>
<textarea
@ -341,7 +341,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
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="Artefact Description"
aria-label="Artifact Description"
disabled={isSubmitting}
/>
<input
@ -350,7 +350,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
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="Artefact Location"
aria-label="Artifact Location"
disabled={isSubmitting}
/>
<input
@ -377,7 +377,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
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 Artefact"
aria-label="Required Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
@ -416,7 +416,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
Logging...
</>
) : (
"Log Artefact"
"Log Artifact"
)}
</button>
</div>
@ -524,16 +524,16 @@ function BulkLogModal({ onClose }: { onClose: () => void }) {
);
}
// Modal Component for Editing Artefact
function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => void }) {
const [name, setName] = useState(artefact.name);
const [description, setDescription] = useState(artefact.description);
const [location, setLocation] = useState(artefact.location);
const [earthquakeId, setEarthquakeId] = useState(artefact.earthquakeId);
const [isRequired, setIsRequired] = useState(artefact.isRequired);
const [isSold, setIsSold] = useState(artefact.isSold);
const [isCollected, setIsCollected] = useState(artefact.isCollected);
const [dateAdded, setDateAdded] = useState(artefact.dateAdded);
// 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);
@ -551,10 +551,10 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
setIsSubmitting(true);
try {
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
alert(`Updated artefact ${name}`);
alert(`Updated artifact ${name}`);
onClose();
} catch {
setError("Failed to update artefact. Please try again.");
setError("Failed to update artifact. Please try again.");
} finally {
setIsSubmitting(false);
}
@ -566,7 +566,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
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 Artefact</h3>
<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
@ -574,14 +574,14 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
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="Artefact Name"
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="Artefact Description"
aria-label="Artifact Description"
disabled={isSubmitting}
/>
<input
@ -589,7 +589,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
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="Artefact Location"
aria-label="Artifact Location"
disabled={isSubmitting}
/>
<input
@ -606,7 +606,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
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 Artefact"
aria-label="Required Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
@ -617,7 +617,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
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 Artefact"
aria-label="Sold Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Sold</label>
@ -628,7 +628,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
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 Artefact"
aria-label="Collected Artifact"
disabled={isSubmitting}
/>
<label className="text-sm text-neutral-600">Collected</label>
@ -685,18 +685,18 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
}
// Filter Logic
const applyFilters = (artefacts: Artefact[], filters: Record<string, string>): Artefact[] => {
return artefacts.filter((artefact) => {
const applyFilters = (artifacts: Artifact[], filters: Record<string, string>): Artifact[] => {
return artifacts.filter((artifact) => {
return (
(filters.id === "" || artefact.id.toString().includes(filters.id)) &&
(filters.name === "" || artefact.name.toLowerCase().includes(filters.name.toLowerCase())) &&
(filters.earthquakeId === "" || artefact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) &&
(filters.location === "" || artefact.location.toLowerCase().includes(filters.location.toLowerCase())) &&
(filters.description === "" || artefact.description.toLowerCase().includes(filters.description.toLowerCase())) &&
(filters.isRequired === "" || (filters.isRequired === "true" ? artefact.isRequired : !artefact.isRequired)) &&
(filters.isSold === "" || (filters.isSold === "true" ? artefact.isSold : !artefact.isSold)) &&
(filters.isCollected === "" || (filters.isCollected === "true" ? artefact.isCollected : !artefact.isCollected)) &&
(filters.dateAdded === "" || artefact.dateAdded === filters.dateAdded)
(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)
);
});
};
@ -706,7 +706,7 @@ export default function Warehouse() {
const [currentPage, setCurrentPage] = useState(1);
const [showLogModal, setShowLogModal] = useState(false);
const [showBulkLogModal, setShowBulkLogModal] = useState(false);
const [editArtefact, setEditArtefact] = useState<Artefact | null>(null);
const [editArtifact, setEditArtifact] = useState<Artifact | null>(null);
const [filters, setFilters] = useState({
id: "",
name: "",
@ -720,29 +720,29 @@ export default function Warehouse() {
});
const [isFiltering, setIsFiltering] = useState(false);
const [sortConfig, setSortConfig] = useState<{
key: keyof Artefact;
key: keyof Artifact;
direction: "asc" | "desc";
} | null>(null);
const artefactsPerPage = 10;
const indexOfLastArtefact = currentPage * artefactsPerPage;
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
const artifactsPerPage = 10;
const indexOfLastArtifact = currentPage * artifactsPerPage;
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
// Apply filters with loading state
const filteredArtefacts = useMemo(() => {
const filteredArtifacts = useMemo(() => {
setIsFiltering(true);
const result = applyFilters(warehouseArtefacts, filters);
const result = applyFilters(warehouseArtifacts, filters);
setIsFiltering(false);
return result;
}, [filters]);
const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
const currentArtifacts = filteredArtifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
// Overview stats
const totalArtefacts = warehouseArtefacts.length;
const totalArtifacts = warehouseArtifacts.length;
const today = "2025-05-04";
const artefactsAddedToday = warehouseArtefacts.filter((a) => a.dateAdded === today).length;
const artefactsSoldToday = warehouseArtefacts.filter((a) => a.isSold && a.dateAdded === today).length;
const artifactsAddedToday = warehouseArtifacts.filter((a) => a.dateAdded === today).length;
const artifactsSoldToday = warehouseArtifacts.filter((a) => a.isSold && a.dateAdded === today).length;
const clearFilters = () => {
setFilters({
@ -768,15 +768,15 @@ export default function Warehouse() {
<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 Artefacts: <span className="font-semibold ml-1">{totalArtefacts}</span>
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">{artefactsAddedToday}</span>
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">{artefactsSoldToday}</span>
Sold Today: <span className="font-semibold ml-1">{artifactsSoldToday}</span>
</div>
</div>
@ -792,7 +792,7 @@ export default function Warehouse() {
onClick={() => setShowLogModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log Single Artefact
Log Single Artifact
</button>
<button
onClick={() => setShowBulkLogModal(true)}
@ -822,11 +822,11 @@ export default function Warehouse() {
</div>
)}
<div className="h-full overflow-y-none">
<ArtefactTable
artefacts={currentArtefacts}
<ArtifactTable
artifacts={currentArtifacts}
filters={filters}
setFilters={setFilters}
setEditArtefact={setEditArtefact}
setEditArtifact={setEditArtifact}
clearSort={() => setSortConfig(null)}
/>
</div>
@ -837,7 +837,7 @@ export default function Warehouse() {
{/* Modals */}
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
{editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />}
{editArtifact && <EditModal artifact={editArtifact} onClose={() => setEditArtifact(null)} />}
</div>
);
}

View File

@ -1 +1 @@
Artefacts
Artifacts
1 Artefacts Artifacts

View File

@ -13,7 +13,7 @@ model User {
passwordHash String
role String @default("GUEST") @db.VarChar(10) // ADMIN, SCIENTIST, GUEST
scientist Scientist? @relation
purchasedArtefacts Artefact[] @relation("UserPurchasedArtefacts")
purchasedArtifacts Artifact[] @relation("UserPurchasedArtifacts")
}
// Scientist model
@ -29,7 +29,7 @@ model Scientist {
subordinates Scientist[] @relation("SuperiorRelation")
earthquakes Earthquake[] @relation("ScientistEarthquakeCreator")
observatories Observatory[] @relation("ScientistObservatoryCreator")
artefacts Artefact[] @relation("ScientistArtefactCreator")
artifacts Artifact[] @relation("ScientistArtifactCreator")
}
// Earthquake model
@ -45,7 +45,7 @@ model Earthquake {
depth Float
creatorId Int?
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
artefacts Artefact[]
artifacts Artifact[]
observatories Observatory[] @relation("EarthquakeObservatory")
}
@ -66,8 +66,8 @@ model Observatory {
earthquakes Earthquake[] @relation("EarthquakeObservatory")
}
// Artefact model
model Artefact {
// Artifact model
model Artifact {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -76,10 +76,10 @@ model Artefact {
earthquakeId Int
earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
creatorId Int?
creator Scientist? @relation("ScientistArtefactCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
creator Scientist? @relation("ScientistArtifactCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
required Boolean @default(true)
shopPrice Float? // In Euros
purchasedById Int?
purchasedBy User? @relation("UserPurchasedArtefacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction)
purchasedBy User? @relation("UserPurchasedArtifacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction)
pickedUp Boolean @default(false)
}

View File

@ -1,4 +1,4 @@
interface Artefact {
interface Artifact {
// todo change to string
id: number;
name: string;
@ -8,4 +8,4 @@ interface Artefact {
price: number;
}
export default Artefact;
export default Artifact;

View File

@ -1,4 +1,5 @@
import { Action } from "easy-peasy";
import { Action } from 'easy-peasy';
// import type { User } from "@prisma/client";
interface Scientist {
@ -13,10 +14,10 @@ interface Scientist {
subordinates: Scientist[];
// earthquakes: Earthquake[];
// observatories: Observatory[];
artefacts: Artefact[];
artifacts: Artifact[];
}
interface Artefact {
interface Artifact {
id: number;
name: string;
description: string;
@ -36,7 +37,7 @@ interface User {
passwordHash: string;
role: string;
scientist: Scientist | undefined;
purchasedArtefacts: Artefact[];
purchasedArtifacts: Artifact[];
}
type Currency = "GBP" | "USD" | "EUR";