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', `
${html}
`); 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.title || '')} ${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(); } }); } } });