Changes: - Add FAL_KEY and GEMINI_API_KEY to .env.example - Update picture-it to use ~/.config/opencode/.env (unified creds) - Remove shodh-memory skill (no longer used) - Remove alphaear-* skills (deprecated) - Remove thai-frontend-dev skill (replaced by website-creator) - Remove theme-factory skill - Add mql-developer skill (MQL5 trading) - Add ecommerce-astro skill (Astro e-commerce) - Add website-creator skill (Next.js + Payload CMS) - Update install script for new skills
180 lines
6.8 KiB
TypeScript
180 lines
6.8 KiB
TypeScript
import { useState } from 'react';
|
||
import StarRating from './StarRating';
|
||
|
||
interface ReviewFormProps {
|
||
productId: string;
|
||
onSubmit: (review: { rating: number; title: string; comment: string; images: File[] }) => void;
|
||
isSubmitting?: boolean;
|
||
}
|
||
|
||
export default function ReviewForm({ productId, onSubmit, isSubmitting = false }: ReviewFormProps) {
|
||
const [rating, setRating] = useState(0);
|
||
const [hoverRating, setHoverRating] = useState(0);
|
||
const [title, setTitle] = useState('');
|
||
const [comment, setComment] = useState('');
|
||
const [images, setImages] = useState<File[]>([]);
|
||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
|
||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const files = Array.from(e.target.files || []);
|
||
if (files.length + images.length > 5) {
|
||
setErrors(prev => ({ ...prev, images: 'สูงสุด 5 รูปเท่านั้น' }));
|
||
return;
|
||
}
|
||
|
||
setImages(prev => [...prev, ...files]);
|
||
|
||
const newPreviews = files.map(file => URL.createObjectURL(file));
|
||
setImagePreviews(prev => [...prev, ...newPreviews]);
|
||
setErrors(prev => {
|
||
const { images: _, ...rest } = prev;
|
||
return rest;
|
||
});
|
||
};
|
||
|
||
const removeImage = (index: number) => {
|
||
setImages(prev => prev.filter((_, i) => i !== index));
|
||
setImagePreviews(prev => {
|
||
URL.revokeObjectURL(prev[index]);
|
||
return prev.filter((_, i) => i !== index);
|
||
});
|
||
};
|
||
|
||
const validate = () => {
|
||
const newErrors: Record<string, string> = {};
|
||
if (rating === 0) newErrors.rating = 'กรุณาให้คะแนน';
|
||
if (!comment.trim()) newErrors.comment = 'กรุณาเขียนรีวิว';
|
||
if (comment.length < 10) newErrors.comment = 'รีวิวต้องมีอย่างน้อย 10 ตัวอักษร';
|
||
setErrors(newErrors);
|
||
return Object.keys(newErrors).length === 0;
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!validate()) return;
|
||
|
||
await onSubmit({ rating, title, comment, images });
|
||
|
||
// Reset form
|
||
setRating(0);
|
||
setHoverRating(0);
|
||
setTitle('');
|
||
setComment('');
|
||
setImages([]);
|
||
setImagePreviews([]);
|
||
};
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="bg-white rounded-xl border p-6 space-y-6">
|
||
<h3 className="font-medium text-lg">เขียนรีวิว</h3>
|
||
|
||
{/* Rating */}
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">คะแนน *</label>
|
||
<div className="flex items-center gap-1">
|
||
{[1, 2, 3, 4, 5].map((star) => (
|
||
<button
|
||
key={star}
|
||
type="button"
|
||
onClick={() => setRating(star)}
|
||
onMouseEnter={() => setHoverRating(star)}
|
||
onMouseLeave={() => setHoverRating(0)}
|
||
className="p-1 hover:scale-110 transition-transform"
|
||
>
|
||
<StarRating
|
||
rating={hoverRating || rating}
|
||
size={28}
|
||
/>
|
||
</button>
|
||
))}
|
||
{rating > 0 && (
|
||
<span className="ml-2 text-sm text-gray-600">
|
||
{rating === 1 ? 'ไม่พอใจมาก' :
|
||
rating === 2 ? 'ไม่พอใจ' :
|
||
rating === 3 ? 'พอใช้' :
|
||
rating === 4 ? 'พอใจ' : 'พอใจมาก'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
{errors.rating && <p className="text-red-500 text-sm mt-1">{errors.rating}</p>}
|
||
</div>
|
||
|
||
{/* Title */}
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1">หัวข้อ (ไม่บังคับ)</label>
|
||
<input
|
||
type="text"
|
||
value={title}
|
||
onChange={(e) => setTitle(e.target.value)}
|
||
placeholder="สรุปประสบการณ์ของคุณ"
|
||
className="w-full border rounded-lg px-3 py-2"
|
||
maxLength={100}
|
||
/>
|
||
</div>
|
||
|
||
{/* Comment */}
|
||
<div>
|
||
<label className="block text-sm font-medium mb-1">รีวิว *</label>
|
||
<textarea
|
||
value={comment}
|
||
onChange={(e) => setComment(e.target.value)}
|
||
placeholder="แชร์ประสบการณ์ที่ใช้สินค้าของคุณ..."
|
||
className={`w-full border rounded-lg px-3 py-2 h-32 resize-none ${errors.comment ? 'border-red-500' : ''}`}
|
||
/>
|
||
<div className="flex justify-between mt-1">
|
||
{errors.comment && <p className="text-red-500 text-sm">{errors.comment}</p>}
|
||
<span className="text-xs text-gray-400 ml-auto">{comment.length}/1000</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Images */}
|
||
<div>
|
||
<label className="block text-sm font-medium mb-2">รูปภาพ (ไม่บังคับ)</label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{imagePreviews.map((preview, i) => (
|
||
<div key={i} className="relative w-20 h-20">
|
||
<img
|
||
src={preview}
|
||
alt={`รูปที่ ${i + 1}`}
|
||
className="w-full h-full object-cover rounded-lg"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeImage(i)}
|
||
className="absolute -top-2 -right-2 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center text-xs"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
{images.length < 5 && (
|
||
<label className="w-20 h-20 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-blue-400 hover:bg-blue-50 transition-colors">
|
||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
<span className="text-xs text-gray-400 mt-1">{images.length}/5</span>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={handleImageChange}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
)}
|
||
</div>
|
||
{errors.images && <p className="text-red-500 text-sm mt-1">{errors.images}</p>}
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{isSubmitting ? 'กำลังส่งรีวิว...' : 'ส่งรีวิว'}
|
||
</button>
|
||
</form>
|
||
);
|
||
}
|