Spaces:
Paused
Paused
| /** | |
| * 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 = ` | |
| <div class="search-empty"> | |
| No matching websites found. Try a different search term. | |
| </div> | |
| `; | |
| return; | |
| } | |
| searchResults.innerHTML = results | |
| .map( | |
| (r) => ` | |
| <a href="${escapeHtml(r.url)}" target="_blank" rel="noopener noreferrer" | |
| class="search-result-item"> | |
| <div> | |
| <div class="result-url">${escapeHtml(r.url)}</div> | |
| <div class="result-domain">${escapeHtml(r.domain)}</div> | |
| </div> | |
| <span class="result-arrow">β</span> | |
| </a> | |
| ` | |
| ) | |
| .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(); | |
| }); | |