Files
OSIT-AE-App-Svelte/src/routes/events/[event_id]/+page.svelte
Scott Idem 718de1457d Fix infinite hydration loop and stabilize global store synchronization
- Refactored layouts to derive account data from stable props instead of reactive stores.
- Wrapped store updates in untrack() with deep equality guards to prevent infinite re-renders.
- Resolved duplicate untrack declarations and missing imports across the project.
- Added fetch safeguards to Element_data_store to prevent redundant API calls.
- Standardized hydration patterns to break circular dependencies during initial load.
2026-02-08 17:15:20 -05:00

649 lines
25 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
interface Props {
/** @type {import('./$types').PageData} */
data: any;
log_lvl?: number;
}
let { data, log_lvl = $bindable(0) }: Props = $props();
// *** Import Svelte specific
import { untrack } from 'svelte';
import { browser } from '$app/environment';
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import Comp_event_session_obj_li_wrapper from '../ae_comp__event_session_obj_li_wrapper.svelte';
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events_functions';
import Comp_event_files_upload from '../ae_comp__event_files_upload.svelte';
import Element_manage_event_file_li_wrap from '$lib/elements/element_manage_event_file_li_direct.svelte';
import Event_page_menu from './event_page_menu.svelte';
// Quickly save the data passed from the parent(s) to the Svelte stores, localStorage, and other.
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]);
let event_id = $derived(data.params.event_id);
$effect(() => {
if (ae_acct) {
untrack(() => {
$events_slct.event_id = ae_acct.slct.event_id;
});
}
});
// *** Initialization & Store Guard ***
if ($events_loc.pres_mgmt) {
if (typeof $events_loc.pres_mgmt.search_version === 'undefined')
$events_loc.pres_mgmt.search_version = 0;
if (typeof $events_loc.pres_mgmt.qry__remote_first === 'undefined')
$events_loc.pres_mgmt.qry__remote_first = false;
if (
typeof $events_loc.pres_mgmt.fulltext_search_qry_str === 'undefined'
)
$events_loc.pres_mgmt.fulltext_search_qry_str = '';
if (typeof $events_loc.pres_mgmt.location_name_qry_str === 'undefined')
$events_loc.pres_mgmt.location_name_qry_str = '';
}
let lq__event_obj = $derived(
liveQuery(async () => {
if (log_lvl)
console.log(
`*** LiveQuery: lq__event_obj *** event_id=${$events_slct.event_id}`
);
return await db_events.event.get($events_slct?.event_id ?? '');
})
);
// JSON formatted configuration options for an event, and specifically for the presentation management module.
$effect(() => {
const remote_cfg = $lq__event_obj?.mod_pres_mgmt_json;
const local_cfg = $events_loc.pres_mgmt;
if (remote_cfg && local_cfg) {
untrack(() => {
events_func.sync_config__event_pres_mgmt({
pres_mgmt_cfg_remote: remote_cfg,
pres_mgmt_cfg_local: local_cfg,
log_lvl: log_lvl
});
});
}
});
let event_session_id_li: Array<string> = $state([]);
let search_debounce_timer: any = null;
let last_search_id = 0;
let last_executed_key = ''; // Search Guard Key
// Stable LiveQuery Pattern (Aether UI V3)
let lq__event_session_obj_li = $derived.by(() => {
const ids = event_session_id_li;
const event_id = $events_slct?.event_id;
return liveQuery(async () => {
// SCENARIO 1: Specific IDs provided (Search Results)
if (Array.isArray(ids) && ids.length > 0) {
if (log_lvl)
console.log(`Session Page LQ: bulkGet ${ids.length} IDs`);
const results = await db_events.session.bulkGet(ids);
return results.filter((item) => item !== undefined);
}
// SCENARIO 2: Fallback broad search (Only if no active filters)
if (
event_id &&
!$events_loc.pres_mgmt.fulltext_search_qry_str &&
!$events_loc.pres_mgmt.location_name_qry_str
) {
if (log_lvl)
console.log(
`Session Page LQ: Fallback search for event: ${event_id}`
);
return await db_events.session
.where('event_id')
.equals(event_id)
.limit(50)
.sortBy('name');
}
return [];
});
});
let lq__event_location_obj_li = $derived(
liveQuery(async () => {
let results = await db_events.location
.where('event_id')
.equals($events_slct.event_id)
.sortBy('name');
return results;
})
);
// Standardized Reactive Search Pattern (Aether UI V3)
let search_params = $derived({
v: $events_loc.pres_mgmt.search_version,
str: ($events_loc.pres_mgmt.fulltext_search_qry_str ?? '')
.toLowerCase()
.trim(),
location: $events_loc.pres_mgmt.location_name_qry_str,
event_id: $events_slct?.event_id,
remote_first: $events_loc.pres_mgmt.qry__remote_first
});
$effect(() => {
const params = search_params;
if (search_debounce_timer) clearTimeout(search_debounce_timer);
search_debounce_timer = setTimeout(() => {
handle_search_refresh(params);
}, 300);
return () => {
if (search_debounce_timer) clearTimeout(search_debounce_timer);
};
});
async function handle_search_refresh(params: any) {
// 1. Guard
const qry_key = JSON.stringify(params);
if (qry_key === last_executed_key) return;
last_executed_key = qry_key;
const current_search_id = ++last_search_id;
const event_id = params.event_id;
const remote_first = params.remote_first;
if (log_lvl)
console.log(
`[Session Search #${current_search_id}] Refreshing (remote=${remote_first}, event=${event_id}, str=${params.str})...`
);
untrack(() => {
$events_sess.pres_mgmt.status_qry__search = 'loading';
});
const qry_str = params.str;
const location_name = params.location;
// 2. FAST PATH: Local IDB Search
if (!remote_first) {
try {
if (event_id) {
let local_results = await db_events.session
.where('event_id')
.equals(event_id)
.filter((session) => {
if (
location_name &&
session.event_location_name !== location_name
)
return false;
if (qry_str) {
const name = (session.name ?? '').toLowerCase();
const code = (session.code ?? '').toLowerCase();
const description = (
session.description ?? ''
).toLowerCase();
const qry_string = (
session.default_qry_str ?? ''
).toLowerCase();
const match =
name.includes(qry_str) ||
code.includes(qry_str) ||
description.includes(qry_str) ||
qry_string.includes(qry_str);
if (!match) return false;
}
return true;
})
.toArray();
local_results.sort((a, b) =>
(a.name ?? '').localeCompare(b.name ?? '')
);
const local_ids = local_results
.map((s) => s.id || s.event_session_id)
.filter(Boolean);
if (current_search_id === last_search_id) {
if (log_lvl)
console.log(
`[Session Search #${current_search_id}] Fast Path found ${local_ids.length} items locally.`
);
untrack(() => {
event_session_id_li = local_ids;
if (local_ids.length > 0)
$events_sess.pres_mgmt.status_qry__search =
'done';
});
}
}
} catch (e) {
if (log_lvl) console.warn('Session Fast Path failed.', e);
}
} else {
untrack(() => {
event_session_id_li = [];
});
}
// 3. REVALIDATE: API Request
try {
const results = await events_func.search__event_session({
api_cfg: $ae_api,
event_id: event_id,
fulltext_search_qry_str: qry_str || null,
like_search_qry_str: qry_str || null,
location_name: location_name || null,
enabled: $events_loc.pres_mgmt.qry_enabled ?? 'enabled',
hidden: $events_loc.pres_mgmt.qry_hidden ?? 'not_hidden',
limit: $events_loc.pres_mgmt.qry_limit__sessions ?? 100,
try_cache: true,
log_lvl: 0
});
if (current_search_id === last_search_id) {
let api_results = results || [];
// Client-side Filter Guard: Ensure API results match local criteria (Backup filter)
api_results = api_results.filter((session) => {
if (
location_name &&
session.event_location_name !== location_name
)
return false;
if (qry_str) {
const name = (session.name ?? '').toLowerCase();
const code = (session.code ?? '').toLowerCase();
const description = (
session.description ?? ''
).toLowerCase();
const qry_string = (
session.default_qry_str ?? ''
).toLowerCase();
const match =
name.includes(qry_str) ||
code.includes(qry_str) ||
description.includes(qry_str) ||
qry_string.includes(qry_str);
if (!match) return false;
}
return true;
});
const api_ids = api_results
.map((s: any) => s.id || s.event_session_id)
.filter(Boolean);
untrack(() => {
$events_slct.event_session_obj_li = api_results;
event_session_id_li = api_ids;
$events_sess.pres_mgmt.status_qry__search = 'done';
});
if (log_lvl)
console.log(
`[Session Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`
);
}
} catch (error) {
if (current_search_id === last_search_id) {
console.error('Session revalidation failed:', error);
untrack(() => {
$events_sess.pres_mgmt.status_qry__search = 'error';
});
}
}
}
if (
$events_loc.pres_mgmt?.save_search_text &&
$events_loc.pres_mgmt?.saved_search__session
) {
$events_loc.pres_mgmt.fulltext_search_qry_str =
$events_loc.pres_mgmt.saved_search__session;
}
if (
$events_loc.pres_mgmt?.save_search_text &&
$events_loc.pres_mgmt?.saved_search__session_location_name
) {
$events_loc.pres_mgmt.location_name_qry_str =
$events_loc.pres_mgmt.saved_search__session_location_name;
}
function handle_search_trigger() {
$events_loc.pres_mgmt.search_version++;
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script>
<svelte:head>
<title>
&AElig;:
{ae_util.shorten_string({
string: $lq__event_obj?.name,
max_length: 12
})}
- Pres Mgmt - {$events_loc?.title}
</title>
</svelte:head>
<Event_page_menu {lq__event_obj} />
{#if !$lq__event_obj}
<div class="flex items-center justify-center p-10 opacity-50">
<span class="fas fa-spinner fa-spin mx-1 text-2xl"></span>
<span>Loading event information...</span>
</div>
{:else if $lq__event_obj?.enable || $ae_loc.trusted_access}
<header class="ae_module_header">
<span class="flex flex-row flex-wrap gap-1 items-center justify-center">
<span class="fas fa-calendar-day m-1 text-neutral-800/80"></span>
<button
type="button"
onclick={() => {
if (
$events_loc.pres_mgmt.show_content__event_view ==
'manage_files'
) {
$events_loc.pres_mgmt.show_content__event_view = null;
} else {
$events_loc.pres_mgmt.show_content__event_view =
'manage_files';
}
}}
class="btn ae_btn_secondary"
class:preset-filled-secondary-500={$events_loc.pres_mgmt
.show_content__event_view == 'manage_files'}
class:preset-filled-tertiary-500={$events_loc.pres_mgmt
.show_content__event_view != 'manage_files'}
class:hidden={!$ae_loc.administrator_access}
title="View event search or manage files for the event"
>
{#if $events_loc.pres_mgmt.show_content__event_view == 'manage_files'}
<span class="fas fa-search m-1"></span>
Event Search?
{:else}
<span class="fas fa-file-archive m-1"></span>
Event Files?
<span
class="badge preset-tonal-success"
class:hidden={!$lq__event_obj?.file_count}
>
<span class="fas fa-file-alt m-1"></span>
{$lq__event_obj?.file_count}×
</span>
{/if}
</button>
</span>
<h2
class="text-2xl font-bold text-center max-w-xs sm:max-w-lg md:max-w-2xl"
>
<span class="sm:inline-block md:hidden">
{$lq__event_obj.cfg_json?.short_name ?? $lq__event_obj?.name}
</span>
<span class="hidden md:inline-block lg:hidden">
{$lq__event_obj.cfg_json?.med_name ?? $lq__event_obj?.name}
</span>
<span class="hidden lg:inline-block">
{$lq__event_obj.cfg_json?.long_name ?? $lq__event_obj?.name}
</span>
</h2>
</header>
{#if !$events_loc.pres_mgmt.show_content__event_view || $events_loc.pres_mgmt.show_content__event_view == 'default'}
<div class="ae_container_actions">
<form
onsubmit={prevent_default(() => handle_search_trigger())}
autocomplete="off"
class="form grow flex flex-row flex-wrap gap-1 justify-center items-center w-full"
>
<button
type="button"
class="btn btn-sm mx-1 ae_btn_warning"
class:hidden={!$ae_loc.authenticated_access}
onclick={() => {
$events_loc.pres_mgmt.location_name_qry_str = '';
$events_loc.pres_mgmt.show_content__session_search_room_name =
!$events_loc.pres_mgmt
.show_content__session_search_room_name;
handle_search_trigger();
}}
title="Search by location name"
>
<span class="fas fa-search-location"></span>
</button>
<select
name="location_name_list"
id="session_location_name_list"
bind:value={$events_loc.pres_mgmt.location_name_qry_str}
class="input text-xs font-bold font-mono min-w-fit w-min max-w-40 transition-all mx-1"
class:hidden={!$ae_loc.authenticated_access ||
!$events_loc.pres_mgmt
.show_content__session_search_room_name}
onchange={() => handle_search_trigger()}
title="Select to filter based on the location/room name"
>
{#if $lq__event_location_obj_li}
<option value="">Location / Room</option>
{#each $lq__event_location_obj_li as event_location_obj}
<option value={event_location_obj?.name}
>{event_location_obj.name}</option
>
{/each}
{/if}
</select>
<button
type="button"
onclick={() => {
$events_loc.pres_mgmt.fulltext_search_qry_str = '';
handle_search_trigger();
}}
class:hidden={!$events_loc.pres_mgmt
.fulltext_search_qry_str}
class="btn btn-sm mx-1 ae_btn_warning"
title="Clear search text"
>
<span class="fas fa-remove-format"></span>
</button>
<input
type="search"
placeholder="Search for a session"
id="session_fulltext_search_qry_str"
bind:value={$events_loc.pres_mgmt.fulltext_search_qry_str}
class="input text-1xl hover:text-2xl font-bold font-mono w-80 transition-all mx-1 ae_btn_info"
onkeyup={(e) => {
if (e.key === 'Enter') handle_search_trigger();
}}
autofocus
data-ignore="true"
/>
<button
type="submit"
class="btn btn-lg text-2xl font-bold w-48 mx-1 ae_btn_primary"
title="Search for a session"
>
{#if $events_sess.pres_mgmt.status_qry__search == 'loading'}
<span
class="fas fa-spinner fa-spin mx-1 text-success-800-200"
></span>
{:else}
<span class="fas fa-search mx-1 text-neutral-800/80"
></span>
{/if}
Search
</button>
{#if $ae_loc.edit_mode}
<label
class="flex items-center gap-1 cursor-pointer bg-surface-200-800 px-2 py-1 rounded-token text-xs font-semibold opacity-70 hover:opacity-100 transition-all"
>
<span> Remote First </span>
<input
type="checkbox"
bind:checked={
$events_loc.pres_mgmt.qry__remote_first
}
onchange={() => handle_search_trigger()}
class="checkbox checkbox-sm"
/>
</label>
{/if}
</form>
</div>
{#if event_session_id_li.length}
<Comp_event_session_obj_li_wrapper
{lq__event_session_obj_li}
hide__session_location={$events_loc.pres_mgmt
?.hide__session_li_location_field}
hide__session_poc={$events_loc.pres_mgmt
?.hide__session_li_poc_field}
hide__launcher_link_legacy={$events_loc.pres_mgmt
?.hide__launcher_link_legacy}
hide__launcher_link={$events_loc.pres_mgmt?.hide__launcher_link}
hide__location_link={$events_loc.pres_mgmt?.hide__location_link}
log_lvl={1}
/>
{:else if $events_sess.pres_mgmt.status_qry__search === 'loading'}
<div
class="flex flex-col items-center justify-center p-20 opacity-50 text-center"
>
<span class="fas fa-spinner fa-spin text-4xl mb-4"></span>
<p class="text-xl">Searching sessions...</p>
</div>
{:else}
<section
class="text-center text-2xl bg-yellow-100 p-4 rounded-md lg:max-w-lg space-y-2 mx-auto"
>
<div>
<span
class="fas fa-exclamation-triangle text-2xl text-yellow-500"
></span>
<strong>No results to show</strong>
<span
class="fas fa-exclamation-triangle text-2xl text-yellow-500"
></span>
<br />
<div class="text-lg">
Please use the search above to find your session.
</div>
</div>
<div>
<strong>Search by:</strong>
<ul class="list-disc list-inside text-lg text-left">
<li>Session name</li>
<li>Session description</li>
<li>Presentation name</li>
<li>Presenter names</li>
<li>Presenter ID (member ID)</li>
</ul>
</div>
</section>
{/if}
{:else if $events_loc.pres_mgmt.show_content__event_view == 'manage_files' && $ae_loc.trusted_access}
{#if $lq__event_obj}
<header>
<h2 class="h3 text-center">{$lq__event_obj?.name}</h2>
<h3 class="h4 text-center">Event - Manage Files</h3>
</header>
{/if}
<div>
<h3 class="h5 text-center">
<span class="fas fa-tasks m-1 text-neutral-800/80"></span>
<span class="fas fa-mail-bulk m-1 text-neutral-800/80"></span>
Manage and Upload Event Files:
</h3>
<Comp_event_files_upload
class_li="border border-gray-300 rounded-md p-2 bg-gray-100 hover:bg-gray-200"
link_to_type="event"
link_to_id={$lq__event_obj?.event_id}
>
{#snippet label()}
<span>
<div class="text-lg">
<span class="fas fa-upload"></span>
<strong class=""
>Upload global event files only!</strong
>
</div>
<div
class="text-sm text-gray-600 dark:text-gray-400 italic"
>
<strong>Global event files only</strong><br />
Recommended: PowerPoint (pptx) or Keynote (key)<br
/>
Media: Audio and videos files should be directly embedded
in PowerPoint (PPTX) files<br />
Supplemental files: mp4, PDF, Word Doc, Excel, txt, etc
</div>
</span>
{/snippet}
</Comp_event_files_upload>
<div class="overflow-x-auto w-max max-w-full">
<Element_manage_event_file_li_wrap
link_to_type={'event'}
link_to_id={$lq__event_obj?.event_id}
allow_basic={$events_loc.auth__kv.session[
$lq__event_obj?.event_id
]}
allow_moderator={$events_loc.auth__kv.session[
$lq__event_obj?.event_id
]}
container_class_li={''}
/>
</div>
</div>
{/if}
{:else}
<div class="bg-red-100 p-4 border border-red-200 rounded-md">
<h2 class="h3">
<span class="fas fa-exclamation-triangle text-red-500 m-1"></span>
Event Disabled
</h2>
<p>
This event is currently disabled. Please contact the event organizer
for more information.
</p>
</div>
{/if}
<style lang="postcss">
</style>