index.html 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Parv's Time Zone Converter</title>
  7. <link rel="icon" type="image/x-icon" href="favicon.ico">
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  9. <style>
  10. :root {
  11. --primary-color: #4a6fa5;
  12. --secondary-color: #6c757d;
  13. --success-color: #28a745;
  14. --warning-color: #ffc107;
  15. --danger-color: #dc3545;
  16. --bg-color: #f8f9fa;
  17. --card-bg: #ffffff;
  18. --text-color: #212529;
  19. --border-color: #dee2e6;
  20. --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  21. --hover-color: #e9ecef;
  22. --slider-height: 6px;
  23. --slider-thumb-size: 18px;
  24. }
  25. .dark-mode {
  26. --primary-color: #6b8cbe;
  27. --secondary-color: #a0a0a0;
  28. --success-color: #3bd264;
  29. --warning-color: #ffd54f;
  30. --danger-color: #e57373;
  31. --bg-color: #121212;
  32. --card-bg: #1e1e1e;
  33. --text-color: #f0f0f0;
  34. --border-color: #444444;
  35. --shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
  36. --hover-color: #2d2d2d;
  37. --slider-height: 8px;
  38. --slider-thumb-size: 22px;
  39. }
  40. * {
  41. margin: 0;
  42. padding: 0;
  43. box-sizing: border-box;
  44. transition: background-color 0.3s, color 0.3s;
  45. }
  46. body {
  47. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  48. background-color: var(--bg-color);
  49. color: var(--text-color);
  50. line-height: 1.6;
  51. padding: 20px;
  52. }
  53. .container {
  54. max-width: 1400px;
  55. margin: 0 auto;
  56. }
  57. header {
  58. display: flex;
  59. justify-content: space-between;
  60. align-items: center;
  61. margin-bottom: 20px;
  62. padding-bottom: 10px;
  63. border-bottom: 1px solid var(--border-color);
  64. flex-wrap: wrap;
  65. gap: 15px;
  66. }
  67. h1 {
  68. font-size: 2rem;
  69. color: var(--primary-color);
  70. }
  71. .controls {
  72. display: flex;
  73. gap: 15px;
  74. flex-wrap: wrap;
  75. }
  76. button {
  77. background-color: var(--primary-color);
  78. color: white;
  79. border: none;
  80. padding: 8px 15px;
  81. border-radius: 4px;
  82. cursor: pointer;
  83. font-size: 0.9rem;
  84. display: flex;
  85. align-items: center;
  86. gap: 5px;
  87. }
  88. button:hover {
  89. opacity: 0.9;
  90. }
  91. .mode-toggle {
  92. background-color: var(--secondary-color);
  93. }
  94. .mode-toggle.active {
  95. background-color: var(--success-color);
  96. }
  97. .timezone-grid {
  98. display: grid;
  99. grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  100. gap: 20px;
  101. margin-bottom: 30px;
  102. }
  103. .timezone-card {
  104. background-color: var(--card-bg);
  105. border-radius: 8px;
  106. padding: 20px;
  107. box-shadow: var(--shadow);
  108. border: 1px solid var(--border-color);
  109. position: relative;
  110. transition: transform 0.2s;
  111. }
  112. .timezone-card:hover {
  113. transform: translateY(-5px);
  114. }
  115. .timezone-header {
  116. display: flex;
  117. justify-content: space-between;
  118. align-items: center;
  119. margin-bottom: 15px;
  120. }
  121. .timezone-name {
  122. font-weight: bold;
  123. font-size: 1.2rem;
  124. color: var(--primary-color);
  125. }
  126. .time-container {
  127. display: flex;
  128. align-items: center;
  129. gap: 10px;
  130. margin-bottom: 10px;
  131. }
  132. .time-display {
  133. font-size: 2rem;
  134. font-weight: bold;
  135. }
  136. .edit-time-btn {
  137. background: none;
  138. border: none;
  139. color: var(--secondary-color);
  140. cursor: pointer;
  141. font-size: 1.2rem;
  142. padding: 5px;
  143. border-radius: 4px;
  144. transition: all 0.2s;
  145. }
  146. .edit-time-btn:hover {
  147. background-color: var(--hover-color);
  148. color: var(--primary-color);
  149. }
  150. .time-input-container {
  151. display: none;
  152. margin-bottom: 15px;
  153. }
  154. .time-input {
  155. padding: 8px 12px;
  156. border: 1px solid var(--border-color);
  157. border-radius: 4px;
  158. width: 100%;
  159. font-size: 1rem;
  160. margin-bottom: 8px;
  161. }
  162. .input-actions {
  163. display: flex;
  164. gap: 8px;
  165. }
  166. .input-btn {
  167. flex: 1;
  168. padding: 6px;
  169. font-size: 0.85rem;
  170. }
  171. .date-display {
  172. color: var(--secondary-color);
  173. margin-bottom: 5px;
  174. }
  175. .gmt-offset {
  176. font-size: 0.9rem;
  177. color: var(--secondary-color);
  178. margin-bottom: 5px;
  179. }
  180. .dst-note {
  181. font-size: 0.85rem;
  182. color: var(--success-color);
  183. font-style: italic;
  184. margin-top: 10px;
  185. }
  186. .delete-btn {
  187. position: absolute;
  188. top: 10px;
  189. right: 10px;
  190. background: none;
  191. border: none;
  192. color: var(--secondary-color);
  193. cursor: pointer;
  194. font-size: 1.2rem;
  195. }
  196. .delete-btn:hover {
  197. color: var(--danger-color);
  198. }
  199. .slider-container {
  200. margin: 20px 0;
  201. position: relative;
  202. }
  203. .slider-marks {
  204. display: flex;
  205. justify-content: space-between;
  206. padding: 0 10px;
  207. margin-top: 5px;
  208. font-size: 0.85rem;
  209. color: var(--secondary-color);
  210. }
  211. .slider {
  212. width: 100%;
  213. height: var(--slider-height);
  214. margin: 15px 0;
  215. -webkit-appearance: none;
  216. appearance: none;
  217. background: linear-gradient(to right, #4a6fa5 0%, #6c757d 100%);
  218. outline: none;
  219. border-radius: 3px;
  220. position: relative;
  221. }
  222. .slider::-webkit-slider-thumb {
  223. -webkit-appearance: none;
  224. appearance: none;
  225. width: var(--slider-thumb-size);
  226. height: var(--slider-thumb-size);
  227. border-radius: 50%;
  228. background: var(--primary-color);
  229. cursor: pointer;
  230. box-shadow: 0 0 10px rgba(0,0,0,0.3);
  231. transition: all 0.2s;
  232. }
  233. .slider::-webkit-slider-thumb:hover {
  234. transform: scale(1.2);
  235. background: var(--success-color);
  236. }
  237. .slider-info {
  238. display: flex;
  239. justify-content: space-between;
  240. font-size: 0.85rem;
  241. color: var(--secondary-color);
  242. margin-top: 5px;
  243. }
  244. .slider-value {
  245. font-weight: bold;
  246. color: var(--primary-color);
  247. }
  248. .add-timezone {
  249. margin-top: 30px;
  250. padding: 20px;
  251. background-color: var(--card-bg);
  252. border-radius: 8px;
  253. box-shadow: var(--shadow);
  254. }
  255. .add-timezone h2 {
  256. margin-bottom: 15px;
  257. color: var(--primary-color);
  258. }
  259. .form-group {
  260. margin-bottom: 15px;
  261. }
  262. label {
  263. display: block;
  264. margin-bottom: 5px;
  265. font-weight: bold;
  266. }
  267. select, input {
  268. width: 100%;
  269. padding: 10px;
  270. border: 1px solid var(--border-color);
  271. border-radius: 4px;
  272. background-color: var(--card-bg);
  273. color: var(--text-color);
  274. }
  275. .search-box {
  276. position: relative;
  277. }
  278. .search-results {
  279. position: absolute;
  280. top: 100%;
  281. left: 0;
  282. right: 0;
  283. background-color: var(--card-bg);
  284. border: 1px solid var(--border-color);
  285. border-top: none;
  286. border-radius: 0 0 4px 4px;
  287. max-height: 200px;
  288. overflow-y: auto;
  289. z-index: 100;
  290. display: none;
  291. }
  292. .search-result-item {
  293. padding: 10px;
  294. cursor: pointer;
  295. }
  296. .search-result-item:hover {
  297. background-color: var(--hover-color);
  298. }
  299. footer {
  300. margin-top: 40px;
  301. text-align: center;
  302. padding: 20px;
  303. border-top: 1px solid var(--border-color);
  304. color: var(--secondary-color);
  305. }
  306. .time-zone-examples {
  307. display: flex;
  308. gap: 15px;
  309. margin-top: 15px;
  310. flex-wrap: wrap;
  311. }
  312. .time-zone-example {
  313. background: var(--hover-color);
  314. padding: 8px 12px;
  315. border-radius: 4px;
  316. font-size: 0.9rem;
  317. cursor: pointer;
  318. transition: all 0.2s;
  319. }
  320. .time-zone-example:hover {
  321. background: var(--primary-color);
  322. color: white;
  323. }
  324. @media (max-width: 768px) {
  325. .timezone-grid {
  326. grid-template-columns: 1fr;
  327. }
  328. header {
  329. flex-direction: column;
  330. align-items: flex-start;
  331. }
  332. .controls {
  333. width: 100%;
  334. justify-content: center;
  335. }
  336. }
  337. </style>
  338. </head>
  339. <body>
  340. <div class="container">
  341. <header>
  342. <h1>Parv's Time Zone Converter</h1>
  343. <div class="controls">
  344. <button id="theme-toggle">
  345. <i class="fas fa-moon"></i> Toggle Dark Mode
  346. </button>
  347. <button id="refresh-btn">
  348. <i class="fas fa-sync-alt"></i> Refresh Times
  349. </button>
  350. <button id="real-time-mode" class="mode-toggle active">
  351. <i class="fas fa-clock"></i> Real-Time Mode
  352. </button>
  353. <button id="converter-mode" class="mode-toggle">
  354. <i class="fas fa-sliders-h"></i> Converter Mode
  355. </button>
  356. </div>
  357. </header>
  358. <div class="timezone-grid" id="timezone-container">
  359. <!-- Timezone cards will be generated here -->
  360. </div>
  361. <div class="add-timezone">
  362. <h2>Add Time Zone</h2>
  363. <div class="form-group">
  364. <label for="timezone-search">Search Time Zones:</label>
  365. <div class="search-box">
  366. <input type="text" id="timezone-search" placeholder="Start typing to search time zones...">
  367. <div class="search-results" id="search-results"></div>
  368. </div>
  369. <div class="time-zone-examples">
  370. <div class="time-zone-example">America/New_York</div>
  371. <div class="time-zone-example">Asia/Kolkata</div>
  372. <div class="time-zone-example">Europe/London</div>
  373. <div class="time-zone-example">Australia/Sydney</div>
  374. </div>
  375. </div>
  376. <button id="add-timezone-btn">
  377. <i class="fas fa-plus"></i> Add Selected Time Zone
  378. </button>
  379. </div>
  380. <footer>
  381. <p>Made by Parv Ashwani &copy; 2025 | All Major Time Zones Supported</p>
  382. </footer>
  383. </div>
  384. <script>
  385. document.addEventListener('DOMContentLoaded', function() {
  386. // DOM elements
  387. const themeToggle = document.getElementById('theme-toggle');
  388. const refreshBtn = document.getElementById('refresh-btn');
  389. const realTimeModeBtn = document.getElementById('real-time-mode');
  390. const converterModeBtn = document.getElementById('converter-mode');
  391. const timezoneContainer = document.getElementById('timezone-container');
  392. const timezoneSearch = document.getElementById('timezone-search');
  393. const searchResults = document.getElementById('search-results');
  394. const addTimezoneBtn = document.getElementById('add-timezone-btn');
  395. // Application state
  396. let isRealTimeMode = true;
  397. let allTimeZones = [];
  398. let selectedTimezone = null;
  399. let baseTime = new Date();
  400. let updateInterval = null;
  401. // Default time zones
  402. const defaultTimeZones = [
  403. { id: 'America/New_York', name: 'EDT/EST', label: 'Eastern Time' },
  404. { id: 'Asia/Kolkata', name: 'IST', label: 'India Standard Time' },
  405. { id: 'America/Chicago', name: 'CDT/CST', label: 'Central Time' },
  406. { id: 'America/Los_Angeles', name: 'PDT/PST', label: 'Pacific Time' }
  407. ];
  408. // User-added time zones
  409. let customTimeZones = JSON.parse(localStorage.getItem('customTimeZones')) || [];
  410. // Initialize the app
  411. function init() {
  412. loadAllTimeZones();
  413. renderAllTimeZones();
  414. startRealTimeUpdates();
  415. // Set up event listeners
  416. setupEventListeners();
  417. // Add example time zone handlers
  418. document.querySelectorAll('.time-zone-example').forEach(example => {
  419. example.addEventListener('click', function() {
  420. timezoneSearch.value = this.textContent;
  421. handleTimezoneSearch();
  422. });
  423. });
  424. }
  425. // Load all available time zones
  426. function loadAllTimeZones() {
  427. try {
  428. allTimeZones = Intl.supportedValuesOf('timeZone').map(tz => {
  429. return {
  430. id: tz,
  431. name: tz.split('/').pop().replace(/_/g, ' '),
  432. label: tz
  433. };
  434. });
  435. } catch (e) {
  436. console.error("Could not load time zones:", e);
  437. // Fallback to a subset of time zones
  438. allTimeZones = [
  439. { id: 'America/New_York', name: 'New York', label: 'Eastern Time' },
  440. { id: 'Europe/London', name: 'London', label: 'Greenwich Mean Time' },
  441. { id: 'Asia/Tokyo', name: 'Tokyo', label: 'Japan Standard Time' },
  442. { id: 'Australia/Sydney', name: 'Sydney', label: 'Australian Eastern Time' },
  443. { id: 'Europe/Paris', name: 'Paris', label: 'Central European Time' },
  444. { id: 'Asia/Dubai', name: 'Dubai', label: 'Gulf Standard Time' },
  445. { id: 'America/Los_Angeles', name: 'Los Angeles', label: 'Pacific Time' },
  446. { id: 'America/Chicago', name: 'Chicago', label: 'Central Time' },
  447. { id: 'Asia/Kolkata', name: 'Kolkata', label: 'India Standard Time' },
  448. { id: 'Asia/Shanghai', name: 'Shanghai', label: 'China Standard Time' }
  449. ];
  450. }
  451. // Sort time zones by name
  452. allTimeZones.sort((a, b) => a.name.localeCompare(b.name));
  453. }
  454. // Set up event listeners
  455. function setupEventListeners() {
  456. // Theme toggle
  457. themeToggle.addEventListener('click', toggleTheme);
  458. // Refresh button
  459. refreshBtn.addEventListener('click', refreshAllTimes);
  460. // Mode toggles
  461. realTimeModeBtn.addEventListener('click', () => setMode(true));
  462. converterModeBtn.addEventListener('click', () => setMode(false));
  463. // Time zone search
  464. timezoneSearch.addEventListener('input', handleTimezoneSearch);
  465. // Add time zone button
  466. addTimezoneBtn.addEventListener('click', addSelectedTimezone);
  467. }
  468. // Toggle between dark and light mode
  469. function toggleTheme() {
  470. document.body.classList.toggle('dark-mode');
  471. localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
  472. // Update icon
  473. const icon = themeToggle.querySelector('i');
  474. if (document.body.classList.contains('dark-mode')) {
  475. icon.className = 'fas fa-sun';
  476. themeToggle.innerHTML = '<i class="fas fa-sun"></i> Toggle Light Mode';
  477. } else {
  478. icon.className = 'fas fa-moon';
  479. themeToggle.innerHTML = '<i class="fas fa-moon"></i> Toggle Dark Mode';
  480. }
  481. }
  482. // Set the application mode (real-time or converter)
  483. function setMode(isRealTime) {
  484. isRealTimeMode = isRealTime;
  485. // Update UI
  486. if (isRealTimeMode) {
  487. realTimeModeBtn.classList.add('active');
  488. converterModeBtn.classList.remove('active');
  489. startRealTimeUpdates();
  490. } else {
  491. realTimeModeBtn.classList.remove('active');
  492. converterModeBtn.classList.add('active');
  493. stopRealTimeUpdates();
  494. // Set base time to current time when switching to converter mode
  495. baseTime = new Date();
  496. updateAllTimes();
  497. }
  498. // Update sliders
  499. updateAllSliders();
  500. }
  501. // Start real-time updates
  502. function startRealTimeUpdates() {
  503. if (updateInterval) clearInterval(updateInterval);
  504. updateInterval = setInterval(updateAllTimes, 1000);
  505. }
  506. // Stop real-time updates
  507. function stopRealTimeUpdates() {
  508. if (updateInterval) {
  509. clearInterval(updateInterval);
  510. updateInterval = null;
  511. }
  512. }
  513. // Refresh all times
  514. function refreshAllTimes() {
  515. baseTime = new Date();
  516. updateAllTimes();
  517. updateAllSliders();
  518. }
  519. // Render all time zone cards
  520. function renderAllTimeZones() {
  521. timezoneContainer.innerHTML = '';
  522. // Add default time zones
  523. defaultTimeZones.forEach(tz => {
  524. timezoneContainer.appendChild(createTimeZoneCard(tz));
  525. });
  526. // Add custom time zones
  527. customTimeZones.forEach(tz => {
  528. timezoneContainer.appendChild(createTimeZoneCard(tz));
  529. });
  530. updateAllTimes();
  531. updateAllSliders();
  532. }
  533. // Create a time zone card
  534. function createTimeZoneCard(timezone) {
  535. const card = document.createElement('div');
  536. card.className = 'timezone-card';
  537. card.dataset.timezone = timezone.id;
  538. const isDefault = defaultTimeZones.some(tz => tz.id === timezone.id);
  539. card.innerHTML = `
  540. ${!isDefault ? '<button class="delete-btn"><i class="fas fa-times"></i></button>' : ''}
  541. <div class="timezone-header">
  542. <div class="timezone-name">${timezone.name}</div>
  543. <div class="timezone-label">${timezone.label}</div>
  544. </div>
  545. <div class="date-display">Loading date...</div>
  546. <div class="time-container">
  547. <div class="time-display">Loading time...</div>
  548. <button class="edit-time-btn" title="Edit time"><i class="fas fa-edit"></i></button>
  549. </div>
  550. <div class="gmt-offset">Loading GMT offset...</div>
  551. <div class="dst-note"></div>
  552. <div class="time-input-container">
  553. <input type="text" class="time-input" placeholder="e.g., 6:26 am" pattern="\\d{1,2}:\\d{2}\\s*[ap]m" required>
  554. <div class="input-actions">
  555. <button class="save-time-btn input-btn">Save</button>
  556. <button class="cancel-time-btn input-btn">Cancel</button>
  557. </div>
  558. </div>
  559. <div class="slider-container">
  560. <div class="slider-marks">
  561. <span>12am</span>
  562. <span>3am</span>
  563. <span>6am</span>
  564. <span>9am</span>
  565. <span>12pm</span>
  566. <span>3pm</span>
  567. <span>6pm</span>
  568. <span>9pm</span>
  569. </div>
  570. <input type="range" class="slider" min="0" max="1440" value="720">
  571. <div class="slider-info">
  572. <span>12:00 AM</span>
  573. <span class="slider-value">12:00 PM</span>
  574. <span>11:59 PM</span>
  575. </div>
  576. </div>
  577. `;
  578. // Add delete functionality for custom time zones
  579. const deleteBtn = card.querySelector('.delete-btn');
  580. if (deleteBtn) {
  581. deleteBtn.addEventListener('click', function() {
  582. removeTimeZone(timezone.id);
  583. });
  584. }
  585. // Add time editing functionality
  586. const editBtn = card.querySelector('.edit-time-btn');
  587. const timeContainer = card.querySelector('.time-container');
  588. const timeInputContainer = card.querySelector('.time-input-container');
  589. const timeInput = card.querySelector('.time-input');
  590. const saveBtn = card.querySelector('.save-time-btn');
  591. const cancelBtn = card.querySelector('.cancel-time-btn');
  592. editBtn.addEventListener('click', function() {
  593. timeContainer.style.display = 'none';
  594. timeInputContainer.style.display = 'block';
  595. timeInput.value = card.querySelector('.time-display').textContent.trim();
  596. });
  597. cancelBtn.addEventListener('click', function() {
  598. timeContainer.style.display = 'flex';
  599. timeInputContainer.style.display = 'none';
  600. });
  601. saveBtn.addEventListener('click', function() {
  602. const timeStr = timeInput.value;
  603. const timeObj = parseTimeInput(timeStr);
  604. if (!timeObj) {
  605. alert('Invalid time format. Please use HH:MM AM/PM (e.g., 6:26 am)');
  606. return;
  607. }
  608. const enteredTotalMinutes = timeObj.hours * 60 + timeObj.minutes;
  609. // Get current time in this time zone
  610. const timezoneId = card.dataset.timezone;
  611. const currentTzTimeStr = baseTime.toLocaleTimeString('en-US', {
  612. timeZone: timezoneId,
  613. hour: '2-digit',
  614. minute: '2-digit',
  615. hour12: false
  616. });
  617. const [currentHours, currentMinutes] = currentTzTimeStr.split(':').map(Number);
  618. const currentTotalMinutes = currentHours * 60 + currentMinutes;
  619. // Calculate the difference in minutes
  620. const diffMinutes = enteredTotalMinutes - currentTotalMinutes;
  621. // Adjust baseTime
  622. baseTime = new Date(baseTime.getTime() + diffMinutes * 60000);
  623. // Update all times and sliders
  624. updateAllTimes();
  625. updateAllSliders();
  626. // Revert to display mode
  627. timeContainer.style.display = 'flex';
  628. timeInputContainer.style.display = 'none';
  629. });
  630. // Add slider functionality
  631. const slider = card.querySelector('.slider');
  632. const sliderValue = card.querySelector('.slider-value');
  633. slider.addEventListener('input', function() {
  634. const totalMinutes = parseInt(this.value);
  635. const hours = Math.floor(totalMinutes / 60);
  636. const minutes = totalMinutes % 60;
  637. // Update slider value display
  638. const timeStr = formatTimeForSlider(hours, minutes);
  639. sliderValue.textContent = timeStr;
  640. if (!isRealTimeMode) {
  641. adjustTimeWithSlider(timezone.id, totalMinutes);
  642. }
  643. });
  644. return card;
  645. }
  646. // Parse time input in HH:MM AM/PM format
  647. function parseTimeInput(str) {
  648. const match = str.match(/(\d{1,2}):(\d{2})\s*([ap]m)/i);
  649. if (!match) return null;
  650. let hours = parseInt(match[1]);
  651. const minutes = parseInt(match[2]);
  652. const period = match[3].toLowerCase();
  653. if (period === 'pm' && hours < 12) hours += 12;
  654. if (period === 'am' && hours === 12) hours = 0;
  655. return { hours, minutes };
  656. }
  657. // Format time for slider display
  658. function formatTimeForSlider(hours, minutes) {
  659. const period = hours >= 12 ? 'PM' : 'AM';
  660. const displayHours = hours % 12 || 12;
  661. return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
  662. }
  663. // Update all time displays
  664. function updateAllTimes() {
  665. const cards = timezoneContainer.querySelectorAll('.timezone-card');
  666. const now = isRealTimeMode ? new Date() : baseTime;
  667. cards.forEach(card => {
  668. const timezone = card.dataset.timezone;
  669. try {
  670. // Format time
  671. const timeStr = now.toLocaleTimeString('en-US', {
  672. timeZone: timezone,
  673. hour: '2-digit',
  674. minute: '2-digit',
  675. hour12: true
  676. });
  677. // Format date
  678. const dateStr = now.toLocaleDateString('en-US', {
  679. timeZone: timezone,
  680. weekday: 'short',
  681. month: 'short',
  682. day: 'numeric',
  683. year: 'numeric'
  684. });
  685. // Get GMT offset
  686. const offset = getGMTOffset(now, timezone);
  687. // Update DOM
  688. card.querySelector('.time-display').textContent = timeStr;
  689. card.querySelector('.date-display').textContent = dateStr;
  690. card.querySelector('.gmt-offset').textContent = offset;
  691. // Check for DST
  692. const isDST = isDaylightSavingTime(now, timezone);
  693. const dstNote = card.querySelector('.dst-note');
  694. if (isDST !== null) {
  695. if (isDST) {
  696. dstNote.textContent = `Daylight Saving Time observed (${getDSTName(timezone)})`;
  697. } else {
  698. dstNote.textContent = 'Standard Time observed';
  699. }
  700. } else {
  701. dstNote.textContent = '';
  702. }
  703. } catch (e) {
  704. console.error(`Error updating time for ${timezone}:`, e);
  705. }
  706. });
  707. }
  708. // Update all sliders based on current time
  709. function updateAllSliders() {
  710. const cards = timezoneContainer.querySelectorAll('.timezone-card');
  711. const now = isRealTimeMode ? new Date() : baseTime;
  712. cards.forEach(card => {
  713. const timezone = card.dataset.timezone;
  714. try {
  715. const timeStr = now.toLocaleTimeString('en-US', {
  716. timeZone: timezone,
  717. hour: '2-digit',
  718. hour12: false,
  719. minute: '2-digit'
  720. });
  721. const [hours, minutes] = timeStr.split(':').map(Number);
  722. const totalMinutes = hours * 60 + minutes;
  723. const slider = card.querySelector('.slider');
  724. const sliderValue = card.querySelector('.slider-value');
  725. slider.value = totalMinutes;
  726. sliderValue.textContent = formatTimeForSlider(hours, minutes);
  727. } catch (e) {
  728. console.error(`Error updating slider for ${timezone}:`, e);
  729. }
  730. });
  731. }
  732. // Get GMT offset string
  733. function getGMTOffset(date, timezone) {
  734. try {
  735. const formatter = new Intl.DateTimeFormat('en-US', {
  736. timeZone: timezone,
  737. timeZoneName: 'shortOffset'
  738. });
  739. const parts = formatter.formatToParts(date);
  740. const offset = parts.find(part => part.type === 'timeZoneName')?.value || '';
  741. return offset.replace('GMT', 'GMT');
  742. } catch (e) {
  743. return "Unknown offset";
  744. }
  745. }
  746. // Check if daylight saving time is observed
  747. function isDaylightSavingTime(date, timezone) {
  748. try {
  749. // Try to get timezone name which often indicates DST
  750. const formatter = new Intl.DateTimeFormat('en-US', {
  751. timeZone: timezone,
  752. timeZoneName: 'long'
  753. });
  754. const tzName = formatter.resolvedOptions().timeZoneName;
  755. if (!tzName) return null;
  756. // Check if the timezone name suggests DST
  757. return formatter.format(date).includes("Daylight") ||
  758. tzName.includes("Daylight") ||
  759. /[+-]\d{2}:\d{2}/.test(tzName);
  760. } catch (e) {
  761. console.error(`Error checking DST for ${timezone}:`, e);
  762. return null;
  763. }
  764. }
  765. // Get DST name for timezone
  766. function getDSTName(timezone) {
  767. if (timezone.includes('New_York')) return 'EDT';
  768. if (timezone.includes('Chicago')) return 'CDT';
  769. if (timezone.includes('Los_Angeles')) return 'PDT';
  770. if (timezone.includes('London') || timezone.includes('Europe')) return 'BST';
  771. return 'DST';
  772. }
  773. // Handle timezone search
  774. function handleTimezoneSearch() {
  775. const query = timezoneSearch.value.toLowerCase();
  776. if (query.length < 2) {
  777. searchResults.style.display = 'none';
  778. return;
  779. }
  780. const results = allTimeZones.filter(tz =>
  781. tz.id.toLowerCase().includes(query) ||
  782. tz.name.toLowerCase().includes(query) ||
  783. tz.label.toLowerCase().includes(query)
  784. ).slice(0, 10); // Limit to 10 results
  785. displaySearchResults(results);
  786. }
  787. // Display search results
  788. function displaySearchResults(results) {
  789. searchResults.innerHTML = '';
  790. if (results.length === 0) {
  791. searchResults.innerHTML = '<div class="search-result-item">No results found</div>';
  792. } else {
  793. results.forEach(tz => {
  794. const item = document.createElement('div');
  795. item.className = 'search-result-item';
  796. item.textContent = `${tz.name} (${tz.id})`;
  797. item.dataset.timezone = tz.id;
  798. item.addEventListener('click', () => {
  799. selectedTimezone = tz;
  800. timezoneSearch.value = `${tz.name} (${tz.id})`;
  801. searchResults.style.display = 'none';
  802. });
  803. searchResults.appendChild(item);
  804. });
  805. }
  806. searchResults.style.display = 'block';
  807. }
  808. // Add selected time zone
  809. function addSelectedTimezone() {
  810. if (!selectedTimezone) {
  811. alert('Please select a time zone from the search results.');
  812. return;
  813. }
  814. // Check if already added
  815. if ([...defaultTimeZones, ...customTimeZones].some(tz => tz.id === selectedTimezone.id)) {
  816. alert('This time zone is already added.');
  817. return;
  818. }
  819. customTimeZones.push(selectedTimezone);
  820. localStorage.setItem('customTimeZones', JSON.stringify(customTimeZones));
  821. timezoneContainer.appendChild(createTimeZoneCard(selectedTimezone));
  822. updateAllTimes();
  823. updateAllSliders();
  824. // Reset selection
  825. selectedTimezone = null;
  826. timezoneSearch.value = '';
  827. }
  828. // Remove a time zone
  829. function removeTimeZone(timezoneId) {
  830. customTimeZones = customTimeZones.filter(tz => tz.id !== timezoneId);
  831. localStorage.setItem('customTimeZones', JSON.stringify(customTimeZones));
  832. renderAllTimeZones();
  833. }
  834. // Adjust time with slider
  835. function adjustTimeWithSlider(timezoneId, minutes) {
  836. const cards = timezoneContainer.querySelectorAll('.timezone-card');
  837. const adjustedCard = Array.from(cards).find(card => card.dataset.timezone === timezoneId);
  838. if (!adjustedCard) return;
  839. // Calculate the new base time based on slider adjustment
  840. const now = new Date();
  841. const adjustedTime = new Date(now);
  842. // Get the current time in the adjusted timezone
  843. const currentHours = now.toLocaleTimeString('en-US', {
  844. timeZone: timezoneId,
  845. hour: '2-digit',
  846. hour12: false
  847. });
  848. const currentMinutes = now.toLocaleTimeString('en-US', {
  849. timeZone: timezoneId,
  850. minute: '2-digit'
  851. });
  852. const currentTotalMinutes = parseInt(currentHours) * 60 + parseInt(currentMinutes);
  853. const minuteDifference = minutes - currentTotalMinutes;
  854. // Adjust the base time
  855. baseTime = new Date(now.getTime() + minuteDifference * 60000);
  856. // Update all times
  857. updateAllTimes();
  858. // Update all sliders except the one being adjusted
  859. cards.forEach(card => {
  860. if (card.dataset.timezone !== timezoneId) {
  861. const tz = card.dataset.timezone;
  862. try {
  863. const timeStr = baseTime.toLocaleTimeString('en-US', {
  864. timeZone: tz,
  865. hour: '2-digit',
  866. hour12: false,
  867. minute: '2-digit'
  868. });
  869. const [hours, minutes] = timeStr.split(':').map(Number);
  870. const totalMinutes = hours * 60 + minutes;
  871. const slider = card.querySelector('.slider');
  872. const sliderValue = card.querySelector('.slider-value');
  873. slider.value = totalMinutes;
  874. sliderValue.textContent = formatTimeForSlider(hours, minutes);
  875. } catch (e) {
  876. console.error(`Error updating slider for ${tz}:`, e);
  877. }
  878. }
  879. });
  880. }
  881. // Initialize the app
  882. init();
  883. // Load saved theme
  884. if (localStorage.getItem('theme') === 'dark') {
  885. document.body.classList.add('dark-mode');
  886. themeToggle.innerHTML = '<i class="fas fa-sun"></i> Toggle Light Mode';
  887. }
  888. });
  889. </script>
  890. </body>
  891. </html>