add admin page and fix default option
This commit is contained in:
parent
956e59fbda
commit
a96cb500ca
377
src/app/admin/page.tsx
Normal file
377
src/app/admin/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/app/api/admin/auth/route.ts
Normal file
11
src/app/api/admin/auth/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
68
src/app/api/admin/discounts/route.ts
Normal file
68
src/app/api/admin/discounts/route.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/api/admin/products/route.ts
Normal file
28
src/app/api/admin/products/route.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
import { useCart } from "../contexts/CartContext";
|
import { useCart } from "../contexts/CartContext";
|
||||||
import { formatDiamonds } from "../utils/formatDiamonds";
|
import { formatDiamonds } from "../utils/formatDiamonds";
|
||||||
import discountCodes from "../data/discountCodes.json";
|
import discountCodes from "../data/discountCodes.json";
|
||||||
|
import type { DiscountCode } from "../types/discount";
|
||||||
|
|
||||||
export default function CartView() {
|
export default function CartView() {
|
||||||
const { state, dispatch } = useCart();
|
const { state, dispatch } = useCart();
|
||||||
@ -32,9 +33,22 @@ export default function CartView() {
|
|||||||
|
|
||||||
const applyDiscountCode = () => {
|
const applyDiscountCode = () => {
|
||||||
const code = discountCode.toLowerCase();
|
const code = discountCode.toLowerCase();
|
||||||
const discountInfo = discountCodes[code as keyof typeof discountCodes];
|
const discountInfo = (discountCodes as Record<string, DiscountCode>)[code];
|
||||||
|
|
||||||
if (discountInfo) {
|
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({
|
dispatch({
|
||||||
type: "SET_DISCOUNT",
|
type: "SET_DISCOUNT",
|
||||||
payload: { code: discountCode, percentage: discountInfo.percentage },
|
payload: { code: discountCode, percentage: discountInfo.percentage },
|
||||||
|
|||||||
@ -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"
|
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) => (
|
{products.map((product) => (
|
||||||
<option key={product.id} value={product.id}>
|
<option key={product.id} value={product.id}>
|
||||||
{product.name} ({getProductPrice(product)})
|
{product.name} ({getProductPrice(product)})
|
||||||
|
|||||||
@ -1 +1,6 @@
|
|||||||
{}
|
{
|
||||||
|
"test": {
|
||||||
|
"percentage": 5,
|
||||||
|
"description": "this is a test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
9
src/types/discount.ts
Normal file
9
src/types/discount.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface DiscountCode {
|
||||||
|
percentage: number;
|
||||||
|
expiration?: string; // ISO date string, optional
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscountCodes {
|
||||||
|
[code: string]: DiscountCode;
|
||||||
|
}
|
||||||
@ -22,44 +22,3 @@ export interface Order {
|
|||||||
playerName: string;
|
playerName: string;
|
||||||
coords: 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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user