<!doctype html>
<html lang="en" data-theme="">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Personal Journal</title>
<style>
:root {
--bg: #1a0b1f; /* deep purple-black */
--card: #2a0d2e; /* darker pinkish-purple */
--muted: #c084fc; /* muted lilac for secondary text */
--text: #fdf2f8; /* light pink-white for primary text */
--accent: #ec4899; /* hot pink */
--accent-2: #8b5cf6; /* vibrant purple */
--danger: #ef4444; /* strong red for errors/danger */
--glass: rgba(255, 0, 128, 0.15); /* pink glow */
--glass-2: rgba(139, 92, 246, 0.15); /* purple glow */
--shadow: 0 8px 24px rgba(236, 72, 153, 0.3); /* pink shadow */
--radius: 12px;
--grid-gap: 18px;
--card-padding: 14px;
--compact-gap: 8px;
--ui-gap: 10px;
--small-radius: 8px;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace;
--ui-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
html,body {
height: 100%;
margin: 0;
font-family: var(--ui-font);
background: linear-gradient(180deg, var(--bg) 0%, #2a0d2e 100%);
color: var(--text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* prevent accidental page-level horizontal scrolling caused by tiny rounding differences */
html, body {
overflow-x: hidden;
}
.app {
max-width: 1100px;
margin: 28px auto;
padding: 18px;
display: grid;
grid-template-columns: 360px 1fr;
gap: var(--grid-gap);
}
.compact .app { gap: var(--compact-gap); }
.compact .card { padding: 8px; border-radius: 8px; }
header {
grid-column: 1/-1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
h1 {
font-size: 18px;
margin: 0;
letter-spacing: -0.2px;
}
.sub {
color: var(--muted);
font-size: 13px;
}
.card {
background: var(--card);
box-shadow: var(--shadow);
border-radius: var(--radius);
padding: var(--card-padding);
}
.controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 12px;
border-radius: 10px;
border: none;
cursor: pointer;
font-weight: 600;
background: transparent;
transition: transform .08s ease, box-shadow .12s ease;
white-space: nowrap; /* prevent text inside buttons from wrapping */
}
.btn:active { transform: translateY(1px); }
.btn-primary {
background: linear-gradient(90deg, var(--accent), var(--accent-2));
color: white;
box-shadow: 0 6px 14px rgba(236, 72, 153, 0.25);
}
.btn-secondary {
background: transparent;
border: 1px solid var(--glass-2);
color: var(--text);
}
.btn-ghost {
background: transparent;
color: var(--text);
opacity: 0.95;
}
.btn-large { padding: 12px 16px; font-size: 15px; border-radius: 12px; }
.row { display: flex; gap: 10px; align-items: center; }
.col { display: flex; flex-direction: column; gap: 8px; }
label { font-size: 13px; color: var(--muted); display: block; }
input[type="text"], input[type="time"], input[type="number"], select, textarea {
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid var(--glass-2);
background: transparent;
color: var(--text);
box-sizing: border-box;
font-size: 14px;
}
textarea { min-height: 80px; resize: vertical; padding-top: 10px; }
.right {
display: flex;
flex-direction: column;
gap: var(--grid-gap);
}
.chart-wrap {
display: grid;
grid-template-columns: 1fr 160px;
gap: 12px;
align-items: center;
}
canvas {
width: 100%; height: 140px; display: block;
border-radius: 8px;
background: linear-gradient(180deg, rgba(139,92,246,0.08), transparent);
}
.overview {
padding: 12px;
border-radius: 10px;
background: linear-gradient(180deg, rgba(236,72,153,0.08), transparent);
min-height: 80px;
}
.muted { color: var(--muted); font-size: 13px; }
.table-wrap {
margin-top:10px;
overflow:auto; /* only scroll when strictly necessary */
-webkit-overflow-scrolling: touch;
}
/* Keep table flexible, allow browser to size columns sensibly */
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
table-layout: auto; /* changed from fixed so actions column isn't squeezed */
word-break: break-word;
}
th,td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid var(--glass-2);
vertical-align: top;
overflow-wrap: anywhere;
}
th {
font-weight: 700;
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.6px;
}
tr:hover td { background: linear-gradient(90deg, rgba(236,72,153,0.05), transparent); }
td.notes-cell {
white-space: pre-wrap;
word-break: break-word;
}
.pill {
display: inline-block;
padding: 6px 8px;
border-radius: 999px;
background: var(--glass);
font-weight: 600;
font-size: 13px;
}
.backdrop {
position: fixed;
inset: 0;
background: rgba(10, 0, 20, 0.75);
display: none;
align-items: center;
justify-content: center;
z-index: 9998;
padding: 20px;
}
.backdrop.show { display: flex; }
.modal {
width: min(720px, 98%);
max-height: 90vh;
overflow: auto;
border-radius: 12px;
}
.modal .card { padding: 18px; }
.emoji-grid { display: grid; grid-template-columns: repeat(8,1fr); gap: 8px; }
.emoji-btn { font-size: 18px; padding: 8px; border-radius: 8px; background: var(--glass); border: none; cursor: pointer; }
.small { font-size: 13px; color: var(--muted); }
/* modal actions (kept intentionally separate from table action cell) */
.actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
/* type list */
.type-item { display: flex; gap: 8px; align-items: center; padding: 8px; border-radius: 8px; background: var(--glass); }
.type-list { display: flex; flex-direction: column; gap: 8px; }
.hidden { display: none !important; }
.controls .card { display: flex; flex-direction: column; gap: 12px; }
.flex { display: flex; gap: 8px; align-items: center; }
.spacer { flex: 1; }
.danger { color: var(--danger); font-weight: 700; }
/* ===========================================
ACTIONS CELL: keep buttons horizontally aligned
=========================================== */
/* A dedicated class applied to the <td> that contains action buttons.
Keeps buttons on a single row and prevents them from expanding to full width. */
.actions-cell {
display: inline-flex;
align-items: center;
justify-content: flex-end; /* keep to the right of the cell */
gap: 8px;
white-space: nowrap; /* prevent internal text wrapping */
flex-wrap: nowrap; /* prevent button wrapping to a new line */
padding: 8px; /* consistent padding with other cells */
box-sizing: border-box;
}
/* Prevent buttons inside the actions cell from stretching */
.actions-cell .btn {
flex: 0 0 auto; /* don't grow or shrink; keep intrinsic size */
padding: 6px 8px;
min-width: 40px; /* keeps buttons comfortably tappable on touch screens */
font-size: 13px;
}
/* Apply a modest fixed width to the Actions column so two buttons usually fit.
Adjust the width to taste (increase if your labels are longer). */
.actions-col {
width: 160px;
min-width: 120px;
max-width: 240px;
vertical-align: middle;
overflow: visible;
}
/* Make sure header cell follows same sizing */
th.actions-col { width: 160px; min-width: 120px; }
/* Responsive behavior:
- The table container already has overflow:auto — if viewport is very narrow,
the table will scroll horizontally instead of wrapping the action buttons.
- On very narrow screens we tighten spacing so buttons remain side-by-side when possible.
*/
@media (max-width: 420px) {
/* Keep the action buttons side-by-side, but reduce spacing */
.actions-cell { gap: 6px; }
.actions-cell .btn { padding: 5px 6px; min-width: 36px; font-size: 13px; }
/* If you want icons only on tiny screens you can swap text to icons in JS/rendering */
}
/* responsive layout rules */
@media (max-width:960px) {
.app { grid-template-columns: 1fr; padding: 12px; }
.chart-wrap{ grid-template-columns: 1fr; }
}
footer {
grid-column: 1/-1;
text-align: center;
color: var(--muted);
margin-top: 10px;
font-size: 12px;
}
</style>
</head>
<body>
<div style="max-width:1100px;margin:24px auto;padding:0 18px;">
<div class="app card" id="root">
<header>
<div>
<h1>Local Journal</h1>
<div class="sub">Private, local-only tracking — entries saved to your browser.</div>
</div>
<div class="row">
<button class="btn btn-secondary" id="openSettings">Settings</button>
</div>
</header>
<!-- Left column -->
<div class="controls">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div><strong>Quick Actions</strong><div class="small muted">Add an entry, manage types, or quick-log.</div></div>
</div>
<div class="row">
<button class="btn btn-primary btn-large" id="btnAddEntry">+ Add Entry</button>
<button class="btn btn-secondary" id="btnQuickLog">Quick Log</button>
</div>
<div class="row">
<button class="btn btn-ghost" id="btnManageTypes">Manage Event Types</button>
<button class="btn btn-ghost" id="btnReminders">Reminders</button>
<button class="btn btn-ghost" id="btnExport">Export</button>
</div>
<div style="border-top:1px dashed var(--glass-2);padding-top:10px" class="small muted">
<div><strong id="totalCount">0</strong> total entries</div>
<div id="overviewText" style="margin-top:6px;">No entries yet — add one to get started.</div>
</div>
</div>
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<strong>Event Types</strong>
<div class="small muted">Manage categories used when logging entries.</div>
</div>
<button class="btn btn-secondary" id="btnManageTypes2">Edit</button>
</div>
<div id="typesPreview" style="margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;"></div>
</div>
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<strong>Data</strong>
<div class="small muted">Export, import, or clear all local data.</div>
</div>
</div>
<div class="row">
<button class="btn btn-secondary" id="btnExport2">Export CSV/JSON</button>
<button class="btn btn-ghost" id="btnImport">Import JSON</button>
<button class="btn btn-ghost danger" id="btnClearAll">Clear All</button>
</div>
</div>
</div>
<!-- Right column -->
<div class="right">
<div class="card chart-wrap">
<canvas id="weekChart" aria-label="Weekly entries chart"></canvas>
<div class="overview card" style="padding:12px;">
<div style="display:flex;flex-direction:column;gap:8px;">
<div><strong id="weekTotal">0</strong> entries in last 7 days</div>
<div class="muted" id="topType">—</div>
<div class="muted" id="topMood">—</div>
</div>
</div>
</div>
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between">
<strong>Entries</strong>
<div class="row">
<button class="btn btn-ghost" id="toggleCollapse">Collapse</button>
<button class="btn btn-secondary" id="btnClearEntries">Clear</button>
</div>
</div>
<div class="table-wrap">
<table id="entriesTable">
<thead>
<tr>
<th style="width:160px">Date</th>
<th>Type</th>
<th class="col-mood">Mood</th>
<th class="col-sev">Severity</th>
<th>Duration</th>
<th>Notes</th>
<th class="actions-col">Actions</th>
</tr>
</thead>
<tbody id="entriesBody">
<!-- rows -->
</tbody>
</table>
</div>
</div>
<footer class="muted">All data stays in your browser. Use Export to make backups.</footer>
</div>
</div>
</div>
<!-- Backdrop & Modals -->
<div class="backdrop" id="backdrop" role="dialog" aria-hidden="true">
<!-- Add Entry Modal -->
<div class="modal card hidden" id="modalAddEntry">
<div class="card">
<h3 id="addEntryTitle">Add Entry</h3>
<div class="row">
<div style="flex:1">
<label for="entryDate">Date & time</label>
<input type="datetime-local" id="entryDate" />
</div>
<div style="width:200px">
<label for="entryType">Type</label>
<select id="entryType"></select>
</div>
</div>
<div id="fieldMood" class="col">
<label>Mood</label>
<div style="display:flex;gap:8px;align-items:center;">
<input type="text" id="entryMood" placeholder="Choose emoji or type" />
<button class="btn btn-ghost" id="openEmojiPicker">😀</button>
</div>
<div id="emojiPicker" class="emoji-grid hidden" style="margin-top:8px;"></div>
</div>
<div id="fieldSeverity" class="col">
<label>Severity (1-10)</label>
<input type="number" id="entrySeverity" min="1" max="10" />
</div>
<div class="col">
<label>Duration (minutes)</label>
<input type="number" id="entryDuration" min="0" />
</div>
<div class="col">
<label>Notes</label>
<textarea id="entryNotes" placeholder="Optional notes..."></textarea>
</div>
<div class="actions">
<button class="btn btn-secondary" id="btnClearEntry">Clear</button>
<button class="btn btn-ghost" id="btnCancelEntry">Cancel</button>
<button class="btn btn-primary" id="btnSaveEntry">Save</button>
</div>
</div>
</div>
<!-- Quick Log Modal -->
<div class="modal card hidden" id="modalQuickLog">
<div class="card">
<h3>Quick Log</h3>
<div class="row">
<div style="flex:1">
<label>Time</label>
<input type="datetime-local" id="quickDate" />
</div>
<div style="width:200px">
<label>Type</label>
<select id="quickType"></select>
</div>
</div>
<div id="quickMoodWrap" class="col">
<label>Mood</label>
<input type="text" id="quickMood" />
</div>
<div class="actions">
<button class="btn btn-ghost" id="btnCancelQuick">Cancel</button>
<button class="btn btn-primary" id="btnSaveQuick">Save</button>
</div>
</div>
</div>
<!-- Manage Types Modal -->
<div class="modal card hidden" id="modalManageTypes">
<div class="card">
<h3>Manage Event Types</h3>
<div class="type-list" id="typeList"></div>
<div style="display:flex;gap:8px;margin-top:12px;align-items:center;">
<input type="text" id="newTypeName" placeholder="New type name" />
<label style="display:flex;align-items:center;gap:8px;"><input type="checkbox" id="newTypeAction" /> action</label>
<button class="btn btn-primary" id="btnAddType">Add</button>
</div>
<div class="actions">
<button class="btn btn-ghost" id="btnCloseTypes">Close</button>
</div>
</div>
</div>
<!-- Reminders Modal -->
<div class="modal card hidden" id="modalReminders">
<div class="card">
<h3>Daily Reminder</h3>
<div class="col">
<label>Reminder time</label>
<input type="time" id="reminderTime" />
</div>
<div class="row" style="align-items:center">
<label style="margin-right:8px;">Enable reminders</label>
<input type="checkbox" id="enableReminders" />
</div>
<div class="small muted" id="reminderStatus"></div>
<div class="actions">
<button class="btn btn-ghost" id="btnCancelReminders">Close</button>
<button class="btn btn-primary" id="btnSaveReminders">Save</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal card hidden" id="modalSettings">
<div class="card">
<h3>Settings</h3>
<div class="col">
<label><input type="checkbox" id="settingMood" /> Enable Mood field</label>
<label><input type="checkbox" id="settingSeverity" /> Enable Severity field</label>
<label><input type="checkbox" id="settingReminders" /> Enable Reminders feature</label>
<label><input type="checkbox" id="settingExport" /> Enable Export/Import</label>
<label><input type="checkbox" id="settingCompact" /> Compact UI</label>
</div>
<div style="margin-top:10px" class="small muted">Changes apply immediately and persist in localStorage.</div>
<div class="actions">
<button class="btn btn-ghost" id="btnCloseSettings">Close</button>
</div>
</div>
</div>
<!-- Export Modal -->
<div class="modal card hidden" id="modalExport">
<div class="card">
<h3>Export</h3>
<div class="col">
<label>Export format</label>
<div class="row">
<button class="btn btn-secondary" id="exportCSV">Download CSV</button>
<button class="btn btn-secondary" id="exportJSON">Download JSON</button>
<button class="btn btn-ghost" id="openImportFile">Import JSON</button>
<input type="file" id="importFile" accept="application/json" class="hidden" />
</div>
</div>
<div class="actions">
<button class="btn btn-ghost" id="btnCloseExport">Close</button>
</div>
</div>
</div>
<!-- View Entry Modal -->
<div class="modal card hidden" id="modalViewEntry">
<div class="card">
<h3>View Entry</h3>
<div id="viewEntryBody" class="col"></div>
<div class="actions">
<button class="btn btn-ghost" id="btnCloseView">Close</button>
<button class="btn btn-secondary" id="btnDeleteEntry">Delete</button>
</div>
</div>
</div>
</div>
<script>
/* =========================
VERSIONED STORAGE KEYS
========================= */
const APP_VERSION = 'v1';
const KEYS = {
ENTRIES: `LOCALJOURNAL_${APP_VERSION}_ENTRIES`,
TYPES: `LOCALJOURNAL_${APP_VERSION}_TYPES`,
REMINDER: `LOCALJOURNAL_${APP_VERSION}_REMINDER`,
SETTINGS: `LOCALJOURNAL_${APP_VERSION}_SETTINGS`
};
/* =========================
DEFAULTS
========================= */
const defaultTypes = [
{ id: 'nssi_action', name: 'NSSI — action', action: true },
{ id: 'nssi_ideation', name: 'NSSI — ideation', action: false },
{ id: 'depressive', name: 'Depressive episode', action: false },
{ id: 'suicidal_ideation', name: 'Suicidal ideation', action: false },
{ id: 'suicidal_action', name: 'Suicidal — action', action: true }
];
const DEFAULT_SETTINGS = {
enableMood: true,
enableSeverity: true,
enableReminders: false,
enableExport: true,
compactUI: false
};
const DEFAULT_REMINDER = { time: null, enabled: false };
/* =========================
Persistence helpers
========================= */
function loadJSON(key, fallback) {
try {
const raw = localStorage.getItem(key);
if (!raw) return fallback;
return JSON.parse(raw);
} catch (e) {
console.warn('Failed to parse localStorage key', key, e);
return fallback;
}
}
function saveJSON(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Failed to save', key, e);
}
}
/* =========================
Utilities
========================= */
function esc(s){ // safe HTML insertion
if (s === null || s === undefined) return '';
return String(s)
.replaceAll('&','&')
.replaceAll('<','<')
.replaceAll('>','>')
.replaceAll('"','"')
.replaceAll("'", ''');
}
function isoDate(ts){ return (new Date(ts)).toISOString(); }
function formatShort(ts){
const d = new Date(ts);
return d.toLocaleString([], {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
}
function friendlyDateTime(ts){
const d = new Date(ts);
return d.toLocaleString();
}
function pad(n){ return String(n).padStart(2,'0'); }
function downloadBlob(filename, blob){
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(()=> URL.revokeObjectURL(url), 5000);
}
function isActionType(typeId){
const t = types.find(x => x.id === typeId);
return !!(t && t.action);
}
/* =========================
App state
========================= */
let entries = loadJSON(KEYS.ENTRIES, []);
let types = loadJSON(KEYS.TYPES, defaultTypes);
let settings = loadJSON(KEYS.SETTINGS, DEFAULT_SETTINGS);
let reminder = loadJSON(KEYS.REMINDER, DEFAULT_REMINDER);
let reminderInterval = null;
/* =========================
DOM refs
========================= */
const rootEl = document.getElementById('root');
const weekCanvas = document.getElementById('weekChart');
const entriesBody = document.getElementById('entriesBody');
const totalCountEl = document.getElementById('totalCount');
const overviewText = document.getElementById('overviewText');
const typesPreview = document.getElementById('typesPreview');
const weekTotalEl = document.getElementById('weekTotal');
const topTypeEl = document.getElementById('topType');
const topMoodEl = document.getElementById('topMood');
const backdrop = document.getElementById('backdrop');
/* Modal nodes */
const modalAddEntry = document.getElementById('modalAddEntry');
const modalQuickLog = document.getElementById('modalQuickLog');
const modalManageTypes = document.getElementById('modalManageTypes');
const modalReminders = document.getElementById('modalReminders');
const modalSettings = document.getElementById('modalSettings');
const modalExport = document.getElementById('modalExport');
const modalViewEntry = document.getElementById('modalViewEntry');
/* inputs */
const entryDate = document.getElementById('entryDate');
const entryType = document.getElementById('entryType');
const entryMood = document.getElementById('entryMood');
const entrySeverity = document.getElementById('entrySeverity');
const entryDuration = document.getElementById('entryDuration');
const entryNotes = document.getElementById('entryNotes');
const emojiPicker = document.getElementById('emojiPicker');
const quickDate = document.getElementById('quickDate');
const quickType = document.getElementById('quickType');
const quickMood = document.getElementById('quickMood');
const typeList = document.getElementById('typeList');
const newTypeName = document.getElementById('newTypeName');
const newTypeAction = document.getElementById('newTypeAction');
const reminderTime = document.getElementById('reminderTime');
const enableRemindersInput = document.getElementById('enableReminders');
const reminderStatus = document.getElementById('reminderStatus');
const settingMood = document.getElementById('settingMood');
const settingSeverity = document.getElementById('settingSeverity');
const settingReminders = document.getElementById('settingReminders');
const settingExport = document.getElementById('settingExport');
const settingCompact = document.getElementById('settingCompact');
const entriesTable = document.getElementById('entriesTable');
let isListCollapsed = false;
let currentViewEntryId = null;
/* =========================
Initial setup
========================= */
function init(){
// apply settings UI
applySettingsToForm();
applySettings();
// wire up UI buttons
wireUI();
// draw initial UI
renderTypeSelectors();
renderTypesPreview();
renderEntries();
drawWeekChart();
setupEmojiPicker();
scheduleReminderLoop();
}
function applySettingsToForm(){
settingMood.checked = !!settings.enableMood;
settingSeverity.checked = !!settings.enableSeverity;
settingReminders.checked = !!settings.enableReminders;
settingExport.checked = !!settings.enableExport;
settingCompact.checked = !!settings.compactUI;
// apply reminder form
reminderTime.value = reminder.time || '';
enableRemindersInput.checked = !!(reminder.enabled);
}
function applySettings(){
// Show/hide mood & severity fields in Add Entry modal and Quick Log
const fieldMood = document.getElementById('fieldMood');
const fieldSeverity = document.getElementById('fieldSeverity');
const quickMoodWrap = document.getElementById('quickMoodWrap');
if (settings.enableMood) { fieldMood.classList.remove('hidden'); quickMoodWrap.classList.remove('hidden'); }
else { fieldMood.classList.add('hidden'); quickMoodWrap.classList.add('hidden'); }
if (settings.enableSeverity) fieldSeverity.classList.remove('hidden'); else fieldSeverity.classList.add('hidden');
// Export/Import availability
if (settings.enableExport) {
document.getElementById('btnExport').classList.remove('hidden');
document.getElementById('btnExport2').classList.remove('hidden');
} else {
document.getElementById('btnExport').classList.add('hidden');
document.getElementById('btnExport2').classList.add('hidden');
}
// Reminders
if (settings.enableReminders) {
document.getElementById('btnReminders').classList.remove('hidden');
} else {
document.getElementById('btnReminders').classList.add('hidden');
}
// compact UI
if (settings.compactUI) document.body.classList.add('compact'); else document.body.classList.remove('compact');
// columns in table
document.querySelectorAll('.col-mood').forEach(n => n.style.display = settings.enableMood ? '' : 'none');
document.querySelectorAll('.col-sev').forEach(n => n.style.display = settings.enableSeverity ? '' : 'none');
}
/* =========================
Event wiring
========================= */
function wireUI(){
document.getElementById('btnAddEntry').addEventListener('click', openAddEntry);
document.getElementById('btnQuickLog').addEventListener('click', openQuickLog);
document.getElementById('btnManageTypes').addEventListener('click', openManageTypes);
document.getElementById('btnManageTypes2').addEventListener('click', openManageTypes);
document.getElementById('btnReminders').addEventListener('click', openReminders);
document.getElementById('btnExport').addEventListener('click', openExport);
document.getElementById('btnExport2').addEventListener('click', openExport);
document.getElementById('btnImport').addEventListener('click', ()=> document.getElementById('importFile').click());
document.getElementById('importFile').addEventListener('change', handleImportFile);
document.getElementById('btnClearAll').addEventListener('click', clearAllData);
document.getElementById('btnClearEntries').addEventListener('click', clearEntries);
document.getElementById('toggleCollapse').addEventListener('click', toggleCollapse);
document.getElementById('btnSaveEntry').addEventListener('click', saveEntryFromModal);
document.getElementById('btnCancelEntry').addEventListener('click', hideBackdrop);
document.getElementById('btnSaveQuick').addEventListener('click', saveQuickLog);
document.getElementById('btnCancelQuick').addEventListener('click', hideBackdrop);
document.getElementById('btnAddType').addEventListener('click', addTypeFromInput);
document.getElementById('btnCloseTypes').addEventListener('click', hideBackdrop);
document.getElementById('btnSaveReminders').addEventListener('click', saveReminderSettings);
document.getElementById('btnCancelReminders').addEventListener('click', hideBackdrop);
document.getElementById('openSettings').addEventListener('click', openSettings);
document.getElementById('btnCloseSettings').addEventListener('click', hideBackdrop);
// settings toggles
settingMood.addEventListener('change', ()=> { settings.enableMood = settingMood.checked; saveJSON(KEYS.SETTINGS, settings); applySettings(); });
settingSeverity.addEventListener('change', ()=> { settings.enableSeverity = settingSeverity.checked; saveJSON(KEYS.SETTINGS, settings); applySettings(); });
settingReminders.addEventListener('change', ()=> { settings.enableReminders = settingReminders.checked; saveJSON(KEYS.SETTINGS, settings); applySettings(); scheduleReminderLoop(); });
settingExport.addEventListener('change', ()=> { settings.enableExport = settingExport.checked; saveJSON(KEYS.SETTINGS, settings); applySettings(); });
settingCompact.addEventListener('change', ()=> { settings.compactUI = settingCompact.checked; saveJSON(KEYS.SETTINGS, settings); applySettings(); });
// emoji
document.getElementById('openEmojiPicker').addEventListener('click', ()=> emojiPicker.classList.toggle('hidden'));
// export modal
document.getElementById('exportCSV').addEventListener('click', exportCSV);
document.getElementById('exportJSON').addEventListener('click', exportJSON);
document.getElementById('openImportFile').addEventListener('click', ()=> document.getElementById('importFile').click());
document.getElementById('btnCloseExport').addEventListener('click', hideBackdrop);
// view modal
document.getElementById('btnCloseView').addEventListener('click', hideBackdrop);
document.getElementById('btnDeleteEntry').addEventListener('click', deleteViewedEntry);
// backdrop click closes
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) hideBackdrop();
});
}
/* =========================
UI: Modals open/close
========================= */
function showBackdrop(){
backdrop.classList.add('show');
// set first modal's aria
backdrop.setAttribute('aria-hidden','false');
}
function hideBackdrop(){
backdrop.classList.remove('show');
// hide all modals
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
backdrop.setAttribute('aria-hidden','true');
}
/* =========================
Add / Quick entries
========================= */
function openAddEntry(){
// prefill
entryDate.value = (new Date()).toISOString().slice(0,16);
entryType.innerHTML = '';
renderTypeSelectors(); // fill select
entrySeverity.value = '';
entryDuration.value = '';
entryMood.value = '';
entryNotes.value = '';
document.getElementById('addEntryTitle').textContent = 'Add Entry';
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
modalAddEntry.classList.remove('hidden');
showBackdrop();
}
function saveEntryFromModal(){
const ts = entryDate.value ? new Date(entryDate.value).toISOString() : new Date().toISOString();
const t = entryType.value;
const moodVal = settings.enableMood ? (entryMood.value || '') : '';
const sevVal = settings.enableSeverity ? (entrySeverity.value ? Number(entrySeverity.value) : null) : null;
const dur = entryDuration.value ? Number(entryDuration.value) : null;
const notesVal = entryNotes.value || '';
const entry = {
id: 'e_' + Date.now(),
timestamp: ts,
type: t,
mood: moodVal,
severity: sevVal,
duration: dur,
notes: notesVal
};
entries.unshift(entry);
saveJSON(KEYS.ENTRIES, entries);
renderEntries();
drawWeekChart();
hideBackdrop();
}
function openQuickLog(){
quickDate.value = (new Date()).toISOString().slice(0,16);
quickType.innerHTML = '';
renderTypeSelectors(true);
quickMood.value = '';
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
modalQuickLog.classList.remove('hidden');
showBackdrop();
}
function saveQuickLog(){
const ts = quickDate.value ? new Date(quickDate.value).toISOString() : new Date().toISOString();
const t = quickType.value;
const moodVal = settings.enableMood ? (quickMood.value || '') : '';
const entry = {
id: 'e_' + Date.now(),
timestamp: ts,
type: t,
mood: moodVal,
severity: null, duration: null, notes: ''
};
entries.unshift(entry);
saveJSON(KEYS.ENTRIES, entries);
renderEntries();
drawWeekChart();
hideBackdrop();
}
/* =========================
Render types selectors & preview
========================= */
function renderTypeSelectors(quick=false){
// fill both selects
const selects = quick ? [quickType] : [entryType, quickType];
selects.forEach(sel => {
if (!sel) return;
sel.innerHTML = '';
types.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.name + (t.action ? ' (action)' : '');
sel.appendChild(opt);
});
});
}
function renderTypesPreview(){
typesPreview.innerHTML = '';
types.slice(0,8).forEach(t => {
const d = document.createElement('div');
d.className = 'pill';
d.textContent = t.name;
typesPreview.appendChild(d);
});
}
/* =========================
Manage types
========================= */
function openManageTypes(){
typeList.innerHTML = '';
types.forEach(t => {
const row = document.createElement('div');
row.className = 'type-item';
row.innerHTML = `
<div style="flex:1">
<input type="text" data-id="${esc(t.id)}" class="typeNameInput" value="${esc(t.name)}" />
</div>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" class="typeActionInput" ${t.action ? 'checked' : ''} />
action
</label>
<button class="btn btn-ghost btn-sm typeRename" data-id="${esc(t.id)}">Save</button>
<button class="btn btn-ghost danger typeDelete" data-id="${esc(t.id)}">Delete</button>
`;
typeList.appendChild(row);
// wire inside
row.querySelector('.typeRename').addEventListener('click', ()=>{
const input = row.querySelector('.typeNameInput');
const chk = row.querySelector('.typeActionInput');
const id = input.getAttribute('data-id');
const idx = types.findIndex(x => x.id === id);
if (idx >= 0){
types[idx].name = input.value.trim() || types[idx].name;
types[idx].action = chk.checked;
saveJSON(KEYS.TYPES, types);
renderTypeSelectors();
renderTypesPreview();
renderEntries();
}
});
row.querySelector('.typeDelete').addEventListener('click', ()=>{
const id = row.querySelector('.typeNameInput').getAttribute('data-id');
if (!confirm('Delete type "' + types.find(x=>x.id===id).name + '"? Existing entries will keep their type id.')) return;
types = types.filter(x=>x.id !== id);
saveJSON(KEYS.TYPES, types);
renderTypeSelectors();
renderTypesPreview();
renderEntries();
openManageTypes();
});
});
// prepare new-type inputs
newTypeName.value = '';
newTypeAction.checked = false;
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
modalManageTypes.classList.remove('hidden');
showBackdrop();
}
function addTypeFromInput(){
const nm = newTypeName.value.trim();
if (!nm) return alert('Please provide a type name.');
const id = nm.toLowerCase().replace(/\s+/g,'_').replace(/[^\w\-]/g,'') + '_' + Date.now().toString(36);
const obj = { id, name: nm, action: !!newTypeAction.checked };
types.push(obj);
saveJSON(KEYS.TYPES, types);
renderTypeSelectors();
renderTypesPreview();
addTypeFromInputClear();
openManageTypes(); // refresh
}
function addTypeFromInputClear(){ newTypeName.value=''; newTypeAction.checked=false; }
/* =========================
Entries rendering & actions
========================= */
function renderEntries(){
// sort entries by timestamp desc
entries.sort((a,b)=> new Date(b.timestamp) - new Date(a.timestamp));
entriesBody.innerHTML = '';
entries.forEach(e => {
const tr = document.createElement('tr');
const typeObj = types.find(t=>t.id===e.type) || { name: e.type || 'Unknown', action: false };
const dateTd = document.createElement('td');
dateTd.innerHTML = `<div style="font-weight:700">${esc(formatShort(e.timestamp))}</div><div class="small muted">${esc(new Date(e.timestamp).toLocaleString())}</div>`;
const typeTd = document.createElement('td');
typeTd.innerHTML = `<div>${esc(typeObj.name)}</div>`;
const moodTd = document.createElement('td');
moodTd.className = 'col-mood';
moodTd.textContent = settings.enableMood ? (e.mood || '') : '';
const sevTd = document.createElement('td');
sevTd.className = 'col-sev';
sevTd.textContent = (settings.enableSeverity && isActionType(e.type) && e.severity) ? String(e.severity) : (isActionType(e.type) ? '—' : '');
const durTd = document.createElement('td');
durTd.textContent = e.duration != null ? (String(e.duration) + ' min') : '—';
const notesTd = document.createElement('td');
notesTd.className = 'notes-cell';
notesTd.textContent = e.notes && e.notes.trim() ? e.notes : '—';
// Actions cell: use classes that keep buttons in a single row and prevent stretching
const actTd = document.createElement('td');
actTd.className = 'actions-cell actions-col';
actTd.innerHTML = `
<button class="btn btn-ghost" data-id="${esc(e.id)}" data-action="view" title="View">View</button>
<button class="btn btn-ghost danger" data-id="${esc(e.id)}" data-action="delete" title="Delete">Delete</button>
`;
tr.appendChild(dateTd);
tr.appendChild(typeTd);
tr.appendChild(moodTd);
tr.appendChild(sevTd);
tr.appendChild(durTd);
tr.appendChild(notesTd);
tr.appendChild(actTd);
entriesBody.appendChild(tr);
// wire actions
actTd.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', (ev)=>{
const id = btn.getAttribute('data-id');
const action = btn.getAttribute('data-action');
if (action === 'view') viewEntry(id);
if (action === 'delete') {
if (confirm('Delete this entry?')) {
entries = entries.filter(x=>x.id !== id);
saveJSON(KEYS.ENTRIES, entries);
renderEntries();
drawWeekChart();
}
}
});
});
});
totalCountEl.textContent = entries.length;
overviewText.textContent = entries.length ? `Most recent: ${entries.length? formatShort(entries[0].timestamp) : '—'}` : 'No entries yet — add one to get started.';
renderTypesPreview();
}
/* =========================
View Entry modal
========================= */
function viewEntry(id){
const ent = entries.find(x => x.id === id);
if (!ent) return alert('Entry not found.');
currentViewEntryId = id;
const typeObj = types.find(t => t.id === ent.type) || { name: ent.type || 'Unknown', action:false };
const wrap = document.getElementById('viewEntryBody');
wrap.innerHTML = `
<div><strong>Date</strong><div class="small muted">${esc(friendlyDateTime(ent.timestamp))}</div></div>
<div style="margin-top:8px"><strong>Type</strong><div class="small muted">${esc(typeObj.name)}</div></div>
${settings.enableMood ? `<div style="margin-top:8px"><strong>Mood</strong><div class="small muted">${esc(ent.mood||'—')}</div></div>` : ''}
${settings.enableSeverity && typeObj.action ? `<div style="margin-top:8px"><strong>Severity</strong><div class="small muted">${esc(ent.severity||'—')}</div></div>` : ''}
<div style="margin-top:8px"><strong>Duration</strong><div class="small muted">${ent.duration != null ? esc(String(ent.duration) + ' min') : '—'}</div></div>
<div style="margin-top:8px"><strong>Notes</strong><div class="small muted">${ent.notes ? esc(ent.notes) : '—'}</div></div>
`;
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
modalViewEntry.classList.remove('hidden');
showBackdrop();
}
function deleteViewedEntry(){
if (!currentViewEntryId) return;
if (!confirm('Delete this entry?')) return;
entries = entries.filter(x => x.id !== currentViewEntryId);
saveJSON(KEYS.ENTRIES, entries);
currentViewEntryId = null;
renderEntries();
drawWeekChart();
hideBackdrop();
}
/* =========================
Chart: weekly bar chart
========================= */
function drawWeekChart(){
const ctx = weekCanvas.getContext('2d');
const DPR = window.devicePixelRatio || 1;
const W = weekCanvas.clientWidth;
const H = weekCanvas.clientHeight;
weekCanvas.width = Math.floor(W * DPR);
weekCanvas.height = Math.floor(H * DPR);
ctx.scale(DPR, DPR);
// compute last 7 days (including today)
const counts = [];
const labels = [];
const today = new Date();
for (let i = 6; i >= 0; i--){
const d = new Date(today);
d.setDate(today.getDate() - i);
const start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0,0,0,0);
const end = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23,59,59,999);
const c = entries.filter(e => {
const t = new Date(e.timestamp);
return t >= start && t <= end;
}).length;
counts.push(c);
labels.push(d.toLocaleDateString([], {weekday:'short'}).slice(0,3));
}
// clear
ctx.clearRect(0,0,W,H);
// visual params
const padding = 18;
const barGap = 10;
const maxVal = Math.max(1, ...counts);
const barAreaW = W - padding*2;
const barW = (barAreaW - (counts.length - 1)*barGap) / counts.length;
const chartH = H - padding*2;
// colors from CSS variables
const barColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#2563eb';
const barColor2 = getComputedStyle(document.documentElement).getPropertyValue('--accent-2').trim() || '#10b981';
const textColor = getComputedStyle(document.documentElement).getPropertyValue('--muted').trim() || '#6b7280';
// draw grid lines
ctx.strokeStyle = 'rgba(0,0,0,0.06)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let row=0; row<=3; row++){
const y = padding + (chartH * row / 3);
ctx.moveTo(padding, y);
ctx.lineTo(W - padding, y);
}
ctx.stroke();
// draw bars
counts.forEach((val, i) => {
const x = padding + i * (barW + barGap);
const h = (val / maxVal) * (chartH - 20);
const y = padding + (chartH - h) - 6;
// gradient
const grad = ctx.createLinearGradient(x, y, x, y + h);
grad.addColorStop(0, barColor);
grad.addColorStop(1, barColor2);
ctx.fillStyle = grad;
// rounded rect
const r = 6;
roundRect(ctx, x, y, barW, h, r);
ctx.fill();
// label (count)
ctx.fillStyle = textColor;
ctx.font = '600 12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(String(val), x + barW / 2, y - 8);
// day label
ctx.font = '12px system-ui, sans-serif';
ctx.fillText(labels[i], x + barW / 2, padding + chartH + 14);
});
// summary stats
const last7 = counts.reduce((a,b)=>a+b,0);
weekTotalEl.textContent = last7;
// top type
const since = new Date();
since.setDate(since.getDate() - 6);
const recent = entries.filter(e => new Date(e.timestamp) >= new Date(since.getFullYear(), since.getMonth(), since.getDate(),0,0,0));
const typeCounts = {};
recent.forEach(r => typeCounts[r.type] = (typeCounts[r.type] || 0) + 1);
let topType = '—';
if (Object.keys(typeCounts).length){
const topId = Object.keys(typeCounts).sort((a,b)=> typeCounts[b]-typeCounts[a])[0];
const tObj = types.find(t=>t.id===topId);
topType = tObj ? `${tObj.name} (${typeCounts[topId]})` : `${topId} (${typeCounts[topId]})`;
}
topTypeEl.textContent = topType;
// top mood
const moodCounts = {};
recent.forEach(r => { if (r.mood) moodCounts[r.mood] = (moodCounts[r.mood]||0)+1; });
let topMood = '—';
if (Object.keys(moodCounts).length){
const m = Object.keys(moodCounts).sort((a,b)=> moodCounts[b]-moodCounts[a])[0];
topMood = `${m} (${moodCounts[m]})`;
}
topMoodEl.textContent = topMood;
}
function roundRect(ctx, x, y, w, h, r){
const radius = Math.min(r, w/2, h/2);
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + w, y, x + w, y + h, radius);
ctx.arcTo(x + w, y + h, x, y + h, radius);
ctx.arcTo(x, y + h, x, y, radius);
ctx.arcTo(x, y, x + w, y, radius);
ctx.closePath();
}
/* =========================
Reminders
========================= */
function openReminders(){
reminderTime.value = reminder.time || '';
enableRemindersInput.checked = !!reminder.enabled;
reminderStatus.textContent = `Current saved: ${reminder.time || 'none'} (enabled: ${reminder.enabled ? 'yes' : 'no'})`;
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
modalReminders.classList.remove('hidden');
showBackdrop();
}
async function saveReminderSettings(){
const t = reminderTime.value || null;
const en = !!enableRemindersInput.checked;
reminder.time = t;
reminder.enabled = en;
saveJSON(KEYS.REMINDER, reminder);
settings.enableReminders = en;
saveJSON(KEYS.SETTINGS, settings);
applySettingsToForm();
applySettings();
scheduleReminderLoop();
hideBackdrop();
if (en){
// request permission if necessary
if (Notification && Notification.permission !== 'granted') {
try {
await Notification.requestPermission();
} catch(e){}
}
}
}
function scheduleReminderLoop(){
if (reminderInterval) {
clearInterval(reminderInterval);
reminderInterval = null;
}
// only run if feature is enabled in settings and reminder.enabled
if (!settings.enableReminders || !reminder.enabled || !reminder.time) return;
// Do a check every 60 seconds (aligned to minute)
checkReminderNow(); // immediate check
reminderInterval = setInterval(checkReminderNow, 60 * 1000);
}
let lastReminderTrigger = null;
function checkReminderNow(){
if (!reminder.time || !reminder.enabled) return;
const now = new Date();
const hh = pad(now.getHours());
const mm = pad(now.getMinutes());
const cur = `${hh}:${mm}`;
if (cur !== reminder.time) return;
// avoid repeat triggers within the same minute
if (lastReminderTrigger === cur) return;
lastReminderTrigger = cur;
// show notification if permission
if (window.Notification && Notification.permission === 'granted') {
new Notification('Reminder', { body: 'Time to log an entry', silent: false });
} else {
// fallback: small on-screen prompt
alert('Reminder: time to log an entry.');
}
}
/* =========================
Export / Import
========================= */
function csvEscape(val){
if (val === null || val === undefined) return '';
const s = String(val).replace(/\r?\n/g,' ');
if (s.includes(',') || s.includes('"')) {
return '"' + s.replaceAll('"','""') + '"';
}
return s;
}
function exportCSV(){
if (!settings.enableExport) return alert('Export is disabled in Settings.');
const header = ['timestamp','type','mood','severity','duration','notes'];
const rows = entries.map(e => [e.timestamp, e.type, e.mood || '', e.severity ?? '', e.duration ?? '', e.notes || '']);
const csv = [header.join(',')].concat(rows.map(r => r.map(csvEscape).join(','))).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
downloadBlob('local-journal-export.csv', blob);
}
function exportJSON(){
if (!settings.enableExport) return alert('Export is disabled in Settings.');
const blob = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json;charset=utf-8' });
downloadBlob('local-journal-export.json', blob);
}
function handleImportFile(e){
const file = e.target.files && e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try{
const parsed = JSON.parse(ev.target.result);
if (!Array.isArray(parsed)) return alert('Invalid JSON: expected an array of entries.');
// basic validation
const good = parsed.filter(p => p.timestamp && p.type);
if (!good.length) return alert('No valid entries found.');
// ask user whether to append or replace
if (!confirm(`Import ${good.length} entries. Click OK to append to existing entries.`)) return;
// ensure ids
const normalized = good.map(x=>{
return {
id: x.id || ('e_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2,8)),
timestamp: x.timestamp,
type: x.type,
mood: x.mood || '',
severity: x.severity ?? null,
duration: x.duration ?? null,
notes: x.notes || ''
};
});
entries = entries.concat(normalized);
saveJSON(KEYS.ENTRIES, entries);
renderEntries();
drawWeekChart();
alert('Import complete.');
}catch(err){
console.error(err);
alert('Failed to import JSON: ' + err.message);
} finally {
e.target.value = '';
}
};
reader.readAsText(file);
}
/* =========================
Clearing
========================= */
function clearAllData(){
if (!confirm('Clear all app data (entries, types, settings, reminders)? This cannot be undone.')) return;
localStorage.removeItem(KEYS.ENTRIES);
localStorage.removeItem(KEYS.TYPES);
localStorage.removeItem(KEYS.SETTINGS);
localStorage.removeItem(KEYS.REMINDER);
entries = [];
types = defaultTypes.slice();
settings = DEFAULT_SETTINGS;
reminder = DEFAULT_REMINDER;
applySettingsToForm();
applySettings();
renderTypeSelectors();
renderEntries();
drawWeekChart();
alert('All data cleared.');
}
function clearEntries(){
if (!confirm('Clear all saved entries?')) return;
entries = [];
saveJSON(KEYS.ENTRIES, entries);
renderEntries();
drawWeekChart();
}
/* =========================
Export modal & import
========================= */
function openExport(){
if (!settings.enableExport) return alert('Export is disabled in settings.');
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
modalExport.classList.remove('hidden');
showBackdrop();
}
/* =========================
Settings modal
========================= */
function openSettings(){
applySettingsToForm();
document.querySelectorAll('.modal').forEach(m=>m.classList.add('hidden'));
modalSettings.classList.remove('hidden');
showBackdrop();
}
/* =========================
Emoji picker
========================= */
const EMOJIS = [
// Saddest / Negative
'😭','😢','😞','😟','🙁','☹️',
'😣','😖','😫','😩',
'😠','😡','🤬',
// Worried / Fearful
'😨','😰','😱','😧',
// Neutral / Ambiguous
'😶','😐','😑','🙄','😬','🤔','🤨',
// Surprised / Shocked
'😯','😮','😲','😳','😵','🤯',
// Slightly Positive
'🙂','😊','😌','😉',
// Happy / Laughing
'😃','😄','😁','😆','😂','🤣',
// Playful / Excited
'😋','😜','🤪','😝','🤩','🥳'
]
function setupEmojiPicker(){
emojiPicker.innerHTML = '';
EMOJIS.forEach(e=>{
const b = document.createElement('button');
b.className = 'emoji-btn';
b.textContent = e;
b.title = e;
b.addEventListener('click', ()=> {
entryMood.value = e;
quickMood.value = e;
emojiPicker.classList.add('hidden');
});
emojiPicker.appendChild(b);
});
}
/* =========================
Toggle collapse list
========================= */
function toggleCollapse(){
isListCollapsed = !isListCollapsed;
entriesBody.parentElement.style.display = isListCollapsed ? 'none' : '';
document.getElementById('toggleCollapse').textContent = isListCollapsed ? 'Expand' : 'Collapse';
}
/* =========================
Import/Export helpers in settings
========================= */
function handleImportJsonText(jsonText){
try{
const parsed = JSON.parse(jsonText);
if (!Array.isArray(parsed)) throw new Error('Expected array');
entries = entries.concat(parsed.map(x => ({
id: x.id || ('e_' + Date.now().toString(36) + Math.random().toString(36).slice(2,5)),
timestamp: x.timestamp,
type: x.type,
mood: x.mood || '',
severity: x.severity ?? null,
duration: x.duration ?? null,
notes: x.notes || ''
})));
saveJSON(KEYS.ENTRIES, entries);
renderEntries();
drawWeekChart();
alert('Imported ' + parsed.length + ' entries.');
}catch(err){
alert('Import failed: ' + err.message);
}
}
/* =========================
Init: ensure types set
========================= */
if (!Array.isArray(types) || types.length === 0){
types = defaultTypes.slice();
saveJSON(KEYS.TYPES, types);
}
if (!settings || typeof settings !== 'object'){ settings = DEFAULT_SETTINGS; saveJSON(KEYS.SETTINGS, settings); }
if (!reminder || typeof reminder !== 'object'){ reminder = DEFAULT_REMINDER; saveJSON(KEYS.REMINDER, reminder); }
init();
/* =========================
Extra: Save settings watchers
========================= */
window.addEventListener('beforeunload', ()=> {
saveJSON(KEYS.ENTRIES, entries);
saveJSON(KEYS.TYPES, types);
saveJSON(KEYS.SETTINGS, settings);
saveJSON(KEYS.REMINDER, reminder);
});
/* =========================
Other UI wiring for convenience
========================= */
document.getElementById('btnExport2').addEventListener('click', openExport);
document.getElementById('btnExport').addEventListener('click', openExport);
document.getElementById('btnQuickLog').addEventListener('click', openQuickLog);
/* === Handle imported file from sidebar input === */
document.getElementById('importFile').addEventListener('change', handleImportFile);
</script>
</body>
</html>
/* Add your styles here */
// Add your code here
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Local Journal — Complete User Guide</title>
<style>
:root{
--bg:#0f1724; --card:#0b1220; --muted:#94a3b8; --text:#e6eef8; --accent:#7c3aed;
--glass: rgba(124,58,237,0.06);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
html,body{margin:0;height:100%;background:linear-gradient(180deg,#071029 0%, #07121d 100%);color:var(--text);}
.wrap{max-width:1000px;margin:28px auto;padding:20px;}
header{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
h1{margin:0;font-size:20px}
.card{background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);border-radius:10px;padding:14px;box-shadow:0 8px 24px rgba(0,0,0,0.6);margin-top:14px}
nav {display:flex; gap:8px; flex-wrap:wrap}
a.btn {padding:8px 10px;border-radius:8px;background:var(--accent);color:white;text-decoration:none;font-weight:600}
.muted{color:var(--muted);font-size:13px}
.toc {display:grid;grid-template-columns: 1fr 220px; gap:12px}
ul {margin:0;padding-left:18px}
pre {background:#031126;padding:12px;border-radius:8px;overflow:auto}
table{width:100%;border-collapse:collapse;margin-top:8px}
th,td{padding:8px;border-bottom:1px solid rgba(255,255,255,0.04);text-align:left;font-family:monospace;font-size:13px}
.id {background:var(--glass);padding:4px 6px;border-radius:6px;font-family:monospace;display:inline-block}
.kbd{background:#071730;padding:2px 6px;border-radius:4px;border:1px solid rgba(255,255,255,0.02);font-family:monospace}
.search{margin-top:8px;display:flex;gap:8px}
input[type="search"]{flex:1;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--text)}
.example {background:linear-gradient(90deg, rgba(124,58,237,0.06), transparent);padding:10px;border-radius:8px}
footer{margin-top:18px;color:var(--muted);font-size:13px}
details summary{cursor:pointer}
.warn{color:#fecaca}
.ok{color:#bbf7d0}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Local Journal — Complete User Guide</h1>
<div class="muted">Reference & walkthrough for the UI in your app (based on your uploaded file). :contentReference[oaicite:1]{index=1}</div>
</header>
<div class="card">
<nav>
<a class="btn" href="#quickstart">Quick start</a>
<a class="btn" href="#ui-map">UI map</a>
<a class="btn" href="#modals">Modals</a>
<a class="btn" href="#data">Data & Storage</a>
<a class="btn" href="#tips">Tips & Troubleshooting</a>
</nav>
<div style="margin-top:12px" class="muted">
This guide documents every control, id, and behavior in the web app so you (or another developer) can use or extend it reliably.
</div>
</div>
<section id="quickstart" class="card">
<h2>Quick start — common tasks</h2>
<ol>
<li><strong>Add a full entry</strong>
<ul>
<li>Click <span class="id">+ Add Entry</span> (ID: <span class="id">btnAddEntry</span>).</li>
<li>In the modal: set <span class="id">Date & time</span> (<span class="id">entryDate</span>), choose a <span class="id">Type</span> (<span class="id">entryType</span>), optionally set <span class="id">Mood</span> (<span class="id">entryMood</span>), <span class="id">Severity</span> (<span class="id">entrySeverity</span>), <span class="id">Duration</span> (<span class="id">entryDuration</span>), and enter <span class="id">Notes</span> (<span class="id">entryNotes</span>).</li>
<li>Click <span class="id">Save</span> (ID: <span class="id">btnSaveEntry</span>).</li>
</ul>
</li>
<li><strong>Quick log</strong>
<ul>
<li>Click <span class="id">Quick Log</span> (ID: <span class="id">btnQuickLog</span>) to add a minimal entry (time + type + optional mood).</li>
<li>Click <span class="id">Save</span> (ID: <span class="id">btnSaveQuick</span>).</li>
</ul>
</li>
<li><strong>View or delete an entry</strong>
<ul>
<li>In the entries table click <span class="id">View</span> or <span class="id">Delete</span> actions for a row (table body ID: <span class="id">entriesBody</span>).</li>
<li>Viewing opens <span class="id">View Entry</span> modal (ID: <span class="id">modalViewEntry</span>); delete confirms and removes from storage.</li>
</ul>
</li>
<li><strong>Export / import</strong>
<ul>
<li>Open Export via <span class="id">Export</span> button (IDs: <span class="id">btnExport</span>, <span class="id">btnExport2</span>) and choose CSV or JSON (IDs: <span class="id">exportCSV</span>, <span class="id">exportJSON</span>).</li>
<li>To import JSON: either use the import file input (ID: <span class="id">importFile</span>) or the import button inside the export modal (<span class="id">openImportFile</span>).</li>
</ul>
</li>
</ol>
</section>
<section id="ui-map" class="card">
<h2>UI map — controls, IDs and what they do</h2>
<p class="muted">This list maps every visible control to its DOM id and the precise behavior implemented in the app.</p>
<h3>Left column — Quick actions & data</h3>
<table>
<tr><th>Label</th><th>ID / selector</th><th>Behavior</th></tr>
<tr><td>+ Add Entry</td><td class="id">#btnAddEntry</td><td>Opens Add Entry modal (<span class="id">#modalAddEntry</span>) prefilled with current datetime.</td></tr>
<tr><td>Quick Log</td><td class="id">#btnQuickLog</td><td>Opens Quick Log modal (<span class="id">#modalQuickLog</span>).</td></tr>
<tr><td>Manage Event Types</td><td class="id">#btnManageTypes, #btnManageTypes2</td><td>Opens Types manager (ID: <span class="id">#modalManageTypes</span>) where types can be edited, added, or deleted.</td></tr>
<tr><td>Reminders</td><td class="id">#btnReminders</td><td>Opens the Reminders modal (ID: <span class="id">#modalReminders</span>). Feature gated by setting <span class="id">settingReminders</span>.</td></tr>
<tr><td>Export / Import</td><td class="id">#btnExport, #btnExport2, #btnImport, #importFile</td><td>Open export modal or open file dialog for JSON import.</td></tr>
<tr><td>Clear All</td><td class="id">#btnClearAll</td><td>Removes all saved localStorage keys after a confirmation prompt.</td></tr>
</table>
<h3>Right column — Chart & Entries</h3>
<table>
<tr><th>Element</th><th>ID</th><th>Behavior / Notes</th></tr>
<tr><td>Weekly chart canvas</td><td class="id">#weekChart</td><td>Custom-drawn bar chart summarizing last 7 days; updates after each add/import/delete.</td></tr>
<tr><td>Entries table</td><td class="id">#entriesTable / #entriesBody</td><td>Shows all saved entries (newest first). Each row has action buttons wired to view/delete.</td></tr>
<tr><td>Collapse / Clear</td><td class="id">#toggleCollapse, #btnClearEntries</td><td>Toggle hides table body or clears only entries (not types/settings).</td></tr>
</table>
<h3>Modals (IDs)</h3>
<ul>
<li><span class="id">#modalAddEntry</span> — Add Entry</li>
<li><span class="id">#modalQuickLog</span> — Quick Log</li>
<li><span class="id">#modalManageTypes</span> — Manage Event Types</li>
<li><span class="id">#modalReminders</span> — Reminders</li>
<li><span class="id">#modalSettings</span> — Settings</li>
<li><span class="id">#modalExport</span> — Export / Import</li>
<li><span class="id">#modalViewEntry</span> — View a single entry</li>
</ul>
<h3>Settings toggles (IDs)</h3>
<p class="muted">These are the persistent settings available in the Settings modal (will be saved to localStorage):</p>
<table>
<tr><th>Toggle</th><th>ID</th><th>What it does</th></tr>
<tr><td>Enable Mood field</td><td class="id">#settingMood</td><td>Show/hide mood inputs in Add Entry and Quick Log.</td></tr>
<tr><td>Enable Severity field</td><td class="id">#settingSeverity</td><td>Show/hide severity input; severity is intended for 'action' types.</td></tr>
<tr><td>Enable Reminders</td><td class="id">#settingReminders</td><td>Controls visibility and scheduling of reminders (uses Notifications API if available).</td></tr>
<tr><td>Enable Export/Import</td><td class="id">#settingExport</td><td>Shows/hides Export buttons.</td></tr>
<tr><td>Compact UI</td><td class="id">#settingCompact</td><td>Toggles compact layout CSS class on body.</td></tr>
</table>
</section>
<section id="modals" class="card">
<h2>Modal details — fields, validation, and flow</h2>
<h3>Add Entry modal (<span class="id">#modalAddEntry</span>)</h3>
<div class="example">
<strong>Fields:</strong>
<ul>
<li><span class="id">entryDate</span> — datetime-local, defaults to now.</li>
<li><span class="id">entryType</span> — select populated from saved types.</li>
<li><span class="id">entryMood</span> — text input, emoji picker available (<span class="id">openEmojiPicker</span> / <span class="id">emojiPicker</span>).</li>
<li><span class="id">entrySeverity</span> — number (1–10).</li>
<li><span class="id">entryDuration</span> — number (minutes).</li>
<li><span class="id">entryNotes</span> — free text.</li>
</ul>
<strong>Save behavior:</strong> click <span class="id">#btnSaveEntry</span>, an entry object is created and prepended to the entries array and persisted to localStorage under the entries key.
</div>
<h3>Manage Event Types (<span class="id">#modalManageTypes</span>)</h3>
<p class="muted">Types are objects with <code>id</code>, <code>name</code>, and <code>action</code> boolean. Action types are treated specially (severity relevance, labeling).</p>
<ul>
<li>Rename: edit the input and click the local Save button next to that type.</li>
<li>Delete: removes type from the types list; existing entries that reference the type keep their raw type id (no automatic remap).</li>
<li>Add new type: use <span class="id">#newTypeName</span>, check <span class="id">#newTypeAction</span> if it's an action, then click <span class="id">#btnAddType</span>.</li>
</ul>
<details>
<summary>Developer note: how new type id is generated</summary>
The id is a sanitized slug plus a timestamp chunk, so collisions are extremely unlikely. Example generation logic uses `Date.now()` and `.toString(36)`.
</details>
<h3>Reminders (<span class="id">#modalReminders</span>)</h3>
<p>Set a time (HH:MM) and enable reminders. When enabled, the app checks every minute. If Notifications permission is granted it will show a Notification; otherwise it falls back to an <code>alert()</code>.</p>
<p class="warn">Browser tabs may be suspended when backgrounded; reminders rely on the tab being active or the browser providing background wakeups. Do not rely on this for urgent alerts.</p>
<h3>Export / Import (<span class="id">#modalExport</span>)</h3>
<ul>
<li>CSV export: downloads a CSV file with headers: <code>timestamp,type,mood,severity,duration,notes</code>.</li>
<li>JSON export: downloads the raw entries array as JSON.</li>
<li>Import: expects an array of entry objects. The importer validates presence of <code>timestamp</code> and <code>type</code>, and will prompt you to append (default) instead of replacing.</li>
</ul>
</section>
<section id="data" class="card">
<h2>Data model & storage</h2>
<p class="muted">All persistent data is saved to <code>localStorage</code> under versioned keys. Changing <code>APP_VERSION</code> in the app script will effectively isolate data to a new set of keys.</p>
<h3>Keys</h3>
<table>
<tr><th>Key constant</th><th>localStorage key</th><th>Contents</th></tr>
<tr><td>ENTRIES</td><td class="id">LOCALJOURNAL_v1_ENTRIES</td><td>Array of entry objects</td></tr>
<tr><td>TYPES</td><td class="id">LOCALJOURNAL_v1_TYPES</td><td>Array of type objects</td></tr>
<tr><td>REMINDER</td><td class="id">LOCALJOURNAL_v1_REMINDER</td><td>Object {time,enabled}</td></tr>
<tr><td>SETTINGS</td><td class="id">LOCALJOURNAL_v1_SETTINGS</td><td>Object of UI toggles</td></tr>
</table>
<h3>Entry object shape</h3>
<pre>{
"id": "e_1678abcd",
"timestamp": "2025-10-01T14:30:00.000Z",
"type": "nssi_ideation",
"mood": "😞",
"severity": 4,
"duration": 15,
"notes": "Contextual notes..."
}</pre>
<h3>Safety & privacy</h3>
<p class="muted">All data is stored locally — no network requests — but files created by Export can be shared. If you clear localStorage using the Clear All button, data cannot be recovered unless you exported it earlier.</p>
</section>
<section id="tips" class="card">
<h2>Tips, troubleshooting & developer notes</h2>
<h3>Common troubleshooting</h3>
<ul>
<li><strong>Entries not saving?</strong> Check that your browser allows localStorage for the page and that you're not in a strict private window that disables storage.</li>
<li><strong>Import shows validation error</strong> — the importer expects an <code>Array</code> at the root and each item must have <code>timestamp</code> and <code>type</code>. If your JSON uses a different shape, convert it before import.</li>
<li><strong>Notifications don't appear</strong> — ensure you granted Notification permission for the site and the tab is open (browsers may restrict background notifications for untrusted origins).</li>
<li><strong>Severity not shown for some entries</strong> — severity is displayed only when the selected type is marked <em>action</em>. See the Types manager.</li>
</ul>
<h3>Advanced usage & extension points (for developers)</h3>
<ol>
<li><strong>Change storage versioning</strong> — update <code>APP_VERSION</code> constant to isolate new data sets.</li>
<li><strong>Custom export:</strong> The export functions use a simple CSV/JSON blob download. Hook <code>exportCSV()</code> or <code>exportJSON()</code> to pipe data elsewhere (e.g., to a server) if you add auth/networking.</li>
<li><strong>Type remapping:</strong> Deleting a type leaves existing entries referencing the old id. A migration routine could map or normalize legacy ids after a type rename/delete.</li>
<li><strong>Tests & automation:</strong> Many DOM hooks are id-based and simple to target in integration tests (Cypress / Playwright). Key hooks: <span class="id">#btnSaveEntry</span>, <span class="id">#importFile</span>, <span class="id">#entriesBody</span>.</li>
</ol>
<h3>Keyboard & accessibility notes</h3>
<p class="muted">The app uses standard inputs and buttons but does not currently implement custom keyboard shortcuts. Modals are shown/hidden by toggling a backdrop element which has ARIA attributes set; further accessibility improvements could include focus trapping and ARIA labels on action buttons.</p>
<h3>Safety: content note</h3>
<p class="warn">The default type list in the app includes sensitive categories (e.g., self-harm related types). If you or your users are collecting highly sensitive information, ensure appropriate safeguards: exit paths, disclaimers, or professional support links. The app does not transmit data off-device.</p>
</section>
<section class="card">
<h2>Quick index of selectors</h2>
<p class="muted">Copy/paste friendly ID list for developers and testers.</p>
<pre>
#btnAddEntry
#btnQuickLog
#btnManageTypes
#btnReminders
#btnExport, #btnExport2
#importFile
#entriesTable, #entriesBody
#weekChart
#modalAddEntry, #modalQuickLog, #modalManageTypes, #modalReminders, #modalSettings, #modalExport, #modalViewEntry
#entryDate, #entryType, #entryMood, #entrySeverity, #entryDuration, #entryNotes
#quickDate, #quickType, #quickMood
#newTypeName, #newTypeAction, #btnAddType
#reminderTime, #enableReminders
#settingMood, #settingSeverity, #settingReminders, #settingExport, #settingCompact
</pre>
</section>
<footer>
<div class="muted">Generated guide — intended to be a comprehensive developer & user reference for the application contained in your uploaded file. If you'd like, I can:
<ul>
<li>Embed this guide into your app as a help panel or a route like <code>/help</code>.</li>
<li>Generate a printable PDF version.</li>
<li>Produce concise "Quick Tips" HTML suitable for new users (2-page cheat sheet).</li>
</ul>
</div>
</footer>
</div>
</body>
</html>