Skip to content

Instantly share code, notes, and snippets.

@ktisakib
Created February 5, 2025 05:48
Show Gist options
  • Save ktisakib/b4deaacac56082c631b8388ab68b7450 to your computer and use it in GitHub Desktop.
Save ktisakib/b4deaacac56082c631b8388ab68b7450 to your computer and use it in GitHub Desktop.
i want select just one image so modify the code
"use client"
import { useActionState, useOptimistic, useTransition } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Plus, X, UploadCloud, QrCode } from "lucide-react"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { useState } from "react"
import { CreateProductSchema, createProductSchema } from "@/types/schemas/product-schema"
import { createProduct } from "@/actions/product-action"
import { z } from "zod"
import { toast } from "sonner"
import { getFirbaseStorage } from "@/lib/firebase"
import { ref, uploadBytes, getDownloadURL } from "firebase/storage"
import { convertToWebP } from "@/lib/utils"
import { Dropdown } from "@/types/common"
import { ProductDropdownCommand } from "@/components/custom-ui/dropdown-command"
import { BarcodeScannerDialog } from "./barcode-scanner-dialog";
interface PreviewImage {
file: File;
previewUrl: string;
}
export function ProductCreateDialog({ categories }: { categories: Dropdown }) {
const [selectedImages, setSelectedImages] = useState<PreviewImage[]>([]);
const [productData, setProductData] = useState<CreateProductSchema>({
name: '',
description: '',
price:0,
inventoryCount: 0,
category: '',
manufacturer: '',
sku: '',
barcode: '',
imageUrl:" ,
isAvailable: true,
});
// const [createProductState, createProductAction, isActionPending] = useActionState(createProduct, null)
const [errors, setErrors] = useState<Partial<Record<keyof CreateProductSchema, string>>>({})
const [isPending, startTransition] = useTransition()
const [barcode, setBarcode] = useState('');
const [isScanning, setIsScanning] = useState(false);
const handleFileSelect = (files: FileList) => {
const maxImages = 5;
if (selectedImages.length + files.length > maxImages) {
toast.error(`Maximum ${maxImages} images allowed`);
return;
}
// Create preview URLs for selected files
const newImages = Array.from(files)
.filter(file => file.type.startsWith('image/'))
.map(file => ({
file,
previewUrl: URL.createObjectURL(file)
}));
setSelectedImages(prev => [...prev, ...newImages]);
};
const removeImage = (indexToRemove: number) => {
setSelectedImages(prev => {
// Revoke the URL of the removed image
URL.revokeObjectURL(prev[indexToRemove].previewUrl);
return prev.filter((_, index) => index !== indexToRemove);
});
};
const updateProductData = (field: keyof CreateProductSchema, value: string | number | boolean | string[]) => {
setProductData(prev => ({ ...prev, [field]: value }))
}
const validateForm = () => {
try {
createProductSchema.parse(productData);
setErrors({});
return true;
} catch (error) {
if (error instanceof z.ZodError) {
setErrors(error.flatten().fieldErrors as Partial<Record<keyof CreateProductSchema, string>>);
}
return false;
}
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!validateForm()) return;
startTransition(async () => {
try {
if (selectedImages.length > 0) {
const storage = getFirbaseStorage();
const imageUploadPromises = selectedImages.map(async ({ file }) => {
const webpFile = await convertToWebP(file);
const storageRef = ref(storage, `products/${Date.now()}-${webpFile.name}`);
const snapshot = await uploadBytes(storageRef, webpFile);
return await getDownloadURL(snapshot.ref);
});
const uploadedUrls = await Promise.all(imageUploadPromises);
productData.imageUrls = uploadedUrls;
// Cleanup preview URLs
selectedImages.forEach(image => URL.revokeObjectURL(image.previewUrl));
setSelectedImages([]);
}
// Call the action inside startTransition
await toast.promise(createProduct(productData), {
loading: 'Creating product...',
success: () => {
setProductData({
name: '',
description: '',
price: 0,
inventoryCount: 0,
category: '',
manufacturer: '',
sku: '',
barcode: '',
imageUrls: [],
isAvailable: true,
});
return 'Product created successfully';
},
error: (err) => err.message || 'Failed to create product'
});
} catch (error: any) {
console.error('Error during product creation:', error);
toast.error(error.message || "Failed to create product");
}
});
};
const handleManualBarcodeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setBarcode(e.target.value);
};
const handleScannedBarcode = (scannedCode: string) => {
setProductData(prev => ({ ...prev, barcode: scannedCode }));
setIsScanning(false);
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<span className="hidden md:flex"> Add product</span>
<Plus className="size-4" aria-hidden="true" />
</Button>
</DialogTrigger>
<DialogContent
className="md:max-w-2xl max-h-[80vh] max-w-sm rounded-2xl overflow-y-auto"
>
<DialogHeader>
<DialogTitle>Create New Product</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6 mt-4">
{/* Product Images - Full width */}
<div className="space-y-2 relative col-span-2">
<Label>Product Images</Label>
<div className="relative">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-4">
{selectedImages.map(({ previewUrl }, index) => (
<div key={previewUrl} className="relative aspect-square">
<img
src={previewUrl}
alt={`Preview ${index + 1}`}
className="rounded-lg object-cover w-full h-full"
/>
<button
type="button"
onClick={() => removeImage(index)}
className="absolute -top-2 -right-2 bg-red-500 rounded-full p-1 text-white hover:bg-red-600"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
{selectedImages.length < 5 && (
<div
className="border-2 relative border-dashed rounded-lg p-4 text-center cursor-pointer hover:border-gray-400 transition-colors"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
handleFileSelect(e.dataTransfer.files);
}}
>
<input
type="file"
accept="image/*"
multiple
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={(e) => e.target.files && handleFileSelect(e.target.files)}
/>
<UploadCloud className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-600">
Drag and drop or click to upload ({selectedImages.length}/5)
</p>
</div>
)}
</div>
{errors.imageUrls && <p className="text-sm text-red-500">{errors.imageUrls}</p>}
</div>
{/* Two column grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Product Name</Label>
<Input
id="name"
value={productData.name}
onChange={(e) => updateProductData('name', e.target.value)}
placeholder="Enter product name"
/>
{errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<ProductDropdownCommand
categories={categories?.data}
selectedCategory={categories?.data?.find(cat => cat.value === productData.category)}
onCategorySelect={(category) => updateProductData('category', category.value)}
/>
{errors.category && <p className="text-sm text-red-500">{errors.category}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="price">Price</Label>
<Input
id="price"
type="number"
step="0.01"
min="0"
value={productData.price || ""}
onChange={(e) => updateProductData('price', e.target.value === "" ? 0 : parseFloat(e.target.value))}
placeholder="0.00"
/>
{errors.price && <p className="text-sm text-red-500">{errors.price}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="inventoryCount">Inventory Count</Label>
<Input
id="inventoryCount"
type="number"
min="0"
value={productData.inventoryCount || ""}
onChange={(e) => updateProductData('inventoryCount', e.target.value === "" ? 0 : parseInt(e.target.value))}
placeholder="0"
/>
{errors.inventoryCount && <p className="text-sm text-red-500">{errors.inventoryCount}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="manufacturer">Manufacturer</Label>
<Input
id="manufacturer"
value={productData.manufacturer}
onChange={(e) => updateProductData('manufacturer', e.target.value)}
placeholder="Manufacturer name"
/>
{errors.manufacturer && <p className="text-sm text-red-500">{errors.manufacturer}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="sku">SKU</Label>
<Input
id="sku"
value={productData.sku}
onChange={(e) => updateProductData('sku', e.target.value)}
placeholder="Stock Keeping Unit"
/>
{errors.sku && <p className="text-sm text-red-500">{errors.sku}</p>}
</div>
<BarcodeScannerDialog
value={productData.barcode || ''}
onChange={(value) => updateProductData('barcode', value)}
error={errors.barcode}
/>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={productData.description}
onChange={(e) => updateProductData('description', e.target.value)}
placeholder="Product description"
/>
{errors.description && <p className="text-sm text-red-500">{errors.description}</p>}
</div>
</div>
<Button type="submit" className="w-full cursor-pointer" disabled={isPending}>
{isPending ? "Creating..." : "Create Product"}
</Button>
</form>
</DialogContent>
</Dialog>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment