My Journal
.entry h3,
.entry p,
.entry .meta {
color: #ffffff;
}
:root
}
body { margin:0; font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:var(--bg); color:var(--text); }
header { padding:16px 20px; border-bottom:2px solid #222; display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
header h1 { margin:0; font-size:20px; font-weight:600; letter-spacing:0.3px; }
.controls { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
input, select, button, textarea { background:var(--panel); color:var(--text); border:1px solid #FFFFFF; border-radius:8px; padding:8px; }
input, select, button { height:40px; }
button { cursor:pointer; }
button.primary { background:var(--accent); border-color:var(--accent); color:#1B0000; font-weight:600; }
button.danger { background:var(--danger); border-color:var(--danger); color:#FFFFFF; }
main { max-width:900px; margin:0 auto; padding:20px; display:grid; grid-template-columns:1fr; gap:16px; }
.editor, .list, .stats { background:var(--panel); border:1px solid #2a2f3a; border-radius:12px; padding:16px; }
.editor textarea { width:100%; min-height:140px; resize:vertical; }
.row { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
.tag { font-size:12px; color:var(--muted); margin-left:6px; }
.entry { border:3px solid #FFFF; border-radius:10px; padding:12px; margin-bottom:10px; background:#2a2F3A; }
.entry h3 { margin:0 0 6px; font-size:16px; }
. meta { font-size:14px; color:var(--muted); margin-bottom:6px; }
.entry .actions { display:flex; gap:6px; }
.empty { color:var(--muted); font-style:italic; padding:10px; }
.pill { padding:4px 8px; border:1px solid #2a2f3a; border-radius:999px; font-size:12px; color:var(--muted); }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; }
@media (min-width:980px){ main{ grid-template-columns:1.2fr 1fr; } .stats{ grid-column:1 / -1; } }
<h2 style="color:#ffffff;"
New entry
How are you feeling? (optional)
π Peaceful
π Joyful
π Overwhelmed
π Anxious
π Sad
π€ Frustrated
π€© Excited
π΄ Tired
π€ Thoughtful
π Grateful
π Confident
π₯³ Celebratory
π’ Lonely
π‘ Angry
π€― Overwhelmed
β€οΈ Loving
π€ Unwell
π€ Hopeful
<button id="saveBtn" class="primary"
History
0 entries
β most common feelings
β top tag
const STORAGE_KEY = 'myJournalEntries_v1';
const els = {
search: document.getElementById('search'),
filterTag: document.getElementById('filterTag'),
exportBtn: document.getElementById('exportBtn'),
importFile: document.getElementById('importFile'),
clearBtn: document.getElementById('clearBtn'),
title: document.getElementById('titleInput'),
content: document.getElementById('contentInput'),
tags: document.getElementById('tagsInput'),
mood: document.getElementById('moodInput'),
save: document.getElementById('saveBtn'),
entries: document.getElementById('entries'),
countStat: document.getElementById('countStat'),
moodStat: document.getElementById('moodStat'),
tagStat: document.getElementById('tagStat'),
};
function nowISO(){ return new Date().toISOString(); }
function read(){ try{ return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; } catch{ return []; } }
function write(data){ localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }
function uid(){ return Math.random().toString(36).slice(2) + Date.now().toString(36); }
function addEntry(entry){
const data = read();
data.unshift(entry);
write(data);
render();
resetEditor();
}
function updateEntry(id, updates){
const data = read().map(e => e.id === id ? { ...e, ...updates, updatedAt: nowISO() } : e);
write(data);
render();
}
function deleteEntry(id){
const data = read().filter(e => e.id !== id);
write(data);
render();
}
function resetEditor(){
els.title.value = '';
els.content.value = '';
els.tags.value = '';
els.mood.value = '';
els.content.focus();
}
function render(){
const query = els.search.value.trim().toLowerCase();
const tagFilter = els.filterTag.value.trim().toLowerCase();
const data = read();
// Update tag filter options
const tags = Array.from(new Set(data.flatMap(e => e.tags)));
els.filterTag.innerHTML = 'Filter by tag' + tags.map(t => `${t}`).join('');
let list = data.filter(e => {
const matchesQuery = !query || (e.title.toLowerCase().includes(query) || e.content.toLowerCase().includes(query) || e.tags.some(t=>t.includes(query)));
const matchesTag = !tagFilter || e.tags.includes(tagFilter);
return matchesQuery && matchesTag;
});
if (!list.length){
els.entries.innerHTML = '
No entries yet. Write your first one above.
';
} else {
els.entries.innerHTML = list.map(e => `
${e.title || 'Untitled'}
${new Date(e.createdAt).toLocaleString()}${e.updatedAt ? ' β’ updated ' + new Date(e.updatedAt).toLocaleString() : ''}
${e.mood ? ' β’ ' + e.mood : '' }
${e.tags.length ? ' β’ tags: ' + e.tags.join(', ') : ''}
${escapeHtml(e.content).replace(/\n/g,'
')}
`).join('');
}
// Stats
els.countStat.textContent = data.length;
els.moodStat.textContent = mode(data.map(e => e.mood).filter(Boolean)) || 'β';
els.tagStat.textContent = mode(data.flatMap(e => e.tags)) || 'β';
}
function escapeHtml(s){
const map = { '&':'&', '':'>', '"':'"', "'":''' };
return s.replace(/[&"']/g, m => map[m]);
}
function mode(arr){
if (!arr.length) return null;
const counts = {};
arr.forEach(v => counts[v] = (counts[v]||0)+1);
return Object.entries(counts).sort((a,b)=>b[1]-a[1])[0][0];
}
// Events
els.save.addEventListener('click', () => {
const title = els.title.value.trim();
const content = els.content.value.trim();
const tags = els.tags.value.split(',').map(t => t.trim().toLowerCase()).filter(Boolean);
const mood = els.mood.value || null;
if (!content){ alert('Write something first.'); return; }
addEntry({ id: uid(), title, content, tags, mood, createdAt: nowISO(), updatedAt: null });
});
els.search.addEventListener('input', render);
els.filterTag.addEventListener('change', render);
els.entries.addEventListener('click', (ev) => {
const btn = ev.target.closest('button');
if (!btn) return;
const article = ev.target.closest('.entry');
const id = article.dataset.id;
const data = read();
const entry = data.find(e => e.id === id);
if (btn.dataset.action === 'delete'){
if (confirm('Delete this entry?')) deleteEntry(id);
} else if (btn.dataset.action === 'edit'){
const newTitle = prompt('Title:', entry.title || '');
const newContent = prompt('Content:', entry.content);
const newTags = prompt('Tags (comma separated):', entry.tags.join(', '));
const newMood = prompt('Mood (optional):', entry.mood || '');
updateEntry(id, {
title: (newTitle||'').trim(),
content: (newContent||'').trim(),
tags: (newTags||'').split(',').map(t => t.trim().toLowerCase()).filter(Boolean),
mood: (newMood||'').trim() || null
});
}
});
els.exportBtn.addEventListener('click', () => {
const blob = new Blob([JSON.stringify(read(), null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'journal-export.json';
a.click();
URL.revokeObjectURL(url);
});
els.importFile.addEventListener('change', async () => {
const file = els.importFile.files[0];
if (!file) return;
const text = await file.text();
try{
const data = JSON.parse(text);
if (!Array.isArray(data)) throw new Error('Invalid file');
write(data);
render();
alert('Imported successfully.');
} catch(e){
alert('Import failed: ' + e.message);
} finally {
els.importFile.value = '';
}
});
els.clearBtn.addEventListener('click', () => {
if (confirm('This will clear all entries on this device. Continue?')){function downloadFile(content, filename, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
localStorage.removeItem(STORAGE_KEY);
render();
}
});
// Initial
render();
</body