Darak / front /explore.html
Antigravity AI
Update full system to Hugging Face
771f178
Raw
History Blame Contribute Delete
19.6 kB
<!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">
<!-- Leaflet Map -->
<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; }
/* 3D Flip Utilities */
.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; }
/* Custom Scrollbar for Masonry */
.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 Setup */
.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; } }
/* Infinite scroll sentinel */
#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; }
/* Card fade-in animation */
@keyframes cardIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.masonry-item { animation: cardIn 0.4s ease both; }
/* Glass Floating Nav */
.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);
}
/* Floating Action Button */
.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 Container */
#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">
<!-- FRONT: INFINITE MASONRY GRID -->
<div class="front-face bg-black overflow-y-auto masonry-scroll h-full w-full">
<!-- Floating Nav -->
<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>
<!-- Masonry Container -->
<div class="max-w-[1600px] mx-auto mt-8 px-2 pb-8">
<div id="masonry-grid" class="masonry-grid">
<!-- Items injected by JS -->
</div>
<!-- Infinite scroll sentinel -->
<div id="scroll-sentinel">
<i class="fa-solid fa-circle-notch fa-spin"></i>
<span id="sentinel-label">جاري تحميل المزيد...</span>
</div>
</div>
<!-- FAB to Flip to Map -->
<div class="fab" id="flip-to-map" title="عرض الخريطة">
<i class="fa-solid fa-map-location-dot"></i>
</div>
</div>
<!-- BACK: FULLSCREEN MAP -->
<div class="back-face relative">
<div id="map"></div>
<!-- Map Overlay Gradient for aesthetics -->
<div class="absolute inset-0 pointer-events-none z-[400] bg-radial-gradient from-transparent to-black/80"></div>
<!-- Map Header Overlay -->
<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>
<!-- FAB to Flip to Grid -->
<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;
// --- 3D FLIP LOGIC ---
flipToMapBtn.addEventListener('click', () => {
flipper.classList.add('flipped');
if(!map) {
initMap();
}
// Important: Leaflet MUST recalculate its size AFTER the 1.2s CSS transition is fully complete
setTimeout(() => { if (map) map.invalidateSize(); }, 1250);
});
flipToGridBtn.addEventListener('click', () => {
flipper.classList.remove('flipped');
});
// --- LOAD DATA (initial page) ---
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;
}
// --- RENDER MASONRY (full re-render for search/reset) ---
function renderMasonry() {
masonryGrid.innerHTML = '';
appendCards(globalDb);
updateLoadMoreBtn();
}
// --- APPEND CARDS (used by load-more) ---
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);
});
}
// --- UPDATE LOAD MORE BUTTON STATE ---
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');
}
}
// --- RENDER MAP ---
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; // Prevent re-initialization
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);
}
// Init Sequence
await loadData();
renderMasonry();
updateLoadMoreBtn();
// Load More Logic
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;
// update map markers if open
if (map) addMarkersToMap(newItems);
}
} catch (e) {
console.error('Load more failed', e);
} finally {
isLoading = false;
updateLoadMoreBtn();
}
});
}
// Semantic Search Logic
if (searchInput) {
searchInput.addEventListener('keypress', async (e) => {
if (e.key !== 'Enter') return;
const query = searchInput.value.trim();
const iconEl = document.querySelector('.fa-magnifying-glass');
// Empty query = reset to full list
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; // search results are a fixed set
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>