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
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user