- Activity loads from tasks.json dynamically - Added task tracking workflow in AGENTS.md - Notes UI improvements - Renamed recipe with date prefix
941 lines
32 KiB
HTML
941 lines
32 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ro">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Echo · Notes</title>
|
||
<link rel="stylesheet" href="common.css">
|
||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||
<script src="swipe-nav.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
<style>
|
||
.main {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: var(--space-5);
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: var(--space-6);
|
||
flex-wrap: wrap;
|
||
gap: var(--space-4);
|
||
}
|
||
|
||
.page-title {
|
||
font-size: var(--text-xl);
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.search-bar {
|
||
width: 300px;
|
||
}
|
||
|
||
/* Date sections (accordion) */
|
||
.date-section {
|
||
margin-bottom: var(--space-3);
|
||
}
|
||
|
||
.date-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: var(--space-3) var(--space-4);
|
||
background: var(--bg-surface);
|
||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
[data-theme="light"] .date-header {
|
||
border-color: rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.date-header:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
/* Section colors */
|
||
[data-section="today"] .date-header {
|
||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.25), rgba(37, 99, 235, 0.15));
|
||
border-left: 3px solid #3b82f6;
|
||
}
|
||
|
||
[data-section="yesterday"] .date-header {
|
||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.25), rgba(124, 58, 237, 0.15));
|
||
border-left: 3px solid #8b5cf6;
|
||
}
|
||
|
||
[data-section="thisWeek"] .date-header {
|
||
background: linear-gradient(135deg, rgba(20, 184, 166, 0.25), rgba(13, 148, 136, 0.15));
|
||
border-left: 3px solid #14b8a6;
|
||
}
|
||
|
||
[data-section="older"] .date-header {
|
||
background: linear-gradient(135deg, rgba(249, 115, 22, 0.25), rgba(234, 88, 12, 0.15));
|
||
border-left: 3px solid #f97316;
|
||
}
|
||
|
||
.date-header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
}
|
||
|
||
.date-header-left svg {
|
||
width: 18px;
|
||
height: 18px;
|
||
color: var(--text-muted);
|
||
transition: transform var(--transition-fast);
|
||
}
|
||
|
||
.date-section.collapsed .date-header-left svg {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.date-label {
|
||
font-size: var(--text-sm);
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.date-sublabel {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-muted);
|
||
margin-left: var(--space-2);
|
||
}
|
||
|
||
.date-section.collapsed .date-content {
|
||
display: none;
|
||
}
|
||
|
||
.date-content {
|
||
padding: var(--space-4) 0;
|
||
}
|
||
|
||
/* Notes grid inside sections */
|
||
.notes-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: var(--space-4);
|
||
}
|
||
|
||
.note-card {
|
||
background: var(--bg-surface);
|
||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--space-4);
|
||
cursor: pointer;
|
||
transition: all var(--transition-base);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
[data-theme="light"] .note-card {
|
||
border-color: rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.note-card:hover {
|
||
border-color: var(--accent);
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.note-title {
|
||
font-size: var(--text-sm);
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.note-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-1);
|
||
}
|
||
|
||
/* Empty section */
|
||
.empty-section {
|
||
padding: var(--space-4);
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
font-size: var(--text-sm);
|
||
}
|
||
|
||
/* Note viewer overlay */
|
||
.note-viewer {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
z-index: 200;
|
||
overflow: auto;
|
||
}
|
||
|
||
.note-viewer.active {
|
||
display: block;
|
||
}
|
||
|
||
.note-viewer-content {
|
||
max-width: 800px;
|
||
margin: var(--space-5) auto;
|
||
background: var(--bg-base);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-lg);
|
||
min-height: calc(100vh - var(--space-10));
|
||
}
|
||
|
||
.note-viewer-header {
|
||
padding: var(--space-4) var(--space-5);
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
position: sticky;
|
||
top: 0;
|
||
background: var(--bg-base);
|
||
z-index: 10;
|
||
}
|
||
|
||
.note-viewer-header h2 {
|
||
font-size: var(--text-lg);
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.note-viewer-body {
|
||
padding: var(--space-6);
|
||
}
|
||
|
||
/* Markdown */
|
||
.markdown-body {
|
||
color: var(--text-secondary);
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.markdown-body h1 {
|
||
font-size: 1.8rem;
|
||
color: var(--text-primary);
|
||
margin-bottom: var(--space-4);
|
||
padding-bottom: var(--space-3);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.markdown-body h2 {
|
||
font-size: 1.3rem;
|
||
color: var(--accent);
|
||
margin-top: var(--space-6);
|
||
margin-bottom: var(--space-3);
|
||
}
|
||
|
||
.markdown-body h3 {
|
||
font-size: 1.1rem;
|
||
color: var(--text-primary);
|
||
margin-top: var(--space-5);
|
||
margin-bottom: var(--space-2);
|
||
}
|
||
|
||
.markdown-body p { margin-bottom: var(--space-4); }
|
||
|
||
.markdown-body ul, .markdown-body ol {
|
||
margin-bottom: var(--space-4);
|
||
padding-left: var(--space-6);
|
||
}
|
||
|
||
.markdown-body li { margin-bottom: var(--space-2); }
|
||
|
||
.markdown-body a {
|
||
color: var(--accent);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.markdown-body a:hover { text-decoration: underline; }
|
||
|
||
.markdown-body code {
|
||
background: var(--bg-surface);
|
||
padding: 2px 6px;
|
||
border-radius: var(--radius-sm);
|
||
font-family: var(--font-mono);
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.markdown-body pre {
|
||
background: var(--bg-surface);
|
||
padding: var(--space-4);
|
||
border-radius: var(--radius-md);
|
||
overflow-x: auto;
|
||
margin-bottom: var(--space-4);
|
||
}
|
||
|
||
.markdown-body pre code {
|
||
background: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.markdown-body blockquote {
|
||
border-left: 3px solid var(--accent);
|
||
padding-left: var(--space-4);
|
||
color: var(--text-muted);
|
||
margin-bottom: var(--space-4);
|
||
}
|
||
|
||
.markdown-body strong { color: var(--text-primary); }
|
||
|
||
/* Tag filter pills */
|
||
.tag-filter {
|
||
margin-bottom: var(--space-5);
|
||
}
|
||
|
||
.tag-filter-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
margin-bottom: var(--space-2);
|
||
}
|
||
|
||
.tag-filter-header svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
color: var(--text-muted);
|
||
transition: transform var(--transition-fast);
|
||
}
|
||
|
||
.tag-filter.collapsed .tag-filter-header svg {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.tag-pills-more {
|
||
display: none;
|
||
margin-top: var(--space-2);
|
||
}
|
||
|
||
.tag-pills-more.expanded {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
/* More tags toggle button - same style as pills */
|
||
.tag-pill.more-toggle {
|
||
background: rgba(100, 116, 139, 0.2);
|
||
border-color: rgba(100, 116, 139, 0.4);
|
||
color: var(--text-muted);
|
||
}
|
||
.tag-pill.more-toggle:hover {
|
||
background: rgba(100, 116, 139, 0.3);
|
||
}
|
||
.tag-pill.more-toggle.expanded {
|
||
background: rgba(100, 116, 139, 0.4);
|
||
}
|
||
|
||
/* Dimmed pills - tags not in visible notes */
|
||
.tag-pill.dimmed {
|
||
opacity: 0.35;
|
||
}
|
||
.tag-pill.dimmed:hover {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.filter-count {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-muted);
|
||
margin-left: var(--space-1);
|
||
}
|
||
|
||
.tag-filter-label {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-muted);
|
||
margin-bottom: var(--space-2);
|
||
display: block;
|
||
}
|
||
|
||
.tag-pills {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
.tag-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-1);
|
||
padding: var(--space-2) var(--space-3);
|
||
background: var(--bg-surface);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: var(--radius-full);
|
||
font-size: var(--text-sm);
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
transition: all var(--transition-fast);
|
||
user-select: none;
|
||
}
|
||
|
||
[data-theme="light"] .tag-pill {
|
||
border-color: rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.tag-pill:hover {
|
||
background: var(--bg-surface-hover);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.tag-pill.active {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
color: white;
|
||
}
|
||
|
||
/* Category pills (📁) - teal */
|
||
.tag-pill.category {
|
||
background: rgba(20, 184, 166, 0.2);
|
||
border-color: rgba(20, 184, 166, 0.5);
|
||
color: #14b8a6;
|
||
}
|
||
.tag-pill.category:hover {
|
||
background: rgba(20, 184, 166, 0.3);
|
||
}
|
||
.tag-pill.category.active {
|
||
background: #14b8a6;
|
||
border-color: #14b8a6;
|
||
color: white;
|
||
}
|
||
|
||
/* Domain pills (@) - purple */
|
||
.tag-pill.domain {
|
||
background: rgba(139, 92, 246, 0.2);
|
||
border-color: rgba(139, 92, 246, 0.5);
|
||
color: #8b5cf6;
|
||
}
|
||
.tag-pill.domain:hover {
|
||
background: rgba(139, 92, 246, 0.3);
|
||
}
|
||
.tag-pill.domain.active {
|
||
background: #8b5cf6;
|
||
border-color: #8b5cf6;
|
||
color: white;
|
||
}
|
||
|
||
/* Light mode adjustments */
|
||
[data-theme="light"] .tag-pill.category {
|
||
color: #0d9488;
|
||
}
|
||
[data-theme="light"] .tag-pill.domain {
|
||
color: #7c3aed;
|
||
}
|
||
|
||
.tag-pill-count {
|
||
font-size: var(--text-xs);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.clear-filters {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-muted);
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
padding: var(--space-2);
|
||
margin-left: var(--space-2);
|
||
}
|
||
|
||
.clear-filters:hover {
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* No results */
|
||
.no-results {
|
||
text-align: center;
|
||
padding: var(--space-10);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.no-results svg {
|
||
width: 48px;
|
||
height: 48px;
|
||
margin-bottom: var(--space-4);
|
||
opacity: 0.5;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.page-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.search-bar { width: 100%; }
|
||
|
||
.notes-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.note-viewer-content {
|
||
margin: 0;
|
||
border-radius: 0;
|
||
min-height: 100vh;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="header">
|
||
<a href="index.html" class="logo">
|
||
<i data-lucide="circle-dot"></i>
|
||
Echo
|
||
</a>
|
||
<nav class="nav">
|
||
<a href="index.html" class="nav-item">
|
||
<i data-lucide="layout-list"></i>
|
||
<span>Tasks</span>
|
||
</a>
|
||
<a href="notes.html" class="nav-item active">
|
||
<i data-lucide="file-text"></i>
|
||
<span>Notes</span>
|
||
</a>
|
||
<a href="files.html" class="nav-item">
|
||
<i data-lucide="folder"></i>
|
||
<span>Files</span>
|
||
</a>
|
||
<a href="grup-sprijin.html" class="nav-item">
|
||
<i data-lucide="heart-handshake"></i>
|
||
<span>Grup</span>
|
||
</a>
|
||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||
<i data-lucide="sun" id="themeIcon"></i>
|
||
</button>
|
||
</nav>
|
||
</header>
|
||
|
||
<main class="main">
|
||
<div class="page-header">
|
||
<h1 class="page-title">Notes</h1>
|
||
<div class="search-bar">
|
||
<input type="text" class="input" id="searchInput" placeholder="Caută în notițe..." oninput="filterNotes()">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tag-filter">
|
||
<div class="tag-pills" id="mainPills"></div>
|
||
<div class="tag-pills-more" id="tagPills"></div>
|
||
</div>
|
||
|
||
<div id="notesContainer">
|
||
<div class="no-results">
|
||
<i data-lucide="loader"></i>
|
||
<p>Se încarcă...</p>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Note viewer overlay -->
|
||
<div class="note-viewer" id="noteViewer" onclick="if(event.target === this) closeNote()">
|
||
<div class="note-viewer-content">
|
||
<div class="note-viewer-header">
|
||
<h2 id="viewerTitle">Titlu</h2>
|
||
<button class="btn btn-ghost" onclick="closeNote()">
|
||
<i data-lucide="x"></i>
|
||
</button>
|
||
</div>
|
||
<div class="note-viewer-body">
|
||
<div id="viewerContent" class="markdown-body"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Theme
|
||
function initTheme() {
|
||
const saved = localStorage.getItem('theme') || 'dark';
|
||
document.documentElement.setAttribute('data-theme', saved);
|
||
updateThemeIcon(saved);
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||
const next = current === 'dark' ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', next);
|
||
localStorage.setItem('theme', next);
|
||
updateThemeIcon(next);
|
||
}
|
||
|
||
function updateThemeIcon(theme) {
|
||
const icon = document.getElementById('themeIcon');
|
||
if (icon) {
|
||
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
initTheme();
|
||
lucide.createIcons();
|
||
|
||
const notesCache = {};
|
||
let notesIndex = [];
|
||
let lastFilteredNotes = null;
|
||
const notesBasePath = "notes-data/";
|
||
const indexPath = notesBasePath + "index.json";
|
||
|
||
// Load notes index from JSON
|
||
async function loadNotesIndex() {
|
||
try {
|
||
const response = await fetch(indexPath + '?t=' + Date.now());
|
||
const data = await response.json();
|
||
notesIndex = Array.isArray(data) ? data : (data.notes || []);
|
||
console.log(`Loaded ${notesIndex.length} notes from index.json`);
|
||
} catch (e) {
|
||
console.error('Failed to load notes index:', e);
|
||
notesIndex = [];
|
||
}
|
||
}
|
||
let selectedTags = new Set();
|
||
|
||
// Extract all tags with counts (including domains and categories)
|
||
function getAllTags() {
|
||
const tagCounts = {};
|
||
notesIndex.forEach(note => {
|
||
// Category tags (📁youtube, 📁retete)
|
||
if (note.category) {
|
||
const catTag = '📁' + note.category;
|
||
tagCounts[catTag] = (tagCounts[catTag] || 0) + 1;
|
||
}
|
||
// Domain tags (@work, @health, etc.)
|
||
if (note.domains) {
|
||
note.domains.forEach(domain => {
|
||
const domainTag = '@' + domain;
|
||
tagCounts[domainTag] = (tagCounts[domainTag] || 0) + 1;
|
||
});
|
||
}
|
||
// Regular tags
|
||
note.tags.forEach(tag => {
|
||
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
||
});
|
||
});
|
||
// Sort: categories first (📁), then domains (@), then by count
|
||
return Object.entries(tagCounts)
|
||
.sort((a, b) => {
|
||
const aIsCat = a[0].startsWith('📁');
|
||
const bIsCat = b[0].startsWith('📁');
|
||
const aIsDomain = a[0].startsWith('@');
|
||
const bIsDomain = b[0].startsWith('@');
|
||
if (aIsCat && !bIsCat) return -1;
|
||
if (!aIsCat && bIsCat) return 1;
|
||
if (aIsDomain && !bIsDomain) return -1;
|
||
if (!aIsDomain && bIsDomain) return 1;
|
||
return b[1] - a[1];
|
||
})
|
||
.map(([tag, count]) => ({ tag, count }));
|
||
}
|
||
|
||
// Render tag pills
|
||
function renderTagPills(visibleNotes = null) {
|
||
const mainContainer = document.getElementById('mainPills');
|
||
const moreContainer = document.getElementById('tagPills');
|
||
const tags = getAllTags();
|
||
|
||
// Calculate which tags appear in visible notes
|
||
const visibleTags = new Set();
|
||
if (visibleNotes && visibleNotes.length > 0) {
|
||
visibleNotes.forEach(note => {
|
||
note.tags.forEach(t => visibleTags.add(t));
|
||
(note.domains || []).forEach(d => visibleTags.add('@' + d));
|
||
if (note.category) visibleTags.add('📁' + note.category);
|
||
});
|
||
}
|
||
const hasFilter = visibleNotes !== null;
|
||
|
||
// Separate: categories + domains vs regular tags
|
||
const mainTags = tags.filter(({tag}) => tag.startsWith('📁') || tag.startsWith('@'));
|
||
const moreTags = tags.filter(({tag}) => !tag.startsWith('📁') && !tag.startsWith('@'));
|
||
|
||
// Check if more section is expanded
|
||
const isExpanded = moreContainer.classList.contains('expanded');
|
||
const activeMoreCount = [...selectedTags].filter(t => !t.startsWith('📁') && !t.startsWith('@')).length;
|
||
|
||
// Render main pills (categories + domains)
|
||
let mainHtml = mainTags.map(({ tag, count }) => {
|
||
let pillClass = 'tag-pill';
|
||
if (tag.startsWith('📁')) pillClass += ' category';
|
||
else if (tag.startsWith('@')) pillClass += ' domain';
|
||
if (selectedTags.has(tag)) pillClass += ' active';
|
||
// Dim tags not in visible notes (unless it's already selected)
|
||
if (hasFilter && !visibleTags.has(tag) && !selectedTags.has(tag)) pillClass += ' dimmed';
|
||
return `<span class="${pillClass}" onclick="toggleTag('${tag}')">
|
||
${tag} <span class="tag-pill-count">(${count})</span>
|
||
</span>`;
|
||
}).join('');
|
||
|
||
// Add "more tags" toggle button inline
|
||
if (moreTags.length > 0) {
|
||
const visibleMoreCount = moreTags.filter(({tag}) => visibleTags.has(tag)).length;
|
||
const moreLabel = activeMoreCount > 0
|
||
? `+${moreTags.length} tags (${activeMoreCount} active)`
|
||
: (hasFilter && visibleMoreCount > 0 ? `+${visibleMoreCount}/${moreTags.length} tags` : `+${moreTags.length} tags`);
|
||
mainHtml += `<span class="tag-pill more-toggle ${isExpanded ? 'expanded' : ''}" onclick="toggleMoreTags()">
|
||
${isExpanded ? '−' : '+'} ${moreLabel}
|
||
</span>`;
|
||
}
|
||
|
||
if (selectedTags.size > 0) {
|
||
mainHtml += `<button class="clear-filters" onclick="clearTagFilters()">✕ Clear</button>`;
|
||
}
|
||
mainContainer.innerHTML = mainHtml;
|
||
|
||
// Render more pills (regular tags)
|
||
const moreHtml = moreTags.map(({ tag, count }) => {
|
||
let pillClass = 'tag-pill';
|
||
if (selectedTags.has(tag)) pillClass += ' active';
|
||
if (hasFilter && !visibleTags.has(tag) && !selectedTags.has(tag)) pillClass += ' dimmed';
|
||
return `<span class="${pillClass}" onclick="toggleTag('${tag}')">
|
||
${tag} <span class="tag-pill-count">(${count})</span>
|
||
</span>`;
|
||
}).join('');
|
||
moreContainer.innerHTML = moreHtml;
|
||
}
|
||
|
||
// Toggle tag selection
|
||
function toggleTag(tag) {
|
||
if (selectedTags.has(tag)) {
|
||
selectedTags.delete(tag);
|
||
} else {
|
||
selectedTags.add(tag);
|
||
}
|
||
renderTagPills();
|
||
filterNotes();
|
||
}
|
||
|
||
// Clear all tag filters
|
||
function clearTagFilters() {
|
||
selectedTags.clear();
|
||
renderTagPills();
|
||
filterNotes();
|
||
}
|
||
|
||
// Toggle more tags section
|
||
function toggleMoreTags() {
|
||
const moreContainer = document.getElementById('tagPills');
|
||
moreContainer.classList.toggle('expanded');
|
||
renderTagPills(lastFilteredNotes);
|
||
}
|
||
|
||
// Filter notes by search and tags
|
||
function filterNotes() {
|
||
const query = document.getElementById('searchInput').value.toLowerCase().trim();
|
||
|
||
let filtered = notesIndex;
|
||
|
||
// Filter by selected tags (AND logic) - includes categories and domains
|
||
if (selectedTags.size > 0) {
|
||
filtered = filtered.filter(note => {
|
||
const allNoteTags = [
|
||
...note.tags,
|
||
...(note.domains || []).map(d => '@' + d),
|
||
note.category ? '📁' + note.category : null
|
||
].filter(Boolean);
|
||
return [...selectedTags].every(tag => allNoteTags.includes(tag));
|
||
});
|
||
}
|
||
|
||
// Filter by search query
|
||
if (query) {
|
||
filtered = filtered.filter(note => {
|
||
const titleMatch = note.title.toLowerCase().includes(query);
|
||
const tagsMatch = note.tags.some(t => t.toLowerCase().includes(query));
|
||
const contentMatch = (notesCache[note.file] || '').toLowerCase().includes(query);
|
||
return titleMatch || tagsMatch || contentMatch;
|
||
});
|
||
}
|
||
|
||
renderNotesAccordion(filtered);
|
||
|
||
// Save filtered notes for tag pills
|
||
lastFilteredNotes = filtered;
|
||
|
||
// Update tag pills to show which tags are in visible notes
|
||
renderTagPills(filtered);
|
||
}
|
||
|
||
// Group notes by date category
|
||
function groupNotesByDate(notes) {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const yesterday = new Date(today);
|
||
yesterday.setDate(yesterday.getDate() - 1);
|
||
|
||
const weekAgo = new Date(today);
|
||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||
|
||
const groups = {
|
||
today: { label: 'Azi', sublabel: formatDate(today), notes: [], expanded: true },
|
||
yesterday: { label: 'Ieri', sublabel: formatDate(yesterday), notes: [], expanded: false },
|
||
thisWeek: { label: 'Săptămâna aceasta', sublabel: '', notes: [], expanded: false },
|
||
older: { label: 'Mai vechi', sublabel: '', notes: [], expanded: false }
|
||
};
|
||
|
||
notes.forEach(note => {
|
||
const noteDate = new Date(note.date);
|
||
noteDate.setHours(0, 0, 0, 0);
|
||
|
||
if (noteDate.getTime() === today.getTime()) {
|
||
groups.today.notes.push(note);
|
||
} else if (noteDate.getTime() === yesterday.getTime()) {
|
||
groups.yesterday.notes.push(note);
|
||
} else if (noteDate >= weekAgo) {
|
||
groups.thisWeek.notes.push(note);
|
||
} else {
|
||
groups.older.notes.push(note);
|
||
}
|
||
});
|
||
|
||
return groups;
|
||
}
|
||
|
||
function formatDate(date) {
|
||
return date.toLocaleDateString('ro-RO', { day: 'numeric', month: 'short', year: 'numeric' });
|
||
}
|
||
|
||
function renderNotesAccordion(notes = notesIndex) {
|
||
const container = document.getElementById('notesContainer');
|
||
|
||
if (notes.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="no-results">
|
||
<i data-lucide="search-x"></i>
|
||
<p>Nicio notiță găsită</p>
|
||
</div>
|
||
`;
|
||
lucide.createIcons();
|
||
return;
|
||
}
|
||
|
||
const groups = groupNotesByDate(notes);
|
||
let html = '';
|
||
|
||
Object.entries(groups).forEach(([key, group]) => {
|
||
if (group.notes.length === 0) return;
|
||
|
||
const collapsed = group.expanded ? '' : 'collapsed';
|
||
|
||
html += `
|
||
<div class="date-section ${collapsed}" data-section="${key}">
|
||
<div class="date-header" onclick="toggleSection('${key}')">
|
||
<div class="date-header-left">
|
||
<i data-lucide="chevron-down"></i>
|
||
<span class="date-label">${group.label}</span>
|
||
<span class="date-sublabel">${group.sublabel}</span>
|
||
</div>
|
||
<span class="badge">${group.notes.length}</span>
|
||
</div>
|
||
<div class="date-content">
|
||
<div class="notes-grid">
|
||
${group.notes.map(note => renderNoteCard(note)).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
lucide.createIcons();
|
||
}
|
||
|
||
function renderNoteCard(note) {
|
||
return `
|
||
<div class="note-card" onclick="openNote('${note.file}')">
|
||
<div class="note-title">${note.title}</div>
|
||
<div class="note-tags">
|
||
${note.tags.map(t => `<span class="tag">${t}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function toggleSection(sectionKey) {
|
||
const section = document.querySelector(`[data-section="${sectionKey}"]`);
|
||
section.classList.toggle('collapsed');
|
||
}
|
||
|
||
async function openNote(file) {
|
||
const note = notesIndex.find(n => n.file === file);
|
||
if (!note) return;
|
||
|
||
document.getElementById('viewerTitle').textContent = note.title;
|
||
document.getElementById('viewerContent').innerHTML = '<p style="color: var(--text-muted)">Se încarcă...</p>';
|
||
document.getElementById('noteViewer').classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// Update URL
|
||
const noteId = file.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.md$/, '');
|
||
history.replaceState(null, '', '#' + noteId);
|
||
|
||
try {
|
||
let content = notesCache[file];
|
||
if (!content) {
|
||
const response = await fetch(notesBasePath + file);
|
||
content = await response.text();
|
||
notesCache[file] = content;
|
||
}
|
||
document.getElementById('viewerContent').innerHTML = marked.parse(content);
|
||
} catch (error) {
|
||
document.getElementById('viewerContent').innerHTML = `<p style="color: var(--error)">Eroare: ${error.message}</p>`;
|
||
}
|
||
}
|
||
|
||
function closeNote() {
|
||
document.getElementById('noteViewer').classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
history.replaceState(null, '', window.location.pathname);
|
||
}
|
||
|
||
async function preloadNotes() {
|
||
for (const note of notesIndex) {
|
||
try {
|
||
const response = await fetch(notesBasePath + note.file);
|
||
notesCache[note.file] = await response.text();
|
||
} catch (e) {
|
||
notesCache[note.file] = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
// searchNotes replaced by filterNotes above
|
||
|
||
// Handle hash for deep links
|
||
function checkHash() {
|
||
if (window.location.hash) {
|
||
const id = window.location.hash.slice(1);
|
||
const note = notesIndex.find(n => {
|
||
const noteId = n.file.replace(/^\d{4}-\d{2}-\d{2}_/, '').replace(/\.md$/, '');
|
||
return noteId === id;
|
||
});
|
||
if (note) {
|
||
openNote(note.file);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ESC to close
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape') closeNote();
|
||
});
|
||
|
||
// Init - load index first, then render
|
||
async function init() {
|
||
await loadNotesIndex();
|
||
renderTagPills();
|
||
renderNotesAccordion();
|
||
preloadNotes();
|
||
checkHash();
|
||
}
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|