diff --git a/.env b/.env new file mode 100644 index 0000000..97160d5 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DATABASE_URL="" +JWT_SECRET_KEY=mysupersecretkey \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2baa5c1..e0b79c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index 99e4e22..58f3594 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/.DS_Store b/public/.DS_Store deleted file mode 100644 index 63b3f4a..0000000 Binary files a/public/.DS_Store and /dev/null differ diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index a5ef4f7..12974e6 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -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 }); } diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts new file mode 100644 index 0000000..45017d2 --- /dev/null +++ b/src/app/api/logout/route.ts @@ -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" }); +} diff --git a/src/app/api/signup/route.ts b/src/app/api/signup/route.ts index 9360f6c..82a3076 100644 --- a/src/app/api/signup/route.ts +++ b/src/app/api/signup/route.ts @@ -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(); diff --git a/src/app/api/warehouse/route.ts b/src/app/api/warehouse/route.ts new file mode 100644 index 0000000..027d92e --- /dev/null +++ b/src/app/api/warehouse/route.ts @@ -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(); + } +} diff --git a/src/app/contact-us/page.tsx b/src/app/contact-us/page.tsx index 955d99c..42d971c 100644 --- a/src/app/contact-us/page.tsx +++ b/src/app/contact-us/page.tsx @@ -31,11 +31,11 @@ const ContactUs = () => { /> {/* Overlay for readability */} -
+
{/* Container */}
{/* Header */} -

Contact Us

+

Contact Us

Have questions or concerns about earthquake preparedness? Contact us using the form below or through the provided contact details. diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4e92c23..c308d00 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,14 +15,25 @@ const inter = Inter({ const store = createStore({ 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({ diff --git a/src/app/our-mission/page.tsx b/src/app/our-mission/page.tsx index a97e068..c089c72 100644 --- a/src/app/our-mission/page.tsx +++ b/src/app/our-mission/page.tsx @@ -1,87 +1,46 @@ "use client"; +import Image from "next/image"; const OurMission = () => { return (

- {/* Overlay to Improve Text Readability */} -
+ {/* Overlay for Readability */} +
{/* Content Area */} -
-

Our Mission

-

- At Earthquake Awareness Initiative, 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. +

+

Our Mission

+

+ At Tremor Tracker, we empower communities worldwide to prepare for + and recover from earthquakes through education, cutting-edge research, and innovative technology.

-

- 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. +

+ 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.

-
-
- Education Icon -

Education

-

- Providing accessible resources to educate people about earthquake preparedness. -

-
-
- Research Icon -

Research

-

- Supporting scientific studies to enhance understanding of seismic activity. -

-
-
- Technology Icon -

Technology

-

- Leveraging innovation to deliver real-time alerts and safety tools. -

-
-
-
-
- ); - return ( -
-
-

Our Mission

-

- At Earthquake Awareness Initiative, 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. -

-

- 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. -

-
-
- Education Icon +
+
+ Education Icon

Education

-

- Providing accessible resources to educate people about earthquake preparedness. +

+ Delivering accessible resources to educate communities on earthquake preparedness.

-
- Research Icon +
+ Research Icon

Research

-

- Supporting scientific studies to enhance understanding of seismic activity. +

+ Advancing scientific studies to deepen understanding of seismic activity.

-
- Technology Icon +
+ Technology Icon

Technology

-

- Leveraging innovation to deliver real-time alerts and safety tools. +

+ Harnessing innovation for real-time alerts and safety solutions.

diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..eeb3f06 --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,22 @@ +"use client"; +import axios from "axios"; +import { useRouter } from "next/navigation"; + +export default function Profile() { + const router = useRouter(); + + return ( +
+

User

+ +
+ ); +} diff --git a/src/app/shop/page.tsx b/src/app/shop/page.tsx index 53a23bc..676854b 100644 --- a/src/app/shop/page.tsx +++ b/src/app/shop/page.tsx @@ -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(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(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} >
-

{artifact.name}

+

{artefact.name}

{artifact.name}

{currencyTickers[selectedCurrency]} - {convertPrice(artifact.price, selectedCurrency)} + {convertPrice(artefact.price, selectedCurrency)}

-

{artifact.description}

-

Location: {artifact.location}

+

{artefact.description}

+

Location: {artefact.location}

@@ -225,10 +225,10 @@ export default function Shop() {
{/* Main Content */}
- {/* Artifact Grid */} + {/* Artefact Grid */}
- {currentArtifacts.map((artifact) => ( - + {currentArtefacts.map((artefact) => ( + ))}
@@ -247,16 +247,16 @@ export default function Shop() {

{currentPage}

{/* Modal */} - {selectedArtifact && } + {selectedArtefact && }
); } diff --git a/src/app/the-team/page.tsx b/src/app/the-team/page.tsx index 821662f..e93d813 100644 --- a/src/app/the-team/page.tsx +++ b/src/app/the-team/page.tsx @@ -34,31 +34,34 @@ const teamMembers = [ export default function Page() { return ( -
+
{/* Header */} -

Meet The Team

-

- Our expert team is made up of scientists around the globe. Our heads of department are shown below. +

Meet Our Team

+

+ Our world-class scientists drive innovation across the globe. Meet our department heads below.

{/* Team Members Section */} -
+
{teamMembers.map((member, index) => ( -
+
{/* Image */} -
-
-
- {member.name} +
+
+
+ {member.name}
{/* Text Content */} -
-

{member.name}

-

{member.title}

-

{member.description}

+
+

{member.name}

+

{member.title}

+

{member.description}

))} diff --git a/src/app/warehouse/page.tsx b/src/app/warehouse/page.tsx index bc8f907..b7f165d 100644 --- a/src/app/warehouse/page.tsx +++ b/src/app/warehouse/page.tsx @@ -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 ( -
+
; 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 }) => (
-
handleSort(key as keyof Artifact)}> +
handleSort(key as keyof Artefact)}>
{label}
@@ -250,11 +250,11 @@ function ArtifactTable({ - {sortedArtifacts.map((artifact) => ( + {sortedArtefacts.map((artefact) => ( setEditArtifact(artifact)} + onClick={() => setEditArtefact(artefact)} > {columns.map(({ key, 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]} ))} @@ -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} >
-

Log New Artifact

+

Log New Artefact

{error &&

{error}

}
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} />