| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Dark Real Estate - Infinite Masonry</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800;900&display=swap" rel="stylesheet"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| |
| |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> |
| |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| fontFamily: { sans: ['Cairo', 'sans-serif'] }, |
| colors: { |
| dark: { bg: '#0B0F19', surface: '#111827', accent: '#F59E0B' } |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| body { margin: 0; padding: 0; overflow: hidden; background: #000; font-family: 'Cairo', sans-serif; } |
| |
| |
| .perspective-1000 { perspective: 2000px; } |
| .transform-style-3d { transform-style: preserve-3d; } |
| .backface-hidden { backface-visibility: hidden; -webkit-backface-visibility: hidden; } |
| |
| .flip-container { width: 100vw; height: 100vh; position: relative; } |
| .flipper { |
| width: 100%; height: 100%; position: relative; |
| transition: transform 1.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); |
| transform-style: preserve-3d; |
| } |
| .flipper.flipped { transform: rotateY(180deg); } |
| |
| .front-face, .back-face { |
| position: absolute; inset: 0; |
| backface-visibility: hidden; |
| -webkit-backface-visibility: hidden; |
| } |
| .back-face { transform: rotateY(180deg); background: #0B0F19; } |
| |
| |
| .masonry-scroll::-webkit-scrollbar { width: 8px; } |
| .masonry-scroll::-webkit-scrollbar-track { background: #000; } |
| .masonry-scroll::-webkit-scrollbar-thumb { background: #333; border-radius: 10px; } |
| .masonry-scroll::-webkit-scrollbar-thumb:hover { background: #F59E0B; } |
| |
| |
| .masonry-grid { |
| column-count: 1; |
| column-gap: 1rem; |
| padding: 1rem; |
| } |
| @media (min-width: 640px) { .masonry-grid { column-count: 2; } } |
| @media (min-width: 1024px) { .masonry-grid { column-count: 3; } } |
| @media (min-width: 1280px) { .masonry-grid { column-count: 4; } } |
| @media (min-width: 1536px) { .masonry-grid { column-count: 5; } } |
| |
| |
| #scroll-sentinel { |
| height: 80px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: #6b7280; |
| font-size: 13px; |
| gap: 8px; |
| } |
| #scroll-sentinel.loading { color: #F59E0B; } |
| #scroll-sentinel.end { color: #374151; } |
| |
| |
| @keyframes cardIn { |
| from { opacity: 0; transform: translateY(20px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| .masonry-item { animation: cardIn 0.4s ease both; } |
| |
| |
| .floating-nav { |
| position: sticky; top: 20px; z-index: 100; |
| background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| |
| |
| .fab { |
| position: fixed; bottom: 40px; right: 40px; z-index: 1000; |
| width: 70px; height: 70px; border-radius: 50%; |
| background: #F59E0B; color: #000; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 24px; cursor: pointer; |
| box-shadow: 0 10px 30px rgba(245, 158, 11, 0.6); |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| .fab:hover { transform: scale(1.1) rotate(15deg); box-shadow: 0 15px 40px rgba(245, 158, 11, 0.8); } |
| |
| |
| #map { width: 100%; height: 100%; position: absolute; inset: 0; } |
| .leaflet-control-zoom { border: none !important; margin-right: 20px !important; margin-bottom: 40px !important; } |
| .leaflet-bar a { background-color: rgba(11,15,25,0.9) !important; color: #F59E0B !important; border-color: rgba(255,255,255,0.1) !important; } |
| |
| .golden-pin { |
| width: 20px; height: 20px; background: #F59E0B; border-radius: 50%; |
| border: 2px solid #FFF; box-shadow: 0 0 15px rgba(245, 158, 11, 0.8); |
| } |
| |
| </style> |
| </head> |
| <body> |
|
|
| <div class="perspective-1000 flip-container"> |
| <div id="flipper" class="flipper"> |
| |
| |
| <div class="front-face bg-black overflow-y-auto masonry-scroll h-full w-full"> |
| |
| |
| <div class="max-w-7xl mx-auto px-4 mt-6 pointer-events-none sticky top-6 z-50"> |
| <nav class="floating-nav rounded-full px-6 py-3 flex items-center justify-between pointer-events-auto"> |
| <div class="flex items-center gap-3 cursor-pointer" onclick="window.location.href='index.html'"> |
| <img src="logo.png" alt="Dark Logo" class="h-8 object-contain drop-shadow-[0_0_10px_rgba(203,161,53,0.5)]"> |
| <span class="text-white font-black text-lg hidden sm:block tracking-wide">دارَك <span class="text-dark-accent">جاليري</span></span> |
| </div> |
| |
| <div class="flex-1 max-w-lg mx-4 flex items-center bg-white/10 rounded-full px-4 py-2 border border-transparent focus-within:border-dark-accent transition"> |
| <i class="fa-solid fa-magnifying-glass text-gray-400"></i> |
| <input type="text" id="semantic-search-input" placeholder="ابحث عن عقارات، مناطق..." class="bg-transparent border-none outline-none text-white px-3 w-full text-sm"> |
| </div> |
| |
| <button id="load-more-btn" class="bg-white/10 hover:bg-white/20 text-white p-2 sm:px-4 sm:py-2 rounded-full text-sm font-bold transition flex items-center gap-2"> |
| <i class="fa-solid fa-plus"></i> <span class="hidden sm:inline">تحميل المزيد</span> |
| </button> |
| </nav> |
| </div> |
|
|
| |
| <div class="max-w-[1600px] mx-auto mt-8 px-2 pb-8"> |
| <div id="masonry-grid" class="masonry-grid"> |
| |
| </div> |
| |
| <div id="scroll-sentinel"> |
| <i class="fa-solid fa-circle-notch fa-spin"></i> |
| <span id="sentinel-label">جاري تحميل المزيد...</span> |
| </div> |
| </div> |
|
|
| |
| <div class="fab" id="flip-to-map" title="عرض الخريطة"> |
| <i class="fa-solid fa-map-location-dot"></i> |
| </div> |
| </div> |
|
|
| |
| <div class="back-face relative"> |
| <div id="map"></div> |
| |
| |
| <div class="absolute inset-0 pointer-events-none z-[400] bg-radial-gradient from-transparent to-black/80"></div> |
| |
| |
| <div class="absolute top-6 left-0 right-0 z-[500] pointer-events-none px-6 flex justify-center"> |
| <div class="bg-black/60 backdrop-blur-md border border-dark-accent/30 rounded-full px-8 py-3 text-white font-black tracking-wide pointer-events-auto shadow-2xl"> |
| <i class="fa-solid fa-location-crosshairs text-dark-accent mr-2"></i> الخريطة التفاعلية |
| </div> |
| </div> |
|
|
| |
| <div class="fab bg-white text-black hover:bg-gray-200" id="flip-to-grid" title="عرض الصور"> |
| <i class="fa-solid fa-images"></i> |
| </div> |
| </div> |
| |
| </div> |
| </div> |
|
|
| <script src="data.js"></script> |
| <script type="module"> |
| import { fetchAllProperties, semanticSearch, triggerScraping } from './api.js'; |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| const flipper = document.getElementById('flipper'); |
| const flipToMapBtn = document.getElementById('flip-to-map'); |
| const flipToGridBtn = document.getElementById('flip-to-grid'); |
| const masonryGrid = document.getElementById('masonry-grid'); |
| const loadMoreBtn = document.getElementById('load-more-btn'); |
| const searchInput = document.getElementById('semantic-search-input'); |
| |
| let map; |
| let globalDb = []; |
| let currentOffset = 0; |
| const PAGE_SIZE = 20; |
| let isLoading = false; |
| let hasMore = true; |
| |
| |
| flipToMapBtn.addEventListener('click', () => { |
| flipper.classList.add('flipped'); |
| if(!map) { |
| initMap(); |
| } |
| |
| setTimeout(() => { if (map) map.invalidateSize(); }, 1250); |
| }); |
| flipToGridBtn.addEventListener('click', () => { |
| flipper.classList.remove('flipped'); |
| }); |
| |
| |
| async function loadData() { |
| isLoading = true; |
| currentOffset = 0; |
| hasMore = true; |
| try { |
| const fetched = await fetchAllProperties(PAGE_SIZE); |
| globalDb = fetched; |
| sessionStorage.setItem('aiRecommendations', JSON.stringify(globalDb)); |
| currentOffset = fetched.length; |
| if (fetched.length < PAGE_SIZE) hasMore = false; |
| } catch { |
| globalDb = typeof propertiesDatabase !== 'undefined' ? propertiesDatabase : []; |
| } |
| isLoading = false; |
| } |
| |
| |
| function renderMasonry() { |
| masonryGrid.innerHTML = ''; |
| appendCards(globalDb); |
| updateLoadMoreBtn(); |
| } |
| |
| |
| function appendCards(props) { |
| const heights = ['h-64', 'h-80', 'h-96', 'h-[28rem]', 'h-[20rem]']; |
| const startIndex = masonryGrid.children.length; |
| props.forEach((prop, i) => { |
| const randomHeight = heights[Math.floor(Math.random() * heights.length)]; |
| const index = startIndex + i; |
| const card = document.createElement('div'); |
| card.className = `masonry-item rounded-2xl overflow-hidden group relative cursor-pointer ${randomHeight}`; |
| const scoreHtml = prop.matchScore |
| ? `<div class="absolute top-4 left-4 bg-dark-accent text-black font-black px-3 py-1 rounded-lg z-20 text-xs shadow-lg"><i class="fa-solid fa-wand-magic-sparkles"></i> ${prop.matchScore}%</div>` |
| : ''; |
| card.innerHTML = ` |
| ${scoreHtml} |
| <img src="${prop.image}" loading="lazy" class="absolute inset-0 w-full h-full object-cover group-hover:scale-110 transition duration-1000 ease-in-out"> |
| <div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-80 group-hover:opacity-100 transition duration-500"></div> |
| <div class="absolute bottom-0 left-0 right-0 p-6 translate-y-4 group-hover:translate-y-0 transition duration-500"> |
| <div class="text-[10px] text-dark-accent font-bold mb-2 tracking-widest uppercase flex items-center gap-1"> |
| <i class="fa-solid fa-building"></i> ${prop.type} |
| </div> |
| <h3 class="font-black text-xl text-white mb-1 leading-tight line-clamp-2">${prop.title}</h3> |
| <div class="flex items-center text-gray-300 text-xs mb-3"> |
| <i class="fa-solid fa-location-dot mr-1"></i> ${prop.location} |
| </div> |
| <div class="text-2xl font-black text-white">${prop.price} <span class="text-sm font-normal text-dark-accent">ج.م</span></div> |
| <div class="flex gap-2 mt-4 opacity-0 group-hover:opacity-100 transition duration-500 delay-100"> |
| <button onclick="window.location.href='property.html?index=${index}'" class="flex-1 bg-dark-accent text-black py-2 rounded-xl text-sm font-bold hover:brightness-110 transition"> |
| استعراض <i class="fa-solid fa-arrow-left mr-1"></i> |
| </button> |
| </div> |
| </div>`; |
| masonryGrid.appendChild(card); |
| }); |
| } |
| |
| |
| function updateLoadMoreBtn() { |
| if (!loadMoreBtn) return; |
| if (!hasMore) { |
| loadMoreBtn.innerHTML = '<i class="fa-solid fa-check"></i> <span class="hidden sm:inline">تم تحميل الكل</span>'; |
| loadMoreBtn.disabled = true; |
| loadMoreBtn.classList.add('opacity-40', 'cursor-not-allowed'); |
| } else { |
| loadMoreBtn.innerHTML = '<i class="fa-solid fa-plus"></i> <span class="hidden sm:inline">تحميل المزيد</span>'; |
| loadMoreBtn.disabled = false; |
| loadMoreBtn.classList.remove('opacity-40', 'cursor-not-allowed'); |
| } |
| } |
| |
| |
| function addMarkersToMap(props) { |
| const icon = L.divIcon({ className: 'custom-div-icon', html: "<div class='golden-pin'></div>", iconSize: [20, 20], iconAnchor: [10, 10] }); |
| props.forEach(prop => { |
| if (prop.lat && prop.lng) { |
| L.marker([prop.lat, prop.lng], { icon }) |
| .addTo(map) |
| .bindPopup(`<b style="font-family:'Cairo'">${prop.title}</b><br><span>${prop.price}</span>`); |
| } |
| }); |
| } |
| |
| function initMap() { |
| if (map) return; |
| |
| map = L.map('map', { zoomControl: false, attributionControl: false }).setView([30.0444, 31.2357], 12); |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 }).addTo(map); |
| L.control.zoom({ position: 'bottomleft' }).addTo(map); |
| addMarkersToMap(globalDb); |
| } |
| |
| |
| await loadData(); |
| renderMasonry(); |
| updateLoadMoreBtn(); |
| |
| |
| if (loadMoreBtn) { |
| loadMoreBtn.addEventListener('click', async () => { |
| if (isLoading || !hasMore) return; |
| isLoading = true; |
| loadMoreBtn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> <span class="hidden sm:inline">جاري التحميل...</span>'; |
| loadMoreBtn.disabled = true; |
| try { |
| const newItems = await fetchAllProperties(PAGE_SIZE, currentOffset); |
| if (newItems.length === 0) { |
| hasMore = false; |
| } else { |
| globalDb = [...globalDb, ...newItems]; |
| currentOffset += newItems.length; |
| appendCards(newItems); |
| if (newItems.length < PAGE_SIZE) hasMore = false; |
| |
| if (map) addMarkersToMap(newItems); |
| } |
| } catch (e) { |
| console.error('Load more failed', e); |
| } finally { |
| isLoading = false; |
| updateLoadMoreBtn(); |
| } |
| }); |
| } |
| |
| |
| if (searchInput) { |
| searchInput.addEventListener('keypress', async (e) => { |
| if (e.key !== 'Enter') return; |
| const query = searchInput.value.trim(); |
| const iconEl = document.querySelector('.fa-magnifying-glass'); |
| |
| |
| if (!query) { |
| await loadData(); |
| renderMasonry(); |
| updateLoadMoreBtn(); |
| return; |
| } |
| |
| if (iconEl) iconEl.className = 'fa-solid fa-circle-notch fa-spin text-dark-accent'; |
| try { |
| const results = await semanticSearch(query); |
| if (results && results.length > 0) { |
| globalDb = results; |
| currentOffset = results.length; |
| hasMore = false; |
| renderMasonry(); |
| updateLoadMoreBtn(); |
| if (map) { |
| map.eachLayer(l => { if (l instanceof L.Marker) map.removeLayer(l); }); |
| addMarkersToMap(globalDb); |
| } |
| } else { |
| masonryGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:#6b7280;padding:60px 0"><i class="fa-solid fa-ghost" style="font-size:2.5rem;opacity:0.4;display:block;margin-bottom:12px"></i>لا توجد نتائج مطابقة لبحثك</div>'; |
| hasMore = false; |
| updateLoadMoreBtn(); |
| } |
| } catch (err) { |
| console.error(err); |
| } finally { |
| if (iconEl) iconEl.className = 'fa-solid fa-magnifying-glass text-gray-400'; |
| } |
| }); |
| } |
| }); |
| </script> |
| </body> |
| </html> |
|
|