WIP Migrate to stimulus.js and tailwind

This commit is contained in:
Björn Fromme
2021-08-19 19:05:47 +02:00
parent 7f5f48921d
commit 018ddef53f
40 changed files with 676 additions and 481 deletions
@@ -68,16 +68,11 @@ class AjaxSearchbarController extends ActionController
* @param array $searchParams
* @return string
*/
public function updateAction(array $searchParams = null)
public function indexAction(array $searchParams)
{
$fixed = (bool)$this->settings['fixed'];
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
if (null === $searchParams) {
$searchParams = $this->searchResultService->getNormalizedSearchParams();
$filterSettings = $this->filterService->getNormalizedFilterSettings([], $forcedConceptUids);
} else {
$filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams);
}
$filterSettings = $this->filterService->getFilterSettingsFromSearchParams($searchParams);
$excludedConceptUids = GeneralUtility::trimExplode(',', $this->settings['excludedConceptUids']);
$excludedRegionUids = GeneralUtility::trimExplode(',', $this->settings['excludedRegionUids']);
$filterOptions = $this->filterService->getFilterOptions(
@@ -87,9 +82,11 @@ class AjaxSearchbarController extends ActionController
$forcedConceptUids
);
return json_encode([
$this->view->assignMultiple([
'filterSettings' => $filterSettings,
'filterOptions' => $filterOptions,
'searchParams' => $searchParams,
], JSON_THROW_ON_ERROR);
'fixed' => $fixed,
]);
}
}
@@ -41,8 +41,8 @@ class GroupsPriceController extends ActionController
});
$this->view->assign('hotel', $hotel);
$this->view->assign('configs', json_encode(array_values($configs)));
$this->view->assign('boards', json_encode($hotel->getGroupsPriceBoards()->toArray()));
$this->view->assign('options', json_encode($hotel->getGroupsPriceOptions()->toArray()));
$this->view->assign('configs', json_encode(array_values($configs), JSON_HEX_APOS));
$this->view->assign('boards', json_encode($hotel->getGroupsPriceBoards()->toArray(), JSON_HEX_APOS));
$this->view->assign('options', json_encode($hotel->getGroupsPriceOptions()->toArray(), JSON_HEX_APOS));
}
}
@@ -143,7 +143,7 @@ class GroupsPriceConfig extends AbstractEntity implements \JsonSerializable
/**
* @return float
*/
public function getPriceAdditionalPerson(): float
public function getPriceAdditionalPerson(): ?float
{
return $this->priceAdditionalPerson;
}
@@ -159,7 +159,7 @@ class GroupsPriceConfig extends AbstractEntity implements \JsonSerializable
/**
* @return float
*/
public function getPriceAdditionalPersonGroup1(): float
public function getPriceAdditionalPersonGroup1(): ?float
{
return $this->priceAdditionalPersonGroup1;
}
@@ -175,7 +175,7 @@ class GroupsPriceConfig extends AbstractEntity implements \JsonSerializable
/**
* @return float
*/
public function getPriceAdditionalPersonGroup2(): float
public function getPriceAdditionalPersonGroup2(): ?float
{
return $this->priceAdditionalPersonGroup2;
}
@@ -191,7 +191,7 @@ class GroupsPriceConfig extends AbstractEntity implements \JsonSerializable
/**
* @return float
*/
public function getPriceAdditionalPersonGroup3(): float
public function getPriceAdditionalPersonGroup3(): ?float
{
return $this->priceAdditionalPersonGroup3;
}
@@ -281,6 +281,7 @@ class FilterService implements SingletonInterface
'uid' => $index,
'min' => $range[0],
'max' => $range[1],
'formatted' => $this->formatPriceRange($range[0], $range[1]),
];
}
}
@@ -303,8 +304,8 @@ class FilterService implements SingletonInterface
$filterOptions = new FilterOptions();
$filterOptions->setPriceRanges($priceRanges);
$filterOptions->setDateFrom(new \DateTime($dateMin));
$filterOptions->setDateTo(new \DateTime($dateMax));
$filterOptions->setDateFrom($dateMin);
$filterOptions->setDateTo($dateMax);
$filterOptions->setConcepts($concepts);
$filterOptions->setDestinations($destinations);
$filterOptions->setHotelTypes($hotelTypes);
@@ -316,6 +317,18 @@ class FilterService implements SingletonInterface
return $filterOptions;
}
public function formatPriceRange(int $min, int $max): string
{
if ($min === 1) {
return sprintf('bis %d €', $max);
}
if ($max === 999) {
return sprintf('ab %d €', $min);
}
return sprintf('%d - %d €', $min, $max);
}
public function getFiltersettingsFromSettings(array $settings): array
{
$filterSettings = $this->getNormalizedFilterSettings();
@@ -256,17 +256,14 @@ class HotelDataService implements SingletonInterface
$hotel['dates'][] = $row['dateStart'];
}
$dateStart = new \DateTime($row['dateStart']);
$dateEnd = new \DateTime($row['dateEnd']);
if ($hotel['minDateStart'] === null
|| $hotel['minDateStart'] > $dateStart) {
$hotel['minDateStart'] = $dateStart;
|| $hotel['minDateStart'] > $row['dateStart']) {
$hotel['minDateStart'] = $row['dateStart'];
}
if ($hotel['maxDateEnd'] === null
|| $hotel['maxDateEnd'] < $dateEnd) {
$hotel['maxDateEnd'] = $dateEnd;
|| $hotel['maxDateEnd'] < $row['dateEnd']) {
$hotel['maxDateEnd'] = $row['dateEnd'];
}
if ($hotel['minPrice'] === null
@@ -157,7 +157,7 @@ tx_epproducts_ajax_json {
1 = dates
}
AjaxSearchbar {
1 = update
1 = index
}
AjaxTable {
1 = datestable
@@ -371,7 +371,7 @@ $iconRegistry->registerIcon(
[
'AjaxSearch' => 'searchresult',
'AjaxDate' => 'dates',
'AjaxSearchbar' => 'update',
'AjaxSearchbar' => 'index',
'AjaxContingent' => 'list,rooms',
'AjaxTable' => 'datestable,pricetable,pricetableHtml,eventPricetable',
'AjaxCalendar' => 'range,contingents,availableRooms',
@@ -381,7 +381,7 @@ $iconRegistry->registerIcon(
[
'AjaxSearch' => 'searchresult',
'AjaxDate' => 'dates',
'AjaxSearchbar' => 'update',
'AjaxSearchbar' => 'index',
'AjaxContingent' => 'list,rooms',
// 'AjaxTable' => 'datestable,pricetable,pricetableHtml,eventPricetable',
'AjaxCalendar' => 'range,contingents,availableRooms',
@@ -94,7 +94,7 @@ lib.stdheader.3.headerClass >
@import 'EXT:ep_theme/Configuration/TypoScript/Page/*'
config {
compressJs = 0
compressJs = 1
compressCss = 1
typolinkCheckRootline = 1
typolinkEnableLinksAcrossDomains = 1
@@ -3,7 +3,6 @@ require('cookieconsent')
require('lazysizes')
require('lazysizes/plugins/unveilhooks/ls.unveilhooks')
require('picturefill')
import Glightbox from 'glightbox'
// Polyfills
import smoothscroll from 'smoothscroll-polyfill'
@@ -11,26 +10,44 @@ smoothscroll.polyfill()
// Alpine stuff
import Alpine from 'alpinejs'
import parseAppConfig from './components/appconfig'
import appendPaCode from './components/pacode'
import daytripDateSelect from './components/daytrip-date-select'
import searchBar from './components/search-bar'
import calendar from './components/calendar'
import contactForm from './components/contact-form'
const appConfig = parseAppConfig()
appendPaCode(appConfig)
Alpine.store('appconfig', appConfig)
Alpine.data('daytripDateSelect', daytripDateSelect)
Alpine.data('searchBar', searchBar)
Alpine.data('calendar', calendar)
Alpine.data('contactForm', contactForm)
Alpine.start()
Glightbox({
selector: 'data-glightbox',
slideEffect: 'fade',
})
document.addEventListener('DOMContentLoaded', () => {
const configElement = document.getElementById('appconfig')
if (null === configElement) {
return {}
}
const appConfig = JSON.parse(configElement.innerHTML)
const paCode = appConfig.paCode
const externalDomains = appConfig.externalDomains
if (paCode) {
document.querySelectorAll('a').forEach(element => {
if (element.hostname === location.hostname) {
return
}
if (externalDomains.indexOf(element.hostname) === -1) {
return
}
let url = element.href
if (!url) {
return
}
if (url.indexOf('agenturcode=') !== -1) {
return
}
const separator = url.indexOf('?') === -1 ? '?' : '&'
url += separator + 'agenturcode=' + paCode
element.href = url
})
}
}, false)
import { Application } from 'stimulus'
import { definitionsFromContext } from 'stimulus/webpack-helpers'
@@ -1,5 +1,5 @@
<template>
<div class="form-wrapper form--border">
<div class="container">
<div class="form-group" v-show="!submitted">
<label>Zeitraum</label>
<input type="text" class="form-control datepicker--visible" placeholder="von... bis" ref="picker"/>
@@ -141,7 +141,6 @@
</template>
<script>
import $ from 'jquery';
import dayjs from 'dayjs'
import 'dayjs/locale/de'
import 'flatpickr';
@@ -150,6 +149,7 @@ import Vue from 'vue';
import axios from 'axios';
export default {
name: 'GroupsPriceCalculator',
props: {
loaderUri: {
type: String,
@@ -159,21 +159,24 @@ export default {
type: String,
required: true
},
configs: {
type: Array,
configsJson: {
type: String,
required: true
},
boards: {
type: Array,
boardsJson: {
type: String,
required: true
},
options: {
type: Array,
optionsJson: {
type: String,
required: true
}
},
data() {
return {
configs: JSON.parse(this.configsJson),
boards: JSON.parse(this.boardsJson),
options: JSON.parse(this.optionsJson),
processing: false,
submitted: false,
picker: null,
@@ -248,27 +251,26 @@ export default {
for (let option of this.selectedOptions) {
options.push(option.uid);
}
let formData = {
'tx_epproducts_ajax[groupsPriceInquiry][name]': this.name,
'tx_epproducts_ajax[groupsPriceInquiry][email]': this.email,
'tx_epproducts_ajax[groupsPriceInquiry][phone]': this.phone,
'tx_epproducts_ajax[groupsPriceInquiry][remarks]': this.remarks,
'tx_epproducts_ajax[groupsPriceInquiry][dateFrom]': this.selectedFrom.format('DD.MM.YYYY'),
'tx_epproducts_ajax[groupsPriceInquiry][dateTo]': this.selectedTo.format('DD.MM.YYYY'),
'tx_epproducts_ajax[groupsPriceInquiry][pax]': this.includesBonusCard ? '' : this.selectedPax,
'tx_epproducts_ajax[groupsPriceInquiry][paxGroup1]': this.includesBonusCard ? this.selectedPaxGroup1 : '',
'tx_epproducts_ajax[groupsPriceInquiry][paxGroup2]': this.includesBonusCard ? this.selectedPaxGroup2 : '',
'tx_epproducts_ajax[groupsPriceInquiry][paxGroup3]': this.includesBonusCard ? this.selectedPaxGroup3 : '',
'tx_epproducts_ajax[groupsPriceInquiry][board]': this.selectedBoard ? this.selectedBoard.uid : null,
'tx_epproducts_ajax[groupsPriceInquiry][options]': options,
'tx_epproducts_ajax[groupsPriceInquiry][summary]': this.priceSummary
};
let formData = new FormData()
formData.set('tx_epproducts_ajax[groupsPriceInquiry][name]', this.name)
formData.set('tx_epproducts_ajax[groupsPriceInquiry][email]', this.email)
formData.set('tx_epproducts_ajax[groupsPriceInquiry][phone]', this.phone)
formData.set('tx_epproducts_ajax[groupsPriceInquiry][remarks]', this.remarks)
formData.set('tx_epproducts_ajax[groupsPriceInquiry][dateFrom]', this.selectedFrom.format('DD.MM.YYYY'))
formData.set('tx_epproducts_ajax[groupsPriceInquiry][dateTo]', this.selectedTo.format('DD.MM.YYYY'))
formData.set('tx_epproducts_ajax[groupsPriceInquiry][pax]', this.includesBonusCard ? '' : this.selectedPax)
formData.set('tx_epproducts_ajax[groupsPriceInquiry][paxGroup1]', this.includesBonusCard ? this.selectedPaxGroup1 : '')
formData.set('tx_epproducts_ajax[groupsPriceInquiry][paxGroup2]', this.includesBonusCard ? this.selectedPaxGroup2 : '')
formData.set('tx_epproducts_ajax[groupsPriceInquiry][paxGroup3]', this.includesBonusCard ? this.selectedPaxGroup3 : '')
formData.set('tx_epproducts_ajax[groupsPriceInquiry][board]', this.selectedBoard ? this.selectedBoard.uid : null)
formData.set('tx_epproducts_ajax[groupsPriceInquiry][options]', options)
formData.set('tx_epproducts_ajax[groupsPriceInquiry][summary]', this.priceSummary)
this.processing = true;
this.submitted = false;
axios({
url: this.endpoint,
method: 'POST',
data: $.param(formData)
data: formData
}).then(response => {
if ('validation' === response.data.status) {
this.formErrors = response.data.errors;
@@ -403,7 +405,7 @@ export default {
},
mounted() {
Vue.nextTick(() => {
this.picker = $(this.$refs.picker).flatpickr({
this.picker = flatpickr(this.$refs.picker, {
mode: 'range',
dateFormat: 'd.m.Y',
locale: German,
@@ -1,7 +0,0 @@
export default () => {
const configElement = document.getElementById('appconfig')
if (null === configElement) {
return {}
}
return JSON.parse(configElement.innerHTML)
}
@@ -1,43 +0,0 @@
import axios from 'axios'
export default props => ({
loading: false,
selectedIndex: null,
selectableMonths: [],
calendarWrapper: null,
init() {
this.calendarWrapper = this.$refs.calendarWrapper
let months = []
let today = new Date()
let year = today.getFullYear()
let month = today.getMonth()
for (let i = 0; i < 20; i++) {
months.push(new Date(year, month, 1))
if (month < 12) {
month++
} else {
month = 1
year++
}
}
this.selectableMonths = months
this.selectedIndex = 0
this.load()
},
load() {
this.loading = true
let selectedMonth = this.selectableMonths[this.selectedIndex]
let data = new URLSearchParams({
'tx_epproducts_ajax[hotel]': props.hotel,
'tx_epproducts_ajax[month]': selectedMonth.getMonth() + 1,
'tx_epproducts_ajax[year]': selectedMonth.getFullYear(),
})
axios.post(props.url, data)
.then(response => (
this.calendarWrapper.innerHTML = response.data
))
.finally(() => {
this.loading = false
})
}
})
@@ -1,25 +0,0 @@
export default appConfig => {
const paCode = appConfig.paCode
const externalDomains = appConfig.externalDomains
if (!paCode) {
return
}
document.querySelectorAll('a').forEach(element => {
if (element.hostname === location.hostname) {
return
}
if (externalDomains.indexOf(element.hostname) === -1) {
return
}
let url = element.href
if (!url) {
return
}
if (url.indexOf('agenturcode=') !== -1) {
return
}
const separator = url.indexOf('?') === -1 ? '?' : '&'
url += separator + 'agenturcode=' + paCode
element.href = url
})
}
@@ -1,19 +0,0 @@
const debounce = (callback, interval) => {
let debounceId
return (...args) => {
clearTimeout(debounceId)
debounceId = setTimeout(() => callback.apply(this, args), interval)
}
}
const throttle = (callback, interval) => {
let enableCall = true
return (...args) => {
if (!enableCall) return
enableCall = false
callback.apply(this, args)
setTimeout(() => enableCall = true, interval)
}
}
export { debounce, throttle }
@@ -0,0 +1,68 @@
import { Controller } from 'stimulus'
import { useFetch } from '../mixins/use_fetch'
export default class extends Controller {
static targets = [
'container',
'field',
'loadingIndicator',
]
static values = {
loading: Boolean,
uri: String,
hotelUid: Number,
}
initialize() {
useFetch(this)
}
connect() {
let months = []
let today = new Date()
let year = today.getFullYear()
let month = today.getMonth()
for (let i = 0; i < 20; i++) {
let selectableDate = new Date(year, month, 1)
months.push(selectableDate)
if (month < 12) {
month++
} else {
month = 1
year++
}
this.fieldTarget.options[i] = new Option(selectableDate.toLocaleDateString('de-DE', {year: 'numeric', month: 'long'}), i.toString())
}
this.selectableMonths = months
this.load(0)
}
select(e) {
let selectedIndex = e.currentTarget.value
this.load(selectedIndex)
}
load(selectedIndex) {
this.loadingValue = true
let selectedMonth = this.selectableMonths[selectedIndex]
let data = new URLSearchParams()
data.set('tx_epproducts_ajax[hotel]', this.hotelUidValue)
data.set('tx_epproducts_ajax[month]', selectedMonth.getMonth() + 1)
data.set('tx_epproducts_ajax[year]', selectedMonth.getFullYear())
fetch(this.uriValue, { method: 'POST', body: data })
.then(this.checkStatus)
.then(this.parseHTML)
.then(html => (
this.containerTarget.innerHTML = html
))
.finally(() => {
this.loadingValue = false
})
}
loadingValueChanged() {
this.loadingIndicatorTarget.classList.toggle('hidden', this.loadingValue === false)
}
}
@@ -26,10 +26,21 @@ export default class extends Controller {
this.toggleByWindowWidth(width)
}
toggle() {
toggle(e) {
e.preventDefault()
this.collapsedValue = !this.collapsedValue
}
open(e) {
e.preventDefault()
this.collapsedValue = false
}
close(e) {
e.preventDefault()
this.collapsedValue = true
}
toggleByWindowWidth(width) {
if (0 !== this.untilValue) {
this.collapsedValue = width < this.untilValue
@@ -23,9 +23,12 @@ export default class extends Controller {
minDate: this.minDateValue,
onChange: selectedDates => {
if (selectedDates.length === 2) {
let dateFrom = flatpickr.formatDate(selectedDates[0], 'Y-m-d')
let dateTo = flatpickr.formatDate(selectedDates[1], 'Y-m-d')
this.dispatch('range-select', {
from: flatpickr.formatDate(selectedDates[0], 'Y-m-d'),
to: flatpickr.formatDate(selectedDates[1], 'Y-m-d'),
from: dateFrom,
to: dateTo,
label: `${dateFrom} - ${dateTo}`
})
}
}
@@ -43,14 +46,20 @@ export default class extends Controller {
selectPreset(e) {
e.preventDefault()
const preset = JSON.parse(e.currentTarget.dataset.preset)
this.picker.setDate([ new Date(preset.dateFrom), new Date(preset.dateTo) ])
this.dispatch('range-select', { from: preset.dateFrom, to: preset.dateTo })
let dateFrom = new Date(preset.dateFrom)
let dateTo = new Date(preset.dateTo)
this.picker.setDate([ dateFrom, dateTo ])
this.dispatch('range-select', {
from: preset.dateFrom,
to: preset.dateTo,
label: `${flatpickr.formatDate(dateFrom, 'd.m.Y')} - ${flatpickr.formatDate(dateTo, 'd.m.Y')}`
})
}
reset(e) {
e.preventDefault()
this.picker.clear()
this.picker.jumpToDate()
this.dispatch('range-select', { from: null, to: null })
this.dispatch('range-select', { from: null, to: null, label: null })
}
}
@@ -0,0 +1,43 @@
import { Controller } from 'stimulus'
import Glightbox from 'glightbox'
export default class extends Controller {
static targets = [ 'thumb' ]
static values = { url: String }
connect() {
let elements = []
if (this.hasUrlValue) {
let caption = this.getCaption(this.element)
let element = { href: this.urlValue, title: caption }
elements.push(element)
} else if (this.element.hasAttribute('href')) {
let caption = this.getCaption(this.element)
let element = { href: this.element.href, title: caption }
elements.push(element)
} else {
this.thumbTargets.forEach(thumb => {
let caption = this.getCaption(thumb)
elements.push({ href: thumb.href, title: caption })
})
}
this.lightbox = Glightbox({
elements
})
}
open(e) {
e.preventDefault()
let index = 1
if (e.currentTarget.dataset.lightboxIndex) {
index = e.currentTarget.dataset.lightboxIndex
}
this.lightbox.openAt(index - 1)
}
getCaption(element) {
return element.hasAttribute('data-lightbox-caption') ? element.dataset.lightboxCaption : null
}
}
@@ -88,6 +88,5 @@ export default class extends Controller {
modeValueChanged() {
this.datesTableContainerTarget.classList.toggle('hidden', this.modeValue === 'prices')
this.priceTableContainerTarget.classList.toggle('hidden', this.modeValue === 'dates')
this.element.scrollIntoView({ block: 'start', behavior: 'smooth' })
}
}
@@ -0,0 +1,56 @@
import { Controller } from 'stimulus'
import { useFetch } from '../mixins/use_fetch'
export default class extends Controller {
static targets = [
'container',
'form',
'destinationUid',
'destinationType',
'dateFrom',
'dateTo',
'nights',
'pax',
'priceRange',
'dateRangeLabel'
]
static values = {
uri: String,
loading: Boolean,
}
initialize() {
useFetch(this)
}
connect() {
}
load() {
this.loadingValue = true
let data = new URLSearchParams()
data.set('tx_epproducts_ajax[searchParams]', JSON.stringify({
dateFrom: this.dateFromTarget.value,
dateTo: this.dateToTarget.value,
}))
fetch(this.uriValue, { method: 'POST', body: data })
.then(this.checkStatus)
.then(this.parseHTML)
.then(html => {
this.containerTarget.innerHTML = html
})
.finally(() => {
this.loadingValue = false
})
}
setDateRange(e) {
this.dateFromTarget.value = e.detail.from
this.dateToTarget.value = e.detail.to
this.dateRangeLabelTarget.value = e.detail.label
this.load()
}
}
@@ -32,8 +32,12 @@ export default class extends Controller {
}
onListValueChanged() {
this.labelTarget.innerText = true === this.onListValue ? 'von der Merkliste entfernen' : 'auf die Merkliste setzen'
this.iconTarget.setAttribute('href', true === this.onListValue ? '#icon-minus-circle' : '#icon-plus-circle')
if (this.hasLabelTarget) {
this.labelTarget.innerText = true === this.onListValue ? 'von der Merkliste entfernen' : 'auf die Merkliste setzen'
}
if (this.hasIconTarget) {
this.iconTarget.setAttribute('href', true === this.onListValue ? '#icon-minus-circle' : '#icon-plus-circle')
}
if (this.hasAriaLabelTarget) {
this.ariaLabelTarget.setAttribute('aria-label', true === this.onListValue ? 'von der Merkliste entfernen' : 'auf die Merkliste setzen')
}
@@ -0,0 +1,10 @@
// Initialize vue app
import Vue from 'vue'
import vueCustomElement from 'vue-custom-element'
import 'document-register-element/build/document-register-element'
import GroupsPriceCalculator from './components/GroupsPriceCalculator'
Vue.use(vueCustomElement)
Vue.config.productionTip = false
Vue.customElement('groups-price-calculator', GroupsPriceCalculator)
@@ -1,4 +1,4 @@
// Initialize vue apps
// Initialize vue app
import Vue from 'vue'
import vueCustomElement from 'vue-custom-element'
import dayjs from 'dayjs'
@@ -117,11 +117,19 @@
</f:else>
</f:if>
</div>
<a href="#" class="teaserbox-watchlist" x-data="watchlistToggle('{item.key}')" x-bind="trigger">
<span data-microtip-position="top" x-bind:aria-label="label" role="tooltip">
<i class="fa fa-plus-circle" x-bind:class="icon"></i>
</span>
</a>
<button class="teaserbox-watchlist"
data-controller="watchlist-toggle"
data-watchlist-toggle-key-value="{item.key}"
data-action="watchlist-toggle#toggle">
<span data-microtip-position="top"
role="tooltip"
aria-label="auf die Merkliste setzen"
data-watchlist-toggle-target="ariaLabel">
<svg class="w-5 h-5 text-ep-secondary">
<use href="#icon-plus-circle" data-watchlist-toggle-target="icon"></use>
</svg>
</span>
</button>
</div>
</f:section>
@@ -25,6 +25,12 @@
<symbol id="icon-bed" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!-- Font Awesome Pro 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M176 256c44.11 0 80-35.89 80-80s-35.89-80-80-80-80 35.89-80 80 35.89 80 80 80zm352-128H304c-8.84 0-16 7.16-16 16v144H64V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v352c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16v-48h512v48c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16V240c0-61.86-50.14-112-112-112z"/></symbol>
<symbol id="icon-bus" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M488 128h-8V80c0-44.8-99.2-80-224-80S32 35.2 32 80v48h-8c-13.25 0-24 10.74-24 24v80c0 13.25 10.75 24 24 24h8v160c0 17.67 14.33 32 32 32v32c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-32h192v32c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32v-32h6.4c16 0 25.6-12.8 25.6-25.6V256h8c13.25 0 24-10.75 24-24v-80c0-13.26-10.75-24-24-24zM112 400c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32zm16-112c-17.67 0-32-14.33-32-32V128c0-17.67 14.33-32 32-32h256c17.67 0 32 14.33 32 32v128c0 17.67-14.33 32-32 32H128zm272 112c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32z"/></symbol>
<symbol id="icon-location-marker" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></symbol>
<symbol id="icon-spinner" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<g>
<path d="M109.25 55.5h-36l12-12a29.54 29.54 0 0 0-49.53 12H18.75A46.04 46.04 0 0 1 96.9 31.84l12.35-12.34v36zm-90.5 17h36l-12 12a29.54 29.54 0 0 0 49.53-12h16.97A46.04 46.04 0 0 1 31.1 96.16L18.74 108.5v-36z"></path>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="960ms" repeatCount="indefinite"></animateTransform>
</g>
</symbol>
<!-- Flags -->
<symbol id="icon-flag-at" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#fff" d="M640 480H0V0h640z"/><path fill="#ed2939" d="M640 480H0V320h640zm0-319.9H0V.1h640z"/></g></symbol>
<symbol id="icon-flag-ch" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#d52b1e" d="M0 0h640v480H0z"/><g fill="#fff"><path d="M170 195h300v90H170z"/><path d="M275 90h90v300h-90z"/></g></g></symbol>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

@@ -2,7 +2,8 @@
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="relative mb-8">
<div class="relative mb-8"
data-controller="lightbox">
<div class="absolute top-0 right-0 mt-4 mr-4 flex space-x-2 text-white">
<svg class="w-8 h-8 md:w-10 h-10">
<use href="#icon-search"></use>
@@ -12,7 +13,8 @@
</span>
</div>
<f:alias map="{image: images.0}">
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}" data-glightbox="gallery-{id}">
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
data-action="lightbox#open">
<img data-src="{f:uri.image(image: image, width: '770', height: '430x+100')}"
class="block w-full h-auto lazyload"
alt="{image.alternative}"
@@ -25,8 +27,10 @@
<f:for each="{images}" as="image" iteration="iteration">
<f:if condition="{iteration.cycle} <= 5">
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
data-glightbox data-gallery="gallery-{id}"
data-caption="{image.originalResource.description}"
data-action="lightbox#open"
data-lightbox-target="thumb"
data-lightbox-index="{iteration.cycle}"
data-lightbox-caption="{image.originalResource.description}"
class="block">
<f:image image="{image}" width="150" height="85c+100" class="block w-full h-auto"
alt="{image.originalResource.alternative}" title="{image.originalResource.title}"/>
@@ -37,35 +41,15 @@
</f:if>
</f:if>
<div class="hidden">
<f:if condition="{thumbnails}">
<f:then>
<f:if condition="{images -> f:count()} == 1">
<f:then>
<f:alias map="{image: images.0}">
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
data-glightbox data-gallery="gallery-{id}" data-caption="{image.originalResource.description}"
title="{image.title}">1</a>
</f:alias>
</f:then>
<f:else>
<f:for each="{images}" as="image" iteration="iteration">
<f:if condition="{iteration.cycle} > 5">
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
data-glightbox data-gallery="gallery-{id}" data-caption="{image.originalResource.description}"
title="{image.originalResource.title}">{iteration.index}</a>
</f:if>
</f:for>
</f:else>
</f:if>
</f:then>
<f:else>
<f:for each="{images}" as="image">
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
data-glightbox data-gallery="gallery-{id}" data-caption="{image.originalResource.description}"
title="{image.originalResource.title}">{iteration.index}</a>
</f:for>
</f:else>
</f:if>
<f:for each="{images}" as="image" iteration="iteration">
<f:if condition="{iteration.cycle} > 5">
<a href="{f:uri.image(image: image, cropVariant: 'zoom', maxWidth: '2560')}"
data-lightbox-target="thumb"
data-lightbox-index="{iteration.cycle}"
data-lightbox-caption="{image.originalResource.description}"
title="{image.originalResource.title}">{iteration.index}</a>
</f:if>
</f:for>
</div>
</div>
@@ -5,10 +5,11 @@
{product.conceptDescription -> f:format.html()}
</div>
<f:if condition="{product.calendarHotel} && {hotel.groupsPriceConfigs}">
<a class="button button--full" data-glightbox
href="{f:uri.action(action: 'index', controller: 'GroupsPrice', arguments: '{hotel: hotel}', pageUid: settings.groupsPricePopupPageUid)}">
<button class="button w-full"
data-controller="lightbox"
data-lightbox-url-value="{f:uri.action(action: 'index', controller: 'GroupsPrice', arguments: '{hotel: hotel}', pageUid: settings.groupsPricePopupPageUid)}">
Preisberechnung
</a>
</button>
</f:if>
<div class="headerbar">
Verpflegung
@@ -4,7 +4,9 @@
Webcam
</p>
<a href="{region.webcam.url}"
data-glightbox data-caption="Webcam {region.name}"
data-controller="lightbox"
data-action="lightbox#open"
data-lightbox-caption="Webcam {region.name}"
title="Webcam {region.name}">
<img src="{ep:uri.externalImage(url: region.webcam.url)}"
width="330"
@@ -1,11 +1,15 @@
<f:if condition="{region.regionMaps}">
<div class="bg-gray-200 p-4 mb-4">
<div class="bg-gray-200 p-4 mb-4"
data-controller="lightbox">
<p class="font-bold">
Pistenplan
</p>
<f:for each="{region.regionMaps}" as="regionMap">
<f:for each="{region.regionMaps}" as="regionMap" iteration="iteration">
<a href="{f:uri.image(src: '{regionMap.uid}', treatIdAsReference: '1', maxWidth: '2560')}"
data-glightbox data-gallery="regionmap-{region.uid}" data-caption="Pistenplan {region.name}"
data-action="lightbox#open"
data-lightbox-target="thumb"
data-lightbox-index="{iteration.cycle}"
data-lightbox-caption="Pistenplan {region.name}"
title="Pistenplan {region.name}">
<img class="img-responsive lazyload"
data-src="{f:uri.image(image: regionMap, width: '330')}"
@@ -0,0 +1,212 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="searchbar {f:if(condition: fixed, then: ' sticky')}"
data-searchbar-target="container"
{f:if(condition: fixed, else: ' data-controller="sticky" data-sticky-offset-value="650" data-sticky-sticky-class="sticky"')}>
<f:form action="processSearch" controller="Search" method="post" name="searchParams"
object="{searchParams}" pluginName="searchresult" extensionName="epproducts"
pageUid="{settings.defaultSearchPageUid}" data="{'searchbar-target': 'form'}">
<f:form.hidden property="destinationUid" data="{'searchbar-target': 'destinationUid'}"/>
<f:form.hidden property="destinationType" data="{'searchbar-target': 'destinationType'}"/>
<f:form.hidden property="dateFrom" data="{'searchbar-target': 'dateFrom'}"/>
<f:form.hidden property="dateTo" data="{'searchbar-target': 'dateTo'}"/>
<f:form.hidden property="nights" data="{'searchbar-target': 'nights'}"/>
<f:form.hidden property="pax" data="{'searchbar-target': 'pax'}"/>
<f:form.hidden property="priceRange" data="{'searchbar-target': 'priceRange'}"/>
<div class="sticky:max-w-screen-xl sticky:mx-auto flex flex-col sticky:flex-row sticky:items-center">
<div class="flex items-start justify-between sticky:flex-1 pt-8 px-8 sticky:py-4 sticky:px-0">
<div class="flex-1"
data-controller="collapse"
data-collapse-collapsed-value="true">
<div class="relative border-b border-r border-white mr-8">
<input type="text"
data-action="click->collapse#toggle"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
placeholder="Zeitraum"
data-searchbar-target="dateRangeLabel"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1">
<use href="#icon-calendar"></use>
</svg>
<div class="hidden absolute z-20 top-100 mt-2 left-0 bg-white p-4 shadow-md"
data-collapse-target="container"
data-controller="datepicker"
<f:if condition="{filterSettings.dateFrom} && {filterSettings.dateTo}">
data-datepicker-date-from-value="{filterSettings.dateFrom}"
data-datepicker-date-to-value="{filterSettings.dateTo}"
</f:if>
data-datepicker-min-date-value="{filterOptions.dateFrom}"
data-datepicker-max-date-value="{filterOptions.dateTo}">
<f:if condition="{settings.datePickerPresets}">
<div class="flex items-center justify-between pb-2">
<f:for each="{settings.datePickerPresets}" as="preset">
<button class="daterange__preset"
data-preset='{preset -> f:format.json()}'
data-action="datepicker#selectPreset">
<span>
{preset.label}
</span>
</button>
</f:for>
</div>
</f:if>
<input type="hidden"
data-datepicker-target="field">
<div class="flex items-center justify-between pt-2">
<button class="daterange__button focus:outline-none"
data-action="collapse#close">
schließen
</button>
<button class="daterange__button focus:outline-none"
data-action="datepicker#reset">
zurücksetzen
</button>
</div>
</div>
</div>
</div>
<div class="w-24"
data-controller="collapse"
data-collapse-collapsed-value="true">
<div class="relative border-b border-r border-white mr-8">
<input type="text"
data-action="click->collapse#toggle"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1">
<use href="#icon-user-group"></use>
</svg>
<div class="hidden absolute top-100 mt-2 left-0 bg-white p-4 shadow-md w-full h-48 overflow-y-scroll"
data-collapse-target="container">
<ul class="m-0 p-0">
<f:for each="{filterOptions.pax}" as="option">
<li>
<a href="#" data-option-value="{option}">
{option}
</a>
</li>
</f:for>
<li>
<a href="{f:uri.typolink(parameter: settings.groupsPageUid)}">
&gt; 20
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="flex-1"
data-controller="collapse"
data-collapse-collapsed-value="true">
<div class="relative border-b border-r border-white mr-8">
<input type="text"
data-action="click->collapse#toggle"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
placeholder="Reiseziel"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1 transform rotate-45">
<use href="#icon-airplane"></use>
</svg>
<div class="hidden absolute top-100 left-0 bg-white p-4 shadow-md min-w-full h-48 overflow-y-scroll"
data-collapse-target="container">
<f:for each="{filterOptions.destinations}" as="destination">
<div class="flex items-start justify-between">
<div class="w-1/2">
<a href="#">
<svg class="block w-8 h-6">
<use href="#icon-flag-{destination.country.code -> f:format.case(mode: 'lower')}"/>
</svg>
<strong>
{destination.country.label}
</strong>
</a>
</div>
<div class="w-1/2">
<ul class="plain-list mb-0">
<f:for each="{destination.regions}" as="region">
<li>
<a href="#">
{region.label}
</a>
</li>
</f:for>
</ul>
</div>
</div>
</f:for>
<div class="flex items-start justify-between">
<div class="w-1/2">
<strong>Konzept</strong>
</div>
<div class="w-1/2">
<ul class="plain-list mb-0">
<f:for each="{filterOptions.concepts}" as="concept">
<li>
<a href="#">
{concept.name}
</a>
</li>
</f:for>
</ul>
</div>
</div>
<f:if condition="{filterSettings.destinationUid} || {filterSettings.conceptUid}">
<button>
zurücksetzen
</button>
</f:if>
</div>
</div>
</div>
<div class="w-32"
data-controller="collapse"
data-collapse-collapsed-value="true">
<div class="relative border-r border-b border-white">
<input type="text"
data-action="click->collapse#toggle"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
placeholder="Preis"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1">
<use href="#icon-euro"></use>
</svg>
<div class="hidden absolute top-100 left-0 bg-white p-4 shadow-md w-full h-48 overflow-y-scroll"
data-collapse-target="container">
<ul class="m-0 p-0">
<li>
<a href="#">
beliebig
</a>
</li>
<f:for each="{filterOptions.priceRanges}" as="range" iteration="iteration">
<li>
<a href="#">
{range.formatted}
</a>
</li>
</f:for>
</ul>
</div>
</div>
</div>
</div>
<button type="submit" class="hidden sticky:block sticky:ml-4">
<svg class="w-8 h-8 text-white">
<use href="#icon-chevron-right"></use>
</svg>
</button>
<div class="flex justify-end mt-4 sticky:hidden">
<button type="submit" class="w-1/3 text-white uppercase py-2">
Detailsuche
</button>
<button type="submit" class="w-1/3 text-white bg-ep-primary-light uppercase py-2">
Alle anzeigen
</button>
</div>
</div>
</f:form>
</div>
</html>
@@ -28,8 +28,8 @@
data="{'search-target': 'dateTo'}"/>
<div data-controller="datepicker"
<f:if condition="{filterSettings.dateFrom} && {filterSettings.dateTo}">
data-datepicker-date-from-value="{filterSettings.dateFrom -> f:format.date(format: 'Y-m-d')}"
data-datepicker-date-to-value="{filterSettings.dateTo -> f:format.date(format: 'Y-m-d')}"
data-datepicker-date-from-value="{filterSettings.dateFrom}"
data-datepicker-date-to-value="{filterSettings.dateTo}"
</f:if>
data-datepicker-min-date-value="{f:format.date(date: 'now', format: 'Y-m-d')}">
<f:if condition="{settings.datePickerPresets}">
@@ -150,8 +150,8 @@
<select name="pax"
data-action="search#filter">
<f:for each="{filterOptions.pax}" as="option">
<option value="<f:format.raw>{option}</f:format.raw>">
<f:format.raw>{option}</f:format.raw>
<option value="{option}">
{option}
</option>
</f:for>
</select>
@@ -0,0 +1,11 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<f:layout name="Default"/>
<f:section name="Main">
<f:render partial="Search/Bar" arguments="{_all}"/>
</f:section>
</html>
@@ -121,8 +121,8 @@
</f:if>
<hr>
<span class="searchresult-item__label">
{f:if(condition: item.daytrip, then: 'Tageweise buchbar', else: '{ep:nightsOptions(options: item.nights, daytrip: item.daytrip)}')}
</span>
{f:if(condition: item.daytrip, then: 'Tageweise buchbar', else: '{ep:nightsOptions(options: item.nights, daytrip: item.daytrip)}')}
</span>
</div>
<div class="teaserbox__aside__bottom">
<span class="searchresult-item__price">ab {item.minPrice} €</span>
@@ -136,20 +136,20 @@
Details{f:if(condition: item.hideBookingButton, else: ' &amp; Buchen')}
</f:link.action>
</div>
<button class="teaserbox-watchlist"
data-controller="watchlist-toggle"
data-watchlist-toggle-key-value="{item.key}"
data-action="watchlist-toggle#toggle">
<span data-microtip-position="top"
role="tooltip"
aria-label="auf die Merkliste setzen"
data-watchlist-toggle-target="ariaLabel">
<svg class="w-5 h-5 text-ep-secondary">
<use href="#icon-plus-circle" data-watchlist-toggle-target="icon"></use>
</svg>
</span>
</button>
</div>
<button class="teaserbox-watchlist"
data-controller="watchlist-toggle"
data-watchlist-toggle-key-value="{item.key}"
data-action="watchlist-toggle#toggle">
<span data-microtip-position="top"
role="tooltip"
aria-label="auf die Merkliste setzen"
data-watchlist-toggle-target="label">
<svg class="w-5 h-5 text-ep-secondary">
<use href="#icon-plus-circle" data-watchlist-toggle-target="icon"></use>
</svg>
</span>
</button>
</div>
</f:for>
</div>
@@ -7,17 +7,21 @@
<f:section name="Main">
<div class="container">
<div class="calendar calandar--single"
x-data='calendar("<f:format.raw>{ep:uri.ajax(action: 'range', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, arguments: '{hotel: data.tx_eptheme_context_hotel}', format: 'html')}</f:format.raw>")'>
<h2>Belegungskalender {hotel.0.data.name}</h2>
<select class="form-control calendar__select" x-model="selectedIndex" x-on:change="load">
<template x-for="(month, index) in selectableMonths">
<option x-bind:value="index" x-text="month.toLocaleDateString('de-DE', {year: 'numeric', month: 'long'})"></option>
</template>
<h2>Belegungskalender {hotel.0.data.name}</h2>
<div data-controller="calendar"
data-calendar-uri-value="{ep:uri.ajax(action: 'range', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, format: 'html')}"
data-calendar-hotel-uid-value="{product.calendarHotel.uid}">
<select class="w-full border-gray-400 focus:border-gray-400"
data-calendar-target="field"
data-action="calendar#select">
</select>
<div class="calendar__inner" x-cloak x-ref="calendarWrapper"></div>
<div class="absolute top-0 left-0 inset-0 w-full h-full bg-overlay-white" x-show="loading">
<f:image class="absolute top-1/2 left-1/2 -translate-xy-1/2" src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif" alt="Loading"/>
<div class="mt-4"
data-calendar-target="container"></div>
<div class="absolute top-0 left-0 inset-0 w-full h-full bg-overlay-white"
data-calendar-target="loadingIndicator">
<svg class="w-6 h-6 text-gray-800 absolute top-1/2 left-1/2 transform -translate-xy-1/2">
<use href="#icon-spinner"></use>
</svg>
</div>
</div>
</div>
@@ -5,36 +5,38 @@
<f:layout name="Default"/>
<f:section name="Main">
<div class="bg-winter">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
<div class="container grid grid-cols-3 gap-8 py-8">
<div class="col-span-3 md:col-span-1 left-content">
<span class="headline--primary text-white">
SchneEPchen
</span>
</div>
<div class="col-span-3 md:col-span-2 right-content">
<div class="w-full md:w-1/4 text-white">
<div class="uppercase text-2xl font-bold mb-2">
Angebot
</div>
<span class="headline headline--4 text-white mb-2 pb-2 border-b-2 border-white">
{data.header}
<div class="bg-winter my-2">
<div class="bg-ep-primary-80">
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: 'hidden md:block transform -translate-y-2 text-white'}"/>
<div class="container grid grid-cols-3 gap-8 py-8">
<div class="col-span-3 md:col-span-1 left-content">
<span class="headline--primary text-white">
SchneEPchen
</span>
<div class="mb-4" data-rte-content>
{data.bodytext -> f:format.nl2br()}
</div>
<div class="col-span-3 md:col-span-2 right-content">
<div class="w-full md:w-1/4 text-white">
<div class="uppercase text-2xl font-bold mb-2">
Angebot
</div>
<span class="headline headline--4 text-white mb-2 pb-2 border-b-2 border-white">
{data.header}
</span>
<div class="mb-4" data-rte-content>
{data.bodytext -> f:format.nl2br()}
</div>
<f:if condition="{data.header_link}">
<a class="btn btn-warning"
href="{f:uri.typolink(parameter: data.header_link)}"
title="{product.name}">
Details
</a>
</f:if>
</div>
<f:if condition="{data.header_link}">
<a class="btn btn-warning"
href="{f:uri.typolink(parameter: data.header_link)}"
title="{product.name}">
Details
</a>
</f:if>
</div>
</div>
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</div>
<f:render partial="Disrupter/Outlines" section="Bottom1" arguments="{classes: 'hidden md:block transform translate-y-2 text-white'}"/>
</div>
</f:section>
@@ -1,27 +1,31 @@
<html data-namespace-typo3-fluid="true" lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers"
xmlns:encore="http://typo3.org/ns/Ssch/Typo3Encore/ViewHelpers"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="container">
<div class="page-header">
<div class="media">
<div class="media-left media-middle">
<f:image class="media-object" width="128" src="EXT:ep_theme/Resources/Public/images/logo_wide.svg" alt=""/>
</div>
<div class="media-body">
<h1 class="media-heading">Preisberechnung <small>{hotel.name}</small></h1>
<div class="container">
<div class="page-header">
<div class="media">
<div class="media-left media-middle">
<f:image class="media-object" width="128" src="EXT:ep_theme/Resources/Public/images/logo_wide.svg" alt=""/>
</div>
<div class="media-body">
<h1 class="media-heading">Preisberechnung <small>{hotel.name}</small></h1>
</div>
</div>
</div>
<f:format.raw>
<groups-price-calculator
configs-json='{configs}'
boards-json='{boards}'
options-json='{options}'
endpoint="{ep:uri.ajax(action: 'processForm', controller: 'AjaxGroupsPrice', pageUid: settings.defaultAjaxUid, arguments: '{hotel: hotel}')}"
loader-uri="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif')}"
>
</groups-price-calculator>
</f:format.raw>
<encore:renderWebpackScriptTags entryName="groups-calculator" buildName="EpThemeConfig"/>
</div>
<groups-price-calculator
:configs='{configs -> f:format.raw()}'
:boards='{boards -> f:format.raw()}'
:options='{options -> f:format.raw()}'
endpoint="{ep:uri.ajax(action: 'processForm', controller: 'AjaxGroupsPrice', pageUid: settings.defaultAjaxUid, arguments: '{hotel: hotel}')}"
loader-uri="{f:uri.image(src: 'EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif')}"
>
</groups-price-calculator>
</div>
</html>
@@ -115,9 +115,11 @@
<div id="main" class="w-full lg:w-2/3">
<div class="lg:pr-4">
<!-- Loader -->
<div class="flex items-center space-x-2 mb-4 hidden border border-ep-primary p-2"
<div class="flex items-center space-x-2 mb-4 hidden bg-ep-secondary text-white p-2"
data-product-details-target="loadingIndicator">
<f:image class="block w-4 h-4" src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif" alt="Loading"/>
<svg class="w-4 h-4 text-white">
<use href="#icon-spinner"></use>
</svg>
<span data-product-details-target="loadingMessage">
Loading...
</span>
@@ -334,12 +336,12 @@
<f:section name="Calendar">
<f:if condition="{product.calendarHotel}">
<f:if condition="{hotel.groupsPriceConfigs}">
<a class="button button--full"
data-glightbox
data-gallery="groups-calculator"
href="{f:uri.action(action: 'index', controller: 'GroupsPrice', arguments: '{hotel: hotel}', pageUid: settings.groupsPricePopupPageUid)}">
<button class="button w-full"
data-controller="lightbox"
data-lightbox-url-value="{f:uri.action(action: 'index', controller: 'GroupsPrice', arguments: '{hotel: hotel}', pageUid: settings.groupsPricePopupPageUid)}"
data-action="lightbox#open">
Preisberechnung
</a>
</button>
</f:if>
<div class="bg-gray-200 p-4 relative mb-8">
<span class="block font-bold mb-2">
@@ -349,10 +351,8 @@
data-calendar-uri-value="{ep:uri.ajax(action: 'range', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, format: 'html')}"
data-calendar-hotel-uid-value="{product.calendarHotel.uid}">
<select class="w-full border-gray-400 focus:border-gray-400"
data-action="calendar#selectMonth">
<template x-for="(month, index) in selectableMonths">
<option value="index" data-text="month.toLocaleDateString('de-DE', {year: 'numeric', month: 'long'})"></option>
</template>
data-calendar-target="field"
data-action="calendar#select">
</select>
<div class="mt-4"
data-calendar-target="container"></div>
@@ -5,186 +5,10 @@
<f:layout name="Default"/>
<f:section name="Main">
<div x-data='searchBar({
<f:format.raw>
uri: "{ep:uri.ajax(controller: "AjaxSearchbar", action: "update", pageUid: settings.defaultAjaxUid)}",
initialSearchParams: {searchParams -> f:format.json()},
initialFilterOptions: {filterOptions -> f:format.json()},
datePresets: {settings.datePickerPresets -> f:format.json()},
sticky: <f:if condition="{fixed}"><f:then>true</f:then><f:else>false</f:else></f:if>
</f:format.raw>
})'>
<div class="searchbar {f:if(condition: fixed, then: ' sticky')}"{f:if(condition: fixed, else: ' data-controller="sticky" data-sticky-offset-value="650" data-sticky-sticky-class="sticky"')}>
<f:form action="processSearch" controller="Search" method="post" name="searchParams"
object="{searchParams}" pluginName="searchresult" extensionName="epproducts"
pageUid="{settings.defaultSearchPageUid}" additionalAttributes="{ref: 'form'}">
<f:form.hidden property="destinationUid" additionalAttributes="{x-model: 'searchParams.destinationUid'}"/>
<f:form.hidden property="destinationType" additionalAttributes="{x-model: 'searchParams.destinationType'}"/>
<f:form.hidden property="dateFrom" value="" additionalAttributes="{x-model: 'searchParams.dateFrom'}"/>
<f:form.hidden property="dateTo" value="" additionalAttributes="{x-model: 'searchParams.dateTo'}"/>
<f:form.hidden property="nights" additionalAttributes="{x-model: 'searchParams.nights'}"/>
<f:form.hidden property="pax" additionalAttributes="{x-model: 'searchParams.pax'}"/>
<f:form.hidden property="priceRange" additionalAttributes="{x-model: 'searchParams.priceRange'}"/>
<f:form.hidden property="pageUid" additionalAttributes="{x-model: 'searchParams.pageUid'}"/>
<div class="sticky:max-w-screen-xl sticky:mx-auto flex flex-col sticky:flex-row sticky:items-center">
<div class="flex items-start justify-between sticky:flex-1 pt-8 px-8 sticky:py-4 sticky:px-0">
<div class="flex-1">
<div class="relative border-b border-r border-white mr-8" x-init="initDatePicker()">
<input type="text"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
placeholder="Zeitraum"
x-bind:value="dateRangeLabel"
x-on:click.prevent="show = show === 'dateRange' ? null : 'dateRange'"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1">
<use href="#icon-calendar"></use>
</svg>
<div class="absolute top-100 mt-2 left-0 bg-white p-4 shadow-md" x-show="show === 'dateRange'" x-cloak>
<template x-if="datePresets">
<div class="flex items-center justify-between pb-2">
<template x-for="preset in datePresets">
<a class="daterange__preset" href="#"
x-on:click.prevent="selectDateRangePreset(preset)">
<i class="fa fa-calendar"></i> <span x-text="preset.label"></span>
</a>
</template>
</div>
</template>
<input x-ref="picker" type="hidden">
<div class="flex items-center justify-between pt-2">
<button class="daterange__button focus:outline-none" x-on:click.prevent="show = null">
<i class="fa fa-times"></i> schließen
</button>
<button class="daterange__button focus:outline-none" x-on:click.prevent="resetDateRange()">
<i class="fa fa-trash-o"></i> zurücksetzen
</button>
</div>
</div>
</div>
</div>
<div class="w-24">
<div class="relative border-b border-r border-white mr-8">
<input type="text"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer"
x-bind:class="loading && 'cursor-not-allowed'"
x-bind:value="searchParams.pax"
x-on:click.prevent="show = show === 'pax' ? null : 'pax'"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1">
<use href="#icon-user-group"></use>
</svg>
<div class="absolute top-100 mt-2 left-0 bg-white p-4 shadow-md w-full h-48 overflow-y-scroll" x-show="show === 'pax'" x-cloak>
<ul class="m-0 p-0">
<template x-for="paxOption in filterOptions.pax">
<li>
<a href="#" x-on:click.prevent="updateSearchParam('pax', paxOption)" x-text="paxOption"></a>
</li>
</template>
<li><a href="{f:uri.typolink(parameter: settings.groupsPageUid)}">&gt; 20</a></li>
</ul>
</div>
</div>
</div>
<div class="flex-1">
<div class="relative border-b border-r border-white mr-8">
<input type="text"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
placeholder="Reiseziel"
x-bind:class="loading && 'cursor-not-allowed'"
x-bind:value="destinationName"
x-on:click.prevent="show = show === 'destination' ? null : 'destination'"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1 transform rotate-45">
<use href="#icon-airplane"></use>
</svg>
<div class="absolute top-100 left-0 bg-white p-4 shadow-md min-w-full h-48 overflow-y-scroll" x-show="show === 'destination'" x-cloak>
<template x-if="filterOptions.destinations && !conceptSelected">
<template x-for="destination in filterOptions.destinations">
<div class="flex items-start justify-between">
<div class="w-1/2">
<a href="#" x-on:click.prevent="selectDestination(destination.country.uid, destination.country.label, 'country')">
<img class="destination-pulldown__flag" x-bind:src="destination.country.flag" x-bind:alt="destination.country.label">
<strong x-text="destination.country.label"></strong>
</a>
</div>
<div class="w-1/2">
<ul class="plain-list mb-0">
<template x-for="region in destination.regions">
<li>
<a href="#" x-on:click.prevent="selectDestination(region.uid, region.label, 'region')" x-text="region.label"></a>
</li>
</template>
</ul>
</div>
</div>
</template>
</template>
<template x-if="filterOptions.concepts && !destinationSelected">
<div class="flex items-start justify-between">
<div class="w-1/2">
<strong>Konzept</strong>
</div>
<div class="w-1/2">
<ul class="plain-list mb-0">
<template x-for="concept in filterOptions.concepts">
<li>
<a href="#" x-on:click.prevent="selectDestination(concept.uid, concept.name, 'concept')" x-text="concept.name"></a>
</li>
</template>
</ul>
</div>
</div>
</template>
<template x-if="destinationSelected || conceptSelected">
<button x-on:click.prevent="resetDestination()">
<i class="fa fa-trash-o"></i>
zurücksetzen
</button>
</template>
</div>
</div>
</div>
<div class="w-32">
<div class="relative border-r border-b border-white">
<input type="text"
class="w-full bg-transparent border-0 ring-0 focus:ring-0 text-white uppercase px-2 py-1 cursor-pointer placeholder-white"
placeholder="Preis"
x-bind:class="loading && 'cursor-not-allowed'"
x-bind:value="priceRangeLabel"
x-on:click.prevent="show = show === 'priceRange' ? null : 'priceRange'"/>
<svg class="w-6 h-6 text-white absolute right-0 top-0 mr-2 mt-1">
<use href="#icon-euro"></use>
</svg>
<div class="absolute top-100 left-0 bg-white p-4 shadow-md w-full h-48 overflow-y-scroll" x-show="show === 'priceRange'" x-transition x-cloak>
<ul class="m-0 p-0">
<li>
<a href="#" x-on:click.prevent="updateSearchParam('priceRange', null)">beliebig</a>
</li>
<template x-for="(range, index) in filterOptions.priceRanges">
<li>
<a href="#"
x-on:click.prevent="updateSearchParam('priceRange', index)"
x-text="priceRangeFormatted(range)"></a>
</li>
</template>
</ul>
</div>
</div>
</div>
</div>
<button type="submit" class="hidden sticky:block sticky:ml-4">
<svg class="w-8 h-8 text-white">
<use href="#icon-chevron-right"></use>
</svg>
</button>
<div class="flex justify-end mt-4 sticky:hidden">
<button type="submit" class="w-1/3 text-white uppercase py-2" x-bind:disabled="loading">
Detailsuche
</button>
<button type="submit" class="w-1/3 text-white bg-ep-primary-light uppercase py-2" x-bind:disabled="loading">
Alle anzeigen
</button>
</div>
</div>
</f:form>
</div>
<div data-controller="searchbar"
data-action="datepicker:range-select->searchbar#setDateRange"
data-searchbar-uri-value="{ep:uri.ajax(controller: 'AjaxSearchbar', action: 'index', pageUid: settings.defaultAjaxUid, format: 'html')}">
<f:render partial="Search/Bar" arguments="{_all}"/>
</div>
</f:section>
@@ -12,9 +12,9 @@
<div data-watchlist-target="container"></div>
<div class="absolute top-0 left-0 inset-0 bg-overlay-white hidden"
data-watchlist-target="loadingIndicator">
<f:image class="absolute top-1/2 left-1/2 transform -translate-xy-1/2"
src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif"
alt="Loading"/>
<svg class="w-6 h-6 text-ep-primary absolute top-1/2 left-1/2 transform -translate-xy-1/2">
<use href="#icon-spinner"></use>
</svg>
</div>
</div>
</ep:container>
+1
View File
@@ -10,6 +10,7 @@ Encore
.setPublicPath('/typo3conf/ext/ep_theme/Resources/Public/')
.addEntry('main', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/main.js')
.addEntry('myep', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/myep.js')
.addEntry('groups-calculator', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/groups-calculator.js')
.addStyleEntry('rte', './public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/rte.scss')
.addExternals({
jquery: 'jQuery',