Made a lotta changes

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

2
.env Normal file
View File

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

155
package-lock.json generated
View File

@ -9,15 +9,19 @@
"version": "0.1.0",
"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"
}
}
}
}

View File

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

Binary file not shown.

View File

@ -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 });
}

View File

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

View File

@ -9,10 +9,10 @@ const usingPrisma = false;
let prisma: PrismaClient;
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();

View File

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

View File

@ -31,11 +31,11 @@ const ContactUs = () => {
/>
{/* Overlay for readability */}
<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.

View File

@ -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({

View File

@ -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
View File

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

View File

@ -2,19 +2,19 @@
import Image from "next/image";
import { 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 &rarr;
</button>
</footer>
{/* Modal */}
{selectedArtifact && <Modal artifact={selectedArtifact} />}
{selectedArtefact && <Modal artefact={selectedArtefact} />}
</div>
);
}

View File

@ -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>
))}

View File

@ -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>
);
}

View File

@ -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);

View File

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

View File

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

View File

@ -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
1 name email password level
2 Lukeshan Thananchayan lukeshan@mail.com $2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge basic undefined
3 Tim Howitz tim.howitz@dyson.com $2b$10$lzDeZYjPNRlme1RI9zaozeVnnFRPdQPH/DTouseAU.8ZLzT14GKxy basic

View File

@ -8,59 +8,78 @@ datasource db {
model User {
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
View File

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

View File

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

View File

@ -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
View File

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

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

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

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

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

View File

@ -1,32 +1,33 @@
{
"compilerOptions": {
"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"]
}