This commit is contained in:
g
2025-02-07 04:43:47 +00:00
parent d3e19c7e09
commit 6c4f3f0866
94 changed files with 11777 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
"use client"
import { useParams } from "next/navigation"
import { useState } from "react"
import Layout from "@/components/kokonutui/layout"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ArrowLeft, Truck, DollarSign, Package, User } from "lucide-react"
import Link from "next/link"
export default function OrderDetails() {
const params = useParams()
const orderId = params.id
// In a real application, you would fetch the order details based on the orderId
const [trackingNumber, setTrackingNumber] = useState("")
const order = {
id: orderId,
customer: "John Doe",
email: "john.doe@example.com",
date: "2023-05-01",
total: "$150.00",
status: "Processing",
tracking: "",
items: [
{ id: 1, name: "Product A", quantity: 2, price: "$50.00" },
{ id: 2, name: "Product B", quantity: 1, price: "$50.00" },
],
shippingAddress: "123 Main St, Anytown, AN 12345",
}
const handleSaveTracking = () => {
// TODO: Implement API call to save tracking number
console.log("Tracking number saved:", trackingNumber)
}
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Order Details: {order.id}</h1>
<Link href="/dashboard/orders">
<Button variant="outline">
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Orders
</Button>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white dark:bg-zinc-800 p-6 rounded-lg border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
<User className="mr-2 h-5 w-5" />
Customer Information
</h2>
<p><strong>Name:</strong> {order.customer}</p>
<p><strong>Email:</strong> {order.email}</p>
<p><strong>Shipping Address:</strong> {order.shippingAddress}</p>
</div>
<div className="bg-white dark:bg-zinc-800 p-6 rounded-lg border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center">
<Package className="mr-2 h-5 w-5" />
Order Summary
</h2>
<p><strong>Order Date:</strong> {order.date}</p>
<p><strong>Total:</strong> {order.total}</p>
<p><strong>Status:</strong> {order.status}</p>
{/* Tracking Input Field */}
<div className="mt-4">
<p className="font-semibold text-gray-900 dark:text-white">Tracking Number:</p>
<Input
type="text"
placeholder="Enter tracking number"
value={trackingNumber}
onChange={(e) => setTrackingNumber(e.target.value)}
className="mt-2"
/>
<Button className="mt-2" onClick={handleSaveTracking}>
Save Tracking
</Button>
</div>
</div>
</div>
<div className="bg-white dark:bg-zinc-800 p-6 rounded-lg border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Order Items</h2>
<table className="w-full">
<thead>
<tr>
<th className="text-left">Product</th>
<th className="text-left">Quantity</th>
<th className="text-left">Price</th>
</tr>
</thead>
<tbody>
{order.items.map((item) => (
<tr key={item.id}>
<td>{item.name}</td>
<td>{item.quantity}</td>
<td>{item.price}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-end space-x-4">
<Button variant="outline">
<Truck className="mr-2 h-4 w-4" />
Update Shipping
</Button>
<Button variant="outline">
<DollarSign className="mr-2 h-4 w-4" />
Process Refund
</Button>
</div>
</div>
</Layout>
)
}

View File

@@ -0,0 +1,21 @@
import Dashboard from "@/components/kokonutui/dashboard";
import { Package } from "lucide-react";
import OrderTable from "@/components/order-table"
export default function OrdersPage() {
return (
<Dashboard>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
<Package className="mr-2 h-6 w-6" />
Orders
</h1>
</div>
{/* ✅ Order Table Component */}
<OrderTable />
</div>
</Dashboard>
);
}

38
app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,38 @@
import Dashboard from "@/components/kokonutui/dashboard";
import Content from "@/components/kokonutui/content";
import { fetchWithAuthorization } from "@/lib/server-utils"
// ✅ Corrected Vendor Type
interface Vendor {
_id: string;
username: string;
storeId: string;
pgpKey: string;
__v: number;
}
interface User {
vendor: Vendor;
}
interface OrderStats {
totalOrders: number;
pendingOrders: number;
completedOrders: number;
cancelledOrders: number;
}
export default async function DashboardPage() {
const [userResponse, orderStats] = await Promise.all([
fetchWithAuthorization<User>("/auth/me"),
fetchWithAuthorization<OrderStats>("/orders/stats"),
]);
const vendor = userResponse.vendor;
return (
<Dashboard>
<Content username={vendor.username} orderStats={orderStats} />
</Dashboard>
);
}

View File

@@ -0,0 +1,203 @@
"use client"
import { useState, useEffect, ChangeEvent } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/kokonutui/layout";
import { Button } from "@/components/ui/button";
import { Product } from "@/models/products";
import { Plus } from "lucide-react";
import { fetchProductData, saveProductData, deleteProductData } from "@/lib/productData";
import { ProductModal } from "@/components/product-modal";
import ProductTable from "@/components/product-table";
export default function ProductsPage() {
const router = useRouter();
const [products, setProducts] = useState<Product[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editing, setEditing] = useState<boolean>(false);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [productData, setProductData] = useState<Product>({
name: "",
description: "",
unitType: "pcs",
category: "",
tieredPricing: [{ minQuantity: 1, pricePerUnit: 0 }],
image: null,
});
// Fetch products and categories
useEffect(() => {
const fetchDataAsync = async () => {
try {
const authToken = document.cookie.split("Authorization=")[1];
const [fetchedProducts, fetchedCategories] = await Promise.all([
fetchProductData(
`${process.env.NEXT_PUBLIC_API_URL}/products`,
authToken
),
fetchProductData(
`${process.env.NEXT_PUBLIC_API_URL}/categories`,
authToken
),
]);
setProducts(fetchedProducts);
setCategories(fetchedCategories);
} catch (error) {
console.error("Error loading data:", error);
} finally {
setLoading(false);
}
};
fetchDataAsync();
}, []);
// Handle input changes
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => setProductData({ ...productData, [e.target.name]: e.target.value });
// Handle tiered pricing changes
const handleTieredPricingChange = (
e: ChangeEvent<HTMLInputElement>,
index: number
) => {
const updatedPricing = [...productData.tieredPricing];
updatedPricing[index][e.target.name] = e.target.value;
setProductData({ ...productData, tieredPricing: updatedPricing });
};
// Save product data after modal form submission
const handleSaveProduct = async (data: Product) => {
const adjustedPricing = data.tieredPricing.map((tier) => ({
minQuantity: tier.minQuantity,
pricePerUnit: typeof tier.pricePerUnit === "string"
? parseFloat(tier.pricePerUnit) // Convert string to number
: tier.pricePerUnit,
}));
const productToSave = {
...data,
pricing: adjustedPricing,
imageBase64: imagePreview || "",
};
try {
const authToken = document.cookie.split("Authorization=")[1];
const apiUrl = editing
? `${process.env.NEXT_PUBLIC_API_URL}/products/${data._id}`
: `${process.env.NEXT_PUBLIC_API_URL}/products`;
const savedProduct = await saveProductData(
apiUrl,
productToSave,
authToken,
editing ? "PUT" : "POST"
);
setProducts((prevProducts) => {
if (editing) {
return prevProducts.map((product) =>
product._id === savedProduct._id ? savedProduct : product
);
} else {
return [...prevProducts, savedProduct];
}
});
setModalOpen(false); // Close modal after saving
} catch (error) {
console.error("Error saving product:", error);
}
};
// Handle delete product
const handleDeleteProduct = async (productId: string) => {
const authToken = document.cookie.split("Authorization=")[1];
try {
await deleteProductData(
`${process.env.NEXT_PUBLIC_API_URL}/products/${productId}`,
authToken
);
// Remove the product from the state
setProducts((prevProducts) =>
prevProducts.filter((product) => product._id !== productId)
);
} catch (error) {
console.error("Error deleting product:", error);
}
};
// Edit product
const handleEditProduct = (product: Product) => {
setProductData({
...product,
tieredPricing: product.pricing.map((tier) => ({
minQuantity: tier.minQuantity,
pricePerUnit: tier.pricePerUnit.toString(),
})),
});
setEditing(true);
setModalOpen(true);
};
// Reset product data when adding a new product
const handleAddNewProduct = () => {
setProductData({
name: "",
description: "",
unitType: "pcs",
category: "",
tieredPricing: [{ minQuantity: 1, pricePerUnit: 0 }],
image: null,
});
setEditing(false);
setModalOpen(true);
};
// Get category name by ID
const getCategoryNameById = (categoryId: string): string => {
const category = categories.find((cat) => cat._id === categoryId);
return category ? category.name : "Unknown Category";
};
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
Product Inventory
</h1>
<Button onClick={handleAddNewProduct}>
<Plus className="mr-2 h-5 w-5" />
Add Product
</Button>
</div>
<ProductTable
products={products}
loading={loading}
onEdit={handleEditProduct}
onDelete={handleDeleteProduct} // Pass handleDeleteProduct
getCategoryNameById={getCategoryNameById}
/>
<ProductModal
open={modalOpen}
onClose={() => setModalOpen(false)}
onSave={handleSaveProduct}
productData={productData}
categories={categories}
editing={editing}
handleChange={handleChange}
handleTieredPricingChange={handleTieredPricingChange}
setProductData={setProductData}
/>
</div>
</Layout>
);
}

View File

@@ -0,0 +1,158 @@
"use client";
import { useState, useEffect, ChangeEvent } from "react";
import Layout from "@/components/kokonutui/layout";
import { Edit, Plus, Trash } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ShippingModal } from "@/components/shipping-modal";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton";
import {
fetchShippingMethods,
addShippingMethod,
deleteShippingMethod,
updateShippingMethod,
} from "@/lib/shippingHelper";
interface ShippingMethod {
_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"
export default function ShippingPage() {
const [shippingMethods, setShippingMethods] = useState<ShippingMethod[]>([]);
const [newShipping, setNewShipping] = useState<ShippingData>({
name: "",
price: 0,
});
const [loading, setLoading] = useState<boolean>(true);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editing, setEditing] = useState<boolean>(false);
useEffect(() => {
const fetchShippingMethodsData = async () => {
try {
const authToken = document.cookie.split("Authorization=")[1];
const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(authToken);
setShippingMethods(
fetchedMethods.filter((method) => method._id)
);
} catch (error) {
console.error("Error loading shipping options:", error);
} finally {
setLoading(false);
}
};
fetchShippingMethodsData();
}, []);
const handleAddShipping = async () => {
if (!newShipping.name || !newShipping.price) return;
try {
const authToken = document.cookie.split("Authorization=")[1];
const updatedMethods: ShippingMethod[] = await addShippingMethod(authToken, newShipping);
setShippingMethods(updatedMethods);
setNewShipping({ name: "", price: 0 }); // No `_id` needed for new entry
setModalOpen(false);
} catch (error) {
console.error("Error adding shipping method:", error);
}
};
const handleUpdateShipping = async () => {
if (!newShipping.name || !newShipping.price || !newShipping._id) return; // Ensure `_id` exists
try {
const authToken = document.cookie.split("Authorization=")[1];
const updatedShipping: ShippingMethod = await updateShippingMethod(authToken, newShipping._id, newShipping);
setShippingMethods((prevMethods) =>
prevMethods.map((method) => (method._id === updatedShipping._id ? updatedShipping : method))
);
setNewShipping({ name: "", price: 0 });
setEditing(false);
setModalOpen(false);
} catch (error) {
console.error("Error updating shipping method:", error);
}
};
const handleDeleteShipping = async (_id: string) => {
try {
const authToken = document.cookie.split("Authorization=")[1];
const response = await deleteShippingMethod(authToken, _id);
if (response.success) {
setShippingMethods((prevMethods) =>
prevMethods.filter((method) => method._id !== _id)
);
} else {
console.error("Deletion was not successful.");
}
} catch (error) {
console.error("Error deleting shipping method:", error);
}
};
const handleEditShipping = (shipping: ShippingMethod) => {
setNewShipping({ ...shipping, _id: shipping._id ?? "" }); // Ensure _id is always a string
setEditing(true);
setModalOpen(true);
};
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
Manage Shipping Options
</h1>
<Button onClick={() => setModalOpen(true)}>
<Plus className="mr-2 h-5 w-5" />
Add Shipping Method
</Button>
</div>
{/* Shipping Methods Table */}
<ShippingTable
shippingMethods={shippingMethods}
loading={loading}
onEditShipping={handleEditShipping}
onDeleteShipping={handleDeleteShipping}
/>
</div>
{/* Shipping Modal */}
<ShippingModal
open={modalOpen}
onClose={() => setModalOpen(false)}
onSave={editing ? handleUpdateShipping : handleAddShipping}
shippingData={newShipping}
editing={editing}
handleChange={(e) =>
setNewShipping({ ...newShipping, [e.target.name]: e.target.value })
}
setShippingData={setNewShipping}
/>
</Layout>
);
}

View File

@@ -0,0 +1,148 @@
"use client";
import { useState, useEffect, ChangeEvent } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/kokonutui/layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Save, Send, Key, MessageSquare, Shield } from "lucide-react";
import { apiRequest } from "@/lib/storeHelper";
import { toast, Toaster } from "sonner";
import BroadcastDialog from "@/components/broadcast-dialog";
// ✅ Define the Storefront Type
interface Storefront {
pgpKey: string;
welcomeMessage: string;
telegramToken: string;
}
export default function StorefrontPage() {
const router = useRouter();
const [storefront, setStorefront] = useState<Storefront>({
pgpKey: "",
welcomeMessage: "",
telegramToken: "",
});
const [broadcastOpen, setBroadcastOpen] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [saving, setSaving] = useState<boolean>(false);
// ✅ Fetch Storefront Data
useEffect(() => {
const fetchStorefront = async () => {
try {
setLoading(true);
const data: Storefront = await apiRequest("/storefront");
setStorefront(data);
} catch (error) {
toast.error("Failed to load storefront data.");
} finally {
setLoading(false);
}
};
fetchStorefront();
}, []);
// ✅ Handle Form Input Changes
const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setStorefront({ ...storefront, [e.target.name]: e.target.value });
};
// ✅ Save Storefront Changes
const saveStorefront = async () => {
try {
setSaving(true);
await apiRequest("/storefront", "PUT", storefront);
toast.success("Storefront updated successfully!");
} catch (error) {
toast.error("Failed to update storefront.");
} finally {
setSaving(false);
}
};
return (
<Layout>
{/* ✅ Dark Themed Toaster for Notifications */}
<Toaster position="top-right" theme="dark" duration={3000} />
{/* Broadcast Dialog Component */}
<BroadcastDialog open={broadcastOpen} setOpen={setBroadcastOpen} />
<div className="max-w-4xl mx-auto p-6 space-y-8">
{/* PGP Key Section */}
<div className="bg-white dark:bg-zinc-800 rounded-xl shadow-lg p-6 border border-gray-100 dark:border-zinc-700">
<div className="flex items-center gap-3 mb-4">
<Key className="h-6 w-6 text-purple-600 dark:text-purple-400" />
<h2 className="text-xl font-semibold text-gray-900 dark:text-zinc-100">
PGP Encryption Key
</h2>
</div>
<div className="space-y-4">
<Textarea
name="pgpKey"
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----"
className="h-48 text-sm bg-gray-50 dark:bg-zinc-900 border-gray-200 dark:border-zinc-700 focus:ring-2 focus:ring-purple-500"
spellCheck={false}
value={storefront.pgpKey}
onChange={handleInputChange}
/>
</div>
</div>
{/* Telegram Configuration */}
<div className="grid gap-6 md:grid-cols-2">
{/* Bot Token */}
<div className="bg-white dark:bg-zinc-800 rounded-xl shadow-lg p-6 border border-gray-100 dark:border-zinc-700">
<div className="flex items-center gap-3 mb-4">
<Shield className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
<h2 className="text-xl font-semibold text-gray-900 dark:text-zinc-100">
Telegram Bot Token
</h2>
</div>
<Input
name="telegramToken"
type="password"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
className="font-mono text-sm bg-gray-50 dark:bg-zinc-900 border-gray-200 dark:border-zinc-700"
value={storefront.telegramToken}
onChange={handleInputChange}
/>
</div>
{/* Welcome Message */}
<div className="bg-white dark:bg-zinc-800 rounded-xl shadow-lg p-6 border border-gray-100 dark:border-zinc-700">
<div className="flex items-center gap-3 mb-4">
<MessageSquare className="h-6 w-6 text-amber-600 dark:text-amber-400" />
<h2 className="text-xl font-semibold text-gray-900 dark:text-zinc-100">
/start Welcome Message
</h2>
</div>
<Textarea
name="welcomeMessage"
placeholder="Welcome to our store!"
className="h-32 text-sm bg-gray-50 dark:bg-zinc-900 border-gray-200 dark:border-zinc-700 focus:ring-2 focus:ring-amber-500"
value={storefront.welcomeMessage}
onChange={handleInputChange}
/>
</div>
</div>
{/* Buttons */}
<div className="sticky bottom-6 mt-8 flex justify-between">
<Button onClick={() => setBroadcastOpen(true)} className="gap-2 bg-emerald-600 hover:bg-emerald-700 text-white">
<Send className="h-5 w-5" /> Broadcast Message
</Button>
<Button onClick={saveStorefront} disabled={saving} className="gap-2 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white">
<Save className="h-5 w-5" /> {saving ? "Saving..." : "Save Configuration"}
</Button>
</div>
</div>
</Layout>
);
}

View File

@@ -0,0 +1,201 @@
"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

@@ -0,0 +1,18 @@
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} />;
}