Skip to content

Instantly share code, notes, and snippets.

@mark05e
Created July 20, 2026 01:58
Show Gist options
  • Select an option

  • Save mark05e/d8328551c85497211316e9f43511fd98 to your computer and use it in GitHub Desktop.

Select an option

Save mark05e/d8328551c85497211316e9f43511fd98 to your computer and use it in GitHub Desktop.
// --- API layer: swap this out if the endpoint/shape ever changes ---
async function fetchTogglEntries(before) {
const url = new URL('https://track.toggl.com/api/v9/me/time_entries');
url.searchParams.set('meta', 'true');
url.searchParams.set('include_sharing', 'true');
url.searchParams.set('include_expenses', 'true');
if (before) url.searchParams.set('before', before);
const res = await fetch(url, { credentials: 'include', headers: { 'Accept': 'application/json' } });
if (!res.ok) throw new Error(`Toggl API error: ${res.status}`);
return res.json(); // array of { id, start, description, duration }
}
// --- UI layer ---
async function showTogglPanel() {
const entriesById = new Map();
let idx = 0;
function regroup() {
const byDay = {};
for (const e of entriesById.values()) {
const d = new Date(e.start).toLocaleDateString('en-CA');
(byDay[d] ??= {})[e.description || '(no description)'] =
(byDay[d]?.[e.description] || 0) + e.duration;
}
return Object.keys(byDay).sort().reverse(); // newest first
}
let byDayDates = [];
const byDayLookup = () => {
const byDay = {};
for (const e of entriesById.values()) {
const d = new Date(e.start).toLocaleDateString('en-CA');
(byDay[d] ??= {})[e.description || '(no description)'] =
(byDay[d]?.[e.description] || 0) + e.duration;
}
return byDay;
};
for (const e of await fetchTogglEntries()) entriesById.set(e.id, e);
byDayDates = regroup();
idx = 0;
const fmt = s => `${Math.floor(s/3600)}h ${Math.floor(s%3600/60)}m ${s%60}s`;
const weekday = d => {
const [y,m,day] = d.split('-').map(Number);
return new Date(y, m-1, day).toLocaleDateString('en-US', { weekday: 'long' });
};
document.getElementById('toggl-panel')?.remove();
const panel = document.createElement('div');
panel.id = 'toggl-panel';
panel.style.cssText = `
position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
background:#1e1e2e; color:#f0f0f0; padding:20px 24px; border-radius:12px;
font-family:sans-serif; box-shadow:0 8px 30px rgba(0,0,0,0.5); z-index:99999;
min-width:340px;
`;
document.body.appendChild(panel);
async function loadMore() {
panel.querySelector('#body').innerHTML = '<p>Loading more dates…</p>';
const oldest = byDayDates[byDayDates.length - 1];
const older = await fetchTogglEntries(oldest);
for (const e of older) entriesById.set(e.id, e);
byDayDates = regroup();
render();
}
function render() {
const day = byDayDates[idx];
const descs = byDayLookup()[day];
const total = Object.values(descs).reduce((a,b) => a+b, 0);
const rows = Object.entries(descs)
.map(([desc,sec]) => `<tr><td style="padding:4px 0;">${desc}</td><td style="text-align:right;">${fmt(sec)}</td></tr>`)
.join('');
panel.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<button id="prevBtn" style="cursor:pointer;">◀</button>
<select id="dateSelect" style="flex:1; margin:0 10px;">
${byDayDates.map((d,i) => `<option value="${i}" ${i===idx?'selected':''}>${d} (${weekday(d)})</option>`).join('')}
<option value="loadmore">— Load more… —</option>
</select>
<button id="nextBtn" style="cursor:pointer;">▶</button>
<button id="closeBtn" style="margin-left:10px; cursor:pointer;">✕</button>
</div>
<div id="body">
<table style="width:100%; border-collapse:collapse;">${rows}</table>
<hr style="border-color:#444;">
<div style="display:flex; justify-content:space-between;"><strong>Total</strong><strong>${fmt(total)}</strong></div>
</div>
`;
panel.querySelector('#prevBtn').onclick = () => { idx = Math.max(0, idx-1); render(); };
panel.querySelector('#nextBtn').onclick = () => { idx = Math.min(byDayDates.length-1, idx+1); render(); };
panel.querySelector('#closeBtn').onclick = () => panel.remove();
panel.querySelector('#dateSelect').onchange = e => {
if (e.target.value === 'loadmore') loadMore();
else { idx = Number(e.target.value); render(); }
};
}
render();
}
showTogglPanel();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment