dmimtrends / script.js
privateuserh's picture
Update script.js
924c40e verified
// 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');