import { fetchRecommendations, fetchChatResponse } from './api.js';
let mapInstance = null;
// ── Toast notification (replaces alert()) ──
function showToast(msg, type = 'info') {
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = `
position:fixed; bottom:24px; left:50%; transform:translateX(-50%);
background:rgba(10,16,32,0.95); border:1px solid rgba(212,175,55,0.4);
color:#f4f1ea; font-family:'Tajawal',sans-serif; font-size:14px;
padding:12px 24px; border-radius:12px; z-index:9999;
box-shadow:0 8px 24px rgba(0,0,0,0.5); pointer-events:none;
animation:fadeInUp 0.3s ease;
`;
if (type === 'error') t.style.borderColor = 'rgba(255,75,75,0.5)';
document.body.appendChild(t);
setTimeout(() => t.remove(), 3500);
}
// ── Sanitize text before inserting into DOM ──
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// ── Append a chat bubble safely ──
function appendMsg(container, role, html) {
const isBot = role === 'bot';
container.insertAdjacentHTML('beforeend', `
`);
container.scrollTop = container.scrollHeight;
}
document.addEventListener('DOMContentLoaded', () => {
// ==========================================
// 3D Background Initialization
// ==========================================
const init3DBackground = () => {
const canvas = document.getElementById('bg-canvas');
if (!canvas || typeof THREE === 'undefined') return;
const scene = new THREE.Scene();
// Fog to blend particles smoothly into the dark background
scene.fog = new THREE.FogExp2(0x05070d, 0.0015);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 400;
const renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// Create Particles for Network
const particleCount = 120;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const velocities = [];
for(let i=0; i {
mouseX = (event.clientX - window.innerWidth / 2) * 0.1;
mouseY = (event.clientY - window.innerHeight / 2) * 0.1;
});
// Animation Loop
const animate = () => {
requestAnimationFrame(animate);
// Move particles
const positionsAttr = geometry.attributes.position.array;
let lineIndex = 0;
for(let i=0; i 400) velocities[i].x *= -1;
if (Math.abs(positionsAttr[i*3+1]) > 400) velocities[i].y *= -1;
if (Math.abs(positionsAttr[i*3+2]) > 400) velocities[i].z *= -1;
// Draw lines to close neighbors
for(let j=i+1; j {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
};
init3DBackground();
// 1. Intersection Observer for Reveal Animations
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.reveal').forEach(el => revealObserver.observe(el));
// Interactive Heart Buttons Helper
const bindHeartButtons = () => {
document.querySelectorAll('.heart-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const targetBtn = e.currentTarget;
targetBtn.classList.toggle('active');
const icon = targetBtn.querySelector('i');
if (targetBtn.classList.contains('active')) {
icon.classList.remove('fa-regular');
icon.classList.add('fa-solid');
} else {
icon.classList.remove('fa-solid');
icon.classList.add('fa-regular');
}
});
});
};
// ==========================================
// Hero Search Bar Logic
// ==========================================
const searchBtn = document.querySelector('.search-btn');
const searchInput = document.getElementById('searchInput');
const micBtn = document.querySelector('.mic-btn');
// Chip quick-search
document.querySelectorAll('.chip[data-q]').forEach(chip => {
chip.addEventListener('click', () => executeSearch(chip.dataset.q));
});
// Clear chat
const clearChatBtn = document.getElementById('clearChatBtn');
if (clearChatBtn) {
clearChatBtn.addEventListener('click', () => {
const history = document.getElementById('inline-chat-history');
if (history) {
history.innerHTML = `
مرحباً! أنا مستشارك العقاري الذكي. كيف يمكنني مساعدتك اليوم؟
`;
inlineChatContext = [];
}
});
}
const executeSearch = async (queryText) => {
if (!queryText) return;
const inlineInput = document.getElementById('inline-chat-input');
const inlineSendBtn = document.getElementById('inline-send-btn');
if (inlineInput && inlineSendBtn) {
inlineInput.value = queryText;
inlineSendBtn.click();
}
// Fix #2: clear the top search bar after dispatching
if (searchInput) searchInput.value = '';
};
if (searchBtn && searchInput) {
searchBtn.addEventListener('click', () => {
executeSearch(searchInput.value.trim());
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
executeSearch(searchInput.value.trim());
}
});
}
if (micBtn && searchInput && searchBtn) {
micBtn.addEventListener('click', () => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SpeechRecognition) {
const recognition = new SpeechRecognition();
recognition.lang = 'ar-EG';
recognition.onstart = () => {
micBtn.style.color = 'var(--gold-bright)';
searchInput.placeholder = 'جاري الاستماع...';
};
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
searchInput.value = transcript;
executeSearch(transcript);
};
recognition.onend = () => {
micBtn.style.color = '';
searchInput.placeholder = 'اسأل عن عقار، منطقة، استثمار...';
};
recognition.start();
} else {
showToast('عذراً، متصفحك لا يدعم البحث الصوتي', 'error');
}
});
}
// ==========================================
// Inline Chat Logic
// ==========================================
const inlineChatContainer = document.getElementById('inline-chat-container');
const inlineInput = document.getElementById('inline-chat-input');
const inlineSendBtn = document.getElementById('inline-send-btn');
const inlineChatHistory = document.getElementById('inline-chat-history');
let inlineChatContext = [];
// Chat mic button
const chatMicBtn = document.getElementById('chatMicBtn');
if (chatMicBtn && inlineInput) {
chatMicBtn.addEventListener('click', () => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) { showToast('متصفحك لا يدعم الإدخال الصوتي', 'error'); return; }
const recognition = new SpeechRecognition();
recognition.lang = 'ar-EG';
recognition.onstart = () => { chatMicBtn.style.color = 'var(--gold-bright)'; chatMicBtn.querySelector('i').className = 'fa-solid fa-circle-stop'; };
recognition.onresult = (e) => { inlineInput.value = e.results[0][0].transcript; inlineSendBtn.click(); };
recognition.onend = () => { chatMicBtn.style.color = ''; chatMicBtn.querySelector('i').className = 'fa-solid fa-microphone'; };
recognition.start();
});
}
if (inlineChatContainer) {
// Send Message
if (inlineSendBtn && inlineInput && inlineChatHistory) {
inlineSendBtn.addEventListener('click', async () => {
const msg = inlineInput.value.trim().slice(0, 1000); // Fix #5: 1000 char limit
if (!msg) return;
// Disable send button while waiting
inlineSendBtn.disabled = true;
inlineSendBtn.innerHTML = '';
appendMsg(inlineChatHistory, 'user', escapeHtml(msg));
inlineInput.value = '';
const typingId = 'typing-' + Date.now();
inlineChatHistory.insertAdjacentHTML('beforeend', `
`);
inlineChatHistory.scrollTop = inlineChatHistory.scrollHeight;
try {
const reply = await fetchChatResponse(msg, inlineChatContext);
document.getElementById(typingId)?.remove();
// Escape reply but allow line breaks
appendMsg(inlineChatHistory, 'bot', escapeHtml(reply).replace(/\n/g, '
'));
// Speak the reply aloud
if (window.speechSynthesis) {
const utt = new SpeechSynthesisUtterance(reply);
utt.lang = 'ar-EG';
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utt);
}
inlineChatContext.push({ role: 'user', content: msg });
inlineChatContext.push({ role: 'assistant', content: reply });
if (inlineChatContext.length > 10) inlineChatContext = inlineChatContext.slice(-10);
} catch (err) {
document.getElementById(typingId)?.remove();
appendMsg(inlineChatHistory, 'bot', 'عذراً، حدث خطأ أثناء الاتصال.');
} finally {
inlineSendBtn.disabled = false;
inlineSendBtn.innerHTML = '';
}
});
inlineInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
inlineSendBtn.click();
}
});
}
}
// ==========================================
// Fix #1: Load real properties into right column
// ==========================================
const loadSuggestedProperties = async () => {
const loading = document.getElementById('props-loading');
const list = document.getElementById('props-list');
const empty = document.getElementById('props-empty');
if (!list) return;
try {
const res = await fetch('/api/properties?limit=3');
const data = await res.json();
const props = data.properties || [];
if (loading) loading.style.display = 'none';
if (props.length === 0) { if (empty) empty.style.display = 'flex'; return; }
list.innerHTML = props.map(p => `
${escapeHtml(p.status || 'للبيع')}
${escapeHtml(p.title || '')}
${escapeHtml(p.location || '')}
${escapeHtml(p.price || '')} ${p.status === 'للإيجار' ? 'ج.م / شهر' : 'ج.م'}
`).join('');
} catch {
if (loading) loading.style.display = 'none';
if (empty) empty.style.display = 'flex';
}
};
loadSuggestedProperties();
// ==========================================
// Fix #4: Login modal keyboard trap + Escape
// ==========================================
const loginModal = document.getElementById('loginModal');
const openLoginBtn = document.getElementById('openLoginBtn');
const closeLoginBtn = document.getElementById('closeLoginBtn');
const openModal = () => {
if (!loginModal) return;
loginModal.style.display = 'flex';
const focusable = loginModal.querySelectorAll('button, input, [tabindex]:not([tabindex="-1"])');
if (focusable[0]) focusable[0].focus();
};
const closeModal = () => { if (loginModal) loginModal.style.display = 'none'; };
if (openLoginBtn) openLoginBtn.addEventListener('click', openModal);
if (closeLoginBtn) closeLoginBtn.addEventListener('click', closeModal);
// Escape key closes modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && loginModal && loginModal.style.display !== 'none') closeModal();
});
// Trap Tab inside modal
if (loginModal) {
loginModal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
const focusable = [...loginModal.querySelectorAll('button, input, [tabindex]:not([tabindex="-1"])')].filter(el => !el.disabled);
if (!focusable.length) return;
const first = focusable[0], last = focusable[focusable.length - 1];
if (e.shiftKey ? document.activeElement === first : document.activeElement === last) {
e.preventDefault();
(e.shiftKey ? last : first).focus();
}
});
// Click outside closes modal
loginModal.addEventListener('click', (e) => { if (e.target === loginModal) closeModal(); });
}
// Modal tab switching
document.querySelectorAll('.modal-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.modal-tab').forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected','false'); });
tab.classList.add('active'); tab.setAttribute('aria-selected','true');
document.getElementById('loginForm').style.display = tab.dataset.tab === 'login' ? 'flex' : 'none';
document.getElementById('registerForm').style.display = tab.dataset.tab === 'register' ? 'flex' : 'none';
});
});
// ==========================================
// Fix #7: Highlight active feature card
// ==========================================
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
document.querySelectorAll('.feat-card').forEach(card => {
const href = card.getAttribute('href') || '';
if (href && currentPage.includes(href.replace('.html',''))) {
card.classList.add('feat-card-active');
}
});
const assistantCard = document.getElementById('assistantSidebar');
if (startChatBtn && assistantCard) {
const defaultView = assistantCard.querySelector('.default-view');
const chatView = assistantCard.querySelector('.chat-view');
const chatHistory = assistantCard.querySelector('.chat-history');
const chatInput = assistantCard.querySelector('.chat-input input');
const sendMsgBtn = assistantCard.querySelector('.send-msg-btn');
const closeBtn = assistantCard.querySelector('.close-chat-btn');
const expandBtn = assistantCard.querySelector('.expand-chat-btn');
let chatContext = [];
const openChat = () => {
if (assistantCard) {
assistantCard.style.display = 'flex';
}
if (defaultView && chatView) {
defaultView.style.display = 'none';
chatView.style.display = 'flex';
}
};
const closeChat = () => {
if (defaultView && chatView) {
chatView.style.display = 'none';
defaultView.style.display = 'block';
}
if (window.innerWidth <= 1300 && assistantCard) {
assistantCard.style.display = 'none';
}
};
startChatBtn.addEventListener('click', () => openChat());
if (closeBtn) closeBtn.addEventListener('click', closeChat);
if (sendMsgBtn && chatInput && chatHistory) {
sendMsgBtn.addEventListener('click', async () => {
const msg = chatInput.value.trim();
if(msg) {
// Add user message
chatHistory.innerHTML += `${msg}
`;
chatInput.value = '';
chatHistory.scrollTop = chatHistory.scrollHeight;
// Show typing indicator
const typingId = 'typing-' + Date.now();
chatHistory.innerHTML += ``;
chatHistory.scrollTop = chatHistory.scrollHeight;
try {
// Call API
const reply = await fetchChatResponse(msg, chatContext);
// Remove typing indicator and add response
const typingEl = document.getElementById(typingId);
if (typingEl) typingEl.remove();
chatHistory.innerHTML += `${reply.replace(/\n/g, '
')}
`;
chatHistory.scrollTop = chatHistory.scrollHeight;
// Update context history (keep last 5 interactions)
chatContext.push({"role": "user", "content": msg});
chatContext.push({"role": "assistant", "content": reply});
if (chatContext.length > 10) chatContext = chatContext.slice(chatContext.length - 10);
} catch(err) {
const typingEl = document.getElementById(typingId);
if (typingEl) typingEl.remove();
chatHistory.innerHTML += `عذراً، حدث خطأ.
`;
}
}
});
// Allow sending with Enter key
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
sendMsgBtn.click();
}
});
}
}
});