Spaces:
Running
Running
Update index.html
Browse files- 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 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 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 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 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 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 287 |
-
|
| 288 |
-
|
| 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://
|
| 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 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 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 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 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 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
}
|
| 420 |
}
|
| 421 |
|
|
@@ -436,16 +536,16 @@
|
|
| 436 |
|
| 437 |
// Check if the parsed date is valid
|
| 438 |
if (isNaN(targetTimestamp)) {
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 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 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 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 |
-
|
| 482 |
-
|
| 483 |
} catch (e) {
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
}
|
| 489 |
|
| 490 |
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
|
| 500 |
-
|
| 501 |
-
|
| 502 |
}
|
| 503 |
|
| 504 |
|
|
@@ -510,7 +610,7 @@
|
|
| 510 |
// --- Progress Bar Update ---
|
| 511 |
let progressPercentage = 100; // Start full
|
| 512 |
if (initialCountdownDuration > 0) {
|
| 513 |
-
|
| 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 |
-
|
| 528 |
|
| 529 |
streamTimeDisplay.textContent = "Stream should be live now!";
|
| 530 |
timezoneDisplay.textContent = ""; // Clear timezone note
|
| 531 |
-
|
| 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 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
};
|
| 575 |
|
| 576 |
updateDigit(daysElement, days);
|
|
@@ -579,14 +679,14 @@
|
|
| 579 |
updateDigit(secondsElement, seconds);
|
| 580 |
|
| 581 |
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 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,
|
| 637 |
}
|
| 638 |
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
-
|
| 641 |
|
| 642 |
-
|
| 643 |
-
|
|
|
|
| 644 |
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 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
|
| 665 |
adminBtn.addEventListener('click', () => {
|
| 666 |
adminModal.classList.add('active');
|
| 667 |
-
|
|
|
|
| 668 |
});
|
| 669 |
|
| 670 |
closeModal.addEventListener('click', () => {
|
| 671 |
adminModal.classList.remove('active');
|
| 672 |
-
messageArea.textContent = ''; // Clear message when closing
|
| 673 |
});
|
| 674 |
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 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 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 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>
|