|
|
@@ -0,0 +1,1049 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>Parv's Time Zone Converter</title>
|
|
|
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
|
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
+ <style>
|
|
|
+ :root {
|
|
|
+ --primary-color: #4a6fa5;
|
|
|
+ --secondary-color: #6c757d;
|
|
|
+ --success-color: #28a745;
|
|
|
+ --warning-color: #ffc107;
|
|
|
+ --danger-color: #dc3545;
|
|
|
+ --bg-color: #f8f9fa;
|
|
|
+ --card-bg: #ffffff;
|
|
|
+ --text-color: #212529;
|
|
|
+ --border-color: #dee2e6;
|
|
|
+ --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
+ --hover-color: #e9ecef;
|
|
|
+ --slider-height: 6px;
|
|
|
+ --slider-thumb-size: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dark-mode {
|
|
|
+ --primary-color: #6b8cbe;
|
|
|
+ --secondary-color: #a0a0a0;
|
|
|
+ --success-color: #3bd264;
|
|
|
+ --warning-color: #ffd54f;
|
|
|
+ --danger-color: #e57373;
|
|
|
+ --bg-color: #121212;
|
|
|
+ --card-bg: #1e1e1e;
|
|
|
+ --text-color: #f0f0f0;
|
|
|
+ --border-color: #444444;
|
|
|
+ --shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
|
|
+ --hover-color: #2d2d2d;
|
|
|
+ --slider-height: 8px;
|
|
|
+ --slider-thumb-size: 22px;
|
|
|
+ }
|
|
|
+
|
|
|
+ * {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ transition: background-color 0.3s, color 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
+ background-color: var(--bg-color);
|
|
|
+ color: var(--text-color);
|
|
|
+ line-height: 1.6;
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ max-width: 1400px;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding-bottom: 10px;
|
|
|
+ border-bottom: 1px solid var(--border-color);
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ h1 {
|
|
|
+ font-size: 2rem;
|
|
|
+ color: var(--primary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .controls {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ button {
|
|
|
+ background-color: var(--primary-color);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ padding: 8px 15px;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ button:hover {
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mode-toggle {
|
|
|
+ background-color: var(--secondary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .mode-toggle.active {
|
|
|
+ background-color: var(--success-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .timezone-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timezone-card {
|
|
|
+ background-color: var(--card-bg);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ position: relative;
|
|
|
+ transition: transform 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timezone-card:hover {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .timezone-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .timezone-name {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 1.2rem;
|
|
|
+ color: var(--primary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-display {
|
|
|
+ font-size: 2rem;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-time-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 1.2rem;
|
|
|
+ padding: 5px;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .edit-time-btn:hover {
|
|
|
+ background-color: var(--hover-color);
|
|
|
+ color: var(--primary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-input-container {
|
|
|
+ display: none;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-input {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ border-radius: 4px;
|
|
|
+ width: 100%;
|
|
|
+ font-size: 1rem;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-btn {
|
|
|
+ flex: 1;
|
|
|
+ padding: 6px;
|
|
|
+ font-size: 0.85rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .date-display {
|
|
|
+ color: var(--secondary-color);
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .gmt-offset {
|
|
|
+ font-size: 0.9rem;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .dst-note {
|
|
|
+ font-size: 0.85rem;
|
|
|
+ color: var(--success-color);
|
|
|
+ font-style: italic;
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .delete-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ right: 10px;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 1.2rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .delete-btn:hover {
|
|
|
+ color: var(--danger-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .slider-container {
|
|
|
+ margin: 20px 0;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .slider-marks {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 0 10px;
|
|
|
+ margin-top: 5px;
|
|
|
+ font-size: 0.85rem;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .slider {
|
|
|
+ width: 100%;
|
|
|
+ height: var(--slider-height);
|
|
|
+ margin: 15px 0;
|
|
|
+ -webkit-appearance: none;
|
|
|
+ appearance: none;
|
|
|
+ background: linear-gradient(to right, #4a6fa5 0%, #6c757d 100%);
|
|
|
+ outline: none;
|
|
|
+ border-radius: 3px;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .slider::-webkit-slider-thumb {
|
|
|
+ -webkit-appearance: none;
|
|
|
+ appearance: none;
|
|
|
+ width: var(--slider-thumb-size);
|
|
|
+ height: var(--slider-thumb-size);
|
|
|
+ border-radius: 50%;
|
|
|
+ background: var(--primary-color);
|
|
|
+ cursor: pointer;
|
|
|
+ box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .slider::-webkit-slider-thumb:hover {
|
|
|
+ transform: scale(1.2);
|
|
|
+ background: var(--success-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .slider-info {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 0.85rem;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ margin-top: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .slider-value {
|
|
|
+ font-weight: bold;
|
|
|
+ color: var(--primary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .add-timezone {
|
|
|
+ margin-top: 30px;
|
|
|
+ padding: 20px;
|
|
|
+ background-color: var(--card-bg);
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ }
|
|
|
+
|
|
|
+ .add-timezone h2 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: var(--primary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+
|
|
|
+ select, input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ border-radius: 4px;
|
|
|
+ background-color: var(--card-bg);
|
|
|
+ color: var(--text-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box {
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-results {
|
|
|
+ position: absolute;
|
|
|
+ top: 100%;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background-color: var(--card-bg);
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ border-top: none;
|
|
|
+ border-radius: 0 0 4px 4px;
|
|
|
+ max-height: 200px;
|
|
|
+ overflow-y: auto;
|
|
|
+ z-index: 100;
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-result-item {
|
|
|
+ padding: 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-result-item:hover {
|
|
|
+ background-color: var(--hover-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ footer {
|
|
|
+ margin-top: 40px;
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px;
|
|
|
+ border-top: 1px solid var(--border-color);
|
|
|
+ color: var(--secondary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-zone-examples {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+ margin-top: 15px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-zone-example {
|
|
|
+ background: var(--hover-color);
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .time-zone-example:hover {
|
|
|
+ background: var(--primary-color);
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .timezone-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+
|
|
|
+ header {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .controls {
|
|
|
+ width: 100%;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <header>
|
|
|
+ <h1>Parv's Time Zone Converter</h1>
|
|
|
+ <div class="controls">
|
|
|
+ <button id="theme-toggle">
|
|
|
+ <i class="fas fa-moon"></i> Toggle Dark Mode
|
|
|
+ </button>
|
|
|
+ <button id="refresh-btn">
|
|
|
+ <i class="fas fa-sync-alt"></i> Refresh Times
|
|
|
+ </button>
|
|
|
+ <button id="real-time-mode" class="mode-toggle active">
|
|
|
+ <i class="fas fa-clock"></i> Real-Time Mode
|
|
|
+ </button>
|
|
|
+ <button id="converter-mode" class="mode-toggle">
|
|
|
+ <i class="fas fa-sliders-h"></i> Converter Mode
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="timezone-grid" id="timezone-container">
|
|
|
+ <!-- Timezone cards will be generated here -->
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="add-timezone">
|
|
|
+ <h2>Add Time Zone</h2>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="timezone-search">Search Time Zones:</label>
|
|
|
+ <div class="search-box">
|
|
|
+ <input type="text" id="timezone-search" placeholder="Start typing to search time zones...">
|
|
|
+ <div class="search-results" id="search-results"></div>
|
|
|
+ </div>
|
|
|
+ <div class="time-zone-examples">
|
|
|
+ <div class="time-zone-example">America/New_York</div>
|
|
|
+ <div class="time-zone-example">Asia/Kolkata</div>
|
|
|
+ <div class="time-zone-example">Europe/London</div>
|
|
|
+ <div class="time-zone-example">Australia/Sydney</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <button id="add-timezone-btn">
|
|
|
+ <i class="fas fa-plus"></i> Add Selected Time Zone
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <footer>
|
|
|
+ <p>Made by Parv Ashwani © 2025 | All Major Time Zones Supported</p>
|
|
|
+ </footer>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
+ // DOM elements
|
|
|
+ const themeToggle = document.getElementById('theme-toggle');
|
|
|
+ const refreshBtn = document.getElementById('refresh-btn');
|
|
|
+ const realTimeModeBtn = document.getElementById('real-time-mode');
|
|
|
+ const converterModeBtn = document.getElementById('converter-mode');
|
|
|
+ const timezoneContainer = document.getElementById('timezone-container');
|
|
|
+ const timezoneSearch = document.getElementById('timezone-search');
|
|
|
+ const searchResults = document.getElementById('search-results');
|
|
|
+ const addTimezoneBtn = document.getElementById('add-timezone-btn');
|
|
|
+
|
|
|
+ // Application state
|
|
|
+ let isRealTimeMode = true;
|
|
|
+ let allTimeZones = [];
|
|
|
+ let selectedTimezone = null;
|
|
|
+ let baseTime = new Date();
|
|
|
+ let updateInterval = null;
|
|
|
+
|
|
|
+ // Default time zones
|
|
|
+ const defaultTimeZones = [
|
|
|
+ { id: 'America/New_York', name: 'EDT/EST', label: 'Eastern Time' },
|
|
|
+ { id: 'Asia/Kolkata', name: 'IST', label: 'India Standard Time' },
|
|
|
+ { id: 'America/Chicago', name: 'CDT/CST', label: 'Central Time' },
|
|
|
+ { id: 'America/Los_Angeles', name: 'PDT/PST', label: 'Pacific Time' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // User-added time zones
|
|
|
+ let customTimeZones = JSON.parse(localStorage.getItem('customTimeZones')) || [];
|
|
|
+
|
|
|
+ // Initialize the app
|
|
|
+ function init() {
|
|
|
+ loadAllTimeZones();
|
|
|
+ renderAllTimeZones();
|
|
|
+ startRealTimeUpdates();
|
|
|
+
|
|
|
+ // Set up event listeners
|
|
|
+ setupEventListeners();
|
|
|
+
|
|
|
+ // Add example time zone handlers
|
|
|
+ document.querySelectorAll('.time-zone-example').forEach(example => {
|
|
|
+ example.addEventListener('click', function() {
|
|
|
+ timezoneSearch.value = this.textContent;
|
|
|
+ handleTimezoneSearch();
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Load all available time zones
|
|
|
+ function loadAllTimeZones() {
|
|
|
+ try {
|
|
|
+ allTimeZones = Intl.supportedValuesOf('timeZone').map(tz => {
|
|
|
+ return {
|
|
|
+ id: tz,
|
|
|
+ name: tz.split('/').pop().replace(/_/g, ' '),
|
|
|
+ label: tz
|
|
|
+ };
|
|
|
+ });
|
|
|
+ } catch (e) {
|
|
|
+ console.error("Could not load time zones:", e);
|
|
|
+ // Fallback to a subset of time zones
|
|
|
+ allTimeZones = [
|
|
|
+ { id: 'America/New_York', name: 'New York', label: 'Eastern Time' },
|
|
|
+ { id: 'Europe/London', name: 'London', label: 'Greenwich Mean Time' },
|
|
|
+ { id: 'Asia/Tokyo', name: 'Tokyo', label: 'Japan Standard Time' },
|
|
|
+ { id: 'Australia/Sydney', name: 'Sydney', label: 'Australian Eastern Time' },
|
|
|
+ { id: 'Europe/Paris', name: 'Paris', label: 'Central European Time' },
|
|
|
+ { id: 'Asia/Dubai', name: 'Dubai', label: 'Gulf Standard Time' },
|
|
|
+ { id: 'America/Los_Angeles', name: 'Los Angeles', label: 'Pacific Time' },
|
|
|
+ { id: 'America/Chicago', name: 'Chicago', label: 'Central Time' },
|
|
|
+ { id: 'Asia/Kolkata', name: 'Kolkata', label: 'India Standard Time' },
|
|
|
+ { id: 'Asia/Shanghai', name: 'Shanghai', label: 'China Standard Time' }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sort time zones by name
|
|
|
+ allTimeZones.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set up event listeners
|
|
|
+ function setupEventListeners() {
|
|
|
+ // Theme toggle
|
|
|
+ themeToggle.addEventListener('click', toggleTheme);
|
|
|
+
|
|
|
+ // Refresh button
|
|
|
+ refreshBtn.addEventListener('click', refreshAllTimes);
|
|
|
+
|
|
|
+ // Mode toggles
|
|
|
+ realTimeModeBtn.addEventListener('click', () => setMode(true));
|
|
|
+ converterModeBtn.addEventListener('click', () => setMode(false));
|
|
|
+
|
|
|
+ // Time zone search
|
|
|
+ timezoneSearch.addEventListener('input', handleTimezoneSearch);
|
|
|
+
|
|
|
+ // Add time zone button
|
|
|
+ addTimezoneBtn.addEventListener('click', addSelectedTimezone);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Toggle between dark and light mode
|
|
|
+ function toggleTheme() {
|
|
|
+ document.body.classList.toggle('dark-mode');
|
|
|
+ localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
|
|
|
+
|
|
|
+ // Update icon
|
|
|
+ const icon = themeToggle.querySelector('i');
|
|
|
+ if (document.body.classList.contains('dark-mode')) {
|
|
|
+ icon.className = 'fas fa-sun';
|
|
|
+ themeToggle.innerHTML = '<i class="fas fa-sun"></i> Toggle Light Mode';
|
|
|
+ } else {
|
|
|
+ icon.className = 'fas fa-moon';
|
|
|
+ themeToggle.innerHTML = '<i class="fas fa-moon"></i> Toggle Dark Mode';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set the application mode (real-time or converter)
|
|
|
+ function setMode(isRealTime) {
|
|
|
+ isRealTimeMode = isRealTime;
|
|
|
+
|
|
|
+ // Update UI
|
|
|
+ if (isRealTimeMode) {
|
|
|
+ realTimeModeBtn.classList.add('active');
|
|
|
+ converterModeBtn.classList.remove('active');
|
|
|
+ startRealTimeUpdates();
|
|
|
+ } else {
|
|
|
+ realTimeModeBtn.classList.remove('active');
|
|
|
+ converterModeBtn.classList.add('active');
|
|
|
+ stopRealTimeUpdates();
|
|
|
+
|
|
|
+ // Set base time to current time when switching to converter mode
|
|
|
+ baseTime = new Date();
|
|
|
+ updateAllTimes();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update sliders
|
|
|
+ updateAllSliders();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Start real-time updates
|
|
|
+ function startRealTimeUpdates() {
|
|
|
+ if (updateInterval) clearInterval(updateInterval);
|
|
|
+ updateInterval = setInterval(updateAllTimes, 1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stop real-time updates
|
|
|
+ function stopRealTimeUpdates() {
|
|
|
+ if (updateInterval) {
|
|
|
+ clearInterval(updateInterval);
|
|
|
+ updateInterval = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Refresh all times
|
|
|
+ function refreshAllTimes() {
|
|
|
+ baseTime = new Date();
|
|
|
+ updateAllTimes();
|
|
|
+ updateAllSliders();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Render all time zone cards
|
|
|
+ function renderAllTimeZones() {
|
|
|
+ timezoneContainer.innerHTML = '';
|
|
|
+
|
|
|
+ // Add default time zones
|
|
|
+ defaultTimeZones.forEach(tz => {
|
|
|
+ timezoneContainer.appendChild(createTimeZoneCard(tz));
|
|
|
+ });
|
|
|
+
|
|
|
+ // Add custom time zones
|
|
|
+ customTimeZones.forEach(tz => {
|
|
|
+ timezoneContainer.appendChild(createTimeZoneCard(tz));
|
|
|
+ });
|
|
|
+
|
|
|
+ updateAllTimes();
|
|
|
+ updateAllSliders();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create a time zone card
|
|
|
+ function createTimeZoneCard(timezone) {
|
|
|
+ const card = document.createElement('div');
|
|
|
+ card.className = 'timezone-card';
|
|
|
+ card.dataset.timezone = timezone.id;
|
|
|
+
|
|
|
+ const isDefault = defaultTimeZones.some(tz => tz.id === timezone.id);
|
|
|
+
|
|
|
+ card.innerHTML = `
|
|
|
+ ${!isDefault ? '<button class="delete-btn"><i class="fas fa-times"></i></button>' : ''}
|
|
|
+ <div class="timezone-header">
|
|
|
+ <div class="timezone-name">${timezone.name}</div>
|
|
|
+ <div class="timezone-label">${timezone.label}</div>
|
|
|
+ </div>
|
|
|
+ <div class="date-display">Loading date...</div>
|
|
|
+ <div class="time-container">
|
|
|
+ <div class="time-display">Loading time...</div>
|
|
|
+ <button class="edit-time-btn" title="Edit time"><i class="fas fa-edit"></i></button>
|
|
|
+ </div>
|
|
|
+ <div class="gmt-offset">Loading GMT offset...</div>
|
|
|
+ <div class="dst-note"></div>
|
|
|
+ <div class="time-input-container">
|
|
|
+ <input type="text" class="time-input" placeholder="e.g., 6:26 am" pattern="\\d{1,2}:\\d{2}\\s*[ap]m" required>
|
|
|
+ <div class="input-actions">
|
|
|
+ <button class="save-time-btn input-btn">Save</button>
|
|
|
+ <button class="cancel-time-btn input-btn">Cancel</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="slider-container">
|
|
|
+ <div class="slider-marks">
|
|
|
+ <span>12am</span>
|
|
|
+ <span>3am</span>
|
|
|
+ <span>6am</span>
|
|
|
+ <span>9am</span>
|
|
|
+ <span>12pm</span>
|
|
|
+ <span>3pm</span>
|
|
|
+ <span>6pm</span>
|
|
|
+ <span>9pm</span>
|
|
|
+ </div>
|
|
|
+ <input type="range" class="slider" min="0" max="1440" value="720">
|
|
|
+ <div class="slider-info">
|
|
|
+ <span>12:00 AM</span>
|
|
|
+ <span class="slider-value">12:00 PM</span>
|
|
|
+ <span>11:59 PM</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // Add delete functionality for custom time zones
|
|
|
+ const deleteBtn = card.querySelector('.delete-btn');
|
|
|
+ if (deleteBtn) {
|
|
|
+ deleteBtn.addEventListener('click', function() {
|
|
|
+ removeTimeZone(timezone.id);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add time editing functionality
|
|
|
+ const editBtn = card.querySelector('.edit-time-btn');
|
|
|
+ const timeContainer = card.querySelector('.time-container');
|
|
|
+ const timeInputContainer = card.querySelector('.time-input-container');
|
|
|
+ const timeInput = card.querySelector('.time-input');
|
|
|
+ const saveBtn = card.querySelector('.save-time-btn');
|
|
|
+ const cancelBtn = card.querySelector('.cancel-time-btn');
|
|
|
+
|
|
|
+ editBtn.addEventListener('click', function() {
|
|
|
+ timeContainer.style.display = 'none';
|
|
|
+ timeInputContainer.style.display = 'block';
|
|
|
+ timeInput.value = card.querySelector('.time-display').textContent.trim();
|
|
|
+ });
|
|
|
+
|
|
|
+ cancelBtn.addEventListener('click', function() {
|
|
|
+ timeContainer.style.display = 'flex';
|
|
|
+ timeInputContainer.style.display = 'none';
|
|
|
+ });
|
|
|
+
|
|
|
+ saveBtn.addEventListener('click', function() {
|
|
|
+ const timeStr = timeInput.value;
|
|
|
+ const timeObj = parseTimeInput(timeStr);
|
|
|
+
|
|
|
+ if (!timeObj) {
|
|
|
+ alert('Invalid time format. Please use HH:MM AM/PM (e.g., 6:26 am)');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const enteredTotalMinutes = timeObj.hours * 60 + timeObj.minutes;
|
|
|
+
|
|
|
+ // Get current time in this time zone
|
|
|
+ const timezoneId = card.dataset.timezone;
|
|
|
+ const currentTzTimeStr = baseTime.toLocaleTimeString('en-US', {
|
|
|
+ timeZone: timezoneId,
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit',
|
|
|
+ hour12: false
|
|
|
+ });
|
|
|
+
|
|
|
+ const [currentHours, currentMinutes] = currentTzTimeStr.split(':').map(Number);
|
|
|
+ const currentTotalMinutes = currentHours * 60 + currentMinutes;
|
|
|
+
|
|
|
+ // Calculate the difference in minutes
|
|
|
+ const diffMinutes = enteredTotalMinutes - currentTotalMinutes;
|
|
|
+
|
|
|
+ // Adjust baseTime
|
|
|
+ baseTime = new Date(baseTime.getTime() + diffMinutes * 60000);
|
|
|
+
|
|
|
+ // Update all times and sliders
|
|
|
+ updateAllTimes();
|
|
|
+ updateAllSliders();
|
|
|
+
|
|
|
+ // Revert to display mode
|
|
|
+ timeContainer.style.display = 'flex';
|
|
|
+ timeInputContainer.style.display = 'none';
|
|
|
+ });
|
|
|
+
|
|
|
+ // Add slider functionality
|
|
|
+ const slider = card.querySelector('.slider');
|
|
|
+ const sliderValue = card.querySelector('.slider-value');
|
|
|
+
|
|
|
+ slider.addEventListener('input', function() {
|
|
|
+ const totalMinutes = parseInt(this.value);
|
|
|
+ const hours = Math.floor(totalMinutes / 60);
|
|
|
+ const minutes = totalMinutes % 60;
|
|
|
+
|
|
|
+ // Update slider value display
|
|
|
+ const timeStr = formatTimeForSlider(hours, minutes);
|
|
|
+ sliderValue.textContent = timeStr;
|
|
|
+
|
|
|
+ if (!isRealTimeMode) {
|
|
|
+ adjustTimeWithSlider(timezone.id, totalMinutes);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return card;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse time input in HH:MM AM/PM format
|
|
|
+ function parseTimeInput(str) {
|
|
|
+ const match = str.match(/(\d{1,2}):(\d{2})\s*([ap]m)/i);
|
|
|
+ if (!match) return null;
|
|
|
+
|
|
|
+ let hours = parseInt(match[1]);
|
|
|
+ const minutes = parseInt(match[2]);
|
|
|
+ const period = match[3].toLowerCase();
|
|
|
+
|
|
|
+ if (period === 'pm' && hours < 12) hours += 12;
|
|
|
+ if (period === 'am' && hours === 12) hours = 0;
|
|
|
+
|
|
|
+ return { hours, minutes };
|
|
|
+ }
|
|
|
+
|
|
|
+ // Format time for slider display
|
|
|
+ function formatTimeForSlider(hours, minutes) {
|
|
|
+ const period = hours >= 12 ? 'PM' : 'AM';
|
|
|
+ const displayHours = hours % 12 || 12;
|
|
|
+ return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update all time displays
|
|
|
+ function updateAllTimes() {
|
|
|
+ const cards = timezoneContainer.querySelectorAll('.timezone-card');
|
|
|
+ const now = isRealTimeMode ? new Date() : baseTime;
|
|
|
+
|
|
|
+ cards.forEach(card => {
|
|
|
+ const timezone = card.dataset.timezone;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Format time
|
|
|
+ const timeStr = now.toLocaleTimeString('en-US', {
|
|
|
+ timeZone: timezone,
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit',
|
|
|
+ hour12: true
|
|
|
+ });
|
|
|
+
|
|
|
+ // Format date
|
|
|
+ const dateStr = now.toLocaleDateString('en-US', {
|
|
|
+ timeZone: timezone,
|
|
|
+ weekday: 'short',
|
|
|
+ month: 'short',
|
|
|
+ day: 'numeric',
|
|
|
+ year: 'numeric'
|
|
|
+ });
|
|
|
+
|
|
|
+ // Get GMT offset
|
|
|
+ const offset = getGMTOffset(now, timezone);
|
|
|
+
|
|
|
+ // Update DOM
|
|
|
+ card.querySelector('.time-display').textContent = timeStr;
|
|
|
+ card.querySelector('.date-display').textContent = dateStr;
|
|
|
+ card.querySelector('.gmt-offset').textContent = offset;
|
|
|
+
|
|
|
+ // Check for DST
|
|
|
+ const isDST = isDaylightSavingTime(now, timezone);
|
|
|
+ const dstNote = card.querySelector('.dst-note');
|
|
|
+
|
|
|
+ if (isDST !== null) {
|
|
|
+ if (isDST) {
|
|
|
+ dstNote.textContent = `Daylight Saving Time observed (${getDSTName(timezone)})`;
|
|
|
+ } else {
|
|
|
+ dstNote.textContent = 'Standard Time observed';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ dstNote.textContent = '';
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`Error updating time for ${timezone}:`, e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update all sliders based on current time
|
|
|
+ function updateAllSliders() {
|
|
|
+ const cards = timezoneContainer.querySelectorAll('.timezone-card');
|
|
|
+ const now = isRealTimeMode ? new Date() : baseTime;
|
|
|
+
|
|
|
+ cards.forEach(card => {
|
|
|
+ const timezone = card.dataset.timezone;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const timeStr = now.toLocaleTimeString('en-US', {
|
|
|
+ timeZone: timezone,
|
|
|
+ hour: '2-digit',
|
|
|
+ hour12: false,
|
|
|
+ minute: '2-digit'
|
|
|
+ });
|
|
|
+
|
|
|
+ const [hours, minutes] = timeStr.split(':').map(Number);
|
|
|
+ const totalMinutes = hours * 60 + minutes;
|
|
|
+
|
|
|
+ const slider = card.querySelector('.slider');
|
|
|
+ const sliderValue = card.querySelector('.slider-value');
|
|
|
+
|
|
|
+ slider.value = totalMinutes;
|
|
|
+ sliderValue.textContent = formatTimeForSlider(hours, minutes);
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`Error updating slider for ${timezone}:`, e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get GMT offset string
|
|
|
+ function getGMTOffset(date, timezone) {
|
|
|
+ try {
|
|
|
+ const formatter = new Intl.DateTimeFormat('en-US', {
|
|
|
+ timeZone: timezone,
|
|
|
+ timeZoneName: 'shortOffset'
|
|
|
+ });
|
|
|
+
|
|
|
+ const parts = formatter.formatToParts(date);
|
|
|
+ const offset = parts.find(part => part.type === 'timeZoneName')?.value || '';
|
|
|
+ return offset.replace('GMT', 'GMT');
|
|
|
+ } catch (e) {
|
|
|
+ return "Unknown offset";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if daylight saving time is observed
|
|
|
+ function isDaylightSavingTime(date, timezone) {
|
|
|
+ try {
|
|
|
+ // Try to get timezone name which often indicates DST
|
|
|
+ const formatter = new Intl.DateTimeFormat('en-US', {
|
|
|
+ timeZone: timezone,
|
|
|
+ timeZoneName: 'long'
|
|
|
+ });
|
|
|
+
|
|
|
+ const tzName = formatter.resolvedOptions().timeZoneName;
|
|
|
+ if (!tzName) return null;
|
|
|
+
|
|
|
+ // Check if the timezone name suggests DST
|
|
|
+ return formatter.format(date).includes("Daylight") ||
|
|
|
+ tzName.includes("Daylight") ||
|
|
|
+ /[+-]\d{2}:\d{2}/.test(tzName);
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`Error checking DST for ${timezone}:`, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get DST name for timezone
|
|
|
+ function getDSTName(timezone) {
|
|
|
+ if (timezone.includes('New_York')) return 'EDT';
|
|
|
+ if (timezone.includes('Chicago')) return 'CDT';
|
|
|
+ if (timezone.includes('Los_Angeles')) return 'PDT';
|
|
|
+ if (timezone.includes('London') || timezone.includes('Europe')) return 'BST';
|
|
|
+ return 'DST';
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle timezone search
|
|
|
+ function handleTimezoneSearch() {
|
|
|
+ const query = timezoneSearch.value.toLowerCase();
|
|
|
+
|
|
|
+ if (query.length < 2) {
|
|
|
+ searchResults.style.display = 'none';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const results = allTimeZones.filter(tz =>
|
|
|
+ tz.id.toLowerCase().includes(query) ||
|
|
|
+ tz.name.toLowerCase().includes(query) ||
|
|
|
+ tz.label.toLowerCase().includes(query)
|
|
|
+ ).slice(0, 10); // Limit to 10 results
|
|
|
+
|
|
|
+ displaySearchResults(results);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Display search results
|
|
|
+ function displaySearchResults(results) {
|
|
|
+ searchResults.innerHTML = '';
|
|
|
+
|
|
|
+ if (results.length === 0) {
|
|
|
+ searchResults.innerHTML = '<div class="search-result-item">No results found</div>';
|
|
|
+ } else {
|
|
|
+ results.forEach(tz => {
|
|
|
+ const item = document.createElement('div');
|
|
|
+ item.className = 'search-result-item';
|
|
|
+ item.textContent = `${tz.name} (${tz.id})`;
|
|
|
+ item.dataset.timezone = tz.id;
|
|
|
+
|
|
|
+ item.addEventListener('click', () => {
|
|
|
+ selectedTimezone = tz;
|
|
|
+ timezoneSearch.value = `${tz.name} (${tz.id})`;
|
|
|
+ searchResults.style.display = 'none';
|
|
|
+ });
|
|
|
+
|
|
|
+ searchResults.appendChild(item);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ searchResults.style.display = 'block';
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add selected time zone
|
|
|
+ function addSelectedTimezone() {
|
|
|
+ if (!selectedTimezone) {
|
|
|
+ alert('Please select a time zone from the search results.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if already added
|
|
|
+ if ([...defaultTimeZones, ...customTimeZones].some(tz => tz.id === selectedTimezone.id)) {
|
|
|
+ alert('This time zone is already added.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ customTimeZones.push(selectedTimezone);
|
|
|
+ localStorage.setItem('customTimeZones', JSON.stringify(customTimeZones));
|
|
|
+
|
|
|
+ timezoneContainer.appendChild(createTimeZoneCard(selectedTimezone));
|
|
|
+ updateAllTimes();
|
|
|
+ updateAllSliders();
|
|
|
+
|
|
|
+ // Reset selection
|
|
|
+ selectedTimezone = null;
|
|
|
+ timezoneSearch.value = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // Remove a time zone
|
|
|
+ function removeTimeZone(timezoneId) {
|
|
|
+ customTimeZones = customTimeZones.filter(tz => tz.id !== timezoneId);
|
|
|
+ localStorage.setItem('customTimeZones', JSON.stringify(customTimeZones));
|
|
|
+ renderAllTimeZones();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Adjust time with slider
|
|
|
+ function adjustTimeWithSlider(timezoneId, minutes) {
|
|
|
+ const cards = timezoneContainer.querySelectorAll('.timezone-card');
|
|
|
+ const adjustedCard = Array.from(cards).find(card => card.dataset.timezone === timezoneId);
|
|
|
+
|
|
|
+ if (!adjustedCard) return;
|
|
|
+
|
|
|
+ // Calculate the new base time based on slider adjustment
|
|
|
+ const now = new Date();
|
|
|
+ const adjustedTime = new Date(now);
|
|
|
+
|
|
|
+ // Get the current time in the adjusted timezone
|
|
|
+ const currentHours = now.toLocaleTimeString('en-US', {
|
|
|
+ timeZone: timezoneId,
|
|
|
+ hour: '2-digit',
|
|
|
+ hour12: false
|
|
|
+ });
|
|
|
+
|
|
|
+ const currentMinutes = now.toLocaleTimeString('en-US', {
|
|
|
+ timeZone: timezoneId,
|
|
|
+ minute: '2-digit'
|
|
|
+ });
|
|
|
+
|
|
|
+ const currentTotalMinutes = parseInt(currentHours) * 60 + parseInt(currentMinutes);
|
|
|
+ const minuteDifference = minutes - currentTotalMinutes;
|
|
|
+
|
|
|
+ // Adjust the base time
|
|
|
+ baseTime = new Date(now.getTime() + minuteDifference * 60000);
|
|
|
+
|
|
|
+ // Update all times
|
|
|
+ updateAllTimes();
|
|
|
+
|
|
|
+ // Update all sliders except the one being adjusted
|
|
|
+ cards.forEach(card => {
|
|
|
+ if (card.dataset.timezone !== timezoneId) {
|
|
|
+ const tz = card.dataset.timezone;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const timeStr = baseTime.toLocaleTimeString('en-US', {
|
|
|
+ timeZone: tz,
|
|
|
+ hour: '2-digit',
|
|
|
+ hour12: false,
|
|
|
+ minute: '2-digit'
|
|
|
+ });
|
|
|
+
|
|
|
+ const [hours, minutes] = timeStr.split(':').map(Number);
|
|
|
+ const totalMinutes = hours * 60 + minutes;
|
|
|
+
|
|
|
+ const slider = card.querySelector('.slider');
|
|
|
+ const sliderValue = card.querySelector('.slider-value');
|
|
|
+
|
|
|
+ slider.value = totalMinutes;
|
|
|
+ sliderValue.textContent = formatTimeForSlider(hours, minutes);
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`Error updating slider for ${tz}:`, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Initialize the app
|
|
|
+ init();
|
|
|
+
|
|
|
+ // Load saved theme
|
|
|
+ if (localStorage.getItem('theme') === 'dark') {
|
|
|
+ document.body.classList.add('dark-mode');
|
|
|
+ themeToggle.innerHTML = '<i class="fas fa-sun"></i> Toggle Light Mode';
|
|
|
+ }
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|