Merge branch 'master' of 192.168.11.4:thowitz-work/tremor-tracker
This commit is contained in:
commit
0a34470e65
6
importersFixed.md
Normal file
6
importersFixed.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
- [x] Import users
|
||||||
|
- [x] Import artefacts
|
||||||
|
- [x] Import earthquakes
|
||||||
|
- [x] Import observatories
|
||||||
|
- [ ] Import requests
|
||||||
|
- [x] Import scientists
|
||||||
6
importersOrder.md
Normal file
6
importersOrder.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
1. Import users
|
||||||
|
2. Import scientists
|
||||||
|
3. Import observatories
|
||||||
|
4. Import earthquakes
|
||||||
|
5. Import artefacts
|
||||||
|
6. Import requests
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -35,7 +35,7 @@
|
|||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-node": "^1.0.2",
|
"react-node": "^1.0.2",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.25.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -8095,9 +8095,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.0",
|
"version": "3.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.3.tgz",
|
||||||
"integrity": "sha512-ficnZKUW0mlNivqeJkosTEkGbJ6NKCtSaOHGx5aXbtfeWMdRyzXLbAIn19my4C/KB7WPY/p9vlGPt+qpOp6c4Q==",
|
"integrity": "sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-node": "^1.0.2",
|
"react-node": "^1.0.2",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.25.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|||||||
@ -70,9 +70,9 @@ model Observatory {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
name String
|
name String
|
||||||
location String
|
location String
|
||||||
longitude String
|
longitude Float
|
||||||
latitude String
|
latitude Float
|
||||||
dateEstablished Int?
|
dateEstablished DateTime
|
||||||
isFunctional Boolean
|
isFunctional Boolean
|
||||||
seismicSensorOnline Boolean @default(true)
|
seismicSensorOnline Boolean @default(true)
|
||||||
creatorId Int?
|
creatorId Int?
|
||||||
@ -88,7 +88,7 @@ model Artefact {
|
|||||||
type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
|
type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
|
||||||
warehouseArea String
|
warehouseArea String
|
||||||
description String
|
description String
|
||||||
imagePath String
|
imageName String
|
||||||
earthquakeId Int
|
earthquakeId Int
|
||||||
earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
|
earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
|
||||||
creatorId Int?
|
creatorId Int?
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
Name,Type,WarehouseArea,Description,earthquakeID,Price,Required,PickedUp,Picture
|
Name,Type,WarehouseArea,Description,EarthquakeCode,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
|
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
|
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
|
Strata Core,Soil,LoggingArea,Soil core with visible stratification showing evidence of liquefaction.,ET-6.9-Brazil-00046,30,no,no,StrataCore.PNG
|
||||||
|
|||||||
|
@ -1,25 +1,26 @@
|
|||||||
Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes
|
Name,Location,Latitude,Longitude,DateEstablished,Functional,SeismicSensorOnline
|
||||||
Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes
|
Pacific Apex Seismic Center,"Aleutian Trench, Alaska, USA",53.0000,-168.0000,1973-06-15,Yes,Yes
|
||||||
Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes
|
Cascadia Quake Research Institute,"Oregon Coast, USA",44.5000,-124.0000,1985-03-22,Yes,Yes
|
||||||
Kermadec Deep Motion Lab,"Kermadec Trench, northeast of New Zealand",-29.5000,-177.0000,2001-05-14,Yes
|
Andes Fault Survey Observatory,"Nazca-South American Plate, Santiago, Chile",-33.4500,-70.6667,1992-10-10,Yes,Yes
|
||||||
Japan Trench Monitoring Station,"Off the coast of Sendai, Japan",38.5000,143.0000,1968-09-01,Yes
|
Kermadec Deep Motion Lab,"Kermadec Trench, northeast of New Zealand",-29.5000,-177.0000,2001-05-14,Yes,Yes
|
||||||
Himalayan Rift Observatory,"Nepal-Tibet border, near Mount Everest",28.5000,86.5000,1998-12-08,Yes
|
Japan Trench Monitoring Station,"Off the coast of Sendai, Japan",38.5000,143.0000,1968-09-01,Yes,Yes
|
||||||
East African Rift Monitoring Center,"Rift Valley, near Nairobi, Kenya",-1.2921,36.8219,2010-03-30,Yes
|
Himalayan Rift Observatory,"Nepal-Tibet border, near Mount Everest",28.5000,86.5000,1998-12-08,Yes,Yes
|
||||||
Reykjavik Seismic Monitoring Hub,"Mid-Atlantic Ridge, Reykjavik, Iceland",64.1355,-21.8954,1957-11-17,Yes
|
East African Rift Monitoring Center,"Rift Valley, near Nairobi, Kenya",-1.2921,36.8219,2010-03-30,Yes,Yes
|
||||||
Azores Tectonic Research Base,"Azores Archipelago, Portugal",37.7412,-25.6756,1982-07-04,Yes
|
Reykjavik Seismic Monitoring Hub,"Mid-Atlantic Ridge, Reykjavik, Iceland",64.1355,-21.8954,1957-11-17,Yes,Yes
|
||||||
Sumatra Subduction Observatory,"West Coast of Indonesia, Aceh Province",4.6951,95.5539,2005-02-16,Yes
|
Azores Tectonic Research Base,"Azores Archipelago, Portugal",37.7412,-25.6756,1982-07-04,Yes,Yes
|
||||||
Tonga Trench Seismographic Unit,"Off the coast of Tonga",-20.0000,-175.0000,1994-08-21,Yes
|
Sumatra Subduction Observatory,"West Coast of Indonesia, Aceh Province",4.6951,95.5539,2005-02-16,Yes,Yes
|
||||||
San Andreas Fault Research Center,"San Francisco Bay Area, California, USA",37.7749,-122.4194,1929-10-07,Yes
|
Tonga Trench Seismographic Unit,"Off the coast of Tonga",-20.0000,-175.0000,1994-08-21,Yes,Yes
|
||||||
Kamchatka Seismic Laboratory,"Kamchatka Peninsula, Russia",56.0000,160.0000,1978-01-12,Yes
|
San Andreas Fault Research Center,"San Francisco Bay Area, California, USA",37.7749,-122.4194,1929-10-07,Yes,Yes
|
||||||
Hawaii Island Seismic Research Station,"Hawaii Big Island, USA",19.8968,-155.5828,1989-05-06,Yes
|
Kamchatka Seismic Laboratory,"Kamchatka Peninsula, Russia",56.0000,160.0000,1978-01-12,Yes,Yes
|
||||||
Cascadia Ridge Offshore Observatory,"Offshore of Vancouver Island, British Columbia, Canada",48.4284,-123.3656,2003-11-18,Yes
|
Hawaii Island Seismic Research Station,"Hawaii Big Island, USA",19.8968,-155.5828,1989-05-06,Yes,Yes
|
||||||
Zagros Fault Zone Research Center,"Western Iran, near Kermanshah",34.0000,46.0000,1990-06-28,Yes
|
Cascadia Ridge Offshore Observatory,"Offshore of Vancouver Island, British Columbia, Canada",48.4284,-123.3656,2003-11-18,Yes,Yes
|
||||||
Manila Trench Seismic Observatory,"South China Sea, west of Luzon, Philippines",15.0000,120.0000,1996-04-09,Yes
|
Zagros Fault Zone Research Center,"Western Iran, near Kermanshah",34.0000,46.0000,1990-06-28,Yes,Yes
|
||||||
Caribbean Subduction Monitoring Station,"Lesser Antilles Subduction Zone, Martinique",14.6000,-61.0000,2008-09-23,Yes
|
Manila Trench Seismic Observatory,"South China Sea, west of Luzon, Philippines",15.0000,120.0000,1996-04-09,Yes,Yes
|
||||||
Gorda Plate Analysis Hub,"Mendocino Triple Junction, California, USA",40.0000,-124.0000,1952-12-01,Yes
|
Caribbean Subduction Monitoring Station,"Lesser Antilles Subduction Zone, Martinique",14.6000,-61.0000,2008-09-23,Yes,Yes
|
||||||
Red Sea Rift Research Base,"Southern Red Sea, Eritrea",15.0000,42.0000,2015-07-15,Yes
|
Gorda Plate Analysis Hub,"Mendocino Triple Junction, California, USA",40.0000,-124.0000,1952-12-01,Yes,Yes
|
||||||
Sumatra Coastal Reserve Observatory,"West Coast of Sumatra, Indonesia",-2.0000,100.5000,1980-03-11,No
|
Red Sea Rift Research Base,"Southern Red Sea, Eritrea",15.0000,42.0000,2015-07-15,Yes,Yes
|
||||||
Antarctic Polar Seismology Station,"Rift vicinity near Ross Ice Shelf, Antarctica",-78.3000,-166.2500,1974-06-01,No
|
Sumatra Coastal Reserve Observatory,"West Coast of Sumatra, Indonesia",-2.0000,100.5000,1980-03-11,Yes,Yes
|
||||||
Yucatan Seismic Monitoring Site,"Cocos Plate near Yucatan Peninsula, Mexico",20.7000,-90.8000,1965-09-23,No
|
Antarctic Polar Seismology Station,"Rift vicinity near Ross Ice Shelf, Antarctica",-78.3000,-166.2500,1974-06-01,Yes,Yes
|
||||||
Makran Subduction Fault Observatory,"Coastal Pakistan and Iran junction",25.5000,62.0000,1990-08-29,No
|
Yucatan Seismic Monitoring Site,"Cocos Plate near Yucatan Peninsula, Mexico",20.7000,-90.8000,1965-09-23,Yes,Yes
|
||||||
Baltic Continental Drift Center,"Southeastern Sweden, Baltic Shield zone",56.0000,15.0000,1987-12-12,No
|
Makran Subduction Fault Observatory,"Coastal Pakistan and Iran junction",25.5000,62.0000,1990-08-29,Yes,Yes
|
||||||
|
Baltic Continental Drift Center,"Southeastern Sweden, Baltic Shield zone",56.0000,15.0000,1987-12-12,Yes,Yes
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
Name,Level,SuperiorName
|
||||||
Dr. Emily Neighbour Carter,Junior,Dr. Rajiv Menon
|
Dr. Emily Neighbour Carter,Junior,Dr. Rajiv Menon
|
||||||
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,Senior,None
|
||||||
Tim Howitz,Admin,None
|
|
||||||
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
|
||||||
|
|||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { PrismaClient } from '@prismaclient';
|
import { prisma } from '@utils/prisma';
|
||||||
|
|
||||||
const usingPrisma = false;
|
|
||||||
let prisma: PrismaClient;
|
|
||||||
if (usingPrisma) prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// todo add specification of date range in request
|
// todo add specification of date range in request
|
||||||
|
|
||||||
@ -35,7 +31,5 @@ export async function POST(req: Request) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in earthquakes endpoint:", error);
|
console.error("Error in earthquakes endpoint:", error);
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
} finally {
|
|
||||||
if (usingPrisma) await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
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";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
let cookieStore;
|
let cookieStore;
|
||||||
try {
|
try {
|
||||||
cookieStore = await cookies();
|
cookieStore = await cookies();
|
||||||
const token = cookieStore.get("jwt")?.value;
|
const token = cookieStore.get("jwt")?.value;
|
||||||
|
if (!token) return NextResponse.json({ error: "No JWT found" }, { status: 401 });
|
||||||
if (!token) {
|
|
||||||
return NextResponse.json({ error: "No JWT found" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await verifyJwt({ token, secret: env.JWT_SECRET_KEY });
|
const payload = await verifyJwt({ token, secret: env.JWT_SECRET_KEY });
|
||||||
|
|
||||||
@ -44,7 +39,5 @@ export async function POST(req: Request) {
|
|||||||
console.error("Error in user endpoint:", error);
|
console.error("Error in user endpoint:", error);
|
||||||
cookieStore?.delete("jwt"); // Delete JWT cookie on error
|
cookieStore?.delete("jwt"); // Delete JWT cookie on error
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
87
src/app/api/import-artefacts/route.ts
Normal file
87
src/app/api/import-artefacts/route.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { parse } from "csv-parse/sync";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import path from "path";
|
||||||
|
import { stringToBool } from "@utils/parsingUtils";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
import { getRandomNumber } from "@utils/maths";
|
||||||
|
|
||||||
|
const csvFilePath = path.resolve(process.cwd(), "public/Artefacts.csv");
|
||||||
|
|
||||||
|
type CsvRow = {
|
||||||
|
Type: string;
|
||||||
|
Name: string;
|
||||||
|
Description: string;
|
||||||
|
WarehouseArea: string;
|
||||||
|
EarthquakeCode: string;
|
||||||
|
Required?: string;
|
||||||
|
Price: string;
|
||||||
|
PickedUp?: string;
|
||||||
|
Picture: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
||||||
|
const records: CsvRow[] = parse(fileContent, {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const failedImports: { row: CsvRow; reason: string }[] = [];
|
||||||
|
|
||||||
|
const artefacts = await Promise.all(
|
||||||
|
records.map(async (row) => {
|
||||||
|
const earthquake = await prisma.earthquake.findUnique({
|
||||||
|
where: { code: row.EarthquakeCode },
|
||||||
|
});
|
||||||
|
|
||||||
|
const creators = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
role: { in: ["SCIENTIST", "ADMIN"] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null;
|
||||||
|
|
||||||
|
if (!earthquake || !randomCreator) {
|
||||||
|
failedImports.push({ row, reason: `Earthquake: ${earthquake}, RandomCreator: ${randomCreator}` });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: row.Name,
|
||||||
|
description: row.Description,
|
||||||
|
type: row.Type,
|
||||||
|
warehouseArea: row.WarehouseArea,
|
||||||
|
earthquakeId: earthquake.id,
|
||||||
|
required: stringToBool(row.Required, true),
|
||||||
|
shopPrice: row.Price && row.Price !== "" ? parseFloat(row.Price) : getRandomNumber(20, 500),
|
||||||
|
pickedUp: stringToBool(row.PickedUp, false),
|
||||||
|
creatorId: randomCreator.id,
|
||||||
|
purchasedById: null,
|
||||||
|
imageName: row.Picture,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validArtefacts = artefacts.filter((artefact): artefact is NonNullable<typeof artefact> => artefact !== null);
|
||||||
|
|
||||||
|
await prisma.artefact.createMany({
|
||||||
|
data: validArtefacts,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedImports.length > 0) {
|
||||||
|
console.warn("Failed imports:", failedImports);
|
||||||
|
await fs.writeFile(path.resolve(process.cwd(), "failed_imports_artefacts.json"), JSON.stringify(failedImports, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
count: validArtefacts.length,
|
||||||
|
failedCount: failedImports.length,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,11 +3,11 @@ import fs from "fs/promises";
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { PrismaClient } from "@prismaclient";
|
import { getRandomNumber } from "@utils/maths";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
// 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 DISTANCE_THRESHOLD_KM = 500; // Max distance for observatory matching
|
||||||
|
|
||||||
type CsvRow = {
|
type CsvRow = {
|
||||||
Date: string;
|
Date: string;
|
||||||
@ -20,49 +20,107 @@ type CsvRow = {
|
|||||||
Depth: string;
|
Depth: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Haversine formula to calculate distance between two points (in km)
|
||||||
|
function getDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||||
|
const R = 6371; // Earth's radius in km
|
||||||
|
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
||||||
|
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
// Get a random user ID
|
|
||||||
const randomUserArr: Array<{ id: number }> = await prisma.$queryRaw`
|
|
||||||
SELECT TOP 1 id FROM [user] ORDER BY NEWID()
|
|
||||||
`;
|
|
||||||
if (!randomUserArr.length) {
|
|
||||||
return NextResponse.json({ error: 'No users found.' }, { status: 404 });
|
|
||||||
}
|
|
||||||
const userId = randomUserArr[0].id;
|
|
||||||
|
|
||||||
// 1. Read the CSV file
|
|
||||||
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
||||||
// 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) => ({
|
const failedImports: { row: CsvRow; reason: string }[] = [];
|
||||||
date: new Date(row.Date),
|
|
||||||
code: row.Code,
|
// Fetch all observatories once to avoid repeated queries
|
||||||
magnitude: parseFloat(row.Magnitude),
|
const observatories = await prisma.observatory.findMany({
|
||||||
type: row.Type,
|
select: { id: true, latitude: true, longitude: true },
|
||||||
latitude: parseFloat(row.Latitude),
|
});
|
||||||
longitude: parseFloat(row.Longitude),
|
|
||||||
location: row.Location,
|
const earthquakes = await Promise.all(
|
||||||
depth: row.Depth, // store as received
|
records.map(async (row) => {
|
||||||
// todo add random selection for creatorId
|
const creators = await prisma.user.findMany({
|
||||||
creatorId: userId,
|
where: { role: { in: ["SCIENTIST", "ADMIN"] } },
|
||||||
}));
|
});
|
||||||
// 4. Bulk create earthquakes in database:
|
const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null;
|
||||||
await prisma.earthquake.createMany({
|
|
||||||
data: earthquakes,
|
if (!randomCreator) {
|
||||||
|
failedImports.push({ row, reason: `RandomCreator: ${randomCreator}` });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eqLat = parseFloat(row.Latitude);
|
||||||
|
const eqLon = parseFloat(row.Longitude);
|
||||||
|
|
||||||
|
// Find observatories within distance threshold
|
||||||
|
let nearbyObservatories = observatories
|
||||||
|
.map((obs) => ({
|
||||||
|
id: obs.id,
|
||||||
|
distance: getDistance(eqLat, eqLon, obs.latitude, obs.longitude),
|
||||||
|
}))
|
||||||
|
.filter((obs) => obs.distance <= DISTANCE_THRESHOLD_KM)
|
||||||
|
.map((obs) => ({ id: obs.id }));
|
||||||
|
|
||||||
|
// If no observatories within range, find the nearest one
|
||||||
|
if (nearbyObservatories.length === 0) {
|
||||||
|
const nearest = observatories.reduce(
|
||||||
|
(min, obs) => {
|
||||||
|
const distance = getDistance(eqLat, eqLon, obs.latitude, obs.longitude);
|
||||||
|
return distance < min.distance ? { id: obs.id, distance } : min;
|
||||||
|
},
|
||||||
|
{ id: -1, distance: Infinity }
|
||||||
|
);
|
||||||
|
if (nearest.id !== -1) {
|
||||||
|
nearbyObservatories = [{ id: nearest.id }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: new Date(row.Date),
|
||||||
|
code: row.Code,
|
||||||
|
magnitude: parseFloat(row.Magnitude),
|
||||||
|
type: row.Type,
|
||||||
|
latitude: eqLat,
|
||||||
|
longitude: eqLon,
|
||||||
|
location: row.Location,
|
||||||
|
depth: row.Depth,
|
||||||
|
creatorId: randomCreator.id,
|
||||||
|
observatories: { connect: nearbyObservatories },
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validEarthquakes = earthquakes.filter(
|
||||||
|
(earthquake): earthquake is NonNullable<typeof earthquake> => earthquake !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bulk insert earthquakes with observatory connections
|
||||||
|
await prisma.earthquake.createMany({
|
||||||
|
data: validEarthquakes,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedImports.length > 0) {
|
||||||
|
console.warn("Failed imports:", failedImports);
|
||||||
|
await fs.writeFile(path.resolve(process.cwd(), "failed_imports_earthquakes.json"), JSON.stringify(failedImports, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
count: validEarthquakes.length,
|
||||||
|
failedCount: failedImports.length,
|
||||||
});
|
});
|
||||||
return NextResponse.json({ success: true, count: earthquakes.length });
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,67 +1,83 @@
|
|||||||
import { parse } from "csv-parse/sync";
|
import { parse } from "csv-parse/sync";
|
||||||
|
import { stringToBool } from "@utils/parsingUtils";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
import { getRandomNumber } from "@utils/maths";
|
||||||
|
|
||||||
import { PrismaClient } from "@prismaclient";
|
|
||||||
|
|
||||||
// CSV location (update filename as needed)
|
|
||||||
const csvFilePath = path.resolve(process.cwd(), "public/observatories.csv");
|
const csvFilePath = path.resolve(process.cwd(), "public/observatories.csv");
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
type CsvRow = {
|
type CsvRow = {
|
||||||
Name: string;
|
Name: string;
|
||||||
Location: string;
|
Location: string;
|
||||||
Latitude: string;
|
Latitude: string;
|
||||||
Longitude: string;
|
Longitude: string;
|
||||||
DateEstablished?: string;
|
DateEstablished: string;
|
||||||
Functional: string;
|
Functional: string;
|
||||||
SeismicSensorOnline?: 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() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
// 1. Read file
|
|
||||||
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
||||||
|
|
||||||
// 2. Parse CSV
|
|
||||||
const records: CsvRow[] = parse(fileContent, {
|
const records: CsvRow[] = parse(fileContent, {
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Map records to Prisma inputs
|
const failedImports: { row: CsvRow; reason: string }[] = [];
|
||||||
const observatories = records.map((row) => ({
|
|
||||||
name: row.Name,
|
const observatories = await Promise.all(
|
||||||
location: row.Location,
|
records.map(async (row) => {
|
||||||
latitude: row.Latitude,
|
const creators = await prisma.user.findMany({
|
||||||
longitude: row.Longitude,
|
where: {
|
||||||
dateEstablished: row.DateEstablished ? parseInt(row.DateEstablished, 10) : null,
|
role: { in: ["SCIENTIST", "ADMIN"] },
|
||||||
functional: stringToBool(row.Functional),
|
},
|
||||||
seismicSensorOnline: row.SeismicSensorOnline ? stringToBool(row.SeismicSensorOnline) : true, // default true per schema
|
});
|
||||||
// todo add random selection of creatorId
|
const randomCreator = creators.length > 0 ? creators[getRandomNumber(0, creators.length - 1)] : null;
|
||||||
creatorId: null,
|
|
||||||
}));
|
if (!randomCreator) {
|
||||||
|
failedImports.push({ row, reason: `RandomCreator: ${randomCreator}` });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: row.Name,
|
||||||
|
location: row.Location,
|
||||||
|
latitude: parseFloat(row.Latitude),
|
||||||
|
longitude: parseFloat(row.Longitude),
|
||||||
|
dateEstablished: new Date(row.DateEstablished),
|
||||||
|
isFunctional: stringToBool(row.Functional),
|
||||||
|
seismicSensorOnline: stringToBool(row.SeismicSensorOnline),
|
||||||
|
creatorId: randomCreator.id,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validObservatories = observatories.filter(
|
||||||
|
(observatory): observatory is NonNullable<typeof observatory> => observatory !== null
|
||||||
|
);
|
||||||
|
|
||||||
// 4. Bulk insert
|
|
||||||
await prisma.observatory.createMany({
|
await prisma.observatory.createMany({
|
||||||
data: observatories,
|
data: validObservatories,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (failedImports.length > 0) {
|
||||||
|
console.warn("Failed imports:", failedImports);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.resolve(process.cwd(), "failed_imports_observatories.json"),
|
||||||
|
JSON.stringify(failedImports, null, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
count: observatories.length,
|
count: validObservatories.length,
|
||||||
|
failedCount: failedImports.length,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,8 @@ import fs from "fs/promises";
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { PrismaClient } from "@prismaclient";
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
const csvFilePath = path.resolve(process.cwd(), "public/requests.csv");
|
const csvFilePath = path.resolve(process.cwd(), "public/requests.csv");
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
type RequestType = "NEW_USER" | "CHANGE_LEVEL" | "DELETE";
|
type RequestType = "NEW_USER" | "CHANGE_LEVEL" | "DELETE";
|
||||||
type RequestOutcome = "FULFILLED" | "REJECTED" | "IN_PROGRESS" | "CANCELLED" | "OTHER";
|
type RequestOutcome = "FULFILLED" | "REJECTED" | "IN_PROGRESS" | "CANCELLED" | "OTHER";
|
||||||
@ -56,7 +54,5 @@ export async function POST() {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,22 +2,18 @@ import { parse } from "csv-parse/sync";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
import { getRandomNumber } from "@utils/maths";
|
||||||
|
|
||||||
import { PrismaClient } from "@prismaclient";
|
|
||||||
|
|
||||||
// Path to CSV file
|
|
||||||
const csvFilePath = path.resolve(process.cwd(), "public/scientists.csv");
|
const csvFilePath = path.resolve(process.cwd(), "public/scientists.csv");
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
type CsvRow = {
|
type CsvRow = {
|
||||||
Name: string;
|
Name: string;
|
||||||
Level?: string;
|
Level: string;
|
||||||
UserId: string;
|
SuperiorName: string;
|
||||||
SuperiorId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeLevel(level: string | undefined): string {
|
function normalizeLevel(level: string | undefined): string {
|
||||||
// Only allow JUNIOR, SENIOR; default JUNIOR
|
|
||||||
if (!level || !level.trim()) return "JUNIOR";
|
if (!level || !level.trim()) return "JUNIOR";
|
||||||
const lv = level.trim().toUpperCase();
|
const lv = level.trim().toUpperCase();
|
||||||
return ["JUNIOR", "SENIOR"].includes(lv) ? lv : "JUNIOR";
|
return ["JUNIOR", "SENIOR"].includes(lv) ? lv : "JUNIOR";
|
||||||
@ -25,35 +21,102 @@ function normalizeLevel(level: string | undefined): string {
|
|||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
// 1. Read the CSV file
|
|
||||||
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
||||||
|
|
||||||
// 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 each record for Prisma
|
const failedImports: { row: CsvRow; reason: string }[] = [];
|
||||||
// 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
|
// Fetch all users to match names
|
||||||
await prisma.scientist.createMany({
|
const users = await prisma.user.findMany({
|
||||||
data: scientists,
|
select: { id: true, name: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true, count: scientists.length });
|
// Process seniors first to ensure superiors exist for juniors
|
||||||
|
const seniorScientists = records.filter((row) => normalizeLevel(row.Level) === "SENIOR");
|
||||||
|
const juniorScientists = records.filter((row) => normalizeLevel(row.Level) === "JUNIOR");
|
||||||
|
const seniorScientistNames = new Map<string, number>(); // Map name to ID
|
||||||
|
|
||||||
|
const seniors = await Promise.all(
|
||||||
|
seniorScientists.map(async (row) => {
|
||||||
|
const user = users.find((u) => u.name === row.Name);
|
||||||
|
if (!user) {
|
||||||
|
failedImports.push({ row, reason: `User not found: ${row.Name}` });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: row.Name,
|
||||||
|
level: normalizeLevel(row.Level),
|
||||||
|
userId: user.id,
|
||||||
|
superiorId: null, // Seniors have no superior
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validSeniors = seniors.filter((senior): senior is NonNullable<typeof senior> => senior !== null);
|
||||||
|
|
||||||
|
// Create senior scientists and store their IDs
|
||||||
|
for (const senior of validSeniors) {
|
||||||
|
try {
|
||||||
|
const scientist = await prisma.scientist.create({ data: senior });
|
||||||
|
seniorScientistNames.set(senior.name, scientist.id);
|
||||||
|
} catch (error: any) {
|
||||||
|
failedImports.push({
|
||||||
|
row: { Name: senior.name, Level: senior.level, SuperiorName: "None" },
|
||||||
|
reason: `Failed to create senior scientist: ${error.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process junior scientists
|
||||||
|
const juniors = await Promise.all(
|
||||||
|
juniorScientists.map(async (row) => {
|
||||||
|
const user = users.find((u) => u.name === row.Name);
|
||||||
|
if (!user) {
|
||||||
|
failedImports.push({ row, reason: `User not found: ${row.Name}` });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let superiorId: number | null = null;
|
||||||
|
if (row.SuperiorName && row.SuperiorName.trim() !== "None") {
|
||||||
|
superiorId = seniorScientistNames.get(row.SuperiorName.trim()) || null;
|
||||||
|
if (!superiorId) {
|
||||||
|
failedImports.push({ row, reason: `Superior not found: ${row.SuperiorName}` });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: row.Name,
|
||||||
|
level: normalizeLevel(row.Level),
|
||||||
|
userId: user.id,
|
||||||
|
superiorId,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validJuniors = juniors.filter((junior): junior is NonNullable<typeof junior> => junior !== null);
|
||||||
|
|
||||||
|
// Bulk create junior scientists
|
||||||
|
await prisma.scientist.createMany({
|
||||||
|
data: validJuniors,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedImports.length > 0) {
|
||||||
|
console.warn("Failed imports:", failedImports);
|
||||||
|
await fs.writeFile(path.resolve(process.cwd(), "failed_imports_scientists.json"), JSON.stringify(failedImports, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
count: validSeniors.length + validJuniors.length,
|
||||||
|
failedCount: failedImports.length,
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,21 +3,23 @@ import fs from "fs/promises";
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { PrismaClient } from "@prismaclient";
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
// Path to users.csv - adjust as needed
|
const csvFilePath = path.resolve(process.cwd(), "public/Users.csv");
|
||||||
const csvFilePath = path.resolve(process.cwd(), "public/users.csv");
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
type CsvRow = {
|
type CsvRow = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
Name: string;
|
Name: string;
|
||||||
Email: string;
|
Email: string;
|
||||||
PasswordHash: string;
|
PasswordHash: string;
|
||||||
Role?: string;
|
Role: string;
|
||||||
|
Scientist: string;
|
||||||
|
PurchasedArtefacts: string;
|
||||||
|
Requests: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeRole(role: string | undefined): string {
|
function normalizeRole(role: string | undefined): string {
|
||||||
// Only allow ADMIN, SCIENTIST, GUEST; default GUEST
|
|
||||||
if (!role || !role.trim()) return "GUEST";
|
if (!role || !role.trim()) return "GUEST";
|
||||||
const r = role.trim().toUpperCase();
|
const r = role.trim().toUpperCase();
|
||||||
return ["ADMIN", "SCIENTIST", "GUEST"].includes(r) ? r : "GUEST";
|
return ["ADMIN", "SCIENTIST", "GUEST"].includes(r) ? r : "GUEST";
|
||||||
@ -25,33 +27,48 @@ function normalizeRole(role: string | undefined): string {
|
|||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
// 1. Read the CSV file
|
|
||||||
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
const fileContent = await fs.readFile(csvFilePath, "utf8");
|
||||||
|
|
||||||
// 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 each CSV row to User model format
|
const failedImports: { row: CsvRow; reason: string }[] = [];
|
||||||
const users = records.map((row) => ({
|
|
||||||
name: row.Name,
|
const users = await Promise.all(
|
||||||
email: row.Email,
|
records.map(async (row) => {
|
||||||
passwordHash: row.PasswordHash,
|
try {
|
||||||
role: normalizeRole(row.Role),
|
return {
|
||||||
}));
|
name: row.Name,
|
||||||
|
email: row.Email,
|
||||||
|
passwordHash: row.PasswordHash,
|
||||||
|
role: normalizeRole(row.Role),
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
failedImports.push({ row, reason: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const validUsers = users.filter((user): user is NonNullable<typeof user> => user !== null);
|
||||||
|
|
||||||
// 4. Bulk create users in database
|
|
||||||
await prisma.user.createMany({
|
await prisma.user.createMany({
|
||||||
data: users,
|
data: validUsers,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true, count: users.length });
|
if (failedImports.length > 0) {
|
||||||
|
console.warn("Failed imports:", failedImports);
|
||||||
|
await fs.writeFile(path.resolve(process.cwd(), "failed_imports_users.json"), JSON.stringify(failedImports, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
count: validUsers.length,
|
||||||
|
failedCount: failedImports.length,
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,11 @@ 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 { env } from "@utils/env";
|
import { env } from "@utils/env";
|
||||||
|
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite";
|
import { findUserByEmail, readUserCsv, User } from "../functions/csvReadWrite";
|
||||||
|
|
||||||
const usingPrisma = false;
|
|
||||||
let prisma: PrismaClient;
|
|
||||||
if (usingPrisma) prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const { email, password } = await req.json(); // Parse incoming JSON data
|
const { email, password } = await req.json(); // Parse incoming JSON data
|
||||||
@ -67,7 +63,5 @@ export async function POST(req: Request) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in signup endpoint:", error);
|
console.error("Error in signup endpoint:", error);
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
} finally {
|
|
||||||
if (usingPrisma) await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { PrismaClient } from '@prismaclient';
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
const usingPrisma = false;
|
|
||||||
let prisma: PrismaClient;
|
|
||||||
if (usingPrisma) prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// todo remove if (usingPrisma) code
|
// todo remove if (usingPrisma) code
|
||||||
// todo add specification of date range in request
|
// todo add specification of date range in request
|
||||||
@ -39,8 +35,7 @@ export async function GET(request: Request) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// todo get earthquakes associated with observatories
|
// todo get earthquakes associated with observatories
|
||||||
let observatories;
|
const observatories = await prisma.observatory.findMany();
|
||||||
if (usingPrisma) observatories = await prisma.observatory.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 });
|
||||||
@ -51,7 +46,5 @@ export async function GET(request: Request) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in observatories endpoint:", error);
|
console.error("Error in observatories endpoint:", error);
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
} finally {
|
|
||||||
if (usingPrisma) await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,17 +2,11 @@ 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 { env } from "@utils/env";
|
import { env } from "@utils/env";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
|
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
|
||||||
|
|
||||||
const usingPrisma = false;
|
|
||||||
let prisma: PrismaClient;
|
|
||||||
if (usingPrisma) prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// todo remove if (usingPrisma) code
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const { email, password, name } = await req.json(); // Parse incoming JSON data
|
const { email, password, name } = await req.json(); // Parse incoming JSON data
|
||||||
@ -26,17 +20,11 @@ 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 foundUser;
|
const foundUser = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
if (usingPrisma) {
|
email: email, // use the email to uniquely identify the user
|
||||||
foundUser = await prisma.user.findUnique({
|
},
|
||||||
where: {
|
});
|
||||||
email: email, // use the email to uniquely identify the user
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
foundUser = findUserByEmail(userData, email);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundUser) {
|
if (foundUser) {
|
||||||
return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 });
|
return NextResponse.json({ message: "Sorry, this email is already in use" }, { status: 409 });
|
||||||
@ -61,20 +49,15 @@ export async function POST(req: Request) {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const passwordHash = await bcryptjs.hash(password, 10);
|
const passwordHash = await bcryptjs.hash(password, 10);
|
||||||
let user;
|
// todo add sending back user
|
||||||
if (usingPrisma) {
|
const user = await prisma.user.create({
|
||||||
// todo add sending back user
|
data: {
|
||||||
user = await prisma.user.create({
|
name,
|
||||||
data: {
|
email,
|
||||||
name,
|
passwordHash,
|
||||||
email,
|
},
|
||||||
passwordHash,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
user = { name, email, password: passwordHash, accessLevel };
|
|
||||||
userData.push(user);
|
|
||||||
}
|
|
||||||
await writeUserCsv(userData);
|
await writeUserCsv(userData);
|
||||||
|
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
|
|||||||
@ -1,108 +1,42 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { cookies } from "next/headers";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { PrismaClient } from '@prismaclient';
|
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
||||||
import { env } from '@utils/env';
|
import { JWTPayload } from "@appTypes/JWT";
|
||||||
import { verifyJwt } from '@utils/verifyJwt';
|
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
||||||
|
import { env } from "@utils/env";
|
||||||
const usingPrisma = false;
|
import { prisma } from "@utils/prisma";
|
||||||
let prisma: PrismaClient;
|
import { verifyJwt } from "@utils/verifyJwt";
|
||||||
if (usingPrisma) prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// todo remove if (usingPrisma) code
|
|
||||||
// todo add specification of date range in request
|
|
||||||
|
|
||||||
// Artefact type
|
|
||||||
interface Artefact {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
location: string;
|
|
||||||
earthquakeId: string;
|
|
||||||
isRequired: boolean;
|
|
||||||
isSold: boolean;
|
|
||||||
isCollected: boolean;
|
|
||||||
dateAdded: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
// todo fix, moron the token will be in the cookie header
|
const authResult = await apiAuthMiddleware();
|
||||||
const json = await req.json(); // Parse incoming JSON data
|
if ("user" in authResult === false) return authResult; // Handle error response
|
||||||
const { token } = json.body;
|
|
||||||
if (!token) return NextResponse.json({ message: "Unauthorised" }, { status: 401 });
|
|
||||||
await verifyJwt({ token, secret: env.JWT_SECRET_KEY });
|
|
||||||
|
|
||||||
const warehouseArtefacts: Artefact[] = [
|
const { user } = authResult;
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Solidified Lava Chunk",
|
|
||||||
description: "A chunk of solidified lava from the 2023 Iceland eruption.",
|
|
||||||
location: "Reykjanes, Iceland",
|
|
||||||
earthquakeId: "EQ2023ICL",
|
|
||||||
isRequired: true,
|
|
||||||
isSold: false,
|
|
||||||
isCollected: false,
|
|
||||||
dateAdded: "2025-05-04",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Tephra Sample",
|
|
||||||
description: "Foreign debris from the 2022 Tonga volcanic eruption.",
|
|
||||||
location: "Tonga",
|
|
||||||
earthquakeId: "EQ2022TGA",
|
|
||||||
isRequired: false,
|
|
||||||
isSold: true,
|
|
||||||
isCollected: true,
|
|
||||||
dateAdded: "2025-05-03",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Ash Sample",
|
|
||||||
description: "Volcanic ash from the 2021 La Palma eruption.",
|
|
||||||
location: "La Palma, Spain",
|
|
||||||
earthquakeId: "EQ2021LPA",
|
|
||||||
isRequired: false,
|
|
||||||
isSold: false,
|
|
||||||
isCollected: false,
|
|
||||||
dateAdded: "2025-05-04",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "Ground Soil",
|
|
||||||
description: "Soil sample from the 2020 Croatia earthquake site.",
|
|
||||||
location: "Zagreb, Croatia",
|
|
||||||
earthquakeId: "EQ2020CRO",
|
|
||||||
isRequired: true,
|
|
||||||
isSold: false,
|
|
||||||
isCollected: false,
|
|
||||||
dateAdded: "2025-05-02",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "Basalt Fragment",
|
|
||||||
description: "Basalt rock from the 2019 New Zealand eruption.",
|
|
||||||
location: "White Island, New Zealand",
|
|
||||||
earthquakeId: "EQ2019NZL",
|
|
||||||
isRequired: false,
|
|
||||||
isSold: true,
|
|
||||||
isCollected: false,
|
|
||||||
dateAdded: "2025-05-04",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let artefacts;
|
if (user.role !== "SCIENTIST" && user.role !== "ADMIN") {
|
||||||
if (usingPrisma) artefacts = await prisma.artefact.findMany();
|
return NextResponse.json({ message: "Not authorised" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const artefacts = await prisma.artefact.findMany({
|
||||||
|
include: {
|
||||||
|
earthquake: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (artefacts) {
|
if (artefacts) {
|
||||||
return NextResponse.json({ message: "Got artefacts successfully", artefacts }, { status: 200 });
|
const extendedArtefacts: ExtendedArtefact[] = artefacts.map((x) => ({
|
||||||
|
...x,
|
||||||
|
location: x.earthquake.location,
|
||||||
|
date: x.earthquake.date,
|
||||||
|
}));
|
||||||
|
return NextResponse.json({ message: "Got artefacts successfully", artefacts: extendedArtefacts }, { status: 200 });
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json({ message: "Got earthquakes successfully", earthquakes: warehouseArtefacts }, { status: 200 });
|
return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 });
|
||||||
// return NextResponse.json({ message: "Failed to get earthquakes" }, { status: 401 });
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in artefacts endpoint:", error);
|
console.error("Error in artefacts endpoint:", error);
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
} finally {
|
|
||||||
if (usingPrisma) await prisma.$disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const OurMission = () => {
|
function OurMission() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative bg-fixed bg-cover bg-center text-white "
|
className="relative h-full bg-fixed bg-cover bg-center text-white "
|
||||||
style={{ backgroundImage: "url('destruction.jpg')", height: 845, overflow: "hidden" }}
|
style={{ backgroundImage: "url('destruction.jpg')", overflow: "hidden" }}
|
||||||
>
|
>
|
||||||
{/* Overlay for Readability */}
|
{/* Overlay for Readability */}
|
||||||
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
<div className="absolute inset-0 bg-black bg-opacity-50"></div>
|
||||||
@ -55,5 +55,5 @@ const OurMission = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
export default OurMission;
|
export default OurMission;
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
import { Dispatch, SetStateAction, useCallback, useState } from "react";
|
||||||
|
|
||||||
import Artefact from '@appTypes/Artefact';
|
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
||||||
import { Currency } from '@appTypes/StoreModel';
|
import { Currency } from "@appTypes/StoreModel";
|
||||||
import { useStoreState } from '@hooks/store';
|
import { useStoreState } from "@hooks/store";
|
||||||
|
|
||||||
// Artefacts Data
|
const artefacts: ExtendedArtefact[] = [
|
||||||
const artefacts: Artefact[] = [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Golden Scarab",
|
name: "Golden Scarab",
|
||||||
@ -215,7 +214,7 @@ export default function Shop() {
|
|||||||
if (currentPage > 1) setCurrentPage((prev) => prev - 1);
|
if (currentPage > 1) setCurrentPage((prev) => prev - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function ArtefactCard({ artefact }: { artefact: Artefact }) {
|
function ArtefactCard({ artefact }: { artefact: ExtendedArtefact }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
|
className="flex flex-col bg-white shadow-md rounded-md overflow-hidden cursor-pointer hover:scale-105 transition-transform"
|
||||||
@ -234,7 +233,7 @@ export default function Shop() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function Modal({ artefact }: { artefact: Artefact }) {
|
function Modal({ artefact }: { artefact: ExtendedArtefact }) {
|
||||||
if (!artefact) return null;
|
if (!artefact) return null;
|
||||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||||
if (e.target === e.currentTarget) setSelectedArtefact(null);
|
if (e.target === e.currentTarget) setSelectedArtefact(null);
|
||||||
|
|||||||
@ -1,19 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Dispatch, SetStateAction, useMemo, useState } from 'react';
|
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||||
import { FaTimes } from 'react-icons/fa';
|
import { FaTimes } from "react-icons/fa";
|
||||||
import { FaCalendarPlus, FaCartShopping, FaWarehouse } 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 { ExtendedArtefact } from "@appTypes/ApiTypes";
|
||||||
|
|
||||||
// import { Artefact } from "@appTypes/Prisma";
|
// import { Artefact } from "@appTypes/Prisma";
|
||||||
|
|
||||||
import type { Artefact } from "@prismaclient";
|
import type { Artefact } from "@prismaclient";
|
||||||
|
|
||||||
interface WarehouseArtefact extends Artefact {
|
|
||||||
location: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warehouse Artefacts Data
|
// Warehouse Artefacts Data
|
||||||
const warehouseArtefacts: WarehouseArtefact[] = [
|
const extendedArtefacts: ExtendedArtefact[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Solidified Lava Chunk",
|
name: "Solidified Lava Chunk",
|
||||||
@ -705,7 +703,7 @@ export default function Warehouse() {
|
|||||||
// Apply filters with loading state
|
// Apply filters with loading state
|
||||||
const filteredArtefacts = useMemo(() => {
|
const filteredArtefacts = useMemo(() => {
|
||||||
setIsFiltering(true);
|
setIsFiltering(true);
|
||||||
const result = applyFilters(warehouseArtefacts, filters);
|
const result = applyFilters(extendedArtefacts, filters);
|
||||||
setIsFiltering(false);
|
setIsFiltering(false);
|
||||||
return result;
|
return result;
|
||||||
}, [filters]);
|
}, [filters]);
|
||||||
@ -713,10 +711,10 @@ export default function Warehouse() {
|
|||||||
const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
|
const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact);
|
||||||
|
|
||||||
// Overview stats
|
// Overview stats
|
||||||
const totalArtefacts = warehouseArtefacts.length;
|
const totalArtefacts = extendedArtefacts.length;
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const artefactsAddedToday = warehouseArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length;
|
const artefactsAddedToday = extendedArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length;
|
||||||
const artefactsSoldToday = warehouseArtefacts.filter(
|
const artefactsSoldToday = extendedArtefacts.filter(
|
||||||
(a) => a.isSold && a.createdAt.toDateString() === today.toDateString()
|
(a) => a.isSold && a.createdAt.toDateString() === today.toDateString()
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
@ -809,8 +807,11 @@ export default function Warehouse() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
|
{/* // todo only admins and senior scientists can log, add not allowed modal for juniors */}
|
||||||
|
{/* // todo add new artefact/pallet saving */}
|
||||||
|
{/* // todo add existing artefact modifying */}
|
||||||
|
{/* // todo add pallet display */}
|
||||||
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
|
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
|
||||||
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
|
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
|
||||||
{editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />}
|
{editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ interface AuthModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||||||
|
// todo add login successful message
|
||||||
const [isLogin, setIsLogin] = useState<boolean>(true);
|
const [isLogin, setIsLogin] = useState<boolean>(true);
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const [isFailed, setIsFailed] = useState<boolean>(false);
|
const [isFailed, setIsFailed] = useState<boolean>(false);
|
||||||
|
|||||||
8
src/types/ApiTypes.ts
Normal file
8
src/types/ApiTypes.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Artefact } from "@prismaclient";
|
||||||
|
|
||||||
|
interface ExtendedArtefact extends Artefact {
|
||||||
|
location: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ExtendedArtefact };
|
||||||
@ -1,14 +0,0 @@
|
|||||||
interface Artefact {
|
|
||||||
// todo change to string
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
location: string;
|
|
||||||
earthquakeID: string;
|
|
||||||
observatory: string;
|
|
||||||
dateReleased: string;
|
|
||||||
image: string;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Artefact;
|
|
||||||
5
src/types/JWT.ts
Normal file
5
src/types/JWT.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
interface JWTPayload {
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { JWTPayload };
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Action } from "easy-peasy";
|
import { Action } from "easy-peasy";
|
||||||
// import type { User } from "@prisma/client";
|
// import type { User } from "@prismaclient";
|
||||||
import { User } from "@appTypes/Prisma";
|
import { User } from "@appTypes/Prisma";
|
||||||
|
|
||||||
type Currency = "GBP" | "USD" | "EUR";
|
type Currency = "GBP" | "USD" | "EUR";
|
||||||
|
|||||||
32
src/utils/apiAuthMiddleware.ts
Normal file
32
src/utils/apiAuthMiddleware.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { verifyJwt } from "@utils/verifyJwt";
|
||||||
|
import type { JWTPayload } from "@/types/JWT";
|
||||||
|
import { env } from "@utils/env";
|
||||||
|
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
|
export async function apiAuthMiddleware() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("jwt")?.value;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({ error: "No JWT found" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = (await verifyJwt({
|
||||||
|
token,
|
||||||
|
secret: env.JWT_SECRET_KEY,
|
||||||
|
})) as unknown as JWTPayload;
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: payload.userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
cookieStore.delete("jwt");
|
||||||
|
return NextResponse.json({ error: "Failed to get user" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, payload };
|
||||||
|
}
|
||||||
5
src/utils/maths.ts
Normal file
5
src/utils/maths.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function getRandomNumber(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getRandomNumber };
|
||||||
7
src/utils/parsingUtils.ts
Normal file
7
src/utils/parsingUtils.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
function stringToBool(val: string | undefined, defaultValue: boolean = false): boolean {
|
||||||
|
if (!val) return defaultValue;
|
||||||
|
const normalized = val.trim().toLowerCase();
|
||||||
|
return normalized === "true" || normalized === "yes";
|
||||||
|
}
|
||||||
|
|
||||||
|
export { stringToBool };
|
||||||
9
src/utils/prisma.ts
Normal file
9
src/utils/prisma.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { PrismaClient } from "@prismaclient";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var prisma: PrismaClient | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export { prisma };
|
||||||
Loading…
x
Reference in New Issue
Block a user