// 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: '', color: 'bg-purple-500' }, theater: { name: "Theater", icon: '', color: 'bg-yellow-500' }, dance: { name: "Dance", icon: '', color: 'bg-pink-500' }, comedy: { name: "Comedy", icon: '', color: 'bg-blue-500' }, emerging: { name: "Emerging", icon: '', color: 'bg-green-500' } }; // Helper to get category metadata function getCategoryMeta(categoryKey) { return performanceCategoriesMeta[categoryKey] || { name: categoryKey, icon: '', 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 = `
${getPlatformIcon(item.category)} ${item.hashtag}
${marketShare.percentage}% (${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)
${item.current_searches.toLocaleString()} searches ${categoryMeta.name}
${getSentimentLabel(item.sentiment)}
`; 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 = ''; 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 = `
${getPlatformIcon(trend.category)}

${trend.hashtag}

${trend.staked_amount > 0 ? '' : ''}

${categoryMeta.name}

${marketShare.percentage}% (${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)
`; 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 = '

Searching...

'; 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 = '

No results found. Consider adding it as a new trend!

'; 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 = `
${getPlatformIcon(item.category)} ${item.hashtag}
${marketShare.percentage}% (${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)
${item.current_searches.toLocaleString()} searches ${categoryMeta.name}
${getSentimentLabel(item.sentiment)}
`; 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 = '

Failed to fetch search results.

'; } } // 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');