1945 lines
58 KiB
HTML
1945 lines
58 KiB
HTML
{% extends "layout.html" %}
|
||
|
||
{% block content %}
|
||
<div class="timetrack-container notes-list-container">
|
||
<div class="admin-header">
|
||
<h2>Notes</h2>
|
||
<div class="admin-actions">
|
||
<button type="button" class="btn btn-sm btn-secondary" id="toggle-sidebar">
|
||
<span>📁</span> Toggle Folders
|
||
</button>
|
||
<a href="{{ url_for('notes.notes_folders') }}" class="btn btn-sm btn-info">
|
||
<span>⚙️</span> Manage Folders
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="notes-layout">
|
||
<!-- Folder Tree Sidebar -->
|
||
<div class="folder-sidebar" id="folder-sidebar">
|
||
<div class="create-note-section">
|
||
<a href="{{ url_for('notes.create_note') }}" class="btn btn-create-note">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||
</svg>
|
||
Create New Note
|
||
</a>
|
||
</div>
|
||
<div class="sidebar-header">
|
||
<h3>Folders</h3>
|
||
<button type="button" class="btn-icon" onclick="showCreateFolderModal()" title="Create Folder">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="folder-tree" id="folder-tree">
|
||
<div class="folder-item root-folder" data-folder="">
|
||
<div class="folder-content {% if not folder_filter %}active{% endif %}" onclick="filterByFolder('')">
|
||
<span class="folder-icon">🏠</span>
|
||
<span class="folder-name">All Notes</span>
|
||
<span class="folder-count">({{ notes|length }})</span>
|
||
</div>
|
||
</div>
|
||
{{ render_folder_tree(folder_tree)|safe }}
|
||
</div>
|
||
|
||
<!-- Tags Section -->
|
||
<div class="tags-section">
|
||
<div class="section-header">
|
||
<div class="section-title" onclick="toggleSection('tags')">
|
||
<span class="section-toggle" id="tags-toggle">▼</span>
|
||
<h3>Tags</h3>
|
||
<span class="tags-count">({{ all_tags|length }})</span>
|
||
</div>
|
||
<button type="button" class="btn-icon" onclick="showAddTagModal()" title="Add Tag">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="tags-list" id="tags-list">
|
||
{% if tag_filter %}
|
||
<div class="tag-item clear-filter" onclick="filterByTag('')">
|
||
<span class="tag-icon">✖️</span>
|
||
<span class="tag-name">Clear filter</span>
|
||
</div>
|
||
{% endif %}
|
||
{% if all_tags %}
|
||
{% for tag in all_tags %}
|
||
<div class="tag-item {% if tag_filter == tag %}active{% endif %}" onclick="filterByTag('{{ tag }}')">
|
||
<span class="tag-icon">🏷️</span>
|
||
<span class="tag-name">{{ tag }}</span>
|
||
<span class="tag-count">({{ tag_counts.get(tag, 0) }})</span>
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div class="no-tags">No tags yet</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Visibility Section -->
|
||
<div class="visibility-section">
|
||
<div class="section-header">
|
||
<div class="section-title" onclick="toggleSection('visibility')">
|
||
<span class="section-toggle" id="visibility-toggle">▼</span>
|
||
<h3>Visibility</h3>
|
||
</div>
|
||
</div>
|
||
<div class="visibility-list" id="visibility-list">
|
||
<div class="visibility-item {% if not visibility_filter %}active{% endif %}" onclick="filterByVisibility('')">
|
||
<span class="visibility-icon">👁️</span>
|
||
<span class="visibility-name">All Notes</span>
|
||
<span class="visibility-count">({{ notes|length }})</span>
|
||
</div>
|
||
<div class="visibility-item {% if visibility_filter == 'private' %}active{% endif %}" onclick="filterByVisibility('private')">
|
||
<span class="visibility-icon">🔒</span>
|
||
<span class="visibility-name">Private</span>
|
||
<span class="visibility-count">({{ visibility_counts.get('private', 0) }})</span>
|
||
</div>
|
||
<div class="visibility-item {% if visibility_filter == 'team' %}active{% endif %}" onclick="filterByVisibility('team')">
|
||
<span class="visibility-icon">👥</span>
|
||
<span class="visibility-name">Team</span>
|
||
<span class="visibility-count">({{ visibility_counts.get('team', 0) }})</span>
|
||
</div>
|
||
<div class="visibility-item {% if visibility_filter == 'company' %}active{% endif %}" onclick="filterByVisibility('company')">
|
||
<span class="visibility-icon">🏢</span>
|
||
<span class="visibility-name">Company</span>
|
||
<span class="visibility-count">({{ visibility_counts.get('company', 0) }})</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Notes Content -->
|
||
<div class="notes-content">
|
||
|
||
<!-- Filter Section -->
|
||
<div class="notes-filter-section">
|
||
<form method="GET" action="{{ url_for('notes.notes_list') }}" class="filter-form" id="filter-form">
|
||
<!-- Hidden inputs for sidebar filters -->
|
||
<input type="hidden" name="folder" id="folder" value="{{ folder_filter or '' }}">
|
||
<input type="hidden" name="tag" id="tag" value="{{ tag_filter or '' }}">
|
||
<input type="hidden" name="visibility" id="visibility" value="{{ visibility_filter or '' }}">
|
||
|
||
<div class="filter-row">
|
||
<div class="search-container">
|
||
<div class="search-bar">
|
||
{% if folder_filter or tag_filter or visibility_filter %}
|
||
<div class="active-filters-inline">
|
||
{% if folder_filter %}
|
||
<span class="filter-chip" onclick="clearFilter('folder')">
|
||
📁 {{ folder_filter }} <span class="remove">×</span>
|
||
</span>
|
||
{% endif %}
|
||
{% if tag_filter %}
|
||
<span class="filter-chip" onclick="clearFilter('tag')">
|
||
🏷️ {{ tag_filter }} <span class="remove">×</span>
|
||
</span>
|
||
{% endif %}
|
||
{% if visibility_filter %}
|
||
<span class="filter-chip" onclick="clearFilter('visibility')">
|
||
{% if visibility_filter == 'private' %}🔒{% elif visibility_filter == 'team' %}👥{% else %}🏢{% endif %}
|
||
{{ visibility_filter|title }} <span class="remove">×</span>
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
<input type="text" name="search" id="search"
|
||
placeholder="{% if folder_filter or tag_filter or visibility_filter %}Add search terms...{% else %}Search notes by title or content...{% endif %}"
|
||
value="{{ request.args.get('search', '') }}" class="search-input">
|
||
<button type="submit" class="btn-search" title="Search">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{% if notes %}
|
||
<!-- Notes Toolbar -->
|
||
<div class="notes-toolbar">
|
||
<div class="toolbar-left">
|
||
<span class="notes-count">{{ notes|length }} notes</span>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<button type="button" class="btn-view-toggle" id="toggle-view" title="Toggle view">
|
||
<svg class="icon-grid" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="display: none;">
|
||
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"/>
|
||
</svg>
|
||
<svg class="icon-list" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||
</svg>
|
||
<span class="view-label">Table View</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Table View (Default) -->
|
||
<div id="table-view" class="notes-view">
|
||
<table class="notes-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="column-pin"></th>
|
||
<th class="column-title">Title</th>
|
||
<th class="column-folder">Folder</th>
|
||
<th class="column-visibility">Visibility</th>
|
||
<th class="column-tags">Tags</th>
|
||
<th class="column-updated">Updated</th>
|
||
<th class="column-actions">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for note in notes %}
|
||
<tr class="note-row {% if note.is_pinned %}pinned{% endif %}" data-note-slug="{{ note.slug }}">
|
||
<td class="column-pin">
|
||
{% if note.is_pinned %}
|
||
<span class="pin-icon" title="Pinned">📌</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="column-title">
|
||
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="note-link">
|
||
{{ note.title }}
|
||
</a>
|
||
<div class="note-associations">
|
||
{% if note.project %}
|
||
<span class="association-badge project" title="Project: {{ note.project.name }}">
|
||
📁 {{ note.project.code }}
|
||
</span>
|
||
{% endif %}
|
||
{% if note.task %}
|
||
<span class="association-badge task" title="Task #{{ note.task.id }}">
|
||
✓ #{{ note.task.id }}
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
<td class="column-folder">
|
||
{% if note.folder %}
|
||
<span class="folder-path">{{ note.folder }}</span>
|
||
{% else %}
|
||
<span class="folder-path muted">Root</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="column-visibility">
|
||
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
|
||
{% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %}
|
||
{{ note.visibility.value }}
|
||
</span>
|
||
</td>
|
||
<td class="column-tags">
|
||
{% if note.tags %}
|
||
{% for tag in note.get_tags_list() %}
|
||
<a href="{{ url_for('notes.notes_list', tag=tag) }}" class="tag-badge">{{ tag }}</a>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</td>
|
||
<td class="column-updated">
|
||
<span class="date-text">{{ note.updated_at|format_date }}</span>
|
||
</td>
|
||
<td class="column-actions">
|
||
<div class="note-actions">
|
||
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="btn-action" title="View">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
||
</svg>
|
||
</a>
|
||
<a href="{{ url_for('notes.view_note_mindmap', slug=note.slug) }}" class="btn-action" title="Mind Map">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<circle cx="8" cy="8" r="2"/>
|
||
<circle cx="3" cy="3" r="1.5"/>
|
||
<circle cx="13" cy="3" r="1.5"/>
|
||
<circle cx="3" cy="13" r="1.5"/>
|
||
<circle cx="13" cy="13" r="1.5"/>
|
||
<path d="M6.5 6.5L4 4M9.5 6.5L12 4M6.5 9.5L4 12M9.5 9.5L12 12" stroke="currentColor" fill="none"/>
|
||
</svg>
|
||
</a>
|
||
<a href="{{ url_for('notes_download.download_note', slug=note.slug, format='md') }}" class="btn-action" title="Download as Markdown">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||
</svg>
|
||
</a>
|
||
{% if note.can_user_edit(g.user) %}
|
||
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn-action" title="Edit">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||
</svg>
|
||
</a>
|
||
<form method="POST" action="{{ url_for('notes.delete_note', slug=note.slug) }}" style="display: inline;"
|
||
onsubmit="return confirm('Are you sure you want to delete this note?')">
|
||
<button type="submit" class="btn-action btn-action-danger" title="Delete">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||
</svg>
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Grid View (Hidden by default) -->
|
||
<div id="grid-view" class="notes-view" style="display: none;">
|
||
<div class="notes-grid">
|
||
{% for note in notes %}
|
||
<div class="note-card" data-note-slug="{{ note.slug }}">
|
||
<div class="note-header">
|
||
<h3 class="note-title">
|
||
<a href="{{ url_for('notes.view_note', slug=note.slug) }}">{{ note.title }}</a>
|
||
{% if note.is_pinned %}
|
||
<span class="pin-icon" title="Pinned">📌</span>
|
||
{% endif %}
|
||
</h3>
|
||
<div class="note-meta">
|
||
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
|
||
{% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %}
|
||
{{ note.visibility.value }}
|
||
</span>
|
||
{% if note.folder %}
|
||
<span class="folder-badge">📁 {{ note.folder }}</span>
|
||
{% endif %}
|
||
<span class="note-date">{{ note.updated_at|format_date }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="note-preview">
|
||
{{ note.get_preview()|safe }}
|
||
</div>
|
||
|
||
<div class="note-footer">
|
||
<div class="note-tags">
|
||
{% if note.tags %}
|
||
{% for tag in note.get_tags_list() %}
|
||
<a href="{{ url_for('notes.notes_list', tag=tag) }}" class="tag-badge">{{ tag }}</a>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</div>
|
||
<div class="note-associations">
|
||
{% if note.project %}
|
||
<span class="association-badge project">
|
||
📁 {{ note.project.code }}
|
||
</span>
|
||
{% endif %}
|
||
{% if note.task %}
|
||
<span class="association-badge task">
|
||
✓ Task #{{ note.task.id }}
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="note-actions">
|
||
<a href="{{ url_for('notes.view_note', slug=note.slug) }}" class="btn-action" title="View">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
||
</svg>
|
||
</a>
|
||
<a href="{{ url_for('notes.view_note_mindmap', slug=note.slug) }}" class="btn-action" title="Mind Map">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<circle cx="8" cy="8" r="2"/>
|
||
<circle cx="3" cy="3" r="1.5"/>
|
||
<circle cx="13" cy="3" r="1.5"/>
|
||
<circle cx="3" cy="13" r="1.5"/>
|
||
<circle cx="13" cy="13" r="1.5"/>
|
||
<path d="M6.5 6.5L4 4M9.5 6.5L12 4M6.5 9.5L4 12M9.5 9.5L12 12" stroke="currentColor" fill="none"/>
|
||
</svg>
|
||
</a>
|
||
<a href="{{ url_for('notes_download.download_note', slug=note.slug, format='md') }}" class="btn-action" title="Download as Markdown">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||
</svg>
|
||
</a>
|
||
{% if note.can_user_edit(g.user) %}
|
||
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn-action" title="Edit">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||
</svg>
|
||
</a>
|
||
<form method="POST" action="{{ url_for('notes.delete_note', slug=note.slug) }}" style="display: inline;"
|
||
onsubmit="return confirm('Are you sure you want to delete this note?')">
|
||
<button type="submit" class="btn-action btn-action-danger" title="Delete">
|
||
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||
</svg>
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
{% else %}
|
||
<div class="no-data">
|
||
<p>No notes found. <a href="{{ url_for('notes.create_note') }}">Create your first note</a>.</p>
|
||
</div>
|
||
{% endif %}
|
||
</div> <!-- End notes-content -->
|
||
</div> <!-- End notes-layout -->
|
||
</div> <!-- End notes-list-container -->
|
||
|
||
<!-- Folder Download Dropdown (Hidden, positioned dynamically) -->
|
||
<div id="folder-download-dropdown" class="folder-download-dropdown">
|
||
<a href="#" data-format="md">Download as Markdown</a>
|
||
<a href="#" data-format="html">Download as HTML</a>
|
||
<a href="#" data-format="txt">Download as Text</a>
|
||
</div>
|
||
|
||
<style>
|
||
/* Notes list specific styles */
|
||
.notes-list-container {
|
||
max-width: none !important;
|
||
width: 100% !important;
|
||
padding: 1rem !important;
|
||
margin: 0 !important;
|
||
}
|
||
|
||
/* Sidebar layout */
|
||
.notes-layout {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.folder-sidebar {
|
||
width: 250px;
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 1rem;
|
||
max-height: calc(100vh - 200px);
|
||
overflow-y: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.create-note-section {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.btn-create-note {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
background: var(--primary-color, #007bff);
|
||
color: white;
|
||
text-decoration: none;
|
||
border-radius: 6px;
|
||
font-weight: 500;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-create-note:hover {
|
||
background: var(--primary-dark, #0056b3);
|
||
color: white;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.btn-create-note svg {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid #dee2e6;
|
||
}
|
||
|
||
.sidebar-header h3 {
|
||
margin: 0;
|
||
font-size: 1.1rem;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-icon {
|
||
width: 30px;
|
||
height: 30px;
|
||
padding: 0;
|
||
border: 1px solid #dee2e6;
|
||
background: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #666;
|
||
}
|
||
|
||
.btn-icon:hover {
|
||
background: #f8f9fa;
|
||
color: #333;
|
||
}
|
||
|
||
.notes-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.notes-layout.sidebar-hidden .notes-content {
|
||
margin-left: 0;
|
||
}
|
||
|
||
/* Folder tree styles */
|
||
.folder-tree {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.folder-item {
|
||
position: relative;
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
.folder-item.has-children > .folder-content::before {
|
||
content: "▶";
|
||
position: absolute;
|
||
left: -15px;
|
||
transition: transform 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.folder-item.has-children.expanded > .folder-content::before {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.folder-content {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0.5rem 0.75rem;
|
||
margin-left: 1rem;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
position: relative;
|
||
}
|
||
|
||
.folder-content:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.folder-content.active {
|
||
background: #e3f2fd;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.folder-content.drag-over {
|
||
background: #e3f2fd;
|
||
border: 2px dashed #2196F3;
|
||
}
|
||
|
||
.root-folder .folder-content {
|
||
margin-left: 0;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.folder-icon {
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.folder-name {
|
||
flex: 1;
|
||
}
|
||
|
||
.folder-count {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.folder-download-btn {
|
||
margin-left: 0.5rem;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
cursor: pointer;
|
||
padding: 0.2rem;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.folder-content:hover .folder-download-btn {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.folder-download-btn:hover {
|
||
opacity: 1 !important;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* Folder download dropdown */
|
||
.folder-download-dropdown {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 100%;
|
||
z-index: 1000;
|
||
min-width: 150px;
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
display: none;
|
||
}
|
||
|
||
.folder-download-dropdown.show {
|
||
display: block;
|
||
}
|
||
|
||
.folder-download-dropdown a {
|
||
display: block;
|
||
padding: 0.5rem 1rem;
|
||
color: #333;
|
||
text-decoration: none;
|
||
font-size: 0.9rem;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.folder-download-dropdown a:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.folder-children {
|
||
margin-left: 1.5rem;
|
||
display: none;
|
||
}
|
||
|
||
.folder-item.expanded > .folder-children {
|
||
display: block;
|
||
}
|
||
|
||
/* Tags section styles */
|
||
.tags-section {
|
||
margin-top: 2rem;
|
||
border-top: 1px solid #dee2e6;
|
||
padding-top: 1rem;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 0.5rem;
|
||
margin: -0.5rem;
|
||
border-radius: 4px;
|
||
transition: background 0.2s;
|
||
flex: 1;
|
||
}
|
||
|
||
.section-title:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.section-title h3 {
|
||
margin: 0;
|
||
font-size: 1.1rem;
|
||
color: #333;
|
||
flex: 1;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.section-toggle {
|
||
font-size: 0.8rem;
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.section-toggle.collapsed {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.tags-count {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
}
|
||
|
||
.tags-list {
|
||
margin-top: 0.5rem;
|
||
transition: max-height 0.3s ease-out;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tags-list.collapsed {
|
||
max-height: 0;
|
||
}
|
||
|
||
.tag-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0.4rem 0.75rem;
|
||
margin: 0.25rem 0;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.tag-item:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.tag-item.active {
|
||
background: #e3f2fd;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tag-item.clear-filter {
|
||
color: #dc3545;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.tag-icon {
|
||
margin-right: 0.5rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.tag-name {
|
||
flex: 1;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.tag-count {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.no-tags {
|
||
padding: 1rem;
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
/* Visibility section styles */
|
||
.visibility-section {
|
||
margin-top: 2rem;
|
||
border-top: 1px solid #dee2e6;
|
||
padding-top: 1rem;
|
||
}
|
||
|
||
.visibility-list {
|
||
margin-top: 0.5rem;
|
||
transition: max-height 0.3s ease-out;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.visibility-list.collapsed {
|
||
max-height: 0;
|
||
}
|
||
|
||
.visibility-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0.4rem 0.75rem;
|
||
margin: 0.25rem 0;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.visibility-item:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.visibility-item.active {
|
||
background: #e3f2fd;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.visibility-icon {
|
||
margin-right: 0.5rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.visibility-name {
|
||
flex: 1;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.visibility-count {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
/* Simplified filter section */
|
||
.notes-filter-section {
|
||
background: #f8f9fa;
|
||
padding: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
border-radius: 8px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.search-container {
|
||
flex: 1;
|
||
min-width: 400px;
|
||
}
|
||
|
||
.search-bar {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
background: white;
|
||
border: 2px solid #e9ecef;
|
||
border-radius: 8px;
|
||
padding: 0.25rem;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.search-bar:focus-within {
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.active-filters-inline {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
padding-left: 0.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.filter-chip {
|
||
background: #e3f2fd;
|
||
color: #333;
|
||
padding: 0.2rem 0.6rem;
|
||
border-radius: 16px;
|
||
font-size: 0.8rem;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-chip:hover {
|
||
background: #bbdefb;
|
||
}
|
||
|
||
.filter-chip .remove {
|
||
font-weight: bold;
|
||
color: #666;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
padding: 0.4rem 0.75rem;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 0.95rem;
|
||
outline: none;
|
||
}
|
||
|
||
.btn-search {
|
||
padding: 0.4rem;
|
||
background: transparent;
|
||
color: #666;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.btn-search:hover {
|
||
background: #e9ecef;
|
||
color: var(--primary-color, #007bff);
|
||
}
|
||
|
||
.btn-search svg {
|
||
display: block;
|
||
}
|
||
|
||
.btn-xs {
|
||
padding: 0.2rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
/* Notes toolbar */
|
||
.notes-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.75rem 0;
|
||
border-bottom: 1px solid #dee2e6;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.notes-count {
|
||
font-size: 0.9rem;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.btn-view-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.4rem 0.8rem;
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 6px;
|
||
color: #333;
|
||
font-size: 0.9rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-view-toggle:hover {
|
||
background: #f8f9fa;
|
||
border-color: #adb5bd;
|
||
}
|
||
|
||
.btn-view-toggle svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.view-label {
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Drag and drop styles */
|
||
.note-row.dragging,
|
||
.note-card.dragging {
|
||
opacity: 0.5;
|
||
cursor: move;
|
||
}
|
||
|
||
.note-row,
|
||
.note-card {
|
||
cursor: grab;
|
||
}
|
||
|
||
.note-row:active,
|
||
.note-card:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.admin-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.admin-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: center;
|
||
}
|
||
|
||
|
||
.notes-filter-section {
|
||
background: #f8f9fa;
|
||
padding: 1.5rem;
|
||
margin-bottom: 2rem;
|
||
border-radius: 8px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.filter-form {
|
||
width: 100%;
|
||
}
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
|
||
|
||
/* Table View Styles */
|
||
.notes-table {
|
||
width: 100%;
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.notes-table thead {
|
||
background: #f8f9fa;
|
||
border-bottom: 2px solid #dee2e6;
|
||
}
|
||
|
||
.notes-table th {
|
||
padding: 1rem;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
font-size: 0.9rem;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.notes-table td {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid #f1f3f5;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.notes-table tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.notes-table tr.pinned {
|
||
background: #fffbf0;
|
||
}
|
||
|
||
.column-pin {
|
||
width: 40px;
|
||
text-align: center;
|
||
}
|
||
|
||
.pin-icon {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.column-title {
|
||
min-width: 300px;
|
||
}
|
||
|
||
.note-link {
|
||
color: #333;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
display: block;
|
||
}
|
||
|
||
.note-link:hover {
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.column-folder {
|
||
width: 150px;
|
||
}
|
||
|
||
.folder-path {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
}
|
||
|
||
.folder-path.muted {
|
||
color: #aaa;
|
||
font-style: italic;
|
||
}
|
||
|
||
.folder-badge {
|
||
background: #e9ecef;
|
||
color: #495057;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.column-visibility {
|
||
width: 120px;
|
||
}
|
||
|
||
.column-tags {
|
||
min-width: 200px;
|
||
}
|
||
|
||
.column-updated {
|
||
width: 150px;
|
||
}
|
||
|
||
.date-text {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
}
|
||
|
||
.column-actions {
|
||
width: 200px;
|
||
}
|
||
|
||
.note-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
justify-content: center;
|
||
}
|
||
|
||
.btn-action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
background: transparent;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
color: #495057;
|
||
transition: all 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-action:hover {
|
||
background: #f8f9fa;
|
||
border-color: #adb5bd;
|
||
color: var(--primary-color, #007bff);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.btn-action-danger:hover {
|
||
background: #f8d7da;
|
||
border-color: #f5c6cb;
|
||
color: #721c24;
|
||
}
|
||
|
||
.note-associations {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.visibility-badge {
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 0.8rem;
|
||
font-weight: 500;
|
||
display: inline-block;
|
||
}
|
||
|
||
.visibility-private {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.visibility-team {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
}
|
||
|
||
.visibility-company {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.tag-badge {
|
||
background: #e9ecef;
|
||
color: #495057;
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 0.75rem;
|
||
text-decoration: none;
|
||
transition: background 0.2s ease;
|
||
display: inline-block;
|
||
margin-right: 0.25rem;
|
||
}
|
||
|
||
.tag-badge:hover {
|
||
background: #dee2e6;
|
||
color: #333;
|
||
}
|
||
|
||
.association-badge {
|
||
padding: 0.2rem 0.5rem;
|
||
border-radius: 4px;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.association-badge.project {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.association-badge.task {
|
||
background: #cce5ff;
|
||
color: #004085;
|
||
}
|
||
|
||
.note-actions {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.btn-xs {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
/* Grid View Styles */
|
||
.notes-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.note-card {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 6px;
|
||
padding: 1rem;
|
||
transition: box-shadow 0.2s ease;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.note-card:hover {
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.note-header {
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.note-title {
|
||
margin: 0 0 0.4rem 0;
|
||
font-size: 1.1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.note-title a {
|
||
color: #333;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.note-title a:hover {
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.note-meta {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
font-size: 0.75rem;
|
||
color: #666;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.note-preview {
|
||
flex: 1;
|
||
margin-bottom: 0.75rem;
|
||
color: #555;
|
||
line-height: 1.4;
|
||
font-size: 0.85rem;
|
||
overflow: hidden;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
|
||
.note-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.75rem;
|
||
flex-wrap: wrap;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.note-tags {
|
||
display: flex;
|
||
gap: 0.3rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* Grid View specific adjustments */
|
||
.note-card .visibility-badge {
|
||
padding: 0.15rem 0.4rem;
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
.note-card .folder-badge {
|
||
padding: 0.15rem 0.4rem;
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
.note-card .tag-badge {
|
||
padding: 0.15rem 0.4rem;
|
||
font-size: 0.7rem;
|
||
margin-right: 0.2rem;
|
||
}
|
||
|
||
.note-card .association-badge {
|
||
padding: 0.15rem 0.4rem;
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
.note-card .note-date {
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.note-card .pin-icon {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.note-card .btn-action {
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
|
||
.note-card .btn-action svg {
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
|
||
/* Responsive design */
|
||
@media (max-width: 1024px) {
|
||
.column-folder,
|
||
.column-tags {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.filter-row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.search-container {
|
||
min-width: 100%;
|
||
}
|
||
|
||
.search-bar {
|
||
flex-wrap: wrap;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.active-filters-inline {
|
||
width: 100%;
|
||
padding: 0 0 0.5rem 0;
|
||
}
|
||
|
||
.notes-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.column-visibility,
|
||
.column-updated {
|
||
display: none;
|
||
}
|
||
|
||
.notes-table {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.folder-sidebar {
|
||
width: 200px;
|
||
}
|
||
}
|
||
|
||
/* Modal styles */
|
||
.modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1.5rem;
|
||
border-bottom: 1px solid #dee2e6;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
cursor: pointer;
|
||
color: #666;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 1rem 1.5rem;
|
||
border-top: 1px solid #dee2e6;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.form-control {
|
||
width: 100%;
|
||
padding: 0.5rem;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.form-text {
|
||
display: block;
|
||
margin-top: 0.25rem;
|
||
font-size: 0.85rem;
|
||
color: #6c757d;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const toggleBtn = document.getElementById('toggle-view');
|
||
const tableView = document.getElementById('table-view');
|
||
const gridView = document.getElementById('grid-view');
|
||
const iconGrid = toggleBtn.querySelector('.icon-grid');
|
||
const iconList = toggleBtn.querySelector('.icon-list');
|
||
const viewLabel = toggleBtn.querySelector('.view-label');
|
||
|
||
// Load saved view preference
|
||
const savedView = localStorage.getItem('notes-view') || 'table';
|
||
if (savedView === 'grid') {
|
||
tableView.style.display = 'none';
|
||
gridView.style.display = 'block';
|
||
iconGrid.style.display = 'none';
|
||
iconList.style.display = 'block';
|
||
viewLabel.textContent = 'Grid View';
|
||
} else {
|
||
iconGrid.style.display = 'block';
|
||
iconList.style.display = 'none';
|
||
viewLabel.textContent = 'Table View';
|
||
}
|
||
|
||
toggleBtn.addEventListener('click', function() {
|
||
if (tableView.style.display === 'none') {
|
||
// Switch to table view
|
||
tableView.style.display = 'block';
|
||
gridView.style.display = 'none';
|
||
iconGrid.style.display = 'block';
|
||
iconList.style.display = 'none';
|
||
viewLabel.textContent = 'Table View';
|
||
localStorage.setItem('notes-view', 'table');
|
||
} else {
|
||
// Switch to grid view
|
||
tableView.style.display = 'none';
|
||
gridView.style.display = 'block';
|
||
iconGrid.style.display = 'none';
|
||
iconList.style.display = 'block';
|
||
viewLabel.textContent = 'Grid View';
|
||
localStorage.setItem('notes-view', 'grid');
|
||
}
|
||
});
|
||
|
||
// Toggle sidebar functionality
|
||
const toggleSidebarBtn = document.getElementById('toggle-sidebar');
|
||
const folderSidebar = document.getElementById('folder-sidebar');
|
||
const notesLayout = document.querySelector('.notes-layout');
|
||
|
||
// Load saved sidebar preference
|
||
const sidebarVisible = localStorage.getItem('notes-sidebar-visible') !== 'false';
|
||
if (!sidebarVisible) {
|
||
folderSidebar.style.display = 'none';
|
||
notesLayout.classList.add('sidebar-hidden');
|
||
}
|
||
|
||
toggleSidebarBtn.addEventListener('click', function() {
|
||
if (folderSidebar.style.display === 'none') {
|
||
folderSidebar.style.display = 'block';
|
||
notesLayout.classList.remove('sidebar-hidden');
|
||
localStorage.setItem('notes-sidebar-visible', 'true');
|
||
} else {
|
||
folderSidebar.style.display = 'none';
|
||
notesLayout.classList.add('sidebar-hidden');
|
||
localStorage.setItem('notes-sidebar-visible', 'false');
|
||
}
|
||
});
|
||
|
||
// Enable drag and drop for notes
|
||
enableDragAndDrop();
|
||
|
||
// Setup folder download dropdown
|
||
setupFolderDownload();
|
||
});
|
||
|
||
// Folder tree functions
|
||
function toggleFolder(event, folderPath) {
|
||
event.stopPropagation();
|
||
const folderItem = event.currentTarget.closest('.folder-item');
|
||
folderItem.classList.toggle('expanded');
|
||
}
|
||
|
||
function filterByFolder(folderPath) {
|
||
// Update the folder filter and submit the form
|
||
const folderSelect = document.getElementById('folder');
|
||
if (folderSelect) {
|
||
folderSelect.value = folderPath;
|
||
folderSelect.form.submit();
|
||
}
|
||
}
|
||
|
||
// Tags section functions
|
||
function toggleSection(section) {
|
||
const toggle = document.getElementById(`${section}-toggle`);
|
||
const list = document.getElementById(`${section}-list`);
|
||
|
||
toggle.classList.toggle('collapsed');
|
||
list.classList.toggle('collapsed');
|
||
|
||
// Save state to localStorage
|
||
const isCollapsed = list.classList.contains('collapsed');
|
||
localStorage.setItem(`notes-${section}-collapsed`, isCollapsed);
|
||
}
|
||
|
||
// Load saved section states
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Check if tags section should be collapsed
|
||
const tagsCollapsed = localStorage.getItem('notes-tags-collapsed') === 'true';
|
||
if (tagsCollapsed) {
|
||
document.getElementById('tags-toggle').classList.add('collapsed');
|
||
document.getElementById('tags-list').classList.add('collapsed');
|
||
}
|
||
|
||
// Check if visibility section should be collapsed
|
||
const visibilityCollapsed = localStorage.getItem('notes-visibility-collapsed') === 'true';
|
||
if (visibilityCollapsed) {
|
||
document.getElementById('visibility-toggle').classList.add('collapsed');
|
||
document.getElementById('visibility-list').classList.add('collapsed');
|
||
}
|
||
});
|
||
|
||
function filterByTag(tag) {
|
||
// Update the tag filter and submit the form
|
||
const tagInput = document.getElementById('tag');
|
||
if (tagInput) {
|
||
tagInput.value = tag;
|
||
tagInput.form.submit();
|
||
}
|
||
}
|
||
|
||
function filterByVisibility(visibility) {
|
||
// Update the visibility filter and submit the form
|
||
const visibilityInput = document.getElementById('visibility');
|
||
if (visibilityInput) {
|
||
visibilityInput.value = visibility;
|
||
visibilityInput.form.submit();
|
||
}
|
||
}
|
||
|
||
function clearFilter(filterType) {
|
||
const input = document.getElementById(filterType);
|
||
if (input) {
|
||
input.value = '';
|
||
input.form.submit();
|
||
}
|
||
}
|
||
|
||
function clearAllFilters() {
|
||
document.getElementById('folder').value = '';
|
||
document.getElementById('tag').value = '';
|
||
document.getElementById('visibility').value = '';
|
||
document.getElementById('filter-form').submit();
|
||
}
|
||
|
||
// Drag and drop functionality
|
||
function enableDragAndDrop() {
|
||
// Make notes draggable
|
||
const noteRows = document.querySelectorAll('.note-row');
|
||
const noteCards = document.querySelectorAll('.note-card');
|
||
|
||
[...noteRows, ...noteCards].forEach(note => {
|
||
note.draggable = true;
|
||
note.addEventListener('dragstart', handleDragStart);
|
||
note.addEventListener('dragend', handleDragEnd);
|
||
});
|
||
|
||
// Make folders droppable
|
||
const folderItems = document.querySelectorAll('.folder-content');
|
||
folderItems.forEach(folder => {
|
||
folder.addEventListener('dragover', handleDragOver);
|
||
folder.addEventListener('drop', handleDrop);
|
||
folder.addEventListener('dragleave', handleDragLeave);
|
||
});
|
||
}
|
||
|
||
let draggedNote = null;
|
||
|
||
function handleDragStart(e) {
|
||
draggedNote = this;
|
||
this.classList.add('dragging');
|
||
|
||
// Get note slug from data attribute
|
||
const noteSlug = this.dataset.noteSlug;
|
||
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
e.dataTransfer.setData('text/html', noteSlug);
|
||
}
|
||
|
||
function handleDragEnd(e) {
|
||
this.classList.remove('dragging');
|
||
draggedNote = null;
|
||
}
|
||
|
||
function handleDragOver(e) {
|
||
if (e.preventDefault) {
|
||
e.preventDefault();
|
||
}
|
||
|
||
e.dataTransfer.dropEffect = 'move';
|
||
this.classList.add('drag-over');
|
||
return false;
|
||
}
|
||
|
||
function handleDragLeave(e) {
|
||
this.classList.remove('drag-over');
|
||
}
|
||
|
||
function handleDrop(e) {
|
||
if (e.stopPropagation) {
|
||
e.stopPropagation();
|
||
}
|
||
|
||
this.classList.remove('drag-over');
|
||
|
||
const noteSlug = e.dataTransfer.getData('text/html');
|
||
const folderPath = this.closest('.folder-item').dataset.folder;
|
||
|
||
if (noteSlug && draggedNote) {
|
||
// Update note folder via API
|
||
updateNoteFolder(noteSlug, folderPath);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function updateNoteFolder(noteSlug, folderPath) {
|
||
fetch(`/api/notes/${noteSlug}/folder`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ folder: folderPath })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Reload the page to show updated folder
|
||
window.location.reload();
|
||
} else {
|
||
alert('Error moving note: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Error moving note to folder');
|
||
});
|
||
}
|
||
|
||
// Create folder modal functions
|
||
function showCreateFolderModal() {
|
||
// Create a simple modal for folder creation
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Create New Folder</h3>
|
||
<button type="button" class="close-btn" onclick="this.closest('.modal').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="createFolderForm">
|
||
<div class="form-group">
|
||
<label for="folderName">Folder Name</label>
|
||
<input type="text" id="folderName" name="name" class="form-control" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="parentFolder">Parent Folder (Optional)</label>
|
||
<select id="parentFolder" name="parent" class="form-control">
|
||
<option value="">Root</option>
|
||
{% for folder in all_folders %}
|
||
<option value="{{ folder }}">{{ folder }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
|
||
<button type="button" class="btn btn-primary" onclick="createFolder()">Create</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
modal.style.display = 'flex';
|
||
document.getElementById('folderName').focus();
|
||
}
|
||
|
||
function createFolder() {
|
||
const form = document.getElementById('createFolderForm');
|
||
const formData = new FormData(form);
|
||
|
||
fetch('/api/notes/folders', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
name: formData.get('name'),
|
||
parent: formData.get('parent')
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
window.location.reload();
|
||
} else {
|
||
alert('Error: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Error creating folder');
|
||
});
|
||
}
|
||
|
||
// Tag management functions
|
||
function showAddTagModal() {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Add Tags to Notes</h3>
|
||
<button type="button" class="close-btn" onclick="this.closest('.modal').remove()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="addTagForm">
|
||
<div class="form-group">
|
||
<label for="newTags">Tags (comma-separated)</label>
|
||
<input type="text" id="newTags" name="tags" class="form-control"
|
||
placeholder="e.g., urgent, review, documentation" required>
|
||
<small class="form-text">Enter one or more tags separated by commas</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Select Notes</label>
|
||
<div class="notes-selection" style="max-height: 300px; overflow-y: auto; border: 1px solid #dee2e6; padding: 0.5rem; border-radius: 4px;">
|
||
{% for note in notes %}
|
||
<label class="note-checkbox" style="display: block; margin: 0.5rem 0;">
|
||
<input type="checkbox" name="note_ids" value="{{ note.id }}" style="margin-right: 0.5rem;">
|
||
{{ note.title }}
|
||
{% if note.tags %}
|
||
<small style="color: #666;">({{ note.tags }})</small>
|
||
{% endif %}
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
|
||
<button type="button" class="btn btn-primary" onclick="addTagsToNotes()">Add Tags</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
modal.style.display = 'flex';
|
||
document.getElementById('newTags').focus();
|
||
}
|
||
|
||
function addTagsToNotes() {
|
||
const form = document.getElementById('addTagForm');
|
||
const formData = new FormData(form);
|
||
const tags = formData.get('tags');
|
||
const noteIds = Array.from(form.querySelectorAll('input[name="note_ids"]:checked'))
|
||
.map(input => input.value);
|
||
|
||
if (!tags || noteIds.length === 0) {
|
||
alert('Please enter tags and select at least one note');
|
||
return;
|
||
}
|
||
|
||
// For now, we'll update each note individually
|
||
// In a production app, you'd want a batch update endpoint
|
||
Promise.all(noteIds.map(noteId =>
|
||
fetch(`/api/notes/${noteId}/tags`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ tags: tags })
|
||
})
|
||
))
|
||
.then(responses => Promise.all(responses.map(r => r.json())))
|
||
.then(results => {
|
||
const failures = results.filter(r => !r.success);
|
||
if (failures.length === 0) {
|
||
window.location.reload();
|
||
} else {
|
||
alert(`Failed to update ${failures.length} notes`);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Error adding tags to notes');
|
||
});
|
||
}
|
||
|
||
// Folder download functions
|
||
function setupFolderDownload() {
|
||
const dropdown = document.getElementById('folder-download-dropdown');
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', function(e) {
|
||
if (!e.target.closest('.folder-download-btn') && !dropdown.contains(e.target)) {
|
||
dropdown.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
// Setup download links
|
||
dropdown.querySelectorAll('a').forEach(link => {
|
||
link.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
const format = this.getAttribute('data-format');
|
||
const folder = dropdown.getAttribute('data-folder');
|
||
if (folder) {
|
||
downloadFolder(folder, format);
|
||
}
|
||
dropdown.classList.remove('show');
|
||
});
|
||
});
|
||
}
|
||
|
||
function showFolderDownloadMenu(event, folderPath) {
|
||
event.stopPropagation();
|
||
const dropdown = document.getElementById('folder-download-dropdown');
|
||
const btn = event.currentTarget;
|
||
|
||
// Position dropdown near the button
|
||
const rect = btn.getBoundingClientRect();
|
||
dropdown.style.position = 'fixed';
|
||
dropdown.style.left = rect.left + 'px';
|
||
dropdown.style.top = (rect.bottom + 5) + 'px';
|
||
|
||
// Store the folder path
|
||
dropdown.setAttribute('data-folder', folderPath);
|
||
|
||
// Show dropdown
|
||
dropdown.classList.add('show');
|
||
}
|
||
|
||
function downloadFolder(folderPath, format) {
|
||
// Encode the folder path for URL
|
||
const encodedPath = encodeURIComponent(folderPath);
|
||
const url = `/notes/folder/${encodedPath}/download/${format}`;
|
||
|
||
// Create a temporary link and click it
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = '';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
}
|
||
</script>
|
||
|
||
{% endblock %}
|
||
|
||
{% macro render_folder_tree(tree, level=0) %}
|
||
{% for folder, children in tree.items() %}
|
||
<div class="folder-item {% if children %}has-children{% endif %}" data-folder="{{ folder }}">
|
||
<div class="folder-content {% if folder_filter == folder %}active{% endif %}">
|
||
{% if children %}
|
||
<span onclick="toggleFolder(event, '{{ folder }}')" style="position: absolute; left: -15px; cursor: pointer;">▶</span>
|
||
{% endif %}
|
||
<span class="folder-icon" onclick="filterByFolder('{{ folder }}')" style="cursor: pointer;">📁</span>
|
||
<span class="folder-name" onclick="filterByFolder('{{ folder }}')" style="cursor: pointer;">{{ folder.split('/')[-1] }}</span>
|
||
<span class="folder-count">({{ folder_counts.get(folder, 0) }})</span>
|
||
{% if folder_counts.get(folder, 0) > 0 %}
|
||
<div class="folder-download-btn" onclick="showFolderDownloadMenu(event, '{{ folder }}')">
|
||
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||
</svg>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% if children %}
|
||
<div class="folder-children">
|
||
{{ render_folder_tree(children, level + 1)|safe }}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% endmacro %} |