|
@@ -1,318 +0,0 @@
|
|
|
-"use client";
|
|
|
|
|
-
|
|
|
|
|
-import { useState } from "react";
|
|
|
|
|
-import { Textarea } from "@heroui/input";
|
|
|
|
|
-import { AddRatingStar } from "./AddRatingStar";
|
|
|
|
|
-import { Button } from "@components/common/button/Button";
|
|
|
|
|
-import { useCustomToast } from "@utils/hooks/useToast";
|
|
|
|
|
-import { useProductReview } from "@utils/hooks/useProductReview";
|
|
|
|
|
-import Image from "next/image";
|
|
|
|
|
-
|
|
|
|
|
-import type { CreateProductReviewInput } from "@/types/review";
|
|
|
|
|
-import { AddUploadImage } from "@components/common/icons/AddUploadImage";
|
|
|
|
|
-
|
|
|
|
|
-interface UploadImgItem {
|
|
|
|
|
- fileName: string;
|
|
|
|
|
- preview: string;
|
|
|
|
|
-}
|
|
|
|
|
-export default function AddProductReview({
|
|
|
|
|
- productId,
|
|
|
|
|
- onClose,
|
|
|
|
|
-}: {
|
|
|
|
|
- productId: string;
|
|
|
|
|
- onClose: () => void;
|
|
|
|
|
-}) {
|
|
|
|
|
- // const [imageFile, setImageFile] = useState<UploadImgItem[]>([]);
|
|
|
|
|
- const [imgList, setImgList] = useState<UploadImgItem[]>([]);
|
|
|
|
|
- const MAX_UPLOAD = 3;
|
|
|
|
|
- // const [imagePreview, setImagePreview] = useState<string | null>(null);
|
|
|
|
|
- const [errors, setErrors] = useState({
|
|
|
|
|
- title: "",
|
|
|
|
|
- comment: "",
|
|
|
|
|
- rating: "",
|
|
|
|
|
- });
|
|
|
|
|
- const [reviewInfo, setReviewInfo] = useState({
|
|
|
|
|
- title: "",
|
|
|
|
|
- comment: "",
|
|
|
|
|
- rating: 0,
|
|
|
|
|
- attachments: null as string | null,
|
|
|
|
|
- });
|
|
|
|
|
- const { showToast } = useCustomToast();
|
|
|
|
|
- const { createProductReview, isLoading } = useProductReview();
|
|
|
|
|
-
|
|
|
|
|
- const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
- const file = e.target.files?.[0];
|
|
|
|
|
- if (!file) return;
|
|
|
|
|
-
|
|
|
|
|
- // 判断是否达到3张上限
|
|
|
|
|
- if (imgList.length >= MAX_UPLOAD) {
|
|
|
|
|
- showToast(`You can only upload up to ${MAX_UPLOAD} images`, "warning");
|
|
|
|
|
- e.target.value = ""; // 清空input,防止重复选
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const reader = new FileReader();
|
|
|
|
|
- reader.onloadend = () => {
|
|
|
|
|
- const newItem: UploadImgItem = {
|
|
|
|
|
- fileName: file.name,
|
|
|
|
|
- preview: reader.result as string,
|
|
|
|
|
- };
|
|
|
|
|
- // 追加到图片数组
|
|
|
|
|
- const newList = [...imgList, newItem];
|
|
|
|
|
- setImgList(newList);
|
|
|
|
|
- // 拼接所有文件名给表单附件
|
|
|
|
|
- setReviewInfo((prev) => ({
|
|
|
|
|
- ...prev,
|
|
|
|
|
- attachments: newList.map((item) => item.fileName).join(","),
|
|
|
|
|
- }));
|
|
|
|
|
- };
|
|
|
|
|
- reader.readAsDataURL(file);
|
|
|
|
|
- // 清空input,允许重复选择同一张图片
|
|
|
|
|
- e.target.value = "";
|
|
|
|
|
- };
|
|
|
|
|
- // 根据下标删除指定图片
|
|
|
|
|
- const removeSingleImage = (targetIndex: number) => {
|
|
|
|
|
- const newList = imgList.filter((_, idx) => idx !== targetIndex);
|
|
|
|
|
- setImgList(newList);
|
|
|
|
|
- // 重新拼接附件字符串
|
|
|
|
|
- setReviewInfo((prev) => ({
|
|
|
|
|
- ...prev,
|
|
|
|
|
- attachments: newList.length
|
|
|
|
|
- ? newList.map((item) => item.fileName).join(",")
|
|
|
|
|
- : null,
|
|
|
|
|
- }));
|
|
|
|
|
- };
|
|
|
|
|
- const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
-
|
|
|
|
|
- const newErrors = {
|
|
|
|
|
- title: "",
|
|
|
|
|
- comment: "",
|
|
|
|
|
- rating: "",
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- if (!reviewInfo.title.trim()) {
|
|
|
|
|
- newErrors.title = "Title is required";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!reviewInfo.comment.trim()) {
|
|
|
|
|
- newErrors.comment = "Comment is required";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (reviewInfo.rating === 0) {
|
|
|
|
|
- newErrors.rating = "Please select a rating";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- setErrors(newErrors);
|
|
|
|
|
-
|
|
|
|
|
- // If there are errors, don't submit
|
|
|
|
|
- if (newErrors.title || newErrors.comment || newErrors.rating) {
|
|
|
|
|
- showToast("Please fill in all required fields", "danger");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const input: CreateProductReviewInput = {
|
|
|
|
|
- productId: Number(productId.split("/").pop()),
|
|
|
|
|
- title: reviewInfo.title,
|
|
|
|
|
- comment: reviewInfo.comment,
|
|
|
|
|
- rating: reviewInfo.rating,
|
|
|
|
|
- name: "Guest User",
|
|
|
|
|
- email: "guest@mail.com",
|
|
|
|
|
- // 读取表单里拼接好的多图文件名,无图则为空字符串
|
|
|
|
|
- attachments: reviewInfo.attachments ?? "",
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- await createProductReview(input);
|
|
|
|
|
-
|
|
|
|
|
- setReviewInfo({
|
|
|
|
|
- title: "",
|
|
|
|
|
- comment: "",
|
|
|
|
|
- rating: 0,
|
|
|
|
|
- attachments: null,
|
|
|
|
|
- });
|
|
|
|
|
- setImgList([]); // 新增:清空多张图片数组
|
|
|
|
|
- setErrors({
|
|
|
|
|
- title: "",
|
|
|
|
|
- comment: "",
|
|
|
|
|
- rating: "",
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error("Error submitting review:", error);
|
|
|
|
|
- showToast("Failed to submit review. Please try again.", "danger");
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <form
|
|
|
|
|
- onSubmit={handleSubmit}
|
|
|
|
|
- className="w-full max-w-4xl mx-auto p-4 md:p-6 rounded-xl relative"
|
|
|
|
|
- >
|
|
|
|
|
- <div className="flex mb-4">
|
|
|
|
|
- <h1 className="text-xl font-semibold">Write a review</h1>
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={onClose}
|
|
|
|
|
- className="absolute top-4 right-4 z-50 p-2 rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-neutral-300 dark:hover:bg-neutral-700 text-gray-500 hover:text-gray-700 dark:hover:text-gray-400 transition-colors"
|
|
|
|
|
- aria-label="Close review form"
|
|
|
|
|
- >
|
|
|
|
|
- <svg
|
|
|
|
|
- xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
- className="h-5 w-5"
|
|
|
|
|
- fill="none"
|
|
|
|
|
- viewBox="0 0 24 24"
|
|
|
|
|
- stroke="currentColor"
|
|
|
|
|
- >
|
|
|
|
|
- <path
|
|
|
|
|
- strokeLinecap="round"
|
|
|
|
|
- strokeLinejoin="round"
|
|
|
|
|
- strokeWidth={2}
|
|
|
|
|
- d="M6 18L18 6M6 6l12 12"
|
|
|
|
|
- />
|
|
|
|
|
- </svg>
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="flex flex-col gap-4">
|
|
|
|
|
- <div className="space-y-4">
|
|
|
|
|
- {/* 多张图片预览 横向排列 */}
|
|
|
|
|
- <div className="flex flex-wrap gap-3 items-start">
|
|
|
|
|
- {imgList.map((imgItem, idx) => (
|
|
|
|
|
- <div
|
|
|
|
|
- key={idx}
|
|
|
|
|
- className="relative w-[120px] h-[120px] border-2 border-dashed border-gray-300 rounded-xl"
|
|
|
|
|
- >
|
|
|
|
|
- <Image
|
|
|
|
|
- src={imgItem.preview}
|
|
|
|
|
- alt="preview"
|
|
|
|
|
- width={120}
|
|
|
|
|
- height={120}
|
|
|
|
|
- className="w-full h-full object-cover rounded-lg"
|
|
|
|
|
- />
|
|
|
|
|
- {/* 单张删除按钮 */}
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={() => removeSingleImage(idx)}
|
|
|
|
|
- className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- <svg
|
|
|
|
|
- xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
- className="h-4 w-4"
|
|
|
|
|
- fill="none"
|
|
|
|
|
- viewBox="0 0 24 24"
|
|
|
|
|
- stroke="currentColor"
|
|
|
|
|
- >
|
|
|
|
|
- <path
|
|
|
|
|
- strokeLinecap="round"
|
|
|
|
|
- strokeLinejoin="round"
|
|
|
|
|
- strokeWidth={2}
|
|
|
|
|
- d="M6 18L18 6M6 6l12 12"
|
|
|
|
|
- />
|
|
|
|
|
- </svg>
|
|
|
|
|
- </button>
|
|
|
|
|
- {/* 图片文件名 */}
|
|
|
|
|
- <p className="text-xs text-gray-500 truncate px-1 mt-1">
|
|
|
|
|
- {imgItem.fileName}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
-
|
|
|
|
|
- {/* 上传按钮:不足3张才显示 */}
|
|
|
|
|
- {imgList.length < MAX_UPLOAD && (
|
|
|
|
|
- <div className="border-2 border-dashed border-gray-300 w-32 h-32 rounded-xl p-6 text-center transition-colors hover:border-primary-500">
|
|
|
|
|
- <label
|
|
|
|
|
- htmlFor="file-upload"
|
|
|
|
|
- className="mx-auto flex cursor-pointer flex-col items-center justify-center gap-2 h-full"
|
|
|
|
|
- >
|
|
|
|
|
- <div className="mx-auto flex h-8 w-8 items-center justify-center rounded-full">
|
|
|
|
|
- <AddUploadImage />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="text-sm text-center">
|
|
|
|
|
- <span className="font-medium">Add Image</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <input
|
|
|
|
|
- id="file-upload"
|
|
|
|
|
- name="file-upload"
|
|
|
|
|
- type="file"
|
|
|
|
|
- className="sr-only"
|
|
|
|
|
- accept="image/*"
|
|
|
|
|
- onChange={handleImageUpload}
|
|
|
|
|
- />
|
|
|
|
|
- </label>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- {/* <p className="text-sm text-gray-400">
|
|
|
|
|
- Max {MAX_UPLOAD} images allowed
|
|
|
|
|
- </p> */}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="flex flex-col gap-4">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label className="block text-base font-semibold mb-1">
|
|
|
|
|
- Rating
|
|
|
|
|
- </label>
|
|
|
|
|
- <AddRatingStar
|
|
|
|
|
- value={reviewInfo.rating}
|
|
|
|
|
- onChange={(value) => {
|
|
|
|
|
- setReviewInfo((prev) => ({ ...prev, rating: value }));
|
|
|
|
|
- setErrors((prev) => ({ ...prev, rating: "" }));
|
|
|
|
|
- }}
|
|
|
|
|
- size="size-6"
|
|
|
|
|
- className="mt-1"
|
|
|
|
|
- />
|
|
|
|
|
- {errors.rating && (
|
|
|
|
|
- <p className="text-red-500 text-sm mt-1">{errors.rating}</p>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <Textarea
|
|
|
|
|
- label="Title"
|
|
|
|
|
- placeholder="Title"
|
|
|
|
|
- labelPlacement="outside"
|
|
|
|
|
- value={reviewInfo.title}
|
|
|
|
|
- onChange={(e) => {
|
|
|
|
|
- setReviewInfo((prev) => ({ ...prev, title: e.target.value }));
|
|
|
|
|
- setErrors((prev) => ({ ...prev, title: "" }));
|
|
|
|
|
- }}
|
|
|
|
|
- minRows={1}
|
|
|
|
|
- maxRows={1}
|
|
|
|
|
- variant="bordered"
|
|
|
|
|
- isInvalid={!!errors.title}
|
|
|
|
|
- errorMessage={errors.title}
|
|
|
|
|
- classNames={{
|
|
|
|
|
- input: "text-sm",
|
|
|
|
|
- label: "px-2 text-base font-semibold",
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- <Textarea
|
|
|
|
|
- label="Comment"
|
|
|
|
|
- placeholder="Comment"
|
|
|
|
|
- labelPlacement="outside"
|
|
|
|
|
- value={reviewInfo.comment}
|
|
|
|
|
- onChange={(e) => {
|
|
|
|
|
- setReviewInfo((prev) => ({ ...prev, comment: e.target.value }));
|
|
|
|
|
- setErrors((prev) => ({ ...prev, comment: "" }));
|
|
|
|
|
- }}
|
|
|
|
|
- minRows={5}
|
|
|
|
|
- variant="bordered"
|
|
|
|
|
- isInvalid={!!errors.comment}
|
|
|
|
|
- errorMessage={errors.comment}
|
|
|
|
|
- classNames={{
|
|
|
|
|
- input: "text-sm",
|
|
|
|
|
- label: "px-2 text-base font-semibold",
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- <div className="w-32">
|
|
|
|
|
- <Button
|
|
|
|
|
- title={isLoading ? "Submitting..." : "Submit"}
|
|
|
|
|
- type="submit"
|
|
|
|
|
- disabled={isLoading}
|
|
|
|
|
- className="w-full mt-4 rounded-2xl"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </form>
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|