From bce4b26ffac94685fe2ddca28baf5d923ee485da Mon Sep 17 00:00:00 2001 From: g Date: Sat, 8 Feb 2025 16:06:39 +0000 Subject: [PATCH] Updates --- app/_document.tsx | 13 ++ app/dashboard/products/page.tsx | 24 ++- app/error.tsx | 13 ++ app/page.tsx | 3 +- components/modals/product-modal.tsx | 321 +++++++++++++++++++--------- lib/productData.ts | 19 ++ lib/types.ts | 2 +- 7 files changed, 280 insertions(+), 115 deletions(-) create mode 100644 app/_document.tsx create mode 100644 app/error.tsx diff --git a/app/_document.tsx b/app/_document.tsx new file mode 100644 index 0000000..a93963c --- /dev/null +++ b/app/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + + + +
+ + + + ); +} \ No newline at end of file diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx index e71f3fe..67674d2 100644 --- a/app/dashboard/products/page.tsx +++ b/app/dashboard/products/page.tsx @@ -9,6 +9,7 @@ import { Plus } from "lucide-react"; import { fetchProductData, saveProductData, + saveProductImage, deleteProductData, } from "@/lib/productData"; import { ProductModal } from "@/components/modals/product-modal"; @@ -105,8 +106,11 @@ export default function ProductsPage() { setProductData({ ...productData, pricing: updatedPricing }); }; + // Save product data after modal form submission - const handleSaveProduct = async (data: Product) => { + const handleSaveProduct = async (data: Product, file?: File | null) => { + console.log("handleSaveProduct:", data, file); + const adjustedPricing = data.pricing.map((tier) => ({ minQuantity: tier.minQuantity, pricePerUnit: @@ -114,26 +118,30 @@ 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" ); - + + if (file) { + await saveProductImage(`${process.env.NEXT_PUBLIC_API_URL}/products/${savedProduct._id}/image`, file, authToken); + } + + // Update state with the saved product setProducts((prevProducts) => { if (editing) { return prevProducts.map((product) => @@ -143,7 +151,7 @@ export default function ProductsPage() { return [...prevProducts, savedProduct]; } }); - + setModalOpen(false); } catch (error) { console.error("Error saving product:", error); @@ -177,7 +185,7 @@ export default function ProductsPage() { minQuantity: tier.minQuantity, pricePerUnit: tier.pricePerUnit, })) - : [{ minQuantity: 1, pricePerUnit: 0 }], // Fallback if undefined + : [{ minQuantity: 1, pricePerUnit: 0 }], }); setEditing(true); setModalOpen(true); diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..fec9d88 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,13 @@ +"use client"; + +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ( +
+

Something went wrong!

+

{error.message}

+ +
+ ); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index d9bd2e1..28e0813 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,5 +2,4 @@ import { redirect } from "next/navigation" export default function Home() { redirect("/dashboard") -} - +} \ No newline at end of file diff --git a/components/modals/product-modal.tsx b/components/modals/product-modal.tsx index 77de904..ec9ea2e 100644 --- a/components/modals/product-modal.tsx +++ b/components/modals/product-modal.tsx @@ -1,16 +1,31 @@ -"use client" +"use client"; -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 { ImageUpload } from "@/components/forms/image-upload" -import { PricingTiers } from "@/components/forms/pricing-tiers" -import type { ProductModalProps, ProductData } from "@/lib/types" -import { toast } from "sonner" -import type React from "react" // Import React +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 { ImageUpload } from "@/components/forms/image-upload"; +import { PricingTiers } from "@/components/forms/pricing-tiers"; +import type { ProductModalProps, ProductData } from "@/lib/types"; +import { toast } from "sonner"; +import type React from "react"; +import { Plus } from "lucide-react"; +import { apiRequest } from "@/lib/storeHelper"; + +type CategorySelectProps = { + categories: { _id: string; name: string }[]; + value: string; + setProductData: React.Dispatch>; + onAddCategory: (newCategory: { _id: string; name: string }) => void; +}; export const ProductModal: React.FC = ({ open, @@ -25,59 +40,89 @@ export const ProductModal: React.FC = ({ handleRemoveTier, setProductData, }) => { - const [imagePreview, setImagePreview] = useState(null) - const [imageDimensions, setImageDimensions] = useState({ width: 300, height: 200 }) + /** + * 1) Store the selected file *separately* from productData.image + */ + const [selectedFile, setSelectedFile] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const [imageDimensions, setImageDimensions] = useState({ width: 300, height: 200 }); + const [localCategories, setLocalCategories] = useState(categories); + // If productData.image is a *URL* (string), show it as a default preview useEffect(() => { if (productData.image && typeof productData.image === "string") { - setImagePreview(productData.image) + setImagePreview(productData.image); } - }, [productData.image]) + }, [productData.image]); + useEffect(() => { + setLocalCategories(categories); + }, [categories]); + + /** + * 2) When user selects a file, store it and create an objectURL for preview + */ const handleImageChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0] + const file = e.target.files?.[0]; if (!file) { - setProductData({ ...productData, image: null }) - setImagePreview(null) - return + // no file selected + setSelectedFile(null); + setImagePreview(null); + return; } - const image = new Image() - const objectUrl = URL.createObjectURL(file) + // For preview + const objectUrl = URL.createObjectURL(file); + setSelectedFile(file); + setImagePreview(objectUrl); + // Optionally, load the image to calculate dimensions + const image = new Image(); image.onload = () => { - const aspectRatio = image.naturalWidth / image.naturalHeight - const width = aspectRatio > 1 ? 300 : 200 * aspectRatio - const height = aspectRatio > 1 ? 300 / aspectRatio : 200 + const aspectRatio = image.naturalWidth / image.naturalHeight; + const width = aspectRatio > 1 ? 300 : 200 * aspectRatio; + const height = aspectRatio > 1 ? 300 / aspectRatio : 200; + setImageDimensions({ width, height }); + }; + image.src = objectUrl; + }; - setProductData({ ...productData, image: file }) - setImagePreview(objectUrl) - setImageDimensions({ width, height }) + /** + * 3) On 'Save', call the parent onSave(productData, selectedFile) + */ + const handleSave = async () => { + if (!productData.category) { + toast.error("Please select or add a category"); + return; } - image.src = objectUrl - } + onSave(productData, selectedFile); + toast.success(editing ? "Product updated!" : "Product added!"); + onClose(); + }; - const handleSave = () => { - onSave(productData) - toast.success(editing ? "Product updated!" : "Product added!") - onClose() - } + const handleAddCategory = (newCategory: { _id: string; name: string }) => { + setLocalCategories((prev) => [...prev, newCategory]); + }; return ( - {editing ? "Edit Product" : "Add Product"} + + {editing ? "Edit Product" : "Add Product"} +
+
= ({ - +
- ) -} + ); +}; const ProductBasicInfo: React.FC<{ - productData: ProductData - handleChange: (e: React.ChangeEvent) => void - categories: { _id: string; name: string }[] - setProductData: React.Dispatch> -}> = ({ productData, handleChange, categories, setProductData }) => ( + productData: ProductData; + handleChange: (e: React.ChangeEvent) => void; + categories: { _id: string; name: string }[]; + setProductData: React.Dispatch>; + onAddCategory: (newCategory: { _id: string; name: string }) => void; +}> = ({ productData, handleChange, categories, setProductData, onAddCategory }) => (
@@ -134,70 +182,135 @@ const ProductBasicInfo: React.FC<{ />
- + - +
-) +); -const CategorySelect: React.FC<{ - categories: { _id: string; name: string }[] - value: string - setProductData: React.Dispatch> -}> = ({ categories, value, setProductData }) => ( -
- - -
-) +const CategorySelect: React.FC = ({ + categories, + value, + setProductData, + onAddCategory, +}) => { + const [isAddingCategory, setIsAddingCategory] = useState(false); + const [newCategoryName, setNewCategoryName] = useState(""); + + const handleAddCategory = async () => { + if (!newCategoryName.trim()) { + toast.error("Category name cannot be empty"); + return; + } + + try { + const response = await apiRequest("/categories", "POST", { name: newCategoryName }); + const newCategory = response.data; + setProductData((prev) => ({ ...prev, category: newCategory._id })); + onAddCategory(newCategory); + setIsAddingCategory(false); + setNewCategoryName(""); + toast.success("New category added successfully"); + } catch (error) { + console.error("Failed to add new category:", error); + toast.error("Failed to add new category"); + } + }; + + return ( +
+ +
+ {isAddingCategory ? ( + setNewCategoryName(e.target.value)} + placeholder="New category name" + className="flex-grow h-9 text-sm" + /> + ) : ( + + )} + +
+
+ ); +}; const UnitTypeSelect: React.FC<{ - value: string - setProductData: React.Dispatch> + value: string; + setProductData: React.Dispatch>; }> = ({ value, setProductData }) => ( -
+
- +
+ + +
-) - +); \ No newline at end of file diff --git a/lib/productData.ts b/lib/productData.ts index 6582c93..e409aff 100644 --- a/lib/productData.ts +++ b/lib/productData.ts @@ -34,6 +34,25 @@ export const saveProductData = async ( } }; +export const saveProductImage = async(url: string, file:File, authToken: string) => { + try{ + const formData = new FormData(); + formData.append("file", file); + + return await fetchData(url, { + method: "POST", + headers: { + Authorization: `Bearer ${authToken}`, + //"Content-Type": "multipart/form-data", + }, + body: formData, + }); + } catch (error) { + console.error("Error uploading image:", error); + throw error; + } +} + export const deleteProductData = async (url: string, authToken: string) => { try { return await fetchData(url, { diff --git a/lib/types.ts b/lib/types.ts index 4c6ee2b..702152b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -4,7 +4,7 @@ import type React from "react" export interface ProductModalProps { open: boolean onClose: () => void - onSave: (productData: ProductData) => void + onSave: (productData: ProductData, imageFile?: File | null) => void; productData: ProductData categories: Category[] editing: boolean