Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
All checks were successful
Build frontend / build (push) Successful in 1m17s

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Rahaf
2026-05-28 18:30:39 +03:00
39 changed files with 7050 additions and 3125 deletions

View File

@ -3,9 +3,10 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { Heart, Bell, CreditCard, Shield, UserPlus } from 'lucide-react';
import { Heart, Bell, CreditCard, Shield, UserPlus, Settings } from 'lucide-react';
import { useFavorites } from '@/app/contexts/FavoritesContext';
import { useNotifications } from '@/app/contexts/NotificationsContext';
import AuthService from '@/app/services/AuthService';
export default function FloatingSidebar({ isRTL, isAdmin }) {
const { favorites } = useFavorites();
@ -24,15 +25,16 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
setTooltip(null);
};
const side = isRTL ? 'left' : 'right';
const positionStyle = {
[side]: 0,
if (!AuthService.isAuthenticated()) return null;
const positionStyle = {
left: '16px',
top: '50%',
transform: 'translateY(-50%)',
};
const cardVariants = {
initial: { opacity: 0, x: isRTL ? -20 : 20 },
initial: { opacity: 0, x: 20 },
animate: { opacity: 1, x: 0, transition: { duration: 0.4, ease: 'easeOut' } },
};
@ -45,30 +47,21 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
const renderTooltip = (id, label) => {
if (tooltip !== id) return null;
return (
<div
className={`absolute ${isRTL ? 'right-full mr-2' : 'left-full ml-2'} top-1/2 -translate-y-1/2 px-2 py-1 bg-gray-800 text-white text-xs rounded-lg whitespace-nowrap z-20 shadow-lg flex items-center`}
>
<span className="relative">
{label}
<span
className={`absolute ${isRTL ? 'right-full -mr-1' : 'left-full -ml-1'} top-1/2 -translate-y-1/2 w-0 h-0 border-t-4 border-b-4 border-transparent ${
isRTL ? 'border-r-4 border-r-gray-800' : 'border-l-4 border-l-gray-800'
}`}
></span>
</span>
<div className="absolute left-full mr-3 top-1/2 -translate-y-1/2 px-3 py-2 bg-gray-800 text-white text-sm rounded-lg whitespace-nowrap z-20 shadow-lg">
{label}
</div>
);
};
return (
<motion.div
className="fixed z-50"
className="fixed z-40 pointer-events-none"
style={positionStyle}
variants={cardVariants}
initial="initial"
animate="animate"
>
<div className="bg-white/90 backdrop-blur-md rounded-2xl shadow-lg border border-gray-200/60 py-3 px-2 flex flex-col gap-3 transition-all duration-300 hover:shadow-xl hover:bg-white/95">
<div className="bg-white/90 backdrop-blur-md rounded-2xl shadow-lg border border-gray-200/60 py-4 px-3 flex flex-col gap-4 transition-all duration-300 hover:shadow-xl hover:bg-white/95 max-h-[75vh] overflow-y-auto pointer-events-auto">
{isAdmin ? (
<>
<motion.div
@ -82,9 +75,9 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/admin/add-admin"
className="flex items-center justify-center w-12 h-12 rounded-xl bg-amber-50 border border-amber-200 text-amber-600 hover:bg-amber-100 transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl bg-amber-50 border border-amber-200 text-amber-600 hover:bg-amber-100 transition-colors"
>
<UserPlus className="w-6 h-6" />
<UserPlus className="w-7 h-7" />
</Link>
{renderTooltip('addAdmin', 'إضافة أدمن')}
</motion.div>
@ -100,9 +93,9 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/admin/privacy"
className="flex items-center justify-center w-12 h-12 rounded-xl bg-slate-50 border border-slate-200 text-slate-700 hover:bg-slate-100 transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl bg-slate-50 border border-slate-200 text-slate-700 hover:bg-slate-100 transition-colors"
>
<Shield className="w-6 h-6" />
<Shield className="w-7 h-7" />
</Link>
{renderTooltip('editPrivacy', 'تعديل سياسة الخصوصية')}
</motion.div>
@ -120,15 +113,15 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/favorites"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<div className="relative">
<Heart className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<Heart className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
{favorites.length > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-amber-500 to-amber-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
>
{favorites.length}
</motion.span>
@ -148,15 +141,15 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/notifications"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<div className="relative">
<Bell className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<Bell className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
{unreadCount > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute -right-1 -top-1 w-5 h-5 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
className="absolute -right-1 -top-1 w-6 h-6 bg-linear-to-r from-red-500 to-red-600 text-white text-xs rounded-full flex items-center justify-center shadow-md"
>
{unreadCount}
</motion.span>
@ -176,12 +169,29 @@ export default function FloatingSidebar({ isRTL, isAdmin }) {
>
<Link
href="/payments"
className="flex items-center justify-center w-12 h-12 rounded-xl transition-colors"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<CreditCard className="w-6 h-6 text-gray-600 transition-colors group-hover:text-amber-600" />
<CreditCard className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('payments', 'المدفوعات')}
</motion.div>
<motion.div
className="relative group"
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="tap"
onMouseEnter={() => showTooltip('settings')}
onMouseLeave={hideTooltip}
>
<Link
href="/settings"
className="flex items-center justify-center w-14 h-14 rounded-xl transition-colors"
>
<Settings className="w-7 h-7 text-gray-600 transition-colors group-hover:text-amber-600" />
</Link>
{renderTooltip('settings', 'الإعدادات')}
</motion.div>
</>
)}
</div>

View File

@ -4,6 +4,7 @@ import { useEffect, useState, useRef } from "react";
import { initializeApp, getApps } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import AuthService from "../services/AuthService";
import { setFCMToken } from "../utils/api";
const firebaseConfig = {
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
@ -71,21 +72,7 @@ export default function NotificationHandler() {
});
if (fcmToken) {
console.log("[FCM] Token:", fcmToken.substring(0, 20) + "...");
const authToken = AuthService.getToken();
if (authToken) {
const apiBase = "https://45.93.137.91.nip.io/api";
await fetch(`${apiBase}/User/SetFCMToken`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({ token: fcmToken, deviceType: 2 }),
});
console.log("[FCM] Token sent to backend");
}
await setFCMToken(fcmToken, 2);
}
onMessage(messaging, (payload) => {

View File

@ -0,0 +1,92 @@
'use client';
import { useState } from 'react';
import { X, Loader2 } from 'lucide-react';
import toast from 'react-hot-toast';
import StarRating from './StarRating';
import { addCustomerRating } from '../../utils/ratings';
const RatingField = ({ label, value, onChange }) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">{label} <span className="text-red-500">*</span></label>
<StarRating rating={value} onRatingChange={onChange} size={28} />
{value === 0 && <p className="text-xs text-red-500">مطلوب</p>}
</div>
);
export default function CustomerRatingForm({ reservationId, onSuccess, onCancel }) {
const [furnitureIntegrityRating, setFurnitureIntegrityRating] = useState(0);
const [termsComplianceRating, setTermsComplianceRating] = useState(0);
const [renterBehaviorRating, setRenterBehaviorRating] = useState(0);
const [comment, setComment] = useState('');
const [loading, setLoading] = useState(false);
const validate = () => {
if (furnitureIntegrityRating === 0) return 'الحفاظ على الأثاث';
if (termsComplianceRating === 0) return 'الالتزام بالشروط';
if (renterBehaviorRating === 0) return 'سلوك المستأجر';
return null;
};
const handleSubmit = async (e) => {
e.preventDefault();
const missing = validate();
if (missing) {
toast.error(`يرجى تقييم: ${missing}`);
return;
}
setLoading(true);
try {
await addCustomerRating({
reservationId,
furnitureIntegrityRating,
termsComplianceRating,
renterBehaviorRating,
comment: comment.trim() || null,
});
toast.success('تم إرسال تقييم المستأجر بنجاح!');
onSuccess?.();
} catch (err) {
toast.error('حدث خطأ، حاول مرة أخرى');
} finally {
setLoading(false);
}
};
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 max-w-lg mx-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-900">تقييم المستأجر</h3>
{onCancel && (
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
<X size={20} />
</button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<RatingField label="الحفاظ على الأثاث" value={furnitureIntegrityRating} onChange={setFurnitureIntegrityRating} />
<RatingField label="الالتزام بالشروط" value={termsComplianceRating} onChange={setTermsComplianceRating} />
<RatingField label="سلوك المستأجر" value={renterBehaviorRating} onChange={setRenterBehaviorRating} />
<div>
<label className="block text-sm font-medium text-gray-700">تعليق (اختياري)</label>
<textarea
rows={3}
value={comment}
onChange={(e) => setComment(e.target.value)}
className="w-full mt-1 px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-amber-500"
placeholder="شارك تجربتك مع المستأجر..."
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 rounded-xl transition flex items-center justify-center gap-2 disabled:opacity-50"
>
{loading && <Loader2 className="w-5 h-5 animate-spin" />}
{loading ? 'جاري الإرسال...' : 'إرسال التقييم'}
</button>
</form>
</div>
);
}

View File

@ -255,7 +255,7 @@ export default function PropertyRatingList({ propertyId }) {
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<div className="flex justify-between items-center mb-4 pb-3 border-b border-gray-100">
<h3 className="text-xl font-bold text-gray-900">تقييمات المستأجرين</h3>
{average !== null && (
{average !== null && average > 0 && (
<div className="flex items-center gap-1 bg-amber-50 px-3 py-1 rounded-full">
<Star className="w-5 h-5 text-amber-500 fill-amber-500" />
<span className="font-bold text-lg">{average.toFixed(1)}</span>