feat(launcher): dedicated Digital Poster session card-grid view
Create launcher_session_view_posters.svelte — a touch-first card-grid layout for Digital Poster sessions, designed for tablet/phone PWA use. Layout: - 1 column on mobile, 2 on sm, 3 on xl - Each poster card: title (line-clamp-3) + presenter name/affiliation + 'Open Poster' action button at card bottom - Poster code (or 1-based index fallback) badge in top-right corner - Card active:scale-[0.98] for tactile touch press feedback - Sticky compact session header strip with name, code, and poster count - Optional 'Session Resources' strip for rare session-level files - overflow-y-auto + grow so the grid scrolls; header strip stays fixed Integration: - launcher_session_view.svelte: import + delegate when type_code==='poster' - launcher_file_cont.svelte: min-w-96 → w-full on poster button so it fills its container (card or list row) without overflow on small screens - WS open/close/zoom command pipeline unchanged (all in launcher_file_cont and +layout.svelte which were not modified for the WS paths)
This commit is contained in:
@@ -258,7 +258,7 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={event_file_id}
|
||||
hosted_file_obj={event_file_obj}
|
||||
classes="btn btn-sm md:btn-md lg:btn-lg preset-tonal-primary border border-primary-500 min-w-96"
|
||||
classes="btn btn-sm md:btn-md lg:btn-lg preset-tonal-primary border border-primary-500 w-full"
|
||||
click={() => {
|
||||
modal__open_event_file_id = event_file_id;
|
||||
modal__event_file_obj = event_file_obj;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
import Launcher_presentation_view from './launcher_presentation_view.svelte';
|
||||
import Launcher_presenter_view from './launcher_presenter_view.svelte';
|
||||
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
|
||||
// WHY: Poster sessions get a dedicated card-grid view optimised for touch/PWA use.
|
||||
import Launcher_session_view_posters from './launcher_session_view_posters.svelte';
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
// import { core_func } from '$lib/ae_core_functions';
|
||||
@@ -163,6 +165,10 @@
|
||||
{/if}
|
||||
|
||||
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
|
||||
{#if type_code === 'poster'}
|
||||
<!-- WHY: Poster sessions use a dedicated touch-first card-grid layout. -->
|
||||
<Launcher_session_view_posters {slct__event_session_id} {log_lvl} />
|
||||
{:else}
|
||||
<!--
|
||||
Session header: flex-col keeps datetime and name on separate rows so
|
||||
the header height is predictable regardless of session name length.
|
||||
@@ -462,6 +468,7 @@
|
||||
<p>No presentations available to display.</p>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}<!-- end type_code !== 'poster' -->
|
||||
{:else}
|
||||
<span class="fas fa-spinner fa-spin"></span>
|
||||
No session selected
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* launcher_session_view_posters.svelte
|
||||
*
|
||||
* WHY: Digital Poster sessions need a dedicated card-grid layout optimised for
|
||||
* touch-first PWA use on tablets and phones. Keeping this separate from the oral
|
||||
* session view avoids cluttering that template with a growing set of poster-specific
|
||||
* branches and lets each view own its own layout concerns cleanly.
|
||||
*
|
||||
* The menu will typically be hidden and the page may be in iframe mode
|
||||
* ($ae_loc.iframe == true), so this view fills the full available width.
|
||||
*
|
||||
* Deployment context: operator tablet / phone PWA, also works on desktop.
|
||||
*/
|
||||
interface Props {
|
||||
slct__event_session_id?: string | null;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
slct__event_session_id = $bindable(null),
|
||||
log_lvl = $bindable(1)
|
||||
}: Props = $props();
|
||||
|
||||
import { liveQuery } from 'dexie';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
|
||||
|
||||
import Event_launcher_file_cont from './launcher_file_cont.svelte';
|
||||
import Launcher_presentation_view from './launcher_presentation_view.svelte';
|
||||
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
|
||||
|
||||
// Session object
|
||||
let lq__event_session_obj = $derived(
|
||||
liveQuery(() => db_events.session.get(slct__event_session_id))
|
||||
);
|
||||
|
||||
// Session-level files (rare — program PDFs, flyers, etc.)
|
||||
let lq__event_file_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!slct__event_session_id) return [];
|
||||
return await db_events.file
|
||||
.where('for_id')
|
||||
.equals(slct__event_session_id)
|
||||
.reverse()
|
||||
.sortBy('created_on');
|
||||
})
|
||||
);
|
||||
|
||||
// Presentations sorted alphabetically — typical for poster sessions
|
||||
let lq__event_presentation_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!slct__event_session_id) return [];
|
||||
return await db_events.presentation
|
||||
.where('event_session_id')
|
||||
.equals(slct__event_session_id)
|
||||
.sortBy('name');
|
||||
})
|
||||
);
|
||||
|
||||
// All presenters for this session; filtered per card in the template
|
||||
let lq__event_presenter_obj_li = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!slct__event_session_id) return [];
|
||||
return await db_events.presenter
|
||||
.where('event_session_id')
|
||||
.equals(slct__event_session_id)
|
||||
.sortBy('full_name');
|
||||
})
|
||||
);
|
||||
|
||||
// Poster count for the session header badge
|
||||
let poster_count = $derived($lq__event_presentation_obj_li?.length ?? 0);
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Digital Poster session view — touch-first card grid.
|
||||
The outer div mirrors the grow/h-full/w-full contract expected by
|
||||
+layout.svelte so this slots in as a drop-in replacement for the oral view.
|
||||
-->
|
||||
<div class="poster_session_view flex flex-col gap-0 w-full h-full overflow-hidden">
|
||||
|
||||
{#if $events_sess.launcher?.loading__session_id_status}
|
||||
<span class="absolute top-0 right-0 text-sm text-gray-400 flex items-center gap-1 p-1 z-10">
|
||||
<span class="fas fa-spinner fa-spin"></span>
|
||||
Loading...
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
|
||||
|
||||
<!-- ── Compact session identity strip ──────────────────────────────── -->
|
||||
<header
|
||||
class="
|
||||
poster_session_header
|
||||
flex flex-row gap-2 items-center justify-between
|
||||
px-2 py-1.5
|
||||
border-b-2 border-surface-300 dark:border-surface-600
|
||||
bg-surface-100/60 dark:bg-surface-800/60
|
||||
shrink-0
|
||||
"
|
||||
>
|
||||
<h2
|
||||
class="text-base font-bold line-clamp-1 grow min-w-0"
|
||||
title={$lq__event_session_obj.name}
|
||||
>
|
||||
<span class="fas fa-images mr-1.5 text-primary-500 opacity-70"></span>
|
||||
{$lq__event_session_obj.name}
|
||||
</h2>
|
||||
|
||||
<span class="flex flex-row gap-1.5 items-center shrink-0">
|
||||
<!-- Poster count badge -->
|
||||
{#if poster_count > 0}
|
||||
<span
|
||||
class="text-xs font-mono font-semibold text-surface-500 bg-surface-200 dark:bg-surface-700 px-2 py-0.5 rounded-full"
|
||||
title="Number of posters in this session"
|
||||
>
|
||||
{poster_count}×
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Session code -->
|
||||
{#if $lq__event_session_obj.code}
|
||||
<span
|
||||
class="text-xs font-mono font-bold text-surface-400 bg-surface-100 dark:bg-surface-800 px-2 py-0.5 rounded border border-surface-300 dark:border-surface-600"
|
||||
title="Session code: {$lq__event_session_obj.code}"
|
||||
>
|
||||
{$lq__event_session_obj.code}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<!-- ── Session-level files (rarely present — program, schedule, etc.) ── -->
|
||||
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
|
||||
<section class="session_resources_strip px-2 pb-2 pt-1 border-b border-surface-200 dark:border-surface-700 shrink-0">
|
||||
<p class="text-[10px] text-surface-500 uppercase font-bold tracking-wider mb-1 opacity-60">
|
||||
Session Resources:
|
||||
</p>
|
||||
<ul class="flex flex-row flex-wrap gap-2">
|
||||
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
|
||||
<li
|
||||
class:hidden={!$events_loc.launcher.show_content__hidden_files &&
|
||||
event_file_obj.hide}
|
||||
>
|
||||
<Event_launcher_file_cont
|
||||
event_file_id={event_file_obj.event_file_id}
|
||||
{event_file_obj}
|
||||
hide_created_on={true}
|
||||
show_bak_download={$ae_loc.trusted_access &&
|
||||
$ae_loc.edit_mode}
|
||||
session_type="poster"
|
||||
open_method="modal"
|
||||
modal_title={$lq__event_session_obj?.name}
|
||||
bind:modal__title={
|
||||
$events_sess.launcher.modal__title
|
||||
}
|
||||
bind:modal__open_event_file_id={
|
||||
$events_sess.launcher
|
||||
.modal__open_event_file_id
|
||||
}
|
||||
bind:modal__event_file_obj={
|
||||
$events_sess.launcher.modal__event_file_obj
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- ── Poster card grid ────────────────────────────────────────────── -->
|
||||
{#if $lq__event_presentation_obj_li === undefined}
|
||||
<!-- Still resolving from Dexie -->
|
||||
<div class="flex items-center justify-center gap-2 p-10 opacity-40 grow">
|
||||
<span class="fas fa-spinner fa-spin text-2xl"></span>
|
||||
<span>Loading posters…</span>
|
||||
</div>
|
||||
|
||||
{:else if $lq__event_presentation_obj_li.length === 0}
|
||||
<!-- Loaded but empty -->
|
||||
<div class="flex flex-col items-center justify-center gap-3 p-12 opacity-40 text-center grow">
|
||||
<span class="fas fa-image text-5xl"></span>
|
||||
<p class="text-lg font-medium">No posters in this session yet.</p>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<!--
|
||||
Grid: 1 col on phone, 2 on tablet (sm), 3 on large desktop (xl).
|
||||
Each card is a self-contained poster unit: title → presenter → action.
|
||||
overflow-y-auto + grow means the grid scrolls within the available
|
||||
height while the session header and resource strip stay fixed above.
|
||||
-->
|
||||
<ul
|
||||
class="
|
||||
poster_card_grid
|
||||
grid
|
||||
grid-cols-1
|
||||
sm:grid-cols-2
|
||||
xl:grid-cols-3
|
||||
gap-3
|
||||
p-3
|
||||
overflow-y-auto
|
||||
grow
|
||||
"
|
||||
>
|
||||
{#each $lq__event_presentation_obj_li as presentation, i (presentation.event_presentation_id)}
|
||||
{@const presenters_for_this = (
|
||||
$lq__event_presenter_obj_li ?? []
|
||||
).filter(
|
||||
(p) =>
|
||||
p.event_presentation_id ===
|
||||
presentation.event_presentation_id
|
||||
)}
|
||||
|
||||
<!--
|
||||
Poster card.
|
||||
min-h-40 prevents very short cards for single-word titles.
|
||||
active:scale-[0.98] gives a tactile press feel on touch.
|
||||
-->
|
||||
<li
|
||||
class="
|
||||
poster_card
|
||||
relative flex flex-col gap-2
|
||||
rounded-xl
|
||||
border border-surface-200 dark:border-surface-700
|
||||
bg-white dark:bg-surface-900
|
||||
hover:border-primary-400 dark:hover:border-primary-500
|
||||
active:scale-[0.98] active:shadow-sm
|
||||
transition-all duration-150
|
||||
shadow-sm hover:shadow-md
|
||||
p-3
|
||||
min-h-40
|
||||
"
|
||||
>
|
||||
<!--
|
||||
Top-right badge: prefer the presentation code (e.g. "P-042")
|
||||
as it matches physical poster board numbers; fall back to
|
||||
the 1-based index so there's always a quick reference number.
|
||||
-->
|
||||
<span
|
||||
class="
|
||||
absolute top-2 right-2
|
||||
text-xs font-mono font-bold leading-tight
|
||||
text-primary-600 dark:text-primary-400
|
||||
bg-primary-50 dark:bg-primary-950/60
|
||||
border border-primary-200 dark:border-primary-800
|
||||
px-2 py-0.5
|
||||
rounded-full
|
||||
"
|
||||
title="{presentation.code
|
||||
? 'Poster code: ' + presentation.code
|
||||
: 'Poster #' + (i + 1)}"
|
||||
>
|
||||
{presentation.code || '#' + (i + 1)}
|
||||
</span>
|
||||
|
||||
<!--
|
||||
Presentation / poster title — the primary search target.
|
||||
pr-14 prevents text from running under the badge.
|
||||
line-clamp-3 keeps cards a predictable height while
|
||||
the full text is accessible via the title attribute.
|
||||
-->
|
||||
<h3
|
||||
class="
|
||||
poster_title
|
||||
text-base md:text-lg
|
||||
font-bold leading-snug
|
||||
line-clamp-3
|
||||
text-surface-950 dark:text-surface-50
|
||||
pr-14
|
||||
grow
|
||||
"
|
||||
title={presentation.name}
|
||||
>
|
||||
{presentation.name}
|
||||
</h3>
|
||||
|
||||
<!--
|
||||
Presenter(s) — shown below the title so the title
|
||||
gets the first visual hit during scanning.
|
||||
Multiple presenters are listed vertically.
|
||||
WHY given_name === 'Group': some events have a single
|
||||
"Group" presenter whose full name is stored in affiliations.
|
||||
-->
|
||||
{#if presenters_for_this.length}
|
||||
<div class="presenter_info space-y-0.5 shrink-0">
|
||||
{#each presenters_for_this as presenter (presenter.event_presenter_id)}
|
||||
<p
|
||||
class="
|
||||
flex flex-row flex-wrap items-baseline gap-x-1.5
|
||||
text-sm text-surface-500 dark:text-surface-400
|
||||
leading-snug
|
||||
"
|
||||
>
|
||||
{#if presenter.given_name && presenter.given_name !== 'Group'}
|
||||
<span
|
||||
class="fas fa-user text-[11px] opacity-50 shrink-0 mt-px"
|
||||
></span>
|
||||
<span
|
||||
class="font-medium text-surface-700 dark:text-surface-300"
|
||||
>{presenter.full_name}</span
|
||||
>
|
||||
{#if presenter.affiliations}
|
||||
<span
|
||||
class="italic text-xs opacity-70 line-clamp-1 min-w-0"
|
||||
title={presenter.affiliations}
|
||||
>
|
||||
— {presenter.affiliations}
|
||||
</span>
|
||||
{/if}
|
||||
{:else if presenter.given_name === 'Group'}
|
||||
<span
|
||||
class="fas fa-users text-[11px] opacity-50 shrink-0 mt-px"
|
||||
></span>
|
||||
<span
|
||||
class="font-medium text-surface-700 dark:text-surface-300"
|
||||
>{presenter.affiliations}</span
|
||||
>
|
||||
{:else}
|
||||
<span class="opacity-40 text-xs">—</span>
|
||||
{/if}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
File action buttons pushed to the bottom of the card via
|
||||
mt-auto. Files may be attached at presentation level,
|
||||
presenter level, or both — render both sub-components so
|
||||
neither source is missed.
|
||||
-->
|
||||
<div class="poster_actions flex flex-col gap-1 mt-auto pt-1 shrink-0">
|
||||
<!-- Presentation-level files (the most common attachment point) -->
|
||||
<Launcher_presentation_view
|
||||
lq__event_presentation_obj={presentation}
|
||||
session_type="poster"
|
||||
/>
|
||||
|
||||
<!--
|
||||
Presenter-level files (some events attach the PDF here instead).
|
||||
hide_name=true because the name is already shown above in
|
||||
the presenter_info section; no need to repeat it.
|
||||
-->
|
||||
{#each presenters_for_this as presenter (presenter.event_presenter_id)}
|
||||
<Launcher_presenter_view_posters
|
||||
lq__event_presenter_obj={presenter}
|
||||
hide_name={true}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<!-- No session selected or still loading -->
|
||||
<div class="flex items-center justify-center gap-2 p-8 opacity-40 grow">
|
||||
<span class="fas fa-spinner fa-spin"></span>
|
||||
<span>No session selected</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user