const POPULAR_NAMES = [ 'New York, Vereinigte Staaten', 'London, Vereinigtes Königreich', 'Los Angeles, Vereinigte Staaten', 'Tokyo, Japan', 'Sydney, Australien', 'Singapore, Singapur', 'Dubai, Vereinigte Arabische Emirate', 'São Paulo, Brasilien', 'Berlin, Deutschland', 'Mexico City, Mexiko' ]; const timezoneByName = new Map(SORTED_TIMEZONE_DATA.map((entry) => [entry.name, entry])); function normalizeText(value) { return value .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); } const localZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const localLabel = buildLocalLabel(localZone); let selected = [ { name: localLabel.name, country: localLabel.country, timeZone: localZone, isLocal: true } ]; const THEME_STORAGE_KEY = 'weltzeit:theme'; const localTimeEl = document.getElementById('local-time'); const localDateEl = document.getElementById('local-date'); const localZoneEl = document.getElementById('local-zone'); const searchInputEl = document.getElementById('search-input'); const searchResultsEl = document.getElementById('search-results'); const selectionListEl = document.getElementById('selection-list'); const resetButtonEl = document.getElementById('reset-button'); const themeToggleButton = document.getElementById('theme-toggle'); const themeToggleIcon = themeToggleButton?.querySelector('.theme-toggle-icon'); const themeToggleLabel = themeToggleButton?.querySelector('.theme-toggle-label'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); let updateTimer = null; let hasExplicitThemePreference = Boolean(window.localStorage.getItem(THEME_STORAGE_KEY)); function updateThemeToggleLabel(theme) { if (!themeToggleButton || !themeToggleIcon || !themeToggleLabel) { return; } if (theme === 'dark') { themeToggleIcon.textContent = '☀︎'; themeToggleLabel.textContent = 'Heller Modus'; } else { themeToggleIcon.textContent = '☾'; themeToggleLabel.textContent = 'Dunkler Modus'; } } function applyTheme(theme, { persist } = { persist: true }) { const normalized = theme === 'dark' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', normalized); updateThemeToggleLabel(normalized); if (persist) { window.localStorage.setItem(THEME_STORAGE_KEY, normalized); } } function resolveInitialTheme() { const stored = window.localStorage.getItem(THEME_STORAGE_KEY); if (stored === 'light' || stored === 'dark') { return stored; } return prefersDark.matches ? 'dark' : 'light'; } function buildLocalLabel(timeZone) { const formatter = Intl.DateTimeFormat(undefined, { timeZone, timeZoneName: 'longGeneric' }); const parts = formatter.formatToParts(new Date()); const zoneName = parts.find((part) => part.type === 'timeZoneName')?.value ?? timeZone; return { name: zoneName, country: '', zoneName }; } function formatClockValue(date, timeZone) { return new Intl.DateTimeFormat(undefined, { timeZone, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date); } function formatDateValue(date, timeZone) { return new Intl.DateTimeFormat(undefined, { timeZone, weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).format(date); } function pad(value) { return String(Math.abs(value)).padStart(2, '0'); } function getOffsetMinutes(date, timeZone) { const withParts = new Intl.DateTimeFormat('en-CA', { timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).formatToParts(date); const partValues = Object.fromEntries( withParts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]) ); const zonedUTC = Date.UTC( Number(partValues.year), Number(partValues.month) - 1, Number(partValues.day), Number(partValues.hour), Number(partValues.minute), Number(partValues.second) ); return Math.round((zonedUTC - date.getTime()) / 60000); } function formatOffsetLabel(offsetMinutes) { if (offsetMinutes === 0) { return 'Entspricht der lokalen Zeit'; } const sign = offsetMinutes > 0 ? '+' : '-'; const hours = Math.floor(Math.abs(offsetMinutes) / 60); const minutes = Math.abs(offsetMinutes) % 60; return `${sign}${pad(hours)}:${pad(minutes)}`; } function updateLocalClock() { const now = new Date(); localTimeEl.textContent = formatClockValue(now, localZone); localDateEl.textContent = formatDateValue(now, localZone); localZoneEl.textContent = localLabel.zoneName || localZone; } function renderSelections() { selectionListEl.innerHTML = ''; const now = new Date(); const baseOffset = getOffsetMinutes(now, selected[0].timeZone); selected.forEach((entry, index) => { const listItem = document.createElement('li'); listItem.className = 'selection-item'; if (index === 0) { listItem.classList.add('highlight'); } const locationContainer = document.createElement('div'); const nameEl = document.createElement('div'); nameEl.className = 'location-name'; nameEl.textContent = entry.name; const metaEl = document.createElement('div'); metaEl.className = 'location-meta'; metaEl.textContent = [entry.country, entry.timeZone].filter(Boolean).join(' • '); locationContainer.appendChild(nameEl); if (metaEl.textContent) { locationContainer.appendChild(metaEl); } const timeEl = document.createElement('div'); timeEl.className = 'time-value'; timeEl.textContent = formatClockValue(now, entry.timeZone); const offsetEl = document.createElement('div'); offsetEl.className = 'offset-value'; const relativeOffset = getOffsetMinutes(now, entry.timeZone) - baseOffset; offsetEl.textContent = formatOffsetLabel(relativeOffset); const removeButton = document.createElement('button'); removeButton.type = 'button'; removeButton.className = 'remove-button'; removeButton.setAttribute('aria-label', `${entry.name} entfernen`); removeButton.textContent = '×'; removeButton.addEventListener('click', () => { removeSelection(entry.timeZone); }); listItem.appendChild(locationContainer); listItem.appendChild(timeEl); listItem.appendChild(offsetEl); if (!entry.isLocal) { listItem.appendChild(removeButton); } else { const placeholder = document.createElement('div'); listItem.appendChild(placeholder); } selectionListEl.appendChild(listItem); }); } function removeSelection(timeZone) { selected = selected.filter((entry) => !(entry.timeZone === timeZone && !entry.isLocal)); if (!selected.some((entry) => entry.isLocal)) { selected.unshift({ name: localLabel.name, country: localLabel.country, timeZone: localZone, isLocal: true }); } renderSelections(); } function addSelection(entry) { if (selected.some((item) => item.timeZone === entry.timeZone)) { return; } selected.push({ name: entry.name, country: entry.country, timeZone: entry.timeZone }); renderSelections(); } function clearResults() { searchResultsEl.innerHTML = ''; } function renderSearchResults(query = '') { const trimmed = query.trim().toLowerCase(); const normalizedQuery = normalizeText(trimmed); let matches; if (!trimmed) { matches = POPULAR_NAMES.map((name) => timezoneByName.get(name)).filter(Boolean); } else { matches = SORTED_TIMEZONE_DATA.filter((entry) => { const haystack = [ entry.name, entry.country, entry.timeZone, ...(entry.keywords || []) ].join(' '); const lowerHaystack = haystack.toLowerCase(); const normalizedHaystack = normalizeText(haystack); return lowerHaystack.includes(trimmed) || normalizedHaystack.includes(normalizedQuery); }).slice(0, 20); } clearResults(); matches.forEach((entry) => { const item = document.createElement('button'); item.type = 'button'; item.className = 'search-result-item'; const alreadySelected = selected.some((current) => current.timeZone === entry.timeZone); if (alreadySelected) { item.disabled = true; item.classList.add('is-selected'); } item.addEventListener('click', () => { if (item.disabled) { return; } addSelection(entry); searchInputEl.value = ''; renderSearchResults(''); searchInputEl.focus(); }); const name = document.createElement('div'); name.className = 'result-name'; name.textContent = entry.name; const zone = document.createElement('div'); zone.className = 'result-zone'; zone.textContent = `${entry.timeZone} • ${entry.country}`; item.appendChild(name); item.appendChild(zone); searchResultsEl.appendChild(item); }); if (!matches.length) { const empty = document.createElement('div'); empty.className = 'search-result-item'; empty.textContent = 'Keine Treffer. Probiere eine andere Stadt, ein anderes Land oder eine andere Zeitzone.'; empty.style.cursor = 'default'; searchResultsEl.appendChild(empty); } } function startClock() { updateLocalClock(); renderSelections(); if (updateTimer) { clearInterval(updateTimer); } updateTimer = setInterval(() => { updateLocalClock(); renderSelections(); }, 1000); } searchInputEl.addEventListener('input', (event) => { renderSearchResults(event.target.value); }); searchInputEl.addEventListener('focus', () => { renderSearchResults(searchInputEl.value); }); resetButtonEl.addEventListener('click', () => { selected = [ { name: localLabel.name, country: localLabel.country, timeZone: localZone, isLocal: true } ]; renderSelections(); }); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { updateLocalClock(); renderSelections(); } }); const initialTheme = resolveInitialTheme(); applyTheme(initialTheme, { persist: false }); hasExplicitThemePreference = Boolean(window.localStorage.getItem(THEME_STORAGE_KEY)); window.addEventListener('load', () => { startClock(); renderSearchResults(''); }); if (themeToggleButton) { themeToggleButton.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme') || resolveInitialTheme(); const next = current === 'dark' ? 'light' : 'dark'; hasExplicitThemePreference = true; applyTheme(next, { persist: true }); }); } prefersDark.addEventListener('change', (event) => { if (hasExplicitThemePreference) { return; } applyTheme(event.matches ? 'dark' : 'light', { persist: false }); });