add admin page and fix default option

This commit is contained in:
Keiran 2025-09-28 12:49:32 +01:00
parent 956e59fbda
commit a96cb500ca
9 changed files with 515 additions and 43 deletions

377
src/app/admin/page.tsx Normal file
View File

@ -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 (
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<div className="bg-gray-800 p-8 rounded-lg border border-gray-700 w-96">
<h1 className="text-2xl font-bold text-center mb-6 text-green-400">
Admin Login
</h1>
<div className="space-y-4">
<input
type="password"
placeholder="Enter admin password"
className="w-full p-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:border-green-400 focus:outline-none"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleLogin()}
/>
<button
onClick={handleLogin}
className="w-full bg-green-600 hover:bg-green-700 text-white p-3 rounded-lg transition-colors"
>
Login
</button>
{error && <p className="text-red-400 text-center">{error}</p>}
</div>
</div>
</div>
);
}
return <AdminDashboard />;
}
function AdminDashboard() {
const [activeTab, setActiveTab] = useState<"products" | "discounts">(
"products",
);
return (
<div className="min-h-screen bg-gray-900">
<header className="bg-green-700 text-white py-6">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold text-center">
Admin Dashboard - ASS AB
</h1>
</div>
</header>
<div className="container mx-auto px-4 py-8">
<div className="flex space-x-1 mb-6 justify-center">
<button
onClick={() => setActiveTab("products")}
className={`px-6 py-3 rounded-lg transition-colors ${
activeTab === "products"
? "bg-green-600 text-white"
: "bg-gray-600 hover:bg-gray-700 text-gray-300"
}`}
>
Products & Prices
</button>
<button
onClick={() => setActiveTab("discounts")}
className={`px-6 py-3 rounded-lg transition-colors ${
activeTab === "discounts"
? "bg-green-600 text-white"
: "bg-gray-600 hover:bg-gray-700 text-gray-300"
}`}
>
Discount Codes
</button>
</div>
<div className="max-w-6xl mx-auto">
{activeTab === "products" && <ProductsManager />}
{activeTab === "discounts" && <DiscountManager />}
</div>
</div>
<footer className="bg-gray-800 text-gray-400 py-6 mt-12">
<div className="container mx-auto px-4 text-center">
<p>© ASS co 2025 - Admin Panel</p>
</div>
</footer>
</div>
);
}
function ProductsManager() {
const [productList, setProductList] = useState(products);
const [editingProduct, setEditingProduct] = useState<string | null>(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 (
<div className="bg-gray-800 rounded-lg border border-gray-700 p-6">
<h2 className="text-2xl font-bold mb-4 text-green-400">Product Prices</h2>
<div className="overflow-x-auto">
<table className="w-full table-auto">
<thead>
<tr className="bg-gray-700">
<th className="px-4 py-3 text-left text-gray-300">Product</th>
<th className="px-4 py-3 text-left text-gray-300">
Price per Stack
</th>
<th className="px-4 py-3 text-left text-gray-300">Actions</th>
</tr>
</thead>
<tbody>
{productList.map((product) => (
<tr key={product.id} className="border-t border-gray-600">
<td className="px-4 py-3 text-white">{product.name}</td>
<td className="px-4 py-3 text-gray-300">
{editingProduct === product.id ? (
<input
type="number"
step="0.001"
defaultValue={product.pricePerStack}
className="w-24 p-2 bg-gray-700 text-white border border-gray-600 rounded focus:border-green-400 focus:outline-none"
onKeyDown={(e) => {
if (e.key === "Enter") {
const target = e.target as HTMLInputElement;
updatePrice(product.id, parseFloat(target.value));
}
}}
/>
) : (
`${product.pricePerStack} diamonds`
)}
</td>
<td className="px-4 py-3">
{editingProduct === product.id ? (
<div className="space-x-2">
<button
onClick={() => {
const input = document.querySelector(
`input[defaultValue="${product.pricePerStack}"]`,
) as HTMLInputElement;
updatePrice(product.id, parseFloat(input.value));
}}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm transition-colors"
>
Save
</button>
<button
onClick={() => setEditingProduct(null)}
className="bg-gray-600 hover:bg-gray-700 text-white px-3 py-1 rounded text-sm transition-colors"
>
Cancel
</button>
</div>
) : (
<button
onClick={() => setEditingProduct(product.id)}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm transition-colors"
>
Edit
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
function DiscountManager() {
const [discountList, setDiscountList] =
useState<Record<string, DiscountCode>>(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 (
<div className="bg-gray-800 rounded-lg border border-gray-700 p-6">
<h2 className="text-2xl font-bold mb-4 text-green-400">Discount Codes</h2>
<div className="mb-6 p-4 bg-gray-700 rounded-lg border border-gray-600">
<h3 className="text-lg font-semibold mb-3 text-white">
Add New Discount Code
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="text"
placeholder="Code (e.g., SAVE20)"
value={newCode}
onChange={(e) => setNewCode(e.target.value)}
className="p-3 bg-gray-600 text-white border border-gray-500 rounded focus:border-green-400 focus:outline-none"
/>
<input
type="number"
placeholder="Percentage"
value={newPercentage}
onChange={(e) => 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"
/>
<input
type="date"
placeholder="Expiration (optional)"
value={newExpiration}
onChange={(e) => setNewExpiration(e.target.value)}
className="p-3 bg-gray-600 text-white border border-gray-500 rounded focus:border-green-400 focus:outline-none"
/>
<input
type="text"
placeholder="Description (optional)"
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
className="p-3 bg-gray-600 text-white border border-gray-500 rounded focus:border-green-400 focus:outline-none"
/>
</div>
<button
onClick={addDiscountCode}
className="mt-4 bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded transition-colors"
>
Add Code
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full table-auto">
<thead>
<tr className="bg-gray-700">
<th className="px-4 py-3 text-left text-gray-300">Code</th>
<th className="px-4 py-3 text-left text-gray-300">Percentage</th>
<th className="px-4 py-3 text-left text-gray-300">Expiration</th>
<th className="px-4 py-3 text-left text-gray-300">Description</th>
<th className="px-4 py-3 text-left text-gray-300">Status</th>
<th className="px-4 py-3 text-left text-gray-300">Actions</th>
</tr>
</thead>
<tbody>
{Object.entries(discountList).map(([code, data]) => {
const isExpired =
data.expiration && new Date() > new Date(data.expiration);
return (
<tr key={code} className="border-t border-gray-600">
<td className="px-4 py-3 text-white font-mono">{code}</td>
<td className="px-4 py-3 text-gray-300">
{data.percentage}%
</td>
<td className="px-4 py-3 text-gray-300">
{data.expiration
? new Date(data.expiration).toLocaleDateString()
: "Never"}
</td>
<td className="px-4 py-3 text-gray-300">
{data.description || "-"}
</td>
<td className="px-4 py-3">
<span
className={`px-2 py-1 rounded text-xs font-semibold ${
isExpired
? "bg-red-900 text-red-300"
: "bg-green-900 text-green-300"
}`}
>
{isExpired ? "Expired" : "Active"}
</span>
</td>
<td className="px-4 py-3">
<button
onClick={() => deleteDiscountCode(code)}
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition-colors"
>
Delete
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}

View File

@ -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 });
}

View File

@ -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 },
);
}
}

View File

@ -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 },
);
}
}

View File

@ -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<string, DiscountCode>)[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 },

View File

@ -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"
>
<option value="">Välj en produkt</option>
{products.map((product) => (
<option key={product.id} value={product.id}>
{product.name} ({getProductPrice(product)})

View File

@ -1 +1,6 @@
{}
{
"test": {
"percentage": 5,
"description": "this is a test"
}
}

9
src/types/discount.ts Normal file
View File

@ -0,0 +1,9 @@
export interface DiscountCode {
percentage: number;
expiration?: string; // ISO date string, optional
description?: string;
}
export interface DiscountCodes {
[code: string]: DiscountCode;
}

View File

@ -22,44 +22,3 @@ export interface Order {
playerName: string;
coords: string;
}
export const products: Product[] = [
{
id: "spruce-log",
name: "Spruce Log",
pricePerStack: 1,
},
{
id: "birch-log",
name: "Birch Log",
pricePerStack: 1,
},
{
id: "oak-log",
name: "Oak Log",
pricePerStack: 1,
},
{
id: "jungle-log",
name: "Jungle Log",
pricePerStack: 1,
},
{
id: "cobblestone",
name: "Cobblestone",
pricePerStack: 1 / 16,
defaultStacksPerDia: 16,
variants: ["Cobblestone", "Stone", "Stone Bricks"],
},
{
id: "sand",
name: "Sand",
pricePerStack: 1,
},
{
id: "gunpowder",
name: "Gunpowder",
pricePerStack: 1 / 4,
defaultStacksPerDia: 4,
},
];