Zwischenstand vom 29.05.2026
This commit is contained in:
@@ -68,6 +68,28 @@
|
||||
<source>Narrow sidebar + wide main column</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="container.title" resname="container.title">
|
||||
<source>VITEC · Container</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.description" resname="container.description">
|
||||
<source>Generic container for other content elements with a custom CSS class</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.column" resname="container.column">
|
||||
<source>Content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.flexform.sheet" resname="container.flexform.sheet">
|
||||
<source>Container Settings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.flexform.label" resname="container.flexform.label">
|
||||
<source>Container Settings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.cssclass.label" resname="container.cssclass.label">
|
||||
<source>CSS Class</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.cssclass.description" resname="container.cssclass.description">
|
||||
<source>Custom CSS class that is rendered on the container in the frontend.</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="column.1" resname="column.1">
|
||||
<source>Column 1</source>
|
||||
</trans-unit>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
||||
<xliff version="1.0">
|
||||
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
|
||||
<header/>
|
||||
<body>
|
||||
<trans-unit id="group.header" resname="group.header">
|
||||
<source>VITEC</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="section.heading" resname="section.heading">
|
||||
<source>Section Heading</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="section.subline" resname="section.subline">
|
||||
<source>Section Subline</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="bg_variant.label" resname="bg_variant.label">
|
||||
<source>Background Variant</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
|
||||
<source>Orange</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
|
||||
<source>Blue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
|
||||
<source>Graphite</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
|
||||
<source>Midnight</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
|
||||
<source>VITEC · Two Columns (50 / 50)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
|
||||
<source>Two equal columns</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
|
||||
<source>VITEC · Three Columns (33 / 33 / 33)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
|
||||
<source>Three equal columns</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
|
||||
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
|
||||
<source>Four equal columns</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
|
||||
<source>VITEC · Two Columns (66 / 33)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
|
||||
<source>Wide main column + narrow sidebar</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
|
||||
<source>VITEC · Two Columns (33 / 66)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
|
||||
<source>Narrow sidebar + wide main column</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="column.1" resname="column.1">
|
||||
<source>Column 1</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.2" resname="column.2">
|
||||
<source>Column 2</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.3" resname="column.3">
|
||||
<source>Column 3</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.4" resname="column.4">
|
||||
<source>Column 4</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.main_66" resname="column.main_66">
|
||||
<source>Main (66%)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
|
||||
<source>Sidebar (33%)</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
||||
<xliff version="1.0">
|
||||
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_containers.xlf" product-name="vitec">
|
||||
<header/>
|
||||
<body>
|
||||
<trans-unit id="group.header" resname="group.header">
|
||||
<source>VITEC</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="section.heading" resname="section.heading">
|
||||
<source>Section Heading</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="section.subline" resname="section.subline">
|
||||
<source>Section Subline</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="bg_variant.label" resname="bg_variant.label">
|
||||
<source>Background Variant</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.none" resname="bg_variant.option.none">
|
||||
<source>None</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.orange" resname="bg_variant.option.orange">
|
||||
<source>Orange</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.blue" resname="bg_variant.option.blue">
|
||||
<source>Blue</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.graphite" resname="bg_variant.option.graphite">
|
||||
<source>Graphite</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="bg_variant.option.midnight" resname="bg_variant.option.midnight">
|
||||
<source>Midnight</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_50_50.title" resname="cols_50_50.title">
|
||||
<source>VITEC · Two Columns (50 / 50)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_50_50.description" resname="cols_50_50.description">
|
||||
<source>Two equal columns</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_33_33_33.title" resname="cols_33_33_33.title">
|
||||
<source>VITEC · Three Columns (33 / 33 / 33)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_33_33_33.description" resname="cols_33_33_33.description">
|
||||
<source>Three equal columns</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_25_25_25_25.title" resname="cols_25_25_25_25.title">
|
||||
<source>VITEC · Four Columns (25 / 25 / 25 / 25)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_25_25_25_25.description" resname="cols_25_25_25_25.description">
|
||||
<source>Four equal columns</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_66_33.title" resname="cols_66_33.title">
|
||||
<source>VITEC · Two Columns (66 / 33)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_66_33.description" resname="cols_66_33.description">
|
||||
<source>Wide main column + narrow sidebar</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="cols_33_66.title" resname="cols_33_66.title">
|
||||
<source>VITEC · Two Columns (33 / 66)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="cols_33_66.description" resname="cols_33_66.description">
|
||||
<source>Narrow sidebar + wide main column</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="container.title" resname="container.title">
|
||||
<source>VITEC · Container</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.description" resname="container.description">
|
||||
<source>Generic container for other content elements with a custom CSS class</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.column" resname="container.column">
|
||||
<source>Content</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.flexform.sheet" resname="container.flexform.sheet">
|
||||
<source>Container Settings</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.cssclass.label" resname="container.cssclass.label">
|
||||
<source>CSS Class</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="container.cssclass.description" resname="container.cssclass.description">
|
||||
<source>Custom CSS class that is rendered on the container in the frontend.</source>
|
||||
</trans-unit>
|
||||
|
||||
<trans-unit id="column.1" resname="column.1">
|
||||
<source>Column 1</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.2" resname="column.2">
|
||||
<source>Column 2</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.3" resname="column.3">
|
||||
<source>Column 3</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.4" resname="column.4">
|
||||
<source>Column 4</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.main_66" resname="column.main_66">
|
||||
<source>Main (66%)</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="column.sidebar_33" resname="column.sidebar_33">
|
||||
<source>Sidebar (33%)</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="EXT:vitec/Resources/Private/Language/locallang_ogimage.xlf">
|
||||
<body>
|
||||
<trans-unit id="mlang_tabs_tab">
|
||||
<source>OG Image</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="mlang_labels_tabdescr">
|
||||
<source>Generate Open Graph images (1200×630) for social media sharing.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="mlang_labels_tablabel">
|
||||
<source>OG Image Generator</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -1,7 +1,7 @@
|
||||
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
||||
xmlns:n="http://typo3.org/ns/GeorgRinger/News/ViewHelpers">
|
||||
<div class="vitec-administration">
|
||||
<f:flashMessages/>
|
||||
<f:flashMessages queueIdentifier="core.template.flashMessages" />
|
||||
<h2>Hi</h2>
|
||||
<f:render section="main"/>
|
||||
</div>
|
||||
|
||||
288
packages/vitec/Resources/Private/Templates/OgImage/Index.html
Normal file
288
packages/vitec/Resources/Private/Templates/OgImage/Index.html
Normal file
@@ -0,0 +1,288 @@
|
||||
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
||||
data-namespace-typo3-fluid="true">
|
||||
|
||||
<f:layout name="Backend/Default" />
|
||||
|
||||
<f:section name="main">
|
||||
|
||||
<div class="module-docheader-bar module-docheader-bar-navigation">
|
||||
<div class="module-docheader-bar-column-left">
|
||||
<h2 class="t3js-title-inlineedit">OG Image Generator</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="vitec-ogimage-wrap">
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
LEFT: FORM
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="col-md-7">
|
||||
|
||||
<f:flashMessages queueIdentifier="core.template.flashMessages" />
|
||||
|
||||
<form action="{f:be.uri(route: 'web_vitecogimage.generate')}" method="post" id="vitecOgForm" data-preview-url="{f:be.uri(route: 'web_vitecogimage.preview')}">
|
||||
|
||||
<!-- Preset quick-picks -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>Quick Presets</strong></div>
|
||||
<div class="card-body d-flex gap-2 flex-wrap">
|
||||
<f:for each="{presets}" as="preset" key="key">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm js-preset"
|
||||
data-preset="{key}"
|
||||
data-bg="{preset.bg_color}"
|
||||
data-title-color="{preset.title_color}"
|
||||
data-subtitle-color="{preset.subtitle_color}">
|
||||
{preset.label}
|
||||
</button>
|
||||
</f:for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>Background</strong></div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<div class="d-flex gap-3">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input me-1 js-bg-type" type="radio"
|
||||
name="ogimage[bg_type]" value="color"
|
||||
<f:if condition="{formData.bg_type} == 'color'">checked</f:if>/>
|
||||
Solid colour
|
||||
</label>
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input me-1 js-bg-type" type="radio"
|
||||
name="ogimage[bg_type]" value="image"
|
||||
<f:if condition="{formData.bg_type} == 'image'">checked</f:if>/>
|
||||
Image
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bg-color-wrap" class="mb-3">
|
||||
<label class="form-label">Background Colour</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<input type="color" class="form-control form-control-color js-live"
|
||||
name="ogimage[bg_color]" id="bgColor"
|
||||
value="{formData.bg_color}" />
|
||||
<input type="text" class="form-control js-hex-sync" style="max-width:120px"
|
||||
data-target="bgColor" value="{formData.bg_color}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bg-image-wrap" class="mb-3" style="display:none">
|
||||
<label class="form-label">Background Image</label>
|
||||
<f:if condition="{backgroundImages}">
|
||||
<f:then>
|
||||
<select class="form-select js-live" name="ogimage[bg_image]">
|
||||
<option value="">— none —</option>
|
||||
<f:for each="{backgroundImages}" as="img">
|
||||
<option value="{img.path}"
|
||||
<f:if condition="{formData.bg_image} == {img.path}">selected</f:if>>{img.label}</option>
|
||||
</f:for>
|
||||
</select>
|
||||
<small class="text-muted">Upload images to <code>fileadmin/og-backgrounds/</code> to list them here.</small>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<input type="text" class="form-control js-live"
|
||||
name="ogimage[bg_image]"
|
||||
placeholder="/fileadmin/og-backgrounds/my-bg.jpg"
|
||||
value="{formData.bg_image}" />
|
||||
<small class="text-muted">No images found in <code>fileadmin/og-backgrounds/</code>. Enter a path manually, or upload files there.</small>
|
||||
</f:else>
|
||||
</f:if>
|
||||
|
||||
<div class="mt-2">
|
||||
<label class="form-label">Dark overlay opacity: <span id="overlayLabel">{formData.overlay_opacity}</span>%</label>
|
||||
<input type="range" class="form-range js-live" min="0" max="90" step="5"
|
||||
name="ogimage[overlay_opacity]" value="{formData.overlay_opacity}"
|
||||
oninput="document.getElementById('overlayLabel').textContent=this.value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>Text</strong></div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-8">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control js-live"
|
||||
name="ogimage[title]" value="{formData.title}"
|
||||
placeholder="Your headline" />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label class="form-label">Size</label>
|
||||
<input type="number" class="form-control js-live"
|
||||
name="ogimage[title_size]" value="{formData.title_size}"
|
||||
min="20" max="120" />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label class="form-label">Colour</label>
|
||||
<input type="color" class="form-control form-control-color js-live"
|
||||
name="ogimage[title_color]" id="titleColor"
|
||||
value="{formData.title_color}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-8">
|
||||
<label class="form-label">Subtitle / Description</label>
|
||||
<input type="text" class="form-control js-live"
|
||||
name="ogimage[subtitle]" value="{formData.subtitle}"
|
||||
placeholder="Short description" />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label class="form-label">Size</label>
|
||||
<input type="number" class="form-control js-live"
|
||||
name="ogimage[subtitle_size]" value="{formData.subtitle_size}"
|
||||
min="14" max="80" />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label class="form-label">Colour</label>
|
||||
<input type="color" class="form-control form-control-color js-live"
|
||||
name="ogimage[subtitle_color]" id="subtitleColor"
|
||||
value="{formData.subtitle_color}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Label / Badge -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>Label / Badge</strong></div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Label text (leave blank to hide)</label>
|
||||
<input type="text" class="form-control js-live"
|
||||
name="ogimage[label_text]" value="{formData.label_text}"
|
||||
placeholder="e.g. VITEC" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-label">Background</label>
|
||||
<input type="color" class="form-control form-control-color js-live"
|
||||
name="ogimage[label_bg_color]" id="labelBg"
|
||||
value="{formData.label_bg_color}" />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-label">Text colour</label>
|
||||
<input type="color" class="form-control form-control-color js-live"
|
||||
name="ogimage[label_text_color]" id="labelText"
|
||||
value="{formData.label_text_color}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Position</label>
|
||||
<select class="form-select js-live" name="ogimage[label_position]">
|
||||
<option value="top-left" <f:if condition="{formData.label_position} == 'top-left'">selected</f:if>>Top Left</option>
|
||||
<option value="top-right" <f:if condition="{formData.label_position} == 'top-right'">selected</f:if>>Top Right</option>
|
||||
<option value="bottom-left" <f:if condition="{formData.label_position} == 'bottom-left'">selected</f:if>>Bottom Left</option>
|
||||
<option value="bottom-right"<f:if condition="{formData.label_position} == 'bottom-right'">selected</f:if>>Bottom Right</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><strong>Output</strong></div>
|
||||
<div class="card-body row">
|
||||
<div class="col-4">
|
||||
<label class="form-label">Format</label>
|
||||
<select class="form-select" name="ogimage[output_format]">
|
||||
<option value="jpg" <f:if condition="{formData.output_format} == 'jpg'">selected</f:if>>JPG</option>
|
||||
<option value="png" <f:if condition="{formData.output_format} == 'png'">selected</f:if>>PNG</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label">Quality (JPG): <span id="qualLabel">{formData.output_quality}</span></label>
|
||||
<input type="range" class="form-range" min="50" max="100" step="5"
|
||||
name="ogimage[output_quality]" value="{formData.output_quality}"
|
||||
oninput="document.getElementById('qualLabel').textContent=this.value"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button type="button" class="btn btn-secondary" id="btnPreview">
|
||||
Preview
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Save Image to fileadmin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div><!-- /col form -->
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
RIGHT: PREVIEW + SAVED IMAGES
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="col-md-5">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>Preview <small class="text-muted">(1200 × 630)</small></strong>
|
||||
<span id="previewSpinner" class="spinner-border spinner-border-sm text-secondary d-none" role="status"></span>
|
||||
</div>
|
||||
<div class="card-body p-2 bg-secondary">
|
||||
<div style="position:relative; padding-top: 52.5%;"><!-- 630/1200 -->
|
||||
<img id="ogPreviewImg" src="" alt="preview"
|
||||
style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;display:none;" />
|
||||
<div id="ogPreviewPlaceholder"
|
||||
style="position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#aaa;">
|
||||
Click "Preview" to render
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<a id="previewDownload" href="#" download="og-preview.png"
|
||||
class="btn btn-sm btn-outline-secondary d-none">
|
||||
Download preview
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<f:if condition="{savedImages}">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Recent saved images</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead><tr><th>File</th><th>Created</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<f:for each="{savedImages}" as="img">
|
||||
<tr>
|
||||
<td class="text-truncate" style="max-width:140px">{img.name}</td>
|
||||
<td><small>{img.created}</small></td>
|
||||
<td>
|
||||
<a href="{img.path}" target="_blank" class="btn btn-xs btn-outline-primary p-1 lh-1">
|
||||
View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</f:for>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</f:if>
|
||||
|
||||
</div><!-- /col preview -->
|
||||
|
||||
</div><!-- /row -->
|
||||
|
||||
</f:section>
|
||||
|
||||
</html>
|
||||
6
packages/vitec/Resources/Public/Icons/vitec-container.svg
Executable file
6
packages/vitec/Resources/Public/Icons/vitec-container.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||
<rect x="1.5" y="2.5" width="13" height="11" rx="1" fill="none" stroke="#000" stroke-width="1.2"/>
|
||||
<rect x="3.5" y="4.5" width="9" height="2.2" rx="0.3" fill="#000" opacity="0.55"/>
|
||||
<rect x="3.5" y="7.6" width="9" height="2.2" rx="0.3" fill="#000" opacity="0.35"/>
|
||||
<rect x="3.5" y="10.7" width="9" height="1.6" rx="0.3" fill="#000" opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 447 B |
7
packages/vitec/Resources/Public/Icons/vitec-ogimage.svg
Normal file
7
packages/vitec/Resources/Public/Icons/vitec-ogimage.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<rect x="1" y="3" width="14" height="10" rx="1.5" fill="#e63946"/>
|
||||
<rect x="1" y="3" width="14" height="10" rx="1.5" stroke="#c1121f" stroke-width="0.5"/>
|
||||
<text x="2" y="10.5" font-size="4.5" font-family="sans-serif" fill="#fff" font-weight="bold">OG</text>
|
||||
<circle cx="11.5" cy="6.5" r="2" fill="#fff" opacity="0.9"/>
|
||||
<polyline points="1,11 5,7 8,9.5 11,6 15,10" stroke="#fff" stroke-width="1" fill="none" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 523 B |
173
packages/vitec/Resources/Public/Javascript/og-image-preview.js
Normal file
173
packages/vitec/Resources/Public/Javascript/og-image-preview.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* OG Image Backend Module – live preview via AJAX
|
||||
* EXT:vitec/Resources/Public/Javascript/og-image-preview.js
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/** Debounce helper */
|
||||
function debounce(fn, ms) {
|
||||
let t;
|
||||
return function (...args) {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn.apply(this, args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
/** Collect all form values into a FormData-compatible plain object */
|
||||
function collectFormData() {
|
||||
const form = document.getElementById('vitecOgForm');
|
||||
if (!form) return {};
|
||||
const fd = new FormData(form);
|
||||
const out = {};
|
||||
for (const [key, val] of fd.entries()) {
|
||||
// key looks like "ogimage[something]"
|
||||
const match = key.match(/^ogimage\[(.+)]$/);
|
||||
if (match) {
|
||||
out[key] = val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** POST to the preview endpoint, update the <img> */
|
||||
async function requestPreview() {
|
||||
const form = document.getElementById('vitecOgForm');
|
||||
const spinner = document.getElementById('previewSpinner');
|
||||
const img = document.getElementById('ogPreviewImg');
|
||||
const placeholder = document.getElementById('ogPreviewPlaceholder');
|
||||
const dl = document.getElementById('previewDownload');
|
||||
|
||||
if (!form) return;
|
||||
|
||||
if (spinner) spinner.classList.remove('d-none');
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams(collectFormData());
|
||||
const previewUrl = form.dataset.previewUrl || window.location.href;
|
||||
|
||||
const resp = await fetch(previewUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
if (!resp.ok) throw new Error('Server error ' + resp.status);
|
||||
|
||||
const json = await resp.json();
|
||||
|
||||
if (json.success && json.dataUri) {
|
||||
img.src = json.dataUri;
|
||||
img.style.display = 'block';
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
|
||||
if (dl) {
|
||||
dl.href = json.dataUri;
|
||||
dl.classList.remove('d-none');
|
||||
}
|
||||
} else {
|
||||
console.warn('[OG Preview]', json.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[OG Preview] fetch failed:', e);
|
||||
} finally {
|
||||
if (spinner) spinner.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedPreview = debounce(requestPreview, 600);
|
||||
|
||||
// ── Wire up events once DOM is ready ──────────────────────────────
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// Manual preview button
|
||||
const btn = document.getElementById('btnPreview');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', requestPreview);
|
||||
}
|
||||
|
||||
// Auto-preview on any .js-live input change
|
||||
document.querySelectorAll('.js-live').forEach(function (el) {
|
||||
el.addEventListener('input', debouncedPreview);
|
||||
el.addEventListener('change', debouncedPreview);
|
||||
});
|
||||
|
||||
// Background type toggle (show colour vs image picker)
|
||||
function syncBgType() {
|
||||
const colorWrap = document.getElementById('bg-color-wrap');
|
||||
const imageWrap = document.getElementById('bg-image-wrap');
|
||||
const selected = document.querySelector('input.js-bg-type:checked');
|
||||
if (!selected) return;
|
||||
if (selected.value === 'image') {
|
||||
colorWrap && (colorWrap.style.display = 'none');
|
||||
imageWrap && (imageWrap.style.display = 'block');
|
||||
} else {
|
||||
colorWrap && (colorWrap.style.display = 'block');
|
||||
imageWrap && (imageWrap.style.display = 'none');
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('input.js-bg-type').forEach(function (el) {
|
||||
el.addEventListener('change', function () {
|
||||
syncBgType();
|
||||
debouncedPreview();
|
||||
});
|
||||
});
|
||||
|
||||
const bgImageSelect = document.querySelector('select[name="ogimage[bg_image]"]');
|
||||
if (bgImageSelect) {
|
||||
bgImageSelect.addEventListener('change', function () {
|
||||
const imageTypeRadio = document.querySelector('input.js-bg-type[value="image"]');
|
||||
if (imageTypeRadio) {
|
||||
imageTypeRadio.checked = true;
|
||||
}
|
||||
syncBgType();
|
||||
requestPreview();
|
||||
});
|
||||
}
|
||||
|
||||
syncBgType(); // initialise on load
|
||||
|
||||
// Hex text ↔ colour picker sync
|
||||
document.querySelectorAll('.js-hex-sync').forEach(function (hexInput) {
|
||||
const targetId = hexInput.dataset.target;
|
||||
const picker = document.getElementById(targetId);
|
||||
if (!picker) return;
|
||||
|
||||
picker.addEventListener('input', function () {
|
||||
hexInput.value = picker.value;
|
||||
});
|
||||
|
||||
hexInput.addEventListener('input', function () {
|
||||
const val = hexInput.value.trim();
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
|
||||
picker.value = val;
|
||||
picker.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Preset buttons
|
||||
document.querySelectorAll('.js-preset').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const bg = btn.dataset.bg;
|
||||
const tc = btn.dataset.titleColor;
|
||||
const sc = btn.dataset.subtitleColor;
|
||||
|
||||
const bgInput = document.getElementById('bgColor');
|
||||
if (bgInput && bg) { bgInput.value = bg; bgInput.dispatchEvent(new Event('input', {bubbles:true})); }
|
||||
|
||||
const tInput = document.getElementById('titleColor');
|
||||
if (tInput && tc) { tInput.value = tc; tInput.dispatchEvent(new Event('input', {bubbles:true})); }
|
||||
|
||||
const sInput = document.getElementById('subtitleColor');
|
||||
if (sInput && sc) { sInput.value = sc; sInput.dispatchEvent(new Event('input', {bubbles:true})); }
|
||||
|
||||
debouncedPreview();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user