Formular-Infrastruktur: Contact/Demo/Helpdesk mit E-Mail- und Salesforce-Delivery, JSON-Renderer-Erweiterungen, Crop-Variants
Build-Artefakte in public/_frontend/assets nicht enthalten.
530
composer.lock
generated
@@ -17,8 +17,8 @@ cd "$(dirname "$0")" || exit 1
|
|||||||
|
|
||||||
echo "[perms] Project root: $(pwd)"
|
echo "[perms] Project root: $(pwd)"
|
||||||
echo "[perms] Setting files 644 / directories 755 under Resources/Public + _assets ..."
|
echo "[perms] Setting files 644 / directories 755 under Resources/Public + _assets ..."
|
||||||
find public/_assets packages/vitec/Resources/Public -type f -exec chmod 644 {} \; 2>/dev/null
|
find public/_assets public/fileadmin/icons packages/vitec/Resources/Public -type f -exec chmod 644 {} \; 2>/dev/null
|
||||||
find public/_assets packages/vitec/Resources/Public -type d -exec chmod 755 {} \; 2>/dev/null
|
find public/_assets public/fileadmin/icons packages/vitec/Resources/Public -type d -exec chmod 755 {} \; 2>/dev/null
|
||||||
|
|
||||||
echo "[perms] Flushing TYPO3 caches ..."
|
echo "[perms] Flushing TYPO3 caches ..."
|
||||||
if [ -x vendor/bin/typo3 ]; then
|
if [ -x vendor/bin/typo3 ]; then
|
||||||
|
|||||||
22
packages/vitec/Classes/Controller/FormController.php
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Controller;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FormController — Extbase registration target for the VITEC form plugins
|
||||||
|
* (Contactform, Demoform, Helpdeskform). In headless mode the JSON output
|
||||||
|
* is produced by FormsJsonRenderer; submissions are handled by
|
||||||
|
* FormSubmissionMiddleware.
|
||||||
|
*/
|
||||||
|
class FormController extends ActionController
|
||||||
|
{
|
||||||
|
public function listAction(): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->htmlResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer;
|
|||||||
use Evomedien\Vitec\UserFunc\EventlistJsonRenderer;
|
use Evomedien\Vitec\UserFunc\EventlistJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\LocationsJsonRenderer;
|
use Evomedien\Vitec\UserFunc\LocationsJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer;
|
use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer;
|
||||||
|
use Evomedien\Vitec\UserFunc\FormsJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer;
|
use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\NewsJsonRenderer;
|
use Evomedien\Vitec\UserFunc\NewsJsonRenderer;
|
||||||
use TYPO3\CMS\Core\Database\Connection;
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
@@ -117,6 +118,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
|||||||
'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'],
|
'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'],
|
||||||
'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'],
|
'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'],
|
||||||
'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'],
|
'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'],
|
||||||
|
'vitec_contactform' => [FormsJsonRenderer::class, 'form'],
|
||||||
|
'vitec_demoform' => [FormsJsonRenderer::class, 'form'],
|
||||||
|
'vitec_helpdeskform' => [FormsJsonRenderer::class, 'form'],
|
||||||
'news_pi1' => [NewsJsonRenderer::class, 'news'],
|
'news_pi1' => [NewsJsonRenderer::class, 'news'],
|
||||||
'news_newsliststicky' => [NewsJsonRenderer::class, 'news'],
|
'news_newsliststicky' => [NewsJsonRenderer::class, 'news'],
|
||||||
'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'],
|
'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'],
|
||||||
|
|||||||
19
packages/vitec/Classes/Forms/Delivery/DeliveryInterface.php
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Forms\Delivery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delivery strategy for VITEC form submissions.
|
||||||
|
* Implementations MUST throw on failure — the middleware logs the error
|
||||||
|
* on the stored submission and keeps the data safe.
|
||||||
|
*/
|
||||||
|
interface DeliveryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string,string> $payload validated, filtered field values
|
||||||
|
* @param array<string,mixed> $settings FlexForm settings of the form plugin
|
||||||
|
*/
|
||||||
|
public function deliver(string $formKey, array $payload, array $settings): void;
|
||||||
|
}
|
||||||
48
packages/vitec/Classes/Forms/Delivery/EmailDelivery.php
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Forms\Delivery;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Mail\MailMessage;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the submission as a plain-text mail to the FlexForm-configured
|
||||||
|
* recipient(s). Sender = system default (MAIL settings); reply-to = the
|
||||||
|
* submitter's email when present.
|
||||||
|
*/
|
||||||
|
final class EmailDelivery implements DeliveryInterface
|
||||||
|
{
|
||||||
|
public function deliver(string $formKey, array $payload, array $settings): void
|
||||||
|
{
|
||||||
|
$recipients = GeneralUtility::trimExplode(',', (string)($settings['recipient'] ?? ''), true);
|
||||||
|
if ($recipients === []) {
|
||||||
|
throw new \RuntimeException('No recipient configured in the form plugin (FlexForm "Recipient").');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject = trim((string)($settings['subject'] ?? ''));
|
||||||
|
if ($subject === '') {
|
||||||
|
$subject = 'VITEC website form: ' . $formKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
foreach ($payload as $field => $value) {
|
||||||
|
$lines[] = str_pad($field . ':', 16) . $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail = GeneralUtility::makeInstance(MailMessage::class);
|
||||||
|
$mail->to(...$recipients)
|
||||||
|
->subject($subject)
|
||||||
|
->text("Form: $formKey\n\n" . implode("\n", $lines));
|
||||||
|
|
||||||
|
$submitterEmail = trim((string)($payload['email'] ?? ''));
|
||||||
|
if ($submitterEmail !== '' && filter_var($submitterEmail, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$mail->replyTo($submitterEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$mail->send()) {
|
||||||
|
throw new \RuntimeException('MailMessage::send() reported failure.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/vitec/Classes/Forms/Delivery/SalesforceDelivery.php
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Forms\Delivery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Salesforce delivery — PREPARED STUB.
|
||||||
|
*
|
||||||
|
* The old website posted the contact form to Salesforce Web-to-Lead
|
||||||
|
* (custom field ids like 00N3z00000DCiyV). When credentials / the target
|
||||||
|
* setup are decided, implement deliver() here (Web-to-Lead POST or REST
|
||||||
|
* API) including the camelCase -> Salesforce field mapping. Until then any
|
||||||
|
* submission with delivery=salesforce is stored with status "failed" and
|
||||||
|
* this message — no data is lost.
|
||||||
|
*/
|
||||||
|
final class SalesforceDelivery implements DeliveryInterface
|
||||||
|
{
|
||||||
|
public function deliver(string $formKey, array $payload, array $settings): void
|
||||||
|
{
|
||||||
|
throw new \RuntimeException(
|
||||||
|
'Salesforce delivery is prepared but not configured yet — submission kept in the log.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
190
packages/vitec/Classes/Forms/FormDefinitions.php
Executable file
@@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Forms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for the VITEC form plugins.
|
||||||
|
*
|
||||||
|
* The SAME definition drives (a) the JSON the React frontend renders the
|
||||||
|
* form from and (b) the server-side validation of submissions in
|
||||||
|
* FormSubmissionMiddleware. Field names are camelCase (React contract);
|
||||||
|
* mapping to Salesforce field ids happens inside the Salesforce delivery.
|
||||||
|
*
|
||||||
|
* Field spec: name, type (text|email|tel|select|textarea), label, required,
|
||||||
|
* options (select only) OR optionsSource (frontend-supplied list, e.g. the
|
||||||
|
* ISO country list).
|
||||||
|
*/
|
||||||
|
final class FormDefinitions
|
||||||
|
{
|
||||||
|
public const HONEYPOT_FIELD = '_website';
|
||||||
|
|
||||||
|
/** CType => form key */
|
||||||
|
public const CTYPE_MAP = [
|
||||||
|
'vitec_contactform' => 'contact',
|
||||||
|
'vitec_demoform' => 'demo',
|
||||||
|
'vitec_helpdeskform' => 'helpdesk',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const SOLUTION_OPTIONS = [
|
||||||
|
'IPTV Distribution',
|
||||||
|
'Digital Signage',
|
||||||
|
'Video Streaming',
|
||||||
|
'Video Contribution',
|
||||||
|
'Situational Awareness and ISR',
|
||||||
|
'Remote Production',
|
||||||
|
'Encoders & Decoders',
|
||||||
|
'Custom Design Service',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
public static function get(string $formKey): ?array
|
||||||
|
{
|
||||||
|
$all = self::all();
|
||||||
|
return $all[$formKey] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public static function all(): array
|
||||||
|
{
|
||||||
|
$contactFields = [
|
||||||
|
self::text('firstName', 'First Name', true),
|
||||||
|
self::text('lastName', 'Last Name', true),
|
||||||
|
self::text('company', 'Company', true),
|
||||||
|
self::field('email', 'email', 'Email', true),
|
||||||
|
self::field('phone', 'tel', 'Phone', true),
|
||||||
|
self::text('street', 'Street', false),
|
||||||
|
self::text('city', 'City', false),
|
||||||
|
self::text('zip', 'Zip / Postal Code', false),
|
||||||
|
self::countrySelect(),
|
||||||
|
self::select('state', 'State', false, [], 'usStates'),
|
||||||
|
self::select('solution', 'Solution of Interest', false, self::SOLUTION_OPTIONS),
|
||||||
|
self::field('message', 'textarea', 'Message', false),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'contact' => [
|
||||||
|
'formKey' => 'contact',
|
||||||
|
'title' => 'Contact VITEC',
|
||||||
|
'fields' => $contactFields,
|
||||||
|
],
|
||||||
|
'demo' => [
|
||||||
|
'formKey' => 'demo',
|
||||||
|
'title' => 'Request a Demo',
|
||||||
|
'fields' => $contactFields,
|
||||||
|
],
|
||||||
|
'helpdesk' => [
|
||||||
|
'formKey' => 'helpdesk',
|
||||||
|
'title' => 'Request Access to Online HelpDesk',
|
||||||
|
'fields' => [
|
||||||
|
self::text('firstName', 'First Name', true),
|
||||||
|
self::text('lastName', 'Last Name', true),
|
||||||
|
self::text('company', 'Company', true),
|
||||||
|
self::field('phone', 'tel', 'Phone Number', false),
|
||||||
|
self::field('email', 'email', 'Email Address', true),
|
||||||
|
self::countrySelect(true),
|
||||||
|
self::text('product', 'Product', false),
|
||||||
|
self::text('serialNumber', 'Serial Number', false),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a submission payload against the definition.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $payload
|
||||||
|
* @return array<string,string> field => error (empty = valid)
|
||||||
|
*/
|
||||||
|
public static function validate(string $formKey, array $payload): array
|
||||||
|
{
|
||||||
|
$definition = self::get($formKey);
|
||||||
|
if ($definition === null) {
|
||||||
|
return ['_form' => 'Unknown form'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
foreach ($definition['fields'] as $field) {
|
||||||
|
$name = $field['name'];
|
||||||
|
$value = trim((string)($payload[$name] ?? ''));
|
||||||
|
|
||||||
|
if (($field['required'] ?? false) && $value === '') {
|
||||||
|
$errors[$name] = 'This field is required.';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($field['type'] === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errors[$name] = 'Please enter a valid email address.';
|
||||||
|
}
|
||||||
|
if (mb_strlen($value) > 5000) {
|
||||||
|
$errors[$name] = 'Value is too long.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce a raw payload to the defined fields (drops everything unknown).
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $payload
|
||||||
|
* @return array<string,string>
|
||||||
|
*/
|
||||||
|
public static function filterPayload(string $formKey, array $payload): array
|
||||||
|
{
|
||||||
|
$definition = self::get($formKey);
|
||||||
|
if ($definition === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($definition['fields'] as $field) {
|
||||||
|
$out[$field['name']] = trim((string)($payload[$field['name']] ?? ''));
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ field DSL
|
||||||
|
|
||||||
|
/** @return array<string,mixed> */
|
||||||
|
private static function field(string $name, string $type, string $label, bool $required): array
|
||||||
|
{
|
||||||
|
return ['name' => $name, 'type' => $type, 'label' => $label, 'required' => $required];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string,mixed> */
|
||||||
|
private static function text(string $name, string $label, bool $required): array
|
||||||
|
{
|
||||||
|
return self::field($name, 'text', $label, $required);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int,string> $options
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private static function select(string $name, string $label, bool $required, array $options, string $optionsSource = ''): array
|
||||||
|
{
|
||||||
|
$field = self::field($name, 'select', $label, $required);
|
||||||
|
if ($options !== []) {
|
||||||
|
$field['options'] = $options;
|
||||||
|
}
|
||||||
|
if ($optionsSource !== '') {
|
||||||
|
// The frontend supplies this list (e.g. ISO countries) — keeps
|
||||||
|
// the definition lean and the lists consistent app-wide.
|
||||||
|
$field['optionsSource'] = $optionsSource;
|
||||||
|
}
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string,mixed> */
|
||||||
|
private static function countrySelect(bool $required = false): array
|
||||||
|
{
|
||||||
|
return self::select('country', 'Country', $required, [], 'countries');
|
||||||
|
}
|
||||||
|
}
|
||||||
170
packages/vitec/Classes/Middleware/FormSubmissionMiddleware.php
Executable file
@@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Middleware;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Forms\Delivery\DeliveryInterface;
|
||||||
|
use Evomedien\Vitec\Forms\Delivery\EmailDelivery;
|
||||||
|
use Evomedien\Vitec\Forms\Delivery\SalesforceDelivery;
|
||||||
|
use Evomedien\Vitec\Forms\FormDefinitions;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headless form endpoint: POST /api/vitec/form/<formKey>
|
||||||
|
*
|
||||||
|
* Accepts JSON (or form-encoded) submissions from the React frontend for
|
||||||
|
* the VITEC form plugins (contact | demo | helpdesk):
|
||||||
|
*
|
||||||
|
* 1. honeypot check (silently accepted, nothing stored)
|
||||||
|
* 2. server-side validation against FormDefinitions (single source of truth)
|
||||||
|
* 3. submission stored in tx_vitec_form_submission (backup log)
|
||||||
|
* 4. delivery via strategy: email (active) or salesforce (prepared stub) —
|
||||||
|
* configured in the FlexForm of the form's plugin element
|
||||||
|
*
|
||||||
|
* A stored-but-undelivered submission still responds success:true — the
|
||||||
|
* data is safe in the log and the delivery status is visible in the backend.
|
||||||
|
*/
|
||||||
|
final class FormSubmissionMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
private const PATH_PATTERN = '#^/api/vitec/form/([a-z]+)/?$#';
|
||||||
|
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$path = $request->getUri()->getPath();
|
||||||
|
if (preg_match(self::PATH_PATTERN, $path, $m) !== 1) {
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
$formKey = $m[1];
|
||||||
|
|
||||||
|
if (strtoupper($request->getMethod()) !== 'POST') {
|
||||||
|
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'POST only']], 405);
|
||||||
|
}
|
||||||
|
if (FormDefinitions::get($formKey) === null) {
|
||||||
|
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unknown form']], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$payload = $this->parseBody($request);
|
||||||
|
|
||||||
|
// Honeypot: pretend success, store nothing.
|
||||||
|
if (trim((string)($payload[FormDefinitions::HONEYPOT_FIELD] ?? '')) !== '') {
|
||||||
|
return new JsonResponse(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = FormDefinitions::validate($formKey, $payload);
|
||||||
|
if ($errors !== []) {
|
||||||
|
return new JsonResponse(['success' => false, 'errors' => $errors], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$clean = FormDefinitions::filterPayload($formKey, $payload);
|
||||||
|
$settings = $this->pluginSettings($formKey);
|
||||||
|
$method = (string)($settings['delivery'] ?? 'email');
|
||||||
|
|
||||||
|
$submissionUid = $this->store($formKey, $clean, $method);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->delivery($method)->deliver($formKey, $clean, $settings);
|
||||||
|
$this->updateStatus($submissionUid, 'sent', '');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->updateStatus($submissionUid, 'failed', $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data is safely logged either way.
|
||||||
|
return new JsonResponse(['success' => true]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unexpected error']], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string,mixed> */
|
||||||
|
private function parseBody(ServerRequestInterface $request): array
|
||||||
|
{
|
||||||
|
$contentType = $request->getHeaderLine('Content-Type');
|
||||||
|
if (str_contains($contentType, 'application/json')) {
|
||||||
|
$decoded = json_decode((string)$request->getBody(), true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
$parsed = $request->getParsedBody();
|
||||||
|
return is_array($parsed) ? $parsed : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delivery settings come from the FlexForm of the (first) plugin element
|
||||||
|
* of this form type — one configuration per form type.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function pluginSettings(string $formKey): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$ctype = array_search($formKey, FormDefinitions::CTYPE_MAP, true);
|
||||||
|
if ($ctype === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||||
|
$row = $qb
|
||||||
|
->select('pi_flexform')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$qb->expr()->eq('CType', $qb->createNamedParameter($ctype)),
|
||||||
|
$qb->expr()->eq('deleted', 0),
|
||||||
|
$qb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->orderBy('uid', 'ASC')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAssociative();
|
||||||
|
if (!$row) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$data = $flexFormService->convertFlexFormContentToArray((string)$row['pi_flexform']);
|
||||||
|
return $data['settings'] ?? [];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function delivery(string $method): DeliveryInterface
|
||||||
|
{
|
||||||
|
return $method === 'salesforce'
|
||||||
|
? GeneralUtility::makeInstance(SalesforceDelivery::class)
|
||||||
|
: GeneralUtility::makeInstance(EmailDelivery::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,string> $payload */
|
||||||
|
private function store(string $formKey, array $payload, string $method): int
|
||||||
|
{
|
||||||
|
$conn = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getConnectionForTable('tx_vitec_form_submission');
|
||||||
|
$conn->insert('tx_vitec_form_submission', [
|
||||||
|
'pid' => 0,
|
||||||
|
'crdate' => time(),
|
||||||
|
'tstamp' => time(),
|
||||||
|
'form_key' => $formKey,
|
||||||
|
'payload' => (string)json_encode($payload, JSON_UNESCAPED_UNICODE),
|
||||||
|
'delivery_method' => $method,
|
||||||
|
'delivery_status' => 'pending',
|
||||||
|
]);
|
||||||
|
return (int)$conn->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateStatus(int $uid, string $status, string $error): void
|
||||||
|
{
|
||||||
|
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||||
|
->getConnectionForTable('tx_vitec_form_submission')
|
||||||
|
->update(
|
||||||
|
'tx_vitec_form_submission',
|
||||||
|
['delivery_status' => $status, 'delivery_error' => mb_substr($error, 0, 1000), 'tstamp' => time()],
|
||||||
|
['uid' => $uid]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer;
|
|||||||
use Evomedien\Vitec\UserFunc\EventlistJsonRenderer;
|
use Evomedien\Vitec\UserFunc\EventlistJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\LocationsJsonRenderer;
|
use Evomedien\Vitec\UserFunc\LocationsJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer;
|
use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer;
|
||||||
|
use Evomedien\Vitec\UserFunc\FormsJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer;
|
use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer;
|
||||||
use Evomedien\Vitec\UserFunc\NewsJsonRenderer;
|
use Evomedien\Vitec\UserFunc\NewsJsonRenderer;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
@@ -82,6 +83,9 @@ final class ContentElementResolver
|
|||||||
'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'],
|
'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'],
|
||||||
'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'],
|
'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'],
|
||||||
'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'],
|
'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'],
|
||||||
|
'vitec_contactform' => [FormsJsonRenderer::class, 'form'],
|
||||||
|
'vitec_demoform' => [FormsJsonRenderer::class, 'form'],
|
||||||
|
'vitec_helpdeskform' => [FormsJsonRenderer::class, 'form'],
|
||||||
'news_pi1' => [NewsJsonRenderer::class, 'news'],
|
'news_pi1' => [NewsJsonRenderer::class, 'news'],
|
||||||
'news_newsliststicky' => [NewsJsonRenderer::class, 'news'],
|
'news_newsliststicky' => [NewsJsonRenderer::class, 'news'],
|
||||||
'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'],
|
'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
|
|||||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||||
|
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialises a Success Story (tx_vitec_domain_model_usecase) DB row to the
|
* Serialises a Success Story (tx_vitec_domain_model_usecase) DB row to the
|
||||||
@@ -43,7 +44,7 @@ final class UsecaseSerializer
|
|||||||
'slug' => (string)($u['slug'] ?? ''),
|
'slug' => (string)($u['slug'] ?? ''),
|
||||||
'subtitle' => (string)($u['subtitle'] ?? ''),
|
'subtitle' => (string)($u['subtitle'] ?? ''),
|
||||||
'teaser' => (string)($u['teaser'] ?? ''),
|
'teaser' => (string)($u['teaser'] ?? ''),
|
||||||
'cardImage' => $this->image($uid, 'card_image'),
|
'cardImage' => $this->image($uid, 'card_image', null, true),
|
||||||
'customerLogo' => $this->image($uid, 'customer_logo'),
|
'customerLogo' => $this->image($uid, 'customer_logo'),
|
||||||
'markets' => $this->relationMulti('tx_vitec_usecase_market_mm', 'tx_vitec_domain_model_market', $uid),
|
'markets' => $this->relationMulti('tx_vitec_usecase_market_mm', 'tx_vitec_domain_model_market', $uid),
|
||||||
'categories' => $this->categories($uid),
|
'categories' => $this->categories($uid),
|
||||||
@@ -100,7 +101,7 @@ final class UsecaseSerializer
|
|||||||
*
|
*
|
||||||
* @return array<string,mixed>|null
|
* @return array<string,mixed>|null
|
||||||
*/
|
*/
|
||||||
public function image(int $recordUid, string $fieldName, ?string $table = null): ?array
|
public function image(int $recordUid, string $fieldName, ?string $table = null, bool $withCropVariants = false): ?array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$table = $table ?? self::TABLE;
|
$table = $table ?? self::TABLE;
|
||||||
@@ -142,43 +143,92 @@ final class UsecaseSerializer
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cropString = (string)($ref['crop'] ?? '');
|
||||||
|
$payload = $this->processImagePayload($imageService, $fileReference, $cropString !== '' ? $cropString : null);
|
||||||
|
|
||||||
$srcset = [];
|
$result = [
|
||||||
foreach (self::IMAGE_WIDTHS as $width) {
|
|
||||||
$variant = $imageService->applyProcessingInstructions(
|
|
||||||
$fileReference,
|
|
||||||
['width' => $width, 'crop' => $ref['crop'] ?? null]
|
|
||||||
);
|
|
||||||
$srcset[] = [
|
|
||||||
'url' => $imageService->getImageUri($variant),
|
|
||||||
'width' => $width,
|
|
||||||
'descriptor' => $width . 'w',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$default = $imageService->applyProcessingInstructions(
|
|
||||||
$fileReference,
|
|
||||||
['width' => 800, 'crop' => $ref['crop'] ?? null]
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'uid' => (int)$ref['uid'],
|
'uid' => (int)$ref['uid'],
|
||||||
'url' => $imageService->getImageUri($default),
|
'url' => $payload['url'],
|
||||||
'title' => (string)($ref['title'] ?? ''),
|
'title' => (string)($ref['title'] ?? ''),
|
||||||
'alternative' => (string)($ref['alternative'] ?? ''),
|
'alternative' => (string)($ref['alternative'] ?? ''),
|
||||||
'description' => (string)($ref['description'] ?? ''),
|
'description' => (string)($ref['description'] ?? ''),
|
||||||
'srcset' => $srcset,
|
'srcset' => $payload['srcset'],
|
||||||
'properties' => [
|
'properties' => [
|
||||||
'width' => $fileReference->getProperty('width'),
|
'width' => $fileReference->getProperty('width'),
|
||||||
'height' => $fileReference->getProperty('height'),
|
'height' => $fileReference->getProperty('height'),
|
||||||
'mimeType' => $fileReference->getProperty('mime_type'),
|
'mimeType' => $fileReference->getProperty('mime_type'),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Zusatz-Zuschnitte (Card 4:3 / Large Card 16:9) nur fuer das "erste Bild"
|
||||||
|
// der karten-faehigen Modelle; faellt auf das Vollbild zurueck, wenn keine
|
||||||
|
// Variante gesetzt ist.
|
||||||
|
if ($withCropVariants) {
|
||||||
|
$result['crops'] = $this->cropVariantPayloads($imageService, $fileReference, $cropString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build {url, srcset} for one crop input (crop JSON string, absolute Area
|
||||||
|
* or null = full image). srcset steps whose processing would fall back to
|
||||||
|
* the original file are skipped (avoids raw-/403-leaks for sources that are
|
||||||
|
* smaller than the requested width).
|
||||||
|
*
|
||||||
|
* @param string|\TYPO3\CMS\Core\Imaging\ImageManipulation\Area|null $crop
|
||||||
|
* @return array{url:string,srcset:array<int,array<string,mixed>>}
|
||||||
|
*/
|
||||||
|
private function processImagePayload(ImageService $imageService, \TYPO3\CMS\Core\Resource\FileReference $fileReference, $crop): array
|
||||||
|
{
|
||||||
|
$srcset = [];
|
||||||
|
foreach (self::IMAGE_WIDTHS as $width) {
|
||||||
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => $width, 'crop' => $crop, 'fileExtension' => 'webp']
|
||||||
|
);
|
||||||
|
if ($variant->usesOriginalFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$srcset[] = [
|
||||||
|
'url' => $imageService->getImageUri($variant),
|
||||||
|
'width' => $width,
|
||||||
|
'descriptor' => $width . 'w',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$default = $imageService->applyProcessingInstructions(
|
||||||
|
$fileReference,
|
||||||
|
['width' => 800, 'crop' => $crop, 'fileExtension' => 'webp']
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'url' => $imageService->getImageUri($default),
|
||||||
|
'srcset' => $srcset,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* card + largeCard crop-variant payloads for a model's first image.
|
||||||
|
* Each variant falls back to the full image when no explicit crop is set.
|
||||||
|
*
|
||||||
|
* @return array<string,array{url:string,srcset:array<int,array<string,mixed>>}>
|
||||||
|
*/
|
||||||
|
private function cropVariantPayloads(ImageService $imageService, \TYPO3\CMS\Core\Resource\FileReference $fileReference, string $cropString): array
|
||||||
|
{
|
||||||
|
$collection = CropVariantCollection::create($cropString);
|
||||||
|
$out = [];
|
||||||
|
foreach (['card', 'largeCard'] as $variantKey) {
|
||||||
|
$area = $collection->getCropArea($variantKey);
|
||||||
|
$crop = $area->isEmpty() ? null : $area->makeAbsoluteBasedOnFile($fileReference);
|
||||||
|
$out[$variantKey] = $this->processImagePayload($imageService, $fileReference, $crop);
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All FAL images of one field (multi-reference), same shape as image().
|
* All FAL images of one field (multi-reference), same shape as image().
|
||||||
*
|
*
|
||||||
@@ -260,13 +310,16 @@ final class UsecaseSerializer
|
|||||||
foreach (self::IMAGE_WIDTHS as $width) {
|
foreach (self::IMAGE_WIDTHS as $width) {
|
||||||
$variant = $imageService->applyProcessingInstructions(
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => $width, 'crop' => $ref['crop'] ?? null]
|
['width' => $width, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
if ($variant->usesOriginalFile()) {
|
||||||
|
continue; // Quelle zu klein fuer diese Breite: kein Raw-/403-Leak
|
||||||
|
}
|
||||||
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
|
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
|
||||||
}
|
}
|
||||||
$default = $imageService->applyProcessingInstructions(
|
$default = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 800, 'crop' => $ref['crop'] ?? null]
|
['width' => 800, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
56
packages/vitec/Classes/Tca/CropVariants.php
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\Tca;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentrale cropVariants-Definition fuer das "erste Bild" der Karten-faehigen
|
||||||
|
* Modelle (Product, Success Story, Market, Solution).
|
||||||
|
*
|
||||||
|
* Liefert drei Zuschnitt-Varianten, die im Backend-Image-Editor je einen
|
||||||
|
* eigenen Tab bekommen:
|
||||||
|
* - default : freier/allgemeiner Zuschnitt (Detail-/Hero-/Listen-Nutzung)
|
||||||
|
* - card : 4:3 fuer die normale Card-Darstellung
|
||||||
|
* - largeCard : 16:9 fuer die grosse/Feature-Card-Darstellung
|
||||||
|
*
|
||||||
|
* Ein freier Zuschnitt ("Free") ist bei jeder Variante zusaetzlich moeglich;
|
||||||
|
* die genannte Ratio ist lediglich der vorgegebene Standard-Rahmen.
|
||||||
|
*
|
||||||
|
* Wird aus den TCA-Dateien der o.g. Modelle heraus referenziert:
|
||||||
|
* 'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||||
|
*/
|
||||||
|
final class CropVariants
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public static function firstImage(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'default' => [
|
||||||
|
'title' => 'Default',
|
||||||
|
'allowedAspectRatios' => [
|
||||||
|
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||||
|
'16:9' => ['title' => '16:9', 'value' => 16 / 9],
|
||||||
|
'4:3' => ['title' => '4:3', 'value' => 4 / 3],
|
||||||
|
'1:1' => ['title' => '1:1', 'value' => 1.0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'card' => [
|
||||||
|
'title' => 'Card (4:3)',
|
||||||
|
'allowedAspectRatios' => [
|
||||||
|
'4:3' => ['title' => '4:3', 'value' => 4 / 3],
|
||||||
|
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'largeCard' => [
|
||||||
|
'title' => 'Large Card (16:9)',
|
||||||
|
'allowedAspectRatios' => [
|
||||||
|
'16:9' => ['title' => '16:9', 'value' => 16 / 9],
|
||||||
|
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -204,13 +204,13 @@ class CustomerlogosJsonRenderer
|
|||||||
foreach (self::IMAGE_WIDTHS as $width) {
|
foreach (self::IMAGE_WIDTHS as $width) {
|
||||||
$variant = $imageService->applyProcessingInstructions(
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => $width, 'crop' => $ref['crop'] ?? null]
|
['width' => $width, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
|
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
|
||||||
}
|
}
|
||||||
$default = $imageService->applyProcessingInstructions(
|
$default = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 400, 'crop' => $ref['crop'] ?? null]
|
['width' => 400, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ class DatasheetsJsonRenderer
|
|||||||
|
|
||||||
$default = $imageService->applyProcessingInstructions(
|
$default = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 400, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class DownloadcardcollectionJsonRenderer
|
|||||||
foreach ([400, 800, 1200, 1600] as $width) {
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
$variant = $imageService->applyProcessingInstructions(
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
$srcset[] = [
|
$srcset[] = [
|
||||||
'url' => $imageService->getImageUri($variant),
|
'url' => $imageService->getImageUri($variant),
|
||||||
@@ -222,7 +222,7 @@ class DownloadcardcollectionJsonRenderer
|
|||||||
|
|
||||||
$default = $imageService->applyProcessingInstructions(
|
$default = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ class EventlistJsonRenderer
|
|||||||
foreach ([400, 800] as $width) {
|
foreach ([400, 800] as $width) {
|
||||||
$variant = $imageService->applyProcessingInstructions(
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
$srcset[] = [
|
$srcset[] = [
|
||||||
'url' => $imageService->getImageUri($variant),
|
'url' => $imageService->getImageUri($variant),
|
||||||
@@ -258,7 +258,7 @@ class EventlistJsonRenderer
|
|||||||
|
|
||||||
$default = $imageService->applyProcessingInstructions(
|
$default = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 400, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
47
packages/vitec/Classes/UserFunc/FaviconsJsonRenderer.php
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headless JSON renderer for the site-wide favicon / app-icon set.
|
||||||
|
*
|
||||||
|
* Emits a stable `favicons` object into every page response so the React
|
||||||
|
* frontend can render the <link>/<meta> head tags from the CMS instead of
|
||||||
|
* hard-coding them. The icon files live under public/fileadmin/icons/ and are
|
||||||
|
* referenced root-relative — identical convention to the image URLs emitted by
|
||||||
|
* UsecaseSerializer::image() (getImageUri also returns /fileadmin/... paths).
|
||||||
|
* If the frontend ever runs on a separate origin from the CMS, it must prefix
|
||||||
|
* these hrefs with the CMS base URL, exactly as it does for image URLs.
|
||||||
|
*
|
||||||
|
* Static by nature (same for every page) — no DB access, exception-free.
|
||||||
|
*/
|
||||||
|
final class FaviconsJsonRenderer
|
||||||
|
{
|
||||||
|
private const BASE = '/fileadmin/icons';
|
||||||
|
|
||||||
|
/** Browser UI / address-bar tint (VITEC navy). */
|
||||||
|
private const THEME_COLOR = '#26358C';
|
||||||
|
|
||||||
|
#[AsAllowedCallable]
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'themeColor' => self::THEME_COLOR,
|
||||||
|
'manifest' => self::BASE . '/site.webmanifest',
|
||||||
|
// Ready-to-render <link> descriptors, ordered most- to least-specific.
|
||||||
|
'links' => [
|
||||||
|
['rel' => 'icon', 'type' => 'image/x-icon', 'href' => self::BASE . '/favicon.ico'],
|
||||||
|
['rel' => 'icon', 'type' => 'image/png', 'sizes' => '32x32', 'href' => self::BASE . '/favicon-32.png'],
|
||||||
|
['rel' => 'icon', 'type' => 'image/png', 'sizes' => '16x16', 'href' => self::BASE . '/favicon-16.png'],
|
||||||
|
['rel' => 'apple-touch-icon', 'sizes' => '180x180', 'href' => self::BASE . '/apple-touch-icon.png'],
|
||||||
|
['rel' => 'manifest', 'href' => self::BASE . '/site.webmanifest'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return json_encode($payload) ?: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
108
packages/vitec/Classes/UserFunc/FormsJsonRenderer.php
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\Vitec\UserFunc;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\ParameterType;
|
||||||
|
use Evomedien\Vitec\Forms\FormDefinitions;
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserFunc: render ONE of the VITEC form plugins as JSON (headless).
|
||||||
|
* Serves vitec_contactform, vitec_demoform and vitec_helpdeskform — the
|
||||||
|
* CType picks the definition from FormDefinitions.
|
||||||
|
*
|
||||||
|
* Output under content.form:
|
||||||
|
* { "formKey": "contact", "title": "…", "endpoint": "/api/vitec/form/contact",
|
||||||
|
* "honeypot": "_website", "fields": [ … ] }
|
||||||
|
*
|
||||||
|
* The React frontend renders the fields generically and POSTs JSON to the
|
||||||
|
* endpoint (handled by FormSubmissionMiddleware). Exception-safe.
|
||||||
|
*/
|
||||||
|
class FormsJsonRenderer
|
||||||
|
{
|
||||||
|
#[AsAllowedCallable]
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
|
||||||
|
if ($row && isset(FormDefinitions::CTYPE_MAP[(string)($row['CType'] ?? '')])) {
|
||||||
|
return $this->renderForRecord($row);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageId = 0;
|
||||||
|
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||||
|
if ($request !== null) {
|
||||||
|
$pageInfo = $request->getAttribute('frontend.page.information');
|
||||||
|
if ($pageInfo !== null) {
|
||||||
|
$pageId = (int)$pageInfo->getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($pageId <= 0) {
|
||||||
|
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||||
|
}
|
||||||
|
if ($pageId <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||||
|
$ces = $qb
|
||||||
|
->select('*')
|
||||||
|
->from('tt_content')
|
||||||
|
->where(
|
||||||
|
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||||
|
$qb->expr()->in('CType', $qb->createNamedParameter(
|
||||||
|
array_keys(FormDefinitions::CTYPE_MAP),
|
||||||
|
\TYPO3\CMS\Core\Database\Connection::PARAM_STR_ARRAY
|
||||||
|
)),
|
||||||
|
$qb->expr()->eq('deleted', 0),
|
||||||
|
$qb->expr()->eq('hidden', 0)
|
||||||
|
)
|
||||||
|
->executeQuery()
|
||||||
|
->fetchAllAssociative();
|
||||||
|
|
||||||
|
if (empty($ces)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->renderForRecord($ces[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $contentElement
|
||||||
|
*/
|
||||||
|
public function renderForRecord(array $contentElement): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$formKey = FormDefinitions::CTYPE_MAP[(string)($contentElement['CType'] ?? '')] ?? '';
|
||||||
|
$definition = FormDefinitions::get($formKey);
|
||||||
|
if ($definition === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||||
|
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||||
|
$settings = $flexFormData['settings'] ?? [];
|
||||||
|
$debugMode = (bool)($settings['debug'] ?? false);
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'formKey' => $definition['formKey'],
|
||||||
|
'title' => $definition['title'],
|
||||||
|
'endpoint' => '/api/vitec/form/' . $definition['formKey'],
|
||||||
|
'honeypot' => FormDefinitions::HONEYPOT_FIELD,
|
||||||
|
'fields' => $definition['fields'],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($debugMode) {
|
||||||
|
$response['debug'] = ['settings' => $settings];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)json_encode($response);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,7 +201,7 @@ class MarketShowJsonRenderer
|
|||||||
foreach ([400, 800, 1200, 1600] as $width) {
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
$variant = $imageService->applyProcessingInstructions(
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
$srcset[] = [
|
$srcset[] = [
|
||||||
'url' => $imageService->getImageUri($variant),
|
'url' => $imageService->getImageUri($variant),
|
||||||
@@ -212,7 +212,7 @@ class MarketShowJsonRenderer
|
|||||||
|
|
||||||
$default = $imageService->applyProcessingInstructions(
|
$default = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ class ModelcardJsonRenderer
|
|||||||
'slug' => (string)($row['slug'] ?? ''),
|
'slug' => (string)($row['slug'] ?? ''),
|
||||||
'subtitle' => (string)($row['subtitle'] ?? ''),
|
'subtitle' => (string)($row['subtitle'] ?? ''),
|
||||||
'teaser' => (string)($row['teaser'] ?? ''),
|
'teaser' => (string)($row['teaser'] ?? ''),
|
||||||
'image' => $serializer->image($uid, 'image', 'tx_vitec_domain_model_product')
|
'image' => $serializer->image($uid, 'image', 'tx_vitec_domain_model_product', true)
|
||||||
?? $serializer->image($uid, 'productimage', 'tx_vitec_domain_model_product'),
|
?? $serializer->image($uid, 'productimage', 'tx_vitec_domain_model_product', true),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ class ModelcardJsonRenderer
|
|||||||
'subtitle' => (string)($row['subtitle'] ?? ''),
|
'subtitle' => (string)($row['subtitle'] ?? ''),
|
||||||
'teaser' => (string)($row['teaser'] ?? ''),
|
'teaser' => (string)($row['teaser'] ?? ''),
|
||||||
'description' => (string)($row['description'] ?? ''),
|
'description' => (string)($row['description'] ?? ''),
|
||||||
'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType]),
|
'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType], true),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ final class NewsJsonRenderer
|
|||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
try {
|
try {
|
||||||
$fileReference = $resourceFactory->getFileReferenceObject((int)$row['uid']);
|
$fileReference = $resourceFactory->getFileReferenceObject((int)$row['uid']);
|
||||||
$processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1400]);
|
$processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1400, 'fileExtension' => 'webp']);
|
||||||
|
|
||||||
$files[] = [
|
$files[] = [
|
||||||
'uid' => (int)$row['uid'],
|
'uid' => (int)$row['uid'],
|
||||||
|
|||||||
@@ -246,7 +246,8 @@ class ProductListJsonRenderer
|
|||||||
[
|
[
|
||||||
'width' => $dimensions['width'],
|
'width' => $dimensions['width'],
|
||||||
'height' => $dimensions['height'],
|
'height' => $dimensions['height'],
|
||||||
'crop' => $fileRefData['crop'] ?? null
|
'crop' => $fileRefData['crop'] ?? null,
|
||||||
|
'fileExtension' => 'webp'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ class ProductListJsonRenderer
|
|||||||
|
|
||||||
$defaultProcessed = $imageService->applyProcessingInstructions(
|
$defaultProcessed = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
$images[] = [
|
$images[] = [
|
||||||
@@ -318,7 +319,7 @@ class ProductListJsonRenderer
|
|||||||
|
|
||||||
$processed = $imageService->applyProcessingInstructions(
|
$processed = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 1200, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 1200, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -327,14 +327,14 @@ class ProductShowJsonRenderer
|
|||||||
|
|
||||||
$processedImage = $imageService->applyProcessingInstructions(
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => '1874c', 'height' => '625c']
|
['width' => '1874c', 'height' => '625c', 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
$srcset = [];
|
$srcset = [];
|
||||||
foreach ([400, 800, 1200, 1600] as $width) {
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
$processedVariant = $imageService->applyProcessingInstructions(
|
$processedVariant = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c']
|
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c', 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
$srcset[] = [
|
$srcset[] = [
|
||||||
'url' => $imageService->getImageUri($processedVariant),
|
'url' => $imageService->getImageUri($processedVariant),
|
||||||
@@ -399,7 +399,7 @@ class ProductShowJsonRenderer
|
|||||||
|
|
||||||
$processedImage = $imageService->applyProcessingInstructions(
|
$processedImage = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 1200]
|
['width' => 1200, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class SolutionShowJsonRenderer
|
|||||||
foreach ([400, 800, 1200, 1600] as $width) {
|
foreach ([400, 800, 1200, 1600] as $width) {
|
||||||
$variant = $imageService->applyProcessingInstructions(
|
$variant = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
$srcset[] = [
|
$srcset[] = [
|
||||||
'url' => $imageService->getImageUri($variant),
|
'url' => $imageService->getImageUri($variant),
|
||||||
@@ -212,7 +212,7 @@ class SolutionShowJsonRenderer
|
|||||||
|
|
||||||
$default = $imageService->applyProcessingInstructions(
|
$default = $imageService->applyProcessingInstructions(
|
||||||
$fileReference,
|
$fileReference,
|
||||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
65
packages/vitec/Configuration/FlexForms/Contactform.xml
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<T3DataStructure>
|
||||||
|
<meta>
|
||||||
|
<langDisable>1</langDisable>
|
||||||
|
</meta>
|
||||||
|
<sheets>
|
||||||
|
<sDEF>
|
||||||
|
<ROOT>
|
||||||
|
<sheetTitle>Contact Form Settings</sheetTitle>
|
||||||
|
<type>array</type>
|
||||||
|
<el>
|
||||||
|
<settings.delivery>
|
||||||
|
<label>Delivery</label>
|
||||||
|
<onChange>reload</onChange>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectSingle</renderType>
|
||||||
|
<items>
|
||||||
|
<numIndex index="0">
|
||||||
|
<label>E-Mail</label>
|
||||||
|
<value>email</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="1">
|
||||||
|
<label>Salesforce (prepared — not active yet)</label>
|
||||||
|
<value>salesforce</value>
|
||||||
|
</numIndex>
|
||||||
|
</items>
|
||||||
|
<default>email</default>
|
||||||
|
</config>
|
||||||
|
</settings.delivery>
|
||||||
|
|
||||||
|
<settings.recipient>
|
||||||
|
<label>Recipient(s)</label>
|
||||||
|
<description>Comma-separated email addresses.</description>
|
||||||
|
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||||
|
<config>
|
||||||
|
<type>input</type>
|
||||||
|
<size>50</size>
|
||||||
|
<eval>trim</eval>
|
||||||
|
</config>
|
||||||
|
</settings.recipient>
|
||||||
|
|
||||||
|
<settings.subject>
|
||||||
|
<label>Mail Subject</label>
|
||||||
|
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||||
|
<config>
|
||||||
|
<type>input</type>
|
||||||
|
<size>50</size>
|
||||||
|
<eval>trim</eval>
|
||||||
|
<default>VITEC website: contact request</default>
|
||||||
|
</config>
|
||||||
|
</settings.subject>
|
||||||
|
|
||||||
|
<settings.debug>
|
||||||
|
<label>Allow Debug Output</label>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.debug>
|
||||||
|
</el>
|
||||||
|
</ROOT>
|
||||||
|
</sDEF>
|
||||||
|
</sheets>
|
||||||
|
</T3DataStructure>
|
||||||
65
packages/vitec/Configuration/FlexForms/Demoform.xml
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<T3DataStructure>
|
||||||
|
<meta>
|
||||||
|
<langDisable>1</langDisable>
|
||||||
|
</meta>
|
||||||
|
<sheets>
|
||||||
|
<sDEF>
|
||||||
|
<ROOT>
|
||||||
|
<sheetTitle>Request Demo Settings</sheetTitle>
|
||||||
|
<type>array</type>
|
||||||
|
<el>
|
||||||
|
<settings.delivery>
|
||||||
|
<label>Delivery</label>
|
||||||
|
<onChange>reload</onChange>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectSingle</renderType>
|
||||||
|
<items>
|
||||||
|
<numIndex index="0">
|
||||||
|
<label>E-Mail</label>
|
||||||
|
<value>email</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="1">
|
||||||
|
<label>Salesforce (prepared — not active yet)</label>
|
||||||
|
<value>salesforce</value>
|
||||||
|
</numIndex>
|
||||||
|
</items>
|
||||||
|
<default>email</default>
|
||||||
|
</config>
|
||||||
|
</settings.delivery>
|
||||||
|
|
||||||
|
<settings.recipient>
|
||||||
|
<label>Recipient(s)</label>
|
||||||
|
<description>Comma-separated email addresses.</description>
|
||||||
|
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||||
|
<config>
|
||||||
|
<type>input</type>
|
||||||
|
<size>50</size>
|
||||||
|
<eval>trim</eval>
|
||||||
|
</config>
|
||||||
|
</settings.recipient>
|
||||||
|
|
||||||
|
<settings.subject>
|
||||||
|
<label>Mail Subject</label>
|
||||||
|
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||||
|
<config>
|
||||||
|
<type>input</type>
|
||||||
|
<size>50</size>
|
||||||
|
<eval>trim</eval>
|
||||||
|
<default>VITEC website: demo request</default>
|
||||||
|
</config>
|
||||||
|
</settings.subject>
|
||||||
|
|
||||||
|
<settings.debug>
|
||||||
|
<label>Allow Debug Output</label>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.debug>
|
||||||
|
</el>
|
||||||
|
</ROOT>
|
||||||
|
</sDEF>
|
||||||
|
</sheets>
|
||||||
|
</T3DataStructure>
|
||||||
65
packages/vitec/Configuration/FlexForms/Helpdeskform.xml
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<T3DataStructure>
|
||||||
|
<meta>
|
||||||
|
<langDisable>1</langDisable>
|
||||||
|
</meta>
|
||||||
|
<sheets>
|
||||||
|
<sDEF>
|
||||||
|
<ROOT>
|
||||||
|
<sheetTitle>HelpDesk Access Settings</sheetTitle>
|
||||||
|
<type>array</type>
|
||||||
|
<el>
|
||||||
|
<settings.delivery>
|
||||||
|
<label>Delivery</label>
|
||||||
|
<onChange>reload</onChange>
|
||||||
|
<config>
|
||||||
|
<type>select</type>
|
||||||
|
<renderType>selectSingle</renderType>
|
||||||
|
<items>
|
||||||
|
<numIndex index="0">
|
||||||
|
<label>E-Mail</label>
|
||||||
|
<value>email</value>
|
||||||
|
</numIndex>
|
||||||
|
<numIndex index="1">
|
||||||
|
<label>Salesforce (prepared — not active yet)</label>
|
||||||
|
<value>salesforce</value>
|
||||||
|
</numIndex>
|
||||||
|
</items>
|
||||||
|
<default>email</default>
|
||||||
|
</config>
|
||||||
|
</settings.delivery>
|
||||||
|
|
||||||
|
<settings.recipient>
|
||||||
|
<label>Recipient(s)</label>
|
||||||
|
<description>Comma-separated email addresses.</description>
|
||||||
|
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||||
|
<config>
|
||||||
|
<type>input</type>
|
||||||
|
<size>50</size>
|
||||||
|
<eval>trim</eval>
|
||||||
|
</config>
|
||||||
|
</settings.recipient>
|
||||||
|
|
||||||
|
<settings.subject>
|
||||||
|
<label>Mail Subject</label>
|
||||||
|
<displayCond>FIELD:settings.delivery:=:email</displayCond>
|
||||||
|
<config>
|
||||||
|
<type>input</type>
|
||||||
|
<size>50</size>
|
||||||
|
<eval>trim</eval>
|
||||||
|
<default>VITEC website: helpdesk access request</default>
|
||||||
|
</config>
|
||||||
|
</settings.subject>
|
||||||
|
|
||||||
|
<settings.debug>
|
||||||
|
<label>Allow Debug Output</label>
|
||||||
|
<config>
|
||||||
|
<type>check</type>
|
||||||
|
<default>0</default>
|
||||||
|
</config>
|
||||||
|
</settings.debug>
|
||||||
|
</el>
|
||||||
|
</ROOT>
|
||||||
|
</sDEF>
|
||||||
|
</sheets>
|
||||||
|
</T3DataStructure>
|
||||||
@@ -88,4 +88,16 @@ return [
|
|||||||
'provider' => SvgIconProvider::class,
|
'provider' => SvgIconProvider::class,
|
||||||
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg',
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg',
|
||||||
],
|
],
|
||||||
|
'vitec-plugin-contactform' => [
|
||||||
|
'provider' => SvgIconProvider::class,
|
||||||
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-contactform.svg',
|
||||||
|
],
|
||||||
|
'vitec-plugin-demoform' => [
|
||||||
|
'provider' => SvgIconProvider::class,
|
||||||
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-demoform.svg',
|
||||||
|
],
|
||||||
|
'vitec-plugin-helpdeskform' => [
|
||||||
|
'provider' => SvgIconProvider::class,
|
||||||
|
'source' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-helpdeskform.svg',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ declare(strict_types=1);
|
|||||||
*/
|
*/
|
||||||
return [
|
return [
|
||||||
'frontend' => [
|
'frontend' => [
|
||||||
|
'vitec/form-submission' => [
|
||||||
|
'target' => \Evomedien\Vitec\Middleware\FormSubmissionMiddleware::class,
|
||||||
|
'after' => [
|
||||||
|
'typo3/cms-core/normalized-params-attribute',
|
||||||
|
],
|
||||||
|
'before' => [
|
||||||
|
'typo3/cms-frontend/site',
|
||||||
|
],
|
||||||
|
],
|
||||||
'vitec/success-story-path-rewrite' => [
|
'vitec/success-story-path-rewrite' => [
|
||||||
'target' => \Evomedien\Vitec\Middleware\SuccessStoryPathRewrite::class,
|
'target' => \Evomedien\Vitec\Middleware\SuccessStoryPathRewrite::class,
|
||||||
'after' => [
|
'after' => [
|
||||||
|
|||||||
@@ -161,6 +161,21 @@ tt_content {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vitec_contactform < lib.contentElementWithHeader
|
||||||
|
vitec_contactform {
|
||||||
|
fields {
|
||||||
|
content {
|
||||||
|
fields {
|
||||||
|
form = USER
|
||||||
|
form.userFunc = Evomedien\Vitec\UserFunc\FormsJsonRenderer->render
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vitec_demoform < tt_content.vitec_contactform
|
||||||
|
vitec_helpdeskform < tt_content.vitec_contactform
|
||||||
|
|
||||||
vitec_modelcard < lib.contentElementWithHeader
|
vitec_modelcard < lib.contentElementWithHeader
|
||||||
vitec_modelcard {
|
vitec_modelcard {
|
||||||
fields {
|
fields {
|
||||||
|
|||||||
@@ -38,6 +38,37 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
|||||||
// Locations: lives in the "VITEC" wizard group, titled "VITEC Locations".
|
// Locations: lives in the "VITEC" wizard group, titled "VITEC Locations".
|
||||||
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']['vitec']
|
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']['vitec']
|
||||||
= 'LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:group.header';
|
= 'LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:group.header';
|
||||||
|
// VITEC Forms — own wizard tab for the three form plugins.
|
||||||
|
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['itemGroups']['vitec_forms']
|
||||||
|
= 'VITEC Forms';
|
||||||
|
ExtensionUtility::registerPlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Contactform',
|
||||||
|
'VITEC Contact Form',
|
||||||
|
'vitec-plugin-contactform',
|
||||||
|
'vitec_forms',
|
||||||
|
'Contact form — email or Salesforce delivery.',
|
||||||
|
'FILE:EXT:vitec/Configuration/FlexForms/Contactform.xml'
|
||||||
|
);
|
||||||
|
ExtensionUtility::registerPlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Demoform',
|
||||||
|
'VITEC Request Demo',
|
||||||
|
'vitec-plugin-demoform',
|
||||||
|
'vitec_forms',
|
||||||
|
'Demo request form — email or Salesforce delivery.',
|
||||||
|
'FILE:EXT:vitec/Configuration/FlexForms/Demoform.xml'
|
||||||
|
);
|
||||||
|
ExtensionUtility::registerPlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Helpdeskform',
|
||||||
|
'VITEC HelpDesk Access',
|
||||||
|
'vitec-plugin-helpdeskform',
|
||||||
|
'vitec_forms',
|
||||||
|
'Request access to the online helpdesk — email or Salesforce delivery.',
|
||||||
|
'FILE:EXT:vitec/Configuration/FlexForms/Helpdeskform.xml'
|
||||||
|
);
|
||||||
|
|
||||||
ExtensionUtility::registerPlugin(
|
ExtensionUtility::registerPlugin(
|
||||||
'Vitec',
|
'Vitec',
|
||||||
'Modelcard',
|
'Modelcard',
|
||||||
|
|||||||
@@ -164,6 +164,11 @@ return [
|
|||||||
'foreign_selector' => 'uid_local',
|
'foreign_selector' => 'uid_local',
|
||||||
'overrideChildTca' => [
|
'overrideChildTca' => [
|
||||||
'columns' => [
|
'columns' => [
|
||||||
|
'crop' => [
|
||||||
|
'config' => [
|
||||||
|
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||||
|
],
|
||||||
|
],
|
||||||
'uid_local' => [
|
'uid_local' => [
|
||||||
'config' => [
|
'config' => [
|
||||||
'appearance' => [
|
'appearance' => [
|
||||||
|
|||||||
@@ -443,6 +443,15 @@ return [
|
|||||||
'foreign_match_fields' => [
|
'foreign_match_fields' => [
|
||||||
'fieldname' => 'productimage',
|
'fieldname' => 'productimage',
|
||||||
],
|
],
|
||||||
|
'overrideChildTca' => [
|
||||||
|
'columns' => [
|
||||||
|
'crop' => [
|
||||||
|
'config' => [
|
||||||
|
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
'appearance' => [
|
'appearance' => [
|
||||||
'collapseAll' => true,
|
'collapseAll' => true,
|
||||||
'levelLinksPosition' => 'top',
|
'levelLinksPosition' => 'top',
|
||||||
|
|||||||
@@ -164,6 +164,11 @@ return [
|
|||||||
'foreign_selector' => 'uid_local',
|
'foreign_selector' => 'uid_local',
|
||||||
'overrideChildTca' => [
|
'overrideChildTca' => [
|
||||||
'columns' => [
|
'columns' => [
|
||||||
|
'crop' => [
|
||||||
|
'config' => [
|
||||||
|
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||||
|
],
|
||||||
|
],
|
||||||
'uid_local' => [
|
'uid_local' => [
|
||||||
'config' => [
|
'config' => [
|
||||||
'appearance' => [
|
'appearance' => [
|
||||||
|
|||||||
@@ -117,7 +117,21 @@ return [
|
|||||||
// --------------------------------------------------------- List view
|
// --------------------------------------------------------- List view
|
||||||
'card_image' => [
|
'card_image' => [
|
||||||
'label' => 'Card Image',
|
'label' => 'Card Image',
|
||||||
'config' => ['type' => 'file', 'maxitems' => 1, 'allowed' => 'common-image-types'],
|
'config' => [
|
||||||
|
'type' => 'file',
|
||||||
|
'maxitems' => 1,
|
||||||
|
'allowed' => 'common-image-types',
|
||||||
|
// Zusatz-Zuschnitte fuer Card-/Large-Card-Darstellung (freier Crop bleibt moeglich).
|
||||||
|
'overrideChildTca' => [
|
||||||
|
'columns' => [
|
||||||
|
'crop' => [
|
||||||
|
'config' => [
|
||||||
|
'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'customer_logo' => [
|
'customer_logo' => [
|
||||||
'label' => 'Customer Logo',
|
'label' => 'Customer Logo',
|
||||||
|
|||||||
41
packages/vitec/Configuration/TCA/tx_vitec_form_submission.php
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'ctrl' => [
|
||||||
|
'title' => 'VITEC Form Submission',
|
||||||
|
'label' => 'form_key',
|
||||||
|
'label_alt' => 'delivery_status',
|
||||||
|
'label_alt_force' => true,
|
||||||
|
'tstamp' => 'tstamp',
|
||||||
|
'crdate' => 'crdate',
|
||||||
|
'default_sortby' => 'crdate DESC',
|
||||||
|
'rootLevel' => 1,
|
||||||
|
'readOnly' => true,
|
||||||
|
'iconfile' => 'EXT:vitec/Resources/Public/Icons/vitec-plugin-contactform.svg',
|
||||||
|
'security' => ['ignorePageTypeRestriction' => true],
|
||||||
|
],
|
||||||
|
'types' => [
|
||||||
|
'1' => ['showitem' => 'form_key, delivery_method, delivery_status, delivery_error, payload'],
|
||||||
|
],
|
||||||
|
'columns' => [
|
||||||
|
'form_key' => [
|
||||||
|
'label' => 'Form',
|
||||||
|
'config' => ['type' => 'input', 'readOnly' => true],
|
||||||
|
],
|
||||||
|
'payload' => [
|
||||||
|
'label' => 'Data (JSON)',
|
||||||
|
'config' => ['type' => 'text', 'rows' => 12, 'readOnly' => true],
|
||||||
|
],
|
||||||
|
'delivery_method' => [
|
||||||
|
'label' => 'Delivery',
|
||||||
|
'config' => ['type' => 'input', 'readOnly' => true],
|
||||||
|
],
|
||||||
|
'delivery_status' => [
|
||||||
|
'label' => 'Status',
|
||||||
|
'config' => ['type' => 'input', 'readOnly' => true],
|
||||||
|
],
|
||||||
|
'delivery_error' => [
|
||||||
|
'label' => 'Delivery Error',
|
||||||
|
'config' => ['type' => 'text', 'rows' => 3, 'readOnly' => true],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -72,3 +72,10 @@ lib.metaMenu {
|
|||||||
page.10.fields.mainNavigation =< lib.mainNavigation
|
page.10.fields.mainNavigation =< lib.mainNavigation
|
||||||
page.10.fields.footerMenu =< lib.footerMenu
|
page.10.fields.footerMenu =< lib.footerMenu
|
||||||
page.10.fields.metaMenu =< lib.metaMenu
|
page.10.fields.metaMenu =< lib.metaMenu
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Favicon / app-icon set (site-wide) — see FaviconsJsonRenderer
|
||||||
|
# Icon files: public/fileadmin/icons/
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
page.10.fields.favicons = USER
|
||||||
|
page.10.fields.favicons.userFunc = Evomedien\Vitec\UserFunc\FaviconsJsonRenderer->render
|
||||||
|
|||||||
@@ -20,3 +20,15 @@
|
|||||||
.t3-page-ce.vitec-ce-collapsed .vitec-collapse-toggle typo3-backend-icon {
|
.t3-page-ce.vitec-ce-collapsed .vitec-collapse-toggle typo3-backend-icon {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Collapsed: the element's own preview header lives in the CE header row —
|
||||||
|
full width on its own line below the icon/button row. */
|
||||||
|
.t3-page-ce-header:has(.vitec-preview-header--in-header) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.element-preview-header.vitec-preview-header--in-header {
|
||||||
|
flex-basis: 100%;
|
||||||
|
width: 100%;
|
||||||
|
order: 10;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||||
|
<rect x="3" y="7" width="26" height="18" rx="2" fill="#0a3d62"/>
|
||||||
|
<path d="M4 9l12 9 12-9" stroke="#ff6a00" stroke-width="2.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 235 B |
0
packages/vitec/Resources/Public/Icons/vitec-plugin-customerlogos.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 367 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||||
|
<rect x="3" y="6" width="26" height="17" rx="2" fill="#0a3d62"/>
|
||||||
|
<path d="M13 11l8 4.5-8 4.5z" fill="#ff6a00"/>
|
||||||
|
<rect x="10" y="25" width="12" height="2.5" rx="1" fill="#9aa2ab"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||||
|
<path d="M16 4a10 10 0 0 0-10 10v6a3 3 0 0 0 3 3h2v-8H8v-1a8 8 0 1 1 16 0v1h-3v8h2a3 3 0 0 0 3-3v-6A10 10 0 0 0 16 4z" fill="#0a3d62"/>
|
||||||
|
<path d="M21 23v1.5a2.5 2.5 0 0 1-2.5 2.5H15" stroke="#ff6a00" stroke-width="2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
0
packages/vitec/Resources/Public/Icons/vitec-plugin-locationlist.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 226 B After Width: | Height: | Size: 226 B |
0
packages/vitec/Resources/Public/Icons/vitec-plugin-modelcard.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 359 B |
BIN
packages/vitec/Resources/Public/Images/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/vitec/Resources/Public/Images/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -118,14 +118,70 @@ class VitecContainerCollapse {
|
|||||||
if (this.isCollapsed(uid)) {
|
if (this.isCollapsed(uid)) {
|
||||||
ce.classList.add('vitec-ce-collapsed');
|
ce.classList.add('vitec-ce-collapsed');
|
||||||
}
|
}
|
||||||
|
this.syncPreviewHeader(ce);
|
||||||
|
|
||||||
button.addEventListener('click', (event) => {
|
button.addEventListener('click', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const collapsed = ce.classList.toggle('vitec-ce-collapsed');
|
const collapsed = ce.classList.toggle('vitec-ce-collapsed');
|
||||||
this.setCollapsed(uid, collapsed);
|
this.setCollapsed(uid, collapsed);
|
||||||
|
this.syncPreviewHeader(ce);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep the element's own `.element-preview-header` visible while collapsed:
|
||||||
|
* it is moved into the CE header row (after `.t3-page-ce-header-right`) and
|
||||||
|
* moved back to its original spot (placeholder marker) on expand.
|
||||||
|
*/
|
||||||
|
syncPreviewHeader(ce) {
|
||||||
|
const element = ce.querySelector(':scope > .t3-page-ce-element');
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const header = element.querySelector(':scope > .t3-page-ce-header');
|
||||||
|
const body = element.querySelector(':scope > .t3-page-ce-body');
|
||||||
|
if (!header || !body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapsed = ce.classList.contains('vitec-ce-collapsed');
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
// Find THIS element's preview header (not one of a nested child CE).
|
||||||
|
let preview = null;
|
||||||
|
for (const candidate of body.querySelectorAll('.element-preview-header')) {
|
||||||
|
if (candidate.closest('.t3-page-ce') === ce) {
|
||||||
|
preview = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!preview || header.contains(preview)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const placeholder = document.createElement('span');
|
||||||
|
placeholder.className = 'vitec-preview-header-placeholder';
|
||||||
|
placeholder.hidden = true;
|
||||||
|
preview.before(placeholder);
|
||||||
|
preview.classList.add('vitec-preview-header--in-header');
|
||||||
|
const headerRight = header.querySelector('.t3-page-ce-header-right');
|
||||||
|
if (headerRight) {
|
||||||
|
headerRight.insertAdjacentElement('afterend', preview);
|
||||||
|
} else {
|
||||||
|
header.appendChild(preview);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const preview = header.querySelector('.element-preview-header.vitec-preview-header--in-header');
|
||||||
|
const placeholder = body.querySelector('.vitec-preview-header-placeholder');
|
||||||
|
if (preview && placeholder) {
|
||||||
|
preview.classList.remove('vitec-preview-header--in-header');
|
||||||
|
placeholder.replaceWith(preview);
|
||||||
|
} else if (preview) {
|
||||||
|
preview.classList.remove('vitec-preview-header--in-header');
|
||||||
|
body.prepend(preview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new VitecContainerCollapse();
|
export default new VitecContainerCollapse();
|
||||||
|
|||||||
@@ -193,4 +193,38 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1750000000] = [
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ExtensionUtility::configurePlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Contactform',
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
ExtensionUtility::configurePlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Demoform',
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
ExtensionUtility::configurePlugin(
|
||||||
|
'Vitec',
|
||||||
|
'Helpdeskform',
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
\Evomedien\Vitec\Controller\FormController::class => 'list'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -281,3 +281,11 @@ CREATE TABLE tx_vitec_domain_model_customer (
|
|||||||
logo int(11) unsigned DEFAULT '0' NOT NULL,
|
logo int(11) unsigned DEFAULT '0' NOT NULL,
|
||||||
emphasize_logo smallint(5) unsigned DEFAULT '0' NOT NULL
|
emphasize_logo smallint(5) unsigned DEFAULT '0' NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tx_vitec_form_submission (
|
||||||
|
form_key varchar(30) DEFAULT '' NOT NULL,
|
||||||
|
payload text,
|
||||||
|
delivery_method varchar(20) DEFAULT '' NOT NULL,
|
||||||
|
delivery_status varchar(20) DEFAULT '' NOT NULL,
|
||||||
|
delivery_error text
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteBase /_frontend/
|
RewriteBase /_frontend/
|
||||||
|
|
||||||
RewriteRule ^index\.html$ - [L]
|
RewriteRule ^index\.html$ - [L]
|
||||||
|
|
||||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||||
RewriteCond %{REQUEST_FILENAME} -d
|
RewriteCond %{REQUEST_FILENAME} -d
|
||||||
RewriteRule ^ - [L]
|
RewriteRule ^ - [L]
|
||||||
|
|
||||||
RewriteRule ^ index.html [L]
|
RewriteRule ^ index.html [L]
|
||||||
|
|
||||||
# ── Cache headers ────────────────────────────────────────────────────────────
|
# ── Cache headers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# index.html: never cache — always fetch fresh so the browser picks up
|
# index.html: never cache — always fetch fresh so the browser picks up
|
||||||
# the latest hashed JS/CSS filenames after a deploy.
|
# the latest hashed JS/CSS filenames after a deploy.
|
||||||
<Files "index.html">
|
<Files "index.html">
|
||||||
Header set Cache-Control "no-store, no-cache, must-revalidate"
|
Header set Cache-Control "no-store, no-cache, must-revalidate"
|
||||||
Header set Pragma "no-cache"
|
Header set Pragma "no-cache"
|
||||||
Header set Expires "0"
|
Header set Expires "0"
|
||||||
</Files>
|
</Files>
|
||||||
|
|
||||||
# Hashed assets (JS, CSS, fonts, images inside /assets/):
|
# Hashed assets (JS, CSS, fonts, images inside /assets/):
|
||||||
# Vite appends a content hash to every filename, so these are immutable.
|
# Vite appends a content hash to every filename, so these are immutable.
|
||||||
# Cache them for 1 year — a new deploy produces new filenames automatically.
|
# Cache them for 1 year — a new deploy produces new filenames automatically.
|
||||||
<FilesMatch "\.(js|css|woff2?|ttf|eot|otf)$">
|
<FilesMatch "\.(js|css|woff2?|ttf|eot|otf)$">
|
||||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|||||||
@@ -21,12 +21,12 @@
|
|||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
<meta name="twitter:title" content="VITEC" />
|
<meta name="twitter:title" content="VITEC" />
|
||||||
<meta name="twitter:description" content="Enterprise IPTV, video streaming, and digital signage solutions." />
|
<meta name="twitter:description" content="Enterprise IPTV, video streaming, and digital signage solutions." />
|
||||||
<link rel="icon" type="image/svg+xml" href="/_frontend/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="../fileadmin/icons/favicon.ico" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<title>VITEC</title>
|
<title>VITEC</title>
|
||||||
<script type="module" crossorigin src="/_frontend/assets/index-CM0zIqrs.js"></script>
|
<script type="module" crossorigin src="/_frontend/assets/index-DPL77F5X.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-B6-rfpgz.css">
|
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-DNSm10MQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="page"></div>
|
<div id="page"></div>
|
||||||
|
|||||||