Compare commits

..

25 Commits

Author SHA1 Message Date
3b2927e896 Merge branch 'master' of 192.168.11.4:thowitz-work/tremor-tracker 2025-05-19 17:06:49 +01:00
8bd3b10368 Fixed some errors 2025-05-19 15:38:24 +01:00
7773801de4 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-19 14:58:35 +01:00
64338d8be7 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-19 14:58:32 +01:00
66986654dd Added todo instructions to import route files 2025-05-19 14:58:08 +01:00
IZZY
48fa0fa7a4 Imagespart1 2025-05-19 14:56:42 +01:00
IZZY
f26dbb465a Merge and added artefact name 2025-05-19 14:41:14 +01:00
1bd327ea1a Switched to local prismaclient 2025-05-19 14:36:29 +01:00
IZZY
de02f94f6a Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-19 13:57:33 +01:00
IZZY
8599ad885f Updated schema 2025-05-19 13:57:27 +01:00
Lukeshan Thananchayan
bd3a7bae27 reverted to user only admin page
user page still requires prisma dependant bits
2025-05-19 13:56:19 +01:00
IZZY
360ca52ed6 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-19 13:38:55 +01:00
Emily Neighbour
80113329ef Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-19 13:32:23 +01:00
Emily Neighbour
662c1cb0d4 modal pop-up's when buying 2025-05-19 13:32:20 +01:00
IZZY
8fa8f6c5cd earthquake file updated 2025-05-19 13:25:10 +01:00
IZZY
3c31b2187b route file no longer uses enums 2025-05-19 13:00:10 +01:00
c407d26e8b Modified Artefact and added Pallet models 2025-05-19 12:51:46 +01:00
IZZY
376f6500d5 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-19 12:42:25 +01:00
IZZY
46411667d6 imports 2025-05-19 12:42:13 +01:00
Lukeshan Thananchayan
24de12e481 d 2025-05-19 12:28:38 +01:00
IZZY
30a9f38888 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-19 12:28:19 +01:00
Emily Neighbour
166f48a3da artifact icon fix 2025-05-18 22:28:57 +01:00
IZZY
20a6034f07 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-17 21:05:21 +01:00
IZZY
f25b8789c0 Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker 2025-05-13 12:07:32 +01:00
IZZY
db3e0f958b All the route files 2025-05-12 14:19:52 +01:00
35 changed files with 2695 additions and 4271 deletions

1
.gitignore vendored
View File

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

View File

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

1140
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,6 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/bcryptjs": "^5.0.2",
"@types/express": "^5.0.1",
"@types/node": "^20",
"@types/react": "^19",

View File

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

31
public/Artefacts.csv Normal file
View File

@ -0,0 +1,31 @@
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

View File

@ -1,4 +1,3 @@
Name,Location,Latitude,Longitude,Date Established,Functional
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
Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes
1 Name Pacific Apex Seismic Center Location Aleutian Trench, Alaska, USA Latitude 53.0000 Longitude -168.0000 Date Established 1973-06-15 Functional Yes
Name Location Latitude Longitude Date Established Functional
1 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
2 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
3 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,21 +1,19 @@
Name,Level,Superior
Dr. Emily Neighbour Carter,Senior,None
Dr. Emily Neighbour Carter,Junior,Dr. Rajiv Menon
Dr. Rajiv Menon,Senior,None
Dr. Izzy Patterson,Senior,None
Dr. Hiroshi Takeda,Senior,None
Dr. Miriam Hassan,Senior,None
Dr. Alice Johnson,Junior,Dr. Emily Neighbour
Dr. Tim Howitz,Junior,Dr. Rajiv Menon
Dr. Alice Johnson,Senior,None
Tim Howitz,Admin,None
Dr. Natalia Petrova,Junior,Dr. Izzy Patteron
Dr. Li Cheng,Junior,Dr. Rajiv Menon
Dr. Javier Ortega,Junior,Dr. Izzy Patterson
Dr. Priya Sharma,Junior,Dr. Hiroshi Takeda
Dr. Lukeshan Thananchayan,Junior,Dr. Miriam Hassan
Dr. Elena Fischer,Junior,Dr. Emily Neighbour
Dr. Elena Fischer,Junior,Dr. Alice Johnson
Dr. Mohammed Al-Farsi,Junior,Dr. Miriam Hassan
Dr. Jane Wong,Junior,Dr. Hiroshi Takeda
Dr. Carlos Gutierrez,Junior,Dr. Rajiv Menon
Dr. Fiona MacLeod,Junior,Dr. Emily Neighbour
Dr. Wei Zhao,Junior,Dr. Miriam Hassan
Dr. Antonio Rosales,Junior,Dr. Izzy Patterson
Dr. Kate Wilson,Junior,Dr. Hiroshi Takeda
1 Name Dr. Emily Neighbour Carter Level Junior Superior Dr. Rajiv Menon
Dr. Emily Neighbour Carter Senior None
2 Dr. Rajiv Menon Dr. Rajiv Menon Senior None None
3 Dr. Izzy Patterson Dr. Izzy Patterson Senior None None
4 Dr. Hiroshi Takeda Dr. Hiroshi Takeda Senior None None
5 Dr. Miriam Hassan Dr. Miriam Hassan Senior None None
6 Dr. Alice Johnson Dr. Alice Johnson Junior Senior Dr. Emily Neighbour None
7 Dr. Tim Howitz Tim Howitz Junior Admin Dr. Rajiv Menon None
8 Dr. Natalia Petrova Dr. Natalia Petrova Junior Dr. Izzy Patteron Dr. Izzy Patteron
9 Dr. Li Cheng Dr. Li Cheng Junior Dr. Rajiv Menon Dr. Rajiv Menon
10 Dr. Javier Ortega Dr. Javier Ortega Junior Dr. Izzy Patterson Dr. Izzy Patterson
11 Dr. Priya Sharma Dr. Priya Sharma Junior Dr. Hiroshi Takeda Dr. Hiroshi Takeda
12 Dr. Lukeshan Thananchayan Dr. Lukeshan Thananchayan Junior Dr. Miriam Hassan Dr. Miriam Hassan
13 Dr. Elena Fischer Dr. Elena Fischer Junior Dr. Emily Neighbour Dr. Alice Johnson
14 Dr. Mohammed Al-Farsi Dr. Mohammed Al-Farsi Junior Dr. Miriam Hassan Dr. Miriam Hassan
15 Dr. Jane Wong Dr. Jane Wong Junior Dr. Hiroshi Takeda Dr. Hiroshi Takeda
16 Dr. Carlos Gutierrez Dr. Carlos Gutierrez Junior Dr. Rajiv Menon Dr. Rajiv Menon
Dr. Fiona MacLeod Junior Dr. Emily Neighbour
17 Dr. Wei Zhao Dr. Wei Zhao Junior Dr. Miriam Hassan Dr. Miriam Hassan
18 Dr. Antonio Rosales Dr. Antonio Rosales Junior Dr. Izzy Patterson Dr. Izzy Patterson
19 Dr. Kate Wilson Dr. Kate Wilson Junior Dr. Hiroshi Takeda Dr. Hiroshi Takeda

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,70 @@
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,18 +1,19 @@
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import fs from "fs/promises";
import path from "path";
import { parse } from "csv-parse/sync";
import fs from "fs/promises";
import { NextResponse } from "next/server";
import path from "path";
// Define the path to your CSV file.
// Place your earthquakes.csv in your project root or `public` directory
import { PrismaClient } from "@prismaclient";
// Path to your earthquakes.csv
const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv");
const prisma = new PrismaClient();
type CsvRow = {
Date: string;
Code: string;
Magnitude: string;
Type: string;
Latitude: string;
Longitude: string;
Location: string;
@ -23,36 +24,28 @@ 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
skip_empty_lines: true,
});
// 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 {
// 3. Transform to fit Earthquake model
const earthquakes = records.map((row) => ({
date: new Date(row.Date),
location: row.Location,
code: row.Code,
magnitude: parseFloat(row.Magnitude),
latitude: row.Latitude,
longitude: row.Longitude,
depth: parseFloat(row.Depth.replace(" km", "")),
// todo add creatorId
creatorId: null
};
});
type: row.Type,
latitude: parseFloat(row.Latitude),
longitude: parseFloat(row.Longitude),
location: row.Location,
depth: row.Depth, // store as received
// todo add random selection for creatorId
creatorId: null,
}));
// 4. Bulk create earthquakes in database:
// Consider chunking if your CSV is large!
await prisma.earthquake.createMany({
data: earthquakes,
skipDuplicates: true, // in case the route is called twice
});
return NextResponse.json({ success: true, count: earthquakes.length });
} catch (error: any) {
console.error(error);

View File

@ -0,0 +1,67 @@
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

@ -0,0 +1,62 @@
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

@ -0,0 +1,59 @@
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

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

View File

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

View File

@ -1,13 +1,11 @@
import bcryptjs from 'bcryptjs';
import { SignJWT } from 'jose';
import { NextResponse } from 'next/server';
import bcryptjs from "bcryptjs";
import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { PrismaClient } from '@prisma/client';
import { env } from '@utils/env';
import { PrismaClient } from "@prismaclient";
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;
let prisma: PrismaClient;

View File

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

View File

@ -39,7 +39,7 @@ export default function Home() {
href="/shop"
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="/artefactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" />
<Image height={100} width={100} src="/artifactIcon.jpg" alt="Technology Icon" className="h-40 w-40 mb-4" />
<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">
View or purchase recently discovered artefacts from seismic events

View File

@ -189,6 +189,11 @@ const artefacts: Artefact[] = [
export default function Shop() {
const [currentPage, setCurrentPage] = useState(1);
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 indexOfLastArtefact = currentPage * artefactsPerPage;
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
@ -198,25 +203,41 @@ export default function Shop() {
const conversionRates = useStoreState((state) => state.currency.conversionRates);
const currencyTickers = useStoreState((state) => state.currency.tickers);
const convertPrice = useCallback((price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2), []);
const convertPrice = useCallback(
(price: number, currency: Currency) => (price * conversionRates[currency]).toFixed(2),
[conversionRates]
);
const handleNextPage = () => {
if (indexOfLastArtefact < artefacts.length) {
setCurrentPage((prev) => prev + 1);
}
if (indexOfLastArtefact < artefacts.length) setCurrentPage((prev) => prev + 1);
};
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 }) {
if (!artefact) return null;
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) {
setSelectedArtefact(null);
}
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) setSelectedArtefact(null);
};
return (
<div
@ -226,10 +247,10 @@ export default function Shop() {
<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>
<Image
height={5000}
width={5000}
src={artefact.image}
alt={artefact.name}
width={500}
height={300}
className="w-full h-64 object-cover rounded-md"
/>
<p className="text-xl font-bold">
@ -243,7 +264,11 @@ export default function Shop() {
<p className="text-neutral-500 mb-2">{artefact.dateReleased}</p>
<div className="flex justify-end gap-4 mt-4 mr-2">
<button
onClick={() => alert("Purchased Successfully!")}
onClick={() => {
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"
>
Buy
@ -254,26 +279,156 @@ export default function Shop() {
);
}
function ArtefactCard({ artefact }: { artefact: Artefact }) {
function PaymentModal({ artefact, onClose }: { artefact: Artefact; onClose: () => void }) {
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 (
<div
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
onClick={() => setSelectedArtefact(artefact)}
className="fixed inset-0 bg-neutral-900 bg-opacity-60 flex justify-center items-center z-10"
onClick={handleOverlayClick}
>
<img src={artefact.image} alt={artefact.name} 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 className="bg-white rounded-xl shadow-2xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold mb-4">Buy {artefact.name}</h2>
{/* ...Image... */}
<form
onSubmit={(e) => {
e.preventDefault();
handlePay();
}}
>
<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>
);
}
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 (
<div
className="min-h-screen relative flex flex-col"
@ -283,10 +438,8 @@ export default function Shop() {
backgroundPosition: "center",
}}
>
{/* Overlay */}
<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">
{/* Title & Subheading */}
<h1 className="text-4xl md:text-4xl font-bold text-center text-white mb-2 tracking-tight drop-shadow-lg">
Artefact Shop
</h1>
@ -294,17 +447,11 @@ export default function Shop() {
Discover extraordinary historical artefacts and collectibles from major seismic events from around the world - now
available for purchase.
</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">
{" "}
{/* gap-10 for more spacing */}
{currentArtefacts.map((artefact) => (
<ArtefactCard key={artefact.id} artefact={artefact} />
))}
</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">
<button
onClick={handlePreviousPage}
@ -327,9 +474,19 @@ export default function Shop() {
</button>
</footer>
</div>
{/* Modal */}
{selectedArtefact && <Modal artefact={selectedArtefact} />}
{artefactToBuy && showPaymentModal && (
<PaymentModal
artefact={artefactToBuy}
onClose={() => {
setShowPaymentModal(false);
setArtefactToBuy(null);
}}
/>
)}
{showThankYouModal && orderNumber && (
<ThankYouModal orderNumber={orderNumber} onClose={() => setShowThankYouModal(false)} />
)}
</div>
);
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,59 +2,56 @@ import random
import datetime
def generate_earthquake_data(start_date, end_date, file_name):
# List of 200 real-world seismic locations, reformatted to remove commas
locations = [
"San Andreas Fault USA", "Cascade Range USA", "Denali Fault System Alaska 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",
"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",
"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",
"Nias Islands Earthquake Zone Indonesia", "Java Trench Indonesia", "Banda Arc Indonesia",
"Sumatra Andaman Megathrust Indonesia", "Kashmir Region India", "Himalayan Subduction Zone Nepal",
"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",
"Kyushu Subduction Zone Japan", "Kuril Kamchatka Trench Russia", "Lake Baikal Rift Zone Russia",
"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",
"Jordan Rift Valley Israel", "Dead Sea Fault Zone Israel", "Eastern Anatolian Fault Turkey",
"Hellenic Arc Greece", "Mediterranean Subduction Complex Italy", "Pyrenees Fault System Spain",
"Aegean Seismic Zone Greece", "Alborz Mountains Fault Zone Iran", "Ligurian Alps Italy",
"Iceland Seismic Zone Iceland", "Mid Atlantic Ridge Atlantic Ocean", "Azores Triple Junction Portugal",
"Reykjanes Ridge Iceland", "Scandinavian Fault Zone Norway", "Barents Sea Rift Norway",
"East African Rift Ethiopia", "South Madagascar Seismic Zone Madagascar", "Cape Verde Rift Atlantic",
"Victoria Seismic Belt Australia", "Bismarck Plate Subduction Zone Papua New Guinea",
"Fiji Plate Boundary Pacific Ocean", "Solomon Islands Seismic Zone Solomon Islands",
"New Hebrides Subduction Vanuatu", "Tonga Kermadec Arc Tonga", "Samoa Seismic Zone Pacific Ocean",
"South Sandwich Plate Collision Zone South Sandwich Islands", "Drake Passage Convergence Zone Antarctica",
"Scotia Plate Boundary Antarctica", "Antarctic Seismic Belt Antarctica", "Ross Sea Fault Zone Antarctica",
"Carlsberg Ridge Indian Ocean", "East Pacific Rise Pacific Ocean", "Indian Ocean Ridge Indian Ocean",
"Macquarie Plate Boundary Australia", "Chagos Laccadive Ridge Indian Ocean", "Moho Tectonic Zone Ethiopia",
"Azores Cape Verde Fault Line Atlantic Ocean", "South Shetland Trench Antarctica",
"Luale Tectonic Boundary Angola", "Banda Sea Subduction Zone Indonesia",
"Guinea Ridge Zone Guinea", "Mauritius Seismic Area Indian Ocean", "Moluccas Sea Plate Collision Indonesia",
"Yucatan Fault Zone Central America", "Offshore Nicaragua Subduction Zone Nicaragua",
"Central Honduras Earthquake Belt Honduras", "Puerto Rico Trench Caribbean Plate",
"Trinidad Seismic Zone Trinidad and Tobago", "Barbadian Subduction Area Barbados",
"Northern Andes Seismic Belt Venezuela", "South Atlantic Rift Brazil", "Acre Seismic Boundary Brazil",
"Rio Grande Rift Zone USA", "Offshore Baja California USA", "Guarare Seismic Region Panama",
"Offshore Vancouver Island Subduction Canada", "Yellowstone Volcanic Zone USA",
"Adelaide Fold Belt Australia", "Tasman Plate Boundary New Zealand", "Offshore Queensland Australia",
"Gansu Fault Zone China", "Xian Seismic Belt China", "Tibet Rift Zone China",
"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"
"San Andreas Fault: USA", "Cascade Range: USA", "Denali Fault System: USA",
"New Madrid Seismic Zone: USA", "Wasatch Fault Zone: USA", "Hayward Fault: USA",
"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",
"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",
"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",
"Sumatra Andaman Megathrust: Indonesia", "Kashmir Region: India", "Himalayan Subduction Zone: Nepal",
"Chiang Mai Rift Zone: Thailand", "Active Faults: Myanmar", "Red River Fault Zone: Vietnam",
"Taiwan Collision Zone: Taiwan", "Ryukyu Trench: Japan", "Kanto Region Fault: Japan",
"Kyushu Subduction Zone: Japan", "Kuril Kamchatka Trench: Russia", "Lake Baikal Rift Zone: Russia",
"Berbera Rift System: Somalia", "Armenian Highlands: Armenia", "Zagros Mountains Fault: 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",
"Hellenic Arc: Greece", "Mediterranean Subduction Complex: Italy", "Pyrenees Fault System: Spain",
"Aegean Seismic Zone: Greece", "Alborz Mountains Fault Zone: Iran", "Ligurian Alps: Italy",
"Iceland Seismic Zone: Iceland", "Mid Atlantic Ridge: Iceland",
"Azores Triple Junction: Portugal", "Reykjanes Ridge: Iceland", "Scandinavian Fault Zone: Norway",
"Barents Sea Rift: Norway", "East African Rift: Ethiopia", "South Madagascar Seismic Zone: Madagascar",
"Cape Verde Rift: Cape Verde", "Victoria Seismic Belt: Australia",
"Bismarck Plate Subduction Zone: Papua New Guinea", "Fiji Plate Boundary: Fiji",
"Solomon Islands Seismic Zone: Solomon Islands", "New Hebrides Subduction: Vanuatu",
"Tonga Kermadec Arc: Tonga", "Samoa Seismic Zone: Samoa",
"South Sandwich Plate Collision Zone: South Georgia and the South Sandwich Islands",
"Drake Passage Convergence Zone: Argentina", "Scotia Plate Boundary: Argentina",
"Antarctic Seismic Belt: Antarctica", "Ross Sea Fault Zone: Antarctica", "Carlsberg Ridge: Maldives",
"East Pacific Rise: Chile", "Indian Ocean Ridge: Mauritius", "Macquarie Plate Boundary: Australia",
"Chagos Laccadive Ridge: Maldives", "Moho Tectonic Zone: Ethiopia",
"Azores Cape Verde Fault Line: Portugal", "South Shetland Trench: Antarctica",
"Luale Tectonic Boundary: Angola", "Banda Sea Subduction Zone: Indonesia",
"Guinea Ridge Zone: Guinea", "Mauritius Seismic Area: Mauritius", "Moluccas Sea Plate Collision: Indonesia",
"Yucatan Fault Zone: Mexico", "Offshore Nicaragua Subduction Zone: Nicaragua",
"Central Honduras Earthquake Belt: Honduras", "Puerto Rico Trench: Puerto Rico",
"Trinidad Seismic Zone: Trinidad and Tobago", "Barbadian Subduction Area: Barbados",
"Northern Andes Seismic Belt: Venezuela", "South Atlantic Rift: Brazil",
"Acre Seismic Boundary: Brazil", "Rio Grande Rift Zone: USA", "Offshore Baja California: Mexico",
"Guarare Seismic Region: Panama", "Offshore Vancouver Island Subduction: Canada",
"Yellowstone Volcanic Zone: USA"
]
types = ["tectonic", "volcanic", "collapse", "explosion"]
type_prefix = {"tectonic": "ET", "volcanic": "EV", "collapse": "EC", "explosion": "EE"}
start = datetime.datetime.strptime(start_date, '%Y-%m-%d')
end = datetime.datetime.strptime(end_date, '%Y-%m-%d')
delta = end - start
earthquake_list = []
unique_counter = 1 # For sequential unique codes
for i in range(delta.days + 1):
date = start + datetime.timedelta(days=i)
@ -63,9 +60,16 @@ def generate_earthquake_data(start_date, end_date, file_name):
lat = round(random.uniform(-90, 90), 4)
lon = round(random.uniform(-180, 180), 4)
location = random.choice(locations)
country = location.split(": ")[1]
place = location.split(": ")[0]
depth = random.randint(5, 150)
# Format data using comma-separated values (CSV)
earthquake_list.append(f"{date.date()},{magnitude},{lat},{lon},{location},{depth} km")
eq_type = random.choice(types)
prefix = type_prefix[eq_type]
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:
file.write("\n".join(earthquake_list))

View File

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

View File

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