Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker
62
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
// Ignores the warning when Git is missing
|
||||||
|
"git.ignoreMissingGitWarning": true,
|
||||||
|
// Disable crash reports being sent to Microsoft.
|
||||||
|
"telemetry.enableCrashReporter": false,
|
||||||
|
// Disable usage data and errors being sent to Microsoft.
|
||||||
|
"telemetry.enableTelemetry": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"cmake.configureOnOpen": false,
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.autopep8",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"autopep8.args": ["--max-line-length=200"],
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"git.confirmSync": false,
|
||||||
|
"editor.rulers": [
|
||||||
|
{
|
||||||
|
"column": 120
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"git.autofetch": true,
|
||||||
|
"files.autoSave": "onFocusChange",
|
||||||
|
"cSpell.language": "en-GB",
|
||||||
|
"C_Cpp.formatting": "disabled",
|
||||||
|
"cmake.showOptionsMovedNotification": false,
|
||||||
|
"diffEditor.renderSideBySide": false,
|
||||||
|
"terminal.integrated.commandsToSkipShell": ["matlab.interrupt"],
|
||||||
|
"[matlab]": {
|
||||||
|
"editor.defaultFormatter": "AffenWiesel.matlab-formatter"
|
||||||
|
},
|
||||||
|
"workbench.editorAssociations": {
|
||||||
|
"*.pdf": "latex-workshop-pdf-hook"
|
||||||
|
},
|
||||||
|
"notebook.output.textLineLimit": 50,
|
||||||
|
"prettier.printWidth": 130,
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"prettier.useTabs": true,
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[cpp]": {
|
||||||
|
"editor.defaultFormatter": "ms-vscode.cpptools"
|
||||||
|
},
|
||||||
|
"C_Cpp.default.cppStandard": "gnu++23",
|
||||||
|
"C_Cpp.default.cStandard": "gnu23",
|
||||||
|
"diffEditor.hideUnchangedRegions.enabled": true,
|
||||||
|
"python.createEnvironment.trigger": "off",
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"importSorter.generalConfiguration.sortOnBeforeSave": true,
|
||||||
|
"cSpell.words": [
|
||||||
|
"vars"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -6,11 +6,12 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
const compat = new FlatCompat({
|
||||||
baseDirectory: __dirname,
|
baseDirectory: __dirname,
|
||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
export default tseslint.config([...compat.extends("next/core-web-vitals", "next/typescript")], {
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
rules: {
|
||||||
];
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
export default eslintConfig;
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
759
package-lock.json
generated
@ -13,13 +13,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.4.1",
|
||||||
"@types/mapbox-gl": "^3.4.1",
|
"@types/mapbox-gl": "^3.4.1",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
|
"easy-peasy": "^6.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"mapbox-gl": "^3.10.0",
|
"mapbox-gl": "^3.10.0",
|
||||||
"next": "15.1.7",
|
"next": "15.1.7",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
@ -28,10 +32,12 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-node": "^1.0.2"
|
"react-node": "^1.0.2",
|
||||||
|
"swr": "^2.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
BIN
public/destruction.jpg
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
public/education.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/facebook.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/insta.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
public/linkedIn.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
public/research.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/road.jpg
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
public/tech.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/tsunamiWaves.jpg
Normal file
|
After Width: | Height: | Size: 464 KiB |
BIN
public/x_logo.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
65
src/app/api/earthquakes/route.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const usingPrisma = false;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
if (usingPrisma) prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
id: "1234",
|
||||||
|
title: "Earthquake in Germany",
|
||||||
|
text1: "Magnitude 8.5",
|
||||||
|
text2: "30 minutes ago",
|
||||||
|
magnitude: 8.5,
|
||||||
|
longitude: 10.4515, // Near Berlin, Germany
|
||||||
|
latitude: 52.52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2134",
|
||||||
|
title: "Earthquake in California",
|
||||||
|
text1: "Magnitude 5.3",
|
||||||
|
text2: "2 hours ago",
|
||||||
|
magnitude: 5.3,
|
||||||
|
longitude: -122.4194, // Near San Francisco, California, USA
|
||||||
|
latitude: 37.7749,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2314",
|
||||||
|
title: "Tremor in Japan",
|
||||||
|
text1: "Magnitude 4.7",
|
||||||
|
text2: "5 hours ago",
|
||||||
|
magnitude: 4.7,
|
||||||
|
longitude: 139.6917, // Near Tokyo, Japan
|
||||||
|
latitude: 35.6762,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2341",
|
||||||
|
title: "Tremor in Spain",
|
||||||
|
text1: "Magnitude 2.1",
|
||||||
|
text2: "10 hours ago",
|
||||||
|
magnitude: 2.1,
|
||||||
|
longitude: -3.7038, // Near Madrid, Spain
|
||||||
|
latitude: 40.4168,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let earthquakes;
|
||||||
|
if (usingPrisma) earthquakes = await prisma.earthquakes.findMany();
|
||||||
|
|
||||||
|
if (earthquakes) {
|
||||||
|
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes }, { status: 200 });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: events }, { status: 200 });
|
||||||
|
// return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in earthquakes endpoint:", error);
|
||||||
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
if (usingPrisma) await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,68 +1,69 @@
|
|||||||
import path from "path";
|
|
||||||
import fs from "fs";
|
|
||||||
import csv from "csv-parser";
|
import csv from "csv-parser";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
accessLevel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {array} - Array of Objects containing user data
|
* @returns {array} - Array of Objects containing user data
|
||||||
*/
|
*/
|
||||||
export async function readUserCsv(): Promise<User[]> {
|
export async function readUserCsv(): Promise<User[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Dynamic CSV location generation
|
// Dynamic CSV location generation
|
||||||
let csvPath = path.dirname(__dirname); // /login
|
let csvPath = path.dirname(__dirname); // /login
|
||||||
csvPath = path.dirname(csvPath); // /api
|
csvPath = path.dirname(csvPath); // /api
|
||||||
csvPath = path.dirname(csvPath); // /app
|
csvPath = path.dirname(csvPath); // /app
|
||||||
csvPath = path.dirname(csvPath); // /src
|
csvPath = path.dirname(csvPath); // /src
|
||||||
csvPath = path.dirname(csvPath); // /[project]
|
csvPath = path.dirname(csvPath); // /[project]
|
||||||
csvPath = path.dirname(csvPath); // /termor-tracker
|
csvPath = path.dirname(csvPath); // /termor-tracker
|
||||||
csvPath = path.join(csvPath, "src", "databases", "Users.csv");
|
csvPath = path.join(csvPath, "src", "databases", "Users.csv");
|
||||||
|
|
||||||
// Forms array for user data
|
// Forms array for user data
|
||||||
let results: User[] = [];
|
let results: User[] = [];
|
||||||
// Reads data and adds it to results
|
// Reads data and adds it to results
|
||||||
fs.createReadStream(csvPath)
|
fs.createReadStream(csvPath)
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on("data", (data) => results.push(data))
|
.on("data", (data) => results.push(data))
|
||||||
.on("end", () => {
|
.on("end", () => {
|
||||||
resolve(results);
|
resolve(results);
|
||||||
})
|
})
|
||||||
.on("error", (error) => {
|
.on("error", (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findUserByEmail(users: User[], email: string): User | undefined {
|
export function findUserByEmail(users: User[], email: string): User | undefined {
|
||||||
return users.find((user) => user.email === email);
|
return users.find((user) => user.email === email);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function passwordStrengthCheck(password: string): Promise<string> {
|
export async function passwordStrengthCheck(password: string): Promise<string> {
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
return "short";
|
return "short";
|
||||||
} else if (password.length > 16) {
|
} else if (password.length > 16) {
|
||||||
return "long";
|
return "long";
|
||||||
}
|
}
|
||||||
const lowercaseRegex = /[a-z]/;
|
const lowercaseRegex = /[a-z]/;
|
||||||
const uppercaseRegex = /[A-Z]/;
|
const uppercaseRegex = /[A-Z]/;
|
||||||
const digitRegex = /\d/;
|
const digitRegex = /\d/;
|
||||||
const specialCharRegex = /[!@#$%^&*]/;
|
const specialCharRegex = /[!@#$%^&*]/;
|
||||||
if (!lowercaseRegex.test(password)) {
|
if (!lowercaseRegex.test(password)) {
|
||||||
return "no lower";
|
return "no lower";
|
||||||
} else if (!uppercaseRegex.test(password)) {
|
} else if (!uppercaseRegex.test(password)) {
|
||||||
return "no upper";
|
return "no upper";
|
||||||
} else if (!digitRegex.test(password)) {
|
} else if (!digitRegex.test(password)) {
|
||||||
return "no digit";
|
return "no digit";
|
||||||
} else if (!specialCharRegex.test(password)) {
|
} else if (!specialCharRegex.test(password)) {
|
||||||
return "no special";
|
return "no special";
|
||||||
} else {
|
} else {
|
||||||
return "secure";
|
return "secure";
|
||||||
}
|
}
|
||||||
return "end of function";
|
return "end of function";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,28 +72,28 @@ export async function passwordStrengthCheck(password: string): Promise<string> {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function writeUserCsv(users: User[]): Promise<void> {
|
export async function writeUserCsv(users: User[]): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Dynamic CSV location generation
|
// Dynamic CSV location generation
|
||||||
let csvPath = path.dirname(__dirname); // /login
|
let csvPath = path.dirname(__dirname); // /login
|
||||||
csvPath = path.dirname(csvPath); // /api
|
csvPath = path.dirname(csvPath); // /api
|
||||||
csvPath = path.dirname(csvPath); // /app
|
csvPath = path.dirname(csvPath); // /app
|
||||||
csvPath = path.dirname(csvPath); // /src
|
csvPath = path.dirname(csvPath); // /src
|
||||||
csvPath = path.dirname(csvPath); // /[project]
|
csvPath = path.dirname(csvPath); // /[project]
|
||||||
csvPath = path.dirname(csvPath); // /termor-tracker
|
csvPath = path.dirname(csvPath); // /termor-tracker
|
||||||
csvPath = path.join(csvPath, "src", "databases", "Users.csv");
|
csvPath = path.join(csvPath, "src", "databases", "Users.csv");
|
||||||
|
|
||||||
// Prepare CSV data as a string
|
// Prepare CSV data as a string
|
||||||
const headers = "name,email,password"; // CSV headers
|
const headers = "name,email,password,level"; // CSV headers
|
||||||
const rows = users.map((user) => `${user.name},${user.email},${user.password}`);
|
const rows = users.map((user) => `${user.name},${user.email},${user.password},${user.accessLevel}`);
|
||||||
const csvData = `${headers}\n${rows.join("\n")}`;
|
const csvData = `${headers}\n${rows.join("\n")}`;
|
||||||
|
|
||||||
// Write data to the file
|
// Write data to the file
|
||||||
fs.writeFile(csvPath, csvData, (error) => {
|
fs.writeFile(csvPath, csvData, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error); // Reject promise on error
|
reject(error); // Reject promise on error
|
||||||
} else {
|
} else {
|
||||||
resolve(); // Resolve the promise if successful
|
resolve(); // Resolve the promise if successful
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1,30 +1,46 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {User, readUserCsv, findUserByEmail} from "../functions/csvReadWrite"
|
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite";
|
||||||
|
|
||||||
|
const usingPrisma = false;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
if (usingPrisma) prisma = new PrismaClient();
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json(); // Parse incoming JSON data
|
const body = await request.json(); // Parse incoming JSON data
|
||||||
const {email, password } = body;
|
const { email, password } = body;
|
||||||
console.log("Login API received data");
|
|
||||||
|
|
||||||
const userData = await readUserCsv();
|
const userData = await readUserCsv();
|
||||||
|
console.log(userData);
|
||||||
|
console.log("Email:", email); // ! remove
|
||||||
|
console.log("Password:", password); // ! remove
|
||||||
|
|
||||||
console.log(userData)
|
let foundUser;
|
||||||
console.log("Email:", email); // ! remove
|
|
||||||
console.log("Password:", password);// ! remove
|
|
||||||
|
|
||||||
const foundUser = findUserByEmail(userData,email)
|
if (usingPrisma) {
|
||||||
if (foundUser && foundUser.password === password) {
|
foundUser = await prisma.user.findUnique({
|
||||||
console.log("User Details Correct")
|
where: {
|
||||||
return NextResponse.json({ message: "Login successful!" }, { status: 200 });
|
email: email, // use the email to uniquely identify the user
|
||||||
} else {
|
},
|
||||||
console.log("User email or password is invalid")
|
});
|
||||||
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
|
} else {
|
||||||
}
|
foundUser = findUserByEmail(userData, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundUser && (await bcrypt.compare(password, usingPrisma ? foundUser.hashedPassword : foundUser.password))) {
|
||||||
} catch (error) {
|
// todo remove password from returned user
|
||||||
console.error("Error in signup endpoint:", error);
|
return NextResponse.json({ message: "Login successful!", user: foundUser }, { status: 200 });
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
} else {
|
||||||
}
|
return NextResponse.json({ message: "Email and/or password are invalid" }, { status: 401 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in signup endpoint:", error);
|
||||||
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
if (usingPrisma) await prisma.$disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
54
src/app/api/observatories/route.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const usingPrisma = false;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
if (usingPrisma) prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
id: "1234",
|
||||||
|
title: "Earthquake - Berlin Observatory",
|
||||||
|
text1: "Logged by ",
|
||||||
|
text2: "30 minutes ago",
|
||||||
|
longitude: 10.4515, // Near Berlin, Germany
|
||||||
|
latitude: 52.52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2134",
|
||||||
|
title: "New Observatory - Phuket, Thailand",
|
||||||
|
text1: "Dr. Neil Armstrong",
|
||||||
|
text2: "2 weeks ago",
|
||||||
|
longitude: -122.4194,
|
||||||
|
latitude: 37.7749,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2314",
|
||||||
|
title: "Observatory Scientist Change",
|
||||||
|
text1: "Dr. Samantha Green new lead scientist",
|
||||||
|
text2: "1 month ago",
|
||||||
|
longitude: 139.6917,
|
||||||
|
latitude: 35.6762,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// todo get earthquakes associated with observatories
|
||||||
|
let observatories;
|
||||||
|
if (usingPrisma) observatories = await prisma.observatories.findMany();
|
||||||
|
|
||||||
|
if (observatories) {
|
||||||
|
return NextResponse.json({ message: "Got observatories successfully", observatories }, { status: 200 });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ message: "Got observatories successfully", observatories: events }, { status: 200 });
|
||||||
|
// return NextResponse.json({ message: "Failed to get observatories" }, { status: 401 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in observatories endpoint:", error);
|
||||||
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
|
} finally {
|
||||||
|
if (usingPrisma) await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,56 +1,83 @@
|
|||||||
|
import bcrypt from "bcrypt";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import {User, readUserCsv, writeUserCsv, findUserByEmail, passwordStrengthCheck} from "../functions/csvReadWrite"
|
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
|
||||||
|
|
||||||
|
const usingPrisma = false;
|
||||||
|
let prisma: PrismaClient;
|
||||||
|
if (usingPrisma) prisma = new PrismaClient();
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json(); // Parse incoming JSON data
|
const body = await request.json(); // Parse incoming JSON data
|
||||||
const {name, email, password } = body;
|
let { email, password, name } = body;
|
||||||
console.log("Signin API received data");
|
const accessLevel = "basic";
|
||||||
|
|
||||||
const userData = await readUserCsv();
|
const userData = await readUserCsv();
|
||||||
|
|
||||||
console.log(userData)
|
console.log(userData);
|
||||||
console.log("Name:", name); // ! remove
|
console.log("Name:", name); // ! remove
|
||||||
console.log("Email:", email); // ! remove
|
console.log("Email:", email); // ! remove
|
||||||
console.log("Password:", password);// ! remove
|
console.log("Password:", password); // ! remove
|
||||||
|
|
||||||
const foundUser = findUserByEmail(userData,email)
|
let foundUser;
|
||||||
|
|
||||||
if (foundUser) {
|
if (usingPrisma) {
|
||||||
console.log("Email already in the system")
|
foundUser = await prisma.user.findUnique({
|
||||||
return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 });
|
where: {
|
||||||
}
|
email: email, // use the email to uniquely identify the user
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
foundUser = findUserByEmail(userData, email);
|
||||||
|
}
|
||||||
|
|
||||||
const passwordCheckResult = await passwordStrengthCheck(password)
|
if (foundUser) {
|
||||||
|
return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 });
|
||||||
|
}
|
||||||
|
|
||||||
if (passwordCheckResult === "short"){
|
const passwordCheckResult = await passwordStrengthCheck(password);
|
||||||
return NextResponse.json({ message: "Your password is shorter than 8 characters" }, { status: 400 });
|
|
||||||
} else if (passwordCheckResult === "long"){
|
|
||||||
return NextResponse.json({ message: "Your password is longer than 16 characters" }, { status: 400 });
|
|
||||||
} else if (passwordCheckResult === "no lower"){
|
|
||||||
return NextResponse.json({ message: "Your password must contain a lowercase letters" }, { status: 400 });
|
|
||||||
} else if (passwordCheckResult === "no upper"){
|
|
||||||
return NextResponse.json({ message: "Your password must contain a uppercase letters" }, { status: 400 });
|
|
||||||
} else if (passwordCheckResult === "no digit"){
|
|
||||||
return NextResponse.json({ message: "Your password must contain a number" }, { status: 400 });
|
|
||||||
} else if (passwordCheckResult === "no special"){
|
|
||||||
return NextResponse.json({ message: "Your password must contain a special character (!@#$%^&*)" }, { status: 400 });
|
|
||||||
} else if (passwordCheckResult === "end of function"){
|
|
||||||
return NextResponse.json({ message: "Password check script failure" }, { status: 500 });
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
userData.push(body)
|
|
||||||
await writeUserCsv(userData)
|
|
||||||
return NextResponse.json({ message: "Account Created" }, { status: 201 });
|
|
||||||
} catch(error) {
|
|
||||||
console.error("Error in writting :", error);
|
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (passwordCheckResult === "short") {
|
||||||
} catch (error) {
|
return NextResponse.json({ message: "Your password is shorter than 8 characters" }, { status: 400 });
|
||||||
console.error("Error in signup endpoint:", error);
|
} else if (passwordCheckResult === "long") {
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ message: "Your password is longer than 16 characters" }, { status: 400 });
|
||||||
}
|
} else if (passwordCheckResult === "no lower") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a lowercase letters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no upper") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a uppercase letters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no digit") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a number" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no special") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a special character (!@#$%^&*)" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "end of function") {
|
||||||
|
return NextResponse.json({ message: "Password check script failure" }, { status: 500 });
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const passwordHash = await bcrypt.hash(password, 10);
|
||||||
|
if (usingPrisma) {
|
||||||
|
// todo add sending back newUser
|
||||||
|
const newUser = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
passwordHash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userData.push({ name, email, password: passwordHash, accessLevel });
|
||||||
|
}
|
||||||
|
await writeUserCsv(userData);
|
||||||
|
return NextResponse.json({ message: "Account Created" }, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in writting :", error);
|
||||||
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in signup endpoint:", error);
|
||||||
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,183 +1,151 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import Image from "next/image";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
const ContactUs = () => {
|
const ContactUs = () => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
message: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e: { target: { name: any; value: any } }) => {
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e: { preventDefault: () => void }) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("Form submitted with data:", formData);
|
console.log("Form submitted with data:", formData);
|
||||||
// Handle form submission, like sending data to a server
|
alert("Thank you for reaching out! We will get back to you soon.");
|
||||||
alert("Thank you for reaching out! We will get back to you soon.");
|
setFormData({ name: "", email: "", message: "" });
|
||||||
setFormData({ name: "", email: "", message: "" }); // Clear form
|
};
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 flex flex-col items-center py-10">
|
<div className="h-full relative text-white border border-black overflow-hidden">
|
||||||
<div className="max-w-4xl mx-auto p-5 bg-white shadow-md rounded-lg">
|
<Image
|
||||||
<h1 className="text-3xl font-bold text-center text-gray-800 mb-6">
|
height={5000}
|
||||||
Contact Us
|
width={5000}
|
||||||
</h1>
|
alt="Logo"
|
||||||
<p className="text-lg text-gray-600 text-center mb-6">
|
className="border border-neutral-300 absolute z-10 -top-20"
|
||||||
Have questions or concerns about earthquake preparedness? Contact us
|
src="/tsunamiWaves.jpg"
|
||||||
using the form below or through the provided contact details.
|
/>
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Flexbox Layout for Form and Contact Details */}
|
{/* Overlay for readability */}
|
||||||
<div className="flex flex-col md:flex-row">
|
<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">
|
||||||
|
{/* 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>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Contact Form */}
|
{/* Content Section */}
|
||||||
<div className="flex-1 mb-6 md:mb-0 md:mr-4">
|
<div className="flex flex-col md:flex-row gap-6">
|
||||||
<form
|
{/* Contact Form Section */}
|
||||||
onSubmit={handleSubmit}
|
<div className="flex-1 bg-white bg-opacity-90 text-neutral-800 rounded-lg shadow-lg p-6">
|
||||||
className="bg-gray-50 p-6 rounded-lg shadow-lg"
|
<form onSubmit={handleSubmit}>
|
||||||
>
|
<div className="mb-4">
|
||||||
<div className="mb-4">
|
<label htmlFor="name" className="block text-neutral-700 font-medium mb-2">
|
||||||
<label
|
Name
|
||||||
htmlFor="name"
|
</label>
|
||||||
className="block text-gray-700 font-medium mb-2"
|
<input
|
||||||
>
|
type="text"
|
||||||
Name
|
name="name"
|
||||||
</label>
|
id="name"
|
||||||
<input
|
value={formData.name}
|
||||||
type="text"
|
onChange={handleChange}
|
||||||
name="name"
|
placeholder="Your Name"
|
||||||
id="name"
|
className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
value={formData.name}
|
required
|
||||||
onChange={handleChange}
|
/>
|
||||||
placeholder="Your Name"
|
</div>
|
||||||
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label htmlFor="email" className="block text-neutral-700 font-medium mb-2">
|
||||||
htmlFor="email"
|
Email
|
||||||
className="block text-gray-700 font-medium mb-2"
|
</label>
|
||||||
>
|
<input
|
||||||
Email
|
type="email"
|
||||||
</label>
|
name="email"
|
||||||
<input
|
id="email"
|
||||||
type="email"
|
value={formData.email}
|
||||||
name="email"
|
onChange={handleChange}
|
||||||
id="email"
|
placeholder="Your Email"
|
||||||
value={formData.email}
|
className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
onChange={handleChange}
|
required
|
||||||
placeholder="Your Email"
|
/>
|
||||||
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
</div>
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label htmlFor="message" className="block text-neutral-700 font-medium mb-2">
|
||||||
htmlFor="message"
|
Message
|
||||||
className="block text-gray-700 font-medium mb-2"
|
</label>
|
||||||
>
|
<textarea
|
||||||
Message
|
name="message"
|
||||||
</label>
|
id="message"
|
||||||
<textarea
|
value={formData.message}
|
||||||
name="message"
|
onChange={handleChange}
|
||||||
id="message"
|
rows={5}
|
||||||
value={formData.message}
|
placeholder="Your Message"
|
||||||
onChange={handleChange}
|
className="w-full p-3 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
rows="5"
|
required
|
||||||
placeholder="Your Message"
|
style={{ resize: "none" }}
|
||||||
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
/>
|
||||||
required
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition duration-200"
|
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition duration-200"
|
||||||
>
|
>
|
||||||
Send Message
|
Send Message
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Details */}
|
{/* Contact Details Section */}
|
||||||
<div className="flex-1 bg-gray-50 p-6 rounded-lg shadow-lg">
|
<div className="w-[45%] bg-white bg-opacity-90 text-neutral-800 rounded-lg shadow-lg p-6">
|
||||||
<h2 className="text-xl font-bold text-gray-800 mb-4">
|
<h2 className="text-xl font-bold text-neutral-800 mb-4">Get in Touch</h2>
|
||||||
Get in Touch
|
<div className="mb-4">
|
||||||
</h2>
|
<h3 className="text-neutral-700 font-medium">Email</h3>
|
||||||
<div className="mb-4">
|
<a href="mailto:getintouch@tremortracker.com" style={{ color: "initial" }}>
|
||||||
<h3 className="text-gray-700 font-medium">Email</h3>
|
getintouch@tremortracker.com
|
||||||
<p className="text-gray-600">support@earthquakesafety.org</p>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-gray-700 font-medium">Phone</h3>
|
<h3 className="text-neutral-700 font-medium">Phone</h3>
|
||||||
<p className="text-gray-600">+1 800 123 4567</p>
|
<p className="text-neutral-600">+44 7538 359022</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-gray-700 font-medium">Address</h3>
|
<h3 className="text-neutral-700 font-medium">Address</h3>
|
||||||
<p className="text-gray-600">
|
<p className="text-neutral-600">1 Swentown Row, Greenwich, London, SE3 0FQ</p>
|
||||||
123 Earthquake Ave, Prepared City, CA 98765
|
</div>
|
||||||
</p>
|
<h2 className="text-xl font-bold text-neutral-800 mb-4 mt-6">Follow Us</h2>
|
||||||
</div>
|
<div className="flex justify-around items-center">
|
||||||
|
<a href="#" className="w-20 h-20 text-blue-600 hover:text-blue-800 transition duration-200">
|
||||||
<h2 className="text-xl font-bold text-gray-800 mb-4 mt-6">
|
<span className="sr-only">Instagram</span>
|
||||||
Follow Us
|
<Image height={200} width={200} alt="Logo" className="z-10" src="/insta.webp" />
|
||||||
</h2>
|
</a>
|
||||||
<div className="flex space-x-4">
|
<a href="#" className="w-20 h-20 text-blue-600 hover:text-blue-800 transition duration-200">
|
||||||
<a
|
<span className="sr-only">Facebook</span>
|
||||||
href="#"
|
<Image height={200} width={200} alt="Logo" className="z-10" src="/facebook.webp" />
|
||||||
className="text-blue-600 hover:text-blue-800 transition duration-200"
|
</a>
|
||||||
>
|
<a href="#" className="w-20 h-20 p-4 text-blue-600 hover:text-blue-800 transition duration-200">
|
||||||
<span className="sr-only">Twitter</span>
|
<span className="sr-only">X</span>
|
||||||
<svg
|
<Image height={200} width={200} alt="Logo" className="z-10 rounded-lg" src="/x_logo.jpg" />
|
||||||
className="h-6 w-6"
|
</a>
|
||||||
fill="currentColor"
|
<a href="#" className="w-20 h-20 flex items-center text-blue-600 hover:text-blue-800 transition duration-200">
|
||||||
viewBox="0 0 24 24"
|
<span className="sr-only">LinkedIn</span>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<Image height={200} width={200} alt="Logo" className="z-10" src="/linkedIn.png" />
|
||||||
>
|
</a>
|
||||||
<path d="M24 4.56a9.83 9.83..." />
|
</div>
|
||||||
</svg>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
<a
|
</div>
|
||||||
href="#"
|
</div>
|
||||||
className="text-blue-600 hover:text-blue-800 transition duration-200"
|
</div>
|
||||||
>
|
);
|
||||||
<span className="sr-only">Facebook</span>
|
|
||||||
<svg
|
|
||||||
className="h-6 w-6"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M22.675 0h-21.35..." />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-blue-600 hover:text-blue-800 transition duration-200"
|
|
||||||
>
|
|
||||||
<span className="sr-only">LinkedIn</span>
|
|
||||||
<svg
|
|
||||||
className="h-6 w-6"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path d="M22.23 0H1.77C0.8..." />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContactUs;
|
export default ContactUs;
|
||||||
|
|||||||
@ -1,80 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Sidebar from "@/components/sidebar";
|
import { useMemo, useState } from "react";
|
||||||
import Map from "@/components/map";
|
import useSWR from "swr";
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
|
import Map from "@components/Map";
|
||||||
|
import Sidebar from "@components/Sidebar";
|
||||||
|
import { fetcher } from "@utils/fetcher";
|
||||||
|
|
||||||
export default function Earthquakes() {
|
export default function Earthquakes() {
|
||||||
const [selectedEventId, setSelectedEventId] = useState("");
|
const [selectedEventId, setSelectedEventId] = useState("");
|
||||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||||
|
// todo properly integrate loading
|
||||||
|
const { data, error, isLoading } = useSWR("/api/earthquakes", fetcher);
|
||||||
|
|
||||||
const events = useMemo(
|
return (
|
||||||
() => [
|
<div className="h-full flex overflow-hidden">
|
||||||
{
|
<div className="flex-grow">
|
||||||
id: "1234",
|
<Map
|
||||||
title: "Earthquake in Germany",
|
events={data ? data.earthquakes : []}
|
||||||
text1: "Magnitude 8.5",
|
selectedEventId={selectedEventId}
|
||||||
text2: "30 minutes ago",
|
setSelectedEventId={setSelectedEventId}
|
||||||
magnitude: 8.5,
|
hoveredEventId={hoveredEventId}
|
||||||
longitude: 10.4515, // Near Berlin, Germany
|
setHoveredEventId={setHoveredEventId}
|
||||||
latitude: 52.52,
|
mapType="Earthquakes"
|
||||||
},
|
></Map>
|
||||||
{
|
</div>
|
||||||
id: "2134",
|
<Sidebar
|
||||||
title: "Earthquake in California",
|
logTitle="Log an Earthquake"
|
||||||
text1: "Magnitude 5.3",
|
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists"
|
||||||
text2: "2 hours ago",
|
recentsTitle="Recent Earthquakes"
|
||||||
magnitude: 5.3,
|
events={data ? data.earthquakes : []}
|
||||||
longitude: -122.4194, // Near San Francisco, California, USA
|
selectedEventId={selectedEventId}
|
||||||
latitude: 37.7749,
|
setSelectedEventId={setSelectedEventId}
|
||||||
},
|
hoveredEventId={hoveredEventId}
|
||||||
{
|
setHoveredEventId={setHoveredEventId}
|
||||||
id: "2314",
|
button1Name="Log an Earthquake"
|
||||||
title: "Tremor in Japan",
|
button2Name="Search Earthquakes"
|
||||||
text1: "Magnitude 4.7",
|
></Sidebar>
|
||||||
text2: "5 hours ago",
|
{/* <SidebarTest></SidebarTest> */}
|
||||||
magnitude: 4.7,
|
</div>
|
||||||
longitude: 139.6917, // Near Tokyo, Japan
|
);
|
||||||
latitude: 35.6762,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2341",
|
|
||||||
title: "Tremor in Spain",
|
|
||||||
text1: "Magnitude 2.1",
|
|
||||||
text2: "10 hours ago",
|
|
||||||
magnitude: 2.1,
|
|
||||||
longitude: -3.7038, // Near Madrid, Spain
|
|
||||||
latitude: 40.4168,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full flex overflow-hidden">
|
|
||||||
<div className="flex-grow">
|
|
||||||
<Map
|
|
||||||
events={events}
|
|
||||||
selectedEventId={selectedEventId}
|
|
||||||
setSelectedEventId={setSelectedEventId}
|
|
||||||
hoveredEventId={hoveredEventId}
|
|
||||||
setHoveredEventId={setHoveredEventId}
|
|
||||||
mapType="Earthquakes"
|
|
||||||
></Map>
|
|
||||||
</div>
|
|
||||||
<Sidebar
|
|
||||||
logTitle="Log an Earthquake"
|
|
||||||
logSubtitle="Record new earthquakes - time/date, location, magnitude, observatory and scientists"
|
|
||||||
recentsTitle="Recent Earthquakes"
|
|
||||||
events={events}
|
|
||||||
selectedEventId={selectedEventId}
|
|
||||||
setSelectedEventId={setSelectedEventId}
|
|
||||||
hoveredEventId={hoveredEventId}
|
|
||||||
setHoveredEventId={setHoveredEventId}
|
|
||||||
button1Name="Log an Earthquake"
|
|
||||||
button2Name="Search Earthquakes"
|
|
||||||
></Sidebar>
|
|
||||||
{/* <SidebarTest></SidebarTest> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @media (prefers-color-scheme: dark) {
|
/* @media (prefers-color-scheme: dark) {
|
||||||
@ -15,29 +15,29 @@
|
|||||||
} */
|
} */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Increase specificity and use !important where necessary */
|
/* Increase specificity and use !important where necessary */
|
||||||
.mapboxgl-popup .mapboxgl-popup-content {
|
.mapboxgl-popup .mapboxgl-popup-content {
|
||||||
@apply rounded-xl p-4 px-5 drop-shadow-lg border border-neutral-300 max-w-xs !important;
|
@apply rounded-xl p-4 px-5 drop-shadow-lg border border-neutral-300 max-w-xs !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the popup tip */
|
/* Hide the popup tip */
|
||||||
.mapboxgl-popup .mapboxgl-popup-tip {
|
.mapboxgl-popup .mapboxgl-popup-tip {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Child elements */
|
/* Child elements */
|
||||||
.mapboxgl-popup-content h3 {
|
.mapboxgl-popup-content h3 {
|
||||||
@apply text-sm font-medium text-neutral-800 !important;
|
@apply text-sm font-medium text-neutral-800 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-popup-content p {
|
.mapboxgl-popup-content p {
|
||||||
@apply text-xs text-neutral-600 !important;
|
@apply text-xs text-neutral-600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-popup-content p + p {
|
.mapboxgl-popup-content p + p {
|
||||||
@apply text-neutral-500 !important;
|
@apply text-neutral-500 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,43 @@
|
|||||||
|
"use client";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Navbar from "@/components/navbar";
|
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
|
import { action, createStore, StoreProvider } from "easy-peasy";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
|
||||||
|
import { StoreModel } from "@appTypes/StoreModel";
|
||||||
|
import Navbar from "@components/Navbar";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
variable: "--font-inter",
|
variable: "--font-inter",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
const store = createStore<StoreModel>({
|
||||||
title: "Tremor Tracker",
|
currency: {
|
||||||
description: "Generated by tim",
|
selectedCurrency: "GBP",
|
||||||
};
|
setSelectedCurrency: action((state, payload) => {
|
||||||
|
state.selectedCurrency = payload;
|
||||||
|
}),
|
||||||
|
currencies: ["GBP", "USD", "EUR"],
|
||||||
|
conversionRates: { GBP: 1, USD: 1.33, EUR: 1.17 },
|
||||||
|
tickers: { GBP: "£", USD: "$", EUR: "€" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${inter.variable} h-[calc(100vh-3.5rem)] flex flex-col min-h-screen antialiased`}>
|
<StoreProvider store={store}>
|
||||||
<Navbar></Navbar>
|
<body className={`${inter.variable} h-[calc(100vh-3.5rem)] flex flex-col min-h-screen antialiased`}>
|
||||||
<div className="flex-1 overflow-y-auto">{children}</div>
|
<Navbar></Navbar>
|
||||||
</body>
|
<div className="flex-1 overflow-y-auto">{children}</div>
|
||||||
</html>
|
</body>
|
||||||
);
|
</StoreProvider>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Sidebar from "@/components/sidebar";
|
import { useMemo, useState } from "react";
|
||||||
import Map from "@/components/map";
|
import useSWR from "swr";
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
|
import Sidebar from "@/components/Sidebar";
|
||||||
|
import Map from "@components/Map";
|
||||||
|
import { fetcher } from "@utils/fetcher";
|
||||||
|
|
||||||
export default function Observatories() {
|
export default function Observatories() {
|
||||||
const [selectedEventId, setSelectedEventId] = useState("");
|
const [selectedEventId, setSelectedEventId] = useState("");
|
||||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
const [hoveredEventId, setHoveredEventId] = useState("");
|
||||||
|
// todo properly integrate loading
|
||||||
|
const { data, error, isLoading } = useSWR("/api/earthquakes", fetcher);
|
||||||
|
|
||||||
const events = useMemo(
|
return (
|
||||||
() => [
|
<div className="h-full flex overflow-hidden">
|
||||||
{
|
<div className="flex-grow">
|
||||||
id: "1234",
|
<Map
|
||||||
title: "Earthquake - Berlin Observatory",
|
events={data ? data.observatories : []}
|
||||||
text1: "Logged by ",
|
selectedEventId={selectedEventId}
|
||||||
text2: "30 minutes ago",
|
setSelectedEventId={setSelectedEventId}
|
||||||
longitude: 10.4515, // Near Berlin, Germany
|
hoveredEventId={hoveredEventId}
|
||||||
latitude: 52.52,
|
setHoveredEventId={setHoveredEventId}
|
||||||
},
|
mapType="observatories"
|
||||||
{
|
></Map>
|
||||||
id: "2134",
|
</div>
|
||||||
title: "New Observatory - Phuket, Thailand",
|
<Sidebar
|
||||||
text1: "Dr. Neil Armstrong",
|
logTitle="Observatory Mapping"
|
||||||
text2: "2 weeks ago",
|
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
|
||||||
longitude: -122.4194,
|
recentsTitle="Observatory Events"
|
||||||
latitude: 37.7749,
|
events={data ? data.observatories : []}
|
||||||
},
|
selectedEventId={selectedEventId}
|
||||||
{
|
setSelectedEventId={setSelectedEventId}
|
||||||
id: "2314",
|
hoveredEventId={hoveredEventId}
|
||||||
title: "Observatory Scientist Change",
|
setHoveredEventId={setHoveredEventId}
|
||||||
text1: "Dr. Samantha Green new lead scientist",
|
button1Name="Log a New Observatory"
|
||||||
text2: "1 month ago",
|
button2Name="Search Observatories"
|
||||||
longitude: 139.6917,
|
></Sidebar>
|
||||||
latitude: 35.6762,
|
{/* <SidebarTest></SidebarTest> */}
|
||||||
},
|
</div>
|
||||||
],
|
);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full flex overflow-hidden">
|
|
||||||
<div className="flex-grow">
|
|
||||||
<Map
|
|
||||||
events={events}
|
|
||||||
selectedEventId={selectedEventId}
|
|
||||||
setSelectedEventId={setSelectedEventId}
|
|
||||||
hoveredEventId={hoveredEventId}
|
|
||||||
setHoveredEventId={setHoveredEventId}
|
|
||||||
mapType="observatories"
|
|
||||||
></Map>
|
|
||||||
</div>
|
|
||||||
<Sidebar
|
|
||||||
logTitle="Observatory Mapping"
|
|
||||||
logSubtitle="Record and search observatories - time/date set-up, location, scientists and recent earthquakes"
|
|
||||||
recentsTitle="Observatory Events"
|
|
||||||
events={events}
|
|
||||||
selectedEventId={selectedEventId}
|
|
||||||
setSelectedEventId={setSelectedEventId}
|
|
||||||
hoveredEventId={hoveredEventId}
|
|
||||||
setHoveredEventId={setHoveredEventId}
|
|
||||||
button1Name="Log a New Observatory"
|
|
||||||
button2Name="Search Observatories"
|
|
||||||
></Sidebar>
|
|
||||||
{/* <SidebarTest></SidebarTest> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +1,93 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
//export default function Page()
|
|
||||||
const OurMission = () => {
|
const OurMission = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center py-10">
|
<div
|
||||||
<div className="max-w-4xl mx-auto p-5 bg-white shadow-lg rounded-lg">
|
className="h-screen relative bg-fixed bg-cover bg-center text-white py-10"
|
||||||
<h1 className="text-3xl font-bold text-center text-gray-800 mb-6">Our Mission</h1>
|
style={{ backgroundImage: "url('destruction.jpg')" }}
|
||||||
<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.
|
{/* Overlay to Improve Text Readability */}
|
||||||
</p>
|
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
|
||||||
<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.
|
{/* Content Area */}
|
||||||
</p>
|
<div className="relative z-10 max-w-4xl mx-auto p-5 bg-white bg-opacity-90 shadow-lg rounded-lg">
|
||||||
<div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6">
|
<h1 className="text-3xl font-bold text-center text-gray-800 mb-6">Our Mission</h1>
|
||||||
<div className="flex flex-col items-center p-4">
|
<p className="text-lg text-gray-600 leading-relaxed mb-4">
|
||||||
<img
|
At <span className="font-semibold text-blue-600">Earthquake Awareness Initiative</span>, our mission is to help people
|
||||||
src="/images/education-icon.png"
|
worldwide prepare for and recover from earthquakes. Through education, research, and innovative technology, we work
|
||||||
alt="Education Icon"
|
tirelessly to empower communities with the knowledge they need to stay safe before, during, and after seismic events.
|
||||||
className="h-16 w-16 mb-4"
|
</p>
|
||||||
/>
|
<p className="text-lg text-gray-600 leading-relaxed mb-4">
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">Education</h3>
|
We aim to bridge the gap between scientific research and community awareness by providing resources, tools, and
|
||||||
<p className="text-sm text-gray-500 text-center">
|
real-time updates for earthquake preparedness. Together, we aspire to save lives, mitigate impacts, and foster
|
||||||
Providing accessible resources to educate people about earthquake preparedness.
|
resilience against nature's powerful forces.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6">
|
||||||
<div className="flex flex-col items-center p-4">
|
<div className="flex flex-col items-center p-10">
|
||||||
<img
|
<img src="education.png" alt="Education Icon" className="h-20 w-20 mb-8" />
|
||||||
src="/images/research-icon.png"
|
<h3 className="text-xl font-bold text-gray-700 mb-2">Education</h3>
|
||||||
alt="Research Icon"
|
<p className="text-sm text-gray-500 text-center">
|
||||||
className="h-16 w-16 mb-4"
|
Providing accessible resources to educate people about earthquake preparedness.
|
||||||
/>
|
</p>
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">Research</h3>
|
</div>
|
||||||
<p className="text-sm text-gray-500 text-center">
|
<div className="flex flex-col items-center p-10">
|
||||||
Supporting scientific studies to enhance understanding of seismic activity.
|
<img src="research.jpg" alt="Research Icon" className="h-20 w-20 mb-4" />
|
||||||
</p>
|
<h3 className="text-xl font-bold text-gray-700 mb-2">Research</h3>
|
||||||
</div>
|
<p className="text-sm text-gray-500 text-center">
|
||||||
<div className="flex flex-col items-center p-4">
|
Supporting scientific studies to enhance understanding of seismic activity.
|
||||||
<img
|
</p>
|
||||||
src="/images/technology-icon.png"
|
</div>
|
||||||
alt="Technology Icon"
|
<div className="flex flex-col items-center p-10">
|
||||||
className="h-16 w-16 mb-4"
|
<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>
|
||||||
<h3 className="text-xl font-bold text-gray-700 mb-2">Technology</h3>
|
<p className="text-sm text-gray-500 text-center">
|
||||||
<p className="text-sm text-gray-500 text-center">
|
Leveraging innovation to deliver real-time alerts and safety tools.
|
||||||
Leveraging innovation to deliver real-time alerts and safety tools.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
return (
|
||||||
|
<div className="min-h-screen bg-neutral-100 flex flex-col items-center justify-center py-10">
|
||||||
|
<div className="max-w-4xl mx-auto p-5 bg-white shadow-lg rounded-lg">
|
||||||
|
<h1 className="text-3xl font-bold text-center text-neutral-800 mb-6">Our Mission</h1>
|
||||||
|
<p className="text-lg text-neutral-600 leading-relaxed mb-4">
|
||||||
|
At <span className="font-semibold text-blue-600">Earthquake Awareness Initiative</span>, our mission is to help people
|
||||||
|
worldwide prepare for and recover from earthquakes. Through education, research, and innovative technology, we work
|
||||||
|
tirelessly to empower communities with the knowledge they need to stay safe before, during, and after seismic events.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg text-neutral-600 leading-relaxed mb-4">
|
||||||
|
We aim to bridge the gap between scientific research and community awareness by providing resources, tools, and
|
||||||
|
real-time updates for earthquake preparedness. Together, we aspire to save lives, mitigate impacts, and foster
|
||||||
|
resilience against nature's powerful forces.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col md:flex-row md:justify-evenly items-center mt-6">
|
||||||
|
<div className="flex flex-col items-center p-4">
|
||||||
|
<img src="/images/education-icon.png" alt="Education Icon" className="h-16 w-16 mb-4" />
|
||||||
|
<h3 className="text-xl font-bold text-neutral-700 mb-2">Education</h3>
|
||||||
|
<p className="text-sm text-neutral-500 text-center">
|
||||||
|
Providing accessible resources to educate people about 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" />
|
||||||
|
<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>
|
||||||
|
</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" />
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OurMission;
|
export default OurMission;
|
||||||
240
src/app/page.tsx
@ -1,128 +1,126 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen text-black">
|
<main className="min-h-screen text-black">
|
||||||
<div className="w-full relative overflow-hidden">
|
<div className="w-full relative overflow-hidden">
|
||||||
<div className="">
|
<div className="">
|
||||||
<Image height={2000} width={2000} alt="Background Image" src="/lava_flows.jpg"></Image>
|
<Image height={2000} width={2000} alt="Background Image" src="/lava_flows.jpg"></Image>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40"></div>
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40"></div>
|
||||||
<div className="absolute inset-0 top-[30%]">
|
<div className="absolute inset-0 top-[30%]">
|
||||||
<Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png"></Image>
|
<Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png"></Image>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-20 w-4/6 px-2 border border-black divide-y">
|
||||||
|
{["Earthquake 1", "Earthquake 2", "Earthquake 3"].map((name) => (
|
||||||
|
<div className="px-5 py-5" key={name}>
|
||||||
|
<p className="ml-3">{name}</p>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-96">Spacer</p>
|
||||||
|
<p className="mt-96">Spacer</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||||
|
// <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||||
|
// <Image
|
||||||
|
// className="dark:invert"
|
||||||
|
// src="/next.svg"
|
||||||
|
// alt="Next.js logo"
|
||||||
|
// width={180}
|
||||||
|
// height={38}
|
||||||
|
// priority
|
||||||
|
// />
|
||||||
|
// <ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||||
|
// <li className="mb-2">
|
||||||
|
// Get started by editing{" "}
|
||||||
|
// <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||||
|
// src/app/page.tsx
|
||||||
|
// </code>
|
||||||
|
// .
|
||||||
|
// </li>
|
||||||
|
// <li>Save and see your changes instantly.</li>
|
||||||
|
// </ol>
|
||||||
|
|
||||||
<div className="mx-auto mt-20 w-4/6 px-2 border border-black divide-y">
|
// <div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||||
{["Earthquake 1", "Earthquake 2", "Earthquake 3"].map((name) => (
|
// <a
|
||||||
<div className="px-5 py-5" key={name}>
|
// className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||||
<p className="ml-3">{name}</p>
|
// href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
<p></p>
|
// target="_blank"
|
||||||
</div>
|
// rel="noopener noreferrer"
|
||||||
))}
|
// >
|
||||||
</div>
|
// <Image
|
||||||
<p className="mt-96">Spacer</p>
|
// className="dark:invert"
|
||||||
<p className="mt-96">Spacer</p>
|
// src="/vercel.svg"
|
||||||
</main>
|
// alt="Vercel logomark"
|
||||||
);
|
// width={20}
|
||||||
|
// height={20}
|
||||||
// return (
|
// />
|
||||||
// <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
// Deploy now
|
||||||
// <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
// </a>
|
||||||
// <Image
|
// <a
|
||||||
// className="dark:invert"
|
// className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||||
// src="/next.svg"
|
// href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
// alt="Next.js logo"
|
// target="_blank"
|
||||||
// width={180}
|
// rel="noopener noreferrer"
|
||||||
// height={38}
|
// >
|
||||||
// priority
|
// Read our docs
|
||||||
// />
|
// </a>
|
||||||
// <ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
// </div>
|
||||||
// <li className="mb-2">
|
// </main>
|
||||||
// Get started by editing{" "}
|
// <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||||
// <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
// <a
|
||||||
// src/app/page.tsx
|
// className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
// </code>
|
// href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
// .
|
// target="_blank"
|
||||||
// </li>
|
// rel="noopener noreferrer"
|
||||||
// <li>Save and see your changes instantly.</li>
|
// >
|
||||||
// </ol>
|
// <Image
|
||||||
|
// aria-hidden
|
||||||
// <div className="flex gap-4 items-center flex-col sm:flex-row">
|
// src="/file.svg"
|
||||||
// <a
|
// alt="File icon"
|
||||||
// className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
// width={16}
|
||||||
// href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
// height={16}
|
||||||
// target="_blank"
|
// />
|
||||||
// rel="noopener noreferrer"
|
// Learn
|
||||||
// >
|
// </a>
|
||||||
// <Image
|
// <a
|
||||||
// className="dark:invert"
|
// className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
// src="/vercel.svg"
|
// href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
// alt="Vercel logomark"
|
// target="_blank"
|
||||||
// width={20}
|
// rel="noopener noreferrer"
|
||||||
// height={20}
|
// >
|
||||||
// />
|
// <Image
|
||||||
// Deploy now
|
// aria-hidden
|
||||||
// </a>
|
// src="/window.svg"
|
||||||
// <a
|
// alt="Window icon"
|
||||||
// className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
// width={16}
|
||||||
// href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
// height={16}
|
||||||
// target="_blank"
|
// />
|
||||||
// rel="noopener noreferrer"
|
// Examples
|
||||||
// >
|
// </a>
|
||||||
// Read our docs
|
// <a
|
||||||
// </a>
|
// className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
// </div>
|
// href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
// </main>
|
// target="_blank"
|
||||||
// <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
// rel="noopener noreferrer"
|
||||||
// <a
|
// >
|
||||||
// className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
// <Image
|
||||||
// href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
// aria-hidden
|
||||||
// target="_blank"
|
// src="/globe.svg"
|
||||||
// rel="noopener noreferrer"
|
// alt="Globe icon"
|
||||||
// >
|
// width={16}
|
||||||
// <Image
|
// height={16}
|
||||||
// aria-hidden
|
// />
|
||||||
// src="/file.svg"
|
// Go to nextjs.org →
|
||||||
// alt="File icon"
|
// </a>
|
||||||
// width={16}
|
// </footer>
|
||||||
// height={16}
|
// </div>
|
||||||
// />
|
// );
|
||||||
// Learn
|
|
||||||
// </a>
|
|
||||||
// <a
|
|
||||||
// className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
// href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
// target="_blank"
|
|
||||||
// rel="noopener noreferrer"
|
|
||||||
// >
|
|
||||||
// <Image
|
|
||||||
// aria-hidden
|
|
||||||
// src="/window.svg"
|
|
||||||
// alt="Window icon"
|
|
||||||
// width={16}
|
|
||||||
// height={16}
|
|
||||||
// />
|
|
||||||
// Examples
|
|
||||||
// </a>
|
|
||||||
// <a
|
|
||||||
// className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
// href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
// target="_blank"
|
|
||||||
// rel="noopener noreferrer"
|
|
||||||
// >
|
|
||||||
// <Image
|
|
||||||
// aria-hidden
|
|
||||||
// src="/globe.svg"
|
|
||||||
// alt="Globe icon"
|
|
||||||
// width={16}
|
|
||||||
// height={16}
|
|
||||||
// />
|
|
||||||
// Go to nextjs.org →
|
|
||||||
// </a>
|
|
||||||
// </footer>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,185 +1,262 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Sidebar from "@/components/sidebar";
|
import Image from "next/image";
|
||||||
import { useState } from "react";
|
import { Dispatch, SetStateAction, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import Artifact from "@appTypes/Artifact";
|
||||||
|
import { Currency } from "@appTypes/StoreModel";
|
||||||
|
import Sidebar from "@components/Sidebar";
|
||||||
|
import { useStoreState } from "@hooks/store";
|
||||||
|
|
||||||
// Artifacts Data
|
// Artifacts Data
|
||||||
const artifacts = [
|
const artifacts: Artifact[] = [
|
||||||
{ id: 1, name: "Golden Scarab", description: "An ancient Egyptian artifact symbolizing rebirth.", location: "Cairo, Egypt", image: "/images/artifact1.jpg", price: 150 },
|
{
|
||||||
{ id: 2, name: "Aztec Sunstone", description: "A replica of the Aztec calendar (inscriptions intact).", location: "Peru", image: "/images/artifact2.jpg", price: 200 },
|
id: 1,
|
||||||
{ id: 3, name: "Medieval Chalice", description: "Used by royalty in medieval ceremonies.", location: "Cambridge, England", image: "/images/artifact3.jpg", price: 120 },
|
name: "Golden Scarab",
|
||||||
{ id: 4, name: "Roman Coin", description: "An authentic Roman coin from the 2nd century CE.", location: "Rome, Italy", image: "/images/artifact4.jpg", price: 80 },
|
description: "An ancient Egyptian artifact symbolizing rebirth.",
|
||||||
{ id: 5, name: "Samurai Mask", description: "Replica of Japanese Samurai battle masks.", location: "Tokyo, Japan", image: "/images/artifact5.jpg", price: 300 },
|
location: "Cairo, Egypt",
|
||||||
{ id: 6, name: "Ancient Greek Vase", description: "Depicts Greek mythology, found in the Acropolis.", location: "Athens, Greece", image: "/images/artifact6.jpg", price: 250 },
|
image: "/artifact1.jpg",
|
||||||
{ id: 7, name: "Incan Pendant", description: "Represents the Sun God Inti.", location: "India", image: "/images/artifact7.jpg", price: 175 },
|
price: 150,
|
||||||
{ id: 8, name: "Persian Carpet Fragment", description: "Ancient Persian artistry.", location: "Petra, Jordan", image: "/images/artifact8.jpg", price: 400 },
|
},
|
||||||
{ id: 9, name: "Stone Buddha", description: "Authentic stone Buddha carving.", location: "India", image: "/images/artifact9.jpg", price: 220 },
|
{
|
||||||
{ id: 10, name: "Victorian Brooch", description: "A beautiful Victorian-era brooch with a ruby centre.", location: "Oxford, England", image: "/images/artifact10.jpg", price: 150 },
|
id: 2,
|
||||||
{ id: 11, name: "Ancient Scroll", description: "A mysterious scroll from ancient times.", location: "Madrid, Spain", image: "/images/artifact11.jpg", price: 500 },
|
name: "Aztec Sunstone",
|
||||||
{ id: 12, name: "Ming Dynasty Porcelain", description: "Porcelain from China's Ming Dynasty.", location: "Beijing, China", image: "/images/artifact12.jpg", price: 300 },
|
description: "A replica of the Aztec calendar (inscriptions intact).",
|
||||||
{ id: 13, name: "African Tribal Mask", description: "A unique tribal mask from Africa.", location: "Nigeria", image: "/images/artifact13.jpg", price: 250 },
|
location: "Peru",
|
||||||
{ id: 14, name: "Crystal Skull", description: "A mystical pre-Columbian artifact.", location: "Colombia", image: "/images/artifact14.jpg", price: 1000 },
|
image: "/artifact2.jpg",
|
||||||
{ id: 15, name: "Medieval Armor Fragment", description: "A fragment of medieval armor.", location: "Normandy, France", image: "/images/artifact15.jpg", price: 400 },
|
price: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Medieval Chalice",
|
||||||
|
description: "Used by royalty in medieval ceremonies.",
|
||||||
|
location: "Cambridge, England",
|
||||||
|
image: "/artifact3.jpg",
|
||||||
|
price: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Roman Coin",
|
||||||
|
description: "An authentic Roman coin from the 2nd century CE.",
|
||||||
|
location: "Rome, Italy",
|
||||||
|
image: "/artifact4.jpg",
|
||||||
|
price: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Samurai Mask",
|
||||||
|
description: "Replica of Japanese Samurai battle masks.",
|
||||||
|
location: "Tokyo, Japan",
|
||||||
|
image: "/artifact5.jpg",
|
||||||
|
price: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: "Ancient Greek Vase",
|
||||||
|
description: "Depicts Greek mythology, found in the Acropolis.",
|
||||||
|
location: "Athens, Greece",
|
||||||
|
image: "/artifact6.jpg",
|
||||||
|
price: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: "Incan Pendant",
|
||||||
|
description: "Represents the Sun God Inti.",
|
||||||
|
location: "India",
|
||||||
|
image: "/artifact7.jpg",
|
||||||
|
price: 175,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: "Persian Carpet Fragment",
|
||||||
|
description: "Ancient Persian artistry.",
|
||||||
|
location: "Petra, Jordan",
|
||||||
|
image: "/artifact8.jpg",
|
||||||
|
price: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
name: "Stone Buddha",
|
||||||
|
description: "Authentic stone Buddha carving.",
|
||||||
|
location: "India",
|
||||||
|
image: "/artifact9.jpg",
|
||||||
|
price: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: "Victorian Brooch",
|
||||||
|
description: "A beautiful Victorian-era brooch with a ruby centre.",
|
||||||
|
location: "Oxford, England",
|
||||||
|
image: "/artifact10.jpg",
|
||||||
|
price: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: "Ancient Scroll",
|
||||||
|
description: "A mysterious scroll from ancient times.",
|
||||||
|
location: "Madrid, Spain",
|
||||||
|
image: "/artifact11.jpg",
|
||||||
|
price: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: "Ming Dynasty Porcelain",
|
||||||
|
description: "Porcelain from China's Ming Dynasty.",
|
||||||
|
location: "Beijing, China",
|
||||||
|
image: "/artifact12.jpg",
|
||||||
|
price: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: "African Tribal Mask",
|
||||||
|
description: "A unique tribal mask from Africa.",
|
||||||
|
location: "Nigeria",
|
||||||
|
image: "/artifact13.jpg",
|
||||||
|
price: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
name: "Crystal Skull",
|
||||||
|
description: "A mystical pre-Columbian artifact.",
|
||||||
|
location: "Colombia",
|
||||||
|
image: "/artifact14.jpg",
|
||||||
|
price: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
name: "Medieval Armor Fragment",
|
||||||
|
description: "A fragment of medieval armor.",
|
||||||
|
location: "Normandy, France",
|
||||||
|
image: "/artifact15.jpg",
|
||||||
|
price: 400,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Modal Component
|
// Modal Component
|
||||||
const Modal = ({ artifact, onClose }) => {
|
|
||||||
if (!artifact) return null;
|
|
||||||
|
|
||||||
const handleOverlayClick = (e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-gray-900 bg-opacity-50 flex justify-center items-center z-50"
|
|
||||||
onClick={handleOverlayClick}>
|
|
||||||
<div className="bg-white rounded-md shadow-lg max-w-lg w-full p-6">
|
|
||||||
<h3 className="text-xl font-bold mb-4">{artifact.name}</h3>
|
|
||||||
<img src={artifact.image} alt={artifact.name} className="w-full h-64 object-cover rounded-md mb-4" />
|
|
||||||
<p className="text-gray-600 mb-2">{artifact.description}</p>
|
|
||||||
<p className="text-gray-500 font-bold mb-4">Location: {artifact.location}</p>
|
|
||||||
<p className="text-red-600 font-bold">Price: ${artifact.price}</p>
|
|
||||||
<div className="flex justify-end gap-4 mt-6">
|
|
||||||
<button
|
|
||||||
onClick={() => alert("Reserved Successfully!")}
|
|
||||||
className="px-4 py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-400">
|
|
||||||
Reserve
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => alert("Purchased Successfully!")}
|
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-500">
|
|
||||||
Buy
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 bg-gray-300 rounded-md hover:bg-gray-400">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ArtifactCard Component
|
|
||||||
const ArtifactCard = ({ artifact, onSelect }) => {
|
|
||||||
const [selectedCurrency, setSelectedCurrency] = useState("USD");
|
|
||||||
const conversionRates = { USD: 1, EUR: 0.94, GBP: 0.81 };
|
|
||||||
const convertPrice = (price, currency) => (price * conversionRates[currency]).toFixed(2);
|
|
||||||
const handleCurrencyChange = (e) => {
|
|
||||||
setSelectedCurrency(e.target.value); // Update selected currency
|
|
||||||
e.stopPropagation(); // Prevent the modal from opening on dropdown click
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
|
|
||||||
onClick={() => onSelect(artifact)} // Opens modal
|
|
||||||
>
|
|
||||||
<img src={artifact.image} alt={artifact.name} className="w-full h-56 object-cover" />
|
|
||||||
<div className="p-4">
|
|
||||||
<h3 className="text-lg font-bold">{artifact.name}</h3>
|
|
||||||
<p className="text-gray-700 mb-2">{artifact.description}</p>
|
|
||||||
<p className="text-gray-500 mb-2">{artifact.location}</p>
|
|
||||||
<p className="text-red-600 font-bold text-md mt-4">
|
|
||||||
{selectedCurrency}: {convertPrice(artifact.price, selectedCurrency)}
|
|
||||||
</p>
|
|
||||||
<select
|
|
||||||
value={selectedCurrency}
|
|
||||||
onChange={handleCurrencyChange} // Handles currency change
|
|
||||||
className="border border-gray-300 rounded-lg px-3 py-1 text-sm items-left"
|
|
||||||
onClick={(e) => e.stopPropagation()} // Prevents triggering the modal
|
|
||||||
>
|
|
||||||
<option value="USD">USD ($)</option>
|
|
||||||
<option value="EUR">EUR (€)</option>
|
|
||||||
<option value="GBP">GBP (£)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Shop Component
|
// Shop Component
|
||||||
export default function Shop() {
|
export default function Shop() {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [selectedArtifact, setSelectedArtifact] = useState(null); // Track selected artifact for modal
|
const [selectedArtifact, setSelectedArtifact] = useState<Artifact | null>(null); // Track selected artifact for modal
|
||||||
const artifactsPerPage = 9; // Number of artifacts per page
|
const artifactsPerPage = 9; // Number of artifacts per page
|
||||||
const indexOfLastArtifact = currentPage * artifactsPerPage;
|
const indexOfLastArtifact = currentPage * artifactsPerPage;
|
||||||
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
|
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
|
||||||
const currentArtifacts = artifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
|
const currentArtifacts = artifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
|
||||||
|
|
||||||
const handleNextPage = () => {
|
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
|
||||||
if (indexOfLastArtifact < artifacts.length) {
|
const conversionRates = useStoreState((state) => state.currency.conversionRates);
|
||||||
setCurrentPage((prev) => prev + 1);
|
const currencyTickers = useStoreState((state) => state.currency.tickers);
|
||||||
}
|
const convertPrice = useCallback((price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2), []);
|
||||||
};
|
|
||||||
|
|
||||||
const handlePreviousPage = () => {
|
const handleNextPage = () => {
|
||||||
if (currentPage > 1) {
|
if (indexOfLastArtifact < artifacts.length) {
|
||||||
setCurrentPage((prev) => prev - 1);
|
setCurrentPage((prev) => prev + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectArtifact = (artifact) => {
|
const handlePreviousPage = () => {
|
||||||
setSelectedArtifact(artifact); // Open modal with selected artifact
|
if (currentPage > 1) {
|
||||||
};
|
setCurrentPage((prev) => prev - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
function Modal({ artifact }: { artifact: Artifact }) {
|
||||||
setSelectedArtifact(null); // Close modal
|
if (!artifact) return null;
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
<div className="flex flex-col min-h-screen bg-gray-100">
|
if (e.target === e.currentTarget) {
|
||||||
{/* Main Content */}
|
setSelectedArtifact(null);
|
||||||
<div className="flex flex-1">
|
}
|
||||||
{/* Artifact Grid */}
|
};
|
||||||
<div className="flex-grow grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 p-6 pr-72">
|
|
||||||
{currentArtifacts.map((artifact) => (
|
|
||||||
<ArtifactCard key={artifact.id} artifact={artifact} onSelect={handleSelectArtifact} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sidebar */}
|
return (
|
||||||
<aside className="w-72 bg-white shadow-lg p-4 h-screen fixed top-10 right-0 overflow-auto">
|
<div
|
||||||
<Sidebar
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
logTitle="Artifact Collection"
|
onClick={handleOverlayClick}
|
||||||
logSubtitle="Record new artifacts - name, description, image, location and price"
|
>
|
||||||
recentsTitle="Recent Updates"
|
<div className="bg-white rounded-xl shadow-2xl max-w-lg w-full p-6">
|
||||||
events={[/* example events if needed */]}
|
<h3 className="text-2xl font-bold mb-4">{artifact.name}</h3>
|
||||||
selectedEventId=""
|
<Image
|
||||||
setSelectedEventId={() => {}}
|
height={5000}
|
||||||
hoveredEventId=""
|
width={5000}
|
||||||
setHoveredEventId={() => {}}
|
src={artifact.image}
|
||||||
button1Name="Add New Artifact"
|
alt={artifact.name}
|
||||||
button2Name="Search Artifacts"
|
className="w-full h-64 object-cover rounded-md"
|
||||||
/>
|
></Image>
|
||||||
</aside>
|
<p className="text-xl font-bold">
|
||||||
</div>
|
{currencyTickers[selectedCurrency]}
|
||||||
|
{convertPrice(artifact.price, selectedCurrency)}
|
||||||
|
</p>
|
||||||
|
<p className="text-neutral-600 mt-2">{artifact.description}</p>
|
||||||
|
<p className="text-neutral-500 font-bold mt-1">Location: {artifact.location}</p>
|
||||||
|
|
||||||
{/* Pagination Footer */}
|
<div className="flex justify-end gap-4 mt-4 mr-2">
|
||||||
<footer className="bg-white border-t border-gray-300 py-2 text-center w-full flex justify-center items-centre">
|
<button
|
||||||
<button
|
onClick={() => alert("Purchased Successfully!")}
|
||||||
onClick={handlePreviousPage}
|
className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||||
disabled={currentPage === 1}
|
>
|
||||||
className={`mx-2 px-4 py-1 bg-blue-700 text-white rounded-md font-bold shadow-md ${
|
Buy
|
||||||
currentPage === 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
|
</button>
|
||||||
}`} >
|
</div>
|
||||||
← Previous
|
</div>
|
||||||
</button>
|
</div>
|
||||||
<button
|
);
|
||||||
onClick={handleNextPage}
|
}
|
||||||
disabled={indexOfLastArtifact >= artifacts.length}
|
|
||||||
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
|
|
||||||
indexOfLastArtifact >= artifacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
|
|
||||||
}`} >
|
|
||||||
Next →
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
{/* Modal */}
|
// ArtifactCard Component
|
||||||
<Modal artifact={selectedArtifact} onClose={handleCloseModal} />
|
function ArtifactCard({ artifact }: { artifact: Artifact }) {
|
||||||
</div>
|
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
|
||||||
|
>
|
||||||
|
<img src={artifact.image} alt={artifact.name} className="w-full h-56 object-cover" />
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="text-lg font-semibold">{artifact.name}</h3>
|
||||||
|
<p className="text-neutral-500 mb-2">{artifact.location}</p>
|
||||||
|
<p className="text-black font-bold text-md mt-2">
|
||||||
|
{currencyTickers[selectedCurrency]}
|
||||||
|
{convertPrice(artifact.price, selectedCurrency)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen bg-neutral-100">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex flex-1 overflow-y-auto">
|
||||||
|
{/* Artifact 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} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Footer */}
|
||||||
|
<footer className="mt-10 bg-white border-t border-neutral-300 py-3 text-center flex justify-center items-center">
|
||||||
|
<button
|
||||||
|
onClick={handlePreviousPage}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
|
||||||
|
currentPage === 1 ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
← Previous
|
||||||
|
</button>
|
||||||
|
<p className="mx-3 text-lg font-bold">{currentPage}</p>
|
||||||
|
<button
|
||||||
|
onClick={handleNextPage}
|
||||||
|
disabled={indexOfLastArtifact >= artifacts.length}
|
||||||
|
className={`mx-2 px-4 py-1 bg-blue-500 text-white rounded-md font-bold shadow-md ${
|
||||||
|
indexOfLastArtifact >= artifacts.length ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
{/* Modal */}
|
||||||
|
{selectedArtifact && <Modal artifact={selectedArtifact} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
/* Container styling */
|
|
||||||
.container {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: #333;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header styling */
|
|
||||||
.header {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subheader {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Team styling */
|
|
||||||
.team {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: center; /* Centers team member cards */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Individual team member card styling */
|
|
||||||
.card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
width: 90%; /* Width relative to the viewport */
|
|
||||||
max-width: 800px; /* Prevent it from getting too large */
|
|
||||||
padding: 15px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Team member image styling */
|
|
||||||
.image {
|
|
||||||
width: 110px; /* Both width and height are equal */
|
|
||||||
height: 110px;
|
|
||||||
border-radius: 50%; /* Makes the image circular */
|
|
||||||
object-fit: cover; /* Ensures the image fits perfectly without distortion */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card text styling */
|
|
||||||
.text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #111;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #777;
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
export default function User() {
|
export default function User() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
<p>User</p>
|
<p>User</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,57 +1,843 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Sidebar from "@/components/sidebar";
|
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
|
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";
|
||||||
|
|
||||||
export default function Warehouse() {
|
// Artifact type
|
||||||
const [selectedEventId, setSelectedEventId] = useState("");
|
interface Artifact {
|
||||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
id: number;
|
||||||
const events = useMemo(
|
name: string;
|
||||||
() => [
|
description: string;
|
||||||
{
|
location: string;
|
||||||
id: "1234",
|
earthquakeId: string;
|
||||||
title: "Artifact found - Germany Earthquake",
|
isRequired: boolean;
|
||||||
text1: "Mortar and pestle",
|
isSold: boolean;
|
||||||
text2: "10 minutes ago",
|
isCollected: boolean;
|
||||||
longitude: 10.4515, // Near Berlin, Germany
|
dateAdded: string;
|
||||||
latitude: 52.52,
|
}
|
||||||
},
|
|
||||||
{
|
// Warehouse Artifacts Data
|
||||||
id: "2134",
|
const warehouseArtifacts: Artifact[] = [
|
||||||
title: "All artifacts from California earthquake transported",
|
{
|
||||||
text1: "India to London",
|
id: 1,
|
||||||
text2: "15 hours ago",
|
name: "Solidified Lava Chunk",
|
||||||
longitude: -122.4194, // Near San Francisco, California, USA
|
description: "A chunk of solidified lava from the 2023 Iceland eruption.",
|
||||||
latitude: 37.7749,
|
location: "Reykjanes, Iceland",
|
||||||
},
|
earthquakeId: "EQ2023ICL",
|
||||||
{
|
isRequired: true,
|
||||||
id: "2341",
|
isSold: false,
|
||||||
title: "7 artifacts destroyed - Spain earthquake",
|
isCollected: false,
|
||||||
text1: "edVases and pots from Madrid",
|
dateAdded: "2025-05-04",
|
||||||
text2: "3 weeks ago",
|
},
|
||||||
longitude: -3.7038, // Near Madrid, Spain
|
{
|
||||||
latitude: 40.4168,
|
id: 2,
|
||||||
},
|
name: "Tephra Sample",
|
||||||
],
|
description: "Foreign debris from the 2022 Tonga volcanic eruption.",
|
||||||
[]
|
location: "Tonga",
|
||||||
);
|
earthquakeId: "EQ2022TGA",
|
||||||
|
isRequired: false,
|
||||||
return (
|
isSold: true,
|
||||||
<div className="w-full h-full">
|
isCollected: true,
|
||||||
<p>
|
dateAdded: "2025-05-03",
|
||||||
warehouse image pasted in here
|
},
|
||||||
</p>
|
{
|
||||||
<Sidebar
|
id: 3,
|
||||||
logTitle="Artifact Retrieval and Tracking"
|
name: "Ash Sample",
|
||||||
logSubtitle="Record new artifacts collected - name, description, time/date found, location found, scientist and collection stage"
|
description: "Volcanic ash from the 2021 La Palma eruption.",
|
||||||
recentsTitle="Artifact News"
|
location: "La Palma, Spain",
|
||||||
events={events}
|
earthquakeId: "EQ2021LPA",
|
||||||
selectedEventId={selectedEventId}
|
isRequired: false,
|
||||||
setSelectedEventId={setSelectedEventId}
|
isSold: false,
|
||||||
hoveredEventId={hoveredEventId}
|
isCollected: false,
|
||||||
setHoveredEventId={setHoveredEventId}
|
dateAdded: "2025-05-04",
|
||||||
button1Name="Log New Artifacts"
|
},
|
||||||
button2Name="Search Artifacts"
|
{
|
||||||
></Sidebar>
|
id: 4,
|
||||||
</div>
|
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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filter Component
|
||||||
|
function FilterInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
options?: string[];
|
||||||
|
}) {
|
||||||
|
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="relative">
|
||||||
|
<div
|
||||||
|
className={`p-1 group-hover:bg-blue-100 rounded transition-colors duration-200 ${
|
||||||
|
!showSelectedFilter && value && "bg-blue-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<IoFilter
|
||||||
|
className={`cursor-pointer text-neutral-500 font-bold group-hover:text-blue-600
|
||||||
|
${!showSelectedFilter && value && "text-blue-600"}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`absolute z-50 mt-2 w-48 bg-white border border-neutral-300 rounded-md shadow-lg p-2 opacity-0 group-hover:opacity-100 group-hover:visible transition-opacity duration-200 pointer-events-none group-hover:pointer-events-auto
|
||||||
|
${type === "date" ? "-right-1/2" : "-left-1/2"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{options ? (
|
||||||
|
<div className="max-h-32 overflow-y-auto">
|
||||||
|
{options.map((opt) => (
|
||||||
|
<div key={opt} className="p-1 hover:bg-blue-100 cursor-pointer text-sm" onClick={() => onChange(opt)}>
|
||||||
|
{opt ? (opt === "true" ? "Yes" : "No") : "All"}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full p-1 border border-neutral-300 rounded-md text-sm"
|
||||||
|
placeholder="Filter..."
|
||||||
|
aria-label="Filter input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{value && showSelectedFilter && (
|
||||||
|
<div className="inline-flex items-center bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-md">
|
||||||
|
{value === "true" ? "Yes" : value === "false" ? "No" : value}
|
||||||
|
<FaTimes className="ml-1 cursor-pointer text-blue-600 hover:text-blue-800" onClick={() => onChange("")} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table Component
|
||||||
|
function ArtifactTable({
|
||||||
|
artifacts,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
setEditArtifact,
|
||||||
|
clearSort,
|
||||||
|
}: {
|
||||||
|
artifacts: Artifact[];
|
||||||
|
filters: Record<string, string>;
|
||||||
|
setFilters: Dispatch<
|
||||||
|
SetStateAction<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
earthquakeId: string;
|
||||||
|
location: string;
|
||||||
|
description: string;
|
||||||
|
isRequired: string;
|
||||||
|
isSold: string;
|
||||||
|
isCollected: string;
|
||||||
|
dateAdded: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
setEditArtifact: (artifact: Artifact) => void;
|
||||||
|
clearSort: () => void;
|
||||||
|
}) {
|
||||||
|
const [sortConfig, setSortConfig] = useState<{
|
||||||
|
key: keyof Artifact;
|
||||||
|
direction: "asc" | "desc";
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleSort = (key: keyof Artifact) => {
|
||||||
|
setSortConfig((prev) => {
|
||||||
|
if (!prev || prev.key !== key) {
|
||||||
|
return { key, direction: "asc" };
|
||||||
|
} else if (prev.direction === "asc") {
|
||||||
|
return { key, direction: "desc" };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSortConfig = () => {
|
||||||
|
setSortConfig(null);
|
||||||
|
clearSort();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedArtifacts = useMemo(() => {
|
||||||
|
if (!sortConfig) return artifacts;
|
||||||
|
const sorted = [...artifacts].sort((a, b) => {
|
||||||
|
const aValue = a[sortConfig.key];
|
||||||
|
const bValue = b[sortConfig.key];
|
||||||
|
if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1;
|
||||||
|
if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}, [artifacts, sortConfig]);
|
||||||
|
|
||||||
|
const columns: { label: string; key: keyof Artifact; width: string }[] = [
|
||||||
|
{ label: "ID", key: "id", width: "5%" },
|
||||||
|
{ label: "Name", key: "name", width: "12%" },
|
||||||
|
{ label: "Earthquake ID", key: "earthquakeId", width: "10%" },
|
||||||
|
{ label: "Location", key: "location", width: "12%" },
|
||||||
|
{ label: "Description", key: "description", width: "25%" },
|
||||||
|
{ label: "Required", key: "isRequired", width: "6%" },
|
||||||
|
{ label: "Sold", key: "isSold", width: "5%" },
|
||||||
|
{ label: "Collected", key: "isCollected", width: "7%" },
|
||||||
|
{ label: "Date Added", key: "dateAdded", width: "8%" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="w-full table-fixed text-left">
|
||||||
|
<thead className="sticky top-0 bg-neutral-100 border-b border-neutral-200 z-10">
|
||||||
|
<tr>
|
||||||
|
{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="select-none">{label}</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-full relative">
|
||||||
|
<FilterInput
|
||||||
|
value={filters[key]}
|
||||||
|
onChange={(value) => {
|
||||||
|
setFilters({ ...filters, [key]: value } as {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
earthquakeId: string;
|
||||||
|
location: string;
|
||||||
|
description: string;
|
||||||
|
isRequired: string;
|
||||||
|
isSold: string;
|
||||||
|
isCollected: string;
|
||||||
|
dateAdded: string;
|
||||||
|
});
|
||||||
|
if (value === "") clearSortConfig();
|
||||||
|
}}
|
||||||
|
type={key === "dateAdded" ? "date" : "text"}
|
||||||
|
options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined}
|
||||||
|
/>
|
||||||
|
{sortConfig?.key === key && (
|
||||||
|
<div className="absolute -right-2 top-3">{sortConfig.direction === "asc" ? "↑" : "↓"}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sortedArtifacts.map((artifact) => (
|
||||||
|
<tr
|
||||||
|
key={artifact.id}
|
||||||
|
className="border-b border-neutral-200 hover:bg-neutral-100 cursor-pointer"
|
||||||
|
onClick={() => setEditArtifact(artifact)}
|
||||||
|
>
|
||||||
|
{columns.map(({ key, width }) => (
|
||||||
|
<td
|
||||||
|
key={key}
|
||||||
|
className={`py-3 px-5 text-sm text-neutral-600 truncate ${key === "name" && "font-medium text-neutral-800"}`}
|
||||||
|
style={{ width }}
|
||||||
|
>
|
||||||
|
{key === "isRequired"
|
||||||
|
? artifact.isRequired
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
: key === "isSold"
|
||||||
|
? artifact.isSold
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
: key === "isCollected"
|
||||||
|
? artifact.isCollected
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
: artifact[key]}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component for Logging Artifact
|
||||||
|
function LogModal({ onClose }: { onClose: () => void }) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [location, setLocation] = useState("");
|
||||||
|
const [earthquakeId, setEarthquakeId] = useState("");
|
||||||
|
const [storageLocation, setStorageLocation] = useState("");
|
||||||
|
const [isRequired, setIsRequired] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLog = async () => {
|
||||||
|
if (!name || !description || !location || !earthquakeId || !storageLocation) {
|
||||||
|
setError("All fields are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||||
|
alert(`Logged ${name} to storage: ${storageLocation}`);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setError("Failed to log artifact. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-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>
|
||||||
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="Description"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Location"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Earthquake ID"
|
||||||
|
value={earthquakeId}
|
||||||
|
onChange={(e) => setEarthquakeId(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="Earthquake ID"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Storage Location (e.g., A-12)"
|
||||||
|
value={storageLocation}
|
||||||
|
onChange={(e) => setStorageLocation(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="Storage Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLog}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
||||||
|
}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Logging...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Log Artifact"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component for Bulk Logging
|
||||||
|
function BulkLogModal({ onClose }: { onClose: () => void }) {
|
||||||
|
const [palletNote, setPalletNote] = useState("");
|
||||||
|
const [storageLocation, setStorageLocation] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLog = async () => {
|
||||||
|
if (!palletNote || !storageLocation) {
|
||||||
|
setError("All fields are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||||
|
alert(`Logged bulk pallet to storage: ${storageLocation}`);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setError("Failed to log pallet. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-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 Bulk Pallet</h3>
|
||||||
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<textarea
|
||||||
|
placeholder="Pallet Delivery Note (e.g., 10 lava chunks, 5 ash samples)"
|
||||||
|
value={palletNote}
|
||||||
|
onChange={(e) => setPalletNote(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-24"
|
||||||
|
aria-label="Pallet Delivery Note"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Storage Location (e.g., B-05)"
|
||||||
|
value={storageLocation}
|
||||||
|
onChange={(e) => setStorageLocation(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="Storage Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLog}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
||||||
|
}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Logging...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Log Pallet"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!name || !description || !location || !earthquakeId || !dateAdded) {
|
||||||
|
setError("All fields are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||||
|
alert(`Updated artifact ${name}`);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setError("Failed to update artifact. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-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>
|
||||||
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={earthquakeId}
|
||||||
|
onChange={(e) => setEarthquakeId(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="Earthquake ID"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Sold</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Collected</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={dateAdded}
|
||||||
|
onChange={(e) => setDateAdded(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-label="Date Added"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
||||||
|
}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Save"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter Logic
|
||||||
|
const applyFilters = (artifacts: Artifact[], filters: Record<string, string>): Artifact[] => {
|
||||||
|
return artifacts.filter((artifact) => {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Warehouse Component
|
||||||
|
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 [filters, setFilters] = useState({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
earthquakeId: "",
|
||||||
|
location: "",
|
||||||
|
description: "",
|
||||||
|
isRequired: "",
|
||||||
|
isSold: "",
|
||||||
|
isCollected: "",
|
||||||
|
dateAdded: "",
|
||||||
|
});
|
||||||
|
const [isFiltering, setIsFiltering] = useState(false);
|
||||||
|
const [sortConfig, setSortConfig] = useState<{
|
||||||
|
key: keyof Artifact;
|
||||||
|
direction: "asc" | "desc";
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const artifactsPerPage = 10;
|
||||||
|
const indexOfLastArtifact = currentPage * artifactsPerPage;
|
||||||
|
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
|
||||||
|
|
||||||
|
// Apply filters with loading state
|
||||||
|
const filteredArtifacts = useMemo(() => {
|
||||||
|
setIsFiltering(true);
|
||||||
|
const result = applyFilters(warehouseArtifacts, filters);
|
||||||
|
setIsFiltering(false);
|
||||||
|
return result;
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
const currentArtifacts = filteredArtifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
|
||||||
|
|
||||||
|
// Overview stats
|
||||||
|
const totalArtifacts = warehouseArtifacts.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 clearFilters = () => {
|
||||||
|
setFilters({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
earthquakeId: "",
|
||||||
|
location: "",
|
||||||
|
description: "",
|
||||||
|
isRequired: "",
|
||||||
|
isSold: "",
|
||||||
|
isCollected: "",
|
||||||
|
dateAdded: "",
|
||||||
|
});
|
||||||
|
setSortConfig(null); // Clear sorting
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-neutral-50">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex flex-1 p-5">
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
{/* Overview Stats */}
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logging Buttons */}
|
||||||
|
<div className="flex justify-end gap-3 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={clearFilters}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
>
|
||||||
|
Clear All Filters
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLogModal(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
Log Single Artifact
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowBulkLogModal(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
|
||||||
|
>
|
||||||
|
Log Bulk Pallet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Card */}
|
||||||
|
<div className="flex-grow bg-white rounded-lg shadow-md border border-neutral-200 overflow-hidden relative">
|
||||||
|
{isFiltering && (
|
||||||
|
<div className="absolute inset-0 bg-white bg-opacity-50 flex items-center justify-center z-20">
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-8 w-8 text-blue-600"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="h-full overflow-y-none">
|
||||||
|
<ArtifactTable
|
||||||
|
artifacts={currentArtifacts}
|
||||||
|
filters={filters}
|
||||||
|
setFilters={setFilters}
|
||||||
|
setEditArtifact={setEditArtifact}
|
||||||
|
clearSort={() => setSortConfig(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
|
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
|
||||||
|
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
|
||||||
|
{editArtifact && <EditModal artifact={editArtifact} onClose={() => setEditArtifact(null)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,168 +1,122 @@
|
|||||||
// tells React if you're using its "server-side rendering" features. this component runs only on the client-side (browser)
|
|
||||||
"use client";
|
"use client";
|
||||||
// importing React hooks or utilities that enhance functionality inside the component
|
import axios from "axios";
|
||||||
import { useState, FormEvent, useRef, MouseEvent, useEffect } from "react";
|
import { FormEvent, MouseEvent, useEffect, useRef, useState } from "react";
|
||||||
/*
|
|
||||||
useState: Used to manage state (data that changes over time).
|
|
||||||
FormEvent: TypeScript definition for form-related events like submission.
|
|
||||||
useRef: Used to access the DOM (the web page elements) directly.
|
|
||||||
MouseEvent: TypeScript definition for mouse-related events like clicks.
|
|
||||||
useEffect: Hook for running side effects (e.g., code that runs when something changes)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// defining a type for the props (inputs) that AuthModal expects
|
|
||||||
interface AuthModalProps {
|
interface AuthModalProps {
|
||||||
isOpen: boolean; // bool for if the modal should be visible
|
isOpen: boolean; // bool for if the modal should be visible
|
||||||
onClose: () => void; //A function that will be executed to close the modal
|
onClose: () => void; //A function that will be executed to close the modal
|
||||||
}
|
}
|
||||||
// creates a React functional component
|
|
||||||
export default function AuthModal({ isOpen, onClose }: AuthModalProps) { //AuthModalProps ensures TypeScript validates the props to match the type definition above
|
|
||||||
const [isLogin, setIsLogin] = useState<boolean>(true);
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isFailed, setIsFailed] = useState<boolean>(false);
|
|
||||||
const [failMessage, setFailMessage] = useState<boolean>(false);
|
|
||||||
/*
|
|
||||||
useState is a React Hook that declares state variables in a functional component. It returns a two-element array
|
|
||||||
The state variable (isLogin) : Represents the current state value (e.g., true initially). This is the value you can use in your component.
|
|
||||||
The state updater function (setIsLogin) : A function that allows you to update the state variable. React takes care of re-rendering the component when the state is updated
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
modalRef allows direct access to the modal DOM element (the container div for the modal). This is useful for detecting if the user clicks outside of the modal
|
|
||||||
*/
|
|
||||||
|
|
||||||
// useEffect runs code after the component renders or when a dependency changes (in this case, isOpen as seen in the end [])
|
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||||||
useEffect(() => {
|
const [isLogin, setIsLogin] = useState<boolean>(true);
|
||||||
if (isOpen) setIsLogin(true); // runs when isOpen changes, if it is true, the login is shown
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
}, [isOpen]);
|
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||||
|
const [failMessage, setFailMessage] = useState<boolean>(false);
|
||||||
|
|
||||||
if (!isOpen) return null; // if is open is false, the model isnt shown
|
useEffect(() => {
|
||||||
|
if (isOpen) setIsLogin(true); // runs when isOpen changes, if it is true, the login is shown
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
// this is an arrow function. e: is used to specify that an event object is expected
|
if (!isOpen) return null; // if is open is false, the model isnt shown
|
||||||
const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
|
|
||||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
.current gives a reference to the actual DOM element (the inner modal container)
|
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||||
e.target refers to the specific element the mouse click event occurred on
|
onClose();
|
||||||
.contains(e.target) checks if the clicked element (e.target) is inside the modal (modalRef.current)
|
}
|
||||||
as Node specifies e.target is a Node type to TypeScript
|
};
|
||||||
? what is a node
|
|
||||||
/!... means we get true if the click is outside the modal
|
|
||||||
Logic : if modalRef.current exists and the click target (e.target) is not inside the modal , then the condition is true
|
|
||||||
This means the user clicked outside the modal
|
|
||||||
*/
|
|
||||||
|
|
||||||
// LS - The following bit contains the more important code for what I'm focused on
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
/*
|
e.preventDefault(); // stops page from refreshing
|
||||||
Note : handleSubmit is typically used as a event handler for submitting a form in react.
|
setIsFailed(false);
|
||||||
For example:
|
const formData = new FormData(e.currentTarget);
|
||||||
<form onSubmit={handleSubmit}>
|
const email = formData.get("email") as string;
|
||||||
<input type="text" name="example" />
|
const password = formData.get("password") as string;
|
||||||
<button type="submit">Submit</button>
|
const name = isLogin ? undefined : (formData.get("name") as string);
|
||||||
</form>
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
e is the parameter passsed into the function
|
|
||||||
async indicates the function runs asyncronously, meaning it performs tasks which take time.
|
|
||||||
an example of this would be API calls
|
|
||||||
*/
|
|
||||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault(); // stops page from refreshing
|
|
||||||
setIsFailed(false)
|
|
||||||
const formData = new FormData(e.currentTarget); // new variable of class FormData is created. This is part of the standard Web API included in modern web browsers
|
|
||||||
const email = formData.get("email") as string; // gets email from form response
|
|
||||||
const password = formData.get("password") as string; // gets password from form response
|
|
||||||
const name = isLogin ? undefined : (formData.get("name") as string);// if the form is in login mode, the name is undefine, otherwise the name is a value obtained from the response
|
|
||||||
|
|
||||||
let endpoint = isLogin ? "/api/login" : "/api/signup"; // sets endpoint for backend code (either sign up or login)
|
try {
|
||||||
const body = isLogin ? { email, password } : { name: name!, email, password };// creates a json body for the backend
|
const res = await axios.post(`/api/${isLogin ? "login" : "signup"}`, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: isLogin ? { email, password } : { name: name!, email, password },
|
||||||
|
});
|
||||||
|
if (res.status) {
|
||||||
|
onClose();
|
||||||
|
} else if (res.status >= 400 && res.status < 500) {
|
||||||
|
console.log("4xx error:", res.data);
|
||||||
|
setFailMessage(res.data.message);
|
||||||
|
setIsFailed(true);
|
||||||
|
} else {
|
||||||
|
console.error("Error:", await res.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Request failed:", error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50" onClick={handleOverlayClick}>
|
||||||
|
<div ref={modalRef} className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsFailed(false);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="absolute text-xl top-0 right-2 text-neutral-500 hover:text-neutral-700"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h2 className="text-2xl font-bold text-center mb-4">{isLogin ? "Login" : "Sign Up"}</h2>
|
||||||
|
|
||||||
try {
|
{/* Form */}
|
||||||
console.log("Sending data to API");
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
const res = await fetch(endpoint, { // sends a request to the server at the end point
|
{!isLogin && (
|
||||||
method: "POST", // Post is used since the form submission modifies the server side state
|
<div>
|
||||||
headers: { "Content-Type": "application/json" }, //indicates it expects a json object returned
|
<label className="block text-sm font-medium text-neutral-700">Full Name</label>
|
||||||
body: JSON.stringify(body), // converts the body to a JSON string to be sent
|
<input
|
||||||
});
|
type="text"
|
||||||
if (res.ok) { //res.ok checks if the response is between 200-299
|
name="name"
|
||||||
console.log("Success!");
|
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
onClose(); // closes UI
|
required={!isLogin}
|
||||||
} else if (res.status >= 400 && res.status <500){
|
/>
|
||||||
const responseBody = await res.json()
|
</div>
|
||||||
console.log("4xx error:", responseBody.message)
|
)}
|
||||||
setFailMessage(responseBody.message)
|
<div>
|
||||||
setIsFailed(true)
|
<label className="block text-sm font-medium text-neutral-700">Email</label>
|
||||||
} else{
|
<input
|
||||||
console.error("Error:", await res.text()); // logs error with error message sent to console
|
type="email"
|
||||||
}
|
name="email"
|
||||||
} catch (error) {// catches any errors (e.g. Not connected to network)
|
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
console.error("Request failed:", error instanceof Error ? error.message : String(error));
|
required
|
||||||
}
|
/>
|
||||||
};
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isFailed && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-red-700">{failMessage}</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="w-full bg-blue-600 text-white p-2 rounded-md hover:bg-blue-700 transition">
|
||||||
|
{isLogin ? "Login" : "Sign Up"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
return (
|
<p className="mt-4 text-center text-sm">
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50" onClick={handleOverlayClick}>
|
{isLogin ? "Need an account?" : "Already have an account?"}{" "}
|
||||||
<div ref={modalRef} className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative">
|
<button onClick={() => setIsLogin(!isLogin)} className="text-blue-600 hover:underline">
|
||||||
<button onClick={() => {setIsFailed(false);onClose();}} className="absolute text-xl top-0 right-2 text-gray-500 hover:text-gray-700" aria-label="Close modal">
|
{isLogin ? "Sign Up" : "Login"}
|
||||||
×
|
</button>
|
||||||
</button>
|
</p>
|
||||||
<h2 className="text-2xl font-bold text-center mb-4">{isLogin ? "Login" : "Sign Up"}</h2>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Form */}
|
);
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
{!isLogin && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">Full Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required={!isLogin}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
className="mt-1 w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{ isFailed && (
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-red-700">{failMessage}</label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<button type="submit" className="w-full bg-blue-600 text-white p-2 rounded-md hover:bg-blue-700 transition">
|
|
||||||
{isLogin ? "Login" : "Sign Up"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p className="mt-4 text-center text-sm">
|
|
||||||
{isLogin ? "Need an account?" : "Already have an account?"}{" "}
|
|
||||||
<button onClick={() => setIsLogin(!isLogin)} className="text-blue-600 hover:underline">
|
|
||||||
{isLogin ? "Sign Up" : "Login"}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,100 +1,101 @@
|
|||||||
import React from "react";
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import React, { Dispatch, SetStateAction } from "react";
|
||||||
import { TbHexagon } from "react-icons/tb";
|
import { TbHexagon } from "react-icons/tb";
|
||||||
|
|
||||||
import Event from "@appTypes/Event";
|
import Event from "@appTypes/Event";
|
||||||
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
logTitle: string;
|
logTitle: string;
|
||||||
logSubtitle: string;
|
logSubtitle: string;
|
||||||
recentsTitle: string;
|
recentsTitle: string;
|
||||||
events: Event[];
|
events: Event[];
|
||||||
selectedEventId: Event["id"];
|
selectedEventId: Event["id"];
|
||||||
setSelectedEventId: Dispatch<SetStateAction<string>>;
|
setSelectedEventId: Dispatch<SetStateAction<string>>;
|
||||||
hoveredEventId: Event["id"];
|
hoveredEventId: Event["id"];
|
||||||
setHoveredEventId: Dispatch<SetStateAction<string>>;
|
setHoveredEventId: Dispatch<SetStateAction<string>>;
|
||||||
button1Name: string;
|
button1Name: string;
|
||||||
button2Name: string;
|
button2Name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MagnitudeNumber({ magnitude }: { magnitude: number }) {
|
function MagnitudeNumber({ magnitude }: { magnitude: number }) {
|
||||||
// Convert magnitude to string with one decimal place
|
// Convert magnitude to string with one decimal place
|
||||||
const magnitudeStr = magnitude.toFixed(1);
|
const magnitudeStr = magnitude.toFixed(1);
|
||||||
const [whole, decimal] = magnitudeStr.split(".");
|
const [whole, decimal] = magnitudeStr.split(".");
|
||||||
|
|
||||||
// Define color based on magnitude (0-10 scale)
|
// Define color based on magnitude (0-10 scale)
|
||||||
return (
|
return (
|
||||||
<div className={`relative`} style={{ color: getMagnitudeColor(magnitude) }}>
|
<div className={`relative`} style={{ color: getMagnitudeColor(magnitude) }}>
|
||||||
<TbHexagon size={40} className="drop-shadow-sm" />
|
<TbHexagon size={40} className="drop-shadow-sm" />
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="flex items-baseline font-mono font-bold tracking-tight">
|
<div className="flex items-baseline font-mono font-bold tracking-tight">
|
||||||
<span className="text-xl -mr-1">{whole}</span>
|
<span className="text-xl -mr-1">{whole}</span>
|
||||||
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
|
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
|
||||||
<span className="text-xs -mr-[1px]">{decimal}</span>
|
<span className="text-xs -mr-[1px]">{decimal}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Sidebar({
|
export default function Sidebar({
|
||||||
logTitle,
|
logTitle,
|
||||||
logSubtitle,
|
logSubtitle,
|
||||||
recentsTitle,
|
recentsTitle,
|
||||||
events,
|
events,
|
||||||
selectedEventId,
|
selectedEventId,
|
||||||
setSelectedEventId,
|
setSelectedEventId,
|
||||||
hoveredEventId,
|
hoveredEventId,
|
||||||
setHoveredEventId,
|
setHoveredEventId,
|
||||||
button1Name,
|
button1Name,
|
||||||
button2Name,
|
button2Name,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col py-6 h-full w-full max-w-xs bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg">
|
<div className={`flex flex-col h-full w-80 relative bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg`}>
|
||||||
<div className="px-6 pb-8 border-b border-neutral-200">
|
<div className="py-6">
|
||||||
<h2 className="text-2xl font-bold text-neutral-800 mb-2">{logTitle}</h2>
|
<div className="px-6 pb-8 border-b border-neutral-200">
|
||||||
<p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p>
|
<h2 className={`text-2xl font-bold text-neutral-800 mb-2`}>{logTitle}</h2>
|
||||||
<Link href="/">
|
<p className="text-sm text-neutral-600 leading-relaxed">{logSubtitle}</p>
|
||||||
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium">
|
|
||||||
{button1Name}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/">
|
|
||||||
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium">
|
|
||||||
{button2Name}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 px-6 pt-6 overflow-y-auto">
|
<Link href="/">
|
||||||
<h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2>
|
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium">
|
||||||
<div className="space-y-3">
|
{button1Name}
|
||||||
{events.map((event) => (
|
</button>
|
||||||
<button
|
</Link>
|
||||||
key={event.title}
|
<Link href="/">
|
||||||
className={`w-full border ${hoveredEventId === event.id ? "bg-neutral-100" : "bg-white"} ${
|
<button className="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors duration-200 font-medium">
|
||||||
selectedEventId === event.id ? "border-neutral-800" : "border-neutral-200"
|
{button2Name}
|
||||||
} rounded-lg p-4 flex items-center gap-4 hover:bg-neutral-100 transition-colors duration-150 shadow-sm text-left`}
|
</button>
|
||||||
onClick={() => {
|
</Link>
|
||||||
setSelectedEventId((prevEventId) => (prevEventId !== event.id ? event.id : ""));
|
</div>
|
||||||
}}
|
|
||||||
onMouseEnter={() => setHoveredEventId(event.id)}
|
<div className="flex-1 px-6 pt-6 overflow-y-auto">
|
||||||
onMouseLeave={() => setHoveredEventId("")}
|
<h2 className="text-xl font-bold text-neutral-800 mb-4">{recentsTitle}</h2>
|
||||||
>
|
<div className="space-y-3">
|
||||||
<div className="flex-1">
|
{events.map((event) => (
|
||||||
<p className="text-sm font-medium text-neutral-800">{event.title}</p>
|
<button
|
||||||
{/* Uncomment if you want to use text1 */}
|
key={event.title}
|
||||||
{/* <p className="text-xs text-neutral-600">{event.text1}</p> */}
|
className={`w-full border ${hoveredEventId === event.id ? "bg-neutral-100" : "bg-white"} ${
|
||||||
<p className="text-xs text-neutral-500 mt-1">{event.text2}</p>
|
selectedEventId === event.id ? "border-neutral-800" : "border-neutral-200"
|
||||||
</div>
|
} rounded-lg p-4 flex items-center gap-4 hover:bg-neutral-100 transition-colors duration-150 shadow-sm text-left`}
|
||||||
{
|
onClick={() => {
|
||||||
(event.magnitude) ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>
|
setSelectedEventId((prevEventId) => (prevEventId !== event.id ? event.id : ""));
|
||||||
}
|
}}
|
||||||
</button>
|
onMouseEnter={() => setHoveredEventId(event.id)}
|
||||||
))}
|
onMouseLeave={() => setHoveredEventId("")}
|
||||||
</div>
|
>
|
||||||
</div>
|
<div className="flex-1">
|
||||||
</div>
|
<p className="text-sm font-medium text-neutral-800">{event.title}</p>
|
||||||
);
|
{/* Uncomment if you want to use text1 */}
|
||||||
|
{/* <p className="text-xs text-neutral-600">{event.text1}</p> */}
|
||||||
|
<p className="text-xs text-neutral-500 mt-1">{event.text2}</p>
|
||||||
|
</div>
|
||||||
|
{event.magnitude ? <MagnitudeNumber magnitude={event.magnitude} /> : <></>}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,191 +1,200 @@
|
|||||||
import { useCallback, useState } from "react";
|
|
||||||
import React, { useRef, useEffect, Dispatch, SetStateAction } from "react";
|
|
||||||
import mapboxgl, { LngLatBounds } from "mapbox-gl";
|
|
||||||
import "mapbox-gl/dist/mapbox-gl.css";
|
import "mapbox-gl/dist/mapbox-gl.css";
|
||||||
import { GiObservatory } from "react-icons/gi";
|
|
||||||
import Event from "@appTypes/Event";
|
import mapboxgl, { LngLatBounds } from "mapbox-gl";
|
||||||
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
|
||||||
import { userAgentFromString } from "next/server";
|
import { userAgentFromString } from "next/server";
|
||||||
|
import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { GiObservatory } from "react-icons/gi";
|
||||||
|
|
||||||
|
import Event from "@appTypes/Event";
|
||||||
|
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
||||||
|
|
||||||
interface MapComponentProps {
|
interface MapComponentProps {
|
||||||
events: Event[];
|
events: Event[];
|
||||||
selectedEventId: Event["id"];
|
selectedEventId: Event["id"];
|
||||||
setSelectedEventId: Dispatch<SetStateAction<string>>;
|
setSelectedEventId: Dispatch<SetStateAction<string>>;
|
||||||
hoveredEventId: Event["id"];
|
hoveredEventId: Event["id"];
|
||||||
setHoveredEventId: Dispatch<SetStateAction<string>>;
|
setHoveredEventId: Dispatch<SetStateAction<string>>;
|
||||||
mapType: String;
|
mapType: String;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map component with location-style pulsing dots, animations, and tooltips
|
// Map component with location-style pulsing dots, animations, and tooltips
|
||||||
function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEventId, setHoveredEventId, mapType }: MapComponentProps) {
|
function MapComponent({
|
||||||
const map = useRef<mapboxgl.Map | null>(null);
|
events,
|
||||||
const markers = useRef<{ [key: string]: mapboxgl.Marker }>({});
|
selectedEventId,
|
||||||
const [mapBounds, setMapBounds] = useState<LngLatBounds>();
|
setSelectedEventId,
|
||||||
|
hoveredEventId,
|
||||||
|
setHoveredEventId,
|
||||||
|
mapType,
|
||||||
|
}: MapComponentProps) {
|
||||||
|
const map = useRef<mapboxgl.Map | null>(null);
|
||||||
|
const markers = useRef<{ [key: string]: mapboxgl.Marker }>({});
|
||||||
|
const [mapBounds, setMapBounds] = useState<LngLatBounds>();
|
||||||
|
|
||||||
const fitToBounds = useCallback((bounds: LngLatBounds) => {
|
const fitToBounds = useCallback((bounds: LngLatBounds) => {
|
||||||
if (map.current && bounds) {
|
if (map.current && bounds) {
|
||||||
map.current!.fitBounds(bounds, { padding: 150, maxZoom: 10 });
|
map.current!.fitBounds(bounds, { padding: 150, maxZoom: 10 });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showPopup = useCallback((eventId: string) => {
|
const showPopup = useCallback((eventId: string) => {
|
||||||
const marker = markers.current[eventId];
|
const marker = markers.current[eventId];
|
||||||
if (marker && map.current) {
|
if (marker && map.current) {
|
||||||
marker.getPopup()?.addTo(map.current);
|
marker.getPopup()?.addTo(map.current);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearAllPopups = useCallback(() => {
|
const clearAllPopups = useCallback(() => {
|
||||||
Object.values(markers.current).forEach((marker) => marker.getPopup()?.remove());
|
Object.values(markers.current).forEach((marker) => marker.getPopup()?.remove());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const flyToEvent = useCallback((event: Event) => {
|
const flyToEvent = useCallback((event: Event) => {
|
||||||
if (map.current) {
|
if (map.current) {
|
||||||
map.current.flyTo({
|
map.current.flyTo({
|
||||||
center: [event.longitude, event.latitude],
|
center: [event.longitude, event.latitude],
|
||||||
zoom: 4,
|
zoom: 4,
|
||||||
speed: 1.5,
|
speed: 1.5,
|
||||||
curve: 1.42,
|
curve: 1.42,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mapboxgl.accessToken = "pk.eyJ1IjoidGltaG93aXR6IiwiYSI6ImNtOGtjcXA5bDA3Ym4ya3NnOWxjbjlxZG8ifQ.6u_KgXEdLTakz910QRAorQ";
|
mapboxgl.accessToken = "pk.eyJ1IjoidGltaG93aXR6IiwiYSI6ImNtOGtjcXA5bDA3Ym4ya3NnOWxjbjlxZG8ifQ.6u_KgXEdLTakz910QRAorQ";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
map.current = new mapboxgl.Map({
|
map.current = new mapboxgl.Map({
|
||||||
container: "map-container",
|
container: "map-container",
|
||||||
style: "mapbox://styles/mapbox/light-v10",
|
style: "mapbox://styles/mapbox/light-v10",
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Map initialization failed:", error);
|
console.error("Map initialization failed:", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
map.current.on("load", () => {
|
map.current.on("load", () => {
|
||||||
// Fit map to bounds
|
// Fit map to bounds
|
||||||
const bounds = new mapboxgl.LngLatBounds();
|
const bounds = new mapboxgl.LngLatBounds();
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
bounds.extend([event.longitude, event.latitude]);
|
bounds.extend([event.longitude, event.latitude]);
|
||||||
});
|
});
|
||||||
fitToBounds(bounds);
|
fitToBounds(bounds);
|
||||||
setMapBounds(bounds);
|
setMapBounds(bounds);
|
||||||
|
|
||||||
// Add markers with location pulse
|
// Add markers with location pulse
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
|
const quakeElement = document.createElement("div");
|
||||||
|
const dotElement = document.createElement("div");
|
||||||
|
const pulseElement = document.createElement("div");
|
||||||
|
|
||||||
const quakeElement = document.createElement("div");
|
if (event.magnitude) {
|
||||||
const dotElement = document.createElement("div");
|
const color = getMagnitudeColor(event.magnitude);
|
||||||
const pulseElement = document.createElement("div");
|
|
||||||
|
|
||||||
if (event.magnitude) {
|
// Create marker container
|
||||||
const color = getMagnitudeColor(event.magnitude);
|
quakeElement.style.width = "50px"; // Increased size to accommodate pulse
|
||||||
|
quakeElement.style.height = "50px";
|
||||||
|
quakeElement.style.position = "absolute";
|
||||||
|
quakeElement.style.display = "flex";
|
||||||
|
quakeElement.style.alignItems = "center";
|
||||||
|
quakeElement.style.justifyContent = "center";
|
||||||
|
|
||||||
// Create marker container
|
// Central dot
|
||||||
quakeElement.style.width = "50px"; // Increased size to accommodate pulse
|
dotElement.style.width = "10px";
|
||||||
quakeElement.style.height = "50px";
|
dotElement.style.height = "10px";
|
||||||
quakeElement.style.position = "absolute";
|
dotElement.style.backgroundColor = color;
|
||||||
quakeElement.style.display = "flex";
|
dotElement.style.borderRadius = "50%";
|
||||||
quakeElement.style.alignItems = "center";
|
dotElement.style.position = "absolute";
|
||||||
quakeElement.style.justifyContent = "center";
|
dotElement.style.zIndex = "2"; // Ensure dot is above pulse
|
||||||
|
|
||||||
// Central dot
|
// Pulsing ring
|
||||||
dotElement.style.width = "10px";
|
pulseElement.className = "location-pulse";
|
||||||
dotElement.style.height = "10px";
|
pulseElement.style.width = "20px"; // Initial size
|
||||||
dotElement.style.backgroundColor = color;
|
pulseElement.style.height = "20px";
|
||||||
dotElement.style.borderRadius = "50%";
|
pulseElement.style.backgroundColor = `${color}80`; // Color with 50% opacity (hex alpha)
|
||||||
dotElement.style.position = "absolute";
|
pulseElement.style.borderRadius = "50%";
|
||||||
dotElement.style.zIndex = "2"; // Ensure dot is above pulse
|
pulseElement.style.position = "absolute";
|
||||||
|
pulseElement.style.zIndex = "1";
|
||||||
|
}
|
||||||
|
|
||||||
// Pulsing ring
|
// Observatory marker
|
||||||
pulseElement.className = "location-pulse";
|
//<GiObservatory />
|
||||||
pulseElement.style.width = "20px"; // Initial size
|
const observatoryElement = document.createElement("div");
|
||||||
pulseElement.style.height = "20px";
|
const root = createRoot(observatoryElement); // `createRoot` is now the standard API
|
||||||
pulseElement.style.backgroundColor = `${color}80`; // Color with 50% opacity (hex alpha)
|
root.render(<GiObservatory className="text-blue-600 text-2xl drop-shadow-lg" />);
|
||||||
pulseElement.style.borderRadius = "50%";
|
|
||||||
pulseElement.style.position = "absolute";
|
|
||||||
pulseElement.style.zIndex = "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observatory marker
|
quakeElement.appendChild(pulseElement);
|
||||||
//<GiObservatory />
|
quakeElement.appendChild(dotElement);
|
||||||
const observatoryElement = document.createElement("div");
|
|
||||||
const root = createRoot(observatoryElement); // `createRoot` is now the standard API
|
|
||||||
root.render(
|
|
||||||
<GiObservatory className="text-blue-600 text-2xl drop-shadow-lg" />
|
|
||||||
);
|
|
||||||
|
|
||||||
quakeElement.appendChild(pulseElement);
|
const marker = new mapboxgl.Marker({ element: mapType === "observatories" ? observatoryElement : quakeElement })
|
||||||
quakeElement.appendChild(dotElement);
|
.setLngLat([event.longitude, event.latitude])
|
||||||
|
.addTo(map.current!);
|
||||||
|
|
||||||
const marker = new mapboxgl.Marker({ element: mapType === "observatories" ? observatoryElement : quakeElement }).setLngLat([event.longitude, event.latitude]).addTo(map.current!);
|
const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(`
|
||||||
|
|
||||||
const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(`
|
|
||||||
<div>
|
<div>
|
||||||
<h3>${event.title}</h3>
|
<h3>${event.title}</h3>
|
||||||
${ mapType === "observatories" ? `<p>${event.text1}</p>` : `<p>Magnitude: ${event.magnitude}</p>`}
|
${mapType === "observatories" ? `<p>${event.text1}</p>` : `<p>Magnitude: ${event.magnitude}</p>`}
|
||||||
<p>${event.text2}</p>
|
<p>${event.text2}</p>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
marker.setPopup(popup);
|
marker.setPopup(popup);
|
||||||
markers.current[event.id] = marker;
|
markers.current[event.id] = marker;
|
||||||
|
|
||||||
// Add hover events
|
// Add hover events
|
||||||
const markerDomElement = marker.getElement();
|
const markerDomElement = marker.getElement();
|
||||||
markerDomElement.style.cursor = "pointer"; // Optional: indicate interactivity
|
markerDomElement.style.cursor = "pointer"; // Optional: indicate interactivity
|
||||||
|
|
||||||
markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id));
|
markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id));
|
||||||
markerDomElement.addEventListener("mouseleave", () => setHoveredEventId(""));
|
markerDomElement.addEventListener("mouseleave", () => setHoveredEventId(""));
|
||||||
markerDomElement.addEventListener("click", () => setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id)));
|
markerDomElement.addEventListener("click", () =>
|
||||||
|
setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id))
|
||||||
|
);
|
||||||
|
|
||||||
// Cleanup event listeners on unmount
|
// Cleanup event listeners on unmount
|
||||||
markerDomElement.dataset.listenersAdded = "true"; // Mark for cleanup
|
markerDomElement.dataset.listenersAdded = "true"; // Mark for cleanup
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
map.current.on("error", (e) => {
|
map.current.on("error", (e) => {
|
||||||
console.error("Mapbox error:", e);
|
console.error("Mapbox error:", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
map.current?.remove();
|
map.current?.remove();
|
||||||
};
|
};
|
||||||
}, [events, setSelectedEventId, setHoveredEventId, fitToBounds]);
|
}, [events, setSelectedEventId, setHoveredEventId, fitToBounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const event = events.find((x) => x.id === selectedEventId);
|
const event = events.find((x) => x.id === selectedEventId);
|
||||||
if (event) flyToEvent(event);
|
if (event) flyToEvent(event);
|
||||||
else if (!selectedEventId) {
|
else if (!selectedEventId) {
|
||||||
if (mapBounds) fitToBounds(mapBounds);
|
if (mapBounds) fitToBounds(mapBounds);
|
||||||
}
|
}
|
||||||
}, [events, selectedEventId, mapBounds, fitToBounds, clearAllPopups, flyToEvent]);
|
}, [events, selectedEventId, mapBounds, fitToBounds, clearAllPopups, flyToEvent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Clear all popups first
|
// Clear all popups first
|
||||||
clearAllPopups();
|
clearAllPopups();
|
||||||
|
|
||||||
// Handle both events if they exist and are different
|
// Handle both events if they exist and are different
|
||||||
if (hoveredEventId && selectedEventId && hoveredEventId !== selectedEventId) {
|
if (hoveredEventId && selectedEventId && hoveredEventId !== selectedEventId) {
|
||||||
showPopup(hoveredEventId);
|
showPopup(hoveredEventId);
|
||||||
showPopup(selectedEventId);
|
showPopup(selectedEventId);
|
||||||
}
|
}
|
||||||
// Handle single event case (either hovered or selected)
|
// Handle single event case (either hovered or selected)
|
||||||
else if (hoveredEventId || selectedEventId) {
|
else if (hoveredEventId || selectedEventId) {
|
||||||
showPopup(hoveredEventId || selectedEventId);
|
showPopup(hoveredEventId || selectedEventId);
|
||||||
}
|
}
|
||||||
}, [hoveredEventId, selectedEventId, clearAllPopups, showPopup]);
|
}, [hoveredEventId, selectedEventId, clearAllPopups, showPopup]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
<div id="map-container" className="w-full h-full" />
|
<div id="map-container" className="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSS for location-style pulsing animation
|
// CSS for location-style pulsing animation
|
||||||
@ -211,10 +220,10 @@ const pulseStyles = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Map(props: MapComponentProps) {
|
export default function Map(props: MapComponentProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style>{pulseStyles}</style>
|
<style>{pulseStyles}</style>
|
||||||
<MapComponent {...props} />
|
<MapComponent {...props} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,75 +1,118 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import AuthModal from "@components/AuthModal";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useMemo } from "react";
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||||
import { FaRegUserCircle } from "react-icons/fa";
|
import { FaRegUserCircle } from "react-icons/fa";
|
||||||
|
|
||||||
function NavbarButton({ name, href, dropdownItems }: { name: string; href: string; dropdownItems?: string[] }) {
|
import { Currency } from "@appTypes/StoreModel";
|
||||||
const pathname = usePathname();
|
import AuthModal from "@components/AuthModal";
|
||||||
const isActive = dropdownItems ? dropdownItems.some((item) => pathname === `/${item.toLowerCase().replace(" ", "-")}`) : pathname === href;
|
import { useStoreActions, useStoreState } from "@hooks/store";
|
||||||
|
|
||||||
return (
|
export default function Navbar({}: // currencySelector,
|
||||||
<button className="flex items-center justify-center px-2 py-4 relative group">
|
{
|
||||||
{dropdownItems ? (
|
// currencySelector?: { selectedCurrency: string; setSelectedCurrency: Dispatch<SetStateAction<"GBP" | "USD" | "EUR">> };
|
||||||
<span className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "group-hover:bg-neutral-200"}`}>{name}</span>
|
}) {
|
||||||
) : (
|
const pathname = usePathname();
|
||||||
<Link href={href} className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "hover:bg-neutral-200"}`}>
|
const selectedCurrency = useStoreState((state) => state.currency.selectedCurrency);
|
||||||
{name}
|
const setSelectedCurrency = useStoreActions((actions) => actions.currency.setSelectedCurrency);
|
||||||
</Link>
|
const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Warehouse", "Shop"], []);
|
||||||
)}
|
// const navOptions = useMemo(() => ["Earthquakes"], []);
|
||||||
{dropdownItems && (
|
const aboutDropdown = ["Contact Us", "Our Mission", "The Team"];
|
||||||
<div className="absolute hidden group-hover:block top-full left-1/2 -translate-x-1/2 w-40 bg-white border border-neutral-300 rounded-lg overflow-hidden shadow-lg z-40">
|
// { label: "Our Mission", path: "/our-mission" },
|
||||||
<ul>
|
// { label: "The Team", path: "/the-team" },
|
||||||
{dropdownItems.map((item) => {
|
// { label: "Contact Us", path: "/contact-us" }]
|
||||||
const itemHref = `/${item.toLowerCase().replace(" ", "-")}`;
|
|
||||||
const isDropdownActive = pathname === itemHref;
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
return (
|
|
||||||
<li key={item}>
|
const currencies = useStoreState((state) => state.currency.currencies);
|
||||||
<Link href={itemHref} className={`block px-4 py-2 hover:bg-neutral-100 ${isDropdownActive ? "bg-neutral-100" : ""}`}>
|
const currencyTickers = useStoreState((state) => state.currency.tickers);
|
||||||
{item}
|
|
||||||
</Link>
|
function NavbarButton({ name, href, dropdownItems }: { name: string; href: string; dropdownItems?: string[] }) {
|
||||||
</li>
|
const isActive = dropdownItems
|
||||||
);
|
? dropdownItems.some((item) => pathname === `/${item.toLowerCase().replace(" ", "-")}`)
|
||||||
})}
|
: pathname === href;
|
||||||
</ul>
|
|
||||||
</div>
|
return (
|
||||||
)}
|
<button className="flex items-center justify-center px-2 py-4 relative group">
|
||||||
</button>
|
{dropdownItems ? (
|
||||||
);
|
<span
|
||||||
}
|
className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "group-hover:bg-neutral-200"}`}
|
||||||
|
>
|
||||||
export default function Navbar() {
|
{name}
|
||||||
const navOptions = useMemo(() => ["Earthquakes", "Observatories", "Warehouse", "Shop"], []);
|
</span>
|
||||||
// const navOptions = useMemo(() => ["Earthquakes"], []);
|
) : (
|
||||||
const aboutDropdown = ["Contact Us", "Our Mission", "The Team"];
|
<Link
|
||||||
// { label: "Our Mission", path: "/our-mission" },
|
href={href}
|
||||||
// { label: "The Team", path: "/the-team" },
|
className={`px-4 py-1.5 rounded-md transition-colors ${isActive ? "bg-neutral-200" : "hover:bg-neutral-200"}`}
|
||||||
// { label: "Contact Us", path: "/contact-us" }]
|
>
|
||||||
|
{name}
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
</Link>
|
||||||
|
)}
|
||||||
return (
|
{dropdownItems && (
|
||||||
<div className="flex sticky top-0 w-full h-14 z-40 font-medium bg-white border border-b-neutral-200">
|
<div className="absolute hidden group-hover:block top-full left-1/2 -translate-x-1/2 w-40 bg-white border border-neutral-300 rounded-lg overflow-hidden shadow-lg z-40">
|
||||||
<div className="my-1 flex aspect-square ml-3 mr-3">
|
<ul>
|
||||||
<Link href="/" className="rounded-full">
|
{dropdownItems.map((item) => {
|
||||||
<Image height={50} width={50} alt="Logo" className="border border-neutral-300 rounded-full" src="/logo.png" />
|
const itemHref = `/${item.toLowerCase().replace(" ", "-")}`;
|
||||||
</Link>
|
const isDropdownActive = pathname === itemHref;
|
||||||
</div>
|
return (
|
||||||
<div className="flex">
|
<li key={item}>
|
||||||
{navOptions.map((name) => (
|
<Link
|
||||||
<NavbarButton name={name} href={`/${name.toLowerCase()}`} key={name} />
|
href={itemHref}
|
||||||
))}
|
className={`block px-4 py-2 hover:bg-neutral-100 ${isDropdownActive ? "bg-neutral-100" : ""}`}
|
||||||
<NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} />
|
>
|
||||||
</div>
|
{item}
|
||||||
<div className="flex-grow" />
|
</Link>
|
||||||
<button className="my-auto mr-4" onClick={() => setIsModalOpen(true)}>
|
</li>
|
||||||
<FaRegUserCircle size={22} />
|
);
|
||||||
</button>
|
})}
|
||||||
<AuthModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
</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">
|
||||||
|
<Link href="/" className="rounded-full">
|
||||||
|
<Image height={50} width={50} alt="Logo" className="border border-neutral-300 rounded-full" src="/logo.png" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{navOptions.map((name) => (
|
||||||
|
<NavbarButton name={name} href={`/${name.toLowerCase()}`} key={name} />
|
||||||
|
))}
|
||||||
|
<NavbarButton name="About Us" href="/about" dropdownItems={aboutDropdown} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow" />
|
||||||
|
|
||||||
|
{pathname.includes("shop") && (
|
||||||
|
<button className="flex items-center justify-center mr-3 py-4 relative group">
|
||||||
|
<span className={`px-4 py-1.5 rounded-md transition-colors`}>{selectedCurrency}</span>
|
||||||
|
|
||||||
|
<div className="absolute hidden group-hover:block top-full left-1/2 -translate-x-1/2 w-24 bg-white border border-neutral-300 rounded-lg overflow-hidden shadow-lg z-40">
|
||||||
|
<ul>
|
||||||
|
{currencies.map((item) => {
|
||||||
|
let ticker = currencyTickers[item];
|
||||||
|
return (
|
||||||
|
<li key={item} onClick={() => setSelectedCurrency(item)}>
|
||||||
|
<div className={`block px-2 py-2 hover:bg-neutral-100 text-md`}>
|
||||||
|
{item} ({ticker})
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className="my-auto mr-4" onClick={() => setIsModalOpen(true)}>
|
||||||
|
<FaRegUserCircle size={22} />
|
||||||
|
</button>
|
||||||
|
<AuthModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +1,42 @@
|
|||||||
import React from 'react';
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen w-64 bg-gray-400 text-white border-l border-gray-700">
|
<div className="flex flex-col h-screen w-64 bg-neutral-400 text-white border-l border-neutral-700">
|
||||||
<div className="flex flex-col p-4 border-b border-gray-700">
|
<div className="flex flex-col p-4 border-b border-neutral-700">
|
||||||
<h2 className="text-xl font-semibold mb-2">Log an Earthquake</h2>
|
<h2 className="text-xl font-semibold mb-2">Log an Earthquake</h2>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-neutral-700">
|
||||||
Record new earthquakes - time/date, location, magnitude, observatory and scientists
|
Record new earthquakes - time/date, location, magnitude, observatory and scientists
|
||||||
</p>
|
</p>
|
||||||
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
|
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
|
||||||
<Link href="/">
|
<Link href="/">Log Event</Link>
|
||||||
Log Event
|
</button>
|
||||||
</Link>
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section: Recent Events - Will need to be replaced with a link to the database*/}
|
{/* Section: Recent Events - Will need to be replaced with a link to the database*/}
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 p-4">
|
||||||
<h2 className="text-xl font-semibold mb-2">Recent Events</h2>
|
<h2 className="text-xl font-semibold mb-2">Recent Events</h2>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600">
|
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
|
||||||
<p className="text-sm">Earthquake in California</p>
|
<p className="text-sm">Earthquake in California</p>
|
||||||
<p className="text-xs text-gray-300">Magnitude 5.3</p>
|
<p className="text-xs text-neutral-300">Magnitude 5.3</p>
|
||||||
<p className="text-xs text-gray-400">2 hours ago</p>
|
<p className="text-xs text-neutral-400">2 hours ago</p>
|
||||||
</li>
|
</li>
|
||||||
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600">
|
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
|
||||||
<p className="text-sm">Tremor in Japan</p>
|
<p className="text-sm">Tremor in Japan</p>
|
||||||
<p className="text-xs text-gray-300">Magnitude 4.7</p>
|
<p className="text-xs text-neutral-300">Magnitude 4.7</p>
|
||||||
<p className="text-xs text-gray-400">5 hours ago</p>
|
<p className="text-xs text-neutral-400">5 hours ago</p>
|
||||||
</li>
|
</li>
|
||||||
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600">
|
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
|
||||||
<p className="text-sm">Tremor in Spain</p>
|
<p className="text-sm">Tremor in Spain</p>
|
||||||
<p className="text-xs text-gray-300">Magnitude 2.1</p>
|
<p className="text-xs text-neutral-300">Magnitude 2.1</p>
|
||||||
<p className="text-xs text-gray-400">10 hours ago</p>
|
<p className="text-xs text-neutral-400">10 hours ago</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|||||||
@ -1,44 +1,40 @@
|
|||||||
import React from 'react';
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen w-64 bg-gray-400 text-white border-l border-gray-700">
|
<div className="flex flex-col h-screen w-64 bg-neutral-400 text-white border-l border-neutral-700">
|
||||||
<div className="flex flex-col p-4 border-b border-gray-700">
|
<div className="flex flex-col p-4 border-b border-neutral-700">
|
||||||
<h2 className="text-xl font-semibold mb-2">Observatories</h2>
|
<h2 className="text-xl font-semibold mb-2">Observatories</h2>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-neutral-700">Observatory events - location, scientists, recent earthquakes</p>
|
||||||
Observatory events - location, scientists, recent earthquakes
|
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
|
||||||
</p>
|
<Link href="/">Observatory News</Link>
|
||||||
<button className="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded">
|
</button>
|
||||||
<Link href="/">
|
</div>
|
||||||
Observatory News
|
|
||||||
</Link>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section: Recent Events - Will need to be replaced with a link to the database*/}
|
{/* Section: Recent Events - Will need to be replaced with a link to the database*/}
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 p-4">
|
||||||
<h2 className="text-xl font-semibold mb-2">Recent Observatory Events</h2>
|
<h2 className="text-xl font-semibold mb-2">Recent Observatory Events</h2>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600">
|
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
|
||||||
<p className="text-sm">Earthquake in California</p>
|
<p className="text-sm">Earthquake in California</p>
|
||||||
<p className="text-xs text-gray-300">Magnitude 5.3</p>
|
<p className="text-xs text-neutral-300">Magnitude 5.3</p>
|
||||||
<p className="text-xs text-gray-400">Cali Observatory</p>
|
<p className="text-xs text-neutral-400">Cali Observatory</p>
|
||||||
</li>
|
</li>
|
||||||
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600">
|
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
|
||||||
<p className="text-sm">Tremor in Japan</p>
|
<p className="text-sm">Tremor in Japan</p>
|
||||||
<p className="text-xs text-gray-300">Magnitude 4.7</p>
|
<p className="text-xs text-neutral-300">Magnitude 4.7</p>
|
||||||
<p className="text-xs text-gray-400">Kyoto Observatory</p>
|
<p className="text-xs text-neutral-400">Kyoto Observatory</p>
|
||||||
</li>
|
</li>
|
||||||
<li className="bg-gray-700 p-3 rounded hover:bg-gray-600">
|
<li className="bg-neutral-700 p-3 rounded hover:bg-neutral-600">
|
||||||
<p className="text-sm">Tremor in Spain</p>
|
<p className="text-sm">Tremor in Spain</p>
|
||||||
<p className="text-xs text-gray-300">Magnitude 2.1</p>
|
<p className="text-xs text-neutral-300">Magnitude 2.1</p>
|
||||||
<p className="text-xs text-gray-400">Madrid Observatory</p>
|
<p className="text-xs text-neutral-400">Madrid Observatory</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|||||||
@ -1,5 +1,2 @@
|
|||||||
name,email,password
|
name,email,password,level
|
||||||
Lukeshan Thananchayan,lukeshan@mail.com,TaylorSwift123
|
Lukeshan Thananchayan,lukeshan@mail.com,$2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge,basic
|
||||||
Emily Neighbour Carter,emily@mail.com,HarryStyles000
|
|
||||||
Izzy Patteron,izzy@mail.com,OliviaRodrigez420
|
|
||||||
Tim Howitz,tim@mail.com,TravisBarker182
|
|
||||||
|
@ -1,67 +1,66 @@
|
|||||||
// Datasource configuration
|
// Datasource configuration
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlserver"
|
provider = "sqlserver"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
// User model
|
// User model
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
email String @unique
|
email String @unique
|
||||||
passwordHash String
|
passwordHash String
|
||||||
firstname String
|
name String
|
||||||
surname String
|
role String @default("USER") @db.VarChar(10)
|
||||||
role String @default("USER") @db.VarChar(10)
|
scientist Scientist? @relation // Optional relation to Scientist
|
||||||
scientist Scientist? @relation // Optional relation to Scientist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Earthquake model
|
// Earthquake model
|
||||||
model Earthquake {
|
model Earthquake {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
date DateTime
|
date DateTime
|
||||||
location String
|
location String
|
||||||
magnitude Float
|
magnitude Float
|
||||||
depth Float
|
depth Float
|
||||||
casualties Int
|
casualties Int
|
||||||
creatorId Int? // Creator's ID (Foreign Key referencing Scientist)
|
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
|
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Points back to Scientist
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observatory model
|
// Observatory model
|
||||||
model Observatory {
|
model Observatory {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
location String
|
location String
|
||||||
longitude String
|
longitude String
|
||||||
latitude String
|
latitude String
|
||||||
dateEstablished Int?
|
dateEstablished Int?
|
||||||
functional Boolean
|
functional Boolean
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
creatorId Int? // Creator's ID (Foreign Key referencing Scientist)
|
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
|
creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Points back to Scientist
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scientist model
|
// Scientist model
|
||||||
model Scientist {
|
model Scientist {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
level String @db.VarChar(10) // Junior or Senior
|
level String @db.VarChar(10) // Junior or Senior
|
||||||
user User @relation(fields: [userId], references: [id]) // Relates to the User model
|
user User @relation(fields: [userId], references: [id]) // Relates to the User model
|
||||||
userId Int @unique
|
userId Int @unique
|
||||||
|
|
||||||
// Self-referencing relation: superior and subordinates
|
// Self-referencing relation: superior and subordinates
|
||||||
superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Parent scientist
|
superior Scientist? @relation("SuperiorRelation", fields: [superiorId], references: [id], onDelete: NoAction, onUpdate: NoAction) // Parent scientist
|
||||||
superiorId Int?
|
superiorId Int?
|
||||||
subordinates Scientist[] @relation("SuperiorRelation") // Scientists who view this one as a superior
|
subordinates Scientist[] @relation("SuperiorRelation") // Scientists who view this one as a superior
|
||||||
|
|
||||||
// Earthquake and Observatory relations
|
// Earthquake and Observatory relations
|
||||||
earthquakes Earthquake[] @relation("ScientistEarthquakeCreator") // Scientists can create earthquakes
|
earthquakes Earthquake[] @relation("ScientistEarthquakeCreator") // Scientists can create earthquakes
|
||||||
observatories Observatory[] @relation("ScientistObservatoryCreator") // Scientists can create observatories
|
observatories Observatory[] @relation("ScientistObservatoryCreator") // Scientists can create observatories
|
||||||
}
|
}
|
||||||
10
src/hooks/store.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"use client";
|
||||||
|
import { createTypedHooks } from "easy-peasy";
|
||||||
|
|
||||||
|
import { StoreModel } from "@appTypes/StoreModel";
|
||||||
|
|
||||||
|
export const typedHooks = createTypedHooks<StoreModel>();
|
||||||
|
|
||||||
|
export const useStoreActions = typedHooks.useStoreActions;
|
||||||
|
export const useStoreDispatch = typedHooks.useStoreDispatch;
|
||||||
|
export const useStoreState = typedHooks.useStoreState;
|
||||||
11
src/types/Artifact.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
interface Artifact {
|
||||||
|
// todo change to string
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
location: string;
|
||||||
|
image: string;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Artifact;
|
||||||
@ -1,11 +1,11 @@
|
|||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
magnitude?: number;
|
magnitude?: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
latitude: number;
|
latitude: number;
|
||||||
text1: string;
|
text1: string;
|
||||||
text2: string;
|
text2: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Event;
|
export default Event;
|
||||||
|
|||||||
17
src/types/StoreModel.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Action } from "easy-peasy";
|
||||||
|
|
||||||
|
type Currency = "GBP" | "USD" | "EUR";
|
||||||
|
|
||||||
|
interface CurrencyModel {
|
||||||
|
selectedCurrency: Currency;
|
||||||
|
setSelectedCurrency: Action<CurrencyModel, Currency>;
|
||||||
|
currencies: Currency[];
|
||||||
|
conversionRates: Record<Currency, number>;
|
||||||
|
tickers: Record<Currency, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoreModel {
|
||||||
|
currency: CurrencyModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { StoreModel, Currency };
|
||||||
3
src/utils/fetcher.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const fetcher = (url: string) => axios.get(url).then((res) => res.data);
|
||||||
@ -1,20 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import resolveConfig from "tailwindcss/resolveConfig";
|
import resolveConfig from "tailwindcss/resolveConfig";
|
||||||
|
|
||||||
import tailwindConfig from "../../tailwind.config";
|
import tailwindConfig from "../../tailwind.config";
|
||||||
|
|
||||||
function getMagnitudeColour(magnitude: number) {
|
function getMagnitudeColour(magnitude: number) {
|
||||||
const fullConfig = resolveConfig(tailwindConfig);
|
const fullConfig = resolveConfig(tailwindConfig);
|
||||||
const colors = fullConfig.theme.colors;
|
const colors = fullConfig.theme.colors;
|
||||||
|
|
||||||
if (magnitude >= 7) {
|
if (magnitude >= 7) {
|
||||||
return colors["red"][600];
|
return colors["red"][600];
|
||||||
} else if (magnitude >= 5) {
|
} else if (magnitude >= 5) {
|
||||||
return colors["orange"][500];
|
return colors["orange"][500];
|
||||||
} else if (magnitude >= 3) {
|
} else if (magnitude >= 3) {
|
||||||
return colors["amber"][500];
|
return colors["amber"][500];
|
||||||
} else {
|
} else {
|
||||||
return colors["blue"][400];
|
return colors["blue"][400];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getMagnitudeColour;
|
export default getMagnitudeColour;
|
||||||
|
|||||||
@ -20,11 +20,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@components/*": ["./src/components/*"],
|
||||||
"@components/*": ["./src/components/*"],
|
"@hooks/*": ["./src/hooks/*"],
|
||||||
"@hooks/*": ["./src/hooks/*"],
|
"@utils/*": ["./src/utils/*"],
|
||||||
"@utils/*": ["./src/utils/*"],
|
"@appTypes/*": ["./src/types/*"],
|
||||||
"@appTypes/*": ["./src/types/*"]
|
"@/*": ["./src/*"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
|||||||