From 468fd69cb51c9c6d745928747e7f7f490a735683 Mon Sep 17 00:00:00 2001 From: g Date: Sat, 8 Feb 2025 00:54:11 +0000 Subject: [PATCH] Cleanup --- app/dashboard/products/page.tsx | 38 ++- components/forms/image-upload.tsx | 40 +++ components/forms/pricing-tiers.tsx | 67 ++++ components/modals/product-modal.tsx | 497 +++++++++++----------------- lib/types.ts | 64 +++- 5 files changed, 375 insertions(+), 331 deletions(-) create mode 100644 components/forms/image-upload.tsx create mode 100644 components/forms/pricing-tiers.tsx diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx index be6ec6e..e71f3fe 100644 --- a/app/dashboard/products/page.tsx +++ b/app/dashboard/products/page.tsx @@ -28,7 +28,7 @@ export default function ProductsPage() { unitType: "pcs", category: "", pricing: [{ minQuantity: 1, pricePerUnit: 0 }], - image: null + image: null, }); // Fetch products and categories @@ -55,15 +55,15 @@ export default function ProductsPage() { authToken ), ]); - + console.log("Fetched Products:", fetchedProducts); - + // Ensure all products have tieredPricing const processedProducts = fetchedProducts.map((product: Product) => ({ ...product, pricing: product.pricing || [{ minQuantity: 1, pricePerUnit: 0 }], })); - + setProducts(processedProducts); setCategories(fetchedCategories); } catch (error) { @@ -75,6 +75,20 @@ export default function ProductsPage() { fetchDataAsync(); }, []); + const handleAddTier = () => { + setProductData((prev) => ({ + ...prev, + pricing: [...prev.pricing, { minQuantity: 1, pricePerUnit: 0 }], + })); + }; + + const handleRemoveTier = (index: number) => { + setProductData((prev) => ({ + ...prev, + pricing: prev.pricing.filter((_, i) => i !== index), + })); + }; + // Handle input changes const handleChange = ( e: ChangeEvent @@ -100,26 +114,26 @@ export default function ProductsPage() { ? parseFloat(tier.pricePerUnit) : tier.pricePerUnit, })); - + const productToSave: Product = { ...data, pricing: adjustedPricing, image: data.image ?? "", // ✅ Prevents undefined error }; - + 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) => @@ -129,7 +143,7 @@ export default function ProductsPage() { return [...prevProducts, savedProduct]; } }); - + setModalOpen(false); } catch (error) { console.error("Error saving product:", error); @@ -159,9 +173,9 @@ export default function ProductsPage() { setProductData({ ...product, pricing: product.pricing - ? product.pricing.map(tier => ({ + ? product.pricing.map((tier) => ({ minQuantity: tier.minQuantity, - pricePerUnit: tier.pricePerUnit + pricePerUnit: tier.pricePerUnit, })) : [{ minQuantity: 1, pricePerUnit: 0 }], // Fallback if undefined }); @@ -219,6 +233,8 @@ export default function ProductsPage() { editing={editing} handleChange={handleChange} handleTieredPricingChange={handleTieredPricingChange} + handleAddTier={handleAddTier} // ✅ Ensure this is passed + handleRemoveTier={handleRemoveTier} // ✅ Ensure this is passed setProductData={setProductData} /> diff --git a/components/forms/image-upload.tsx b/components/forms/image-upload.tsx new file mode 100644 index 0000000..83b6221 --- /dev/null +++ b/components/forms/image-upload.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { ChangeEvent } from "react"; +import { Input } from "@/components/ui/input"; + +interface ImageUploadProps { + imagePreview: string | null; + handleImageChange: (e: ChangeEvent) => void; + imageDimensions: { width: number; height: number }; +} + +export const ImageUpload = ({ + imagePreview, + handleImageChange, + imageDimensions, +}: ImageUploadProps) => ( +
+ +
+ {imagePreview ? ( + Preview + ) : ( + No Image Selected + )} +
+ +
+); \ No newline at end of file diff --git a/components/forms/pricing-tiers.tsx b/components/forms/pricing-tiers.tsx new file mode 100644 index 0000000..e1a7946 --- /dev/null +++ b/components/forms/pricing-tiers.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Trash, PlusCircle } from "lucide-react"; + +interface PricingTiersProps { + pricing: any[]; + handleTierChange: (e: React.ChangeEvent, index: number) => void; + handleRemoveTier: (index: number) => void; + handleAddTier: () => void; +} + +export const PricingTiers = ({ + pricing, + handleTierChange, + handleRemoveTier, + handleAddTier, +}: PricingTiersProps) => ( +
+

Tiered Pricing

+ + {pricing?.length > 0 ? ( + pricing.map((tier, index) => ( +
+ handleTierChange(e, index)} + className="h-8 text-sm px-2 flex-1" + /> + handleTierChange(e, index)} + className="h-8 text-sm px-2 flex-1" + /> + +
+ )) + ) : ( +

No pricing tiers added.

+ )} + + +
+); \ No newline at end of file diff --git a/components/modals/product-modal.tsx b/components/modals/product-modal.tsx index 3d61932..665752f 100644 --- a/components/modals/product-modal.tsx +++ b/components/modals/product-modal.tsx @@ -1,58 +1,15 @@ "use client"; -import { ChangeEvent, useState, useEffect } from "react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog"; +import { 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 { Trash, PlusCircle } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ImageUpload } from "@/components/forms/image-upload"; +import { PricingTiers } from "@/components/forms/pricing-tiers"; +import { ProductModalProps, ProductData } from "@/lib/types"; import { toast } from "sonner"; -import { Product } from "@/models/products"; - -interface Category { - _id: string; - name: string; -} - -interface PricingTier { - minQuantity: number; - pricePerUnit: number; - _id?: string; -} - -interface ProductData { - name: string; - description: string; - unitType: string; - category: string; - pricing: PricingTier[]; - image?: string | File | null | undefined; -} - -interface ProductModalProps { - open: boolean; - onClose: () => void; - onSave: (productData: ProductData) => void; - productData: ProductData; - categories: any[]; - editing: boolean; - handleChange: (e: ChangeEvent) => void; - handleTieredPricingChange: (e: ChangeEvent, index: number) => void; // ✅ Added this - setProductData: React.Dispatch>; -} export const ProductModal = ({ open, @@ -65,11 +22,7 @@ export const ProductModal = ({ setProductData, }: ProductModalProps) => { const [imagePreview, setImagePreview] = useState(null); - const [newCategory, setNewCategory] = useState(""); - const [imageDimensions, setImageDimensions] = useState({ - width: 300, - height: 200, - }); + const [imageDimensions, setImageDimensions] = useState({ width: 300, height: 200 }); useEffect(() => { if (productData.image && typeof productData.image === "string") { @@ -77,71 +30,60 @@ export const ProductModal = ({ } }, [productData.image]); - const handleImageChange = (e: ChangeEvent) => { + const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - if (file) { - const image = new Image(); - const objectUrl = URL.createObjectURL(file); - - image.onload = () => { - const aspectRatio = image.naturalWidth / image.naturalHeight; - let width = 300; - let height = 200; - - if (aspectRatio > 1) { - width = 300; - height = 300 / aspectRatio; - } else { - height = 200; - width = 200 * aspectRatio; - } - - setProductData({ ...productData, image: file }); - setImagePreview(objectUrl); - setImageDimensions({ width, height }); - }; - - image.src = objectUrl; - } else { + if (!file) { setProductData({ ...productData, image: null }); setImagePreview(null); + return; } + + const image = new Image(); + const objectUrl = URL.createObjectURL(file); + + image.onload = () => { + const aspectRatio = image.naturalWidth / image.naturalHeight; + const width = aspectRatio > 1 ? 300 : 200 * aspectRatio; + const height = aspectRatio > 1 ? 300 / aspectRatio : 200; + + setProductData({ ...productData, image: file }); + setImagePreview(objectUrl); + setImageDimensions({ width, height }); + }; + + image.src = objectUrl; + }; + + const handleTierChange = (e: React.ChangeEvent, index: number) => { + const { name, valueAsNumber } = e.target; + + if (!["minQuantity", "pricePerUnit"].includes(name)) return; // ✅ Ensure valid keys + + const updatedPricing = [...productData.pricing]; + (updatedPricing[index] as any)[name] = isNaN(valueAsNumber) ? 0 : valueAsNumber; + + setProductData({ ...productData, pricing: updatedPricing }); + }; + + + const handleAddTier = () => { + setProductData(prev => ({ + ...prev, + pricing: [...prev.pricing, { minQuantity: 1, pricePerUnit: 0 }] + })); + }; + + const handleRemoveTier = (index: number) => { + const updatedPricing = productData.pricing.filter((_, i) => i !== index); + setProductData({ ...productData, pricing: updatedPricing }); }; const handleSave = () => { onSave(productData); - toast.success( - editing ? "Product updated successfully!" : "Product added successfully!" - ); + toast.success(editing ? "Product updated!" : "Product added!"); onClose(); }; - const handleTieredPricingChange = ( - e: ChangeEvent, - index: number - ) => { - const { name, valueAsNumber } = e.target; - - if (!productData.pricing) return; - const updatedPricing = [...productData.pricing]; - - updatedPricing[index] = { - ...updatedPricing[index], - [name]: isNaN(valueAsNumber) ? 0 : valueAsNumber, - }; - - setProductData((prev) => ({ - ...prev, - tieredPricing: updatedPricing, - image: prev.image ?? null, - })); - - const convertToProductData = (product: Product): ProductData => ({ - ...product, - image: product.image ?? null, // ✅ Ensures the type is correct - }); - }; - return ( @@ -152,216 +94,155 @@ export const ProductModal = ({
+ {/* Left Column */}
-
- - -
- - {/* Description */} -
- -