15 Commits

Author SHA1 Message Date
Scott Idem
83e271a323 Version updates 2026-03-27 19:37:40 -04:00
Scott Idem
ace90ad043 docs(todo): document flowbite-svelte ModalProps errors and orphaned ShadCN packages
Records the root cause of the 2026-03-27 hidden-error discovery (broken ambient
declaration masking 31 pre-existing svelte-check errors), the lesson learned, and
two follow-up tasks: fix ModalProps.children across 26 files, remove shadcn-svelte
and bits-ui from package.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 19:35:44 -04:00
Scott Idem
d139ed1bd0 fix(types): add aria-hidden to IconProps augment; remove orphaned ShadCN components
- lucide-augment.d.ts: add `aria-hidden?: string | boolean` to IconProps
  (SVGAttributes drops this too in @lucide/svelte ≥ 0.577.0)
- Remove src/lib/components/ui/ — ShadCN primitives with zero importers;
  bits-ui API drift was generating ~20 type errors for dead code

svelte-check: 31 errors remaining (all ModalProps.children — flowbite-svelte
API change, deferred to next session), 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 19:32:24 -04:00
Scott Idem
3d988222a1 fix(types): move @lucide/svelte augmentation to module-context file
app.d.ts is a script-context declaration file. A `declare module 'x' {}`
in a script file is an ambient module declaration that completely replaces
the package's types — not an augmentation. This caused svelte-check to see
@lucide/svelte as exporting only IconProps, producing 1131 "class" errors
and 237 "no exported member" errors for every icon import.

Moving the augmentation to src/lucide-augment.d.ts with `export {}` makes
it a module file, so `declare module` becomes a proper augmentation that
merges with the package types. Result: Lucide errors drop from 1368 to 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 19:15:08 -04:00
Scott Idem
5433a906bb fix(types): restore class prop on Lucide IconProps after 0.577.0 breakage
@lucide/svelte >=0.577.0 dropped `class` from IconProps — it now derives
props purely from SVGAttributes<SVGSVGElement>, which TS types without
`class`. Every <SomeIcon class="..." /> in the codebase errored (1131
errors). Augment IconProps in app.d.ts to re-add `class?: string`.
Root cause: 0.561.0 → 0.577.0 bump in commit 366c6629 (2026-03-10).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 18:35:52 -04:00
Scott Idem
d89218be15 feat(leads): implement Stripe payment component for exhibit licenses
Full implementation of ae_comp__exhibit_payment.svelte (was a 9-line stub).
Reads Stripe config from $ae_loc.site_cfg_json per-event. License tier
selector (1/3/6/10 users) uses {#key} remount pattern to work around
stripe-buy-button web component ignoring attribute changes after mount.
Three states: paid confirmation (priority=true), not-configured hint, payment
form. client_reference_id=exhibit_id ties payments to booth records.
TypeScript declaration for stripe-buy-button added to app.d.ts via
svelte/elements augmentation. exhibit_id prop wired in +page.svelte and
ae_tab__manage.svelte.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 18:29:12 -04:00
Scott Idem
a8e9bd6694 Updated to do 2026-03-27 17:04:56 -04:00
Scott Idem
6cd3b5f8f9 More notes and comments updates 2026-03-27 16:21:51 -04:00
Scott Idem
b33c1b16f6 fix(idaa): check UUID against trusted/admin lists directly for Jitsi moderator
$ae_loc.trusted_access is only ever upgraded, never downgraded — it sticks
across Novi impersonation even though a different UUID is in the URL. Instead,
check user_id directly against $idaa_loc.novi_admin_li / novi_trusted_li so
the moderator grant is tied to the specific UUID being used, not the inherited
session access level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 15:17:38 -04:00
Scott Idem
d7a0857bed fix(idaa): load Jitsi external API script dynamically to eliminate race condition
<svelte:head> scripts load asynchronously with no lifecycle hook to await
completion, so onMount could call init_jitsi() before JitsiMeetExternalAPI
was defined. Replace with a dynamic script loader that is awaited between
fetch_novi_data() and init_jitsi(). Also uses the domain from URL params
rather than the hardcoded jitsi.dgrzone.com hostname.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 15:09:19 -04:00
Scott Idem
6939c058d8 Documentation updates 2026-03-27 14:53:28 -04:00
Scott Idem
b88a7de358 feat(idaa): trusted/admin users always get Jitsi moderator role
Rather than hardcoding the IDAA admins group UUID or making an extra
API call, re-use the access level already established by the IDAA layout.
If $ae_loc.trusted_access is set (verified against novi_trusted_li /
novi_admin_li), the user is a moderator immediately. Only regular
authenticated members fall through to the group membership check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:52:31 -04:00
Scott Idem
27f0bd21fb fix(idaa): fall back to site config group list when g_uuid not in URL
Older Novi pages that haven't been updated to pass g_uuid still need
the moderator check to work. Use [g_uuid] when present, otherwise fall
back to novi_idaa_group_guid_li from site config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:31:34 -04:00
Scott Idem
f111670f60 feat(idaa): use URL g_uuid for Jitsi moderator group check
Instead of checking membership across all groups in novi_idaa_group_guid_li
(site config), pass the single g_uuid from the URL param. Each Novi iframe
page supplies the group relevant to that specific meeting, so checking just
that one group is both more precise and avoids unnecessary Novi API calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:27:06 -04:00
Scott Idem
045efa71e1 fix(layout): show sys bar in iframe when show_menu=true for trusted users
The {#if} gate only allowed the sys bar to mount for admins or
trusted+edit_mode users in an iframe. Trusted staff using show_menu=true
had sys_menu.hide set correctly but the component never mounted. Add
!sys_menu.hide as an escape hatch so the URL override actually works.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:25:18 -04:00
42 changed files with 400 additions and 724 deletions

View File

@@ -547,24 +547,30 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
## IDAA Novi Groups and Moderators
### IDAA Couples Meeting = "e9e162f0-3d03-4241-9682-340135ec3fb8"
### "IDAA Association Admins Group" = "409e91dc-f5a3-486c-a964-71b7d19e6841"
"Gregory X Boehm" "00ee764c-7559-496b-9d18-40d3e9092c0c"
"Kee B. PARK" "24ab3297-bfce-473c-9311-4b31e3a8974f"
"Laura Lander" "ac697456-61fe-4f7d-a8b8-d04866032320"
"Nancy J Duff-Boehm" "5c7c09bc-4f23-432c-bfd9-87a66b548502"
"Owen Lander" "9671a2c4-ff95-48c2-bcde-5c6eba95cded"
"Susan Park" "4a9f94c5-d766-4808-ab76-117c9e43903a"
* Scott
* Michelle
* Brie
### "IDAA Couples Meeting" = "e9e162f0-3d03-4241-9682-340135ec3fb8"
* "Gregory X Boehm" "00ee764c-7559-496b-9d18-40d3e9092c0c"
* "Kee B. PARK" "24ab3297-bfce-473c-9311-4b31e3a8974f"
* "Laura Lander" "ac697456-61fe-4f7d-a8b8-d04866032320"
* "Nancy J Duff-Boehm" "5c7c09bc-4f23-432c-bfd9-87a66b548502"
* "Owen Lander" "9671a2c4-ff95-48c2-bcde-5c6eba95cded"
* "Susan Park" "4a9f94c5-d766-4808-ab76-117c9e43903a"
### "Student/Resident Meeting Moderators" "d76d2c00-962d-40f6-a2e8-ed9c85594d96"
"Melissa Eve Valasky" "182d1db3-caa9-41bc-b04a-2facc6859aeb"
"Steven L. Klein" "5724aad7-6d89-47e7-8943-966fd22911bd"
* "Melissa Eve Valasky" "182d1db3-caa9-41bc-b04a-2facc6859aeb"
* "Steven L. Klein" "5724aad7-6d89-47e7-8943-966fd22911bd"
### "IDAA BIPOC Meeting" "873d3ad0-2605-4ccf-824c-638c16b2b9cf"
"Paula Lynn Bailey-Walton" "68383ba2-0989-4860-9ea6-073f9698df67"
"Tasha Hudson" "03d5408c-3c13-4c3a-a93f-49871f9050b1"
* "Paula Lynn Bailey-Walton" "68383ba2-0989-4860-9ea6-073f9698df67"
* "Tasha Hudson" "03d5408c-3c13-4c3a-a93f-49871f9050b1"
---

View File

@@ -260,3 +260,10 @@ Guard in [ae_tab__manage.svelte](src/routes/events/[event_id]/(leads)/leads/exhi
- Export endpoint: `GET /v3/action/event_exhibit/{id}/tracking_export` — requires `leads_api_access`
- Custom questions are stored per-exhibit in `leads_custom_questions_json` (not global)
- The exhibitor landing page link format: `/events/[event_id]/leads/exhibit/[exhibit_exhibit_id]/`
## Old Files for Reference
@backups/legacy/events_leads_v2/exhibit/[slug]/+page.svelte
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_manage.svelte
@backups/legacy/events_leads_v2/exhibit/[slug]/leads_payment.svelte

View File

@@ -32,29 +32,37 @@ frontend pass the location directly in the Launcher URL without the extra lookup
### [Svelte] State reference warnings
- [x] **`svelte-check` fully clean — 0 errors, 0 warnings.** All 42 `state_referenced_locally` warnings fixed (2026-03-11). CSS `@apply`/`@reference` warnings in `ae_idaa_comp__event_obj_id_edit.svelte` also resolved — Tailwind utilities inlined, `<style>` block removed. (2026-03-16)
### [TypeScript] svelte-check hidden errors — discovered 2026-03-27
**HOW WE FOUND THIS:** The `@lucide/svelte` 0.577.0 update (2026-03-10) dropped `class` from
`IconProps`. Fixing it required a `declare module '@lucide/svelte'` augmentation. That
augmentation was mistakenly placed in `app.d.ts`, which is a *script-context* declaration file
(no `export {}`). In that context, `declare module` is an **ambient replacement**, not a merge —
it wiped all icon exports from svelte-check's view, surfacing 1368 previously hidden errors.
Once moved to `src/lucide-augment.d.ts` (a proper module file with `export {}`), the masking
lifted and the real pre-existing errors became visible.
**Lesson:** A broken ambient declaration can silently hide unrelated errors. If svelte-check
suddenly jumps to 0 errors, verify it's not because a bad `.d.ts` replaced a package's types.
**Current state (2026-03-27):** 31 errors, 0 warnings — all `ModalProps.children`.
- [ ] **[flowbite-svelte] `ModalProps.children` — 31 errors across 26 files.** The flowbite-svelte
`Modal` component API changed; `children` is no longer a direct prop (now Svelte snippet-based).
Affected files span journals, pres_mgmt, events/settings, and IDAA archives.
Run `npx svelte-check 2>&1 | grep ModalProps` to get the current list.
Fix pattern: replace `children` prop binding with Svelte snippet syntax per flowbite-svelte docs.
- [ ] **[package.json] Remove orphaned ShadCN/bits-ui packages.** `shadcn-svelte` and `bits-ui`
remain in `package.json` but have no usages — `src/lib/components/ui/` was removed 2026-03-27
(trashed to `~/tmp/gemini_trash/shadcn_components_ui_2026-03-27`). Safe to remove both packages
from `dependencies` when convenient.
### [Badges] Remaining badge work before first live event
- **Badge print controls UX polish:** Scott has improvements in mind — TBD next session.
File: `ae_comp__badge_print_controls.svelte`.
### [Badges] Zebra ZC10L Hardware Testing — ~week of 2026-03-16
Scott is renting a Zebra ZC10L for one day to do real-world badge printing tests before
Axonius (mid-April). See `documentation/PROJECT__AE_Events_Zebra_Hardware_Test_Day.md`
for the full checklist and prep plan.
**Pre-test work (do before printer arrives):**
- [x] **Debug outlines are gated**`print/+page.svelte` prints nothing; outlines live in
`static/ae-print-badge.css` behind `html.debug_outlines` class (toggled by the "Show debug
outlines" checkbox in the controls panel, trusted-only). Won't appear in print unless explicitly
enabled. No action needed. (verified 2026-03-18)
- [x] **Zebra ZC10L Linux driver** — installed CUPS driver; verified card prints. (2026-03-27)
- [x] **`style_href` wired** — `print/+page.svelte` already loads `style_href` via `<svelte:head>`
and it's in `properties_to_save`. (verified 2026-03-18)
- [x] **`duplex=0` hides badge back** — `duplex` is in `properties_to_save`; v2 badge render
gates `{#if show_badge_back}` on `duplex != null && !!duplex`. Set `duplex=0` on the template
to suppress the back section for single-sided PVC. (verified 2026-03-18)
- [x] **Set up test event + PVC template** in dev DB with `layout: badge_3.5x5.5_pvc`,
`duplex=0`, badge records with varied name lengths, HTML in fields, different badge_type_codes,
edge cases (very long name, HTML markup, no affiliations, all ticket/option codes). (2026-03-27)
### [Badges] Zebra ZC10L Hardware Testing
- [x] **Hardware test day complete.** Real-world badge printing tested on Zebra ZC10L. Driver installed, test data set up, print verified. (2026-03-27)
### [Leads] Exhibitor Lead Scanning — IN PROGRESS (demo-ready prep)
Module is substantially built as a PWA (no Electron). Core flow works end-to-end.
@@ -65,7 +73,7 @@ Full audit: `src/routes/events/[event_id]/(leads)/` and `src/lib/ae_events/ae_ev
- Exhibit search/landing (`/leads/`) — SWR, local + API search, sort
- Exhibit detail page — 4-tab layout, sticky header with Add/List toggle, auto-refresh timer
- Tab 1 (Start): sign-in via shared passcode OR licensed user (email + passcode)
- Tab 2 (Add): QR scan (rapid vs. qualify mode) + manual badge search; duplicate detection on both
- Tab 2 (Add): QR scan (confirm mode — replaced rapid/qualify) + manual badge search; duplicate/re-enable detection on both
- Tab 3 (List): SWR lead list, licensee filter (All / My Leads), sort options, export button
- Tab 4 (Manage): admin tools, booth profile edit, passcode, license mgmt, custom questions config, app settings (refresh interval, clear IDB/localStorage, reload)
- Lead detail page: view/edit custom question responses, exhibitor notes (TipTap), priority/enable flags
@@ -81,8 +89,12 @@ Full audit: `src/routes/events/[event_id]/(leads)/` and `src/lib/ae_events/ae_ev
Opt-in model: `allow_tracking` must be explicitly `true` on the badge. Also added `allow_tracking`
and `agree_to_tc` to `ae_EventBadge` in `ae_types.ts`.
**Demo note:** ensure test badges have `allow_tracking = true` or no one can be added.
- [ ] **Payment component**`ae_comp__exhibit_payment.svelte` is a stub (Stripe placeholder only);
omit from demo or hide the payment tab via "Show Payment Tab" toggle in Manage settings
- [x] **Payment component**`ae_comp__exhibit_payment.svelte` fully implemented (2026-03-27).
Reads Stripe config from `$ae_loc.site_cfg_json` (`stripe_publishable_key`, `stripe_btn_1/3/6/10_license`).
License tier selector (1/3/6/10 users) with `{#key}` remount pattern for Stripe web component.
3 states: paid confirmation (priority=true), admin setup hint / "contact organizer" (no Stripe config),
payment form. `client_reference_id=exhibit_id`. TypeScript declaration in `app.d.ts`.
Stripe keys verified visible in `$ae_loc.site_cfg_json` on dev/demo site. Keys need validity check in Stripe dashboard.
- [ ] **End-to-end smoke test** — sign in with shared passcode, scan/search a badge, add a lead,
view detail, add notes/responses, export CSV; verify on mobile (Chrome/Safari PWA)
- [x] **Install prompt** — PWA install nudge implemented (2026-03-16). `pwa_install.svelte.ts`
@@ -98,7 +110,7 @@ Full audit: `src/routes/events/[event_id]/(leads)/` and `src/lib/ae_events/ae_ev
- [x] **Remote deploy script:** `aether_container_env/deploy.sh` — SSH-triggered from workstation via `npm run deploy:remote:test/prod`. Handles git pull (ff-only) + docker build + restart. Tested and working on test env. (2026-03-25)
- [x] **`.env.default` cleanup:** Removed 16 dead variables, added missing `AE_NETWORK_NAME`/`CONTAINER_DOZZLE`/`AE_DOZZLE_PORT`, parameterized all container names (`CONTAINER_MARIADB`, `CONTAINER_PMA`, `CONTAINER_AE_OPS`) with `:-default` fallbacks in compose. ("Dozzle" = log viewer container.) (2026-03-26)
- [ ] **Prod deploy:** Run `npm run deploy:remote:prod` (off-peak). Prerequisites: both repos pushed to Bitbucket ✓; verify `.env.prod` exists in `/srv/apps/prod_aether_app_sveltekit/` on Linode before running.
- [ ] **Bitbucket → API token migration:** Bitbucket is deprecating app passwords — creation disabled 2025-09-09, existing passwords expire 2026-06-09. Migrate git remotes on workstation + Linode to use API tokens before then. See [Bitbucket API tokens docs](https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/).
- [x] **Bitbucket → SSH migration:** Switched all three repos (`aether_app_sveltekit`, `aether_container_env`, `aether_api_fastapi`) to SSH remotes (`git@bitbucket.org`) on workstation. App passwords deprecated — SSH unaffected. (2026-03-27)
- [ ] **Branch strategy cleanup:** All environments (test, prod, bak) currently pull from same branches. `deploy.sh` defaults are `ae_app_3x_llm` / `development` — acceptable for now but should establish proper branch separation (e.g. `main`/`master` for prod).
- [ ] **Tier 2 deploy (Gitea webhook):** Push-triggered deploys via Gitea webhook → listener on Linode → `deploy.sh`. Deferred until Gitea usage is more established.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "osit-aether-app-svelte",
"version": "3.00.05",
"version": "3.00.07",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "osit-aether-app-svelte",
"version": "3.00.05",
"version": "3.00.07",
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.0",

12
src/app.d.ts vendored
View File

@@ -22,3 +22,15 @@ declare global {
// eslint-disable-next-line no-var
var native_app: any;
}
// Stripe Buy Button web component — needed so Svelte templates accept the element without TS errors.
declare module 'svelte/elements' {
interface IntrinsicElements {
'stripe-buy-button': {
'buy-button-id': string;
'publishable-key': string;
'client-reference-id'?: string;
[attr: string]: any;
};
}
}

View File

@@ -1,7 +0,0 @@
[Dolphin]
Timestamp=2024,12,2,17,34,30.327
Version=4
ViewMode=1
[Settings]
HiddenFilesShown=true

View File

@@ -1,11 +0,0 @@
{
"rules": {
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^\\$\\$(Props|Events|Slots|Generic)$"
}
]
}
}

View File

@@ -1,78 +0,0 @@
<script lang="ts" module>
import type { WithElementRef } from 'bits-ui';
import type {
HTMLAnchorAttributes,
HTMLButtonAttributes
} from 'svelte/elements';
import { type VariantProps, tv } from 'tailwind-variants';
export const buttonVariants = tv({
base: 'ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border-input bg-background hover:bg-accent hover:text-accent-foreground border',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
class?: any;
};
</script>
<script lang="ts">
import { cn } from '$lib/utils/utils.js';
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
type = 'button',
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
{href}
{...restProps}>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
{type}
{...restProps}>
{@render children?.()}
</button>
{/if}

View File

@@ -1,9 +0,0 @@
import Root, { buttonVariants } from './button.svelte';
export {
Root,
//
Root as Button,
buttonVariants
};

View File

@@ -1,17 +0,0 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants
} from './button.svelte';
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant
};

View File

@@ -1,42 +0,0 @@
<script lang="ts">
import {
DropdownMenu as DropdownMenuPrimitive,
type WithoutChildrenOrChild
} from 'bits-ui';
import Check from 'lucide-svelte/icons/check';
import Minus from 'lucide-svelte/icons/minus';
import { cn } from '$lib/utils/utils.js';
import type { Snippet } from 'svelte';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
class={cn(
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50',
className
)}
{...restProps}>
{#snippet children({ checked, indeterminate })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn('size-4', !checked && 'text-transparent')} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
{sideOffset}
class={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md outline-hidden',
className
)}
{...restProps} />
</DropdownMenuPrimitive.Portal>

View File

@@ -1,18 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.GroupHeadingProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
{...restProps} />

View File

@@ -1,22 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils/utils.js';
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className
)}
{...restProps} />

View File

@@ -1,22 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils/utils.js';
import { type WithElementRef } from 'bits-ui';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
{...restProps}>
{@render children?.()}
</div>

View File

@@ -1,32 +0,0 @@
<script lang="ts">
import {
DropdownMenu as DropdownMenuPrimitive,
type WithoutChild
} from 'bits-ui';
import Circle from 'lucide-svelte/icons/circle';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
class={cn(
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50',
className
)}
{...restProps}>
{#snippet children({ checked })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
class={cn('bg-muted -mx-1 my-1 h-px', className)}
{...restProps} />

View File

@@ -1,19 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { type WithElementRef } from 'bits-ui';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
class={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...restProps}>
{@render children?.()}
</span>

View File

@@ -1,18 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
class={cn(
'bg-popover text-popover-foreground z-50 min-w-32 rounded-md border p-1 shadow-lg focus:outline-hidden',
className
)}
{...restProps} />

View File

@@ -1,27 +0,0 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
class={cn(
'data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className
)}
{...restProps}>
{@render children?.()}
<ChevronRight class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>

View File

@@ -1,50 +0,0 @@
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
import Content from './dropdown-menu-content.svelte';
import GroupHeading from './dropdown-menu-group-heading.svelte';
import Item from './dropdown-menu-item.svelte';
import Label from './dropdown-menu-label.svelte';
import RadioItem from './dropdown-menu-radio-item.svelte';
import Separator from './dropdown-menu-separator.svelte';
import Shortcut from './dropdown-menu-shortcut.svelte';
import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
const RadioGroup = DropdownMenuPrimitive.RadioGroup;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
GroupHeading as DropdownMenuGroupHeading,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger
};

View File

@@ -1,50 +0,0 @@
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
import Content from './dropdown-menu-content.svelte';
import GroupHeading from './dropdown-menu-group-heading.svelte';
import Item from './dropdown-menu-item.svelte';
import Label from './dropdown-menu-label.svelte';
import RadioItem from './dropdown-menu-radio-item.svelte';
import Separator from './dropdown-menu-separator.svelte';
import Shortcut from './dropdown-menu-shortcut.svelte';
import SubContent from './dropdown-menu-sub-content.svelte';
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
const RadioGroup = DropdownMenuPrimitive.RadioGroup;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
GroupHeading as DropdownMenuGroupHeading,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger
};

View File

@@ -1,7 +0,0 @@
import Root from './input.svelte';
export {
Root,
//
Root as Input
};

View File

@@ -1,7 +0,0 @@
import Root from './input.svelte';
export {
Root,
//
Root as Input
};

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import type { WithElementRef } from 'bits-ui';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
value = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
</script>
<input
bind:this={ref}
class={cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:value
{...restProps} />

View File

@@ -1,17 +0,0 @@
import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose
};

View File

@@ -1,17 +0,0 @@
import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close;
export {
Root,
Content,
Trigger,
Close,
//
Root as Popover,
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose
};

View File

@@ -1,27 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils/utils.js';
import { Popover as PopoverPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
align = 'center',
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
portalProps?: PopoverPrimitive.PortalProps;
} = $props();
</script>
<PopoverPrimitive.Portal {...portalProps}>
<PopoverPrimitive.Content
bind:ref
{sideOffset}
{align}
class={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden',
className
)}
{...restProps} />
</PopoverPrimitive.Portal>

View File

@@ -1,7 +0,0 @@
import Root from './separator.svelte';
export {
Root,
//
Root as Separator
};

View File

@@ -1,7 +0,0 @@
import Root from './separator.svelte';
export {
Root,
//
Root as Separator
};

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
orientation = 'horizontal',
...restProps
}: SeparatorPrimitive.RootProps = $props();
</script>
<SeparatorPrimitive.Root
bind:ref
class={cn(
'bg-border shrink-0',
orientation === 'horizontal' ? 'h-px w-full' : 'min-h-full w-px',
className
)}
{orientation}
{...restProps} />

View File

@@ -1,18 +0,0 @@
import { Tooltip as TooltipPrimitive } from 'bits-ui';
import Content from './tooltip-content.svelte';
const Root = TooltipPrimitive.Root;
const Trigger = TooltipPrimitive.Trigger;
const Provider = TooltipPrimitive.Provider;
export {
Root,
Trigger,
Content,
Provider,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider
};

View File

@@ -1,18 +0,0 @@
import { Tooltip as TooltipPrimitive } from 'bits-ui';
import Content from './tooltip-content.svelte';
const Root = TooltipPrimitive.Root;
const Trigger = TooltipPrimitive.Trigger;
const Provider = TooltipPrimitive.Provider;
export {
Root,
Trigger,
Content,
Provider,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider
};

View File

@@ -1,20 +0,0 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
import { cn } from '$lib/utils/utils.js';
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
...restProps
}: TooltipPrimitive.ContentProps = $props();
</script>
<TooltipPrimitive.Content
bind:ref
{sideOffset}
class={cn(
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
className
)}
{...restProps} />

26
src/lucide-augment.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
// Module augmentation to restore `class` prop on @lucide/svelte IconProps.
//
// WHY this file exists and why it is separate from app.d.ts:
// @lucide/svelte ≥ 0.577.0 dropped `class` from IconProps (it now derives props purely from
// SVGAttributes<SVGSVGElement>, which TypeScript types without `class`). Every
// <SomeIcon class="..." /> in the codebase errors without this.
//
// WHY it cannot live in app.d.ts:
// app.d.ts is a script-context declaration file (no top-level import/export). A
// `declare module 'x' {}` in a script file is an AMBIENT MODULE DECLARATION that completely
// replaces the package's types — not an augmentation. That caused svelte-check to see
// @lucide/svelte as exporting only IconProps, making every icon import a "no exported member"
// error. Putting the declaration here with `export {}` makes this file a module, so
// `declare module` becomes a proper augmentation that merges with the package types.
//
// If a future lucide update re-adds `class` natively, this file becomes a harmless no-op.
// Root cause: @lucide/svelte 0.561.0 → 0.577.0 bump in chore commit 366c6629 (2026-03-10).
export {};
declare module '@lucide/svelte' {
interface IconProps {
class?: string;
'aria-hidden'?: string | boolean;
}
}

View File

@@ -411,7 +411,7 @@ $effect(() => {
</div>
{/if}
{#if browser && (!$ae_loc?.iframe || $ae_loc?.administrator_access || ($ae_loc?.trusted_access && $ae_loc.edit_mode))}
{#if browser && (!$ae_loc?.iframe || !$ae_loc?.sys_menu?.hide || $ae_loc?.administrator_access || ($ae_loc?.trusted_access && $ae_loc.edit_mode))}
<!-- print:hidden wrapper: sys/debug menus are fixed overlays — must not appear on printed pages -->
<div class="print:hidden">
<E_app_sys_bar

View File

@@ -478,7 +478,7 @@ function toggle_manage_tab() {
<Tab_add exhibit_id={page.params.exhibit_id ?? ''} />
{:else if active_tab === 'payment'}
<div class="mx-auto w-full max-w-4xl">
<Comp_exhibit_payment />
<Comp_exhibit_payment exhibit_id={page.params.exhibit_id ?? ''} />
</div>
{:else if active_tab === 'list'}
<div class="flex w-full flex-col space-y-6">

View File

@@ -1,11 +1,183 @@
<script lang="ts">
/**
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_payment.svelte
* Leads Payment Stub.
* ae_comp__exhibit_payment.svelte
* Leads Payment — Stripe Buy Button integration.
*
* Stripe config is read from $ae_loc.site_cfg_json (set per-event by admin in Site Settings):
* stripe_publishable_key — Stripe publishable key (pk_live_... or pk_test_...)
* stripe_btn_1_license — Stripe Buy Button ID for the 1-user license tier
* stripe_btn_3_license — Buy Button ID for 3-user tier
* stripe_btn_6_license — Buy Button ID for 6-user tier
* stripe_btn_10_license — Buy Button ID for 10-user tier
*
* client_reference_id = exhibit_id — ties each Stripe payment back to this booth record.
* Payment status (priority flag) is read live from Dexie (IDB).
*
* WHY {#key btn_payment_id}: stripe-buy-button is a web component that ignores attribute
* changes after its initial mount. Keying on btn_payment_id forces Svelte to fully
* remount the element whenever the selected license tier changes.
*/
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_loc } from '$lib/stores/ae_stores';
import { AlertTriangle, CheckCircle, CreditCard } from '@lucide/svelte';
interface Props {
exhibit_id: string;
}
let { exhibit_id }: Props = $props();
const lq__exhibit_obj = liveQuery(() => {
if (!exhibit_id) return undefined;
return db_events.exhibit.get(exhibit_id);
});
// Stripe config from site_cfg_json — set per-event by admin in Site Settings.
const stripe_publishable_key = $derived(
($ae_loc.site_cfg_json?.stripe_publishable_key as string | undefined) ?? null
);
const stripe_btn_ids = $derived({
1: ($ae_loc.site_cfg_json?.stripe_btn_1_license as string | undefined) ?? null,
3: ($ae_loc.site_cfg_json?.stripe_btn_3_license as string | undefined) ?? null,
6: ($ae_loc.site_cfg_json?.stripe_btn_6_license as string | undefined) ?? null,
10: ($ae_loc.site_cfg_json?.stripe_btn_10_license as string | undefined) ?? null,
});
const license_tiers = [
{ qty: 1, label: '1 user license', price: '$120' },
{ qty: 3, label: 'Up to 3 user licenses', price: '$330' },
{ qty: 6, label: 'Up to 6 user licenses', price: '$660' },
{ qty: 10, label: 'Up to 10 user licenses', price: '$1,100' },
] as const;
type LicenseQty = 1 | 3 | 6 | 10;
let selected_qty: LicenseQty = $state(1);
const btn_payment_id = $derived(stripe_btn_ids[selected_qty] ?? null);
const is_stripe_configured = $derived(!!stripe_publishable_key);
// Inject the Stripe Buy Button JS once per session (idempotent — skips if already present).
// document.head is outside Svelte's managed DOM tree so this is safe.
$effect(() => {
if (!is_stripe_configured) return;
if (document.querySelector('script[src*="stripe.com/v3/buy-button"]')) return;
const script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/buy-button.js';
script.async = true;
document.head.appendChild(script);
});
</script>
<div class="exhibit-payment card p-4">
<h3 class="h3">Payment & Licensing</h3>
<p>Placeholder for Stripe integration.</p>
<div class="exhibit-payment space-y-6">
{#if $lq__exhibit_obj?.priority}
<!-- Paid Confirmation — shown when admin has marked this booth as paid (priority = true) -->
<div class="card preset-tonal-success border-success-500/30 border p-6">
<div class="mb-3 flex items-center gap-3">
<CheckCircle size="1.5em" class="text-success-500" />
<h4 class="text-success-500 text-lg font-bold">Marked as Paid</h4>
</div>
<p class="text-sm">
Thank you for your payment. You have purchased
<strong>{$lq__exhibit_obj?.license_max ?? 0}</strong> user license(s)
for lead retrieval at this event.
</p>
{#if ($lq__exhibit_obj?.leads_device_sm_qty ?? 0) > 0}
<p class="mt-2 text-sm">
Rental device(s): <strong>{$lq__exhibit_obj?.leads_device_sm_qty}</strong>.
Pick them up at onsite registration.
</p>
{:else}
<p class="mt-2 text-sm opacity-60">
No rental devices. Use your own device(s) with this service.
</p>
{/if}
</div>
{:else if !is_stripe_configured}
<!-- Stripe not configured — show a setup hint only to admins -->
{#if $ae_loc.administrator_access}
<div
class="card preset-tonal-warning border-warning-500/30 flex items-start gap-3 border p-4">
<AlertTriangle size="1.2em" class="text-warning-500 mt-0.5 shrink-0" />
<div class="text-sm">
<p class="font-bold">Stripe not configured for this site.</p>
<p class="mt-1 opacity-70">
Add <code class="font-mono text-xs">stripe_publishable_key</code>,
<code class="font-mono text-xs">stripe_btn_1_license</code>,
<code class="font-mono text-xs">stripe_btn_3_license</code>,
<code class="font-mono text-xs">stripe_btn_6_license</code>, and
<code class="font-mono text-xs">stripe_btn_10_license</code>
to <strong>Site Config JSON</strong> to enable payment.
</p>
</div>
</div>
{:else}
<p class="py-4 text-center text-sm opacity-50">
Online payment is not available at this time. Please contact the event organizer.
</p>
{/if}
{:else}
<!-- Payment Form — Stripe configured, booth not yet marked as paid -->
<div class="space-y-6">
<div class="flex items-center gap-2">
<CreditCard size="1.2em" class="text-success-500" />
<h4 class="text-lg font-bold">Purchase Licenses</h4>
</div>
<div class="card preset-tonal-surface border-surface-500/10 border p-4 text-sm">
<p>
Each person from your booth who will scan attendee badges needs their own user
license. You can use your own smartphone, tablet, or laptop — rental devices are
not required.
</p>
</div>
<!-- License Tier Selector -->
<div class="space-y-1">
<label class="label">
<span class="text-xs font-black tracking-widest uppercase opacity-40"
>Select License Tier</span>
<select class="select mt-2 w-full" bind:value={selected_qty}>
{#each license_tiers as tier (tier.qty)}
<option value={tier.qty}>{tier.label} {tier.price}</option>
{/each}
</select>
</label>
<p class="text-xs opacity-50">One license per team member who will scan badges.</p>
</div>
<!-- Stripe Buy Button -->
<div class="flex flex-col items-center gap-3">
{#if stripe_publishable_key && btn_payment_id}
<!-- {#key} forces full remount when tier changes — stripe-buy-button ignores
attribute updates after initial render. See component comment above. -->
{#key btn_payment_id}
<stripe-buy-button
buy-button-id={btn_payment_id}
publishable-key={stripe_publishable_key}
client-reference-id={exhibit_id}>
</stripe-buy-button>
{/key}
<p class="text-center text-xs opacity-40">
Payment processed securely via Stripe. Verify quantities on the checkout
page.
</p>
<div class="card preset-tonal-warning w-full p-3 text-xs">
<strong>Note:</strong> Payment confirmation may take up to 2 business days
to reflect in your account status. Contact
<a
href="mailto:exhibits@oneskyit.com"
class="font-medium hover:underline">exhibits@oneskyit.com</a>
with questions.
</div>
{:else}
<p class="text-sm opacity-50">
No payment button is configured for this tier. Contact the event organizer.
</p>
{/if}
</div>
</div>
{/if}
</div>

View File

@@ -428,7 +428,7 @@ function handle_signout() {
{#if show_billing}
<div
class="bg-surface-500/5 border-surface-500/10 animate-in fade-in slide-in-from-top-2 border-t p-4">
<Comp_exhibit_payment />
<Comp_exhibit_payment {exhibit_id} />
</div>
{/if}
</div>

View File

@@ -477,23 +477,47 @@ async function fetch_novi_data() {
}
}
const novi_idaa_group_guid_li =
$ae_loc.site_cfg_json?.novi_idaa_group_guid_li ?? [];
const moderatorIdSet = await get_novi_group_moderators(
novi_idaa_group_guid_li,
novi_api_root_url,
novi_api_key
);
const normalizedUserId = String(user_id ?? '')
.toLowerCase()
.trim();
if (normalizedUserId && moderatorIdSet.has(normalizedUserId)) {
// Trusted/admin users are always moderators. Check the UUID directly against the
// known lists rather than $ae_loc.trusted_access — that flag is only upgraded, never
// downgraded, so it sticks across Novi impersonation (which does a full iframe reload
// with a different UUID but doesn't reset the inherited access level).
const admin_li: string[] = $idaa_loc.novi_admin_li ?? [];
const trusted_li: string[] = $idaa_loc.novi_trusted_li ?? [];
const is_trusted_uuid = user_id
? admin_li.includes(user_id) || trusted_li.includes(user_id)
: false;
if (is_trusted_uuid) {
is_moderator = true;
console.log(`Jitsi: User ${user_id} is a moderator.`);
console.log(`Jitsi: User ${user_id} is moderator via admin/trusted UUID list.`);
} else {
is_moderator = false; // Explicitly set to false if not in the set
console.log(`Jitsi: User ${user_id} is not a moderator.`);
// For regular authenticated members, check the specific meeting group.
// Prefer g_uuid from URL (per-meeting, more precise); fall back to the global
// novi_idaa_group_guid_li list for older Novi pages not yet passing g_uuid.
const group_uuid = url_params.g_uuid ?? null;
const group_guid_li = group_uuid
? [group_uuid]
: ($ae_loc.site_cfg_json?.novi_idaa_group_guid_li ?? []);
if (group_uuid) {
console.log(`Jitsi: Checking moderator via URL g_uuid: ${group_uuid}`);
} else {
console.log(`Jitsi: No g_uuid in URL — falling back to site config group list (${group_guid_li.length} groups).`);
}
const moderatorIdSet = await get_novi_group_moderators(
group_guid_li,
novi_api_root_url,
novi_api_key
);
const normalizedUserId = String(user_id ?? '')
.toLowerCase()
.trim();
if (normalizedUserId && moderatorIdSet.has(normalizedUserId)) {
is_moderator = true;
console.log(`Jitsi: User ${user_id} is a moderator.`);
} else {
is_moderator = false;
console.log(`Jitsi: User ${user_id} is not a moderator.`);
}
}
} else {
console.warn(
@@ -506,9 +530,31 @@ async function fetch_novi_data() {
email_input = email ?? '';
}
/**
* Dynamically loads the Jitsi external API script for a given domain and waits for it.
* Using <svelte:head> for this script is not reliable — it loads asynchronously and there
* is no lifecycle hook to await its completion, causing a race with onMount/init_jitsi.
* Loading it here lets us sequence it correctly: fetch_novi_data → load script → init_jitsi.
*/
function load_jitsi_script(jitsi_domain: string): Promise<void> {
return new Promise((resolve, reject) => {
// @ts-expect-error — JitsiMeetExternalAPI is a global injected by the Jitsi script
if (typeof JitsiMeetExternalAPI !== 'undefined') {
resolve(); // Already loaded (e.g. resync after first load)
return;
}
const script = document.createElement('script');
script.src = `https://${jitsi_domain}/external_api.js`;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load Jitsi script from ${jitsi_domain}`));
document.head.appendChild(script);
});
}
async function handle_novi_resync() {
console.log('Jitsi: Manually re-syncing Novi Data...');
await fetch_novi_data();
await load_jitsi_script(domain ?? 'jitsi.dgrzone.com');
await init_jitsi();
}
@@ -518,11 +564,24 @@ onMount(async () => {
'Jitsi: onMount - fetching user data and initializing Jitsi...'
);
}
// $ae_loc.sys_menu.hide = true;
await fetch_novi_data();
// --- All data fetched, now initialize Jitsi ---
if (!domain) {
console.error('Jitsi: domain not set after fetch_novi_data — cannot load Jitsi script.');
return;
}
try {
await load_jitsi_script(domain);
} catch (err) {
console.error('Jitsi: Failed to load external API script:', err);
const container = document.getElementById(jitsi_container_id);
if (container) container.innerHTML = '<h1>Jitsi API script failed to load. Please refresh the page.</h1>';
return;
}
// --- All data fetched and script ready, now initialize Jitsi ---
await init_jitsi();
});
@@ -814,9 +873,6 @@ async function init_jitsi() {
}
</script>
<svelte:head>
<script src="https://jitsi.dgrzone.com/external_api.js"></script>
</svelte:head>
{#if show_jitsi_container}
<div id={jitsi_container_id} class="jitsi-container"></div>

View File

@@ -4,6 +4,7 @@ export function load({ url }) {
return {
params: {
uuid: url.searchParams.get('uuid'),
g_uuid: url.searchParams.get('g_uuid'),
email: url.searchParams.get('email'),
full_name: url.searchParams.get('full_name'),
moderator: url.searchParams.get('moderator'),

View File

@@ -26,28 +26,61 @@
</p>
<!-- IDAA and Novi specific JavaScript to get current Novi user info and load Jitsi iframe -->
<script>
let novi_customer_uid = '<%=Novi.User.CustomerUniqueId%>'; // NOTE: The Novi UUID for the current current user/customer
// WARNING: Do not change this.
// NOTE: The Novi UUID for the current current user/customer
let novi_customer_uid = '<%=Novi.User.CustomerUniqueId%>';
console.log(`Novi's Current User's ID: ${novi_customer_uid}`);
let novi_group_uid = 'check-Novi-Group-UID';
// let novi_category_id = ''; // Not in use yet or at all?
/* *** ** * CHANGES START HERE * ** *** */
// NOTE: Change the novi_group_uid value to use one of the desired Novi group UID for this meetings moderators.
// NOTE: IDAA staff are always moderators for any Jitsi meeting.
let novi_group_uid =
'check-Novi-Group-UID';
// Example Novi group UIDs:
// * "IDAA Couples Meeting" uses "e9e162f0-3d03-4241-9682-340135ec3fb8"
// * "Student/Resident Meeting Moderators" uses "d76d2c00-962d-40f6-a2e8-ed9c85594d96"
// * "IDAA BIPOC Meeting" uses "873d3ad0-2605-4ccf-824c-638c16b2b9cf"
// NOTE: Change the room_name value to the desired Jitsi room name for the meeting.
let room_name =
'IDAA-Example-Meeting';
// Example meeting room names:
// 'IDAA-Meeting' 'IDAA-Student-and-Resident-Meeting' 'IDAA-Couples-Meeting' 'IDAA-BIPOC-Meeting'
let room_name = 'IDAA-Example-Meeting'; // // NOTE: Change this example meeting room name
// * 'IDAA-Meeting'
// * 'IDAA-Student-and-Resident-Meeting'
// * 'IDAA-Couples-Meeting'
// * 'IDAA-BIPOC-Meeting'
// WARNING: Do not use spaces in the room name. Use dashes or underscores instead. The room name must be unique to avoid conflicts with other meetings.
// WARNING:Do *not* use relative paths here. They must be direct to the site OSIT is hosting for IDAA. This value must point to the Svelte Jitsi page.
// NOTE: Change the idaa_osit_ae_api_root_url value to use one of the example URLs below.
let idaa_osit_ae_api_root_url =
'https://dev-idaa.oneskyit.com/idaa/video_conferences'; // NOTE: DO NOT CHANGE THIS VALUE
// Example URLs: 'https://sk-idaa.oneskyit.com/idaa/video_conferences' OR 'https://dev-idaa.oneskyit.com/idaa/video_conferences' OR 'http://idaa.localhost:5173/idaa/video_conferences
'https://dev-idaa.oneskyit.com/idaa/video_conferences';
// Example URLs:
// * production 'https://sk-idaa.oneskyit.com/idaa/video_conferences'
// * production 'https://idaa.oneskyit.com/idaa/video_conferences'
// * testing 'https://test-idaa.oneskyit.com/idaa/video_conferences'
// * development 'https://dev-idaa.oneskyit.com/idaa/video_conferences'
// WARNING: Do *not* use relative paths here. They must be direct to the site OSIT is hosting for IDAA. This value must point to the Svelte Jitsi page.
// WARNING: Do *not* change this value. It is required for access control to the IDAA AE API.
let idaa_osit_ae_site_key = 'restricted-access'; // DO NOT CHANGE THIS VALUE
// NOTE: IGNORE: This is not currently used, but will be soon for an added layer of access control on the API.
let idaa_osit_ae_site_key =
'restricted-access';
// Example site keys: '8VTOJ0X5hvT6JdiTJsGEzQ' OR 'restricted-access' OR 'restricted'
// NOTE: IGNORE: For now this is not used
// let novi_category_id = ''; // Not in use yet or at all
/* *** ** * CHANGES END HERE * ** *** */
let idaa_ae_params = new URLSearchParams(document.location.search);
let idaa_ae_iframe_element = document.getElementById(
'ae_idaa_jitsi_meeting_iframe'