A
Anonymous

File Upload - Copy this React, Tailwind Component to your project

Import { useState, useEffect } from "react"; import { FaStar } from "react icons/fa"; import { toast } from "react toastify"; import { addReviewToProduct, fetchReviewsByProductId, updateReview, deleteReview, } from "../../../services/reviewsApi"; import { useSelector } from "react redux"; import { useNavigate } from "react router dom"; import { storage } from "../../../firebase"; // Import firebase config import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; import { motion, AnimatePresence } from "framer motion"; const ProductReview = ({ productId }) => { const [reviews, setReviews] = useState([]); const [newReview, setNewReview] = useState({ rating: 5, comment: "" }); const [imageFile, setImageFile] = useState(null); const [imageUrl, setImageUrl] = useState(""); const [editingReview, setEditingReview] = useState(null); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [reviewToDelete, setReviewToDelete] = useState(null); const { userInfo, isLoggedIn } = useSelector((state) => state.user); const navigate = useNavigate(); useEffect(() => { if (!isLoggedIn) { navigate("/login"); } }, [isLoggedIn, navigate]); useEffect(() => { const loadReviews = async () => { try { const data = await fetchReviewsByProductId(productId); setReviews(data); } catch (error) { toast.error("Không thể tải đánh giá. Vui lòng thử lại!"); } }; loadReviews(); }, [productId]); const handleAddReview = async (e) => { e.preventDefault(); if (!newReview.comment.trim()) { toast.error("Vui lòng nhập nội dung đánh giá"); return; } const review = { rating: newReview.rating, comment: newReview.comment, user_id: userInfo.id, image: imageUrl, }; try { if (editingReview) { await updateReview(editingReview.id, review); toast.success("Đánh giá đã được cập nhật!"); } else { await addReviewToProduct(productId, review); toast.success("Đánh giá đã được thêm thành công!"); } const data = await fetchReviewsByProductId(productId); setReviews(data); setNewReview({ rating: 5, comment: "" }); setImageFile(null); setImageUrl(""); setEditingReview(null); } catch (error) { toast.error("Không thể thêm hoặc cập nhật đánh giá. Vui lòng thử lại!"); } }; const handleEditReview = (review) => { setEditingReview(review); setNewReview({ rating: review.rating, comment: review.comment }); setImageUrl(review.image || ""); }; const handleCancelEdit = () => { setEditingReview(null); setNewReview({ rating: 5, comment: "" }); setImageFile(null); setImageUrl(""); }; const handleDeleteReview = async () => { if (!reviewToDelete) return; try { await deleteReview(reviewToDelete.id); const updatedReviews = reviews.filter( (review) => review.id !== reviewToDelete.id ); setReviews(updatedReviews); setIsDeleteDialogOpen(false); toast.success("Đánh giá đã được xóa!"); } catch (error) { toast.error("Không thể xóa đánh giá. Vui lòng thử lại!"); } }; const handleImageChange = (e) => { const file = e.target.files[0]; if (file) { setImageFile(file); uploadImage(file); } }; const uploadImage = (file) => { const storageRef = ref(storage, `reviews/${Date.now()}_${file.name}`); const uploadTask = uploadBytesResumable(storageRef, file); uploadTask.on( "state_changed", (snapshot) => { }, (error) => { toast.error("Có lỗi xảy ra khi tải ảnh lên. Vui lòng thử lại."); }, () => { getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { setImageUrl(downloadURL); }); } ); }; const dialogVariants = { hidden: { opacity: 0, scale: 0.8 }, visible: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.8 }, }; return ( <div className="mt 16"> <h2 className="text 2xl font bold text gray 900 mb 8"> Đánh giá của khách hàng </h2> <form onSubmit={handleAddReview} className="mb 8 bg gray 50 p 6 rounded lg" > <div className="mb 4"> <label className="block text gray 700 mb 2">Đánh giá</label> <div className="flex gap 1"> {[1, 2, 3, 4, 5].map((star) => ( <button key={star} type="button" onClick={() => setNewReview({ ...newReview, rating: star })} className="focus:outline none" > <FaStar className={ star <= newReview.rating ? "text yellow 400" : "text gray 300" } /> </button> ))} </div> </div> <div className="mb 4"> <label className="block text gray 700 mb 2">Đánh giá của bạn</label> <textarea value={newReview.comment} onChange={(e) => setNewReview({ ...newReview, comment: e.target.value }) } className="w full px 3 py 2 border rounded md focus:outline none focus:ring 2 focus:ring gray 300" rows="4" ></textarea> </div> <div className="mb 4"> <label className="block text gray 700 mb 2">Chọn hình ảnh (tùy chọn)</label> <input type="file" accept="image/*" onChange={handleImageChange} className="block w full text sm text gray 600" /> </div> <div className="flex gap 4"> <button type="submit" className="bg black text white px 6 py 2 rounded md hover:bg gray 800 focus:outline none focus:ring 2 focus:ring gray 300" > {editingReview ? "Cập nhật đánh giá" : "Gửi đánh giá"} </button> {editingReview && ( <button type="button" onClick={handleCancelEdit} className="bg gray 400 text white px 6 py 2 rounded md hover:bg gray 500 focus:outline none focus:ring 2 focus:ring gray 300" > Hủy </button> )} </div> </form> <div className="space y 6"> {reviews.length === 0 ? ( <div className="text center text gray 500"> <p>Chưa có đánh giá nào, hãy là người đầu tiên đánh giá!</p> </div> ) : ( reviews.map((review, index) => ( <div key={review.id || index} className="relative bg white p 6 rounded lg shadow sm group hover:bg gray 50 transition colors" > <div className="flex justify between"> <div className="flex items center gap 2"> <img src={review.user_avatar || "/default avatar.jpg"} alt="User Avatar" className="w 8 h 8 rounded full" /> <div> <div className="font semibold text gray 800"> {review.user_name} </div> <div className="flex gap 1"> {[1, 2, 3, 4, 5].map((star) => ( <FaStar key={star} className={ star <= review.rating ? "text yellow 400" : "text gray 300" } /> ))} </div> </div> </div> <div className="flex gap 2"> <button onClick={() => handleEditReview(review)} className="text blue 500 hover:text blue 700" > Chỉnh sửa </button> <button onClick={() => { setIsDeleteDialogOpen(true); setReviewToDelete(review); }} className="text red 500 hover:text red 700" > Xóa </button> </div> </div> <p className="text gray 700 mt 4">{review.comment}</p> {review.image && ( <img src={review.image} alt="Review" className="mt 4 max w full h auto rounded md" /> )} </div> )) )} </div> <AnimatePresence> {isDeleteDialogOpen && ( <motion.div className="fixed inset 0 flex justify center items center z 50" variants={dialogVariants} initial="hidden" animate="visible" exit="exit" > <div className="bg white p 8 rounded lg shadow lg"> <h3 className="text lg font semibold mb 4"> Bạn chắc chắn muốn xóa đánh giá này? </h3> <div className="flex gap 4"> <button onClick={handleDeleteReview} className="bg red 600 text white px 6 py 2 rounded md hover:bg red 700" > Xóa </button> <button onClick={() => setIsDeleteDialogOpen(false)} className="bg gray 300 text gray 700 px 6 py 2 rounded md hover:bg gray 400" > Hủy </button> </div> </div> </motion.div> )} </AnimatePresence> </div> ); }; export default ProductReview; Do you have any suggestions to make this code look better? Make the file selection button more attractive.

Prompt
Component Preview

About

FileUpload - Easily upload images with a sleek interface, integrated with Firebase. Built with React and Tailwind for seamless user expe. Start coding now!

Share

Last updated 1 month ago