WIP Migrate to stimulus.js and tailwind

This commit is contained in:
Björn Fromme
2021-08-17 19:31:42 +02:00
parent 39f1c72972
commit 0d01f4212b
17 changed files with 1023 additions and 617 deletions
@@ -30,18 +30,25 @@ namespace EP\EpProducts\Controller;
use EP\EpProducts\Domain\Model\Date;
use EP\EpProducts\Domain\Model\Hotel;
use EP\EpProducts\Domain\Model\Product;
use EP\EpProducts\Domain\Repository\ProductRepository;
use EP\EpProducts\Service\DateService;
use EP\EpProducts\Service\FilterService;
use EP\EpProducts\Service\ResellerDataService;
use EP\EpProducts\Traits\RequestArgumentTypeConversionTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
class AjaxTableController extends ActionController
{
use RequestArgumentTypeConversionTrait;
/**
* @var ProductRepository
*/
protected $productRepository;
/**
* @var \EP\EpProducts\Service\FilterService
*/
@@ -64,17 +71,85 @@ class AjaxTableController extends ActionController
*/
public function __construct
(
ProductRepository $productRepository,
DateService $dateService,
FilterService $filterService,
ResellerDataService $resellerDataService
)
{
) {
parent::__construct();
$this->productRepository = $productRepository;
$this->dateService = $dateService;
$this->filterService = $filterService;
$this->resellerDataService = $resellerDataService;
}
public function initializeDatesAction()
{
if ($this->request->hasArgument('dateFrom')) {
$dateFrom = $this->request->getArgument('dateFrom');
$this->request->setArgument('dateFrom', $dateFrom === '' ? null : $dateFrom);
}
if ($this->request->hasArgument('dateTo')) {
$dateTo = $this->request->getArgument('dateTo');
$this->request->setArgument('dateTo', $dateTo === '' ? null : $dateTo);
}
}
/**
* @param Product $product
* @param Hotel $hotel
* @param string $paCode
* @param string $dateFrom
* @param string $dateTo
*
* @return string
* @throws \Doctrine\DBAL\DBALException
*/
public function datestableAction(Product $product, Hotel $hotel, $paCode = null, $dateFrom = null, $dateTo = null)
{
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
$filterSettings = $this->filterService->getNormalizedFilterSettings([], $forcedConceptUids);
$filterSettings['dateFrom'] = $dateFrom;
$filterSettings['dateTo'] = $dateTo;
$altProductLink = null;
$altLabel = null;
if ($product->getAltProduct()) {
$altProduct = $product->getAltProduct();
if ($this->productRepository->hasDates($altProduct)) {
/** @var ContentObjectRenderer $contentObject */
$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$altProductLink = $contentObject->typoLink_URL(
[
'parameter' => $altProduct->getDetailPage(),
]
);
$altLabel = $product->getAltLabel();
}
}
if ($reseller = $this->resellerDataService->getResellerForCurrentDomain()) {
$paCode = $reseller['paCode'];
}
$datesTable = $this->dateService->getDatesTable([
'product' => $product,
'hotel' => $hotel,
'filterSettings' => $filterSettings,
'template' => $this->settings['bpnBookingUrlTemplateCode'],
'paCode' => $paCode,
]);
$hasSurchargeOrDiscount = $this->dateService->checkSurcharcheOrDiscount($datesTable);
$this->view->assignMultiple([
'dates' => $datesTable,
'bookable' => $product->getBookable(),
'hotel' => $hotel,
'altProductLink' => $altProductLink,
'altLabel' => $altLabel,
'hasSurcharge' => $hasSurchargeOrDiscount['surcharge'],
'hasDiscount' => $hasSurchargeOrDiscount['discount'],
]);
}
/**
* @param Product $product
* @param Hotel $hotel
@@ -88,7 +163,7 @@ class AjaxTableController extends ActionController
(
Product $product,
Hotel $hotel,
Date $date = null,
Date $date,
$paCode = null
) {
$forcedConceptUids = GeneralUtility::trimExplode(',', $this->settings['forcedConceptUids'], true);
@@ -106,16 +181,20 @@ class AjaxTableController extends ActionController
'template' => $this->settings['bpnBookingUrlTemplateCode'],
'paCode' => $paCode,
]);
$hasSurchargeOrDiscount = $this->dateService->checkSurcharcheOrDiscount($priceTable);
$dateStart = $date ? $date->getDateStart()->format('d.m.Y') : '';
$dateEnd = $date ? $date->getDateEnd()->format('d.m.Y') : '';
$dateRange = sprintf('%s - %s', $dateStart, $dateEnd);
return json_encode([
$this->view->assignMultiple([
'dateRange' => $dateRange,
'servicesIncluded' => $date ? $date->getServicesIncluded() : [],
'hotel' => $hotel,
'servicesIncluded' => $date->getServicesIncluded(),
'prices' => $priceTable,
], JSON_THROW_ON_ERROR);
'hasSurcharge' => $hasSurchargeOrDiscount['surcharge'],
'hasDiscount' => $hasSurchargeOrDiscount['discount'],
]);
}
/**
@@ -122,7 +122,7 @@ class DateService implements SingletonInterface
'template' => $template,
'paCode' => $paCode,
'isDayTrip' => $product->isDaytrip(),
'isHideBookingButton' => $product->getHideBookingButton(),
'showBookingButton' => false === $product->getHideBookingButton(),
'nonBookableDateUids' => $nonBookableDateUids,
]);
}
@@ -167,7 +167,7 @@ class DateService implements SingletonInterface
'template' => $template,
'paCode' => $paCode,
'isDayTrip' => $product->isDaytrip(),
'isHideBookingButton' => $product->getHideBookingButton(),
'showBookingButton' => false === $product->getHideBookingButton(),
'nonBookableDateUids' => $nonBookableDateUids,
]);
}
@@ -198,10 +198,10 @@ class DateService implements SingletonInterface
public function preprocessPriceTable(array $options): array
{
$resolver = new OptionsResolver();
$resolver->setDefined(['paCode', 'isDayTrip', 'isHideBookingButton', 'nonBookableDateUids']);
$resolver->setDefined(['paCode', 'isDayTrip', 'showBookingButton', 'nonBookableDateUids']);
$resolver->setRequired(['priceTableData', 'template']);
$resolver->setDefault('isDayTrip', false);
$resolver->setDefault('isHideBookingButton', false);
$resolver->setDefault('showBookingButton', true);
$resolvedOptions = $resolver->resolve($options);
$priceTable = [];
@@ -212,7 +212,7 @@ class DateService implements SingletonInterface
$nonBookableDateUids = $resolvedOptions['nonBookableDateUids'];
foreach ($priceTableData as $row) {
$isHideBookingButton = $resolvedOptions['isHideBookingButton'] || in_array($row['dateBusProId'], $nonBookableDateUids, false);
$showBookingButton = $resolvedOptions['showBookingButton'] || !in_array($row['dateBusProId'], $nonBookableDateUids, false);
$dateStart = new \DateTime($row['dateStart']);
$dateEnd = new \DateTime($row['dateEnd']);
@@ -229,23 +229,23 @@ class DateService implements SingletonInterface
'dateStart' => $dateStart->format('d.m.Y'),
'dateEnd' => $dateEnd->format('d.m.Y'),
'season' => $row['season'],
'dateBusIncluded' => (bool) $row['dateBusIncluded'],
'dateSkipassIncluded' => (bool) $row['dateSkipassIncluded'],
'summer' => $row['season'] === 's',
'busIncluded' => (bool) $row['dateBusIncluded'],
'skipassIncluded' => (bool) $row['dateSkipassIncluded'],
'roomUid' => $row['roomUid'],
'roomAvailable' => $row['roomAvailable'] && $row['roomPrice'],
'roomName' => $row['roomName'],
'roomPrice' => $row['roomPrice'],
'roomBusPrice' => $row['busPrice'],
'roomDiscount' => $row['roomDiscount'],
'roomNights' => $row['roomNights'],
'roomOptionalServices' => $optionalServices,
'roomHasOptionalServices' => count($optionalServices) > 0,
'available' => $row['roomAvailable'] && $row['roomPrice'],
'minPrice' => $row['roomPrice'],
'busPrice' => $row['busPrice'],
'discount' => $row['roomDiscount'],
'nights' => $row['roomNights'],
'optionalServices' => $optionalServices,
'hasOptionalServices' => count($optionalServices) > 0,
'dateBusProId' => $row['dateBusProId'],
'hotelBusProId' => $row['hotelBusProId'],
'roomBusProId' => $row['roomBusProId'],
'bookingUrl' => $bookingUrl,
'isHideBookingButton' => $isHideBookingButton,
'showBookingButton' => !$isHideBookingButton,
'showBookingButton' => $showBookingButton,
];
}
@@ -272,10 +272,10 @@ class DateService implements SingletonInterface
public function preprocessDatesTable(array $options): array
{
$resolver = new OptionsResolver();
$resolver->setDefined(['paCode', 'isDayTrip', 'isHideBookingButton', 'nonBookableDateUids']);
$resolver->setDefined(['paCode', 'isDayTrip', 'showBookingButton', 'nonBookableDateUids']);
$resolver->setRequired(['dateTableData', 'template']);
$resolver->setDefault('isDayTrip', false);
$resolver->setDefault('isHideBookingButton', false);
$resolver->setDefault('showBookingButton', true);
$resolver->setDefault('nonBookableDateUids', []);
$resolvedOptions = $resolver->resolve($options);
@@ -288,7 +288,7 @@ class DateService implements SingletonInterface
$nonBookableDateUids = $resolvedOptions['nonBookableDateUids'];
foreach ($dateTableData as $row) {
$isHideBookingButton = $resolvedOptions['isHideBookingButton'] || in_array($row['dateBusProId'], $nonBookableDateUids, false);
$showBookingButton = $resolvedOptions['showBookingButton'] || !in_array($row['dateBusProId'], $nonBookableDateUids, false);
$dateStart = new \DateTime($row['dateStart']);
$dateEnd = new \DateTime($row['dateEnd']);
@@ -304,21 +304,21 @@ class DateService implements SingletonInterface
'dateUid' => $row['dateUid'],
'dateStart' => $dateStart->format('d.m.y'),
'dateEnd' => $dateEnd->format('d.m.y'),
'dateMinPrice' => $row['dateMinPrice'],
'datePseudoPrice' => $row['datePseudoPrice'],
'dateBusPrice' => $row['dateBusPrice'] ?? 0,
'dateDiscount' => $row['dateDiscount'] ?? 0,
'dateNights' => $row['dateNights'],
'dateBusIncluded' => (bool) $row['dateBusIncluded'],
'dateSkipassIncluded' => (bool) $row['dateSkipassIncluded'],
'dateServicesIncluded' => unserialize($row['dateServicesIncluded'], ['allowed_classes' => false]),
'dateServicesGeneral' => unserialize($row['dateServicesGeneral'], ['allowed_classes' => false]),
'minPrice' => $row['dateMinPrice'],
'pseudoPrice' => $row['datePseudoPrice'],
'busPrice' => $row['dateBusPrice'] ?? 0,
'discount' => $row['dateDiscount'] ?? 0,
'nights' => $row['dateNights'],
'busIncluded' => (bool) $row['dateBusIncluded'],
'skipassIncluded' => (bool) $row['dateSkipassIncluded'],
'servicesIncluded' => unserialize($row['dateServicesIncluded'], ['allowed_classes' => false]),
'servicesGeneral' => unserialize($row['dateServicesGeneral'], ['allowed_classes' => false]),
'season' => $row['season'],
'summer' => $row['season'] === 's',
'available' => $row['available'],
'unavailable' => $row['available'] === 0,
'bookingUrl' => $bookingUrl,
'isHideBookingButton' => $isHideBookingButton,
'showBookingButton' => !$isHideBookingButton,
'showBookingButton' => $showBookingButton,
];
}
@@ -420,18 +420,18 @@ class DateService implements SingletonInterface
return $priceTable;
}
public function checkSurcharcheOrDiscount(array $datesTableData): array
public function checkSurcharcheOrDiscount(array $tableData): array
{
$data = [
'surcharge' => false,
'discount' => false,
];
foreach ($datesTableData as $row) {
if ($row['dateBusPrice'] && $row['dateBusPrice'] > 0) {
foreach ($tableData as $row) {
if ($row['busPrice'] && $row['busPrice'] > 0) {
$data['surcharge'] = true;
}
if ($row['dateDiscount'] && $row['dateDiscount'] < 0) {
if ($row['discount'] && $row['discount'] < 0) {
$data['discount'] = true;
}
}
@@ -159,9 +159,10 @@ tx_epproducts_ajax_json {
1 = update
}
AjaxTable {
1 = pricetable
2 = pricetableHtml
3 = eventPricetable
1 = datestable
2 = pricetable
3 = pricetableHtml
4 = eventPricetable
}
AjaxCalendar {
1 = range
@@ -373,7 +373,7 @@ $iconRegistry->registerIcon(
'AjaxDate' => 'dates',
'AjaxSearchbar' => 'update',
'AjaxContingent' => 'list,rooms',
'AjaxTable' => 'pricetable,pricetableHtml,eventPricetable',
'AjaxTable' => 'datestable,pricetable,pricetableHtml,eventPricetable',
'AjaxCalendar' => 'range,contingents,availableRooms',
'AjaxWatchlist' => 'list',
'AjaxGroupsPrice' => 'processForm',
@@ -383,7 +383,7 @@ $iconRegistry->registerIcon(
'AjaxDate' => 'dates',
'AjaxSearchbar' => 'update',
'AjaxContingent' => 'list,rooms',
'AjaxTable' => 'pricetable,pricetableHtml,eventPricetable',
'AjaxTable' => 'datestable,pricetable,pricetableHtml,eventPricetable',
'AjaxCalendar' => 'range,contingents,availableRooms',
'AjaxWatchlist' => 'list',
'AjaxGroupsPrice' => 'processForm',
@@ -13,7 +13,6 @@ smoothscroll.polyfill()
import Alpine from 'alpinejs'
import parseAppConfig from './components/appconfig'
import appendPaCode from './components/pacode'
import productDetail from './components/product-detail'
import daytripDateSelect from './components/daytrip-date-select'
import searchBar from './components/search-bar'
import calendar from './components/calendar'
@@ -22,7 +21,6 @@ import contactForm from './components/contact-form'
const appConfig = parseAppConfig()
appendPaCode(appConfig)
Alpine.store('appconfig', appConfig)
Alpine.data('productDetail', productDetail)
Alpine.data('daytripDateSelect', daytripDateSelect)
Alpine.data('searchBar', searchBar)
Alpine.data('calendar', calendar)
@@ -1,98 +0,0 @@
import axios from 'axios'
export default props => ({
uris: props.uris,
show: props.hotelOnTop ? 'hotel' : 'dates',
tableMode: 'dates',
datesRows: [],
pricesRows: [],
servicesIncluded: [],
dateRange: '',
available: false,
singleDate: false,
bookable: true,
hasSurcharge: false,
hasDiscount: false,
altLabel: null,
altProductLink: null,
loading: false,
loaded: false,
addBus: false,
discountBus: false,
showAll: false,
init() {
// Load dates instantly when initial view is dates
if ('dates' === this.show) {
this.loadDates()
}
// Scroll to top when switching table mode
this.$watch('tableMode', () => {
this.$el.scrollIntoView({ block: 'start', behavior: 'smooth' })
})
// Load dates (once) when switching to dates view
this.$watch('show', () => {
if ('dates' === this.show && false === this.loaded) {
this.loadDates()
}
})
},
loadDates() {
this.loading = true
axios.get(this.uris.dates)
.then(response => {
const json = response.data
if (json.dates.length === 1) {
this.singleDate = true
const dateUid = json.dates[0].dateUid
this.loadPrices(dateUid)
} else {
this.singleDate = false
this.datesRows = json.dates
this.bookable = json.bookable
this.altLabel = json.altLabel
this.altProductLink = json.altProductLink
this.hasSurcharge = json.hasSurcharge
this.hasDiscount = json.hasDiscount
this.available = json.dates.length > 0
this.tableMode = 'dates'
}
}).catch(() => {
}).finally(() => {
this.loading = false
this.loaded = true
})
},
loadPrices(dateUid) {
this.loading = true
const data = new URLSearchParams({
'tx_epproducts_ajax[date]': dateUid,
'tx_epproducts_ajax[paCode]': this.$store.appconfig.paCode,
})
axios.post(this.uris.pricetable, data)
.then(response => {
const json = response.data
this.pricesRows = json.prices
this.servicesIncluded = json.servicesIncluded
this.dateRange = json.dateRange
this.tableMode = 'prices'
}).catch(() => {
}).finally(() => {
this.loading = false
})
},
calculatePrice(minPrice, busPrice, discount) {
if (this.addBus) {
return minPrice + busPrice
}
if (this.discountBus) {
return minPrice + discount
}
return minPrice
},
showBusIcon(busPrice, busIncluded) {
return (
busPrice && this.addBus ||
busIncluded && !this.discountBus
)
},
})
@@ -0,0 +1,18 @@
import { Controller } from 'stimulus'
export default class extends Controller {
static targets = [ 'content' ]
open(e) {
this.contentTargets.forEach(element => {
let uid = element.dataset.uid
element.classList.toggle('hidden', e.detail !== uid)
})
this.element.classList.remove('hidden')
}
close() {
this.element.classList.add('hidden', true)
}
}
@@ -0,0 +1,95 @@
import { Controller } from 'stimulus'
import { useDispatch } from 'stimulus-use'
export default class extends Controller {
static targets = [
'row',
'price',
'busIcon',
'surchargeToggleLabel',
'discountToggleLabel',
'rowToggleLabel',
'rowToggleIcon',
]
static values = {
mode: String,
showAllRows: Boolean,
}
initialize() {
useDispatch(this)
}
toggleSurcharge() {
this.modeValue = this.modeValue === 'surcharge' ? '' : 'surcharge'
}
toggleDiscount() {
this.modeValue = this.modeValue === 'discount' ? '' : 'discount'
}
toggleRows() {
this.showAllRowsValue = !this.showAllRowsValue
}
openModal(e) {
let eventName = `${e.currentTarget.dataset.type}-modal:open`
this.dispatch(eventName, e.currentTarget.dataset.uid)
}
modeValueChanged() {
this.priceTargets.forEach(price => {
let room = parseInt(price.dataset.room)
let bus = parseInt(price.dataset.bus)
let discount = parseInt(price.dataset.discount)
price.innerText = `${this.calculatePrice(room, bus, discount)}`
})
this.busIconTargets.forEach(icon => {
let busPrice = parseInt(icon.dataset.busPrice)
let busIncluded = !!icon.dataset.busIncluded
icon.classList.toggle('hidden', this.hideBusIcon(busPrice, busIncluded))
})
if (this.hasSurchargeToggleLabelTarget) {
let labelDefault = this.surchargeToggleLabelTarget.dataset.labelDefault
let labelAlt = this.surchargeToggleLabelTarget.dataset.labelAlt
this.surchargeToggleLabelTarget.innerText = this.modeValue === 'surcharge' ? labelDefault : labelAlt
}
if (this.hasDiscountToggleLabelTarget) {
let labelDefault = this.discountToggleLabelTarget.dataset.labelDefault
let labelAlt = this.discountToggleLabelTarget.dataset.labelAlt
this.discountToggleLabelTarget.innerText = this.modeValue === 'discount' ? labelDefault : labelAlt
}
}
showAllRowsValueChanged() {
let additionalRows = this.rowTargets.slice(5)
if (additionalRows.length) {
additionalRows.forEach(row => {
row.classList.toggle('hidden', this.showAllRowsValue === false)
})
}
if (this.hasRowToggleLabelTarget && this.hasRowToggleIconTarget) {
this.rowToggleLabelTarget.innerText = this.showAllRowsValue ? 'weniger Termine anzeigen' : 'alle Termine anzeigen'
this.rowToggleIconTarget.setAttribute('href', this.showAllRowsValue ? '#icon-minus-circle' : '#icon-plus-circle')
}
}
calculatePrice(minPrice, busPrice, discount) {
if ('surcharge' === this.modeValue) {
return minPrice + busPrice
}
if ('discount' === this.modeValue) {
return minPrice + discount
}
return minPrice
}
hideBusIcon(busPrice, busIncluded) {
return (
busPrice && this.modeValue !== 'surcharge' ||
busIncluded && this.modeValue === 'discount'
)
}
}
@@ -0,0 +1,93 @@
import { Controller } from 'stimulus'
import { useFetch } from '../mixins/use_fetch'
import { useConfig } from '../mixins/use_config'
export default class extends Controller {
static targets = [
'loadingIndicator',
'loadingMessage',
'datesTableContainer',
'priceTableContainer',
]
static values = {
datestableUri: String,
pricetableUri: String,
travelAlertUri: String,
loading: Boolean,
loaded: Boolean,
mode: String,
}
initialize() {
useFetch(this)
useConfig(this)
}
connect() {
if ('dates' === this.modeValue) {
this.loadDates()
}
}
showDates() {
this.modeValue = 'dates'
}
showPrices() {
this.modeValue = 'prices'
}
loadDates() {
if (true === this.loadedValue) {
this.modeValue = 'dates'
return
}
this.loadingValue = true
this.datesTableContainerTarget.innerHTML = ''
this.modeValue = 'dates'
fetch(this.datestableUriValue)
.then(this.checkStatus)
.then(this.parseHTML)
.then(html => {
this.datesTableContainerTarget.innerHTML = html
}).catch(() => {
}).finally(() => {
this.loadingValue = false
this.loadedValue = true
})
}
loadPrices(e) {
this.loadingValue = true
this.priceTableContainerTarget.innerHTML = ''
this.modeValue = 'prices'
let paCode = this.getConfig().paCode
let dateUid = e.currentTarget.dataset.dateUid
const data = new URLSearchParams({
'tx_epproducts_ajax[date]': dateUid,
'tx_epproducts_ajax[paCode]': paCode,
})
fetch(this.pricetableUriValue, { method: 'POST', body: data })
.then(this.checkStatus)
.then(this.parseHTML)
.then(html => {
this.priceTableContainerTarget.innerHTML = html
}).catch(() => {
}).finally(() => {
this.loadingValue = false
})
}
loadingValueChanged() {
this.loadingMessageTarget.innerText = this.modeValue === 'prices' ? 'Lade Buchungsptionen...' : 'Lade Termine...'
this.loadingIndicatorTarget.classList.toggle('hidden', this.loadingValue === false)
}
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,27 @@
import { Controller } from 'stimulus'
import { useDispatch } from 'stimulus-use'
export default class extends Controller {
static targets = [ 'tab', 'button' ]
initialize() {
useDispatch(this)
}
select(e) {
e.preventDefault()
let selectedTab = e.currentTarget.dataset.tab
this.buttonTargets.forEach(element => {
let currentTab = element.dataset.tab
element.classList.toggle('button--active', selectedTab === currentTab)
})
this.tabTargets.forEach(element => {
let currentTab = element.dataset.tab
element.classList.toggle('hidden', selectedTab !== currentTab)
})
this.dispatch('selected', selectedTab)
this.element.scrollIntoView({ block: 'start', behavior: 'smooth' })
}
}
@@ -8,7 +8,11 @@ export default class extends Controller {
onList: Boolean
}
static targets = [ 'label', 'icon' ]
static targets = [
'label',
'icon',
'ariaLabel',
]
initialize() {
useDispatch(this)
@@ -28,7 +32,10 @@ export default class extends Controller {
}
onListValueChanged() {
this.labelTarget.setAttribute('aria-label', true === this.onListValue ? 'von der Merkliste entfernen' : 'auf die Merkliste setzen')
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.hasAriaLabelTarget) {
this.ariaLabelTarget.setAttribute('aria-label', true === this.onListValue ? 'von der Merkliste entfernen' : 'auf die Merkliste setzen')
}
}
}
@@ -1,5 +1,5 @@
.button {
@apply inline-flex items-center justify-center px-4 py-2 md:px-8;
@apply inline-flex items-center justify-center space-x-2 px-4 py-2 md:px-8;
@apply uppercase leading-none;
@apply transition-colors outline-none focus:outline-none;
@apply bg-ep-secondary border-2 border-ep-secondary text-white;
@@ -24,7 +24,7 @@
@apply font-bold;
&:hover,
&.active {
&.button--active {
@apply bg-white border-ep-secondary text-ep-secondary;
}
}
@@ -149,7 +149,7 @@
<span data-microtip-position="top"
role="tooltip"
aria-label="auf die Merkliste setzen"
data-watchlist-toggle-target="label">
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>
@@ -0,0 +1,266 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers">
<f:layout name="Default"/>
<f:section name="Main">
<div class="headerbar">
Buchungsoptionen {hotel.name}
</div>
<div data-controller="pricetable">
<f:if condition="{hasSurcharge} || {hasDiscount}">
<div class="w-full flex justify-end mb-2">
<f:if condition="{hasSurcharge}">
<button data-action="pricetable#toggleSurcharge"
class="flex items-center space-x-1 focus:outline-none text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="surchargeToggleLabel"
data-label-default="Preise für Eigenanreise anzeigen"
data-label-alt="Preise für Busanreise anzeigen">
Preise für Eigenanreise anzeigen
</span>
</button>
</f:if>
<f:if condition="{hasDiscount}">
<button data-action="pricetable#toggleDiscount"
class="flex items-center space-x-1 focus:outline-none text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="discountToggleLabel"
data-label-default="Preise für Busanreise anzeigen"
data-label-alt="Preise für Eigenanreise anzeigen">
Preise für Busanreise anzeigen
</span>
</button>
</f:if>
</div>
</f:if>
<div class="w-full max-w-full overflow-x-scroll mb-4">
<table class="min-w-full border-collapse">
<thead>
<tr>
<th class="p-1 text-left">
Termin
</th>
<th class="p-1 text-left">
<span data-microtip-position="bottom" aria-label="Nächte" role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-bed"></use>
</svg>
</span>
</th>
<th class="p-1 text-left whitespace-nowrap">
Preis ab
</th>
<th class="p-1 text-left">
inkl.
</th>
<th class="hidden lg:table-cell"></th>
</tr>
</thead>
<tbody>
<f:for each="{dates}" as="row" iteration="iteration">
<tr class="even:bg-gray-200{f:if(condition: '{iteration.cycle} > 5', then: ' hidden')}"
data-pricetable-target="row">
<td class="px-2 py-1">
<f:if condition="{bookable} && {row.available} && {row.showBookingButton}">
<f:then>
<button class="text-ep-primary-light"
data-action="product-details#loadPrices"
data-date-uid="{row.dateUid}">
{row.dateStart} - {row.dateEnd}
</button>
<div class="md:hidden">
<button class="button--small"
data-action="product-details#loadPrices"
data-date-uid="{row.dateUid}">
Details
</button>
<a href="{row.bookingUrl}" target="_blank"
class="button button--small bg-green-500 border-green-500">
Buchen
</a>
</div>
</f:then>
<f:else>
<span>
{row.dateStart} - {row.dateEnd}
</span>
</f:else>
</f:if>
</td>
<td class="px-2 py-1">
{row.nights}
</td>
<td class="px-2 py-1">
<f:if condition="{row.pseudoPrice}">
<span class="text-sm line-through">
{row.pseudoPrice} €
</span>
</f:if>
<span class="font-bold"
data-pricetable-target="price"
data-room="{row.minPrice}"
data-bus="{row.busPrice}"
data-discount="{row.discount}">
{row.minPrice} €
</span>
</td>
<td class="px-2 py-1">
<div class="hidden md:flex items-center space-x-2">
<f:if condition="{row.busIncluded}">
<span data-microtip-position="top"
aria-label="Busfahrt"
role="tooltip"
data-pricetable-target="busIcon"
data-bus-included="{row.busIncluded}"
data-bus-price="{row.busPrice}">
<svg class="w-4 h-4">
<use href="#icon-bus"></use>
</svg>
</span>
</f:if>
<f:if condition="{row.skipassIncluded}">
<span data-microtip-position="top"
aria-label="{f:if(condition: row.summer, then: 'Bergbahnticket inklusive', else: 'Skipass inklusive')}"
role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-tag"></use>
</svg>
</span>
</f:if>
<button class="button button--light button--small hidden md:block"
data-action="pricetable#openModal"
data-type="services"
data-uid="{row.dateUid}"
data-microtip-position="top"
aria-label="Alle Inklusivleistungen anzeigen"
role="tooltip">
Leistungen
</button>
<button class="md:hidden">
<svg class="w-4 h-4">
<use href="#icon-info-circle"></use>
</svg>
</button>
</div>
</td>
<td class="px-2 py-1 hidden sm:table-cell">
<button class="button button--small"
data-action="product-details#loadPrices"
data-date-uid="{row.dateUid}">
Details
</button>
<f:if condition="{row.available} && {row.showBookingButton}">
<a href="{row.bookingUrl}"
target="_blank"
class="button button--small bg-green-500 border-green-500">
Buchen
</a>
</f:if>
<f:if condition="{row.showBookingButton}">
<f:else>
<a href="#"
class="button button--small bg-gray-300">
Reisen-Alert
</a>
</f:else>
</f:if>
<f:if condition="{row.available} && {row.showBookingButton}">
<f:else>
<span class="inline-block px-2 mx-1 text-xs text-gray-500">
ausgebucht
</span>
</f:else>
</f:if>
</td>
</tr>
</f:for>
</tbody>
</table>
</div>
<f:if condition="{dates -> f:count()} > 5 || {altProductLink}">
<div class="w-full flex justify-between pb-4">
<f:if condition="{dates -> f:count()} > 5">
<button class="flex items-center space-x-1 focus:outline-none text-ep-primary"
data-action="pricetable#toggleRows">
<svg class="w-4 h-4">
<use href="#icon-plus-circle"
data-pricetable-target="rowToggleIcon"></use>
</svg>
<span data-pricetable-target="rowToggleLabel">
Alle Termine anzeigen
</span>
</button>
</f:if>
<f:if condition="{altProductLink}">
<a href="{altProductLink}"
title="{altLabel}"
class="flex items-center space-x-1 focus:outline-none text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span>
{altLabel}
</span>
</a>
</f:if>
</div>
</f:if>
</div>
<div class="hidden fixed inset-0 w-full h-full z-50"
data-controller="modal"
data-action="pricetable:services-modal:open@window->modal#open">
<div class="absolute inset-0 w-full h-full bg-overlay-black z-25 flex items-center justify-center"
data-action="click->modal#close"></div>
<div class="absolute top-1/2 left-1/2 transform -translate-xy-1/2 bg-white shadow w-full max-w-xl rounded">
<div class="shadow flex items-center justify-between p-4 bg-ep-primary text-white rounded-t">
<span class="text-xl font-bold">
Inklusivleistungen {hotel.name}
</span>
<button class="inline-block"
data-action="modal#close">
<svg class="w-8 h-8">
<use href="#icon-close"></use>
</svg>
</button>
</div>
<div class="p-4">
<f:for each="{dates}" as="row">
<table class="hidden table table-striped"
data-modal-target="content"
data-uid="{row.dateUid}">
<tbody>
<f:for each="{row.servicesIncluded}" as="service">
<tr>
<td>
<div class="flex items-center space-x-2">
<svg class="w-6 h-6 text-ep-primary">
<use href="#icon-check"></use>
</svg>
<span>
{service}
</span>
</div>
</td>
</tr>
</f:for>
</tbody>
</table>
</f:for>
</div>
</div>
</div>
</f:section>
</html>
@@ -0,0 +1,254 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"
xmlns:ep="http://typo3.org/ns/EP/EpTheme/ViewHelpers">
<f:layout name="Default"/>
<f:section name="Main">
<div class="headerbar flex items-center justify-between">
<span>Buchungsoptionen</span>
<span>{hotel.name} {dateRange}</span>
</div>
<div data-controller="pricetable"
data-pricetable-show-all-rows-value="true">
<f:if condition="{hasSurcharge} || {hasDiscount}">
<div class="w-full flex justify-end mb-2">
<f:if condition="{hasSurcharge}">
<button data-action="pricetable#toggleSurcharge"
class="flex items-center space-x-1 focus:outline-none ep:text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="surchargeToggleLabel"
data-label-default="Preise für Eigenanreise anzeigen"
data-label-alt="Preise für Busanreise anzeigen">
Preise für Eigenanreise anzeigen
</span>
</button>
</f:if>
<f:if condition="{hasDiscount}">
<button data-action="pricetable#toggleDiscount"
class="flex items-center space-x-1 focus:outline-none ep:text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span data-pricetable-target="discountToggleLabel"
data-label-default="Preise für Busanreise anzeigen"
data-label-alt="Preise für Eigenanreise anzeigen">
Preise für Busanreise anzeigen
</span>
</button>
</f:if>
</div>
</f:if>
<div class="w-full max-w-full overflow-x-scroll mb-4">
<table class="min-w-full border-collapse">
<thead>
<tr>
<th class="text-left">
Zimmertyp
</th>
<th class="text-left">
Preis
</th>
<th class="text-left">
inkl.
</th>
<th></th>
<th class="hidden lg:table-cell"></th>
</tr>
</thead>
<tbody>
<f:for each="{prices}" as="row">
<tr class="even:bg-gray-200"
data-pricetable-target="row">
<td class="px-2 py-1">
<f:if condition="{row.showBookingButton} && {row.available}">
<f:then>
<a href="{row.bookingUrl}"
target="_blank">
{row.roomName}
</a>
<a class="button button--small button--action lg:hidden"
href="{row.bookingUrl}"
target="_blank">
Jetzt buchen
</a>
</f:then>
<f:else>
<span>
{row.roomName}
</span>
</f:else>
</f:if>
<f:if condition="{row.available}">
<f:else>
<div class="lg:hidden">
<span class="inline-block px-2 mx-1 text-xs text-gray-500">
ausgebucht
</span>
</div>
</f:else>
</f:if>
</td>
<td class="px-2 py-1 whitespace-nowrap"
data-pricetable-target="price"
data-room="{row.minPrice}"
data-bus="{row.busPrice}"
data-discount="{row.discount}">
{row.minPrice} €
</td>
<td class="px-2 py-1">
<div class="flex items-center space-x-1">
<f:if condition="{row.busIncluded}">
<span data-microtip-position="top"
aria-label="Busfahrt"
role="tooltip"
data-pricetable-target="busIcon"
data-bus-included="{row.busIncluded}"
data-bus-price="{row.busPrice}">
<svg class="w-4 h-4">
<use href="#icon-bus"></use>
</svg>
</span>
</f:if>
<f:if condition="{row.skipassIncluded}">
<span data-microtip-position="top"
aria-label="{f:if(condition: row.summer, then: 'Bergbahnticket inklusive', else: 'Skipass inklusive')}"
role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-tag"></use>
</svg>
</span>
</f:if>
</div>
</td>
<td class="px-2 py-1">
<f:if condition="{row.hasOptionalServices} && {row.available}">
<button class="hidden sm:block button button--light button--small"
data-action="pricetable#openModal"
data-type="options"
data-uid="{row.roomUid}"
data-microtip-position="top"
aria-label="Alle Zusatzleistungen anzeigen"
role="tooltip">
Optionen
</button>
<button class="sm:hidden text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-info-circle"></use>
</svg>
</button>
</f:if>
</td>
<td class="hidden lg:table-cell px-2 py-1">
<f:if condition="{row.isHideBookingButton}">
<f:then>
<a class="button button--small button--mute"
href="#">
Reisen-Alert
</a>
</f:then>
<f:else>
<f:if condition="{row.available}">
<f:then>
<a class="button button--small button--action"
href="{row.bookingUrl}" target="_blank">
Buchen
</a>
</f:then>
<f:else>
<span class="inline-block px-2 mx-1 text-xs text-gray-500">
ausgebucht
</span>
</f:else>
</f:if>
</f:else>
</f:if>
</td>
</tr>
</f:for>
</tbody>
</table>
</div>
<f:if condition="{isSingleDate}">
<f:else>
<button class="button button--small mb-4"
data-action="product-details#showDates">
<svg class="w-4 h-4 text-white">
<use href="#icon-chevron-left"></use>
</svg>
<span>zurück zur Terminübersicht</span>
</button>
</f:else>
</f:if>
<div class="headerbar">
Inklusivleistungen
</div>
<div class="bg-gray-200 p-4 mb-8">
<ul class="list-none m-0 p-0">
<f:for each="{servicesIncluded}" as="service">
<li class="flex items-center space-x-2">
<svg class="w-6 h-6 text-ep-primary">
<use href="#icon-check"></use>
</svg>
<span>
{service}
</span>
</li>
</f:for>
</ul>
</div>
</div>
<div class="hidden fixed inset-0 w-full h-full z-50"
data-controller="modal"
data-action="pricetable:options-modal:open@window->modal#open">
<div class="absolute inset-0 w-full h-full bg-overlay-black z-25 flex items-center justify-center"
data-action="click->modal#close"></div>
<div class="absolute top-1/2 left-1/2 transform -translate-xy-1/2 bg-white shadow w-full max-w-xl rounded">
<div class="absolute top-0 left-0 inset-x-0 shadow flex items-center justify-between p-4 bg-ep-primary text-white rounded-t">
<span class="text-xl font-bold">
Optionale Zusatzleistungen
</span>
<button class="inline-block"
data-action="modal#close">
<svg class="w-8 h-8">
<use href="#icon-close"></use>
</svg>
</button>
</div>
<div class="h-128 p-4 mt-12 overflow-y-scroll">
<f:for each="{prices}" as="row">
<div class="hidden"
data-modal-target="content"
data-uid="{row.roomUid}">
<f:for each="{row.optionalServices}" as="section">
<div class="py-2 -mx-1">
<div class="font-bold text-xl px-1">
{section.label}
</div>
<f:for each="{section.services}" as="service">
<dl class="flex items-start justify-between p-1 hover:bg-gray-200">
<dt class="font-normal">
{service.label}:
</dt>
<dd class="whitespace-nowrap">
{service.price} €
</dd>
</dl>
</f:for>
</div>
</f:for>
</div>
</f:for>
</div>
</div>
</div>
</f:section>
</html>
@@ -9,54 +9,71 @@
<article>
<f:render partial="Product/HeaderDetail" arguments="{_all}"/>
<div class="container"
id="detail"
x-data='productDetail({
uris: {
dates: "<ep:uri.ajax action="dates" controller="AjaxDate" arguments="{product: product, hotel: hotel}" format="json" pageUid="{settings.defaultAjaxUid}"/>",
pricetable: "<ep:uri.ajax action="pricetable" controller="AjaxTable" arguments="{product: product, hotel: hotel}" format="html" pageUid="{settings.defaultAjaxUid}"/>",
travelAlert: "<f:uri.typolink parameter="{settings.travelAlertPageUid}"/>"
},
hotelOnTop: <f:if condition="{settings.hotelOnTop}"><f:then>true</f:then><f:else>false</f:else></f:if>
})'>
data-controller="product-details"
data-action="tabs:selected->product-details#loadDates"
<f:if condition="{settings.hotelOnTop}">
<f:else>
data-product-details-mode-value="dates"
</f:else>
</f:if>
data-product-details-datestable-uri-value="{ep:uri.ajax(action: 'datestable', controller: 'AjaxTable', arguments: '{product: product, hotel: hotel}', format: 'html', pageUid: settings.defaultAjaxUid)}"
data-product-details-pricetable-uri-value="{ep:uri.ajax(action: 'pricetable', controller: 'AjaxTable', arguments: '{product: product, hotel: hotel}', format: 'html', pageUid: settings.defaultAjaxUid)}"
data-product-details-travel-alert-uri-value="{f:uri.typolink(parameter: settings.travelAlertPageUid)}">
<f:render partial="Header/Header" arguments="{layout: 8, subheader: '{product.region.name} / {product.name}', header: product.headline}"/>
<div class="flex flex-col lg:flex-row-reverse lg:items-start">
<div class="flex flex-col lg:flex-row-reverse lg:items-start"
data-controller="tabs">
<div class="w-full lg:w-1/3">
<div class="lg:pl-4">
<div class="grid grid-cols-2 gap-2">
<f:if condition="{settings.hotelOnTop}">
<f:then>
<button class="button button--light button--active col-span-1 md:col-span-2"
data-action="tabs#select"
data-tabs-target="button"
data-tab="hotel">
Unterkunft
</button>
<button class="button button--light col-span-1 md:col-span-2"
x-bind:class="{ 'button--active': show === 'hotel'}"
x-on:click.prevent="show = 'hotel'">Unterkunft</button>
data-action="tabs#select"
data-tabs-target="button"
data-tab="dates">
Termine/Preise
</button>
</f:then>
<f:else>
<button class="button button--light button--active col-span-1 md:col-span-2"
data-action="tabs#select"
data-tabs-target="button"
data-tab="dates">
Termine/Preise
</button>
<button class="button button--light col-span-1 md:col-span-2"
x-bind:class="{ 'button--active': show === 'dates'}"
x-on:click.prevent="show = 'dates'">Termine/Preise</button>
</f:else>
</f:if>
<f:if condition="{settings.hotelOnTop}">
<f:then>
<button class="button button--light col-span-1 md:col-span-2"
x-bind:class="{ 'button--active': show === 'dates'}"
x-on:click.prevent="show = 'dates'">Termine/Preise</button>
</f:then>
<f:else>
<button class="button button--light col-span-1 md:col-span-2"
x-bind:class="{ 'button--active': show === 'hotel'}"
x-on:click.prevent="show = 'hotel'">Unterkunft</button>
data-action="tabs#select"
data-tabs-target="button"
data-tab="hotel">
Unterkunft
</button>
</f:else>
</f:if>
<button class="button button--light col-span-1 md:col-span-2"
x-bind:class="{ 'button--active': show === 'region'}"
x-on:click.prevent="show = 'region'">Gebiet</button>
data-action="tabs#select"
data-tabs-target="button"
data-tab="region">
Gebiet
</button>
<button class="button button--light col-span-1 md:col-span-2"
x-bind:class="{ 'button--active': show === 'city'}"
x-on:click.prevent="show = 'city'">Ort</button>
data-action="tabs#select"
data-tabs-target="button"
data-tab="city">
Ort
</button>
<f:if condition="{product.journey}">
<button class="button button--light col-span-1 md:col-span-2"
x-bind:class="{ 'button--active': show === 'journey'}"
x-on:click.prevent="show = 'journey'">Anreise</button>
data-action="tabs#select"
data-tabs-target="button"
data-tab="journey">
Anreise
</button>
</f:if>
<f:if condition="{product.faqs}">
<button class="button button--light col-span-1 md:col-span-2"
@@ -68,17 +85,17 @@
</f:if>
<div class="col-span-2">
<p class="mb-4">
<button class="flex items-center space-x-2"
<button class="button button--light w-full"
data-controller="watchlist-toggle"
data-watchlist-toggle-key-value="{hotel.uid}:{product.uid}"
data-action="watchlist-toggle#toggle">
<span data-microtip-position="top" aria-label="" role="tooltip" data-watchlist-toggle-target="label">
<svg class="w-5 h-5">
<use href="#icon-plus-circle"
data-watchlist-toggle-target="icon"></use>
</svg>
<svg class="w-5 h-5">
<use href="#icon-plus-circle"
data-watchlist-toggle-target="icon"></use>
</svg>
<span data-watchlist-toggle-target="label">
Auf die Merkliste setzen
</span>
<span></span>
</button>
</p>
<p class="mb-4">
@@ -98,17 +115,18 @@
<div id="main" class="w-full lg:w-2/3">
<div class="lg:pr-4">
<!-- Loader -->
<div x-show="loading"
x-cloak
class="flex items-center space-x-2 mb-4">
<div class="flex items-center space-x-2 mb-4 hidden border border-ep-primary 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"/>
<span>Loading...</span>
<span data-product-details-target="loadingMessage">
Loading...
</span>
</div>
<!-- /Loader -->
<!-- Dates -->
<div x-show="show === 'dates' && !loading"
x-transition.opacity
x-cloak>
<div class="{f:if(condition: settings.hotelOnTop, then: 'hidden')}"
data-tabs-target="tab"
data-tab="dates">
<f:if condition="{isProductWithExcludedConcept}">
<f:if condition="{product.region.season} == 'w'">
<div class="alert alert-info">
@@ -125,12 +143,8 @@
<f:render section="DaytripDateSelect" arguments="{_all}"/>
</f:then>
<f:else>
<div x-show="tableMode === 'dates'">
<f:render section="DatesTable" arguments="{_all}"/>
</div>
<div x-show="tableMode === 'prices'">
<f:render section="PriceTable" arguments="{_all}"/>
</div>
<div class="hidden" data-product-details-target="datesTableContainer"></div>
<div class="hidden" data-product-details-target="priceTableContainer"></div>
<f:if condition="{isProductWithNoInfoConcept}">
<f:else>
<template x-if="!available">
@@ -140,28 +154,34 @@
</f:if>
</f:else>
</f:if>
</div>
<!-- /Dates -->
<div x-show="show === 'dates' || show === 'prices'" x-cloak>
<f:render partial="Product/Details" arguments="{_all}"/>
</div>
<!-- /Dates -->
<!-- Hotel -->
<div x-show="show === 'hotel'" x-transition.opacity x-cloak>
<div class="{f:if(condition: settings.hotelOnTop, else: 'hidden')}"
data-tabs-target="tab"
data-tab="hotel">
<f:render partial="Hotel/Details" arguments="{hotel: hotel, bars: 1, showNonBookableHint: 0}"/>
</div>
<!-- /Hotel -->
<!-- City -->
<div x-show="show === 'city'" x-transition.opacity x-cloak>
<div class="hidden"
data-tabs-target="tab"
data-tab="city">
<f:render partial="City/Details" arguments="{city: product.city, bars: 1}"/>
</div>
<!-- /City -->
<!-- Region -->
<div x-show="show === 'region'" x-transition.opacity x-cloak>
<div class="hidden"
data-tabs-target="tab"
data-tab="region">
<f:render partial="Region/Details" arguments="{region: product.region, bars: 1}"/>
</div>
<!-- /Region -->
<!-- Journey -->
<div x-show="show === 'journey'" x-transition.opacity x-cloak>
<div class="hidden"
data-tabs-target="tab"
data-tab="journey">
<f:render partial="Journey/Details" arguments="{journey: product.journey, bars: 1}"/>
</div>
<!-- /Journey -->
@@ -173,8 +193,11 @@
</div>
</div>
</article>
<div x-data x-init='fbq("track", "ViewContent", { "content_ids": [ {product.busProId -> f:format.raw()} ], "content_name": "{product.nameInternal -> f:format.raw()}", "content_type": "product" })'></div>
<f:render partial="Faq" arguments="{faqs: product.faqs, settings: settings}"/>
<div data-controller="fbpixel"
data-fbpixel-props-value='{ "content_ids": [ {product.busProId -> f:format.raw()} ], "content_name": "{product.nameInternal -> f:format.raw()}", "content_type": "product" }'></div>
<div id="faq">
<f:render partial="Faq" arguments="{faqs: product.faqs, settings: settings}"/>
</div>
</f:section>
<f:section name="Sidebar">
@@ -219,360 +242,8 @@
</f:if>
</f:section>
<f:section name="DatesTable">
<div x-show="bookable"
x-cloak>
<div class="headerbar">
Buchungsoptionen {hotel.name}
</div>
<f:render section="PriceToggle"/>
<div class="w-full max-w-full overflow-x-scroll mb-4">
<table class="min-w-full border-collapse">
<thead>
<tr>
<th class="p-1 text-left">
Termin
</th>
<th class="p-1 text-left">
<span data-microtip-position="bottom" aria-label="Nächte" role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-bed"></use>
</svg>
</span>
</th>
<th class="p-1 text-left whitespace-nowrap">
Preis ab
</th>
<th class="p-1 text-left">
inkl.
</th>
<th class="hidden lg:table-cell"></th>
</tr>
</thead>
<tbody>
<template x-for="(row, index) in datesRows" x-bind:key="row.dateUid">
<tr class="even:bg-gray-200" x-bind:class="{ 'hidden': index > 4 && !showAll }">
<td class="px-2 py-1">
<template x-if="bookable && row.available && !row.isHideBookingButton">
<a class="text-ep-primary-light" href="#"
x-on:click.prevent="loadPrices(row.dateUid)"
x-text="row.dateStart + ' - ' + row.dateEnd"></a>
<div class="md:hidden">
<button x-on:click.prevent="loadPrices(row.dateUid)" class="button button--small">
Details
</button>
<a x-bind:href="row.bookingUrl" target="_blank" class="button button--small bg-green-500 border-green-500">
Buchen
</a>
</div>
</template>
<template x-if="!bookable || !row.available || row.isHideBookingButton">
<span x-text="row.dateStart + ' - ' + row.dateEnd"></span>
</template>
</td>
<td class="px-2 py-1"
x-text="row.dateNights"></td>
<td class="px-2 py-1">
<span class="text-sm line-through" x-show="row.datePseudoPrice" x-text="row.datePseudoPrice + ' €'"></span>
<strong x-text="calculatePrice(row.dateMinPrice, row.dateBusPrice, row.dateDiscount) + ' €'"></strong>
</td>
<td class="px-2 py-1">
<div class="hidden md:flex items-center space-x-2">
<div x-show="showBusIcon(row.dateBusPrice, row.dateBusIncluded)" data-microtip-position="top" aria-label="Busfahrt inklusive" role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-bus"></use>
</svg>
</div>
<div x-show="row.dateSkipassIncluded" data-microtip-position="top" x-bind:aria-label="row.season === 's' ? 'Bergbahnticket inklusive' : 'Skipass inklusive'" role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-tag"></use>
</svg>
</div>
<button class="button button--light button--small hidden md:block"
data-microtip-position="top" aria-label="Alle Inklusivleistungen anzeigen" role="tooltip">
Leistungen
</button>
<button class="md:hidden">
<svg class="w-4 h-4">
<use href="#icon-info-circle"></use>
</svg>
</button>
</div>
</td>
<td class="px-2 py-1 hidden sm:table-cell">
<button x-show="row.available || row.isHideBookingButton" class="button button--small"
x-on:click.prevent="loadPrices(row.dateUid)">
Details
</button>
<a x-show="row.available && !row.isHideBookingButton" x-bind:href="row.bookingUrl"
target="_blank"
class="button button--small bg-green-500 border-green-500">
Buchen
</a>
<a x-show="row.isHideBookingButton" x-bind:href="uris.travelAlertUrl"
class="button button--small bg-gray-300">
Reisen-Alert
</a>
<span x-show="!row.available && !row.isHideBookingButton"
class="inline-block px-2 mx-1 text-xs text-gray-500">
ausgebucht
</span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<template x-if="datesRows.length > 5 || altProductLink">
<div class="grid grid-cols-2 gap-8 pb-4">
<button x-show="datesRows.length > 5"
x-on:click.prevent="showAll = !showAll"
class="flex items-center space-x-1 focus:outline-none ep:text-ep-primary">
<svg class="w-4 h-4">
<use x-bind:href="showAll ? '#icon-minus-circle' : '#icon-plus-circle'"></use>
</svg>
<span x-text="showAll ? 'Weniger Termine anzeigen' : 'Alle Termine anzeigen'"></span>
</button>
<template x-if="altProductLink">
<a x-bind:href="altProductLink"
x-bind:title="altLabel"
class="flex items-center space-x-1 focus:outline-none ep:text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span x-text="altLabel"></span>
</a>
</template>
</div>
</template>
<template x-for="row in datesRows">
<div x-bind:id="'services-' + row.dateUid"
x-transition:enter="transition duration-200 ease-in-out"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition duration-200 ease-in-out"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
x-cloak
class="hidden fixed inset-0 w-full h-full z-50">
<div class="absolute inset-0 w-full h-full bg-overlay-black"></div>
<div class="absolute top-1/2 left-1/2 transform -translate-xy-1/2 bg-white p-8 shadow w-full max-w-3xl rounded">
<div class="flex justify-between border-b border-gray-200 pb-2 mb-4">
<span class="text-lg font-bold">
Inklusivleistungen
</span>
<button class="inline-block">
<svg class="w-8 h-8">
<use href="#icon-close"></use>
</svg>
</button>
</div>
<table class="table table-striped">
<tbody>
<template x-for="service in row.dateServicesIncluded">
<tr>
<td>
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
<span x-text="service"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</div>
</f:section>
<f:section name="PriceTable">
<div class="headerbar flex items-center justify-between">
<span>Buchungsoptionen</span>
<span x-text="dateRange"></span>
</div>
<f:render section="PriceToggle"/>
<div class="w-full max-w-full overflow-x-scroll mb-4">
<table class="min-w-full border-collapse">
<thead>
<tr>
<th class="text-left">
Zimmertyp
</th>
<th class="text-left">
Preis
</th>
<th class="text-left">
inkl.
</th>
<th></th>
<th class="hidden lg:table-cell"></th>
</tr>
</thead>
<tbody>
<template x-for="(row, index) in pricesRows" x-bind:key="row.roomBusProId">
<tr class="even:bg-gray-200">
<td class="px-2 py-1">
<span x-show="row.isHideBookingButton || !row.roomAvailable" x-text="row.roomName"></span>
<a x-show="!row.isHideBookingButton && row.roomAvailable"
x-bind:href="row.bookingUrl" target="_blank"
x-text="row.roomName"></a>
<a class="button button--small button--action lg:hidden"
x-show="!row.isHideBookingButton && row.roomAvailable"
x-bind:href="row.bookingUrl" target="_blank">
Jetzt buchen
</a>
<div class="lg:hidden"
x-show="!row.roomAvailable">
<span class="inline-block px-2 mx-1 text-xs text-gray-500">
ausgebucht
</span>
</div>
</td>
<td class="px-2 py-1 whitespace-nowrap" x-bind:class="!row.roomAvailable && !row.isHideBookingButton && 'line-through'"
x-text="calculatePrice(row.roomPrice, row.roomBusPrice, row.roomDiscount) + ' €'"></td>
<td class="px-2 py-1">
<div class="flex items-center space-x-1">
<span x-show="showBusIcon(row.roomBusPrice, row.dateBusIncluded)" data-microtip-position="top" aria-label="Busfahrt inklusive" role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-bus"></use>
</svg>
</span>
<span x-show="row.dateSkipassIncluded" data-microtip-position="top" x-bind:aria-label="row.season === 's' ? 'Bergbahnticket inklusive' : 'Skipass inklusive'" role="tooltip">
<svg class="w-4 h-4">
<use href="#icon-tag"></use>
</svg>
</span>
</div>
</td>
<td class="px-2 py-1">
<button x-show="row.roomHasOptionalServices && row.roomAvailable"
class="hidden sm:block button button--light button--small"
data-microtip-position="top" aria-label="Alle Zusatzleistungen anzeigen" role="tooltip">
Optionen
</button>
<button x-show="row.roomHasOptionalServices && row.roomAvailable"
class="sm:hidden ep:text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-info-circle"></use>
</svg>
</button>
</td>
<td class="hidden lg:table-cell px-2 py-1">
<a class="button button--small button--action"
x-show="row.roomAvailable && !row.isHideBookingButton"
x-bind:href="row.bookingUrl" target="_blank">
Buchen
</a>
<a class="button button--small button--mute"
x-show="row.isHideBookingButton"
x-bind:href="uris.travelAlertUrl">
Reisen-Alert
</a>
<span x-show="!row.roomAvailable && !row.isHideBookingButton"
class="inline-block px-2 mx-1 text-xs text-gray-500">
ausgebucht
</span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<template x-for="row in pricesRows">
<div x-bind:id="'optional-services-' + row.roomUid"
x-transition:enter="transition duration-200 ease-in-out"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition duration-200 ease-in-out"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="hidden fixed inset-0 w-full h-full z-50">
<div class="absolute inset-0 w-full h-full bg-overlay-black z-25 flex items-center justify-center"></div>
<div class="absolute top-1/2 left-1/2 transform -translate-xy-1/2 bg-white p-8 shadow w-full max-w-3xl h-128 overflow-y-scroll rounded">
<div class="flex justify-between pb-2 mb-4">
<span class="text-2xl font-bold">
Optionale Zusatzleistungen
</span>
<button class="inline-block">
<svg class="w-8 h-8">
<use href="#icon-close"></use>
</svg>
</button>
</div>
<template x-for="section in row.roomOptionalServices">
<div class="py-2 -mx-1">
<div class="font-bold text-xl px-1" x-text="section.label"></div>
<template x-for="optionalService in section.services">
<dl class="flex items-start justify-between p-1 hover:bg-gray-200">
<dt class="font-normal" x-text="optionalService.label + ':'"></dt>
<dd class="whitespace-nowrap" x-text="optionalService.price + ' €'"></dd>
</dl>
</template>
</div>
</template>
</div>
</div>
</template>
<template x-if="!singleDate">
<button class="button button--small mb-4"
x-on:click.prevent="tableMode = 'dates'">
<svg class="w-4 h-4 text-white">
<use href="#icon-chevron-left"></use>
</svg>
<span>zurück zur Terminübersicht</span>
</button>
</template>
<div class="headerbar">
Inklusivleistungen
</div>
<div class="bg-gray-200 p-4 mb-8">
<ul class="list-none m-0 p-0">
<template x-for="service in servicesIncluded">
<li class="flex items-center space-x-2">
<svg class="w-6 h-6 text-ep-primary">
<use href="#icon-check"></use>
</svg>
<span x-text="service"></span>
</li>
</template>
</ul>
</div>
</f:section>
<f:section name="PriceToggle">
<div class="w-full flex justify-end mb-2" x-show="hasSurcharge || hasDiscount">
<template x-if="hasSurcharge">
<button x-on:click.prevent="addBus = !addBus"
class="flex items-center space-x-1 focus:outline-none ep:text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span x-show="addBus">
Preise für Eigenanreise anzeigen
</span>
<span x-show="! addBus">
Preise für Busanreise anzeigen
</span>
</button>
</template>
<template x-if="hasDiscount">
<button x-on:click.prevent="discountBus = !discountBus"
class="flex items-center space-x-1 focus:outline-none ep:text-ep-primary">
<svg class="w-4 h-4">
<use href="#icon-arrow-circle-right"></use>
</svg>
<span x-show="discountBus">
Preise für Busanreise anzeigen
</span>
<span x-show="!discountBus">
Preise für Eigenanreise anzeigen
</span>
</button>
</template>
</div>
</f:section>
<f:section name="DaytripDateSelect">
<f:comment><!--
<div x-data='daytripDateSelect({
<f:format.raw>
hotelName: "{hotel.name}",
@@ -657,6 +328,7 @@
</div>
</div>
</div>
--></f:comment>
</f:section>
<f:section name="Calendar">
@@ -673,23 +345,19 @@
<span class="block font-bold mb-2">
Belegungskalender
</span>
<div x-data='calendar({
<f:format.raw>
url: "{ep:uri.ajax(action: 'range', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, format: 'html')}",
hotel: {product.calendarHotel.uid}
</f:format.raw>
})'>
<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"
x-model="selectedIndex"
x-on:change="load">
data-action="calendar#selectMonth">
<template x-for="(month, index) in selectableMonths">
<option x-bind:value="index" x-text="month.toLocaleDateString('de-DE', {year: 'numeric', month: 'long'})"></option>
<option value="index" data-text="month.toLocaleDateString('de-DE', {year: 'numeric', month: 'long'})"></option>
</template>
</select>
<div class="mt-4"
x-ref="calendarWrapper"
x-cloak></div>
<div class="absolute top-0 left-0 inset-0 w-full h-full bg-overlay-white" x-show="loading">
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">
<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>
</div>
+39 -41
View File
@@ -3,8 +3,6 @@ const tinycolor = require('tinycolor2')
module.exports = {
purge: {
mode: 'layers',
layers: ['components', 'utilities'],
content: [
'./public/typo3conf/ext/ep_theme/Configuration/TypoScript/**/*.typoscript',
'./public/typo3conf/ext/ep_theme/Resources/Private/Layouts/**/*.html',
@@ -34,27 +32,27 @@ module.exports = {
},
extend: {
colors: {
'overlay': {
'black': tinycolor('#000000').setAlpha(0.85).toRgbString(),
'white': tinycolor('#ffffff').setAlpha(0.85).toRgbString(),
overlay: {
black: tinycolor('#000000').setAlpha(0.85).toRgbString(),
white: tinycolor('#ffffff').setAlpha(0.85).toRgbString(),
},
'concept': {
'default': '#005ca9',
'bdg': '#005ca9',
'skr': '#005ca9',
'evt': '#ed5454',
'fam': '#7ca7d5',
'grp': '#ffa300',
'prg': '#005ca9',
'ind': '#009fe3',
'wkd': '#44d04e',
'spz': '#436a8a',
'epe': '#436a8a',
'sfl': '#999',
concept: {
default: '#005ca9',
bdg: '#005ca9',
skr: '#005ca9',
evt: '#ed5454',
fam: '#7ca7d5',
grp: '#ffa300',
prg: '#005ca9',
ind: '#009fe3',
wkd: '#44d04e',
spz: '#436a8a',
epe: '#436a8a',
sfl: '#999',
},
'ep': {
'primary': '#3d8ccb',
'secondary': '#f9c700',
ep: {
primary: '#3d8ccb',
secondary: '#f9c700',
'primary-light': '#009fff',
'primary-medium': '#165883',
'primary-dark': '#18527B',
@@ -64,25 +62,25 @@ module.exports = {
'secondary-10': tinycolor('#f0bc44').setAlpha(0.1).toRgbString(),
'secondary-80': tinycolor('#f0bc44').setAlpha(0.8).toRgbString(),
},
'sbw': {
'primary': '#f0bc44',
'secondary': '#ee525a',
sbw: {
primary: '#f0bc44',
secondary: '#ee525a',
'primary-dark': '#c7a20c',
'primary-10': tinycolor('#f0bc44').setAlpha(0.1).toRgbString(),
'primary-50': tinycolor('#f0bc44').setAlpha(0.5).toRgbString(),
'primary-80': tinycolor('#f0bc44').setAlpha(0.8).toRgbString(),
'overlay-secondary': tinycolor('#c20000').setAlpha(0.55).toRgbString(),
},
'snz': {
'primary': '#82C3C6',
'secondary': '#f9c33d',
snz: {
primary: '#82C3C6',
secondary: '#f9c33d',
'primary-10': tinycolor('#82C3C6').setAlpha(0.1).toRgbString(),
'primary-50': tinycolor('#82C3C6').setAlpha(0.5).toRgbString(),
'primary-80': tinycolor('#82C3C6').setAlpha(0.8).toRgbString(),
},
'uch': {
'primary': '#b5e400',
'secondary': '#19a3dc',
uch: {
primary: '#b5e400',
secondary: '#19a3dc',
'primary-dark': '#b5e400',
'primary-10': tinycolor('#b5e400').setAlpha(0.1).toRgbString(),
'primary-50': tinycolor('#b5e400').setAlpha(0.5).toRgbString(),
@@ -110,23 +108,23 @@ module.exports = {
textColor: [ 'sbw', 'sbw-hover', 'snz', 'snz-hover', 'uch', 'uch-hover' ],
borderColor: [ 'sbw','snz', 'uch' ],
display: ['group-hover', 'sticky'],
visibility: ['group-hover'],
opacity: ['group-hover'],
rotate: ['group-hover'],
flex: ['sticky'],
flexDirection: ['even', 'odd', 'sticky'],
alignItems: ['sticky'],
padding: ['even', 'odd', 'first', 'last', 'sticky'],
margin: ['even', 'odd', 'first', 'last', 'sticky'],
maxWidth: ['sticky'],
visibility: [ 'group-hover' ],
opacity: [ 'group-hover' ],
rotate: [ 'group-hover' ],
flex: [ 'sticky' ],
flexDirection: [ 'even', 'odd', 'sticky' ],
alignItems: [ 'sticky' ],
padding: [ 'even', 'odd', 'first', 'last', 'sticky' ],
margin: [ 'even', 'odd', 'first', 'last', 'sticky' ],
maxWidth: [ 'sticky' ],
},
},
plugins: [
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/forms'),
require('tailwindcss-alt')({
variants: ['sbw', 'snz', 'uch'],
}),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/forms'),
plugin(function ({addVariant, e}) {
addVariant('first-type', ({modifySelectors, separator}) => {
modifySelectors(({className}) => {