This commit is contained in:
g
2025-02-07 21:33:13 +00:00
parent 8900bbcc76
commit 891f57d729
50 changed files with 165 additions and 444 deletions

View File

@@ -0,0 +1,71 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Send } from "lucide-react";
import { toast } from "sonner";
import { apiRequest } from "@/lib/storeHelper";
interface BroadcastDialogProps {
open: boolean;
setOpen: (open: boolean) => void;
}
export default function BroadcastDialog({ open, setOpen }: BroadcastDialogProps) {
const [broadcastMessage, setBroadcastMessage] = useState("");
const [isSending, setIsSending] = useState(false);
const sendBroadcast = async () => {
if (!broadcastMessage.trim()) {
toast.warning("Broadcast message cannot be empty.");
return;
}
try {
setIsSending(true);
const response = await apiRequest("/storefront/broadcast", "POST", { message: broadcastMessage });
if (response.error) throw new Error(response.error);
toast.success(`Broadcast sent to ${response.totalUsers} users!`);
setBroadcastMessage("");
setOpen(false);
} catch (error) {
toast.error("Failed to send broadcast message.");
} finally {
setIsSending(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<Send className="h-6 w-6 text-emerald-600" />
<DialogTitle>Global Broadcast Message</DialogTitle>
</DialogHeader>
<Textarea
placeholder="Type your message here..."
value={broadcastMessage}
onChange={(e) => setBroadcastMessage(e.target.value)}
/>
<DialogFooter>
<Button variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={sendBroadcast}
disabled={isSending}
className="bg-emerald-600 hover:bg-emerald-700"
>
{isSending ? "Sending..." : "Send Broadcast"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,150 @@
"use client";
import { ChangeEvent, useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Product } from "@/models/products";
interface Category {
_id: string;
name: string;
}
interface ProductModalProps {
open: boolean;
onClose: () => void;
onSave: (productData: Product) => void;
productData: Product;
categories: Category[];
editing: boolean;
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
setProductData: React.Dispatch<React.SetStateAction<Product>>;
handleTieredPricingChange: (
e: ChangeEvent<HTMLInputElement>,
index: number
) => void;
}
export const ProductModal = ({
open,
onClose,
onSave,
productData,
categories,
editing,
handleChange,
handleTieredPricingChange,
setProductData,
}: ProductModalProps) => {
const [imagePreview, setImagePreview] = useState<string | null>(null);
// Update image preview when product data changes
useEffect(() => {
if (productData.image && typeof productData.image === "string") {
setImagePreview(productData.image);
}
}, [productData.image]);
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setProductData({ ...productData, image: file });
setImagePreview(URL.createObjectURL(file));
} else {
setProductData({ ...productData, image: null });
setImagePreview(null);
}
};
// ✅ FIXED: Moved Inside the Component & Used Type Assertion
const handleTieredPricingChangeInternal = (
e: ChangeEvent<HTMLInputElement>,
index: number
) => {
const updatedPricing = [...productData.tieredPricing];
const field = e.target.name as keyof (typeof productData.tieredPricing)[number];
updatedPricing[index] = {
...updatedPricing[index],
[field]: e.target.valueAsNumber || 0,
};
setProductData({
...productData,
tieredPricing: updatedPricing,
});
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-lg">
{editing ? "Edit Product" : "Add Product"}
</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Product Name</label>
<Input name="name" placeholder="Product Name" value={productData.name} onChange={handleChange} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description</label>
<Textarea name="description" placeholder="Product Description" value={productData.description} onChange={handleChange} rows={3} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Category</label>
<Select value={productData.category} onValueChange={(value) => setProductData({ ...productData, category: value })}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem key={cat._id} value={cat._id}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<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">
<Input name="minQuantity" type="number" min="1" placeholder="Quantity" value={tier.minQuantity} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
<Input name="pricePerUnit" type="number" step="0.01" placeholder="Price" value={tier.pricePerUnit} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
</div>
))}
<Button variant="outline" size="sm" onClick={() => setProductData({ ...productData, tieredPricing: [...productData.tieredPricing, { minQuantity: 1, pricePerUnit: 0 }] })}>
+ Add Tier
</Button>
</div>
</div>
<DialogFooter className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button onClick={() => onSave(productData)}>{editing ? "Update Product" : "Create Product"}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,74 @@
"use client";
import { ChangeEvent } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ShippingData } from "@/lib/types";
interface ShippingModalProps {
open: boolean;
onClose: () => void;
onSave: (shippingData: ShippingData) => void; // ✅ Allow passing shippingData
shippingData: ShippingData;
setShippingData: React.Dispatch<React.SetStateAction<ShippingData>>;
editing: boolean;
handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
}
export const ShippingModal = ({
open,
onClose,
onSave,
shippingData,
editing,
handleChange,
setShippingData,
}: ShippingModalProps) => {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-lg">
{editing ? "Edit Shipping Method" : "Add Shipping Method"}
</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Method Name</label>
<Input
name="name"
placeholder="Shipping Method Name"
value={shippingData.name}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Price</label>
<Input
name="price"
type="number"
step="0.01"
placeholder="Price (£)"
value={shippingData.price}
onChange={handleChange}
/>
</div>
</div>
<DialogFooter className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={() => onSave(shippingData)}>
{editing ? "Update Shipping Method" : "Create Shipping Method"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};