Compare commits

...

14 Commits

Author SHA1 Message Date
dce7e51a53 More renaming 2025-05-13 10:24:58 +01:00
b0ec146bf0 Renamed to british spelling 2025-05-13 10:00:09 +01:00
Emily Neighbour
579c1c205a shop extras 2025-05-12 21:38:46 +01:00
Emily Neighbour
52f17d5a00 emily user to edit admin 2025-05-12 14:00:03 +01:00
Emily Neighbour
8a45f49a04 admin button added 2025-05-12 13:52:47 +01:00
Emily Neighbour
8d007a7393 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-12 13:49:18 +01:00
Emily Neighbour
8fefc67428 extra bits 2025-05-12 13:49:15 +01:00
IZZY
4890af1e25 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-12 13:44:57 +01:00
IZZY
628b4a9370 Did stuff 2025-05-12 13:42:44 +01:00
Emily Neighbour
06403d254f Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-12 13:26:01 +01:00
IZZY
1cdace58b0 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-12 13:10:55 +01:00
IZZY
28e6a556f8 Fixed prisma schema file 2025-05-12 13:10:51 +01:00
IZZY
0b6167ed4b Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-12 12:20:40 +01:00
IZZY
b615524c6c Updated all the bcrypt files to bcryptjs 2025-05-12 09:58:25 +01:00
25 changed files with 3118 additions and 1508 deletions

2
.env
View File

@ -1,2 +1,2 @@
DATABASE_URL="" DATABASE_URL="sqlserver://UK-DIET-SQL-T1:1433;database=Group8_DB;user=UserGroup8;password=aFgbsH1f2evK6xyP;trustServerCertificate=true"
JWT_SECRET_KEY=mysupersecretkey JWT_SECRET_KEY=mysupersecretkey

1844
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,9 @@
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/mapbox-gl": "^3.4.1", "@types/mapbox-gl": "^3.4.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"csv-parse": "^5.6.0",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"easy-peasy": "^6.1.0", "easy-peasy": "^6.1.0",
@ -29,11 +29,11 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mapbox-gl": "^3.10.0", "mapbox-gl": "^3.10.0",
"next": "15.1.7", "next": "^15.1.7",
"path": "^0.12.7", "path": "^0.12.7",
"prisma": "^6.4.1", "prisma": "^6.4.1",
"react": "^19.0.0", "react": "^19.1.0",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-node": "^1.0.2", "react-node": "^1.0.2",
@ -42,7 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^5.0.2",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",

85
prisma/schema.prisma Normal file
View File

@ -0,0 +1,85 @@
// Datasource configuration
datasource db {
provider = "sqlserver"
url = env("DATABASE_URL")
}
// User model
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
name String
email String @unique
passwordHash String
role String @default("GUEST") @db.VarChar(10) // ADMIN, SCIENTIST, GUEST
scientist Scientist? @relation
purchasedArtefacts Artefact[] @relation("UserPurchasedArtefacts")
}
// Scientist model
model Scientist {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
name String
level String @db.VarChar(10) // JUNIOR, SENIOR
user User @relation(fields: [userId], references: [id])
userId Int @unique
superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
superiorId Int?
subordinates Scientist[] @relation("SuperiorRelation")
earthquakes Earthquake[] @relation("ScientistEarthquakeCreator")
observatories Observatory[] @relation("ScientistObservatoryCreator")
artefacts Artefact[] @relation("ScientistArtefactCreator")
}
// Earthquake model
model Earthquake {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
date DateTime
location String
latitude String
longitude String
magnitude Float
depth Float
creatorId Int?
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
artefacts Artefact[]
observatories Observatory[] @relation("EarthquakeObservatory")
}
// Observatory model
model Observatory {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
location String
longitude String
latitude String
dateEstablished Int?
functional Boolean
seismicSensorOnline Boolean @default(true)
creatorId Int?
creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
earthquakes Earthquake[] @relation("EarthquakeObservatory")
}
// Artefact model
model Artefact {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
warehouseArea String // Examples: "ZoneA-Shelf1", "ZoneB-Rack2", "ZoneC-Bin3"
earthquakeId Int
earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
creatorId Int?
creator Scientist? @relation("ScientistArtefactCreator", 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)
pickedUp Boolean @default(false)
}

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 KiB

1573
public/earthquakes.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,54 +6,28 @@ const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient(); if (usingPrisma) prisma = new PrismaClient();
export async function GET(request: Request) { export async function POST(req: Request) {
try { try {
const events = [ const json = await req.json(); // Parse incoming JSON data
{ const {rangeDaysPrev} = json.body
id: "1234",
title: "Earthquake in Germany",
text1: "Magnitude 8.5",
text2: "30 minutes ago",
magnitude: 8.5,
longitude: 10.4515, // Near Berlin, Germany
latitude: 52.52,
},
{
id: "2134",
title: "Earthquake in California",
text1: "Magnitude 5.3",
text2: "2 hours ago",
magnitude: 5.3,
longitude: -122.4194, // Near San Francisco, California, USA
latitude: 37.7749,
},
{
id: "2314",
title: "Tremor in Japan",
text1: "Magnitude 4.7",
text2: "5 hours ago",
magnitude: 4.7,
longitude: 139.6917, // Near Tokyo, Japan
latitude: 35.6762,
},
{
id: "2341",
title: "Tremor in Spain",
text1: "Magnitude 2.1",
text2: "10 hours ago",
magnitude: 2.1,
longitude: -3.7038, // Near Madrid, Spain
latitude: 40.4168,
},
];
let earthquakes; const now = new Date()
if (usingPrisma) earthquakes = await prisma.earthquakes.findMany(); const rangeBeginning = new Date();
rangeBeginning.setDate(rangeBeginning.getDate() - rangeDaysPrev)
const earthquakes = await prisma.earthquake.findMany(
{where: {
date: {
gte: rangeBeginning,
lte: now
}
}}
);
if (earthquakes) { if (earthquakes) {
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 }); return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 });
} else { } else {
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: events }, { status: 200 }); return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 });
// return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 }); // return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 });
} }
} catch (error) { } catch (error) {

View File

@ -0,0 +1,63 @@
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import fs from "fs/promises";
import path from "path";
import { parse } from "csv-parse/sync";
// Define the path to your CSV file.
// Place your earthquakes.csv in your project root or `public` directory
const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv");
const prisma = new PrismaClient();
type CsvRow = {
Date: string;
Magnitude: string;
Latitude: string;
Longitude: string;
Location: string;
Depth: string;
};
export async function POST() {
try {
// 1. Read the CSV file
const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse the CSV
const records: CsvRow[] = parse(fileContent, {
columns: true,
skip_empty_lines: true
});
// 3. Transform each CSV row to Earthquake model
// Since your prisma model expects: name, date (DateTime), location, magnitude (float), depth (float). We'll fill casualties/creatorId as zero/null for now.
const earthquakes = records.map(row => {
// You may want to add better parsing & validation depending on your actual data
return {
date: new Date(row.Date),
location: row.Location,
magnitude: parseFloat(row.Magnitude),
latitude: row.Latitude,
longitude: row.Longitude,
depth: parseFloat(row.Depth.replace(" km", "")),
// todo add creatorId
creatorId: null
};
});
// 4. Bulk create earthquakes in database:
// Consider chunking if your CSV is large!
await prisma.earthquake.createMany({
data: earthquakes,
skipDuplicates: true, // in case the route is called twice
});
return NextResponse.json({ success: true, count: earthquakes.length });
} catch (error: any) {
console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
}
}

View File

@ -1,11 +1,11 @@
import bcrypt from 'bcrypt'; import bcryptjs from "bcryptjs";
import { SignJWT } from 'jose'; import { SignJWT } from "jose";
import { NextResponse } from 'next/server'; import { NextResponse } from "next/server";
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from "@prisma/client";
import { env } from '@utils/env'; import { env } from "@utils/env";
import { findUserByEmail, readUserCsv, User } from '../functions/csvReadWrite'; import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite";
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
@ -33,7 +33,7 @@ export async function POST(req: Request) {
user = findUserByEmail(userData, email); user = findUserByEmail(userData, email);
} }
if (user && bcrypt.compareSync(password, usingPrisma ? user.hashedPassword : user.password)) { if (user && bcryptjs.compareSync(password, usingPrisma ? user.passwordHash : user.password)) {
// todo remove password from returned user // todo remove password from returned user
// get user and relations // get user and relations
@ -45,12 +45,12 @@ export async function POST(req: Request) {
include: { include: {
earthquakes: true, earthquakes: true,
observatories: true, observatories: true,
artifacts: true, artefacts: true,
superior: true, superior: true,
subordinates: true, subordinates: true,
}, },
}, },
purchasedArtifacts: true, purchasedArtefacts: true,
}, },
}); });

View File

@ -1,4 +1,4 @@
import bcrypt from "bcrypt"; import bcryptjs from "bcryptjs";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
@ -56,7 +56,7 @@ export async function POST(req: Request) {
return NextResponse.json({ message: "Password check script failure" }, { status: 500 }); return NextResponse.json({ message: "Password check script failure" }, { status: 500 });
} else { } else {
try { try {
const passwordHash = await bcrypt.hash(password, 10); const passwordHash = await bcryptjs.hash(password, 10);
if (usingPrisma) { if (usingPrisma) {
// todo add sending back newUser // todo add sending back newUser
const newUser = await prisma.user.create({ const newUser = await prisma.user.create({

View File

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

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import Image from 'next/image'; import Image from "next/image";
import React, { useState } from 'react'; import React, { useState } from "react";
const ContactUs = () => { const ContactUs = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@ -37,8 +37,8 @@ const ContactUs = () => {
{/* Header */} {/* Header */}
<h1 className="text-4xl font-bold text-center text-neutral-50 mb-6">Contact Us</h1> <h1 className="text-4xl font-bold text-center text-neutral-50 mb-6">Contact Us</h1>
<p className="text-lg text-center max-w-2xl text-neutral-200 mb-6"> <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 Have questions or concerns about earthquakes, observatories or artefacts? Contact us via phone, email, social media or
contact details. using the form below with the relevant contact details.
</p> </p>
{/* Content Section */} {/* Content Section */}

View File

@ -23,17 +23,17 @@ const store = createStore<StoreModel>({
conversionRates: { GBP: 0.85, USD: 1.14, EUR: 1 }, conversionRates: { GBP: 0.85, USD: 1.14, EUR: 1 },
tickers: { GBP: "£", USD: "$", EUR: "€" }, tickers: { GBP: "£", USD: "$", EUR: "€" },
}, },
user: null, // user: null,
// user: { user: {
// id: 123456, id: 123456,
// createdAt: new Date(8.64e15), createdAt: new Date(8.64e15),
// email: "tim.howitz@dyson.com", email: "emily.neighbour@dyson.com",
// passwordHash: "", passwordHash: "",
// name: "Tim Howitz", name: "Emily Neighbour",
// role: "ADMIN", role: "ADMIN",
// scientist: undefined, scientist: undefined,
// purchasedArtifacts: [], purchasedArtefacts: [],
// }, },
}); });
export default function RootLayout({ export default function RootLayout({

View File

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

View File

@ -39,10 +39,10 @@ export default function Home() {
href="/shop" href="/shop"
className="flex flex-col items-center p-6 hover:bg-white hover:bg-opacity-10 rounded-xl transition-colors duration-300" 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="/artifactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" /> <Image height={100} width={100} src="/artefactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" />
<h3 className="text-xl font-bold text-black mb-4">Artifacts</h3> <h3 className="text-xl font-bold text-black mb-4">Artefacts</h3>
<p className="text-md text-black text-center max-w-xs opacity-90"> <p className="text-md text-black text-center max-w-xs opacity-90">
View or purchase recently discovered artifacts from seismic events View or purchase recently discovered artefacts from seismic events
</p> </p>
</Link> </Link>
</div> </div>

View File

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

View File

@ -1,75 +1,73 @@
"use client"; "use client";
const teamMembers = [ const teamMembers = [
{ {
name: "Tim Howitz", name: "Tim Howitz",
title: "Chief Crack Inspector", title: "Chief Crack Inspector",
description: description:
"Tim is responsible for analysing structures on a day-to-day basis. In his home life he is a keen outdoors enthusiast, with three kids and a dog.", "Tim is responsible for analysing structures on a day-to-day basis. In his home life he is a keen outdoors enthusiast, with three kids and a dog.",
image: "/Timthescientist.PNG", image: "/Timthescientist.PNG",
}, },
{ {
name: "Emily Neighbour", name: "Emily Neighbour",
title: "Chief Software Engineer", title: "Chief Software Engineer",
description: description:
"Emily focuses on vital earthquake prediction models. In her personal life, her hobbies include knitting, birdwatching, and becoming a mute.", "Emily focuses on vital earthquake prediction models. In her personal life, her hobbies include knitting, birdwatching, and becoming a mute.",
image: "/Emilythescientist.PNG", image: "/Emilythescientist.PNG",
}, },
{ {
name: "Izzy Patterson", name: "Izzy Patterson",
title: "Chief Geologist", title: "Chief Geologist",
description: description:
"Izzy's team are responsible for inspecting the rocks that make up our planet. For enjoyment she likes to look at rocks, sometimes she likes to lick them.", "Izzy's team are responsible for inspecting the rocks that make up our planet. For enjoyment she likes to look at rocks, sometimes she likes to lick them.",
image: "/Izzythescientist.PNG", image: "/Izzythescientist.PNG",
}, },
{ {
name: "Lukeshan Thananchayan", name: "Lukeshan Thananchayan",
title: "Chief Duster", title: "Chief Duster",
description: description:
"Lukeshan and his team look at the dust particles created by an earthquake and how to minimise their damage. For pleasure, he likes to play Monopoly on repeat. Maybe one day he'll get a hotel!", "Lukeshan and his team look at the dust particles created by an earthquake and how to minimise their damage. For pleasure, he likes to play Monopoly on repeat. Maybe one day he'll get a hotel!",
image: "/Lukeshanthescientist.PNG", image: "/Lukeshanthescientist.PNG",
}, },
]; ];
export default function Page() { export default function Page() {
return ( return (
<div <div
className="min-h-screen relative flex flex-col items-center justify-center px-4 py-20" className="min-h-screen relative flex flex-col items-center justify-center px-4 py-30"
style={{ backgroundImage: "url('tectonicPlate.jpg')", backgroundSize: "cover", backgroundPosition: "center" }} style={{ backgroundImage: "url('tectonicPlate.jpg')", backgroundSize: "cover", backgroundPosition: "center" }}
> >
{/* Overlay */} {/* Overlay */}
<div className="absolute inset-0 bg-black bg-opacity-50 pointer-events-none"></div> <div className="absolute inset-0 bg-black bg-opacity-50 pointer-events-none"></div>
{/* Header */} {/* Header */}
<div className="relative z-10 flex flex-col items-center mb-1"> <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"> <h1 className="text-4xl font-bold text-center text-white mb-4 tracking-tight drop-shadow-lg">Meet the Team</h1>
Meet the Team <p className="text-lg text-center max-w-2xl text-white mb-12 drop-shadow-md">
</h1> Our world-class scientists and engineers drive innovation across the globe. Meet our four department heads:
<p className="text-lg text-center max-w-2xl text-white mb-12 drop-shadow-md"> </p>
Our world-class scientists and engineers drive innovation across the globe. Meet our four department heads: </div>
</p> {/* Team Members Section */}
</div> <div className="relative z-10 flex flex-col items-center gap-6 w-full max-w-3xl">
{/* Team Members Section */} {teamMembers.map((member, index) => (
<div className="relative z-10 flex flex-col items-center gap-6 w-full max-w-3xl"> <div
{teamMembers.map((member, index) => ( key={index}
<div className="flex bg-white bg-opacity-90 rounded-3xl border border-neutral-200 w-full shadow-md hover:shadow-lg transition-shadow duration-300"
key={index} >
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">
{/* Image */} <div className="relative w-20 h-20">
<div className="flex items-center ml-6"> <div className="absolute inset-0 rounded-full overflow-hidden ring-4 ring-neutral-100">
<div className="relative w-20 h-20"> <img src={member.image} alt={member.name} className="h-full w-full object-cover" />
<div className="absolute inset-0 rounded-full overflow-hidden ring-4 ring-neutral-100"> </div>
<img src={member.image} alt={member.name} className="h-full w-full object-cover" /> </div>
</div> </div>
</div> {/* Text Content */}
</div> <div className="flex flex-col items-start pl-8 py-4 pr-6">
{/* Text Content */} <h2 className="text-2xl font-bold text-neutral-800">{member.name}</h2>
<div className="flex flex-col items-start pl-8 py-4 pr-6"> <p className="text-md text-neutral-500 font-semibold">{member.title}</p>
<h2 className="text-2xl font-bold text-neutral-800">{member.name}</h2> <p className="text-neutral-600 mt-3 text-left text-sm leading-relaxed">{member.description}</p>
<p className="text-md text-neutral-500 font-semibold">{member.title}</p> </div>
<p className="text-neutral-600 mt-3 text-left text-sm leading-relaxed">{member.description}</p> </div>
</div> ))}
</div> </div>
))} </div>
</div> );
</div> }
);
}

View File

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

View File

@ -1,14 +1,13 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useMemo, useState } from "react"; 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 { Currency } from "@appTypes/StoreModel";
import AuthModal from "@components/AuthModal"; import AuthModal from "@components/AuthModal";
import { useStoreActions, useStoreState } from "@hooks/store"; import { useStoreActions, useStoreState } from "@hooks/store";
import { useRouter } from "next/navigation";
export default function Navbar({}: // currencySelector, export default function Navbar({}: // currencySelector,
{ {
@ -18,7 +17,7 @@ export default function Navbar({}: // currencySelector,
const user = useStoreState((state) => state.user); const user = useStoreState((state) => state.user);
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency); const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
const setSelectedCurrency = useStoreActions((actions) => actions.currency.setSelectedCurrency); const setSelectedCurrency = useStoreActions((actions) => actions.currency.setSelectedCurrency);
const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Shop","Administrator"], []); const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Shop"], []);
// const navOptions = useMemo(() => ["Earthquakes"], []); // const navOptions = useMemo(() => ["Earthquakes"], []);
const aboutDropdown = ["Contact Us", "Our Mission", "The Team"]; const aboutDropdown = ["Contact Us", "Our Mission", "The Team"];
// { label: "Our Mission", path: "/our-mission" }, // { label: "Our Mission", path: "/our-mission" },
@ -109,7 +108,6 @@ export default function Navbar({}: // currencySelector,
<NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} /> <NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} />
</div> </div>
<div className="flex-grow" /> <div className="flex-grow" />
{pathname.includes("shop") && ( {pathname.includes("shop") && (
<button className="flex items-center justify-center mr-3 py-4 relative group"> <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> <span className={`px-4 py-1.5 rounded-md transition-colors`}>{selectedCurrency}</span>
@ -130,13 +128,16 @@ export default function Navbar({}: // currencySelector,
</div> </div>
</button> </button>
)} )}
{user && (user.role === "SCIENTIST" || user.role === "ADMIN") && ( {user && (user.role === "SCIENTIST" || user.role === "ADMIN") && (
<div className="flex h-full mr-5"> <div className="flex h-full mr-5">
<ManagementNavbarButton name="Warehouse" href="/warehouse"></ManagementNavbarButton> <ManagementNavbarButton name="Warehouse" href="/warehouse"></ManagementNavbarButton>
</div> </div>
)} )}
{user && user.role === "ADMIN" && (
<div className="flex h-full mr-5">
<ManagementNavbarButton name="Admin" href="/administrator"></ManagementNavbarButton>
</div>
)}
<button className="my-auto mr-4" onClick={() => (user ? router.push("/profile") : setIsModalOpen(true))}> <button className="my-auto mr-4" onClick={() => (user ? router.push("/profile") : setIsModalOpen(true))}>
<FaRegUserCircle size={22} /> <FaRegUserCircle size={22} />
</button> </button>

View File

@ -0,0 +1 @@
Artefacts
1 Artefacts

View File

@ -1 +0,0 @@
Artifacts
1 Artifacts

View File

@ -1,85 +0,0 @@
// Datasource configuration
datasource db {
provider = "sqlserver"
url = env("DATABASE_URL")
}
// User model
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
name String
email String @unique
passwordHash String
role String @default("GUEST") @db.VarChar(10) // ADMIN, SCIENTIST, GUEST
scientist Scientist? @relation
purchasedArtifacts Artifact[] @relation("UserPurchasedArtifacts")
}
// Scientist model
model Scientist {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
name String
level String @db.VarChar(10) // JUNIOR, SENIOR
user User @relation(fields: [userId], references: [id])
userId Int @unique
superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
superiorId Int?
subordinates Scientist[] @relation("SuperiorRelation")
earthquakes Earthquake[] @relation("ScientistEarthquakeCreator")
observatories Observatory[] @relation("ScientistObservatoryCreator")
artifacts Artifact[] @relation("ScientistArtifactCreator")
}
// Earthquake model
model Earthquake {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
date DateTime
locaiton String
latitude String
longitude String
magnitude Float
depth Float
creatorId Int?
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
artifacts Artifact[]
observatories Observatory[] @relation("EarthquakeObservatory")
}
// Observatory model
model Observatory {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
location String
longitude String
latitude String
dateEstablished Int?
functional Boolean
seismicSensorOnline Boolean @default(true)
creatorId Int?
creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
earthquakes Earthquake[] @relation("EarthquakeObservatory")
}
// Artifact model
model Artifact {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
warehouseArea String // Examples: "ZoneA-Shelf1", "ZoneB-Rack2", "ZoneC-Bin3"
earthquakeId Int
earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
creatorId Int?
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("UserPurchasedArtifacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction)
pickedUp Boolean @default(false)
}

View File

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

View File

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