globe-time / index.html
chipenstain's picture
🐳 24/03 - 17:01 - can you extend app by adding UI widgets: cities, city search, realtime tyme showing, account
d4e49a2 verified
Raw
History Blame Contribute Delete
22.1 kB
<!DOCTYPE html>
<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>