12 KiB
P44 OCR Viewer — War diary landing page (p44-landing.html) Create a new standalone file p44-landing.html in the same directory as p44-ocr-viewer.html. Served by launch_viewer.py on the same localhost server. No frameworks, vanilla JS only.
PART 1 — Data config At the top of the script, define the three war diaries as a static array. Progress is calculated from localStorage keys written by the viewer: javascriptconst DIARIES = [ { id: 'calgary-highlanders', title: 'Calgary Highlanders', subtitle: 'War Diary — September 1944', tags: ['Infantry', 'Sep 1944', '343 pages'], totalPages: 343, viewerUrl: 'p44-ocr-viewer.html', flagsKey: 'p44-flags', pass2FlagsKey: 'p44-pass2-flags', }, { id: 'black-watch', title: 'Black Watch (RHR) of Canada', subtitle: 'War Diary — September 1944', tags: ['Infantry', 'Sep 1944', '211 pages'], totalPages: 211, viewerUrl: 'p44-ocr-viewer.html', flagsKey: 'p44-flags', pass2FlagsKey: 'p44-pass2-flags', }, { id: '5cib', title: '5th Canadian Infantry Brigade', subtitle: 'War Diary — September 1944', tags: ['Brigade HQ', 'Sep 1944', '178 pages'], totalPages: 178, viewerUrl: 'p44-ocr-viewer.html', flagsKey: 'p44-flags', pass2FlagsKey: 'p44-pass2-flags', }, ]; Note: All three diaries currently share the same localStorage keys since the viewer only supports one diary at a time. This is a known limitation — flagsKey and pass2FlagsKey are included in the config so they're easy to make per-diary later when the server handles multiple diaries.
PART 2 — Progress calculation javascriptfunction getProgress(diary) { let pass1Done = 0, pass2Done = 0;
try { const flags = JSON.parse(localStorage.getItem(diary.flagsKey) || '{}'); pass1Done = Object.values(flags).filter(f => f === 'done').length; } catch (_) {}
try { const pass2Flags = JSON.parse(localStorage.getItem(diary.pass2FlagsKey) || '{}'); pass2Done = Object.values(pass2Flags).filter(f => f === 'done').length; } catch (_) {}
return { pass1Pct: Math.round((pass1Done / diary.totalPages) * 100), pass2Pct: Math.round((pass2Done / diary.totalPages) * 100), pass1Done, pass2Done, }; }
PART 3 — Role detection javascriptfunction getCurrentRole() { return localStorage.getItem('p44-role') || 'pass1'; }
PART 4 — Card renderer javascriptfunction renderCard(diary) { const role = getCurrentRole(); const prog = getProgress(diary); const pass1Complete = prog.pass1Pct === 100; const isViewOnly = role === 'pass1' && pass1Complete;
const tagHtml = diary.tags.map(t => '' + t + '' ).join('');
const actionBtn = isViewOnly ? 'View only' : 'Open diary';
return <div class="card" id="card-${diary.id}"> <div class="card-top"> <div class="thumb" aria-hidden="true"> <div class="thumb-lines"> ${Array(10).fill(0).map((_, i) => '<div class="thumb-line' + (i % 2 ? ' dim' : '') + '"></div>' ).join('')} </div> </div> <div class="card-meta"> <div class="card-title">${diary.title}</div> <div class="card-sub">${diary.subtitle}</div> <div class="card-tags">${tagHtml}</div> </div> </div> <div class="card-progress"> <div class="prog-row"> <span class="prog-label p1">Pass 1</span> <div class="prog-track"> <div class="prog-fill p1" style="width:${prog.pass1Pct}%"></div> </div> <span class="prog-pct p1">${prog.pass1Pct}%</span> </div> <div class="prog-row"> <span class="prog-label p2">Pass 2</span> <div class="prog-track"> <div class="prog-fill p2" style="width:${prog.pass2Pct}%"></div> </div> <span class="prog-pct p2">${prog.pass2Pct}%</span> </div> </div> <div class="card-action">${actionBtn}</div> </div>;
}
PART 5 — Open diary javascriptfunction openDiary(diaryId) { const diary = DIARIES.find(d => d.id === diaryId); if (!diary) return; window.location.href = diary.viewerUrl; }
PART 6 — Init javascriptfunction init() { const role = getCurrentRole(); document.getElementById('role-badge').textContent = role === 'pass2' ? 'Pass 2 — Senior Editor' : 'Pass 1 — Volunteer';
const container = document.getElementById('cards'); container.innerHTML = DIARIES.map(renderCard).join(''); }
document.addEventListener('DOMContentLoaded', init);
PART 7 — Full HTML file html
<html lang="en"> <head> <style> *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }body { background: #111120; font-family: 'Georgia', 'Times New Roman', serif; color: #ddd4bb; min-height: 100vh; }
#topbar { background: #16213e; border-bottom: 1px solid #2e2e4e; padding: 10px 24px; display: flex; align-items: center; gap: 12px; }
#topbar h1 { font-size: 13px; letter-spacing: .12em; color: #c8a84b; text-transform: uppercase; }
#role-badge { font-size: 9px; padding: 2px 8px; border: 1px solid #8a6f2e; border-radius: 3px; color: #8a6f2e; letter-spacing: .06em; margin-left: auto; }
#content { max-width: 720px; margin: 0 auto; padding: 28px 24px; }
.section-label { font-size: 9px; letter-spacing: .12em; text-transform: uppercase; color: #8a6f2e; margin-bottom: 16px; }
#cards { display: flex; flex-direction: column; gap: 12px; }
.card { background: #16213e; border: 0.5px solid #2e2e4e; border-radius: 6px; padding: 16px 18px; cursor: pointer; transition: border-color .15s; }
.card:hover { border-color: #c8a84b; }
.card-top { display: flex; align-items: flex-start; gap: 14px; }
.thumb { width: 56px; height: 72px; background: #0f2a0f; border: 0.5px solid #2e4e2e; border-radius: 3px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.thumb-lines { width: 40px; }
.thumb-line { height: 2px; background: #2e4e2e; border-radius: 1px; margin-bottom: 3px; }
.thumb-line.dim { background: #1e3e1e; }
.card-meta { flex: 1; min-width: 0; }
.card-title { font-size: 13px; color: #e8d9b0; letter-spacing: .03em; margin-bottom: 4px; }
.card-sub { font-size: 10px; color: #8a6f2e; letter-spacing: .05em; margin-bottom: 10px; }
.card-tags { display: flex; gap: 6px; flex-wrap: wrap; }
.tag { font-size: 9px; padding: 2px 7px; border-radius: 3px; letter-spacing: .05em; background: #0f1a3e; color: #6a8fd8; border: 0.5px solid #2e3e6e; }
.card-progress { margin-top: 14px; border-top: 0.5px solid #2e2e4e; padding-top: 12px; }
.prog-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
.prog-row:last-child { margin-bottom: 0; }
.prog-label { font-size: 9px; letter-spacing: .08em; text-transform: uppercase; width: 52px; flex-shrink: 0; }
.prog-label.p1 { color: #c8a84b; } .prog-label.p2 { color: #6a8fd8; }
.prog-track { flex: 1; height: 5px; background: #0f1a0f; border-radius: 3px; overflow: hidden; }
.prog-fill { height: 100%; border-radius: 3px; transition: width .3s ease; } .prog-fill.p1 { background: #c8a84b; } .prog-fill.p2 { background: #378ADD; }
.prog-pct { font-size: 10px; width: 32px; text-align: right; flex-shrink: 0; }
.prog-pct.p1 { color: #c8a84b; } .prog-pct.p2 { color: #6a8fd8; }
.card-action { margin-top: 12px; }
.btn-open, .btn-view { padding: 4px 14px; border-radius: 3px; font-size: 10px; font-family: 'Georgia', serif; letter-spacing: .05em; cursor: pointer; background: transparent; }
.btn-open { border: 1px solid #c8a84b; color: #c8a84b; }
.btn-open:hover { background: rgba(200,168,75,.08); }
.btn-view { border: 1px solid #5f5e5a; color: #888780; }
.btn-view:hover { background: rgba(136,135,128,.08); }
#footer { text-align: center; padding: 20px; font-size: 10px; color: #5f5e5a; letter-spacing: .06em; } </style>
</head>⚜ P44 War Diary Transcription
Pass 1 — VolunteerVERIFICATION CHECKLIST:
Page loads at http://localhost:XXXX/p44-landing.html Three diary cards visible with correct titles and tags Role badge in top right reflects p44-role from localStorage Pass 1 progress bar reads from p44-flags — counts pages with flag done Pass 2 progress bar reads from p44-pass2-flags — counts pages with flag done Diary with Pass 1 at 100% shows "View only" button for Pass 1 role All other diaries show "Open diary" button Clicking "Open diary" or "View only" navigates to p44-ocr-viewer.html Pass 2 role always sees "Open diary" on all cards regardless of progress Footer shows CRMA credit