diff --git a/app/contexts/FavoritesContext.js b/app/contexts/FavoritesContext.js index e31c161..b4772bb 100644 --- a/app/contexts/FavoritesContext.js +++ b/app/contexts/FavoritesContext.js @@ -1,6 +1,8 @@ 'use client'; -import { createContext, useContext, useState, useEffect } from 'react'; +import { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { getUserFavoriteProperties, addFavoriteProperty, removeFavoriteProperty } from '../utils/api'; +import AuthService from '../services/AuthService'; const FavoritesContext = createContext(); @@ -12,42 +14,106 @@ export const useFavorites = () => { return context; }; +function mapApiFavorite(item) { + const info = item.propertyInformation || {}; + let details = {}; + try { details = JSON.parse(info.detailsJSON || '{}'); } catch {} + + const price = item.monthlyRent || item.dailyRent || 0; + const priceUnit = item.monthlyRent ? 'monthly' : 'daily'; + const buildingType = info.buildingType ?? 0; + const type = { 0: 'apartment', 1: 'villa', 2: 'house' }[buildingType] || 'apartment'; + const typeLabel = { 0: 'شقة', 1: 'فيلا', 2: 'بيت' }[buildingType] || 'عقار'; + const address = info.address || ''; + const addressParts = address.split(',').map(s => s.trim()).filter(Boolean); + const images = info.images || []; + const resolvedImages = images.map(img => { + if (!img) return ''; + if (img.startsWith('http')) return img; + return `http://45.93.137.91${img.startsWith('/') ? '' : '/'}${img}`; + }); + + return { + id: info.id || item.propertyInformationId, + faveId: item.id, // needed to remove from favorites + title: `${typeLabel} في ${addressParts[0] || address}`, + type, + typeLabel, + price, + priceUnit, + bedrooms: info.numberOfBedRooms || 0, + bathrooms: info.numberOfBathRooms || 0, + area: info.space || 0, + location: { + city: addressParts[addressParts.length - 1] || '', + district: addressParts[0] || '', + }, + images: resolvedImages, + rating: item.rating || 0, + deposit: item.deposit || 0, + }; +} + export const FavoritesProvider = ({ children }) => { const [favorites, setFavorites] = useState([]); + const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - const stored = localStorage.getItem('favorites'); - if (stored) { - try { - setFavorites(JSON.parse(stored)); - } catch (e) { - console.error('Failed to parse favorites', e); - } + const fetchFavorites = useCallback(async () => { + if (!AuthService.isAuthenticated()) { + setFavorites([]); + return; + } + setIsLoading(true); + try { + const data = await getUserFavoriteProperties(); + const items = Array.isArray(data) ? data : []; + setFavorites(items.map(mapApiFavorite)); + } catch (err) { + console.error('[Favorites] Failed to fetch:', err); + setFavorites([]); + } finally { + setIsLoading(false); } }, []); useEffect(() => { - localStorage.setItem('favorites', JSON.stringify(favorites)); - }, [favorites]); + fetchFavorites(); + }, [fetchFavorites]); - const addFavorite = (property) => { - setFavorites(prev => { - if (prev.some(p => p.id === property.id)) return prev; - return [...prev, property]; - }); + const addFavorite = async (propId) => { + if (!AuthService.isAuthenticated()) return false; + try { + await addFavoriteProperty(propId); + await fetchFavorites(); // refresh list + return true; + } catch (err) { + console.error('[Favorites] Add failed:', err); + return false; + } }; - const removeFavorite = (propertyId) => { - setFavorites(prev => prev.filter(p => p.id !== propertyId)); + const removeFavorite = async (propId) => { + if (!AuthService.isAuthenticated()) return false; + // Find the faveId for this property + const fav = favorites.find(f => f.id === propId); + if (!fav) return false; + try { + await removeFavoriteProperty(fav.faveId); + setFavorites(prev => prev.filter(f => f.id !== propId)); + return true; + } catch (err) { + console.error('[Favorites] Remove failed:', err); + return false; + } }; - const isFavorite = (propertyId) => { - return favorites.some(p => p.id === propertyId); + const isFavorite = (propId) => { + return favorites.some(f => f.id === propId); }; return ( - + {children} ); -}; \ No newline at end of file +}; diff --git a/app/favorites/page.js b/app/favorites/page.js index 600c9dd..2ee57ea 100644 --- a/app/favorites/page.js +++ b/app/favorites/page.js @@ -11,8 +11,7 @@ import AuthService from '@/app/services/AuthService'; export default function FavoritesPage() { const router = useRouter(); - const { favorites, removeFavorite } = useFavorites(); - const [isLoading, setIsLoading] = useState(true); + const { favorites, isLoading: favoritesLoading, removeFavorite } = useFavorites(); const [isAdmin, setIsAdmin] = useState(false); useEffect(() => { @@ -21,14 +20,13 @@ export default function FavoritesPage() { return; } setIsAdmin(AuthService.isAdmin()); - setIsLoading(false); }, [router]); const formatCurrency = (amount) => { return amount?.toLocaleString() + ' ل.س'; }; - if (isLoading) { + if (favoritesLoading) { return (
diff --git a/app/properties/page.js b/app/properties/page.js index 3396f32..f8edd7e 100644 --- a/app/properties/page.js +++ b/app/properties/page.js @@ -32,6 +32,9 @@ import { import Image from 'next/image'; import Link from 'next/link'; import { getRentProperties, getSaleProperties } from '../utils/api'; +import { useFavorites } from '@/app/contexts/FavoritesContext'; +import AuthService from '@/app/services/AuthService'; +import toast, { Toaster } from 'react-hot-toast'; // Map API data to UI format function mapApiProperty(item, index) { @@ -95,9 +98,25 @@ function extractCity(address) { // API-only — no fallback data const PropertyCard = ({ property, viewMode = 'grid' }) => { - const [isFavorite, setIsFavorite] = useState(false); + const { isFavorite: checkFavorite, addFavorite, removeFavorite } = useFavorites(); + const [favLoading, setFavLoading] = useState(false); const [currentImage, setCurrentImage] = useState(0); + const isFav = checkFavorite(property.id); + + const toggleFavorite = async (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!AuthService.isAuthenticated()) { toast.error('سجل الدخول أولاً'); return; } + setFavLoading(true); + if (isFav) { + await removeFavorite(property.id); + } else { + await addFavorite(property.id); + } + setFavLoading(false); + }; + const formatCurrency = (amount) => { return amount?.toLocaleString() + ' ل.س'; }; @@ -150,10 +169,11 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => { )}
@@ -231,10 +251,11 @@ const PropertyCard = ({ property, viewMode = 'grid' }) => { />
@@ -622,6 +643,7 @@ export default function PropertiesPage() { )} + ); } diff --git a/app/property/[id]/PropertyDetail.js b/app/property/[id]/PropertyDetail.js index b927a1f..374025b 100644 --- a/app/property/[id]/PropertyDetail.js +++ b/app/property/[id]/PropertyDetail.js @@ -47,6 +47,7 @@ import { } from 'lucide-react'; import { getRentProperty, getSaleProperty, bookReservation, checkAvailability, getAvailableDateRanges } from '../../utils/api'; import AuthService from '../../services/AuthService'; +import { useFavorites } from '@/app/contexts/FavoritesContext'; import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums'; // Copy to clipboard that works on HTTP too @@ -161,6 +162,7 @@ function mapApiDetail(item) { export default function PropertyDetailsPage() { const params = useParams(); + const { isFavorite, addFavorite, removeFavorite } = useFavorites(); const [currentImage, setCurrentImage] = useState(0); const [showContact, setShowContact] = useState(false); const [showShareMenu, setShowShareMenu] = useState(false); @@ -172,6 +174,7 @@ export default function PropertyDetailsPage() { const [bookingSuccess, setBookingSuccess] = useState(false); const [availableRanges, setAvailableRanges] = useState([]); const [calendarMonth, setCalendarMonth] = useState(new Date()); + const [favLoading, setFavLoading] = useState(false); const [selectingEnd, setSelectingEnd] = useState(false); const [showLoginDialog, setShowLoginDialog] = useState(false); @@ -417,8 +420,24 @@ export default function PropertyDetailsPage() { العودة إلى العقارات
- {/* Share Dropdown */}
diff --git a/app/utils/api.js b/app/utils/api.js index bf64724..134a0fb 100644 --- a/app/utils/api.js +++ b/app/utils/api.js @@ -352,3 +352,17 @@ export function isEmail(value) { export function isPhoneNumber(value) { return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, '')); } + +// ─── Favorites ─── + +export async function getUserFavoriteProperties() { + return apiFetch('/FavoriteProperty/GetUserFavoriteProperties'); +} + +export async function addFavoriteProperty(propId) { + return apiFetch(`/FavoriteProperty/Add?propId=${propId}`, { method: 'POST' }); +} + +export async function removeFavoriteProperty(favePropId) { + return apiFetch(`/FavoriteProperty/Remove?favePropId=${favePropId}`, { method: 'DELETE' }); +}