diff --git a/src/lib/ae_events/ae_events__event_badge_template.editable_fields.ts b/src/lib/ae_events/ae_events__event_badge_template.editable_fields.ts index 91d4002d..acecd762 100644 --- a/src/lib/ae_events/ae_events__event_badge_template.editable_fields.ts +++ b/src/lib/ae_events/ae_events__event_badge_template.editable_fields.ts @@ -4,6 +4,7 @@ export const editable_fields__event_badge_template = [ 'logo_filename', 'logo_path', 'header_path', + 'background_image_path', 'secondary_header_path', 'footer_path', 'header_row_1', diff --git a/src/lib/ae_events/ae_events__event_badge_template.ts b/src/lib/ae_events/ae_events__event_badge_template.ts index 294c72c0..802ebcb5 100644 --- a/src/lib/ae_events/ae_events__event_badge_template.ts +++ b/src/lib/ae_events/ae_events__event_badge_template.ts @@ -15,6 +15,7 @@ export const properties_to_save = [ 'logo_filename', 'logo_path', 'header_path', + 'background_image_path', 'secondary_header_path', 'footer_path', 'header_row_1', @@ -36,6 +37,8 @@ export const properties_to_save = [ 'layout', 'style_filename', 'style_href', + 'cfg_json', + 'other_json', 'duplex', 'enable', 'hide', diff --git a/src/lib/ae_events/db_events.ts b/src/lib/ae_events/db_events.ts index df77689f..a63add75 100644 --- a/src/lib/ae_events/db_events.ts +++ b/src/lib/ae_events/db_events.ts @@ -223,6 +223,7 @@ export interface Badge_template { logo_path?: null | string; header_path?: null | string; + background_image_path?: null | string; secondary_header_path?: null | string; footer_path?: null | string; @@ -256,6 +257,7 @@ export interface Badge_template { duplex?: null | number | boolean; cfg_json?: null | string; + other_json?: null | string; data_json?: null | string; enable?: null | boolean; diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte index a5b7ea8b..8e615ac8 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/ae_comp__badge_obj_view.svelte @@ -185,6 +185,51 @@ let show_badge_back = $derived( !!$lq__event_badge_template_obj?.duplex ); +// Full-badge background image: when set on the template, covers the entire badge_front +// with the image (background-size: cover). The header section is suppressed — the +// background image already contains the logo/event design. Text content is rendered +// on top via z-index. When unset, the normal header_path / logo+text header logic applies. +let bg_image_path = $derived( + $lq__event_badge_template_obj?.background_image_path ?? null +); + +// Parse template cfg_json into an object for per-template flags (hide header/footer, QR) +let template_cfg = $derived.by(() => { + const raw = $lq__event_badge_template_obj?.cfg_json; + if (!raw) return {}; + try { + return typeof raw === 'string' ? JSON.parse(raw) : raw; + } catch { + return {}; + } +}); + +// Visibility / QR flags: cfg_json overrides top-level fields when present. +let hide_badge_header = $derived.by(() => { + const cfg = $template_cfg || {}; + if (Object.prototype.hasOwnProperty.call(cfg, 'hide_badge_header')) return !!cfg.hide_badge_header; + // Default: if a background image is present, hide header; otherwise show. + return !!$bg_image_path; +}); + +let hide_badge_footer = $derived.by(() => { + const cfg = $template_cfg || {}; + if (Object.prototype.hasOwnProperty.call(cfg, 'hide_badge_footer')) return !!cfg.hide_badge_footer; + return false; +}); + +let use_show_qr_front = $derived.by(() => { + const cfg = $template_cfg || {}; + if (Object.prototype.hasOwnProperty.call(cfg, 'show_qr_front')) return !!cfg.show_qr_front; + return $lq__event_badge_template_obj?.show_qr_front ?? false; +}); + +let use_show_qr_back = $derived.by(() => { + const cfg = $template_cfg || {}; + if (Object.prototype.hasOwnProperty.call(cfg, 'show_qr_back')) return !!cfg.show_qr_back; + return $lq__event_badge_template_obj?.show_qr_back ?? true; +}); + /** * Layout-aware section heights for Element_fit_text. * @@ -201,14 +246,18 @@ let show_badge_back = $derived( * Flex values: 'around' | 'between' | 'even' | 'center' | 'start' | 'end' * (mapped to space-around, space-between, space-evenly, center, flex-start, flex-end) * - * TODO: Move to badge template config (cfg_json) once multi-layout events are needed. - * Tune these values with real badge data and a ruler. + * Per-template overrides: set cfg_json.fit_heights on the badge template record to + * override any subset of these keys without a code deploy. This is especially useful + * when background_image_path is set and the default layout heights need tuning to + * align the text with the background image's designed zones. + * Example cfg_json: { "fit_heights": { "grp_name_title": "1.8in", "name": "1.4in" } } */ let fit_heights = $derived.by(() => { const layout = $lq__event_badge_template_obj?.layout ?? 'badge_4x6_fanfold'; + let base: Record; if (layout === 'badge_3.5x5.5_pvc') { // 3.5" × 5.5" PVC card — single-sided, compact - return { + base = { grp_name_title: '1.6in', grp_name_title_flex: 'around', name: '1.4in', @@ -220,7 +269,7 @@ let fit_heights = $derived.by(() => { }; } else if (layout === 'badge_4x5_fanfold') { // 4" × 5" fanfold — slightly taller, duplex - return { + base = { grp_name_title: '2.1in', grp_name_title_flex: 'around', name: '1.6in', @@ -232,7 +281,7 @@ let fit_heights = $derived.by(() => { }; } else { // Default: badge_4x6_fanfold — 4" × 6", most room - return { + base = { grp_name_title: '2.5in', grp_name_title_flex: 'around', name: '1.9in', @@ -243,6 +292,22 @@ let fit_heights = $derived.by(() => { location: '0.55in' }; } + + // Apply per-template cfg_json overrides so admins can fine-tune text area + // heights/flex values via the DB without a code deploy. + // cfg_json is stored as a JSON string — parse it before reading fit_heights key. + let cfg_overrides: Record | null = null; + const cfg_raw = $lq__event_badge_template_obj?.cfg_json; + if (cfg_raw && typeof cfg_raw === 'string') { + try { cfg_overrides = JSON.parse(cfg_raw)?.fit_heights ?? null; } catch { /* ignore invalid JSON */ } + } else if (cfg_raw && typeof cfg_raw === 'object') { + // Already parsed (future-proof if ever stored as object) + cfg_overrides = (cfg_raw as any)?.fit_heights ?? null; + } + if (cfg_overrides && typeof cfg_overrides === 'object') { + return { ...base, ...cfg_overrides }; + } + return base; }); /** @@ -456,7 +521,9 @@ const code_to_icon: { text-center hover:outline-2 hover:outline-red-500/75 hover:outline-dashed " - style={demo_bg_style}> + style="{bg_image_path + ? `background-image: url('${bg_image_path}'); background-size: cover; background-position: top center; background-repeat: no-repeat;` + : ''}{demo_bg_style ? ` ${demo_bg_style}` : ''}"> + {#if !hide_badge_header} + {#if $lq__event_badge_template_obj.header_path} +
- check header path -
- {:else} -
- - + {/if} {/if}
- {#if eff_badge?.ticket_1_code || eff_badge?.ticket_2_code || eff_badge?.ticket_3_code || $lq__event_badge_template_obj?.show_qr_front} + {#if eff_badge?.ticket_1_code || eff_badge?.ticket_2_code || eff_badge?.ticket_3_code || use_show_qr_front}
@@ -638,7 +710,7 @@ const code_to_icon: { >{/if}
- {#if $lq__event_badge_template_obj?.show_qr_front} + {#if use_show_qr_front} {#await qr_data_url} {:then result} @@ -654,6 +726,7 @@ const code_to_icon: { {/if}
+ {#if !hide_badge_footer} + {/if} @@ -724,9 +799,10 @@ const code_to_icon: { Back of badge - {#if $lq__event_badge_template_obj.secondary_header_path} -
- check secondary header path -
- {:else if $lq__event_badge_template_obj.header_path} -
- check primary header path -
- {:else} -
- - + {/if} {/if}
{/if} - {#if $lq__event_badge_template_obj.show_qr_back} + {#if use_show_qr_back}
diff --git a/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte b/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte index 55f7e8e2..999c2286 100644 --- a/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte +++ b/src/routes/events/[event_id]/(badges)/templates/ae_comp__badge_template_form.svelte @@ -30,6 +30,7 @@ function prevent_default(fn: () => void) { // Form fields (Runes) let name = $state(''); +let background_image_path = $state(''); let header_path = $state(''); let logo_path = $state(''); let header_row_1 = $state(''); @@ -44,6 +45,14 @@ let ticket_1_text = $state(''); let ticket_2_text = $state(''); let ticket_3_text = $state(''); +// Advanced / cfg_json states +let advanced_open = $state(false); +let existing_cfg_raw = $state(''); +let cfg_hide_badge_header = $state(false); +let cfg_hide_badge_footer = $state(false); +let cfg_show_qr_front = $state(false); +let cfg_show_qr_back = $state(true); + let submit_status = $state('idle'); // idle, loading, success, error // Load template data if in edit mode @@ -65,19 +74,45 @@ async function load_template(id: string) { }); if (template_obj) { name = template_obj.name || ''; + background_image_path = template_obj.background_image_path || ''; header_path = template_obj.header_path || ''; logo_path = template_obj.logo_path || ''; header_row_1 = template_obj.header_row_1 || ''; header_row_2 = template_obj.header_row_2 || ''; secondary_header_path = template_obj.secondary_header_path || ''; footer_text = template_obj.footer_text || ''; - show_qr_front = template_obj.show_qr_front ?? true; - show_qr_back = template_obj.show_qr_back ?? true; wireless_ssid = template_obj.wireless_ssid || ''; wireless_password = template_obj.wireless_password || ''; ticket_1_text = template_obj.ticket_1_text || ''; ticket_2_text = template_obj.ticket_2_text || ''; ticket_3_text = template_obj.ticket_3_text || ''; + + // Preserve existing cfg_json and expose cfg flags in the form + existing_cfg_raw = template_obj.cfg_json || ''; + let parsed_cfg: any = {}; + try { + parsed_cfg = existing_cfg_raw + ? typeof existing_cfg_raw === 'string' + ? JSON.parse(existing_cfg_raw) + : existing_cfg_raw + : {}; + } catch { + parsed_cfg = {}; + } + + cfg_hide_badge_header = parsed_cfg.hide_badge_header ?? false; + cfg_hide_badge_footer = parsed_cfg.hide_badge_footer ?? false; + cfg_show_qr_front = parsed_cfg.hasOwnProperty('show_qr_front') + ? parsed_cfg.show_qr_front + : (template_obj.show_qr_front ?? false); + cfg_show_qr_back = parsed_cfg.hasOwnProperty('show_qr_back') + ? parsed_cfg.show_qr_back + : (template_obj.show_qr_back ?? true); + + // Keep top-level fields in sync for backward compatibility + show_qr_front = cfg_show_qr_front; + show_qr_back = cfg_show_qr_back; + submit_status = 'idle'; } else { submit_status = 'error'; @@ -91,21 +126,42 @@ async function load_template(id: string) { async function handle_submit() { submit_status = 'loading'; + + // Merge cfg_json preserving unknown keys, then set our cfg flags + let cfg_obj: any = {}; + try { + cfg_obj = existing_cfg_raw + ? typeof existing_cfg_raw === 'string' + ? JSON.parse(existing_cfg_raw) + : existing_cfg_raw + : {}; + } catch { + cfg_obj = {}; + } + + cfg_obj.hide_badge_header = cfg_hide_badge_header; + cfg_obj.hide_badge_footer = cfg_hide_badge_footer; + cfg_obj.show_qr_front = cfg_show_qr_front; + cfg_obj.show_qr_back = cfg_show_qr_back; + const data_to_save: key_val = { name, + background_image_path, header_path, logo_path, header_row_1, header_row_2, secondary_header_path, footer_text, - show_qr_front, - show_qr_back, + // keep top-level show_qr fields in sync for backward compatibility + show_qr_front: cfg_show_qr_front, + show_qr_back: cfg_show_qr_back, wireless_ssid, wireless_password, ticket_1_text, ticket_2_text, - ticket_3_text + ticket_3_text, + cfg_json: JSON.stringify(cfg_obj) }; try { @@ -153,7 +209,17 @@ function handle_cancel() { + {#if background_image_path} +

+ ⚠ When a background image is set, the header path and logo/text header are hidden on the badge front — the background image covers the full badge. +

+ {/if} + + +
+ + {#if advanced_open} +
+ + + + +

+ These values are saved into cfg_json. Existing cfg_json keys are preserved. +

+
+ {/if} +
+