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())
|
!($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 show_cache_reset_btn = $state(false);
|
||||||
let cache_reset_timer: any = null;
|
let cache_reset_timer: any = null;
|
||||||
|
|
||||||
@@ -111,6 +119,13 @@ async function handle_cache_reset() {
|
|||||||
}
|
}
|
||||||
$idaa_sess.recovery_meetings.search_version++;
|
$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)
|
// Standardized Reactive Search Pattern (Aether UI V3)
|
||||||
@@ -441,24 +456,40 @@ if (browser) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
{#if has_active_filters}
|
||||||
class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
|
<!-- Guided empty state: filters are active but returned no results.
|
||||||
No recovery meetings found matching your criteria.
|
Distinct from the zero-unfiltered-results path (which indicates a data problem).
|
||||||
</div>
|
Here the member may simply have narrowed too far — offer a one-click escape. -->
|
||||||
{#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">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm preset-tonal-warning m-auto"
|
class="btn btn-sm preset-tonal-primary m-auto"
|
||||||
onclick={handle_cache_reset}>
|
onclick={clear_filters}>
|
||||||
<span class="fas fa-sync-alt m-1"></span>
|
<span class="fas fa-times m-1"></span>
|
||||||
Refresh Meeting Cache
|
Clear all filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,10 +38,25 @@ import {
|
|||||||
import Event_obj_id_edit from '../ae_idaa_comp__event_obj_id_edit.svelte';
|
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 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 Help_tech from '$lib/app_components/e_app_help_tech.svelte';
|
||||||
|
import { api } from '$lib/api/api';
|
||||||
|
|
||||||
// *** Quickly pull out data from parent(s)
|
// *** Quickly pull out data from parent(s)
|
||||||
let ae_acct = $derived(data[data.account_id]);
|
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;
|
$idaa_sess.recovery_meetings.edit__event_obj = null;
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!ae_acct) return;
|
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(() => {
|
onDestroy(() => {
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -255,6 +319,23 @@ onDestroy(() => {
|
|||||||
<!-- <span class="fas fa-times m-1"></span> View Other Meetings -->
|
<!-- <span class="fas fa-times m-1"></span> View Other Meetings -->
|
||||||
</a>
|
</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 -->
|
<!-- View (default)/Edit event_id toggle -->
|
||||||
{#if $idaa_sess.recovery_meetings.edit__event_obj}
|
{#if $idaa_sess.recovery_meetings.edit__event_obj}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user