Refactor
This commit is contained in:
71
components/modals/broadcast-dialog.tsx
Normal file
71
components/modals/broadcast-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
150
components/modals/product-modal.tsx
Normal file
150
components/modals/product-modal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
74
components/modals/shipping-modal.tsx
Normal file
74
components/modals/shipping-modal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user