Compare commits

...

6 Commits

Author SHA1 Message Date
bef133ad5b Fixing the map problem
Some checks failed
Build frontend / build (push) Failing after 1m1s
2026-06-14 18:58:06 +03:00
a9eb1cc684 Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m18s
2026-06-14 08:33:02 -07:00
13b563e35e fixing SendGeneralReport 2026-06-14 08:22:59 -07:00
5d593d593f Merge branch 'main' of http://45.93.137.91:3000/Rahaf/SweetHome
Some checks failed
Build frontend / build (push) Failing after 1m30s
2026-06-14 18:04:26 +03:00
51850b85c2 Added blocked page with api 2026-06-14 18:04:05 +03:00
8cacf464d1 privacy screen api
Some checks failed
Build frontend / build (push) Failing after 1m27s
2026-06-14 07:45:13 -07:00
5 changed files with 531 additions and 125 deletions

View File

@ -126,6 +126,7 @@ export default function ClientLayout({ children }) {
const isAuthPage = [ const isAuthPage = [
"/login", "/login",
"/blocked",
"/register", "/register",
"/forgot-password", "/forgot-password",
"/auth/choose-role", "/auth/choose-role",

166
app/blocked/page.js Normal file
View File

@ -0,0 +1,166 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
import { ShieldAlert, LogOut, MessageSquare, Send, Loader2 } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast';
import AuthService from '../services/AuthService';
import { sendGeneralReport } from '../utils/api';
export default function BlockedPage() {
const router = useRouter();
const [form, setForm] = useState({ subject: '', body: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSent, setIsSent] = useState(false);
const handleLogout = () => {
AuthService.deleteToken();
router.replace('/');
};
const updateField = (field, value) => {
setForm((current) => ({ ...current, [field]: value }));
if (isSent) setIsSent(false);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!form.subject.trim() || !form.body.trim()) {
toast.error('يرجى تعبئة الموضوع والرسالة');
return;
}
setIsSubmitting(true);
try {
await sendGeneralReport(form.subject.trim(), form.body.trim());
setIsSent(true);
setForm({ subject: '', body: '' });
toast.success('تم إرسال طلب الدعم بنجاح');
} catch (error) {
toast.error('حدث خطأ أثناء إرسال طلب الدعم. حاول مرة أخرى');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-b from-red-50 via-white to-amber-50 flex items-center justify-center p-4" dir="rtl">
<Toaster position="top-center" reverseOrder={false} />
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
className="w-full max-w-5xl"
>
<div className="text-center mb-10">
<motion.div
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
className="w-24 h-24 bg-red-100 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-red-100"
>
<ShieldAlert className="w-12 h-12 text-red-600" />
</motion.div>
<h1 className="text-4xl font-bold text-gray-900 mb-3">الحساب محظور</h1>
<p className="text-gray-600 text-lg max-w-2xl mx-auto">
تم تقييد وصولك إلى التطبيق. يمكنك تسجيل الخروج أو مراسلة دعم العملاء للمساعدة في حل المشكلة.
</p>
</div>
<div className="grid md:grid-cols-2 gap-6">
<motion.div
initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8 flex flex-col justify-between"
>
<div>
<div className="w-14 h-14 bg-red-50 rounded-2xl flex items-center justify-center mb-6">
<LogOut className="w-7 h-7 text-red-600" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">تسجيل الخروج</h2>
<p className="text-gray-600 leading-7">
إنهاء الجلسة الحالية وإزالة بيانات الدخول من هذا الجهاز.
</p>
</div>
<button
type="button"
onClick={handleLogout}
className="mt-8 w-full bg-red-600 hover:bg-red-700 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3"
>
<LogOut className="w-5 h-5" />
تسجيل الخروج
</button>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="bg-white rounded-3xl shadow-sm border border-gray-200 p-8"
>
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 bg-amber-50 rounded-2xl flex items-center justify-center">
<MessageSquare className="w-7 h-7 text-amber-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">مراسلة دعم العملاء</h2>
<p className="text-gray-600 mt-1">أرسل تفاصيل المشكلة وسنقوم بمراجعتها.</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">الموضوع</label>
<input
type="text"
value={form.subject}
onChange={(event) => updateField('subject', event.target.value)}
placeholder="اكتب موضوع الرسالة"
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-2">الرسالة</label>
<textarea
value={form.body}
onChange={(event) => updateField('body', event.target.value)}
rows={6}
placeholder="اشرح المشكلة بالتفصيل"
className="w-full px-4 py-3 bg-white border border-gray-200 rounded-2xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-gray-900 placeholder-gray-400 resize-none"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-amber-500 hover:bg-amber-600 text-white rounded-2xl py-4 font-bold transition-colors flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
جاري الإرسال...
</>
) : isSent ? (
<>
<Send className="w-5 h-5" />
تم الإرسال
</>
) : (
<>
<Send className="w-5 h-5" />
إرسال الرسالة
</>
)}
</button>
</form>
</motion.div>
</div>
</motion.div>
</div>
);
}

View File

@ -1,105 +1,217 @@
// 'use client';
// import { motion } from 'framer-motion';
// import { Shield, Lock, Eye, Database, RefreshCw, Trash2, CheckCircle } from 'lucide-react';
// const sections = [
// {
// title: 'المقدمة',
// icon: Shield,
// content:
// 'نحن في SweetHome نلتزم بحماية خصوصية مستخدمينا. توضح سياسة الخصوصية هذه كيفية جمع واستخدام وحماية المعلومات الشخصية التي تقدمها عند استخدام منصتنا. باستخدامك للمنصة، فإنك توافق على الممارسات الموضحة في هذه السياسة.',
// },
// {
// title: 'المعلومات التي نجمعها',
// icon: Database,
// content:
// 'نجمع المعلومات التي تقدمها مباشرة عند إنشاء حساب، مثل الاسم، البريد الإلكتروني، رقم الهاتف، ومعلومات الدفع. كما نجمع معلومات حول استخدامك للمنصة، مثل العقارات التي تتصفحها، الحجوزات التي تقوم بها، وتقييماتك. قد نجمع أيضاً معلومات تقنية مثل عنوان IP ونوع المتصفح.',
// },
// {
// title: 'كيف نستخدم معلوماتك',
// icon: Eye,
// content:
// 'نستخدم معلوماتك لتقديم وتحسين خدماتنا، ومعالجة الحجوزات والمدفوعات، والتواصل معك بشأن حساباتك وحجوزاتك، وإرسال التحديثات والعروض الترويجية (بموافقتك)، وتحسين تجربة المستخدم وتطوير ميزات جديدة، والامتثال للالتزامات القانونية.',
// },
// {
// title: 'حماية البيانات وأمانها',
// icon: Lock,
// content:
// 'نحن نتخذ إجراءات أمنية مناسبة لحماية بياناتك الشخصية من الوصول غير المصرح به أو التعديل أو الإفصاح أو الإتلاف. تشمل هذه الإجراءات التشفير، وجدران الحماية، وضوابط الوصول الصارمة. ومع ذلك، لا يمكن ضمان أمان مطلق لنقل البيانات عبر الإنترنت.',
// },
// {
// title: 'مشاركة البيانات مع أطراف ثالثة',
// icon: RefreshCw,
// content:
// 'لا نبيع أو نشارك معلوماتك الشخصية مع أطراف ثالثة لأغراض تسويقية دون موافقتك الصريحة. قد نشارك معلوماتك مع مزودي الخدمة الذين يساعدوننا في تشغيل المنصة (مثل معالجة الدفعات)، مع الالتزام باتفاقيات سرية صارمة. قد نكشف عن معلوماتك إذا كان ذلك مطلوباً بموجب القانون.',
// },
// {
// title: 'حقوقك وخياراتك',
// icon: Trash2,
// content:
// 'لديك الحق في الوصول إلى بياناتك الشخصية وتحديثها أو تصحيحها أو حذفها في أي وقت. يمكنك إدارة تفضيلات الاتصال من إعدادات حسابك. يمكنك طلب حذف حسابك وجميع بياناتك المرتبطة به من خلال التواصل مع فريق الدعم. سنستجيب لطلباتك في أقرب وقت ممكن وفقاً للقوانين المعمول بها.',
// },
// ];
// export default function PrivacyPage() {
// return (
// <div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl">
// <div className="container mx-auto px-4 max-w-4xl">
// <motion.div
// initial={{ opacity: 0, y: -20 }}
// animate={{ opacity: 1, y: 0 }}
// className="text-center mb-12"
// >
// <div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
// <Shield className="w-10 h-10 text-amber-600" />
// </div>
// <h1 className="text-4xl font-bold text-gray-900 mb-4">سياسة الخصوصية</h1>
// <p className="text-lg text-gray-600 max-w-2xl mx-auto">
// نلتزم بحماية خصوصيتك وأمان بياناتك الشخصية
// </p>
// </motion.div>
// <div className="space-y-6">
// {sections.map((section, index) => {
// const Icon = section.icon;
// return (
// <motion.div
// key={index}
// initial={{ opacity: 0, y: 20 }}
// animate={{ opacity: 1, y: 0 }}
// transition={{ delay: index * 0.1 }}
// className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
// >
// <div className="flex items-start gap-4">
// <div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center shrink-0">
// <Icon className="w-6 h-6 text-amber-600" />
// </div>
// <div>
// <h2 className="text-xl font-bold text-gray-900 mb-3">{section.title}</h2>
// <p className="text-gray-600 leading-relaxed">{section.content}</p>
// </div>
// </div>
// </motion.div>
// );
// })}
// </div>
// <motion.div
// initial={{ opacity: 0 }}
// animate={{ opacity: 1 }}
// transition={{ delay: 0.6 }}
// className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
// >
// <CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
// <div>
// <p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
// <p className="text-amber-700">
// تم آخر تحديث لسياسة الخصوصية في 1 مايو 2026. للمزيد من المعلومات أو الاستفسارات، يرجى التواصل مع فريق الدعم.
// </p>
// </div>
// </motion.div>
// </div>
// </div>
// );
// }
'use client'; 'use client';
import { motion } from 'framer-motion'; import { useEffect, useState } from 'react';
import { Shield, Lock, Eye, Database, RefreshCw, Trash2, CheckCircle } from 'lucide-react'; import { Languages, Loader2, Shield } from 'lucide-react';
const sections = [ const API_BASE = 'http://45.93.137.91/api';
{
title: 'المقدمة', const ENDPOINTS = {
icon: Shield, ar: '/Configuration/GetARPrivacyPolicy',
content: en: '/Configuration/GetENPrivacyPolicy',
'نحن في SweetHome نلتزم بحماية خصوصية مستخدمينا. توضح سياسة الخصوصية هذه كيفية جمع واستخدام وحماية المعلومات الشخصية التي تقدمها عند استخدام منصتنا. باستخدامك للمنصة، فإنك توافق على الممارسات الموضحة في هذه السياسة.', };
},
{
title: 'المعلومات التي نجمعها',
icon: Database,
content:
'نجمع المعلومات التي تقدمها مباشرة عند إنشاء حساب، مثل الاسم، البريد الإلكتروني، رقم الهاتف، ومعلومات الدفع. كما نجمع معلومات حول استخدامك للمنصة، مثل العقارات التي تتصفحها، الحجوزات التي تقوم بها، وتقييماتك. قد نجمع أيضاً معلومات تقنية مثل عنوان IP ونوع المتصفح.',
},
{
title: 'كيف نستخدم معلوماتك',
icon: Eye,
content:
'نستخدم معلوماتك لتقديم وتحسين خدماتنا، ومعالجة الحجوزات والمدفوعات، والتواصل معك بشأن حساباتك وحجوزاتك، وإرسال التحديثات والعروض الترويجية (بموافقتك)، وتحسين تجربة المستخدم وتطوير ميزات جديدة، والامتثال للالتزامات القانونية.',
},
{
title: 'حماية البيانات وأمانها',
icon: Lock,
content:
'نحن نتخذ إجراءات أمنية مناسبة لحماية بياناتك الشخصية من الوصول غير المصرح به أو التعديل أو الإفصاح أو الإتلاف. تشمل هذه الإجراءات التشفير، وجدران الحماية، وضوابط الوصول الصارمة. ومع ذلك، لا يمكن ضمان أمان مطلق لنقل البيانات عبر الإنترنت.',
},
{
title: 'مشاركة البيانات مع أطراف ثالثة',
icon: RefreshCw,
content:
'لا نبيع أو نشارك معلوماتك الشخصية مع أطراف ثالثة لأغراض تسويقية دون موافقتك الصريحة. قد نشارك معلوماتك مع مزودي الخدمة الذين يساعدوننا في تشغيل المنصة (مثل معالجة الدفعات)، مع الالتزام باتفاقيات سرية صارمة. قد نكشف عن معلوماتك إذا كان ذلك مطلوباً بموجب القانون.',
},
{
title: 'حقوقك وخياراتك',
icon: Trash2,
content:
'لديك الحق في الوصول إلى بياناتك الشخصية وتحديثها أو تصحيحها أو حذفها في أي وقت. يمكنك إدارة تفضيلات الاتصال من إعدادات حسابك. يمكنك طلب حذف حسابك وجميع بياناتك المرتبطة به من خلال التواصل مع فريق الدعم. سنستجيب لطلباتك في أقرب وقت ممكن وفقاً للقوانين المعمول بها.',
},
];
export default function PrivacyPage() { export default function PrivacyPage() {
return ( const [language, setLanguage] = useState('ar');
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl"> const [policyText, setPolicyText] = useState('');
<div className="container mx-auto px-4 max-w-4xl"> const [loading, setLoading] = useState(false);
<motion.div const [error, setError] = useState('');
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-12"
>
<div className="w-20 h-20 bg-amber-100 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg shadow-amber-100">
<Shield className="w-10 h-10 text-amber-600" />
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">سياسة الخصوصية</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
نلتزم بحماية خصوصيتك وأمان بياناتك الشخصية
</p>
</motion.div>
<div className="space-y-6"> useEffect(() => {
{sections.map((section, index) => { const controller = new AbortController();
const Icon = section.icon;
return ( const loadPolicy = async () => {
<motion.div try {
key={index} setLoading(true);
initial={{ opacity: 0, y: 20 }} setError('');
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }} const response = await fetch(`${API_BASE}${ENDPOINTS[language]}`, {
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow" method: 'GET',
> cache: 'no-store',
<div className="flex items-start gap-4"> signal: controller.signal,
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center shrink-0"> headers: {
<Icon className="w-6 h-6 text-amber-600" /> Accept: 'text/plain',
</div> },
<div> });
<h2 className="text-xl font-bold text-gray-900 mb-3">{section.title}</h2>
<p className="text-gray-600 leading-relaxed">{section.content}</p> if (!response.ok) {
</div> throw new Error(`HTTP ${response.status}`);
</div> }
</motion.div>
); const text = await response.text();
})}
console.log('API RESPONSE:', text);
setPolicyText(text.trim());
} catch (err) {
if (err.name !== 'AbortError') {
setPolicyText('');
setError(
language === 'ar'
? 'تعذر تحميل النص من الخادم.'
: 'Failed to load text from the server.'
);
}
} finally {
setLoading(false);
}
};
loadPolicy();
return () => controller.abort();
}, [language]);
return (
<div
dir={language === 'ar' ? 'rtl' : 'ltr'}
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
>
<div className="mx-auto max-w-3xl px-4">
<div className="mb-8 flex items-center justify-between">
<div className="flex items-center gap-2 text-gray-900">
<Shield className="h-6 w-6 text-amber-600" />
<span className="text-lg font-bold">
{language === 'ar' ? 'سياسة الخصوصية' : 'Privacy Policy'}
</span>
</div>
<button
type="button"
onClick={() => setLanguage(language === 'ar' ? 'en' : 'ar')}
className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-4 py-2 text-sm font-semibold text-gray-700 shadow-sm transition hover:shadow-md"
>
<Languages className="h-4 w-4" />
{language === 'ar' ? 'English' : 'العربية'}
</button>
</div> </div>
<motion.div {loading && (
initial={{ opacity: 0 }} <div className="mb-6 flex items-center gap-3 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-amber-800">
animate={{ opacity: 1 }} <Loader2 className="h-5 w-5 animate-spin" />
transition={{ delay: 0.6 }} <span>{language === 'ar' ? 'جاري التحميل...' : 'Loading...'}</span>
className="mt-8 bg-amber-50 rounded-2xl border border-amber-200 p-6 flex items-start gap-4"
>
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
<div>
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p>
<p className="text-amber-700">
تم آخر تحديث لسياسة الخصوصية في 1 مايو 2026. للمزيد من المعلومات أو الاستفسارات، يرجى التواصل مع فريق الدعم.
</p>
</div> </div>
</motion.div> )}
{error && (
<div className="mb-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-red-700">
{error}
</div>
)}
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="whitespace-pre-wrap leading-8 text-gray-800">
{policyText}
</div>
</div>
</div> </div>
</div> </div>
); );
} }

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import toast, { Toaster } from "react-hot-toast"; import toast, { Toaster } from "react-hot-toast";
import Link from "next/link"; import Link from "next/link";
@ -69,23 +69,70 @@ import { useFavorites } from "@/app/contexts/FavoritesContext";
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../../enums"; import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../../enums";
import PropertyRatingList from "@/app/components/ratings/PropertyRatingList"; import PropertyRatingList from "@/app/components/ratings/PropertyRatingList";
import { getPropertyAverageRating } from "../../utils/ratings"; import { getPropertyAverageRating } from "../../utils/ratings";
import dynamic from "next/dynamic";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
const MapContainer = dynamic( function PropertyDetailMap({ lat, lng, title }) {
() => import("react-leaflet").then((m) => m.MapContainer), const mapRef = useRef(null);
{ ssr: false }, const mapInstanceRef = useRef(null);
); const markerRef = useRef(null);
const TileLayer = dynamic(
() => import("react-leaflet").then((m) => m.TileLayer), useEffect(() => {
{ ssr: false }, if (!mapRef.current || mapInstanceRef.current) return;
);
const Marker = dynamic(() => import("react-leaflet").then((m) => m.Marker), { if (mapRef.current._leaflet_id && !mapInstanceRef.current) {
ssr: false, delete mapRef.current._leaflet_id;
}); }
const Popup = dynamic(() => import("react-leaflet").then((m) => m.Popup), {
ssr: false, 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",
});
const map = L.map(mapRef.current, {
center: [lat, lng],
zoom: 14,
scrollWheelZoom: false,
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
}).addTo(map);
const marker = L.marker([lat, lng]).addTo(map).bindPopup(title);
mapInstanceRef.current = map;
markerRef.current = marker;
map.invalidateSize();
return () => {
markerRef.current?.remove();
markerRef.current = null;
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, [lat, lng, title]);
useEffect(() => {
if (mapInstanceRef.current) {
mapInstanceRef.current.setView([lat, lng], 14);
markerRef.current?.setLatLng([lat, lng]);
markerRef.current?.setPopupContent(title);
mapInstanceRef.current.invalidateSize();
}
}, [lat, lng, title]);
return <div ref={mapRef} className="h-full w-full" />;
}
function formatCurrency(amount) { function formatCurrency(amount) {
if (!amount || isNaN(amount)) return "0"; if (!amount || isNaN(amount)) return "0";
@ -1243,19 +1290,11 @@ export default function PropertyDetailsPage() {
className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-200" className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-200"
> >
<div className="h-64"> <div className="h-64">
<MapContainer <PropertyDetailMap
center={[property.location.lat, property.location.lng]} lat={property.location.lat}
zoom={14} lng={property.location.lng}
className="h-full w-full" title={property.title}
scrollWheelZoom={false} />
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker
position={[property.location.lat, property.location.lng]}
>
<Popup>{property.title}</Popup>
</Marker>
</MapContainer>
</div> </div>
<div className="p-3 bg-amber-50 text-center text-sm text-amber-700 flex items-center justify-center gap-2"> <div className="p-3 bg-amber-50 text-center text-sm text-amber-700 flex items-center justify-center gap-2">
<Info className="w-4 h-4" /> <Info className="w-4 h-4" />

View File

@ -449,14 +449,48 @@
// }); // });
// } // }
import AuthService from '../services/AuthService';
import AuthService from '../services/AuthService';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
const REPORT_API_BASE = process.env.NEXT_PUBLIC_REPORT_API_URL || 'http://45.93.137.91/api';
function isFormData(value) { function isFormData(value) {
return typeof FormData !== 'undefined' && value instanceof FormData; return typeof FormData !== 'undefined' && value instanceof FormData;
} }
class ApiBlockedError extends Error {
constructor(message = 'Your account is blocked') {
super(message);
this.name = 'ApiBlockedError';
this.status = 451;
}
}
export function isApiBlockedError(error) {
return error instanceof ApiBlockedError || error?.status === 451;
}
function redirectToBlockedPage() {
if (typeof window !== 'undefined' && window.location.pathname !== '/blocked') {
window.location.replace('/blocked');
}
}
function assertNotBlocked(response) {
if (response.status === 451) {
redirectToBlockedPage();
throw new ApiBlockedError();
}
}
function buildApiUrl(base, endpoint) {
return `${base.replace(/\/$/, '')}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
}
/** /**
* Generic API fetch — attaches auth token, unwraps { data } envelope * Generic API fetch — attaches auth token, unwraps { data } envelope
*/ */
@ -475,7 +509,13 @@ async function apiFetch(endpoint, options = {}) {
headers['Content-Type'] = 'application/json'; headers['Content-Type'] = 'application/json';
} }
const res = await fetch(`${API_BASE}${endpoint}`, { const url = `${API_BASE}${endpoint}`;
console.log('API Request:', url);
console.log('API Method:', options.method || 'GET');
console.log('API Body:', hasBody ? options.body : null);
const res = await fetch(url, {
...options, ...options,
headers, headers,
body: body:
@ -484,8 +524,13 @@ async function apiFetch(endpoint, options = {}) {
: options.body, : options.body,
}); });
console.log('API Response Status:', res.status);
console.log('API Response OK:', res.ok);
assertNotBlocked(res);
if (!res.ok && res.status !== 206) { if (!res.ok && res.status !== 206) {
const text = await res.text().catch(() => ''); const text = await res.text().catch(() => '');
console.error('API Error Response:', text || res.statusText);
throw new Error(`API ${res.status}: ${text || res.statusText}`); throw new Error(`API ${res.status}: ${text || res.statusText}`);
} }
@ -524,6 +569,36 @@ async function authFetch(endpoint, body, token = null) {
body: bodyIsFormData ? body : JSON.stringify(body), body: bodyIsFormData ? body : JSON.stringify(body),
}); });
assertNotBlocked(res);
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
if (data && typeof data === 'object' && 'data' in data) {
data = data.data;
}
} catch {
data = text;
}
const message = typeof data === 'object' && data?.message ? data.message : null;
return { status: res.status, data, ok: res.ok || res.status === 206, message };
}
async function reportFetch(endpoint, body) {
const res = await fetch(buildApiUrl(REPORT_API_BASE, endpoint), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
assertNotBlocked(res);
const text = await res.text(); const text = await res.text();
let data = null; let data = null;
@ -721,6 +796,8 @@ export async function uploadPicture(file) {
body: formData, body: formData,
}); });
assertNotBlocked(res);
const text = await res.text(); const text = await res.text();
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${text}`); if (!res.ok) throw new Error(`Upload failed: ${res.status} ${text}`);
@ -746,6 +823,8 @@ async function multipartAuthFetch(endpoint, formData) {
body: formData, body: formData,
}); });
assertNotBlocked(res);
const text = await res.text(); const text = await res.text();
let data = null; let data = null;
@ -948,6 +1027,8 @@ export async function registerRealEstateAgent(formData) {
body: formData, body: formData,
}); });
assertNotBlocked(res);
const text = await res.text(); const text = await res.text();
let data = null; let data = null;
@ -1028,8 +1109,15 @@ export async function filterRentProperties(params = {}) {
// ─── Reports ─── // ─── Reports ───
export async function sendGeneralReport(subject, reportBody) {
return reportFetch('/Reports/SendGeneralReport', {
subject,
body: reportBody,
});
}
export async function submitReport(subject, body) { export async function submitReport(subject, body) {
return apiFetch('/Reports', { return apiFetch('/Reports/SendGeneralReport', {
method: 'POST', method: 'POST',
body: { subject, body }, body: { subject, body },
}); });