Spaces:
Running
Running
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize Lucide Icons | |
| lucide.createIcons(); | |
| // Elements | |
| const themeToggle = document.getElementById('themeToggle'); | |
| const startScrape = document.getElementById('startScrape'); | |
| const userInput = document.getElementById('userInput'); | |
| const landingHero = document.getElementById('landingHero'); | |
| const loadingState = document.getElementById('loadingState'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const statusText = document.getElementById('statusText'); | |
| const progressBar = document.getElementById('progressBar'); | |
| // Config Elements | |
| const reviewCountType = document.getElementById('reviewCountType'); | |
| const sortOrder = document.getElementById('sortOrder'); | |
| const starRating = document.getElementById('starRating'); | |
| // Theme Logic | |
| themeToggle.addEventListener('click', () => { | |
| const html = document.documentElement; | |
| if (html.classList.contains('dark')) { | |
| html.classList.remove('dark'); | |
| localStorage.setItem('theme', 'light'); | |
| } else { | |
| html.classList.add('dark'); | |
| localStorage.setItem('theme', 'dark'); | |
| } | |
| }); | |
| // Load Saved Theme | |
| if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| // Scrape Logic | |
| startScrape.addEventListener('click', async () => { | |
| const identifier = userInput.value.trim(); | |
| if (!identifier) { | |
| alert('Please enter an app name or URL'); | |
| return; | |
| } | |
| // 1. Transition to Loading | |
| landingHero.classList.add('hidden'); | |
| loadingState.classList.remove('hidden'); | |
| // 2. Mock Progress Simulation | |
| let progress = 0; | |
| const progressInterval = setInterval(() => { | |
| if (progress < 90) { | |
| progress += Math.random() * 5; | |
| progressBar.style.width = `${progress}%`; | |
| if (progress < 30) statusText.innerText = 'Connecting to Google Play...'; | |
| else if (progress < 60) statusText.innerText = 'Bypassing Protection...'; | |
| else statusText.innerText = 'Parsing Reviews...'; | |
| } | |
| }, 500); | |
| // 3. Actual API Call | |
| try { | |
| const countValue = reviewCountType.value === 'fixed_500' ? 500 : (reviewCountType.value === 'all' ? 'all' : 150); | |
| const response = await fetch('/scrape', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| identifier, | |
| review_count_type: reviewCountType.value === 'all' ? 'all' : 'fixed', | |
| review_count: countValue, | |
| sort_order: sortOrder.value, | |
| star_rating: starRating.value | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.error || 'Failed to fetch reviews'); | |
| } | |
| // 4. Success - Render Results | |
| clearInterval(progressInterval); | |
| progressBar.style.width = '100%'; | |
| statusText.innerText = 'Done!'; | |
| setTimeout(() => { | |
| renderResults(data); | |
| loadingState.classList.add('hidden'); | |
| resultsSection.classList.remove('hidden'); | |
| }, 600); | |
| } catch (err) { | |
| clearInterval(progressInterval); | |
| alert(err.message); | |
| loadingState.classList.add('hidden'); | |
| landingHero.classList.remove('hidden'); | |
| progressBar.style.width = '0%'; | |
| } | |
| }); | |
| function renderResults(data) { | |
| const { app_info, reviews } = data; | |
| // Update Header | |
| document.getElementById('appIcon').src = app_info.icon; | |
| document.getElementById('appTitle').innerText = app_info.title; | |
| document.getElementById('appScore').innerText = app_info.score.toFixed(1); | |
| document.getElementById('appReviewCount').innerText = formatNumber(app_info.reviews); | |
| document.getElementById('appDesc').innerText = app_info.summary; | |
| document.getElementById('reviewStats').innerText = `Showing ${reviews.length} results`; | |
| // Stars | |
| const stars = Math.round(app_info.score); | |
| const starContainer = document.getElementById('starContainer'); | |
| starContainer.innerHTML = ''; | |
| for (let i = 0; i < 5; i++) { | |
| const starIcon = document.createElement('i'); | |
| starIcon.setAttribute('data-lucide', 'star'); | |
| starIcon.classList.add('w-5', 'h-5'); | |
| if (i < stars) starIcon.classList.add('fill-current'); | |
| starContainer.appendChild(starIcon); | |
| } | |
| // Review Feed | |
| const feed = document.getElementById('reviewFeed'); | |
| feed.innerHTML = ''; | |
| reviews.forEach(review => { | |
| const card = document.createElement('div'); | |
| card.className = 'bg-white dark:bg-slate-900 p-6 rounded-2xl border border-slate-200 dark:border-slate-800 space-y-3 transition-hover hover:border-primary/30'; | |
| card.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div class="flex items-center gap-3"> | |
| <img src="${review.userImage || 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y'}" | |
| class="w-10 h-10 rounded-full border border-slate-200 dark:border-slate-700"> | |
| <div> | |
| <div class="font-bold text-sm">${review.userName}</div> | |
| <div class="text-xs text-slate-400">${new Date(review.at).toLocaleDateString()}</div> | |
| </div> | |
| </div> | |
| <div class="flex text-yellow-500"> | |
| ${Array(review.score).fill('<i data-lucide="star" class="w-3 h-3 fill-current"></i>').join('')} | |
| </div> | |
| </div> | |
| <p class="text-slate-600 dark:text-slate-400 text-sm leading-relaxed">${review.content}</p> | |
| <div class="flex items-center gap-4 text-xs font-bold text-slate-400"> | |
| <span class="flex items-center gap-1"><i data-lucide="thumbs-up" class="w-3 h-3"></i> ${review.thumbsUpCount}</span> | |
| </div> | |
| `; | |
| feed.appendChild(card); | |
| }); | |
| lucide.createIcons(); | |
| // Download Action | |
| document.getElementById('downloadCSV').onclick = () => { | |
| downloadCSV(data.reviews, `${app_info.appId}_reviews.csv`); | |
| }; | |
| } | |
| function downloadCSV(arr, filename) { | |
| const headers = Object.keys(arr[0]).join(','); | |
| const rows = arr.map(obj => Object.values(obj).map(val => `"${val}"`).join(',')).join('\\n'); | |
| const csvContent = "data:text/csv;charset=utf-8," + headers + '\\n' + rows; | |
| const encodedUri = encodeURI(csvContent); | |
| const link = document.createElement("a"); | |
| link.setAttribute("href", encodedUri); | |
| link.setAttribute("download", filename); | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } | |
| function formatNumber(num) { | |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; | |
| if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; | |
| return num; | |
| } | |
| }); | |