Made a lotta changes
This commit is contained in:
parent
2ce882f36e
commit
1ef5330717
155
package-lock.json
generated
155
package-lock.json
generated
@ -9,15 +9,19 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.4.1",
|
||||
"@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-parser": "^3.2.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"easy-peasy": "^6.1.0",
|
||||
"express": "^5.1.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"jose": "^6.0.11",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
@ -30,7 +34,8 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-node": "^1.0.2",
|
||||
"swr": "^2.3.3"
|
||||
"swr": "^2.3.3",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@ -1586,6 +1591,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||
@ -1619,11 +1634,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "20.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
|
||||
"integrity": "sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
@ -2453,6 +2473,12 @@
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -3084,6 +3110,18 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@ -5272,6 +5319,15 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -5326,6 +5382,28 @@
|
||||
"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": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.3.tgz",
|
||||
@ -5370,6 +5448,27 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
@ -5477,6 +5576,42 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@ -5484,6 +5619,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@ -8145,7 +8286,6 @@
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
@ -8529,6 +8669,15 @@
|
||||
"funding": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,15 +12,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.4.1",
|
||||
"@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-parser": "^3.2.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"easy-peasy": "^6.1.0",
|
||||
"express": "^5.1.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"jose": "^6.0.11",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
@ -33,7 +37,8 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-node": "^1.0.2",
|
||||
"swr": "^2.3.3"
|
||||
"swr": "^2.3.3",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
BIN
public/.DS_Store
vendored
BIN
public/.DS_Store
vendored
Binary file not shown.
@ -1,5 +1,7 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { env } from "@utils/env";
|
||||
import { NextResponse } from "next/server";
|
||||
import { SignJWT } from "jose";
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
@ -9,31 +11,64 @@ const usingPrisma = false;
|
||||
let prisma: PrismaClient;
|
||||
if (usingPrisma) prisma = new PrismaClient();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await request.json(); // Parse incoming JSON data
|
||||
const { email, password } = body;
|
||||
const json = await req.json(); // Parse incoming JSON data
|
||||
const { email, password } = json.body;
|
||||
|
||||
const userData = await readUserCsv();
|
||||
console.log(userData);
|
||||
console.log("Email:", email); // ! remove
|
||||
console.log("Password:", password); // ! remove
|
||||
|
||||
let foundUser;
|
||||
let user;
|
||||
|
||||
if (usingPrisma) {
|
||||
foundUser = await prisma.user.findUnique({
|
||||
user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: email, // use the email to uniquely identify the user
|
||||
email, // use the email to uniquely identify the user
|
||||
},
|
||||
});
|
||||
} 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
|
||||
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 {
|
||||
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
|
||||
}
|
||||
|
||||
8
src/app/api/logout/route.ts
Normal file
8
src/app/api/logout/route.ts
Normal 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" });
|
||||
}
|
||||
@ -9,10 +9,10 @@ const usingPrisma = false;
|
||||
let prisma: PrismaClient;
|
||||
if (usingPrisma) prisma = new PrismaClient();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await request.json(); // Parse incoming JSON data
|
||||
let { email, password, name } = body;
|
||||
const json = await req.json(); // Parse incoming JSON data
|
||||
let { email, password, name } = json.body;
|
||||
const accessLevel = "basic";
|
||||
|
||||
const userData = await readUserCsv();
|
||||
|
||||
105
src/app/api/warehouse/route.ts
Normal file
105
src/app/api/warehouse/route.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -31,11 +31,11 @@ const ContactUs = () => {
|
||||
/>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="max-w-4xl mx-auto p-5 mt-20">
|
||||
{/* 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">
|
||||
Have questions or concerns about earthquake preparedness? Contact us using the form below or through the provided
|
||||
contact details.
|
||||
|
||||
@ -15,14 +15,25 @@ const inter = Inter({
|
||||
|
||||
const store = createStore<StoreModel>({
|
||||
currency: {
|
||||
selectedCurrency: "GBP",
|
||||
selectedCurrency: "EUR",
|
||||
setSelectedCurrency: action((state, payload) => {
|
||||
state.selectedCurrency = payload;
|
||||
}),
|
||||
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: "€" },
|
||||
},
|
||||
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({
|
||||
|
||||
@ -1,87 +1,46 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
|
||||
const OurMission = () => {
|
||||
return (
|
||||
<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')" }}
|
||||
>
|
||||
{/* Overlay to Improve Text Readability */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
|
||||
{/* Overlay for Readability */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="relative z-10 max-w-4xl mx-auto p-5 bg-white bg-opacity-90 shadow-lg rounded-lg">
|
||||
<h1 className="text-3xl font-bold text-center text-gray-800 mb-6">Our Mission</h1>
|
||||
<p className="text-lg text-gray-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.
|
||||
<div className="relative z-10 max-w-5xl mx-auto p-8 bg-white bg-opacity-95 shadow-xl rounded-3xl">
|
||||
<h1 className="text-4xl font-extrabold text-center text-neutral-800 mb-8 tracking-tight">Our Mission</h1>
|
||||
<p className="text-xl text-neutral-600 leading-relaxed mb-6 max-w-3xl mx-auto">
|
||||
At <span className="font-semibold text-blue-600">Tremor Tracker</span>, we empower communities worldwide to prepare for
|
||||
and recover from earthquakes through education, cutting-edge research, and innovative technology.
|
||||
</p>
|
||||
<p className="text-lg text-gray-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 className="text-xl text-neutral-600 leading-relaxed mb-8 max-w-3xl mx-auto">
|
||||
We bridge scientific insights with public awareness, delivering resources, tools, and real-time updates to enhance
|
||||
preparedness, save lives, and build resilience against seismic events.
|
||||
</p>
|
||||
<div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6">
|
||||
<div className="flex flex-col items-center p-10">
|
||||
<img src="education.png" alt="Education Icon" className="h-20 w-20 mb-8" />
|
||||
<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" />
|
||||
<div className="flex flex-col md:flex-row md:justify-evenly gap-8 mt-8">
|
||||
<div className="flex flex-col items-center p-6 hover:bg-neutral-50 rounded-xl transition-colors duration-300">
|
||||
<Image height={100} width={100} src="/education.png" alt="Education Icon" className="h-20 w-20 mb-4" />
|
||||
<h3 className="text-xl font-bold text-neutral-700 mb-2">Education</h3>
|
||||
<p className="text-sm text-neutral-500 text-center">
|
||||
Providing accessible resources to educate people about earthquake preparedness.
|
||||
<p className="text-sm text-neutral-500 text-center max-w-xs">
|
||||
Delivering accessible resources to educate communities on earthquake preparedness.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<img src="/images/research-icon.png" alt="Research Icon" className="h-16 w-16 mb-4" />
|
||||
<div className="flex flex-col items-center p-6 hover:bg-neutral-50 rounded-xl transition-colors duration-300">
|
||||
<Image height={100} width={100} src="/research.jpg" alt="Research Icon" className="h-20 w-20 mb-4" />
|
||||
<h3 className="text-xl font-bold text-neutral-700 mb-2">Research</h3>
|
||||
<p className="text-sm text-neutral-500 text-center">
|
||||
Supporting scientific studies to enhance understanding of seismic activity.
|
||||
<p className="text-sm text-neutral-500 text-center max-w-xs">
|
||||
Advancing scientific studies to deepen understanding of seismic activity.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-4">
|
||||
<img src="/images/technology-icon.png" alt="Technology Icon" className="h-16 w-16 mb-4" />
|
||||
<div className="flex flex-col items-center p-6 hover:bg-neutral-50 rounded-xl transition-colors duration-300">
|
||||
<Image height={100} width={100} src="/tech.jpg" alt="Technology Icon" className="h-20 w-20 mb-4" />
|
||||
<h3 className="text-xl font-bold text-neutral-700 mb-2">Technology</h3>
|
||||
<p className="text-sm text-neutral-500 text-center">
|
||||
Leveraging innovation to deliver real-time alerts and safety tools.
|
||||
<p className="text-sm text-neutral-500 text-center max-w-xs">
|
||||
Harnessing innovation for real-time alerts and safety solutions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
22
src/app/profile/page.tsx
Normal file
22
src/app/profile/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -2,19 +2,19 @@
|
||||
import Image from "next/image";
|
||||
import { Dispatch, SetStateAction, useCallback, useState } from "react";
|
||||
|
||||
import Artifact from "@appTypes/Artifact";
|
||||
import Artefact from "@appTypes/Artefact";
|
||||
import { Currency } from "@appTypes/StoreModel";
|
||||
import Sidebar from "@components/Sidebar";
|
||||
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",
|
||||
image: "/artefact1.jpg",
|
||||
price: 150,
|
||||
},
|
||||
{
|
||||
@ -22,7 +22,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Aztec Sunstone",
|
||||
description: "A replica of the Aztec calendar (inscriptions intact).",
|
||||
location: "Peru",
|
||||
image: "/artifact2.jpg",
|
||||
image: "/artefact2.jpg",
|
||||
price: 200,
|
||||
},
|
||||
{
|
||||
@ -30,7 +30,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Medieval Chalice",
|
||||
description: "Used by royalty in medieval ceremonies.",
|
||||
location: "Cambridge, England",
|
||||
image: "/artifact3.jpg",
|
||||
image: "/artefact3.jpg",
|
||||
price: 120,
|
||||
},
|
||||
{
|
||||
@ -38,7 +38,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Roman Coin",
|
||||
description: "An authentic Roman coin from the 2nd century CE.",
|
||||
location: "Rome, Italy",
|
||||
image: "/artifact4.jpg",
|
||||
image: "/artefact4.jpg",
|
||||
price: 80,
|
||||
},
|
||||
{
|
||||
@ -46,7 +46,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Samurai Mask",
|
||||
description: "Replica of Japanese Samurai battle masks.",
|
||||
location: "Tokyo, Japan",
|
||||
image: "/artifact5.jpg",
|
||||
image: "/artefact5.jpg",
|
||||
price: 300,
|
||||
},
|
||||
{
|
||||
@ -54,7 +54,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Ancient Greek Vase",
|
||||
description: "Depicts Greek mythology, found in the Acropolis.",
|
||||
location: "Athens, Greece",
|
||||
image: "/artifact6.jpg",
|
||||
image: "/artefact6.jpg",
|
||||
price: 250,
|
||||
},
|
||||
{
|
||||
@ -62,7 +62,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Incan Pendant",
|
||||
description: "Represents the Sun God Inti.",
|
||||
location: "India",
|
||||
image: "/artifact7.jpg",
|
||||
image: "/artefact7.jpg",
|
||||
price: 175,
|
||||
},
|
||||
{
|
||||
@ -70,7 +70,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Persian Carpet Fragment",
|
||||
description: "Ancient Persian artistry.",
|
||||
location: "Petra, Jordan",
|
||||
image: "/artifact8.jpg",
|
||||
image: "/artefact8.jpg",
|
||||
price: 400,
|
||||
},
|
||||
{
|
||||
@ -78,7 +78,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Stone Buddha",
|
||||
description: "Authentic stone Buddha carving.",
|
||||
location: "India",
|
||||
image: "/artifact9.jpg",
|
||||
image: "/artefact9.jpg",
|
||||
price: 220,
|
||||
},
|
||||
{
|
||||
@ -86,7 +86,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Victorian Brooch",
|
||||
description: "A beautiful Victorian-era brooch with a ruby centre.",
|
||||
location: "Oxford, England",
|
||||
image: "/artifact10.jpg",
|
||||
image: "/artefact10.jpg",
|
||||
price: 150,
|
||||
},
|
||||
{
|
||||
@ -94,7 +94,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Ancient Scroll",
|
||||
description: "A mysterious scroll from ancient times.",
|
||||
location: "Madrid, Spain",
|
||||
image: "/artifact11.jpg",
|
||||
image: "/artefact11.jpg",
|
||||
price: 500,
|
||||
},
|
||||
{
|
||||
@ -102,7 +102,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Ming Dynasty Porcelain",
|
||||
description: "Porcelain from China's Ming Dynasty.",
|
||||
location: "Beijing, China",
|
||||
image: "/artifact12.jpg",
|
||||
image: "/artefact12.jpg",
|
||||
price: 300,
|
||||
},
|
||||
{
|
||||
@ -110,15 +110,15 @@ const artifacts: Artifact[] = [
|
||||
name: "African Tribal Mask",
|
||||
description: "A unique tribal mask from Africa.",
|
||||
location: "Nigeria",
|
||||
image: "/artifact13.jpg",
|
||||
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",
|
||||
image: "/artefact14.jpg",
|
||||
price: 1000,
|
||||
},
|
||||
{
|
||||
@ -126,7 +126,7 @@ const artifacts: Artifact[] = [
|
||||
name: "Medieval Armor Fragment",
|
||||
description: "A fragment of medieval armor.",
|
||||
location: "Normandy, France",
|
||||
image: "/artifact15.jpg",
|
||||
image: "/artefact15.jpg",
|
||||
price: 400,
|
||||
},
|
||||
];
|
||||
@ -135,11 +135,11 @@ const artifacts: Artifact[] = [
|
||||
// Shop Component
|
||||
export default function Shop() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedArtifact, setSelectedArtifact] = useState<Artifact | null>(null); // Track selected artifact for modal
|
||||
const artifactsPerPage = 9; // Number of artifacts per page
|
||||
const indexOfLastArtifact = currentPage * artifactsPerPage;
|
||||
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
|
||||
const currentArtifacts = artifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
|
||||
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null); // Track selected artefact for modal
|
||||
const artefactsPerPage = 9; // Number of artefacts per page
|
||||
const indexOfLastArtefact = currentPage * artefactsPerPage;
|
||||
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
|
||||
const currentArtefacts = artefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
|
||||
|
||||
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
|
||||
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 handleNextPage = () => {
|
||||
if (indexOfLastArtifact < artifacts.length) {
|
||||
if (indexOfLastArtefact < artefacts.length) {
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
@ -158,12 +158,12 @@ export default function Shop() {
|
||||
}
|
||||
};
|
||||
|
||||
function Modal({ artifact }: { artifact: Artifact }) {
|
||||
if (!artifact) return null;
|
||||
function Modal({ artefact }: { artefact: Artefact }) {
|
||||
if (!artefact) return null;
|
||||
|
||||
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setSelectedArtifact(null);
|
||||
setSelectedArtefact(null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -173,20 +173,20 @@ export default function Shop() {
|
||||
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>
|
||||
<h3 className="text-2xl font-bold mb-4">{artefact.name}</h3>
|
||||
<Image
|
||||
height={5000}
|
||||
width={5000}
|
||||
src={artifact.image}
|
||||
alt={artifact.name}
|
||||
src={artefact.image}
|
||||
alt={artefact.name}
|
||||
className="w-full h-64 object-cover rounded-md"
|
||||
></Image>
|
||||
<p className="text-xl font-bold">
|
||||
{currencyTickers[selectedCurrency]}
|
||||
{convertPrice(artifact.price, selectedCurrency)}
|
||||
{convertPrice(artefact.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>
|
||||
<p className="text-neutral-600 mt-2">{artefact.description}</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">
|
||||
<button
|
||||
@ -201,20 +201,20 @@ export default function Shop() {
|
||||
);
|
||||
}
|
||||
|
||||
// ArtifactCard Component
|
||||
function ArtifactCard({ artifact }: { artifact: Artifact }) {
|
||||
// ArtefactCard Component
|
||||
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={() => 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">
|
||||
<h3 className="text-lg font-semibold">{artifact.name}</h3>
|
||||
<p className="text-neutral-500 mb-2">{artifact.location}</p>
|
||||
<h3 className="text-lg font-semibold">{artefact.name}</h3>
|
||||
<p className="text-neutral-500 mb-2">{artefact.location}</p>
|
||||
<p className="text-black font-bold text-md mt-2">
|
||||
{currencyTickers[selectedCurrency]}
|
||||
{convertPrice(artifact.price, selectedCurrency)}
|
||||
{convertPrice(artefact.price, selectedCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -225,10 +225,10 @@ export default function Shop() {
|
||||
<div className="flex flex-col min-h-screen bg-neutral-100">
|
||||
{/* Main Content */}
|
||||
<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">
|
||||
{currentArtifacts.map((artifact) => (
|
||||
<ArtifactCard key={artifact.id} artifact={artifact} />
|
||||
{currentArtefacts.map((artefact) => (
|
||||
<ArtefactCard key={artefact.id} artefact={artefact} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -247,16 +247,16 @@ export default function Shop() {
|
||||
<p className="mx-3 text-lg font-bold">{currentPage}</p>
|
||||
<button
|
||||
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 ${
|
||||
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 →
|
||||
</button>
|
||||
</footer>
|
||||
{/* Modal */}
|
||||
{selectedArtifact && <Modal artifact={selectedArtifact} />}
|
||||
{selectedArtefact && <Modal artefact={selectedArtefact} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -34,31 +34,34 @@ const teamMembers = [
|
||||
|
||||
export default function Page() {
|
||||
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 */}
|
||||
<h1 className="text-4xl font-bold mb-5">Meet The Team</h1>
|
||||
<p className="text-lg text-neutral-600 mb-5">
|
||||
Our expert team is made up of scientists around the globe. Our heads of department are shown below.
|
||||
<h1 className="text-5xl font-extrabold text-neutral-800 mb-6 tracking-tight">Meet Our Team</h1>
|
||||
<p className="text-xl text-neutral-600 mb-12 max-w-2xl mx-auto">
|
||||
Our world-class scientists drive innovation across the globe. Meet our department heads below.
|
||||
</p>
|
||||
|
||||
{/* 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) => (
|
||||
<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 */}
|
||||
<div className="flex items-center ml-4">
|
||||
<div className="relative w-24 h-24">
|
||||
<div className="absolute inset-0 rounded-full overflow-hidden">
|
||||
<Image height={5000} width={5000} src={member.image} alt={member.name} className="h-full w-full object-cover" />
|
||||
<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-6 py-4">
|
||||
<h2 className="text-2xl font-bold">{member.name}</h2>
|
||||
<p className="text-md text-neutral-600 font-bold">{member.title}</p>
|
||||
<p className="text-neutral-600 mt-2 text-left">{member.description}</p>
|
||||
<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>
|
||||
))}
|
||||
|
||||
@ -4,9 +4,9 @@ import { FaCalendarPlus, FaWarehouse, FaCartShopping } from "react-icons/fa6";
|
||||
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5";
|
||||
import { FaTimes } from "react-icons/fa";
|
||||
import { SetStateAction, Dispatch } from "react";
|
||||
// import type { Artefact } from "@prisma/client";
|
||||
|
||||
// Artifact type
|
||||
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",
|
||||
@ -91,7 +91,7 @@ function FilterInput({
|
||||
}) {
|
||||
const showSelectedFilter = type === "text" && !["true", "false"].includes(options?.at(-1)!);
|
||||
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={`p-1 group-hover:bg-blue-100 rounded transition-colors duration-200 ${
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||||
body: isLogin ? { email, password } : { name: name!, email, password },
|
||||
});
|
||||
if (res.status) {
|
||||
res.data.user;
|
||||
onClose();
|
||||
} else if (res.status >= 400 && res.status < 500) {
|
||||
console.log("4xx error:", res.data);
|
||||
|
||||
@ -8,20 +8,23 @@ 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,
|
||||
{
|
||||
// currencySelector?: { selectedCurrency: string; setSelectedCurrency: Dispatch<SetStateAction<"GBP" | "USD" | "EUR">> };
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
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", "Warehouse", "Shop"], []);
|
||||
const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Shop"], []);
|
||||
// const navOptions = useMemo(() => ["Earthquakes"], []);
|
||||
const aboutDropdown = ["Contact Us", "Our Mission", "The Team"];
|
||||
// { label: "Our Mission", path: "/our-mission" },
|
||||
// { label: "The Team", path: "/the-team" },
|
||||
// { label: "Contact Us", path: "/contact-us" }]
|
||||
const router = useRouter();
|
||||
|
||||
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 (
|
||||
<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">
|
||||
@ -109,7 +131,13 @@ export default function Navbar({}: // currencySelector,
|
||||
</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} />
|
||||
</button>
|
||||
<AuthModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||
|
||||
@ -1 +1 @@
|
||||
Artifacts
|
||||
Artefacts
|
||||
|
@ -1,2 +1,3 @@
|
||||
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
|
||||
|
@ -8,59 +8,78 @@ datasource db {
|
||||
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
|
||||
role String @default("USER") @db.VarChar(10)
|
||||
scientist Scientist? @relation // Optional relation to Scientist
|
||||
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
|
||||
name String @db.VarChar(255)
|
||||
date DateTime
|
||||
location String
|
||||
magnitude Float
|
||||
depth Float
|
||||
casualties Int
|
||||
creatorId Int? // Creator's ID (Foreign Key referencing Scientist)
|
||||
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Points back to Scientist
|
||||
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)
|
||||
artefacts Artefact[]
|
||||
observatories Observatory[] @relation("EarthquakeObservatory")
|
||||
}
|
||||
|
||||
// Observatory model
|
||||
model Observatory {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
name String @db.VarChar(255)
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
name String
|
||||
location String
|
||||
longitude String
|
||||
latitude String
|
||||
dateEstablished Int?
|
||||
functional Boolean
|
||||
description String? @db.Text
|
||||
creatorId Int? // Creator's ID (Foreign Key referencing Scientist)
|
||||
creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Points back to Scientist
|
||||
seismicSensorOnline Boolean @default(true)
|
||||
creatorId Int?
|
||||
creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
earthquakes Earthquake[] @relation("EarthquakeObservatory")
|
||||
}
|
||||
|
||||
// Scientist model
|
||||
model Scientist {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
level String @db.VarChar(10) // Junior or Senior
|
||||
user User @relation(fields: [userId], references: [id]) // Relates to the User model
|
||||
userId Int @unique
|
||||
|
||||
// Self-referencing relation: superior and subordinates
|
||||
superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Parent scientist
|
||||
superiorId Int?
|
||||
subordinates Scientist[] @relation("SuperiorRelation") // Scientists who view this one as a superior
|
||||
|
||||
// Earthquake and Observatory relations
|
||||
earthquakes Earthquake[] @relation("ScientistEarthquakeCreator") // Scientists can create earthquakes
|
||||
observatories Observatory[] @relation("ScientistObservatoryCreator") // Scientists can create observatories
|
||||
// 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)
|
||||
}
|
||||
25
src/middleware.ts
Normal file
25
src/middleware.ts
Normal 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"],
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
interface Artifact {
|
||||
interface Artefact {
|
||||
// todo change to string
|
||||
id: number;
|
||||
name: string;
|
||||
@ -8,4 +8,4 @@ interface Artifact {
|
||||
price: number;
|
||||
}
|
||||
|
||||
export default Artifact;
|
||||
export default Artefact;
|
||||
|
||||
@ -1,4 +1,43 @@
|
||||
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";
|
||||
|
||||
@ -12,6 +51,7 @@ interface CurrencyModel {
|
||||
|
||||
interface StoreModel {
|
||||
currency: CurrencyModel;
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
export type { StoreModel, Currency };
|
||||
|
||||
16
src/utils/env.ts
Normal file
16
src/utils/env.ts
Normal 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
7
src/utils/verifyJwt.ts
Normal 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
21
src/zod/EnvZod.ts
Normal 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 };
|
||||
@ -1,32 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node", // Use "node" module resolution strategy
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@hooks/*": ["./src/hooks/*"],
|
||||
"@utils/*": ["./src/utils/*"],
|
||||
"@appTypes/*": ["./src/types/*"],
|
||||
"@/*": ["./src/*"],
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node", // Use "node" module resolution strategy
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@hooks/*": ["./src/hooks/*"],
|
||||
"@utils/*": ["./src/utils/*"],
|
||||
"@appTypes/*": ["./src/types/*"],
|
||||
"@zod/*": ["./src/zod/*"],
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user