/** * RandomWeb — Frontend Application Logic * Handles random redirect, search, submission, and real-time counter. */ // ─── Configuration ────────────────────────────────────────── const SUPABASE_URL = 'https://oyxgydfmaocqxictnmou.supabase.co'; const SUPABASE_KEY = 'sb_publishable_9l3BSqU-mIdYLEgZB2Pv2Q_UUZXU385'; const API_BASE = '/api'; // ─── Supabase Client ──────────────────────────────────────── const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY); // ─── DOM Elements ─────────────────────────────────────────── const randomBtn = document.getElementById('random-btn'); const btnText = randomBtn.querySelector('.btn-text'); const searchInput = document.getElementById('search-input'); const searchResults = document.getElementById('search-results'); const submitForm = document.getElementById('submit-form'); const submitInput = document.getElementById('submit-input'); const submitBtn = document.getElementById('submit-btn'); const submitFeedback = document.getElementById('submit-feedback'); const counterValue = document.getElementById('counter-value'); const headerActiveCount = document.getElementById('header-active-count'); const toastContainer = document.getElementById('toast-container'); // ─── State ────────────────────────────────────────────────── let currentCount = 0; let targetCount = 0; let animationFrame = null; let searchDebounceTimer = null; // ─── Utility Functions ────────────────────────────────────── function formatNumber(num) { if (num >= 1_000_000) { return (num / 1_000_000).toFixed(2) + 'M'; } if (num >= 1_000) { return (num / 1_000).toFixed(1) + 'K'; } return num.toLocaleString(); } function formatNumberFull(num) { return num.toLocaleString(); } function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => { toast.classList.add('toast-exiting'); setTimeout(() => toast.remove(), 300); }, 4000); } // ─── Animated Counter ─────────────────────────────────────── function animateCounter(target) { targetCount = target; if (animationFrame) { cancelAnimationFrame(animationFrame); } const startCount = currentCount; const diff = target - startCount; const duration = Math.min(1500, Math.max(300, Math.abs(diff) * 10)); const startTime = performance.now(); function step(timestamp) { const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); // Ease-out cubic const eased = 1 - Math.pow(1 - progress, 3); currentCount = Math.round(startCount + diff * eased); counterValue.textContent = formatNumberFull(currentCount); headerActiveCount.textContent = formatNumber(currentCount); if (progress < 1) { animationFrame = requestAnimationFrame(step); } else { currentCount = target; counterValue.textContent = formatNumberFull(target); headerActiveCount.textContent = formatNumber(target); } } animationFrame = requestAnimationFrame(step); } // ─── Fetch Stats (Initial) ───────────────────────────────── async function fetchStats() { try { const response = await fetch(`${API_BASE}/stats`); if (response.ok) { const data = await response.json(); animateCounter(data.active_count); } } catch (err) { console.warn('Failed to fetch stats:', err); // Fallback: query Supabase directly try { const { data, error } = await supabase .from('stats') .select('active_count') .eq('id', 1) .single(); if (!error && data) { animateCounter(data.active_count); } } catch (e) { console.warn('Supabase fallback also failed:', e); } } } // ─── Realtime Subscription ────────────────────────────────── function setupRealtimeSubscription() { const channel = supabase .channel('stats-realtime') .on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'stats', filter: 'id=eq.1', }, (payload) => { const newCount = payload.new.active_count; if (newCount !== undefined && newCount !== targetCount) { animateCounter(newCount); } } ) .subscribe((status) => { if (status === 'SUBSCRIBED') { console.log('Realtime subscription active'); } }); } // Also poll every 30 seconds as a fallback setInterval(fetchStats, 30000); // ─── Random Button ────────────────────────────────────────── randomBtn.addEventListener('click', async () => { if (randomBtn.classList.contains('loading')) return; randomBtn.classList.add('loading'); btnText.textContent = 'Finding a website...'; try { const response = await fetch(`${API_BASE}/random`); if (response.ok) { const data = await response.json(); if (data.url) { btnText.textContent = 'Redirecting...'; // Small delay for visual feedback setTimeout(() => { window.open(data.url, '_blank', 'noopener,noreferrer'); randomBtn.classList.remove('loading'); btnText.textContent = 'Take Me Somewhere Random'; }, 500); return; } } // API failed, try direct Supabase query const { data: websites, error } = await supabase .rpc('get_random_active_website'); if (!error && websites && websites.length > 0) { btnText.textContent = 'Redirecting...'; setTimeout(() => { window.open(websites[0].url, '_blank', 'noopener,noreferrer'); randomBtn.classList.remove('loading'); btnText.textContent = 'Take Me Somewhere Random'; }, 500); return; } showToast('No active websites found yet. The system is still indexing.', 'info'); } catch (err) { console.error('Random fetch error:', err); showToast('Failed to get a random website. Please try again.', 'error'); } randomBtn.classList.remove('loading'); btnText.textContent = 'Take Me Somewhere Random'; }); // ─── Search ───────────────────────────────────────────────── searchInput.addEventListener('input', (e) => { const query = e.target.value.trim(); clearTimeout(searchDebounceTimer); if (query.length < 2) { searchResults.innerHTML = ''; return; } searchDebounceTimer = setTimeout(() => performSearch(query), 300); }); async function performSearch(query) { try { const response = await fetch( `${API_BASE}/search?q=${encodeURIComponent(query)}&limit=15` ); if (response.ok) { const results = await response.json(); renderSearchResults(results); return; } // Fallback to direct Supabase const { data, error } = await supabase .from('websites') .select('url, domain, is_active') .or(`url.ilike.%${query}%,domain.ilike.%${query}%`) .eq('is_active', true) .limit(15); if (!error && data) { renderSearchResults(data); } } catch (err) { console.error('Search error:', err); } } function renderSearchResults(results) { if (!results || results.length === 0) { searchResults.innerHTML = `
No matching websites found. Try a different search term.
`; return; } searchResults.innerHTML = results .map( (r) => `
${escapeHtml(r.url)}
${escapeHtml(r.domain)}
` ) .join(''); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ─── Submit Form ──────────────────────────────────────────── submitForm.addEventListener('submit', async (e) => { e.preventDefault(); const url = submitInput.value.trim(); if (!url) return; submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; submitFeedback.className = 'submit-feedback'; submitFeedback.style.display = 'none'; try { const response = await fetch(`${API_BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const data = await response.json(); if (response.ok) { submitFeedback.className = 'submit-feedback success'; submitFeedback.textContent = data.message || 'URL submitted successfully!'; submitInput.value = ''; } else { submitFeedback.className = 'submit-feedback error'; submitFeedback.textContent = data.detail || 'Failed to submit URL. Please check the format.'; } } catch (err) { submitFeedback.className = 'submit-feedback error'; submitFeedback.textContent = 'Network error. Please try again.'; } submitBtn.disabled = false; submitBtn.textContent = 'Submit URL'; }); // ─── Initialize ───────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { fetchStats(); setupRealtimeSubscription(); });