diff --git a/public/typo3conf/ext/ep_products/Classes/Controller/AjaxTableController.php b/public/typo3conf/ext/ep_products/Classes/Controller/AjaxTableController.php index 55ab9d3c..b5738fda 100644 --- a/public/typo3conf/ext/ep_products/Classes/Controller/AjaxTableController.php +++ b/public/typo3conf/ext/ep_products/Classes/Controller/AjaxTableController.php @@ -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'], + ]); } /** diff --git a/public/typo3conf/ext/ep_products/Classes/Service/DateService.php b/public/typo3conf/ext/ep_products/Classes/Service/DateService.php index eb4e1863..4e87d041 100644 --- a/public/typo3conf/ext/ep_products/Classes/Service/DateService.php +++ b/public/typo3conf/ext/ep_products/Classes/Service/DateService.php @@ -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; } } diff --git a/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript b/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript index 254f27f1..1e0a6eef 100644 --- a/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript +++ b/public/typo3conf/ext/ep_products/Configuration/TypoScript/setup.typoscript @@ -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 diff --git a/public/typo3conf/ext/ep_products/ext_localconf.php b/public/typo3conf/ext/ep_products/ext_localconf.php index 64bee374..a705c706 100644 --- a/public/typo3conf/ext/ep_products/ext_localconf.php +++ b/public/typo3conf/ext/ep_products/ext_localconf.php @@ -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', diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/_app.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/_app.js index be22dc92..78964631 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/_app.js +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/_app.js @@ -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) diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/components/product-detail.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/components/product-detail.js deleted file mode 100644 index 88253c6d..00000000 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/components/product-detail.js +++ /dev/null @@ -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 - ) - }, -}) diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/modal_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/modal_controller.js new file mode 100644 index 00000000..3c8a443f --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/modal_controller.js @@ -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) + } +} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/pricetable_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/pricetable_controller.js new file mode 100644 index 00000000..1a069b05 --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/pricetable_controller.js @@ -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' + ) + } +} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/product_details_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/product_details_controller.js new file mode 100644 index 00000000..0fdab505 --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/product_details_controller.js @@ -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' }) + } +} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/tabs_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/tabs_controller.js new file mode 100644 index 00000000..c868fa5d --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/tabs_controller.js @@ -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' }) + } + +} diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/watchlist_toggle_controller.js b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/watchlist_toggle_controller.js index 5b61720a..860da717 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/watchlist_toggle_controller.js +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/js/controllers/watchlist_toggle_controller.js @@ -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') + } } } diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/components/_buttons.scss b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/components/_buttons.scss index 2c17530d..528b7309 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/components/_buttons.scss +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Assets/scss/components/_buttons.scss @@ -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; } } diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html index 2a1f74f3..b8922ce7 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Partials/Search/Result.html @@ -149,7 +149,7 @@ + data-watchlist-toggle-target="ariaLabel"> diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Datestable.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Datestable.html new file mode 100644 index 00000000..14446eae --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Datestable.html @@ -0,0 +1,266 @@ + + + + + +
+ Buchungsoptionen {hotel.name} +
+ +
+ + +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ Termin + + + + + + + + Preis ab + + inkl. +
+ + + +
+ + + Buchen + +
+
+ + + {row.dateStart} - {row.dateEnd} + + +
+
+ {row.nights} + + + + {row.pseudoPrice} € + + + + {row.minPrice} € + + + +
+
+ + +
+ + + + + + + + + + {altLabel} + + + +
+
+ +
+ + +
+ + diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Pricetable.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Pricetable.html new file mode 100644 index 00000000..342474fe --- /dev/null +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/AjaxTable/Pricetable.html @@ -0,0 +1,254 @@ + + + + + +
+ Buchungsoptionen + {hotel.name} {dateRange} +
+ +
+ + +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ Zimmertyp + + Preis + + inkl. +
+ + + + {row.roomName} + + + Jetzt buchen + + + + + {row.roomName} + + + + + +
+ + ausgebucht + +
+
+
+
+ {row.minPrice} € + +
+ + + + + + + + + + + + + + +
+
+ + + + +
+
+ + + + + +
+ Inklusivleistungen +
+
+
    + +
  • + + + + + {service} + +
  • +
    +
+
+ +
+ + +
+ + diff --git a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html index c16a757d..2279b95d 100644 --- a/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html +++ b/public/typo3conf/ext/ep_theme/Resources/Private/Templates/Product/Detail.html @@ -9,54 +9,71 @@
+ data-controller="product-details" + data-action="tabs:selected->product-details#loadDates" + + + data-product-details-mode-value="dates" + + + 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)}"> -
+
+ + data-action="tabs#select" + data-tabs-target="button" + data-tab="dates"> + Termine/Preise + + - - - - - - - - + data-action="tabs#select" + data-tabs-target="button" + data-tab="hotel"> + Unterkunft + + data-action="tabs#select" + data-tabs-target="button" + data-tab="region"> + Gebiet + + data-action="tabs#select" + data-tabs-target="button" + data-tab="city"> + Ort + + data-action="tabs#select" + data-tabs-target="button" + data-tab="journey"> + Anreise +

@@ -98,17 +115,18 @@

-
+ -
+
@@ -125,12 +143,8 @@ -
- -
-
- -
+ +