Compare commits
No commits in common. "5024ad4324d6a6401bfb6d4b563b013a9e6a6d3a" and "1c08f0364c63755de46c0ab29e8096c5d30ceb9c" have entirely different histories.
5024ad4324
...
1c08f0364c
@ -1,11 +1,14 @@
|
|||||||
import bcryptjs from "bcryptjs";
|
import bcryptjs from "bcryptjs";
|
||||||
import { validatePassword } from "@utils/validation";
|
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { env } from "@utils/env";
|
import { env } from "@utils/env";
|
||||||
import { prisma } from "@utils/prisma";
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
|
import { passwordStrengthCheck } from "@utils/validation";
|
||||||
|
|
||||||
|
// todo check email doesn't already exist
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const { email, password, name } = await req.json();
|
const { email, password, name } = await req.json();
|
||||||
@ -20,11 +23,23 @@ export async function POST(req: Request) {
|
|||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordCheckResult = validatePassword(password);
|
const passwordCheckResult = await passwordStrengthCheck(password);
|
||||||
if ("message" in passwordCheckResult) {
|
|
||||||
return NextResponse.json({ message: passwordCheckResult.message }, { status: passwordCheckResult.status });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (passwordCheckResult === "short") {
|
||||||
|
return NextResponse.json({ message: "Your password is shorter than 8 characters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "long") {
|
||||||
|
return NextResponse.json({ message: "Your password is longer than 16 characters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no lower") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a lowercase letters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no upper") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a uppercase letters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no digit") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a number" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no special") {
|
||||||
|
return NextResponse.json({ message: "Your password must contain a special character (!@#$%^&*)" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "end of function") {
|
||||||
|
return NextResponse.json({ message: "Password check script failure" }, { status: 500 });
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
const newUser = await prisma.user.create({
|
const newUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
@ -82,6 +97,7 @@ export async function POST(req: Request) {
|
|||||||
console.error("Error creating user:", error);
|
console.error("Error creating user:", error);
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} 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 });
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { env } from "@utils/env";
|
|||||||
import { prisma } from "@utils/prisma";
|
import { prisma } from "@utils/prisma";
|
||||||
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
||||||
|
|
||||||
import { validatePassword } from "@utils/validation";
|
import { passwordStrengthCheck } from "@utils/validation";
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
@ -13,22 +13,11 @@ export async function POST(req: Request) {
|
|||||||
if ("user" in authResult === false) return authResult;
|
if ("user" in authResult === false) return authResult;
|
||||||
|
|
||||||
const { user } = authResult;
|
const { user } = authResult;
|
||||||
const {
|
const { userId, email, name, password, requestedRole } = await req.json();
|
||||||
userId,
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
password,
|
|
||||||
requestedRole,
|
|
||||||
}: {
|
|
||||||
userId?: number;
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
password?: string;
|
|
||||||
requestedRole?: string;
|
|
||||||
} = await req.json();
|
|
||||||
|
|
||||||
// Trying to update a different user than themselves
|
// Trying to update a different user than themselves
|
||||||
// Only available to admins
|
// Only available to admins
|
||||||
|
// todo add senior scientists being able to update their juniors
|
||||||
if (userId && userId !== user.id) {
|
if (userId && userId !== user.id) {
|
||||||
if (user.role !== "ADMIN") {
|
if (user.role !== "ADMIN") {
|
||||||
return NextResponse.json({ message: "Not authorised" }, { status: 401 });
|
return NextResponse.json({ message: "Not authorised" }, { status: 401 });
|
||||||
@ -45,12 +34,25 @@ export async function POST(req: Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo move to dedicated function
|
||||||
// Validate password strength if provided
|
// Validate password strength if provided
|
||||||
let passwordHash = user.passwordHash;
|
let passwordHash = user.passwordHash;
|
||||||
if (password) {
|
if (password) {
|
||||||
const passwordCheckResult = validatePassword(password);
|
const passwordCheckResult = await passwordStrengthCheck(password);
|
||||||
if ("message" in passwordCheckResult) {
|
if (passwordCheckResult === "short") {
|
||||||
return NextResponse.json({ message: passwordCheckResult.message }, { status: passwordCheckResult.status });
|
return NextResponse.json({ message: "Password is shorter than 8 characters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "long") {
|
||||||
|
return NextResponse.json({ message: "Password is longer than 16 characters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no lower") {
|
||||||
|
return NextResponse.json({ message: "Password must contain lowercase letters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no upper") {
|
||||||
|
return NextResponse.json({ message: "Password must contain uppercase letters" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no digit") {
|
||||||
|
return NextResponse.json({ message: "Password must contain a number" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "no special") {
|
||||||
|
return NextResponse.json({ message: "Password must contain a special character (!@#$%^&*)" }, { status: 400 });
|
||||||
|
} else if (passwordCheckResult === "end of function") {
|
||||||
|
return NextResponse.json({ message: "Password check script failure" }, { status: 500 });
|
||||||
}
|
}
|
||||||
passwordHash = await bcryptjs.hash(password, 10);
|
passwordHash = await bcryptjs.hash(password, 10);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { env } from "@utils/env";
|
|
||||||
import { prisma } from "@utils/prisma";
|
|
||||||
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
try {
|
|
||||||
const authResult = await apiAuthMiddleware();
|
|
||||||
if ("user" in authResult === false) return authResult;
|
|
||||||
|
|
||||||
const { user } = authResult;
|
|
||||||
const { userId }: { userId: number } = await req.json();
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return NextResponse.json({ message: "User id required to delete" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userId !== user.id && user.role !== "ADMIN") {
|
|
||||||
return NextResponse.json({ message: "Not authorised" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
// Handle Scientist and its subordinates
|
|
||||||
const scientist = await tx.scientist.findUnique({ where: { userId: userId } });
|
|
||||||
if (scientist) {
|
|
||||||
// Unlink subordinates
|
|
||||||
await tx.scientist.updateMany({
|
|
||||||
where: { superiorId: scientist.id },
|
|
||||||
data: { superiorId: null },
|
|
||||||
});
|
|
||||||
// Delete Scientist
|
|
||||||
await tx.scientist.delete({ where: { userId: userId } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete Requests
|
|
||||||
await tx.request.deleteMany({ where: { requestingUserId: userId } });
|
|
||||||
|
|
||||||
// Unlink Observatories (set creatorId to null)
|
|
||||||
await tx.observatory.updateMany({
|
|
||||||
where: { creatorId: userId },
|
|
||||||
data: { creatorId: null },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unlink Artefacts (set creatorId to null)
|
|
||||||
await tx.artefact.updateMany({
|
|
||||||
where: { creatorId: userId },
|
|
||||||
data: { creatorId: null },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete User (Orders and Earthquakes are handled automatically)
|
|
||||||
await tx.user.delete({ where: { id: userId } });
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ message: "User deleted successfully" }, { status: 200 });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in delete-user endpoint:", error);
|
|
||||||
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
|
||||||
import { prisma } from "@utils/prisma";
|
|
||||||
import { writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const formData = await request.formData();
|
|
||||||
const id = formData.get("id") as string;
|
|
||||||
const name = formData.get("name") as string | null;
|
|
||||||
const description = formData.get("description") as string | null;
|
|
||||||
const warehouseArea = formData.get("warehouseArea") as string | null;
|
|
||||||
const earthquakeCode = formData.get("earthquakeCode") as string | null;
|
|
||||||
const image = formData.get("image") as File | null;
|
|
||||||
|
|
||||||
const authResult = await apiAuthMiddleware();
|
|
||||||
if ("user" in authResult === false) return authResult;
|
|
||||||
|
|
||||||
const { user } = authResult;
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return NextResponse.json({ error: "Artefact ID required" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const artefact = await prisma.artefact.findUnique({
|
|
||||||
where: { id: parseInt(id) },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!artefact) {
|
|
||||||
return NextResponse.json({ error: "Artefact not found" }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role !== "ADMIN" && user.role !== "SCIENTIST") {
|
|
||||||
return NextResponse.json({ error: "Not authorized" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role === "SCIENTIST") {
|
|
||||||
const scientist = await prisma.scientist.findUnique({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
subordinates: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!scientist || scientist.level !== "SENIOR") {
|
|
||||||
return NextResponse.json({ message: "Not authorised" }, { status: 401 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let earthquakeId = artefact.earthquakeId;
|
|
||||||
if (earthquakeCode) {
|
|
||||||
const linkedEarthquake = await prisma.earthquake.findUnique({ where: { code: earthquakeCode } });
|
|
||||||
if (!linkedEarthquake) {
|
|
||||||
return NextResponse.json({ error: "Earthquake code not found" }, { status: 400 });
|
|
||||||
}
|
|
||||||
earthquakeId = linkedEarthquake.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageName = artefact.imageName;
|
|
||||||
if (image) {
|
|
||||||
const buffer = Buffer.from(await image.arrayBuffer());
|
|
||||||
const extension = image.type === "image/jpeg" ? "jpg" : "png";
|
|
||||||
imageName = `${name || artefact.name}-${new Date().toLocaleDateString("en-GB")}.${extension}`;
|
|
||||||
const imagePath = join(process.cwd(), "public", imageName);
|
|
||||||
await writeFile(imagePath, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedArtefact = await prisma.artefact.update({
|
|
||||||
where: { id: parseInt(id) },
|
|
||||||
data: {
|
|
||||||
name: name || artefact.name,
|
|
||||||
description: description || artefact.description,
|
|
||||||
warehouseArea: warehouseArea || artefact.warehouseArea,
|
|
||||||
earthquakeId,
|
|
||||||
imageName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ message: "Artefact updated successfully", artefact: updatedArtefact }, { status: 200 });
|
|
||||||
} catch (e: any) {
|
|
||||||
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,40 +5,21 @@ import { prisma } from "@utils/prisma";
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { palletNote, warehouseArea } = body;
|
const { palletNote, warehouseLocation } = body;
|
||||||
|
|
||||||
const authResult = await apiAuthMiddleware();
|
const authResult = await apiAuthMiddleware();
|
||||||
if ("user" in authResult === false) return authResult; // Handle error response
|
if ("user" in authResult === false) return authResult; // Handle error response
|
||||||
|
|
||||||
const { user } = authResult;
|
const { user } = authResult;
|
||||||
|
|
||||||
if (user.role !== "ADMIN" && user.role !== "SCIENTIST") {
|
if (!palletNote || !warehouseLocation) {
|
||||||
return NextResponse.json({ error: "Not authorized" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role === "SCIENTIST") {
|
|
||||||
const scientist = await prisma.scientist.findUnique({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
subordinates: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!scientist || scientist.level !== "SENIOR") {
|
|
||||||
return NextResponse.json({ message: "Not authorised" }, { status: 401 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!palletNote || !warehouseArea) {
|
|
||||||
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.pallet.create({
|
await prisma.pallet.create({
|
||||||
data: {
|
data: {
|
||||||
palletNote,
|
palletNote,
|
||||||
warehouseArea,
|
warehouseArea: warehouseLocation,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,70 +1,35 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
||||||
import { prisma } from "@utils/prisma";
|
import { prisma } from "@utils/prisma";
|
||||||
import { writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const formData = await request.formData();
|
const body = await request.json();
|
||||||
const name = formData.get("name") as string;
|
const { name, type, description, location, earthquakeCode, warehouseLocation } = body;
|
||||||
const type = formData.get("type") as string;
|
|
||||||
const description = formData.get("description") as string;
|
|
||||||
const earthquakeCode = formData.get("earthquakeCode") as string;
|
|
||||||
const warehouseArea = formData.get("warehouseArea") as string;
|
|
||||||
const image = formData.get("image") as File | null;
|
|
||||||
|
|
||||||
const authResult = await apiAuthMiddleware();
|
const authResult = await apiAuthMiddleware();
|
||||||
if ("user" in authResult === false) return authResult;
|
if ("user" in authResult === false) return authResult; // Handle error response
|
||||||
|
|
||||||
const { user } = authResult;
|
const { user } = authResult;
|
||||||
|
|
||||||
if (!name || !type || !description || !earthquakeCode || !warehouseArea) {
|
if (!name || !type || !description || !location || !earthquakeCode || !warehouseLocation) {
|
||||||
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.role !== "ADMIN" && user.role !== "SCIENTIST") {
|
|
||||||
return NextResponse.json({ error: "Not authorized" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role === "SCIENTIST") {
|
|
||||||
const scientist = await prisma.scientist.findUnique({
|
|
||||||
where: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
subordinates: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!scientist || scientist.level !== "SENIOR") {
|
|
||||||
return NextResponse.json({ message: "Not authorised" }, { status: 401 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkedEarthquake = await prisma.earthquake.findUnique({ where: { code: earthquakeCode } });
|
const linkedEarthquake = await prisma.earthquake.findUnique({ where: { code: earthquakeCode } });
|
||||||
|
|
||||||
if (!linkedEarthquake) {
|
if (!linkedEarthquake) {
|
||||||
return NextResponse.json({ error: "Earthquake code not found" }, { status: 400 });
|
return NextResponse.json({ error: "Earthquake code not found" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageName = "NoImageFound.PNG";
|
|
||||||
if (image) {
|
|
||||||
const buffer = Buffer.from(await image.arrayBuffer());
|
|
||||||
const extension = image.type === "image/jpeg" ? "jpg" : "png";
|
|
||||||
imageName = `${name}-${new Date().toLocaleDateString("en-GB")}.${extension}`;
|
|
||||||
const imagePath = join(process.cwd(), "public", imageName);
|
|
||||||
await writeFile(imagePath, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.artefact.create({
|
await prisma.artefact.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
description,
|
description,
|
||||||
earthquakeId: linkedEarthquake.id,
|
earthquakeId: linkedEarthquake.id,
|
||||||
warehouseArea: warehouseArea,
|
warehouseArea: warehouseLocation,
|
||||||
imageName,
|
imageName: "NoImageFound.PNG",
|
||||||
creatorId: user.id,
|
creatorId: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -229,8 +229,9 @@ export default function Profile() {
|
|||||||
}
|
}
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
|
// todo add delete user route
|
||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
"/api/user/delete",
|
"/api/delete-user",
|
||||||
{ userId: user!.id },
|
{ userId: user!.id },
|
||||||
{ headers: { "Content-Type": "application/json" } }
|
{ headers: { "Content-Type": "application/json" } }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -295,6 +295,7 @@ export default function Shop() {
|
|||||||
"#" + Math.random().toString(24).substring(2, 10).toUpperCase() + new Date().toLocaleDateString("en-GB");
|
"#" + Math.random().toString(24).substring(2, 10).toUpperCase() + new Date().toLocaleDateString("en-GB");
|
||||||
const orderNum = genOrder();
|
const orderNum = genOrder();
|
||||||
|
|
||||||
|
// todo add display of error
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/api/shop/purchase", {
|
const response = await axios.post("/api/shop/purchase", {
|
||||||
@ -308,8 +309,8 @@ export default function Shop() {
|
|||||||
setShowThankYouModal(true);
|
setShowThankYouModal(true);
|
||||||
setCart((c) => c.filter((a) => !artefactsToBuy.map((x) => x.id).includes(a.id)));
|
setCart((c) => c.filter((a) => !artefactsToBuy.map((x) => x.id).includes(a.id)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError("Payment Failed");
|
|
||||||
console.error("Error posting artefacts:", error);
|
console.error("Error posting artefacts:", error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import Image from "next/image";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
||||||
@ -11,12 +10,6 @@ import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
|||||||
|
|
||||||
import { fetcher } from "@utils/axiosHelpers";
|
import { fetcher } from "@utils/axiosHelpers";
|
||||||
|
|
||||||
// Function to validate earthquake code format
|
|
||||||
const validateEarthquakeCode = (code: string): boolean => {
|
|
||||||
const pattern = /^E[A-Z]-[0-9]\.[0-9]-[A-Za-z]+-[0-9]{5}$/;
|
|
||||||
return pattern.test(code);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter Component
|
// Filter Component
|
||||||
function FilterInput({
|
function FilterInput({
|
||||||
value,
|
value,
|
||||||
@ -40,7 +33,8 @@ function FilterInput({
|
|||||||
>
|
>
|
||||||
<IoFilter
|
<IoFilter
|
||||||
className={`cursor-pointer text-neutral-500 font-bold group-hover:text-blue-600
|
className={`cursor-pointer text-neutral-500 font-bold group-hover:text-blue-600
|
||||||
${!showSelectedFilter && value && "text-blue-600"}`}
|
${!showSelectedFilter && value && "text-blue-600"}
|
||||||
|
`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -78,119 +72,17 @@ function FilterInput({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal Component for Bulk Logging
|
// Modal Component for Logging Artefact
|
||||||
function BulkLogModal({ onClose }: { onClose: () => void }) {
|
|
||||||
const [palletNote, setPalletNote] = useState("");
|
|
||||||
const [warehouseArea, setWarehouseArea] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleLog() {
|
|
||||||
if (!palletNote || !warehouseArea) {
|
|
||||||
setError("All fields are required.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await axios.post("/api/warehouse/log-bulk", {
|
|
||||||
palletNote,
|
|
||||||
warehouseArea,
|
|
||||||
});
|
|
||||||
|
|
||||||
alert(`Logged bulk pallet to storage: ${warehouseArea}`);
|
|
||||||
onClose();
|
|
||||||
} catch {
|
|
||||||
setError("Failed to log pallet. Please try again.");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
|
||||||
onClick={handleOverlayClick}
|
|
||||||
>
|
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
|
|
||||||
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log Bulk Pallet</h3>
|
|
||||||
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<textarea
|
|
||||||
placeholder="Pallet Delivery Note (e.g., 10 lava chunks, 5 ash samples)"
|
|
||||||
value={palletNote}
|
|
||||||
onChange={(e) => setPalletNote(e.target.value)}
|
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-24"
|
|
||||||
aria-label="Pallet Delivery Note"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Warehouse Area (e.g., B-05)"
|
|
||||||
value={warehouseArea}
|
|
||||||
onChange={(e) => setWarehouseArea(e.target.value)}
|
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
|
||||||
aria-label="Warehouse Area"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-3 mt-4">
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleLog}
|
|
||||||
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
|
||||||
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
|
||||||
}`}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<svg
|
|
||||||
className="animate-spin h-5 w-5 text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
Logging...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Log Pallet"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogModal({ onClose }: { onClose: () => void }) {
|
function LogModal({ onClose }: { onClose: () => void }) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [type, setType] = useState("");
|
const [type, setType] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
const [location, setLocation] = useState("");
|
||||||
const [earthquakeCode, setEarthquakeCode] = useState("");
|
const [earthquakeCode, setEarthquakeCode] = useState("");
|
||||||
const [warehouseArea, setWarehouseArea] = useState("");
|
const [warehouseLocation, setWarehouseLocation] = useState("");
|
||||||
const [isRequired, setIsRequired] = useState(true);
|
const [isRequired, setIsRequired] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [image, setImage] = useState<File | null>(null);
|
|
||||||
|
|
||||||
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
@ -198,47 +90,25 @@ function LogModal({ onClose }: { onClose: () => void }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
// todo add uploading image
|
||||||
if (e.target.files && e.target.files[0]) {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
setError("Image size must be less than 5MB");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!["image/jpeg", "image/png"].includes(file.type)) {
|
|
||||||
setError("Only JPEG or PNG images are allowed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setImage(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleLog() {
|
async function handleLog() {
|
||||||
if (!name || !type || !description || !earthquakeCode || !warehouseArea) {
|
if (!name || !type || !description || !location || !earthquakeCode || !warehouseLocation) {
|
||||||
setError("All fields are required.");
|
setError("All fields are required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!validateEarthquakeCode(earthquakeCode)) {
|
|
||||||
setError("Earthquake Code must be in format: EX-M.M-Country-##### (e.g., EC-3.9-Belgium-05467)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
await axios.post("/api/warehouse/log", {
|
||||||
formData.append("name", name);
|
name,
|
||||||
formData.append("type", type);
|
type,
|
||||||
formData.append("description", description);
|
description,
|
||||||
formData.append("earthquakeCode", earthquakeCode);
|
location,
|
||||||
formData.append("warehouseArea", warehouseArea);
|
earthquakeCode,
|
||||||
if (image) {
|
warehouseLocation,
|
||||||
formData.append("image", image);
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios.post("/api/warehouse/log", formData, {
|
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
alert(`Logged ${name} to storage: ${warehouseArea}`);
|
// todo replace with better alert
|
||||||
|
alert(`Logged ${name} to storage: ${warehouseLocation}`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch {
|
} catch {
|
||||||
setError("Failed to log artefact. Please try again.");
|
setError("Failed to log artefact. Please try again.");
|
||||||
@ -256,15 +126,6 @@ function LogModal({ onClose }: { onClose: () => void }) {
|
|||||||
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log New Artefact</h3>
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log New Artefact</h3>
|
||||||
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/jpeg,image/png"
|
|
||||||
onChange={handleImageChange}
|
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
|
||||||
aria-label="Artefact Image"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
@ -276,7 +137,7 @@ function LogModal({ onClose }: { onClose: () => void }) {
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Type (e.g., Lava, Tephra, Ash)"
|
placeholder="Type (e.g., Lava, Tephra, Ash"
|
||||||
value={type}
|
value={type}
|
||||||
onChange={(e) => setType(e.target.value)}
|
onChange={(e) => setType(e.target.value)}
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
@ -293,8 +154,18 @@ function LogModal({ onClose }: { onClose: () => void }) {
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Earthquake Code (e.g., EC-3.9-Belgium-05467)"
|
placeholder="Location"
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-label="Artefact Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Earthquake Code"
|
||||||
value={earthquakeCode}
|
value={earthquakeCode}
|
||||||
|
// todo check code is correct format
|
||||||
onChange={(e) => setEarthquakeCode(e.target.value)}
|
onChange={(e) => setEarthquakeCode(e.target.value)}
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
aria-label="Earthquake ID"
|
aria-label="Earthquake ID"
|
||||||
@ -302,11 +173,11 @@ function LogModal({ onClose }: { onClose: () => void }) {
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Warehouse Area (e.g., A-12)"
|
placeholder="Warehouse Location (e.g., A-12)"
|
||||||
value={warehouseArea}
|
value={warehouseLocation}
|
||||||
onChange={(e) => setWarehouseArea(e.target.value)}
|
onChange={(e) => setWarehouseLocation(e.target.value)}
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
aria-label="Warehouse Area"
|
aria-label="Storage Location"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -363,54 +234,137 @@ function LogModal({ onClose }: { onClose: () => void }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose: () => void }) {
|
// Modal Component for Bulk Logging
|
||||||
const [name, setName] = useState(artefact.name);
|
function BulkLogModal({ onClose }: { onClose: () => void }) {
|
||||||
const [type, setType] = useState(artefact.type);
|
const [palletNote, setPalletNote] = useState("");
|
||||||
const [description, setDescription] = useState(artefact.description);
|
const [warehouseLocation, setWarehouseLocation] = useState("");
|
||||||
const [warehouseArea, setWarehouseArea] = useState(artefact.warehouseArea);
|
|
||||||
const [earthquakeCode, setEarthquakeCode] = useState(artefact.earthquakeCode);
|
|
||||||
const [isRequired, setIsRequired] = useState(artefact.isRequired);
|
|
||||||
const [isSold, setIsSold] = useState(artefact.isSold);
|
|
||||||
const [isCollected, setIsCollected] = useState(artefact.isCollected);
|
|
||||||
const [image, setImage] = useState<File | null>(null);
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
function handleOverlayClick(e: { target: any; currentTarget: any }) {
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleLog() {
|
||||||
if (!name || !type || !description || !earthquakeCode || !warehouseArea) {
|
if (!palletNote || !warehouseLocation) {
|
||||||
setError("All fields are required.");
|
setError("All fields are required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!validateEarthquakeCode(earthquakeCode)) {
|
|
||||||
setError("Earthquake Code must be in format: XX-M.M-Country-##### (e.g., EC-3.9-Belgium-05467)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
await axios.post("/api/warehouse/log-bulk", {
|
||||||
formData.append("id", artefact.id.toString());
|
palletNote,
|
||||||
formData.append("name", name);
|
warehouseLocation,
|
||||||
formData.append("type", type);
|
|
||||||
formData.append("description", description);
|
|
||||||
formData.append("earthquakeCode", earthquakeCode);
|
|
||||||
formData.append("warehouseArea", warehouseArea);
|
|
||||||
formData.append("isRequired", isRequired.toString());
|
|
||||||
formData.append("isSold", isSold.toString());
|
|
||||||
formData.append("isCollected", isCollected.toString());
|
|
||||||
if (image) {
|
|
||||||
formData.append("image", image);
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios.post("/api/warehouse/edit-artefact", formData, {
|
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// todo replace with better alert
|
||||||
|
alert(`Logged bulk pallet to storage: ${warehouseLocation}`);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setError("Failed to log pallet. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log Bulk Pallet</h3>
|
||||||
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<textarea
|
||||||
|
placeholder="Pallet Delivery Note (e.g., 10 lava chunks, 5 ash samples)"
|
||||||
|
value={palletNote}
|
||||||
|
onChange={(e) => setPalletNote(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-24"
|
||||||
|
aria-label="Pallet Delivery Note"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Storage Location (e.g., B-05)"
|
||||||
|
value={warehouseLocation}
|
||||||
|
onChange={(e) => setWarehouseLocation(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-label="Storage Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLog}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
||||||
|
}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
Logging...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Log Pallet"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component for Editing Artefact
|
||||||
|
function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose: () => void }) {
|
||||||
|
const [name, setName] = useState(artefact.name);
|
||||||
|
const [description, setDescription] = useState(artefact.description);
|
||||||
|
const [location, setLocation] = useState(artefact.location);
|
||||||
|
const [earthquakeCode, setEarthquakeCode] = useState(artefact.earthquakeCode);
|
||||||
|
const [isRequired, setIsRequired] = useState(artefact.isRequired);
|
||||||
|
const [isSold, setIsSold] = useState(artefact.isSold);
|
||||||
|
const [isCollected, setIsCollected] = useState(artefact.isCollected);
|
||||||
|
const [createdAt, setDateAdded] = useState(new Date(artefact.createdAt).toLocaleDateString("en-GB"));
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!name || !description || !location || !earthquakeCode || !createdAt) {
|
||||||
|
setError("All fields are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||||
alert(`Updated artefact ${name}`);
|
alert(`Updated artefact ${name}`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch {
|
} catch {
|
||||||
@ -418,22 +372,7 @@ function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose:
|
|||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function handleImageChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
if (e.target.files && e.target.files[0]) {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
setError("Image size must be less than 5MB");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!["image/jpeg", "image/png"].includes(file.type)) {
|
|
||||||
setError("Only JPEG or PNG images are allowed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setImage(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -444,46 +383,15 @@ function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose:
|
|||||||
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Edit Artefact</h3>
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Edit Artefact</h3>
|
||||||
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{artefact.imageName && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<Image
|
|
||||||
src={`/uploads/${artefact.imageName}`}
|
|
||||||
alt="Artefact"
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
className="object-cover rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/jpeg,image/png"
|
|
||||||
onChange={handleImageChange}
|
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
|
||||||
aria-label="Artefact Image"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-neutral-600">Created At: {new Date(artefact.createdAt).toLocaleDateString("en-GB")}</p>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
aria-label="Artefact Name"
|
aria-label="Artefact Name"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Type (e.g., Lava, Tephra, Ash)"
|
|
||||||
value={type}
|
|
||||||
onChange={(e) => setType(e.target.value)}
|
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
|
||||||
aria-label="Artefact Type"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Description"
|
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-16"
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-16"
|
||||||
@ -492,20 +400,18 @@ function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose:
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Earthquake Code (e.g., EC-3.9-Belgium-05467)"
|
value={location}
|
||||||
value={earthquakeCode}
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
onChange={(e) => setEarthquakeCode(e.target.value)}
|
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
aria-label="Earthquake ID"
|
aria-label="Artefact Location"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Warehouse Area (e.g., A-12)"
|
value={earthquakeCode}
|
||||||
value={warehouseArea}
|
onChange={(e) => setEarthquakeCode(e.target.value)}
|
||||||
onChange={(e) => setWarehouseArea(e.target.value)}
|
|
||||||
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500"
|
||||||
aria-label="Storage Location"
|
aria-label="Earthquake ID"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -524,7 +430,7 @@ function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose:
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSold}
|
checked={isSold}
|
||||||
onChange={(e) => setIsSold(e.target.checked)}
|
onChange={(e) => setIsSold(e.target.checked)}
|
||||||
className="w-4 h-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
|
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
|
||||||
aria-label="Sold Artefact"
|
aria-label="Sold Artefact"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
@ -541,6 +447,14 @@ function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose:
|
|||||||
/>
|
/>
|
||||||
<label className="text-sm text-neutral-600">Collected</label>
|
<label className="text-sm text-neutral-600">Collected</label>
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-3 mt-4">
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
<button
|
<button
|
||||||
@ -569,7 +483,7 @@ function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose:
|
|||||||
<path
|
<path
|
||||||
className="opacity-75"
|
className="opacity-75"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
d="M4 12a8 8 0 018-8V723C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
Saving...
|
Saving...
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -23,6 +23,8 @@ const COLUMNS = [
|
|||||||
{ label: "Date", key: "date" },
|
{ label: "Date", key: "date" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// todo modify slightly
|
||||||
|
|
||||||
export default function EarthquakeSearchModal({
|
export default function EarthquakeSearchModal({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export function passwordStrengthCheck(password: string): string {
|
export async function passwordStrengthCheck(password: string): Promise<string> {
|
||||||
if (password.length < 8) {
|
if (password.length < 8) {
|
||||||
return "short";
|
return "short";
|
||||||
} else if (password.length > 16) {
|
} else if (password.length > 16) {
|
||||||
@ -21,24 +21,3 @@ export function passwordStrengthCheck(password: string): string {
|
|||||||
}
|
}
|
||||||
return "end of function";
|
return "end of function";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validatePassword(password: string) {
|
|
||||||
const result = passwordStrengthCheck(password);
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case "short":
|
|
||||||
return { message: "Password is shorter than 8 characters", status: 400 };
|
|
||||||
case "long":
|
|
||||||
return { message: "Password is longer than 16 characters", status: 400 };
|
|
||||||
case "no lower":
|
|
||||||
return { message: "Password must contain lowercase letters", status: 400 };
|
|
||||||
case "no upper":
|
|
||||||
return { message: "Password must contain uppercase letters", status: 400 };
|
|
||||||
case "no digit":
|
|
||||||
return { message: "Password must contain a number", status: 400 };
|
|
||||||
case "no special":
|
|
||||||
return { message: "Password must contain a special character (!@#$%^&*)", status: 400 };
|
|
||||||
default:
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user