From a96cb500ca1d4bafa52b351c76adc921b90cf4bb Mon Sep 17 00:00:00 2001 From: Keiran Date: Sun, 28 Sep 2025 12:49:32 +0100 Subject: [PATCH] add admin page and fix default option --- src/app/admin/page.tsx | 377 +++++++++++++++++++++++++++ src/app/api/admin/auth/route.ts | 11 + src/app/api/admin/discounts/route.ts | 68 +++++ src/app/api/admin/products/route.ts | 28 ++ src/components/CartView.tsx | 16 +- src/components/ProductSelection.tsx | 1 + src/data/discountCodes.json | 7 +- src/types/discount.ts | 9 + src/types/product.ts | 41 --- 9 files changed, 515 insertions(+), 43 deletions(-) create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/api/admin/auth/route.ts create mode 100644 src/app/api/admin/discounts/route.ts create mode 100644 src/app/api/admin/products/route.ts create mode 100644 src/types/discount.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..82fa097 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,377 @@ +"use client"; + +import { useState } from "react"; +import { products } from "../../data/products"; +import discountCodes from "../../data/discountCodes.json"; +import type { DiscountCode } from "../../types/discount"; + +export default function AdminPage() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + const handleLogin = async () => { + const response = await fetch("/api/admin/auth", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ password }), + }); + + if (response.ok) { + setIsAuthenticated(true); + setError(""); + } else { + setError("Invalid password"); + } + }; + + if (!isAuthenticated) { + return ( +
+
+

+ Admin Login +

+
+ setPassword(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleLogin()} + /> + + {error &&

{error}

} +
+
+
+ ); + } + + return ; +} + +function AdminDashboard() { + const [activeTab, setActiveTab] = useState<"products" | "discounts">( + "products", + ); + + return ( +
+
+
+

+ Admin Dashboard - ASS AB +

+
+
+ +
+
+ + +
+ +
+ {activeTab === "products" && } + {activeTab === "discounts" && } +
+
+ +
+
+

© ASS co 2025 - Admin Panel

+
+
+
+ ); +} + +function ProductsManager() { + const [productList, setProductList] = useState(products); + const [editingProduct, setEditingProduct] = useState(null); + + const updatePrice = async (productId: string, newPrice: number) => { + const response = await fetch("/api/admin/products", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ productId, pricePerStack: newPrice }), + }); + + if (response.ok) { + setProductList((prev) => + prev.map((product) => + product.id === productId + ? { ...product, pricePerStack: newPrice } + : product, + ), + ); + setEditingProduct(null); + } + }; + + return ( +
+

Product Prices

+
+ + + + + + + + + + {productList.map((product) => ( + + + + + + ))} + +
Product + Price per Stack + Actions
{product.name} + {editingProduct === product.id ? ( + { + if (e.key === "Enter") { + const target = e.target as HTMLInputElement; + updatePrice(product.id, parseFloat(target.value)); + } + }} + /> + ) : ( + `${product.pricePerStack} diamonds` + )} + + {editingProduct === product.id ? ( +
+ + +
+ ) : ( + + )} +
+
+
+ ); +} + +function DiscountManager() { + const [discountList, setDiscountList] = + useState>(discountCodes); + const [newCode, setNewCode] = useState(""); + const [newPercentage, setNewPercentage] = useState(0); + const [newExpiration, setNewExpiration] = useState(""); + const [newDescription, setNewDescription] = useState(""); + + const addDiscountCode = async () => { + if (!newCode || newPercentage <= 0) return; + + const codeData: DiscountCode = { + percentage: newPercentage, + description: newDescription, + }; + + if (newExpiration) { + codeData.expiration = new Date(newExpiration).toISOString(); + } + + const response = await fetch("/api/admin/discounts", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code: newCode, ...codeData }), + }); + + if (response.ok) { + setDiscountList((prev) => ({ + ...prev, + [newCode]: codeData, + })); + setNewCode(""); + setNewPercentage(0); + setNewExpiration(""); + setNewDescription(""); + } + }; + + const deleteDiscountCode = async (code: string) => { + const response = await fetch("/api/admin/discounts", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code }), + }); + + if (response.ok) { + setDiscountList((prev) => { + const updated = { ...prev }; + delete updated[code]; + return updated; + }); + } + }; + + return ( +
+

Discount Codes

+ +
+

+ Add New Discount Code +

+
+ setNewCode(e.target.value)} + className="p-3 bg-gray-600 text-white border border-gray-500 rounded focus:border-green-400 focus:outline-none" + /> + setNewPercentage(parseInt(e.target.value))} + className="p-3 bg-gray-600 text-white border border-gray-500 rounded focus:border-green-400 focus:outline-none" + /> + setNewExpiration(e.target.value)} + className="p-3 bg-gray-600 text-white border border-gray-500 rounded focus:border-green-400 focus:outline-none" + /> + setNewDescription(e.target.value)} + className="p-3 bg-gray-600 text-white border border-gray-500 rounded focus:border-green-400 focus:outline-none" + /> +
+ +
+ +
+ + + + + + + + + + + + + {Object.entries(discountList).map(([code, data]) => { + const isExpired = + data.expiration && new Date() > new Date(data.expiration); + return ( + + + + + + + + + ); + })} + +
CodePercentageExpirationDescriptionStatusActions
{code} + {data.percentage}% + + {data.expiration + ? new Date(data.expiration).toLocaleDateString() + : "Never"} + + {data.description || "-"} + + + {isExpired ? "Expired" : "Active"} + + + +
+
+
+ ); +} diff --git a/src/app/api/admin/auth/route.ts b/src/app/api/admin/auth/route.ts new file mode 100644 index 0000000..1b82381 --- /dev/null +++ b/src/app/api/admin/auth/route.ts @@ -0,0 +1,11 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + const { password } = await request.json(); + + if (password === process.env.ADMIN_PASSWORD) { + return NextResponse.json({ success: true }); + } + + return NextResponse.json({ error: "Invalid password" }, { status: 401 }); +} diff --git a/src/app/api/admin/discounts/route.ts b/src/app/api/admin/discounts/route.ts new file mode 100644 index 0000000..cf99bc6 --- /dev/null +++ b/src/app/api/admin/discounts/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import { promises as fs } from "fs"; +import path from "path"; +import type { DiscountCode } from "../../../../types/discount"; + +const discountCodesPath = path.join( + process.cwd(), + "src/data/discountCodes.json", +); + +export async function POST(request: NextRequest) { + try { + const { code, percentage, expiration, description } = await request.json(); + + const fileContent = await fs.readFile(discountCodesPath, "utf8"); + const discountCodes = JSON.parse(fileContent); + + const newDiscountCode: DiscountCode = { + percentage, + description, + }; + + if (expiration) { + newDiscountCode.expiration = expiration; + } + + discountCodes[code] = newDiscountCode; + + await fs.writeFile( + discountCodesPath, + JSON.stringify(discountCodes, null, 2), + "utf8", + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error adding discount code:", error); + return NextResponse.json( + { error: "Failed to add discount code" }, + { status: 500 }, + ); + } +} + +export async function DELETE(request: NextRequest) { + try { + const { code } = await request.json(); + + const fileContent = await fs.readFile(discountCodesPath, "utf8"); + const discountCodes = JSON.parse(fileContent); + + delete discountCodes[code]; + + await fs.writeFile( + discountCodesPath, + JSON.stringify(discountCodes, null, 2), + "utf8", + ); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error deleting discount code:", error); + return NextResponse.json( + { error: "Failed to delete discount code" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/admin/products/route.ts b/src/app/api/admin/products/route.ts new file mode 100644 index 0000000..52cbc15 --- /dev/null +++ b/src/app/api/admin/products/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { promises as fs } from "fs"; +import path from "path"; + +const productsPath = path.join(process.cwd(), "src/data/products.ts"); + +export async function PUT(request: NextRequest) { + try { + const { productId, pricePerStack } = await request.json(); + + const fileContent = await fs.readFile(productsPath, "utf8"); + + const updatedContent = fileContent.replace( + new RegExp(`(id:\\s*"${productId}"[\\s\\S]*?pricePerStack:\\s*)[\\d.]+`), + `$1${pricePerStack}`, + ); + + await fs.writeFile(productsPath, updatedContent, "utf8"); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error updating product:", error); + return NextResponse.json( + { error: "Failed to update product" }, + { status: 500 }, + ); + } +} diff --git a/src/components/CartView.tsx b/src/components/CartView.tsx index 6ad29af..ad6d876 100644 --- a/src/components/CartView.tsx +++ b/src/components/CartView.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { useCart } from "../contexts/CartContext"; import { formatDiamonds } from "../utils/formatDiamonds"; import discountCodes from "../data/discountCodes.json"; +import type { DiscountCode } from "../types/discount"; export default function CartView() { const { state, dispatch } = useCart(); @@ -32,9 +33,22 @@ export default function CartView() { const applyDiscountCode = () => { const code = discountCode.toLowerCase(); - const discountInfo = discountCodes[code as keyof typeof discountCodes]; + const discountInfo = (discountCodes as Record)[code]; if (discountInfo) { + // Check if discount code has expired + if (discountInfo.expiration) { + const expirationDate = new Date(discountInfo.expiration); + const now = new Date(); + if (now > expirationDate) { + dispatch({ + type: "SET_DISCOUNT", + payload: { code: "", percentage: 0 }, + }); + return; + } + } + dispatch({ type: "SET_DISCOUNT", payload: { code: discountCode, percentage: discountInfo.percentage }, diff --git a/src/components/ProductSelection.tsx b/src/components/ProductSelection.tsx index c417695..0b96aeb 100644 --- a/src/components/ProductSelection.tsx +++ b/src/components/ProductSelection.tsx @@ -57,6 +57,7 @@ export default function ProductSelection() { }} className="w-full bg-gray-700 text-white p-3 rounded border border-gray-600 focus:border-green-400 focus:outline-none" > + {products.map((product) => (