privateuserh commited on
Commit
fe59e74
·
verified ·
1 Parent(s): f696d54

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +692 -0
index.html CHANGED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>DMIM - Performance Art Trend Explorer</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ /* Your existing CSS styles */
11
+ ::-webkit-scrollbar { width: 4px; }
12
+ ::-webkit-scrollbar-track { background: #f1f1f1; }
13
+ ::-webkit-scrollbar-thumb { background: #4a6fdc; border-radius: 3px; }
14
+ ::-webkit-scrollbar-thumb:hover { background: #3a5bc7; }
15
+ .trend-card:hover { transform: translateY(-2px); }
16
+ .dmim-bg { background-color: #4a6fdc; }
17
+ * { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
18
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
19
+ .explainer-animate { animation: fadeIn 0.3s ease-out forwards; }
20
+ .percentage-up { color: #10b981; }
21
+ .percentage-down { color: #ef4444; }
22
+ .percentage-neutral { color: #6b7280; }
23
+ .sentiment-positive { background-color: rgba(16, 185, 129, 0.1); border-left: 3px solid #10b981; }
24
+ .sentiment-negative { background-color: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; }
25
+ .sentiment-neutral { background-color: rgba(156, 163, 175, 0.1); border-left: 3px solid #9ca3af; }
26
+ .sentiment-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; cursor: pointer; }
27
+ .sentiment-slider.positive::-webkit-slider-thumb { background: #10b981; }
28
+ .sentiment-slider.negative::-webkit-slider-thumb { background: #ef4444; }
29
+ .sentiment-slider.neutral::-webkit-slider-thumb { background: #6b7280; }
30
+ /* Floating button styles */
31
+ .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; }
32
+ .floating-btn:hover { transform: scale(1.1); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); }
33
+ /* Legend modal styles */
34
+ .legend-item { display: flex; align-items: center; margin-bottom: 12px; }
35
+ .legend-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; margin-right: 12px; flex-shrink: 0; }
36
+ </style>
37
+ </head>
38
+ <body class="bg-gray-50 font-sans text-gray-800">
39
+ <script>
40
+ // Set the base URL for your deployed Cloudflare Worker API
41
+ // *** IMPORTANT: REPLACE THIS WITH THE ACTUAL URL OF YOUR DEPLOYED CLOUDFLARE WORKER ***
42
+ const CLOUDFLARE_WORKER_API_BASE_URL = 'https://dmim-worker-backend.<your-subdomain>.workers.dev';
43
+
44
+ // Global state, will be populated by fetch calls
45
+ let allTrends = [];
46
+ let userData = {
47
+ dmimBalance: 0,
48
+ savedTrends: []
49
+ };
50
+ let currentSentimentTrend = null;
51
+
52
+ // Static metadata for categories (icons, colors) - these are frontend-only display properties
53
+ // and don't need to be fetched from the database.
54
+ const performanceCategoriesMeta = {
55
+ music: { name: "Music", icon: '<i class="fas fa-music text-purple-500"></i>', color: 'bg-purple-500' },
56
+ theater: { name: "Theater", icon: '<i class="fas fa-theater-masks text-yellow-500"></i>', color: 'bg-yellow-500' },
57
+ dance: { name: "Dance", icon: '<i class="fas fa-child text-pink-500"></i>', color: 'bg-pink-500' },
58
+ comedy: { name: "Comedy", icon: '<i class="fas fa-laugh-squint text-blue-500"></i>', color: 'bg-blue-500' },
59
+ emerging: { name: "Emerging", icon: '<i class="fas fa-star text-green-500"></i>', color: 'bg-green-500' }
60
+ };
61
+
62
+ // Helper to get category metadata
63
+ function getCategoryMeta(categoryKey) {
64
+ return performanceCategoriesMeta[categoryKey] || { name: categoryKey, icon: '<i class="fas fa-hashtag"></i>', color: 'bg-gray-500' };
65
+ }
66
+
67
+ // Function to calculate market share percentage with sentiment adjustment
68
+ // This logic remains client-side, using current_searches and sentiment from fetched D1 data.
69
+ function calculateMarketShare(trend) {
70
+ const totalAllSearches = allTrends.reduce((sum, t) => sum + t.current_searches, 0);
71
+
72
+ const globalPercentage = totalAllSearches > 0 ? (trend.current_searches / totalAllSearches) * 100 : 0;
73
+
74
+ const rawChange = trend.previous_searches > 0
75
+ ? ((trend.current_searches - trend.previous_searches) / trend.previous_searches) * 100
76
+ : trend.current_searches > 0 ? 100 : 0;
77
+
78
+ // Use the sentiment value directly from the fetched trend object (which comes from D1)
79
+ const sentimentToUse = trend.sentiment || 0;
80
+ const sentimentMultiplier = 1 + (sentimentToUse / 200);
81
+ const adjustedChange = rawChange * sentimentMultiplier;
82
+
83
+ return {
84
+ percentage: adjustedChange > 0 ? globalPercentage.toFixed(2) : (globalPercentage * sentimentMultiplier).toFixed(2), // Apply sentiment to displayed percentage too
85
+ change: adjustedChange.toFixed(2),
86
+ rawChange: rawChange.toFixed(2),
87
+ category: trend.category,
88
+ sentiment: sentimentToUse,
89
+ sentimentHeadline: trend.sentiment_headline || ""
90
+ };
91
+ }
92
+
93
+ // Helper functions for CSS classes and labels (remain unchanged)
94
+ function getPercentageClass(change) {
95
+ const numChange = parseFloat(change);
96
+ if (numChange > 0) return 'percentage-up';
97
+ if (numChange < 0) return 'percentage-down';
98
+ return 'percentage-neutral';
99
+ }
100
+
101
+ function getSentimentClass(sentiment) {
102
+ const numSentiment = parseInt(sentiment);
103
+ if (numSentiment > 20) return 'sentiment-positive';
104
+ if (numSentiment < -20) return 'sentiment-negative';
105
+ return 'sentiment-neutral';
106
+ }
107
+
108
+ function getSentimentLabel(sentiment) {
109
+ const numSentiment = parseInt(sentiment);
110
+ if (numSentiment > 20) return 'Positive';
111
+ if (numSentiment < -20) return 'Negative';
112
+ return 'Neutral';
113
+ }
114
+
115
+ function getPlatformIcon(platform) {
116
+ return getCategoryMeta(platform).icon;
117
+ }
118
+
119
+ // Function to render trending cards
120
+ function renderTrendingCards() {
121
+ const container = document.getElementById('trendingCardsContainer');
122
+ container.innerHTML = '';
123
+
124
+ const topTrends = [...allTrends].sort((a, b) => b.current_searches - a.current_searches).slice(0, 8);
125
+
126
+ topTrends.forEach(item => {
127
+ const marketShare = calculateMarketShare(item);
128
+ const percentageClass = getPercentageClass(marketShare.change);
129
+ const sentimentClass = getSentimentClass(item.sentiment);
130
+ const categoryMeta = getCategoryMeta(item.category);
131
+
132
+ const card = document.createElement('div');
133
+ card.className = `trend-card bg-white rounded-lg shadow-sm p-4 h-40 flex flex-col justify-between transition cursor-pointer ${sentimentClass}`;
134
+ card.innerHTML = `
135
+ <div class="flex items-start justify-between mb-2">
136
+ <div>
137
+ <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full">
138
+ ${getPlatformIcon(item.category)}
139
+ <span class="ml-1">${item.hashtag}</span>
140
+ </span>
141
+ </div>
142
+ <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full">
143
+ <i class="fas fa-chart-line ${percentageClass} mr-1"></i>
144
+ ${marketShare.percentage}%
145
+ <span class="ml-1 ${percentageClass}">(${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)</span>
146
+ </span>
147
+ </div>
148
+ <div class="flex justify-between items-center mb-3">
149
+ <span class="text-xs text-gray-500">${item.current_searches.toLocaleString()} searches</span>
150
+ <span class="text-xs px-2 py-1 rounded-full ${categoryMeta.color} text-white">
151
+ ${categoryMeta.name}
152
+ </span>
153
+ </div>
154
+ <div class="flex justify-between items-center text-xs text-gray-500">
155
+ <span>
156
+ <span class="${getSentimentClass(item.sentiment).replace('sentiment-', 'text-')}">
157
+ <i class="fas ${item.sentiment > 20 ? 'fa-smile' : item.sentiment < -20 ? 'fa-frown' : 'fa-meh'}"></i>
158
+ ${getSentimentLabel(item.sentiment)}
159
+ </span>
160
+ </span>
161
+ <button class="save-trend-btn text-gray-400 hover:text-dmim-bg" data-hashtag="${item.hashtag}">
162
+ <i class="fas fa-bookmark"></i> Save
163
+ </button>
164
+ </div>
165
+ `;
166
+ container.appendChild(card);
167
+
168
+ card.addEventListener('click', function() {
169
+ openSentimentModal(item.hashtag);
170
+ });
171
+ });
172
+
173
+ document.querySelectorAll('.save-trend-btn').forEach(btn => {
174
+ btn.addEventListener('click', function(e) {
175
+ e.stopPropagation();
176
+ const hashtag = this.getAttribute('data-hashtag');
177
+ saveTrend(hashtag);
178
+ });
179
+ });
180
+ }
181
+
182
+ // Function to render saved trends
183
+ function renderSavedTrends() {
184
+ const container = document.getElementById('savedTrendsContainer');
185
+ container.innerHTML = '';
186
+
187
+ const trendSelect = document.getElementById('trendSelect');
188
+ trendSelect.innerHTML = '<option value="">-- Select a saved trend --</option>';
189
+
190
+ userData.savedTrends.forEach(trend => {
191
+ const marketShare = calculateMarketShare(trend);
192
+ // Prioritize user_sentiment if explicitly set, else use global sentiment
193
+ const sentimentClass = getSentimentClass(trend.user_sentiment !== null ? trend.user_sentiment : trend.sentiment);
194
+ const categoryMeta = getCategoryMeta(trend.category);
195
+
196
+ const element = document.createElement('div');
197
+ element.className = `flex items-center p-3 rounded-lg bg-white shadow-sm cursor-pointer hover:bg-gray-50 ${sentimentClass}`;
198
+ element.innerHTML = `
199
+ <div class="flex-shrink-0 mr-3">
200
+ <div class="w-8 h-8 rounded-full flex items-center justify-center text-white ${categoryMeta.color}">
201
+ ${getPlatformIcon(trend.category)}
202
+ </div>
203
+ </div>
204
+ <div class="flex-1">
205
+ <div class="flex items-center">
206
+ <h4 class="font-medium text-sm">${trend.hashtag}</h4>
207
+ ${trend.staked_amount > 0 ? '<span class="ml-1 text-green-500"><i class="fas fa-check-circle"></i></span>' : ''}
208
+ </div>
209
+ <div class="flex items-center">
210
+ <p class="text-gray-500 text-xs mr-2">${categoryMeta.name}</p>
211
+ <span class="text-xs ${percentageClass}">
212
+ ${marketShare.percentage}% (${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)
213
+ </span>
214
+ </div>
215
+ </div>
216
+ <button class="text-gray-500 hover:text-dmim-bg sentiment-btn" data-hashtag="${trend.hashtag}">
217
+ <i class="fas fa-ellipsis-v"></i>
218
+ </button>
219
+ `;
220
+ container.appendChild(element);
221
+
222
+ const option = document.createElement('option');
223
+ option.value = trend.hashtag;
224
+ option.textContent = trend.hashtag;
225
+ trendSelect.appendChild(option);
226
+
227
+ element.addEventListener('click', function() {
228
+ openSentimentModal(trend.hashtag);
229
+ });
230
+ });
231
+
232
+ document.getElementById('dmimBalance').textContent = userData.dmimBalance + ' DMIM';
233
+ document.getElementById('dmimBalanceDisplay').textContent = userData.dmimBalance + ' DMIM';
234
+ }
235
+
236
+ // Function to open sentiment modal
237
+ async function openSentimentModal(hashtag) {
238
+ currentSentimentTrend = hashtag;
239
+
240
+ const trendData = allTrends.find(t => t.hashtag === hashtag);
241
+
242
+ if (!trendData) {
243
+ showToast('Trend data not found for sentiment adjustment.');
244
+ return;
245
+ }
246
+
247
+ document.getElementById('sentimentTrendName').textContent = hashtag;
248
+ // Display either user_sentiment (if explicitly set) or global sentiment from the database
249
+ document.getElementById('sentimentSlider').value = trendData.user_sentiment !== null ? trendData.user_sentiment : trendData.sentiment;
250
+ document.getElementById('sentimentHeadline').value = trendData.user_sentiment_headline || trendData.sentiment_headline || "";
251
+
252
+ updateSentimentSlider(document.getElementById('sentimentSlider').value);
253
+
254
+ document.getElementById('sentimentModal').classList.remove('hidden');
255
+ }
256
+
257
+ // Function to update sentiment slider appearance (logic remains same)
258
+ function updateSentimentSlider(value) {
259
+ const slider = document.getElementById('sentimentSlider');
260
+ slider.value = value;
261
+ slider.classList.remove('positive', 'negative', 'neutral');
262
+ if (value > 20) {
263
+ slider.classList.add('positive');
264
+ } else if (value < -20) {
265
+ slider.classList.add('negative');
266
+ } else {
267
+ slider.classList.add('neutral');
268
+ }
269
+
270
+ const impactText = document.getElementById('sentimentImpactText');
271
+ if (value > 20) {
272
+ impactText.textContent = `Positive sentiment will boost growth by ${Math.round(value/2)}%.`;
273
+ impactText.className = "text-sm text-green-600";
274
+ } else if (value < -20) {
275
+ impactText.textContent = `Negative sentiment will reduce growth by ${Math.round(Math.abs(value)/2)}%.`;
276
+ impactText.className = "text-sm text-red-600";
277
+ } else {
278
+ impactText.textContent = "Neutral sentiment will not affect trend growth.";
279
+ impactText.className = "text-sm text-gray-600";
280
+ }
281
+ }
282
+
283
+ // Function to show search results
284
+ async function showSearchResults(query) {
285
+ const container = document.getElementById('searchResultsContainer');
286
+ container.innerHTML = '<p class="text-center text-gray-500 mt-8">Searching...</p>';
287
+
288
+ if (!query) {
289
+ container.innerHTML = '';
290
+ return;
291
+ }
292
+
293
+ try {
294
+ const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/search?query=${encodeURIComponent(query)}`);
295
+ if (!response.ok) {
296
+ const errorData = await response.json();
297
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
298
+ }
299
+ const results = await response.json();
300
+
301
+ container.innerHTML = '';
302
+ if (results.length === 0) {
303
+ container.innerHTML = '<p class="text-center text-gray-500 mt-8">No results found. Consider adding it as a new trend!</p>';
304
+ showToast(`No results found for ${query}`);
305
+ return;
306
+ }
307
+
308
+ results.forEach(item => {
309
+ const marketShare = calculateMarketShare(item);
310
+ const percentageClass = getPercentageClass(marketShare.change);
311
+ const sentimentClass = getSentimentClass(item.sentiment);
312
+ const categoryMeta = getCategoryMeta(item.category);
313
+
314
+ const card = document.createElement('div');
315
+ card.className = `trend-card bg-white rounded-lg shadow-sm p-4 h-40 flex flex-col justify-between transition cursor-pointer ${sentimentClass}`;
316
+ card.innerHTML = `
317
+ <div class="flex items-start justify-between mb-2">
318
+ <div>
319
+ <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full">
320
+ ${getPlatformIcon(item.category)}
321
+ <span class="ml-1">${item.hashtag}</span>
322
+ </span>
323
+ </div>
324
+ <span class="inline-flex items-center bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full">
325
+ <i class="fas fa-chart-line ${percentageClass} mr-1"></i>
326
+ ${marketShare.percentage}%
327
+ <span class="ml-1 ${percentageClass}">(${marketShare.change > 0 ? '+' : ''}${marketShare.change}%)</span>
328
+ </span>
329
+ </div>
330
+ <div class="flex justify-between items-center mb-3">
331
+ <span class="text-xs text-gray-500">${item.current_searches.toLocaleString()} searches</span>
332
+ <span class="text-xs px-2 py-1 rounded-full ${categoryMeta.color} text-white">
333
+ ${categoryMeta.name}
334
+ </span>
335
+ </div>
336
+ <div class="flex justify-between items-center text-xs text-gray-500">
337
+ <span>
338
+ <span class="${getSentimentClass(item.sentiment).replace('sentiment-', 'text-')}">
339
+ <i class="fas ${item.sentiment > 20 ? 'fa-smile' : item.sentiment < -20 ? 'fa-frown' : 'fa-meh'}"></i>
340
+ ${getSentimentLabel(item.sentiment)}
341
+ </span>
342
+ </span>
343
+ <button class="save-trend-btn text-gray-400 hover:text-dmim-bg" data-hashtag="${item.hashtag}">
344
+ <i class="fas fa-bookmark"></i> Save
345
+ </button>
346
+ </div>
347
+ `;
348
+ container.appendChild(card);
349
+
350
+ card.addEventListener('click', function() {
351
+ openSentimentModal(item.hashtag);
352
+ });
353
+ });
354
+
355
+
356
+ document.querySelectorAll('.save-trend-btn').forEach(btn => {
357
+ btn.addEventListener('click', function(e) {
358
+ e.stopPropagation();
359
+ const hashtag = this.getAttribute('data-hashtag');
360
+ saveTrend(hashtag);
361
+ });
362
+ });
363
+
364
+ showToast(`Found ${results.length} results for ${query}`);
365
+
366
+ } catch (error) {
367
+ console.error("Error searching trends:", error);
368
+ showToast(`Error searching for ${query}: ${error.message}`);
369
+ container.innerHTML = '<p class="text-center text-red-500 mt-8">Failed to fetch search results.</p>';
370
+ }
371
+ }
372
+
373
+ // Function to save a trend (calls backend API)
374
+ async function saveTrend(hashtag) {
375
+ // Check if already saved locally to prevent unnecessary API calls
376
+ if (userData.savedTrends.some(t => t.hashtag === hashtag)) {
377
+ showToast('This trend is already saved');
378
+ return;
379
+ }
380
+
381
+ try {
382
+ const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends/${encodeURIComponent(hashtag)}/save`, {
383
+ method: 'PUT',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ });
386
+ if (!response.ok) {
387
+ const errorData = await response.json();
388
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
389
+ }
390
+
391
+ await initApp();
392
+ showToast(`${hashtag} saved to your library`);
393
+ } catch (error) {
394
+ console.error("Error saving trend:", error);
395
+ showToast(`Error saving ${hashtag}: ${error.message}`);
396
+ }
397
+ }
398
+
399
+ // Function to stake DMIM to a trend (calls backend API)
400
+ async function stakeDmim(hashtag, amount) {
401
+ if (!hashtag || !amount || amount <= 0) {
402
+ showToast('Please select a trend and enter a valid amount');
403
+ return;
404
+ }
405
+
406
+ if (amount > userData.dmimBalance) {
407
+ showToast('Insufficient DMIM balance');
408
+ return;
409
+ }
410
+
411
+ try {
412
+ const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends/${encodeURIComponent(hashtag)}/stake`, {
413
+ method: 'PUT',
414
+ headers: { 'Content-Type': 'application/json' },
415
+ body: JSON.stringify({ amount: amount })
416
+ });
417
+ if (!response.ok) {
418
+ const errorData = await response.json();
419
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
420
+ }
421
+
422
+ userData.dmimBalance -= amount; // Optimistic update
423
+ await initApp();
424
+ showToast(`Staked ${amount} DMIM to ${hashtag}`);
425
+ } catch (error) {
426
+ console.error("Error staking DMIM:", error);
427
+ showToast(`Error staking DMIM to ${hashtag}: ${error.message}`);
428
+ }
429
+ }
430
+
431
+ // Function to add DMIM tokens (client-side simulation for demo)
432
+ // In a real app, this would be an API call to a proper financial system
433
+ async function addDmim(amount) {
434
+ userData.dmimBalance += amount;
435
+ renderSavedTrends();
436
+ showToast(`Added ${amount} DMIM to your balance`);
437
+ }
438
+
439
+ // Function to save sentiment for a trend (calls backend API)
440
+ async function saveSentiment(hashtag, sentiment, headline) {
441
+ try {
442
+ const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends/${encodeURIComponent(hashtag)}/sentiment`, {
443
+ method: 'PUT',
444
+ headers: { 'Content-Type': 'application/json' },
445
+ body: JSON.stringify({ sentiment: sentiment, headline: headline })
446
+ });
447
+ if (!response.ok) {
448
+ const errorData = await response.json();
449
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
450
+ }
451
+
452
+ await initApp();
453
+ showToast(`Sentiment updated for ${hashtag}`);
454
+ } catch (error) {
455
+ console.error("Error saving sentiment:", error);
456
+ showToast(`Error updating sentiment for ${hashtag}: ${error.message}`);
457
+ } finally {
458
+ document.getElementById('sentimentModal').classList.add('hidden');
459
+ }
460
+ }
461
+
462
+ // Initialize the app - fetches all data from backend
463
+ async function initApp() {
464
+ try {
465
+ // Fetch all trends
466
+ const trendsResponse = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends`);
467
+ if (!trendsResponse.ok) {
468
+ const errorData = await trendsResponse.json();
469
+ throw new Error(errorData.error || `Failed to fetch trends with status: ${trendsResponse.status}`);
470
+ }
471
+ allTrends = await trendsResponse.json();
472
+
473
+ // Filter saved trends for the local userData object
474
+ userData.savedTrends = allTrends.filter(t => t.is_saved_by_user);
475
+
476
+ // Fetch DMIM balance
477
+ const dmimResponse = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/dmim_balance`);
478
+ if (dmimResponse.ok) {
479
+ const dmimData = await dmimResponse.json();
480
+ userData.dmimBalance = dmimData.balance;
481
+ } else {
482
+ console.warn("Could not fetch DMIM balance, using default for demo.");
483
+ userData.dmimBalance = 1000; // Fallback for demo if endpoint fails
484
+ }
485
+
486
+ renderTrendingCards();
487
+ renderSavedTrends();
488
+
489
+ } catch (error) {
490
+ console.error("Failed to initialize app from backend:", error);
491
+ showToast(`Failed to load data: ${error.message}. Please try again.`);
492
+ }
493
+ }
494
+
495
+ // --- Event Listeners (remain mostly the same, call updated async functions) ---
496
+
497
+ // Tab switching functionality
498
+ document.querySelectorAll('.tab-button').forEach(button => {
499
+ button.addEventListener('click', function() {
500
+ document.querySelectorAll('.tab-button').forEach(btn => {
501
+ btn.classList.remove('active', 'text-dmim-bg');
502
+ btn.classList.add('text-gray-500');
503
+ });
504
+ this.classList.add('active', 'text-dmim-bg');
505
+ this.classList.remove('text-gray-500');
506
+ document.querySelectorAll('#mainContent > div').forEach(tab => {
507
+ tab.classList.add('hidden');
508
+ });
509
+ const tabId = this.getAttribute('data-tab');
510
+ document.getElementById(tabId).classList.remove('hidden');
511
+ document.getElementById('mainContent').scrollTo(0, 0);
512
+ if (tabId === 'searchTab') {
513
+ setTimeout(() => { document.getElementById('trendSearchInput').focus(); }, 100);
514
+ }
515
+ });
516
+ });
517
+
518
+ // Search trend functionality
519
+ document.getElementById('searchTrendBtn').addEventListener('click', function() {
520
+ const query = document.getElementById('trendSearchInput').value.trim();
521
+ showSearchResults(query);
522
+ });
523
+
524
+ document.getElementById('trendSearchInput').addEventListener('keypress', function(e) {
525
+ if (e.key === 'Enter') {
526
+ const query = this.value.trim();
527
+ showSearchResults(query);
528
+ }
529
+ });
530
+
531
+ // Add trend button
532
+ document.getElementById('addTrendBtn').addEventListener('click', function() {
533
+ document.getElementById('addTrendModal').classList.remove('hidden');
534
+ // Reset fields for new trend
535
+ document.getElementById('newHashtag').value = '';
536
+ document.querySelectorAll('.platform-btn').forEach(b => {
537
+ b.classList.remove('bg-dmim-bg', 'text-white');
538
+ b.classList.add('bg-gray-200', 'text-gray-700');
539
+ });
540
+ });
541
+
542
+ // Cancel add trend
543
+ document.getElementById('cancelAddTrend').addEventListener('click', function() {
544
+ document.getElementById('addTrendModal').classList.add('hidden');
545
+ });
546
+
547
+ // Save new trend
548
+ document.getElementById('saveTrend').addEventListener('click', async function() {
549
+ const hashtag = document.getElementById('newHashtag').value.trim();
550
+ const platform = document.querySelector('.platform-btn.bg-dmim-bg')?.getAttribute('data-platform');
551
+
552
+ if (!hashtag || !platform) {
553
+ showToast('Please enter a hashtag and select a platform');
554
+ return;
555
+ }
556
+
557
+ try {
558
+ const response = await fetch(`${CLOUDFLARE_WORKER_API_BASE_URL}/trends`, {
559
+ method: 'POST',
560
+ headers: { 'Content-Type': 'application/json' },
561
+ body: JSON.stringify({
562
+ hashtag: hashtag,
563
+ category: platform,
564
+ current_searches: 1000,
565
+ previous_searches: 0,
566
+ sentiment: 0,
567
+ sentiment_headline: "",
568
+ is_saved_by_user: true, // Auto-save when creating
569
+ staked_amount: 0,
570
+ user_sentiment: 0,
571
+ user_sentiment_headline: ""
572
+ })
573
+ });
574
+ if (!response.ok) {
575
+ const errorData = await response.json();
576
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
577
+ }
578
+
579
+ await initApp();
580
+ document.getElementById('addTrendModal').classList.add('hidden');
581
+ showToast(`${hashtag} added and saved to your library`);
582
+
583
+ } catch (error) {
584
+ console.error("Error adding new trend:", error);
585
+ showToast(`Error adding new trend ${hashtag}: ${error.message}`);
586
+ }
587
+ });
588
+
589
+ // Platform selection in add trend modal
590
+ document.querySelectorAll('.platform-btn').forEach(btn => {
591
+ btn.addEventListener('click', function() {
592
+ document.querySelectorAll('.platform-btn').forEach(b => {
593
+ b.classList.remove('bg-dmim-bg', 'text-white');
594
+ b.classList.add('bg-gray-200', 'text-gray-700');
595
+ });
596
+ this.classList.remove('bg-gray-200', 'text-gray-700');
597
+ this.classList.add('bg-dmim-bg', 'text-white');
598
+ });
599
+ });
600
+
601
+ // Stake DMIM button
602
+ document.getElementById('stakeBtn').addEventListener('click', function() {
603
+ const hashtag = document.getElementById('trendSelect').value;
604
+ const amount = parseFloat(document.getElementById('stakeAmount').value);
605
+ stakeDmim(hashtag, amount);
606
+ });
607
+
608
+ // DMIM balance button
609
+ document.getElementById('dmimBalanceBtn').addEventListener('click', function() {
610
+ document.getElementById('dmimModal').classList.remove('hidden');
611
+ });
612
+
613
+ // Close DMIM modal
614
+ document.getElementById('closeDmimModal').addEventListener('click', function() {
615
+ document.getElementById('dmimModal').classList.add('hidden');
616
+ });
617
+
618
+ // Add DMIM button (calls client-side for demo)
619
+ document.getElementById('addDmimBtn').addEventListener('click', function() {
620
+ addDmim(1000);
621
+ });
622
+
623
+ // Explainer button
624
+ document.getElementById('explainerBtn').addEventListener('click', function() {
625
+ document.getElementById('explainerModal').classList.remove('hidden');
626
+ });
627
+
628
+ // Close explainer modal
629
+ document.getElementById('closeExplainerModal').addEventListener('click', function() {
630
+ document.getElementById('explainerModal').classList.add('hidden');
631
+ });
632
+
633
+ // Close explainer button
634
+ document.getElementById('closeExplainerBtn').addEventListener('click', function() {
635
+ document.getElementById('explainerModal').classList.add('hidden');
636
+ });
637
+
638
+ // Stake info button
639
+ document.getElementById('stakeInfoBtn').addEventListener('click', function() {
640
+ document.getElementById('explainerModal').classList.remove('hidden');
641
+ });
642
+
643
+ // Sentiment slider change
644
+ document.getElementById('sentimentSlider').addEventListener('input', function() {
645
+ updateSentimentSlider(this.value);
646
+ });
647
+
648
+ // Save sentiment (calls backend API)
649
+ document.getElementById('saveSentiment').addEventListener('click', function() {
650
+ const sentiment = parseInt(document.getElementById('sentimentSlider').value);
651
+ const headline = document.getElementById('sentimentHeadline').value.trim();
652
+ saveSentiment(currentSentimentTrend, sentiment, headline);
653
+ });
654
+
655
+ // Cancel sentiment
656
+ document.getElementById('cancelSentiment').addEventListener('click', function() {
657
+ document.getElementById('sentimentModal').classList.add('hidden');
658
+ });
659
+
660
+ // Sentiment help button
661
+ document.getElementById('sentimentHelpBtn').addEventListener('click', function() {
662
+ document.getElementById('sentimentLegendModal').classList.remove('hidden');
663
+ });
664
+
665
+ // Close legend modal
666
+ document.getElementById('closeLegendModal').addEventListener('click', function() {
667
+ document.getElementById('sentimentLegendModal').classList.add('hidden');
668
+ });
669
+
670
+ // Close legend button
671
+ document.getElementById('closeLegendBtn').addEventListener('click', function() {
672
+ document.getElementById('sentimentLegendModal').classList.add('hidden');
673
+ });
674
+
675
+ // Toast notification function
676
+ function showToast(message) {
677
+ const toast = document.getElementById('toast');
678
+ const toastMessage = document.getElementById('toastMessage');
679
+
680
+ toastMessage.textContent = message;
681
+ toast.classList.remove('hidden');
682
+
683
+ setTimeout(() => {
684
+ toast.classList.add('hidden');
685
+ }, 3000);
686
+ }
687
+
688
+ // Initialize the app when DOM is loaded
689
+ document.addEventListener('DOMContentLoaded', initApp);
690
+ </script>
691
+ <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>
692
+ </html>