Darak / front /main.js
Antigravity AI
Fix hardcoded localhost URLs to use relative paths for Hugging Face deployment
3f5ff7f
Raw
History Blame Contribute Delete
24.3 kB
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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// ── Append a chat bubble safely ──
function appendMsg(container, role, html) {
const isBot = role === 'bot';
container.insertAdjacentHTML('beforeend', `
<div class="chat-msg ${isBot ? 'bot-msg' : 'user-msg'}">
<div class="avatar-ic"><i class="fa-solid fa-${isBot ? 'robot' : 'user'}"></i></div>
<div class="msg-bubble">${html}</div>
</div>`);
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<particleCount; i++) {
positions[i*3] = (Math.random() - 0.5) * 800;
positions[i*3+1] = (Math.random() - 0.5) * 800;
positions[i*3+2] = (Math.random() - 0.5) * 800;
velocities.push({
x: (Math.random() - 0.5) * 0.5,
y: (Math.random() - 0.5) * 0.5,
z: (Math.random() - 0.5) * 0.5
});
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
color: 0xd4af37,
size: 3,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
const particleSystem = new THREE.Points(geometry, material);
scene.add(particleSystem);
// Lines for Network
const lineMaterial = new THREE.LineBasicMaterial({
color: 0xd4af37,
transparent: true,
opacity: 0.15,
blending: THREE.AdditiveBlending
});
const lineGeometry = new THREE.BufferGeometry();
const linePositions = new Float32Array(particleCount * particleCount * 3);
lineGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3));
const linesMesh = new THREE.LineSegments(lineGeometry, lineMaterial);
scene.add(linesMesh);
// Mouse interaction
let mouseX = 0;
let mouseY = 0;
document.addEventListener('mousemove', (event) => {
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<particleCount; i++) {
positionsAttr[i*3] += velocities[i].x;
positionsAttr[i*3+1] += velocities[i].y;
positionsAttr[i*3+2] += velocities[i].z;
// Bounce off imaginary bounds
if (Math.abs(positionsAttr[i*3]) > 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<particleCount; j++) {
const dx = positionsAttr[i*3] - positionsAttr[j*3];
const dy = positionsAttr[i*3+1] - positionsAttr[j*3+1];
const dz = positionsAttr[i*3+2] - positionsAttr[j*3+2];
const distSq = dx*dx + dy*dy + dz*dz;
if(distSq < 15000) {
linePositions[lineIndex++] = positionsAttr[i*3];
linePositions[lineIndex++] = positionsAttr[i*3+1];
linePositions[lineIndex++] = positionsAttr[i*3+2];
linePositions[lineIndex++] = positionsAttr[j*3];
linePositions[lineIndex++] = positionsAttr[j*3+1];
linePositions[lineIndex++] = positionsAttr[j*3+2];
}
}
}
geometry.attributes.position.needsUpdate = true;
linesMesh.geometry.setDrawRange(0, lineIndex / 3);
linesMesh.geometry.attributes.position.needsUpdate = true;
// Camera reacts to mouse
camera.position.x += (mouseX - camera.position.x) * 0.05;
camera.position.y += (-mouseY - camera.position.y) * 0.05;
camera.lookAt(scene.position);
renderer.render(scene, camera);
};
animate();
// Handle Resize
window.addEventListener('resize', () => {
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 = `<div class="chat-msg bot-msg"><div class="avatar-ic"><i class="fa-solid fa-robot"></i></div><div class="msg-bubble">مرحباً! أنا مستشارك العقاري الذكي. كيف يمكنني مساعدتك اليوم؟</div></div>`;
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 = '<i class="fa-solid fa-spinner fa-spin"></i>';
appendMsg(inlineChatHistory, 'user', escapeHtml(msg));
inlineInput.value = '';
const typingId = 'typing-' + Date.now();
inlineChatHistory.insertAdjacentHTML('beforeend', `
<div id="${typingId}" class="chat-msg bot-msg">
<div class="avatar-ic"><i class="fa-solid fa-robot"></i></div>
<div class="msg-bubble"><div class="typing-indicator"><span></span><span></span><span></span></div></div>
</div>`);
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, '<br>'));
// 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', '<span style="color:#ff6b6b">عذراً، حدث خطأ أثناء الاتصال.</span>');
} finally {
inlineSendBtn.disabled = false;
inlineSendBtn.innerHTML = '<i class="fa-solid fa-paper-plane"></i>';
}
});
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 => `
<div class="prop-card" onclick="window.location.href='explore.html'">
<div class="prop-img-wrap">
<img src="${escapeHtml(p.image || '')}" alt="${escapeHtml(p.title || '')}" loading="lazy">
<span class="prop-badge${p.status === 'للإيجار' ? ' rent' : ''}">${escapeHtml(p.status || 'للبيع')}</span>
</div>
<div class="prop-info">
<div class="prop-name">${escapeHtml(p.title || '')}</div>
<div class="prop-loc"><i class="fa-solid fa-location-dot"></i> ${escapeHtml(p.location || '')}</div>
<div class="prop-price">${escapeHtml(p.price || '')} <span>${p.status === 'للإيجار' ? 'ج.م / شهر' : 'ج.م'}</span></div>
</div>
</div>`).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 += `<div style="background:var(--gold); color:black; padding:8px; border-radius:10px; align-self:flex-end; max-width:80%;">${msg}</div>`;
chatInput.value = '';
chatHistory.scrollTop = chatHistory.scrollHeight;
// Show typing indicator
const typingId = 'typing-' + Date.now();
chatHistory.innerHTML += `<div id="${typingId}" style="background:rgba(255,255,255,0.05); padding:10px 16px; border-radius:14px; align-self:flex-start; max-width:80%;"><div class="typing-indicator"><span></span><span></span><span></span></div></div>`;
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 += `<div style="background:rgba(255,255,255,0.1); padding:8px; border-radius:10px; align-self:flex-start; max-width:80%;">${reply.replace(/\n/g, '<br>')}</div>`;
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 += `<div style="background:rgba(255,0,0,0.2); padding:8px; border-radius:10px; align-self:flex-start; max-width:80%;">عذراً، حدث خطأ.</div>`;
}
}
});
// Allow sending with Enter key
chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
sendMsgBtn.click();
}
});
}
}
});