Added terms with api
Some checks failed
Build frontend / build (push) Failing after 1m30s

This commit is contained in:
Rahaf
2026-06-16 13:59:17 +03:00
parent 01ac4f8d6c
commit f892fc4a4d
2 changed files with 224 additions and 73 deletions

View File

@ -1,62 +1,143 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { FileText, Shield, CheckCircle } from 'lucide-react'; import { FileText, Shield, CheckCircle, Languages, Loader2, AlertCircle } from 'lucide-react';
import { getTerms } from '../utils/api'; import { getARTerms, getENTerms } from '../utils/api';
const staticTerms = [ const containerVariants = {
{ hidden: { opacity: 0 },
title: 'مقدمة', visible: {
content: opacity: 1,
'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة. تحتفظ المنصة بحق تعديل هذه الشروط في أي وقت مع إشعار المستخدمين.', transition: { staggerChildren: 0.08 },
}, },
{ };
title: 'استخدام المنصة',
content: const itemVariants = {
'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني أو مخالف للقوانين السارية. كما يلتزم المستخدم بعدم محاولة الوصول غير المصرح به إلى أي جزء من المنصة أو الخوادم أو الأنظمة المتصلة بها.', hidden: { opacity: 0, y: 24 },
}, visible: { opacity: 1, y: 0 },
{ };
title: 'حقوق ومسؤوليات المالك',
content: const FALLBACK_TERMS = {
'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر. يلتزم المالك بتحديث معلومات العقار بشكل دوري. المنصة غير مسؤولة عن أي نزاعات تنشأ بين المالك والمستأجر. يجب على المالك الالتزام بجميع القوانين المحلية المتعلقة بتأجير العقارات.', ar: [
}, {
{ title: 'مقدمة',
title: 'حقوق ومسؤوليات المستأجر', description:
content: 'مرحباً بك في منصة SweetHome. باستخدامك للمنصة، فإنك توافق على الالتزام بشروط الاستخدام هذه. إذا كنت لا توافق على أي جزء من هذه الشروط، يرجى عدم استخدام المنصة.',
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات. يجب على المستأجر الالتزام بقوانين المنزل ومواعيد تسجيل الوصول والمغادرة. المنصة غير مسؤولة عن أي سلوك غير لائق من قبل المستأجرين.', },
}, {
{ title: 'استخدام المنصة',
title: 'الدفع والعمولات', description:
content: 'يُسمح باستخدام المنصة للأغراض المشروعة فقط. يلتزم المستخدم بعدم استخدام المنصة في أي نشاط غير قانوني.',
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز. جميع المدفوعات تتم عبر قنوات الدفع الآمنة في المنصة. أي رسوم إلغاء أو استرداد تخضع لسياسة الإلغاء المحددة في كل عقار.', },
}, {
{ title: 'حقوق ومسؤوليات المالك',
title: 'خصوصية البيانات', description:
content: 'يتحمل المالك مسؤولية دقة المعلومات المقدمة عن العقار بما في ذلك الصور والوصف والسعر والتوفر.',
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا. نحن لا نشارك معلوماتك مع أطراف ثالثة دون موافقتك، إلا عندما يقتضي القانون ذلك.', },
}, {
]; title: 'حقوق ومسؤوليات المستأجر',
description:
'يلتزم المستأجر باستخدام العقار بطريقة مسؤولة وعدم التسبب في أي ضرر للممتلكات.',
},
{
title: 'الدفع والعمولات',
description:
'تتقاضى المنصة عمولة على كل حصة ناجحة وفقاً للنسبة المحددة في وقت الحجز.',
},
{
title: 'خصوصية البيانات',
description:
'نحن نأخذ خصوصية بياناتك على محمل الجد. يتم جمع واستخدام البيانات الشخصية وفقاً لسياسة الخصوصية الخاصة بنا.',
},
],
en: [
{
title: 'Introduction',
description:
'Welcome to SweetHome. By using our platform, you agree to comply with these terms. If you do not agree, please do not use the platform.',
},
{
title: 'Platform Usage',
description:
'The platform may only be used for lawful purposes. Users must not engage in any illegal activity.',
},
{
title: 'Owner Rights & Responsibilities',
description:
'Owners are responsible for the accuracy of property information including images, description, price, and availability.',
},
{
title: 'Tenant Rights & Responsibilities',
description:
'Tenants must use the property responsibly and not cause any damage to the property.',
},
{
title: 'Payment & Commissions',
description:
'The platform charges a commission on each successful booking according to the rate specified at the time of booking.',
},
{
title: 'Data Privacy',
description:
'We take your data privacy seriously. Personal data is collected and used in accordance with our Privacy Policy.',
},
],
};
export default function TermsPage() { export default function TermsPage() {
const [terms, setTerms] = useState(staticTerms); const [terms, setTerms] = useState([]);
const [language, setLanguage] = useState('ar');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => { useEffect(() => {
async function fetchTerms() { const controller = new AbortController();
const fetchTerms = async () => {
try { try {
const data = await getTerms(); setLoading(true);
if (data && Array.isArray(data) && data.length > 0) { setError('');
setTerms(data);
const fetcher = language === 'ar' ? getARTerms : getENTerms;
const data = await fetcher();
if (!data) {
setTerms(FALLBACK_TERMS[language]);
return;
} }
const raw = Array.isArray(data) ? data : data.terms || data.items || data.data || [];
if (!Array.isArray(raw) || raw.length === 0) {
setTerms(FALLBACK_TERMS[language]);
return;
}
const mapped = raw.map((item) => ({
title: item.title || item.name || '',
description: item.description || item.content || item.body || item.text || '',
}));
setTerms(mapped);
} catch { } catch {
// fall back to static terms setTerms(FALLBACK_TERMS[language]);
setError('');
} finally {
setLoading(false);
} }
} };
fetchTerms(); fetchTerms();
}, []);
return () => controller.abort();
}, [language]);
return ( return (
<div className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12" dir="rtl"> <div
dir={language === 'ar' ? 'rtl' : 'ltr'}
className="min-h-screen bg-gradient-to-b from-amber-50/50 to-white py-12"
>
<div className="container mx-auto px-4 max-w-4xl"> <div className="container mx-auto px-4 max-w-4xl">
<motion.div <motion.div
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
@ -66,33 +147,95 @@ export default function TermsPage() {
<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"> <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">
<FileText className="w-10 h-10 text-amber-600" /> <FileText className="w-10 h-10 text-amber-600" />
</div> </div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">شروط الاستخدام</h1>
<div className="flex items-center justify-center gap-4 mb-4">
<h1 className="text-4xl font-bold text-gray-900">
{language === 'ar' ? 'شروط الاستخدام' : 'Terms of Use'}
</h1>
<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>
<p className="text-lg text-gray-600 max-w-2xl mx-auto"> <p className="text-lg text-gray-600 max-w-2xl mx-auto">
يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة {language === 'ar'
? 'يرجى قراءة شروط الاستخدام التالية بعناية قبل استخدام المنصة'
: 'Please read the following terms of use carefully before using the platform'}
</p> </p>
</motion.div> </motion.div>
<div className="space-y-6"> {loading && (
{terms.map((term, index) => ( <div className="flex items-center justify-center gap-3 mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-amber-800">
<motion.div <Loader2 className="h-5 w-5 animate-spin" />
key={index} <span>
initial={{ opacity: 0, y: 20 }} {language === 'ar'
animate={{ opacity: 1, y: 0 }} ? 'جاري تحميل شروط الاستخدام...'
transition={{ delay: index * 0.1 }} : 'Loading terms of use...'}
className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow" </span>
> </div>
<div className="flex items-start gap-4"> )}
<div className="w-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
<Shield className="w-5 h-5 text-amber-600" /> {error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 rounded-2xl border border-red-200 bg-red-50 p-4 flex items-center gap-3 text-red-700"
>
<AlertCircle className="h-5 w-5 shrink-0" />
<span>{error}</span>
</motion.div>
)}
{!loading && terms.length === 0 && !error && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-16 text-gray-500"
>
<FileText className="h-16 w-16 mx-auto mb-4 text-gray-300" />
<p className="text-xl font-medium">
{language === 'ar'
? 'لا توجد شروط استخدام متاحة حالياً'
: 'No terms of use available'}
</p>
</motion.div>
)}
{terms.length > 0 && (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-6"
>
{terms.map((term, index) => (
<motion.div
key={index}
variants={itemVariants}
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-10 h-10 bg-amber-100 rounded-xl flex items-center justify-center shrink-0 mt-1">
<Shield className="w-5 h-5 text-amber-600" />
</div>
<div className="min-w-0 flex-1">
{term.title && (
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2>
)}
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">
{term.description}
</p>
</div>
</div> </div>
<div> </motion.div>
<h2 className="text-xl font-bold text-gray-900 mb-3">{term.title}</h2> ))}
<p className="text-gray-600 leading-relaxed">{term.content}</p> </motion.div>
</div> )}
</div>
</motion.div>
))}
</div>
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@ -102,9 +245,13 @@ export default function TermsPage() {
> >
<CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" /> <CheckCircle className="w-6 h-6 text-amber-600 shrink-0 mt-0.5" />
<div> <div>
<p className="font-bold text-amber-800 mb-1">آخر تحديث</p> <p className="font-bold text-amber-800 mb-1">
{language === 'ar' ? 'آخر تحديث' : 'Last Updated'}
</p>
<p className="text-amber-700"> <p className="text-amber-700">
تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات. {language === 'ar'
? 'تم آخر تحديث لشروط الاستخدام في 1 مايو 2026. يرجى مراجعة هذه الصفحة بشكل دوري للاطلاع على أي تغييرات.'
: 'Last updated on May 1, 2026. Please review this page periodically for any changes.'}
</p> </p>
</div> </div>
</motion.div> </motion.div>

View File

@ -704,8 +704,12 @@ export async function bookReservation(propertyInfoId, startDate, endDate) {
// ─── Terms ─── // ─── Terms ───
export async function getTerms() { export async function getARTerms() {
return apiFetch('/Terms/GetTerms'); return apiFetch('/Configuration/GetARTerms');
}
export async function getENTerms() {
return apiFetch('/Configuration/GetENTerms');
} }
// ─── Profile ─── // ─── Profile ───
@ -1151,11 +1155,11 @@ export async function updateSaleReport(id, data) {
}); });
} }
// ─── Terms (Add) ─── // ─── Terms (Add or Update) ───
export async function addTerm(name, description) { export async function addOrUpdateTerms(terms) {
return apiFetch('/Terms', { return apiFetch('/Terms/AddOrUpdateTerms', {
method: 'POST', method: 'POST',
body: { name, description }, body: terms,
}); });
} }