Initial commit - PromptTech
This commit is contained in:
174
frontend/src/components/ImageUploadManager.js
Normal file
174
frontend/src/components/ImageUploadManager.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { X, Upload, GripVertical, Star } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Label } from "./ui/label";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
|
||||
|
||||
const ImageUploadManager = ({ images = [], onChange, token }) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [draggedIndex, setDraggedIndex] = useState(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleFileUpload = async (files) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const uploadedUrls = [];
|
||||
|
||||
for (const file of files) {
|
||||
console.log("Uploading file:", {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await axios.post(`${API}/upload/image`, formData, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
uploadedUrls.push(response.data.url);
|
||||
console.log("Upload successful:", response.data);
|
||||
}
|
||||
|
||||
onChange([...images, ...uploadedUrls]);
|
||||
toast.success(`Uploaded ${uploadedUrls.length} image(s)`);
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload images");
|
||||
console.error("Upload error:", error);
|
||||
if (error.response) {
|
||||
console.error("Error response:", error.response.data);
|
||||
}
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e) => {
|
||||
const files = Array.from(e.target.files);
|
||||
if (files.length > 0) {
|
||||
handleFileUpload(files);
|
||||
}
|
||||
e.target.value = ""; // Reset input
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index) => {
|
||||
const newImages = images.filter((_, i) => i !== index);
|
||||
onChange(newImages);
|
||||
};
|
||||
|
||||
const handleDragStart = (e, index) => {
|
||||
setDraggedIndex(index);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
};
|
||||
|
||||
const handleDragOver = (e, index) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (draggedIndex === null || draggedIndex === index) return;
|
||||
|
||||
const newImages = [...images];
|
||||
const draggedImage = newImages[draggedIndex];
|
||||
|
||||
newImages.splice(draggedIndex, 1);
|
||||
newImages.splice(index, 0, draggedImage);
|
||||
|
||||
setDraggedIndex(index);
|
||||
onChange(newImages);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDraggedIndex(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Product Images</Label>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{uploading ? "Uploading..." : "Add Images"}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{images.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{images.map((url, index) => (
|
||||
<div
|
||||
key={index}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="relative group border border-border rounded-md overflow-hidden cursor-move hover:border-primary transition-colors"
|
||||
>
|
||||
<img
|
||||
src={
|
||||
url.startsWith("/uploads")
|
||||
? `${process.env.REACT_APP_BACKEND_URL}${url}`
|
||||
: url
|
||||
}
|
||||
alt={`Product ${index + 1}`}
|
||||
className="w-full h-32 object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<GripVertical className="h-5 w-5 text-white" />
|
||||
{index === 0 && (
|
||||
<Star className="h-5 w-5 text-yellow-400 fill-yellow-400" />
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImage(index)}
|
||||
className="absolute top-2 right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
{index === 0 && (
|
||||
<div className="absolute bottom-2 left-2 bg-yellow-500 text-white text-xs px-2 py-1 rounded">
|
||||
Primary
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-2 border-dashed border-border rounded-md p-8 text-center text-muted-foreground">
|
||||
<Upload className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">
|
||||
No images yet. Click "Add Images" to upload.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{images.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Drag images to reorder. First image is the primary image.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploadManager;
|
||||
Reference in New Issue
Block a user