|
4 | 4 | <script>fetch('https://api.countapi.xyz/hit/rmkr-dev.github.io/time');</script> |
5 | 5 | <meta charset="UTF-8"> |
6 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
7 | | - <title>Time Zone Overlay | RMKR Dev</title> |
| 7 | + <title>Global Time Sync | RMKR Dev</title> |
8 | 8 |
|
9 | 9 | <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23FFC300' d='M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z'/%3E%3C/svg%3E"> |
10 | | - |
11 | 10 | <script src="https://cdn.tailwindcss.com"></script> |
12 | 11 | <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"> |
13 | 12 | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" /> |
|
18 | 17 | --color-accent: #FFC300; --color-text: #F0F4F8; |
19 | 18 | --color-muted: #A0AEC0; --color-line: #2D3748; |
20 | 19 | } |
21 | | - body { font-family: 'Inter', sans-serif; background-color: var(--color-bg); color: var(--color-text); overflow-x: hidden; } |
22 | | - .wrap { max-width: 1100px; margin: 0 auto; padding: 2rem; } |
| 20 | + body { font-family: 'Inter', sans-serif; background-color: var(--color-bg); color: var(--color-text); } |
| 21 | + .wrap { max-width: 1200px; margin: 0 auto; padding: 2rem; } |
23 | 22 |
|
24 | 23 | .timeline-row { |
25 | 24 | background: var(--color-card); |
26 | 25 | border: 1px solid var(--color-line); |
27 | 26 | border-radius: 12px; |
28 | | - padding: 1.25rem; |
29 | | - margin-bottom: 0.75rem; |
| 27 | + padding: 1rem 1.5rem; |
| 28 | + margin-bottom: 0.5rem; |
| 29 | + display: flex; |
| 30 | + align-items: center; |
| 31 | + gap: 20px; |
| 32 | + animation: slideIn 0.3s ease-out; |
30 | 33 | } |
31 | 34 |
|
32 | | - /* The scrollable grid container */ |
33 | | - .grid-container { |
34 | | - overflow-x: auto; |
35 | | - padding-bottom: 8px; |
36 | | - scrollbar-width: thin; |
37 | | - scrollbar-color: var(--color-line) transparent; |
38 | | - } |
| 35 | + @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } |
39 | 36 |
|
40 | | - .hour-grid { |
41 | | - display: grid; |
42 | | - grid-template-columns: repeat(24, minmax(40px, 1fr)); |
43 | | - gap: 4px; |
44 | | - margin-top: 10px; |
45 | | - min-width: 960px; /* Ensures cells don't get too squished */ |
46 | | - } |
| 37 | + .tz-info { width: 240px; flex-shrink: 0; border-right: 1px solid var(--color-line); padding-right: 15px; } |
| 38 | + |
| 39 | + .grid-viewport { flex-grow: 1; overflow-x: auto; scrollbar-width: none; } |
| 40 | + .grid-viewport::-webkit-scrollbar { display: none; } |
| 41 | + |
| 42 | + .hour-grid { display: grid; grid-template-columns: repeat(24, 42px); gap: 4px; } |
47 | 43 |
|
48 | 44 | .hour-cell { |
49 | | - height: 45px; |
| 45 | + height: 48px; |
50 | 46 | background: rgba(255, 255, 255, 0.03); |
51 | | - border: 1px solid rgba(255, 255, 255, 0.05); |
| 47 | + border: 1px solid rgba(255, 255, 255, 0.08); |
52 | 48 | border-radius: 6px; |
53 | 49 | display: flex; |
54 | 50 | flex-direction: column; |
55 | 51 | align-items: center; |
56 | 52 | justify-content: center; |
57 | 53 | font-size: 0.7rem; |
58 | | - color: var(--color-muted); |
59 | 54 | cursor: pointer; |
60 | 55 | transition: all 0.2s; |
61 | 56 | } |
62 | 57 |
|
63 | | - .hour-cell:hover { background: rgba(255, 195, 0, 0.15); border-color: var(--color-accent); } |
64 | | - .hour-cell.active { background: var(--color-accent) !important; color: var(--color-bg) !important; font-weight: 800; border-color: var(--color-accent); transform: scale(1.05); z-index: 2; } |
65 | | - .hour-cell.business { border-bottom: 3px solid #4ade80; } |
66 | | - .hour-cell.night { background: rgba(0, 0, 0, 0.2); } |
67 | | - |
68 | | - .ampm { font-size: 0.55rem; opacity: 0.7; } |
| 58 | + .hour-cell:hover { border-color: var(--color-accent); background: rgba(255, 195, 0, 0.1); } |
| 59 | + .hour-cell.active { |
| 60 | + background: var(--color-accent) !important; |
| 61 | + color: var(--color-bg) !important; |
| 62 | + font-weight: 800; |
| 63 | + transform: scale(1.05); |
| 64 | + z-index: 10; |
| 65 | + } |
| 66 | + .hour-cell.work { border-bottom: 3px solid #4ade80; } |
| 67 | + .hour-cell.night { opacity: 0.4; background: rgba(0,0,0,0.2); } |
69 | 68 |
|
70 | | - select { |
| 69 | + input#tz-search { |
71 | 70 | background: #0C1524; color: white; border: 1px solid var(--color-line); |
72 | | - padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; outline: none; |
| 71 | + padding: 12px 18px; border-radius: 10px; width: 350px; outline: none; |
| 72 | + box-shadow: 0 4px 15px rgba(0,0,0,0.2); |
73 | 73 | } |
| 74 | + input#tz-search:focus { border-color: var(--color-accent); } |
74 | 75 |
|
75 | | - .btn-gold { background: var(--color-accent); color: var(--color-bg); padding: 0.5rem 1.25rem; border-radius: 6px; font-weight: 700; transition: all 0.2s; } |
76 | | - .btn-gold:hover { opacity: 0.9; transform: translateY(-1px); } |
| 76 | + .btn-add { |
| 77 | + background: var(--color-accent); color: var(--color-bg); |
| 78 | + font-weight: 800; padding: 12px 24px; border-radius: 10px; |
| 79 | + text-transform: uppercase; font-size: 0.75rem; letter-spacing: 1px; |
| 80 | + } |
77 | 81 | </style> |
78 | 82 | </head> |
79 | 83 | <body> |
80 | 84 |
|
81 | 85 | <div class="wrap"> |
82 | | - <header class="flex justify-between items-center mb-8 border-b border-[--color-line] pb-4"> |
| 86 | + <header class="flex justify-between items-center mb-10"> |
83 | 87 | <div> |
84 | | - <h1 class="text-2xl font-bold tracking-tight">Time <span class="text-[--color-accent]">Sync</span> Overlay</h1> |
85 | | - <p class="text-xs text-[--color-muted] mt-1 uppercase tracking-widest">Global Meeting Planner</p> |
| 88 | + <h1 class="text-3xl font-black text-white italic">TIME<span class="text-[--color-accent] not-italic">SYNC</span></h1> |
| 89 | + <p class="text-[10px] text-[--color-muted] font-bold tracking-[0.2em] mt-1 uppercase">Principal Engineering Toolkit</p> |
| 90 | + </div> |
| 91 | + <div class="flex gap-4"> |
| 92 | + <a href="../stats" class="text-[--color-muted] hover:text-[--color-accent] flex items-center gap-2 text-xs font-bold uppercase"> |
| 93 | + <i class="fa-solid fa-chart-simple"></i> Stats |
| 94 | + </a> |
| 95 | + <a href="../" class="bg-[--color-card] w-10 h-10 flex items-center justify-center rounded-xl border border-[--color-line] hover:border-[--color-accent] transition"> |
| 96 | + <i class="fa-solid fa-house-user"></i> |
| 97 | + </a> |
86 | 98 | </div> |
87 | | - <a href="../" class="text-sm font-bold text-[--color-accent] border border-[--color-accent] px-3 py-1 rounded hover:bg-[--color-accent] hover:text-[--color-bg] transition-all"><i class="fa-solid fa-house mr-1"></i> Hub</a> |
88 | 99 | </header> |
89 | 100 |
|
90 | | - <div class="flex flex-wrap gap-4 mb-6 items-center bg-[#18233C] p-5 rounded-2xl border border-[--color-line]"> |
91 | | - <div class="flex flex-col"> |
92 | | - <label class="text-[10px] uppercase font-bold text-[--color-muted] mb-2 ml-1">Add Location</label> |
93 | | - <div class="flex gap-2"> |
94 | | - <select id="tz-selector" class="w-64"> |
95 | | - <option value="UTC">UTC (Universal Time)</option> |
96 | | - <option value="Asia/Kolkata">India (IST)</option> |
97 | | - <option value="America/New_York">New York (EST/EDT)</option> |
98 | | - <option value="Europe/London">London (GMT/BST)</option> |
99 | | - <option value="Asia/Singapore">Singapore (SGT)</option> |
100 | | - <option value="America/Los_Angeles">Pacific Time (PST/PDT)</option> |
101 | | - <option value="Europe/Berlin">Berlin (CET/CEST)</option> |
102 | | - <option value="Australia/Sydney">Sydney (AEST/AEDT)</option> |
103 | | - <option value="Asia/Dubai">Dubai (GST)</option> |
104 | | - <option value="Asia/Tokyo">Tokyo (JST)</option> |
105 | | - </select> |
106 | | - <button onclick="addTimezone()" class="btn-gold">Add</button> |
| 101 | + <div class="bg-[--color-card] p-6 rounded-2xl mb-8 border border-[--color-line] flex flex-wrap gap-6 items-center"> |
| 102 | + <div class="flex flex-col gap-2"> |
| 103 | + <span class="text-[10px] font-black text-[--color-muted] uppercase tracking-widest ml-1">Global Database Search</span> |
| 104 | + <div class="flex gap-3"> |
| 105 | + <input type="text" id="tz-search" list="all-timezones" placeholder="Type city or country (e.g. London, UTC, IST)..."> |
| 106 | + <datalist id="all-timezones"></datalist> |
| 107 | + <button onclick="addZone()" class="btn-add hover:brightness-110 active:scale-95 transition">Add Zone</button> |
107 | 108 | </div> |
108 | 109 | </div> |
109 | 110 |
|
110 | | - <div class="ml-auto text-right bg-[#0C1524] px-4 py-2 rounded-lg border border-[--color-line]"> |
111 | | - <p class="text-[9px] uppercase font-bold text-[--color-muted]">Your System Time</p> |
112 | | - <p id="local-clock" class="text-lg font-mono text-[--color-accent] font-bold">00:00:00</p> |
| 111 | + <div class="ml-auto flex items-center gap-8"> |
| 112 | + <div class="text-right border-r border-[--color-line] pr-8"> |
| 113 | + <p class="text-[10px] uppercase text-[--color-muted] font-bold mb-1">Internal Reference</p> |
| 114 | + <p id="utc-clock" class="font-mono text-xl text-white font-black">00:00 <span class="text-[--color-accent] text-xs">UTC</span></p> |
| 115 | + </div> |
| 116 | + <button onclick="resetToNow()" class="text-[10px] font-black text-[--color-accent] border-2 border-[--color-accent] px-4 py-2 rounded-lg hover:bg-[--color-accent] hover:text-[--color-bg] transition-all"> |
| 117 | + SYNC TO NOW |
| 118 | + </button> |
113 | 119 | </div> |
114 | 120 | </div> |
115 | 121 |
|
116 | | - <div id="timelines-root"></div> |
| 122 | + <div id="timeline-container" class="space-y-3"> |
| 123 | + </div> |
117 | 124 |
|
118 | | - <footer class="mt-8 text-center text-[10px] text-[--color-muted] uppercase tracking-widest opacity-50"> |
119 | | - WorldTimeBuddy Logic — DST Inclusive — Client-Side Only |
120 | | - </footer> |
| 125 | + <div class="mt-8 p-4 bg-blue-900/10 border border-blue-900/30 rounded-xl flex items-center gap-4"> |
| 126 | + <i class="fa-solid fa-circle-info text-blue-400"></i> |
| 127 | + <p class="text-xs text-blue-200/70"><b>Pro-Tip:</b> Click any hour cell to align the timeline. Green underlines indicate 9 AM - 5 PM business hours for that specific region.</p> |
| 128 | + </div> |
121 | 129 | </div> |
122 | 130 |
|
123 | 131 | <script> |
124 | | - // Configuration |
125 | | - // We use a "Global Reference Hour" in UTC (0-23) to sync everything |
126 | | - let globalRefUtcHour = new Date().getUTCHours(); |
127 | | - let timezones = []; |
128 | | - |
129 | | - // Initialize with Local and UTC |
| 132 | + // Initial state: Only show the user's current local timezone |
| 133 | + let activeUtcIndex = new Date().getUTCHours(); |
130 | 134 | const localTZ = Intl.DateTimeFormat().resolvedOptions().timeZone; |
131 | | - timezones.push(localTZ); |
132 | | - if (localTZ !== 'UTC') timezones.push('UTC'); |
133 | | - |
134 | | - function updateClocks() { |
| 135 | + let selectedZones = [localTZ]; |
| 136 | + |
| 137 | + // Populate searchable IANA database |
| 138 | + const allZones = Intl.supportedValuesOf('timeZone'); |
| 139 | + const datalist = document.getElementById('all-timezones'); |
| 140 | + allZones.forEach(zone => { |
| 141 | + let opt = document.createElement('option'); |
| 142 | + opt.value = zone; |
| 143 | + datalist.appendChild(opt); |
| 144 | + }); |
| 145 | + |
| 146 | + function updateUTC() { |
135 | 147 | const now = new Date(); |
136 | | - document.getElementById('local-clock').innerText = now.toLocaleTimeString([], { hour12: true }); |
| 148 | + document.getElementById('utc-clock').innerHTML = |
| 149 | + `${now.getUTCHours().toString().padStart(2,'0')}:${now.getUTCMinutes().toString().padStart(2,'0')} <span class="text-[--color-accent] text-xs">UTC</span>`; |
137 | 150 | } |
138 | 151 |
|
139 | | - function addTimezone() { |
140 | | - const select = document.getElementById('tz-selector'); |
141 | | - const tz = select.value; |
142 | | - if (!timezones.includes(tz)) { |
143 | | - timezones.push(tz); |
| 152 | + function addZone() { |
| 153 | + const input = document.getElementById('tz-search'); |
| 154 | + const val = input.value; |
| 155 | + if (allZones.includes(val) && !selectedZones.includes(val)) { |
| 156 | + selectedZones.push(val); |
144 | 157 | render(); |
| 158 | + input.value = ""; |
| 159 | + } else if (val === "UTC" && !selectedZones.includes("UTC")) { |
| 160 | + selectedZones.push("UTC"); |
| 161 | + render(); |
| 162 | + input.value = ""; |
145 | 163 | } |
146 | 164 | } |
147 | 165 |
|
148 | | - function removeTimezone(index) { |
149 | | - timezones.splice(index, 1); |
150 | | - render(); |
| 166 | + function removeZone(index) { |
| 167 | + if (selectedZones.length > 1) { |
| 168 | + selectedZones.splice(index, 1); |
| 169 | + render(); |
| 170 | + } |
151 | 171 | } |
152 | 172 |
|
153 | | - function selectHour(utcHour) { |
154 | | - globalRefUtcHour = utcHour; |
| 173 | + function resetToNow() { |
| 174 | + activeUtcIndex = new Date().getUTCHours(); |
155 | 175 | render(); |
156 | 176 | } |
157 | 177 |
|
158 | 178 | function render() { |
159 | | - const root = document.getElementById('timelines-root'); |
160 | | - root.innerHTML = ''; |
| 179 | + const container = document.getElementById('timeline-container'); |
| 180 | + container.innerHTML = ''; |
161 | 181 |
|
162 | | - timezones.forEach((tz, tzIndex) => { |
| 182 | + selectedZones.forEach((zone, idx) => { |
163 | 183 | const row = document.createElement('div'); |
164 | | - row.className = 'timeline-row animate-in fade-in duration-500'; |
| 184 | + row.className = 'timeline-row'; |
165 | 185 |
|
166 | | - // Get current time info for the header |
167 | 186 | const now = new Date(); |
168 | | - const timeStr = now.toLocaleTimeString('en-US', { timeZone: tz, hour: 'numeric', minute: '2-digit', hour12: true }); |
169 | | - const dateStr = now.toLocaleDateString('en-US', { timeZone: tz, weekday: 'short', month: 'short', day: 'numeric' }); |
| 187 | + const timeInZone = now.toLocaleTimeString('en-US', { timeZone: zone, hour: '2-digit', minute: '2-digit', hour12: true }); |
| 188 | + const offsetName = new Intl.DateTimeFormat('en-US', {timeZone: zone, timeZoneName:'short'}).formatToParts(now).find(p => p.type === 'timeZoneName').value; |
170 | 189 |
|
171 | 190 | row.innerHTML = ` |
172 | | - <div class="flex justify-between items-end mb-3"> |
173 | | - <div class="flex items-baseline gap-2"> |
174 | | - <span class="font-bold text-lg text-[--color-accent] leading-none">${tz.split('/').pop().replace('_', ' ')}</span> |
175 | | - <span class="text-[10px] font-mono text-[--color-muted] uppercase opacity-60">${tz}</span> |
| 191 | + <div class="tz-info"> |
| 192 | + <div class="flex justify-between items-center mb-1"> |
| 193 | + <span class="font-black text-white text-sm tracking-tight">${zone.split('/').pop().replace('_', ' ')}</span> |
| 194 | + <span class="text-[11px] font-mono text-[--color-accent] font-bold">${timeInZone}</span> |
176 | 195 | </div> |
177 | | - <div class="text-right"> |
178 | | - <span class="text-xs font-bold text-white block">${timeStr}</span> |
179 | | - <span class="text-[9px] text-[--color-muted] uppercase font-medium">${dateStr}</span> |
| 196 | + <div class="flex justify-between items-center"> |
| 197 | + <span class="text-[9px] text-[--color-muted] font-bold uppercase tracking-tighter">${offsetName}</span> |
| 198 | + ${idx !== 0 ? `<button onclick="removeZone(${idx})" class="text-[9px] font-black text-red-400/50 hover:text-red-400 uppercase tracking-widest">Delete</button>` : `<span class="text-[9px] text-blue-400 font-black uppercase tracking-widest">Local</span>`} |
180 | 199 | </div> |
181 | | - ${tzIndex > 0 ? `<button onclick="removeTimezone(${tzIndex})" class="ml-4 text-[--color-line] hover:text-red-400 transition-colors"><i class="fa-solid fa-circle-minus"></i></button>` : ''} |
182 | 200 | </div> |
183 | | - <div class="grid-container"> |
184 | | - <div class="hour-grid" id="grid-${tzIndex}"></div> |
| 201 | + <div class="grid-viewport"> |
| 202 | + <div class="hour-grid"></div> |
185 | 203 | </div> |
186 | 204 | `; |
187 | | - root.appendChild(row); |
188 | 205 |
|
189 | | - const grid = document.getElementById(`grid-${tzIndex}`); |
| 206 | + const grid = row.querySelector('.hour-grid'); |
190 | 207 |
|
191 | | - // Logic: We always render 24 blocks. |
192 | | - // The "spine" is UTC. We calculate what hour in THIS timezone corresponds to UTC 0 to 23. |
193 | | - for (let utcHour = 0; utcHour < 24; utcHour++) { |
194 | | - // Create a date object set to Today at utcHour:00 UTC |
195 | | - const refDate = new Date(); |
196 | | - refDate.setUTCHours(utcHour, 0, 0, 0); |
197 | | - |
198 | | - // Convert that specific UTC moment to the target timezone's hour |
199 | | - const tzHourStr = refDate.toLocaleTimeString('en-US', { timeZone: tz, hour: 'numeric', hour12: false }); |
200 | | - const tzHour = parseInt(tzHourStr); |
201 | | - const isAMPM = refDate.toLocaleTimeString('en-US', { timeZone: tz, hour: 'numeric', hour12: true }); |
202 | | - |
| 208 | + // Build 24-hour UTC-aligned spine |
| 209 | + for (let i = 0; i < 24; i++) { |
203 | 210 | const cell = document.createElement('div'); |
204 | | - const isBusiness = tzHour >= 9 && tzHour <= 18; |
205 | | - const isNight = tzHour < 6 || tzHour >= 22; |
206 | | - const isActive = utcHour === globalRefUtcHour; |
207 | | - |
208 | | - cell.className = `hour-cell ${isBusiness ? 'business' : ''} ${isNight ? 'night' : ''} ${isActive ? 'active' : ''}`; |
| 211 | + const d = new Date(); |
| 212 | + d.setUTCHours(i, 0, 0, 0); |
209 | 213 |
|
210 | | - // Format display (e.g., "12 AM" or "5 PM") |
211 | | - const [hPart, ampmPart] = isAMPM.split(' '); |
212 | | - cell.innerHTML = `<span>${hPart}</span><span class="ampm">${ampmPart}</span>`; |
| 214 | + const localHour = parseInt(d.toLocaleTimeString('en-US', { timeZone: zone, hour12: false, hour: 'numeric' })); |
| 215 | + const ampm = d.toLocaleTimeString('en-US', { timeZone: zone, hour: 'numeric', hour12: true }).split(' ')[1]; |
| 216 | + |
| 217 | + const isWork = localHour >= 9 && localHour <= 17; |
| 218 | + const isNight = localHour >= 22 || localHour <= 6; |
| 219 | + const isActive = i === activeUtcIndex; |
| 220 | + |
| 221 | + cell.className = `hour-cell ${isWork ? 'work' : ''} ${isNight ? 'night' : ''} ${isActive ? 'active' : ''}`; |
| 222 | + cell.innerHTML = `<span class="text-xs font-bold">${localHour % 12 || 12}</span><span class="text-[7px] font-black uppercase opacity-60">${ampm}</span>`; |
213 | 223 |
|
214 | | - cell.onclick = () => selectHour(utcHour); |
| 224 | + cell.onclick = () => { activeUtcIndex = i; render(); }; |
215 | 225 | grid.appendChild(cell); |
216 | 226 | } |
| 227 | + container.appendChild(row); |
217 | 228 | }); |
218 | 229 | } |
219 | 230 |
|
220 | | - setInterval(updateClocks, 1000); |
| 231 | + setInterval(updateUTC, 1000); |
221 | 232 | render(); |
222 | | - updateClocks(); |
| 233 | + updateUTC(); |
223 | 234 | </script> |
| 235 | + |
224 | 236 | </body> |
225 | 237 | </html> |
0 commit comments