← back to Ventura Corridor
Add LinkedIn activity ledger — quick-entry log with stats, filters, CSV export
c6254fa7c964fbaa542f0c940f55e557ac644df6 · 2026-06-23 15:30:05 -0700 · Steve
Files touched
A linkedin-activity-ledger.html
Diff
commit c6254fa7c964fbaa542f0c940f55e557ac644df6
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 23 15:30:05 2026 -0700
Add LinkedIn activity ledger — quick-entry log with stats, filters, CSV export
---
linkedin-activity-ledger.html | 463 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 463 insertions(+)
diff --git a/linkedin-activity-ledger.html b/linkedin-activity-ledger.html
new file mode 100644
index 0000000..609a712
--- /dev/null
+++ b/linkedin-activity-ledger.html
@@ -0,0 +1,463 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>LinkedIn Activity Ledger — DW</title>
+<style>
+ :root {
+ --navy: #0D1B2A;
+ --navy2: #162336;
+ --navy3: #1e2f45;
+ --gold: #C9A84C;
+ --gold2: #e4c06e;
+ --white: #F5F0E8;
+ --muted: #8a9bb0;
+ --green: #2ecc71;
+ --red: #e74c3c;
+ --blue: #3498db;
+ --purple: #9b59b6;
+ --orange: #e67e22;
+ --teal: #1abc9c;
+ }
+ * { box-sizing: border-box; margin: 0; padding: 0; }
+ body { background: var(--navy); color: var(--white); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; min-height: 100vh; }
+
+ header {
+ background: var(--navy2);
+ border-bottom: 2px solid var(--gold);
+ padding: 16px 24px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ header h1 { font-size: 1.1rem; color: var(--gold); letter-spacing: 0.05em; text-transform: uppercase; }
+ header .subtitle { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }
+
+ .stats-bar {
+ background: var(--navy2);
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid #1e2f45;
+ overflow-x: auto;
+ }
+ .stat {
+ flex: 1;
+ min-width: 100px;
+ padding: 12px 16px;
+ text-align: center;
+ border-right: 1px solid #1e2f45;
+ }
+ .stat:last-child { border-right: none; }
+ .stat .val { font-size: 1.6rem; font-weight: 700; color: var(--gold); }
+ .stat .lbl { font-size: 0.65rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 2px; }
+ .stat.today .val { color: var(--green); }
+ .stat.week .val { color: var(--blue); }
+
+ .entry-panel {
+ background: var(--navy2);
+ border-bottom: 1px solid var(--navy3);
+ padding: 16px 24px;
+ }
+ .entry-panel h2 { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 12px; }
+ .entry-row {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ }
+ .entry-row select,
+ .entry-row input[type="text"] {
+ background: var(--navy3);
+ border: 1px solid #2a3f5f;
+ color: var(--white);
+ padding: 10px 14px;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ outline: none;
+ }
+ .entry-row select:focus,
+ .entry-row input[type="text"]:focus { border-color: var(--gold); }
+ #actType { min-width: 160px; }
+ #actWho { flex: 1; min-width: 200px; }
+ #actNotes { flex: 2; min-width: 250px; }
+ .btn-log {
+ background: var(--gold);
+ color: var(--navy);
+ border: none;
+ padding: 10px 24px;
+ border-radius: 6px;
+ font-weight: 700;
+ font-size: 0.9rem;
+ cursor: pointer;
+ letter-spacing: 0.04em;
+ white-space: nowrap;
+ }
+ .btn-log:hover { background: var(--gold2); }
+ .entry-row label { font-size: 0.7rem; color: var(--muted); display: block; margin-bottom: 4px; }
+ .entry-field { display: flex; flex-direction: column; }
+
+ .controls {
+ padding: 12px 24px;
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ background: var(--navy);
+ border-bottom: 1px solid var(--navy3);
+ flex-wrap: wrap;
+ }
+ .filter-btn {
+ background: var(--navy2);
+ border: 1px solid #2a3f5f;
+ color: var(--muted);
+ padding: 6px 14px;
+ border-radius: 20px;
+ font-size: 0.78rem;
+ cursor: pointer;
+ transition: all 0.15s;
+ }
+ .filter-btn.active, .filter-btn:hover { background: var(--navy3); color: var(--white); border-color: var(--gold); }
+ .controls-right { margin-left: auto; display: flex; gap: 8px; }
+ .btn-sm {
+ background: transparent;
+ border: 1px solid #2a3f5f;
+ color: var(--muted);
+ padding: 6px 14px;
+ border-radius: 6px;
+ font-size: 0.78rem;
+ cursor: pointer;
+ }
+ .btn-sm:hover { border-color: var(--gold); color: var(--white); }
+ .btn-danger { border-color: #5a1a1a; color: #e74c3c; }
+ .btn-danger:hover { background: #3a0a0a; border-color: var(--red); }
+
+ #ledger-table {
+ width: 100%;
+ border-collapse: collapse;
+ }
+ #ledger-table th {
+ background: var(--navy2);
+ color: var(--muted);
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ padding: 10px 16px;
+ text-align: left;
+ border-bottom: 1px solid var(--navy3);
+ position: sticky;
+ top: 0;
+ }
+ #ledger-table td {
+ padding: 11px 16px;
+ font-size: 0.85rem;
+ border-bottom: 1px solid #162336;
+ vertical-align: middle;
+ }
+ #ledger-table tr:hover td { background: var(--navy2); }
+ .table-wrap { overflow-x: auto; max-height: calc(100vh - 380px); overflow-y: auto; }
+
+ .badge {
+ display: inline-block;
+ padding: 3px 10px;
+ border-radius: 12px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+ .badge-like { background: #1a3a5c; color: #60a8e0; }
+ .badge-comment { background: #2a1a4a; color: #b07de0; }
+ .badge-follow { background: #1a3a2a; color: #5eda9a; }
+ .badge-inmail { background: #3a2a0a; color: #e0a020; }
+ .badge-reply { background: #1a3a3a; color: #40c0c0; }
+ .badge-call { background: #3a1a1a; color: #e07070; }
+ .badge-connect { background: #2a2a1a; color: #c0c040; }
+ .badge-other { background: #2a2a2a; color: #a0a0a0; }
+
+ .ts { color: var(--muted); font-size: 0.75rem; }
+ .notes-cell { color: var(--muted); font-size: 0.8rem; max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+ .who-cell { font-weight: 500; }
+ .del-btn { background: none; border: none; color: #3a1a1a; cursor: pointer; font-size: 0.9rem; padding: 2px 6px; border-radius: 4px; }
+ .del-btn:hover { color: var(--red); background: #2a0a0a; }
+
+ .empty-state {
+ text-align: center;
+ padding: 60px 24px;
+ color: var(--muted);
+ }
+ .empty-state .big { font-size: 2.5rem; margin-bottom: 12px; }
+ .empty-state p { font-size: 0.9rem; }
+
+ .flash {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ background: var(--green);
+ color: #0a2a0a;
+ padding: 10px 20px;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ opacity: 0;
+ transition: opacity 0.2s;
+ pointer-events: none;
+ z-index: 100;
+ }
+ .flash.show { opacity: 1; }
+
+ @media (max-width: 600px) {
+ .entry-row { flex-direction: column; }
+ .stats-bar { flex-wrap: wrap; }
+ .stat { min-width: 50%; }
+ }
+</style>
+</head>
+<body>
+
+<header>
+ <div>
+ <h1>LinkedIn Activity Ledger</h1>
+ <div class="subtitle">Designer Wallcoverings — All manual activity tracked here</div>
+ </div>
+ <div style="text-align:right;">
+ <div style="font-size:0.8rem;color:var(--gold)" id="hdr-date"></div>
+ <div style="font-size:0.7rem;color:var(--muted)" id="hdr-time"></div>
+ </div>
+</header>
+
+<div class="stats-bar">
+ <div class="stat today"><div class="val" id="s-today">0</div><div class="lbl">Today</div></div>
+ <div class="stat week"><div class="val" id="s-week">0</div><div class="lbl">This Week</div></div>
+ <div class="stat"><div class="val" id="s-likes">0</div><div class="lbl">Likes</div></div>
+ <div class="stat"><div class="val" id="s-comments">0</div><div class="lbl">Comments</div></div>
+ <div class="stat"><div class="val" id="s-follows">0</div><div class="lbl">Follows</div></div>
+ <div class="stat"><div class="val" id="s-inmails">0</div><div class="lbl">InMails</div></div>
+ <div class="stat"><div class="val" id="s-replies">0</div><div class="lbl">Replies</div></div>
+ <div class="stat"><div class="val" id="s-calls">0</div><div class="lbl">Calls</div></div>
+</div>
+
+<div class="entry-panel">
+ <h2>Log Activity</h2>
+ <div class="entry-row">
+ <div class="entry-field">
+ <label>Type</label>
+ <select id="actType">
+ <option value="like">👍 Like</option>
+ <option value="comment">💬 Comment</option>
+ <option value="follow">➕ Follow</option>
+ <option value="inmail">📧 InMail Sent</option>
+ <option value="reply">↩️ Reply Received</option>
+ <option value="call">📞 Call</option>
+ <option value="connect">🤝 Connection Request</option>
+ <option value="other">• Other</option>
+ </select>
+ </div>
+ <div class="entry-field" style="flex:1;min-width:180px">
+ <label>Person / Company</label>
+ <input type="text" id="actWho" placeholder="Name or company" />
+ </div>
+ <div class="entry-field" style="flex:2;min-width:220px">
+ <label>Notes (optional)</label>
+ <input type="text" id="actNotes" placeholder="What you said, what happened, outcome..." />
+ </div>
+ <button class="btn-log" onclick="logActivity()">+ Log</button>
+ </div>
+</div>
+
+<div class="controls">
+ <button class="filter-btn active" onclick="setFilter('all', this)">All</button>
+ <button class="filter-btn" onclick="setFilter('like', this)">👍 Likes</button>
+ <button class="filter-btn" onclick="setFilter('comment', this)">💬 Comments</button>
+ <button class="filter-btn" onclick="setFilter('follow', this)">➕ Follows</button>
+ <button class="filter-btn" onclick="setFilter('inmail', this)">📧 InMails</button>
+ <button class="filter-btn" onclick="setFilter('reply', this)">↩️ Replies</button>
+ <button class="filter-btn" onclick="setFilter('call', this)">📞 Calls</button>
+ <div class="controls-right">
+ <button class="btn-sm" onclick="exportCSV()">Export CSV</button>
+ <button class="btn-sm btn-danger" onclick="clearAll()">Clear All</button>
+ </div>
+</div>
+
+<div class="table-wrap">
+ <table id="ledger-table">
+ <thead>
+ <tr>
+ <th style="width:150px">Date & Time</th>
+ <th style="width:130px">Type</th>
+ <th>Person / Company</th>
+ <th>Notes</th>
+ <th style="width:40px"></th>
+ </tr>
+ </thead>
+ <tbody id="ledger-body">
+ </tbody>
+ </table>
+ <div class="empty-state" id="empty-state">
+ <div class="big">📋</div>
+ <p>No activities logged yet.<br>Use the form above to log your first LinkedIn action.</p>
+ </div>
+</div>
+
+<div class="flash" id="flash"></div>
+
+<script>
+const STORE_KEY = 'dw_li_ledger';
+
+function getEntries() {
+ try { return JSON.parse(localStorage.getItem(STORE_KEY) || '[]'); } catch(e) { return []; }
+}
+function saveEntries(arr) {
+ localStorage.setItem(STORE_KEY, JSON.stringify(arr));
+}
+
+function badgeClass(type) {
+ const map = { like:'badge-like', comment:'badge-comment', follow:'badge-follow',
+ inmail:'badge-inmail', reply:'badge-reply', call:'badge-call',
+ connect:'badge-connect', other:'badge-other' };
+ return map[type] || 'badge-other';
+}
+function badgeLabel(type) {
+ const map = { like:'Like', comment:'Comment', follow:'Follow', inmail:'InMail',
+ reply:'Reply', call:'Call', connect:'Connect', other:'Other' };
+ return map[type] || type;
+}
+
+function fmtDate(iso) {
+ const d = new Date(iso);
+ return d.toLocaleString(undefined, { month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
+}
+
+let currentFilter = 'all';
+
+function setFilter(type, el) {
+ currentFilter = type;
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
+ el.classList.add('active');
+ render();
+}
+
+function render() {
+ const entries = getEntries();
+ const filtered = currentFilter === 'all' ? entries : entries.filter(e => e.type === currentFilter);
+ const tbody = document.getElementById('ledger-body');
+ const empty = document.getElementById('empty-state');
+
+ if (filtered.length === 0) {
+ tbody.innerHTML = '';
+ empty.style.display = 'block';
+ } else {
+ empty.style.display = 'none';
+ tbody.innerHTML = filtered.slice().reverse().map((e, i) => {
+ const realIdx = entries.indexOf(e);
+ return `<tr>
+ <td class="ts">${fmtDate(e.ts)}</td>
+ <td><span class="badge ${badgeClass(e.type)}">${badgeLabel(e.type)}</span></td>
+ <td class="who-cell">${esc(e.who || '—')}</td>
+ <td class="notes-cell" title="${esc(e.notes || '')}">${esc(e.notes || '')}</td>
+ <td><button class="del-btn" onclick="deleteEntry(${realIdx})" title="Delete">✕</button></td>
+ </tr>`;
+ }).join('');
+ }
+
+ updateStats(entries);
+}
+
+function updateStats(entries) {
+ const now = new Date();
+ const todayStr = now.toDateString();
+ const weekAgo = new Date(now - 7 * 86400000);
+
+ const count = t => entries.filter(e => e.type === t).length;
+ const todayCount = entries.filter(e => new Date(e.ts).toDateString() === todayStr).length;
+ const weekCount = entries.filter(e => new Date(e.ts) >= weekAgo).length;
+
+ document.getElementById('s-today').textContent = todayCount;
+ document.getElementById('s-week').textContent = weekCount;
+ document.getElementById('s-likes').textContent = count('like');
+ document.getElementById('s-comments').textContent = count('comment');
+ document.getElementById('s-follows').textContent = count('follow');
+ document.getElementById('s-inmails').textContent = count('inmail');
+ document.getElementById('s-replies').textContent = count('reply');
+ document.getElementById('s-calls').textContent = count('call');
+}
+
+function logActivity() {
+ const type = document.getElementById('actType').value;
+ const who = document.getElementById('actWho').value.trim();
+ const notes = document.getElementById('actNotes').value.trim();
+
+ const entry = { ts: new Date().toISOString(), type, who, notes };
+ const entries = getEntries();
+ entries.push(entry);
+ saveEntries(entries);
+
+ document.getElementById('actWho').value = '';
+ document.getElementById('actNotes').value = '';
+ document.getElementById('actWho').focus();
+
+ render();
+ flash('Logged ✓');
+}
+
+function deleteEntry(idx) {
+ const entries = getEntries();
+ entries.splice(idx, 1);
+ saveEntries(entries);
+ render();
+}
+
+function clearAll() {
+ if (confirm('Clear ALL activity log entries? This cannot be undone.')) {
+ localStorage.removeItem(STORE_KEY);
+ render();
+ }
+}
+
+function exportCSV() {
+ const entries = getEntries();
+ if (!entries.length) { alert('No entries to export.'); return; }
+ const header = 'Date,Time,Type,Person/Company,Notes';
+ const rows = entries.map(e => {
+ const d = new Date(e.ts);
+ const date = d.toLocaleDateString();
+ const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ return [date, time, e.type, e.who || '', e.notes || ''].map(v => `"${String(v).replace(/"/g,'""')}"`).join(',');
+ });
+ const csv = [header, ...rows].join('\n');
+ const blob = new Blob([csv], { type: 'text/csv' });
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = `linkedin-activity-${new Date().toISOString().slice(0,10)}.csv`;
+ a.click();
+}
+
+function flash(msg) {
+ const el = document.getElementById('flash');
+ el.textContent = msg;
+ el.classList.add('show');
+ setTimeout(() => el.classList.remove('show'), 1800);
+}
+
+function esc(s) {
+ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
+}
+
+function updateClock() {
+ const now = new Date();
+ document.getElementById('hdr-date').textContent = now.toLocaleDateString(undefined, { weekday:'long', month:'long', day:'numeric' });
+ document.getElementById('hdr-time').textContent = now.toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' });
+}
+
+// Enter key to log
+document.addEventListener('keydown', e => {
+ if (e.key === 'Enter' && (e.target.id === 'actWho' || e.target.id === 'actNotes')) {
+ logActivity();
+ }
+});
+
+updateClock();
+setInterval(updateClock, 30000);
+render();
+</script>
+</body>
+</html>
← ff0ed97 Add LinkedIn pitch library — posts, comment openers, InMails
·
back to Ventura Corridor
·
Add LinkedIn marketing calendar — 33 content pieces: 5 Event 10268df →