timezones/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Find the best time to meet across multiple timezones with an interactive comparison table and markdown export.">
<title>Timezone Meeting Planner</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg-color: #fafafa;
--card-bg: white;
--text-color: #333;
--text-secondary: #666;
--border-color: #ddd;
--primary-color: #0066cc;
--primary-hover: #0052a3;
--secondary-color: #6c757d;
--secondary-hover: #5a6268;
--danger-color: #dc3545;
--danger-hover: #c82333;
--accent-bg: #f8f9fa;
--hover-bg: #f0f0f0;
--selected-bg: #e3f2fd;
--shadow: 0 1px 3px rgba(0,0,0,0.1);
--code-bg: #f4f4f4;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--card-bg: #2d2d2d;
--text-color: #e0e0e0;
--text-secondary: #aaa;
--border-color: #404040;
--primary-color: #4da3ff;
--primary-hover: #6bb3ff;
--secondary-color: #8a949e;
--secondary-hover: #9aa5b0;
--danger-color: #ff6b6b;
--danger-hover: #ff8585;
--accent-bg: #363636;
--hover-bg: #404040;
--selected-bg: #1e3a5f;
--shadow: 0 1px 3px rgba(0,0,0,0.3);
--code-bg: #1e1e1e;
}
table {
color: var(--text-color);
}
th {
color: var(--text-color);
}
.date-indicator {
color: #777;
}
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--text-color);
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: var(--bg-color);
transition: background-color 0.2s;
}
h1 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
.description {
color: var(--text-secondary);
font-size: 0.95rem;
margin-bottom: 1.5rem;
}
.controls {
background: var(--card-bg);
padding: 20px;
border-radius: 8px;
box-shadow: var(--shadow);
margin-bottom: 20px;
border: 1px solid var(--border-color);
}
.timezone-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: start;
}
.timezone-row input[type="text"] {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.9rem;
background: var(--card-bg);
color: var(--text-color);
}
.name-input {
width: 140px;
}
.tz-container {
flex: 1;
position: relative;
min-width: 200px;
}
.tz-input {
width: 100%;
}
.tz-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-top: none;
border-radius: 0 0 4px 4px;
z-index: 100;
display: none;
box-shadow: var(--shadow);
}
.tz-dropdown.active {
display: block;
}
.tz-option {
padding: 8px 12px;
cursor: pointer;
font-size: 0.9rem;
color: var(--text-color);
}
.tz-option:hover, .tz-option.selected {
background: var(--hover-bg);
}
button {
padding: 8px 16px;
border: 1px solid var(--border-color);
background: var(--card-bg);
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
color: var(--text-color);
}
button:hover {
background: var(--hover-bg);
}
button.primary {
background: var(--primary-color);
color: #ffffff;
border-color: var(--primary-color);
font-weight: 500;
}
button.primary:hover {
background: var(--primary-hover);
}
button.secondary {
background: var(--secondary-color);
color: #ffffff;
border-color: var(--secondary-color);
font-weight: 500;
}
button.secondary:hover {
background: var(--secondary-hover);
}
button.danger {
color: var(--danger-color);
border-color: var(--danger-color);
}
button.danger:hover {
background: var(--danger-color);
color: white;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.actions {
margin-top: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
margin-left: auto;
color: var(--text-color);
}
.error-message {
color: var(--danger-color);
font-size: 0.85rem;
margin-bottom: 10px;
padding: 8px;
background: rgba(220, 53, 69, 0.1);
border-radius: 4px;
display: none;
}
.error-message.visible {
display: block;
}
.loading {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: 0.9rem;
margin: 10px 0;
}
.loading::after {
content: '';
width: 16px;
height: 16px;
border: 2px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.shortcut-hint {
font-size: 0.75rem;
opacity: 0.8;
margin-left: 4px;
}
table {
width: 100%;
background: var(--card-bg);
border-collapse: collapse;
box-shadow: var(--shadow);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-color);
user-select: none;
}
th, td {
padding: 12px;
text-align: center;
border-bottom: 1px solid var(--border-color);
}
th {
background: var(--accent-bg);
font-weight: 600;
font-size: 0.9rem;
position: sticky;
top: 0;
}
td {
font-size: 0.9rem;
cursor: pointer;
}
tr:hover {
background: var(--hover-bg);
}
tr.selected {
background: var(--selected-bg);
}
tr.selected td {
font-weight: 500;
}
.hour-cell {
font-weight: 600;
color: var(--text-secondary);
width: 80px;
}
.date-indicator {
font-size: 0.75rem;
color: #999;
margin-left: 4px;
}
.hidden {
display: none;
}
.markdown-container {
margin-top: 20px;
}
.markdown-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background: var(--accent-bg);
padding: 10px 15px;
border-radius: 4px 4px 0 0;
border: 1px solid var(--border-color);
border-bottom: none;
}
.markdown-view {
background: var(--code-bg);
border: 1px solid var(--border-color);
border-radius: 0 0 4px 4px;
padding: 15px;
font-family: 'Courier New', Consolas, monospace;
font-size: 0.85rem;
white-space: pre;
overflow-x: auto;
line-height: 1.4;
color: var(--text-color);
}
.url-display {
margin-top: 10px;
padding: 10px;
background: var(--accent-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: 'Courier New', Consolas, monospace;
font-size: 0.8rem;
word-break: break-all;
color: var(--text-color);
}
@media (max-width: 768px) {
.timezone-row {
flex-direction: column;
align-items: stretch;
}
.name-input, .tz-container {
width: 100%;
}
.actions {
flex-direction: column;
align-items: stretch;
}
.checkbox-label, .shortcut-hint {
margin-left: 0;
margin-top: 10px;
}
table {
font-size: 0.85rem;
}
th, td {
padding: 8px 4px;
}
.markdown-header {
flex-direction: column;
gap: 10px;
}
.tz-dropdown {
max-height: 150px;
font-size: 0.85rem;
}
}
</style>
</head>
<body>
<h1>Timezone Meeting Planner</h1>
<p class="description">Find the best time to meet across multiple timezones with an interactive comparison table and markdown export.</p>
<div class="controls">
<div id="errorContainer" class="error-message"></div>
<div id="loadingIndicator" class="loading hidden">Loading timezones...</div>
<div id="inputs"></div>
<div class="actions">
<button id="addBtn" onclick="addTimezone()">Add Timezone</button>
<button class="primary" onclick="generateTable()">Generate Table<span class="shortcut-hint">Ctrl+Enter</span></button>
<button class="secondary hidden" id="toggleViewBtn" onclick="toggleView()">Generate as Markdown</button>
<button class="danger" onclick="clearAll()">Clear</button>
<label class="checkbox-label">
<input type="checkbox" id="includeUtc"> Include UTC
</label>
</div>
<div class="actions" style="margin-top: 10px;">
<button onclick="generateShareableUrl()">Generate Shareable URL</button>
<div id="urlDisplay" class="url-display hidden"></div>
</div>
</div>
<div id="output" class="hidden">
<table id="timetable">
<thead>
<tr id="headerRow">
<th>Hour</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<div id="markdownContainer" class="markdown-container hidden">
<div class="markdown-header">
<span>Markdown format</span>
<button class="primary" onclick="copyToClipboard()">Copy to Clipboard</button>
</div>
<pre id="markdownView" class="markdown-view"></pre>
</div>
</div>
<script>
let allZones = [];
let timezones = [];
try {
allZones = Intl.supportedValuesOf('timeZone');
timezones = ['UTC', ...allZones.filter(tz => tz !== 'UTC')];
} catch (e) {
console.error('Intl API not supported');
timezones = ['UTC'];
}
let timezoneCount = 0;
const maxZones = 5;
const minZones = 2;
let activeDropdown = null;
let selectedIndex = -1;
let currentZones = [];
let currentStartHour = 0;
let isMarkdownView = false;
let selectedHourIndices = new Set();
let lastSelectedIndex = null;
let debounceTimer = null;
const STORAGE_KEY = 'tzplanner_state';
function encodeBase64(str) {
try {
return btoa(encodeURIComponent(str));
} catch (e) {
return btoa(str);
}
}
function decodeBase64(str) {
try {
return decodeURIComponent(atob(str));
} catch (e) {
try {
return atob(str);
} catch (e2) {
return null;
}
}
}
function debounce(func, wait) {
return function executedFunction(...args) {
const later = () => {
clearTimeout(debounceTimer);
func(...args);
};
clearTimeout(debounceTimer);
debounceTimer = setTimeout(later, wait);
};
}
function saveState() {
const rows = document.querySelectorAll('.timezone-row');
const state = {
zones: [],
includeUtc: document.getElementById('includeUtc').checked,
selectedHours: Array.from(selectedHourIndices)
};
rows.forEach(row => {
const nameInput = row.querySelector('.name-input');
const tzInput = row.querySelector('.tz-input');
state.zones.push({
name: nameInput.value,
timezone: tzInput.dataset.value || tzInput.value
});
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function loadState() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (!saved) return false;
const state = JSON.parse(saved);
if (!state.zones || state.zones.length < minZones) return false;
document.getElementById('includeUtc').checked = state.includeUtc || false;
if (state.selectedHours) {
selectedHourIndices = new Set(state.selectedHours);
}
document.getElementById('inputs').innerHTML = '';
timezoneCount = 0;
state.zones.forEach(zone => {
addTimezone(zone.name, zone.timezone);
});
return true;
} catch (e) {
return false;
}
}
function loadFromUrl() {
const params = new URLSearchParams(window.location.search);
const dataParam = params.get('d');
if (!dataParam) return false;
try {
const decoded = decodeBase64(dataParam);
if (!decoded) return false;
const data = JSON.parse(decoded);
if (!data.zones || !Array.isArray(data.zones) || data.zones.length < minZones) return false;
document.getElementById('inputs').innerHTML = '';
timezoneCount = 0;
data.zones.forEach(zone => {
addTimezone(zone.name || '', zone.timezone || '');
});
document.getElementById('includeUtc').checked = data.utc || false;
if (data.selected) {
selectedHourIndices = new Set(data.selected);
}
setTimeout(() => generateTable(true), 100);
return true;
} catch (e) {
console.error('Failed to load from URL:', e);
return false;
}
}
function showError(msg) {
const errorEl = document.getElementById('errorContainer');
errorEl.textContent = msg;
errorEl.classList.add('visible');
setTimeout(() => errorEl.classList.remove('visible'), 5000);
}
function clearError() {
document.getElementById('errorContainer').classList.remove('visible');
}
function isOutputVisible() {
return !document.getElementById('output').classList.contains('hidden');
}
function autoGenerate() {
clearError();
if (isOutputVisible() && timezoneCount >= minZones) {
const rows = document.querySelectorAll('.timezone-row');
let allFilled = true;
for (const row of rows) {
const tzInput = row.querySelector('.tz-input');
const tz = tzInput.dataset.value || tzInput.value;
if (!tz || !timezones.includes(tz)) {
allFilled = false;
break;
}
}
if (allFilled) {
saveState();
generateTable(true);
}
}
}
const debouncedAutoGenerate = debounce(autoGenerate, 150);
function createTimezoneSearch(container, prefillName = '', prefillTz = '') {
const wrapper = document.createElement('div');
wrapper.className = 'tz-container';
const input = document.createElement('input');
input.type = 'text';
input.className = 'tz-input';
input.placeholder = 'Search timezone...';
input.autocomplete = 'off';
if (prefillTz) {
input.value = prefillTz;
input.dataset.value = prefillTz;
}
const dropdown = document.createElement('div');
dropdown.className = 'tz-dropdown';
wrapper.appendChild(input);
wrapper.appendChild(dropdown);
let filteredZones = [];
function selectValue(tz) {
input.value = tz;
input.dataset.value = tz;
dropdown.classList.remove('active');
debouncedAutoGenerate();
}
input.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
dropdown.innerHTML = '';
selectedIndex = -1;
if (query.length === 0) {
dropdown.classList.remove('active');
return;
}
filteredZones = timezones.filter(tz => tz.toLowerCase().includes(query));
if (filteredZones.length === 0) {
dropdown.classList.remove('active');
return;
}
filteredZones.slice(0, 50).forEach((tz, index) => {
const div = document.createElement('div');
div.className = 'tz-option';
div.textContent = tz;
div.dataset.index = index;
div.dataset.value = tz;
div.addEventListener('click', () => selectValue(tz));
dropdown.appendChild(div);
});
dropdown.classList.add('active');
activeDropdown = dropdown;
});
input.addEventListener('keydown', (e) => {
if (!dropdown.classList.contains('active')) return;
const options = dropdown.querySelectorAll('.tz-option');
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, options.length - 1);
updateSelection(options);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateSelection(options);
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && options[selectedIndex]) {
selectValue(options[selectedIndex].dataset.value);
} else if (filteredZones.length > 0) {
selectValue(filteredZones[0]);
}
} else if (e.key === 'Escape') {
dropdown.classList.remove('active');
selectedIndex = -1;
}
});
input.addEventListener('blur', () => {
setTimeout(() => {
dropdown.classList.remove('active');
if (!input.dataset.value && input.value) {
const match = timezones.find(tz => tz.toLowerCase() === input.value.toLowerCase());
if (match) {
input.value = match;
input.dataset.value = match;
debouncedAutoGenerate();
}
}
}, 200);
});
input.addEventListener('focus', () => {
if (input.value.length > 0 && filteredZones.length > 0) {
dropdown.classList.add('active');
}
});
return { input, dropdown, wrapper };
}
function updateSelection(options) {
options.forEach((opt, idx) => {
if (idx === selectedIndex) {
opt.classList.add('selected');
opt.scrollIntoView({ block: 'nearest' });
} else {
opt.classList.remove('selected');
}
});
}
function addTimezone(prefillName = '', prefillTz = '') {
if (timezoneCount >= maxZones) return;
const container = document.getElementById('inputs');
const row = document.createElement('div');
row.className = 'timezone-row';
row.dataset.index = timezoneCount;
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'name-input';
nameInput.placeholder = 'Name (optional)';
nameInput.value = prefillName;
nameInput.addEventListener('blur', debouncedAutoGenerate);
const tzComponents = createTimezoneSearch(row, prefillName, prefillTz);
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.onclick = () => {
row.remove();
timezoneCount--;
updateButtons();
if (timezoneCount < minZones) {
clearTable();
localStorage.removeItem(STORAGE_KEY);
} else {
debouncedAutoGenerate();
}
};
row.appendChild(nameInput);
row.appendChild(tzComponents.wrapper);
row.appendChild(removeBtn);
container.appendChild(row);
timezoneCount++;
updateButtons();
if (timezoneCount < minZones) clearTable();
}
function updateButtons() {
document.getElementById('addBtn').disabled = timezoneCount >= maxZones;
}
function clearTable() {
document.getElementById('output').classList.add('hidden');
document.getElementById('toggleViewBtn').classList.add('hidden');
isMarkdownView = false;
selectedHourIndices.clear();
lastSelectedIndex = null;
updateToggleButton();
}
function clearAll() {
document.getElementById('inputs').innerHTML = '';
timezoneCount = 0;
document.getElementById('includeUtc').checked = false;
document.getElementById('urlDisplay').classList.add('hidden');
localStorage.removeItem(STORAGE_KEY);
clearTable();
const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
for (let i = 0; i < minZones; i++) {
if (i === 0 && browserTz && timezones.includes(browserTz)) {
addTimezone('', browserTz);
} else {
addTimezone();
}
}
}
function getCurrentHourInTimezone(timezone) {
const now = new Date();
const timeString = now.toLocaleString('en-US', {
timeZone: timezone,
hour12: false,
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
});
const hour = parseInt(timeString.split(':')[0]);
return hour;
}
function formatTimezoneHeader(name) {
if (name === 'UTC') return 'UTC';
const parts = name.split('/');
return (parts[parts.length - 1] || name).replace(/_/g, ' ');
}
function generateTable(auto = false) {
const rows = document.querySelectorAll('.timezone-row');
const zones = [];
for (const row of rows) {
const nameInput = row.querySelector('.name-input');
const tzInput = row.querySelector('.tz-input');
const tz = tzInput.dataset.value || tzInput.value;
const name = nameInput.value.trim() || tz;
if (!tz) {
if (!auto) showError('Please fill in all timezones');
return;
}
if (!timezones.includes(tz)) {
if (!auto) showError(`Invalid timezone: ${tz}`);
return;
}
zones.push({ name, timezone: tz });
}
if (zones.length < minZones) {
if (!auto) showError(`Please add at least ${minZones} timezones`);
return;
}
if (document.getElementById('includeUtc').checked) {
zones.unshift({ name: 'UTC', timezone: 'UTC' });
}
currentZones = zones;
const firstZoneHour = getCurrentHourInTimezone(zones[0].timezone);
currentStartHour = firstZoneHour;
renderTable();
document.getElementById('output').classList.remove('hidden');
document.getElementById('toggleViewBtn').classList.remove('hidden');
if (isMarkdownView) {
document.getElementById('markdownView').textContent = generateMarkdown();
}
}
function renderTable() {
const headerRow = document.getElementById('headerRow');
headerRow.innerHTML = '<th>Hour</th>';
currentZones.forEach(z => {
const th = document.createElement('th');
th.textContent = z.name;
headerRow.appendChild(th);
});
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
const baseDate = new Date();
baseDate.setMinutes(0, 0, 0);
for (let i = 0; i < 24; i++) {
const hour = (currentStartHour + i) % 24;
const row = document.createElement('tr');
row.dataset.hourIndex = i;
if (selectedHourIndices.has(i)) {
row.classList.add('selected');
}
row.addEventListener('click', (e) => handleRowClick(e, row, i));
const hourCell = document.createElement('td');
hourCell.className = 'hour-cell';
hourCell.textContent = `${hour.toString().padStart(2, '0')}:00`;
row.appendChild(hourCell);
const currentBase = new Date(baseDate);
currentBase.setHours(baseDate.getHours() + i);
currentZones.forEach(zone => {
const cell = document.createElement('td');
const timeStr = currentBase.toLocaleTimeString('en-US', {
timeZone: zone.timezone,
hour: 'numeric',
minute: '2-digit',
hour12: true
});
const zoneDate = new Date(currentBase.toLocaleString('en-US', { timeZone: zone.timezone }));
const baseDateCopy = new Date(currentBase.toLocaleString('en-US', { timeZone: currentZones[0].timezone }));
const dayDiff = Math.round((zoneDate - baseDateCopy) / 86400000);
let dayIndicator = '';
if (dayDiff === 1) dayIndicator = ' <span class="date-indicator">(+1)</span>';
else if (dayDiff === -1) dayIndicator = ' <span class="date-indicator">(-1)</span>';
cell.innerHTML = timeStr + dayIndicator;
row.appendChild(cell);
});
tbody.appendChild(row);
}
}
function handleRowClick(e, row, index) {
if (e.ctrlKey || e.metaKey) {
if (selectedHourIndices.has(index)) {
selectedHourIndices.delete(index);
row.classList.remove('selected');
} else {
selectedHourIndices.add(index);
row.classList.add('selected');
lastSelectedIndex = index;
}
} else if (e.shiftKey && lastSelectedIndex !== null) {
const start = Math.min(lastSelectedIndex, index);
const end = Math.max(lastSelectedIndex, index);
for (let i = start; i <= end; i++) {
selectedHourIndices.add(i);
}
renderTable();
} else {
selectedHourIndices.clear();
selectedHourIndices.add(index);
lastSelectedIndex = index;
renderTable();
}
saveState();
}
function generateMarkdown() {
const baseDate = new Date();
baseDate.setMinutes(0, 0, 0);
const headers = currentZones.map(z => formatTimezoneHeader(z.name));
const colWidths = headers.map(h => Math.max(h.length, 6));
let markdown = '| Hour |';
headers.forEach((h, i) => {
markdown += ' ' + h.padEnd(colWidths[i]) + ' |';
});
markdown += '\n|------|';
headers.forEach((_, i) => {
markdown += '-'.repeat(colWidths[i] + 2) + '|';
});
markdown += '\n';
for (let i = 0; i < 24; i++) {
const hour = (currentStartHour + i) % 24;
const hourStr = `${hour.toString().padStart(2, '0')}:00`;
markdown += `| ${hourStr} |`;
const currentBase = new Date(baseDate);
currentBase.setHours(baseDate.getHours() + i);
currentZones.forEach((zone, idx) => {
const timeStr = currentBase.toLocaleTimeString('en-US', {
timeZone: zone.timezone,
hour: 'numeric',
minute: '2-digit',
hour12: true
});
const zoneDate = new Date(currentBase.toLocaleString('en-US', { timeZone: zone.timezone }));
const baseDateCopy = new Date(currentBase.toLocaleString('en-US', { timeZone: currentZones[0].timezone }));
const dayDiff = Math.round((zoneDate - baseDateCopy) / 86400000);
let timeWithIndicator = timeStr;
if (dayDiff === 1) timeWithIndicator += ' (+1)';
else if (dayDiff === -1) timeWithIndicator += ' (-1)';
const padded = timeWithIndicator.padEnd(colWidths[idx]);
markdown += ` ${padded} |`;
});
markdown += '\n';
}
return markdown;
}
function toggleView() {
const table = document.getElementById('timetable');
const markdownContainer = document.getElementById('markdownContainer');
if (isMarkdownView) {
table.classList.remove('hidden');
markdownContainer.classList.add('hidden');
isMarkdownView = false;
} else {
document.getElementById('markdownView').textContent = generateMarkdown();
table.classList.add('hidden');
markdownContainer.classList.remove('hidden');
isMarkdownView = true;
}
updateToggleButton();
}
function updateToggleButton() {
const btn = document.getElementById('toggleViewBtn');
if (isMarkdownView) {
btn.textContent = 'Show Table';
} else {
btn.textContent = 'Generate as Markdown';
}
}
function copyToClipboard() {
const code = document.getElementById('markdownView').textContent;
navigator.clipboard.writeText(code).then(() => {
showError('Copied to clipboard!');
setTimeout(clearError, 2000);
}).catch(() => {
const textarea = document.createElement('textarea');
textarea.value = code;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showError('Copied to clipboard!');
setTimeout(clearError, 2000);
});
}
function generateShareableUrl() {
const rows = document.querySelectorAll('.timezone-row');
const zones = [];
for (const row of rows) {
const nameInput = row.querySelector('.name-input');
const tzInput = row.querySelector('.tz-input');
const tz = tzInput.dataset.value || tzInput.value;
if (tz) {
zones.push({
name: nameInput.value,
timezone: tz
});
}
}
if (zones.length < minZones) {
showError(`Please add at least ${minZones} timezones first`);
return;
}
const data = {
zones: zones,
utc: document.getElementById('includeUtc').checked
};
if (selectedHourIndices.size > 0) {
data.selected = Array.from(selectedHourIndices);
}
const jsonStr = JSON.stringify(data);
const encoded = encodeBase64(jsonStr);
const url = `${window.location.origin}${window.location.pathname}?d=${encoded}`;
const urlDisplay = document.getElementById('urlDisplay');
urlDisplay.textContent = url;
urlDisplay.classList.remove('hidden');
navigator.clipboard.writeText(url).then(() => {
showError('URL copied to clipboard!');
setTimeout(clearError, 3000);
}).catch(() => {
showError('URL generated (copy manually)');
});
}
document.getElementById('includeUtc').addEventListener('change', debouncedAutoGenerate);
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
generateTable();
}
});
document.getElementById('loadingIndicator').classList.remove('hidden');
setTimeout(() => {
document.getElementById('loadingIndicator').classList.add('hidden');
if (!loadFromUrl() && !loadState()) {
const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
for (let i = 0; i < minZones; i++) {
if (i === 0 && browserTz && timezones.includes(browserTz)) {
addTimezone('', browserTz);
} else {
addTimezone();
}
}
}
}, 100);
document.addEventListener('click', (e) => {
if (!e.target.closest('.tz-container')) {
document.querySelectorAll('.tz-dropdown').forEach(d => d.classList.remove('active'));
}
});
</script>
</body>
</html>