Spaces:
Running
Running
| // script.js | |
| // Set the base URL for your deployed Cloudflare Worker API | |
| // *** IMPORTANT: REPLACE THIS WITH THE ACTUAL URL OF YOUR DEPLOYED CLOUDFLARE WORKER *** | |
| const CLOUDFLARE_WORKER_API_BASE_URL = 'https://dmimapiworker.dmimx.workers.dev'; | |
| // Global state, will be populated by fetch calls | |
| let allTrends = []; | |
| let userData = { | |
| dmimBalance: 0, | |
| savedTrends: [] | |
| }; | |
| let currentSentimentTrend = null; | |
| // Static metadata for categories (icons, colors) - these are frontend-only display properties | |
| const performanceCategoriesMeta = { | |
| music: { name: "Music", icon: '<i class="fas fa-music text-purple-500"></i>', color: 'bg-purple-500' }, | |
| theater: { name: "Theater", icon: '<i class="fas fa-theater-masks text-yellow-500"></i>', color: 'bg-yellow-500' }, | |
| dance: { name: "Dance", icon: '<i class="fas fa-child text-pink-500"></i>', color: 'bg-pink-500' }, | |
| comedy: { name: "Comedy", icon: '<i class="fas fa-laugh-squint text-blue-500"></i>', color: 'bg-blue-500' }, | |
| emerging: { name: "Emerging", icon: '<i class="fas fa-star text-green-500"></i>', color: 'bg-green-500' } | |
| }; | |
| // Helper to get category metadata | |
| function getCategoryMeta(categoryKey) { | |
| return performanceCategoriesMeta[categoryKey] || { name: categoryKey, icon: '<i class="fas fa-hashtag"></i>', color: 'bg-gray-500' }; | |
| } | |
| // Function to calculate market share percentage with sentiment adjustment | |
| function calculateMarketShare(trend) { | |
| const totalAllSearches = allTrends.reduce((sum, t) => sum + t.current_searches, 0); | |
| const globalPercentage = totalAllSearches > 0 ? (trend.current_searches / totalAllSearches) * 100 : 0; | |
| const rawChange = trend.previous_searches > 0 | |
| ? ((trend.current_searches - trend.previous_searches) / trend.previous_searches) * 100 | |
| : trend.current_searches > 0 ? 100 : 0; | |
| const sentimentToUse = trend.sentiment || 0; | |
| const sentimentMultiplier = 1 + (sentimentToUse / 200); | |
| const adjustedChange = rawChange * sentimentMultiplier; | |
| return { | |
| percentage: adjustedChange > 0 ? globalPercentage.toFixed(2) : (globalPercentage * sentimentMultiplier).toFixed(2), // Apply sentiment to displayed percentage too | |
| change: adjustedChange.toFixed(2), | |
| rawChange: rawChange.toFixed(2), | |
| category: trend.category, | |
| sentiment: sentimentToUse, | |
| sentimentHeadline: trend.sentiment_headline || "" | |
| }; | |
| } | |
| // Helper functions for CSS classes and labels | |
| function getPercentageClass(change) { | |
| const numChange = parseFloat(change); | |
| if (numChange > 0) return 'percentage-up'; | |
| if (numChange < 0) return 'percentage-down'; | |
| return 'percentage-neutral'; | |
| } | |
| function getSentimentClass(sentiment) { | |
| const numSentiment = parseInt(sentiment); | |
| if (numSentiment > 20) return 'sentiment-positive'; | |
| if (numSentiment < -20) return 'sentiment-negative'; | |
| return 'sentiment-neutral'; | |
| } | |
| function getSentimentLabel(sentiment) { | |
| const numSentiment = parseInt(sentiment); | |
| if (numSentiment > 20) return 'Positive'; | |
| if (numSentiment < -20) return 'Negative'; | |
| return 'Neutral'; | |
| } | |
| function getPlatformIcon(platform) { | |
| return getCategoryMeta(platform).icon; | |
| } | |
| // Function to render trending cards | |
| function renderTrendingCards() { | |
| const container = document.getElementById('trendingCardsContainer'); | |
| container.innerHTML = ''; | |
| const topTrends = [...allTrends].sort((a, b) => b.current_searches - a.current_searches).slice(0, 8); | |
| topTrends.forEach(item => { | |
| const marketShare = calculateMarketShare(item); | |
| const percentageClass = getPercentageClass(marketShare.change); | |
| const sentimentClass = getSentimentClass(item.sentiment); | |
| const categoryMeta = getCategoryMeta(item.category); | |
| const card = document.createElement('div'); | |
| card.className = `trend-card bg-white rounded-lg shadow-sm p-4 h-40 flex flex-col justify-between transition cursor-pointer ${sentimentClass}`; | |
| card.innerHTML = ` | |
| <div class="flex items-start justify-between mb-2"> | |
| <div> | |
| <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full"> | |
| ${getPlatformIcon(item.category)} | |
| <span class="ml-1">${item.hashtag}</span> | |
| </span> | |
| </div> | |
| <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full"> | |
| <i class="fas fa-chart-line ${percentageClass} mr-1"></i> | |
| ${marketShare.percentage}% | |
| <span class="ml-1 ${percentageClass}">(${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)</span> | |
| </span> | |
| </div> | |
| <div class="flex justify-between items-center mb-3"> | |
| <span class="text-xs text-gray-500">${item.current_searches.toLocaleString()} searches</span> | |
| <span class="text-xs px-2 py-1 rounded-full ${categoryMeta.color} text-white"> | |
| ${categoryMeta.name} | |
| </span> | |
| </div> | |
| <div class="flex justify-between items-center text-xs text-gray-500"> | |
| <span> | |
| <span class="${getSentimentClass(item.sentiment).replace('sentiment-', 'text-')}"> | |
| <i class="fas ${item.sentiment > 20 ? 'fa-smile' : item.sentiment < -20 ? 'fa-frown' : 'fa-meh'}"></i> | |
| ${getSentimentLabel(item.sentiment)} | |
| </span> | |
| </span> | |
| <button class="save-trend-btn text-gray-400 hover:text-dmim-bg" data-hashtag="${item.hashtag}"> | |
| <i class="fas fa-bookmark"></i> Save | |
| </button> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| card.addEventListener('click', function() { | |
| openSentimentModal(item.hashtag); | |
| }); | |
| }); | |
| document.querySelectorAll('.save-trend-btn').forEach(btn => { | |
| btn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| const hashtag = this.getAttribute('data-hashtag'); | |
| saveTrend(hashtag); | |
| }); | |
| }); | |
| } | |
| // Function to render saved trends | |
| function renderSavedTrends() { | |
| const container = document.getElementById('savedTrendsContainer'); | |
| container.innerHTML = ''; | |
| const trendSelect = document.getElementById('trendSelect'); | |
| trendSelect.innerHTML = '<option value="">-- Select a saved trend --</option>'; | |
| userData.savedTrends.forEach(trend => { | |
| const marketShare = calculateMarketShare(trend); | |
| const sentimentClass = getSentimentClass(trend.user_sentiment !== null ? trend.user_sentiment : trend.sentiment); | |
| const categoryMeta = getCategoryMeta(trend.category); | |
| const element = document.createElement('div'); | |
| element.className = `flex items-center p-3 rounded-lg bg-white shadow-sm cursor-pointer hover:bg-gray-50 ${sentimentClass}`; | |
| element.innerHTML = ` | |
| <div class="flex-shrink-0 mr-3"> | |
| <div class="w-8 h-8 rounded-full flex items-center justify-center text-white ${categoryMeta.color}"> | |
| ${getPlatformIcon(trend.category)} | |
| </div> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="flex items-center"> | |
| <h4 class="font-medium text-sm">${trend.hashtag}</h4> | |
| ${trend.staked_amount > 0 ? '<span class="ml-1 text-green-500"><i class="fas fa-check-circle"></i></span>' : ''} | |
| </div> | |
| <div class="flex items-center"> | |
| <p class="text-gray-500 text-xs mr-2">${categoryMeta.name}</p> | |
| <span class="text-xs ${percentageClass}"> | |
| ${marketShare.percentage}% (${marketShare.change > 0 ? '+' : ''}${marketShare.change}%) | |
| </span> | |
| </div> | |
| </div> | |
| <button class="text-gray-500 hover:text-dmim-bg sentiment-btn" data-hashtag="${trend.hashtag}"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| `; | |
| container.appendChild(element); | |
| const option = document.createElement('option'); | |
| option.value = trend.hashtag; | |
| option.textContent = trend.hashtag; | |
| trendSelect.appendChild(option); | |
| element.addEventListener('click', function() { | |
| openSentimentModal(trend.hashtag); | |
| }); | |
| }); | |
| document.getElementById('dmimBalance').textContent = userData.dmimBalance + ' DMIM'; | |
| document.getElementById('dmimBalanceDisplay').textContent = userData.dmimBalance + ' DMIM'; | |
| } | |
| // Function to open sentiment modal | |
| async function openSentimentModal(hashtag) { | |
| currentSentimentTrend = hashtag; | |
| const trendData = allTrends.find(t => t.hashtag === hashtag); | |
| if (!trendData) { | |
| showToast('Trend data not found for sentiment adjustment.'); | |
| return; | |
| } | |
| document.getElementById('sentimentTrendName').textContent = hashtag; | |
| document.getElementById('sentimentSlider').value = trendData.user_sentiment !== null ? trendData.user_sentiment : trendData.sentiment; | |
| document.getElementById('sentimentHeadline').value = trendData.user_sentiment_headline || trendData.sentiment_headline || ""; | |
| updateSentimentSlider(document.getElementById('sentimentSlider').value); | |
| document.getElementById('sentimentModal').classList.remove('hidden'); | |
| } | |
| // Function to update sentiment slider appearance | |
| function updateSentimentSlider(value) { | |
| const slider = document.getElementById('sentimentSlider'); | |
| slider.value = value; | |
| slider.classList.remove('positive', 'negative', 'neutral'); | |
| if (value > 20) { | |
| slider.classList.add('positive'); | |
| } else if (value < -20) { | |
| slider.classList.add('negative'); | |
| } else { | |
| slider.classList.add('neutral'); | |
| } | |
| const impactText = document.getElementById('sentimentImpactText'); | |
| if (value > 20) { | |
| impactText.textContent = `Positive sentiment will boost growth by ${Math.round(value/2)}%.`; | |
| impactText.className = "text-sm text-green-600"; | |
| } else if (value < -20) { | |
| impactText.textContent = `Negative sentiment will reduce growth by ${Math.round(Math.abs(value)/2)}%.`; | |
| impactText.className = "text-sm text-red-600"; | |
| } else { | |
| impactText.textContent = "Neutral sentiment will not affect trend growth."; | |
| impactText.className = "text-sm text-gray-600"; | |
| } | |
| } | |
| // Function to show search results | |
| async function showSearchResults(query) { | |
| const container = document.getElementById('searchResultsContainer'); | |
| container.innerHTML = '<p class="text-center text-gray-500 mt-8">Searching...</p>'; | |
| if (!query) { | |
| container.innerHTML = ''; | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/search?query=${encodeURIComponent(query)}`); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || `HTTP error! status: ${response.status}`); | |
| } | |
| const results = await response.json(); | |
| container.innerHTML = ''; | |
| if (results.length === 0) { | |
| container.innerHTML = '<p class="text-center text-gray-500 mt-8">No results found. Consider adding it as a new trend!</p>'; | |
| showToast(`No results found for ${query}`); | |
| return; | |
| } | |
| results.forEach(item => { | |
| const marketShare = calculateMarketShare(item); | |
| const percentageClass = getPercentageClass(marketShare.change); | |
| const sentimentClass = getSentimentClass(item.sentiment); | |
| const categoryMeta = getCategoryMeta(item.category); | |
| const card = document.createElement('div'); | |
| card.className = `trend-card bg-white rounded-lg shadow-sm p-4 h-40 flex flex-col justify-between transition cursor-pointer ${sentimentClass}`; | |
| card.innerHTML = ` | |
| <div class="flex items-start justify-between mb-2"> | |
| <div> | |
| <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full"> | |
| ${getPlatformIcon(item.category)} | |
| <span class="ml-1">${item.hashtag}</span> | |
| </span> | |
| </div> | |
| <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full"> | |
| <i class="fas fa-chart-line ${percentageClass} mr-1"></i> | |
| ${marketShare.percentage}% | |
| <span class="ml-1 ${percentageClass}">(${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)</span> | |
| </span> | |
| </div> | |
| <div class="flex justify-between items-center mb-3"> | |
| <span class="text-xs text-gray-500">${item.current_searches.toLocaleString()} searches</span> | |
| <span class="text-xs px-2 py-1 rounded-full ${categoryMeta.color} text-white"> | |
| ${categoryMeta.name} | |
| </span> | |
| </div> | |
| <div class="flex justify-between items-center text-xs text-gray-500"> | |
| <span> | |
| <span class="${getSentimentClass(item.sentiment).replace('sentiment-', 'text-')}"> | |
| <i class="fas ${item.sentiment > 20 ? 'fa-smile' : item.sentiment < -20 ? 'fa-frown' : 'fa-meh'}"></i> | |
| ${getSentimentLabel(item.sentiment)} | |
| </span> | |
| </span> | |
| <button class="save-trend-btn text-gray-400 hover:text-dmim-bg" data-hashtag="${item.hashtag}"> | |
| <i class="fas fa-bookmark"></i> Save | |
| </button> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| card.addEventListener('click', function() { | |
| openSentimentModal(item.hashtag); | |
| }); | |
| }); | |
| document.querySelectorAll('.save-trend-btn').forEach(btn => { | |
| btn.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| const hashtag = this.getAttribute('data-hashtag'); | |
| saveTrend(hashtag); | |
| }); | |
| }); | |
| showToast(`Found ${results.length} results for ${query}`); | |
| } catch (error) { | |
| console.error("Error searching trends:", error); | |
| showToast(`Error searching for ${query}: ${error.message}`); | |
| container.innerHTML = '<p class="text-center text-red-500 mt-8">Failed to fetch search results.</p>'; | |
| } | |
| } | |
| // Function to save a trend (calls backend API) | |
| async function saveTrend(hashtag) { | |
| if (userData.savedTrends.some(t => t.hashtag === hashtag)) { | |
| showToast('This trend is already saved'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends/${encodeURIComponent(hashtag)}/save`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || `HTTP error! status: ${response.status}`); | |
| } | |
| await initApp(); | |
| showToast(`${hashtag} saved to your library`); | |
| } catch (error) { | |
| console.error("Error saving trend:", error); | |
| showToast(`Error saving ${hashtag}: ${error.message}`); | |
| } | |
| } | |
| // Function to stake DMIM to a trend (calls backend API) | |
| async function stakeDmim(hashtag, amount) { | |
| if (!hashtag || !amount || amount <= 0) { | |
| showToast('Please select a trend and enter a valid amount'); | |
| return; | |
| } | |
| if (amount > userData.dmimBalance) { | |
| showToast('Insufficient DMIM balance'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends/${encodeURIComponent(hashtag)}/stake`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ amount: amount }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || `HTTP error! status: ${response.status}`); | |
| } | |
| userData.dmimBalance -= amount; // Optimistic update | |
| await initApp(); | |
| showToast(`Staked ${amount} DMIM to ${hashtag}`); | |
| } catch (error) { | |
| console.error("Error staking DMIM:", error); | |
| showToast(`Error staking DMIM to ${hashtag}: ${error.message}`); | |
| } | |
| } | |
| // Function to add DMIM tokens (client-side simulation for demo) | |
| async function addDmim(amount) { | |
| userData.dmimBalance += amount; | |
| renderSavedTrends(); | |
| showToast(`Added ${amount} DMIM to your balance`); | |
| } | |
| // Function to save sentiment for a trend (calls backend API) | |
| async function saveSentiment(hashtag, sentiment, headline) { | |
| try { | |
| const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends/${encodeURIComponent(hashtag)}/sentiment`, { | |
| method: 'PUT', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ sentiment: sentiment, headline: headline }) | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || `HTTP error! status: ${response.status}`); | |
| } | |
| await initApp(); | |
| showToast(`Sentiment updated for ${hashtag}`); | |
| } catch (error) { | |
| console.error("Error saving sentiment:", error); | |
| showToast(`Error updating sentiment for ${hashtag}: ${error.message}`); | |
| } finally { | |
| document.getElementById('sentimentModal').classList.add('hidden'); | |
| } | |
| } | |
| // Initialize the app - fetches all data from backend | |
| async function initApp() { | |
| try { | |
| // Fetch all trends | |
| const trendsResponse = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends`); | |
| if (!trendsResponse.ok) { | |
| const errorData = await trendsResponse.json(); | |
| throw new Error(errorData.error || `Failed to fetch trends with status: ${trendsResponse.status}`); | |
| } | |
| allTrends = await trendsResponse.json(); | |
| // Filter saved trends for the local userData object | |
| userData.savedTrends = allTrends.filter(t => t.is_saved_by_user); | |
| // Fetch DMIM balance | |
| const dmimResponse = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/dmim_balance`); | |
| if (dmimResponse.ok) { | |
| const dmimData = await dmimResponse.json(); | |
| userData.dmimBalance = dmimData.balance; | |
| } else { | |
| console.warn("Could not fetch DMIM balance, using default for demo."); | |
| userData.dmimBalance = 1000; | |
| } | |
| renderTrendingCards(); | |
| renderSavedTrends(); | |
| } catch (error) { | |
| console.error("Failed to initialize app from backend:", error); | |
| showToast(`Failed to load data: ${error.message}. Please try again.`); | |
| } | |
| } | |
| // --- Event Listeners --- | |
| // Tab switching functionality | |
| document.querySelectorAll('.tab-button').forEach(button => { | |
| button.addEventListener('click', function() { | |
| document.querySelectorAll('.tab-button').forEach(btn => { | |
| btn.classList.remove('active', 'text-dmim-bg'); | |
| btn.classList.add('text-gray-500'); | |
| }); | |
| this.classList.add('active', 'text-dmim-bg'); | |
| this.classList.remove('text-gray-500'); | |
| document.querySelectorAll('#mainContent > div').forEach(tab => { | |
| tab.classList.add('hidden'); | |