Standardize Exhibit Leads module: Rich text support and naming alignment

- Integrated TipTap rich text editor for Booth Descriptions and Exhibitor Notes.
- Implemented strip_html utility for clean search/preview of rich text fields.
- Renamed exhibit loading functions to follow load_ae_obj_id__event_exhibit* pattern.
- Hardened property processors with 'undefined' string guards and automatic reload triggers.
- Resolved type mismatches and naming inconsistencies across the Leads module.
- Verified zero-error state via svelte-check.
This commit is contained in:
Scott Idem
2026-02-09 15:30:00 -05:00
parent e7895cee07
commit fe4380f819
15 changed files with 91 additions and 35 deletions

15
TODO.md
View File

@@ -85,6 +85,7 @@ This is a list of tasks to be completed before the next event/show/conference.
## Current Priorities (Feb 3, 2026)
1. **Codebase Hygiene:**
- [ ] **CRUD v2 Review:** Perform a comprehensive review/enhancement of 'Element_ae_crud_v2.svelte' (Mobile accessibility, 'Select' mode, workflow optimization).
- [ ] **Type Mismatch Resolution:** Resolve remaining simple type mismatches flagged by `svelte-check` (currently ~160).
- [ ] **Bite-Sized Refactoring:** Split large components (>800 lines) into modular sub-components (e.g., `ae_idaa_comp__event_obj_id_edit.svelte`).
2. **Reactivity & Performance:**
@@ -119,10 +120,16 @@ This is a list of tasks to be completed before the next event/show/conference.
## Recent Accomplishments (Feb 8, 2026)
- [x] **Zero-Error Compiler State:** Successfully cleared all 68 'svelte-check' errors across the entire application.
- [x] **Event Settings Hardening:** Refactored complex JSON configuration editors to use a temporary string-buffer pattern, preventing data corruption and resolving multiple TypeScript assignment errors.
- [x] **Prop Reactivity Pass:** Standardized prop synchronization logic in core components (Sign-In, File Upload, Layout) to adhere to Svelte 5 Runes best practices.
- [x] **Triple-ID TypeScript Alignment:** Updated 'ae_types.ts' and refined property processing to fully support semantic string IDs across core modules.
- **Exhibitor Leads Module (V3 Completion):**
- [x] **Authentication:** Implemented dual-mode Sign-In (Shared Passcode / Licensee) with persistent state and Admin pre-fill.
- [x] **Portal Management:** Built comprehensive Manage tab with Admin Tools, Licensee management (license_li_json), and dynamic Question editor.
- [x] **Lead List Filtering:** Implemented reactive licensee filtering (My Leads vs All Leads) with 'Hard Guard' post-filtering for robust data narrowing.
- [x] **Sync Telemetry:** Added real-time refresh countdown and 'Last Sync' timestamp indicators.
- [x] **Lead Detail Editor:** Built dynamic form for editing custom responses/qualifiers based on booth-specific question definitions.
- [x] **Duplicate Prevention:** Implemented 'Already Captured' detection in both Manual Search and QR Scanner with direct 'View' links.
- **Zero-Error State:** Successfully reached a zero-error baseline in `npm run check` after resolving all typing and ReferenceErrors in core modules.
- **Performance:** Optimized IDAA Bulletin Board by disabling redundant comment fetches in list views.
- **Terminology:** Standardized on 'Licensed User' (licensee) terminology across all Leads components.
## Recent Accomplishments (Feb 7, 2026)

View File

@@ -341,6 +341,7 @@ import { load_ae_obj_id__archive_content } from '$lib/ae_archives/ae_archives__a
import { load_ae_obj_id__event } from '$lib/ae_events/ae_events__event';
// import { load_ae_obj_id__event_badge } from "$lib/ae_events/ae_events__event_badge";
import { load_ae_obj_id__event_exhibit } from '$lib/ae_events/ae_events__exhibit';
import { load_ae_obj_id__event_device } from '$lib/ae_events/ae_events__event_device';
// import { load_ae_obj_id__event_exhibit } from "$lib/ae_events/ae_events__event_exhibit";
import { load_ae_obj_id__event_file } from '$lib/ae_events/ae_events__event_file';
@@ -410,6 +411,7 @@ async function update_ae_obj_id_crud_v2({
if (object_type == 'journal') load_ae_obj_id__journal({ api_cfg, journal_id: object_id, log_lvl });
if (object_type == 'journal_entry') load_ae_obj_id__journal_entry({ api_cfg, journal_entry_id: object_id, log_lvl });
if (object_type == 'event') load_ae_obj_id__event({ api_cfg, event_id: object_id, log_lvl });
if (object_type == 'event_exhibit') load_ae_obj_id__event_exhibit({ api_cfg, exhibit_id: object_id, log_lvl });
if (object_type == 'event_device') load_ae_obj_id__event_device({ api_cfg, event_device_id: object_id, log_lvl });
if (object_type == 'event_file') load_ae_obj_id__event_file({ api_cfg, event_file_id: object_id, log_lvl });
if (object_type == 'event_location') load_ae_obj_id__event_location({ api_cfg, event_location_id: object_id, log_lvl });

View File

@@ -89,6 +89,11 @@ async function _process_generic_props<T extends Record<string, any>>({
const updated = processed_obj.updated_on ?? processed_obj.created_on;
const name = processed_obj.name ?? '';
// Guard: Prevent literal "undefined" string from showing in description
if ((processed_obj as any).description === 'undefined') {
(processed_obj as any).description = '';
}
(processed_obj as any).tmp_sort_1 = `${group}_${priority}_${sort}_${updated}`;
(processed_obj as any).tmp_sort_2 = `${group}_${priority}_${sort}_${name}_${updated}`;
@@ -119,7 +124,7 @@ export async function process_ae_obj__exhibit_props({
/**
* Load Single Exhibit (SWR Pattern)
*/
export async function load_ae_obj_id__exhibit({
export async function load_ae_obj_id__event_exhibit({
api_cfg,
exhibit_id,
view = 'default',
@@ -134,7 +139,7 @@ export async function load_ae_obj_id__exhibit({
}): Promise<ae_EventExhibit | null> {
const start_time = performance.now();
if (log_lvl) {
console.log(`🔎 [Trace] load_ae_obj_id__exhibit: START (id=${exhibit_id}, try_cache=${try_cache})`);
console.log(`🔎 [Trace] load_ae_obj_id__event_exhibit: START (id=${exhibit_id}, try_cache=${try_cache})`);
}
// 1. FAST PATH: Return cached data immediately
@@ -192,7 +197,7 @@ async function _refresh_exhibit_id_background({ api_cfg, exhibit_id, view, try_c
/**
* Load Collection of Exhibits (SWR Pattern)
*/
export async function load_ae_obj_li__exhibit({
export async function load_ae_obj_li__event_exhibit({
api_cfg,
event_id,
enabled = 'enabled',
@@ -222,7 +227,7 @@ export async function load_ae_obj_li__exhibit({
}): Promise<ae_EventExhibit[]> {
const start_time = performance.now();
if (log_lvl) {
console.log(`🔎 [Trace] load_ae_obj_li__exhibit: START (event=${event_id}, try_cache=${try_cache})`);
console.log(`🔎 [Trace] load_ae_obj_li__event_exhibit: START (event=${event_id}, try_cache=${try_cache})`);
}
if (try_cache) {

View File

@@ -101,6 +101,11 @@ async function _process_generic_props<T extends Record<string, any>>({
const updated = processed_obj.updated_on ?? processed_obj.created_on;
const name = processed_obj.event_badge_full_name ?? '';
// Guard: Prevent literal "undefined" string from showing in notes
if ((processed_obj as any).exhibitor_notes === 'undefined') {
(processed_obj as any).exhibitor_notes = '';
}
(processed_obj as any).tmp_sort_1 = `${group}_${priority}_${sort}_${updated}`;
(processed_obj as any).tmp_sort_2 = `${group}_${priority}_${sort}_${name}_${updated}`;
@@ -131,7 +136,7 @@ export async function process_ae_obj__exhibit_tracking_props({
/**
* Load Single Lead (SWR Pattern)
*/
export async function load_ae_obj_id__exhibit_tracking({
export async function load_ae_obj_id__event_exhibit_tracking({
api_cfg,
exhibit_tracking_id,
view = 'default',
@@ -146,7 +151,7 @@ export async function load_ae_obj_id__exhibit_tracking({
}): Promise<ae_EventExhibitTracking | null> {
const start_time = performance.now();
if (log_lvl) {
console.log(`🔎 [Trace] load_ae_obj_id__exhibit_tracking: START (id=${exhibit_tracking_id}, try_cache=${try_cache})`);
console.log(`🔎 [Trace] load_ae_obj_id__event_exhibit_tracking: START (id=${exhibit_tracking_id}, try_cache=${try_cache})`);
}
// 1. FAST PATH: Return cached data immediately
@@ -204,7 +209,7 @@ async function _refresh_tracking_id_background({ api_cfg, exhibit_tracking_id, v
/**
* Load Collection of Leads (SWR Pattern)
*/
export async function load_ae_obj_li__exhibit_tracking({
export async function load_ae_obj_li__event_exhibit_tracking({
api_cfg,
exhibit_id,
enabled = 'enabled',
@@ -233,7 +238,7 @@ export async function load_ae_obj_li__exhibit_tracking({
}): Promise<ae_EventExhibitTracking[]> {
const start_time = performance.now();
if (log_lvl) {
console.log(`🔎 [Trace] load_ae_obj_li__exhibit_tracking: START (exhibit=${exhibit_id}, try_cache=${try_cache})`);
console.log(`🔎 [Trace] load_ae_obj_li__event_exhibit_tracking: START (exhibit=${exhibit_id}, try_cache=${try_cache})`);
}
if (try_cache) {

View File

@@ -7,8 +7,8 @@ import * as event_device from '$lib/ae_events/ae_events__event_device';
import * as event_file from '$lib/ae_events/ae_events__event_file';
import {
load_ae_obj_id__exhibit,
load_ae_obj_li__exhibit,
load_ae_obj_id__event_exhibit,
load_ae_obj_li__event_exhibit,
search__exhibit,
create_ae_obj__exhibit,
update_ae_obj__exhibit
@@ -16,8 +16,8 @@ import {
import {
search__exhibit_tracking,
load_ae_obj_id__exhibit_tracking,
load_ae_obj_li__exhibit_tracking,
load_ae_obj_id__event_exhibit_tracking,
load_ae_obj_li__event_exhibit_tracking,
create_ae_obj__exhibit_tracking,
update_ae_obj__exhibit_tracking,
download_export__event_exhibit_tracking
@@ -73,13 +73,13 @@ const export_obj = {
update_ae_obj__event_device: event_device.update_ae_obj__event_device,
// Event Exhibits
load_ae_obj_id__exhibit: load_ae_obj_id__exhibit,
load_ae_obj_li__exhibit: load_ae_obj_li__exhibit,
load_ae_obj_id__event_exhibit: load_ae_obj_id__event_exhibit,
load_ae_obj_li__event_exhibit: load_ae_obj_li__event_exhibit,
search__exhibit: search__exhibit,
create_ae_obj__exhibit: create_ae_obj__exhibit,
update_ae_obj__exhibit: update_ae_obj__exhibit,
load_ae_obj_id__exhibit_tracking: load_ae_obj_id__exhibit_tracking,
load_ae_obj_li__exhibit_tracking: load_ae_obj_li__exhibit_tracking,
load_ae_obj_id__event_exhibit_tracking: load_ae_obj_id__event_exhibit_tracking,
load_ae_obj_li__event_exhibit_tracking: load_ae_obj_li__event_exhibit_tracking,
search__exhibit_tracking: search__exhibit_tracking,
create_ae_obj__exhibit_tracking: create_ae_obj__exhibit_tracking,
update_ae_obj__exhibit_tracking: update_ae_obj__exhibit_tracking,

View File

@@ -265,6 +265,14 @@ export const shorten_string = function shorten_string({
return new_string;
};
/**
* Strips HTML tags from a string.
*/
export function strip_html(html: string): string {
if (!html) return '';
return html.replace(/<[^>]*>?/gm, '');
}
// Svelte action to set focus on an element
function set_focus(node: HTMLElement, focus: boolean) {
if (focus) {
@@ -341,6 +349,7 @@ export const ae_util = {
to_title_case: to_title_case,
shorten_string: shorten_string,
shorten_filename: shorten_filename,
strip_html: strip_html,
file_extension_icon: file_extension_icon,
file_extension_icon_lucide: file_extension_icon_lucide,
format_html: format_html,

View File

@@ -101,6 +101,8 @@
// import { ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger } from '$lib/ae_stores';
// *** Import Aether core components
import AE_Comp_Editor_TipTap from '$lib/elements/AE_Comp_Editor_TipTap.svelte';
// *** Import Aether module variables and functions
// *** Import Aether module components
@@ -364,6 +366,13 @@
rows={textarea_rows}
class="textarea"
></textarea>
{:else if field_type == 'tiptap'}
<div class="w-full min-w-96 text-left">
<AE_Comp_Editor_TipTap
bind:content={new_field_value}
placeholder="Start typing..."
/>
</div>
{:else}
<input bind:value={new_field_value} class="input w-fit" />
{/if}

View File

@@ -48,11 +48,11 @@ Represents a single lead captured by an exhibitor. It links an exhibitor to an a
- `/events/[event_id]/(leads)`: The main entry point for the Leads module within a specific event, typically displays a list of available exhibits.
- `+page.svelte`: Renders the list of exhibits.
- `+page.ts`: Loads the data for available exhibits using `events_func.load_ae_obj_li__exhibit`.
- `+page.ts`: Loads the data for available exhibits using `events_func.load_ae_obj_li__event_exhibit`.
- `+layout.svelte`/`+layout.ts`: Provides a common layout and data for the module, including a submenu.
- `/events/[event_id]/(leads)/exhibit/[slug]`: Dynamic route for managing leads for a specific exhibitor within an event. The `[slug]` corresponds to `event_exhibit_id`.
- `+page.svelte`: The primary interface for an exhibitor, orchestrating lead capture and management components.
- `+page.ts`: Loads specific `Exhibit` data and associated `Exhibit_tracking` (leads) using `events_func.load_ae_obj_id__exhibit` and `events_func.load_ae_obj_li__exhibit_tracking`.
- `+page.ts`: Loads specific `Exhibit` data and associated `Exhibit_tracking` (leads) using `events_func.load_ae_obj_id__event_exhibit` and `events_func.load_ae_obj_li__event_exhibit_tracking`.
### Core Components (within `src/routes/events/[event_id]/(leads)/exhibit/[slug]/`)

View File

@@ -12,7 +12,7 @@ export async function load({ params, parent }) {
const event_id = params.event_id;
if (browser && event_id) {
events_func.load_ae_obj_li__exhibit({
events_func.load_ae_obj_li__event_exhibit({
api_cfg: ae_acct.api,
event_id: event_id,
limit: 100,

View File

@@ -19,13 +19,13 @@ export async function load({ params, parent }) {
});
if (browser && exhibit_id) {
events_func.load_ae_obj_id__exhibit({
events_func.load_ae_obj_id__event_exhibit({
api_cfg: ae_acct.api,
exhibit_id: exhibit_id,
log_lvl: 0
});
events_func.load_ae_obj_li__exhibit_tracking({
events_func.load_ae_obj_li__event_exhibit_tracking({
api_cfg: ae_acct.api,
exhibit_id: exhibit_id,
limit: 250,

View File

@@ -10,6 +10,7 @@
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { page } from '$app/state';
import { events_func } from '$lib/ae_events_functions';
import { ae_util } from '$lib/ae_utils/ae_utils';
import {
LoaderCircle,
UserPlus,
@@ -228,9 +229,11 @@
const email = (
tracking.event_badge_email ?? ''
).toLowerCase();
const notes = (
tracking.exhibitor_notes ?? ''
).toLowerCase();
const notes = ae_util.strip_html(tracking.exhibitor_notes ?? '').toLowerCase();
// Guard: Prevent "undefined" from being searched
if (tracking.exhibitor_notes === 'undefined') {
tracking.exhibitor_notes = '';
}
const qry_string = (
tracking.default_qry_str ?? ''
).toLowerCase();

View File

@@ -87,7 +87,7 @@
>
<FileText size="1em" class="inline mr-1" />
{ae_util.shorten_string({
string: event_tracking_obj.exhibitor_notes,
string: ae_util.strip_html(event_tracking_obj.exhibitor_notes),
max_length: 100
})}
</div>

View File

@@ -66,6 +66,7 @@
field_name="priority"
field_type="boolean"
current_field_value={$lq__exhibit_obj?.priority}
object_reload={true}
/>
</div>
@@ -81,6 +82,7 @@
field_name="license_max"
field_type="number"
current_field_value={$lq__exhibit_obj?.license_max}
object_reload={true}
class_li="w-16 font-mono text-right"
/>
</div>
@@ -97,6 +99,7 @@
field_name="leads_device_sm_qty"
field_type="number"
current_field_value={$lq__exhibit_obj?.leads_device_sm_qty}
object_reload={true}
class_li="w-16 font-mono text-right"
/>
</div>
@@ -113,6 +116,7 @@
field_name="leads_device_lg_qty"
field_type="number"
current_field_value={$lq__exhibit_obj?.leads_device_lg_qty}
object_reload={true}
class_li="w-16 font-mono text-right"
/>
</div>
@@ -140,6 +144,7 @@
field_name="name"
field_type="text"
current_field_value={$lq__exhibit_obj?.name}
object_reload={true}
hide_element={false}
display_block={true}
class_li="font-bold text-xl"
@@ -157,9 +162,9 @@
object_type="event_exhibit"
object_id={exhibit_id}
field_name="description"
field_type="textarea"
field_type="tiptap"
current_field_value={$lq__exhibit_obj?.description}
textarea_rows={4}
object_reload={true}
hide_element={false}
display_block={true}
class_li="text-sm"
@@ -181,6 +186,14 @@
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="text-[10px] uppercase font-black opacity-40 tracking-widest mb-1">Staff Passcode</div>
<!-- Add a clear read-only display for admins to see the code at a glance -->
{#if $ae_loc.administrator_access}
<div class="font-mono text-xl tracking-widest font-bold text-primary-500 mb-2">
{$lq__exhibit_obj?.staff_passcode || '----'}
</div>
{/if}
<Element_ae_crud_v2
api_cfg={$ae_api}
object_type="event_exhibit"
@@ -188,6 +201,7 @@
field_name="staff_passcode"
field_type="text"
current_field_value={$lq__exhibit_obj?.staff_passcode}
object_reload={true}
class_li="font-mono text-xl tracking-widest font-bold"
hide_element={false}
display_block={true}

View File

@@ -193,13 +193,15 @@
object_type="event_exhibit_tracking"
object_id={exhibit_tracking_id ?? ''}
field_name="exhibitor_notes"
field_type="textarea"
field_type="tiptap"
current_field_value={$lq__lead_obj.exhibitor_notes}
textarea_rows={6}
object_reload={true}
display_block={true}
/>
{:else if $lq__lead_obj.exhibitor_notes}
<p class="whitespace-pre-wrap leading-relaxed">{$lq__lead_obj.exhibitor_notes}</p>
<div class="prose dark:prose-invert max-w-none leading-relaxed">
{@html $lq__lead_obj.exhibitor_notes}
</div>
{:else}
<div class="h-full flex items-center justify-center italic opacity-30 text-sm">
No notes have been added for this lead yet.

View File

@@ -14,7 +14,7 @@ export async function load({ params, parent }) {
if (browser && exhibit_tracking_id) {
// Refresh the specific Lead (Tracking) object
events_func.load_ae_obj_id__exhibit_tracking({
events_func.load_ae_obj_id__event_exhibit_tracking({
api_cfg: ae_acct.api,
exhibit_tracking_id: exhibit_tracking_id,
log_lvl: 0