1267 lines
44 KiB
HTML
1267 lines
44 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 · KB</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;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.note-file-link {
|
|
opacity: 0.4;
|
|
transition: opacity var(--transition-fast);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.note-file-link:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.note-file-link svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.note-tags {
|
|
display: inline;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.note-tag {
|
|
font-size: var(--text-sm);
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.note-tag.domain {
|
|
color: #f97316;
|
|
}
|
|
|
|
.note-tag.type {
|
|
color: #8b5cf6;
|
|
}
|
|
|
|
.note-tag.tag {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.note-tags-toggle {
|
|
color: #94a3b8;
|
|
cursor: pointer;
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.note-tags-toggle:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.note-tags-full {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
/* 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: flex-start;
|
|
gap: var(--space-3);
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--bg-base);
|
|
z-index: 10;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.note-viewer-header h2 {
|
|
font-size: var(--text-lg);
|
|
color: var(--text-primary);
|
|
font-weight: 600;
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.viewer-path {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
font-family: var(--font-mono);
|
|
text-decoration: none;
|
|
opacity: 0.7;
|
|
transition: opacity var(--transition-fast);
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.viewer-path:hover {
|
|
opacity: 1;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.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); }
|
|
|
|
/* Filter bar with pills */
|
|
.filter-bar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
margin-bottom: var(--space-4);
|
|
padding: var(--space-3);
|
|
background: var(--bg-surface);
|
|
border-radius: var(--radius-lg);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
[data-theme="light"] .filter-bar {
|
|
border-color: rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.filter-separator {
|
|
color: var(--text-muted);
|
|
opacity: 0.3;
|
|
margin: 0 var(--space-1);
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
padding: var(--space-1) var(--space-2);
|
|
background: var(--bg-secondary);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: var(--radius-full);
|
|
font-size: var(--text-sm);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
[data-theme="light"] .filter-pill {
|
|
border-color: rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.filter-pill:hover {
|
|
background: var(--bg-surface-hover);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.filter-pill.active {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.filter-pill.dimmed {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.filter-pill.dimmed:hover {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* Category pills - teal */
|
|
.filter-pill.category {
|
|
background: rgba(20, 184, 166, 0.15);
|
|
border-color: rgba(20, 184, 166, 0.4);
|
|
color: #14b8a6;
|
|
}
|
|
.filter-pill.category:hover {
|
|
background: rgba(20, 184, 166, 0.25);
|
|
}
|
|
.filter-pill.category.active {
|
|
background: #14b8a6;
|
|
color: white;
|
|
}
|
|
|
|
/* Project pills - blue */
|
|
.filter-pill.project {
|
|
background: rgba(59, 130, 246, 0.15);
|
|
border-color: rgba(59, 130, 246, 0.4);
|
|
color: #3b82f6;
|
|
}
|
|
.filter-pill.project:hover {
|
|
background: rgba(59, 130, 246, 0.25);
|
|
}
|
|
.filter-pill.project.active {
|
|
background: #3b82f6;
|
|
color: white;
|
|
}
|
|
|
|
/* Domain pills - orange */
|
|
.filter-pill.domain {
|
|
background: rgba(249, 115, 22, 0.15);
|
|
border-color: rgba(249, 115, 22, 0.4);
|
|
color: #f97316;
|
|
}
|
|
.filter-pill.domain:hover {
|
|
background: rgba(249, 115, 22, 0.25);
|
|
}
|
|
.filter-pill.domain.active {
|
|
background: #f97316;
|
|
color: white;
|
|
}
|
|
|
|
/* Type pills - purple */
|
|
.filter-pill.type {
|
|
background: rgba(139, 92, 246, 0.15);
|
|
border-color: rgba(139, 92, 246, 0.4);
|
|
color: #8b5cf6;
|
|
}
|
|
.filter-pill.type:hover {
|
|
background: rgba(139, 92, 246, 0.25);
|
|
}
|
|
.filter-pill.type.active {
|
|
background: #8b5cf6;
|
|
color: white;
|
|
}
|
|
|
|
/* Tag pills - gray */
|
|
.filter-pill.tag {
|
|
background: rgba(100, 116, 139, 0.15);
|
|
border-color: rgba(100, 116, 139, 0.4);
|
|
color: #94a3b8;
|
|
}
|
|
.filter-pill.tag:hover {
|
|
background: rgba(100, 116, 139, 0.25);
|
|
}
|
|
.filter-pill.tag.active {
|
|
background: #64748b;
|
|
color: white;
|
|
}
|
|
|
|
.filter-pill-count {
|
|
font-size: var(--text-xs);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.filter-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
margin-left: auto;
|
|
}
|
|
|
|
.filter-btn {
|
|
padding: var(--space-2);
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
border-radius: var(--radius-md);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.filter-btn:hover {
|
|
background: var(--bg-surface-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.filter-btn.clear:hover {
|
|
background: rgba(239, 68, 68, 0.2);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.filter-btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.more-tags-toggle {
|
|
color: var(--text-muted);
|
|
font-size: var(--text-sm);
|
|
cursor: pointer;
|
|
padding: var(--space-1) var(--space-2);
|
|
}
|
|
|
|
.more-tags-toggle:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.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>KB</span>
|
|
</a>
|
|
<a href="files.html" class="nav-item">
|
|
<i data-lucide="folder"></i>
|
|
<span>Files</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">Knowledge Base</h1>
|
|
<div class="search-bar">
|
|
<input type="text" class="input" id="searchInput" placeholder="Caută în notițe..." oninput="filterNotes()">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filter-bar" id="filterBar">
|
|
<div class="filter-group" id="categoryPills"></div>
|
|
<span class="filter-separator" id="sep1">|</span>
|
|
<div class="filter-group" id="projectPills"></div>
|
|
<span class="filter-separator" id="sep2">|</span>
|
|
<div class="filter-group" id="typePills"></div>
|
|
<span class="filter-separator" id="sep3">|</span>
|
|
<div class="filter-group" id="tagPills"></div>
|
|
<span class="more-tags-toggle" id="moreTagsToggle" onclick="toggleMoreTags()"></span>
|
|
<div class="filter-actions">
|
|
<button class="filter-btn clear" onclick="clearFilters()" title="Resetează" id="clearBtn" style="display: none;">
|
|
<i data-lucide="x"></i>
|
|
</button>
|
|
<button class="filter-btn" onclick="refreshIndex()" title="Reîncarcă">
|
|
<i data-lucide="refresh-cw" id="refreshIcon"></i>
|
|
</button>
|
|
</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>
|
|
<a id="viewerPath" href="#" class="viewer-path" target="_blank"></a>
|
|
<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 = [];
|
|
}
|
|
}
|
|
// Current filter state
|
|
let selectedFilters = {
|
|
category: null,
|
|
project: null,
|
|
domain: null,
|
|
type: null,
|
|
tags: new Set()
|
|
};
|
|
let showAllTags = false;
|
|
|
|
// Get counts for each filter value
|
|
function getFilterCounts(filteredNotes = null) {
|
|
const notes = filteredNotes || notesIndex;
|
|
const counts = {
|
|
categories: {},
|
|
projects: {},
|
|
domains: {},
|
|
types: {},
|
|
tags: {}
|
|
};
|
|
|
|
notes.forEach(note => {
|
|
if (note.category) counts.categories[note.category] = (counts.categories[note.category] || 0) + 1;
|
|
if (note.project) counts.projects[note.project] = (counts.projects[note.project] || 0) + 1;
|
|
if (note.domains) note.domains.forEach(d => counts.domains[d] = (counts.domains[d] || 0) + 1);
|
|
if (note.types) note.types.forEach(t => counts.types[t] = (counts.types[t] || 0) + 1);
|
|
if (note.tags) note.tags.forEach(t => counts.tags[t] = (counts.tags[t] || 0) + 1);
|
|
});
|
|
|
|
return counts;
|
|
}
|
|
|
|
// Render filter pills
|
|
function renderFilterPills(filteredNotes = null) {
|
|
const counts = getFilterCounts();
|
|
const visibleCounts = filteredNotes ? getFilterCounts(filteredNotes) : counts;
|
|
|
|
// Categories
|
|
const catHtml = Object.entries(counts.categories)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([cat, count]) => {
|
|
const isActive = selectedFilters.category === cat;
|
|
const isVisible = visibleCounts.categories[cat] > 0;
|
|
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
|
|
return `<span class="filter-pill category ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('category', '${cat}')">
|
|
📁${cat} <span class="filter-pill-count">${count}</span>
|
|
</span>`;
|
|
}).join('');
|
|
document.getElementById('categoryPills').innerHTML = catHtml;
|
|
|
|
// Projects (only show if category=projects or no category selected)
|
|
const showProjects = !selectedFilters.category || selectedFilters.category === 'projects';
|
|
const projHtml = showProjects && Object.keys(counts.projects).length > 0 ? Object.entries(counts.projects)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([proj, count]) => {
|
|
const isActive = selectedFilters.project === proj;
|
|
const isVisible = visibleCounts.projects[proj] > 0;
|
|
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
|
|
return `<span class="filter-pill project ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('project', '${proj}')">
|
|
📂${proj} <span class="filter-pill-count">${count}</span>
|
|
</span>`;
|
|
}).join('') : '';
|
|
document.getElementById('projectPills').innerHTML = projHtml;
|
|
document.getElementById('sep1').style.display = projHtml ? '' : 'none';
|
|
|
|
// Domains (@work, @health, etc.) - orange
|
|
const domainHtml = Object.entries(counts.domains)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([domain, count]) => {
|
|
const isActive = selectedFilters.domain === domain;
|
|
const isVisible = visibleCounts.domains[domain] > 0;
|
|
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
|
|
return `<span class="filter-pill domain ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('domain', '${domain}')">
|
|
@${domain} <span class="filter-pill-count">${count}</span>
|
|
</span>`;
|
|
}).join('');
|
|
|
|
// Types (@meditatie, @exercitiu, etc.)
|
|
const typeHtml = Object.entries(counts.types)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([type, count]) => {
|
|
const isActive = selectedFilters.type === type;
|
|
const isVisible = visibleCounts.types[type] > 0;
|
|
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
|
|
return `<span class="filter-pill type ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('type', '${type}')">
|
|
@${type} <span class="filter-pill-count">${count}</span>
|
|
</span>`;
|
|
}).join('');
|
|
|
|
// Combine domains and types in typePills
|
|
document.getElementById('typePills').innerHTML = domainHtml + typeHtml;
|
|
document.getElementById('sep2').style.display = (domainHtml || typeHtml) ? '' : 'none';
|
|
|
|
// Tags - collapsed by default, show only count
|
|
const tagEntries = Object.entries(counts.tags).sort((a, b) => b[1] - a[1]);
|
|
const activeTagsCount = selectedFilters.tags.size;
|
|
|
|
let tagHtml = '';
|
|
if (showAllTags) {
|
|
// Show all tags
|
|
tagHtml = tagEntries.map(([tag, count]) => {
|
|
const isActive = selectedFilters.tags.has(tag);
|
|
const isVisible = visibleCounts.tags[tag] > 0;
|
|
const dimmed = filteredNotes && !isVisible && !isActive ? 'dimmed' : '';
|
|
return `<span class="filter-pill tag ${isActive ? 'active' : ''} ${dimmed}" onclick="toggleFilter('tag', '${tag}')">
|
|
#${tag} <span class="filter-pill-count">${count}</span>
|
|
</span>`;
|
|
}).join('');
|
|
} else if (activeTagsCount > 0) {
|
|
// Show only active tags
|
|
tagHtml = [...selectedFilters.tags].map(tag => {
|
|
const count = counts.tags[tag] || 0;
|
|
return `<span class="filter-pill tag active" onclick="toggleFilter('tag', '${tag}')">
|
|
#${tag} <span class="filter-pill-count">${count}</span>
|
|
</span>`;
|
|
}).join('');
|
|
}
|
|
document.getElementById('tagPills').innerHTML = tagHtml;
|
|
document.getElementById('sep3').style.display = (tagHtml || tagEntries.length > 0) ? '' : 'none';
|
|
|
|
// More tags toggle
|
|
const moreToggle = document.getElementById('moreTagsToggle');
|
|
if (tagEntries.length > 0) {
|
|
const hiddenCount = activeTagsCount > 0 ? tagEntries.length - activeTagsCount : tagEntries.length;
|
|
if (showAllTags) {
|
|
moreToggle.textContent = `-${hiddenCount} tags`;
|
|
} else {
|
|
moreToggle.textContent = `+${hiddenCount} tags`;
|
|
}
|
|
moreToggle.style.display = '';
|
|
} else {
|
|
moreToggle.style.display = 'none';
|
|
}
|
|
|
|
// Clear button
|
|
const hasFilters = selectedFilters.category || selectedFilters.project ||
|
|
selectedFilters.domain || selectedFilters.type || selectedFilters.tags.size > 0;
|
|
document.getElementById('clearBtn').style.display = hasFilters ? '' : 'none';
|
|
}
|
|
|
|
// Toggle filter
|
|
function toggleFilter(filterType, value) {
|
|
if (filterType === 'tag') {
|
|
if (selectedFilters.tags.has(value)) {
|
|
selectedFilters.tags.delete(value);
|
|
} else {
|
|
selectedFilters.tags.add(value);
|
|
}
|
|
} else {
|
|
selectedFilters[filterType] = selectedFilters[filterType] === value ? null : value;
|
|
// Reset child filters when parent changes
|
|
if (filterType === 'category' && value !== 'projects') {
|
|
selectedFilters.project = null;
|
|
}
|
|
}
|
|
filterNotes();
|
|
}
|
|
|
|
// Toggle more tags
|
|
function toggleMoreTags() {
|
|
showAllTags = !showAllTags;
|
|
renderFilterPills(lastFilteredNotes);
|
|
}
|
|
|
|
// Toggle tags in note card
|
|
function toggleNoteTags(noteId) {
|
|
const full = document.getElementById('tags_' + noteId);
|
|
const toggle = full.parentElement;
|
|
const count = toggle.querySelector('.note-tags-count');
|
|
if (full.style.display === 'none') {
|
|
full.style.display = 'inline';
|
|
count.style.display = 'none';
|
|
} else {
|
|
full.style.display = 'none';
|
|
count.style.display = 'inline';
|
|
}
|
|
}
|
|
|
|
// Clear all filters
|
|
function clearFilters() {
|
|
selectedFilters = { category: null, project: null, domain: null, type: null, tags: new Set() };
|
|
showAllTags = false;
|
|
document.getElementById('searchInput').value = '';
|
|
filterNotes();
|
|
}
|
|
|
|
// Refresh index from server
|
|
async function refreshIndex() {
|
|
const icon = document.getElementById('refreshIcon');
|
|
if (icon) icon.style.animation = 'spin 1s linear infinite';
|
|
|
|
try {
|
|
const response = await fetch('api/refresh-index', { method: 'POST' });
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
await loadNotesIndex();
|
|
filterNotes();
|
|
lucide.createIcons();
|
|
console.log('Index refreshed:', data.message);
|
|
} else {
|
|
console.error('Refresh failed:', data.error);
|
|
alert('Eroare: ' + (data.error || 'Unknown error'));
|
|
}
|
|
} catch (e) {
|
|
console.error('Refresh error:', e);
|
|
alert('Eroare la refresh: ' + e.message);
|
|
} finally {
|
|
if (icon) icon.style.animation = '';
|
|
}
|
|
}
|
|
|
|
// Filter notes
|
|
function filterNotes() {
|
|
const query = document.getElementById('searchInput').value.toLowerCase().trim();
|
|
let filtered = notesIndex;
|
|
|
|
// Filter by category
|
|
if (selectedFilters.category) {
|
|
filtered = filtered.filter(n => n.category === selectedFilters.category);
|
|
}
|
|
|
|
// Filter by project
|
|
if (selectedFilters.project) {
|
|
filtered = filtered.filter(n => n.project === selectedFilters.project);
|
|
}
|
|
|
|
// Filter by domain (@work, @health, etc.)
|
|
if (selectedFilters.domain) {
|
|
filtered = filtered.filter(n => n.domains && n.domains.includes(selectedFilters.domain));
|
|
}
|
|
|
|
// Filter by type (@meditatie, @exercitiu, etc.)
|
|
if (selectedFilters.type) {
|
|
filtered = filtered.filter(n => n.types && n.types.includes(selectedFilters.type));
|
|
}
|
|
|
|
// Filter by tags (AND)
|
|
if (selectedFilters.tags.size > 0) {
|
|
filtered = filtered.filter(n => {
|
|
const noteTags = n.tags || [];
|
|
return [...selectedFilters.tags].every(t => noteTags.includes(t));
|
|
});
|
|
}
|
|
|
|
// Filter by search
|
|
if (query) {
|
|
filtered = filtered.filter(n => {
|
|
const titleMatch = n.title.toLowerCase().includes(query);
|
|
const tagsMatch = (n.tags || []).some(t => t.toLowerCase().includes(query));
|
|
const contentMatch = (notesCache[n.file] || '').toLowerCase().includes(query);
|
|
return titleMatch || tagsMatch || contentMatch;
|
|
});
|
|
}
|
|
|
|
lastFilteredNotes = filtered;
|
|
renderFilterPills(filtered);
|
|
renderNotesAccordion(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) {
|
|
// Domains (portocaliu), Types (mov), Tags colapsate cu expand
|
|
const tags = note.tags || [];
|
|
const noteId = note.file.replace(/[^a-zA-Z0-9]/g, '_');
|
|
const tagsHtml = tags.length > 0
|
|
? `<span class="note-tags-toggle" onclick="event.stopPropagation(); toggleNoteTags('${noteId}')">
|
|
<span class="note-tags-count">${tags.length} tags</span>
|
|
<span class="note-tags-full" id="tags_${noteId}" style="display:none">${tags.join(' · ')}</span>
|
|
</span>`
|
|
: '';
|
|
|
|
const allTags = [
|
|
...(note.domains || []).map(d => `<span class="note-tag domain">${d}</span>`),
|
|
...(note.types || []).map(t => `<span class="note-tag type">${t}</span>`),
|
|
tagsHtml
|
|
].filter(Boolean).join('');
|
|
|
|
// Convert notes-data/ to kb/ for files.html links
|
|
const filesPath = note.file.replace(/^notes-data\//, 'kb/');
|
|
return `
|
|
<div class="note-card" onclick="openNote('${note.file}')">
|
|
<div class="note-title">
|
|
${note.title}
|
|
<a href="files.html#${filesPath}" class="note-file-link" onclick="event.stopPropagation()" title="${filesPath}">
|
|
<i data-lucide="external-link"></i>
|
|
</a>
|
|
</div>
|
|
<div class="note-tags">${allTags}</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;
|
|
const pathEl = document.getElementById('viewerPath');
|
|
// Convert notes-data/ to kb/ for display and links
|
|
const filesPath = note.file.replace(/^notes-data\//, 'kb/');
|
|
pathEl.textContent = filesPath;
|
|
pathEl.href = 'files.html#' + filesPath;
|
|
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(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(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();
|
|
renderFilterPills();
|
|
renderNotesAccordion(notesIndex);
|
|
lucide.createIcons();
|
|
preloadNotes();
|
|
checkHash();
|
|
}
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|