Complete Exhibitor Leads Authentication and Portal Structure
- Implemented dual-mode Sign-In (Shared Passcode / Licensed User) with persistent state. - Added Manager Bypass logic for seamless admin access. - Implemented Welcome/Start tab with redirection to Lead List upon authentication. - Added Sign-Out functionality and Header navigation improvements. - Integrated all Manage tab sub-sections (Licenses, Questions, Payment).
This commit is contained in:
@@ -19,7 +19,8 @@
|
|||||||
List as ListIcon,
|
List as ListIcon,
|
||||||
LogIn,
|
LogIn,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Search
|
Search,
|
||||||
|
LogOut
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import Comp_exhibit_tracking_search from './ae_comp__exhibit_tracking_search.svelte';
|
import Comp_exhibit_tracking_search from './ae_comp__exhibit_tracking_search.svelte';
|
||||||
import Comp_exhibit_tracking_obj_li from './ae_comp__exhibit_tracking_obj_li.svelte';
|
import Comp_exhibit_tracking_obj_li from './ae_comp__exhibit_tracking_obj_li.svelte';
|
||||||
@@ -45,8 +46,22 @@
|
|||||||
$events_loc.leads.refresh_interval_sec = 25;
|
$events_loc.leads.refresh_interval_sec = 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Sign-In State (Derived) ---
|
||||||
|
// 1. Manager Access (Bypass) OR 2. Valid Exhibit Auth entry
|
||||||
|
let is_signed_in = $derived(
|
||||||
|
$ae_loc.manager_access ||
|
||||||
|
!!$events_loc.leads.auth_exhibit_kv?.[page.params.exhibit_id ?? '']
|
||||||
|
);
|
||||||
|
|
||||||
// --- Tab State (Sticky via Store) ---
|
// --- Tab State (Sticky via Store) ---
|
||||||
let active_tab = $derived($events_loc.leads.tab?.[page.params.exhibit_id ?? ''] ?? 'list');
|
let active_tab = $derived.by(() => {
|
||||||
|
const exhibit_id = page.params.exhibit_id;
|
||||||
|
if (!exhibit_id) return 'start';
|
||||||
|
const saved_tab = $events_loc.leads.tab?.[exhibit_id] ?? 'list';
|
||||||
|
// If signed in but stuck on start tab, go to list
|
||||||
|
if (is_signed_in && saved_tab === 'start') return 'list';
|
||||||
|
return saved_tab;
|
||||||
|
});
|
||||||
let previous_main_tab = $state('list'); // To remember if we were on 'add' or 'list' before going to 'manage'
|
let previous_main_tab = $state('list'); // To remember if we were on 'add' or 'list' before going to 'manage'
|
||||||
|
|
||||||
function set_active_tab(new_tab: string) {
|
function set_active_tab(new_tab: string) {
|
||||||
@@ -56,9 +71,6 @@
|
|||||||
$events_loc.leads.tab[exhibit_id] = new_tab;
|
$events_loc.leads.tab[exhibit_id] = new_tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock sign-in state for now
|
|
||||||
let is_signed_in = $state(true);
|
|
||||||
|
|
||||||
let tracking_id_li: Array<string> = $state([]); let search_debounce_timer: any = null;
|
let tracking_id_li: Array<string> = $state([]); let search_debounce_timer: any = null;
|
||||||
let last_search_id = 0;
|
let last_search_id = 0;
|
||||||
let last_executed_key = '';
|
let last_executed_key = '';
|
||||||
@@ -296,6 +308,16 @@
|
|||||||
set_active_tab('manage');
|
set_active_tab('manage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handle_signout() {
|
||||||
|
const exhibit_id = page.params.exhibit_id;
|
||||||
|
if (!exhibit_id) return;
|
||||||
|
if (confirm('Sign out from this booth?')) {
|
||||||
|
delete $events_loc.leads.auth_exhibit_kv[exhibit_id];
|
||||||
|
$events_sess.leads.entered_passcode = null;
|
||||||
|
set_active_tab('start');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -311,46 +333,58 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-1 sm:gap-2">
|
<div class="flex items-center gap-1 sm:gap-2">
|
||||||
<!-- Add Lead / Lead List Toggle -->
|
{#if is_signed_in}
|
||||||
<button
|
<!-- Add Lead / Lead List Toggle -->
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-sm variant-filled-primary font-bold shadow-sm px-2 sm:px-4"
|
type="button"
|
||||||
onclick={toggle_main_tab}
|
class="btn btn-sm variant-filled-primary font-bold shadow-sm px-2 sm:px-4"
|
||||||
>
|
onclick={toggle_main_tab}
|
||||||
{#if active_tab === 'add'}
|
>
|
||||||
<ListIcon size="1.25em" class="sm:mr-2" />
|
{#if active_tab === 'add'}
|
||||||
<span class="hidden sm:inline">Lead List</span>
|
<ListIcon size="1.25em" class="sm:mr-2" />
|
||||||
{:else}
|
<span class="hidden sm:inline">Lead List</span>
|
||||||
<Plus size="1.25em" class="sm:mr-2" />
|
{:else}
|
||||||
<span class="hidden sm:inline">Add Lead</span>
|
<Plus size="1.25em" class="sm:mr-2" />
|
||||||
{/if}
|
<span class="hidden sm:inline">Add Lead</span>
|
||||||
</button>
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Payment (Conditional) -->
|
<!-- Payment (Conditional) -->
|
||||||
{#if $ae_loc.show_leads_payment}
|
{#if $ae_loc.show_leads_payment}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm transition-colors px-2 sm:px-3"
|
||||||
|
class:variant-filled-success={active_tab === 'payment'}
|
||||||
|
class:variant-ghost-success={active_tab !== 'payment'}
|
||||||
|
onclick={() => set_active_tab('payment')}
|
||||||
|
title="Payment & Upgrades"
|
||||||
|
>
|
||||||
|
<CreditCard size="1.25em" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Manage / Config -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm transition-colors px-2 sm:px-3"
|
class="btn btn-sm transition-colors px-2 sm:px-3"
|
||||||
class:variant-filled-success={active_tab === 'payment'}
|
class:variant-filled-surface={active_tab === 'manage'}
|
||||||
class:variant-ghost-success={active_tab !== 'payment'}
|
class:variant-ghost-surface={active_tab !== 'manage'}
|
||||||
onclick={() => set_active_tab('payment')}
|
onclick={toggle_manage_tab}
|
||||||
title="Payment & Upgrades"
|
title="Manage Exhibit"
|
||||||
>
|
>
|
||||||
<CreditCard size="1.25em" />
|
<Settings size="1.25em" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Sign Out -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm variant-ghost-error px-2 sm:px-3"
|
||||||
|
onclick={handle_signout}
|
||||||
|
title="Sign Out"
|
||||||
|
>
|
||||||
|
<LogOut size="1.25em" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Manage / Config -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm transition-colors px-2 sm:px-3"
|
|
||||||
class:variant-filled-surface={active_tab === 'manage'}
|
|
||||||
class:variant-ghost-surface={active_tab !== 'manage'}
|
|
||||||
onclick={toggle_manage_tab}
|
|
||||||
title="Manage Exhibit"
|
|
||||||
>
|
|
||||||
<Settings size="1.25em" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,203 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_signin.svelte
|
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_signin.svelte
|
||||||
* Exhibitor Login Form Stub.
|
* Exhibitor Sign-In Component - Handles both Shared Passcode and Licensed User login.
|
||||||
*/
|
*/
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
import { db_events } from '$lib/ae_events/db_events';
|
||||||
|
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
|
||||||
|
import { Key, Mail, Lock, User, ArrowRight, LoaderCircle, AlertCircle, CheckCircle2 } from 'lucide-svelte';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
|
const exhibit_id = $derived(page.params.exhibit_id ?? '');
|
||||||
|
|
||||||
|
let lq__exhibit_obj = $derived(
|
||||||
|
liveQuery(async () => {
|
||||||
|
if (!exhibit_id) return null;
|
||||||
|
return await db_events.exhibit.get(exhibit_id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Form State
|
||||||
|
let signin_mode = $state('passcode'); // 'passcode' or 'licensed'
|
||||||
|
let passcode = $state('');
|
||||||
|
let email = $state('');
|
||||||
|
let user_passcode = $state('');
|
||||||
|
let status = $state('idle'); // 'idle', 'submitting', 'error', 'success'
|
||||||
|
let error_msg = $state('');
|
||||||
|
|
||||||
|
async function handle_signin() {
|
||||||
|
if (!$lq__exhibit_obj) return;
|
||||||
|
status = 'submitting';
|
||||||
|
error_msg = '';
|
||||||
|
|
||||||
|
// Delay for better UX
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
if (signin_mode === 'passcode') {
|
||||||
|
// 1. Shared Passcode logic
|
||||||
|
if (passcode === $lq__exhibit_obj.staff_passcode) {
|
||||||
|
// SUCCESS
|
||||||
|
complete_signin($lq__exhibit_obj.staff_passcode, 'shared');
|
||||||
|
} else {
|
||||||
|
status = 'error';
|
||||||
|
error_msg = 'Invalid shared passcode. Please check with your booth manager.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2. Licensed User logic
|
||||||
|
try {
|
||||||
|
const licenses = JSON.parse($lq__exhibit_obj.license_li_json || '[]');
|
||||||
|
const found = licenses.find((l: any) => l.email.toLowerCase() === email.toLowerCase().trim());
|
||||||
|
|
||||||
|
if (found && found.passcode === user_passcode) {
|
||||||
|
// SUCCESS
|
||||||
|
complete_signin(found.email, 'licensed');
|
||||||
|
} else {
|
||||||
|
status = 'error';
|
||||||
|
error_msg = 'Invalid email or personal passcode.';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
status = 'error';
|
||||||
|
error_msg = 'System error validating licenses.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function complete_signin(key: string, type: string) {
|
||||||
|
status = 'success';
|
||||||
|
|
||||||
|
// Save to persistent store
|
||||||
|
if (!$events_loc.leads.auth_exhibit_kv) $events_loc.leads.auth_exhibit_kv = {};
|
||||||
|
|
||||||
|
$events_loc.leads.auth_exhibit_kv[exhibit_id] = {
|
||||||
|
key: key,
|
||||||
|
type: type,
|
||||||
|
updated_on: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also update session passcode if shared mode
|
||||||
|
if (type === 'shared') {
|
||||||
|
$events_sess.leads.entered_passcode = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger a reload or UI update if needed
|
||||||
|
// (The parent +page.svelte should reactively update is_signed_in)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="exhibit-signin p-4 card">
|
<div class="exhibit-signin card p-6 variant-filled-surface shadow-xl border border-surface-500/20 space-y-6">
|
||||||
<h3 class="h3">Exhibitor Sign In</h3>
|
|
||||||
<p>Placeholder for login logic.</p>
|
<!-- Tab Toggle -->
|
||||||
|
<div class="flex p-1 bg-surface-500/10 rounded-xl">
|
||||||
|
<button
|
||||||
|
class="flex-1 flex items-center justify-center gap-2 py-2 rounded-lg text-sm font-bold transition-all"
|
||||||
|
class:bg-surface-100-900={signin_mode === 'passcode'}
|
||||||
|
class:shadow-sm={signin_mode === 'passcode'}
|
||||||
|
class:opacity-50={signin_mode !== 'passcode'}
|
||||||
|
onclick={() => signin_mode = 'passcode'}
|
||||||
|
>
|
||||||
|
<Lock size="1.2em" /> Shared Code
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex-1 flex items-center justify-center gap-2 py-2 rounded-lg text-sm font-bold transition-all"
|
||||||
|
class:bg-surface-100-900={signin_mode === 'licensed'}
|
||||||
|
class:shadow-sm={signin_mode === 'licensed'}
|
||||||
|
class:opacity-50={signin_mode !== 'licensed'}
|
||||||
|
onclick={() => signin_mode = 'licensed'}
|
||||||
|
>
|
||||||
|
<User size="1.2em" /> Licensed User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forms -->
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); handle_signin(); }} class="space-y-4">
|
||||||
|
|
||||||
|
{#if signin_mode === 'passcode'}
|
||||||
|
<div class="space-y-2 animate-in fade-in slide-in-from-left-2">
|
||||||
|
<label class="label">
|
||||||
|
<span class="text-[10px] uppercase font-bold opacity-50 ml-1 tracking-widest">Booth Passcode</span>
|
||||||
|
<div class="input-group input-group-divider grid-cols-[auto_1fr] variant-soft rounded-xl overflow-hidden border border-surface-500/20">
|
||||||
|
<div class="input-group-shim"><Key size="1.2em" /></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={passcode}
|
||||||
|
placeholder="Enter shared code..."
|
||||||
|
class="bg-transparent font-mono tracking-[0.3em] font-bold text-center"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-4 animate-in fade-in slide-in-from-right-2">
|
||||||
|
<label class="label">
|
||||||
|
<span class="text-[10px] uppercase font-bold opacity-50 ml-1 tracking-widest">Email Address</span>
|
||||||
|
<div class="input-group input-group-divider grid-cols-[auto_1fr] variant-soft rounded-xl overflow-hidden border border-surface-500/20">
|
||||||
|
<div class="input-group-shim"><Mail size="1.2em" /></div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
placeholder="your@email.com"
|
||||||
|
class="bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="label">
|
||||||
|
<span class="text-[10px] uppercase font-bold opacity-50 ml-1 tracking-widest">Personal Passcode</span>
|
||||||
|
<div class="input-group input-group-divider grid-cols-[auto_1fr] variant-soft rounded-xl overflow-hidden border border-surface-500/20">
|
||||||
|
<div class="input-group-shim"><Key size="1.2em" /></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={user_passcode}
|
||||||
|
placeholder="Your code..."
|
||||||
|
class="bg-transparent font-mono font-bold"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if status === 'error'}
|
||||||
|
<div class="p-3 rounded-lg variant-soft-error flex items-start gap-3 animate-shake">
|
||||||
|
<AlertCircle size="1.2em" class="shrink-0 mt-0.5" />
|
||||||
|
<p class="text-xs font-bold leading-tight">{error_msg}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-lg variant-filled-primary w-full font-bold shadow-lg shadow-primary-500/20 group"
|
||||||
|
disabled={status === 'submitting'}
|
||||||
|
>
|
||||||
|
{#if status === 'submitting'}
|
||||||
|
<LoaderCircle size="1.5em" class="animate-spin mr-2" />
|
||||||
|
Signing In...
|
||||||
|
{:else if status === 'success'}
|
||||||
|
<CheckCircle2 size="1.5em" class="mr-2" />
|
||||||
|
Welcome!
|
||||||
|
{:else}
|
||||||
|
Get Started <ArrowRight size="1.2em" class="ml-2 group-hover:translate-x-1 transition-transform" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-[10px] text-center opacity-40 italic">
|
||||||
|
Check your welcome email or ask your booth manager for login details.
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
/* Shake animation for errors */
|
||||||
|
@keyframes shake {
|
||||||
|
0%, 100% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-4px); }
|
||||||
|
75% { transform: translateX(4px); }
|
||||||
|
}
|
||||||
|
.animate-shake {
|
||||||
|
animation: shake 0.2s ease-in-out 0s 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,64 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// Page for viewing/editing a single lead
|
/**
|
||||||
|
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__start.svelte
|
||||||
|
* Tab 1: Start / Sign In / Welcome.
|
||||||
|
*/
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
import { db_events } from '$lib/ae_events/db_events';
|
||||||
|
import Comp_exhibit_signin from './ae_comp__exhibit_signin.svelte';
|
||||||
|
import { LayoutGrid, CheckCircle2, UserCheck, ShieldCheck } from 'lucide-svelte';
|
||||||
|
|
||||||
|
const exhibit_id = $derived(page.params.exhibit_id ?? '');
|
||||||
|
|
||||||
|
let lq__exhibit_obj = $derived(
|
||||||
|
liveQuery(async () => {
|
||||||
|
if (!exhibit_id) return null;
|
||||||
|
return await db_events.exhibit.get(exhibit_id);
|
||||||
|
})
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="h1">Lead Details</h1>
|
<div class="ae-tab-start w-full space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
|
|
||||||
<p>This page will show the details for a single lead. A Lead is actually in the event_exhibit_tracking table in the MariaDB or exhibit_tracking table in the Indexed DB ae_events_db.</p>
|
<!-- Hero / Welcome Section -->
|
||||||
|
<section class="text-center space-y-4 py-6">
|
||||||
|
<div class="inline-flex p-4 rounded-full bg-primary-500/10 text-primary-500 mb-2">
|
||||||
|
<LayoutGrid size="3em" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-3xl font-black tracking-tight">
|
||||||
|
Welcome to the<br />
|
||||||
|
<span class="text-primary-500">Exhibitor Portal</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg opacity-60 max-w-md mx-auto">
|
||||||
|
Ready to capture leads for <span class="font-bold text-surface-900-100">{$lq__exhibit_obj?.name || 'this exhibit'}</span>?
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Features Grid (Compact) -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-2xl mx-auto px-4">
|
||||||
|
<div class="flex flex-col items-center text-center p-4 rounded-xl variant-soft-surface">
|
||||||
|
<CheckCircle2 size="1.5em" class="text-success-500 mb-2" />
|
||||||
|
<span class="text-xs font-bold uppercase tracking-wider">Fast Capture</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center text-center p-4 rounded-xl variant-soft-surface">
|
||||||
|
<UserCheck size="1.5em" class="text-secondary-500 mb-2" />
|
||||||
|
<span class="text-xs font-bold uppercase tracking-wider">Staff IDs</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center text-center p-4 rounded-xl variant-soft-surface">
|
||||||
|
<ShieldCheck size="1.5em" class="text-primary-500 mb-2" />
|
||||||
|
<span class="text-xs font-bold uppercase tracking-wider">Secure Sync</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sign In Area -->
|
||||||
|
<div class="w-full max-w-md mx-auto">
|
||||||
|
<Comp_exhibit_signin />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Footer -->
|
||||||
|
<div class="text-center pt-8 opacity-40">
|
||||||
|
<p class="text-[10px] uppercase font-black tracking-[0.2em]">Powered by Aether Platform</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user