Live relative-time counters via shared [data-ts] ticker in layout foot (#46)
CI / test (push) Successful in 20s
CI / docker (push) Skipped

This commit is contained in:
2026-09-08 21:41:29 -05:00
parent b3112d2a35
commit ebcf7eec80
6 changed files with 27 additions and 6 deletions
BIN
View File
Binary file not shown.
+3 -2
View File
@@ -127,6 +127,7 @@ func (a *apiServer) renderPaste(w http.ResponseWriter, row *PasteRow, justCreate
"LineCount": lines, "LineCount": lines,
"SizeBytes": len(row.Content), "SizeBytes": len(row.Content),
"CreatedAgo": agoString(row.CreatedAt), "CreatedAgo": agoString(row.CreatedAt),
"CreatedAtUnix": row.CreatedAt,
"ViewCount": row.ViewCount, "ViewCount": row.ViewCount,
"Visibility": row.Visibility, "Visibility": row.Visibility,
"ExpiresAt": row.ExpiresAt.Valid, "ExpiresAt": row.ExpiresAt.Valid,
@@ -179,13 +180,13 @@ func (a *apiServer) handlePasteView(w http.ResponseWriter, r *http.Request) {
a.renderPaste(w, row, false, "") a.renderPaste(w, row, false, "")
return return
} }
renderPage(w, "unlock.html", map[string]any{"Page": "unlock", "ID": row.ID, "Wrong": true, "CreatedAgo": agoString(row.CreatedAt)}) renderPage(w, "unlock.html", map[string]any{"Page": "unlock", "ID": row.ID, "Wrong": true, "CreatedAgo": agoString(row.CreatedAt), "CreatedAtUnix": row.CreatedAt})
return return
} }
// check cookie // check cookie
c, err := r.Cookie("pw_" + row.ID) c, err := r.Cookie("pw_" + row.ID)
if err != nil || c.Value != "1" { if err != nil || c.Value != "1" {
renderPage(w, "unlock.html", map[string]any{"Page": "unlock", "ID": row.ID, "Wrong": false, "CreatedAgo": agoString(row.CreatedAt)}) renderPage(w, "unlock.html", map[string]any{"Page": "unlock", "ID": row.ID, "Wrong": false, "CreatedAgo": agoString(row.CreatedAt), "CreatedAtUnix": row.CreatedAt})
return return
} }
} }
+21 -1
View File
@@ -1 +1,21 @@
{{define "foot"}}{{end}} {{define "foot"}}<script>
// live relative-time counters (#46): tick any [data-ts] (epoch seconds) every second
(function () {
function fmt(ts) {
const s = Math.max(0, Math.floor(Date.now() / 1000) - ts);
if (s < 60) return s + 's ago';
if (s < 3600) return Math.floor(s / 60) + 'm ago';
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
return Math.floor(s / 86400) + 'd ago';
}
function tick() {
document.querySelectorAll('[data-ts]').forEach(el => {
const ts = parseInt(el.dataset.ts, 10);
if (!isNaN(ts)) el.textContent = fmt(ts);
});
}
setInterval(tick, 1000);
document.addEventListener('DOMContentLoaded', tick);
tick();
})();
</script>{{end}}
+1 -1
View File
@@ -118,7 +118,7 @@ async function load() {
`<tr class="row" data-href="/${esc(it.id)}"><td><a class="slug" href="/${esc(it.id)}">${esc(it.id)}</a>` + `<tr class="row" data-href="/${esc(it.id)}"><td><a class="slug" href="/${esc(it.id)}">${esc(it.id)}</a>` +
(it.title ? `<div class="paste-sub">${esc(it.title)}</div>` : `<div class="paste-sub dim">none</div>`) + `</td>` + (it.title ? `<div class="paste-sub">${esc(it.title)}</div>` : `<div class="paste-sub dim">none</div>`) + `</td>` +
`<td><span class="badge">${esc(it.language || 'text')}</span></td>` + `<td><span class="badge">${esc(it.language || 'text')}</span></td>` +
`<td class="dim">${fmtSize(it.size)}</td><td class="dim">${it.view_count}</td><td class="dim">${ago(it.created_at)}</td>` + `<td class="dim">${fmtSize(it.size)}</td><td class="dim">${it.view_count}</td><td class="dim" data-ts="${it.created_at}">${ago(it.created_at)}</td>` +
(it.custom_slug ? `<td><a class="slug url-link" href="/${esc(it.custom_slug)}">/${esc(it.custom_slug)}</a></td>` : `<td class="dim">none</td>`) + (it.custom_slug ? `<td><a class="slug url-link" href="/${esc(it.custom_slug)}">/${esc(it.custom_slug)}</a></td>` : `<td class="dim">none</td>`) +
`<td class="dim"><a class="id-link" href="/${esc(it.id)}">${esc(it.id)}</a></td></tr>` `<td class="dim"><a class="id-link" href="/${esc(it.id)}">${esc(it.id)}</a></td></tr>`
).join(''); ).join('');
+1 -1
View File
@@ -22,7 +22,7 @@
<span class="stats-k">Language</span><span class="stats-v">{{if .Language}}{{.Language}}{{else}}text{{end}}</span> <span class="stats-k">Language</span><span class="stats-v">{{if .Language}}{{.Language}}{{else}}text{{end}}</span>
<span class="stats-k">Size</span><span class="stats-v">{{.SizeHuman}} ({{.LineCount}} lines)</span> <span class="stats-k">Size</span><span class="stats-v">{{.SizeHuman}} ({{.LineCount}} lines)</span>
<span class="stats-k">Views</span><span class="stats-v">{{.ViewCount}}</span> <span class="stats-k">Views</span><span class="stats-v">{{.ViewCount}}</span>
<span class="stats-k">Created</span><span class="stats-v">{{.CreatedAgo}}</span> <span class="stats-k">Created</span><span class="stats-v" data-ts="{{.CreatedAtUnix}}">{{.CreatedAgo}}</span>
{{if .ExpiresAt}}<span class="stats-k">Expires</span><span class="stats-v">in {{.ExpiresIn}}</span>{{end}} {{if .ExpiresAt}}<span class="stats-k">Expires</span><span class="stats-v">in {{.ExpiresIn}}</span>{{end}}
<span class="stats-k">Password</span><span class="stats-v">{{if .HasPassword}}protected{{else}}none{{end}}</span> <span class="stats-k">Password</span><span class="stats-v">{{if .HasPassword}}protected{{else}}none{{end}}</span>
{{if .BurnAfterRead}}<span class="stats-k">Burn</span><span class="stats-v">burn after read</span>{{end}} {{if .BurnAfterRead}}<span class="stats-k">Burn</span><span class="stats-v">burn after read</span>{{end}}
+1 -1
View File
@@ -15,7 +15,7 @@
<button class="btn" type="submit">Unlock</button> <button class="btn" type="submit">Unlock</button>
</form> </form>
</div> </div>
<div class="foot">Created {{.CreatedAgo}}</div> <div class="foot">Created <span data-ts="{{.CreatedAtUnix}}">{{.CreatedAgo}}</span></div>
</div> </div>
</div> </div>
<script> <script>