Made a lotta changes

This commit is contained in:
Tim Howitz 2025-05-09 10:30:12 +01:00
parent 2ce882f36e
commit 1ef5330717
27 changed files with 762 additions and 304 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL=""
JWT_SECRET_KEY=mysupersecretkey

155
package-lock.json generated
View File

@ -9,15 +9,19 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@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", "bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"dotenv": "^16.5.0",
"easy-peasy": "^6.1.0", "easy-peasy": "^6.1.0",
"express": "^5.1.0", "express": "^5.1.0",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"jose": "^6.0.11",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -30,7 +34,8 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-node": "^1.0.2", "react-node": "^1.0.2",
"swr": "^2.3.3" "swr": "^2.3.3",
"zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@ -1586,6 +1591,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/mapbox__point-geometry": { "node_modules/@types/mapbox__point-geometry": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
@ -1619,11 +1634,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.19", "version": "20.17.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
"integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==", "integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~6.19.2"
@ -2453,6 +2473,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/busboy": { "node_modules/busboy": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@ -3084,6 +3110,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -3147,6 +3185,15 @@
} }
} }
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -5272,6 +5319,15 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/jose": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
"integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -5326,6 +5382,28 @@
"json5": "lib/cli.js" "json5": "lib/cli.js"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jstransform": { "node_modules/jstransform": {
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.3.tgz", "resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.3.tgz",
@ -5370,6 +5448,27 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwt-decode": { "node_modules/jwt-decode": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
@ -5477,6 +5576,42 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -5484,6 +5619,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -8145,7 +8286,6 @@
"version": "6.19.8", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
@ -8529,6 +8669,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zod": {
"version": "3.24.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View File

@ -12,15 +12,19 @@
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.4.1", "@prisma/client": "^6.4.1",
"@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", "bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"csv-parser": "^3.2.0", "csv-parser": "^3.2.0",
"dotenv": "^16.5.0",
"easy-peasy": "^6.1.0", "easy-peasy": "^6.1.0",
"express": "^5.1.0", "express": "^5.1.0",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"jose": "^6.0.11",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -33,7 +37,8 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-node": "^1.0.2", "react-node": "^1.0.2",
"swr": "^2.3.3" "swr": "^2.3.3",
"zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",

BIN
public/.DS_Store vendored

Binary file not shown.

View File

@ -1,5 +1,7 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { env } from "@utils/env";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { SignJWT } from "jose";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
@ -9,31 +11,64 @@ const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient(); if (usingPrisma) prisma = new PrismaClient();
export async function POST(request: Request) { export async function POST(req: Request) {
try { try {
const body = await request.json(); // Parse incoming JSON data const json = await req.json(); // Parse incoming JSON data
const { email, password } = body; const { email, password } = json.body;
const userData = await readUserCsv(); const userData = await readUserCsv();
console.log(userData); console.log(userData);
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove
console.log("Password:", password); // ! remove console.log("Password:", password); // ! remove
let foundUser; let user;
if (usingPrisma) { if (usingPrisma) {
foundUser = await prisma.user.findUnique({ user = await prisma.user.findUnique({
where: { where: {
email: email, // use the email to uniquely identify the user email, // use the email to uniquely identify the user
}, },
}); });
} else { } else {
foundUser = findUserByEmail(userData, email); user = findUserByEmail(userData, email);
} }
if (foundUser && (await bcrypt.compare(password, usingPrisma ? foundUser.hashedPassword : foundUser.password))) { if (user && bcrypt.compareSync(password, usingPrisma ? user.hashedPassword : user.password)) {
// todo remove password from returned user // todo remove password from returned user
return NextResponse.json({ message: "Login successful!", user: foundUser }, { status: 200 });
// get user and relations
if (usingPrisma)
user = await prisma.user.findUnique({
where: { id: user.id },
include: {
scientist: {
include: {
earthquakes: true,
observatories: true,
artefacts: true,
superior: true,
subordinates: true,
},
},
purchasedArtefacts: true,
},
});
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ userId: user.id })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("2w")
.sign(secret);
const response = NextResponse.json({ message: "Login successful!", user, token }, { status: 200 });
response.cookies.set("jwt", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 3600 * 168 * 2, // 2 weeks
path: "/",
});
return response;
} else { } else {
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 }); return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
} }

View File

@ -0,0 +1,8 @@
// app/api/logout/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
(await cookies()).delete("jwt");
return NextResponse.json({ message: "Logged out" });
}

View File

@ -9,10 +9,10 @@ const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient(); if (usingPrisma) prisma = new PrismaClient();
export async function POST(request: Request) { export async function POST(req: Request) {
try { try {
const body = await request.json(); // Parse incoming JSON data const json = await req.json(); // Parse incoming JSON data
let { email, password, name } = body; let { email, password, name } = json.body;
const accessLevel = "basic"; const accessLevel = "basic";
const userData = await readUserCsv(); const userData = await readUserCsv();

View File

@ -0,0 +1,105 @@
import { NextResponse } from "next/server";
import { env } from "@utils/env";
import { PrismaClient } from "@prisma/client";
import { verifyJwt } from "@utils/verifyJwt";
const usingPrisma = false;
let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient();
// Artefact type
interface Artefact {
id: number;
name: string;
description: string;
location: string;
earthquakeId: string;
isRequired: boolean;
isSold: boolean;
isCollected: boolean;
dateAdded: string;
}
export async function POST(req: Request) {
try {
// todo fix, moron the token will be in the cookie header
const json = await req.json(); // Parse incoming JSON data
const { token } = json.body;
if (!token) return NextResponse.json({ message: "Unauthorised" }, { status: 401 });
await verifyJwt({ token, secret: env.JWT_SECRET_KEY });
const warehouseArtefacts: Artefact[] = [
{
id: 1,
name: "Solidified Lava Chunk",
description: "A chunk of solidified lava from the 2023 Iceland eruption.",
location: "Reykjanes, Iceland",
earthquakeId: "EQ2023ICL",
isRequired: true,
isSold: false,
isCollected: false,
dateAdded: "2025-05-04",
},
{
id: 2,
name: "Tephra Sample",
description: "Foreign debris from the 2022 Tonga volcanic eruption.",
location: "Tonga",
earthquakeId: "EQ2022TGA",
isRequired: false,
isSold: true,
isCollected: true,
dateAdded: "2025-05-03",
},
{
id: 3,
name: "Ash Sample",
description: "Volcanic ash from the 2021 La Palma eruption.",
location: "La Palma, Spain",
earthquakeId: "EQ2021LPA",
isRequired: false,
isSold: false,
isCollected: false,
dateAdded: "2025-05-04",
},
{
id: 4,
name: "Ground Soil",
description: "Soil sample from the 2020 Croatia earthquake site.",
location: "Zagreb, Croatia",
earthquakeId: "EQ2020CRO",
isRequired: true,
isSold: false,
isCollected: false,
dateAdded: "2025-05-02",
},
{
id: 5,
name: "Basalt Fragment",
description: "Basalt rock from the 2019 New Zealand eruption.",
location: "White Island, New Zealand",
earthquakeId: "EQ2019NZL",
isRequired: false,
isSold: true,
isCollected: false,
dateAdded: "2025-05-04",
},
];
let artefacts;
if (usingPrisma) artefacts = await prisma.artefacts.findMany();
if (artefacts) {
return NextResponse.json({ message: "Got artefacts successfully", artefacts }, { status: 200 });
} else {
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 artefacts endpoint:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
} finally {
if (usingPrisma) await prisma.$disconnect();
}
}

View File

@ -31,11 +31,11 @@ const ContactUs = () => {
/> />
{/* Overlay for readability */} {/* Overlay for readability */}
<div className="absolute overflow-hidden w-full h-full bg-gradient-to-b from-black/70 via-black/40 to-transparent flex flex-col items-center z-20"> <div className="absolute overflow-hidden w-full h-full bg-gradient-to-b from-black/80 via-black/40 to-black/20 flex flex-col items-center z-20">
{/* Container */} {/* Container */}
<div className="max-w-4xl mx-auto p-5 mt-20"> <div className="max-w-4xl mx-auto p-5 mt-20">
{/* Header */} {/* Header */}
<h1 className="text-4xl font-bold text-center text-white 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 text-neutral-200 mb-6"> <p className="text-lg text-center text-neutral-200 mb-6">
Have questions or concerns about earthquake preparedness? Contact us using the form below or through the provided Have questions or concerns about earthquake preparedness? Contact us using the form below or through the provided
contact details. contact details.

View File

@ -15,14 +15,25 @@ const inter = Inter({
const store = createStore<StoreModel>({ const store = createStore<StoreModel>({
currency: { currency: {
selectedCurrency: "GBP", selectedCurrency: "EUR",
setSelectedCurrency: action((state, payload) => { setSelectedCurrency: action((state, payload) => {
state.selectedCurrency = payload; state.selectedCurrency = payload;
}), }),
currencies: ["GBP", "USD", "EUR"], currencies: ["GBP", "USD", "EUR"],
conversionRates: { GBP: 1, USD: 1.33, EUR: 1.17 }, conversionRates: { GBP: 0.85, USD: 1.14, EUR: 1 },
tickers: { GBP: "£", USD: "$", EUR: "€" }, 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,
// purchasedArtefacts: [],
// },
}); });
export default function RootLayout({ export default function RootLayout({

View File

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

22
src/app/profile/page.tsx Normal file
View File

@ -0,0 +1,22 @@
"use client";
import axios from "axios";
import { useRouter } from "next/navigation";
export default function Profile() {
const router = useRouter();
return (
<div className="w-full h-full">
<p>User</p>
<button
className="bg-neutral-500"
onClick={async () => {
await axios.get("/api/logout");
router.push("/");
}}
>
Log out
</button>
</div>
);
}

View File

@ -2,19 +2,19 @@
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 Sidebar from "@components/Sidebar"; import Sidebar from "@components/Sidebar";
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", image: "/artefact1.jpg",
price: 150, price: 150,
}, },
{ {
@ -22,7 +22,7 @@ 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", image: "/artefact2.jpg",
price: 200, price: 200,
}, },
{ {
@ -30,7 +30,7 @@ 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", image: "/artefact3.jpg",
price: 120, price: 120,
}, },
{ {
@ -38,7 +38,7 @@ 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", image: "/artefact4.jpg",
price: 80, price: 80,
}, },
{ {
@ -46,7 +46,7 @@ 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", image: "/artefact5.jpg",
price: 300, price: 300,
}, },
{ {
@ -54,7 +54,7 @@ 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", image: "/artefact6.jpg",
price: 250, price: 250,
}, },
{ {
@ -62,7 +62,7 @@ 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", image: "/artefact7.jpg",
price: 175, price: 175,
}, },
{ {
@ -70,7 +70,7 @@ 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", image: "/artefact8.jpg",
price: 400, price: 400,
}, },
{ {
@ -78,7 +78,7 @@ 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", image: "/artefact9.jpg",
price: 220, price: 220,
}, },
{ {
@ -86,7 +86,7 @@ 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", image: "/artefact10.jpg",
price: 150, price: 150,
}, },
{ {
@ -94,7 +94,7 @@ 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", image: "/artefact11.jpg",
price: 500, price: 500,
}, },
{ {
@ -102,7 +102,7 @@ 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", image: "/artefact12.jpg",
price: 300, price: 300,
}, },
{ {
@ -110,15 +110,15 @@ 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", 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", image: "/artefact14.jpg",
price: 1000, price: 1000,
}, },
{ {
@ -126,7 +126,7 @@ 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", image: "/artefact15.jpg",
price: 400, price: 400,
}, },
]; ];
@ -135,11 +135,11 @@ const artifacts: Artifact[] = [
// Shop Component // Shop Component
export default function Shop() { export default function Shop() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [selectedArtifact, setSelectedArtifact] = useState<Artifact | null>(null); // Track selected artifact for modal const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null); // Track selected artefact for modal
const artifactsPerPage = 9; // Number of artifacts per page const artefactsPerPage = 9; // Number of artefacts per page
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);
@ -147,7 +147,7 @@ export default function Shop() {
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 = () => { const handleNextPage = () => {
if (indexOfLastArtifact < artifacts.length) { if (indexOfLastArtefact < artefacts.length) {
setCurrentPage((prev) => prev + 1); setCurrentPage((prev) => prev + 1);
} }
}; };
@ -158,12 +158,12 @@ export default function Shop() {
} }
}; };
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);
} }
}; };
@ -173,20 +173,20 @@ export default function Shop() {
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"
></Image> ></Image>
<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"> <div className="flex justify-end gap-4 mt-4 mr-2">
<button <button
@ -201,20 +201,20 @@ export default function Shop() {
); );
} }
// ArtifactCard Component // ArtefactCard Component
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)} // Opens modal onClick={() => setSelectedArtefact(artefact)} // Opens modal
> >
<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-black font-bold text-md mt-2">
{currencyTickers[selectedCurrency]} {currencyTickers[selectedCurrency]}
{convertPrice(artifact.price, selectedCurrency)} {convertPrice(artefact.price, selectedCurrency)}
</p> </p>
</div> </div>
</div> </div>
@ -225,10 +225,10 @@ export default function Shop() {
<div className="flex flex-col min-h-screen bg-neutral-100"> <div className="flex flex-col min-h-screen bg-neutral-100">
{/* Main Content */} {/* Main Content */}
<div className="flex flex-1 overflow-y-auto"> <div className="flex flex-1 overflow-y-auto">
{/* Artifact Grid */} {/* Artefact Grid */}
<div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 p-6"> <div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 p-6">
{currentArtifacts.map((artifact) => ( {currentArtefacts.map((artefact) => (
<ArtifactCard key={artifact.id} artifact={artifact} /> <ArtefactCard key={artefact.id} artefact={artefact} />
))} ))}
</div> </div>
</div> </div>
@ -247,16 +247,16 @@ export default function Shop() {
<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>
{/* Modal */} {/* Modal */}
{selectedArtifact && <Modal artifact={selectedArtifact} />} {selectedArtefact && <Modal artefact={selectedArtefact} />}
</div> </div>
); );
} }

View File

@ -34,31 +34,34 @@ const teamMembers = [
export default function Page() { export default function Page() {
return ( return (
<div className="h-full text-center bg-gradient-to-b from-neutral-100 to-neutral-50 pt-10"> <div className="h-full text-center bg-gradient-to-b from-neutral-50 to-neutral-100 pt-14 px-4">
{/* Header */} {/* Header */}
<h1 className="text-4xl font-bold mb-5">Meet The Team</h1> <h1 className="text-5xl font-extrabold text-neutral-800 mb-6 tracking-tight">Meet Our Team</h1>
<p className="text-lg text-neutral-600 mb-5"> <p className="text-xl text-neutral-600 mb-12 max-w-2xl mx-auto">
Our expert team is made up of scientists around the globe. Our heads of department are shown below. Our world-class scientists drive innovation across the globe. Meet our department heads below.
</p> </p>
{/* Team Members Section */} {/* Team Members Section */}
<div className="flex flex-col items-center gap-8"> <div className="flex flex-col items-center gap-6">
{teamMembers.map((member, index) => ( {teamMembers.map((member, index) => (
<div key={index} className="flex bg-white align-middle rounded-2xl border border-neutral-300 w-1/2 shadow-sm"> <div
key={index}
className="flex bg-white rounded-3xl border border-neutral-200 w-full max-w-[50%] shadow-md hover:shadow-lg transition-shadow duration-300"
>
{/* Image */} {/* Image */}
<div className="flex items-center ml-4"> <div className="flex items-center ml-6">
<div className="relative w-24 h-24"> <div className="relative w-20 h-20">
<div className="absolute inset-0 rounded-full overflow-hidden"> <div className="absolute inset-0 rounded-full overflow-hidden ring-4 ring-neutral-100">
<Image height={5000} width={5000} src={member.image} alt={member.name} className="h-full w-full object-cover" /> <img src={member.image} alt={member.name} className="h-full w-full object-cover" />
</div> </div>
</div> </div>
</div> </div>
{/* Text Content */} {/* Text Content */}
<div className="flex flex-col items-start pl-6 py-4"> <div className="flex flex-col items-start pl-8 py-4 pr-6">
<h2 className="text-2xl font-bold">{member.name}</h2> <h2 className="text-2xl font-bold text-neutral-800">{member.name}</h2>
<p className="text-md text-neutral-600 font-bold">{member.title}</p> <p className="text-md text-neutral-500 font-semibold">{member.title}</p>
<p className="text-neutral-600 mt-2 text-left">{member.description}</p> <p className="text-neutral-600 mt-3 text-left text-sm leading-relaxed">{member.description}</p>
</div> </div>
</div> </div>
))} ))}

View File

@ -4,9 +4,9 @@ import { FaCalendarPlus, FaWarehouse, FaCartShopping } from "react-icons/fa6";
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5"; import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5";
import { FaTimes } from "react-icons/fa"; import { FaTimes } from "react-icons/fa";
import { SetStateAction, Dispatch } from "react"; import { SetStateAction, Dispatch } from "react";
// import type { Artefact } from "@prisma/client";
// Artifact type interface Artefact {
interface Artifact {
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",
@ -91,7 +91,7 @@ function FilterInput({
}) { }) {
const showSelectedFilter = type === "text" && !["true", "false"].includes(options?.at(-1)!); const showSelectedFilter = type === "text" && !["true", "false"].includes(options?.at(-1)!);
return ( return (
<div className="flex h-full pl-0.5 pr-1 items-center group"> <div className="flex h-full pl-0.5 pr-2 items-center group">
<div className="relative"> <div className="relative">
<div <div
className={`p-1 group-hover:bg-blue-100 rounded transition-colors duration-200 ${ className={`p-1 group-hover:bg-blue-100 rounded transition-colors duration-200 ${
@ -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

@ -39,6 +39,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
body: isLogin ? { email, password } : { name: name!, email, password }, body: isLogin ? { email, password } : { name: name!, email, password },
}); });
if (res.status) { if (res.status) {
res.data.user;
onClose(); onClose();
} else if (res.status >= 400 && res.status < 500) { } else if (res.status >= 400 && res.status < 500) {
console.log("4xx error:", res.data); console.log("4xx error:", res.data);

View File

@ -8,20 +8,23 @@ 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,
{ {
// currencySelector?: { selectedCurrency: string; setSelectedCurrency: Dispatch<SetStateAction<"GBP" | "USD" | "EUR">> }; // currencySelector?: { selectedCurrency: string; setSelectedCurrency: Dispatch<SetStateAction<"GBP" | "USD" | "EUR">> };
}) { }) {
const pathname = usePathname(); const pathname = usePathname();
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", "Warehouse", "Shop"], []); 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" },
// { label: "The Team", path: "/the-team" }, // { label: "The Team", path: "/the-team" },
// { label: "Contact Us", path: "/contact-us" }] // { label: "Contact Us", path: "/contact-us" }]
const router = useRouter();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@ -73,6 +76,25 @@ export default function Navbar({}: // currencySelector,
); );
} }
function ManagementNavbarButton({ name, href, dropdownItems }: { name: string; href: string; dropdownItems?: string[] }) {
const isActive = dropdownItems
? dropdownItems.some((item) => pathname === `/${item.toLowerCase().replace(" ", "-")}`)
: pathname === href;
return (
<button className="flex items-center justify-center px-2 py-4 relative group">
<Link
href={href}
className={`px-3 py-1 border-2 rounded-md transition-colors ${
isActive ? "border-neutral-500" : " hover:border-neutral-500"
}`}
>
{name}
</Link>
</button>
);
}
return ( return (
<div className="flex sticky top-0 w-full h-14 z-40 font-medium bg-white border border-b-neutral-200"> <div className="flex sticky top-0 w-full h-14 z-40 font-medium bg-white border border-b-neutral-200">
<div className="my-1 flex aspect-square ml-3 mr-3"> <div className="my-1 flex aspect-square ml-3 mr-3">
@ -109,7 +131,13 @@ export default function Navbar({}: // currencySelector,
</button> </button>
)} )}
<button className="my-auto mr-4" onClick={() => setIsModalOpen(true)}> {user && (user.role === "SCIENTIST" || user.role === "ADMIN") && (
<div className="flex h-full mr-5">
<ManagementNavbarButton name="Warehouse" href="/warehouse"></ManagementNavbarButton>
</div>
)}
<button className="my-auto mr-4" onClick={() => (user ? router.push("/profile") : setIsModalOpen(true))}>
<FaRegUserCircle size={22} /> <FaRegUserCircle size={22} />
</button> </button>
<AuthModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} /> <AuthModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />

View File

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

View File

@ -1,2 +1,3 @@
name,email,password,level name,email,password,level
Lukeshan Thananchayan,lukeshan@mail.com,$2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge,basic Lukeshan Thananchayan,lukeshan@mail.com,$2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge,undefined
Tim Howitz,tim.howitz@dyson.com,$2b$10$lzDeZYjPNRlme1RI9zaozeVnnFRPdQPH/DTouseAU.8ZLzT14GKxy,basic
1 name email password level
2 Lukeshan Thananchayan lukeshan@mail.com $2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge basic undefined
3 Tim Howitz tim.howitz@dyson.com $2b$10$lzDeZYjPNRlme1RI9zaozeVnnFRPdQPH/DTouseAU.8ZLzT14GKxy basic

View File

@ -8,59 +8,78 @@ datasource db {
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
name String
email String @unique email String @unique
passwordHash String 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 name String
role String @default("USER") @db.VarChar(10) level String @db.VarChar(10) // JUNIOR, SENIOR
scientist Scientist? @relation // Optional relation to Scientist 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 // Earthquake model
model Earthquake { model Earthquake {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
name String @db.VarChar(255) date DateTime
date DateTime locaiton String
location String latitude String
magnitude Float longitude String
depth Float magnitude Float
casualties Int depth Float
creatorId Int? // Creator's ID (Foreign Key referencing Scientist) creatorId Int?
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Points back to Scientist creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
artefacts Artefact[]
observatories Observatory[] @relation("EarthquakeObservatory")
} }
// Observatory model // Observatory model
model Observatory { model Observatory {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
name String @db.VarChar(255) name String
location String location String
longitude String longitude String
latitude String latitude String
dateEstablished Int? dateEstablished Int?
functional Boolean functional Boolean
description String? @db.Text seismicSensorOnline Boolean @default(true)
creatorId Int? // Creator's ID (Foreign Key referencing Scientist) creatorId Int?
creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Points back to Scientist creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
earthquakes Earthquake[] @relation("EarthquakeObservatory")
} }
// Scientist model // Artefact model
model Scientist { model Artefact {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String createdAt DateTime @default(now())
createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
level String @db.VarChar(10) // Junior or Senior type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
user User @relation(fields: [userId], references: [id]) // Relates to the User model warehouseArea String // Examples: "ZoneA-Shelf1", "ZoneB-Rack2", "ZoneC-Bin3"
userId Int @unique earthquakeId Int
earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
// Self-referencing relation: superior and subordinates creatorId Int?
superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Parent scientist creator Scientist? @relation("ScientistArtefactCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
superiorId Int? required Boolean @default(true)
subordinates Scientist[] @relation("SuperiorRelation") // Scientists who view this one as a superior shopPrice Float? // In Euros
purchasedById Int?
// Earthquake and Observatory relations purchasedBy User? @relation("UserPurchasedArtefacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction)
earthquakes Earthquake[] @relation("ScientistEarthquakeCreator") // Scientists can create earthquakes pickedUp Boolean @default(false)
observatories Observatory[] @relation("ScientistObservatoryCreator") // Scientists can create observatories }
}

25
src/middleware.ts Normal file
View File

@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";
import { verifyJwt } from "@utils/verifyJwt";
export async function middleware(req: NextRequest) {
const token = req.cookies.get("jwt")?.value;
if (!token) return NextResponse.redirect(new URL("/", req.url));
const secret = process.env.JWT_SECRET_KEY;
if (!secret) return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
try {
const payload = await verifyJwt({ token, secret });
const response = NextResponse.next();
response.headers.set("user", JSON.stringify(payload));
return response;
} catch (err) {
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
}
}
export const config = {
matcher: ["/warehouse", "/profile", "/admin"],
};

View File

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

View File

@ -1,4 +1,43 @@
import { Action } from "easy-peasy"; import { Action } from "easy-peasy";
// import type { User } from "@prisma/client";
interface Scientist {
id: number;
createdAt: Date;
name: string;
level: string;
user: User;
userId: number;
superior: Scientist | undefined;
superiorId: number | undefined;
subordinates: Scientist[];
// earthquakes: Earthquake[];
// observatories: Observatory[];
artefacts: Artefact[];
}
interface Artefact {
id: number;
name: string;
description: string;
location: string;
earthquakeId: string;
isRequired: boolean;
isSold: boolean;
isCollected: boolean;
dateAdded: string;
}
interface User {
id: number;
createdAt: Date;
name: string;
email: string;
passwordHash: string;
role: string;
scientist: Scientist | undefined;
purchasedArtefacts: Artefact[];
}
type Currency = "GBP" | "USD" | "EUR"; type Currency = "GBP" | "USD" | "EUR";
@ -12,6 +51,7 @@ interface CurrencyModel {
interface StoreModel { interface StoreModel {
currency: CurrencyModel; currency: CurrencyModel;
user: User | null;
} }
export type { StoreModel, Currency }; export type { StoreModel, Currency };

16
src/utils/env.ts Normal file
View File

@ -0,0 +1,16 @@
import * as dotenv from "dotenv";
import { envZod, EnvType } from "@zod/EnvZod";
dotenv.config();
let env: EnvType;
try {
env = envZod.parse(process.env);
} catch (error) {
console.error("INVALID ENV VARIABLE(S)", error, process.env);
// incorrect arguments code
process.exit(9);
}
export { env };

7
src/utils/verifyJwt.ts Normal file
View File

@ -0,0 +1,7 @@
import { jwtVerify } from "jose";
export async function verifyJwt({ token, secret }: { token: string; secret: string }) {
const encodedSecret = new TextEncoder().encode(secret);
const { payload } = await jwtVerify(token, encodedSecret);
return payload;
}

21
src/zod/EnvZod.ts Normal file
View File

@ -0,0 +1,21 @@
import { z } from "zod";
function stringToBool(defaultVal: boolean) {
return z.preprocess((val) => {
if (typeof val === "string") {
if (["1", "true"].includes(val.toLowerCase())) return true;
if (["0", "false"].includes(val.toLowerCase())) return false;
}
return val;
}, z.boolean().default(defaultVal));
}
const envZod = z.object({
DEBUG: stringToBool(false),
DATABASE_URL: z.string(),
JWT_SECRET_KEY: z.string(),
});
type EnvType = z.infer<typeof envZod>;
export { envZod, type EnvType };

View File

@ -1,32 +1,33 @@
{ {
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node", // Use "node" module resolution strategy "moduleResolution": "node", // Use "node" module resolution strategy
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"target": "ESNext", "target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "ESNext", "module": "ESNext",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
} }
], ],
"paths": { "paths": {
"@components/*": ["./src/components/*"], "@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"], "@hooks/*": ["./src/hooks/*"],
"@utils/*": ["./src/utils/*"], "@utils/*": ["./src/utils/*"],
"@appTypes/*": ["./src/types/*"], "@appTypes/*": ["./src/types/*"],
"@/*": ["./src/*"], "@zod/*": ["./src/zod/*"],
} "@/*": ["./src/*"]
}, }
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], },
"exclude": ["node_modules"] "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
} }