Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Advanced Countdown Timer</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
body { | |
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
} | |
.glass-card { | |
background: rgba(255, 255, 255, 0.85); | |
backdrop-filter: blur(10px); | |
-webkit-backdrop-filter: blur(10px); | |
border-radius: 16px; | |
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.1); | |
} | |
.countdown-digit { | |
transition: all 0.3s ease; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); | |
} | |
.countdown-digit:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); | |
} | |
.firework { | |
position: absolute; | |
width: 5px; | |
height: 5px; | |
border-radius: 50%; | |
box-shadow: 0 0 10px 5px; | |
animation: firework-animation 1s ease-out; | |
opacity: 0; | |
} | |
@keyframes firework-animation { | |
0% { transform: translate(0, 0); opacity: 1; } | |
100% { transform: translate(var(--tx), var(--ty)); opacity: 0; } | |
} | |
.confetti { | |
position: absolute; | |
width: 10px; | |
height: 10px; | |
opacity: 0; | |
animation: confetti-fall 3s ease-in forwards; | |
} | |
@keyframes confetti-fall { | |
0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; } | |
100% { transform: translateY(100vh) rotate(360deg); opacity: 1; } | |
} | |
.pulse { | |
animation: pulse 1s infinite; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.05); } | |
100% { transform: scale(1); } | |
} | |
.flip-in { | |
animation: flipIn 0.5s ease-out; | |
} | |
@keyframes flipIn { | |
0% { transform: rotateX(90deg); opacity: 0; } | |
100% { transform: rotateX(0deg); opacity: 1; } | |
} | |
.btn-primary { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
transition: all 0.3s ease; | |
} | |
.btn-primary:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 6px 12px rgba(102, 126, 234, 0.25); | |
} | |
.input-field { | |
transition: all 0.3s ease; | |
border: 1px solid #e2e8f0; | |
} | |
.input-field:focus { | |
border-color: #667eea; | |
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); | |
} | |
.tab-btn { | |
transition: all 0.3s ease; | |
border-bottom: 3px solid transparent; | |
} | |
.tab-btn.active { | |
border-bottom-color: #667eea; | |
color: #667eea; | |
font-weight: 600; | |
} | |
.datetime-input { | |
position: relative; | |
} | |
.datetime-input::-webkit-calendar-picker-indicator { | |
background: transparent; | |
bottom: 0; | |
color: transparent; | |
cursor: pointer; | |
height: auto; | |
left: 0; | |
position: absolute; | |
right: 0; | |
top: 0; | |
width: auto; | |
} | |
</style> | |
</head> | |
<body class="min-h-screen text-gray-800 font-sans"> | |
<div id="app" class="container mx-auto px-4 py-12"> | |
<!-- Main Page --> | |
<div id="main-page" class="text-center max-w-3xl mx-auto"> | |
<div class="glass-card p-8 mb-8"> | |
<h1 class="text-4xl font-bold mb-4 text-gray-800">Countdown Timer</h1> | |
<p class="text-lg text-gray-600 mb-8">Create countdowns by selecting a future date or setting duration</p> | |
<form id="countdown-form" class="space-y-6"> | |
<div class="text-left"> | |
<label for="title" class="block mb-2 text-gray-700">Countdown Title</label> | |
<input type="text" id="title" placeholder="New Year's Eve" | |
class="input-field w-full px-4 py-3 rounded-lg bg-white"> | |
</div> | |
<div class="flex border-b border-gray-200 mb-6"> | |
<button type="button" id="duration-tab" class="tab-btn active py-2 px-4">Duration</button> | |
<button type="button" id="datetime-tab" class="tab-btn py-2 px-4">Date & Time</button> | |
</div> | |
<div id="duration-inputs"> | |
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
<div class="text-left"> | |
<label for="days" class="block mb-2 text-gray-700">Days</label> | |
<input type="number" id="days" min="0" value="0" | |
class="input-field w-full px-4 py-3 rounded-lg bg-white"> | |
</div> | |
<div class="text-left"> | |
<label for="hours" class="block mb-2 text-gray-700">Hours</label> | |
<input type="number" id="hours" min="0" max="23" value="0" | |
class="input-field w-full px-4 py-3 rounded-lg bg-white"> | |
</div> | |
<div class="text-left"> | |
<label for="minutes" class="block mb-2 text-gray-700">Minutes</label> | |
<input type="number" id="minutes" min="0" max="59" value="10" | |
class="input-field w-full px-4 py-3 rounded-lg bg-white"> | |
</div> | |
<div class="text-left"> | |
<label for="seconds" class="block mb-2 text-gray-700">Seconds</label> | |
<input type="number" id="seconds" min="0" max="59" value="0" | |
class="input-field w-full px-4 py-3 rounded-lg bg-white"> | |
</div> | |
</div> | |
</div> | |
<div id="datetime-inputs" class="hidden"> | |
<div class="text-left"> | |
<label for="target-datetime" class="block mb-2 text-gray-700">Target Date & Time</label> | |
<input type="datetime-local" id="target-datetime" | |
class="datetime-input input-field w-full px-4 py-3 rounded-lg bg-white" | |
min=""> | |
</div> | |
<div class="mt-4 text-left"> | |
<label for="timezone" class="block mb-2 text-gray-700">Timezone</label> | |
<select id="timezone" class="input-field w-full px-4 py-3 rounded-lg bg-white"> | |
<option value="local">Local Time</option> | |
<option value="utc">UTC</option> | |
</select> | |
</div> | |
</div> | |
<div class="text-left"> | |
<label class="block mb-2 text-gray-700">Finish Animation</label> | |
<div class="grid grid-cols-2 md:grid-cols-4 gap-3"> | |
<label class="flex items-center space-x-2 cursor-pointer p-3 rounded-lg hover:bg-gray-100"> | |
<input type="radio" name="animation" value="fireworks" checked class="form-radio h-5 w-5 text-indigo-600"> | |
<span class="text-gray-700">Fireworks</span> | |
</label> | |
<label class="flex items-center space-x-2 cursor-pointer p-3 rounded-lg hover:bg-gray-100"> | |
<input type="radio" name="animation" value="confetti" class="form-radio h-5 w-5 text-indigo-600"> | |
<span class="text-gray-700">Confetti</span> | |
</label> | |
<label class="flex items-center space-x-2 cursor-pointer p-3 rounded-lg hover:bg-gray-100"> | |
<input type="radio" name="animation" value="pulse" class="form-radio h-5 w-5 text-indigo-600"> | |
<span class="text-gray-700">Pulse</span> | |
</label> | |
<label class="flex items-center space-x-2 cursor-pointer p-3 rounded-lg hover:bg-gray-100"> | |
<input type="radio" name="animation" value="none" class="form-radio h-5 w-5 text-indigo-600"> | |
<span class="text-gray-700">None</span> | |
</label> | |
</div> | |
</div> | |
<button type="submit" class="btn-primary mt-6 px-8 py-3 rounded-full font-semibold"> | |
Start Countdown <i class="fas fa-play ml-2"></i> | |
</button> | |
</form> | |
</div> | |
<div class="glass-card p-6"> | |
<h2 class="text-2xl font-semibold mb-4 text-gray-800">How to Use</h2> | |
<div class="text-left space-y-2 text-gray-600"> | |
<p>1. Choose between setting a duration or selecting a specific future date/time</p> | |
<p>2. Add a title and choose a finish animation</p> | |
<p>3. Share the URL with others - all settings are included</p> | |
<p class="mt-4 font-medium">Example URL: <code class="bg-gray-100 px-2 py-1 rounded">?title=New+Year&target=2024-01-01T00:00&timezone=utc&animation=fireworks</code></p> | |
</div> | |
</div> | |
</div> | |
<!-- Countdown Page --> | |
<div id="countdown-page" class="hidden text-center"> | |
<div class="glass-card p-8 max-w-2xl mx-auto"> | |
<h1 id="countdown-title" class="text-3xl font-bold mb-8 text-gray-800">Countdown</h1> | |
<div class="flex justify-center space-x-4 mb-8"> | |
<div class="countdown-digit bg-white rounded-lg p-4 w-20 text-center"> | |
<div id="days-display" class="text-3xl font-bold text-indigo-600">00</div> | |
<div class="text-sm text-gray-500">Days</div> | |
</div> | |
<div class="countdown-digit bg-white rounded-lg p-4 w-20 text-center"> | |
<div id="hours-display" class="text-3xl font-bold text-indigo-600">00</div> | |
<div class="text-sm text-gray-500">Hours</div> | |
</div> | |
<div class="countdown-digit bg-white rounded-lg p-4 w-20 text-center"> | |
<div id="minutes-display" class="text-3xl font-bold text-indigo-600">00</div> | |
<div class="text-sm text-gray-500">Minutes</div> | |
</div> | |
<div class="countdown-digit bg-white rounded-lg p-4 w-20 text-center"> | |
<div id="seconds-display" class="text-3xl font-bold text-indigo-600">00</div> | |
<div class="text-sm text-gray-500">Seconds</div> | |
</div> | |
</div> | |
<div id="target-time-display" class="text-gray-600 mb-4"> | |
Counting down to: <span id="target-time" class="font-medium"></span> | |
</div> | |
<div id="finished-message" class="hidden text-2xl font-bold mb-6"> | |
<div class="inline-block px-6 py-3 bg-indigo-600 text-white rounded-full"> | |
Time's Up! | |
</div> | |
</div> | |
<button id="new-countdown" class="btn-primary mt-4 px-6 py-2 rounded-full font-medium"> | |
<i class="fas fa-redo mr-2"></i> New Countdown | |
</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Set min datetime to now | |
const now = new Date(); | |
const timezoneOffset = now.getTimezoneOffset() * 60000; | |
const localISOTime = new Date(now - timezoneOffset).toISOString().slice(0, 16); | |
document.getElementById('target-datetime').min = localISOTime; | |
// Tab switching | |
const durationTab = document.getElementById('duration-tab'); | |
const datetimeTab = document.getElementById('datetime-tab'); | |
const durationInputs = document.getElementById('duration-inputs'); | |
const datetimeInputs = document.getElementById('datetime-inputs'); | |
durationTab.addEventListener('click', function() { | |
durationTab.classList.add('active'); | |
datetimeTab.classList.remove('active'); | |
durationInputs.classList.remove('hidden'); | |
datetimeInputs.classList.add('hidden'); | |
}); | |
datetimeTab.addEventListener('click', function() { | |
datetimeTab.classList.add('active'); | |
durationTab.classList.remove('active'); | |
datetimeInputs.classList.remove('hidden'); | |
durationInputs.classList.add('hidden'); | |
}); | |
// Check URL parameters | |
const urlParams = new URLSearchParams(window.location.search); | |
const hasDurationParams = urlParams.has('days') || urlParams.has('hours') || urlParams.has('minutes') || urlParams.has('seconds'); | |
const hasDatetimeParam = urlParams.has('target'); | |
if (hasDurationParams || hasDatetimeParam) { | |
const title = urlParams.get('title') || 'Countdown'; | |
const animation = urlParams.get('animation') || 'fireworks'; | |
if (hasDatetimeParam) { | |
// Start countdown from datetime | |
const target = urlParams.get('target'); | |
const timezone = urlParams.get('timezone') || 'local'; | |
// Switch to datetime tab | |
datetimeTab.click(); | |
// Set the datetime input | |
document.getElementById('target-datetime').value = target; | |
document.getElementById('timezone').value = timezone; | |
// Calculate time remaining | |
const targetDate = new Date(target); | |
const now = new Date(); | |
let diffInSeconds = Math.floor((targetDate - now) / 1000); | |
if (diffInSeconds > 0) { | |
startCountdown(title, diffInSeconds, animation, targetDate, timezone); | |
} else { | |
// If time is in the past, show finished state | |
document.getElementById('main-page').classList.add('hidden'); | |
document.getElementById('countdown-page').classList.remove('hidden'); | |
document.getElementById('countdown-title').textContent = title; | |
document.getElementById('finished-message').classList.remove('hidden'); | |
countdownFinished(animation); | |
} | |
} else { | |
// Start countdown from duration params | |
const days = parseInt(urlParams.get('days')) || 0; | |
const hours = parseInt(urlParams.get('hours')) || 0; | |
const minutes = parseInt(urlParams.get('minutes')) || 0; | |
const seconds = parseInt(urlParams.get('seconds')) || 0; | |
const totalSeconds = days * 86400 + hours * 3600 + minutes * 60 + seconds; | |
startCountdown(title, totalSeconds, animation); | |
} | |
} | |
// Form submission | |
const form = document.getElementById('countdown-form'); | |
form.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
const title = document.getElementById('title').value || 'Countdown'; | |
const animation = document.querySelector('input[name="animation"]:checked').value; | |
// Check which tab is active | |
const isDurationTabActive = durationTab.classList.contains('active'); | |
if (isDurationTabActive) { | |
// Duration mode | |
const days = parseInt(document.getElementById('days').value) || 0; | |
const hours = parseInt(document.getElementById('hours').value) || 0; | |
const minutes = parseInt(document.getElementById('minutes').value) || 0; | |
const seconds = parseInt(document.getElementById('seconds').value) || 0; | |
const totalSeconds = days * 86400 + hours * 3600 + minutes * 60 + seconds; | |
// Update URL with parameters | |
const params = new URLSearchParams(); | |
if (title !== 'Countdown') params.set('title', title); | |
if (days > 0) params.set('days', days); | |
if (hours > 0) params.set('hours', hours); | |
if (minutes > 0) params.set('minutes', minutes); | |
if (seconds > 0) params.set('seconds', seconds); | |
params.set('animation', animation); | |
window.history.pushState({}, '', `?${params.toString()}`); | |
startCountdown(title, totalSeconds, animation); | |
} else { | |
// Datetime mode | |
const targetDatetime = document.getElementById('target-datetime').value; | |
const timezone = document.getElementById('timezone').value; | |
if (!targetDatetime) { | |
alert('Please select a future date and time'); | |
return; | |
} | |
// Calculate time remaining | |
let targetDate = new Date(targetDatetime); | |
const now = new Date(); | |
let diffInSeconds = Math.floor((targetDate - now) / 1000); | |
if (diffInSeconds <= 0) { | |
alert('Please select a time in the future'); | |
return; | |
} | |
// Format target date for URL | |
let targetForUrl; | |
if (timezone === 'utc') { | |
targetForUrl = targetDate.toISOString().slice(0, 16); | |
} else { | |
// Local time - need to adjust for timezone offset | |
const timezoneOffset = now.getTimezoneOffset() * 60000; | |
const localISOTime = new Date(targetDate.getTime() - timezoneOffset).toISOString().slice(0, 16); | |
targetForUrl = localISOTime; | |
} | |
// Update URL with parameters | |
const params = new URLSearchParams(); | |
if (title !== 'Countdown') params.set('title', title); | |
params.set('target', targetForUrl); | |
params.set('timezone', timezone); | |
params.set('animation', animation); | |
window.history.pushState({}, '', `?${params.toString()}`); | |
startCountdown(title, diffInSeconds, animation, targetDate, timezone); | |
} | |
}); | |
// New countdown button | |
document.getElementById('new-countdown').addEventListener('click', function() { | |
window.history.pushState({}, '', window.location.pathname); | |
document.getElementById('main-page').classList.remove('hidden'); | |
document.getElementById('countdown-page').classList.add('hidden'); | |
clearInterval(window.countdownInterval); | |
document.getElementById('finished-message').classList.add('hidden'); | |
document.getElementById('target-time-display').classList.add('hidden'); | |
}); | |
}); | |
function startCountdown(title, totalSeconds, animationType, targetDate = null, timezone = null) { | |
// Switch to countdown page | |
document.getElementById('main-page').classList.add('hidden'); | |
document.getElementById('countdown-page').classList.remove('hidden'); | |
document.getElementById('countdown-title').textContent = title; | |
document.getElementById('finished-message').classList.add('hidden'); | |
// Show target time if available | |
if (targetDate) { | |
let displayDate; | |
if (timezone === 'utc') { | |
displayDate = targetDate.toUTCString(); | |
} else { | |
displayDate = targetDate.toLocaleString(); | |
} | |
document.getElementById('target-time').textContent = displayDate; | |
document.getElementById('target-time-display').classList.remove('hidden'); | |
} else { | |
document.getElementById('target-time-display').classList.add('hidden'); | |
} | |
// Start the countdown | |
updateCountdownDisplay(totalSeconds); | |
if (targetDate) { | |
// For datetime mode, we need to recalculate the remaining time each second | |
// to account for any time drift | |
window.countdownInterval = setInterval(function() { | |
const now = new Date(); | |
totalSeconds = Math.floor((targetDate - now) / 1000); | |
updateCountdownDisplay(totalSeconds); | |
if (totalSeconds <= 0) { | |
clearInterval(window.countdownInterval); | |
countdownFinished(animationType); | |
} | |
}, 1000); | |
} else { | |
// For duration mode, simple countdown is sufficient | |
window.countdownInterval = setInterval(function() { | |
totalSeconds--; | |
updateCountdownDisplay(totalSeconds); | |
if (totalSeconds <= 0) { | |
clearInterval(window.countdownInterval); | |
countdownFinished(animationType); | |
} | |
}, 1000); | |
} | |
} | |
function updateCountdownDisplay(totalSeconds) { | |
const days = Math.floor(totalSeconds / 86400); | |
const hours = Math.floor((totalSeconds % 86400) / 3600); | |
const minutes = Math.floor((totalSeconds % 3600) / 60); | |
const seconds = totalSeconds % 60; | |
document.getElementById('days-display').textContent = days.toString().padStart(2, '0'); | |
document.getElementById('hours-display').textContent = hours.toString().padStart(2, '0'); | |
document.getElementById('minutes-display').textContent = minutes.toString().padStart(2, '0'); | |
document.getElementById('seconds-display').textContent = seconds.toString().padStart(2, '0'); | |
} | |
function countdownFinished(animationType) { | |
document.getElementById('finished-message').classList.remove('hidden'); | |
switch(animationType) { | |
case 'fireworks': | |
createFireworks(); | |
break; | |
case 'confetti': | |
createConfetti(); | |
break; | |
case 'pulse': | |
document.getElementById('finished-message').classList.add('pulse'); | |
break; | |
// 'none' case does nothing | |
} | |
} | |
function createFireworks() { | |
const colors = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff']; | |
const container = document.getElementById('app'); | |
for (let i = 0; i < 30; i++) { | |
setTimeout(() => { | |
const firework = document.createElement('div'); | |
firework.className = 'firework'; | |
firework.style.left = `${Math.random() * 100}%`; | |
firework.style.top = `${Math.random() * 100}%`; | |
firework.style.color = colors[Math.floor(Math.random() * colors.length)]; | |
// Random direction for particles | |
const angle = Math.random() * Math.PI * 2; | |
const distance = 30 + Math.random() * 70; | |
const tx = Math.cos(angle) * distance; | |
const ty = Math.sin(angle) * distance; | |
firework.style.setProperty('--tx', `${tx}px`); | |
firework.style.setProperty('--ty', `${ty}px`); | |
container.appendChild(firework); | |
// Remove after animation | |
setTimeout(() => { | |
firework.remove(); | |
}, 1000); | |
}, i * 200); | |
} | |
} | |
function createConfetti() { | |
const colors = ['#ff0000', '#ffff00', '#00ff00', '#00ffff', '#0000ff', '#ff00ff']; | |
const container = document.getElementById('app'); | |
for (let i = 0; i < 80; i++) { | |
setTimeout(() => { | |
const confetti = document.createElement('div'); | |
confetti.className = 'confetti'; | |
confetti.style.left = `${Math.random() * 100}%`; | |
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; | |
confetti.style.animationDelay = `${Math.random() * 2}s`; | |
// Random shape | |
if (Math.random() > 0.5) { | |
confetti.style.borderRadius = '50%'; | |
} else { | |
confetti.style.transform = 'rotate(45deg)'; | |
} | |
container.appendChild(confetti); | |
// Remove after animation | |
setTimeout(() => { | |
confetti.remove(); | |
}, 3000); | |
}, i * 100); | |
} | |
} | |
</script> | |
</body> | |
</html> |