Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
<title>DMIM - Performance Art Trend Explorer</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<link rel="stylesheet" href="styles.css" | |
/* Your existing CSS styles */ | |
::-webkit-scrollbar { width: 4px; } | |
::-webkit-scrollbar-track { background: #f1f1f1; } | |
::-webkit-scrollbar-thumb { background: #4a6fdc; border-radius: 3px; } | |
::-webkit-scrollbar-thumb:hover { background: #3a5bc7; } | |
.trend-card:hover { transform: translateY(-2px); } | |
.dmim-bg { background-color: #4a6fdc; } | |
* { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } | |
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
.explainer-animate { animation: fadeIn 0.3s ease-out forwards; } | |
.percentage-up { color: #10b981; } | |
.percentage-down { color: #ef4444; } | |
.percentage-neutral { color: #6b7280; } | |
.sentiment-positive { background-color: rgba(16, 185, 129, 0.1); border-left: 3px solid #10b981; } | |
.sentiment-negative { background-color: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; } | |
.sentiment-neutral { background-color: rgba(156, 163, 175, 0.1); border-left: 3px solid #9ca3af; } | |
.sentiment-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; cursor: pointer; } | |
.sentiment-slider.positive::-webkit-slider-thumb { background: #10b981; } | |
.sentiment-slider.negative::-webkit-slider-thumb { background: #ef4444; } | |
.sentiment-slider.neutral::-webkit-slider-thumb { background: #6b7280; } | |
/* Floating button styles */ | |
.floating-btn { position: fixed; bottom: 80px; right: 20px; width: 50px; height: 50px; border-radius: 50%; background-color: #4a6fdc; color: white; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); z-index: 40; cursor: pointer; transition: all 0.3s ease; } | |
.floating-btn:hover { transform: scale(1.1); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); } | |
/* Legend modal styles */ | |
.legend-item { display: flex; align-items: center; margin-bottom: 12px; } | |
.legend-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 12px; flex-shrink: 0; } | |
</style> | |
</head> | |
<body class="bg-gray-50 font-sans text-gray-800"> | |
<script src="script.js"></script> | |
<script> | |
// ... | |
const BACKEND_API_BASE_URL = "/api"; // This relative path will be handled by your FastAPI proxy | |
// ... (rest of your frontend logic to interact with backend endpoints) | |
// Example: | |
async function fetchAllTrends() { | |
try { | |
const response = await fetch(`${BACKEND_API_BASE_URL}/trends`); | |
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |
const data = await response.json(); | |
// Process data and update UI | |
return data; | |
} catch (error) { | |
console.error("Error fetching trends:", error); | |
showToast('Error fetching trends.'); | |
return []; | |
} | |
} | |
// Ensure your static assets are correctly linked | |
// e.g., <link rel="stylesheet" href="/static/styles.css"> | |
// <script src="/static/script.js"></script> | |
</script> | |
<script> | |
// 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 | |
// and don't need to be fetched from the database. | |
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 | |
// This logic remains client-side, using current_searches and sentiment from fetched D1 data. | |
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; | |
// Use the sentiment value directly from the fetched trend object (which comes from D1) | |
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 (remain unchanged) | |
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); | |
// Prioritize user_sentiment if explicitly set, else use global sentiment | |
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; | |
// Display either user_sentiment (if explicitly set) or global sentiment from the database | |
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 (logic remains same) | |
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) { | |
// Check if already saved locally to prevent unnecessary API calls | |
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) | |
// In a real app, this would be an API call to a proper financial system | |
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; // Fallback for demo if endpoint fails | |
} | |
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 (remain mostly the same, call updated async functions) --- | |
// 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'); | |
}); | |
const tabId = this.getAttribute('data-tab'); | |
document.getElementById(tabId).classList.remove('hidden'); | |
document.getElementById('mainContent').scrollTo(0, 0); | |
if (tabId === 'searchTab') { | |
setTimeout(() => { document.getElementById('trendSearchInput').focus(); }, 100); | |
} | |
}); | |
}); | |
// Search trend functionality | |
document.getElementById('searchTrendBtn').addEventListener('click', function() { | |
const query = document.getElementById('trendSearchInput').value.trim(); | |
showSearchResults(query); | |
}); | |
document.getElementById('trendSearchInput').addEventListener('keypress', function(e) { | |
if (e.key === 'Enter') { | |
const query = this.value.trim(); | |
showSearchResults(query); | |
} | |
}); | |
// Add trend button | |
document.getElementById('addTrendBtn').addEventListener('click', function() { | |
document.getElementById('addTrendModal').classList.remove('hidden'); | |
// Reset fields for new trend | |
document.getElementById('newHashtag').value = ''; | |
document.querySelectorAll('.platform-btn').forEach(b => { | |
b.classList.remove('bg-dmim-bg', 'text-white'); | |
b.classList.add('bg-gray-200', 'text-gray-700'); | |
}); | |
}); | |
// Cancel add trend | |
document.getElementById('cancelAddTrend').addEventListener('click', function() { | |
document.getElementById('addTrendModal').classList.add('hidden'); | |
}); | |
// Save new trend | |
document.getElementById('saveTrend').addEventListener('click', async function() { | |
const hashtag = document.getElementById('newHashtag').value.trim(); | |
const platform = document.querySelector('.platform-btn.bg-dmim-bg')?.getAttribute('data-platform'); | |
if (!hashtag || !platform) { | |
showToast('Please enter a hashtag and select a platform'); | |
return; | |
} | |
try { | |
const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends`, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ | |
hashtag: hashtag, | |
category: platform, | |
current_searches: 1000, | |
previous_searches: 0, | |
sentiment: 0, | |
sentiment_headline: "", | |
is_saved_by_user: true, // Auto-save when creating | |
staked_amount: 0, | |
user_sentiment: 0, | |
user_sentiment_headline: "" | |
}) | |
}); | |
if (!response.ok) { | |
const errorData = await response.json(); | |
throw new Error(errorData.error || `HTTP error! status: ${response.status}`); | |
} | |
await initApp(); | |
document.getElementById('addTrendModal').classList.add('hidden'); | |
showToast(`${hashtag} added and saved to your library`); | |
} catch (error) { | |
console.error("Error adding new trend:", error); | |
showToast(`Error adding new trend ${hashtag}: ${error.message}`); | |
} | |
}); | |
// Platform selection in add trend modal | |
document.querySelectorAll('.platform-btn').forEach(btn => { | |
btn.addEventListener('click', function() { | |
document.querySelectorAll('.platform-btn').forEach(b => { | |
b.classList.remove('bg-dmim-bg', 'text-white'); | |
b.classList.add('bg-gray-200', 'text-gray-700'); | |
}); | |
this.classList.remove('bg-gray-200', 'text-gray-700'); | |
this.classList.add('bg-dmim-bg', 'text-white'); | |
}); | |
}); | |
// Stake DMIM button | |
document.getElementById('stakeBtn').addEventListener('click', function() { | |
const hashtag = document.getElementById('trendSelect').value; | |
const amount = parseFloat(document.getElementById('stakeAmount').value); | |
stakeDmim(hashtag, amount); | |
}); | |
// DMIM balance button | |
document.getElementById('dmimBalanceBtn').addEventListener('click', function() { | |
document.getElementById('dmimModal').classList.remove('hidden'); | |
}); | |
// Close DMIM modal | |
document.getElementById('closeDmimModal').addEventListener('click', function() { | |
document.getElementById('dmimModal').classList.add('hidden'); | |
}); | |
// Add DMIM button (calls client-side for demo) | |
document.getElementById('addDmimBtn').addEventListener('click', function() { | |
addDmim(1000); | |
}); | |
// Explainer button | |
document.getElementById('explainerBtn').addEventListener('click', function() { | |
document.getElementById('explainerModal').classList.remove('hidden'); | |
}); | |
// Close explainer modal | |
document.getElementById('closeExplainerModal').addEventListener('click', function() { | |
document.getElementById('explainerModal').classList.add('hidden'); | |
}); | |
// Close explainer button | |
document.getElementById('closeExplainerBtn').addEventListener('click', function() { | |
document.getElementById('explainerModal').classList.add('hidden'); | |
}); | |
// Stake info button | |
document.getElementById('stakeInfoBtn').addEventListener('click', function() { | |
document.getElementById('explainerModal').classList.remove('hidden'); | |
}); | |
// Sentiment slider change | |
document.getElementById('sentimentSlider').addEventListener('input', function() { | |
updateSentimentSlider(this.value); | |
}); | |
// Save sentiment (calls backend API) | |
document.getElementById('saveSentiment').addEventListener('click', function() { | |
const sentiment = parseInt(document.getElementById('sentimentSlider').value); | |
const headline = document.getElementById('sentimentHeadline').value.trim(); | |
saveSentiment(currentSentimentTrend, sentiment, headline); | |
}); | |
// Cancel sentiment | |
document.getElementById('cancelSentiment').addEventListener('click', function() { | |
document.getElementById('sentimentModal').classList.add('hidden'); | |
}); | |
// Sentiment help button | |
document.getElementById('sentimentHelpBtn').addEventListener('click', function() { | |
document.getElementById('sentimentLegendModal').classList.remove('hidden'); | |
}); | |
// Close legend modal | |
document.getElementById('closeLegendModal').addEventListener('click', function() { | |
document.getElementById('sentimentLegendModal').classList.add('hidden'); | |
}); | |
// Close legend button | |
document.getElementById('closeLegendBtn').addEventListener('click', function() { | |
document.getElementById('sentimentLegendModal').classList.add('hidden'); | |
}); | |
// Toast notification function | |
function showToast(message) { | |
const toast = document.getElementById('toast'); | |
const toastMessage = document.getElementById('toastMessage'); | |
toastMessage.textContent = message; | |
toast.classList.remove('hidden'); | |
setTimeout(() => { | |
toast.classList.add('hidden'); | |
}, 3000); | |
} | |
// Initialize the app when DOM is loaded | |
document.addEventListener('DOMContentLoaded', initApp); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=privateuserh/privdmi2-01pa" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p> | |
</body> | |
</html> | |