Compare commits
15 Commits
1e2c9d9b74
...
83e271a323
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e271a323 | ||
|
|
ace90ad043 | ||
|
|
d139ed1bd0 | ||
|
|
3d988222a1 | ||
|
|
5433a906bb | ||
|
|
d89218be15 | ||
|
|
a8e9bd6694 | ||
|
|
6cd3b5f8f9 | ||
|
|
b33c1b16f6 | ||
|
|
d7a0857bed | ||
|
|
6939c058d8 | ||
|
|
b88a7de358 | ||
|
|
27f0bd21fb | ||
|
|
f111670f60 | ||
|
|
045efa71e1 |
@@ -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"
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
@@ -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
4
package-lock.json
generated
@@ -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
12
src/app.d.ts
vendored
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[Dolphin]
|
||||
Timestamp=2024,12,2,17,34,30.327
|
||||
Version=4
|
||||
ViewMode=1
|
||||
|
||||
[Settings]
|
||||
HiddenFilesShown=true
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^\\$\\$(Props|Events|Slots|Generic)$"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -1,9 +0,0 @@
|
||||
import Root, { buttonVariants } from './button.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input
|
||||
};
|
||||
@@ -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} />
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator
|
||||
};
|
||||
@@ -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} />
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
26
src/lucide-augment.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user