Edit add properties for owner
This commit is contained in:
@ -159,7 +159,7 @@ export default function ClientLayout({ children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="min-h-screen flex flex-col">
|
||||||
{!isAuthPage && !isAuthenticated && (
|
{!isAuthPage && !isAuthenticated && (
|
||||||
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm">
|
<nav className="fixed top-0 left-0 right-0 bg-white/95 backdrop-blur-sm border-b border-gray-200 z-50 transition-all duration-300 shadow-sm">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
@ -714,14 +714,15 @@ export default function ClientLayout({ children }) {
|
|||||||
<NotificationsProvider>
|
<NotificationsProvider>
|
||||||
<FavoritesProvider>
|
<FavoritesProvider>
|
||||||
<main
|
<main
|
||||||
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} min-h-screen bg-gradient-to-b from-gray-50 to-white ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
className={`${!isAuthPage && !isProfilePage && !isAuthenticated ? "pt-20" : ""} flex-1 flex flex-col bg-gray-50 ${currentLanguage === "ar" ? "text-right" : "text-left"}`}
|
||||||
>
|
>
|
||||||
{children}
|
<div className="flex-1">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{isAuthenticated && !isAuthPage && (
|
||||||
|
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{isAuthenticated && !isAuthPage && (
|
|
||||||
<BottomNav isOwner={isOwner} isOwnerOrAgent={isOwnerOrAgent} />
|
|
||||||
)}
|
|
||||||
</FavoritesProvider>
|
</FavoritesProvider>
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
|
|
||||||
@ -801,6 +802,6 @@ export default function ClientLayout({ children }) {
|
|||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
<NotificationHandler />
|
<NotificationHandler />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,8 +33,8 @@ export default function BottomNav({ isOwner, isOwnerOrAgent }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50">
|
<nav className="bg-gray-50 pb-4 pt-2">
|
||||||
<div className="bg-white/95 backdrop-blur-sm border border-gray-200 rounded-3xl shadow-lg px-2 py-2 flex items-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
{items.map((it) => {
|
{items.map((it) => {
|
||||||
const Icon = it.icon;
|
const Icon = it.icon;
|
||||||
const active = isActive(it.href);
|
const active = isActive(it.href);
|
||||||
|
|||||||
85
app/components/property/AddPropertyMap.js
Normal file
85
app/components/property/AddPropertyMap.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default function AddPropertyMap({ mapCenter, mapZoom, selectedLocation, onMapClick, onMarkerDragEnd }) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const markerRef = useRef(null);
|
||||||
|
const LRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function init() {
|
||||||
|
const L = await import('leaflet');
|
||||||
|
LRef.current = L;
|
||||||
|
|
||||||
|
delete L.Icon.Default.prototype._getIconUrl;
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||||
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!containerRef.current || mapRef.current) return;
|
||||||
|
|
||||||
|
const map = L.map(containerRef.current, {
|
||||||
|
center: mapCenter,
|
||||||
|
zoom: mapZoom,
|
||||||
|
doubleClickZoom: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
map.on('click', (e) => {
|
||||||
|
const pos = [e.latlng.lat, e.latlng.lng];
|
||||||
|
placeMarker(pos);
|
||||||
|
map.setView(pos, mapZoom);
|
||||||
|
onMapClick(pos);
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.current = map;
|
||||||
|
setTimeout(() => map.invalidateSize(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
mapRef.current.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function placeMarker(pos) {
|
||||||
|
const L = LRef.current;
|
||||||
|
if (!L || !mapRef.current) return;
|
||||||
|
|
||||||
|
if (markerRef.current) {
|
||||||
|
markerRef.current.setLatLng(pos);
|
||||||
|
} else {
|
||||||
|
markerRef.current = L.marker(pos, { draggable: true }).addTo(mapRef.current);
|
||||||
|
markerRef.current.on('dragend', (ev) => {
|
||||||
|
const p = ev.target.getLatLng();
|
||||||
|
onMarkerDragEnd(p.lat, p.lng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
map.setView(mapCenter, mapZoom);
|
||||||
|
}, [mapCenter, mapZoom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedLocation || !mapRef.current) return;
|
||||||
|
const pos = [selectedLocation.lat, selectedLocation.lng];
|
||||||
|
mapRef.current.setView(pos, mapZoom);
|
||||||
|
placeMarker(pos);
|
||||||
|
}, [selectedLocation]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} className="w-full h-full z-0" />;
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@ -68,21 +67,8 @@ import {
|
|||||||
CurrencyLabels
|
CurrencyLabels
|
||||||
} from '../../../enums';
|
} from '../../../enums';
|
||||||
|
|
||||||
const MapContainer = dynamic(() => import('react-leaflet').then(mod => mod.MapContainer), { ssr: false });
|
import dynamic from 'next/dynamic';
|
||||||
const TileLayer = dynamic(() => import('react-leaflet').then(mod => mod.TileLayer), { ssr: false });
|
const AddPropertyMap = dynamic(() => import('@/app/components/property/AddPropertyMap'), { ssr: false });
|
||||||
const Marker = dynamic(() => import('react-leaflet').then(mod => mod.Marker), { ssr: false });
|
|
||||||
const Popup = dynamic(() => import('react-leaflet').then(mod => mod.Popup), { ssr: false });
|
|
||||||
import { useMapEvents } from 'react-leaflet';
|
|
||||||
|
|
||||||
function MapClickHandler({ onMapClick }) {
|
|
||||||
const map = useMapEvents({
|
|
||||||
click: (e) => {
|
|
||||||
const { lat, lng } = e.latlng;
|
|
||||||
onMapClick([lat, lng]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddPropertyPage() {
|
export default function AddPropertyPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -123,7 +109,7 @@ export default function AddPropertyPage() {
|
|||||||
[PropertyTerm.NO_PARTIES]: false
|
[PropertyTerm.NO_PARTIES]: false
|
||||||
},
|
},
|
||||||
|
|
||||||
offerType: 'daily',
|
offerType: '',
|
||||||
|
|
||||||
dailyPrice: '',
|
dailyPrice: '',
|
||||||
monthlyPrice: '',
|
monthlyPrice: '',
|
||||||
@ -174,13 +160,13 @@ export default function AddPropertyPage() {
|
|||||||
const [mapCenter, setMapCenter] = useState([33.5138, 36.2765]);
|
const [mapCenter, setMapCenter] = useState([33.5138, 36.2765]);
|
||||||
const [mapZoom, setMapZoom] = useState(13);
|
const [mapZoom, setMapZoom] = useState(13);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [mapLoaded, setMapLoaded] = useState(false);
|
|
||||||
const [currencies, setCurrencies] = useState([]);
|
const [currencies, setCurrencies] = useState([]);
|
||||||
const [selectedCurrencyId, setSelectedCurrencyId] = useState(Currency.SYP);
|
const [selectedCurrencyId, setSelectedCurrencyId] = useState(Currency.SYP);
|
||||||
|
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [agreeToTerms, setAgreeToTerms] = useState(false);
|
||||||
|
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
@ -188,7 +174,7 @@ export default function AddPropertyPage() {
|
|||||||
{ id: 'apartment', label: 'شقة', icon: Building },
|
{ id: 'apartment', label: 'شقة', icon: Building },
|
||||||
{ id: 'villa', label: 'فيلا', icon: Home },
|
{ id: 'villa', label: 'فيلا', icon: Home },
|
||||||
{ id: 'sweet', label: 'سويت', icon: Sofa },
|
{ id: 'sweet', label: 'سويت', icon: Sofa },
|
||||||
{ id: 'room', label: 'غرفة', icon: DoorOpen },
|
{ id: 'room', label: 'غرفة ضمن شقة (سكن مشترك)', icon: DoorOpen },
|
||||||
{ id: 'studio', label: 'استوديو', icon: Sofa },
|
{ id: 'studio', label: 'استوديو', icon: Sofa },
|
||||||
{ id: 'office', label: 'مكتب', icon: Building },
|
{ id: 'office', label: 'مكتب', icon: Building },
|
||||||
{ id: 'farms', label: 'مزرعة', icon: Trees },
|
{ id: 'farms', label: 'مزرعة', icon: Trees },
|
||||||
@ -219,17 +205,6 @@ export default function AddPropertyPage() {
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const L = require('leaflet');
|
|
||||||
delete L.Icon.Default.prototype._getIconUrl;
|
|
||||||
L.Icon.Default.mergeOptions({
|
|
||||||
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
|
||||||
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
|
||||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setMapLoaded(true);
|
|
||||||
|
|
||||||
// Fetch available currencies
|
// Fetch available currencies
|
||||||
getCurrencies().then((data) => {
|
getCurrencies().then((data) => {
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
@ -353,6 +328,25 @@ const handleMapClick = async (coords) => {
|
|||||||
setMapZoom(18);
|
setMapZoom(18);
|
||||||
toast.success('تم تحديد الموقع', { id: 'location' });
|
toast.success('تم تحديد الموقع', { id: 'location' });
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
const handleMarkerDragEnd = async (lat, lng) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=ar`
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
setSelectedLocation({
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
address: data.display_name || 'موقع محدد'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setSelectedLocation({
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
address: 'موقع محدد'
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const confirmLocation = () => {
|
const confirmLocation = () => {
|
||||||
if (selectedLocation) {
|
if (selectedLocation) {
|
||||||
@ -382,8 +376,8 @@ const handleMapClick = async (coords) => {
|
|||||||
const handleImageUpload = async (files) => {
|
const handleImageUpload = async (files) => {
|
||||||
const newImages = Array.from(files);
|
const newImages = Array.from(files);
|
||||||
|
|
||||||
if (formData.images.length + newImages.length > 10) {
|
if (formData.images.length + newImages.length > 5) {
|
||||||
toast.error('يمكنك رفع 10 صور كحد أقصى');
|
toast.error('يمكنك رفع 5 صور كحد أقصى');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -545,13 +539,13 @@ const handleMapClick = async (coords) => {
|
|||||||
if (purpose === 'sale') {
|
if (purpose === 'sale') {
|
||||||
if (!formData.salePrice) newErrors.salePrice = 'سعر البيع مطلوب';
|
if (!formData.salePrice) newErrors.salePrice = 'سعر البيع مطلوب';
|
||||||
} else {
|
} else {
|
||||||
if (formData.offerType === 'daily' && !formData.dailyPrice) {
|
if (!formData.offerType) {
|
||||||
|
newErrors.offerType = 'الرجاء اختيار نوع العرض';
|
||||||
|
} else if (formData.offerType === 'daily' && !formData.dailyPrice) {
|
||||||
newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
||||||
}
|
} else if (formData.offerType === 'monthly' && !formData.monthlyPrice) {
|
||||||
if (formData.offerType === 'monthly' && !formData.monthlyPrice) {
|
|
||||||
newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
||||||
}
|
} else if (formData.offerType === 'both') {
|
||||||
if (formData.offerType === 'both') {
|
|
||||||
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
if (!formData.dailyPrice) newErrors.dailyPrice = 'السعر اليومي مطلوب';
|
||||||
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
if (!formData.monthlyPrice) newErrors.monthlyPrice = 'السعر الشهري مطلوب';
|
||||||
}
|
}
|
||||||
@ -562,8 +556,11 @@ const handleMapClick = async (coords) => {
|
|||||||
if (!formData.lat || !formData.lng) {
|
if (!formData.lat || !formData.lng) {
|
||||||
newErrors.location = 'الرجاء تحديد موقع العقار على الخريطة';
|
newErrors.location = 'الرجاء تحديد موقع العقار على الخريطة';
|
||||||
}
|
}
|
||||||
if (formData.images.length === 0) {
|
if (formData.images.length < 2) {
|
||||||
newErrors.images = 'يجب رفع صورة واحدة على الأقل';
|
newErrors.images = 'يجب رفع صورتين على الأقل';
|
||||||
|
}
|
||||||
|
if (formData.images.length > 5) {
|
||||||
|
newErrors.images = 'يمكن رفع 5 صور كحد أقصى';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -707,6 +704,8 @@ const handleMapClick = async (coords) => {
|
|||||||
transition: { duration: 0.5 }
|
transition: { duration: 0.5 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currencySymbol = selectedCurrencyId === Currency.USD ? '$' : 'SP';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 py-8">
|
<div className="min-h-screen bg-gray-50 py-8">
|
||||||
<Toaster position="top-center" reverseOrder={false} />
|
<Toaster position="top-center" reverseOrder={false} />
|
||||||
@ -854,8 +853,9 @@ const handleMapClick = async (coords) => {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Square className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<Square className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={formData.space}
|
min="0"
|
||||||
|
value={formData.space}
|
||||||
onChange={(e) => setFormData({...formData, space: e.target.value})}
|
onChange={(e) => setFormData({...formData, space: e.target.value})}
|
||||||
className="w-full pr-10 pl-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full pr-10 pl-3 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
placeholder="مثال: 120"
|
placeholder="مثال: 120"
|
||||||
@ -963,6 +963,7 @@ const handleMapClick = async (coords) => {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-2">عدد الصالونات</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">عدد الصالونات</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
min="0"
|
||||||
value={formData.salons}
|
value={formData.salons}
|
||||||
onChange={(e) => setFormData({...formData, salons: e.target.value})}
|
onChange={(e) => setFormData({...formData, salons: e.target.value})}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
@ -970,9 +971,10 @@ const handleMapClick = async (coords) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">عدد البلكونات</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">عدد الشرفات</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
min="0"
|
||||||
value={formData.balconies}
|
value={formData.balconies}
|
||||||
onChange={(e) => setFormData({...formData, balconies: e.target.value})}
|
onChange={(e) => setFormData({...formData, balconies: e.target.value})}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
@ -1113,11 +1115,18 @@ const handleMapClick = async (coords) => {
|
|||||||
سعر البيع (ل.س) <span className="text-red-500">*</span>
|
سعر البيع (ل.س) <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
min="0"
|
||||||
value={formData.salePrice || ''}
|
value={formData.salePrice || ''}
|
||||||
onChange={(e) => setFormData({...formData, salePrice: e.target.value})}
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, salePrice: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
||||||
errors.salePrice ? 'border-red-500' : 'border-gray-300'
|
errors.salePrice ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
@ -1190,52 +1199,25 @@ const handleMapClick = async (coords) => {
|
|||||||
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
onChange={(e) => setSelectedCurrencyId(parseInt(e.target.value))}
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
>
|
>
|
||||||
{Object.entries(CurrencyLabels).map(([id, label]) => (
|
<option value={Currency.USD}>دولار امريكي $</option>
|
||||||
<option key={id} value={id}>
|
<option value={Currency.SYP}>عملة سورية SP</option>
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Deposit field */}
|
{/* Price and deposit fields - conditional on offerType */}
|
||||||
<div>
|
{errors.offerType && (
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<p className="text-red-500 text-sm mt-1">{errors.offerType}</p>
|
||||||
مبلغ الضمان (العربون)
|
)}
|
||||||
</label>
|
|
||||||
<div className="relative">
|
{!formData.offerType && (
|
||||||
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
<div className="border-2 border-dashed border-gray-200 rounded-2xl p-8 text-center">
|
||||||
<input
|
<DollarSign className="w-8 h-8 text-gray-300 mx-auto mb-3" />
|
||||||
type="number"
|
<p className="text-gray-400 font-medium">اختر نوع العرض اولا</p>
|
||||||
value={formData.deposit || ''}
|
|
||||||
onChange={(e) => setFormData({...formData, deposit: e.target.value})}
|
|
||||||
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
||||||
placeholder="مثال: 500000"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Payment period picker */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
مدة السداد
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={formData.allowedPaymentPeriod}
|
|
||||||
onChange={(e) => setFormData({...formData, allowedPaymentPeriod: e.target.value})}
|
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
|
||||||
>
|
|
||||||
<option value="">اختر المدة</option>
|
|
||||||
<option value="1.00:00:00">يومي</option>
|
|
||||||
<option value="7.00:00:00">أسبوعي</option>
|
|
||||||
<option value="30.00:00:00">شهري</option>
|
|
||||||
<option value="90.00:00:00">ربع سنوي</option>
|
|
||||||
<option value="365.00:00:00">سنوي</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{(formData.offerType === 'daily' || formData.offerType === 'both') && (
|
{formData.offerType === 'daily' && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="daily"
|
key="daily"
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
@ -1245,14 +1227,21 @@ const handleMapClick = async (coords) => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
السعر اليومي (ل.س) <span className="text-red-500">*</span>
|
سعر الايجار اليومي ({currencySymbol}) <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
min="0"
|
||||||
value={formData.dailyPrice}
|
value={formData.dailyPrice}
|
||||||
onChange={(e) => setFormData({...formData, dailyPrice: e.target.value})}
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, dailyPrice: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
||||||
errors.dailyPrice ? 'border-red-500' : 'border-gray-300'
|
errors.dailyPrice ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
@ -1263,10 +1252,32 @@ const handleMapClick = async (coords) => {
|
|||||||
<p className="text-red-500 text-sm mt-1">{errors.dailyPrice}</p>
|
<p className="text-red-500 text-sm mt-1">{errors.dailyPrice}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
مبلغ التأمين ({currencySymbol})
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.deposit || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, deposit: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder="مثال: 500000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(formData.offerType === 'monthly' || formData.offerType === 'both') && (
|
{formData.offerType === 'monthly' && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="monthly"
|
key="monthly"
|
||||||
initial={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
@ -1276,14 +1287,21 @@ const handleMapClick = async (coords) => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
السعر الشهري (ل.س) <span className="text-red-500">*</span>
|
سعر الايجار الشهري ({currencySymbol}) <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DollarSign className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
min="0"
|
||||||
value={formData.monthlyPrice}
|
value={formData.monthlyPrice}
|
||||||
onChange={(e) => setFormData({...formData, monthlyPrice: e.target.value})}
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, monthlyPrice: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
||||||
errors.monthlyPrice ? 'border-red-500' : 'border-gray-300'
|
errors.monthlyPrice ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
@ -1294,6 +1312,115 @@ const handleMapClick = async (coords) => {
|
|||||||
<p className="text-red-500 text-sm mt-1">{errors.monthlyPrice}</p>
|
<p className="text-red-500 text-sm mt-1">{errors.monthlyPrice}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
مبلغ التأمين ({currencySymbol})
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.deposit || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, deposit: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder="مثال: 500000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formData.offerType === 'both' && (
|
||||||
|
<motion.div
|
||||||
|
key="both"
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
سعر الايجار اليومي ({currencySymbol}) <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.dailyPrice}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, dailyPrice: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
||||||
|
errors.dailyPrice ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
placeholder="مثال: 50000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.dailyPrice && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{errors.dailyPrice}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
سعر الايجار الشهري ({currencySymbol}) <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.monthlyPrice}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, monthlyPrice: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
|
className={`w-full pr-12 pl-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 ${
|
||||||
|
errors.monthlyPrice ? 'border-red-500' : 'border-gray-300'
|
||||||
|
}`}
|
||||||
|
placeholder="مثال: 1000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.monthlyPrice && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{errors.monthlyPrice}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
مبلغ التأمين ({currencySymbol})
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 font-bold text-sm flex items-center justify-center">{currencySymbol}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={formData.deposit || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '' || parseFloat(val) >= 0) {
|
||||||
|
setFormData({...formData, deposit: val});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (['-', 'e', 'E', '+'].includes(e.key)) e.preventDefault() }}
|
||||||
|
className="w-full pr-12 pl-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
placeholder="مثال: 500000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@ -1333,65 +1460,24 @@ const handleMapClick = async (coords) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative w-full h-96 rounded-xl overflow-hidden border-2 border-gray-200 mb-4">
|
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 mb-4 text-sm text-amber-800 leading-relaxed">
|
||||||
{mapLoaded && (
|
<Search className="w-4 h-4 inline ml-1" />
|
||||||
<MapContainer
|
ابحث أولاً ثم اضغط على النتيجة لتحريك الخريطة و تثبيت العلامة .
|
||||||
center={mapCenter}
|
<br />
|
||||||
zoom={mapZoom}
|
<MapPin className="w-4 h-4 inline ml-1" />
|
||||||
style={{ height: '100%', width: '100%' }}
|
اضغط على الخريطة لتحديد موقع العقار
|
||||||
className="z-0"
|
|
||||||
doubleClickZoom={false}
|
|
||||||
>
|
|
||||||
<TileLayer
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MapClickHandler onMapClick={handleMapClick} />
|
|
||||||
|
|
||||||
{selectedLocation && (
|
|
||||||
<Marker
|
|
||||||
position={[selectedLocation.lat, selectedLocation.lng]}
|
|
||||||
draggable={true}
|
|
||||||
eventHandlers={{
|
|
||||||
dragend: async (e) => {
|
|
||||||
const marker = e.target;
|
|
||||||
const position = marker.getLatLng();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${position.lat}&lon=${position.lng}&accept-language=ar`
|
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
setSelectedLocation({
|
|
||||||
lat: position.lat,
|
|
||||||
lng: position.lng,
|
|
||||||
address: data.display_name || 'موقع محدد'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setSelectedLocation({
|
|
||||||
lat: position.lat,
|
|
||||||
lng: position.lng,
|
|
||||||
address: 'موقع محدد'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Popup>
|
|
||||||
<div className="text-right p-2 max-w-xs">
|
|
||||||
<p className="font-bold text-sm mb-1">موقع العقار</p>
|
|
||||||
<p className="text-xs text-gray-600">{selectedLocation.address}</p>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
</Marker>
|
|
||||||
)}
|
|
||||||
</MapContainer>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-full h-96 rounded-xl overflow-hidden border-2 border-gray-200 mb-4">
|
||||||
|
<AddPropertyMap
|
||||||
|
mapCenter={mapCenter}
|
||||||
|
mapZoom={mapZoom}
|
||||||
|
selectedLocation={selectedLocation}
|
||||||
|
onMapClick={handleMapClick}
|
||||||
|
onMarkerDragEnd={handleMarkerDragEnd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{selectedLocation && !formData.lat && (
|
{selectedLocation && !formData.lat && (
|
||||||
<button
|
<button
|
||||||
onClick={confirmLocation}
|
onClick={confirmLocation}
|
||||||
@ -1438,10 +1524,16 @@ const handleMapClick = async (coords) => {
|
|||||||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
<p className="text-gray-600 font-medium">اضغط لرفع الصور</p>
|
<p className="text-gray-600 font-medium">اضغط لرفع الصور</p>
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
<p className="text-xs text-gray-500 mt-2">
|
||||||
JPEG, PNG, JPG • حتى 5MB • 800x600 بكسل • حد أقصى 10 صور
|
JPEG, PNG, JPG • حتى 5MB • 800x600 بكسل • 2-5 صور
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 mt-2 mb-4 text-center">
|
||||||
|
يرجى رفع عدة صور واضحة
|
||||||
|
<br />
|
||||||
|
(اضغط على الرفع أكثر من مرة)
|
||||||
|
</p>
|
||||||
|
|
||||||
{errors.images && (
|
{errors.images && (
|
||||||
<p className="text-red-500 text-sm text-center mt-2">{errors.images}</p>
|
<p className="text-red-500 text-sm text-center mt-2">{errors.images}</p>
|
||||||
)}
|
)}
|
||||||
@ -1472,6 +1564,32 @@ const handleMapClick = async (coords) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{step === 4 && (
|
||||||
|
<div className="border-t border-gray-200 pt-6 mt-8">
|
||||||
|
<div className="bg-gray-50 rounded-2xl p-5 border border-gray-200">
|
||||||
|
<p className="text-sm text-gray-700 leading-relaxed mb-4">
|
||||||
|
أنا صاحب العقار أقر أنني مالك العقار أو مخول قانونياً بعرضه للإيجار أو البيع،
|
||||||
|
وأن جميع المعلومات المدخلة صحيحة، وأتحمل كامل المسؤولية عن أي معلومات غير صحيحة.
|
||||||
|
</p>
|
||||||
|
<label className="flex items-start gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={agreeToTerms}
|
||||||
|
onChange={(e) => setAgreeToTerms(e.target.checked)}
|
||||||
|
className="w-5 h-5 mt-0.5 text-amber-500 rounded shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
أوافق على{' '}
|
||||||
|
<Link href="/terms" target="_blank" className="text-amber-600 underline hover:text-amber-700">
|
||||||
|
شروط الاستخدام
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -1499,7 +1617,7 @@ const handleMapClick = async (coords) => {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isLoading}
|
disabled={isLoading || !agreeToTerms}
|
||||||
className="flex-1 py-3 px-4 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
className="flex-1 py-3 px-4 bg-gradient-to-r from-amber-500 to-amber-600 text-white rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user