[object Object]

← 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

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 &amp; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
+}
+
+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 →