Skip to content
Snippets Groups Projects
Commit fddf413e authored by Stefan Galinski's avatar Stefan Galinski :video_game:
Browse files

Merge branch 'task_bootstrap5' into 'master'

Bootstrap 5 Support

See merge request !50
parents f8312771 afbcdfdf
No related branches found
No related tags found
1 merge request!50Bootstrap 5 Support
Showing
with 1181 additions and 2 deletions
<?php
/***************************************************************
* Copyright notice
*
* (c) sgalinski Internet Services (https://www.sgalinski.de)
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
namespace SGalinski\SgJobs\ViewHelpers;
use Exception;
use SimpleXMLElement;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* Class RenderSvgViewHelper
*/
class RenderSvgViewHelper extends AbstractTagBasedViewHelper {
/**
* Name of the tag to be created by this view helper
*
* @var string
* @api
*/
protected $tagName = 'svg';
/**
* @var array
*/
protected static array $svgInstances = [];
/**
* The directory path where the SVGs are located
*
* @var string
*/
protected string $directoryPath = __DIR__ . '/../../Resources/Public/Images/Svg/';
/**
* Register the ViewHelper arguments
*/
public function initializeArguments(): void {
parent::initializeArguments();
$this->registerArgument(
'name', 'string', 'The SVG name, also supports folders (e.g. fontawesome/solid/ad)', TRUE
);
$this->registerArgument(
'color',
'string',
'The color',
FALSE,
'currentColor'
);
$this->registerArgument(
'colorAttribute',
'string',
'The color target to affect: fill, stroke. Leave empty for both'
);
$this->registerArgument('id', 'string', 'The HTML id attribute');
$this->registerArgument('path', 'string', 'Path to the SVG file if not in default');
$this->registerArgument('class', 'string', 'The HTML class attribute');
$this->registerArgument('title', 'string', 'The HTML title attribute');
$this->registerArgument('width', 'string', 'The HTML width attribute');
$this->registerArgument('height', 'string', 'The HTML height attribute');
$this->registerArgument('viewBox', 'string', 'The HTML viewBox attribute');
$this->registerArgument(
'viewBoxOnly',
'boolean',
'If only the viewBox attribute should be used instead of width/height'
);
$this->registerArgument('stroke-width', 'string', 'The HTML stroke-width attribute');
$this->registerArgument('style', 'string', 'Inline CSS styles');
$this->registerArgument('use', 'string', 'Inline CSS styles');
$this->registerArgument(
'createNewInstance',
'boolean',
'Creates a new instance without reusing other ones'
);
$this->registerArgument('preserveColors', 'boolean', 'Preserves the original colors');
$this->registerArgument(
'createColorAttribute', 'boolean', 'Creates the color target for cases when it doesn\'t exist.', FALSE,
FALSE
);
}
/**
* Render the SVG file as an inline SVG element.
*
* @return string The rendered SVG element
* @throws Exception
*/
public function render(): string {
$name = $this->arguments['name'];
$path = $this->arguments['path'];
$width = $this->arguments['width'];
$height = $this->arguments['height'];
$viewBox = $this->arguments['viewBox'];
$viewBoxOnly = $this->arguments['viewBoxOnly'];
$strokeWidth = $this->arguments['stroke-width'];
$color = $this->arguments['color'];
$colorAttribute = $this->arguments['colorAttribute'];
$id = $this->arguments['id'];
$class = $this->arguments['class'];
$style = $this->arguments['style'];
$title = $this->arguments['title'];
$createNewInstance = $this->arguments['createNewInstance'];
$preserveColors = $this->arguments['preserveColors'];
$createColorAttribute = $this->arguments['createColorAttribute'];
$src = $path ?? $this->directoryPath . '/' . $name . '.svg';
// Get the content of the SVG file
$content = file_get_contents($src);
// Create a unique ID for the SVG element
if (!$id) {
$id = 'svg-' . md5($src . $width . $height . $color);
}
// Load the SVG into a SimpleXMLElement object
$svg = new SimpleXMLElement($content);
// Set the attributes of the SVG element
if ($width > 0) {
$this->addOrReplaceAttribute($svg, 'width', $width);
}
if ($height > 0) {
$this->addOrReplaceAttribute($svg, 'height', $height);
}
if (!empty($viewBox)) {
$this->addOrReplaceAttribute($svg, 'viewBox', $viewBox);
}
if ($viewBoxOnly) {
unset($svg->attributes()->width, $svg->attributes()->height);
}
if ($strokeWidth > 0) {
$this->setStrokeWidth($svg, $strokeWidth);
}
if (!empty($color) && !$preserveColors) {
switch ($colorAttribute) {
case 'fill':
$this->setFill($svg, $color, $createColorAttribute);
break;
case 'stroke':
$this->setStroke($svg, $color, $createColorAttribute);
break;
default:
$this->setFill($svg, $color, $createColorAttribute);
$this->setStroke($svg, $color, $createColorAttribute);
}
}
if ($style) {
$this->addOrReplaceAttribute($svg, 'style', $style);
}
if ($class) {
$this->addOrReplaceAttribute($svg, 'class', $class);
}
if ($title) {
$this->addOrReplaceAttribute($svg, 'title', $title);
}
if ($createNewInstance) {
return $svg->asXML();
}
// Extract the SVG contents
$contents = $this->getContents($svg, TRUE);
// Check if the SVG has already been rendered and use the <use> tag if possible
if (isset(static::$svgInstances[$id])) {
$use = $svg->addChild('use');
// The boolean conversion of SimpleXMLElement is broken, therefore we MUST use instanceof
if ($use instanceof SimpleXMLElement) {
$use->addAttribute('href', '#' . $id);
}
return $svg->asXML();
}
// Add the unique ID to the list of rendered SVGs
static::$svgInstances[$id] = $id;
$contentsElement = new SimpleXMLElement($contents);
$group = $svg->addChild('g');
// The boolean conversion of SimpleXMLElement is broken, therefore we MUST explicitly check against null
if ($group === NULL) {
return $svg->asXML();
}
$group->addAttribute('id', $id);
$this->xmlAdopt($group, $contentsElement);
return $svg->asXML();
}
/**
* Set the fill color of an SVG element.
*
* @param SimpleXMLElement $element The SVG element
* @param string $fill The fill color to set
* @param boolean $createColorAttribute If the attribute should be added
*/
protected function setFill(SimpleXMLElement $element, string $fill, bool $createColorAttribute): void {
foreach ($element->children() as $child) {
$this->setFill($child, $fill, $createColorAttribute);
}
if ($createColorAttribute) {
$element->addAttribute('fill', $fill);
} else if (isset($element->attributes()->fill)) {
$element->attributes()->fill = $fill;
}
}
/**
* Set the stroke color of an SVG element.
*
* @param SimpleXMLElement $element The SVG element
* @param string $stroke The stroke color to set
* @param boolean $createColorAttribute If the attribute should be added
*/
protected function setStroke(SimpleXMLElement $element, string $stroke, bool $createColorAttribute): void {
foreach ($element->children() as $child) {
$this->setStroke($child, $stroke, $createColorAttribute);
}
if ($createColorAttribute) {
$element->addAttribute('stroke', $stroke);
} else if (isset($element->attributes()->stroke)) {
$element->attributes()->stroke = $stroke;
}
}
/**
* Set the stroke width of an SVG element.
*
* @param SimpleXMLElement $element The SVG element
* @param string $strokeWidth The stroke width to set
*/
protected function setStrokeWidth(SimpleXMLElement $element, string $strokeWidth): void {
foreach ($element->children() as $child) {
$this->setStrokeWidth($child, $strokeWidth);
}
foreach ($element->path as $path) {
$this->addOrReplaceAttribute($path, 'stroke-width', $strokeWidth);
}
}
/**
* Gets the contents of the SVG file
*
* @param SimpleXMLElement $svg
* @param bool $removeNode
* @return string
*/
private function getContents(SimpleXMLElement $svg, bool $removeNode = FALSE): string {
$contents = '';
foreach ($svg->children() as $child) {
$contents .= $child->asXML() . "\n";
if ($removeNode) {
$dom = dom_import_simplexml($child);
$dom->parentNode->removeChild($dom);
}
}
return $contents;
}
/**
* Inserts a new SimpleXMLElement at the given root
*
* @param SimpleXMLElement $root
* @param SimpleXMLElement $newElement
* @return void
*/
private function xmlAdopt(SimpleXMLElement $root, SimpleXMLElement $newElement): void {
$node = $root->addChild($newElement->getName(), (string) $newElement);
if ($node === NULL) {
return;
}
foreach ($newElement->attributes() as $attr => $value) {
$node->addAttribute($attr, $value);
}
foreach ($newElement->children() as $ch) {
$this->xmlAdopt($node, $ch);
}
}
/**
* Adds or replaces an attribute
*
* @param SimpleXMLElement $element
* @param string $attributeName
* @param string $attributeValue
* @return void
*/
private function addOrReplaceAttribute(
SimpleXMLElement $element,
string $attributeName,
string $attributeValue
): void {
if (isset($element[$attributeName])) {
// Replace existing attribute value
unset($element[$attributeName]);
}
$element->addAttribute($attributeName, $attributeValue);
}
}
......@@ -52,3 +52,9 @@ config.recordLinks {
}
}
}
[{$plugin.tx_project_theme.config.bootstrapVersion} == 5]
plugin.tx_sgjobs.view.templateRootPaths.10 = EXT:sg_jobs/Resources/Private/Templates/Bootstrap5/
plugin.tx_sgjobs.view.partialRootPaths.10 = EXT:sg_jobs/Resources/Private/Partials/Bootstrap5/
plugin.tx_sgjobs.view.layoutRootPaths.10 = EXT:sg_jobs/Resources/Private/Layouts/Bootstrap5/
[end]
......@@ -462,6 +462,10 @@
<source><![CDATA[Interested?<br>– Apply now!]]></source>
<target><![CDATA[Interesse geweckt?<br>– Dann bewirb dich jetzt!]]></target>
</trans-unit>
<trans-unit id="frontend.overview" resname="frontend.overview" approved="yes">
<source><![CDATA[Overview]]></source>
<target><![CDATA[Überblick]]></target>
</trans-unit>
<trans-unit id="frontend.jobDetailsCta" resname="frontend.jobDetailsCta" approved="yes">
<source><![CDATA[To the details]]></source>
<target><![CDATA[Zu den Details]]></target>
......@@ -516,4 +520,4 @@
</trans-unit>
</body>
</file>
</xliff>
\ No newline at end of file
</xliff>
......@@ -372,6 +372,9 @@
<trans-unit id="frontend.jobApplyNow" resname="frontend.jobApplyNow">
<source><![CDATA[Interested?<br>– Apply now!]]></source>
</trans-unit>
<trans-unit id="frontend.overview" resname="frontend.overview">
<source><![CDATA[Overview]]></source>
</trans-unit>
<trans-unit id="frontend.jobDetailsCta" resname="frontend.jobDetailsCta">
<source><![CDATA[To the details]]></source>
</trans-unit>
......@@ -413,4 +416,4 @@
</trans-unit>
</body>
</file>
</xliff>
\ No newline at end of file
</xliff>
<f:render section="main" />
<f:form id="sgjobs-filter" class="text-bg-light p-6 rounded shadow-small vstack gap-8 mb-2" action="index" controller="Joblist" method="get"
objectName="filters" object="{filters}" hiddenFieldClassName="d-none">
<f:form.hidden value="{limit}" name="filter-limit"/>
<f:form.hidden id="filter-recordPageId" name="recordPageIds" value="{recordPageIds}"/>
<div class="row default-content-element">
<div class="col-md-3 col-sm-6 col-12">
<label for="filter-countries" class="form-label">
<f:translate key="frontend.filter.countries"/>
</label>
<f:form.select
class="form-select"
size="1"
value="{selectedCountry}"
property="filterCountry"
optionValueField="value"
options="{countries}"
prependOptionLabel="{f:translate(key: 'frontend.filter.selections.all')}"
prependOptionValue="0"
id="filter-countries"/>
</div>
<div class="col-md-3 col-sm-6 col-12">
<label for="filter-locations" class="form-label">
<f:translate key="frontend.filter.locations"/>
</label>
<f:form.select
class="form-select"
size="1"
value="{selectedLocation}"
property="filterLocation"
optionValueField="value"
options="{cities}"
prependOptionLabel="{f:translate(key: 'frontend.filter.selections.all')}"
prependOptionValue="0"
id="filter-locations"/>
</div>
<div class="col-md-3 col-sm-6 col-12">
<label for="filter-departments" class="form-label">
<f:translate key="frontend.filter.departments"/>
</label>
<f:form.select
class="form-select"
size="1"
value="{selectedDepartment}"
property="filterDepartment"
optionLabelField="title"
optionValueField="uid"
options="{departments}"
prependOptionLabel="{f:translate(key: 'frontend.filter.selections.all')}"
prependOptionValue="0"
id="filter-departments"/>
</div>
<div class="col-md-3 col-sm-6 col-12">
<label for="filter-experienceLevels" class="form-label">
<f:translate key="frontend.filter.experienceLevels"/>
</label>
<f:form.select
class="form-select"
size="1"
value="{selectedExperienceLevel}"
property="filterExperienceLevel"
optionLabelField="title"
optionValueField="uid"
options="{experienceLevels}"
prependOptionLabel="{f:translate(key: 'frontend.filter.selections.all')}"
prependOptionValue="0"
id="filter-experienceLevels"/>
</div>
</div>
<div class="row default-content-element">
<div class="d-flex flex-column flex-sm-row justify-content-sm-between align-items-sm-end gap-4">
<div class="form-check">
<f:form.checkbox
class="form-check-input"
property="filterRemote"
id="filter-remote"
value="remote"
checked="{selectedRemote}"/>
<label class="form-check-label" for="filter-remote">
<f:translate key="frontend.filter.remote"/>
</label>
</div>
<f:form.submit class="btn btn-md btn-primary" value="{f:translate(key:'frontend.filter.submit')}"/>
</div>
</div>
</f:form>
<div class="col-md-6 col-sm-6 col-12">
<div class="card shadow-small border-top h-100">
<div class="card-header text-center">
<h2 class="card-title h4">{job.title}</h2>
</div>
<div class="card-body">
<f:if condition="{job.description}">
<f:format.html parseFuncTSPath="lib.parseFunc_RTE">{job.description}</f:format.html>
</f:if>
<f:if condition="{job.qualification}">
<ul>
<li>
<f:format.raw>
<f:translate key="frontend.jobStart" />
</f:format.raw>
<f:if condition="{job.alternativeStartDate}">
<f:then>
{job.alternativeStartDate}
</f:then>
<f:else>
<f:format.date date="{job.startDate}" format="d.m.Y" />
</f:else>
</f:if>
</li>
<li>
<f:for each="{job.company}" as="company" iteration="iterator">
{company.city}{f:if(condition: '!{iterator.isLast} || {iterator.isLast} &&
{job.telecommutePossible} ', then: ', ')}
</f:for>
<f:if condition="{job.telecommutePossible}">
<f:translate key="frontend.remote" />
</f:if>
</li>
</ul>
</f:if>
</div>
<div class="card-footer bg-default text-center">
<f:link.action id="offer-{job.uid}" pageUid="{settings.applyPage}" controller="Joblist" action="applyForm"
pluginName="JobApplication" arguments="{jobId: job.uid}"
class="btn btn-link text-decoration-none stretched-link w-100 text-uppercase fw-bold">
<f:format.raw>
<f:translate key="frontend.jobDetailsCta" />
</f:format.raw>
</f:link.action>
</div>
</div>
</div>
\ No newline at end of file
<div class="col-12 col-sm">
<div class="default-content-element">
<div class="card shadow-small border-top">
<div class="card-header text-center">
<h2 class="card-title h4">{job.title}</h2>
</div>
<div class="card-body">
<f:if condition="{job.description}">
<f:format.html parseFuncTSPath="lib.parseFunc_RTE">{job.description}</f:format.html>
</f:if>
<f:if condition="{job.qualification}">
<div class="sgjobs-highlight-area">
<ul>
<li>
<f:format.raw>
<f:translate key="frontend.jobStart" />
</f:format.raw>
<f:if condition="{job.alternativeStartDate}">
<f:then>
{job.alternativeStartDate}
</f:then>
<f:else>
<f:format.date date="{job.startDate}" format="d.m.Y" />
</f:else>
</f:if>
</li>
<f:if condition="{job.companies} || {job.telecommutePossible}">
<li>
<f:for each="{job.companies}" as="company" iteration="iterator">
{company.city}{f:if(condition: '!{iterator.isLast} || {iterator.isLast} &&
{job.telecommutePossible} ', then: ', ')}
</f:for>
<f:if condition="{job.telecommutePossible}">
<f:translate key="frontend.remote" />
</f:if>
</li>
</f:if>
</ul>
</div>
</f:if>
</div>
<div class="card-footer bg-default text-center">
<f:link.action class="sg-jobs-job card shadow-small" id="offer-{job.uid}" pageUid="{applyPageUid}"
controller="Joblist" action="applyForm" pluginName="JobApplication" arguments="{jobId: job.uid}"
class="btn btn-link text-decoration-none stretched-link w-100 text-uppercase fw-bold">
<f:format.raw>
<f:translate key="frontend.jobDetailsCta" />
</f:format.raw>
</f:link.action>
</div>
</div>
</div>
</div>
\ No newline at end of file
{namespace h=SGalinski\SgJobs\ViewHelpers}
{namespace base=SGalinski\ProjectBase\ViewHelpers}
{namespace sgajax=SGalinski\SgAjax\ViewHelpers}
<f:layout name="Default" />
<f:section name="main">
<f:asset.script identifier="dropzone-js" crossorigin="anonymous" src="EXT:sg_jobs/Resources/Public/node_modules/dropzone/dist/dropzone-min.js" priority="1" />
<f:asset.css identifier="dropzone-css" crossorigin="anonymous" href="EXT:sg_jobs/Resources/Public/node_modules/dropzone/dist/dropzone.css" priority="1" />
<f:if condition="{job}">
<f:render partial="ApplyFormSchema" arguments="{_all}" />
</f:if>
<div class="default-content-element">
<h2>
<f:if condition="{job}">
<f:then>
<f:translate key="frontend.apply.applyAs" />
<span>{job.title}</span>
</f:then>
<f:else>
<f:translate key="frontend.apply.unsolicitedApplication" />
</f:else>
</f:if>
</h2>
</div>
<f:if condition="{job}">
<div class="default-content-element">
<div class="row sgjobs-description">
<input id="maxFileSize" type="hidden" data-maxFileSize="{maxFileSize}" />
<input id="maxFileSizeMessage" type="hidden" data-maxFileSizeMessage="{maxFileSizeMessage}" />
<div class="col-md-8 col-sm-6 col-12 vstack gap-8">
<div class="mb-last-child-0"><f:format.html parseFuncTSPath="lib.parseFunc_RTE">{job.task}</f:format.html></div>
<div class="mb-last-child-0">
<h3 class="h4">
<f:translate key="frontend.qualification" />
</h3>
<f:format.html parseFuncTSPath="lib.parseFunc_RTE">{job.qualification}</f:format.html>
</div>
</div>
<div class="col-md-4 col-sm-6 col-12 vstack gap-8">
<div class="card shadow-small border-top text-bg-light">
<div class="card-header">
<h3 class="card-title h4">
<f:format.raw>
<f:translate key="frontend.overview" />
</f:format.raw>
</h3>
</div>
<div class="card-body">
<ul>
<li>
<f:format.raw>
<f:translate key="frontend.jobStart" />
</f:format.raw>
<f:if condition="{job.alternativeStartDate}">
<f:then>
{job.alternativeStartDate}
</f:then>
<f:else>
<f:format.date date="{job.startDate}" format="d.m.Y" />
</f:else>
</f:if>
</li>
<li>
<f:if condition="!{job.telecommutePossible}">
<f:then>
<f:format.raw>
<f:translate key="frontend.locationLabel" />
</f:format.raw><br>
<f:for as="company" each="{job.companies}">
<span class="sgjobs-company-location-wrap">
{company.name}<br>
{company.street}<br>
<f:if condition="{company.state}">
{company.state}<br>
</f:if>
{company.zip} {company.city}<br>
</span>
</f:for>
</f:then>
<f:else>
<f:format.raw>
<f:translate key="frontend.jobLocationRemote" />
</f:format.raw>
</f:else>
</f:if>
</li>
<f:if condition="{job.experienceLevel -> f:count()} > 0">
<li>
<strong>
<f:translate key="frontend.experienceLevel" />:
</strong>
<f:for each="{job.experienceLevel}" as="experienceLevel" iteration="iterator">
{experienceLevel.title}{f:if(condition: '!{iterator.isLast}', then: ', ')}
</f:for>
</li>
</f:if>
</ul>
</div>
</div>
<div class="card shadow-small border-top text-bg-light">
<div class="card-header">
<h3 class="card-title h4">
<f:format.raw>
<f:translate key="frontend.jobApplyNow" />
</f:format.raw>
</h3>
</div>
<div class="card-body">
<f:if condition="!{job.hideApplyByPostal}">
<p>
<f:format.raw>
<f:translate key="frontend.job.via.post" />
</f:format.raw><br>
{job.firstCompany.name}<br>
<f:if condition="{job.contact}">
<f:then>
{job.contact.title} {job.contact.firstName} {job.contact.lastName}<br>
</f:then>
<f:else>
{job.firstCompany.contact.title} {job.firstCompany.contact.firstName} {job.firstCompany.contact.lastName}<br>
</f:else>
</f:if>
<f:if condition="{job.contact} && {job.contact.street}">
<f:then>
{job.contact.street}<br>
<f:if condition="{job.contact.state}">
{job.contact.state}<br>
</f:if>
{job.contact.zip} {job.contact.city}
<f:if condition="{job.contact.country}">
<br>{job.contact.country}
</f:if>
</f:then>
<f:else>
<f:if condition="{job.firstCompany.contact} && {job.firstCompany.contact.street}">
<f:then>
{job.firstCompany.contact.street}<br>
<f:if condition="{job.firstCompany.contact.state}">
{job.firstCompany.contact.state}<br>
</f:if>
{job.firstCompany.contact.zip} {job.firstCompany.contact.city}
<f:if condition="{job.firstCompany.contact.country}">
<br>{job.firstCompany.contact.country}
</f:if>
</f:then>
<f:else>
{job.firstCompany.street}<br>
<f:if condition="{job.firstCompany.state}">
{job.firstCompany.state}<br>
</f:if>
{job.firstCompany.zip} {job.firstCompany.city}
<f:if condition="{job.firstCompany.country}">
<br>{job.firstCompany.country}
</f:if>
</f:else>
</f:if>
</f:else>
</f:if>
</p>
</f:if>
<f:if condition="!{job.hideApplyByEmail}">
<p>
<f:format.raw>
<f:translate key="frontend.job.via.email" />
</f:format.raw>
<br>
<f:format.html parseFuncTSPath="lib.parseFunc">
<a href="mailto:{f:if(condition: '{job.contact}', then: '{job.contact.email}', else: '{job.company.contact.email}')}">
<f:translate key="frontend.emailContact" />
</a>
</f:format.html>
</p>
</f:if>
</div>
<div class="card-footer card-footer--static-bg text-center text-bg-light">
<p>
<f:format.raw>
<f:translate key="frontend.job.suggestForm" />
</f:format.raw>
</p>
<a href="{f:if(condition: '{job.applyExternalLink}', then: '{job.applyExternalLink}', else: '#apply')}" class="btn btn-default w-100">
<f:translate key="frontend.applyNow" />
</a>
</div>
</div>
<f:if condition="{job.contact}">
<f:then>
<f:render section="contactBox" arguments="{contact: job.contact, hideApplyByEmail: job.hideApplyByEmail}" />
</f:then>
<f:else>
<f:render section="contactBox" arguments="{contact: job.company.contact, hideApplyByEmail: job.hideApplyByEmail}" />
</f:else>
</f:if>
<f:if condition="{job.attachment}">
<f:link.typolink target="_blank" class="btn btn-info d-flex w-100" parameter="{job.attachment.0.originalResource.publicUrl}">
<h:renderSvg name="fontawesome/regular/file-pdf" width="17" height="19" color="currentColor" createColorAttribute="TRUE" />
<f:translate key="frontend.attachment" extensionName="SgJobs" />
<span class="visually-hidden">(PDF, {job.attachment.0.originalResource.size -> f:format.bytes()})</span>
</f:link.typolink>
</f:if>
</div>
</div>
</div>
</f:if>
<f:if condition="!{job.applyExternalLink}">
<div class="default-content-element">
<div class="row">
<f:if condition="{job}">
<h2>
<f:translate key="frontend.apply.applyAsNow" arguments="{0: '{job.title}'}" />
</h2>
</f:if>
<f:form action="apply" class="vstack gap-8 needs-validation" novalidate="novalidate" id="apply" controller="Joblist" method="post" name="applyData" object="{applyData}" enctype="multipart/form-data" hiddenFieldClassName="d-none">
<f:if condition="{job}">
<f:then>
<f:form.hidden property="job" value="{job.uid}" />
<f:form.hidden property="jobId" value="{job.jobId}" />
<f:form.hidden property="jobTitle" value="{job.title}" />
</f:then>
</f:if>
<input type="hidden" name="tx_sgjobs_jobapplication[folderName]" value="{folderName}" />
<f:if condition="{internalError}">
<ul class="sg-jobs-validation-error parsley-errors-list filled">
<li class="parsley-required">
<f:translate key="frontend.apply.error.general" />
: {internalError}
</li>
</ul>
</f:if>
<div class="row">
<f:if condition="!{job}">
<div class="col">
<f:render section="formLabel" arguments="{label-for: 'company', label-text: 'company'}" />
<f:form.select property="company" multiple="0" size="1" id="apply-company" class="form-select" options="{companies}" optionLabelField="city" optionValueField="uid" prependOptionLabel="{f:translate(key:'frontend.apply.country.empty')}" required="required" />
<f:render section="formValidation" arguments="{form-field: 'company'}" />
</div>
</f:if>
<div class="col">
<f:render section="formLabel" arguments="{label-for: 'gender', label-text: 'gender'}" />
<f:form.select property="gender" id="apply-gender" class="form-select" options="{male: '{f:translate(key: \'frontend.apply.gender.male\')}', female: '{f:translate(key: \'frontend.apply.gender.female\')}', other: '{f:translate(key: \'frontend.apply.gender.other\')}'}" />
<f:render section="formValidation" arguments="{form-field: 'gender'}" />
</div>
</div>
<div class="row">
<f:render section="formTextField" arguments="{wrapper-class: 'col', field-id: 'firstName', field-text: 'first_name', required: 'required'}" />
<f:render section="formTextField" arguments="{wrapper-class: 'col', field-id: 'lastName', field-text: 'last_name', required: 'required'}" />
</div>
<div class="row">
<f:render section="formTextField" arguments="{wrapper-class: 'col col-sm-6', field-id: 'street', field-text: 'street', required: 'required'}" />
<f:render section="formTextField" arguments="{wrapper-class: 'col-6 col-sm-4', field-id: 'city', field-text: 'city', required: 'required'}" />
<f:render section="formTextField" arguments="{wrapper-class: 'col-6 col-sm-2', field-id: 'zip', field-text: 'zip', required: 'required'}" />
</div>
<div class="row">
<div class="col">
<f:render section="formLabel" arguments="{label-for: 'country', label-text: 'country'}" />
<f:form.countrySelect value="DE" property="country" id="apply-country" class="form-select" required="required" />
<f:render section="formValidation" arguments="{form-field: 'county'}" />
</div>
<div class="col">
<f:render section="formLabel" arguments="{label-for: 'nationality', label-text: 'nationality'}" />
<f:form.countrySelect value="DE" property="nationality" id="apply-nationality" class="form-select" required="required" />
<f:render section="formValidation" arguments="{form-field: 'nationality'}" />
</div>
</div>
<div class="row">
<f:render section="formTextField" arguments="{wrapper-class: 'col', field-id: 'education', field-text: 'education', required: 'required'}" />
<div class="col">
<f:render section="formLabel" arguments="{label-for: 'birthDate', label-text: 'birthDate'}" />
<f:form.textfield type="date" property="birthDate" id="apply-birthDate" class="form-control" placeholder="{f:translate(key:'frontend.apply.birthDate')}" required="required" />
<f:render section="formValidation" arguments="{form-field: 'birthDate'}" />
</div>
</div>
<div class="row">
<f:render section="formTextField" arguments="{wrapper-class: 'col', field-id: 'phone', field-text: 'phone', required: 'required'}" />
<f:render section="formTextField" arguments="{wrapper-class: 'col', field-id: 'mobile', field-text: 'mobile'}" />
</div>
<div class="row">
<div class="col-6">
<f:render section="formLabel" arguments="{label-for: 'email', label-text: 'email'}" />
<f:form.textfield type="email" property="email" id="apply-email" class="form-control" placeholder="{f:translate(key:'frontend.apply.email')}" required="required" />
<f:render section="formValidation" arguments="{form-field: 'email'}" />
</div>
</div>
<div class="row">
<div class="col">
<label for="apply-cover-letter" class="form-label filled">
<f:translate key="frontend.apply.cover_letter" />
(
<f:translate key="frontend.apply.allowed_file_extensions" />
{allowedFileExtensions})
</label>
<div class="coverLetter-upload jobs-upload rounded" data-max-file-amount="1" data-valid-file-extensions="{settings.fileUpload.fileTypes}" data-max-file-size="{maxFileSize}" data-pid="{storagePid}" data-inner-text="{f:translate(key: 'frontend.DropFiles', extensionName: 'sg_jobs')}" data-cancel-upload="{f:translate(key: 'frontend.CancelUpload', extensionName: 'sg_jobs')}" data-remove-file="{f:translate(key: 'frontend.RemoveFile', extensionName: 'sg_jobs')}" data-file-type-error="{f:translate(key: 'frontend.FileType', extensionName: 'sg_jobs')}" data-upload-ajax="{sgajax:uri.ajax(extensionName: 'SgJobs', controller: 'Ajax\Upload', action: 'uploadCoverletter', format: 'json', parameters: '{pageId: storagePid}')}">
</div>
<f:if condition="{coverLetter.name}">
<p class="help-block">
Aktuell: {coverLetter.name}
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][name]" value="{coverLetter.name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][type]" value="{coverLetter.type}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][tmp_name]" value="{coverLetter.tmp_name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][error]" value="0" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][coverLetter][submittedFile][size]" value="{coverLetter.size}" />
</p>
</f:if>
<f:render section="formValidation" arguments="{form-field: 'coverLetter'}" />
</div>
</div>
<div class="row">
<div class="col">
<label for="apply-cv" class="form-label filled">
<f:translate key="frontend.apply.cv" />
(
<f:translate key="frontend.apply.allowed_file_extensions" />
{allowedFileExtensions})
</label>
<div class="cv-upload jobs-upload rounded" data-max-file-amount="1" data-valid-file-extensions="{settings.fileUpload.fileTypes}" data-max-file-size="{maxFileSize}" data-pid="{storagePid}" data-inner-text="{f:translate(key: 'frontend.DropFiles', extensionName: 'sg_jobs')}" data-cancel-upload="{f:translate(key: 'frontend.CancelUpload', extensionName: 'sg_jobs')}" data-remove-file="{f:translate(key: 'frontend.RemoveFile', extensionName: 'sg_jobs')}" data-file-type-error="{f:translate(key: 'frontend.FileType', extensionName: 'sg_jobs')}" data-upload-ajax="{sgajax:uri.ajax(extensionName: 'SgJobs', controller: 'Ajax\Upload', action: 'uploadCv', format: 'json', parameters: '{pageId: storagePid}')}">
</div>
<f:if condition="{cv.name}">
<p class="help-block">
Aktuell: {cv.name}
<f:comment>
<!-- Important, due to a fluid cache issue with the fluid syntax-->
</f:comment>
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][name]" value="{cv.name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][type]" value="{cv.type}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][tmp_name]" value="{cv.tmp_name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][error]" value="0" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][cv][submittedFile][size]" value="{cv.size}" />
</p>
</f:if>
<f:render section="formValidation" arguments="{form-field: 'cv'}" />
</div>
</div>
<div class="row">
<div class="col">
<label for="apply-certificate" class="form-label filled">
<f:translate key="frontend.apply.certificate" />
(
<f:translate key="frontend.apply.allowed_file_extensions" />
{allowedFileExtensions})
</label>
<div class="certificate-upload jobs-upload rounded" data-max-file-amount="1" data-valid-file-extensions="{settings.fileUpload.fileTypes}" data-max-file-size="{maxFileSize}" data-pid="{storagePid}" data-inner-text="{f:translate(key: 'frontend.DropFiles', extensionName: 'sg_jobs')}" data-cancel-upload="{f:translate(key: 'frontend.CancelUpload', extensionName: 'sg_jobs')}" data-remove-file="{f:translate(key: 'frontend.RemoveFile', extensionName: 'sg_jobs')}" data-file-type-error="{f:translate(key: 'frontend.FileType', extensionName: 'sg_jobs')}" data-upload-ajax="{sgajax:uri.ajax(extensionName: 'SgJobs', controller: 'Ajax\Upload', action: 'uploadCv', format: 'json', parameters: '{pageId: storagePid}')}">
</div>
<f:if condition="{certificate.name}">
<p class="help-block">
Aktuell: {certificate.name}
<f:comment>
<!-- Important, due to a fluid cache issue with the fluid syntax-->
</f:comment>
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][name]" value="{certificate.name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][type]" value="{certificate.type}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][tmp_name]" value="{certificate.tmp_name}" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][error]" value="0" />
<input type="hidden" name="tx_sgjobs_jobapplication[applyData][certificate][submittedFile][size]" value="{certificate.size}" />
</p>
</f:if>
<f:render section="formValidation" arguments="{form-field: 'certificate'}" />
</div>
</div>
<div class="row mb-4">
<div class="col">
<f:render section="formLabel" arguments="{label-for: 'message', label-text: 'message'}" />
<f:form.textarea class="form-control" rows="10" property="message" id="apply-message" placeholder="{f:translate(key:'frontend.apply.message')}" />
</div>
</div>
<div class="form-check mb-4">
<f:form.checkbox class="form-check-input" id="privacy-policy" property="privacyPolicy" value="1" additionalAttributes="{required: 'required'}" />
<label class="form-check-label" for="privacy-policy">
<f:format.raw>
<f:translate key="frontend.apply.privacyPolicy" arguments="{0: '{f:render(section:\'privacyPolicyCheckboxLink\')}'}" />
</f:format.raw>
</label>
<f:render section="formValidation" arguments="{form-field: 'privacyPolicy'}" />
</div>
<div class="d-flex justify-content-end pt-4">
<button type="submit" class="btn btn-lg btn-primary">{f:translate(key:'frontend.applyNow')}</button>
</div>
</f:form>
</div>
</div>
</f:if>
<f:if condition="{job.relatedJobs}">
<f:then>
<div class="default-content-element">
<div class="row">
<div class="col">
<h2>
<f:translate key="frontend.apply.relatedJobs" />
</h2>
<div class="row default-content-element equal-height-columns stretch-first-child">
<f:for each="{job.relatedJobs}" as="relatedJob">
<f:render partial="Teaser" arguments="{job: relatedJob, applyPageUid: settings.applyPage}" />
</f:for>
</div>
</div>
</div>
</div>
</f:then>
<f:else>
<f:if condition="{settings.enableAutomaticRelatedJobs} && {relatedJobs}">
<div class="default-content-element">
<div class="row">
<div class="col">
<h2>
<f:translate key="frontend.apply.relatedJobs" />
</h2>
<div class="row default-content-element equal-height-columns stretch-first-child">
<f:for each="{relatedJobs}" as="relatedJob">
<f:render partial="Teaser" arguments="{job: relatedJob, applyPageUid: settings.applyPage}" />
</f:for>
</div>
</div>
</div>
</div>
</f:if>
</f:else>
</f:if>
</f:section>
<f:section name="privacyPolicyCheckboxLink">
<f:link.typolink target="_blank" parameter="{settings.privacyPolicyPage}">
<f:translate key="frontend.apply.privacyPolicy.link" />
</f:link.typolink>
</f:section>
<f:section name="contactBox">
<div class="card shadow-small border-top text-bg-light">
<div class="card-header">
<h3 class="card-title h4">
<f:translate key="frontend.apply.contact" />
</h3>
</div>
<div class="card-body">
<p class="mb-0 fw-bolder">
{contact.title} {contact.firstName} {contact.lastName}
</p>
<f:if condition="{contact.phone}">
<p class="mb-0">{contact.phone}</p>
</f:if>
<f:if condition="{contact.email} && !{hideApplyByEmail}">
<p class="mb-0">
<f:comment>
<!-- Spam Protection (lib.parseFunc encodes adresses) -->
</f:comment>
<f:format.html parseFuncTSPath="lib.parseFunc">
<a href="mailto:{contact.email}">
<f:translate key="frontend.emailContact" />
</a>
</f:format.html>
</p>
</f:if>
<f:if condition="{contact.image}">
<f:image image="{contact.image}" maxWidth="100" maxHeight="100" alt="{contact.title} {contact.firstName} {contact.lastName}" />
</f:if>
</div>
<div class="card-footer card-footer--static-bg text-bg-light">
<p class="fw-bolder text-center">
<f:translate key="frontend.apply.recommend" />
</p>
<base:sharer />
</div>
</div>
</f:section>
<f:section name="formLabel">
<label for="apply-{label-for}" class="form-label">
<f:translate key="frontend.apply.{label-text}" />
</label>
</f:section>
<f:section name="formTextField">
<div class="{wrapper-class}">
<label for="apply-{field-id}" class="form-label">
<f:translate key="frontend.apply.{field-text}" />
</label>
<f:form.textfield property="{field-id}" id="apply-{field-id}" class="form-control" placeholder="{f:translate(key:'frontend.apply.{field-text}')}" required="{required}" />
<f:render section="formValidation" arguments="{form-field: '{field-id}'}" />
</div>
</f:section>
<f:section name="formValidation">
<f:form.validationResults for="applyData.{form-field}">
<ul class="text-danger mb-0">
<f:for each="{validationResults.errors}" as="error">
<li class="pt-2">{error.message}</li>
</f:for>
</ul>
</f:form.validationResults>
</f:section>
{namespace sg=SGalinski\SgJobs\ViewHelpers}
<f:layout name="Default" />
<f:section name="main">
<f:render partial="JoblistListSchema" arguments="{_all}" />
<div id="sgjobs-joblist" class="default-content-element vstack gap-8">
<f:render
partial="Filter"
arguments="{recordPageId: recordPageId, filters: filters, countries: countries, cities: cities, companies: companies, departments: departments, experienceLevels: experienceLevels,
functions: functions, selectedCountry: selectedCountry, selectedCompany: selectedCompany,
selectedLocation: selectedLocation, selectedDepartment: selectedDepartment,
selectedExperienceLevel: selectedExperienceLevel, selectedFunction: selectedFunction, selectedRemote: selectedRemote,
limit: limit }"
/>
<div class="row default-content-element equal-height-columns stretch-first-child">
<f:for each="{jobs}" as="job">
<f:render partial="Job" arguments="{job: job}"/>
</f:for>
</div>
<div id="sgjobs-pagination">
<f:format.raw><sg:pageBrowser numberOfPages="{numberOfPages}" /></f:format.raw>
</div>
</div>
</f:section>
{namespace sg=SGalinski\SgJobs\ViewHelpers}
<f:layout name="PageBrowser" />
<f:section name="main">
<f:if condition="{numberOfPages} > 1">
<nav aria-label="Jobs pagination">
<ul class="pagination mb-0">
<li class="page-item {f:if(condition: '!{prevPageExist}', then: 'disabled')}">
<f:variable name="prevPage" value="{currentPage - 1}" />
<f:if condition="{prevPage} == 0">
<f:then>
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}"
additionalAttributes="{aria-label: 'Previous'}">
&laquo;
</f:link.action>
</f:then>
<f:else>
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}"
additionalParams="{tx_sgjobs_pagebrowser: {currentPage: prevPage}}"
additionalAttributes="{aria-label: 'Previous'}">
&laquo;
</f:link.action>
</f:else>
</f:if>
</li>
<f:if condition="{enableLessPages} && {showLessPages}">
<li class="page-item">
<f:variable name="lessPage" value="{currentPage - 2}" />
<f:if condition="{lessPage} == 0">
<f:then>
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}">
...
</f:link.action>
</f:then>
<f:else>
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}"
additionalParams="{tx_sgjobs_pagebrowser: {currentPage: lessPage}}">
...
</f:link.action>
</f:else>
</f:if>
</li>
</f:if>
<f:for each="{pageLinks}" as="pageLink">
<f:if condition="{pageLink.isCurrentPage}">
<f:then>
<li class="page-item active">
<a href="#" class="page-link">
{pageLink.number}
</a>
</li>
</f:then>
<f:else>
<li class="tx-pagebrowse-page">
<f:if condition="{pageLink.page} == 0">
<f:then>
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}">
{pageLink.number}
</f:link.action>
</f:then>
<f:else>
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}"
additionalParams="{tx_sgjobs_pagebrowser: {currentPage: pageLink.page}}">
{pageLink.number}
</f:link.action>
</f:else>
</f:if>
</li>
</f:else>
</f:if>
</f:for>
<f:if condition="{enableMorePages} && {showNextPages}">
<li class="page-item">
<f:variable name="morePage" value="{currentPage + 2}" />
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}"
additionalParams="{tx_sgjobs_pagebrowser: {currentPage: morePage}}">
...
</f:link.action>
</li>
</f:if>
<li class="page-item {f:if(condition: '!{nextPageExist}', then: 'disabled')}">
<f:variable name="nextPage" value="{currentPage + 1}" />
<f:link.action addQueryString="untrusted" class="page-link"
argumentsToBeExcludedFromQueryString="{0: 'tx_sgjobs_pagebrowser'}"
additionalParams="{tx_sgjobs_pagebrowser: {currentPage: nextPage}}"
additionalAttributes="{aria-label: 'Next'}">
&raquo;
</f:link.action>
</li>
</ul>
</nav>
</f:if>
</f:section>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M64 464l48 0 0 48-48 0c-35.3 0-64-28.7-64-64L0 64C0 28.7 28.7 0 64 0L229.5 0c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3L384 304l-48 0 0-144-80 0c-17.7 0-32-14.3-32-32l0-80L64 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16zM176 352l32 0c30.9 0 56 25.1 56 56s-25.1 56-56 56l-16 0 0 32c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-48 0-80c0-8.8 7.2-16 16-16zm32 80c13.3 0 24-10.7 24-24s-10.7-24-24-24l-16 0 0 48 16 0zm96-80l32 0c26.5 0 48 21.5 48 48l0 64c0 26.5-21.5 48-48 48l-32 0c-8.8 0-16-7.2-16-16l0-128c0-8.8 7.2-16 16-16zm32 128c8.8 0 16-7.2 16-16l0-64c0-8.8-7.2-16-16-16l-16 0 0 96 16 0zm80-112c0-8.8 7.2-16 16-16l48 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0 0 32 32 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-32 0 0 48c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-64 0-64z"/></svg>
\ No newline at end of file
form.was-validated .dropzone {
border-color: var(--b-form-invalid-border-color);
&.dz-started {
border-color: var(--b-form-valid-border-color);
}
}
.dropzone {
border-radius: var(--b-border-radius);
border-style: dashed;
.dz-message {
display: grid;
&::before {
content: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='36' width='22' viewBox='0 0 384 512'><path d='M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM216 408c0 13.3-10.7 24-24 24s-24-10.7-24-24V305.9l-31 31c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l72-72c9.4-9.4 24.6-9.4 33.9 0l72 72c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-31-31V408z'/></svg>");
}
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment