Files
opencode-skill/skills/ecommerce-astro/scripts/templates/src/components/review/ReviewForm.tsx
Kunthawat Greethong b26c8199a5 Update skills: add website-creator, mql-developer, ecommerce-astro
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
2026-04-16 17:40:27 +07:00

180 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}