Fix build errors: corrected import paths, added missing RatingList component, fixed syntax errors in rating components
All checks were successful
Build frontend / build (push) Successful in 54s

This commit is contained in:
Claw AI
2026-04-14 14:22:39 +00:00
parent ae600ad41b
commit db949aaeba
5 changed files with 651 additions and 3 deletions

View File

@ -0,0 +1,216 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Star, Edit2, X, Check, Clock } from 'lucide-react';
import StarRating from './StarRating.js';
import toast, { Toaster } from 'react-hot-toast';
import { rateProperty, rateCustomer, getUserPropertyRating, canRateProperty } from '../../utils/ratings.js';
const RatingForm = ({
propertyId,
userId,
propertyOwner = false,
initialRating = 0,
initialComment = '',
onSubmitSuccess
}) => {
const [rating, setRating] = useState(initialRating);
const [comment, setComment] = useState(initialComment);
const [loading, setLoading] = useState(false);
const [showForm, setShowForm] = useState(false);
const [userRating, setUserRating] = useState(null);
// Check if user has already rated
useState(() => {
async function fetchUserRating() {
try {
const rating = await getUserPropertyRating(propertyId, userId);
if (rating) {
setUserRating(rating);
setRating(rating.rating);
setComment(rating.comment || '');
}
} catch (error) {
console.error('[RatingForm] Failed to fetch user rating:', error);
}
}
if (propertyId && userId) {
fetchUserRating();
}
}, [propertyId, userId]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!rating) {
toast.error('يرجى إعطاء تقييم من 1 إلى 5 نجوم');
return;
}
setLoading(true);
try {
const ratingData = {
propertyId,
customerId: userId,
rating,
comment: comment.trim() || null
};
await rateProperty(ratingData);
toast.success('تم إرسال التقييم بنجاح!');
// Reset form
setRating(0);
setComment('');
setShowForm(false);
if (onSubmitSuccess) {
onSubmitSuccess();
}
} catch (error) {
console.error('[RatingForm] Failed to submit rating:', error);
toast.error('حدث خطأ أثناء إرسال التقييم. يرجى المحاولة مرة أخرى.');
} finally {
setLoading(false);
}
};
const handleEdit = () => {
setShowForm(true);
setRating(userRating?.rating || 0);
setComment(userRating?.comment || '');
};
const handleCancel = () => {
setShowForm(false);
setRating(userRating?.rating || 0);
setComment(userRating?.comment || '');
};
if (!propertyId || !userId) {
return null;
}
return (
<div className="space-y-4">
<Toaster position="top-center" reverseOrder={false} />
{/* Display existing rating */}
{userRating && !showForm && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gray-50 rounded-xl p-4 border border-gray-200"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Star className="w-5 h-5 text-amber-500" />
<span className="font-medium text-gray-900">{userRating.rating}</span>
<span className="text-sm text-gray-500">من 5</span>
</div>
<button
onClick={handleEdit}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm hover:bg-gray-200 transition-colors flex items-center gap-1"
>
<Edit2 className="w-4 h-4" />
تعديل
</button>
</div>
{userRating.comment && (
<div className="text-gray-600 text-sm mb-3 line-clamp-3">
"{userRating.comment}"
</div>
)}
<div className="flex items-center gap-2 text-xs text-gray-400">
<Clock className="w-3 h-3" />
<span>{userRating.createdAt ? new Date(userRating.createdAt).toLocaleDateString('ar-SA') : ''}</span>
</div>
</motion.div>
)}
{/* Rating form */}
{showForm && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-xl p-6 border border-gray-200 shadow-sm"
>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
تقييمك للعقار
</label>
<div className="flex items-center gap-2">
<StarRating
rating={rating}
onRatingChange={setRating}
readOnly={false}
size={28}
color="#ffc107"
/>
<span className="text-lg font-bold text-gray-900">{rating || '1'}</span>
<span className="text-sm text-gray-400">/5</span>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
تعليق (اختياري)
</label>
<textarea
rows="3"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="شارك تجربتك مع العقار..."
className="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-amber-500 transition-all resize-none"
/>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={handleCancel}
disabled={loading}
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
إلغاء
</button>
<button
type="submit"
disabled={loading || !rating}
className="flex-1 px-4 py-2 bg-amber-500 text-white rounded-lg font-medium hover:bg-amber-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-1"
>
{loading ? (
<div className="w-4 h-4 border-2 border-white/50 border-t-white rounded-full animate-spin" />
) : (
<Check className="w-5 h-5" />
)}
{loading ? 'إرسال' : 'إرسال التقييم'}
</button>
</div>
</form>
</motion.div>
)}
{/* Add rating button */}
{!userRating && !showForm && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-amber-50 border-2 border-amber-200 rounded-xl p-4 text-center cursor-pointer hover:border-amber-300 transition-all"
onClick={() => setShowForm(true)}
>
<Star className="w-8 h-8 text-amber-500 mx-auto mb-2" />
<h3 className="font-bold text-amber-700 mb-2">قيّم هذا العقار</h3>
<p className="text-sm text-amber-600">شارك تجربتك مع المستأجرين الآخرين</p>
</motion.div>
)}
</div>
);
};
export default RatingForm;

View File

@ -0,0 +1,149 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Star } from 'lucide-react';
import { getPropertyRatings } from '../../utils/ratings.js';
import toast, { Toaster } from 'react-hot-toast';
const RatingList = ({ propertyId, userId }) => {
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchReviews = async () => {
if (!propertyId) {
setLoading(false);
return;
}
try {
setLoading(true);
const data = await getPropertyReviews(propertyId);
setReviews(data || []);
setError(null);
} catch (err) {
console.error('[RatingList] Failed to fetch reviews:', err);
setError('فشل تحميل التقييمات');
setReviews([]);
} finally {
setLoading(false);
}
};
fetchReviews();
}, [propertyId]);
if (loading) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-8"
>
<div className="w-10 h-10 border-2 border-gray-200 border-t-gray-500 rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-500">جاري تحميل التقييمات...</p>
</motion.div>
);
}
if (error) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-8"
>
<p className="text-red-500">{error}</p>
</motion.div>
);
}
if (reviews.length === 0) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-8"
>
<p className="text-gray-500">لا توجد تقييمات حتى الآن. كن أول من يقيم هذا العقار!</p>
</motion.div>
);
}
// Calculate average rating
const averageRating = reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-2xl p-6 shadow-sm border border-gray-100"
>
<Toaster position="top-center" reverseOrder={false} />
<div className="space-y-4">
{/* Header with average rating */}
<div className="flex items-center justify-between pb-3 border-b border-gray-100">
<div>
<h2 className="text-xl font-bold text-gray-900">تقييمات المستأجرين</h2>
<p className="text-sm text-gray-500">
{reviews.length} تقييمات
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={`w-5 h-5 ${index < Math.floor(averageRating) ? 'text-amber-500' : 'text-gray-300'}`}
/>
))}
{averageRating % 1 !== 0 && (
<Star className="w-5 h-5 text-amber-400" />
)}
<span className="font-bold text-gray-900 ml-2">{averageRating.toFixed(1)}</span>
</div>
</div>
</div>
{/* Reviews list */}
<div className="space-y-4">
{reviews.map((review, index) => (
<div key={index} className="border-t border-gray-100 pt-4 first:border-t-0 first:pt-0">
<div className="flex justify-between items-start mb-2">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0">
<Star className="w-6 h-6 text-gray-600" />
</div>
<div>
<div className="font-medium text-gray-900">{review.userName || 'مستأجر'}</div>
<div className="flex items-center gap-1 mt-1 text-sm">
{Array.from({ length: 5 }).map((_, starIndex) => (
<Star
key={starIndex}
className={`w-4 h-4 ${starIndex < review.rating ? 'text-amber-500' : 'text-gray-300'}`}
/>
))}
<span className="ml-1 text-xs text-gray-500">({review.rating}/5)</span>
</div>
</div>
</div>
<div className="text-xs text-gray-400">
{review.createdAt ? new Date(review.createdAt).toLocaleDateString('ar-SA') : ''}
</div>
</div>
{review.comment && (
<p className="text-gray-700 text-sm leading-relaxed">{review.comment}</p>
)}
</div>
))}
</div>
</div>
</motion.div>
);
};
export default RatingList;

View File

@ -0,0 +1,90 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Star } from 'lucide-react';
const StarRating = ({
rating,
onRatingChange,
maxStars = 5,
size = 24,
color = '#ffc107',
readOnly = false,
className = ''
}) => {
const [hoverRating, setHoverRating] = useState(null);
const handleClick = (value) => {
if (!readOnly && onRatingChange) {
onRatingChange(value);
}
};
const handleMouseEnter = (value) => {
if (!readOnly) {
setHoverRating(value);
}
};
const handleMouseLeave = () => {
if (!readOnly) {
setHoverRating(null);
}
};
const getStarIcon = (index) => {
const currentRating = hoverRating !== null ? hoverRating : rating;
if (currentRating > index) {
const hasHalfStar = currentRating % 1 > 0.5 && index + 0.5 <= currentRating;
if (hasHalfStar) {
// For half star, we'll use a combination approach or just show full star
// Since we don't have StarOutline, we'll approximate with full stars
return <Star className={`w-${size} h-${size} text-${color}`} />;
}
return <Star className={`w-${size} h-${size} text-${color}`} />;
}
return <Star className={`w-${size} h-${size} text-gray-400`} />;
};
return (
<div className={`flex gap-1 ${className}`} onMouseLeave={handleMouseLeave}>
{[...Array(maxStars)].map((_, index) => (
<motion.div
key={index}
whileHover={{ scale: readOnly ? 1 : 1.1 }}
onClick={() => handleClick(index + 1)}
onMouseEnter={() => handleMouseEnter(index + 1)}
>
{getStarIcon(index)}
</motion.div>
))}
</div>
);
};
export default StarRating;
// Helper functions
export function getStarCount(rating, maxStars = 5) {
return Math.round(rating * maxStars) / maxStars;
}
export function formatRating(rating) {
if (rating === 0) return 'لا يوجد تقييم';
return `${rating.toFixed(1)}`; // Show 1 decimal place
}
export function getRatingColor(rating) {
if (rating >= 4.5) return 'text-green-600';
if (rating >= 3.5) return 'text-yellow-600';
if (rating >= 2.5) return 'text-orange-600';
return 'text-red-600';
}
export function getRatingText(rating) {
if (rating >= 4.5) return 'ممتاز';
if (rating >= 3.5) return 'جيد جداً';
if (rating >= 2.5) return 'جيد';
if (rating >= 1.5) return 'مقبول';
return 'ضعيف';
}