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,
|
||||
LogIn,
|
||||
LayoutGrid,
|
||||
Search
|
||||
Search,
|
||||
LogOut
|
||||
} from 'lucide-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';
|
||||
@@ -45,8 +46,22 @@
|
||||
$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) ---
|
||||
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'
|
||||
|
||||
function set_active_tab(new_tab: string) {
|
||||
@@ -56,9 +71,6 @@
|
||||
$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 last_search_id = 0;
|
||||
let last_executed_key = '';
|
||||
@@ -296,6 +308,16 @@
|
||||
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>
|
||||
|
||||
<section
|
||||
@@ -311,46 +333,58 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 sm:gap-2">
|
||||
<!-- Add Lead / Lead List Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
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" />
|
||||
<span class="hidden sm:inline">Lead List</span>
|
||||
{:else}
|
||||
<Plus size="1.25em" class="sm:mr-2" />
|
||||
<span class="hidden sm:inline">Add Lead</span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if is_signed_in}
|
||||
<!-- Add Lead / Lead List Toggle -->
|
||||
<button
|
||||
type="button"
|
||||
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" />
|
||||
<span class="hidden sm:inline">Lead List</span>
|
||||
{:else}
|
||||
<Plus size="1.25em" class="sm:mr-2" />
|
||||
<span class="hidden sm:inline">Add Lead</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Payment (Conditional) -->
|
||||
{#if $ae_loc.show_leads_payment}
|
||||
<!-- Payment (Conditional) -->
|
||||
{#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
|
||||
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"
|
||||
class:variant-filled-surface={active_tab === 'manage'}
|
||||
class:variant-ghost-surface={active_tab !== 'manage'}
|
||||
onclick={toggle_manage_tab}
|
||||
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>
|
||||
{/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>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,11 +1,203 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 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>
|
||||
|
||||
<div class="exhibit-signin p-4 card">
|
||||
<h3 class="h3">Exhibitor Sign In</h3>
|
||||
<p>Placeholder for login logic.</p>
|
||||
<div class="exhibit-signin card p-6 variant-filled-surface shadow-xl border border-surface-500/20 space-y-6">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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">
|
||||
// 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>
|
||||
|
||||
<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">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
<!-- 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