← 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 & 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>