Compare commits

..

No commits in common. "3b2927e896222449a55b0a0cc325cfac29b67b10" and "31a0c622d5fa41e3dcb87bba6ce591139422b592" have entirely different histories.

35 changed files with 4270 additions and 2694 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
node_modules node_modules
.next .next
generated

View File

@ -55,7 +55,6 @@
}, },
"importSorter.generalConfiguration.sortOnBeforeSave": true, "importSorter.generalConfiguration.sortOnBeforeSave": true,
"cSpell.words": [ "cSpell.words": [
"prismaclient",
"vars" "vars"
] ]
} }

1140
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@types/bcryptjs": "^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",

View File

@ -1,13 +1,9 @@
// Datasource configuration
datasource db { datasource db {
provider = "sqlserver" provider = "sqlserver"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma/client"
}
// User model // User model
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
@ -15,19 +11,9 @@ model User {
name String name String
email String @unique email String @unique
passwordHash String passwordHash String
role String @default("GUEST") @db.VarChar(10) // Allowed: ADMIN, SCIENTIST, GUEST role String @default("GUEST") @db.VarChar(10) // ADMIN, SCIENTIST, GUEST
scientist Scientist? @relation scientist Scientist? @relation
purchasedArtefacts Artefact[] @relation("UserPurchasedArtefacts") purchasedArtefacts Artefact[] @relation("UserPurchasedArtefacts")
requests Request[] @relation("UserRequests")
}
model Request {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
requestType String @db.VarChar(20) // Allowed: NEW_USER, CHANGE_LEVEL, DELETE
requestingUser User @relation("UserRequests", fields: [requestingUserId], references: [id])
requestingUserId Int
outcome String @default("IN_PROGRESS") @db.VarChar(20) // Allowed: FULFILLED, REJECTED, IN_PROGRESS, CANCELLED, OTHER
} }
// Scientist model // Scientist model
@ -46,27 +32,24 @@ model Scientist {
artefacts Artefact[] @relation("ScientistArtefactCreator") artefacts Artefact[] @relation("ScientistArtefactCreator")
} }
// 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
date DateTime date DateTime
code String @unique
magnitude Float
type String // e.g. 'volcanic'
latitude Float
longitude Float
location String location String
depth String latitude String
longitude String
magnitude Float
depth Float
creatorId Int? creatorId Int?
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id]) creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
artefacts Artefact[] artefacts Artefact[]
observatories Observatory[] @relation("EarthquakeObservatory") observatories Observatory[] @relation("EarthquakeObservatory")
} }
// Observatory model
model Observatory { model Observatory {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -76,38 +59,27 @@ model Observatory {
longitude String longitude String
latitude String latitude String
dateEstablished Int? dateEstablished Int?
isFunctional Boolean functional Boolean
seismicSensorOnline Boolean @default(true) seismicSensorOnline Boolean @default(true)
creatorId Int? creatorId Int?
creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) creator Scientist? @relation("ScientistObservatoryCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
earthquakes Earthquake[] @relation("EarthquakeObservatory") earthquakes Earthquake[] @relation("EarthquakeObservatory")
} }
// Artefact model
model Artefact { model Artefact {
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
type String @db.VarChar(50) // Lava, Tephra, Ash, Soil type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
warehouseArea String warehouseArea String // Examples: "ZoneA-Shelf1", "ZoneB-Rack2", "ZoneC-Bin3"
description String
earthquakeId Int earthquakeId Int
earthquake Earthquake @relation(fields: [earthquakeId], references: [id]) earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
creatorId Int? creatorId Int?
creator Scientist? @relation("ScientistArtefactCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction) creator Scientist? @relation("ScientistArtefactCreator", fields: [creatorId], references: [id], onDelete: NoAction, onUpdate: NoAction)
isRequired Boolean @default(true) required Boolean @default(true)
dateAddedToShop DateTime? shopPrice Float? // In Euros
shopPrice Float?
isSold Boolean @default(false)
purchasedById Int? purchasedById Int?
purchasedBy User? @relation("UserPurchasedArtefacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction) purchasedBy User? @relation("UserPurchasedArtefacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction)
isCollected Boolean @default(false) pickedUp Boolean @default(false)
}
model Pallet {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
warehouseArea String
palletNote String
} }

View File

@ -1,31 +0,0 @@
Name,Type,WarehouseArea,Description,earthquakeID,Price,Required,PickedUp,Picture
Echo Bomb,Lava,ShelvingAreaA,A dense glossy black volcanic bomb with minor vesicles.,EV-7.4-Mexico-00035,120,no,no,EchoBomb.PNG
Silvershade Ash,Ash,ShelvingAreaD,Fine light-grey volcanic ash collected near a village.,EV-6.0-Iceland-00018,40,no,no,SilvershadeAsh.PNG
Strata Core,Soil,LoggingArea,Soil core with visible stratification showing evidence of liquefaction.,ET-6.9-Brazil-00046,30,no,no,StrataCore.PNG
Opal Clast,Tephra,ShelvingAreaM,Pumice clast with sharp edges and clear layering.,EV-8.3-Iceland-00127,65,no,no,OpalClast.PNG
Magnetite Ash,Ash,PalletDeliveryArea,Bagged black ash sample with high magnetic content.,EV-5.3-Myanmar-00088,28,yes,no,MagnetiteAsh.PNG
Ropeform Fragment,Lava,ShelvingAreaR,Ropey pahoehoe lava fragment with preserved ripples.,EV-6.9-Ethiopia-00012,85,no,no,NoImageFound.PNG
Glassy Bed,Soil,ShelvingAreaB,Sandy soil deposit with glassy volcanic spherules visible under microscope.,EV-7.8-USA-00167,48,no,no,NoImageFound.PNG
Groundwater Shard,Tephra,ShelvingAreaJ,Layered tephra shard showing interaction with groundwater.,EV-4.9-Chile-00083,49,no,no,NoImageFound.PNG
Sulfur Ghost,Ash,ShelvingAreaG,Grey volcanic ash particle with very high sulfur content.,EV-6.4-Armenia-00219,38,no,no,NoImageFound.PNG
Obsidian Pillar,Lava,ShelvingAreaC,Dense basalt sample with well-developed columnar structure.,EV-7.7-Jordan-00037,100,yes,no,NoImageFound.PNG
Peat Nest,Soil,ShelvingAreaP,Peaty soil from earthquake liquefaction area with organic inclusions.,ET-5.1-New Zealand-00032,52,no,no,NoImageFound.PNG
Biotite Veil,Ash,PalletDeliveryArea,Ash with visible biotite flakes: lightly compacted.,EV-3.1-USA-00101,32,no,no,NoImageFound.PNG
Foamcore Pumice,Lava,ShelvingAreaF,High-porosity pumice formed during an explosive event.,EV-8.4-Barbados-00071,77,no,no,NoImageFound.PNG
Redslide Soil,Soil,ShelvingAreaQ,Aggregated red clay soil collected from a collapsed hillside.,EC-7.5-Nicaragua-00078,26,no,no,NoImageFound.PNG
Spectrum Clast,Tephra,LoggingArea,Multi-coloured tephra clast showing mixed eruption sources.,EV-8.3-Antarctica-00030,54,no,no,NoImageFound.PNG
Olivine Stone,Lava,ShelvingAreaH,Dense polished lava with visible olivine inclusions.,EV-9.0-Russia-00171,132,no,no,NoImageFound.PNG
Blended Bar,Soil,ShelvingAreaN,Sandy soil deeply mixed with volcanic tephra fragments.,EV-6.4-Panama-00197,45,no,no,NoImageFound.PNG
Potash Powder,Ash,ShelvingAreaX,Very fine mantle ash with high potassium content: glassy texture.,EV-4.4-Ecuador-00253,56,no,no,NoImageFound.PNG
Shatterrock,Lava,ShelvingAreaS,Massive hand specimen of aa lava showing sharp vesicular cavities.,EV-7.4-South Africa-00228,117,no,no,NoImageFound.PNG
Humic Loam,Soil,PalletDeliveryArea,Dark humic soil showing traces of ash fallout.,EV-3.2-Argentina-00175,20,no,no,NoImageFound.PNG
Charcoal Drift,Ash,ShelvingAreaK,Coarse ash mixed with fragmented charcoal.,EV-8.6-Myanmar-00056,47,yes,no,NoImageFound.PNG
Spindle Bomb,Lava,ShelvingAreaU,Spindle-shaped lava bomb: partially vitrified.,EV-8.9-Japan-00076,84,no,no,NoImageFound.PNG
Bubblewall Tephra,Tephra,ShelvingAreaV,Vitric tephra particle with bubble wall shards: transparent edges.,EV-7.5-Greece-00248,88,no,no,NoImageFound.PNG
Layercake Earth,Soil,ShelvingAreaZ,Compacted soil with tephra lenses: showing sediment mixing.,EV-6.0-Guatemala-00165,39,no,no,NoImageFound.PNG
Creamdust Sample,Ash,ShelvingAreaW,Bagged cream-coloured ash collected from roof dusting incident.,EV-4.1-Maldives-00185,25,no,no,NoImageFound.PNG
Faultblock Tephra,Tephra,ShelvingAreaL,Blocky tephra fragment: impacted during ground rupture.,EV-5.6-Antarctica-00251,51,no,no,NoImageFound.PNG
Basalt Boulder,Lava,LoggingArea,Weathered basalt boulder from lava flow front.,EV-5.4-Spain-00129,69,no,no,NoImageFound.PNG
Glassloam,Soil,ShelvingAreaT,Loam with embedded tephric glass and altered feldspar.,EV-3.7-Argentina-00208,30,no,no,NoImageFound.PNG
Nanoshade Ash,Ash,ShelvingAreaO,Ash sample with nano-crystalline silica.,EV-7.4-Mauritius-00183,42,no,no,NoImageFound.PNG
Ironflame Scoria,Lava,ShelvingAreaE,Vesicular scoria: deep red: indicative of high iron content.,EV-3.4-Indonesia-00138,44,no,no,NoImageFound.PNG
1 Name Type WarehouseArea Description earthquakeID Price Required PickedUp Picture
2 Echo Bomb Lava ShelvingAreaA A dense glossy black volcanic bomb with minor vesicles. EV-7.4-Mexico-00035 120 no no EchoBomb.PNG
3 Silvershade Ash Ash ShelvingAreaD Fine light-grey volcanic ash collected near a village. EV-6.0-Iceland-00018 40 no no SilvershadeAsh.PNG
4 Strata Core Soil LoggingArea Soil core with visible stratification showing evidence of liquefaction. ET-6.9-Brazil-00046 30 no no StrataCore.PNG
5 Opal Clast Tephra ShelvingAreaM Pumice clast with sharp edges and clear layering. EV-8.3-Iceland-00127 65 no no OpalClast.PNG
6 Magnetite Ash Ash PalletDeliveryArea Bagged black ash sample with high magnetic content. EV-5.3-Myanmar-00088 28 yes no MagnetiteAsh.PNG
7 Ropeform Fragment Lava ShelvingAreaR Ropey pahoehoe lava fragment with preserved ripples. EV-6.9-Ethiopia-00012 85 no no NoImageFound.PNG
8 Glassy Bed Soil ShelvingAreaB Sandy soil deposit with glassy volcanic spherules visible under microscope. EV-7.8-USA-00167 48 no no NoImageFound.PNG
9 Groundwater Shard Tephra ShelvingAreaJ Layered tephra shard showing interaction with groundwater. EV-4.9-Chile-00083 49 no no NoImageFound.PNG
10 Sulfur Ghost Ash ShelvingAreaG Grey volcanic ash particle with very high sulfur content. EV-6.4-Armenia-00219 38 no no NoImageFound.PNG
11 Obsidian Pillar Lava ShelvingAreaC Dense basalt sample with well-developed columnar structure. EV-7.7-Jordan-00037 100 yes no NoImageFound.PNG
12 Peat Nest Soil ShelvingAreaP Peaty soil from earthquake liquefaction area with organic inclusions. ET-5.1-New Zealand-00032 52 no no NoImageFound.PNG
13 Biotite Veil Ash PalletDeliveryArea Ash with visible biotite flakes: lightly compacted. EV-3.1-USA-00101 32 no no NoImageFound.PNG
14 Foamcore Pumice Lava ShelvingAreaF High-porosity pumice formed during an explosive event. EV-8.4-Barbados-00071 77 no no NoImageFound.PNG
15 Redslide Soil Soil ShelvingAreaQ Aggregated red clay soil collected from a collapsed hillside. EC-7.5-Nicaragua-00078 26 no no NoImageFound.PNG
16 Spectrum Clast Tephra LoggingArea Multi-coloured tephra clast showing mixed eruption sources. EV-8.3-Antarctica-00030 54 no no NoImageFound.PNG
17 Olivine Stone Lava ShelvingAreaH Dense polished lava with visible olivine inclusions. EV-9.0-Russia-00171 132 no no NoImageFound.PNG
18 Blended Bar Soil ShelvingAreaN Sandy soil deeply mixed with volcanic tephra fragments. EV-6.4-Panama-00197 45 no no NoImageFound.PNG
19 Potash Powder Ash ShelvingAreaX Very fine mantle ash with high potassium content: glassy texture. EV-4.4-Ecuador-00253 56 no no NoImageFound.PNG
20 Shatterrock Lava ShelvingAreaS Massive hand specimen of aa lava showing sharp vesicular cavities. EV-7.4-South Africa-00228 117 no no NoImageFound.PNG
21 Humic Loam Soil PalletDeliveryArea Dark humic soil showing traces of ash fallout. EV-3.2-Argentina-00175 20 no no NoImageFound.PNG
22 Charcoal Drift Ash ShelvingAreaK Coarse ash mixed with fragmented charcoal. EV-8.6-Myanmar-00056 47 yes no NoImageFound.PNG
23 Spindle Bomb Lava ShelvingAreaU Spindle-shaped lava bomb: partially vitrified. EV-8.9-Japan-00076 84 no no NoImageFound.PNG
24 Bubblewall Tephra Tephra ShelvingAreaV Vitric tephra particle with bubble wall shards: transparent edges. EV-7.5-Greece-00248 88 no no NoImageFound.PNG
25 Layercake Earth Soil ShelvingAreaZ Compacted soil with tephra lenses: showing sediment mixing. EV-6.0-Guatemala-00165 39 no no NoImageFound.PNG
26 Creamdust Sample Ash ShelvingAreaW Bagged cream-coloured ash collected from roof dusting incident. EV-4.1-Maldives-00185 25 no no NoImageFound.PNG
27 Faultblock Tephra Tephra ShelvingAreaL Blocky tephra fragment: impacted during ground rupture. EV-5.6-Antarctica-00251 51 no no NoImageFound.PNG
28 Basalt Boulder Lava LoggingArea Weathered basalt boulder from lava flow front. EV-5.4-Spain-00129 69 no no NoImageFound.PNG
29 Glassloam Soil ShelvingAreaT Loam with embedded tephric glass and altered feldspar. EV-3.7-Argentina-00208 30 no no NoImageFound.PNG
30 Nanoshade Ash Ash ShelvingAreaO Ash sample with nano-crystalline silica. EV-7.4-Mauritius-00183 42 no no NoImageFound.PNG
31 Ironflame Scoria Lava ShelvingAreaE Vesicular scoria: deep red: indicative of high iron content. EV-3.4-Indonesia-00138 44 no no NoImageFound.PNG

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,28 @@
"use client"; "use client";
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
type Role = "ADMIN" | "GUEST" | "SCIENTIST"; type Role = "admin" | "user" | "editor";
const roleLabels: Record<Role, string> = {
ADMIN: "Admin",
GUEST: "Guest",
SCIENTIST: "Scientist",
};
type User = { type User = {
id: number;
email: string; email: string;
name: string; name: string;
role: Role; role: Role;
password: string; password: string;
createdAt: string;
}; };
const initialUsers: User[] = [ const initialUsers: User[] = [ // todo - add user reading function
{ email: "john@example.com", name: "John Doe", role: "ADMIN", password: "secret1", createdAt: "2024-06-21T09:15:01Z" ,id:1}, { email: "john@example.com", name: "John Doe", role: "admin", password: "secret1" },
{ email: "jane@example.com", name: "Jane Smith", role: "GUEST", password: "secret2", createdAt: "2024-06-21T10:01:09Z" ,id:2}, { email: "jane@example.com", name: "Jane Smith", role: "user", password: "secret2" },
{ email: "bob@example.com", name: "Bob Brown", role: "SCIENTIST", password: "secret3", createdAt: "2024-06-21T12:13:45Z" ,id:3}, { email: "bob@example.com", name: "Bob Brown", role: "editor", password: "secret3" },
{ email: "alice@example.com", name: "Alice Johnson",role: "GUEST", password: "secret4", createdAt: "2024-06-20T18:43:20Z" ,id:4}, { email: "alice@example.com", name: "Alice Johnson", role: "user", password: "secret4" },
{ email: "eve@example.com", name: "Eve Black", role: "ADMIN", password: "secret5", createdAt: "2024-06-20T19:37:10Z" ,id:5}, { email: "eve@example.com", name: "Eve Black", role: "admin", password: "secret5" },
{ email: "dave@example.com", name: "Dave Clark", role: "GUEST", password: "pw", createdAt: "2024-06-19T08:39:10Z" ,id:6}, { email: "dave@example.com", name: "Dave Clark", role: "user", password: "pw" },
{ email: "fred@example.com", name: "Fred Fox", role: "GUEST", password: "pw", createdAt: "2024-06-19T09:11:52Z" ,id:7}, { email: "fred@example.com", name: "Fred Fox", role: "user", password: "pw" },
{ email: "ginny@example.com", name: "Ginny Hall", role: "SCIENTIST", password: "pw", createdAt: "2024-06-17T14:56:27Z" ,id:8}, { email: "ginny@example.com", name: "Ginny Hall", role: "editor", password: "pw" },
{ email: "harry@example.com", name: "Harry Lee", role: "ADMIN", password: "pw", createdAt: "2024-06-16T19:28:11Z" ,id:9}, { email: "harry@example.com", name: "Harry Lee", role: "admin", password: "pw" },
{ email: "ivy@example.com", name: "Ivy Volt", role: "ADMIN", password: "pw", createdAt: "2024-06-15T21:04:05Z" ,id:10}, { email: "ivy@example.com", name: "Ivy Volt", role: "admin", password: "pw" },
{ email: "kate@example.com", name: "Kate Moss", role: "SCIENTIST", password: "pw", createdAt: "2024-06-14T11:16:35Z" ,id:11}, { email: "kate@example.com", name: "Kate Moss", role: "editor", password: "pw" },
{ email: "leo@example.com", name: "Leo Garrison", role: "GUEST", password: "pw", createdAt: "2024-06-12T08:02:51Z" ,id:12}, { email: "leo@example.com", name: "Leo Garrison", role: "user", password: "pw" },
{ email: "isaac@example.com", name: "Isaac Yang", role: "GUEST", password: "pw", createdAt: "2024-06-12T15:43:29Z" ,id:13}, { email: "isaac@example.com", name: "Isaac Yang", role: "user", password: "pw" },
]; ];
const sortFields = [ // Sort box options const sortFields = [ // Sort box options
{ label: "Name", value: "name" }, { label: "Name", value: "name" },
{ label: "Email", value: "email" }, { label: "Email", value: "email" },
@ -44,7 +37,7 @@ export default function AdminPage() {
const [users, setUsers] = useState<User[]>(initialUsers); const [users, setUsers] = useState<User[]>(initialUsers);
const [selectedEmail, setSelectedEmail] = useState<string | null>(null); const [selectedEmail, setSelectedEmail] = useState<string | null>(null);
// Local edit state for SCIENTIST form // Local edit state for editor form
const [editUser, setEditUser] = useState<User | null>(null); const [editUser, setEditUser] = useState<User | null>(null);
// Reset editUser when the selected user changes // Reset editUser when the selected user changes
React.useEffect(() => { React.useEffect(() => {
@ -135,7 +128,7 @@ export default function AdminPage() {
setEditUser(null); setEditUser(null);
}; };
const allRoles: Role[] = ["ADMIN", "GUEST", "SCIENTIST"]; const allRoles: Role[] = ["admin", "user", "editor"];
// Tooltip handling for email field // Tooltip handling for email field
const [showEmailTooltip, setShowEmailTooltip] = useState(false); const [showEmailTooltip, setShowEmailTooltip] = useState(false);
@ -154,15 +147,14 @@ export default function AdminPage() {
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
/> />
<button <select
type="button" value={searchField}
className="border rounded-lg px-2 py-1 text-sm bg-white hover:bg-neutral-100 transition font-semibold" onChange={e => setSearchField(e.target.value as "name" | "email")}
style={{ width: "80px" }} // fixed width, adjust as needed className="border rounded-lg px-2 py-1 text-sm"
onClick={() => setSearchField(field => field === "name" ? "email" : "name")}
title={`Switch to searching by ${searchField === "name" ? "Email" : "Name"}`}
> >
{searchField === "name" ? "Email" : "Name"} <option value="name">Name</option>
</button> <option value="email">Email</option>
</select>
</div> </div>
{/* Filter and Sort Buttons */} {/* Filter and Sort Buttons */}
<div className="flex gap-2 items-center mb-2"> <div className="flex gap-2 items-center mb-2">
@ -195,7 +187,7 @@ export default function AdminPage() {
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0 className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
${roleFilter===role ? "font-bold text-blue-600" : ""}`} ${roleFilter===role ? "font-bold text-blue-600" : ""}`}
> >
{roleLabels[role]} {role}
</button> </button>
))} ))}
</div> </div>
@ -254,7 +246,7 @@ export default function AdminPage() {
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium truncate">{user.name}</span> <span className="text-sm font-medium truncate">{user.name}</span>
<span className="ml-1 text-xs px-2 py-0.5 rounded-lg bg-gray-200 text-gray-700">{roleLabels[user.role]}</span> <span className="ml-1 text-xs px-2 py-0.5 rounded-lg bg-gray-200 text-gray-700">{user.role}</span>
</div> </div>
<div className="flex items-center justify-between mt-0.5"> <div className="flex items-center justify-between mt-0.5">
<span className="text-xs text-gray-600 truncate">{user.email}</span> <span className="text-xs text-gray-600 truncate">{user.email}</span>
@ -273,14 +265,6 @@ export default function AdminPage() {
<div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow"> <div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-bold mb-6">Edit User</h2> <h2 className="text-lg font-bold mb-6">Edit User</h2>
<form className="space-y-4" onSubmit={handleUpdate}> <form className="space-y-4" onSubmit={handleUpdate}>
<div className="flex items-center gap-2 mb-2">
<label className="text-sm font-medium text-gray-700">Account Creation Time:</label>
<span className="text-sm text-gray-500">{editUser.createdAt}</span>
</div>
<div className="flex items-center gap-2 mb-2">
<label className="text-sm font-medium text-gray-700">Account ID Number:</label>
<span className="text-sm text-gray-500">{editUser.id}</span>
</div>
<div className="relative"> <div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Email (unique): Email (unique):
@ -325,7 +309,7 @@ export default function AdminPage() {
onChange={handleEditChange} onChange={handleEditChange}
> >
{allRoles.map((role) => ( {allRoles.map((role) => (
<option key={role} value={role}>{roleLabels[role]}</option> <option key={role} value={role}>{role}</option>
))} ))}
</select> </select>
</div> </div>

View File

@ -1,6 +1,6 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from "@prisma/client";
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;

View File

@ -1,70 +0,0 @@
import { parse } from "csv-parse/sync";
import fs from "fs/promises";
import { NextResponse } from "next/server";
import path from "path";
import { PrismaClient } from "@prismaclient";
// CSV location
const csvFilePath = path.resolve(process.cwd(), "public/artefacts.csv");
const prisma = new PrismaClient();
type CsvRow = {
Type: string;
Name: string;
Description: string;
WarehouseArea: string;
EarthquakeId: string;
Required?: string;
ShopPrice?: string;
PickedUp?: string;
};
function stringToBool(val: string | undefined, defaultValue: boolean = false): boolean {
if (!val) return defaultValue;
return /^true$/i.test(val.trim());
}
export async function POST() {
try {
// 1. Read file
const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse CSV
const records: CsvRow[] = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// 3. Map records to artefact input
const artefacts = records.map((row) => ({
name: row.Name,
description: row.Description,
type: row.Type,
warehouseArea: row.WarehouseArea,
// todo get earthquakeId where code === row.EarthquakeCode
earthquakeId: parseInt(row.EarthquakeId, 10),
required: stringToBool(row.Required, true), // default TRUE
shopPrice: row.ShopPrice && row.ShopPrice !== "" ? parseFloat(row.ShopPrice) : null,
pickedUp: stringToBool(row.PickedUp, false), // default FALSE
// todo add random selection for creatorId
creatorId: null,
purchasedById: null,
}));
// 4. Bulk insert
await prisma.artefact.createMany({
data: artefacts,
});
return NextResponse.json({
success: true,
count: artefacts.length,
});
} catch (error: any) {
console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
}
}

View File

@ -1,19 +1,18 @@
import { parse } from "csv-parse/sync";
import fs from "fs/promises";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import fs from "fs/promises";
import path from "path"; import path from "path";
import { parse } from "csv-parse/sync";
import { PrismaClient } from "@prismaclient"; // Define the path to your CSV file.
// Place your earthquakes.csv in your project root or `public` directory
// Path to your earthquakes.csv
const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv"); const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv");
const prisma = new PrismaClient(); const prisma = new PrismaClient();
type CsvRow = { type CsvRow = {
Date: string; Date: string;
Code: string;
Magnitude: string; Magnitude: string;
Type: string;
Latitude: string; Latitude: string;
Longitude: string; Longitude: string;
Location: string; Location: string;
@ -24,28 +23,36 @@ export async function POST() {
try { try {
// 1. Read the CSV file // 1. Read the CSV file
const fileContent = await fs.readFile(csvFilePath, "utf8"); const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse the CSV // 2. Parse the CSV
const records: CsvRow[] = parse(fileContent, { const records: CsvRow[] = parse(fileContent, {
columns: true, columns: true,
skip_empty_lines: true, skip_empty_lines: true
}); });
// 3. Transform to fit Earthquake model
const earthquakes = records.map((row) => ({ // 3. Transform each CSV row to Earthquake model
// Since your prisma model expects: name, date (DateTime), location, magnitude (float), depth (float). We'll fill casualties/creatorId as zero/null for now.
const earthquakes = records.map(row => {
// You may want to add better parsing & validation depending on your actual data
return {
date: new Date(row.Date), date: new Date(row.Date),
code: row.Code,
magnitude: parseFloat(row.Magnitude),
type: row.Type,
latitude: parseFloat(row.Latitude),
longitude: parseFloat(row.Longitude),
location: row.Location, location: row.Location,
depth: row.Depth, // store as received magnitude: parseFloat(row.Magnitude),
// todo add random selection for creatorId latitude: row.Latitude,
creatorId: null, longitude: row.Longitude,
})); depth: parseFloat(row.Depth.replace(" km", "")),
// todo add creatorId
creatorId: null
};
});
// 4. Bulk create earthquakes in database: // 4. Bulk create earthquakes in database:
// Consider chunking if your CSV is large!
await prisma.earthquake.createMany({ await prisma.earthquake.createMany({
data: earthquakes, data: earthquakes,
skipDuplicates: true, // in case the route is called twice
}); });
return NextResponse.json({ success: true, count: earthquakes.length }); return NextResponse.json({ success: true, count: earthquakes.length });
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);

View File

@ -1,67 +0,0 @@
import { parse } from "csv-parse/sync";
import fs from "fs/promises";
import { NextResponse } from "next/server";
import path from "path";
import { PrismaClient } from "@prismaclient";
// CSV location (update filename as needed)
const csvFilePath = path.resolve(process.cwd(), "public/observatories.csv");
const prisma = new PrismaClient();
type CsvRow = {
Name: string;
Location: string;
Latitude: string;
Longitude: string;
DateEstablished?: string;
Functional: string;
SeismicSensorOnline?: string;
};
function stringToBool(val: string | undefined): boolean {
// Accepts "TRUE", "true", "True", etc.
if (!val) return false;
return /^true$/i.test(val.trim());
}
export async function POST() {
try {
// 1. Read file
const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse CSV
const records: CsvRow[] = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// 3. Map records to Prisma inputs
const observatories = records.map((row) => ({
name: row.Name,
location: row.Location,
latitude: row.Latitude,
longitude: row.Longitude,
dateEstablished: row.DateEstablished ? parseInt(row.DateEstablished, 10) : null,
functional: stringToBool(row.Functional),
seismicSensorOnline: row.SeismicSensorOnline ? stringToBool(row.SeismicSensorOnline) : true, // default true per schema
// todo add random selection of creatorId
creatorId: null,
}));
// 4. Bulk insert
await prisma.observatory.createMany({
data: observatories,
});
return NextResponse.json({
success: true,
count: observatories.length,
});
} catch (error: any) {
console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
}
}

View File

@ -1,62 +0,0 @@
import { parse } from "csv-parse/sync";
import fs from "fs/promises";
import { NextResponse } from "next/server";
import path from "path";
import { PrismaClient } from "@prismaclient";
const csvFilePath = path.resolve(process.cwd(), "public/requests.csv");
const prisma = new PrismaClient();
type RequestType = "NEW_USER" | "CHANGE_LEVEL" | "DELETE";
type RequestOutcome = "FULFILLED" | "REJECTED" | "IN_PROGRESS" | "CANCELLED" | "OTHER";
type CsvRow = {
RequestType: string;
RequestingUserId: string;
Outcome?: string;
};
const validRequestTypes: RequestType[] = ["NEW_USER", "CHANGE_LEVEL", "DELETE"];
const validOutcomes: RequestOutcome[] = ["FULFILLED", "REJECTED", "IN_PROGRESS", "CANCELLED", "OTHER"];
function normalizeRequestType(type: string | undefined): RequestType {
if (!type) return "NEW_USER";
const norm = type.trim().toUpperCase().replace(" ", "_");
return (validRequestTypes.includes(norm as RequestType) ? norm : "NEW_USER") as RequestType;
}
function normalizeOutcome(outcome: string | undefined): RequestOutcome {
if (!outcome) return "IN_PROGRESS";
const norm = outcome.trim().toUpperCase().replace(" ", "_");
return (validOutcomes.includes(norm as RequestOutcome) ? norm : "IN_PROGRESS") as RequestOutcome;
}
export async function POST() {
try {
const fileContent = await fs.readFile(csvFilePath, "utf8");
const records: CsvRow[] = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
const requests = records.map((row) => ({
requestType: normalizeRequestType(row.RequestType),
requestingUserId: parseInt(row.RequestingUserId, 10),
outcome: normalizeOutcome(row.Outcome),
}));
const filteredRequests = requests.filter((r) => !isNaN(r.requestingUserId));
await prisma.request.createMany({
data: filteredRequests,
});
return NextResponse.json({ success: true, count: filteredRequests.length });
} catch (error: any) {
console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
}
}

View File

@ -1,59 +0,0 @@
import { parse } from "csv-parse/sync";
import fs from "fs/promises";
import { NextResponse } from "next/server";
import path from "path";
import { PrismaClient } from "@prismaclient";
// Path to CSV file
const csvFilePath = path.resolve(process.cwd(), "public/scientists.csv");
const prisma = new PrismaClient();
type CsvRow = {
Name: string;
Level?: string;
UserId: string;
SuperiorId?: string;
};
function normalizeLevel(level: string | undefined): string {
// Only allow JUNIOR, SENIOR; default JUNIOR
if (!level || !level.trim()) return "JUNIOR";
const lv = level.trim().toUpperCase();
return ["JUNIOR", "SENIOR"].includes(lv) ? lv : "JUNIOR";
}
export async function POST() {
try {
// 1. Read the CSV file
const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse the CSV
const records: CsvRow[] = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// 3. Transform each record for Prisma
// todo add senior scientists first
const scientists = records.map((row) => ({
name: row.Name,
level: normalizeLevel(row.Level),
userId: parseInt(row.UserId, 10),
// todo get superior id by name from db
superiorId: row.SuperiorId && row.SuperiorId.trim() !== "" ? parseInt(row.SuperiorId, 10) : null,
}));
// 4. Bulk create scientists in database
await prisma.scientist.createMany({
data: scientists,
});
return NextResponse.json({ success: true, count: scientists.length });
} catch (error: any) {
console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
}
}

View File

@ -1,57 +0,0 @@
import { parse } from "csv-parse/sync";
import fs from "fs/promises";
import { NextResponse } from "next/server";
import path from "path";
import { PrismaClient } from "@prismaclient";
// Path to users.csv - adjust as needed
const csvFilePath = path.resolve(process.cwd(), "public/users.csv");
const prisma = new PrismaClient();
type CsvRow = {
Name: string;
Email: string;
PasswordHash: string;
Role?: string;
};
function normalizeRole(role: string | undefined): string {
// Only allow ADMIN, SCIENTIST, GUEST; default GUEST
if (!role || !role.trim()) return "GUEST";
const r = role.trim().toUpperCase();
return ["ADMIN", "SCIENTIST", "GUEST"].includes(r) ? r : "GUEST";
}
export async function POST() {
try {
// 1. Read the CSV file
const fileContent = await fs.readFile(csvFilePath, "utf8");
// 2. Parse the CSV
const records: CsvRow[] = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
// 3. Transform each CSV row to User model format
const users = records.map((row) => ({
name: row.Name,
email: row.Email,
passwordHash: row.PasswordHash,
role: normalizeRole(row.Role),
}));
// 4. Bulk create users in database
await prisma.user.createMany({
data: users,
});
return NextResponse.json({ success: true, count: users.length });
} catch (error: any) {
console.error(error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
} finally {
await prisma.$disconnect();
}
}

View File

@ -1,11 +1,11 @@
import bcryptjs from "bcryptjs"; import bcryptjs from 'bcryptjs';
import { SignJWT } from "jose"; import { SignJWT } from 'jose';
import { NextResponse } from "next/server"; import { NextResponse } from 'next/server';
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from '@prisma/client';
import { env } from "@utils/env"; import { env } from '@utils/env';
import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite"; import { findUserByEmail, readUserCsv, User } from '../functions/csvReadWrite';
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
@ -20,16 +20,23 @@ export async function POST(req: Request) {
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove
console.log("Password:", password); // ! remove console.log("Password:", password); // ! remove
let user = await prisma.user.findUnique({ let user;
if (usingPrisma) {
user = await prisma.user.findUnique({
where: { where: {
email, // use the email to uniquely identify the user email, // use the email to uniquely identify the user
}, },
}); });
} else {
user = findUserByEmail(userData, email);
}
if (user && bcryptjs.compareSync(password, user.passwordHash)) { if (user && bcryptjs.compareSync(password, usingPrisma ? user.hashedPassword : user.password)) {
// todo remove password from returned user // todo remove password from returned user
// get user and relations // get user and relations
if (usingPrisma)
user = await prisma.user.findUnique({ user = await prisma.user.findUnique({
where: { id: user.id }, where: { id: user.id },
include: { include: {
@ -47,7 +54,7 @@ export async function POST(req: Request) {
}); });
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
const token = await new SignJWT({ userId: user!.id }) const token = await new SignJWT({ userId: user.id })
.setProtectedHeader({ alg: "HS256" }) .setProtectedHeader({ alg: "HS256" })
.setExpirationTime("2w") .setExpirationTime("2w")
.sign(secret); .sign(secret);

View File

@ -1,6 +1,6 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from "@prisma/client";
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
@ -37,7 +37,7 @@ export async function GET(request: Request) {
// todo get earthquakes associated with observatories // todo get earthquakes associated with observatories
let observatories; let observatories;
if (usingPrisma) observatories = await prisma.observatory.findMany(); if (usingPrisma) observatories = await prisma.observatories.findMany();
if (observatories) { if (observatories) {
return NextResponse.json({ message: "Got observatories successfully", observatories }, { status: 200 }); return NextResponse.json({ message: "Got observatories successfully", observatories }, { status: 200 });

View File

@ -1,11 +1,13 @@
import bcryptjs from "bcryptjs"; import bcryptjs from 'bcryptjs';
import { SignJWT } from "jose"; import { SignJWT } from 'jose';
import { NextResponse } from "next/server"; import { NextResponse } from 'next/server';
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from '@prisma/client';
import { env } from "@utils/env"; import { env } from '@utils/env';
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite"; import {
findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv
} from '../functions/csvReadWrite';
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;

View File

@ -1,7 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { PrismaClient } from "@prismaclient";
import { env } from "@utils/env"; import { env } from "@utils/env";
import { PrismaClient } from "@prisma/client";
import { verifyJwt } from "@utils/verifyJwt"; import { verifyJwt } from "@utils/verifyJwt";
const usingPrisma = false; const usingPrisma = false;
@ -88,7 +88,7 @@ export async function POST(req: Request) {
]; ];
let artefacts; let artefacts;
if (usingPrisma) artefacts = await prisma.artefact.findMany(); if (usingPrisma) artefacts = await prisma.artefacts.findMany();
if (artefacts) { if (artefacts) {
return NextResponse.json({ message: "Got artefacts successfully", artefacts }, { status: 200 }); return NextResponse.json({ message: "Got artefacts successfully", artefacts }, { status: 200 });

View File

@ -39,7 +39,7 @@ export default function Home() {
href="/shop" href="/shop"
className="flex flex-col items-center p-6 hover:bg-white hover:bg-opacity-10 rounded-xl transition-colors duration-300" className="flex flex-col items-center p-6 hover:bg-white hover:bg-opacity-10 rounded-xl transition-colors duration-300"
> >
<Image height={100} width={100} src="/artifactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" /> <Image height={100} width={100} src="/artefactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" />
<h3 className="text-xl font-bold text-black mb-4">Artefacts</h3> <h3 className="text-xl font-bold text-black mb-4">Artefacts</h3>
<p className="text-md text-black text-center max-w-xs opacity-90"> <p className="text-md text-black text-center max-w-xs opacity-90">
View or purchase recently discovered artefacts from seismic events View or purchase recently discovered artefacts from seismic events

View File

@ -189,11 +189,6 @@ const artefacts: Artefact[] = [
export default function Shop() { export default function Shop() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null); const [selectedArtefact, setSelectedArtefact] = useState<Artefact | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [artefactToBuy, setArtefactToBuy] = useState<Artefact | null>(null);
const [showThankYouModal, setShowThankYouModal] = useState(false);
const [orderNumber, setOrderNumber] = useState<string | null>(null);
const artefactsPerPage = 12; const artefactsPerPage = 12;
const indexOfLastArtefact = currentPage * artefactsPerPage; const indexOfLastArtefact = currentPage * artefactsPerPage;
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage; const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
@ -203,41 +198,25 @@ export default function Shop() {
const conversionRates = useStoreState((state) => state.currency.conversionRates); const conversionRates = useStoreState((state) => state.currency.conversionRates);
const currencyTickers = useStoreState((state) => state.currency.tickers); const currencyTickers = useStoreState((state) => state.currency.tickers);
const convertPrice = useCallback( const convertPrice = useCallback((price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2), []);
(price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2),
[conversionRates]
);
const handleNextPage = () => { const handleNextPage = () => {
if (indexOfLastArtefact < artefacts.length) setCurrentPage((prev) => prev + 1); if (indexOfLastArtefact < artefacts.length) {
setCurrentPage((prev) => prev + 1);
}
}; };
const handlePreviousPage = () => { const handlePreviousPage = () => {
if (currentPage > 1) setCurrentPage((prev) => prev - 1); if (currentPage > 1) {
setCurrentPage((prev) => prev - 1);
}
}; };
function ArtefactCard({ artefact }: { artefact: Artefact }) {
return (
<div
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
onClick={() => setSelectedArtefact(artefact)}
>
<Image src={artefact.image} alt={artefact.name} width={500} height={300} className="w-full h-56 object-cover" />
<div className="p-4">
<h3 className="text-lg font-semibold">{artefact.name}</h3>
<p className="text-neutral-500 mb-2">{artefact.location}</p>
<p className="text-neutral-500 mb-2">{artefact.earthquakeID}</p>
<p className="text-black font-bold text-md mt-2">
{currencyTickers[selectedCurrency]}
{convertPrice(artefact.price, selectedCurrency)}
</p>
</div>
</div>
);
}
function Modal({ artefact }: { artefact: Artefact }) { function Modal({ artefact }: { artefact: Artefact }) {
if (!artefact) return null; if (!artefact) return null;
const handleOverlayClick = (e: React.MouseEvent) => { const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) setSelectedArtefact(null); if (e.target === e.currentTarget) {
setSelectedArtefact(null);
}
}; };
return ( return (
<div <div
@ -247,10 +226,10 @@ export default function Shop() {
<div className="bg-white rounded-xl shadow-2xl max-w-lg w-full p-6"> <div className="bg-white rounded-xl shadow-2xl max-w-lg w-full p-6">
<h3 className="text-2xl font-bold mb-4">{artefact.name}</h3> <h3 className="text-2xl font-bold mb-4">{artefact.name}</h3>
<Image <Image
height={5000}
width={5000}
src={artefact.image} src={artefact.image}
alt={artefact.name} alt={artefact.name}
width={500}
height={300}
className="w-full h-64 object-cover rounded-md" className="w-full h-64 object-cover rounded-md"
/> />
<p className="text-xl font-bold"> <p className="text-xl font-bold">
@ -264,11 +243,7 @@ export default function Shop() {
<p className="text-neutral-500 mb-2">{artefact.dateReleased}</p> <p className="text-neutral-500 mb-2">{artefact.dateReleased}</p>
<div className="flex justify-end gap-4 mt-4 mr-2"> <div className="flex justify-end gap-4 mt-4 mr-2">
<button <button
onClick={() => { onClick={() => alert("Purchased Successfully!")}
setArtefactToBuy(artefact); // Set artefact for payment modal
setShowPaymentModal(true); // Show payment modal
setSelectedArtefact(null); // Close this modal
}}
className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" className="px-10 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
> >
Buy Buy
@ -279,156 +254,26 @@ export default function Shop() {
); );
} }
function PaymentModal({ artefact, onClose }: { artefact: Artefact; onClose: () => void }) { function ArtefactCard({ artefact }: { artefact: Artefact }) {
const [cardNumber, setCardNumber] = useState("");
const [expiry, setExpiry] = useState("");
const [cvc, setCvc] = useState("");
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [remember, setRemember] = useState(false);
const [error, setError] = useState("");
function validateEmail(email: string) {
return (
email.includes("@") &&
(email.endsWith(".com") || email.endsWith(".co.uk") || email.endsWith(".org") || email.endsWith(".org.uk"))
);
}
function validateCardNumber(number: string) {
return /^\d{12,19}$/.test(number.replace(/\s/g, "")); // 12-19 digits
}
function validateCVC(number: string) {
return /^\d{3,4}$/.test(number);
}
function validateExpiry(exp: string) {
return /^\d{2}\/\d{2}$/.test(exp);
}
function handlePay() {
setError("");
if (!validateEmail(email)) {
setError("Please enter a valid email ending");
return;
}
if (!validateCardNumber(cardNumber)) {
setError("Card number must be 12-19 digits.");
return;
}
if (!validateExpiry(expiry)) {
setError("Expiry must be in MM/YY format.");
return;
}
if (!validateCVC(cvc)) {
setError("CVC must be 3 or 4 digits.");
return;
}
const genOrder = () => "#" + Math.random().toString(36).substring(2, 10).toUpperCase();
setOrderNumber(genOrder());
onClose();
setShowThankYouModal(true);
}
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) onClose();
};
return ( return (
<div <div
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-10" className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
onClick={handleOverlayClick} onClick={() => setSelectedArtefact(artefact)}
> >
<div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6"> <img src={artefact.image} alt={artefact.name} className="w-full h-56 object-cover" />
<h2 className="text-2xl font-bold mb-4">Buy {artefact.name}</h2> <div className="p-4">
{/* ...Image... */} <h3 className="text-lg font-semibold">{artefact.name}</h3>
<form <p className="text-neutral-500 mb-2">{artefact.location}</p>
onSubmit={(e) => { <p className="text-neutral-500 mb-2">{artefact.earthquakeID}</p>
e.preventDefault(); <p className="text-black font-bold text-md mt-2">
handlePay(); {currencyTickers[selectedCurrency]}
}} {convertPrice(artefact.price, selectedCurrency)}
> </p>
<input
className="w-full mb-2 px-3 py-2 border rounded"
placeholder="Email Address"
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
required
autoFocus
/>
<input
className="w-full mb-2 px-3 py-2 border rounded"
placeholder="Cardholder Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<input
className="w-full mb-2 px-3 py-2 border rounded"
placeholder="Card Number"
value={cardNumber}
onChange={(e) => setCardNumber(e.target.value.replace(/\D/g, ""))}
maxLength={19}
required
inputMode="numeric"
pattern="\d*"
/>
<div className="flex gap-2">
<input
className="w-1/2 mb-2 px-3 py-2 border rounded"
placeholder="MM/YY"
value={expiry}
onChange={(e) => setExpiry(e.target.value.replace(/[^0-9/]/g, ""))}
maxLength={5}
required
inputMode="numeric"
/>
<input
className="w-1/2 mb-2 px-3 py-2 border rounded"
placeholder="CVC"
value={cvc}
onChange={(e) => setCvc(e.target.value.replace(/\D/g, ""))}
maxLength={4}
required
inputMode="numeric"
/>
</div>
<label className="inline-flex items-center mb-4">
<input type="checkbox" checked={remember} onChange={() => setRemember((r) => !r)} className="mr-2" />
Remember me
</label>
{error && <p className="text-red-600 mb-2">{error}</p>}
<div className="flex justify-end gap-2 mt-2">
<button type="button" onClick={onClose} className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md mr-2">
Cancel
</button>
<button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
Pay
</button>
</div>
</form>
</div> </div>
</div> </div>
); );
} }
function ThankYouModal({ orderNumber, onClose }: { orderNumber: string; onClose: () => void }) {
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) onClose();
};
return (
<div
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-50"
onClick={handleOverlayClick}
>
<div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-8 text-center">
<h2 className="text-3xl font-bold mb-4">Thank you for your purchase!</h2>
<p className="mb-4">Your order number is:</p>
<p className="text-2xl font-mono font-bold mb-6">{orderNumber}</p>
<button className="px-8 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700" onClick={onClose}>
Close
</button>
</div>
</div>
);
}
return ( return (
<div <div
className="min-h-screen relative flex flex-col" className="min-h-screen relative flex flex-col"
@ -438,8 +283,10 @@ export default function Shop() {
backgroundPosition: "center", backgroundPosition: "center",
}} }}
> >
{/* Overlay */}
<div className="absolute inset-0 bg-black bg-opacity-50 z-0"></div> <div className="absolute inset-0 bg-black bg-opacity-50 z-0"></div>
<div className="relative z-10 flex flex-col items-center w-full px-2 py-12"> <div className="relative z-10 flex flex-col items-center w-full px-2 py-12">
{/* Title & Subheading */}
<h1 className="text-4xl md:text-4xl font-bold text-center text-white mb-2 tracking-tight drop-shadow-lg"> <h1 className="text-4xl md:text-4xl font-bold text-center text-white mb-2 tracking-tight drop-shadow-lg">
Artefact Shop Artefact Shop
</h1> </h1>
@ -447,11 +294,17 @@ export default function Shop() {
Discover extraordinary historical artefacts and collectibles from major seismic events from around the world - now Discover extraordinary historical artefacts and collectibles from major seismic events from around the world - now
available for purchase. available for purchase.
</p> </p>
{/* Artefact Grid */}
<div className="w-full max-w-7xl grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 p-2"> <div className="w-full max-w-7xl grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 p-2">
{" "}
{/* gap-10 for more spacing */}
{currentArtefacts.map((artefact) => ( {currentArtefacts.map((artefact) => (
<ArtefactCard key={artefact.id} artefact={artefact} /> <ArtefactCard key={artefact.id} artefact={artefact} />
))} ))}
</div> </div>
{/* Pagination Footer */}
<footer className="mt-10 bg-white bg-opacity-90 border-neutral-300 py-3 text-center flex justify-center items-center w-100 max-w-7xl rounded-lg"> <footer className="mt-10 bg-white bg-opacity-90 border-neutral-300 py-3 text-center flex justify-center items-center w-100 max-w-7xl rounded-lg">
<button <button
onClick={handlePreviousPage} onClick={handlePreviousPage}
@ -474,19 +327,9 @@ export default function Shop() {
</button> </button>
</footer> </footer>
</div> </div>
{/* Modal */}
{selectedArtefact && <Modal artefact={selectedArtefact} />} {selectedArtefact && <Modal artefact={selectedArtefact} />}
{artefactToBuy && showPaymentModal && (
<PaymentModal
artefact={artefactToBuy}
onClose={() => {
setShowPaymentModal(false);
setArtefactToBuy(null);
}}
/>
)}
{showThankYouModal && orderNumber && (
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
)}
</div> </div>
); );
} }

View File

@ -1,68 +1,68 @@
"use client"; "use client";
import { Dispatch, SetStateAction, useMemo, useState } from "react"; import { useState, useMemo } from "react";
import { FaTimes } from "react-icons/fa"; import { FaCalendarPlus, FaWarehouse, FaCartShopping } from "react-icons/fa6";
import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6";
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5"; import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5";
import { FaTimes } from "react-icons/fa";
// import { Artefact } from "@appTypes/Prisma"; import { SetStateAction, Dispatch } from "react";
// import type { Artefact } from "@prisma/client";
import type { Artefact } from "@prismaclient"; import { Artefact } from "@appTypes/Prisma";
interface WarehouseArtefact extends Artefact {
location: string;
}
// Warehouse Artefacts Data // Warehouse Artefacts Data
const warehouseArtefacts: WarehouseArtefact[] = [ const warehouseArtefacts: Artefact[] = [
{ {
id: 1, id: 1,
name: "Solidified Lava Chunk", name: "Solidified Lava Chunk",
description: "A chunk of solidified lava from the 2023 Iceland eruption.", description: "A chunk of solidified lava from the 2023 Iceland eruption.",
location: "Reykjanes, Iceland", location: "Reykjanes, Iceland",
earthquakeId: "EQ2023ICL",
isRequired: true, isRequired: true,
isSold: false, isSold: false,
isCollected: false, isCollected: false,
createdAt: "2025-05-04", dateAdded: "2025-05-04",
}, },
{ {
id: 2, id: 2,
name: "Tephra Sample", name: "Tephra Sample",
description: "Foreign debris from the 2022 Tonga volcanic eruption.", description: "Foreign debris from the 2022 Tonga volcanic eruption.",
location: "Tonga", location: "Tonga",
earthquakeId: "EQ2022TGA",
isRequired: false, isRequired: false,
isSold: true, isSold: true,
isCollected: true, isCollected: true,
createdAt: "2025-05-03", dateAdded: "2025-05-03",
}, },
{ {
id: 3, id: 3,
name: "Ash Sample", name: "Ash Sample",
description: "Volcanic ash from the 2021 La Palma eruption.", description: "Volcanic ash from the 2021 La Palma eruption.",
location: "La Palma, Spain", location: "La Palma, Spain",
earthquakeId: "EQ2021LPA",
isRequired: false, isRequired: false,
isSold: false, isSold: false,
isCollected: false, isCollected: false,
createdAt: "2025-05-04", dateAdded: "2025-05-04",
}, },
{ {
id: 4, id: 4,
name: "Ground Soil", name: "Ground Soil",
description: "Soil sample from the 2020 Croatia earthquake site.", description: "Soil sample from the 2020 Croatia earthquake site.",
location: "Zagreb, Croatia", location: "Zagreb, Croatia",
earthquakeId: "EQ2020CRO",
isRequired: true, isRequired: true,
isSold: false, isSold: false,
isCollected: false, isCollected: false,
createdAt: "2025-05-02", dateAdded: "2025-05-02",
}, },
{ {
id: 5, id: 5,
name: "Basalt Fragment", name: "Basalt Fragment",
description: "Basalt rock from the 2019 New Zealand eruption.", description: "Basalt rock from the 2019 New Zealand eruption.",
location: "White Island, New Zealand", location: "White Island, New Zealand",
earthquakeId: "EQ2019NZL",
isRequired: false, isRequired: false,
isSold: true, isSold: true,
isCollected: false, isCollected: false,
createdAt: "2025-05-04", dateAdded: "2025-05-04",
}, },
]; ];
@ -184,7 +184,7 @@ function ArtefactTable({
{ label: "Required", key: "isRequired", width: "6%" }, { label: "Required", key: "isRequired", width: "6%" },
{ label: "Sold", key: "isSold", width: "5%" }, { label: "Sold", key: "isSold", width: "5%" },
{ label: "Collected", key: "isCollected", width: "7%" }, { label: "Collected", key: "isCollected", width: "7%" },
{ label: "Date Added", key: "createdAt", width: "8%" }, { label: "Date Added", key: "dateAdded", width: "8%" },
]; ];
return ( return (
@ -204,7 +204,7 @@ function ArtefactTable({
setFilters({ ...filters, [key]: value } as Record<string, string>); setFilters({ ...filters, [key]: value } as Record<string, string>);
if (value === "") clearSortConfig(); if (value === "") clearSortConfig();
}} }}
type={key === "createdAt" ? "date" : "text"} type={key === "dateAdded" ? "date" : "text"}
options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined} options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined}
/> />
{sortConfig?.key === key && ( {sortConfig?.key === key && (
@ -500,7 +500,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
const [isRequired, setIsRequired] = useState(artefact.isRequired); const [isRequired, setIsRequired] = useState(artefact.isRequired);
const [isSold, setIsSold] = useState(artefact.isSold); const [isSold, setIsSold] = useState(artefact.isSold);
const [isCollected, setIsCollected] = useState(artefact.isCollected); const [isCollected, setIsCollected] = useState(artefact.isCollected);
const [createdAt, setDateAdded] = useState(artefact.createdAt.toDateString()); const [dateAdded, setDateAdded] = useState(artefact.dateAdded);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -511,7 +511,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
}; };
const handleSave = async () => { const handleSave = async () => {
if (!name || !description || !location || !earthquakeId || !createdAt) { if (!name || !description || !location || !earthquakeId || !dateAdded) {
setError("All fields are required."); setError("All fields are required.");
return; return;
} }
@ -602,7 +602,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
</div> </div>
<input <input
type="date" type="date"
value={createdAt} value={dateAdded}
onChange={(e) => setDateAdded(e.target.value)} onChange={(e) => setDateAdded(e.target.value)}
className="w-full p-2 border border-neutral-300 rounded-md focus:ring-2 focus:ring-blue-500" className="w-full p-2 border border-neutral-300 rounded-md focus:ring-2 focus:ring-blue-500"
aria-label="Date Added" aria-label="Date Added"
@ -657,14 +657,13 @@ const applyFilters = (artefacts: Artefact[], filters: Record<string, string>): A
return ( return (
(filters.id === "" || artefact.id.toString().includes(filters.id)) && (filters.id === "" || artefact.id.toString().includes(filters.id)) &&
(filters.name === "" || artefact.name.toLowerCase().includes(filters.name.toLowerCase())) && (filters.name === "" || artefact.name.toLowerCase().includes(filters.name.toLowerCase())) &&
// todo fix
(filters.earthquakeId === "" || artefact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) && (filters.earthquakeId === "" || artefact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) &&
(filters.location === "" || artefact.location.toLowerCase().includes(filters.location.toLowerCase())) && (filters.location === "" || artefact.location.toLowerCase().includes(filters.location.toLowerCase())) &&
(filters.description === "" || artefact.description.toLowerCase().includes(filters.description.toLowerCase())) && (filters.description === "" || artefact.description.toLowerCase().includes(filters.description.toLowerCase())) &&
(filters.isRequired === "" || (filters.isRequired === "true" ? artefact.isRequired : !artefact.isRequired)) && (filters.isRequired === "" || (filters.isRequired === "true" ? artefact.isRequired : !artefact.isRequired)) &&
(filters.isSold === "" || (filters.isSold === "true" ? artefact.isSold : !artefact.isSold)) && (filters.isSold === "" || (filters.isSold === "true" ? artefact.isSold : !artefact.isSold)) &&
(filters.isCollected === "" || (filters.isCollected === "true" ? artefact.isCollected : !artefact.isCollected)) && (filters.isCollected === "" || (filters.isCollected === "true" ? artefact.isCollected : !artefact.isCollected)) &&
(filters.createdAt === "" || artefact.createdAt.toDateString() === filters.createdAt) (filters.dateAdded === "" || artefact.dateAdded === filters.dateAdded)
); );
}); });
}; };
@ -684,7 +683,7 @@ export default function Warehouse() {
isRequired: "", isRequired: "",
isSold: "", isSold: "",
isCollected: "", isCollected: "",
createdAt: "", dateAdded: "",
}); });
const [isFiltering, setIsFiltering] = useState(false); const [isFiltering, setIsFiltering] = useState(false);
const [sortConfig, setSortConfig] = useState<{ const [sortConfig, setSortConfig] = useState<{
@ -708,11 +707,9 @@ export default function Warehouse() {
// Overview stats // Overview stats
const totalArtefacts = warehouseArtefacts.length; const totalArtefacts = warehouseArtefacts.length;
const today = new Date(); const today = "2025-05-04";
const artefactsAddedToday = warehouseArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length; const artefactsAddedToday = warehouseArtefacts.filter((a) => a.dateAdded === today).length;
const artefactsSoldToday = warehouseArtefacts.filter( const artefactsSoldToday = warehouseArtefacts.filter((a) => a.isSold && a.dateAdded === today).length;
(a) => a.isSold && a.createdAt.toDateString() === today.toDateString()
).length;
const clearFilters = () => { const clearFilters = () => {
setFilters({ setFilters({
@ -724,7 +721,7 @@ export default function Warehouse() {
isRequired: "", isRequired: "",
isSold: "", isSold: "",
isCollected: "", isCollected: "",
createdAt: "", dateAdded: "",
}); });
setSortConfig(null); // Clear sorting setSortConfig(null); // Clear sorting
}; };

View File

@ -0,0 +1 @@
Artefacts
1 Artefacts

File diff suppressed because it is too large Load Diff

View File

@ -2,56 +2,59 @@ import random
import datetime import datetime
def generate_earthquake_data(start_date, end_date, file_name): def generate_earthquake_data(start_date, end_date, file_name):
# List of 200 real-world seismic locations, reformatted to remove commas
locations = [ locations = [
"San Andreas Fault: USA", "Cascade Range: USA", "Denali Fault System: USA", "San Andreas Fault USA", "Cascade Range USA", "Denali Fault System Alaska USA",
"New Madrid Seismic Zone: USA", "Wasatch Fault Zone: USA", "Hayward Fault: USA", "New Madrid Seismic Zone USA", "Wasatch Fault Zone USA", "Hayward Fault California USA",
"Guerrero Gap: Mexico", "Cocos Plate Subduction Zone: Mexico", "Motagua Fault Zone: Guatemala", "Guerrero Gap Mexico", "Cocos Plate Subduction Zone Mexico", "Motagua Fault Zone Guatemala",
"Caribbean Plate Boundary: Jamaica", "Los Angeles Basin: USA", "Seattle Tacoma Fault Zone: USA", "Caribbean North American Plate Boundary", "Los Angeles Basin USA", "Seattle Tacoma Fault Zone USA",
"San Juan Fault Zone: Argentina", "Nazca Ridge Subduction: Peru", "Cotopaxi Region: Ecuador", "San Juan Fault Zone Argentina", "Nazca Ridge Subduction Peru", "Cotopaxi Region Ecuador",
"Colombian Andes Plate Boundary: Colombia", "Atacama Fault Zone: Chile", "Cape Fold Belt: South Africa", "Colombian Andes Plate Boundary Colombia", "Atacama Fault Zone Chile", "Cape Fold Belt South Africa",
"Alpine Fault: New Zealand", "Hikurangi Subduction Zone: New Zealand", "Papua Fold Belt: Papua New Guinea", "Alpine Fault New Zealand", "Hikurangi Subduction Zone New Zealand", "Papua Fold Belt Papua New Guinea",
"Nias Islands Earthquake Zone: Indonesia", "Java Trench: Indonesia", "Banda Arc: Indonesia", "Nias Islands Earthquake Zone Indonesia", "Java Trench Indonesia", "Banda Arc Indonesia",
"Sumatra Andaman Megathrust: Indonesia", "Kashmir Region: India", "Himalayan Subduction Zone: Nepal", "Sumatra Andaman Megathrust Indonesia", "Kashmir Region India", "Himalayan Subduction Zone Nepal",
"Chiang Mai Rift Zone: Thailand", "Active Faults: Myanmar", "Red River Fault Zone: Vietnam", "Chiang Mai Rift Zone Thailand", "Active Faults in Myanmar", "Red River Fault Zone Vietnam",
"Taiwan Collision Zone: Taiwan", "Ryukyu Trench: Japan", "Kanto Region Fault: Japan", "Taiwan Collision Zone Taiwan", "Ryukyu Trench Japan", "Kanto Region Fault Japan",
"Kyushu Subduction Zone: Japan", "Kuril Kamchatka Trench: Russia", "Lake Baikal Rift Zone: Russia", "Kyushu Subduction Zone Japan", "Kuril Kamchatka Trench Russia", "Lake Baikal Rift Zone Russia",
"Berbera Rift System: Somalia", "Armenian Highlands: Armenia", "Zagros Mountains Fault: Iran", "Berbera Rift System Somalia", "Armenian Highlands Collision Zone Armenia", "Zagros Mountains Fault Iran",
"Makran Subduction Zone: Pakistan", "Hindu Kush Earthquake Belt: Afghanistan", "Caspian Sea Collision Zone: Iran", "Makran Subduction Zone Pakistan", "Hindu Kush Earthquake Belt Afghanistan", "Caspian Sea Collision Zone Iran",
"Jordan Rift Valley: Jordan", "Dead Sea Fault Zone: Israel", "Eastern Anatolian Fault: Turkey", "Jordan Rift Valley Israel", "Dead Sea Fault Zone Israel", "Eastern Anatolian Fault Turkey",
"Hellenic Arc: Greece", "Mediterranean Subduction Complex: Italy", "Pyrenees Fault System: Spain", "Hellenic Arc Greece", "Mediterranean Subduction Complex Italy", "Pyrenees Fault System Spain",
"Aegean Seismic Zone: Greece", "Alborz Mountains Fault Zone: Iran", "Ligurian Alps: Italy", "Aegean Seismic Zone Greece", "Alborz Mountains Fault Zone Iran", "Ligurian Alps Italy",
"Iceland Seismic Zone: Iceland", "Mid Atlantic Ridge: Iceland", "Iceland Seismic Zone Iceland", "Mid Atlantic Ridge Atlantic Ocean", "Azores Triple Junction Portugal",
"Azores Triple Junction: Portugal", "Reykjanes Ridge: Iceland", "Scandinavian Fault Zone: Norway", "Reykjanes Ridge Iceland", "Scandinavian Fault Zone Norway", "Barents Sea Rift Norway",
"Barents Sea Rift: Norway", "East African Rift: Ethiopia", "South Madagascar Seismic Zone: Madagascar", "East African Rift Ethiopia", "South Madagascar Seismic Zone Madagascar", "Cape Verde Rift Atlantic",
"Cape Verde Rift: Cape Verde", "Victoria Seismic Belt: Australia", "Victoria Seismic Belt Australia", "Bismarck Plate Subduction Zone Papua New Guinea",
"Bismarck Plate Subduction Zone: Papua New Guinea", "Fiji Plate Boundary: Fiji", "Fiji Plate Boundary Pacific Ocean", "Solomon Islands Seismic Zone Solomon Islands",
"Solomon Islands Seismic Zone: Solomon Islands", "New Hebrides Subduction: Vanuatu", "New Hebrides Subduction Vanuatu", "Tonga Kermadec Arc Tonga", "Samoa Seismic Zone Pacific Ocean",
"Tonga Kermadec Arc: Tonga", "Samoa Seismic Zone: Samoa", "South Sandwich Plate Collision Zone South Sandwich Islands", "Drake Passage Convergence Zone Antarctica",
"South Sandwich Plate Collision Zone: South Georgia and the South Sandwich Islands", "Scotia Plate Boundary Antarctica", "Antarctic Seismic Belt Antarctica", "Ross Sea Fault Zone Antarctica",
"Drake Passage Convergence Zone: Argentina", "Scotia Plate Boundary: Argentina", "Carlsberg Ridge Indian Ocean", "East Pacific Rise Pacific Ocean", "Indian Ocean Ridge Indian Ocean",
"Antarctic Seismic Belt: Antarctica", "Ross Sea Fault Zone: Antarctica", "Carlsberg Ridge: Maldives", "Macquarie Plate Boundary Australia", "Chagos Laccadive Ridge Indian Ocean", "Moho Tectonic Zone Ethiopia",
"East Pacific Rise: Chile", "Indian Ocean Ridge: Mauritius", "Macquarie Plate Boundary: Australia", "Azores Cape Verde Fault Line Atlantic Ocean", "South Shetland Trench Antarctica",
"Chagos Laccadive Ridge: Maldives", "Moho Tectonic Zone: Ethiopia", "Luale Tectonic Boundary Angola", "Banda Sea Subduction Zone Indonesia",
"Azores Cape Verde Fault Line: Portugal", "South Shetland Trench: Antarctica", "Guinea Ridge Zone Guinea", "Mauritius Seismic Area Indian Ocean", "Moluccas Sea Plate Collision Indonesia",
"Luale Tectonic Boundary: Angola", "Banda Sea Subduction Zone: Indonesia", "Yucatan Fault Zone Central America", "Offshore Nicaragua Subduction Zone Nicaragua",
"Guinea Ridge Zone: Guinea", "Mauritius Seismic Area: Mauritius", "Moluccas Sea Plate Collision: Indonesia", "Central Honduras Earthquake Belt Honduras", "Puerto Rico Trench Caribbean Plate",
"Yucatan Fault Zone: Mexico", "Offshore Nicaragua Subduction Zone: Nicaragua", "Trinidad Seismic Zone Trinidad and Tobago", "Barbadian Subduction Area Barbados",
"Central Honduras Earthquake Belt: Honduras", "Puerto Rico Trench: Puerto Rico", "Northern Andes Seismic Belt Venezuela", "South Atlantic Rift Brazil", "Acre Seismic Boundary Brazil",
"Trinidad Seismic Zone: Trinidad and Tobago", "Barbadian Subduction Area: Barbados", "Rio Grande Rift Zone USA", "Offshore Baja California USA", "Guarare Seismic Region Panama",
"Northern Andes Seismic Belt: Venezuela", "South Atlantic Rift: Brazil", "Offshore Vancouver Island Subduction Canada", "Yellowstone Volcanic Zone USA",
"Acre Seismic Boundary: Brazil", "Rio Grande Rift Zone: USA", "Offshore Baja California: Mexico", "Adelaide Fold Belt Australia", "Tasman Plate Boundary New Zealand", "Offshore Queensland Australia",
"Guarare Seismic Region: Panama", "Offshore Vancouver Island Subduction: Canada", "Gansu Fault Zone China", "Xian Seismic Belt China", "Tibet Rift Zone China",
"Yellowstone Volcanic Zone: USA" "Chengdu Seismic Zone China", "Fujian Fault Zone China", "Jiuzhaigou Seismic Area China",
"Karakoram Fault Zone India", "Andaman Nicobar Subduction India", "Mumbai Rift Zone India",
"Cape York Seismic Zone Papua New Guinea", "Merewether Fault Australia", "Gulf of Aden Rift Zone",
"Oman Subduction Zone", "Ras Al Khaimah Fault Zone UAE", "Djibouti Rift Zone Africa",
"Mogadishu Seismic Zone Somalia", "Mozambique Channel Rift Mozambique", "Botswana Seismic Zone Africa",
"Victoria Lake Microplate Africa", "Nairobi Rift Axis Kenya", "Sumba Island Subduction Zone Indonesia"
] ]
types = ["tectonic", "volcanic", "collapse", "explosion"]
type_prefix = {"tectonic": "ET", "volcanic": "EV", "collapse": "EC", "explosion": "EE"}
start = datetime.datetime.strptime(start_date, '%Y-%m-%d') start = datetime.datetime.strptime(start_date, '%Y-%m-%d')
end = datetime.datetime.strptime(end_date, '%Y-%m-%d') end = datetime.datetime.strptime(end_date, '%Y-%m-%d')
delta = end - start delta = end - start
earthquake_list = [] earthquake_list = []
unique_counter = 1 # For sequential unique codes
for i in range(delta.days + 1): for i in range(delta.days + 1):
date = start + datetime.timedelta(days=i) date = start + datetime.timedelta(days=i)
@ -60,16 +63,9 @@ def generate_earthquake_data(start_date, end_date, file_name):
lat = round(random.uniform(-90, 90), 4) lat = round(random.uniform(-90, 90), 4)
lon = round(random.uniform(-180, 180), 4) lon = round(random.uniform(-180, 180), 4)
location = random.choice(locations) location = random.choice(locations)
country = location.split(": ")[1]
place = location.split(": ")[0]
depth = random.randint(5, 150) depth = random.randint(5, 150)
eq_type = random.choice(types) # Format data using comma-separated values (CSV)
prefix = type_prefix[eq_type] earthquake_list.append(f"{date.date()},{magnitude},{lat},{lon},{location},{depth} km")
code = f"{prefix}-{magnitude}-{country}-{unique_counter:05d}"
# Output: Date,Code,Magnitude,Type,Lat,Lon,Location,Depth
earthquake_list.append(f"{date.date()},{code},{magnitude},{eq_type},{lat},{lon},{place},{depth} km")
unique_counter += 1
with open(file_name, "w") as file: with open(file_name, "w") as file:
file.write("\n".join(earthquake_list)) file.write("\n".join(earthquake_list))

View File

@ -1,3 +1,4 @@
Name,Location,Latitude,Longitude,Date Established,Functional
Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes
Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes
Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes
1 Pacific Apex Seismic Center Name Aleutian Trench, Alaska, USA Location 53.0000 Latitude -168.0000 Longitude 1973-06-15 Date Established Yes Functional
1 Name Location Latitude Longitude Date Established Functional
2 Pacific Apex Seismic Center Pacific Apex Seismic Center Aleutian Trench, Alaska, USA Aleutian Trench, Alaska, USA 53.0000 53.0000 -168.0000 -168.0000 1973-06-15 1973-06-15 Yes
3 Cascadia Quake Research Institute Cascadia Quake Research Institute Oregon Coast, USA Oregon Coast, USA 44.5000 44.5000 -124.0000 -124.0000 1985-03-22 1985-03-22 Yes
4 Andes Fault Survey Observatory Andes Fault Survey Observatory Nazca-South American Plate, Santiago, Chile Nazca-South American Plate, Santiago, Chile -33.4500 -33.4500 -70.6667 -70.6667 1992-10-10 1992-10-10 Yes

View File

@ -1,19 +1,21 @@
Dr. Emily Neighbour Carter,Junior,Dr. Rajiv Menon Name,Level,Superior
Dr. Emily Neighbour Carter,Senior,None
Dr. Rajiv Menon,Senior,None Dr. Rajiv Menon,Senior,None
Dr. Izzy Patterson,Senior,None Dr. Izzy Patterson,Senior,None
Dr. Hiroshi Takeda,Senior,None Dr. Hiroshi Takeda,Senior,None
Dr. Miriam Hassan,Senior,None Dr. Miriam Hassan,Senior,None
Dr. Alice Johnson,Senior,None Dr. Alice Johnson,Junior,Dr. Emily Neighbour
Tim Howitz,Admin,None Dr. Tim Howitz,Junior,Dr. Rajiv Menon
Dr. Natalia Petrova,Junior,Dr. Izzy Patteron Dr. Natalia Petrova,Junior,Dr. Izzy Patteron
Dr. Li Cheng,Junior,Dr. Rajiv Menon Dr. Li Cheng,Junior,Dr. Rajiv Menon
Dr. Javier Ortega,Junior,Dr. Izzy Patterson Dr. Javier Ortega,Junior,Dr. Izzy Patterson
Dr. Priya Sharma,Junior,Dr. Hiroshi Takeda Dr. Priya Sharma,Junior,Dr. Hiroshi Takeda
Dr. Lukeshan Thananchayan,Junior,Dr. Miriam Hassan Dr. Lukeshan Thananchayan,Junior,Dr. Miriam Hassan
Dr. Elena Fischer,Junior,Dr. Alice Johnson Dr. Elena Fischer,Junior,Dr. Emily Neighbour
Dr. Mohammed Al-Farsi,Junior,Dr. Miriam Hassan Dr. Mohammed Al-Farsi,Junior,Dr. Miriam Hassan
Dr. Jane Wong,Junior,Dr. Hiroshi Takeda Dr. Jane Wong,Junior,Dr. Hiroshi Takeda
Dr. Carlos Gutierrez,Junior,Dr. Rajiv Menon Dr. Carlos Gutierrez,Junior,Dr. Rajiv Menon
Dr. Fiona MacLeod,Junior,Dr. Emily Neighbour
Dr. Wei Zhao,Junior,Dr. Miriam Hassan Dr. Wei Zhao,Junior,Dr. Miriam Hassan
Dr. Antonio Rosales,Junior,Dr. Izzy Patterson Dr. Antonio Rosales,Junior,Dr. Izzy Patterson
Dr. Kate Wilson,Junior,Dr. Hiroshi Takeda Dr. Kate Wilson,Junior,Dr. Hiroshi Takeda
1 Dr. Emily Neighbour Carter Name Junior Level Dr. Rajiv Menon Superior
2 Dr. Emily Neighbour Carter Senior None
3 Dr. Rajiv Menon Dr. Rajiv Menon Senior None None
4 Dr. Izzy Patterson Dr. Izzy Patterson Senior None None
5 Dr. Hiroshi Takeda Dr. Hiroshi Takeda Senior None None
6 Dr. Miriam Hassan Dr. Miriam Hassan Senior None None
7 Dr. Alice Johnson Dr. Alice Johnson Senior Junior None Dr. Emily Neighbour
8 Tim Howitz Dr. Tim Howitz Admin Junior None Dr. Rajiv Menon
9 Dr. Natalia Petrova Dr. Natalia Petrova Junior Dr. Izzy Patteron Dr. Izzy Patteron
10 Dr. Li Cheng Dr. Li Cheng Junior Dr. Rajiv Menon Dr. Rajiv Menon
11 Dr. Javier Ortega Dr. Javier Ortega Junior Dr. Izzy Patterson Dr. Izzy Patterson
12 Dr. Priya Sharma Dr. Priya Sharma Junior Dr. Hiroshi Takeda Dr. Hiroshi Takeda
13 Dr. Lukeshan Thananchayan Dr. Lukeshan Thananchayan Junior Dr. Miriam Hassan Dr. Miriam Hassan
14 Dr. Elena Fischer Dr. Elena Fischer Junior Dr. Alice Johnson Dr. Emily Neighbour
15 Dr. Mohammed Al-Farsi Dr. Mohammed Al-Farsi Junior Dr. Miriam Hassan Dr. Miriam Hassan
16 Dr. Jane Wong Dr. Jane Wong Junior Dr. Hiroshi Takeda Dr. Hiroshi Takeda
17 Dr. Carlos Gutierrez Dr. Carlos Gutierrez Junior Dr. Rajiv Menon Dr. Rajiv Menon
18 Dr. Fiona MacLeod Junior Dr. Emily Neighbour
19 Dr. Wei Zhao Dr. Wei Zhao Junior Dr. Miriam Hassan Dr. Miriam Hassan
20 Dr. Antonio Rosales Dr. Antonio Rosales Junior Dr. Izzy Patterson Dr. Izzy Patterson
21 Dr. Kate Wilson Dr. Kate Wilson Junior Dr. Hiroshi Takeda Dr. Hiroshi Takeda

View File

@ -1,4 +1,4 @@
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); const prisma = new PrismaClient();

View File

@ -25,7 +25,6 @@
"@utils/*": ["./src/utils/*"], "@utils/*": ["./src/utils/*"],
"@appTypes/*": ["./src/types/*"], "@appTypes/*": ["./src/types/*"],
"@zod/*": ["./src/zod/*"], "@zod/*": ["./src/zod/*"],
"@prismaclient": ["./src/generated/prisma/client"],
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },