← back to Ventura Corridor

linkedin-activity-ledger.html

464 lines

<!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>