search
// ── 2. Initialize client ──────────────────────────────────── const SUPABASE_URL = 'https://mkbsldkrybtxeonuxixi.supabase.co'; const SUPABASE_ANON = 'YOUR_ANON_KEY_HERE'; // ← paste your anon key const { createClient } = supabase; const db = createClient(SUPABASE_URL, SUPABASE_ANON); // ── 3. Auth state ─────────────────────────────────────────── let currentUser = null; async function initAuth() { // Get session on page load const { data: { session } } = await db.auth.getSession(); if (session) { currentUser = session.user; await loadUserProfile(); updateNavForUser(); } // Listen for auth changes (login, logout, token refresh) db.auth.onAuthStateChange(async (event, session) => { if (event === 'SIGNED_IN') { currentUser = session.user; await loadUserProfile(); updateNavForUser(); } if (event === 'SIGNED_OUT') { currentUser = null; updateNavForGuest(); } }); } // ── 4. Load user profile ──────────────────────────────────── let userProfile = null; async function loadUserProfile() { if (!currentUser) return; const { data, error } = await db .from('profiles') .select('*') .eq('id', currentUser.id) .single(); if (data) { userProfile = data; updateNavForUser(); } } // ── 5. Update nav UI based on auth state ─────────────────── function updateNavForUser() { if (!userProfile) return; // Update avatar initial const initials = (userProfile.display_name || userProfile.email || 'U') .charAt(0).toUpperCase(); document.querySelectorAll('.user-avatar-initial').forEach(el => { el.textContent = initials; }); // Update display name in user menu const nameEl = document.getElementById('userMenuName'); if (nameEl) nameEl.textContent = userProfile.display_name || 'My Account'; const emailEl = document.getElementById('userMenuEmail'); if (emailEl) emailEl.textContent = userProfile.email || currentUser.email; // Update plan badge const planEl = document.getElementById('userMenuPlan'); if (planEl) planEl.textContent = capitalize(userProfile.plan || 'Starter') + ' Plan'; } function updateNavForGuest() { document.querySelectorAll('.user-avatar-initial').forEach(el => { el.textContent = '?'; }); const nameEl = document.getElementById('userMenuName'); const emailEl = document.getElementById('userMenuEmail'); const planEl = document.getElementById('userMenuPlan'); if (nameEl) nameEl.textContent = 'My Account'; if (emailEl) emailEl.textContent = 'Sign in to continue'; if (planEl) planEl.textContent = 'Starter Plan'; } // ── 6. Sign Up ────────────────────────────────────────────── async function signUp(email, password, displayName) { showAuthLoading(true); const { data, error } = await db.auth.signUp({ email, password, options: { data: { full_name: displayName } } }); showAuthLoading(false); if (error) { showAuthError(error.message); return false; } // Profile is auto-created by the database trigger showToast('Account created!', 'Check your email to confirm your account.'); closeAuthModal(); return true; } // ── 7. Sign In ────────────────────────────────────────────── async function signIn(email, password) { showAuthLoading(true); const { data, error } = await db.auth.signInWithPassword({ email, password }); showAuthLoading(false); if (error) { showAuthError(error.message); return false; } showToast('Welcome back!', `Signed in as ${email}`); closeAuthModal(); return true; } // ── 8. Google OAuth ───────────────────────────────────────── async function signInWithGoogle() { const { error } = await db.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: `${window.location.origin}/auth/callback` } }); if (error) showAuthError(error.message); } // ── 9. Sign Out ───────────────────────────────────────────── async function signOut() { try { await db.auth.signOut(); } catch(e) { console.warn('Supabase signOut error:', e); } currentUser = null; userProfile = null; // Close menu safely try { closeUserMenu(); } catch(e) {} // Reset user menu display updateNavForGuest(); showToast('Signed out', 'See you next time.'); // Use original navigate directly to avoid any override issues _originalNavigate('marketplace'); } // ── 10. Load Real Tours ───────────────────────────────────── async function loadTours(options = {}) { const { category, limit = 20, status = 'active' } = options; let query = db .from('tours') .select(` id, title, description, location, price, category, tour_type, content_url, thumbnail_url, status, bedrooms, bathrooms, sqft, year_built, badge, duration, views_count, leads_count, created_at, updated_at, profiles (display_name, studio_name, avatar_url) `) .eq('status', status) .order('created_at', { ascending: false }) .limit(limit); if (category && category !== 'All Estates') { query = query.eq('category', category); } const { data, error } = await query; if (error) { console.error('Error loading tours:', error.message); return []; } return data || []; } // ── 11. Render real tours into the grid ───────────────────── async function renderRealEstates() { const grid = document.getElementById('estatesGrid'); if (!grid) return; // Show loading skeleton grid.innerHTML = Array(8).fill(`
`).join(''); const tours = await loadTours(); if (tours.length === 0) { // Fall back to mock data if no real tours yet renderEstates(); return; } grid.innerHTML = tours.map(tour => { const studio = tour.profiles?.studio_name || tour.profiles?.display_name || 'Agent'; const img = tour.thumbnail_url || 'https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=900&q=80'; const price = tour.price ? `$${Number(tour.price).toLocaleString()}` : 'Price on request'; const views = formatNumber(tour.views_count || 0) + ' VIEWS'; const when = timeAgo(tour.created_at); return `
${tour.title}
${tour.duration || '—'}
${tour.badge ? `
${tour.badge}
` : ''}
domain

${tour.title}

${studio}

${views} ${when}
`; }).join(''); } // ── 12. Log impression ────────────────────────────────────── async function logImpression(tourId) { const visitorId = getVisitorId(); await db.from('impressions').insert({ tour_id: tourId, visitor_id: visitorId, session_duration: 0 }); } // ── 13. Save / Unsave tour ────────────────────────────────── async function saveToSupabase(tourId) { if (!currentUser) { openAuthModal('login'); return false; } const { error } = await db.from('saved_tours').insert({ user_id: currentUser.id, tour_id: tourId }); return !error; } async function unsaveFromSupabase(tourId) { if (!currentUser) return false; const { error } = await db.from('saved_tours').delete() .eq('user_id', currentUser.id) .eq('tour_id', tourId); return !error; } async function loadSavedTours() { if (!currentUser) return []; const { data, error } = await db .from('saved_tours') .select(` tour_id, tours ( id, title, location, price, thumbnail_url, views_count, duration, profiles (display_name, studio_name) ) `) .eq('user_id', currentUser.id) .order('saved_at', { ascending: false }); if (error) return []; return (data || []).map(row => row.tours).filter(Boolean); } // ── 14. Submit lead ───────────────────────────────────────── async function submitLead(tourId, { name, email, phone, message }) { const { error } = await db.from('leads').insert({ tour_id: tourId, name, email, phone, message }); if (error) { showToast('Error', 'Could not submit. Please try again.'); return false; } showToast('Request sent!', 'The agent will contact you shortly.'); return true; } // ── 15. Publish a new listing ─────────────────────────────── async function publishListing(formData) { if (!currentUser) { openAuthModal('login'); return false; } const { data, error } = await db.from('tours').insert({ owner_id: currentUser.id, title: formData.title, description: formData.description, location: formData.location, price: formData.price, category: formData.category, tour_type: formData.tourType, content_url: formData.contentUrl, raw_html: formData.rawHtml, status: 'active' }).select().single(); if (error) { showToast('Error', error.message); return false; } showToast('Published!', 'Your estate is now live on the marketplace.'); navigate('dashboard'); return true; } // ── 16. Auth Modal ────────────────────────────────────────── function openAuthModal(mode = 'login') { // Remove existing modal if any document.getElementById('authModal')?.remove(); const modal = document.createElement('div'); modal.id = 'authModal'; modal.className = 'fixed inset-0 z-[110] flex items-center justify-center p-4'; modal.innerHTML = `
`; document.body.appendChild(modal); setTimeout(() => document.getElementById('authEmail')?.focus(), 100); // Store current mode on modal modal.dataset.mode = mode; } function closeAuthModal() { document.getElementById('authModal')?.remove(); } function switchAuthMode() { const modal = document.getElementById('authModal'); const current = modal?.dataset.mode || 'login'; closeAuthModal(); openAuthModal(current === 'login' ? 'signup' : 'login'); } async function handleAuthSubmit() { const modal = document.getElementById('authModal'); const mode = modal?.dataset.mode || 'login'; const email = document.getElementById('authEmail')?.value?.trim(); const password = document.getElementById('authPassword')?.value; const name = document.getElementById('authName')?.value?.trim(); if (!email || !password) { showAuthError('Please fill in all fields.'); return; } if (mode === 'signup') { await signUp(email, password, name); } else { await signIn(email, password); } } function showAuthLoading(show) { const el = document.getElementById('authLoading'); if (el) el.classList.toggle('hidden', !show); } function showAuthError(msg) { const el = document.getElementById('authError'); if (el) { el.textContent = msg; el.classList.remove('hidden'); } } // ── 17. Wire Sign Out button ──────────────────────────────── // The existing user menu has a "Sign Out" button — wire it here document.addEventListener('DOMContentLoaded', () => { const signOutBtn = document.querySelector('[onclick*="signOut"]'); // Already handled above via onclick="signOut()" }); // ── 18. Guard protected pages ─────────────────────────────── // Patch navigate safely after DOM is ready const _originalNavigate = window.navigate; window.navigate = async function(view) { const protectedViews = ['dashboard', 'newListing', 'saved', 'settings']; if (protectedViews.includes(view) && !currentUser) { openAuthModal('login'); return; } // Call original if (typeof _originalNavigate === 'function') { _originalNavigate(view); } // Load real data when navigating to marketplace if (view === 'marketplace') { renderRealEstates(); } }; // ── 19. Utility functions ─────────────────────────────────── function formatNumber(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; return String(n); } function timeAgo(dateStr) { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); const hrs = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); const wks = Math.floor(days / 7); const mos = Math.floor(days / 30); if (mins < 60) return `${mins}m ago`; if (hrs < 24) return `${hrs}h ago`; if (days < 7) return `${days}d ago`; if (wks < 5) return `${wks}w ago`; return `${mos}mo ago`; } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function getVisitorId() { let id = localStorage.getItem('pvr_visitor'); if (!id) { id = crypto.randomUUID(); localStorage.setItem('pvr_visitor', id); } return id; } // ── 20. Boot ──────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', async () => { await initAuth(); // Load real estates on the marketplace view if (document.getElementById('view-marketplace')) { renderRealEstates(); } });