This commit is contained in:
g
2025-02-07 05:22:21 +00:00
parent f19797e752
commit 3205bb1a6b
11 changed files with 1021 additions and 432 deletions

View File

@@ -1,14 +1,18 @@
"use client" "use client";
import { useState, useEffect, ChangeEvent } from "react"; import { useState, useEffect, ChangeEvent } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Layout from "@/components/kokonutui/layout"; import Layout from "@/components/kokonutui/layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Product } from "@/models/products"; import { Product } from "@/models/products";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { fetchProductData, saveProductData, deleteProductData } from "@/lib/productData"; import {
import { ProductModal } from "@/components/product-modal"; fetchProductData,
import ProductTable from "@/components/product-table"; saveProductData,
deleteProductData,
} from "@/lib/productData";
import { ProductModal } from "@/components/product-modal";
import ProductTable from "@/components/product-table";
export default function ProductsPage() { export default function ProductsPage() {
const router = useRouter(); const router = useRouter();
@@ -17,7 +21,7 @@ export default function ProductsPage() {
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [modalOpen, setModalOpen] = useState<boolean>(false); const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const [imagePreview, setImagePreview] = useState<string | null>(null); const [imagePreview, setImagePreview] = useState<string | null>(null);
const [productData, setProductData] = useState<Product>({ const [productData, setProductData] = useState<Product>({
name: "", name: "",
description: "", description: "",
@@ -66,7 +70,8 @@ export default function ProductsPage() {
index: number index: number
) => { ) => {
const updatedPricing = [...productData.tieredPricing]; const updatedPricing = [...productData.tieredPricing];
updatedPricing[index][e.target.name] = e.target.value; const name = e.target.name as "minQuantity" | "pricePerUnit";
updatedPricing[index][name] = e.target.valueAsNumber || 0;
setProductData({ ...productData, tieredPricing: updatedPricing }); setProductData({ ...productData, tieredPricing: updatedPricing });
}; };
@@ -74,30 +79,31 @@ export default function ProductsPage() {
const handleSaveProduct = async (data: Product) => { const handleSaveProduct = async (data: Product) => {
const adjustedPricing = data.tieredPricing.map((tier) => ({ const adjustedPricing = data.tieredPricing.map((tier) => ({
minQuantity: tier.minQuantity, minQuantity: tier.minQuantity,
pricePerUnit: typeof tier.pricePerUnit === "string" pricePerUnit:
? parseFloat(tier.pricePerUnit) // Convert string to number typeof tier.pricePerUnit === "string"
: tier.pricePerUnit, ? parseFloat(tier.pricePerUnit) // Convert string to number
: tier.pricePerUnit,
})); }));
const productToSave = { const productToSave = {
...data, ...data,
pricing: adjustedPricing, pricing: adjustedPricing,
imageBase64: imagePreview || "", imageBase64: imagePreview || "",
}; };
try { try {
const authToken = document.cookie.split("Authorization=")[1]; const authToken = document.cookie.split("Authorization=")[1];
const apiUrl = editing const apiUrl = editing
? `${process.env.NEXT_PUBLIC_API_URL}/products/${data._id}` ? `${process.env.NEXT_PUBLIC_API_URL}/products/${data._id}`
: `${process.env.NEXT_PUBLIC_API_URL}/products`; : `${process.env.NEXT_PUBLIC_API_URL}/products`;
const savedProduct = await saveProductData( const savedProduct = await saveProductData(
apiUrl, apiUrl,
productToSave, productToSave,
authToken, authToken,
editing ? "PUT" : "POST" editing ? "PUT" : "POST"
); );
setProducts((prevProducts) => { setProducts((prevProducts) => {
if (editing) { if (editing) {
return prevProducts.map((product) => return prevProducts.map((product) =>
@@ -107,7 +113,7 @@ export default function ProductsPage() {
return [...prevProducts, savedProduct]; return [...prevProducts, savedProduct];
} }
}); });
setModalOpen(false); // Close modal after saving setModalOpen(false); // Close modal after saving
} catch (error) { } catch (error) {
console.error("Error saving product:", error); console.error("Error saving product:", error);
@@ -122,7 +128,7 @@ export default function ProductsPage() {
`${process.env.NEXT_PUBLIC_API_URL}/products/${productId}`, `${process.env.NEXT_PUBLIC_API_URL}/products/${productId}`,
authToken authToken
); );
// Remove the product from the state // Remove the product from the state
setProducts((prevProducts) => setProducts((prevProducts) =>
prevProducts.filter((product) => product._id !== productId) prevProducts.filter((product) => product._id !== productId)
@@ -136,9 +142,9 @@ export default function ProductsPage() {
const handleEditProduct = (product: Product) => { const handleEditProduct = (product: Product) => {
setProductData({ setProductData({
...product, ...product,
tieredPricing: product.pricing.map((tier) => ({ tieredPricing: product.tieredPricing.map(tier => ({
minQuantity: tier.minQuantity, minQuantity: tier.minQuantity,
pricePerUnit: tier.pricePerUnit.toString(), pricePerUnit: tier.pricePerUnit
})), })),
}); });
setEditing(true); setEditing(true);
@@ -178,12 +184,12 @@ export default function ProductsPage() {
</Button> </Button>
</div> </div>
<ProductTable <ProductTable
products={products} products={products}
loading={loading} loading={loading}
onEdit={handleEditProduct} onEdit={handleEditProduct}
onDelete={handleDeleteProduct} // Pass handleDeleteProduct onDelete={handleDeleteProduct} // Pass handleDeleteProduct
getCategoryNameById={getCategoryNameById} getCategoryNameById={getCategoryNameById}
/> />
<ProductModal <ProductModal

View File

@@ -21,17 +21,7 @@ import {
updateShippingMethod, updateShippingMethod,
} from "@/lib/shippingHelper"; } from "@/lib/shippingHelper";
interface ShippingMethod { import { ShippingMethod, ShippingData } from "@/lib/types";
_id?: string; // Required after fetching
name: string;
price: number;
}
interface ShippingData {
_id?: string; // Optional for new entry
name: string;
price: number;
}
import { ShippingTable } from "@/components/shipping-table" import { ShippingTable } from "@/components/shipping-table"
@@ -50,10 +40,14 @@ export default function ShippingPage() {
try { try {
const authToken = document.cookie.split("Authorization=")[1]; const authToken = document.cookie.split("Authorization=")[1];
const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(authToken); const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(authToken);
setShippingMethods( // Ensure `_id` is always a string
fetchedMethods.filter((method) => method._id) const sanitizedMethods: ShippingMethod[] = fetchedMethods.map((method) => ({
); ...method,
_id: method._id ?? "", // Default to empty string if undefined
}));
setShippingMethods(sanitizedMethods);
} catch (error) { } catch (error) {
console.error("Error loading shipping options:", error); console.error("Error loading shipping options:", error);
} finally { } finally {
@@ -114,7 +108,10 @@ export default function ShippingPage() {
}; };
const handleEditShipping = (shipping: ShippingMethod) => { const handleEditShipping = (shipping: ShippingMethod) => {
setNewShipping({ ...shipping, _id: shipping._id ?? "" }); // Ensure _id is always a string setNewShipping({
...shipping,
_id: shipping._id ?? "", // ✅ Ensure _id is always a string
});
setEditing(true); setEditing(true);
setModalOpen(true); setModalOpen(true);
}; };

View File

@@ -1,201 +0,0 @@
"use client";
import { useState } from "react";
import Layout from "@/components/kokonutui/layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Send } from "lucide-react";
export default function WithdrawalsPageClient({ balances, withdrawals }) {
const [withdrawalData, setWithdrawalData] = useState({
currency: "bitcoin",
address: "",
amount: "",
});
const handleWithdraw = async () => {
const { currency, address, amount } = withdrawalData;
if (!currency || !address || !amount || parseFloat(amount) <= 0) {
alert("Please provide valid withdrawal details.");
return;
}
try {
const authToken = document.cookie.split("authToken=")[1];
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/withdraw`, {
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ currency, address, amount }),
});
if (!res.ok) throw new Error("Failed to process withdrawal");
alert("Withdrawal request submitted successfully!");
setWithdrawalData({ currency: "bitcoin", address: "", amount: "" });
} catch (error) {
console.error("Error processing withdrawal:", error);
alert("Failed to process withdrawal. Please try again.");
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setWithdrawalData({ ...withdrawalData, [name]: value });
};
return (
<Layout>
<div className="space-y-6">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
Withdraw Funds
</h1>
{/* Balances Display */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="shadow-sm bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700">
<CardHeader>
<CardTitle>Bitcoin Balance</CardTitle>
</CardHeader>
<CardContent>
<p className="text-lg font-semibold text-gray-900 dark:text-white">
{balances.bitcoin} BTC
</p>
</CardContent>
</Card>
<Card className="shadow-sm bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700">
<CardHeader>
<CardTitle>Litecoin Balance</CardTitle>
</CardHeader>
<CardContent>
<p className="text-lg font-semibold text-gray-900 dark:text-white">
{balances.litecoin} LTC
</p>
</CardContent>
</Card>
<Card className="shadow-sm bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700">
<CardHeader>
<CardTitle>Monero Balance</CardTitle>
</CardHeader>
<CardContent>
<p className="text-lg font-semibold text-gray-900 dark:text-white">
{balances.monero} XMR
</p>
</CardContent>
</Card>
</div>
{/* Withdrawal Form */}
<div className="bg-white dark:bg-zinc-800 p-6 rounded-lg shadow border border-gray-200 dark:border-gray-700 space-y-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Request Withdrawal
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Currency
</label>
<Select
value={withdrawalData.currency}
onValueChange={(value) =>
setWithdrawalData({ ...withdrawalData, currency: value })
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select Currency" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bitcoin">Bitcoin</SelectItem>
<SelectItem value="litecoin">Litecoin</SelectItem>
<SelectItem value="monero">Monero</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Withdrawal Address
</label>
<Input
name="address"
placeholder="Enter the withdrawal address"
value={withdrawalData.address}
onChange={handleChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Amount
</label>
<Input
name="amount"
placeholder="Enter the amount to withdraw"
type="number"
min="0"
step="any"
value={withdrawalData.amount}
onChange={handleChange}
/>
</div>
</div>
<Button onClick={handleWithdraw} className="w-full">
<Send className="mr-2 h-5 w-5" />
Request Withdrawal
</Button>
</div>
{/* Withdrawals History */}
<div className="bg-white dark:bg-zinc-800 p-6 rounded-lg shadow border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Withdrawals History
</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Currency</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Address</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{withdrawals.length > 0 ? (
withdrawals.map((withdrawal) => (
<TableRow key={withdrawal.id}>
<TableCell>{withdrawal.date}</TableCell>
<TableCell>{withdrawal.currency.toUpperCase()}</TableCell>
<TableCell>{withdrawal.amount}</TableCell>
<TableCell>{withdrawal.address}</TableCell>
<TableCell>{withdrawal.status}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center">
No withdrawals found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</Layout>
);
}

View File

@@ -1,18 +0,0 @@
import WithdrawalsPageClient from "./WithdrawalsPageClient";
export default async function WithdrawalsPage() {
const authToken = ""; // Fetch token from cookies if needed
const [balancesRes, withdrawalsRes] = await Promise.all([
fetch(`${process.env.NEXT_PUBLIC_API_URL}/balances`, {
headers: { Authorization: `Bearer ${authToken}` },
}),
fetch(`${process.env.NEXT_PUBLIC_API_URL}/withdrawals`, {
headers: { Authorization: `Bearer ${authToken}` },
}),
]);
const balances = balancesRes.ok ? await balancesRes.json() : {};
const withdrawals = withdrawalsRes.ok ? await withdrawalsRes.json() : [];
return <WithdrawalsPageClient balances={balances} withdrawals={withdrawals} />;
}

View File

@@ -30,16 +30,14 @@ interface ProductModalProps {
onClose: () => void; onClose: () => void;
onSave: (productData: Product) => void; onSave: (productData: Product) => void;
productData: Product; productData: Product;
categories: Category[]; // Define categories as an array of Category categories: Category[];
editing: boolean; editing: boolean;
handleChange: ( handleChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> setProductData: React.Dispatch<React.SetStateAction<Product>>;
) => void;
handleTieredPricingChange: ( handleTieredPricingChange: (
e: ChangeEvent<HTMLInputElement>, e: ChangeEvent<HTMLInputElement>,
index: number index: number
) => void; ) => void;
setProductData: React.Dispatch<React.SetStateAction<Product>>;
} }
export const ProductModal = ({ export const ProductModal = ({
@@ -55,10 +53,10 @@ export const ProductModal = ({
}: ProductModalProps) => { }: ProductModalProps) => {
const [imagePreview, setImagePreview] = useState<string | null>(null); const [imagePreview, setImagePreview] = useState<string | null>(null);
// Update image preview when product data changes (for editing purposes) // Update image preview when product data changes
useEffect(() => { useEffect(() => {
if (productData.image && typeof productData.image === "string") { if (productData.image && typeof productData.image === "string") {
setImagePreview(productData.image); // For existing product image setImagePreview(productData.image);
} }
}, [productData.image]); }, [productData.image]);
@@ -73,21 +71,23 @@ export const ProductModal = ({
} }
}; };
// Handle tiered pricing change // ✅ FIXED: Moved Inside the Component & Used Type Assertion
const handleTieredPricingChangeInternal = ( const handleTieredPricingChangeInternal = (
e: ChangeEvent<HTMLInputElement>, e: ChangeEvent<HTMLInputElement>,
index: number index: number
) => { ) => {
const updatedPricing = [...productData.tieredPricing]; const updatedPricing = [...productData.tieredPricing];
const updatedTier = updatedPricing[index]; const field = e.target.name as keyof (typeof productData.tieredPricing)[number];
// Ensure pricePerUnit is a number updatedPricing[index] = {
const value = e.target.name === "pricePerUnit" ...updatedPricing[index],
? parseFloat(e.target.value) || 0 // If value is invalid, default to 0 [field]: e.target.valueAsNumber || 0,
: e.target.value; };
updatedTier[e.target.name] = value; setProductData({
setProductData({ ...productData, tieredPricing: updatedPricing }); ...productData,
tieredPricing: updatedPricing,
});
}; };
return ( return (
@@ -98,36 +98,21 @@ export const ProductModal = ({
{editing ? "Edit Product" : "Add Product"} {editing ? "Edit Product" : "Add Product"}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Product Name</label> <label className="text-sm font-medium">Product Name</label>
<Input <Input name="name" placeholder="Product Name" value={productData.name} onChange={handleChange} />
name="name"
placeholder="Product Name"
value={productData.name}
onChange={handleChange}
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Description</label> <label className="text-sm font-medium">Description</label>
<Textarea <Textarea name="description" placeholder="Product Description" value={productData.description} onChange={handleChange} rows={3} />
name="description"
placeholder="Product Description"
value={productData.description}
onChange={handleChange}
rows={3}
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Category</label> <label className="text-sm font-medium">Category</label>
<Select <Select value={productData.category} onValueChange={(value) => setProductData({ ...productData, category: value })}>
value={productData.category}
onValueChange={(value) =>
setProductData({ ...productData, category: value })
}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a category" /> <SelectValue placeholder="Select a category" />
</SelectTrigger> </SelectTrigger>
@@ -141,127 +126,25 @@ export const ProductModal = ({
</Select> </Select>
</div> </div>
{productData.category && (
<div className="space-y-2">
<h3 className="text-sm font-medium">Tiered Pricing</h3>
{productData.tieredPricing.map((tier, index) => (
<div key={index} className="flex gap-2">
<div className="flex-1 space-y-1">
<label className="text-xs text-muted-foreground">
Quantity
</label>
<Input
name="minQuantity"
type="number"
min="1"
placeholder="Quantity"
value={tier.minQuantity}
onChange={(e) => handleTieredPricingChangeInternal(e, index)}
/>
</div>
<div className="flex-1 space-y-1">
<label className="text-xs text-muted-foreground">
Price
</label>
<Input
name="pricePerUnit"
type="number"
step="0.01"
placeholder="Price"
value={tier.pricePerUnit}
onChange={(e) => handleTieredPricingChangeInternal(e, index)}
/>
</div>
</div>
))}
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={() =>
setProductData({
...productData,
tieredPricing: [
...productData.tieredPricing,
{ minQuantity: 1, pricePerUnit: 0 }, // Initialize pricePerUnit as a number
],
})
}
>
+ Add Tier
</Button>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Unit Type</label> <h3 className="text-sm font-medium">Tiered Pricing</h3>
<Select {productData.tieredPricing.map((tier, index) => (
value={productData.unitType} <div key={index} className="flex gap-2">
onValueChange={(value) => <Input name="minQuantity" type="number" min="1" placeholder="Quantity" value={tier.minQuantity} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
setProductData({ ...productData, unitType: value }) <Input name="pricePerUnit" type="number" step="0.01" placeholder="Price" value={tier.pricePerUnit} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
}
>
<SelectTrigger>
<SelectValue placeholder="Select unit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pcs">Pieces</SelectItem>
<SelectItem value="gr">Grams</SelectItem>
<SelectItem value="kg">Kilograms</SelectItem>
</SelectContent>
</Select>
</div>
{/* Image Upload Section */}
<div className="space-y-2">
<label className="text-sm font-medium">Product Image</label>
<Input
type="file"
accept="image/*"
className="file:text-foreground"
onChange={handleImageChange}
/>
{imagePreview && (
<div className="mt-2">
<img
src={imagePreview}
alt="Product preview"
className="rounded-md border w-32 h-32 object-cover"
/>
<p className="text-xs text-muted-foreground mt-1">
Click the upload button above to change image
</p>
</div> </div>
)} ))}
{!imagePreview && <Button variant="outline" size="sm" onClick={() => setProductData({ ...productData, tieredPricing: [...productData.tieredPricing, { minQuantity: 1, pricePerUnit: 0 }] })}>
productData.image && + Add Tier
typeof productData.image === "string" && ( </Button>
<div className="mt-2">
<img
src={productData.image}
alt="Existing product"
className="rounded-md border w-32 h-32 object-cover"
/>
<p className="text-xs text-muted-foreground mt-1">
Existing product image
</p>
</div>
)}
<p className="text-xs text-muted-foreground">
Upload a product image (JPEG, PNG, WEBP)
</p>
</div> </div>
</div> </div>
<DialogFooter className="flex justify-end gap-2"> <DialogFooter className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}> <Button variant="outline" onClick={onClose}>Cancel</Button>
Cancel <Button onClick={() => onSave(productData)}>{editing ? "Update Product" : "Create Product"}</Button>
</Button>
<Button onClick={() => onSave(productData)}>
{editing ? "Update Product" : "Create Product"}
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };

View File

@@ -5,20 +5,17 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
interface ShippingData {
name: string; import { ShippingData } from "@/lib/types";
price: number;
_id?: string; // Optional for new entry
}
interface ShippingModalProps { interface ShippingModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSave: (shippingData: ShippingData) => void; onSave: (shippingData: ShippingData) => void; // ✅ Allow passing shippingData
shippingData: { name: string; price: number }; // Define type of shippingData shippingData: ShippingData;
setShippingData: React.Dispatch<React.SetStateAction<ShippingData>>;
editing: boolean; editing: boolean;
handleChange: (e: ChangeEvent<HTMLInputElement>) => void; handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
setShippingData: React.Dispatch<React.SetStateAction<{ name: string; price: 0.00 }>>;
} }
export const ShippingModal = ({ export const ShippingModal = ({

View File

@@ -1,13 +1,16 @@
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Edit, Trash } from "lucide-react"; import { Edit, Trash } from "lucide-react";
interface ShippingMethod { import { ShippingMethod } from "@/lib/types";
_id: string;
name: string;
price: number;
}
interface ShippingTableProps { interface ShippingTableProps {
shippingMethods: ShippingMethod[]; shippingMethods: ShippingMethod[];
@@ -46,9 +49,7 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70" className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70"
> >
<TableCell className="font-medium">{method.name}</TableCell> <TableCell className="font-medium">{method.name}</TableCell>
<TableCell className="text-center"> <TableCell className="text-center">£{method.price}</TableCell>
£{method.price}
</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
@@ -63,7 +64,7 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
size="sm" size="sm"
variant="ghost" variant="ghost"
className="text-red-600 hover:text-red-700 dark:text-red-400" className="text-red-600 hover:text-red-700 dark:text-red-400"
onClick={() => onDeleteShipping(method._id)} onClick={() => onDeleteShipping(method._id ?? "")}
> >
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
</Button> </Button>

12
lib/types.ts Normal file
View File

@@ -0,0 +1,12 @@
// lib/types.ts
export interface ShippingMethod {
_id?: string; // Optional before saving, required after fetching
name: string;
price: number;
}
export interface ShippingData {
_id?: string; // Optional before saving
name: string;
price: number;
}

View File

@@ -1,10 +1,13 @@
// In models/products.ts
export interface Product { export interface Product {
_id?: string; _id?: string;
name: string; name: string;
description: string; description: string;
unitType: "pcs" | "gr" | "kg"; unitType: string;
category: string; category: string;
tieredPricing: { minQuantity: number; pricePerUnit: number }[]; // Make pricePerUnit a number tieredPricing: Array<{
image?: string | null; minQuantity: number;
} pricePerUnit: number;
}>;
image?: string | File | null;
}

908
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -63,6 +63,7 @@
"@types/node": "^22", "@types/node": "^22",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"eslint": "^9.19.0",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5" "typescript": "^5"