Created
July 20, 2026 01:58
-
-
Save mark05e/d8328551c85497211316e9f43511fd98 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // --- 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