Session 1: bug fix, global search, admin trash, lists CRUD

This commit is contained in:
2026-02-28 03:52:12 +00:00
parent f36ea194f3
commit 5773808ae4
14 changed files with 1211 additions and 2 deletions

View File

@@ -44,6 +44,10 @@
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
Links
</a>
<a href="/lists" class="nav-item {{ 'active' if active_nav == 'lists' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
Lists
</a>
<a href="/capture" class="nav-item {{ 'active' if active_nav == 'capture' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
Capture
@@ -76,6 +80,10 @@
</div>
<div class="nav-section" style="margin-top: auto; padding-bottom: 12px;">
<a href="/admin/trash" class="nav-item {{ 'active' if active_nav == 'trash' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Trash
</a>
<a href="/domains" class="nav-item {{ 'active' if active_nav == 'domains' }}">
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m-7-7h6m6 0h6"/></svg>
Manage Domains
@@ -99,6 +107,11 @@
<span class="topbar-env">DEV</span>
{% endif %}
<div class="topbar-spacer"></div>
<button class="search-trigger" onclick="openSearch()" title="Search (Cmd/K)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<span>Search...</span>
<kbd>&#8984;K</kbd>
</button>
</header>
<div class="page-content">
@@ -106,6 +119,19 @@
</div>
</main>
</div>
<!-- Search Modal -->
<div id="search-modal" class="search-modal hidden">
<div class="search-modal-backdrop" onclick="closeSearch()"></div>
<div class="search-modal-content">
<div class="search-modal-input-wrap">
<svg class="search-modal-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="search-input" class="search-modal-input" placeholder="Search tasks, projects, notes..." autocomplete="off">
<kbd class="search-modal-esc" onclick="closeSearch()">Esc</kbd>
</div>
<div id="search-results" class="search-results"></div>
</div>
</div>
<script src="/static/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block content %}
<!-- Breadcrumb -->
<div class="breadcrumb">
<a href="/lists">Lists</a>
<span class="sep">/</span>
{% if domain %}<span style="color: {{ domain.color or 'var(--accent)' }}">{{ domain.name }}</span><span class="sep">/</span>{% endif %}
{% if project %}<a href="/projects/{{ project.id }}">{{ project.name }}</a><span class="sep">/</span>{% endif %}
<span>{{ item.name }}</span>
</div>
<div class="detail-header">
<h1 class="detail-title">{{ item.name }}</h1>
<div class="flex gap-2">
<a href="/lists/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
<form action="/lists/{{ item.id }}/delete" method="post" data-confirm="Delete this list?" style="display:inline">
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</div>
</div>
<div class="detail-meta mt-2">
<span class="detail-meta-item">
<span class="row-tag">{{ item.list_type }}</span>
</span>
{% if item.description %}
<p class="text-secondary mt-1">{{ item.description }}</p>
{% endif %}
{% if item.tags %}
<div class="mt-1">
{% for tag in item.tags %}
<span class="row-tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Add item form -->
<form class="quick-add mt-3" action="/lists/{{ item.id }}/items/add" method="post">
<input type="text" name="content" placeholder="Add item..." required>
<button type="submit" class="btn btn-primary btn-sm">Add</button>
</form>
<!-- List items -->
{% if list_items %}
<div class="card mt-2">
{% for li in list_items %}
<div class="list-row {{ 'completed' if li.completed }}">
{% if item.list_type == 'checklist' %}
<div class="row-check">
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
<input type="checkbox" id="li-{{ li.id }}" {{ 'checked' if li.completed }}
onchange="this.form.submit()">
<label for="li-{{ li.id }}"></label>
</form>
</div>
{% endif %}
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if li.completed }}">
{{ li.content }}
</span>
<div class="row-actions">
<form action="/lists/{{ item.id }}/items/{{ li.id }}/delete" method="post" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</div>
</div>
<!-- Child items -->
{% for child in child_map.get(li.id|string, []) %}
<div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;">
{% if item.list_type == 'checklist' %}
<div class="row-check">
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">
<input type="checkbox" id="li-{{ child.id }}" {{ 'checked' if child.completed }}
onchange="this.form.submit()">
<label for="li-{{ child.id }}"></label>
</form>
</div>
{% endif %}
<span class="row-title" style="{{ 'text-decoration: line-through; opacity: 0.6;' if child.completed }}">
{{ child.content }}
</span>
<div class="row-actions">
<form action="/lists/{{ item.id }}/items/{{ child.id }}/delete" method="post" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
{% else %}
<div class="empty-state mt-3">
<div class="empty-state-icon">&#9744;</div>
<div class="empty-state-text">No items yet. Add one above.</div>
</div>
{% endif %}
{% endblock %}

82
templates/list_form.html Normal file
View File

@@ -0,0 +1,82 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1 class="page-title">{{ page_title }}</h1>
</div>
<div class="card">
<form method="post" action="{{ '/lists/' ~ item.id ~ '/edit' if item else '/lists/create' }}">
<div class="form-grid">
<div class="form-group full-width">
<label class="form-label">Name *</label>
<input type="text" name="name" class="form-input" required
value="{{ item.name if item else '' }}">
</div>
<div class="form-group">
<label class="form-label">Domain *</label>
<select name="domain_id" class="form-select" required>
<option value="">Select domain...</option>
{% for d in domains %}
<option value="{{ d.id }}"
{{ 'selected' if (item and item.domain_id|string == d.id|string) or (not item and prefill_domain_id == d.id|string) }}>
{{ d.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Area</label>
<select name="area_id" class="form-select">
<option value="">None</option>
{% for a in areas %}
<option value="{{ a.id }}"
{{ 'selected' if item and item.area_id and item.area_id|string == a.id|string }}>
{{ a.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Project</label>
<select name="project_id" class="form-select">
<option value="">None</option>
{% for p in projects %}
<option value="{{ p.id }}"
{{ 'selected' if (item and item.project_id and item.project_id|string == p.id|string) or (not item and prefill_project_id == p.id|string) }}>
{{ p.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Type</label>
<select name="list_type" class="form-select">
<option value="checklist" {{ 'selected' if item and item.list_type == 'checklist' }}>Checklist</option>
<option value="ordered" {{ 'selected' if item and item.list_type == 'ordered' }}>Ordered</option>
<option value="reference" {{ 'selected' if item and item.list_type == 'reference' }}>Reference</option>
</select>
</div>
<div class="form-group full-width">
<label class="form-label">Description</label>
<textarea name="description" class="form-textarea" rows="3">{{ item.description if item and item.description else '' }}</textarea>
</div>
<div class="form-group">
<label class="form-label">Tags</label>
<input type="text" name="tags" class="form-input" placeholder="tag1, tag2, ..."
value="{{ item.tags|join(', ') if item and item.tags else '' }}">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{ 'Save Changes' if item else 'Create List' }}</button>
<a href="{{ '/lists/' ~ item.id if item else '/lists' }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock %}

60
templates/lists.html Normal file
View File

@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Lists<span class="page-count">{{ items|length }}</span></h1>
<a href="/lists/create" class="btn btn-primary">+ New List</a>
</div>
<!-- Filters -->
<form class="filters-bar" method="get" action="/lists">
<select name="domain_id" class="filter-select" onchange="this.form.submit()">
<option value="">All Domains</option>
{% for d in domains %}
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
{% endfor %}
</select>
<select name="project_id" class="filter-select" onchange="this.form.submit()">
<option value="">All Projects</option>
{% for p in projects %}
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
{% endfor %}
</select>
</form>
{% if items %}
<div class="card mt-3">
{% for item in items %}
<div class="list-row">
<span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span>
<span class="row-meta">
{{ item.completed_count }}/{{ item.item_count }} items
</span>
{% if item.item_count > 0 %}
<div class="progress-bar" style="width: 80px;">
<div class="progress-fill" style="width: {{ (item.completed_count / item.item_count * 100) if item.item_count > 0 else 0 }}%"></div>
</div>
{% endif %}
<span class="row-tag">{{ item.list_type }}</span>
{% if item.domain_name %}
<span class="row-domain-tag" style="background: {{ item.domain_color or '#4F6EF7' }}22; color: {{ item.domain_color or '#4F6EF7' }}">{{ item.domain_name }}</span>
{% endif %}
{% if item.project_name %}
<span class="row-tag">{{ item.project_name }}</span>
{% endif %}
<div class="row-actions">
<a href="/lists/{{ item.id }}/edit" class="btn btn-ghost btn-xs">Edit</a>
<form action="/lists/{{ item.id }}/delete" method="post" data-confirm="Delete this list?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state mt-3">
<div class="empty-state-icon">&#9776;</div>
<div class="empty-state-text">No lists yet</div>
<a href="/lists/create" class="btn btn-primary">Create First List</a>
</div>
{% endif %}
{% endblock %}

37
templates/search.html Normal file
View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Search</h1>
</div>
<form class="search-page-form" method="get" action="/search">
<input type="text" name="q" value="{{ query }}" class="form-input" placeholder="Search tasks, projects, notes, contacts..." autofocus style="max-width: 600px;">
<button type="submit" class="btn btn-primary">Search</button>
</form>
{% if query %}
<p class="text-muted mt-2">{{ results|length }} result{{ 's' if results|length != 1 }} for "{{ query }}"</p>
{% endif %}
{% if results %}
<div class="card mt-3">
{% for item in results %}
<div class="list-row">
<span class="search-type-badge search-type-{{ item.type }}">{{ item.type_label }}</span>
<span class="row-title"><a href="{{ item.url }}">{{ item.name }}</a></span>
{% if item.context %}
<span class="row-meta">{{ item.context }}</span>
{% endif %}
{% if item.status %}
<span class="status-badge status-{{ item.status }}">{{ item.status|replace('_', ' ') }}</span>
{% endif %}
</div>
{% endfor %}
</div>
{% elif query %}
<div class="empty-state mt-3">
<div class="empty-state-icon">&#128269;</div>
<div class="empty-state-text">No results found for "{{ query }}"</div>
</div>
{% endif %}
{% endblock %}

54
templates/trash.html Normal file
View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Trash<span class="page-count">{{ total_deleted }}</span></h1>
{% if total_deleted > 0 %}
<form action="/admin/trash/empty" method="post" data-confirm="Permanently delete ALL {{ total_deleted }} items? This cannot be undone." style="display:inline">
<button type="submit" class="btn btn-danger">Empty Trash</button>
</form>
{% endif %}
</div>
<!-- Type filter -->
<div class="filters-bar">
<a href="/admin/trash" class="btn {{ 'btn-primary' if not current_type else 'btn-secondary' }} btn-sm">
All ({{ total_deleted }})
</a>
{% for entity in trash_entities %}
{% set count = entity_counts.get(entity.table, 0) %}
{% if count > 0 %}
<a href="/admin/trash?entity_type={{ entity.table }}" class="btn {{ 'btn-primary' if current_type == entity.table else 'btn-secondary' }} btn-sm">
{{ entity.label }} ({{ count }})
</a>
{% endif %}
{% endfor %}
</div>
{% if deleted_items %}
<div class="card mt-3">
{% for item in deleted_items %}
<div class="list-row">
<span class="search-type-badge search-type-{{ item.table }}">{{ item.type_label }}</span>
<span class="row-title">{{ item.name }}</span>
{% if item.deleted_at %}
<span class="row-meta">Deleted {{ item.deleted_at.strftime('%Y-%m-%d %H:%M') if item.deleted_at else '' }}</span>
{% endif %}
<div class="row-actions" style="opacity: 1;">
<form action="/admin/trash/{{ item.table }}/{{ item.id }}/restore" method="post" style="display:inline">
<button type="submit" class="btn btn-secondary btn-xs">Restore</button>
</form>
<form action="/admin/trash/{{ item.table }}/{{ item.id }}/permanent-delete" method="post"
data-confirm="Permanently delete '{{ item.name }}'? This cannot be undone." style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Delete Forever</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state mt-3">
<div class="empty-state-icon">&#128465;</div>
<div class="empty-state-text">Trash is empty</div>
</div>
{% endif %}
{% endblock %}