From 3a81887c56c927ea111af5c48f754848e3300cc2 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Mon, 18 May 2026 17:00:51 -0400 Subject: [PATCH] feat(idaa): add guided empty state for filtered results + star button on meeting detail - +page.svelte: when search returns zero results and filters are active, show "No meetings found for these filters" with a one-click "Clear all filters" button instead of the bare no-results message. The 8s cache-reset escape hatch is unchanged and still fires only when zero results appear with no filters set. - [event_id]/+page.svelte: add star/favorites button to the detail page nav bar alongside Back/Edit. Loads the same idaa_meetings_favorites data_store record on mount; PATCHes the shared record on toggle. State is optimistic with rollback. Co-Authored-By: Claude Sonnet 4.6 --- .../(idaa)/recovery_meetings/+page.svelte | 57 ++++++++++--- .../recovery_meetings/[event_id]/+page.svelte | 81 +++++++++++++++++++ 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte b/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte index 28c997d7..08867184 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/+page.svelte @@ -73,6 +73,14 @@ let no_results_no_filters = $derived( !($idaa_loc.recovery_meetings.qry__fulltext_str?.trim()) ); +// True when any filter dimension is active — drives the guided empty state. +let has_active_filters = $derived( + !!$idaa_loc.recovery_meetings.qry__physical || + !!$idaa_loc.recovery_meetings.qry__virtual || + !!$idaa_loc.recovery_meetings.qry__type || + !!($idaa_loc.recovery_meetings.qry__fulltext_str?.trim()) +); + let show_cache_reset_btn = $state(false); let cache_reset_timer: any = null; @@ -111,6 +119,13 @@ async function handle_cache_reset() { } $idaa_sess.recovery_meetings.search_version++; } +function clear_filters() { + $idaa_loc.recovery_meetings.qry__physical = null; + $idaa_loc.recovery_meetings.qry__virtual = null; + $idaa_loc.recovery_meetings.qry__type = null; + $idaa_loc.recovery_meetings.qry__fulltext_str = null; + $idaa_sess.recovery_meetings.search_version++; +} // ───────────────────────────────────────────────────────────────────────────────────── // Standardized Reactive Search Pattern (Aether UI V3) @@ -441,24 +456,40 @@ if (browser) { {:else} -
- No recovery meetings found matching your criteria. -
- {#if show_cache_reset_btn} - + {#if has_active_filters} +
-

Still not seeing meetings? Your local cache may be out of date.

+

No meetings found for these filters.

+ {:else} +
+ No recovery meetings found matching your criteria. +
+ {#if show_cache_reset_btn} + +
+

Still not seeing meetings? Your local cache may be out of date.

+ +
+ {/if} {/if} {/if} diff --git a/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte b/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte index 58bf5a27..ce510300 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/[event_id]/+page.svelte @@ -38,10 +38,25 @@ import { import Event_obj_id_edit from '../ae_idaa_comp__event_obj_id_edit.svelte'; import Event_obj_id_view from '.././ae_idaa_comp__event_obj_id_view.svelte'; import Help_tech from '$lib/app_components/e_app_help_tech.svelte'; +import { api } from '$lib/api/api'; // *** Quickly pull out data from parent(s) let ae_acct = $derived(data[data.account_id]); +// Favorites stored in data_store (code: idaa_meetings_favorites, scoped to IDAA account_id). +// Same shared record used by the meeting list view; see ae_idaa_comp__event_obj_li.svelte. +let ds_fav_id = $state(null); +let ds_fav_json = $state>({}); +let fav_in_progress = $state(false); + +let event_id_for_fav = $derived(ae_acct?.slct?.event_id ?? null); +let is_fav = $derived.by(() => { + const my_uuid = $idaa_loc.novi_uuid; + if (!my_uuid || !event_id_for_fav) return false; + const my_favs = ds_fav_json[my_uuid]; + return Array.isArray(my_favs) && my_favs.includes(event_id_for_fav); +}); + $idaa_sess.recovery_meetings.edit__event_obj = null; $effect(() => { if (!ae_acct) return; @@ -131,6 +146,55 @@ onMount(() => { } }); +onMount(async () => { + if (!$idaa_loc.novi_uuid || !$ae_api?.base_url) return; + try { + const result = await api.get_data_store({ + api_cfg: $ae_api, + code: 'idaa_meetings_favorites' + }); + const rec_id = result?.data_store_id || result?.id; + if (rec_id) { + ds_fav_id = rec_id; + let raw = result.json ?? result.json_str ?? null; + if (typeof raw === 'string') { + try { raw = JSON.parse(raw); } catch { raw = {}; } + } + ds_fav_json = (raw as Record) ?? {}; + } + } catch (err) { + console.warn('[favorites] Failed to load favorites data store:', err); + } +}); + +async function toggle_favorite() { + const my_uuid = $idaa_loc.novi_uuid; + const event_id = event_id_for_fav; + if (!my_uuid || !event_id || !ds_fav_id || fav_in_progress) return; + fav_in_progress = true; + const current_user_list: string[] = Array.isArray(ds_fav_json[my_uuid]) + ? [...ds_fav_json[my_uuid]] : []; + const new_user_list = is_fav + ? current_user_list.filter((id) => id !== event_id) + : [...current_user_list, event_id]; + const prev_json = ds_fav_json; + const new_full_json = { ...ds_fav_json, [my_uuid]: new_user_list }; + ds_fav_json = new_full_json; + try { + await api.update_ae_obj({ + api_cfg: $ae_api, + obj_type: 'data_store', + obj_id: ds_fav_id, + fields: { json: JSON.stringify(new_full_json) } + }); + } catch (err) { + console.warn('[favorites] API persist failed, rolling back:', err); + ds_fav_json = prev_json; + } finally { + fav_in_progress = false; + } +} + onDestroy(() => { if (log_lvl) { console.log( @@ -255,6 +319,23 @@ onDestroy(() => { + {#if $idaa_loc.novi_uuid && ds_fav_id} + + {/if} + {#if $idaa_sess.recovery_meetings.edit__event_obj}