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 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
||||
No recovery meetings found matching your criteria.
|
||||
</div>
|
||||
{#if show_cache_reset_btn}
|
||||
<!-- Escape hatch: surfaces after 8s when zero results + no active filters.
|
||||
With ~140 active meetings, zero unfiltered results always indicates
|
||||
stale IDB data. Clears the event cache and triggers a fresh API fetch. -->
|
||||
{#if has_active_filters}
|
||||
<!-- Guided empty state: filters are active but returned no results.
|
||||
Distinct from the zero-unfiltered-results path (which indicates a data problem).
|
||||
Here the member may simply have narrowed too far — offer a one-click escape. -->
|
||||
<div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<p class="text-sm opacity-75">Still not seeing meetings? Your local cache may be out of date.</p>
|
||||
<p>No meetings found for these filters.</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-warning m-auto"
|
||||
onclick={handle_cache_reset}>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Refresh Meeting Cache
|
||||
class="btn btn-sm preset-tonal-primary m-auto"
|
||||
onclick={clear_filters}>
|
||||
<span class="fas fa-times m-1"></span>
|
||||
Clear all filters
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
||||
No recovery meetings found matching your criteria.
|
||||
</div>
|
||||
{#if show_cache_reset_btn}
|
||||
<!-- Escape hatch: surfaces after 8s when zero results + no active filters.
|
||||
With ~140 active meetings, zero unfiltered results always indicates
|
||||
stale IDB data. Clears the event cache and triggers a fresh API fetch. -->
|
||||
<div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
|
||||
<p class="text-sm opacity-75">Still not seeing meetings? Your local cache may be out of date.</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm preset-tonal-warning m-auto"
|
||||
onclick={handle_cache_reset}>
|
||||
<span class="fas fa-sync-alt m-1"></span>
|
||||
Refresh Meeting Cache
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
let ds_fav_json = $state<Record<string, string[]>>({});
|
||||
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<string, string[]>) ?? {};
|
||||
}
|
||||
} 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(() => {
|
||||
<!-- <span class="fas fa-times m-1"></span> View Other Meetings -->
|
||||
</a>
|
||||
|
||||
{#if $idaa_loc.novi_uuid && ds_fav_id}
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggle_favorite}
|
||||
disabled={fav_in_progress}
|
||||
aria-pressed={is_fav}
|
||||
style="background:none; border:none; box-shadow:none; padding:4px 8px; cursor:pointer; line-height:1; opacity:{is_fav ? '1' : '0.5'}; transition:opacity 0.15s, color 0.15s;"
|
||||
title={is_fav ? 'Remove from My Meetings' : 'Add to My Meetings'}>
|
||||
{#if fav_in_progress}
|
||||
<span class="fas fa-spinner fa-spin" style="font-size:1rem;"></span>
|
||||
{:else}
|
||||
<span class="fas fa-star" style="font-size:1rem; color:{is_fav ? '#d97706' : 'currentColor'};"></span>
|
||||
{/if}
|
||||
<span style="font-size:0.85rem; margin-left:3px;">{is_fav ? 'In My Meetings' : 'Add to My Meetings'}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- View (default)/Edit event_id toggle -->
|
||||
{#if $idaa_sess.recovery_meetings.edit__event_obj}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user