Lineage11 commited on
Commit
be62d20
·
verified ·
1 Parent(s): 4dda832

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +262 -160
index.html CHANGED
@@ -77,11 +77,11 @@
77
  transform: translateY(-20px); /* Start slightly up */
78
  opacity: 0;
79
  transition: all 0.3s ease;
80
- }
81
  .modal.active .modal-content {
82
  transform: translateY(0); /* Slide into place */
83
  opacity: 1;
84
- }
85
 
86
  /* Ripple Effect Styles */
87
  /* Apply to the clickable elements within the countdown */
@@ -132,25 +132,25 @@
132
  animation: float 15s ease-in-out infinite alternate;
133
  }
134
 
135
- /* Add variations for different particles */
136
- .particle:nth-child(2n) {
137
- width: 3px;
138
- height: 3px;
139
- background-color: rgba(0, 163, 165, 0.6); /* Teal color */
140
- animation-duration: 20s;
141
- }
142
- .particle:nth-child(3n) {
143
- width: 7px;
144
- height: 7px;
145
- background-color: rgba(138, 123, 113, 0.6); /* Warmgrey color */
146
- animation-duration: 25s;
147
- }
148
- .particle:nth-child(4n) {
149
- width: 4px;
150
- height: 4px;
151
- background-color: rgba(171, 138, 92, 0.6); /* Sandstone color */
152
- animation-duration: 18s;
153
- }
154
 
155
 
156
  /* Basic float keyframes (used as a fallback/starting point) */
@@ -178,15 +178,78 @@
178
  transition: width 1s linear; /* Smooth transition for the bar */
179
  }
180
 
181
- /* Countdown Digit Animation (Subtle Scale) */
182
- .countdown-digit > div { /* Target the inner div with the number */
183
- transition: transform 0.3s ease-out; /* Smooth transition */
184
- }
185
- /* Optional: Add a class in JS to trigger animation on update */
186
- .countdown-digit > div.animate-scale {
187
- transform: scale(1.1);
188
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  </style>
192
  </head>
@@ -199,13 +262,13 @@
199
  <div class="particle" style="top: 30%; left: 60%;"></div>
200
  <div class="particle" style="top: 70%; left: 10%;"></div>
201
  <div class="particle" style="top: 20%; left: 90%;"></div>
202
- <div class="particle" style="top: 60%; left: 30%;"></div>
203
- <div class="particle" style="top: 40%; left: 70%;"></div>
204
- <div class="particle" style="top: 90%; left: 50%;"></div>
205
- <div class="particle" style="top: 5%; left: 5%;"></div>
206
- <div class="particle" style="top: 95%; left: 95%;"></div>
207
- <div class="particle" style="top: 15%; left: 75%;"></div>
208
- <div class="particle" style="top: 85%; left: 25%;"></div>
209
  </div>
210
 
211
 
@@ -261,12 +324,40 @@
261
 
262
  <div class="text-grey">
263
  <p class="mt-2 text-sm">Stream will begin on: <span id="streamTimeDisplay" class="font-medium text-teal">Loading...</span> <span id="timezoneDisplay" class="font-medium text-eucalypt text-xs">(Your Local Time)</span></p>
264
- <p id="errorDisplay" class="mt-2 text-sm text-orange font-semibold"></p> </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  </div>
266
  </main>
267
 
268
  <footer class="py-6 text-center text-warmgrey text-sm relative z-10">
269
- <p>© <span id="currentYear"></span> Live Stream Countdown. All rights reserved.</p>
270
  </footer>
271
 
272
  <div id="adminModal" class="modal fixed inset-0 flex items-center justify-center p-4 z-50">
@@ -283,9 +374,9 @@
283
  <p class="mb-4 text-warmgrey text-left">
284
  This countdown is controlled by a Google Sheet. To update the stream date, time, or URL, you need to edit the sheet directly.
285
  </p>
286
- <p class="mb-4 text-orange text-sm text-left font-semibold">
287
- Remember: After editing the sheet, you MUST re-publish it to the web (as CSV) for changes to appear here on the page.
288
- </p>
289
 
290
  <a id="editSheetLink" href="https://docs.google.com/spreadsheets/d/1Qj4NF2jjrG7wnpvYDqu6Dvi59GDaIO8pzr_DLWJif-8/edit?gid=0#gid=0" target="_blank" class="inline-block w-full bg-teal hover:bg-opacity-80 text-white py-3 rounded font-bold transition duration-300 ease-in-out transform hover:scale-105">
291
  Edit Google Sheet
@@ -295,7 +386,7 @@
295
  </div>
296
  </div>
297
 
298
- <audio id="countdownEndSound" src="https://www.soundjay.com/button/sounds/button-1.mp3" preload="auto"></audio>
299
 
300
 
301
  <script>
@@ -312,6 +403,14 @@
312
  // Threshold for pre-stream message (in milliseconds)
313
  const preStreamThreshold = 5 * 60 * 1000; // 5 minutes
314
 
 
 
 
 
 
 
 
 
315
 
316
  // --- DOM Elements ---
317
  const adminBtn = document.getElementById('adminBtn');
@@ -336,6 +435,7 @@
336
  const preStreamMessageElement = document.getElementById('preStreamMessage');
337
  const countdownEndSound = document.getElementById('countdownEndSound');
338
  const progressBar = document.getElementById('progressBar');
 
339
 
340
 
341
  // Elements for effects
@@ -359,11 +459,11 @@
359
  errorDisplay.textContent = '';
360
 
361
  const response = await fetch(csvUrl);
362
- if (!response.ok) {
363
- // Attempt to read body for more specific error if possible, otherwise use status
364
- const errorBody = await response.text().catch(() => `Status: ${response.status}`);
365
- throw new Error(`HTTP error fetching sheet: ${response.status} - ${errorBody.substring(0, 100)}...`); // Limit error body length
366
- }
367
  const data = await response.text();
368
  const rows = data.split("\n").map(row => row.split(",").map(cell => cell.trim())); // Split rows and cells, trim whitespace
369
 
@@ -385,16 +485,16 @@
385
 
386
  if (dateString && urlString) {
387
  // Only update if both values are found and are different from current settings
388
- if (dateString !== currentCountdownDateString || urlString !== currentStreamTargetUrl) {
389
- currentCountdownDateString = dateString;
390
- currentStreamTargetUrl = urlString;
391
- console.log("Settings fetched successfully:", { date: currentCountdownDateString, url: currentStreamTargetUrl });
392
- // Restart countdown with new settings
393
- startCountdown(currentCountdownDateString, currentStreamTargetUrl);
394
- } else {
395
- console.log("Settings fetched, no changes detected.");
396
- // Continue the existing countdown if no changes
397
- }
398
 
399
  } else {
400
  errorDisplay.textContent = 'Error: Settings (countdown_date or stream_url) not found in Google Sheet.';
@@ -410,12 +510,12 @@
410
  } catch (error) {
411
  errorDisplay.textContent = 'Error fetching settings. Check sheet URL and publish status.';
412
  console.error("Failed to fetch settings from Google Sheet:", error);
413
- // Indicate error to user and stop countdown on fetch error
414
- countdownDisplay.textContent = "Error fetching settings.";
415
- clearInterval(countdownInterval);
416
- streamReady.classList.add('hidden');
417
- preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
418
- progressBar.style.width = '0%'; // Reset progress bar
419
  }
420
  }
421
 
@@ -436,16 +536,16 @@
436
 
437
  // Check if the parsed date is valid
438
  if (isNaN(targetTimestamp)) {
439
- errorDisplay.textContent = `Error: Invalid date format from sheet: ${dateString}`;
440
- console.error("Invalid date string received from Google Sheet:", dateString);
441
- countdownDisplay.textContent = "Error loading settings.";
442
- clearInterval(countdownInterval);
443
- streamReady.classList.add('hidden');
444
- preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
445
- progressBar.style.width = '0%'; // Reset progress bar
446
- streamTimeDisplay.textContent = "Invalid Date";
447
- timezoneDisplay.textContent = "";
448
- return; // Stop execution if date is invalid
449
  }
450
 
451
  // Calculate the initial duration for the progress bar
@@ -467,38 +567,38 @@
467
 
468
  // Update the display below the countdown with the target time
469
  const options = {
470
- weekday: 'long',
471
- year: 'numeric',
472
- month: 'long',
473
- day: 'numeric',
474
- hour: '2-digit',
475
- minute: '2-digit',
476
- timeZoneName: 'short' // Show the user's local timezone abbreviation
477
  };
478
  // Use the Date object created from the string for consistent display
479
  // Note: This displays the target moment converted to the *user's* local time zone.
480
  try {
481
- streamTimeDisplay.textContent = targetDate.toLocaleString(undefined, options); // Use the parsed Date object
482
- timezoneDisplay.textContent = "(Your Local Time)"; // Indicate this is local time
483
  } catch (e) {
484
- // Handle potential invalid date string parsing errors (less likely now with isNaN check)
485
- streamTimeDisplay.textContent = `Error formatting date: ${dateString}`;
486
- timezoneDisplay.textContent = "";
487
- console.error("Error formatting date string:", dateString, e);
488
  }
489
 
490
 
491
- // Ensure the countdown display is visible if the date is in the future
492
- if (targetTimestamp > new Date().getTime()) {
493
- countdownDisplay.classList.remove('hidden'); // Make sure grid is visible
494
- // Ensure individual elements are visible if the parent was hidden
495
- if(daysElement) daysElement.parentElement.classList.remove('hidden');
496
- if(hoursElement) hoursElement.parentElement.classList.remove('hidden'); // Removed extra ')'
497
- if(minutesElement) minutesElement.parentElement.classList.remove('hidden'); // Removed extra ')'
498
- if(secondsElement) secondsElement.parentElement.classList.remove('hidden'); // Removed extra ')'
499
 
500
- streamReady.classList.add('hidden');
501
- }
502
  }
503
 
504
 
@@ -510,7 +610,7 @@
510
  // --- Progress Bar Update ---
511
  let progressPercentage = 100; // Start full
512
  if (initialCountdownDuration > 0) {
513
- progressPercentage = Math.max(0, (distance / initialCountdownDuration) * 100);
514
  }
515
  progressBar.style.width = `${progressPercentage}%`;
516
 
@@ -524,11 +624,11 @@
524
 
525
  // Ensure stream link is correctly set one last time
526
  streamLink.href = streamTargetUrl;
527
- streamLink.target = "_blank"; // Ensure it opens in a new tab
528
 
529
  streamTimeDisplay.textContent = "Stream should be live now!";
530
  timezoneDisplay.textContent = ""; // Clear timezone note
531
- errorDisplay.textContent = ""; // Clear any lingering error messages
532
 
533
  // Play sound effect (if loaded and allowed by browser)
534
  if (countdownEndSound) {
@@ -559,18 +659,18 @@
559
  // Update display elements, using ?.textContent for safety just in case
560
  // Add a temporary class to trigger animation
561
  const updateDigit = (element, value) => {
562
- const stringValue = value.toString().padStart(2, '0');
563
- if (element && element.textContent !== stringValue) {
564
- element.textContent = stringValue;
565
- // Trigger animation class
566
- element.classList.add('animate-scale');
567
- // Remove class after animation duration
568
- setTimeout(() => {
569
- element.classList.remove('animate-scale');
570
- }, 300); // Match CSS transition duration
571
- } else if (element) {
572
- element.textContent = stringValue; // Update even if value is same, just in case
573
- }
574
  };
575
 
576
  updateDigit(daysElement, days);
@@ -579,14 +679,14 @@
579
  updateDigit(secondsElement, seconds);
580
 
581
 
582
- // Ensure countdown elements are visible if they were hidden after countdown ended previously
583
- countdownDisplay.classList.remove('hidden');
584
- streamReady.classList.add('hidden');
585
- // Also make sure the individual number divs are not hidden if you added display:none to them
586
- if(daysElement && daysElement.parentElement.classList.contains('hidden')) daysElement.parentElement.classList.remove('hidden');
587
- if(hoursElement && hoursElement.parentElement.classList.contains('hidden')) hoursElement.parentElement.classList.remove('hidden');
588
- if(minutesElement && minutesElement.parentElement.classList.contains('hidden')) minutesElement.parentElement.classList.remove('hidden');
589
- if(secondsElement && secondsElement.parentElement.classList.contains('hidden')) secondsElement.parentElement.classList.remove('hidden');
590
 
591
  }
592
 
@@ -633,68 +733,70 @@
633
 
634
  // Schedule the next animation cycle
635
  // Use a timeout slightly longer than the max animation duration
636
- setTimeout(animateParticles, 30000); // 30 seconds
637
  }
638
 
 
 
 
639
 
640
- // --- Event Listeners & Initialization ---
641
 
642
- // Set current year in footer
643
- currentYear.textContent = new Date().getFullYear();
 
644
 
645
- // Fetch settings and start countdown on page load
646
- document.addEventListener('DOMContentLoaded', () => {
647
- fetchSettings(sheetCsvUrl); // Initial fetch
648
-
649
- // Set interval to refetch settings periodically
650
- setInterval(() => {
651
- fetchSettings(sheetCsvUrl);
652
- }, fetchInterval);
653
-
654
- // Add ripple effect listener to each countdown digit
655
- countdownDigits.forEach(digit => {
656
- digit.addEventListener('click', createRipple);
657
  });
 
658
 
659
- // Start particle animation
660
- animateParticles();
661
- });
662
 
 
663
 
664
- // Admin Modal functionality
665
  adminBtn.addEventListener('click', () => {
666
  adminModal.classList.add('active');
667
- messageArea.textContent = ''; // Clear any old messages
 
668
  });
669
 
670
  closeModal.addEventListener('click', () => {
671
  adminModal.classList.remove('active');
672
- messageArea.textContent = ''; // Clear message when closing
673
  });
674
 
675
- // Ensure the link in the modal has the correct URL (it's hardcoded in HTML, but good practice to confirm)
676
- // This also sets target="_blank" to ensure it opens in a new tab
677
- if(editSheetLink) { // Check if element exists
678
- editSheetLink.href = googleSheetEditUrl;
679
- editSheetLink.target = "_blank";
680
- }
681
-
682
-
683
- // Close modal when clicking outside
684
- adminModal.addEventListener('click', (e) => {
685
- if (e.target === adminModal) {
686
  adminModal.classList.remove('active');
687
- messageArea.textContent = ''; // Clear message when closing
688
  }
689
  });
690
 
691
- // Prevent modal content clicks from closing modal
692
- if(adminModal.querySelector('.modal-content')) {
693
- adminModal.querySelector('.modal-content').addEventListener('click', (e) => {
694
- e.stopPropagation(); // Stop click from bubbling up to modal overlay
695
- });
696
- }
 
 
 
 
 
 
 
 
 
 
 
 
697
 
698
  </script>
699
  </body>
700
- </html>
 
77
  transform: translateY(-20px); /* Start slightly up */
78
  opacity: 0;
79
  transition: all 0.3s ease;
80
+ }
81
  .modal.active .modal-content {
82
  transform: translateY(0); /* Slide into place */
83
  opacity: 1;
84
+ }
85
 
86
  /* Ripple Effect Styles */
87
  /* Apply to the clickable elements within the countdown */
 
132
  animation: float 15s ease-in-out infinite alternate;
133
  }
134
 
135
+ /* Add variations for different particles */
136
+ .particle:nth-child(2n) {
137
+ width: 3px;
138
+ height: 3px;
139
+ background-color: rgba(0, 163, 165, 0.6); /* Teal color */
140
+ animation-duration: 20s;
141
+ }
142
+ .particle:nth-child(3n) {
143
+ width: 7px;
144
+ height: 7px;
145
+ background-color: rgba(138, 123, 113, 0.6); /* Warmgrey color */
146
+ animation-duration: 25s;
147
+ }
148
+ .particle:nth-child(4n) {
149
+ width: 4px;
150
+ height: 4px;
151
+ background-color: rgba(171, 138, 92, 0.6); /* Sandstone color */
152
+ animation-duration: 18s;
153
+ }
154
 
155
 
156
  /* Basic float keyframes (used as a fallback/starting point) */
 
178
  transition: width 1s linear; /* Smooth transition for the bar */
179
  }
180
 
181
+ /* Countdown Digit Animation (Subtle Scale) */
182
+ .countdown-digit > div { /* Target the inner div with the number */
183
+ transition: transform 0.3s ease-out; /* Smooth transition */
184
+ }
185
+ /* Optional: Add a class in JS to trigger animation on update */
186
+ .countdown-digit > div.animate-scale {
187
+ transform: scale(1.1);
188
+ }
189
+
190
+ /* Recent Streams Section Styling (New) */
191
+ #recentStreamsSection {
192
+ max-width: 800px;
193
+ margin-top: 2rem; /* Spacing from main content */
194
+ padding: 1.5rem;
195
+ background-color: rgba(0, 0, 0, 0.3); /* Semi-transparent dark background */
196
+ border-radius: 12px;
197
+ border: 1px solid rgba(0, 163, 165, 0.2); /* Teal border */
198
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
199
+ text-align: left;
200
+ }
201
+
202
+ #recentStreamsList {
203
+ list-style: none;
204
+ padding: 0;
205
+ margin: 0;
206
+ }
207
+
208
+ #recentStreamsList li {
209
+ background-color: rgba(51, 51, 51, 0.7); /* Charcoal with transparency */
210
+ border-radius: 8px;
211
+ padding: 1rem;
212
+ margin-bottom: 0.75rem;
213
+ display: flex;
214
+ flex-direction: column; /* Stack elements on small screens */
215
+ align-items: flex-start;
216
+ gap: 0.5rem;
217
+ transition: background-color 0.3s ease;
218
+ }
219
+
220
+ #recentStreamsList li:hover {
221
+ background-color: rgba(51, 51, 51, 0.9);
222
+ }
223
+
224
+ #recentStreamsList li:last-child {
225
+ margin-bottom: 0;
226
+ }
227
+
228
+ #recentStreamsList li a {
229
+ font-weight: bold;
230
+ color: #f88d0d; /* Gold for stream titles */
231
+ text-decoration: none;
232
+ font-size: 1.1rem;
233
+ transition: color 0.3s ease;
234
+ }
235
 
236
+ #recentStreamsList li a:hover {
237
+ color: #ffb74d; /* Lighter gold on hover */
238
+ }
239
+
240
+ #recentStreamsList li p {
241
+ color: #e0e0e0; /* Lighter text for dates/descriptions */
242
+ font-size: 0.9rem;
243
+ }
244
+
245
+ /* Responsive adjustments for recent streams */
246
+ @media (min-width: 768px) {
247
+ #recentStreamsList li {
248
+ flex-direction: row; /* Row layout on larger screens */
249
+ justify-content: space-between;
250
+ align-items: center;
251
+ }
252
+ }
253
 
254
  </style>
255
  </head>
 
262
  <div class="particle" style="top: 30%; left: 60%;"></div>
263
  <div class="particle" style="top: 70%; left: 10%;"></div>
264
  <div class="particle" style="top: 20%; left: 90%;"></div>
265
+ <div class="particle" style="top: 60%; left: 30%;"></div>
266
+ <div class="particle" style="top: 40%; left: 70%;"></div>
267
+ <div class="particle" style="top: 90%; left: 50%;"></div>
268
+ <div class="particle" style="top: 5%; left: 5%;"></div>
269
+ <div class="particle" style="top: 95%; left: 95%;"></div>
270
+ <div class="particle" style="top: 15%; left: 75%;"></div>
271
+ <div class="particle" style="top: 85%; left: 25%;"></div>
272
  </div>
273
 
274
 
 
324
 
325
  <div class="text-grey">
326
  <p class="mt-2 text-sm">Stream will begin on: <span id="streamTimeDisplay" class="font-medium text-teal">Loading...</span> <span id="timezoneDisplay" class="font-medium text-eucalypt text-xs">(Your Local Time)</span></p>
327
+ <p id="errorDisplay" class="mt-2 text-sm text-orange font-semibold"></p>
328
+ </div>
329
+
330
+ <div id="recentStreamsSection" class="w-full mx-auto px-4 sm:px-6 lg:px-8">
331
+ <h3 class="text-2xl font-display font-semibold mb-4 text-gold text-center">Past Streams</h3>
332
+ <ul id="recentStreamsList">
333
+ <li class="p-4 bg-charcoal bg-opacity-70 rounded-lg mb-3 flex flex-col md:flex-row justify-between items-start md:items-center">
334
+ <div>
335
+ <a href="https://www.youtube.com/watch?v=oldstream1" target="_blank" class="text-gold hover:underline text-lg font-bold">Awesome Stream Replay 1</a>
336
+ <p class="text-warmgrey text-sm">Streamed on: 2024-01-15</p>
337
+ </div>
338
+ <a href="https://www.youtube.com/watch?v=oldstream1" target="_blank" class="mt-2 md:mt-0 bg-teal hover:bg-opacity-80 text-white py-2 px-4 rounded-full text-sm transition duration-300">Watch Replay</a>
339
+ </li>
340
+ <li class="p-4 bg-charcoal bg-opacity-70 rounded-lg mb-3 flex flex-col md:flex-row justify-between items-start md:items-center">
341
+ <div>
342
+ <a href="https://www.youtube.com/watch?v=oldstream2" target="_blank" class="text-gold hover:underline text-lg font-bold">Deep Dive into Tech</a>
343
+ <p class="text-warmgrey text-sm">Streamed on: 2023-11-20</p>
344
+ </div>
345
+ <a href="https://www.youtube.com/watch?v=oldstream2" target="_blank" class="mt-2 md:mt-0 bg-teal hover:bg-opacity-80 text-white py-2 px-4 rounded-full text-sm transition duration-300">Watch Replay</a>
346
+ </li>
347
+ <li class="p-4 bg-charcoal bg-opacity-70 rounded-lg flex flex-col md:flex-row justify-between items-start md:items-center">
348
+ <div>
349
+ <a href="https://www.youtube.com/watch?v=oldstream3" target="_blank" class="text-gold hover:underline text-lg font-bold">Gaming Marathon Highlights</a>
350
+ <p class="text-warmgrey text-sm">Streamed on: 2023-09-01</p>
351
+ </div>
352
+ <a href="https://www.youtube.com/watch?v=oldstream3" target="_blank" class="mt-2 md:mt-0 bg-teal hover:bg-opacity-80 text-white py-2 px-4 rounded-full text-sm transition duration-300">Watch Replay</a>
353
+ </li>
354
+ </ul>
355
+ </div>
356
  </div>
357
  </main>
358
 
359
  <footer class="py-6 text-center text-warmgrey text-sm relative z-10">
360
+ <p>© <span id="currentYear"></span> Live Stream Countdown. All rights reserved. | <a href="recent-streams.html" class="text-teal hover:text-eucalypt transition duration-300">Recent Streams</a></p>
361
  </footer>
362
 
363
  <div id="adminModal" class="modal fixed inset-0 flex items-center justify-center p-4 z-50">
 
374
  <p class="mb-4 text-warmgrey text-left">
375
  This countdown is controlled by a Google Sheet. To update the stream date, time, or URL, you need to edit the sheet directly.
376
  </p>
377
+ <p class="mb-4 text-orange text-sm text-left font-semibold">
378
+ Remember: After editing the sheet, you MUST re-publish it to the web (as CSV) for changes to appear here on the page.
379
+ </p>
380
 
381
  <a id="editSheetLink" href="https://docs.google.com/spreadsheets/d/1Qj4NF2jjrG7wnpvYDqu6Dvi59GDaIO8pzr_DLWJif-8/edit?gid=0#gid=0" target="_blank" class="inline-block w-full bg-teal hover:bg-opacity-80 text-white py-3 rounded font-bold transition duration-300 ease-in-out transform hover:scale-105">
382
  Edit Google Sheet
 
386
  </div>
387
  </div>
388
 
389
+ <audio id="countdownEndSound" src="https://github.com/selli106/avlink/raw/refs/heads/main/ding-36029.mp3" preload="auto"></audio>
390
 
391
 
392
  <script>
 
403
  // Threshold for pre-stream message (in milliseconds)
404
  const preStreamThreshold = 5 * 60 * 1000; // 5 minutes
405
 
406
+ // New: Recent Streams Data (You'd ideally fetch this from a sheet/database too)
407
+ const recentStreams = [
408
+ { title: "Awesome Stream Replay 1", date: "2024-01-15", url: "https://www.youtube.com/watch?v=oldstream1" },
409
+ { title: "Deep Dive into Tech", date: "2023-11-20", url: "https://www.youtube.com/watch?v=oldstream2" },
410
+ { title: "Gaming Marathon Highlights", date: "2023-09-01", url: "https://www.youtube.com/watch?v=oldstream3" },
411
+ // Add more recent streams here
412
+ ];
413
+
414
 
415
  // --- DOM Elements ---
416
  const adminBtn = document.getElementById('adminBtn');
 
435
  const preStreamMessageElement = document.getElementById('preStreamMessage');
436
  const countdownEndSound = document.getElementById('countdownEndSound');
437
  const progressBar = document.getElementById('progressBar');
438
+ const recentStreamsList = document.getElementById('recentStreamsList'); // New element
439
 
440
 
441
  // Elements for effects
 
459
  errorDisplay.textContent = '';
460
 
461
  const response = await fetch(csvUrl);
462
+ if (!response.ok) {
463
+ // Attempt to read body for more specific error if possible, otherwise use status
464
+ const errorBody = await response.text().catch(() => `Status: ${response.status}`);
465
+ throw new Error(`HTTP error fetching sheet: ${response.status} - ${errorBody.substring(0, 100)}...`); // Limit error body length
466
+ }
467
  const data = await response.text();
468
  const rows = data.split("\n").map(row => row.split(",").map(cell => cell.trim())); // Split rows and cells, trim whitespace
469
 
 
485
 
486
  if (dateString && urlString) {
487
  // Only update if both values are found and are different from current settings
488
+ if (dateString !== currentCountdownDateString || urlString !== currentStreamTargetUrl) {
489
+ currentCountdownDateString = dateString;
490
+ currentStreamTargetUrl = urlString;
491
+ console.log("Settings fetched successfully:", { date: currentCountdownDateString, url: currentStreamTargetUrl });
492
+ // Restart countdown with new settings
493
+ startCountdown(currentCountdownDateString, currentStreamTargetUrl);
494
+ } else {
495
+ console.log("Settings fetched, no changes detected.");
496
+ // Continue the existing countdown if no changes
497
+ }
498
 
499
  } else {
500
  errorDisplay.textContent = 'Error: Settings (countdown_date or stream_url) not found in Google Sheet.';
 
510
  } catch (error) {
511
  errorDisplay.textContent = 'Error fetching settings. Check sheet URL and publish status.';
512
  console.error("Failed to fetch settings from Google Sheet:", error);
513
+ // Indicate error to user and stop countdown on fetch error
514
+ countdownDisplay.textContent = "Error fetching settings.";
515
+ clearInterval(countdownInterval);
516
+ streamReady.classList.add('hidden');
517
+ preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
518
+ progressBar.style.width = '0%'; // Reset progress bar
519
  }
520
  }
521
 
 
536
 
537
  // Check if the parsed date is valid
538
  if (isNaN(targetTimestamp)) {
539
+ errorDisplay.textContent = `Error: Invalid date format from sheet: ${dateString}`;
540
+ console.error("Invalid date string received from Google Sheet:", dateString);
541
+ countdownDisplay.textContent = "Error loading settings.";
542
+ clearInterval(countdownInterval);
543
+ streamReady.classList.add('hidden');
544
+ preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
545
+ progressBar.style.width = '0%'; // Reset progress bar
546
+ streamTimeDisplay.textContent = "Invalid Date";
547
+ timezoneDisplay.textContent = "";
548
+ return; // Stop execution if date is invalid
549
  }
550
 
551
  // Calculate the initial duration for the progress bar
 
567
 
568
  // Update the display below the countdown with the target time
569
  const options = {
570
+ weekday: 'long',
571
+ year: 'numeric',
572
+ month: 'long',
573
+ day: 'numeric',
574
+ hour: '2-digit',
575
+ minute: '2-digit',
576
+ timeZoneName: 'short' // Show the user's local timezone abbreviation
577
  };
578
  // Use the Date object created from the string for consistent display
579
  // Note: This displays the target moment converted to the *user's* local time zone.
580
  try {
581
+ streamTimeDisplay.textContent = targetDate.toLocaleString(undefined, options); // Use the parsed Date object
582
+ timezoneDisplay.textContent = "(Your Local Time)"; // Indicate this is local time
583
  } catch (e) {
584
+ // Handle potential invalid date string parsing errors (less likely now with isNaN check)
585
+ streamTimeDisplay.textContent = `Error formatting date: ${dateString}`;
586
+ timezoneDisplay.textContent = "";
587
+ console.error("Error formatting date string:", dateString, e);
588
  }
589
 
590
 
591
+ // Ensure the countdown display is visible if the date is in the future
592
+ if (targetTimestamp > new Date().getTime()) {
593
+ countdownDisplay.classList.remove('hidden'); // Make sure grid is visible
594
+ // Ensure individual elements are visible if the parent was hidden
595
+ if(daysElement) daysElement.parentElement.classList.remove('hidden');
596
+ if(hoursElement) hoursElement.parentElement.classList.remove('hidden');
597
+ if(minutesElement) minutesElement.parentElement.classList.remove('hidden');
598
+ if(secondsElement) secondsElement.parentElement.classList.remove('hidden');
599
 
600
+ streamReady.classList.add('hidden');
601
+ }
602
  }
603
 
604
 
 
610
  // --- Progress Bar Update ---
611
  let progressPercentage = 100; // Start full
612
  if (initialCountdownDuration > 0) {
613
+ progressPercentage = Math.max(0, (distance / initialCountdownDuration) * 100);
614
  }
615
  progressBar.style.width = `${progressPercentage}%`;
616
 
 
624
 
625
  // Ensure stream link is correctly set one last time
626
  streamLink.href = streamTargetUrl;
627
+ streamLink.target = "_blank"; // Ensure it opens in a new tab
628
 
629
  streamTimeDisplay.textContent = "Stream should be live now!";
630
  timezoneDisplay.textContent = ""; // Clear timezone note
631
+ errorDisplay.textContent = ""; // Clear any lingering error messages
632
 
633
  // Play sound effect (if loaded and allowed by browser)
634
  if (countdownEndSound) {
 
659
  // Update display elements, using ?.textContent for safety just in case
660
  // Add a temporary class to trigger animation
661
  const updateDigit = (element, value) => {
662
+ const stringValue = value.toString().padStart(2, '0');
663
+ if (element && element.textContent !== stringValue) {
664
+ element.textContent = stringValue;
665
+ // Trigger animation class
666
+ element.classList.add('animate-scale');
667
+ // Remove class after animation duration
668
+ setTimeout(() => {
669
+ element.classList.remove('animate-scale');
670
+ }, 300); // Match CSS transition duration
671
+ } else if (element) {
672
+ element.textContent = stringValue; // Update even if value is same, just in case
673
+ }
674
  };
675
 
676
  updateDigit(daysElement, days);
 
679
  updateDigit(secondsElement, seconds);
680
 
681
 
682
+ // Ensure countdown elements are visible if they were hidden after countdown ended previously
683
+ countdownDisplay.classList.remove('hidden');
684
+ streamReady.classList.add('hidden');
685
+ // Also make sure the individual number divs are not hidden if you added display:none to them
686
+ if(daysElement && daysElement.parentElement.classList.contains('hidden')) daysElement.parentElement.classList.remove('hidden');
687
+ if(hoursElement && hoursElement.parentElement.classList.contains('hidden')) hoursElement.parentElement.classList.remove('hidden');
688
+ if(minutesElement && minutesElement.parentElement.classList.contains('hidden')) minutesElement.parentElement.classList.remove('hidden');
689
+ if(secondsElement && secondsElement.parentElement.classList.contains('hidden')) secondsElement.parentElement.classList.remove('hidden');
690
 
691
  }
692
 
 
733
 
734
  // Schedule the next animation cycle
735
  // Use a timeout slightly longer than the max animation duration
736
+ setTimeout(animateParticles, 30 * 1000); // Rerun animation every 30 seconds
737
  }
738
 
739
+ // Populate recent streams
740
+ function populateRecentStreams() {
741
+ if (!recentStreamsList) return; // Exit if the element doesn't exist
742
 
743
+ recentStreamsList.innerHTML = ''; // Clear existing content
744
 
745
+ recentStreams.forEach(stream => {
746
+ const listItem = document.createElement('li');
747
+ listItem.className = 'p-4 bg-charcoal bg-opacity-70 rounded-lg mb-3 flex flex-col md:flex-row justify-between items-start md:items-center';
748
 
749
+ listItem.innerHTML = `
750
+ <div>
751
+ <a href="${stream.url}" target="_blank" class="text-gold hover:underline text-lg font-bold">${stream.title}</a>
752
+ <p class="text-warmgrey text-sm">Streamed on: ${stream.date}</p>
753
+ </div>
754
+ <a href="${stream.url}" target="_blank" class="mt-2 md:mt-0 bg-teal hover:bg-opacity-80 text-white py-2 px-4 rounded-full text-sm transition duration-300">Watch Replay</a>
755
+ `;
756
+ recentStreamsList.appendChild(listItem);
 
 
 
 
757
  });
758
+ }
759
 
 
 
 
760
 
761
+ // --- Event Listeners and Initial Calls ---
762
 
763
+ // Admin Modal
764
  adminBtn.addEventListener('click', () => {
765
  adminModal.classList.add('active');
766
+ messageArea.textContent = ''; // Clear message when opening
767
+ editSheetLink.href = googleSheetEditUrl; // Ensure link is set
768
  });
769
 
770
  closeModal.addEventListener('click', () => {
771
  adminModal.classList.remove('active');
 
772
  });
773
 
774
+ // Close modal if clicking outside content
775
+ adminModal.addEventListener('click', (event) => {
776
+ if (event.target === adminModal) {
 
 
 
 
 
 
 
 
777
  adminModal.classList.remove('active');
 
778
  }
779
  });
780
 
781
+
782
+ // Ripple effect event listeners
783
+ countdownDigits.forEach(digit => {
784
+ digit.addEventListener('click', createRipple);
785
+ });
786
+
787
+ // Set current year in footer
788
+ document.getElementById('currentYear').textContent = new Date().getFullYear();
789
+
790
+ // Initial fetch and set up recurring fetch
791
+ fetchSettings(sheetCsvUrl);
792
+ setInterval(() => fetchSettings(sheetCsvUrl), fetchInterval);
793
+
794
+ // Start particle animation
795
+ animateParticles();
796
+
797
+ // Populate recent streams on load
798
+ populateRecentStreams();
799
 
800
  </script>
801
  </body>
802
+ </html>