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
|
<AE_Comp_Hosted_Files_Download_Button
|
||||||
hosted_file_id={event_file_id}
|
hosted_file_id={event_file_id}
|
||||||
hosted_file_obj={event_file_obj}
|
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={() => {
|
click={() => {
|
||||||
modal__open_event_file_id = event_file_id;
|
modal__open_event_file_id = event_file_id;
|
||||||
modal__event_file_obj = event_file_obj;
|
modal__event_file_obj = event_file_obj;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
import Launcher_presentation_view from './launcher_presentation_view.svelte';
|
import Launcher_presentation_view from './launcher_presentation_view.svelte';
|
||||||
import Launcher_presenter_view from './launcher_presenter_view.svelte';
|
import Launcher_presenter_view from './launcher_presenter_view.svelte';
|
||||||
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.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 { liveQuery } from 'dexie';
|
||||||
// import { core_func } from '$lib/ae_core_functions';
|
// import { core_func } from '$lib/ae_core_functions';
|
||||||
@@ -163,6 +165,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
|
{#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
|
Session header: flex-col keeps datetime and name on separate rows so
|
||||||
the header height is predictable regardless of session name length.
|
the header height is predictable regardless of session name length.
|
||||||
@@ -462,6 +468,7 @@
|
|||||||
<p>No presentations available to display.</p>
|
<p>No presentations available to display.</p>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
{/if}<!-- end type_code !== 'poster' -->
|
||||||
{:else}
|
{:else}
|
||||||
<span class="fas fa-spinner fa-spin"></span>
|
<span class="fas fa-spinner fa-spin"></span>
|
||||||
No session selected
|
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