privateuserh commited on
Commit
4db9a6b
·
verified ·
1 Parent(s): e642dfc

Upload index.html

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