<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Emotion</title>
<style>
:root{
--bg:#f6f7fb; --card:#fff; --text:#222; --muted:#666; --accent:#4e6ef2;
--danger:#c0392b; --success:#2d9f6a;
}
[data-theme="dark"]{
--bg:#0f1115; --card:#111316; --text:#e7ecef; --muted:#b6c0cc; --accent:#7f98ff;
}
*{box-sizing:border-box}
body{
margin:0; font-family:Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background:var(--bg); color:var(--text); padding:28px;
}
/* Header */
header{display:flex;align-items:center;gap:16px;justify-content:space-between;max-width:1200px;margin:0 auto 22px}
h1{font-size:20px;margin:0}
.controls{display:flex;gap:10px;align-items:center}
button, .btn {
background:var(--accent); color:#fff; border:0; padding:10px 14px; border-radius:10px; cursor:pointer; font-weight:600;
}
.btn.secondary{background:transparent;color:var(--accent);border:1px solid rgba(0,0,0,0.06)}
.btn.ghost{background:transparent;border:1px dashed rgba(0,0,0,0.06);color:var(--muted)}
.btn.large { padding:14px 18px; font-size:15px; border-radius:12px; }
.card{background:var(--card); padding:18px; border-radius:14px; box-shadow:0 10px 30px rgba(25,30,40,0.05); margin-bottom:16px; max-width:1200px; margin-left:auto;margin-right:auto;}
.muted{color:var(--muted);font-size:13px}
/* layout */
.layout{display:grid;grid-template-columns: 1fr 460px; gap:18px; max-width:1200px;margin:0 auto;}
@media(max-width:980px){ .layout{grid-template-columns:1fr} header{flex-direction:column;align-items:flex-start} }
/* simplified main panel */
.main-actions { display:flex;flex-direction:column;gap:12px; align-items:flex-start; }
.action-row { display:flex; gap:12px; flex-wrap:wrap; }
.small-muted{font-size:12px;color:var(--muted)}
.pill{padding:8px 12px;border-radius:999px;border:1px solid rgba(0,0,0,0.06);background:transparent;font-size:13px}
/* entries table (less dense) */
.entries-table{width:100%;border-collapse:collapse;margin-top:12px}
.entries-table th, .entries-table td{padding:12px;text-align:left;border-bottom:1px solid #eef1f6;font-size:14px}
.entries-table th{background:rgba(0,0,0,0.02);position:sticky;top:0}
.emoji{font-size:20px;line-height:1}
/* modals */
.modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1200;align-items:center;justify-content:center;padding:20px}
.modal{background:var(--card);border-radius:12px;box-shadow:0 12px 40px rgba(5,10,20,0.45);max-width:740px;width:100%;padding:18px;max-height:90vh;overflow:auto}
.modal .modal-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
.modal .modal-body{margin-top:12px;display:grid;gap:12px}
label{display:block;font-size:13px;color:var(--muted); margin-bottom:6px}
input[type="text"], input[type="number"], input[type="time"], select, textarea {
width:100%; padding:10px; border-radius:10px; border:1px solid #e3e6ee; font-size:15px; background:transparent;color:var(--text)
}
textarea{min-height:110px;resize:vertical}
.emoji-grid{display:flex;gap:10px;flex-wrap:wrap}
.emoji-btn{font-size:22px;padding:8px;border-radius:10px;border:1px solid transparent;background:transparent;cursor:pointer}
.emoji-btn.selected{box-shadow:inset 0 -4px 0 rgba(0,0,0,0.06);border-color:rgba(0,0,0,0.06);transform:translateY(-1px)}
.hr{height:1px;background:linear-gradient(90deg, transparent, rgba(0,0,0,0.04), transparent);margin:12px 0;border-radius:2px}
/* small helpers */
.muted-block{padding:10px;border-radius:10px;background:rgba(0,0,0,0.02);font-size:13px;color:var(--muted)}
.right{margin-left:auto}
</style>
</head>
<body>
<header class="card">
<div>
<h1>Emotion & Safety Tracker</h1>
<div class="muted">This emotional tracker is developed to be fully local (serverless), private and easy to use. This is developed to be downloaded and used locally.</div>
</div>
<div class="controls">
<button id="openAddEntry" class="btn large">Add Entry</button>
<button id="quickLogBtn" class="btn secondary">Quick Log</button>
<button id="openFilters" class="btn ghost">Filters</button>
<button id="openSettings" class="btn ghost">Settings</button>
<button id="toggleTheme" class="btn ghost">Dark</button>
<button id="exportCsvBtn" class="btn secondary">Export CSV</button>
<label class="btn secondary" for="importJsonFile" style="margin:0">Import JSON<input type="file" id="importJsonFile" accept=".json"></label>
</div>
</header>
<div class="layout">
<!-- left: main actions + short info -->
<div>
<div class="card main-actions">
<div class="action-row">
<button id="manageTypesBtn" class="btn">Manage Categories</button>
<button id="openReminders" class="btn secondary">Reminders</button>
<button id="crisisInfoBtn" class="btn ghost">Immediate Help</button>
</div>
<div class="hr"></div>
<div class="muted-block">
<strong>Quick overview</strong>
<div style="margin-top:8px" id="overviewText">No entries yet.</div>
<div style="margin-top:10px"><span class="small-muted">Last automatic time:</span> <span id="autoTime">—</span></div>
</div>
<div class="hr"></div>
<div style="display:flex;gap:8px;align-items:center">
<button id="openExport" class="btn secondary">Export / Backup</button>
<button id="openManageTypes" class="btn ghost">Categories</button>
</div>
</div>
</div>
<!-- right: chart + entries -->
<div>
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center">
<strong>Weekly Summary (last 7 days)</strong>
<div class="small-muted">Events per day</div>
</div>
<div style="margin-top:12px">
<canvas id="weekChart" width="800" height="160" style="width:100%;height:160px"></canvas>
</div>
</div>
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between">
<strong>Entries</strong>
<div class="small-muted" id="entriesCount">0 entries</div>
</div>
<table class="entries-table" id="entriesTable" aria-live="polite" style="margin-top:12px">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th class="col-mood">Mood</th>
<th class="col-severity">Severity</th>
<th style="width:140px">Actions</th>
</tr>
</thead>
<tbody id="entriesBody"></tbody>
</table>
<div id="noEntriesMsg" class="muted" style="margin-top:12px">No entries yet. Click "Add Entry" or use Quick Log.</div>
</div>
</div>
</div>
<footer style="max-width:1200px;margin:18px auto 80px;text-align:center;color:var(--muted);font-size:13px">
<div>Data stored locally in your browser (no servers). Use Export to backup or share with a clinician.</div>
</footer>
<!-- Import file input (hidden) -->
<input type="file" id="importJsonFile" accept=".json" style="display:none" />
<!-- ===== Modals ===== -->
<!-- Settings Modal -->
<div id="settingsBackdrop" class="modal-backdrop">
<div class="modal">
<div class="modal-head">
<strong>Settings</strong>
<div><button id="closeSettings" class="btn ghost">Close</button></div>
</div>
<div class="modal-body">
<div>
<label><input type="checkbox" id="set_enableMood"> Enable Mood field</label>
<div class="small-muted">Show mood picker and include mood in entries/export.</div>
</div>
<div>
<label><input type="checkbox" id="set_enableSeverity"> Enable Severity for action types</label>
<div class="small-muted">Severity field appears when the event type is an action (e.g., "NSSI Action").</div>
</div>
<div>
<label><input type="checkbox" id="set_enableReminders"> Enable Reminders</label>
<div class="small-muted">Allow setting daily reminders (browser notifications).</div>
</div>
<div>
<label><input type="checkbox" id="set_enableExport"> Enable Export/Import</label>
<div class="small-muted">Show export/import buttons.</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:10px">
<button id="saveSettingsBtn" class="btn">Save</button>
</div>
</div>
</div>
</div>
<!-- Add / Edit Entry Modal -->
<div id="entryModalBackdrop" class="modal-backdrop" role="dialog" aria-modal="true">
<div class="modal" id="entryModal">
<div class="modal-head">
<strong id="entryModalTitle">Add Entry</strong>
<div>
<button id="closeEntryModal" class="btn ghost">Close</button>
</div>
</div>
<div class="modal-body">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="small-muted">Auto time: <span id="autoTimeModal">—</span></div>
<div class="small-muted">All data stored locally</div>
</div>
<div>
<label for="modalTypeSelect">Event Type</label>
<select id="modalTypeSelect"></select>
</div>
<div style="display:flex;gap:12px">
<!-- Mood (replaces old Intensity) -->
<div style="flex:1">
<label for="modalMoodLabel">Mood (quick)</label>
<div id="modalMoodPicker" class="emoji-grid mood-feature">
<button type="button" class="emoji-btn" data-emoji="😄">😄</button>
<button type="button" class="emoji-btn" data-emoji="🙂">🙂</button>
<button type="button" class="emoji-btn" data-emoji="😐">😐</button>
<button type="button" class="emoji-btn" data-emoji="😢">😢</button>
<button type="button" class="emoji-btn" data-emoji="😨">😨</button>
<button type="button" class="emoji-btn" data-emoji="😡">😡</button>
<button type="button" class="emoji-btn" data-emoji="🤯">🤯</button>
</div>
</div>
<!-- Severity (only for action types) -->
<div style="flex:1">
<label for="modalSeverity">Severity (1-10)</label>
<input id="modalSeverity" type="number" min="1" max="10" value="5" />
<div class="small-muted" style="margin-top:6px">Visible only for action types.</div>
</div>
</div>
<div style="display:flex;gap:12px;margin-top:6px">
<div style="flex:1">
<label for="modalDuration">Duration (minutes)</label>
<input id="modalDuration" type="number" min="0" placeholder="e.g., 30">
</div>
<div style="flex:1">
<label for="modalSeverityPlaceholder" class="small-muted"> </label>
<!-- placeholder to keep layout consistent -->
</div>
</div>
<div>
<label for="modalNotes">Notes / Additional info</label>
<textarea id="modalNotes" placeholder="Optional — triggers, context, coping used..."></textarea>
</div>
<div style="display:flex;gap:10px;align-items:center">
<button id="saveEntryBtn" class="btn">Save Entry</button>
<button id="saveCloseEntryBtn" class="btn secondary">Save & Close</button>
<button id="clearModalBtn" class="btn ghost">Clear</button>
</div>
</div>
</div>
</div>
<!-- Quick Log Modal -->
<div id="quickModalBackdrop" class="modal-backdrop">
<div class="modal">
<div class="modal-head">
<strong>Quick Log</strong>
<div><button id="closeQuick" class="btn ghost">Close</button></div>
</div>
<div class="modal-body">
<div>
<label>Type</label>
<select id="quickType"></select>
</div>
<div style="display:flex;gap:12px">
<div style="flex:1">
<label>Mood</label>
<select id="quickMood" class="mood-feature"><option value="">—</option><option>😄</option><option>🙂</option><option>😐</option><option>😢</option><option>😨</option><option>😡</option><option>🤯</option></select>
</div>
<div style="flex:1">
<label> </label>
<div class="small-muted">Quick entries omit severity/duration.</div>
</div>
</div>
<div>
<label>Notes (optional)</label>
<input id="quickNotes" />
</div>
<div style="display:flex;justify-content:flex-end;gap:8px">
<button id="saveQuick" class="btn">Save</button>
</div>
</div>
</div>
</div>
<!-- Filters Modal -->
<div id="filterBackdrop" class="modal-backdrop">
<div class="modal">
<div class="modal-head"><strong>Filters</strong><div><button id="closeFilters" class="btn ghost">Close</button></div></div>
<div class="modal-body">
<div style="display:flex;gap:12px">
<div style="flex:1">
<label>From</label>
<input id="filterFrom" type="date" />
</div>
<div style="flex:1">
<label>To</label>
<input id="filterTo" type="date" />
</div>
</div>
<div>
<label>Event Type</label>
<select id="filterType"><option value="">All</option></select>
</div>
<div style="display:flex;gap:12px">
<div style="flex:1">
<label>Search notes</label>
<input id="filterSearch" placeholder="text in notes" />
</div>
<div style="flex:1"></div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="applyFilters" class="btn">Apply</button>
<button id="clearFilters" class="btn ghost">Clear</button>
</div>
</div>
</div>
</div>
<!-- Manage Types Modal -->
<div id="typesBackdrop" class="modal-backdrop">
<div class="modal">
<div class="modal-head"><strong>Manage Event Types</strong><div><button id="closeTypes" class="btn ghost">Close</button></div></div>
<div class="modal-body">
<div style="display:flex;gap:8px">
<input id="newTypeInput" placeholder="New type (e.g., Panic Attack)" />
<button id="addTypeBtn" class="btn">Add</button>
</div>
<ul id="typesList" style="list-style:none;padding-left:0;margin-top:10px"></ul>
</div>
</div>
</div>
<!-- Reminders Modal -->
<div id="reminderBackdrop" class="modal-backdrop">
<div class="modal">
<div class="modal-head"><strong>Reminders</strong><div><button id="closeReminders" class="btn ghost">Close</button></div></div>
<div class="modal-body">
<div class="small-muted">Set a daily reminder time. Browser Notification permission is required for pop-up reminders and the tab must be open for them to trigger.</div>
<div style="display:flex;gap:8px;margin-top:10px;align-items:center">
<input id="reminderTime" type="time" />
<button id="setReminderBtn" class="btn">Set</button>
<button id="clearReminderBtn" class="btn ghost">Clear</button>
</div>
<div id="reminderStatus" class="small-muted" style="margin-top:10px"></div>
</div>
</div>
</div>
<!-- View Entry Modal -->
<div id="viewBackdrop" class="modal-backdrop">
<div class="modal" id="viewModal">
<div class="modal-head"><strong>Entry Details</strong><div><button id="closeView" class="btn ghost">Close</button></div></div>
<div class="modal-body" id="viewBody"></div>
</div>
</div>
<!-- Export modal (simple) -->
<div id="exportBackdrop" class="modal-backdrop">
<div class="modal">
<div class="modal-head"><strong>Export / Backup</strong><div><button id="closeExport" class="btn ghost">Close</button></div></div>
<div class="modal-body">
<div class="small-muted">Download a CSV or JSON backup of your entries.</div>
<div style="display:flex;gap:10px;margin-top:12px">
<button id="exportCsvBtn2" class="btn">Export CSV</button>
<button id="exportJsonBtn" class="btn secondary">Export JSON</button>
</div>
</div>
</div>
</div>
<script>
/* ========= Data model & storage ========= */
const STORAGE_KEYS = {
ENTRIES: 'emotion_tracker_entries_v1',
TYPES: 'emotion_tracker_types_v1',
THEME: 'emotion_tracker_theme_v1',
REMINDER: 'emotion_tracker_reminder_v1',
SETTINGS: 'emotion_tracker_settings_v1'
};
function loadJSON(k, fallback){ try { return JSON.parse(localStorage.getItem(k) || 'null') ?? fallback; } catch(e){ return fallback; } }
function saveJSON(k,v){ localStorage.setItem(k, JSON.stringify(v)); }
/* initial state */
let entries = loadJSON(STORAGE_KEYS.ENTRIES, []);
let types = loadJSON(STORAGE_KEYS.TYPES, [
"NSSI Urge","NSSI Action","Depressive Episode","Suicidal Thought","Suicidal Action"
]);
let reminder = loadJSON(STORAGE_KEYS.REMINDER, null);
/* default settings */
const DEFAULT_SETTINGS = {
enableMood: true,
enableSeverity: true,
enableReminders: true,
enableExport: true
};
let settings = loadJSON(STORAGE_KEYS.SETTINGS, DEFAULT_SETTINGS);
/* dom refs */
const entriesBody = document.getElementById('entriesBody');
const entriesCount = document.getElementById('entriesCount');
const noEntriesMsg = document.getElementById('noEntriesMsg');
const weekChart = document.getElementById('weekChart');
const ctxWeek = weekChart.getContext('2d');
const autoTime = document.getElementById('autoTime');
const autoTimeModal = document.getElementById('autoTimeModal');
const overviewText = document.getElementById('overviewText');
/* header controls */
const openAddEntry = document.getElementById('openAddEntry');
const quickLogBtn = document.getElementById('quickLogBtn');
const openFilters = document.getElementById('openFilters');
const toggleTheme = document.getElementById('toggleTheme');
const openSettings = document.getElementById('openSettings');
/* file import */
const importJsonFile = document.getElementById('importJsonFile');
/* modal backdrops */
const settingsBackdrop = document.getElementById('settingsBackdrop');
const closeSettings = document.getElementById('closeSettings');
const entryModalBackdrop = document.getElementById('entryModalBackdrop');
const entryModal = document.getElementById('entryModal');
const closeEntryModal = document.getElementById('closeEntryModal');
const quickBackdrop = document.getElementById('quickModalBackdrop');
const closeQuick = document.getElementById('closeQuick');
const filterBackdrop = document.getElementById('filterBackdrop');
const closeFilters = document.getElementById('closeFilters');
const typesBackdrop = document.getElementById('typesBackdrop');
const closeTypes = document.getElementById('closeTypes');
const reminderBackdrop = document.getElementById('reminderBackdrop');
const closeReminders = document.getElementById('closeReminders');
const viewBackdrop = document.getElementById('viewBackdrop');
const closeView = document.getElementById('closeView');
const exportBackdrop = document.getElementById('exportBackdrop');
const closeExport = document.getElementById('closeExport');
/* entry modal inputs */
const modalTypeSelect = document.getElementById('modalTypeSelect');
const modalSeverity = document.getElementById('modalSeverity');
const modalDuration = document.getElementById('modalDuration');
const modalMoodPicker = document.getElementById('modalMoodPicker');
const modalNotes = document.getElementById('modalNotes');
const saveEntryBtn = document.getElementById('saveEntryBtn');
const saveCloseEntryBtn = document.getElementById('saveCloseEntryBtn');
const clearModalBtn = document.getElementById('clearModalBtn');
/* quick modal inputs */
const quickType = document.getElementById('quickType');
const quickMood = document.getElementById('quickMood');
const quickNotes = document.getElementById('quickNotes');
const saveQuick = document.getElementById('saveQuick');
/* filters */
const filterFrom = document.getElementById('filterFrom');
const filterTo = document.getElementById('filterTo');
const filterType = document.getElementById('filterType');
const filterSearch = document.getElementById('filterSearch');
const applyFilters = document.getElementById('applyFilters');
const clearFilters = document.getElementById('clearFilters');
/* types modal */
const newTypeInput = document.getElementById('newTypeInput');
const addTypeBtn = document.getElementById('addTypeBtn');
const typesListEl = document.getElementById('typesList');
const manageTypesBtn = document.getElementById('manageTypesBtn');
const openManageTypes = document.getElementById('openManageTypes');
/* reminders */
const openReminders = document.getElementById('openReminders');
const reminderTime = document.getElementById('reminderTime');
const setReminderBtn = document.getElementById('setReminderBtn');
const clearReminderBtn = document.getElementById('clearReminderBtn');
const reminderStatus = document.getElementById('reminderStatus');
/* export buttons */
const exportCsvBtn = document.getElementById('exportCsvBtn');
const exportCsvBtn2 = document.getElementById('exportCsvBtn2');
const exportJsonBtn = document.getElementById('exportJsonBtn');
const openExport = document.getElementById('openExport');
/* settings inputs */
const set_enableMood = document.getElementById('set_enableMood');
const set_enableSeverity = document.getElementById('set_enableSeverity');
const set_enableReminders = document.getElementById('set_enableReminders');
const set_enableExport = document.getElementById('set_enableExport');
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
/* crisis info */
const crisisInfoBtn = document.getElementById('crisisInfoBtn');
/* helpers */
function nowLocalString(){ return formatShort(new Date()); }
function isoDate(x){ const d = new Date(x); return d.toISOString().slice(0,10); }
/* friendly date/time formatting: M/D/YY, h:mm AM/PM (no seconds) */
function formatShort(dateOrTs){
const d = (dateOrTs instanceof Date) ? dateOrTs : new Date(dateOrTs);
try {
return new Intl.DateTimeFormat(undefined, {
month: 'numeric', day: 'numeric', year: '2-digit',
hour: 'numeric', minute: '2-digit',
hour12: true
}).format(d);
} catch(e){
// fallback
const m = d.getMonth()+1, day = d.getDate(), yy = String(d.getFullYear()).slice(-2);
let hours = d.getHours();
const am = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12 || 12;
const mins = String(d.getMinutes()).padStart(2,'0');
return `${m}/${day}/${yy}, ${hours}:${mins} ${am}`;
}
}
function friendlyDateTime(x){ return formatShort(x); }
function saveEntries(){ saveJSON(STORAGE_KEYS.ENTRIES, entries); }
/* escape */
function esc(s){ return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]); }
/* helper: consider a type an "action" if it contains the substring 'action' (case-insensitive) */
function isActionType(type) {
if (!type) return false;
return String(type).toLowerCase().includes('action');
}
/* populate selects and UI bits */
function populateTypeSelects(){
modalTypeSelect.innerHTML = types.map(t=>`<option value="${esc(t)}">${esc(t)}</option>`).join('');
quickType.innerHTML = `<option value="">—</option>${types.map(t=>`<option value="${esc(t)}">${esc(t)}</option>`).join('')}`;
filterType.innerHTML = `<option value="">All</option>${types.map(t=>`<option value="${esc(t)}">${esc(t)}</option>`).join('')}`;
}
function renderTypesList(){
typesListEl.innerHTML = types.map((t,i)=>`
<li style="display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(0,0,0,0.03)">
<span>${esc(t)}</span>
<div style="display:flex;gap:8px">
<button class="btn ghost" data-edit="${i}">Rename</button>
<button class="btn" data-delete="${i}">Delete</button>
</div>
</li>`).join('') || '<div class="small-muted">No types defined.</div>';
}
/* entries rendering with basic summary */
function getFilteredEntries(){
let list = entries.slice();
const from = filterFrom.value;
const to = filterTo.value;
const fType = filterType.value;
const fSearch = (filterSearch.value||'').trim().toLowerCase();
if (from) list = list.filter(e => isoDate(e.timestamp) >= from);
if (to) list = list.filter(e => isoDate(e.timestamp) <= to);
if (fType) list = list.filter(e => e.type === fType);
if (fSearch) list = list.filter(e => (e.notes||'').toLowerCase().includes(fSearch) || (e.type||'').toLowerCase().includes(fSearch));
// default sort newest first
list.sort((a,b)=>b.timestamp - a.timestamp);
return list;
}
function renderEntries(){
const list = getFilteredEntries();
entriesBody.innerHTML = list.map((e, idx) => {
const moodCell = settings.enableMood ? `<td class="col-mood">${esc(e.mood||'')}</td>` : '';
const severityCell = (settings.enableSeverity && isActionType(e.type)) ? `<td class="col-severity">${esc(String(e.severity || ''))}</td>` : `<td class="col-severity"></td>`;
return `<tr>
<td style="white-space:nowrap">${esc(friendlyDateTime(e.timestamp))}</td>
<td>${esc(e.type)}</td>
${moodCell}
${severityCell}
<td>
<button class="btn ghost" data-view="${entries.indexOf(e)}">View</button>
<button class="btn" data-delete="${entries.indexOf(e)}">Delete</button>
</td>
</tr>`;
}).join('');
entriesCount.textContent = `${entries.length} entries`;
noEntriesMsg.style.display = entries.length ? 'none' : 'block';
overviewText.textContent = `Total entries: ${entries.length}. Most recent: ${entries.length ? friendlyDateTime(entries[entries.length-1].timestamp) : '—'}`;
applyColumnVisibility();
}
/* chart */
function computeLast7DaysCounts(){
const counts = Array(7).fill(0);
const labels = [];
const today = new Date(); today.setHours(0,0,0,0);
for (let i=6;i>=0;i--){
const d = new Date(today);
d.setDate(today.getDate() - i);
labels.push(d.toLocaleDateString(undefined, {weekday:'short',month:'short',day:'numeric'}));
const iso = d.toISOString().slice(0,10);
counts[6-i] = entries.filter(e => isoDate(e.timestamp) === iso).length;
}
return {labels, counts};
}
function drawWeekChart(){
const {labels, counts} = computeLast7DaysCounts();
ctxWeek.clearRect(0,0,weekChart.width,weekChart.height);
const w = weekChart.width, h = weekChart.height;
const pad = 20;
const chartW = w - pad*2, chartH = h - pad*2;
const max = Math.max(1, ...counts);
const barW = chartW / counts.length * 0.6;
const gap = (chartW - barW*counts.length) / (counts.length - 1 || 1);
// choose text color for readability in dark mode
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const labelColor = isDark ? '#e7ecef' : getComputedStyle(document.documentElement).getPropertyValue('--muted') || '#666';
const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent') || '#4e6ef2';
ctxWeek.font = isDark ? '13px Inter, Arial' : '12px Inter, Arial';
ctxWeek.textBaseline = 'top';
for (let i=0;i<counts.length;i++){
const x = pad + i*(barW+gap);
const barH = (counts[i]/max) * (chartH);
const y = pad + (chartH - barH);
// bar
ctxWeek.fillStyle = accent;
ctxWeek.fillRect(x, y, barW, barH);
// label
ctxWeek.fillStyle = labelColor;
const label = labels[i].split(',')[0];
ctxWeek.fillText(label, x, h - 18);
// count (above bar)
ctxWeek.fillText(String(counts[i]), x, Math.max(2, y - 16));
}
}
/* add/delete entries */
function addEntry(entry){
entries.push(entry);
saveEntries();
renderAll();
}
function deleteEntry(index){
if (!confirm('Delete this entry?')) return;
entries.splice(index,1);
saveEntries();
renderAll();
}
function clearAll(){
if (!confirm('Clear all entries? This cannot be undone.')) return;
entries = [];
saveEntries();
renderAll();
}
/* exports: include mood & severity, no tags */
function toCsv(arr){
const h = ['timestamp','type','mood','severity','duration','notes'];
const rows = arr.map(e=>[
`"${new Date(e.timestamp).toISOString()}"`,
`"${(e.type||'').replace(/"/g,'""')}"`,
`"${(e.mood||'').replace(/"/g,'""')}"`,
e.severity || '',
e.duration || '',
`"${(e.notes||'').replace(/"/g,'""')}"`
].join(','));
return h.join(',') + '\n' + rows.join('\n');
}
function download(filename, text, mime='text/plain'){
const blob = new Blob([text], {type: mime + ';charset=utf-8;'});
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);
}
/* theme */
function setTheme(theme){
if (theme === 'dark') document.documentElement.setAttribute('data-theme','dark');
else document.documentElement.removeAttribute('data-theme');
toggleTheme.textContent = theme === 'dark' ? 'Light' : 'Dark';
saveJSON(STORAGE_KEYS.THEME, theme);
}
const savedTheme = loadJSON(STORAGE_KEYS.THEME, (window.matchMedia && window.matchMedia('(prefers-color-scheme:dark)').matches) ? 'dark':'light');
setTheme(savedTheme);
/* reminders */
async function requestNotificationPermission(){ if (!('Notification' in window)) return false; if (Notification.permission === 'granted') return true; if (Notification.permission === 'denied') return false; const p = await Notification.requestPermission(); return p === 'granted';}
function scheduleReminder(timeStr){ reminder = {time: timeStr}; saveJSON(STORAGE_KEYS.REMINDER, reminder); reminderStatus.textContent = `Reminder set daily at ${timeStr} (tab must be open).`; }
function clearReminder(){ reminder = null; localStorage.removeItem(STORAGE_KEYS.REMINDER); reminderStatus.textContent = 'No reminder set.'; }
function initReminderUI(){ if (reminder && reminder.time){ reminderTime.value = reminder.time; reminderStatus.textContent = `Reminder set daily at ${reminder.time} (tab must be open).`; } else reminderStatus.textContent = 'No reminder set.'; }
setInterval(()=>{
if (!reminder || !reminder.time) return;
const hhmm = new Date().toTimeString().slice(0,5);
if (hhmm === reminder.time && Notification.permission === 'granted'){
new Notification('Check-in reminder', {body: 'Time to reflect briefly and log how you are feeling.'});
}
}, 60000);
/* ======= UI wiring (modals & settings) ======= */
function showBackdrop(el){ el.style.display = 'flex'; el.setAttribute('aria-hidden','false'); }
function hideBackdrop(el){ el.style.display = 'none'; el.setAttribute('aria-hidden','true'); }
/* apply settings to UI (hide/show features) */
function applySettings(){
// save settings
saveJSON(STORAGE_KEYS.SETTINGS, settings);
// Mood
document.querySelectorAll('.mood-feature').forEach(el => {
el.style.display = settings.enableMood ? '' : 'none';
});
// Reminders button visibility
document.getElementById('openReminders').style.display = settings.enableReminders ? '' : 'none';
// Export/Import buttons visibility
document.getElementById('exportCsvBtn').style.display = settings.enableExport ? '' : 'none';
document.querySelector('label[for="importJsonFile"]').style.display = settings.enableExport ? '' : 'none';
document.getElementById('openExport').style.display = settings.enableExport ? '' : 'none';
// Severity column visibility in table header
document.querySelectorAll('.col-severity').forEach(h => { h.style.display = settings.enableSeverity ? '' : 'none'; });
// Render entries since column visibility may change
renderEntries();
}
/* Settings modal */
openSettings.addEventListener('click', ()=>{
// populate current settings in checkboxes
set_enableMood.checked = settings.enableMood;
set_enableSeverity.checked = settings.enableSeverity;
set_enableReminders.checked = settings.enableReminders;
set_enableExport.checked = settings.enableExport;
showBackdrop(settingsBackdrop);
});
closeSettings.addEventListener('click', ()=> hideBackdrop(settingsBackdrop));
saveSettingsBtn.addEventListener('click', ()=>{
settings.enableMood = !!set_enableMood.checked;
settings.enableSeverity = !!set_enableSeverity.checked;
settings.enableReminders = !!set_enableReminders.checked;
settings.enableExport = !!set_enableExport.checked;
applySettings();
hideBackdrop(settingsBackdrop);
});
/* open add entry modal */
openAddEntry.addEventListener('click', ()=>{
populateTypeSelects();
modalTypeSelect.value = types[0] || '';
modalSeverity.value = 5;
modalDuration.value = '';
modalNotes.value = '';
modalMoodPicker.querySelectorAll('.emoji-btn').forEach(b=>b.classList.remove('selected'));
autoTimeModal.textContent = friendlyDateTime(new Date());
showBackdrop(entryModalBackdrop);
// ensure severity visibility for initial type
toggleSeverityVisibility(modalTypeSelect.value);
});
/* close entry modal */
closeEntryModal.addEventListener('click', ()=> hideBackdrop(entryModalBackdrop));
/* modal mood pickers */
modalMoodPicker.addEventListener('click', (e)=>{
if (!settings.enableMood) return;
const b = e.target.closest('.emoji-btn');
if (!b) return;
modalMoodPicker.querySelectorAll('.emoji-btn').forEach(x=>x.classList.remove('selected'));
b.classList.add('selected');
});
/* when type changes, toggle severity field visibility */
modalTypeSelect.addEventListener('change', (e)=>{
toggleSeverityVisibility(e.target.value);
});
function toggleSeverityVisibility(type) {
const showBecauseAction = isActionType(type);
const severityEl = document.getElementById('modalSeverity');
const label = severityEl.previousElementSibling;
if (!settings.enableSeverity) {
severityEl.style.display = 'none';
if(label) label.style.display = 'none';
} else {
severityEl.style.display = showBecauseAction ? '' : 'none';
if(label) label.style.display = showBecauseAction ? '' : 'none';
}
}
/* save entry (build object with mood & conditional severity) */
saveEntryBtn.addEventListener('click', ()=>{
const chosenType = modalTypeSelect.value || 'Unspecified';
const mood = settings.enableMood ? (modalMoodPicker.querySelector('.emoji-btn.selected')||{}).dataset.emoji || '' : '';
const severity = (settings.enableSeverity && isActionType(chosenType)) ? (Number(modalSeverity.value) || null) : null;
const entry = {
timestamp: Date.now(),
type: chosenType,
mood,
severity,
duration: modalDuration.value ? Number(modalDuration.value) : null,
notes: modalNotes.value || ''
};
addEntry(entry);
// keep modal open for quick repeated entries
modalNotes.value = '';
modalMoodPicker.querySelectorAll('.emoji-btn').forEach(b=>b.classList.remove('selected'));
});
/* save & close */
saveCloseEntryBtn.addEventListener('click', ()=>{
saveEntryBtn.click();
hideBackdrop(entryModalBackdrop);
});
/* clear modal */
clearModalBtn.addEventListener('click', ()=>{
modalSeverity.value = 5; modalDuration.value=''; modalNotes.value=''; modalMoodPicker.querySelectorAll('.emoji-btn').forEach(b=>b.classList.remove('selected'));
});
/* quick log modal open */
quickLogBtn.addEventListener('click', ()=>{
populateTypeSelects();
quickType.value = '';
quickMood.value = '';
quickNotes.value = '';
showBackdrop(quickBackdrop);
});
closeQuick.addEventListener('click', ()=> hideBackdrop(quickBackdrop));
saveQuick.addEventListener('click', ()=> {
const chosenType = quickType.value || 'Quick';
const mood = settings.enableMood ? (quickMood.value || '') : '';
const entry = {
timestamp: Date.now(),
type: chosenType,
mood,
severity: null,
duration: null,
notes: quickNotes.value || ''
};
addEntry(entry);
hideBackdrop(quickBackdrop);
});
/* filters modal */
openFilters.addEventListener('click', ()=> {
populateTypeSelects();
showBackdrop(filterBackdrop);
});
closeFilters.addEventListener('click', ()=> hideBackdrop(filterBackdrop));
applyFilters.addEventListener('click', ()=> { renderEntries(); hideBackdrop(filterBackdrop); });
clearFilters.addEventListener('click', ()=> {
filterFrom.value=''; filterTo.value=''; filterType.value=''; filterSearch.value='';
renderEntries();
});
/* manage types modal */
manageTypesBtn.addEventListener('click', ()=> { populateTypeSelects(); renderTypesList(); showBackdrop(typesBackdrop); });
openManageTypes.addEventListener('click', ()=> manageTypesBtn.click());
closeTypes.addEventListener('click', ()=> hideBackdrop(typesBackdrop));
addTypeBtn.addEventListener('click', ()=>{
const v = (newTypeInput.value||'').trim();
if (!v) return alert('Type name required');
types.push(v);
saveJSON(STORAGE_KEYS.TYPES, types);
newTypeInput.value = '';
populateTypeSelects();
renderTypesList();
});
typesListEl.addEventListener('click', (ev)=>{
const edit = ev.target.closest('[data-edit]');
const del = ev.target.closest('[data-delete]');
if (edit){
const i = Number(edit.dataset.edit);
const newLabel = prompt('Rename type', types[i]);
if (newLabel && newLabel.trim()){
types[i] = newLabel.trim();
saveJSON(STORAGE_KEYS.TYPES, types);
populateTypeSelects();
renderTypesList();
}
} else if (del){
const i = Number(del.dataset.delete);
if (!confirm('Delete this type? Existing entries keep their type text.')) return;
types.splice(i,1);
saveJSON(STORAGE_KEYS.TYPES, types);
populateTypeSelects();
renderTypesList();
}
});
/* reminders modal */
openReminders.addEventListener('click', ()=> { if (!settings.enableReminders) return alert('Reminders disabled in Settings.'); showBackdrop(reminderBackdrop); initReminderUI(); });
closeReminders.addEventListener('click', ()=> hideBackdrop(reminderBackdrop));
setReminderBtn.addEventListener('click', async ()=>{
if (!settings.enableReminders) return alert('Reminders disabled in Settings.');
const t = reminderTime.value;
if (!t) return alert('Choose a time first.');
const ok = await requestNotificationPermission();
if (!ok) alert('Notification permission needed for reminders.');
scheduleReminder(t);
initReminderUI();
});
clearReminderBtn.addEventListener('click', ()=> { clearReminder(); initReminderUI(); });
/* export modal */
openExport.addEventListener('click', ()=> { if (!settings.enableExport) return alert('Export disabled in Settings.'); showBackdrop(exportBackdrop); });
closeExport.addEventListener('click', ()=> hideBackdrop(exportBackdrop));
exportCsvBtn.addEventListener('click', ()=> { if (!settings.enableExport) return alert('Export disabled in Settings.'); download('emotion-entries.csv', toCsv(entries), 'text/csv'); });
exportCsvBtn2.addEventListener('click', ()=> exportCsvBtn.click());
exportJsonBtn.addEventListener('click', ()=> { if (!settings.enableExport) return alert('Export disabled in Settings.'); download('emotion-entries.json', JSON.stringify(entries,null,2), 'application/json'); });
/* import file */
importJsonFile.addEventListener('change', (e)=>{
if (!settings.enableExport) { e.target.value=''; return alert('Import disabled in Settings.'); }
const f = e.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.onload = function(ev){
try {
const data = JSON.parse(ev.target.result);
if (!Array.isArray(data)) throw new Error('Expected an array of entries');
data.forEach(it=>{
if (!it.timestamp) it.timestamp = Date.now();
// remove tags handling (tags removed)
});
entries = entries.concat(data);
saveEntries();
renderAll();
alert('Import complete. Added ' + data.length + ' entries.');
} catch(err){
alert('Failed to import: ' + err.message);
}
};
reader.readAsText(f);
e.target.value = '';
});
/* theme toggle */
toggleTheme.addEventListener('click', ()=>{
const cur = loadJSON(STORAGE_KEYS.THEME, 'light');
const next = cur === 'dark' ? 'light' : 'dark';
setTheme(next);
// redraw chart to use new color scheme
drawWeekChart();
});
/* view and delete buttons in table */
entriesBody.addEventListener('click', (e)=>{
const view = e.target.closest('[data-view]');
const del = e.target.closest('[data-delete]');
if (view){
const idx = Number(view.dataset.view);
const entry = entries[idx];
const html = `
<div style="display:grid;gap:10px">
<div><strong>${esc(entry.type)}</strong> — ${friendlyDateTime(entry.timestamp)}</div>
<div><strong>Mood:</strong> ${esc(entry.mood||'')}</div>
<div><strong>Severity:</strong> ${esc(String(entry.severity||''))}</div>
<div><strong>Duration:</strong> ${entry.duration ? esc(String(entry.duration)+' min') : '—'}</div>
<div><strong>Notes:</strong><div style="margin-top:6px" class="muted-block">${esc(entry.notes||'')}</div></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="viewDeleteBtn" class="btn" data-delete="${idx}">Delete</button>
<button id="viewCloseBtn" class="btn secondary">Close</button>
</div>
</div>`;
document.getElementById('viewBody').innerHTML = html;
showBackdrop(viewBackdrop);
} else if (del){
const idx = Number(del.dataset.delete);
if (confirm('Delete this entry?')) deleteEntry(idx);
}
});
/* closure for view modal's internal buttons */
viewBackdrop.addEventListener('click', (e)=>{
if (e.target.id === 'viewCloseBtn' || e.target === viewBackdrop) hideBackdrop(viewBackdrop);
const del = e.target.closest('#viewDeleteBtn');
if (del){ deleteEntry(Number(del.dataset.delete)); hideBackdrop(viewBackdrop); }
});
closeView.addEventListener('click', ()=> hideBackdrop(viewBackdrop));
/* crisis info */
crisisInfoBtn.addEventListener('click', ()=>{
alert('If you are in immediate danger call emergency services (e.g., 911 in the U.S.) or the Suicide & Crisis Lifeline: 988 (U.S.).\\n\\nFor international resources, contact local emergency services or local crisis hotlines. Reach out to a trusted person or professional.');
});
/* filters & rendering */
applyFilters.addEventListener('click', ()=> { renderEntries(); hideBackdrop(filterBackdrop); });
/* reminder init */
initReminderUI();
/* periodic auto-time update */
setInterval(()=> {
autoTime.textContent = friendlyDateTime(new Date());
if (autoTimeModal) autoTimeModal.textContent = friendlyDateTime(new Date());
}, 1000);
/* import button in header (visible label uses hidden input) */
document.querySelector('label[for="importJsonFile"]').addEventListener('click', ()=> importJsonFile.click());
/* keyboard: ESC to close modals */
document.addEventListener('keydown', (e)=>{
if (e.key === 'Escape'){
[entryModalBackdrop, quickBackdrop, filterBackdrop, typesBackdrop, reminderBackdrop, viewBackdrop, exportBackdrop, settingsBackdrop].forEach(b=>{ if (b.style.display === 'flex') hideBackdrop(b); });
}
});
/* CSV/JSON export via header */
exportCsvBtn.addEventListener('click', ()=> { if (!settings.enableExport) return alert('Export disabled in Settings.'); download('emotion-entries.csv', toCsv(entries), 'text/csv'); });
exportJsonBtn.addEventListener('click', ()=> { if (!settings.enableExport) return alert('Export disabled in Settings.'); download('emotion-entries.json', JSON.stringify(entries,null,2), 'application/json'); });
/* show/hide helper for backdrops clicking outside modal */
document.querySelectorAll('.modal-backdrop').forEach(back => {
back.addEventListener('click', (e)=> {
if (e.target === back) hideBackdrop(back);
});
});
/* initial render and helpers */
function renderAll(){
populateTypeSelects();
renderTypesList();
renderEntries();
drawWeekChart();
initReminderUI();
applySettings(); // apply UI settings (mood/severity/export/reminders)
}
renderAll();
/* allow adding from entry modal by pressing Ctrl+Enter */
modalNotes.addEventListener('keydown', (e)=> { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') saveEntryBtn.click(); });
/* helpers: import file label hooking for second label */
document.querySelectorAll('label[for="importJsonFile"]').forEach(l => l.addEventListener('keydown', (e)=>{ if(e.key==='Enter') importJsonFile.click(); }));
/* initialize: persist types default and settings */
saveJSON(STORAGE_KEYS.TYPES, types);
saveJSON(STORAGE_KEYS.SETTINGS, settings);
/* utility: hide/show columns according to settings */
function applyColumnVisibility(){
// mood column
document.querySelectorAll('.col-mood').forEach(n => { n.style.display = settings.enableMood ? '' : 'none'; });
// severity
document.querySelectorAll('.col-severity').forEach(n => { n.style.display = settings.enableSeverity ? '' : 'none'; });
}
/* ============= Do not transmit data anywhere ===============
This app stores data locally in your browser and does not send data to any server.
Use Export to backup.
Download index.html to have your own fully locally deployed version.
============================================================== */
</script>
</body>
</html>
/* Add your styles here */
// Add your code here