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;
<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">
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 = () => {
}
}
+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;
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;
});
}
+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) => {
document.getElementById('loadingIndicator').classList.remove('hidden');
setTimeout(() => {
document.getElementById('loadingIndicator').classList.add('hidden');
- if (!loadState()) {
+ if (!loadFromUrl() && !loadState()) {
const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
for (let i = 0; i < minZones; i++) {
if (i === 0 && browserTz && timezones.includes(browserTz)) {
## Goal
Develop a single-file, client-side web application that generates an interactive timezone comparison table to help distributed teams find optimal meeting times across 2-5 global locations, with exportable markdown formatting for easy sharing.
+Users can generate a URL which includes the timezones they have added
+and the names they have assigned to each timezone. This link can be
+shared and viewed by anyone on the internet.
+
+
## Requirements
### MUST:
- The button text in Generate Table should not use grey on blue which is very hard to read
- Support selecting multiple hours in the table, not just a single hour:
- This should be done through holding the select key and selecting the rows
+- Support a URL generate optional button which creates a URL with the
+ timezones and the names assigned to each timezone:
+ - The path of the URL should be encoded so it's not long like
+ "/?zones=%5B%7B%22name%22%3A%22foo%22%2C%22timezone%22%3A%22Australia%2FMelbourne%22%7D%2C%7B%22name%22%3A%22abc%22%2C%22timezone%22%3A%22America%2FNew_York%22%7D%2C%7B%22name%22%3A%22mma%22%2C%22timezone%22%3A%22Asia%2FAmman%22%7D%5D&utc=false&selected=13%2C14%2C15%2C16%2C17".
+ Instead it should be encoded with an encoding scheme such as base64.
### SHOULD:
- Support custom names for timezones
- [x] Dark mode support (media query based with CSS variables)
- [x] Handle browser timezone detection as default first entry
- [x] Fix dropdown positioning on mobile screens (<400px width)
+- [ ] Encode generated URL
### Phase 3: Advanced Features (Future)
**Objective**: Extend functionality for power users.