Spaces:
Running
Running
🐳 24/03 - 17:01 - can you extend app by adding UI widgets: cities, city search, realtime tyme showing, account
d4e49a2 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Globe with Time-Based City Markers</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/globe.gl@2.24.0/dist/globe.gl.min.js"></script> | |
| <style> | |
| .globe-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .time-slider { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| z-index: 10; | |
| width: 80%; | |
| max-width: 600px; | |
| } | |
| .city-tooltip { | |
| position: absolute; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| pointer-events: none; | |
| font-size: 14px; | |
| z-index: 100; | |
| transition: opacity 0.3s; | |
| opacity: 0; | |
| } | |
| .time-display { | |
| position: absolute; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 20px; | |
| z-index: 10; | |
| font-size: 18px; | |
| font-weight: bold; | |
| } | |
| .marker { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| background: #ff4757; | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| box-shadow: 0 0 10px #ff4757; | |
| z-index: 5; | |
| transition: all 0.3s ease; | |
| } | |
| .marker.active { | |
| width: 16px; | |
| height: 16px; | |
| background: #ff6b81; | |
| box-shadow: 0 0 15px #ff6b81; | |
| } | |
| .legend { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 10px; | |
| border-radius: 8px; | |
| z-index: 10; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| margin: 5px 0; | |
| } | |
| .legend-color { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| } | |
| .cities-sidebar { | |
| backdrop-filter: blur(10px); | |
| } | |
| .city-item { | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .city-item:hover { | |
| background: rgba(75, 85, 99, 0.5); | |
| } | |
| .city-item.active { | |
| background: rgba(59, 130, 246, 0.3); | |
| border-left: 3px solid #3b82f6; | |
| } | |
| .city-time { | |
| font-family: monospace; | |
| font-size: 0.85rem; | |
| color: #9ca3af; | |
| } | |
| #city-search { | |
| transition: all 0.3s; | |
| } | |
| #city-search:focus { | |
| width: 300px; | |
| } | |
| .search-highlight { | |
| background: rgba(59, 130, 246, 0.5); | |
| border-radius: 2px; | |
| } | |
| @media (max-width: 768px) { | |
| .cities-sidebar { | |
| width: 200px; | |
| left: 10px; | |
| top: 80px; | |
| bottom: 120px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white h-screen flex flex-col"> | |
| <header class="py-4 px-6 bg-gray-800 shadow-lg"> | |
| <h1 class="text-2xl md:text-3xl font-bold text-center">Global Time Zone Visualization</h1> | |
| </header> | |
| <main class="flex-1 relative overflow-hidden"> | |
| <div id="globe-container" class="globe-container w-full h-full"></div> | |
| <!-- Cities Sidebar --> | |
| <div class="cities-sidebar absolute left-4 top-24 bottom-32 w-64 bg-gray-800 rounded-lg shadow-xl z-20 overflow-hidden flex flex-col border border-gray-700"> | |
| <div class="p-4 border-b border-gray-700 bg-gray-800"> | |
| <h2 class="text-lg font-semibold flex items-center gap-2"> | |
| <i data-lucide="map-pin" class="w-5 h-5 text-blue-400"></i> | |
| Cities | |
| </h2> | |
| <p class="text-xs text-gray-400 mt-1">Click to navigate</p> | |
| </div> | |
| <div class="flex-1 overflow-y-auto p-2 space-y-1" id="cities-list"> | |
| <!-- Cities populated by JS --> | |
| </div> | |
| <div class="p-3 border-t border-gray-700 bg-gray-800"> | |
| <div class="text-xs text-gray-400 text-center" id="cities-count">15 cities</div> | |
| </div> | |
| </div> | |
| <!-- Account Modal --> | |
| <div id="account-modal" class="hidden absolute top-20 right-4 w-72 bg-gray-800 rounded-lg shadow-2xl z-30 border border-gray-700 overflow-hidden"> | |
| <div class="p-4 bg-gray-700"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center"> | |
| <i data-lucide="user" class="w-6 h-6"></i> | |
| </div> | |
| <div> | |
| <h3 class="font-semibold">Guest User</h3> | |
| <p class="text-xs text-gray-400">guest@example.com</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-2 space-y-1"> | |
| <button class="w-full text-left px-3 py-2 hover:bg-gray-700 rounded flex items-center gap-3 text-sm transition-colors"> | |
| <i data-lucide="settings" class="w-4 h-4 text-gray-400"></i> | |
| Settings | |
| </button> | |
| <button class="w-full text-left px-3 py-2 hover:bg-gray-700 rounded flex items-center gap-3 text-sm transition-colors"> | |
| <i data-lucide="bookmark" class="w-4 h-4 text-gray-400"></i> | |
| Saved Locations | |
| </button> | |
| <button class="w-full text-left px-3 py-2 hover:bg-gray-700 rounded flex items-center gap-3 text-sm transition-colors"> | |
| <i data-lucide="bell" class="w-4 h-4 text-gray-400"></i> | |
| Notifications | |
| </button> | |
| <div class="border-t border-gray-700 my-2"></div> | |
| <button class="w-full text-left px-3 py-2 hover:bg-gray-700 rounded flex items-center gap-3 text-sm text-red-400 transition-colors"> | |
| <i data-lucide="log-out" class="w-4 h-4"></i> | |
| Sign Out | |
| </button> | |
| </div> | |
| </div> | |
| <div class="time-display" id="time-display"> | |
| 12:00 PM UTC | |
| </div> | |
| <div class="legend"> | |
| <div class="legend-item"> | |
| <div class="legend-color bg-red-500"></div> | |
| <span>Morning (6AM-12PM)</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="legend-color bg-yellow-500"></div> | |
| <span>Afternoon (12PM-6PM)</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="legend-color bg-blue-500"></div> | |
| <span>Evening (6PM-12AM)</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="legend-color bg-purple-500"></div> | |
| <span>Night (12AM-6AM)</span> | |
| </div> | |
| </div> | |
| <div class="time-slider bg-gray-800 p-4 rounded-lg shadow-xl"> | |
| <input type="range" min="0" max="23" value="12" step="1" class="w-full" id="time-slider"> | |
| <div class="flex justify-between mt-2"> | |
| <span>12AM</span> | |
| <span>6AM</span> | |
| <span>12PM</span> | |
| <span>6PM</span> | |
| <span>12AM</span> | |
| </div> | |
| </div> | |
| <div class="city-tooltip" id="city-tooltip"></div> | |
| </main> | |
| <footer class="py-3 px-6 bg-gray-800 text-center text-sm"> | |
| <p>Drag to rotate the globe | Scroll to zoom | Click on markers for details</p> | |
| </footer> | |
| <script> | |
| // Major cities data with coordinates | |
| const cities = [ | |
| { name: "New York", lat: 40.7128, lng: -74.0060, timezone: "America/New_York" }, | |
| { name: "London", lat: 51.5074, lng: -0.1278, timezone: "Europe/London" }, | |
| { name: "Tokyo", lat: 35.6762, lng: 139.6503, timezone: "Asia/Tokyo" }, | |
| { name: "Sydney", lat: -33.8688, lng: 151.2093, timezone: "Australia/Sydney" }, | |
| { name: "Dubai", lat: 25.2048, lng: 55.2708, timezone: "Asia/Dubai" }, | |
| { name: "Moscow", lat: 55.7558, lng: 37.6173, timezone: "Europe/Moscow" }, | |
| { name: "Beijing", lat: 39.9042, lng: 116.4074, timezone: "Asia/Shanghai" }, | |
| { name: "Rio de Janeiro", lat: -22.9068, lng: -43.1729, timezone: "America/Sao_Paulo" }, | |
| { name: "Cape Town", lat: -33.9249, lng: 18.4241, timezone: "Africa/Johannesburg" }, | |
| { name: "Los Angeles", lat: 34.0522, lng: -118.2437, timezone: "America/Los_Angeles" }, | |
| { name: "Paris", lat: 48.8566, lng: 2.3522, timezone: "Europe/Paris" }, | |
| { name: "Singapore", lat: 1.3521, lng: 103.8198, timezone: "Asia/Singapore" }, | |
| { name: "Mumbai", lat: 19.0760, lng: 72.8777, timezone: "Asia/Kolkata" }, | |
| { name: "Mexico City", lat: 19.4326, lng: -99.1332, timezone: "America/Mexico_City" }, | |
| { name: "Toronto", lat: 43.6532, lng: -79.3832, timezone: "America/Toronto" } | |
| ]; | |
| // Initialize the globe | |
| const globeContainer = document.getElementById('globe-container'); | |
| const world = Globe() | |
| .globeImageUrl('//unpkg.com/three-globe/example/img/earth-night.jpg') | |
| .bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png') | |
| .backgroundColor('rgba(0,0,0,0)') | |
| .showAtmosphere(true) | |
| .atmosphereColor('rgba(100, 149, 237, 0.3)') | |
| .atmosphereAltitude(0.25) | |
| (globeContainer); | |
| // Set initial camera position | |
| world.controls().autoRotate = true; | |
| world.controls().autoRotateSpeed = 0.5; | |
| world.controls().enableZoom = true; | |
| world.controls().enablePan = true; | |
| // Time slider functionality | |
| const timeSlider = document.getElementById('time-slider'); | |
| const timeDisplay = document.getElementById('time-display'); | |
| const cityTooltip = document.getElementById('city-tooltip'); | |
| // Function to update time display | |
| function updateTimeDisplay(hour) { | |
| const ampm = hour >= 12 ? 'PM' : 'AM'; | |
| const displayHour = hour % 12 === 0 ? 12 : hour % 12; | |
| timeDisplay.textContent = `${displayHour}:00 ${ampm} UTC`; | |
| // Update city markers based on local time | |
| updateCityMarkers(hour); | |
| } | |
| // Function to get local time in city based on UTC hour | |
| function getLocalTime(utcHour, timezoneOffset) { | |
| // Simplified calculation (in a real app, use a proper timezone library) | |
| let localHour = utcHour + timezoneOffset; | |
| if (localHour < 0) localHour += 24; | |
| if (localHour >= 24) localHour -= 24; | |
| return Math.floor(localHour); | |
| } | |
| // Function to determine time of day category | |
| function getTimeCategory(hour) { | |
| if (hour >= 6 && hour < 12) return 'morning'; | |
| if (hour >= 12 && hour < 18) return 'afternoon'; | |
| if (hour >= 18 || hour < 0) return 'evening'; | |
| return 'night'; | |
| } | |
| // Function to get marker color based on time of day | |
| function getMarkerColor(category) { | |
| switch(category) { | |
| case 'morning': return '#ef4444'; // red-500 | |
| case 'afternoon': return '#eab308'; // yellow-500 | |
| case 'evening': return '#3b82f6'; // blue-500 | |
| case 'night': return '#a855f7'; // purple-500 | |
| default: return '#6b7280'; // gray-500 | |
| } | |
| } | |
| // Function to update city markers | |
| function updateCityMarkers(utcHour) { | |
| // Clear existing markers | |
| world.pointsData([]); | |
| // Create new markers with time-based colors | |
| const pointsData = cities.map(city => { | |
| // Simplified timezone offset (in a real app, use a proper timezone library) | |
| // This is a rough approximation - actual timezones can vary | |
| const timezoneOffset = Math.round(city.lng / 15); | |
| const localHour = getLocalTime(utcHour, timezoneOffset); | |
| const timeCategory = getTimeCategory(localHour); | |
| const color = getMarkerColor(timeCategory); | |
| return { | |
| lat: city.lat, | |
| lng: city.lng, | |
| size: 0.15, | |
| color: color, | |
| city: city.name, | |
| localTime: localHour, | |
| timeCategory: timeCategory | |
| }; | |
| }); | |
| // Add points to the globe | |
| world.pointsData(pointsData); | |
| // Add hover effects | |
| world.onPointHover(highlightMarker); | |
| world.onPointClick(showCityInfo); | |
| // Add tooltip functionality | |
| function highlightMarker(point) { | |
| if (point) { | |
| world.pointColor(p => | |
| p === point ? '#ffffff' : p.color | |
| ); | |
| world.pointAltitude(p => | |
| p === point ? 0.2 : 0.1 | |
| ); | |
| // Show tooltip | |
| const ampm = point.localTime >= 12 ? 'PM' : 'AM'; | |
| const displayHour = point.localTime % 12 === 0 ? 12 : point.localTime % 12; | |
| cityTooltip.innerHTML = ` | |
| <strong>${point.city}</strong><br> | |
| Local Time: ${displayHour}:00 ${ampm}<br> | |
| (${point.timeCategory}) | |
| `; | |
| cityTooltip.style.opacity = '1'; | |
| } else { | |
| world.pointColor(p => p.color); | |
| world.pointAltitude(0.1); | |
| cityTooltip.style.opacity = '0'; | |
| } | |
| } | |
| function showCityInfo(point) { | |
| if (point) { | |
| // Center on the city | |
| world.controls().autoRotate = false; | |
| world.pointOfView( | |
| { lat: point.lat, lng: point.lng, altitude: 1.5 }, | |
| 1000 | |
| ); | |
| // Show larger tooltip | |
| const ampm = point.localTime >= 12 ? 'PM' : 'AM'; | |
| const displayHour = point.localTime % 12 === 0 ? 12 : point.localTime % 12; | |
| cityTooltip.innerHTML = ` | |
| <div class="mb-2 text-lg font-bold">${point.city}</div> | |
| <div class="mb-1">Local Time: ${displayHour}:00 ${ampm}</div> | |
| <div>Time of Day: <span class="capitalize">${point.timeCategory}</span></div> | |
| `; | |
| cityTooltip.style.opacity = '1'; | |
| cityTooltip.style.width = '180px'; | |
| // Reset after delay | |
| setTimeout(() => { | |
| world.controls().autoRotate = true; | |
| }, 3000); | |
| } | |
| } | |
| // Position tooltip near mouse | |
| globeContainer.addEventListener('mousemove', (e) => { | |
| cityTooltip.style.left = `${e.clientX + 10}px`; | |
| cityTooltip.style.top = `${e.clientY + 10}px`; | |
| }); | |
| } | |
| // Initialize with current UTC hour | |
| const now = new Date(); | |
| const currentUTCHour = now.getUTCHours(); | |
| timeSlider.value = currentUTCHour; | |
| updateTimeDisplay(currentUTCHour); | |
| // Add slider event listener | |
| timeSlider.addEventListener('input', () => { | |
| updateTimeDisplay(parseInt(timeSlider.value)); | |
| // Update cities list to reflect new times | |
| if (typeof citySearch !== 'undefined' && citySearch) { | |
| populateCitiesList(citySearch.value); | |
| } | |
| }); | |
| // Initialize Lucide icons | |
| lucide.createIcons(); | |
| // Cities Panel functionality | |
| const citiesList = document.getElementById('cities-list'); | |
| const citySearch = document.getElementById('city-search'); | |
| const citiesCount = document.getElementById('cities-count'); | |
| let activeCity = null; | |
| function populateCitiesList(searchTerm = '') { | |
| citiesList.innerHTML = ''; | |
| const filteredCities = cities.filter(city => | |
| city.name.toLowerCase().includes(searchTerm.toLowerCase()) | |
| ); | |
| filteredCities.forEach(city => { | |
| const timezoneOffset = Math.round(city.lng / 15); | |
| const currentSliderHour = parseInt(timeSlider.value); | |
| const localHour = getLocalTime(currentSliderHour, timezoneOffset); | |
| const ampm = localHour >= 12 ? 'PM' : 'AM'; | |
| const displayHour = localHour % 12 === 0 ? 12 : localHour % 12; | |
| const cityItem = document.createElement('div'); | |
| cityItem.className = 'city-item bg-gray-700/50'; | |
| cityItem.innerHTML = ` | |
| <div class="flex flex-col"> | |
| <span class="font-medium text-sm">${city.name}</span> | |
| <span class="city-time">${displayHour}:00 ${ampm}</span> | |
| </div> | |
| <div class="w-2 h-2 rounded-full" style="background: ${getMarkerColor(getTimeCategory(localHour))}"></div> | |
| `; | |
| cityItem.addEventListener('click', () => { | |
| // Remove active class from previous | |
| document.querySelectorAll('.city-item').forEach(el => el.classList.remove('active')); | |
| cityItem.classList.add('active'); | |
| activeCity = city; | |
| // Focus globe on city | |
| world.controls().autoRotate = false; | |
| world.pointOfView( | |
| { lat: city.lat, lng: city.lng, altitude: 1.5 }, | |
| 1000 | |
| ); | |
| // Show tooltip | |
| const timeCategory = getTimeCategory(localHour); | |
| cityTooltip.innerHTML = ` | |
| <div class="mb-2 text-lg font-bold">${city.name}</div> | |
| <div class="mb-1">Local Time: ${displayHour}:00 ${ampm}</div> | |
| <div>Time of Day: <span class="capitalize">${timeCategory}</span></div> | |
| `; | |
| cityTooltip.style.opacity = '1'; | |
| setTimeout(() => { | |
| world.controls().autoRotate = true; | |
| }, 3000); | |
| }); | |
| citiesList.appendChild(cityItem); | |
| }); | |
| citiesCount.textContent = `${filteredCities.length} cities`; | |
| } | |
| // Search functionality | |
| citySearch.addEventListener('input', (e) => { | |
| populateCitiesList(e.target.value); | |
| }); | |
| // Real-time clock | |
| function updateRealtimeClock() { | |
| const now = new Date(); | |
| const hours = String(now.getUTCHours()).padStart(2, '0'); | |
| const minutes = String(now.getUTCMinutes()).padStart(2, '0'); | |
| const seconds = String(now.getUTCSeconds()).padStart(2, '0'); | |
| const clockEl = document.getElementById('realtime-clock'); | |
| if (clockEl) { | |
| clockEl.textContent = `${hours}:${minutes}:${seconds} UTC`; | |
| } | |
| } | |
| setInterval(updateRealtimeClock, 1000); | |
| updateRealtimeClock(); | |
| // Account modal toggle | |
| const accountBtn = document.getElementById('account-btn'); | |
| const accountModal = document.getElementById('account-modal'); | |
| accountBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| accountModal.classList.toggle('hidden'); | |
| }); | |
| // Close modal when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (!accountModal.contains(e.target) && !accountBtn.contains(e.target)) { | |
| accountModal.classList.add('hidden'); | |
| } | |
| }); | |
| // Initialize cities list | |
| populateCitiesList(); | |
| </script> | |
| <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=chipenstain/globe-time" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |