Compare commits
14 Commits
53c6292e1a
...
dce7e51a53
| Author | SHA1 | Date | |
|---|---|---|---|
| dce7e51a53 | |||
| b0ec146bf0 | |||
|
|
579c1c205a | ||
|
|
52f17d5a00 | ||
|
|
8a45f49a04 | ||
|
|
8d007a7393 | ||
|
|
8fefc67428 | ||
|
|
4890af1e25 | ||
|
|
628b4a9370 | ||
|
|
06403d254f | ||
|
|
1cdace58b0 | ||
|
|
28e6a556f8 | ||
|
|
0b6167ed4b | ||
|
|
b615524c6c |
2
.env
2
.env
@ -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
|
||||
1844
package-lock.json
generated
1844
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -15,9 +15,9 @@
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/mapbox-gl": "^3.4.1",
|
||||
"axios": "^1.9.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"csv-parse": "^5.6.0",
|
||||
"csv-parser": "^3.2.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"easy-peasy": "^6.1.0",
|
||||
@ -29,11 +29,11 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mapbox-gl": "^3.10.0",
|
||||
"next": "15.1.7",
|
||||
"next": "^15.1.7",
|
||||
"path": "^0.12.7",
|
||||
"prisma": "^6.4.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-node": "^1.0.2",
|
||||
@ -42,7 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/bcryptjs": "^5.0.2",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
|
||||
85
prisma/schema.prisma
Normal file
85
prisma/schema.prisma
Normal 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)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 280 KiB |
1573
public/earthquakes.csv
Normal file
1573
public/earthquakes.csv
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,54 +6,28 @@ const usingPrisma = false;
|
||||
let prisma: PrismaClient;
|
||||
if (usingPrisma) prisma = new PrismaClient();
|
||||
|
||||
export async function GET(request: Request) {
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const events = [
|
||||
{
|
||||
id: "1234",
|
||||
title: "Earthquake in Germany",
|
||||
text1: "Magnitude 8.5",
|
||||
text2: "30 minutes ago",
|
||||
magnitude: 8.5,
|
||||
longitude: 10.4515, // Near Berlin, Germany
|
||||
latitude: 52.52,
|
||||
},
|
||||
{
|
||||
id: "2134",
|
||||
title: "Earthquake in California",
|
||||
text1: "Magnitude 5.3",
|
||||
text2: "2 hours ago",
|
||||
magnitude: 5.3,
|
||||
longitude: -122.4194, // Near San Francisco, California, USA
|
||||
latitude: 37.7749,
|
||||
},
|
||||
{
|
||||
id: "2314",
|
||||
title: "Tremor in Japan",
|
||||
text1: "Magnitude 4.7",
|
||||
text2: "5 hours ago",
|
||||
magnitude: 4.7,
|
||||
longitude: 139.6917, // Near Tokyo, Japan
|
||||
latitude: 35.6762,
|
||||
},
|
||||
{
|
||||
id: "2341",
|
||||
title: "Tremor in Spain",
|
||||
text1: "Magnitude 2.1",
|
||||
text2: "10 hours ago",
|
||||
magnitude: 2.1,
|
||||
longitude: -3.7038, // Near Madrid, Spain
|
||||
latitude: 40.4168,
|
||||
},
|
||||
];
|
||||
const json = await req.json(); // Parse incoming JSON data
|
||||
const {rangeDaysPrev} = json.body
|
||||
|
||||
let earthquakes;
|
||||
if (usingPrisma) earthquakes = await prisma.earthquakes.findMany();
|
||||
const now = new Date()
|
||||
const rangeBeginning = new Date();
|
||||
rangeBeginning.setDate(rangeBeginning.getDate() - rangeDaysPrev)
|
||||
|
||||
const earthquakes = await prisma.earthquake.findMany(
|
||||
{where: {
|
||||
date: {
|
||||
gte: rangeBeginning,
|
||||
lte: now
|
||||
}
|
||||
}}
|
||||
);
|
||||
|
||||
if (earthquakes) {
|
||||
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: events }, { status: 200 });
|
||||
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 });
|
||||
// return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 });
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
63
src/app/api/import-earthquakes/route.ts
Normal file
63
src/app/api/import-earthquakes/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { SignJWT } from 'jose';
|
||||
import { NextResponse } from 'next/server';
|
||||
import bcryptjs from "bcryptjs";
|
||||
import { SignJWT } from "jose";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { env } from '@utils/env';
|
||||
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;
|
||||
@ -33,7 +33,7 @@ export async function POST(req: Request) {
|
||||
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
|
||||
|
||||
// get user and relations
|
||||
@ -45,12 +45,12 @@ export async function POST(req: Request) {
|
||||
include: {
|
||||
earthquakes: true,
|
||||
observatories: true,
|
||||
artifacts: true,
|
||||
artefacts: true,
|
||||
superior: true,
|
||||
subordinates: true,
|
||||
},
|
||||
},
|
||||
purchasedArtifacts: true,
|
||||
purchasedArtefacts: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import bcryptjs from "bcryptjs";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
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 });
|
||||
} else {
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
const passwordHash = await bcryptjs.hash(password, 10);
|
||||
if (usingPrisma) {
|
||||
// todo add sending back newUser
|
||||
const newUser = await prisma.user.create({
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { env } from '@utils/env';
|
||||
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();
|
||||
|
||||
// Artifact type
|
||||
interface Artifact {
|
||||
// Artefact type
|
||||
interface Artefact {
|
||||
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 warehouseArtifacts: Artifact[] = [
|
||||
const warehouseArtefacts: Artefact[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Solidified Lava Chunk",
|
||||
@ -87,17 +87,17 @@ export async function POST(req: Request) {
|
||||
},
|
||||
];
|
||||
|
||||
let artifacts;
|
||||
if (usingPrisma) artifacts = await prisma.artifacts.findMany();
|
||||
let artefacts;
|
||||
if (usingPrisma) artefacts = await prisma.artefacts.findMany();
|
||||
|
||||
if (artifacts) {
|
||||
return NextResponse.json({ message: "Got artifacts successfully", artifacts }, { status: 200 });
|
||||
if (artefacts) {
|
||||
return NextResponse.json({ message: "Got artefacts successfully", artefacts }, { status: 200 });
|
||||
} 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 });
|
||||
}
|
||||
} 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 });
|
||||
} finally {
|
||||
if (usingPrisma) await prisma.$disconnect();
|
||||
|
||||
@ -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({
|
||||
@ -37,8 +37,8 @@ const ContactUs = () => {
|
||||
{/* Header */}
|
||||
<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">
|
||||
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.
|
||||
Have questions or concerns about earthquakes, observatories or artefacts? Contact us via phone, email, social media or
|
||||
using the form below with the relevant contact details.
|
||||
</p>
|
||||
|
||||
{/* Content Section */}
|
||||
|
||||
@ -23,17 +23,17 @@ const store = createStore<StoreModel>({
|
||||
conversionRates: { GBP: 0.85, USD: 1.14, EUR: 1 },
|
||||
tickers: { GBP: "£", USD: "$", EUR: "€" },
|
||||
},
|
||||
user: null,
|
||||
// user: {
|
||||
// id: 123456,
|
||||
// createdAt: new Date(8.64e15),
|
||||
// email: "tim.howitz@dyson.com",
|
||||
// passwordHash: "",
|
||||
// name: "Tim Howitz",
|
||||
// role: "ADMIN",
|
||||
// scientist: undefined,
|
||||
// purchasedArtifacts: [],
|
||||
// },
|
||||
// user: null,
|
||||
user: {
|
||||
id: 123456,
|
||||
createdAt: new Date(8.64e15),
|
||||
email: "emily.neighbour@dyson.com",
|
||||
passwordHash: "",
|
||||
name: "Emily Neighbour",
|
||||
role: "ADMIN",
|
||||
scientist: undefined,
|
||||
purchasedArtefacts: [],
|
||||
},
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@ -1,61 +1,59 @@
|
||||
"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"
|
||||
style={{ backgroundImage: "url('destruction.jpg')" }}
|
||||
>
|
||||
{/* Overlay for Readability */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
{/* 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>
|
||||
{/* 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-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-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-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>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="relative bg-fixed bg-cover bg-center text-white "
|
||||
style={{ backgroundImage: "url('destruction.jpg')", height: 845, overflow: "hidden" }}
|
||||
>
|
||||
{/* Overlay for Readability */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
{/* Centered content */}
|
||||
<div className="relative z-20 flex flex-col items-center justify-center h-full py-auto">
|
||||
{/* 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>
|
||||
{/* 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-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-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-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;
|
||||
@ -39,10 +39,10 @@ export default function Home() {
|
||||
href="/shop"
|
||||
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" />
|
||||
<h3 className="text-xl font-bold text-black mb-4">Artifacts</h3>
|
||||
<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">Artefacts</h3>
|
||||
<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>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
"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 Artifact from '@appTypes/Artifact';
|
||||
import { Currency } from '@appTypes/StoreModel';
|
||||
import { useStoreState } from '@hooks/store';
|
||||
import Artefact from "@appTypes/Artefact";
|
||||
import { Currency } from "@appTypes/StoreModel";
|
||||
import { useStoreState } from "@hooks/store";
|
||||
|
||||
// Artifacts Data
|
||||
const artifacts: Artifact[] = [
|
||||
// Artefacts Data
|
||||
const artefacts: Artefact[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Golden Scarab",
|
||||
description: "An ancient Egyptian artifact symbolizing rebirth.",
|
||||
description: "An ancient Egyptian artefact symbolizing rebirth.",
|
||||
location: "Cairo, Egypt",
|
||||
image: "/artifact1.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact1.jpg",
|
||||
price: 150,
|
||||
},
|
||||
{
|
||||
@ -21,7 +24,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Aztec Sunstone",
|
||||
description: "A replica of the Aztec calendar (inscriptions intact).",
|
||||
location: "Peru",
|
||||
image: "/artifact2.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact2.jpg",
|
||||
price: 200,
|
||||
},
|
||||
{
|
||||
@ -29,7 +35,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Medieval Chalice",
|
||||
description: "Used by royalty in medieval ceremonies.",
|
||||
location: "Cambridge, England",
|
||||
image: "/artifact3.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact3.jpg",
|
||||
price: 120,
|
||||
},
|
||||
{
|
||||
@ -37,7 +46,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Roman Coin",
|
||||
description: "An authentic Roman coin from the 2nd century CE.",
|
||||
location: "Rome, Italy",
|
||||
image: "/artifact4.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact4.jpg",
|
||||
price: 80,
|
||||
},
|
||||
{
|
||||
@ -45,7 +57,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Samurai Mask",
|
||||
description: "Replica of Japanese Samurai battle masks.",
|
||||
location: "Tokyo, Japan",
|
||||
image: "/artifact5.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact5.jpg",
|
||||
price: 300,
|
||||
},
|
||||
{
|
||||
@ -53,7 +68,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Ancient Greek Vase",
|
||||
description: "Depicts Greek mythology, found in the Acropolis.",
|
||||
location: "Athens, Greece",
|
||||
image: "/artifact6.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact6.jpg",
|
||||
price: 250,
|
||||
},
|
||||
{
|
||||
@ -61,7 +79,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Incan Pendant",
|
||||
description: "Represents the Sun God Inti.",
|
||||
location: "India",
|
||||
image: "/artifact7.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact7.jpg",
|
||||
price: 175,
|
||||
},
|
||||
{
|
||||
@ -69,7 +90,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Persian Carpet Fragment",
|
||||
description: "Ancient Persian artistry.",
|
||||
location: "Petra, Jordan",
|
||||
image: "/artifact8.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact8.jpg",
|
||||
price: 400,
|
||||
},
|
||||
{
|
||||
@ -77,7 +101,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Stone Buddha",
|
||||
description: "Authentic stone Buddha carving.",
|
||||
location: "India",
|
||||
image: "/artifact9.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact9.jpg",
|
||||
price: 220,
|
||||
},
|
||||
{
|
||||
@ -85,7 +112,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Victorian Brooch",
|
||||
description: "A beautiful Victorian-era brooch with a ruby centre.",
|
||||
location: "Oxford, England",
|
||||
image: "/artifact10.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact10.jpg",
|
||||
price: 150,
|
||||
},
|
||||
{
|
||||
@ -93,7 +123,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Ancient Scroll",
|
||||
description: "A mysterious scroll from ancient times.",
|
||||
location: "Madrid, Spain",
|
||||
image: "/artifact11.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact11.jpg",
|
||||
price: 500,
|
||||
},
|
||||
{
|
||||
@ -101,7 +134,10 @@ const artifacts: Artifact[] = [
|
||||
name: "Ming Dynasty Porcelain",
|
||||
description: "Porcelain from China's Ming Dynasty.",
|
||||
location: "Beijing, China",
|
||||
image: "/artifact12.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact12.jpg",
|
||||
price: 300,
|
||||
},
|
||||
{
|
||||
@ -109,15 +145,21 @@ const artifacts: Artifact[] = [
|
||||
name: "African Tribal Mask",
|
||||
description: "A unique tribal mask from Africa.",
|
||||
location: "Nigeria",
|
||||
image: "/artifact13.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact13.jpg",
|
||||
price: 250,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Crystal Skull",
|
||||
description: "A mystical pre-Columbian artifact.",
|
||||
description: "A mystical pre-Columbian artefact.",
|
||||
location: "Colombia",
|
||||
image: "/artifact14.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact14.jpg",
|
||||
price: 1000,
|
||||
},
|
||||
{
|
||||
@ -125,159 +167,169 @@ const artifacts: Artifact[] = [
|
||||
name: "Medieval Armor Fragment",
|
||||
description: "A fragment of medieval armor.",
|
||||
location: "Normandy, France",
|
||||
image: "/artifact15.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact15.jpg",
|
||||
price: 400,
|
||||
},
|
||||
{
|
||||
{
|
||||
id: 16,
|
||||
name: "Medieval Helmet Fragment",
|
||||
description: "A fragment of a medieval helmet.",
|
||||
location: "Normandy, France",
|
||||
image: "/artifact16.jpg",
|
||||
earthquakeID: "h",
|
||||
observatory: "jhd",
|
||||
dateReleased: "12/02/2025",
|
||||
image: "/artefact16.jpg",
|
||||
price: 500,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Shop() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
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 [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null);
|
||||
const artefactsPerPage = 12;
|
||||
const indexOfLastArtefact = currentPage * artefactsPerPage;
|
||||
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
|
||||
const currentArtefacts = artefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
|
||||
|
||||
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
|
||||
const conversionRates = useStoreState((state) => state.currency.conversionRates);
|
||||
const currencyTickers = useStoreState((state) => state.currency.tickers);
|
||||
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 (indexOfLastArtifact < artifacts.length) {
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
const handlePreviousPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
const handleNextPage = () => {
|
||||
if (indexOfLastArtefact < artefacts.length) {
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
const handlePreviousPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
function Modal({ artifact }: { artifact: Artifact }) {
|
||||
if (!artifact) return null;
|
||||
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
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">{artifact.name}</h3>
|
||||
<Image
|
||||
height={5000}
|
||||
width={5000}
|
||||
src={artifact.image}
|
||||
alt={artifact.name}
|
||||
className="w-full h-64 object-cover rounded-md"
|
||||
/>
|
||||
<p className="text-xl font-bold">
|
||||
{currencyTickers[selectedCurrency]}
|
||||
{convertPrice(artifact.price, selectedCurrency)}
|
||||
</p>
|
||||
<p className="text-neutral-600 mt-2">{artifact.description}</p>
|
||||
<p className="text-neutral-500 font-bold mt-1">Location: {artifact.location}</p>
|
||||
<div className="flex justify-end gap-4 mt-4 mr-2">
|
||||
<button
|
||||
onClick={() => alert("Purchased Successfully!")}
|
||||
className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function Modal({ artefact }: { artefact: Artefact }) {
|
||||
if (!artefact) return null;
|
||||
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setSelectedArtefact(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>
|
||||
<Image
|
||||
height={5000}
|
||||
width={5000}
|
||||
src={artefact.image}
|
||||
alt={artefact.name}
|
||||
className="w-full h-64 object-cover rounded-md"
|
||||
/>
|
||||
<p className="text-xl font-bold">
|
||||
{currencyTickers[selectedCurrency]}
|
||||
{convertPrice(artefact.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-500 mb-2">{artefact.earthquakeID}</p>
|
||||
<p className="text-neutral-500 mb-2">{artefact.observatory}</p>
|
||||
<p className="text-neutral-500 mb-2">{artefact.dateReleased}</p>
|
||||
<div className="flex justify-end gap-4 mt-4 mr-2">
|
||||
<button
|
||||
onClick={() => alert("Purchased Successfully!")}
|
||||
className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactCard({ artifact }: { artifact: Artifact }) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
|
||||
onClick={() => setSelectedArtifact(artifact)}
|
||||
>
|
||||
<img src={artifact.image} alt={artifact.name} className="w-full h-56 object-cover" />
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-semibold">{artifact.name}</h3>
|
||||
<p className="text-neutral-500 mb-2">{artifact.location}</p>
|
||||
<p className="text-black font-bold text-md mt-2">
|
||||
{currencyTickers[selectedCurrency]}
|
||||
{convertPrice(artifact.price, selectedCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function ArtefactCard({ artefact }: { artefact: Artefact }) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
|
||||
onClick={() => setSelectedArtefact(artefact)}
|
||||
>
|
||||
<img src={artefact.image} alt={artefact.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>
|
||||
<p className="text-neutral-500 mb-2">{artefact.earthquakeID}</p>
|
||||
<p className="text-black font-bold text-md mt-2">
|
||||
{currencyTickers[selectedCurrency]}
|
||||
{convertPrice(artefact.price, selectedCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen relative flex flex-col"
|
||||
style={{
|
||||
backgroundImage: "url('/artefacts.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">
|
||||
Artefact Shop
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-center text-white mb-10 drop-shadow-md max-w-2xl">
|
||||
Discover extraordinary historical artefacts and collectibles from major seismic events from around the world - now
|
||||
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>
|
||||
{/* 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 */}
|
||||
{currentArtefacts.map((artefact) => (
|
||||
<ArtefactCard key={artefact.id} artefact={artefact} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<button
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<p className="mx-3 text-lg font-bold">{currentPage}</p>
|
||||
<button
|
||||
onClick={handleNextPage}
|
||||
disabled={indexOfLastArtifact >= artifacts.length}
|
||||
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
|
||||
indexOfLastArtifact >= artifacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
|
||||
}`}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
{/* 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">
|
||||
<button
|
||||
onClick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<p className="mx-3 text-lg font-bold">{currentPage}</p>
|
||||
<button
|
||||
onClick={handleNextPage}
|
||||
disabled={indexOfLastArtefact >= artefacts.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"
|
||||
}`}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
{selectedArtifact && <Modal artifact={selectedArtifact} />}
|
||||
</div>
|
||||
);
|
||||
{/* Modal */}
|
||||
{selectedArtefact && <Modal artefact={selectedArtefact} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,75 +1,73 @@
|
||||
"use client";
|
||||
const teamMembers = [
|
||||
{
|
||||
name: "Tim Howitz",
|
||||
title: "Chief Crack Inspector",
|
||||
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.",
|
||||
image: "/Timthescientist.PNG",
|
||||
},
|
||||
{
|
||||
name: "Emily Neighbour",
|
||||
title: "Chief Software Engineer",
|
||||
description:
|
||||
"Emily focuses on vital earthquake prediction models. In her personal life, her hobbies include knitting, birdwatching, and becoming a mute.",
|
||||
image: "/Emilythescientist.PNG",
|
||||
},
|
||||
{
|
||||
name: "Izzy Patterson",
|
||||
title: "Chief Geologist",
|
||||
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.",
|
||||
image: "/Izzythescientist.PNG",
|
||||
},
|
||||
{
|
||||
name: "Lukeshan Thananchayan",
|
||||
title: "Chief Duster",
|
||||
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!",
|
||||
image: "/Lukeshanthescientist.PNG",
|
||||
},
|
||||
{
|
||||
name: "Tim Howitz",
|
||||
title: "Chief Crack Inspector",
|
||||
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.",
|
||||
image: "/Timthescientist.PNG",
|
||||
},
|
||||
{
|
||||
name: "Emily Neighbour",
|
||||
title: "Chief Software Engineer",
|
||||
description:
|
||||
"Emily focuses on vital earthquake prediction models. In her personal life, her hobbies include knitting, birdwatching, and becoming a mute.",
|
||||
image: "/Emilythescientist.PNG",
|
||||
},
|
||||
{
|
||||
name: "Izzy Patterson",
|
||||
title: "Chief Geologist",
|
||||
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.",
|
||||
image: "/Izzythescientist.PNG",
|
||||
},
|
||||
{
|
||||
name: "Lukeshan Thananchayan",
|
||||
title: "Chief Duster",
|
||||
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!",
|
||||
image: "/Lukeshanthescientist.PNG",
|
||||
},
|
||||
];
|
||||
export default function Page() {
|
||||
return (
|
||||
<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 */}
|
||||
<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 the 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="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 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">
|
||||
<div className="relative w-20 h-20">
|
||||
<div className="absolute inset-0 rounded-full overflow-hidden ring-4 ring-neutral-100">
|
||||
<img src={member.image} alt={member.name} className="h-full w-full object-cover" />
|
||||
</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>
|
||||
<p className="text-md text-neutral-500 font-semibold">{member.title}</p>
|
||||
<p className="text-neutral-600 mt-3 text-left text-sm leading-relaxed">{member.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
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" }}
|
||||
>
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50 pointer-events-none"></div>
|
||||
{/* Header */}
|
||||
<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 the 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="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 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">
|
||||
<div className="relative w-20 h-20">
|
||||
<div className="absolute inset-0 rounded-full overflow-hidden ring-4 ring-neutral-100">
|
||||
<img src={member.image} alt={member.name} className="h-full w-full object-cover" />
|
||||
</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>
|
||||
<p className="text-md text-neutral-500 font-semibold">{member.title}</p>
|
||||
<p className="text-neutral-600 mt-3 text-left text-sm leading-relaxed">{member.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
"use 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';
|
||||
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";
|
||||
|
||||
// import type { Artifact } from "@prisma/client";
|
||||
// import type { Artefact } from "@prisma/client";
|
||||
|
||||
interface Artifact {
|
||||
interface Artefact {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
@ -18,8 +18,8 @@ interface Artifact {
|
||||
dateAdded: string;
|
||||
}
|
||||
|
||||
// Warehouse Artifacts Data
|
||||
const warehouseArtifacts: Artifact[] = [
|
||||
// Warehouse Artefacts Data
|
||||
const warehouseArtefacts: Artefact[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Solidified Lava Chunk",
|
||||
@ -140,14 +140,14 @@ function FilterInput({
|
||||
}
|
||||
|
||||
// Table Component
|
||||
function ArtifactTable({
|
||||
artifacts,
|
||||
function ArtefactTable({
|
||||
artefacts,
|
||||
filters,
|
||||
setFilters,
|
||||
setEditArtifact,
|
||||
setEditArtefact,
|
||||
clearSort,
|
||||
}: {
|
||||
artifacts: Artifact[];
|
||||
artefacts: Artefact[];
|
||||
filters: Record<string, string>;
|
||||
setFilters: Dispatch<
|
||||
SetStateAction<{
|
||||
@ -162,15 +162,15 @@ function ArtifactTable({
|
||||
dateAdded: string;
|
||||
}>
|
||||
>;
|
||||
setEditArtifact: (artifact: Artifact) => void;
|
||||
setEditArtefact: (artefact: Artefact) => void;
|
||||
clearSort: () => void;
|
||||
}) {
|
||||
const [sortConfig, setSortConfig] = useState<{
|
||||
key: keyof Artifact;
|
||||
key: keyof Artefact;
|
||||
direction: "asc" | "desc";
|
||||
} | null>(null);
|
||||
|
||||
const handleSort = (key: keyof Artifact) => {
|
||||
const handleSort = (key: keyof Artefact) => {
|
||||
setSortConfig((prev) => {
|
||||
if (!prev || prev.key !== key) {
|
||||
return { key, direction: "asc" };
|
||||
@ -186,9 +186,9 @@ function ArtifactTable({
|
||||
clearSort();
|
||||
};
|
||||
|
||||
const sortedArtifacts = useMemo(() => {
|
||||
if (!sortConfig) return artifacts;
|
||||
const sorted = [...artifacts].sort((a, b) => {
|
||||
const sortedArtefacts = useMemo(() => {
|
||||
if (!sortConfig) return artefacts;
|
||||
const sorted = [...artefacts].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 ArtifactTable({
|
||||
return 0;
|
||||
});
|
||||
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: "Name", key: "name", width: "12%" },
|
||||
{ label: "Earthquake ID", key: "earthquakeId", width: "10%" },
|
||||
@ -217,7 +217,7 @@ function ArtifactTable({
|
||||
{columns.map(({ label, key, width }) => (
|
||||
<th key={key} className="text-sm px-5 font-semibold text-neutral-800 cursor-pointer" style={{ width }}>
|
||||
<div className="flex h-11 items-center">
|
||||
<div className="flex h-full items-center" onClick={() => handleSort(key as keyof Artifact)}>
|
||||
<div className="flex h-full items-center" onClick={() => handleSort(key as keyof Artefact)}>
|
||||
<div className="select-none">{label}</div>
|
||||
</div>
|
||||
<div className="h-full relative">
|
||||
@ -250,11 +250,11 @@ function ArtifactTable({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedArtifacts.map((artifact) => (
|
||||
{sortedArtefacts.map((artefact) => (
|
||||
<tr
|
||||
key={artifact.id}
|
||||
key={artefact.id}
|
||||
className="border-b border-neutral-200 hover:bg-neutral-100 cursor-pointer"
|
||||
onClick={() => setEditArtifact(artifact)}
|
||||
onClick={() => setEditArtefact(artefact)}
|
||||
>
|
||||
{columns.map(({ key, width }) => (
|
||||
<td
|
||||
@ -263,18 +263,18 @@ function ArtifactTable({
|
||||
style={{ width }}
|
||||
>
|
||||
{key === "isRequired"
|
||||
? artifact.isRequired
|
||||
? artefact.isRequired
|
||||
? "Yes"
|
||||
: "No"
|
||||
: key === "isSold"
|
||||
? artifact.isSold
|
||||
? artefact.isSold
|
||||
? "Yes"
|
||||
: "No"
|
||||
: key === "isCollected"
|
||||
? artifact.isCollected
|
||||
? artefact.isCollected
|
||||
? "Yes"
|
||||
: "No"
|
||||
: artifact[key]}
|
||||
: artefact[key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@ -284,7 +284,7 @@ function ArtifactTable({
|
||||
);
|
||||
}
|
||||
|
||||
// Modal Component for Logging Artifact
|
||||
// Modal Component for Logging Artefact
|
||||
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 artifact. Please try again.");
|
||||
setError("Failed to log artefact. 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 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>}
|
||||
<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="Artifact Name"
|
||||
aria-label="Artefact 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="Artifact Description"
|
||||
aria-label="Artefact 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="Artifact Location"
|
||||
aria-label="Artefact 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 Artifact"
|
||||
aria-label="Required Artefact"
|
||||
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 Artifact"
|
||||
"Log Artefact"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@ -524,16 +524,16 @@ function BulkLogModal({ onClose }: { onClose: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@ -551,10 +551,10 @@ function EditModal({ artifact, onClose }: { artifact: Artifact; onClose: () => v
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||
alert(`Updated artifact ${name}`);
|
||||
alert(`Updated artefact ${name}`);
|
||||
onClose();
|
||||
} catch {
|
||||
setError("Failed to update artifact. Please try again.");
|
||||
setError("Failed to update artefact. Please try again.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@ -566,7 +566,7 @@ function EditModal({ artifact, onClose }: { artifact: Artifact; 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 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>}
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
@ -574,14 +574,14 @@ function EditModal({ artifact, onClose }: { artifact: Artifact; 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="Artifact Name"
|
||||
aria-label="Artefact Name"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-16"
|
||||
aria-label="Artifact Description"
|
||||
aria-label="Artefact Description"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<input
|
||||
@ -589,7 +589,7 @@ function EditModal({ artifact, onClose }: { artifact: Artifact; 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="Artifact Location"
|
||||
aria-label="Artefact Location"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<input
|
||||
@ -606,7 +606,7 @@ function EditModal({ artifact, onClose }: { artifact: Artifact; 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 Artifact"
|
||||
aria-label="Required Artefact"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<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}
|
||||
onChange={(e) => setIsSold(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
|
||||
aria-label="Sold Artifact"
|
||||
aria-label="Sold Artefact"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<label className="text-sm text-neutral-600">Sold</label>
|
||||
@ -628,7 +628,7 @@ function EditModal({ artifact, onClose }: { artifact: Artifact; 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 Artifact"
|
||||
aria-label="Collected Artefact"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<label className="text-sm text-neutral-600">Collected</label>
|
||||
@ -685,18 +685,18 @@ function EditModal({ artifact, onClose }: { artifact: Artifact; onClose: () => v
|
||||
}
|
||||
|
||||
// Filter Logic
|
||||
const applyFilters = (artifacts: Artifact[], filters: Record<string, string>): Artifact[] => {
|
||||
return artifacts.filter((artifact) => {
|
||||
const applyFilters = (artefacts: Artefact[], filters: Record<string, string>): Artefact[] => {
|
||||
return artefacts.filter((artefact) => {
|
||||
return (
|
||||
(filters.id === "" || artifact.id.toString().includes(filters.id)) &&
|
||||
(filters.name === "" || artifact.name.toLowerCase().includes(filters.name.toLowerCase())) &&
|
||||
(filters.earthquakeId === "" || artifact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) &&
|
||||
(filters.location === "" || artifact.location.toLowerCase().includes(filters.location.toLowerCase())) &&
|
||||
(filters.description === "" || artifact.description.toLowerCase().includes(filters.description.toLowerCase())) &&
|
||||
(filters.isRequired === "" || (filters.isRequired === "true" ? artifact.isRequired : !artifact.isRequired)) &&
|
||||
(filters.isSold === "" || (filters.isSold === "true" ? artifact.isSold : !artifact.isSold)) &&
|
||||
(filters.isCollected === "" || (filters.isCollected === "true" ? artifact.isCollected : !artifact.isCollected)) &&
|
||||
(filters.dateAdded === "" || artifact.dateAdded === filters.dateAdded)
|
||||
(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)
|
||||
);
|
||||
});
|
||||
};
|
||||
@ -706,7 +706,7 @@ export default function Warehouse() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [showLogModal, setShowLogModal] = useState(false);
|
||||
const [showBulkLogModal, setShowBulkLogModal] = useState(false);
|
||||
const [editArtifact, setEditArtifact] = useState<Artifact | null>(null);
|
||||
const [editArtefact, setEditArtefact] = useState<Artefact | 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 Artifact;
|
||||
key: keyof Artefact;
|
||||
direction: "asc" | "desc";
|
||||
} | null>(null);
|
||||
|
||||
const artifactsPerPage = 10;
|
||||
const indexOfLastArtifact = currentPage * artifactsPerPage;
|
||||
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
|
||||
const artefactsPerPage = 10;
|
||||
const indexOfLastArtefact = currentPage * artefactsPerPage;
|
||||
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
|
||||
|
||||
// Apply filters with loading state
|
||||
const filteredArtifacts = useMemo(() => {
|
||||
const filteredArtefacts = useMemo(() => {
|
||||
setIsFiltering(true);
|
||||
const result = applyFilters(warehouseArtifacts, filters);
|
||||
const result = applyFilters(warehouseArtefacts, filters);
|
||||
setIsFiltering(false);
|
||||
return result;
|
||||
}, [filters]);
|
||||
|
||||
const currentArtifacts = filteredArtifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
|
||||
const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
|
||||
|
||||
// Overview stats
|
||||
const totalArtifacts = warehouseArtifacts.length;
|
||||
const totalArtefacts = warehouseArtefacts.length;
|
||||
const today = "2025-05-04";
|
||||
const artifactsAddedToday = warehouseArtifacts.filter((a) => a.dateAdded === today).length;
|
||||
const artifactsSoldToday = warehouseArtifacts.filter((a) => a.isSold && a.dateAdded === today).length;
|
||||
const artefactsAddedToday = warehouseArtefacts.filter((a) => a.dateAdded === today).length;
|
||||
const artefactsSoldToday = warehouseArtefacts.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 Artifacts: <span className="font-semibold ml-1">{totalArtifacts}</span>
|
||||
Total Artefacts: <span className="font-semibold ml-1">{totalArtefacts}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-md text-neutral-600">
|
||||
<IoToday className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
|
||||
Added Today: <span className="font-semibold ml-1">{artifactsAddedToday}</span>
|
||||
Added Today: <span className="font-semibold ml-1">{artefactsAddedToday}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-md text-neutral-600">
|
||||
<FaCartShopping className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
|
||||
Sold Today: <span className="font-semibold ml-1">{artifactsSoldToday}</span>
|
||||
Sold Today: <span className="font-semibold ml-1">{artefactsSoldToday}</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 Artifact
|
||||
Log Single Artefact
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowBulkLogModal(true)}
|
||||
@ -822,11 +822,11 @@ export default function Warehouse() {
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full overflow-y-none">
|
||||
<ArtifactTable
|
||||
artifacts={currentArtifacts}
|
||||
<ArtefactTable
|
||||
artefacts={currentArtefacts}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
setEditArtifact={setEditArtifact}
|
||||
setEditArtefact={setEditArtefact}
|
||||
clearSort={() => setSortConfig(null)}
|
||||
/>
|
||||
</div>
|
||||
@ -837,7 +837,7 @@ export default function Warehouse() {
|
||||
{/* Modals */}
|
||||
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
|
||||
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
|
||||
{editArtifact && <EditModal artifact={editArtifact} onClose={() => setEditArtifact(null)} />}
|
||||
{editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||
import { FaRegUserCircle } from "react-icons/fa";
|
||||
|
||||
import { Currency } from "@appTypes/StoreModel";
|
||||
import AuthModal from "@components/AuthModal";
|
||||
import { useStoreActions, useStoreState } from "@hooks/store";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Navbar({}: // currencySelector,
|
||||
{
|
||||
@ -18,7 +17,7 @@ export default function Navbar({}: // currencySelector,
|
||||
const user = useStoreState((state) => state.user);
|
||||
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
|
||||
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 aboutDropdown = ["Contact Us", "Our Mission", "The Team"];
|
||||
// { label: "Our Mission", path: "/our-mission" },
|
||||
@ -109,7 +108,6 @@ export default function Navbar({}: // currencySelector,
|
||||
<NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} />
|
||||
</div>
|
||||
<div className="flex-grow" />
|
||||
|
||||
{pathname.includes("shop") && (
|
||||
<button className="flex items-center justify-center mr-3 py-4 relative group">
|
||||
<span className={`px-4 py-1.5 rounded-md transition-colors`}>{selectedCurrency}</span>
|
||||
@ -130,13 +128,16 @@ export default function Navbar({}: // currencySelector,
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{user && (user.role === "SCIENTIST" || user.role === "ADMIN") && (
|
||||
<div className="flex h-full mr-5">
|
||||
<ManagementNavbarButton name="Warehouse" href="/warehouse"></ManagementNavbarButton>
|
||||
</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))}>
|
||||
<FaRegUserCircle size={22} />
|
||||
</button>
|
||||
|
||||
1
src/databases/Artefacts.csv
Normal file
1
src/databases/Artefacts.csv
Normal file
@ -0,0 +1 @@
|
||||
Artefacts
|
||||
|
@ -1 +0,0 @@
|
||||
Artifacts
|
||||
|
@ -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)
|
||||
}
|
||||
@ -1,11 +1,14 @@
|
||||
interface Artifact {
|
||||
interface Artefact {
|
||||
// todo change to string
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
location: string;
|
||||
earthquakeID: string;
|
||||
observatory: string;
|
||||
dateReleased: string;
|
||||
image: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export default Artifact;
|
||||
export default Artefact;
|
||||
@ -1,4 +1,4 @@
|
||||
import { Action } from 'easy-peasy';
|
||||
import { Action } from "easy-peasy";
|
||||
|
||||
// import type { User } from "@prisma/client";
|
||||
|
||||
@ -14,10 +14,10 @@ interface Scientist {
|
||||
subordinates: Scientist[];
|
||||
// earthquakes: Earthquake[];
|
||||
// observatories: Observatory[];
|
||||
artifacts: Artifact[];
|
||||
artefacts: Artefact[];
|
||||
}
|
||||
|
||||
interface Artifact {
|
||||
interface Artefact {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
@ -37,7 +37,7 @@ interface User {
|
||||
passwordHash: string;
|
||||
role: string;
|
||||
scientist: Scientist | undefined;
|
||||
purchasedArtifacts: Artifact[];
|
||||
purchasedArtefacts: Artefact[];
|
||||
}
|
||||
|
||||
type Currency = "GBP" | "USD" | "EUR";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user