Files
web-tasblet-dashboard/dashboard.html
2026-04-29 22:07:16 +01:00

889 lines
41 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;200;300&display=swap" rel="stylesheet">
<script src="https://widgets.coingecko.com/gecko-coin-list-widget.js" async></script>
<script src="https://widgets.coingecko.com/gecko-coin-price-chart-widget.js" async></script>
<script src="https://widgets.coingecko.com/gecko-coin-heatmap-widget.js" async></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #000000;
--text: #ffffff;
--subtext: #8e8e93;
--divider: #1c1c1e;
--accent: #ff9f0a;
--modal-bg: #1c1c1e;
--modal-hover: #2c2c2e;
--tab-bar-h: 54px;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: -apple-system, 'Helvetica Neue', sans-serif;
overflow: hidden;
}
/* ─── TAB BAR ─── */
#tab-bar {
position: fixed;
bottom: 0; left: 0; right: 0;
height: var(--tab-bar-h);
background: rgba(8,8,8,0.94);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-top: 1px solid var(--divider);
display: flex;
align-items: center;
padding: 0 6px;
z-index: 300;
gap: 1px;
}
.tab-btn {
flex: 1;
height: 42px;
background: none;
border: none;
cursor: pointer;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
color: var(--subtext);
transition: background 0.15s, color 0.15s;
position: relative;
}
.tab-btn:hover { background: rgba(255,255,255,0.05); }
.tab-btn.active { color: var(--accent); }
.tab-icon { font-size: 14px; line-height: 1; }
.tab-label { font-size: 8px; font-weight: 500; letter-spacing: 0.05em; text-transform: uppercase; }
.tab-key {
position: absolute;
top: 3px; right: 5px;
font-size: 6.5px;
font-family: 'Roboto Mono', monospace;
opacity: 0.35;
}
.tab-btn.active .tab-key { opacity: 0.55; }
/* ─── PANELS ─── */
.panel {
position: fixed;
inset: 0;
bottom: var(--tab-bar-h);
overflow-y: auto;
display: none;
flex-direction: column;
background: var(--bg);
height: calc(100% - var(--tab-bar-h));
}
.panel.active { display: flex; }
/* ─── PANEL HEADER ─── */
.panel-header {
position: sticky;
top: 0; z-index: 100;
padding: 20px 28px 12px;
background: rgba(0,0,0,0.88);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--divider);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.panel-header h1 {
font-size: 34px;
font-weight: 700;
letter-spacing: -0.5px;
}
.header-actions { display: flex; gap: 12px; align-items: center; }
.icon-btn {
background: var(--modal-bg);
border: none;
color: var(--accent);
width: 36px; height: 36px;
border-radius: 50%;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, transform 0.1s;
}
.icon-btn:hover { background: var(--modal-hover); transform: scale(1.08); }
.icon-btn:active { transform: scale(0.94); }
/* ─── CLOCK LIST ─── */
#clock-list { flex: 1; padding: 0; list-style: none; }
.clock-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 28px;
border-bottom: 1px solid var(--divider);
animation: fadeIn 0.35s ease both;
position: relative;
overflow: hidden;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.clock-location { font-size: 13px; color: var(--subtext); letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 2px; }
.clock-city { font-size: 22px; font-weight: 600; letter-spacing: -0.3px; margin-bottom: 4px; }
.clock-day-info { font-size: 13px; color: var(--subtext); }
.clock-right {
text-align: right;
display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
}
.clock-digital {
font-family: 'Roboto Mono', monospace;
font-size: 52px;
font-weight: 100;
letter-spacing: -1px;
line-height: 1;
text-shadow: 0 0 30px rgba(255,255,255,0.07);
}
.clock-digital .colon { animation: blink 1s step-start infinite; }
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0.15; } }
.clock-ampm { font-size: 16px; font-weight: 300; color: var(--subtext); font-family: inherit; margin-left: 4px; }
.clock-seconds { font-family: 'Roboto Mono', monospace; font-size: 13px; color: var(--subtext); }
.delete-btn {
position: absolute; left: 0; top: 0;
height: 100%; width: 36px;
background: #ff3b30; border: none; color: #fff; font-size: 20px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
transform: translateX(-100%);
transition: transform 0.22s ease;
}
.clock-item:hover .delete-btn { transform: translateX(0); }
.clock-item:hover { padding-left: 64px; transition: padding 0.22s ease; }
#clock-empty {
display: none;
flex-direction: column; align-items: center; justify-content: center;
flex: 1; gap: 12px; color: var(--subtext); padding: 60px 28px;
}
#clock-empty svg { opacity: 0.25; }
#clock-empty p { font-size: 17px; text-align: center; line-height: 1.5; }
.icon-btn.slideshow-active { color: var(--accent2); background: rgba(48,209,88,0.15); }
/* ─── WIDGET PANELS (tabs 2-6): compact bar + full-height widget ─── */
.widget-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px 6px 16px;
flex-shrink: 0;
border-bottom: 1px solid var(--divider);
background: rgba(0,0,0,0.9);
height: 38px;
}
.widget-bar-title {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--subtext);
text-transform: uppercase;
}
.widget-fill {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
gecko-coin-list-widget,
gecko-coin-price-chart-widget,
gecko-coin-heatmap-widget {
display: block;
width: 100% !important;
flex: 1;
min-height: 0;
}
/* ─── PLACEHOLDER PANELS ─── */
.placeholder-body {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
color: var(--subtext);
padding: 40px;
}
.ph-key {
font-family: 'Roboto Mono', monospace;
font-size: 64px;
font-weight: 100;
color: #1a1a1a;
letter-spacing: -4px;
}
.placeholder-body p { font-size: 15px; text-align: center; line-height: 1.6; max-width: 260px; }
.ph-hint {
font-size: 11px;
background: var(--modal-bg);
border-radius: 8px;
padding: 7px 14px;
font-family: 'Roboto Mono', monospace;
}
/* ─── MODAL ─── */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.72);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
z-index: 400;
display: flex; align-items: flex-end; justify-content: center;
opacity: 0; pointer-events: none;
transition: opacity 0.25s;
}
.modal-overlay.open { opacity: 1; pointer-events: all; }
.modal {
background: var(--modal-bg);
border-radius: 16px 16px 0 0;
width: 100%; max-width: 540px; max-height: 80vh;
display: flex; flex-direction: column;
transform: translateY(100%);
transition: transform 0.32s cubic-bezier(0.32,0.72,0,1);
margin-bottom: var(--tab-bar-h);
}
.modal-overlay.open .modal { transform: translateY(0); }
.modal-handle { width: 36px; height: 4px; background: var(--subtext); border-radius: 2px; margin: 12px auto 0; opacity: 0.4; }
.modal-header {
padding: 16px 20px 12px;
display: flex; align-items: center; justify-content: space-between;
border-bottom: 1px solid #333;
}
.modal-header h2 { font-size: 18px; font-weight: 600; }
.modal-close {
background: #3a3a3c; border: none; color: var(--subtext);
width: 28px; height: 28px; border-radius: 50%; font-size: 14px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.modal-close:hover { background: #48484a; }
.modal-search {
margin: 12px 16px;
background: #2c2c2e; border: none; border-radius: 10px;
color: var(--text); font-size: 15px; padding: 10px 14px;
width: calc(100% - 32px); outline: none;
}
.modal-search::placeholder { color: var(--subtext); }
.tz-list { overflow-y: auto; flex: 1; list-style: none; }
.tz-list::-webkit-scrollbar { width: 4px; }
.tz-list::-webkit-scrollbar-thumb { background: #3a3a3c; border-radius: 2px; }
.tz-group-label {
font-size: 12px; font-weight: 600; color: var(--subtext);
text-transform: uppercase; letter-spacing: 0.08em;
padding: 10px 20px 4px;
background: var(--modal-bg); position: sticky; top: 0;
}
.tz-item {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; cursor: pointer;
border-bottom: 1px solid #2c2c2e;
transition: background 0.12s;
}
.tz-item:hover { background: var(--modal-hover); }
.tz-city-name { font-size: 16px; font-weight: 500; }
.tz-tz-name { font-size: 12px; color: var(--subtext); margin-top: 2px; }
.tz-current-time { font-family: 'Roboto Mono', monospace; font-size: 15px; font-weight: 300; color: var(--subtext); }
.tz-item.added .tz-city-name { color: var(--accent); }
.tz-added-badge { font-size: 11px; background: var(--accent); color: #000; border-radius: 4px; padding: 2px 6px; font-weight: 600; margin-left: 8px; }
/* ─── TOAST ─── */
#key-toast {
position: fixed;
top: 16px; left: 50%;
transform: translateX(-50%) translateY(-50px);
background: rgba(28,28,30,0.96);
border: 1px solid #2c2c2e;
border-radius: 10px;
padding: 7px 16px;
font-size: 12px;
font-family: 'Roboto Mono', monospace;
color: var(--subtext);
z-index: 500;
pointer-events: none;
opacity: 0;
transition: transform 0.28s cubic-bezier(0.32,0.72,0,1), opacity 0.28s;
white-space: nowrap;
}
#key-toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
#key-toast strong { color: var(--accent); }
</style>
</head>
<body>
<!-- ══════════════ PANEL 1: WORLD CLOCK ══════════════ -->
<div class="panel active" id="panel-1">
<header class="panel-header">
<h1>World Clock</h1>
<div class="header-actions">
<button class="icon-btn" id="slideshow-btn" title="Start slideshow (S)">
<svg id="slideshow-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<polygon points="3,2 14,8 3,14"/>
</svg>
</button>
<button class="icon-btn" id="fs-btn" title="Toggle fullscreen">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/>
</svg>
</button>
<button class="icon-btn" id="add-btn" title="Add clock">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="8" y1="1" x2="8" y2="15"/><line x1="1" y1="8" x2="15" y2="8"/>
</svg>
</button>
</div>
</header>
<ul id="clock-list"></ul>
<div id="clock-empty">
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" stroke="white" stroke-width="1.5" stroke-linecap="round">
<circle cx="26" cy="26" r="22"/>
<line x1="26" y1="14" x2="26" y2="26"/><line x1="26" y1="26" x2="35" y2="32"/>
<circle cx="26" cy="26" r="2" fill="white"/>
</svg>
<p>No clocks added yet.<br/>Tap <strong style="color:var(--accent)">+</strong> to add a city.</p>
</div>
</div>
<!-- ══════════════ PANEL 2: CRYPTO ══════════════ -->
<div class="panel" id="panel-2">
<div class="widget-bar">
<span class="widget-bar-title">Crypto</span>
<button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button>
</div>
<div class="widget-fill">
<gecko-coin-list-widget locale="en" dark-mode="true" outlined="true" coin-ids="bitcoin,ethereum,tether,pax-gold,solana,binancecoin,ripple" initial-currency="usd"></gecko-coin-list-widget>
</div>
</div>
<!-- ══════════════ PANEL 3: HEATMAP ══════════════ -->
<div class="panel" id="panel-3">
<div class="widget-bar">
<span class="widget-bar-title">Heatmap</span>
<button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button>
</div>
<div class="widget-fill">
<gecko-coin-heatmap-widget locale="en" dark-mode="true" outlined="true" top="10"></gecko-coin-heatmap-widget>
</div>
</div>
<!-- ══════════════ PANEL 4: BTC CHART ══════════════ -->
<div class="panel" id="panel-4">
<div class="widget-bar">
<span class="widget-bar-title">Bitcoin</span>
<button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button>
</div>
<div class="widget-fill">
<gecko-coin-price-chart-widget locale="en" dark-mode="true" outlined="true" initial-currency="usd"></gecko-coin-price-chart-widget>
</div>
</div>
<!-- ══════════════ PANEL 5: ETH CHART ══════════════ -->
<div class="panel" id="panel-5">
<div class="widget-bar">
<span class="widget-bar-title">Ethereum</span>
<button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button>
</div>
<div class="widget-fill">
<gecko-coin-price-chart-widget locale="en" dark-mode="true" outlined="true" coin-id="ethereum" initial-currency="usd"></gecko-coin-price-chart-widget>
</div>
</div>
<!-- ══════════════ PANEL 6: PAX GOLD CHART ══════════════ -->
<div class="panel" id="panel-6">
<div class="widget-bar">
<span class="widget-bar-title">PAX Gold</span>
<button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button>
</div>
<div class="widget-fill">
<gecko-coin-price-chart-widget locale="en" dark-mode="true" outlined="true" coin-id="pax-gold" initial-currency="usd"></gecko-coin-price-chart-widget>
</div>
</div>
<div class="panel" id="panel-7">
<header class="panel-header"><h1>Tab 7</h1><div class="header-actions"><button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button></div></header>
<div class="placeholder-body"><div class="ph-key">7</div><p>Empty tab — press <strong style="color:var(--accent)">7</strong> to jump here.</p><span class="ph-hint">Keys 19, 0 switch tabs</span></div>
</div>
<div class="panel" id="panel-8">
<header class="panel-header"><h1>Tab 8</h1><div class="header-actions"><button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button></div></header>
<div class="placeholder-body"><div class="ph-key">8</div><p>Empty tab — press <strong style="color:var(--accent)">8</strong> to jump here.</p><span class="ph-hint">Keys 19, 0 switch tabs</span></div>
</div>
<div class="panel" id="panel-9">
<header class="panel-header"><h1>Tab 9</h1><div class="header-actions"><button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button></div></header>
<div class="placeholder-body"><div class="ph-key">9</div><p>Empty tab — press <strong style="color:var(--accent)">9</strong> to jump here.</p><span class="ph-hint">Keys 19, 0 switch tabs</span></div>
</div>
<div class="panel" id="panel-10">
<header class="panel-header"><h1>Tab 10</h1><div class="header-actions"><button class="icon-btn fs-all" title="Toggle fullscreen"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg></button></div></header>
<div class="placeholder-body"><div class="ph-key">0</div><p>Empty tab — press <strong style="color:var(--accent)">0</strong> to jump here.</p><span class="ph-hint">Keys 19, 0 switch tabs</span></div>
</div>
<!-- ══════════════ TAB BAR ══════════════ -->
<nav id="tab-bar">
<button class="tab-btn active" data-tab="1"><span class="tab-key">1</span><span class="tab-icon">🕐</span><span class="tab-label">Clock</span></button>
<button class="tab-btn" data-tab="2"><span class="tab-key">2</span><span class="tab-icon"></span><span class="tab-label">Crypto</span></button>
<button class="tab-btn" data-tab="3"><span class="tab-key">3</span><span class="tab-icon">🔥</span><span class="tab-label">Heat</span></button>
<button class="tab-btn" data-tab="4"><span class="tab-key">4</span><span class="tab-icon"></span><span class="tab-label">BTC</span></button>
<button class="tab-btn" data-tab="5"><span class="tab-key">5</span><span class="tab-icon">Ξ</span><span class="tab-label">ETH</span></button>
<button class="tab-btn" data-tab="6"><span class="tab-key">6</span><span class="tab-icon">🥇</span><span class="tab-label">PAXG</span></button>
<button class="tab-btn" data-tab="7"><span class="tab-key">7</span><span class="tab-icon"></span><span class="tab-label">Tab 7</span></button>
<button class="tab-btn" data-tab="8"><span class="tab-key">8</span><span class="tab-icon"></span><span class="tab-label">Tab 8</span></button>
<button class="tab-btn" data-tab="9"><span class="tab-key">9</span><span class="tab-icon"></span><span class="tab-label">Tab 9</span></button>
<button class="tab-btn" data-tab="10"><span class="tab-key">0</span><span class="tab-icon"></span><span class="tab-label">Tab 10</span></button>
</nav>
<!-- CLOCK MODAL -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal" role="dialog">
<div class="modal-handle"></div>
<div class="modal-header">
<h2>Choose a City</h2>
<button class="modal-close" id="modal-close"></button>
</div>
<input class="modal-search" id="modal-search" type="text" placeholder="Search city or timezone…" autocomplete="off" />
<ul class="tz-list" id="tz-list"></ul>
</div>
</div>
<div id="key-toast"></div>
<script>
// ─── TAB SYSTEM ───────────────────────────────────────────
let activeTab = 1;
let slideshowTimer = null;
const TAB_NAMES = {
1:'World Clock', 2:'Crypto', 3:'Heatmap', 4:'Bitcoin', 5:'Ethereum', 6:'PAX Gold',
7:'Tab 7', 8:'Tab 8', 9:'Tab 9', 10:'Tab 10'
};
function switchTab(n, fromSlideshow = false) {
if (n < 1 || n > 10 || n === activeTab) return;
// Manual switch stops slideshow
if (!fromSlideshow && slideshowTimer) slideshowOff();
document.getElementById(`panel-${activeTab}`)?.classList.remove('active');
document.querySelector(`.tab-btn[data-tab="${activeTab}"]`)?.classList.remove('active');
activeTab = n;
document.getElementById(`panel-${n}`)?.classList.add('active');
document.querySelector(`.tab-btn[data-tab="${n}"]`)?.classList.add('active');
showToast(`<strong>${n === 10 ? '0' : n}</strong> &nbsp;·&nbsp; ${TAB_NAMES[n]}`);
// Force widgets to fill their container by setting explicit pixel height
requestAnimationFrame(() => {
sizeWidgets();
window.dispatchEvent(new Event('resize'));
// second pass after widget may have initialised
setTimeout(() => { sizeWidgets(); window.dispatchEvent(new Event('resize')); }, 300);
});
}
document.querySelectorAll('.tab-btn').forEach(btn =>
btn.addEventListener('click', () => switchTab(+btn.dataset.tab))
);
// ─── KEYBOARD ─────────────────────────────────────────────
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
const modalOpen = document.getElementById('modal-overlay').classList.contains('open');
if (e.key === 'Escape' && modalOpen) { closeModal(); return; }
if (modalOpen) return;
if (e.key >= '1' && e.key <= '9') { switchTab(+e.key); return; }
if (e.key === '0') { switchTab(10); return; }
if (e.key === 'f' || e.key === 'F') toggleFullscreen();
if (e.key === 'r' || e.key === 'R') { showToast('↻ Refreshing…'); setTimeout(() => location.reload(), 400); }
if (e.key === 's' || e.key === 'S') toggleSlideshow();
});
// ─── TOAST ────────────────────────────────────────────────
let toastTimer;
function showToast(msg) {
const t = document.getElementById('key-toast');
t.innerHTML = msg;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 1500);
}
// ─── FULLSCREEN ───────────────────────────────────────────
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
} else {
document.exitFullscreen().catch(() => {});
}
}
const FS_ICON_OUT = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M10 1h5v5M15 10v5h-5M6 15H1v-5"/></svg>`;
const FS_ICON_IN = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 1H1v5M10 1h5v5M15 10h-5v5M6 15H1v-5"/></svg>`;
document.addEventListener('fullscreenchange', () => {
const inFS = !!document.fullscreenElement;
document.querySelectorAll('#fs-btn, .fs-all').forEach(b => {
b.innerHTML = inFS ? FS_ICON_IN : FS_ICON_OUT;
b.title = inFS ? 'Exit fullscreen' : 'Toggle fullscreen';
});
});
document.getElementById('fs-btn').addEventListener('click', toggleFullscreen);
document.querySelectorAll('.fs-all').forEach(b => b.addEventListener('click', toggleFullscreen));
// ─── CLOCK DATA ───────────────────────────────────────────
const ALL_TZ = [
{ city:"New York", tz:"America/New_York", region:"Americas" },
{ city:"Los Angeles", tz:"America/Los_Angeles", region:"Americas" },
{ city:"Chicago", tz:"America/Chicago", region:"Americas" },
{ city:"Denver", tz:"America/Denver", region:"Americas" },
{ city:"Phoenix", tz:"America/Phoenix", region:"Americas" },
{ city:"Toronto", tz:"America/Toronto", region:"Americas" },
{ city:"Vancouver", tz:"America/Vancouver", region:"Americas" },
{ city:"Mexico City", tz:"America/Mexico_City", region:"Americas" },
{ city:"São Paulo", tz:"America/Sao_Paulo", region:"Americas" },
{ city:"Buenos Aires", tz:"America/Argentina/Buenos_Aires", region:"Americas" },
{ city:"Bogotá", tz:"America/Bogota", region:"Americas" },
{ city:"Lima", tz:"America/Lima", region:"Americas" },
{ city:"Santiago", tz:"America/Santiago", region:"Americas" },
{ city:"Caracas", tz:"America/Caracas", region:"Americas" },
{ city:"Havana", tz:"America/Havana", region:"Americas" },
{ city:"Anchorage", tz:"America/Anchorage", region:"Americas" },
{ city:"Honolulu", tz:"Pacific/Honolulu", region:"Americas" },
{ city:"London", tz:"Europe/London", region:"Europe" },
{ city:"Paris", tz:"Europe/Paris", region:"Europe" },
{ city:"Berlin", tz:"Europe/Berlin", region:"Europe" },
{ city:"Madrid", tz:"Europe/Madrid", region:"Europe" },
{ city:"Rome", tz:"Europe/Rome", region:"Europe" },
{ city:"Amsterdam", tz:"Europe/Amsterdam", region:"Europe" },
{ city:"Brussels", tz:"Europe/Brussels", region:"Europe" },
{ city:"Zurich", tz:"Europe/Zurich", region:"Europe" },
{ city:"Vienna", tz:"Europe/Vienna", region:"Europe" },
{ city:"Stockholm", tz:"Europe/Stockholm", region:"Europe" },
{ city:"Oslo", tz:"Europe/Oslo", region:"Europe" },
{ city:"Copenhagen", tz:"Europe/Copenhagen", region:"Europe" },
{ city:"Helsinki", tz:"Europe/Helsinki", region:"Europe" },
{ city:"Warsaw", tz:"Europe/Warsaw", region:"Europe" },
{ city:"Prague", tz:"Europe/Prague", region:"Europe" },
{ city:"Budapest", tz:"Europe/Budapest", region:"Europe" },
{ city:"Bucharest", tz:"Europe/Bucharest", region:"Europe" },
{ city:"Athens", tz:"Europe/Athens", region:"Europe" },
{ city:"Istanbul", tz:"Europe/Istanbul", region:"Europe" },
{ city:"Kyiv", tz:"Europe/Kyiv", region:"Europe" },
{ city:"Moscow", tz:"Europe/Moscow", region:"Europe" },
{ city:"Lisbon", tz:"Europe/Lisbon", region:"Europe" },
{ city:"Dublin", tz:"Europe/Dublin", region:"Europe" },
{ city:"Dubai", tz:"Asia/Dubai", region:"Asia & Pacific" },
{ city:"Mumbai", tz:"Asia/Kolkata", region:"Asia & Pacific" },
{ city:"Delhi", tz:"Asia/Kolkata", region:"Asia & Pacific" },
{ city:"Karachi", tz:"Asia/Karachi", region:"Asia & Pacific" },
{ city:"Dhaka", tz:"Asia/Dhaka", region:"Asia & Pacific" },
{ city:"Kathmandu", tz:"Asia/Kathmandu", region:"Asia & Pacific" },
{ city:"Bangkok", tz:"Asia/Bangkok", region:"Asia & Pacific" },
{ city:"Ho Chi Minh", tz:"Asia/Ho_Chi_Minh", region:"Asia & Pacific" },
{ city:"Singapore", tz:"Asia/Singapore", region:"Asia & Pacific" },
{ city:"Kuala Lumpur", tz:"Asia/Kuala_Lumpur", region:"Asia & Pacific" },
{ city:"Jakarta", tz:"Asia/Jakarta", region:"Asia & Pacific" },
{ city:"Manila", tz:"Asia/Manila", region:"Asia & Pacific" },
{ city:"Hong Kong", tz:"Asia/Hong_Kong", region:"Asia & Pacific" },
{ city:"Taipei", tz:"Asia/Taipei", region:"Asia & Pacific" },
{ city:"Shanghai", tz:"Asia/Shanghai", region:"Asia & Pacific" },
{ city:"Beijing", tz:"Asia/Shanghai", region:"Asia & Pacific" },
{ city:"Seoul", tz:"Asia/Seoul", region:"Asia & Pacific" },
{ city:"Tokyo", tz:"Asia/Tokyo", region:"Asia & Pacific" },
{ city:"Osaka", tz:"Asia/Tokyo", region:"Asia & Pacific" },
{ city:"Riyadh", tz:"Asia/Riyadh", region:"Asia & Pacific" },
{ city:"Tehran", tz:"Asia/Tehran", region:"Asia & Pacific" },
{ city:"Baghdad", tz:"Asia/Baghdad", region:"Asia & Pacific" },
{ city:"Tel Aviv", tz:"Asia/Jerusalem", region:"Asia & Pacific" },
{ city:"Almaty", tz:"Asia/Almaty", region:"Asia & Pacific" },
{ city:"Ulaanbaatar", tz:"Asia/Ulaanbaatar", region:"Asia & Pacific" },
{ city:"Vladivostok", tz:"Asia/Vladivostok", region:"Asia & Pacific" },
{ city:"Sydney", tz:"Australia/Sydney", region:"Asia & Pacific" },
{ city:"Melbourne", tz:"Australia/Melbourne", region:"Asia & Pacific" },
{ city:"Brisbane", tz:"Australia/Brisbane", region:"Asia & Pacific" },
{ city:"Perth", tz:"Australia/Perth", region:"Asia & Pacific" },
{ city:"Auckland", tz:"Pacific/Auckland", region:"Asia & Pacific" },
{ city:"Fiji", tz:"Pacific/Fiji", region:"Asia & Pacific" },
{ city:"Guam", tz:"Pacific/Guam", region:"Asia & Pacific" },
{ city:"Cairo", tz:"Africa/Cairo", region:"Africa" },
{ city:"Lagos", tz:"Africa/Lagos", region:"Africa" },
{ city:"Nairobi", tz:"Africa/Nairobi", region:"Africa" },
{ city:"Johannesburg", tz:"Africa/Johannesburg", region:"Africa" },
{ city:"Casablanca", tz:"Africa/Casablanca", region:"Africa" },
{ city:"Accra", tz:"Africa/Accra", region:"Africa" },
{ city:"Addis Ababa", tz:"Africa/Addis_Ababa", region:"Africa" },
{ city:"Dar es Salaam", tz:"Africa/Dar_es_Salaam", region:"Africa" },
{ city:"Algiers", tz:"Africa/Algiers", region:"Africa" },
{ city:"Khartoum", tz:"Africa/Khartoum", region:"Africa" },
{ city:"UTC", tz:"UTC", region:"UTC" },
{ city:"Reykjavik", tz:"Atlantic/Reykjavik", region:"UTC" },
];
// ─── CLOCK LOGIC ──────────────────────────────────────────
let clocks = JSON.parse(localStorage.getItem('worldclocks') || '[]');
const save = () => localStorage.setItem('worldclocks', JSON.stringify(clocks));
function nowIn(tz) { return new Date(new Date().toLocaleString('en-US', { timeZone: tz })); }
function tzOffset(tz) {
const now = new Date();
const local = new Date(now.toLocaleString('en-US', { timeZone: tz }));
const diff = (local - now) / 60000;
const sign = diff >= 0 ? '+' : '-';
const abs = Math.abs(Math.round(diff));
const h = Math.floor(abs / 60), m = abs % 60;
return `UTC${sign}${h}${m ? ':' + String(m).padStart(2,'0') : ''}`;
}
function formatTime(d) {
let h = d.getHours();
const m = String(d.getMinutes()).padStart(2,'0');
const ampm = h >= 12 ? 'PM' : 'AM';
return { h: String(h % 12 || 12), m, ampm };
}
function dayRelative(tz) {
const here = new Date(), there = nowIn(tz);
const hd = new Date(here.getFullYear(), here.getMonth(), here.getDate());
const td = new Date(there.getFullYear(), there.getMonth(), there.getDate());
const diff = Math.round((td - hd) / 86400000);
const dn = there.toLocaleDateString('en-US', { weekday:'long', timeZone:tz });
if (diff === 0) return dn + ', today';
if (diff === 1) return dn + ', tomorrow';
if (diff === -1) return dn + ', yesterday';
return dn + (diff > 0 ? ` +${diff}d` : ` ${diff}d`);
}
function renderClocks() {
const list = document.getElementById('clock-list');
const empty = document.getElementById('clock-empty');
if (!clocks.length) { list.innerHTML = ''; empty.style.display = 'flex'; return; }
empty.style.display = 'none';
const sorted = [...clocks].sort((a,b) => nowIn(a.tz) - nowIn(b.tz));
if (list.children.length !== clocks.length) {
list.innerHTML = '';
sorted.forEach((c, i) => {
const li = document.createElement('li');
li.className = 'clock-item';
li.style.animationDelay = `${i * 0.06}s`;
li.innerHTML = clockHTML(c);
li.querySelector('.delete-btn').addEventListener('click', () => {
clocks = clocks.filter(x => !(x.city === c.city && x.tz === c.tz));
save(); renderClocks();
});
list.appendChild(li);
});
} else {
sorted.forEach((c, i) => tickClock(list.children[i], c));
}
}
function clockHTML(c) {
const d = nowIn(c.tz); const { h, m, ampm } = formatTime(d);
const sec = String(d.getSeconds()).padStart(2,'0');
return `
<button class="delete-btn" title="Remove">✕</button>
<div class="clock-left">
<div class="clock-location">${tzOffset(c.tz)}</div>
<div class="clock-city">${c.city}</div>
<div class="clock-day-info">${dayRelative(c.tz)}</div>
</div>
<div class="clock-right">
<div class="clock-digital">${h}<span class="colon">:</span>${m}<span class="clock-ampm">${ampm}</span></div>
<div class="clock-seconds">:${sec}</div>
</div>`;
}
function tickClock(li, c) {
const d = nowIn(c.tz); const { h, m, ampm } = formatTime(d);
const sec = String(d.getSeconds()).padStart(2,'0');
const dig = li.querySelector('.clock-digital');
if (dig) dig.innerHTML = `${h}<span class="colon">:</span>${m}<span class="clock-ampm">${ampm}</span>`;
const secEl = li.querySelector('.clock-seconds');
if (secEl) secEl.textContent = ':' + sec;
const dayEl = li.querySelector('.clock-day-info');
if (dayEl) dayEl.textContent = dayRelative(c.tz);
}
setInterval(renderClocks, 1000);
// ─── MODAL ────────────────────────────────────────────────
function openModal() {
document.getElementById('modal-overlay').classList.add('open');
document.getElementById('modal-search').focus();
renderTZList('');
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('open');
document.getElementById('modal-search').value = '';
}
function renderTZList(query) {
const ul = document.getElementById('tz-list');
const q = query.toLowerCase();
const filtered = ALL_TZ.filter(t =>
t.city.toLowerCase().includes(q) || t.tz.toLowerCase().includes(q) || t.region.toLowerCase().includes(q)
);
const groups = {};
filtered.forEach(t => { if (!groups[t.region]) groups[t.region] = []; groups[t.region].push(t); });
ul.innerHTML = '';
for (const [region, items] of Object.entries(groups)) {
const lbl = document.createElement('li');
lbl.className = 'tz-group-label';
lbl.textContent = region;
ul.appendChild(lbl);
items.forEach(t => {
const isAdded = clocks.some(c => c.city === t.city && c.tz === t.tz);
const { h, m, ampm } = formatTime(nowIn(t.tz));
const li = document.createElement('li');
li.className = 'tz-item' + (isAdded ? ' added' : '');
li.innerHTML = `
<div class="tz-item-left">
<div class="tz-city-name">${t.city}${isAdded ? '<span class="tz-added-badge">Added</span>' : ''}</div>
<div class="tz-tz-name">${t.tz} · ${tzOffset(t.tz)}</div>
</div>
<div class="tz-current-time">${h}:${m} ${ampm}</div>`;
li.addEventListener('click', () => {
if (!clocks.find(c => c.city === t.city && c.tz === t.tz)) {
clocks.push({ city: t.city, tz: t.tz }); save(); renderClocks();
}
closeModal();
});
ul.appendChild(li);
});
}
if (!filtered.length) ul.innerHTML = '<li style="padding:24px;text-align:center;color:#8e8e93;">No results found</li>';
}
document.getElementById('add-btn').addEventListener('click', openModal);
document.getElementById('modal-close').addEventListener('click', closeModal);
document.getElementById('modal-overlay').addEventListener('click', e => {
if (e.target === document.getElementById('modal-overlay')) closeModal();
});
document.getElementById('modal-search').addEventListener('input', e => renderTZList(e.target.value));
// ─── WIDGET SIZING ────────────────────────────────────────
function sizeWidgets() {
const tabBarH = document.getElementById('tab-bar').offsetHeight;
const viewH = window.innerHeight;
['gecko-coin-list-widget', 'gecko-coin-price-chart-widget', 'gecko-coin-heatmap-widget'].forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
const bar = el.closest('.panel')?.querySelector('.widget-bar');
const barH = bar ? bar.offsetHeight : 0;
const h = viewH - tabBarH - barH;
// Apply to the custom element itself
el.style.cssText += `height:${h}px !important; min-height:${h}px !important; max-height:${h}px !important;`;
el.setAttribute('height', h);
// Apply to any child iframe (gecko widgets render one)
const tryFrame = (root) => {
if (!root) return;
root.querySelectorAll('iframe').forEach(f => {
f.style.height = h + 'px';
f.style.minHeight = h + 'px';
f.height = h;
});
};
tryFrame(el);
tryFrame(el.shadowRoot);
});
});
// Also force widget-fill divs to exact remaining height
document.querySelectorAll('.widget-fill').forEach(fill => {
const bar = fill.closest('.panel')?.querySelector('.widget-bar');
const barH = bar ? bar.offsetHeight : 0;
const h = viewH - tabBarH - barH;
fill.style.height = h + 'px';
fill.style.minHeight = h + 'px';
fill.style.maxHeight = h + 'px';
});
}
window.addEventListener('resize', sizeWidgets);
// Re-run sizeWidgets whenever any new iframe appears inside a gecko widget
// (they load asynchronously after the custom element registers)
const iframeObserver = new MutationObserver(() => sizeWidgets());
document.querySelectorAll('gecko-coin-list-widget, gecko-coin-price-chart-widget, gecko-coin-heatmap-widget').forEach(el => {
iframeObserver.observe(el, { childList: true, subtree: true });
});
// ─── SLIDESHOW ────────────────────────────────────────────
const SLIDESHOW_TABS = [1, 2, 3, 4, 5, 6]; // only populated tabs
const SLIDESHOW_INTERVAL = 10000;
const PLAY_ICON = `<polygon points="3,2 14,8 3,14"/>`;
const PAUSE_ICON = `<rect x="3" y="2" width="4" height="12" rx="1"/><rect x="9" y="2" width="4" height="12" rx="1"/>`;
function slideshowOn() {
const btn = document.getElementById('slideshow-btn');
btn.classList.add('slideshow-active');
btn.title = 'Stop slideshow (S)';
btn.querySelector('svg').innerHTML = PAUSE_ICON;
// advance immediately then every 10s
advanceSlideshow();
slideshowTimer = setInterval(advanceSlideshow, SLIDESHOW_INTERVAL);
}
function slideshowOff() {
clearInterval(slideshowTimer);
slideshowTimer = null;
const btn = document.getElementById('slideshow-btn');
btn.classList.remove('slideshow-active');
btn.title = 'Start slideshow (S)';
btn.querySelector('svg').innerHTML = PLAY_ICON;
}
function toggleSlideshow() {
slideshowTimer ? slideshowOff() : slideshowOn();
}
function advanceSlideshow() {
const idx = SLIDESHOW_TABS.indexOf(activeTab);
const next = SLIDESHOW_TABS[(idx + 1) % SLIDESHOW_TABS.length];
switchTab(next, true);
}
document.getElementById('slideshow-btn').addEventListener('click', toggleSlideshow);
// ─── INIT ─────────────────────────────────────────────────
renderClocks();
sizeWidgets();
setTimeout(sizeWidgets, 200);
setTimeout(sizeWidgets, 600);
setTimeout(sizeWidgets, 1500);
setTimeout(sizeWidgets, 3000);
</script>
</body>
</html>