diff --git a/src/lib/stores/ae_idaa_stores.ts b/src/lib/stores/ae_idaa_stores.ts index 978c2d30..25fedabb 100644 --- a/src/lib/stores/ae_idaa_stores.ts +++ b/src/lib/stores/ae_idaa_stores.ts @@ -98,7 +98,12 @@ const idaa_local_data_struct: key_val = { qry__fulltext_str: null, qry__physical: null, qry__type: null, - qry__virtual: null + qry__virtual: null, + + // Favorites filter — when true, only show meetings the member has starred. + // Favorites are stored server-side in event.mod_meetings_json.favorite (array of Novi UUIDs), + // so they persist across browsers without requiring a Novi API write capability. + qry__favorites_only: false } }; diff --git a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_li.svelte b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_li.svelte index 3985b8ce..02c3a8de 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_li.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_li.svelte @@ -37,8 +37,53 @@ import { idaa_slct, idaa_trig } from '$lib/stores/ae_idaa_stores'; +import { db_events } from '$lib/ae_events/db_events'; +import { events_func } from '$lib/ae_events/ae_events_functions'; import MyClipboard from '$lib/app_components/e_app_clipboard.svelte'; +// Tracks event IDs currently being toggled to prevent double-clicks +let favorites_in_progress = $state>(new Set()); + +function check_fav(event_obj: Record): boolean { + const my_uuid = $idaa_loc.novi_uuid; + if (!my_uuid) return false; + const mod = event_obj.mod_meetings_json as Record | null | undefined; + return Array.isArray(mod?.favorite) && (mod.favorite as string[]).includes(my_uuid); +} + +async function toggle_favorite(event_obj: Record) { + const my_uuid = $idaa_loc.novi_uuid; + const obj_id = event_obj.id as string; // Dexie primary key (same value as event_id) + const event_id = event_obj.event_id as string; // V3 API UUID + + if (!my_uuid || !event_id || favorites_in_progress.has(event_id)) return; + + favorites_in_progress.add(event_id); + + const current_json = (event_obj.mod_meetings_json as Record) ?? {}; + const current_list: string[] = Array.isArray(current_json.favorite) ? [...(current_json.favorite as string[])] : []; + const already_fav = current_list.includes(my_uuid); + const new_list = already_fav ? current_list.filter((u) => u !== my_uuid) : [...current_list, my_uuid]; + const new_json = { ...current_json, favorite: new_list }; + + // Optimistic IDB update — liveQuery reactively re-renders the card instantly + await db_events.event.update(obj_id, { mod_meetings_json: new_json }); + + try { + await events_func.update_ae_obj__event({ + api_cfg: $ae_api, + event_id, + data_kv: { mod_meetings_json: new_json }, + try_cache: true + }); + } catch (err) { + console.warn('[favorites] API persist failed, rolling back:', err); + await db_events.event.update(obj_id, { mod_meetings_json: current_json }); + } finally { + favorites_in_progress.delete(event_id); + } +} + // Derived list of visible items (Refactored 2026-02-05) // // WHY: The parent search logic fetches matching records into the local IndexedDB. @@ -78,8 +123,27 @@ let visible_event_obj_li = $derived.by(() => { `visible_event_obj_li: Input=${list.length}, Output=${filtered.length} (trusted=${$ae_loc.trusted_access})` ); + // Favorites: pin starred meetings to the top of the list. + // Runs before the limit slice so favorites are never cut off. + const my_uuid = $idaa_loc.novi_uuid; + if (my_uuid) { + filtered.sort((a, b) => { + const a_fav = check_fav(a); + const b_fav = check_fav(b); + if (a_fav && !b_fav) return -1; + if (!a_fav && b_fav) return 1; + return 0; // stable — preserves existing sort within each group + }); + } + + // Favorites-only filter — applied after sort so starred items are already at top + const starred_only = $idaa_loc.recovery_meetings.qry__favorites_only; + const shown = starred_only && my_uuid + ? filtered.filter((item) => check_fav(item)) + : filtered; + // Final safety slice to respect the user's limit selection - return filtered.slice(0, $idaa_loc.recovery_meetings.qry__limit || 150); + return shown.slice(0, $idaa_loc.recovery_meetings.qry__limit || 150); }); $effect(() => { if (log_lvl) { @@ -129,10 +193,33 @@ $effect(() => { flex-row items-center justify-between gap-2 text-2xl font-bold "> - - {idaa_event_obj?.name ?? 'Recovery Meeting'} + + + {idaa_event_obj?.name ?? 'Recovery Meeting'} + + + {#if $idaa_loc.novi_uuid} + {@const fav = check_fav(idaa_event_obj)} + {@const pending = favorites_in_progress.has(idaa_event_obj?.event_id)} + + + {/if}
{ Loading...
+ {:else if $idaa_loc.recovery_meetings.qry__favorites_only} +
+ +

No starred meetings yet.

+

Tap the ★ on any meeting card to add it to My Meetings.

+
{:else}
diff --git a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte index 62cf9e1f..a769bece 100644 --- a/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte +++ b/src/routes/idaa/(idaa)/recovery_meetings/ae_idaa_comp__event_obj_qry.svelte @@ -411,6 +411,29 @@ function prevent_default(fn: (event: T) => void) { + {#if $idaa_loc.novi_uuid} + + {/if} + {#if ($ae_loc.trusted_access && $ae_loc.edit_mode) || $idaa_loc.novi_uuid}