diff --git a/src/routes/entries.rs b/src/routes/entries.rs index ac45681..366efcf 100644 --- a/src/routes/entries.rs +++ b/src/routes/entries.rs @@ -39,6 +39,7 @@ pub struct EntryView { pub available_in: Option, pub is_available: bool, pub visit_count: i64, + pub tags: Vec, } /// Entry with visit count for queries that join entries with visits @@ -266,7 +267,7 @@ async fn fetch_entries_for_user(db: &sqlx::SqlitePool, user_id: &str) -> Vec<(En entries.into_iter().map(|e| e.into_entry_and_count()).collect() } -pub fn build_entry_view(entry: Entry, visit_count: i64, now: DateTime) -> EntryView { +pub fn build_entry_view(entry: Entry, visit_count: i64, tags: Vec, now: DateTime) -> EntryView { let (is_available, available_in) = calculate_availability(&entry, now); EntryView { id: entry.id, @@ -277,9 +278,31 @@ pub fn build_entry_view(entry: Entry, visit_count: i64, now: DateTime) -> E available_in, is_available, visit_count, + tags, } } +async fn fetch_tags_for_entries(db: &sqlx::SqlitePool, entry_ids: &[String]) -> HashMap> { + if entry_ids.is_empty() { + return HashMap::new(); + } + let placeholders = entry_ids.iter().map(|_| "?").collect::>().join(","); + let query = format!( + "SELECT et.entry_id, t.name FROM entry_tags et JOIN tags t ON t.id = et.tag_id WHERE et.entry_id IN ({})", + placeholders + ); + let mut q = sqlx::query_as::<_, (String, String)>(&query); + for id in entry_ids { + q = q.bind(id); + } + let rows: Vec<(String, String)> = q.fetch_all(db).await.unwrap_or_default(); + let mut map: HashMap> = HashMap::new(); + for (entry_id, tag_name) in rows { + map.entry(entry_id).or_default().push(tag_name); + } + map +} + async fn list_filtered_entries( db: &sqlx::SqlitePool, user: User, @@ -288,9 +311,15 @@ async fn list_filtered_entries( let entries = fetch_entries_for_user(db, &user.id).await; let now = Utc::now(); + let entry_ids: Vec = entries.iter().map(|(e, _)| e.id.clone()).collect(); + let mut tags_map = fetch_tags_for_entries(db, &entry_ids).await; + let entry_views: Vec = entries .into_iter() - .map(|(entry, visit_count)| build_entry_view(entry, visit_count, now)) + .map(|(entry, visit_count)| { + let tags = tags_map.remove(&entry.id).unwrap_or_default(); + build_entry_view(entry, visit_count, tags, now) + }) .filter(|ev| match filter { "ready" => ev.is_available, "waiting" => !ev.is_available, @@ -390,19 +419,18 @@ async fn visit_entry( .await?; let now_dt = Utc::now(); - let (is_available, available_in) = calculate_availability(&entry, now_dt); + + let tags: Vec<(String,)> = sqlx::query_as( + "SELECT t.name FROM tags t JOIN entry_tags et ON et.tag_id = t.id WHERE et.entry_id = ?" + ) + .bind(&entry.id) + .fetch_all(&state.db) + .await + .unwrap_or_default(); + let tags: Vec = tags.into_iter().map(|(name,)| name).collect(); let template = EntryTemplate { - entry: EntryView { - id: entry.id, - url: entry.url, - title: entry.title, - description: entry.description, - last_viewed: format_last_viewed(&entry.dismissed_at, now_dt), - available_in, - is_available, - visit_count: visit_count.0, - }, + entry: build_entry_view(entry, visit_count.0, tags, now_dt), }; Ok(Html(template.render()?)) } diff --git a/src/routes/tags.rs b/src/routes/tags.rs index fe85c2e..9f3b065 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -164,7 +164,7 @@ async fn show_tag( .into_iter() .map(|r| { let (entry, count) = r.into_entry_and_count(); - build_entry_view(entry, count, now) + build_entry_view(entry, count, vec![name.clone()], now) }) .collect(); diff --git a/static/style.css b/static/style.css index 853e0a3..3ee54b5 100644 --- a/static/style.css +++ b/static/style.css @@ -106,10 +106,89 @@ header nav a.active { .header-actions { display: flex; + justify-content: space-between; + align-items: baseline; gap: 1rem; margin-top: 0.75rem; } +/* Search trigger */ +.search-trigger { + display: flex; + align-items: center; + gap: 0.25rem; + cursor: pointer; +} + +.search-icon { + color: var(--gray-400); + flex-shrink: 0; + transition: color 0.15s ease; +} + +.search-trigger:hover .search-icon, +.search-trigger.active .search-icon { + color: var(--gray-600); +} + +.search-swap { + display: grid; + min-width: 10rem; + justify-items: end; +} + +.search-swap > * { + grid-row: 1; + grid-column: 1; + transition: opacity 0.15s ease; +} + +.search-trigger:hover .search-swap .date, +.search-trigger.active .search-swap .date { + opacity: 0; + pointer-events: none; +} + +.search-swap .search-field { + -webkit-appearance: none; + appearance: none; + width: 100%; + opacity: 0; + pointer-events: none; + font-size: 0.8125rem; + font-family: inherit; + color: var(--black); + background: transparent; + border: none; + border-bottom: 1px solid transparent; + border-radius: 0; + padding: 0 0 1px 0; + text-align: right; + box-shadow: none; +} + +.search-swap .search-field:focus { + outline: none; + box-shadow: none; +} + +.search-swap .search-field::placeholder { + color: var(--gray-400); +} + +.search-swap .search-field::-webkit-search-cancel-button, +.search-swap .search-field::-webkit-search-decoration { + -webkit-appearance: none; + display: none; +} + +.search-trigger:hover .search-swap .search-field, +.search-trigger.active .search-swap .search-field { + opacity: 1; + pointer-events: auto; + border-bottom-color: var(--gray-200); +} + .header-actions a { font-size: 0.8125rem; color: var(--gray-600); @@ -124,6 +203,9 @@ header nav a.active { .date { display: none; } + .search-trigger { + display: none; + } } /* Main */ diff --git a/templates/base.html b/templates/base.html index 4d54c5d..4415ab2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -21,7 +21,9 @@ Tags Collections {% endif %} + {% block nav_end %} + {% endblock %} {% block header_actions %}{% endblock %} diff --git a/templates/entries/entry.html b/templates/entries/entry.html index f8615aa..e3c1347 100644 --- a/templates/entries/entry.html +++ b/templates/entries/entry.html @@ -1,4 +1,4 @@ -
+
- Ready - / - Waiting - / - Unseen - / - All +{% block nav_end %} +
+ +
+ + +
{% endblock %} {% block header_actions %}
+
+ Ready + / + Waiting + / + Unseen + / + All +
+ New Link
{% endblock %} @@ -40,5 +47,74 @@ {% endfor %} {% endif %}
+ + {% endblock %}