diff --git a/classes/controller/FrontController.php b/classes/controller/FrontController.php index 5252400ca9..daf8c31fa4 100644 --- a/classes/controller/FrontController.php +++ b/classes/controller/FrontController.php @@ -1216,6 +1216,42 @@ public function initHeader() 'WK_DISPLAY_PROPERTIES_LINK_IN_HEADER' => Configuration::get('WK_DISPLAY_PROPERTIES_LINK_IN_HEADER'), )); + if ($this->php_self === 'index') { + $mediaTypeInt = (int)(Configuration::get('QLO_HEADER_MEDIA_TYPE') ?: HotelHeaderImage::MEDIA_TYPE_IMAGE); + if ($mediaTypeInt === HotelHeaderImage::MEDIA_TYPE_VIDEO) { + $videoConfig = HotelHeaderImage::getVideoConfig(); + if ($videoConfig) { + $mimeMap = array('mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg'); + if ($videoConfig['source_type'] === 'upload') { + $ext = strtolower(pathinfo($videoConfig['name'], PATHINFO_EXTENSION)); + } else { + $urlPath = parse_url($videoConfig['name'], PHP_URL_PATH); + $ext = strtolower(pathinfo($urlPath ?: '', PATHINFO_EXTENSION)); + } + $videoConfig['mime_type'] = isset($mimeMap[$ext]) ? $mimeMap[$ext] : 'video/mp4'; + $headerMediaItems = array($videoConfig); + } else { + $headerMediaItems = array(); + } + } else { + $headerMediaItems = HotelHeaderImage::getItems(); + } + $this->context->smarty->assign(array( + 'QLO_HEADER_MEDIA_TYPE' => $mediaTypeInt, + 'QLO_HEADER_MEDIA_TYPE_IMAGE' => HotelHeaderImage::MEDIA_TYPE_IMAGE, + 'QLO_HEADER_MEDIA_TYPE_VIDEO' => HotelHeaderImage::MEDIA_TYPE_VIDEO, + 'WK_HEADER_NAV_TYPE_DOTS' => HotelHeaderImage::NAV_TYPE_DOTS, + 'QLO_HEADER_ANIM_TYPE_SLIDE' => HotelHeaderImage::ANIM_TYPE_SLIDE, + 'headerMediaItems' => $headerMediaItems, + 'headerSliderConfig' => array( + 'nav_type' => (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_DOTS), + 'auto_play' => (int)Configuration::get('QLO_HEADER_SLIDER_AUTO_PLAY'), + 'interval' => (int)Configuration::get('QLO_HEADER_SLIDER_INTERVAL'), + 'anim_type' => (int)(Configuration::get('QLO_HEADER_SLIDER_ANIM_TYPE') ?: HotelHeaderImage::ANIM_TYPE_SLIDE), + ), + )); + } + $this->context->smarty->assign($this->initLogoAndFavicon()); } diff --git a/modules/hotelreservationsystem/classes/HotelHeaderImage.php b/modules/hotelreservationsystem/classes/HotelHeaderImage.php new file mode 100644 index 0000000000..bdbd82eccf --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelHeaderImage.php @@ -0,0 +1,289 @@ + 'htl_header_image', + 'primary' => 'id_header_image', + 'multilang' => true, + 'fields' => array( + 'name' => array('type' => self::TYPE_STRING, 'size' => 512), + 'tag_line' => array('type' => self::TYPE_STRING, 'lang' => true, 'size' => 512), + 'tag_line_color' => array('type' => self::TYPE_STRING, 'validate' => 'isColor', 'size' => 7), + 'tag_line_font_size' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'tag_line_font_weight' => array('type' => self::TYPE_STRING, 'size' => 10), + 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + ), + ); + + public function __construct($id = null, $id_lang = null, $id_shop = null) + { + $this->image_dir = _PS_IMG_DIR_.'hotel_header_media/'; + parent::__construct($id, $id_lang, $id_shop); + } + + /** + * @inheritdoc + */ + public function delete() + { + if (!$this->deleteImageFile() + || !parent::delete() + || !$this->cleanPositions() + ) { + return false; + } + return true; + } + + /** + * Deletes the physical image file from disk. + * + * @return bool + */ + public function deleteImageFile() + { + if (!$this->name) { + return true; + } + $filePath = $this->image_dir.$this->name; + if (file_exists($filePath) && !unlink($filePath)) { + return false; + } + return true; + } + + /** + * Fetches image rows, optionally filtered by active status. + * + * @param int|null $active 1 = active only, 0 = inactive only, null = all + * @param int|null $idLang Language id for tag_line; null = context default + * @param bool $withAllLangs true = also include tag_lines[id_lang] map (admin edit forms) + * @return array + */ + public static function getItems($active = 1, $idLang = null, $withAllLangs = false) + { + if (!$idLang) { + $idLang = (int)Context::getContext()->language->id; + } + + if (!$withAllLangs) { + $sql = 'SELECT m.*, IFNULL(ml.`tag_line`, \'\') AS `tag_line` + FROM `'._DB_PREFIX_.'htl_header_image` m + LEFT JOIN `'._DB_PREFIX_.'htl_header_image_lang` ml + ON (m.`id_header_image` = ml.`id_header_image` + AND ml.`id_lang` = '.(int)$idLang.')'; + if ($active !== null) { + $sql .= ' WHERE m.`active` = '.(int)$active; + } + $sql .= ' ORDER BY m.`position` ASC'; + return Db::getInstance()->executeS($sql) ?: array(); + } + + $sql = 'SELECT m.*, ml.`id_lang`, IFNULL(ml.`tag_line`, \'\') AS `lang_tag_line` + FROM `'._DB_PREFIX_.'htl_header_image` m + LEFT JOIN `'._DB_PREFIX_.'htl_header_image_lang` ml + ON m.`id_header_image` = ml.`id_header_image`'; + if ($active !== null) { + $sql .= ' WHERE m.`active` = '.(int)$active; + } + $sql .= ' ORDER BY m.`position` ASC, ml.`id_lang` ASC'; + + $rows = Db::getInstance()->executeS($sql); + if (!$rows) { + return array(); + } + + $itemsMap = array(); + foreach ($rows as $row) { + $id = (int)$row['id_header_image']; + if (!isset($itemsMap[$id])) { + $itemsMap[$id] = $row; + $itemsMap[$id]['tag_line'] = ''; + $itemsMap[$id]['tag_lines'] = array(); + unset($itemsMap[$id]['id_lang'], $itemsMap[$id]['lang_tag_line']); + } + if (isset($row['id_lang'])) { + $langId = (int)$row['id_lang']; + $tagLine = $row['lang_tag_line']; + $itemsMap[$id]['tag_lines'][$langId] = $tagLine; + if ($langId === (int)$idLang) { + $itemsMap[$id]['tag_line'] = $tagLine; + } + } + } + + return array_values($itemsMap); + } + + /** + * Returns the next available position value (MAX + 1). + * + * @return int + */ + public function getHigherPosition() + { + $position = Db::getInstance()->getValue( + 'SELECT MAX(`position`) FROM `'._DB_PREFIX_.'htl_header_image`' + ); + return (is_numeric($position) ? (int)$position : -1) + 1; + } + + /** + * Resequences position values to be contiguous (0-based) after a deletion. + * Uses a single CASE...WHEN UPDATE instead of N individual queries. + * + * @return bool + */ + public function cleanPositions() + { + $items = Db::getInstance()->executeS( + 'SELECT `id_header_image` FROM `'._DB_PREFIX_.'htl_header_image` ORDER BY `position` ASC' + ); + if (!$items) { + return true; + } + $cases = ''; + $ids = array(); + foreach ($items as $i => $item) { + $id = (int)$item['id_header_image']; + $cases .= ' WHEN '.$id.' THEN '.$i; + $ids[] = $id; + } + return (bool)Db::getInstance()->execute( + 'UPDATE `'._DB_PREFIX_.'htl_header_image` + SET `position` = CASE `id_header_image`'.$cases.' END + WHERE `id_header_image` IN ('.implode(',', $ids).')' + ); + } + + /** + * Returns the stored video configuration, or null when no video is set. + * + * @return array|null ['source_type' => 'upload'|'url', 'name' => string] + */ + public static function getVideoConfig() + { + $name = Configuration::get('QLO_HEADER_VIDEO_NAME'); + $sourceType = Configuration::get('QLO_HEADER_VIDEO_SOURCE_TYPE'); + if (!$sourceType || !$name) { + return null; + } + return array('source_type' => $sourceType, 'name' => $name); + } + + /** + * Persists video source type and name/url to Configuration. + * + * @param string $sourceType 'upload' or 'url' + * @param string $name Filename (upload) or full URL (url) + * @return bool + */ + public static function saveVideoConfig($sourceType, $name) + { + return Configuration::updateValue('QLO_HEADER_VIDEO_SOURCE_TYPE', pSQL($sourceType)) + && Configuration::updateValue('QLO_HEADER_VIDEO_NAME', pSQL($name)); + } + + /** + * Clears video configuration and deletes the uploaded file from disk if applicable. + * + * @return bool + */ + public static function deleteVideoConfig() + { + $sourceType = Configuration::get('QLO_HEADER_VIDEO_SOURCE_TYPE'); + $name = Configuration::get('QLO_HEADER_VIDEO_NAME'); + if ($sourceType === 'upload' && $name) { + $filePath = _PS_IMG_DIR_.'hotel_header_media/'.$name; + if (file_exists($filePath)) { + unlink($filePath); + } + } + Configuration::updateValue('QLO_HEADER_VIDEO_SOURCE_TYPE', ''); + Configuration::updateValue('QLO_HEADER_VIDEO_NAME', ''); + return true; + } + + /** + * Creates the upload directory with security controls if it does not exist. + * + * @return bool + */ + public static function createMediaDirectory() + { + $dir = _PS_IMG_DIR_.'hotel_header_media/'; + if (!is_dir($dir) && !mkdir($dir, 0755, true)) { + return false; + } + if (!file_exists($dir.'index.php') && file_exists(_PS_IMG_DIR_.'index.php')) { + copy(_PS_IMG_DIR_.'index.php', $dir.'index.php'); + } + if (!file_exists($dir.'.htaccess')) { + file_put_contents( + $dir.'.htaccess', + "Options -ExecCGI\nAddHandler cgi-script .php .php3 .php4 .php5 .phtml .pl .py .jsp .asp .cgi\nphp_flag engine off\n" + ); + } + return true; + } +} diff --git a/modules/hotelreservationsystem/classes/HotelHelper.php b/modules/hotelreservationsystem/classes/HotelHelper.php index f7f4f1c2b1..0687b9c0d3 100644 --- a/modules/hotelreservationsystem/classes/HotelHelper.php +++ b/modules/hotelreservationsystem/classes/HotelHelper.php @@ -1057,7 +1057,48 @@ public function insertDefaultHotelEntries() Configuration::updateValue('WK_TITLE_HEADER_BLOCK', $home_banner_default_title); Configuration::updateValue('WK_CONTENT_HEADER_BLOCK', $home_banner_default_content); - Configuration::updateValue('WK_HOTEL_HEADER_IMAGE', 'hotel_header_image.jpg'); + Configuration::updateValue('QLO_HEADER_MEDIA_TYPE', HotelHeaderImage::MEDIA_TYPE_IMAGE); + Configuration::updateValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_DOTS); + Configuration::updateValue('QLO_HEADER_SLIDER_AUTO_PLAY', 1); + Configuration::updateValue('QLO_HEADER_SLIDER_INTERVAL', 5000); + Configuration::updateValue('QLO_HEADER_SLIDER_ANIM_TYPE', HotelHeaderImage::ANIM_TYPE_SLIDE); + Configuration::updateValue('QLO_HOTEL_NAME_ENABLE', 0); + Configuration::updateValue('QLO_HEADER_CONTENT_ALIGN', HotelHeaderImage::CONTENT_ALIGN_CENTER); + Configuration::updateValue('QLO_HEADER_VIDEO_SOURCE_TYPE', ''); + Configuration::updateValue('QLO_HEADER_VIDEO_NAME', ''); + HotelHeaderImage::createMediaDirectory(); + $defaultImgSrc = _PS_IMG_DIR_.'hotel_header_image.jpg'; + $defaultImgDest = 'default_hotel_header_image.jpg'; + $mediaDir = _PS_IMG_DIR_.'hotel_header_media/'; + if (file_exists($defaultImgSrc) && !file_exists($mediaDir.$defaultImgDest)) { + @copy($defaultImgSrc, $mediaDir.$defaultImgDest); + } + if (file_exists($mediaDir.$defaultImgDest)) { + Db::getInstance()->execute( + 'INSERT INTO `'._DB_PREFIX_.'htl_header_image` + (`name`, `position`, `active`, `date_add`, `date_upd`) + VALUES (\''.pSQL($defaultImgDest).'\', 0, 1, NOW(), NOW())' + ); + $newId = (int)Db::getInstance()->Insert_ID(); + if ($newId) { + $defaultTagLines = array( + 'en' => 'A place where comfort and luxury are blended with nature!', + 'nl' => 'Een plek waar comfort en luxe worden gecombineerd met de natuur!', + 'fr' => 'Un endroit où le confort et le luxe se mêlent à la nature!', + 'de' => 'Ein Ort, an dem Komfort und Luxus mit der Natur verschmelzen!', + 'ru' => 'Место, где комфорт и роскошь сочетаются с природой!', + 'es' => '¡Un lugar donde el confort y el lujo se mezclan con la naturaleza!', + ); + foreach (Language::getLanguages(false) as $lang) { + $tagLine = isset($defaultTagLines[$lang['iso_code']]) ? $defaultTagLines[$lang['iso_code']] : $defaultTagLines['en']; + Db::getInstance()->execute( + 'INSERT INTO `'._DB_PREFIX_.'htl_header_image_lang` + (`id_header_image`, `id_lang`, `tag_line`) + VALUES ('.$newId.', '.(int)$lang['id_lang'].', \''.pSQL($tagLine).'\')' + ); + } + } + } Configuration::updateValue('WK_ALLOW_ADVANCED_PAYMENT', 1); Configuration::updateValue('WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT', 10); Configuration::updateValue('WK_ADVANCED_PAYMENT_INC_TAX', 1); @@ -1080,14 +1121,6 @@ public function insertDefaultHotelEntries() // lang fields $languages = Language::getLanguages(false); - $htlTagLineLang = array( - 'en' => 'A place where comfort and luxury are blended with nature!', - 'nl' => 'Een plek waar comfort en luxe worden gecombineerd met de natuur!', - 'fr' => 'Un endroit où le confort et le luxe se mêlent à la nature!', - 'de' => 'Ein Ort, an dem Komfort und Luxus mit der Natur verschmelzen!', - 'ru' => 'Место, где комфорт и роскошь сочетаются с природой!', - 'es' => '¡Un lugar donde el confort y el lujo se mezclan con la naturaleza!', - ); $htlShortDescLang = array( 'en' => 'We offer elegant rooms, gourmet dining, and attentive service for a memorable stay.', @@ -1106,25 +1139,21 @@ public function insertDefaultHotelEntries() 'es' => 'ft', ); $WK_HTL_CHAIN_NAME = array(); - $WK_HTL_TAG_LINE = array(); $WK_HTL_SHORT_DESC = array(); $defaultDimensionUnit = array(); foreach ($languages as $lang) { - if (isset($htlTagLineLang[$lang['iso_code']])) { - $WK_HTL_TAG_LINE[$lang['id_lang']] = $htlTagLineLang[$lang['iso_code']]; + if (isset($htlShortDescLang[$lang['iso_code']])) { $WK_HTL_SHORT_DESC[$lang['id_lang']] = $htlShortDescLang[$lang['iso_code']]; $WK_HTL_CHAIN_NAME[$lang['id_lang']] = $homeBannerTitleLang[$lang['iso_code']]; $defaultDimensionUnit[$lang['id_lang']] = $defaultDimensionUnitLang[$lang['iso_code']]; } else { $defaultDimensionUnit[$lang['id_lang']] = $defaultDimensionUnitLang['en']; $WK_HTL_CHAIN_NAME[$lang['id_lang']] = $homeBannerTitleLang['en']; - $WK_HTL_TAG_LINE[$lang['id_lang']] = $htlTagLineLang['en']; $WK_HTL_SHORT_DESC[$lang['id_lang']] = $htlShortDescLang['en']; } } Configuration::updateValue('WK_HTL_CHAIN_NAME', $WK_HTL_CHAIN_NAME); - Configuration::updateValue('WK_HTL_TAG_LINE', $WK_HTL_TAG_LINE); Configuration::updateValue('WK_HTL_SHORT_DESC', $WK_HTL_SHORT_DESC); Configuration::updateValue('WK_DIMENSION_UNIT', $defaultDimensionUnit); diff --git a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php index 4dfdabc016..9892bcc154 100644 --- a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php +++ b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php @@ -513,12 +513,32 @@ public function getModuleSql() PRIMARY KEY (`id_room_type_bed_type`) ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_header_image` ( + `id_header_image` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(512) NOT NULL, + `tag_line_color` VARCHAR(7) NOT NULL DEFAULT '#ffffff', + `tag_line_font_size` TINYINT(3) UNSIGNED NOT NULL DEFAULT '16', + `tag_line_font_weight` VARCHAR(10) NOT NULL DEFAULT '400', + `position` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1', + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + PRIMARY KEY (`id_header_image`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_header_image_lang` ( + `id_header_image` INT(10) UNSIGNED NOT NULL, + `id_lang` INT(11) NOT NULL, + `tag_line` VARCHAR(512) NOT NULL DEFAULT '', + PRIMARY KEY (`id_header_image`, `id_lang`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8;", + "INSERT INTO `"._DB_PREFIX_."htl_settings_link` (`id_settings_link`, `icon`, `link`, `new_window`, `position`, `unremovable`, `active`, `date_add`, `date_upd`) VALUES (1, 'icon-cogs', 'index.php?controller=AdminHotelGeneralSettings', 0, 0, 1, 1, NOW(), NOW()), (2, 'icon-dollar', 'index.php?controller=AdminHotelFeaturePricesSettings', 0, 2, 1, 1, NOW(), NOW()), (3, 'icon-plus-square', 'index.php?controller=AdminRoomTypeGlobalDemand', 0, 3, 1, 1, NOW(), NOW()), (4, 'icon-file-text', 'index.php?controller=AdminAboutHotelBlockSetting', 0, 4, 0, 1, NOW(), NOW()), - (5, 'icon-th-list', 'index.php?controller=AdminFeaturesModuleSetting', 0, 5, 0, 1, NOW(), NOW());", + (5, 'icon-th-list', 'index.php?controller=AdminFeaturesModuleSetting', 0, 5, 0, 1, NOW(), NOW()), + (6, 'icon-picture-o', 'index.php?controller=AdminHotelHeaderImage', 0, 6, 1, 1, NOW(), NOW());", "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_settings_link_lang` ( `id_settings_link` int(10) unsigned NOT NULL, @@ -552,6 +572,11 @@ public function getModuleSql() SELECT 5, `id_lang`, 'Hotel Amenities Block', 'Configure Hotels Amenities settings. You can display hotel amenities images using this block. This block will be displayed on home page.' FROM `"._DB_PREFIX_."lang` ORDER BY `id_lang`;", + + "INSERT INTO `"._DB_PREFIX_."htl_settings_link_lang` (`id_settings_link`, `id_lang`, `name`, `hint`) + SELECT 6, `id_lang`, 'Landing Page Header Media', 'Configure and manage header images or videos displayed on the home page.' + FROM `"._DB_PREFIX_."lang` + ORDER BY `id_lang`;", "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_connected_room` ( `id_connected_room` int(11) NOT NULL AUTO_INCREMENT, `id_room` int(11) NOT NULL, @@ -627,6 +652,8 @@ public function dropTables() `'._DB_PREFIX_.'htl_access`, `'._DB_PREFIX_.'htl_settings_link`, `'._DB_PREFIX_.'htl_settings_link_lang`, + `'._DB_PREFIX_.'htl_header_image`, + `'._DB_PREFIX_.'htl_header_image_lang`, `'._DB_PREFIX_.'htl_connected_room`' ); } diff --git a/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php b/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php index c59bd79a8b..42523f12d2 100644 --- a/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php +++ b/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php @@ -30,10 +30,6 @@ public function __construct() $this->bootstrap = true; parent::__construct(); - $psImgUrl = $this->context->link->getMediaLink(_PS_IMG_.Configuration::get('WK_HOTEL_HEADER_IMAGE')); - if ($imgExist = (bool)Tools::file_get_contents($psImgUrl)) { - $image = ''; - } $objHotelInfo = new HotelBranchInformation(); if (!$hotelsInfo = $objHotelInfo->hotelBranchesInfo(false, 1)) { $hotelsInfo = array(); @@ -136,14 +132,6 @@ public function __construct() 'validation' => 'isGenericName', 'hint' => $this->l('Enter Hotel name in case of single hotel or enter your hotels chain name in case of multiple hotels.'), ), - 'WK_HTL_TAG_LINE' => array( - 'title' => $this->l('Hotel Tag Line'), - 'type' => 'textareaLang', - 'lang' => true, - 'required' => true, - 'validation' => 'isGenericName', - 'hint' => $this->l('This will display hotel tag line in hotel page.'), - ), 'WK_HTL_SHORT_DESC' => array( 'title' => $this->l('Hotel Short Description'), 'type' => 'textareaLang', @@ -166,14 +154,6 @@ public function __construct() 'hint' => $this->l('The year when your hotel site was launched.'), 'type' => 'text', 'class' => 'fixed-width-xxl', - ), - 'WK_HTL_HEADER_IMAGE' => array( - 'title' => $this->l('Header Background Image'), - 'type' => 'file', - 'image' => $imgExist ? $image : false, - 'hint' => $this->l('This image appears as header background image on home page.'), - 'name' => 'WK_HOTEL_HEADER_IMAGE', - 'url' => _PS_IMG_, ), 'WK_DISPLAY_PROPERTIES_LINK_IN_HEADER' => array( 'title' => $this->l('Display Our Properties link in Header'), @@ -569,17 +549,6 @@ public function postProcess() } } } - if (!trim(Tools::getValue('WK_HTL_TAG_LINE_'.$defaultLangId))) { - $this->errors[] = $this->l('Hotel tag line is required at least in ').$objDefaultLanguage['name']; - } else { - foreach ($languages as $lang) { - if (trim(Tools::getValue('WK_HTL_TAG_LINE_'.$lang['id_lang']))) { - if (!Validate::isGenericName(Tools::getValue('WK_HTL_TAG_LINE_'.$lang['id_lang']))) { - $this->errors[] = $this->l('Invalid Hotel tag line in ').$lang['name']; - } - } - } - } if (!trim(Tools::getValue('WK_HTL_SHORT_DESC_'.$defaultLangId))) { $this->errors[] = $this->l('Hotel short description is required at least in '). $objDefaultLanguage['name']; @@ -592,24 +561,6 @@ public function postProcess() } } } - if ($_FILES['WK_HOTEL_HEADER_IMAGE']['name']) { - if ($error = ImageManager::validateUpload($_FILES['WK_HOTEL_HEADER_IMAGE'], Tools::getMaxUploadSize())) { - $this->errors[] = $error; - } - - if (!count($this->errors)) { - $file_name = 'hotel_header_image_'.time().'.jpg'; - $img_path = _PS_IMG_DIR_.$file_name; - - if (ImageManager::resize($_FILES['WK_HOTEL_HEADER_IMAGE']['tmp_name'], $img_path)) { - $olderHeaderImg = _PS_IMG_DIR_.Configuration::get('WK_HOTEL_HEADER_IMAGE'); - Configuration::updateValue('WK_HOTEL_HEADER_IMAGE', $file_name); - Tools::deleteFile($olderHeaderImg); - } else { - $this->errors[] = $this->l('Some error occured while uoploading image.Please try again.'); - } - } - } if (!Validate::isUnsignedInt(Tools::getValue('WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT'))) { $this->errors[] = $this->l('Invalid minimum partial payment percentage.'); } elseif (Tools::getValue('WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT') <= 0) { @@ -662,11 +613,6 @@ public function postProcess() Tools::getValue('WK_HTL_CHAIN_NAME_'.$defaultLangId) ); } - if (!trim(Tools::getValue('WK_HTL_TAG_LINE_'.$lang['id_lang']))) { - $_POST['WK_HTL_TAG_LINE_'.$lang['id_lang']] = trim( - Tools::getValue('WK_HTL_TAG_LINE_'.$defaultLangId) - ); - } if (!trim(Tools::getValue('WK_HTL_SHORT_DESC_'.$lang['id_lang']))) { $_POST['WK_HTL_SHORT_DESC_'.$lang['id_lang']] = trim( Tools::getValue('WK_HTL_SHORT_DESC_'.$defaultLangId) diff --git a/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php b/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php new file mode 100644 index 0000000000..3e0b5a3728 --- /dev/null +++ b/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php @@ -0,0 +1,661 @@ +table = 'htl_header_image'; + $this->className = 'HotelHeaderImage'; + $this->bootstrap = true; + $this->identifier = 'id_header_image'; + $this->lang = true; + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->l('Delete selected'), + 'confirm' => $this->l('Delete selected images? This cannot be undone.'), + 'icon' => 'icon-trash', + ), + ); + } + + public function initContent() + { + if (!$this->ajax) { + $this->display = 'view'; + } + parent::initContent(); + } + + public function initToolbar() + { + parent::initToolbar(); + $this->page_header_toolbar_title = $this->l('Header Image Configuration'); + unset($this->toolbar_btn['back']); + } + + public function renderView() + { + $this->meta_title = $this->l('Header Image Configuration'); + $mediaType = (int)Tools::getValue( + 'QLO_HEADER_MEDIA_TYPE', + (int)(Configuration::get('QLO_HEADER_MEDIA_TYPE') ?: HotelHeaderImage::MEDIA_TYPE_IMAGE) + ); + $languages = Language::getLanguages(false); + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + + $imageItems = HotelHeaderImage::getItems(null, $defaultLangId, true); + $shopId = (int)$this->context->shop->id; + foreach ($imageItems as &$item) { + $item['tag_lines_json'] = json_encode((object)$item['tag_lines']); + $srcPath = _PS_IMG_DIR_.'hotel_header_media/'.$item['name']; + $cacheName = 'htl_header_image_mini_'.(int)$item['id_header_image'].'_'.$shopId.'.jpg'; + $item['thumb'] = ImageManager::thumbnail($srcPath, $cacheName, 45, 'jpg', false); + } + unset($item); + + $videoItem = HotelHeaderImage::getVideoConfig(); + $videoMimeType = 'video/mp4'; + if ($videoItem) { + if ($videoItem['source_type'] === 'upload') { + $ext = strtolower(pathinfo($videoItem['name'], PATHINFO_EXTENSION)); + } else { + $urlPath = parse_url($videoItem['name'], PHP_URL_PATH); + $ext = strtolower(pathinfo($urlPath ?: '', PATHINFO_EXTENSION)); + } + $mimeMap = array('mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg'); + $videoMimeType = isset($mimeMap[$ext]) ? $mimeMap[$ext] : 'video/mp4'; + } + + Media::addJsDef(array( + 'qloHmCurrentIndex' => self::$currentIndex, + 'qloHmToken' => $this->token, + 'qloHmMediaType' => $mediaType, + 'qloHmMediaTypeImage' => HotelHeaderImage::MEDIA_TYPE_IMAGE, + 'qloHmMediaTypeVideo' => HotelHeaderImage::MEDIA_TYPE_VIDEO, + 'qloHmMaxUpload' => Tools::getMaxUploadSize((int)Configuration::get('PS_LIMIT_UPLOAD_IMAGE_VALUE') * 1024 * 1024), + 'qloHmMaxVideoUpload' => Tools::getMaxUploadSize(), + 'qloHmDefaultLangId' => $defaultLangId, + 'qloHmI18n' => array( + 'noFileSelected' => $this->l('Please select at least one image file.'), + 'deleteFailed' => $this->l('Delete failed.'), + 'requestFailed' => $this->l('Request failed.'), + 'imageUploadedSuccess'=> $this->l('Image uploaded successfully.'), + 'imageUpdatedSuccess' => $this->l('Image updated successfully.'), + 'uploadFailed' => $this->l('Upload failed.'), + 'updateFailed' => $this->l('Update failed.'), + 'editLabel' => $this->l('Edit'), + 'deleteImageLabel' => $this->l('Delete this image'), + 'fileTooLarge' => $this->l('File exceeds the maximum allowed upload size.'), + ), + )); + + $this->tpl_view_vars = array( + 'mediaType' => $mediaType, + 'imageItems' => $imageItems, + 'videoItem' => $videoItem, + 'videoMimeType' => $videoMimeType, + 'config' => array( + 'QLO_HEADER_MEDIA_TYPE' => $mediaType, + 'QLO_HEADER_SLIDER_NAV_TYPE' => (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_DOTS)), + 'QLO_HEADER_SLIDER_AUTO_PLAY' => (int)Tools::getValue('QLO_HEADER_SLIDER_AUTO_PLAY', (int)Configuration::get('QLO_HEADER_SLIDER_AUTO_PLAY')), + 'QLO_HEADER_SLIDER_INTERVAL' => (int)Tools::getValue('QLO_HEADER_SLIDER_INTERVAL', (int)Configuration::get('QLO_HEADER_SLIDER_INTERVAL') ?: 5000), + 'QLO_HEADER_SLIDER_ANIM_TYPE' => (int)Tools::getValue('QLO_HEADER_SLIDER_ANIM_TYPE', (int)(Configuration::get('QLO_HEADER_SLIDER_ANIM_TYPE') ?: HotelHeaderImage::ANIM_TYPE_SLIDE)), + 'QLO_HOTEL_NAME_ENABLE' => (int)Tools::getValue('QLO_HOTEL_NAME_ENABLE', (int)Configuration::get('QLO_HOTEL_NAME_ENABLE')), + 'QLO_HEADER_CONTENT_ALIGN' => (int)Tools::getValue('QLO_HEADER_CONTENT_ALIGN', (int)(Configuration::get('QLO_HEADER_CONTENT_ALIGN') ?: HotelHeaderImage::CONTENT_ALIGN_CENTER)), + ), + 'languages' => $languages, + 'defaultLangId' => $defaultLangId, + 'imgBaseUrl' => $this->context->link->getMediaLink(_PS_IMG_.'hotel_header_media/'), + 'maxUpload' => Tools::formatBytes(Tools::getMaxUploadSize()), + 'maxImageUpload' => Tools::formatBytes(Tools::getMaxUploadSize((int)Configuration::get('PS_LIMIT_UPLOAD_IMAGE_VALUE') * 1024 * 1024)), + ); + + return parent::renderView(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitHeaderMedia')) { + $this->processSaveSettings(); + } + parent::postProcess(); + } + + protected function processBulkDelete() + { + $ids = Tools::getValue($this->table.'Box', array()); + $activeImages = HotelHeaderImage::getItems(1); + $activeIds = array_column($activeImages, 'id_header_image'); + $activeToDelete = array_intersect(array_map('intval', (array)$ids), array_map('intval', $activeIds)); + + if (count($activeIds) - count($activeToDelete) < 1) { + $this->errors[] = $this->l('At least one active image is required.'); + return; + } + + parent::processBulkDelete(); + } + + protected function processBulkDisableSelection() + { + $ids = Tools::getValue($this->table.'Box', array()); + $activeImages = HotelHeaderImage::getItems(1); + $activeIds = array_column($activeImages, 'id_header_image'); + $activeToDisable = array_intersect(array_map('intval', (array)$ids), array_map('intval', $activeIds)); + + if (count($activeIds) - count($activeToDisable) < 1) { + $this->errors[] = $this->l('At least one image must remain active.'); + return; + } + + parent::processBulkDisableSelection(); + } + + protected function processSaveSettings() + { + $mediaType = (int)Tools::getValue('QLO_HEADER_MEDIA_TYPE', HotelHeaderImage::MEDIA_TYPE_IMAGE); + $autoPlay = (int)(bool)Tools::getValue('QLO_HEADER_SLIDER_AUTO_PLAY', 1); + $previousMediaType = (int)Configuration::get('QLO_HEADER_MEDIA_TYPE'); + + if (!in_array($mediaType, array(HotelHeaderImage::MEDIA_TYPE_IMAGE, HotelHeaderImage::MEDIA_TYPE_VIDEO))) { + $this->errors[] = $this->l('Invalid media type selected.'); + return; + } + + $existingVideo = HotelHeaderImage::getVideoConfig(); + $hasNewVideoFile = isset($_FILES['header_video_file']) && !empty($_FILES['header_video_file']['size']); + $hasNewVideoUrl = (Tools::getValue('source_type', '') === 'url' && trim(Tools::getValue('video_url', '')) !== ''); + + if ($mediaType === HotelHeaderImage::MEDIA_TYPE_VIDEO && !$existingVideo && !$hasNewVideoFile && !$hasNewVideoUrl) { + $this->errors[] = $this->l('Please upload or link a video before switching the header to Video mode.'); + return; + } + if ($mediaType === HotelHeaderImage::MEDIA_TYPE_IMAGE && !HotelHeaderImage::getItems(1)) { + $this->errors[] = $this->l('Please add at least one active image before switching the header to Image mode.'); + return; + } + if ($autoPlay) { + $interval = Tools::getValue('QLO_HEADER_SLIDER_INTERVAL', 5000); + if ((string)$interval !== '' && (!Validate::isUnsignedInt($interval) || (int)$interval < 500)) { + $this->errors[] = $this->l('Auto Slide Interval must be at least 500 milliseconds.'); + return; + } + } + + $navType = (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_DOTS); + if (!in_array($navType, array(HotelHeaderImage::NAV_TYPE_DOTS, HotelHeaderImage::NAV_TYPE_ARROWS, HotelHeaderImage::NAV_TYPE_BOTH))) { + $navType = HotelHeaderImage::NAV_TYPE_DOTS; + } + $animType = (int)Tools::getValue('QLO_HEADER_SLIDER_ANIM_TYPE', HotelHeaderImage::ANIM_TYPE_SLIDE); + if (!in_array($animType, array(HotelHeaderImage::ANIM_TYPE_SLIDE, HotelHeaderImage::ANIM_TYPE_FADE, HotelHeaderImage::ANIM_TYPE_ZOOM, HotelHeaderImage::ANIM_TYPE_BLUR))) { + $animType = HotelHeaderImage::ANIM_TYPE_SLIDE; + } + + if ($mediaType === HotelHeaderImage::MEDIA_TYPE_VIDEO) { + $imagesToDrop = array(); + if ($previousMediaType === HotelHeaderImage::MEDIA_TYPE_IMAGE) { + $imagesToDrop = HotelHeaderImage::getItems(null); + array_shift($imagesToDrop); + } + if ($imagesToDrop && Tools::getValue('confirm_delete_images') !== '1') { + $this->errors[] = $this->l('Switching to Video will delete all images except the first one. Please confirm this action.'); + return; + } + + $this->processSaveVideo(); + + if (!count($this->errors)) { + foreach ($imagesToDrop as $imgData) { + $obj = new HotelHeaderImage((int)$imgData['id_header_image']); + if (Validate::isLoadedObject($obj)) { + $obj->delete(); + } + } + } + } + + $contentAlign = (int)Tools::getValue('QLO_HEADER_CONTENT_ALIGN', HotelHeaderImage::CONTENT_ALIGN_CENTER); + if (!in_array($contentAlign, array(HotelHeaderImage::CONTENT_ALIGN_LEFT, HotelHeaderImage::CONTENT_ALIGN_CENTER, HotelHeaderImage::CONTENT_ALIGN_RIGHT))) { + $contentAlign = HotelHeaderImage::CONTENT_ALIGN_CENTER; + } + + if (!count($this->errors)) { + Configuration::updateValue('QLO_HEADER_MEDIA_TYPE', $mediaType); + Configuration::updateValue('QLO_HOTEL_NAME_ENABLE', (int)(bool)Tools::getValue('QLO_HOTEL_NAME_ENABLE', 0)); + Configuration::updateValue('QLO_HEADER_CONTENT_ALIGN', $contentAlign); + Configuration::updateValue('QLO_HEADER_SLIDER_NAV_TYPE', $navType); + Configuration::updateValue('QLO_HEADER_SLIDER_AUTO_PLAY', $autoPlay); + if ($autoPlay) { + Configuration::updateValue('QLO_HEADER_SLIDER_INTERVAL', (int)Tools::getValue('QLO_HEADER_SLIDER_INTERVAL', 5000)); + Configuration::updateValue('QLO_HEADER_SLIDER_ANIM_TYPE', $animType); + } + Tools::redirectAdmin(self::$currentIndex.'&conf=4&token='.$this->token); + } + } + + protected function processSaveVideo() + { + $sourceType = Tools::getValue('source_type', 'upload'); + $videoUrl = trim(Tools::getValue('video_url', '')); + $file = isset($_FILES['header_video_file']) ? $_FILES['header_video_file'] : null; + $hasNewFile = ($file && isset($file['error']) && $file['error'] === UPLOAD_ERR_OK && $file['size'] > 0); + $hasNewUrl = ($sourceType === 'url' && $videoUrl !== ''); + + if ($sourceType === 'url' && $videoUrl === '') { + $this->errors[] = $this->l('Please enter a video URL.'); + return; + } + if ($hasNewUrl && !Validate::isAbsoluteUrl($videoUrl)) { + $this->errors[] = $this->l('Please enter a valid video URL.'); + return; + } + if ($hasNewFile) { + $allowedExts = array('mp4', 'webm', 'ogg'); + $allowedMimes = array('video/mp4', 'video/webm', 'video/ogg', 'video/x-matroska'); + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!in_array($ext, $allowedExts)) { + $this->errors[] = $this->l('Only .mp4, .webm, and .ogg video formats are allowed.'); + return; + } + // Use actual disk size — $_FILES['size'] is user-supplied and can be spoofed. + if (filesize($file['tmp_name']) > Tools::getMaxUploadSize()) { + $this->errors[] = $this->l('Video file exceeds the maximum allowed upload size.'); + return; + } + if (!function_exists('finfo_open')) { + $this->errors[] = $this->l('Server cannot verify file type. Please enable the fileinfo PHP extension.'); + return; + } + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $detectedMime = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + if (!in_array($detectedMime, $allowedMimes)) { + $this->errors[] = $this->l('Invalid video file type detected.'); + return; + } + } + + if ($hasNewUrl) { + HotelHeaderImage::deleteVideoConfig(); + HotelHeaderImage::saveVideoConfig('url', $videoUrl); + } elseif ($hasNewFile) { + if (!HotelHeaderImage::createMediaDirectory()) { + $this->errors[] = $this->l('Could not create the media directory.'); + return; + } + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + do { + $uniqueName = bin2hex(random_bytes(16)).'.'.$ext; + } while (file_exists(_PS_IMG_DIR_.'hotel_header_media/'.$uniqueName)); + if (!move_uploaded_file($file['tmp_name'], _PS_IMG_DIR_.'hotel_header_media/'.$uniqueName)) { + $this->errors[] = $this->l('Failed to save video file.'); + return; + } + HotelHeaderImage::deleteVideoConfig(); + HotelHeaderImage::saveVideoConfig('upload', $uniqueName); + } + } + + public function ajaxProcessUploadImage() + { + $response = array('errors' => array(), 'success' => false); + $file = isset($_FILES['header_image_file']) ? $_FILES['header_image_file'] : null; + + if (!$file || !$file['size']) { + $response['errors'][] = $this->l('No file received.'); + $this->ajaxDie(json_encode($response)); + } + + if ($error = ImageManager::validateUpload($file, Tools::getMaxUploadSize((int)Configuration::get('PS_LIMIT_UPLOAD_IMAGE_VALUE') * 1024 * 1024))) { + $response['errors'][] = $error; + $this->ajaxDie(json_encode($response)); + } + + if (!HotelHeaderImage::createMediaDirectory()) { + $response['errors'][] = $this->l('Could not create the media directory.'); + $this->ajaxDie(json_encode($response)); + } + + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + $isGif = ($ext === 'gif'); + $outExt = $isGif ? 'gif' : 'jpg'; + do { + $uniqueName = bin2hex(random_bytes(16)).'.'.$outExt; + } while (file_exists(_PS_IMG_DIR_.'hotel_header_media/'.$uniqueName)); + + $destPath = _PS_IMG_DIR_.'hotel_header_media/'.$uniqueName; + $saved = $isGif + ? (bool)move_uploaded_file($file['tmp_name'], $destPath) + : (bool)ImageManager::resize($file['tmp_name'], $destPath); + + if (!$saved) { + $response['errors'][] = $this->l('Failed to save image file.'); + $this->ajaxDie(json_encode($response)); + } + + $tagLineByLang = array(); + foreach (Language::getLanguages(false) as $lang) { + $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + } + foreach ($tagLineByLang as $tagLineValue) { + if (!Validate::isGenericName($tagLineValue)) { + if (file_exists($destPath)) { + unlink($destPath); + } + $response['errors'][] = $this->l('Invalid tag line. Characters < > = { } are not allowed.'); + $this->ajaxDie(json_encode($response)); + } + } + + $tagLineColor = Tools::getValue('tag_line_color', '#ffffff'); + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $tagLineColor)) { + $response['errors'][] = $this->l('Invalid tag line color. Use a 6-digit hex value (e.g. #ffffff).'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontSize = (int)Tools::getValue('tag_line_font_size', 16); + if ($tagLineFontSize < 8 || $tagLineFontSize > 72) { + $response['errors'][] = $this->l('Tag line font size must be between 8 and 72 pixels.'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontWeight = Tools::getValue('tag_line_font_weight', '400'); + if (!in_array($tagLineFontWeight, array('300', '400', '600', '700'))) { + $response['errors'][] = $this->l('Invalid tag line font weight.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage(); + $objImage->name = $uniqueName; + $objImage->position = $objImage->getHigherPosition(); + $objImage->active = (int)(bool)Tools::getValue('active', 1); + $objImage->tag_line = $tagLineByLang; + $objImage->tag_line_color = $tagLineColor; + $objImage->tag_line_font_size = $tagLineFontSize; + $objImage->tag_line_font_weight = $tagLineFontWeight; + + if (!$objImage->save()) { + if (file_exists($destPath)) { + unlink($destPath); + } + $response['errors'][] = $this->l('Failed to save image record.'); + $this->ajaxDie(json_encode($response)); + } + + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + $response['success'] = true; + $response['id'] = (int)$objImage->id; + $response['active'] = (int)$objImage->active; + $response['imgUrl'] = $this->context->link->getMediaLink(_PS_IMG_.'hotel_header_media/'.$uniqueName); + $response['tag_line'] = isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : ''; + $response['tag_lines_json'] = json_encode((object)$tagLineByLang); + $response['tag_line_color'] = $tagLineColor; + $response['tag_line_font_size'] = $tagLineFontSize; + $response['tag_line_font_weight'] = $tagLineFontWeight; + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessEditImage() + { + $response = array('errors' => array(), 'success' => false); + $id = (int)Tools::getValue('id_header_image'); + + if (!$id) { + $response['errors'][] = $this->l('Invalid item ID.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage($id); + if (!Validate::isLoadedObject($objImage)) { + $response['errors'][] = $this->l('Image not found.'); + $this->ajaxDie(json_encode($response)); + } + + $tagLineByLang = array(); + foreach (Language::getLanguages(false) as $lang) { + $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + } + + $activeVal = Tools::getValue('active'); + if ($activeVal !== false) { + $newActive = (int)(bool)$activeVal; + if ($newActive === 0 && (int)$objImage->active === 1) { + $activeImages = HotelHeaderImage::getItems(1); + if (is_array($activeImages) && count($activeImages) <= 1) { + $response['errors'][] = $this->l('At least one image must remain active.'); + $this->ajaxDie(json_encode($response)); + } + } + $objImage->active = $newActive; + } + + $tagLineColor = Tools::getValue('tag_line_color', $objImage->tag_line_color); + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $tagLineColor)) { + $response['errors'][] = $this->l('Invalid tag line color. Use a 6-digit hex value (e.g. #ffffff).'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontSize = (int)Tools::getValue('tag_line_font_size', $objImage->tag_line_font_size); + if ($tagLineFontSize < 8 || $tagLineFontSize > 72) { + $response['errors'][] = $this->l('Tag line font size must be between 8 and 72 pixels.'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontWeight = Tools::getValue('tag_line_font_weight', $objImage->tag_line_font_weight); + if (!in_array($tagLineFontWeight, array('300', '400', '600', '700'))) { + $response['errors'][] = $this->l('Invalid tag line font weight.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage->tag_line = $tagLineByLang; + $objImage->tag_line_color = $tagLineColor; + $objImage->tag_line_font_size = $tagLineFontSize; + $objImage->tag_line_font_weight = $tagLineFontWeight; + if (!$objImage->save()) { + $response['errors'][] = $this->l('Failed to update image.'); + $this->ajaxDie(json_encode($response)); + } + + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + $response['success'] = true; + $response['active'] = (int)$objImage->active; + $response['confirmations'] = $this->l('Image updated successfully.'); + $response['tag_line'] = isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : ''; + $response['tag_lines_json'] = json_encode((object)$tagLineByLang); + $response['tag_line_color'] = $tagLineColor; + $response['tag_line_font_size'] = $tagLineFontSize; + $response['tag_line_font_weight'] = $tagLineFontWeight; + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessDeleteMedia() + { + $response = array('errors' => array(), 'success' => false); + $id = (int)Tools::getValue('id_header_image'); + + if (!$id) { + $response['errors'][] = $this->l('Invalid item ID.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage($id); + if (!Validate::isLoadedObject($objImage)) { + $response['errors'][] = $this->l('Image not found.'); + $this->ajaxDie(json_encode($response)); + } + + if ($objImage->active) { + $activeImages = HotelHeaderImage::getItems(1); + if (is_array($activeImages) && count($activeImages) <= 1) { + $response['errors'][] = $this->l('At least one active image is required'); + $this->ajaxDie(json_encode($response)); + } + } + + if (!$objImage->delete()) { + $response['errors'][] = $this->l('Unable to delete image.'); + $this->ajaxDie(json_encode($response)); + } + + $response['success'] = true; + $response['confirmations'] = $this->l('Image deleted successfully.'); + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessDeleteVideo() + { + $response = array('errors' => array(), 'success' => false); + + if (!HotelHeaderImage::getVideoConfig()) { + $response['errors'][] = $this->l('No video is currently set.'); + $this->ajaxDie(json_encode($response)); + } + + HotelHeaderImage::deleteVideoConfig(); + $response['success'] = true; + $response['confirmations'] = $this->l('Video deleted successfully.'); + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessToggleImageActive() + { + $response = array('errors' => array(), 'success' => false); + $id = (int)Tools::getValue('id_header_image'); + $active = (int)(bool)Tools::getValue('active'); + + if (!$id) { + $response['errors'][] = $this->l('Invalid item ID.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage($id); + if (!Validate::isLoadedObject($objImage)) { + $response['errors'][] = $this->l('Image not found.'); + $this->ajaxDie(json_encode($response)); + } + + if ($active === 0) { + $activeImages = HotelHeaderImage::getItems(1); + if (is_array($activeImages) && count($activeImages) <= 1) { + $response['errors'][] = $this->l('At least one image must remain active.'); + $this->ajaxDie(json_encode($response)); + } + } + + $objImage->active = $active; + if (!$objImage->save()) { + $response['errors'][] = $this->l('Unable to update active status.'); + $this->ajaxDie(json_encode($response)); + } + + $response['success'] = true; + $response['confirmations'] = $this->l('The status has been successfully updated.'); + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessSaveImagePositions() + { + $ids = Tools::getValue('image_ids', array()); + if (!is_array($ids)) { + $this->ajaxDie(json_encode(array('success' => false))); + } + + $sanitizedIds = array_values(array_filter(array_map('intval', $ids), function ($id) { + return $id > 0; + })); + if (count($sanitizedIds) !== count($ids)) { + $this->ajaxDie(json_encode(array('success' => false, 'errors' => array($this->l('Invalid image IDs.'))))); + } + + if ($sanitizedIds) { + $validRows = Db::getInstance()->executeS( + 'SELECT `id_header_image` FROM `'._DB_PREFIX_.'htl_header_image` + WHERE `id_header_image` IN ('.implode(',', $sanitizedIds).')' + ); + if (!$validRows || count($validRows) !== count($sanitizedIds)) { + $this->ajaxDie(json_encode(array('success' => false, 'errors' => array($this->l('One or more image IDs are invalid.'))))); + } + } + + foreach ($sanitizedIds as $position => $id) { + Db::getInstance()->execute( + 'UPDATE `'._DB_PREFIX_.'htl_header_image` + SET `position` = '.(int)$position.' + WHERE `id_header_image` = '.(int)$id + ); + } + $this->ajaxDie(json_encode(array( + 'success' => true, + 'confirmations' => $this->l('The selected images have successfully been moved.'), + ))); + } + + public function ajaxProcessBulkUpdateTagLines() + { + $response = array('errors' => array(), 'success' => false); + $languages = Language::getLanguages(false); + $tagLineByLang = array(); + foreach ($languages as $lang) { + $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + } + foreach ($tagLineByLang as $tagLineValue) { + if (!Validate::isGenericName($tagLineValue)) { + $response['errors'][] = $this->l('Invalid tag line. Characters < > = { } are not allowed.'); + $this->ajaxDie(json_encode($response)); + } + } + + $ids = Db::getInstance()->executeS( + 'SELECT `id_header_image` FROM `'._DB_PREFIX_.'htl_header_image`' + ); + if (!$ids) { + $response['errors'][] = $this->l('No images found.'); + $this->ajaxDie(json_encode($response)); + } + + foreach ($ids as $row) { + $objImage = new HotelHeaderImage((int)$row['id_header_image']); + $objImage->tag_line = $tagLineByLang; + $objImage->save(); + } + + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + $response['success'] = true; + $response['tag_line'] = isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : ''; + $response['tag_lines_json'] = json_encode((object)$tagLineByLang); + $response['confirmations'] = $this->l('Tag line updated for all images.'); + $this->ajaxDie(json_encode($response)); + } + + public function setMedia() + { + parent::setMedia(); + $this->addJqueryPlugin('tablednd'); + $this->addJqueryPlugin('colorpicker'); + $this->addJS(_MODULE_DIR_.'hotelreservationsystem/views/js/HotelHeaderImageAdmin.js'); + $this->addCSS(_MODULE_DIR_.'hotelreservationsystem/views/css/HotelReservationAdmin.css'); + } +} diff --git a/modules/hotelreservationsystem/define.php b/modules/hotelreservationsystem/define.php index 46ac79645a..e28bc33d37 100644 --- a/modules/hotelreservationsystem/define.php +++ b/modules/hotelreservationsystem/define.php @@ -61,6 +61,7 @@ require_once 'classes/HotelSettingsLink.php'; require_once 'classes/HotelBookingDocument.php'; +require_once 'classes/HotelHeaderImage.php'; // Web services classes require_once 'classes/WebserviceSpecificManagementHotelAri.php'; diff --git a/modules/hotelreservationsystem/hotelreservationsystem.php b/modules/hotelreservationsystem/hotelreservationsystem.php index 5246337d7c..b3b5b79269 100644 --- a/modules/hotelreservationsystem/hotelreservationsystem.php +++ b/modules/hotelreservationsystem/hotelreservationsystem.php @@ -74,6 +74,9 @@ public function hookDisplayHeader() } } //End + if (Tools::getValue('controller') == 'index') { + $this->context->controller->addJS($this->_path.'views/js/HotelHeaderMediaFront.js'); + } $this->context->controller->addCSS($this->_path.'/views/css/HotelReservationFront.css'); $this->context->controller->addJS($this->_path.'/views/js/HotelReservationFront.js'); } @@ -325,12 +328,20 @@ public function hookDisplayLeftColumn() public function hookDisplayAfterHookTop() { if (Tools::getValue('controller') == 'index') { - $this->context->smarty->assign( - array( - 'WK_HTL_CHAIN_NAME' => Configuration::get('WK_HTL_CHAIN_NAME', $this->context->language->id), - 'WK_HTL_TAG_LINE' => Configuration::get('WK_HTL_TAG_LINE', $this->context->language->id), - ) - ); + $headerMediaItems = $this->context->smarty->getTemplateVars('headerMediaItems'); + $firstItem = ($headerMediaItems && !empty($headerMediaItems[0])) ? $headerMediaItems[0] : array(); + $tagLine = !empty($firstItem['tag_line']) ? $firstItem['tag_line'] : ''; + $this->context->smarty->assign(array( + 'WK_HTL_CHAIN_NAME' => Configuration::get('WK_HTL_CHAIN_NAME', $this->context->language->id), + 'wkHeaderMediaTagLine' => $tagLine, + 'wkTagLineColor' => !empty($firstItem['tag_line_color']) ? $firstItem['tag_line_color'] : '#ffffff', + 'wkTagLineFontSize' => !empty($firstItem['tag_line_font_size']) ? (int)$firstItem['tag_line_font_size'] : 16, + 'wkTagLineFontWeight' => !empty($firstItem['tag_line_font_weight']) ? $firstItem['tag_line_font_weight'] : '400', + 'QLO_HOTEL_NAME_ENABLE' => (int)Configuration::get('QLO_HOTEL_NAME_ENABLE'), + 'wkHeaderContentAlign' => (int)(Configuration::get('QLO_HEADER_CONTENT_ALIGN') ?: HotelHeaderImage::CONTENT_ALIGN_CENTER), + 'QLO_HEADER_MEDIA_TYPE' => (int)(Configuration::get('QLO_HEADER_MEDIA_TYPE') ?: HotelHeaderImage::MEDIA_TYPE_IMAGE), + 'QLO_HEADER_MEDIA_TYPE_VIDEO' => HotelHeaderImage::MEDIA_TYPE_VIDEO, + )); return $this->display(__FILE__, 'headerHotelDescBlock.tpl'); } } @@ -486,7 +497,6 @@ public function hookActionObjectLanguageAddAfter($params) // update configuration keys $configKeys = array( 'WK_HTL_CHAIN_NAME', - 'WK_HTL_TAG_LINE', 'WK_HTL_SHORT_DESC', ); HotelHelper::updateConfigurationLangKeys($newIdLang, $configKeys); @@ -530,6 +540,7 @@ public function callInstallTab() $this->installTab('AdminHotelGeneralSettings', 'Hotel General Configuration', 'AdminHotelConfigurationSetting', false); $this->installTab('AdminHotelFeaturePricesSettings', 'Advanced Price Rules', 'AdminHotelConfigurationSetting', false); $this->installTab('AdminRoomTypeGlobalDemand', 'Additional Demand Configuration', 'AdminHotelConfigurationSetting', false); + $this->installTab('AdminHotelHeaderImage', 'Header Image Configuration', 'AdminHotelConfigurationSetting', false); $this->installTab('AdminBookingDocument', 'Booking Documents', false, false); return true; @@ -637,9 +648,10 @@ public function deleteConfigVars() 'WK_ROOM_LEFT_WARNING_NUMBER', 'WK_HTL_ESTABLISHMENT_YEAR', 'WK_HTL_CHAIN_NAME', + 'WK_HTL_TAG_LINE', 'WK_TITLE_HEADER_BLOCK', 'WK_CONTENT_HEADER_BLOCK', - 'WK_HTL_HEADER_IMAGE', + 'WK_HOTEL_HEADER_IMAGE', 'WK_ALLOW_ADVANCED_PAYMENT', 'WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT', 'WK_ADVANCED_PAYMENT_INC_TAX', @@ -648,7 +660,16 @@ public function deleteConfigVars() 'WK_HOTEL_NAME_ENABLE', 'WK_CUSTOMER_SUPPORT_PHONE_NUMBER', 'WK_CUSTOMER_SUPPORT_EMAIL', - 'WK_DISPLAY_CONTACT_PAGE_HOTEL_LIST' + 'WK_DISPLAY_CONTACT_PAGE_HOTEL_LIST', + 'QLO_HEADER_MEDIA_TYPE', + 'QLO_HOTEL_NAME_ENABLE', + 'QLO_HEADER_CONTENT_ALIGN', + 'QLO_HEADER_VIDEO_SOURCE_TYPE', + 'QLO_HEADER_VIDEO_NAME', + 'QLO_HEADER_SLIDER_NAV_TYPE', + 'QLO_HEADER_SLIDER_AUTO_PLAY', + 'QLO_HEADER_SLIDER_INTERVAL', + 'QLO_HEADER_SLIDER_ANIM_TYPE', ); foreach ($configKeys as $key) { if (!Configuration::deleteByName($key)) { diff --git a/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css b/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css index e3b6077b2c..8d2e778180 100644 --- a/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css +++ b/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css @@ -605,3 +605,158 @@ p.room_cat_data .error-border { outline: rgb(210, 124, 130) solid 1px; border-radius: 2px;} + +.qlo-video-preview-card { + background: #111; + border-radius: 4px; + overflow: hidden; + margin-bottom: 10px; + max-width: 480px; + display: inline-block; + vertical-align: top; + width: 100%; +} + +.qlo-video-preview-player { + display: block; + width: 100%; + max-height: 240px; + background: #000; +} + +.qlo-video-url-preview { + padding: 14px 16px; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 0; + word-break: break-all; + font-size: 13px; + color: #444; +} + +.qlo-video-url-preview .icon-link { + margin-right: 6px; + color: #5bc0de; + font-size: 14px; +} + +.wk-inline-hint { + display: inline-block; + margin: 6px 0 0 10px; + font-size: 12px; + vertical-align: middle; +} + +@keyframes wkSlideInLeft { + from { + opacity: 0; + transform: translateX(-16px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.qlo-anim-slide-left { + animation: wkSlideInLeft 0.22s ease forwards; +} + +.qlo-img-thumb { + width: 120px; + height: 75px; + -o-object-fit: cover; + object-fit: cover; + display: block; +} + +#qlo-image-table tbody tr.qlo-img-row:hover { + background-color: #f9f9f9; +} + +.qlo-img-tagline-cell { + max-width: 220px; + color: #555; + vertical-align: middle !important; +} + +.wk-row-placeholder { + background: #eef6ff !important; + border: 1px dashed #99c0e0 !important; +} + +.wk-row-placeholder td { + padding: 0 !important; + height: 85px; +} + + +/* ---- File list in add-image form ---- */ +.qlo-files-list { + list-style: none; + padding: 0; + margin: 8px 0 0; + max-width: 520px; +} +.qlo-files-list li { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border: 1px solid #e0e0e0; + border-radius: 3px; + margin-bottom: 5px; + background: #f9f9f9; + font-size: 13px; +} +.qlo-files-list li:first-child { + margin-top: 5px; +} +.qlo-files-list li:last-child { + margin-bottom: 0; +} +.qlo-files-list .qlo-file-name { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 500; + color: #333; +} +.qlo-files-list .qlo-file-size { + font-size: 11px; + color: #999; + white-space: nowrap; + flex-shrink: 0; +} +.qlo-files-list .qlo-remove-file { + flex-shrink: 0; + padding: 3px 8px; + line-height: 1; + font-size: 13px; + color: #999; + border-color: #ccc; + background: #fff; +} +.qlo-files-list .qlo-remove-file:hover, +.qlo-files-list .qlo-remove-file:focus { + color: #c0392b; + border-color: #c0392b; + background: #fff5f5; +} + +/* ---- Edit preview image in form ---- */ +#qlo-img-edit-thumb { + max-width: 160px; + max-height: 100px; + object-fit: cover; + border-radius: 3px; +} + +/* ---- Inline multilingual input (inside input-group-btn) ---- */ +.wk-inline-lang-input { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} diff --git a/modules/hotelreservationsystem/views/css/HotelReservationFront.css b/modules/hotelreservationsystem/views/css/HotelReservationFront.css index b268f77b86..4a81c829da 100644 --- a/modules/hotelreservationsystem/views/css/HotelReservationFront.css +++ b/modules/hotelreservationsystem/views/css/HotelReservationFront.css @@ -1,3 +1,69 @@ +/** +* NOTICE OF LICENSE +* +* This source file is subject to the Open Software License version 3.0 +* that is bundled with this package in the file LICENSE.md +* It is also available through the world-wide-web at this URL: +* https://opensource.org/license/osl-3-0-php +* If you did not receive a copy of the license and are unable to +* obtain it through the world-wide-web, please send an email +* to support@qloapps.com so we can send you a copy immediately. +* +* DISCLAIMER +* +* Do not edit or add to this file if you wish to upgrade this module to a newer +* versions in the future. If you wish to customize this module for your needs +* please refer to https://store.webkul.com/customisation-guidelines for more information. +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*/ + +.header-ext-dots { + position: absolute; + bottom: 14px; + left: 0; + right: 0; + text-align: center; + z-index: 15; + pointer-events: none; +} +.header-ext-dots .owl-dot { + display: inline-block; + margin: 0 5px; + cursor: pointer; + pointer-events: all; +} +.header-ext-dots .owl-dot span { + display: block; + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + border: 1px solid rgba(255, 255, 255, 0.8); + transition: background 0.25s ease; +} +.header-ext-dots .owl-dot.active span, +.header-ext-dots .owl-dot:hover span { + background: #ffffff; +} + +/* ===== Content alignment (1=left, 2=center, 3=right) ===== */ +.header-align-1 { text-align: left; } +.header-align-2 { text-align: center; } +.header-align-3 { text-align: right; } + +.header-align-1 .heasder-desc-hr-first, +.header-align-1 .heasder-desc-hr-second { margin-left: 0; margin-right: auto; } +.header-align-3 .heasder-desc-hr-first, +.header-align-3 .heasder-desc-hr-second { margin-left: auto; margin-right: 0; } + +@media (max-width: 767px) { + .header-align-1, + .header-align-3 { text-align: center; } +} + /* ===== headerHotelDescBlock.tpl ===== */ .header-desc-container { bottom: 0; @@ -48,13 +114,244 @@ } .heasder-desc-hr-second { - width: 190px; - margin-top: 30px; + display: none; } @media (max-width: 767px) { .topSubSecondaryBlock { - margin-top: 0px; } } + margin-top: 0px; } + +/* ===== Mobile header responsive ===== */ + +/* + * The index page sets height:100% via inline style on html/body/#page/.header-container/
. + * On mobile portrait this makes the header fill the full viewport (~750px+), causing + * landscape hotel images to be cropped and zoomed beyond recognition. + * Reset ancestors to auto and lock the header to a landscape-ish fixed height. + */ +html, body { + height: auto !important; +} +#page, .header-container { + height: auto !important; +} +body#index #header { + height: 60vw !important; + min-height: 240px !important; + max-height: 420px !important; +} + +/* Force the full owl carousel height chain — owl JS sets inline height on + stage/items during responsive recalc, collapsing the background images. */ +body#index .header-media-layer, +body#index .header-media-layer .owl-carousel, +body#index .header-media-layer .owl-stage-outer, +body#index .header-media-layer .owl-stage, +body#index .header-media-layer .owl-item { + height: 100% !important; +} + +/* Keep background images filling the now-landscape slot */ +body#index .header-media-layer, +body#index .header-slide-img { + background-size: cover !important; + background-position: center center !important; +} + +/* Scale down oversized text */ +.header-hotel-name { + font-size: 22px !important; + margin-top: 0; + margin-bottom: 8px; +} +.js-header-tagline { + font-size: 13px !important; + line-height: 18px; +} +.header-desc-inner-wrapper { + padding-left: 10px; + padding-right: 10px; +} +/* Prev/next buttons — touch-friendly on mobile */ +.header-media-btn { + padding: 5px 9px; + font-size: 15px; +} +.js-header-media-prev { left: 6px; } +.js-header-media-next { right: 6px; } + +/* Dots clear from bottom edge */ +.header-ext-dots { + bottom: 10px; +} +} + +/* ==== Header Media (front-end) ==== */ + +#header { + background: transparent; +} + +body#index #header { + position: relative; +} + +/* Owl carousel slide animation classes */ +.wkFadeOut { animation: wkFadeOut 0.7s forwards; } +.wkFadeIn { animation: wkFadeIn 0.7s forwards; } +.wkZoomOut { animation: wkZoomOut 0.7s forwards; } +.wkZoomIn { animation: wkZoomIn 0.7s forwards; } +.wkBlurOut { animation: wkBlurOut 0.7s forwards; } +.wkBlurIn { animation: wkBlurIn 0.7s forwards; } +.wkSlideOut { animation: wkSlideOut 0.7s forwards; } +.wkSlideIn { animation: wkSlideIn 0.7s forwards; } + +@keyframes wkFadeOut { from { opacity:1; } to { opacity:0; } } +@keyframes wkFadeIn { from { opacity:0; } to { opacity:1; } } +@keyframes wkZoomOut { from { opacity:1; transform:scale(1); } to { opacity:0; transform:scale(1.15); } } +@keyframes wkZoomIn { from { opacity:0; transform:scale(0.88); } to { opacity:1; transform:scale(1); } } +@keyframes wkBlurOut { from { opacity:1; filter:blur(0); } to { opacity:0; filter:blur(10px); } } +@keyframes wkBlurIn { from { opacity:0; filter:blur(10px); } to { opacity:1; filter:blur(0); } } +@keyframes wkSlideOut { from { opacity:1; transform:translateX(0); } to { opacity:0; transform:translateX(-60px); } } +@keyframes wkSlideIn { from { opacity:0; transform:translateX(60px); } to { opacity:1; transform:translateX(0); } } + +/* Tag-line slide animation on slide change */ +.js-header-tagline { + transition: opacity 0.4s ease, transform 0.4s ease; +} +.js-header-tagline.wk-tagline-out { + opacity: 0; + transform: translateY(-10px); +} +.js-header-tagline.wk-tagline-in { + opacity: 1; + transform: translateY(0); +} + +.header-media-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -11; + overflow: hidden; + background-size: cover; + background-position: center center; + background-repeat: no-repeat; +} + +.header-media-layer::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.28); + z-index: 1; + pointer-events: none; +} + +.header-media-layer .owl-carousel, +.header-media-layer .owl-stage-outer, +.header-media-layer .owl-stage, +.header-media-layer .owl-item { + height: 100%; +} + +.header-slide-img { + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + background-repeat: no-repeat; +} + +.header-video-layer { + background-color: #000; +} + +.header-bg-video { + position: absolute; + top: 50%; + left: 50%; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + transform: translate(-50%, -50%); + object-fit: cover; +} + +.header-slide-video { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.header-slide-video .header-bg-video { + position: absolute; + top: 50%; + left: 50%; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + transform: translate(-50%, -50%); + object-fit: cover; +} + +.header-media-nav-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + pointer-events: none; +} + +@media (min-width: 768px) { + .header-media-nav-wrapper { + z-index: 11; + } + /* Push dots above the search panel (which sits at bottom:0, ~90px tall) */ + .header-ext-dots { + bottom: 120px; + } +} + +.header-media-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + pointer-events: all; + background: rgba(0, 0, 0, 0.4); + color: #fff; + border: none; + border-radius: 2px; + padding: 8px 14px; + font-size: 20px; + line-height: 1; + cursor: pointer; + transition: background 0.2s; + outline: none; +} + +.header-media-btn:hover { + background: rgba(0, 0, 0, 0.65); + color: #fff; +} + +.js-header-media-prev { + left: 15px; +} + +.js-header-media-next { + right: 15px; +} /* ===== headerHotelDescBlock.tpl ===== */ diff --git a/modules/hotelreservationsystem/views/js/HotelHeaderImageAdmin.js b/modules/hotelreservationsystem/views/js/HotelHeaderImageAdmin.js new file mode 100644 index 0000000000..2d6c04189a --- /dev/null +++ b/modules/hotelreservationsystem/views/js/HotelHeaderImageAdmin.js @@ -0,0 +1,633 @@ +/** +* NOTICE OF LICENSE +* +* This source file is subject to the Open Software License version 3.0 +* that is bundled with this package in the file LICENSE.md +* It is also available through the world-wide-web at this URL: +* https://opensource.org/license/osl-3-0-php +* If you did not receive a copy of the license and are unable to +* obtain it through the world-wide-web, please send an email +* to support@qloapps.com so we can send you a copy immediately. +* +* DISCLAIMER +* +* Do not edit or add to this file if you wish to upgrade this module to a newer +* versions in the future. If you wish to customize this module for your needs +* please refer to https://store.webkul.com/customisation-guidelines for more information. +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*/ + +$(document).ready(function () { + + $('#qlo-media-type').on('change', function () { + var type = parseInt($(this).val(), 10); + if (type === qloHmMediaTypeImage) { + $('#qlo-video-settings').hide(); + $('#qlo-image-settings, #qlo-image-panel, #qlo-slider-info').show(); + closeImgForm(); + } else { + closeImgForm(); + $('#qlo-image-settings, #qlo-image-panel, #qlo-slider-info').hide(); + $('#qlo-video-settings').show(); + } + }); + + $('#qlo-header-media-form').on('submit', function (e) { + var newType = parseInt($('#qlo-media-type').val(), 10); + if (newType === qloHmMediaTypeVideo && qloHmMediaType === qloHmMediaTypeImage) { + var imageCount = $('#qlo-image-tbody .qlo-img-row').length; + if (imageCount > 1 && $('#qlo-confirm-delete-images').val() !== '1') { + e.preventDefault(); + $('#qlo-confirm-switch-video-modal').modal('show'); + return false; + } + } + }); + + $('#qlo-confirm-switch-video-btn').on('click', function () { + $('#qlo-confirm-switch-video-modal').modal('hide'); + $('#qlo-confirm-delete-images').val('1'); + $('#qlo-header-media-form')[0].submit(); + }); + + var pendingBulkDeleteForm = null; + var pendingBulkDeleteAction = null; + + $(document).on('click', '.qlo-bulk-delete-images-trigger', function (e) { + e.preventDefault(); + pendingBulkDeleteForm = $(this).closest('form').get(0); + pendingBulkDeleteAction = $(this).data('action'); + $('#qlo-confirm-bulk-delete-modal').modal('show'); + }); + + $('#qlo-confirm-bulk-delete-btn').on('click', function () { + $('#qlo-confirm-bulk-delete-modal').modal('hide'); + if (pendingBulkDeleteForm && pendingBulkDeleteAction) { + sendBulkAction(pendingBulkDeleteForm, pendingBulkDeleteAction); + } + }); + + $(document).on('change', 'input[name="QLO_HEADER_SLIDER_AUTO_PLAY"]', function () { + if ($(this).val() === '1') { + $('#qlo-slide-interval-group, #qlo-slide-anim-group').show(); + } else { + $('#qlo-slide-interval-group, #qlo-slide-anim-group').hide(); + } + }); + + $(document).on('change', '#qlo-img-form-file', function () { + renderFileListHm(this.files); + }); + + $(document).on('click', '.qlo-remove-file', function () { + var removeIdx = parseInt($(this).data('index'), 10); + var fileInput = document.getElementById('qlo-img-form-file'); + if (!fileInput || !fileInput.files) { return; } + try { + var dt = new DataTransfer(); + for (var i = 0; i < fileInput.files.length; i++) { + if (i !== removeIdx) { dt.items.add(fileInput.files[i]); } + } + fileInput.files = dt.files; + } catch (_e) { + } + renderFileListHm(fileInput.files); + }); + + function renderFileListHm(files) { + var $list = $('#qlo-img-files-list').empty(); + if (!files || !files.length) { $list.hide(); return; } + $list.show(); + for (var i = 0; i < files.length; i++) { + (function (idx, file) { + var size = file.size < 1048576 + ? (file.size / 1024).toFixed(1) + ' KB' + : (file.size / 1048576).toFixed(1) + ' MB'; + var $li = $( + '
  • ' + + '' + escapeHtmlHm(file.name) + '' + + '(' + size + ')' + + '' + + '
  • ' + ); + $list.append($li); + })(i, files[i]); + } + } + + hideOtherLanguage(qloHmDefaultLangId); + + $(document).on('change', '#qlo-source-type', function () { + if ($(this).val() === 'url') { + $('#qlo-vid-file-wrap').hide(); + $('#qlo-vid-url-wrap').show(); + $('#qlo-current-video-wrap').hide(); + } else { + $('#qlo-vid-file-wrap').show(); + $('#qlo-vid-url-wrap').hide(); + if ($('#qlo-current-video-wrap').length) { + $('#qlo-current-video-wrap').show(); + } + } + }); + + $(document).on('change', '#qlo-video-file-input', function () { + var file = this.files && this.files[0] ? this.files[0] : null; + var $hint = $('.qlo-vid-filename'); + if (file && typeof qloHmMaxVideoUpload !== 'undefined' && qloHmMaxVideoUpload > 0 && file.size > qloHmMaxVideoUpload) { + this.value = ''; + $hint.hide(); + showErrorMessage(qloHmI18n.fileTooLarge); + return; + } + if (file) { + $hint.text(file.name).show(); + } else { + $hint.hide(); + } + }); + + $(document).on('click', '#qlo-vid-file-add-btn', function () { + $('#qlo-video-file-input').trigger('click'); + }); + + var videoUrlMimeMap = { mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg' }; + + $(document).on('input change', '#qlo-video-url-input', function () { + var url = $.trim($(this).val()); + var $wrap = $('#qlo-video-url-preview-wrap'); + var $player = $('#qlo-video-url-preview-player')[0]; + var $source = $('#qlo-video-url-preview-source'); + + if (!/^https?:\/\//i.test(url)) { + $wrap.hide(); + return; + } + + var ext = (url.split('?')[0].split('.').pop() || '').toLowerCase(); + $source.attr('src', url); + $source.attr('type', videoUrlMimeMap[ext] || 'video/mp4'); + if ($player) { $player.load(); } + $wrap.show(); + }); + + $('#qlo-add-image-btn').on('click', function () { + openImgForm('add'); + }); + + $('#qlo-img-file-add-btn').on('click', function () { + $('#qlo-img-form-file').trigger('click'); + }); + + $(document).on('click', '.qlo-edit-img', function () { + var $row = $(this).closest('tr.qlo-img-row'); + var id = parseInt($row.data('id'), 10); + var tagLines = $row.data('tag-lines') || {}; + var isActive = $row.find('.list-action-enable').hasClass('action-enabled'); + var thumbSrc = $row.attr('data-thumb-src') || ''; + var tlColor = $row.attr('data-tag-line-color') || '#ffffff'; + var tlFontSize = parseInt($row.attr('data-tag-line-font-size') || 16, 10); + var tlFontWeight = $row.attr('data-tag-line-font-weight') || '400'; + openImgForm('edit', id, tagLines, isActive, thumbSrc, tlColor, tlFontSize, tlFontWeight); + }); + + $('#qlo-bulk-tagline-apply').on('click', function () { + var tagLines = {}; + $('.qlo-bulk-tagline-field').each(function () { + tagLines[$(this).data('lang')] = $.trim($(this).val()); + }); + var postData = { ajax: 1, action: 'bulkUpdateTagLines', token: qloHmToken }; + $.each(tagLines, function (langId, val) { + postData['tag_line_' + langId] = val; + }); + var $btn = $(this).prop('disabled', true); + $('#page-loader').show(); + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: postData, + success: function (raw) { + $btn.prop('disabled', false); + var data = safeParseJsonHm(raw); + if (data && data.success) { + var updatedTagLines = safeParseJsonHm(data.tag_lines_json) || tagLines; + $('#qlo-image-tbody .qlo-img-row').each(function () { + $(this).attr('data-tag-lines', data.tag_lines_json); + $(this).data('tag-lines', updatedTagLines); + var display = data.tag_line || ''; + $(this).find('.qlo-img-tagline-cell').text( + display.length > 50 ? display.substring(0, 50) + '...' : (display || '—') + ); + }); + $('#qlo-bulk-tagline-modal').modal('hide'); + showSuccessMessage(data.confirmations); + } else { + showErrorMessage(data ? data.errors.join('
    ') : qloHmI18n.updateFailed); + } + }, + error: function () { $btn.prop('disabled', false); showErrorMessage(qloHmI18n.requestFailed); } + }).always(function () { $('#page-loader').hide(); }); + }); + + $('#qlo-img-form-upload-btn').on('click', function () { + var fileInput = document.getElementById('qlo-img-form-file'); + var files = fileInput ? Array.prototype.slice.call(fileInput.files) : []; + if (!files.length) { + showErrorMessage((typeof qloHmI18n !== 'undefined') ? qloHmI18n.noFileSelected : 'Please select at least one image file.'); + return; + } + uploadImagesWithTagLine(files); + }); + + $('#qlo-img-form-save-btn').on('click', function () { + var id = parseInt($.trim($('#qlo-img-form-id').val()), 10); + if (id) { + saveEditedImage(id); + } + }); + + $(document).on('click', '.qlo-img-row .list-action-enable', function (e) { + e.preventDefault(); + var $link = $(this); + var $row = $link.closest('tr.qlo-img-row'); + var id = parseInt($row.data('id'), 10); + var active = $link.hasClass('action-enabled') ? 0 : 1; + $('#page-loader').show(); + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: { ajax: 1, action: 'toggle_image_active', id_header_image: id, active: active, token: qloHmToken }, + success: function (raw) { + var data = safeParseJsonHm(raw); + if (data && data.success) { + $link.toggleClass('action-enabled action-disabled'); + $link.find('i').toggleClass('hidden'); + showSuccessMessage(data.confirmations); + } else { + showErrorMessage(data ? data.errors.join('
    ') : qloHmI18n.updateFailed); + } + }, + error: function () { showErrorMessage(qloHmI18n.requestFailed); } + }).always(function () { $('#page-loader').hide(); }); + }); + + $(document).on('click', '.qlo-delete-img', function () { + var id = parseInt($(this).data('id'), 10); + $('#page-loader').show(); + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: { ajax: 1, action: 'deleteMedia', id_header_image: id, token: qloHmToken }, + success: function (raw) { + var data = safeParseJsonHm(raw); + if (data && data.success) { + var $row = $('#qlo-image-tbody .qlo-img-row[data-id="' + id + '"]'); + if (parseInt($('#qlo-img-form-id').val(), 10) === id) { + closeImgForm(); + } + $row.fadeOut(250, function () { + $row.remove(); + var count = $('#qlo-image-tbody .qlo-img-row').length; + $('#qlo-image-count').text(count); + if (!count) { + $('#qlo-no-images').show(); + } + updateBulkActionsVisibility(); + }); + showSuccessMessage(data.confirmations || qloHmI18n.deleteFailed); + } else { + showErrorMessage(data ? data.errors.join('
    ') : qloHmI18n.deleteFailed); + } + }, + error: function () { showErrorMessage(qloHmI18n.requestFailed); } + }).always(function () { $('#page-loader').hide(); }); + }); + + if ($('#qlo-image-table').length) { + var _hmDragOriginalOrder = null; + $('#qlo-image-table').tableDnD({ + dragHandle: 'dragHandle', + onDragClass: 'myDragClass', + onDragStart: function (table) { + _hmDragOriginalOrder = []; + $(table).find('tbody tr.qlo-img-row').each(function () { + _hmDragOriginalOrder.push(parseInt($(this).data('id'), 10)); + }); + }, + onDrop: function (table) { + var ids = []; + $(table).find('tbody tr.qlo-img-row').each(function (i) { + var id = parseInt($(this).data('id'), 10); + ids.push(id); + $(this).find('.positions').text(i + 1); + }); + if (_hmDragOriginalOrder && JSON.stringify(ids) === JSON.stringify(_hmDragOriginalOrder)) { + return; + } + $('#page-loader').show(); + $.post(qloHmCurrentIndex, { + ajax: 1, + action: 'saveImagePositions', + image_ids: ids, + token: qloHmToken + }, function (raw) { + var data = safeParseJsonHm(raw); + if (data && data.success) { + showSuccessMessage(data.confirmations); + } + }).always(function () { $('#page-loader').hide(); }); + } + }); + } + + function openImgForm(mode, id, tagLines, isActive, imgUrl, tlColor, tlFontSize, tlFontWeight) { + $('#qlo-img-form-id').val(id || ''); + $('#qlo-form-upload-progress').hide(); + + if (mode === 'edit') { + $('#qlo-img-modal-title-add').hide(); + $('#qlo-img-modal-title-edit').show(); + $('#qlo-img-add-footer').hide(); + $('#qlo-img-edit-footer').show(); + $('#qlo-img-form-file-group').hide(); + $('#qlo-img-files-list').hide().empty(); + $('#qlo-img-form-add-active-group').hide(); + $('#qlo-img-form-edit-group').show(); + if (isActive) { + $('#qlo_img_active_edit_on').prop('checked', true); + } else { + $('#qlo_img_active_edit_off').prop('checked', true); + } + tagLines = tagLines || {}; + $('.qlo-form-tagline-field').each(function () { + var langId = $(this).data('lang'); + $(this).val(tagLines[langId] || ''); + }); + if (imgUrl) { + $('#qlo-img-edit-thumb').attr('src', imgUrl); + $('#qlo-img-edit-preview-group').show(); + } else { + $('#qlo-img-edit-preview-group').hide(); + } + $('#qlo-img-tl-color').val(tlColor || '#ffffff'); + $('#qlo-img-tl-font-size').val(tlFontSize || 16); + var fw = tlFontWeight || '400'; + $('#qlo-img-tl-font-weight').val(fw); + } else { + $('#qlo-img-modal-title-edit').hide(); + $('#qlo-img-modal-title-add').show(); + $('#qlo-img-edit-footer').hide(); + $('#qlo-img-add-footer').show(); + $('#qlo-img-form-file-group').show(); + $('#qlo-img-form-add-active-group').show(); + $('#qlo-img-form-edit-group').hide(); + $('#qlo-img-edit-preview-group').hide(); + var fileInput = document.getElementById('qlo-img-form-file'); + if (fileInput) { + fileInput.value = ''; + } + $('#qlo-img-files-list').hide().empty(); + $('#qlo_img_active_add_on').prop('checked', true); + $('.qlo-form-tagline-field').val(''); + $('#qlo-img-tl-color').val('#ffffff'); + $('#qlo-img-tl-font-size').val(16); + $('#qlo-img-tl-font-weight').val('400'); + } + + $('#qlo-img-tl-color').trigger('keyup'); + + hideOtherLanguage(id_language); + + $('#qlo-img-form-modal').modal('show'); + } + + function closeImgForm() { + $('#qlo-img-form-modal').modal('hide'); + } + + function getFormTagLines() { + var tagLines = {}; + $('.qlo-form-tagline-field').each(function () { + tagLines[$(this).data('lang')] = $.trim($(this).val()); + }); + return tagLines; + } + + function saveEditedImage(id) { + var tagLines = getFormTagLines(); + var active = parseInt($('input[name="qlo_img_active_edit"]:checked').val() || 0, 10); + var tlColor = $('#qlo-img-tl-color').val() || '#ffffff'; + var tlFontSize = parseInt($('#qlo-img-tl-font-size').val() || 16, 10); + var tlFontWeight = $('#qlo-img-tl-font-weight').val() || '400'; + var postData = { + ajax: 1, action: 'edit_image', id_header_image: id, active: active, token: qloHmToken, + tag_line_color: tlColor, tag_line_font_size: tlFontSize, tag_line_font_weight: tlFontWeight + }; + $.each(tagLines, function (langId, val) { + postData['tag_line_' + langId] = val; + }); + + $('#page-loader').show(); + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: postData, + success: function (raw) { + var resp = safeParseJsonHm(raw); + if (resp && resp.success) { + var $row = $('#qlo-image-tbody .qlo-img-row[data-id="' + id + '"]'); + var updatedTagLines = safeParseJsonHm(resp.tag_lines_json) || tagLines; + $row.attr('data-tag-lines', resp.tag_lines_json); + $row.data('tag-lines', updatedTagLines); + $row.attr('data-tag-line-color', resp.tag_line_color || '#ffffff'); + $row.attr('data-tag-line-font-size', resp.tag_line_font_size || 16); + $row.attr('data-tag-line-font-weight', resp.tag_line_font_weight || '400'); + var display = resp.tag_line || ''; + $row.find('.qlo-img-tagline-cell').text( + display.length > 50 ? display.substring(0, 50) + '...' : (display || '—') + ); + var nowActive = parseInt(resp.active, 10); + var $toggle = $row.find('.list-action-enable'); + $toggle.toggleClass('action-enabled', !!nowActive) + .toggleClass('action-disabled', !nowActive); + $toggle.find('i.icon-check').toggleClass('hidden', !nowActive); + $toggle.find('i.icon-remove').toggleClass('hidden', !!nowActive); + $toggle.attr('href', $toggle.attr('href').replace(/id_header_image=\d+/, 'id_header_image=' + id) + .replace(/active=\d+/, 'active=' + (nowActive ? 0 : 1))); + closeImgForm(); + showSuccessMessage(resp.confirmations || qloHmI18n.imageUpdatedSuccess); + } else { + showErrorMessage(resp ? resp.errors.join('
    ') : qloHmI18n.updateFailed); + } + }, + error: function () { showErrorMessage(qloHmI18n.requestFailed); } + }).always(function () { $('#page-loader').hide(); }); + } + + function uploadImagesWithTagLine(files) { + var index = 0; + var tagLines = getFormTagLines(); + var active = parseInt($('input[name="qlo_img_active_add"]:checked').val() || 1, 10); + var tlColor = $('#qlo-img-tl-color').val() || '#ffffff'; + var tlFontSize = parseInt($('#qlo-img-tl-font-size').val() || 16, 10); + var tlFontWeight = $('#qlo-img-tl-font-weight').val() || '400'; + $('#qlo-form-upload-progress').show(); + $('#qlo-img-form-upload-btn').prop('disabled', true); + $('#page-loader').show(); + + function next() { + if (index >= files.length) { + $('#qlo-form-upload-progress').hide(); + $('#qlo-img-form-upload-btn').prop('disabled', false); + $('#page-loader').hide(); + closeImgForm(); + return; + } + var file = files[index++]; + if (typeof qloHmMaxUpload !== 'undefined' && qloHmMaxUpload > 0 && file.size > qloHmMaxUpload) { + showErrorMessage(qloHmI18n.fileTooLarge); + next(); + return; + } + var fd = new FormData(); + fd.append('header_image_file', file); + fd.append('ajax', '1'); + fd.append('action', 'uploadImage'); + fd.append('token', qloHmToken); + fd.append('active', active); + fd.append('tag_line_color', tlColor); + fd.append('tag_line_font_size', tlFontSize); + fd.append('tag_line_font_weight', tlFontWeight); + $.each(tagLines, function (langId, val) { + fd.append('tag_line_' + langId, val); + }); + + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: fd, + processData: false, + contentType: false, + success: function (raw) { + var resp = safeParseJsonHm(raw); + if (resp && resp.success) { + var uploadedTagLines = safeParseJsonHm(resp.tag_lines_json) || tagLines; + appendImageRow( + resp.id, resp.imgUrl, resp.tag_line || '', uploadedTagLines, resp.tag_lines_json || '{}', + parseInt(resp.active, 10), + resp.tag_line_color || '#ffffff', + resp.tag_line_font_size || 16, + resp.tag_line_font_weight || '400' + ); + showSuccessMessage(qloHmI18n.imageUploadedSuccess); + } else { + showErrorMessage(file.name + ': ' + (resp ? resp.errors.join(', ') : qloHmI18n.uploadFailed)); + } + next(); + }, + error: function () { + showErrorMessage(file.name + ': ' + qloHmI18n.requestFailed); + next(); + } + }); + } + next(); + } + + function appendImageRow(id, imgUrl, tagLineDisplay, tagLines, tagLinesJson, active, tlColor, tlFontSize, tlFontWeight) { + id = parseInt(id, 10); + active = active !== undefined ? !!active : true; + tlColor = tlColor || '#ffffff'; + tlFontSize = tlFontSize || 16; + tlFontWeight = tlFontWeight || '400'; + var display = tagLineDisplay || ''; + var cellText = display.length > 50 ? display.substring(0, 50) + '...' : (display || '—'); + var pos = parseInt($('#qlo-image-count').text(), 10) + 1; + var nextActive = active ? 0 : 1; + var toggleUrl = qloHmCurrentIndex + '&ajax=1&action=toggle_image_active&id_header_image=' + id + '&active=' + nextActive + '&token=' + qloHmToken; + var toggleClass = active ? 'action-enabled' : 'action-disabled'; + var checkHidden = active ? '' : ' hidden'; + var removeHidden = active ? ' hidden' : ''; + + var $row = $( + '' + + '' + + '' + + '' + escapeHtmlHm(cellText) + '' + + '' + + '
    ' + pos + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + ); + $row.data('tag-lines', tagLines); + + $('#qlo-image-tbody').append($row); + $('#qlo-image-table').tableDnDUpdate(); + $('#qlo-no-images').hide(); + $('#qlo-image-count').text(parseInt($('#qlo-image-count').text(), 10) + 1); + updateBulkActionsVisibility(); + } + + function updateBulkActionsVisibility() { + var count = $('#qlo-image-tbody .qlo-img-row').length; + $('#qlo-bulk-actions-row').toggle(count > 1); + } + + function safeParseJsonHm(raw) { + try { return JSON.parse(raw); } catch (_e) { return null; } + } + + function escapeHtmlHm(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function escapeAttrHm(str) { + return String(str) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + } + + +}); diff --git a/modules/hotelreservationsystem/views/js/HotelHeaderMediaFront.js b/modules/hotelreservationsystem/views/js/HotelHeaderMediaFront.js new file mode 100644 index 0000000000..fac729d23b --- /dev/null +++ b/modules/hotelreservationsystem/views/js/HotelHeaderMediaFront.js @@ -0,0 +1,166 @@ +/** +* NOTICE OF LICENSE +* +* This source file is subject to the Open Software License version 3.0 +* that is bundled with this package in the file LICENSE.md +* It is also available through the world-wide-web at this URL: +* https://opensource.org/license/osl-3-0-php +* If you did not receive a copy of the license and are unable to +* obtain it through the world-wide-web, please send an email +* to support@qloapps.com so we can send you a copy immediately. +* +* DISCLAIMER +* +* Do not edit or add to this file if you wish to upgrade this module to a newer +* versions in the future. If you wish to customize this module for your needs +* please refer to https://store.webkul.com/customisation-guidelines for more information. +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*/ + + +$(document).ready(function () { + + /* Background video autoplay fallback (handles strict browser autoplay policies) */ + var $bgVideo = $('video.header-bg-video'); + if ($bgVideo.length) { + $bgVideo.each(function () { + var vid = this; + vid.muted = true; + var p = vid.play(); + if (p !== undefined) { + p.catch(function () {}); + } + }); + } + + var $imgCarousel = $('.header-img-carousel'); + if (!$imgCarousel.length) { return; } + + var autoPlay = parseInt($imgCarousel.data('auto-play'), 10) === 1; + var interval = parseInt($imgCarousel.data('interval'), 10) || 5000; + var navType = parseInt($imgCarousel.data('nav-type'), 10) || 1; // 1=dots 2=arrows 3=both + var animType = parseInt($imgCarousel.data('anim-type'), 10) || 1; // 1=slide 2=fade 3=zoom 4=blur + var animNames = {1: 'Slide', 2: 'Fade', 3: 'Zoom', 4: 'Blur'}; + var animLabel = animNames[animType] || 'Slide'; + + var showDots = (navType === 1 || navType === 3); + $imgCarousel.owlCarousel({ + loop: true, + items: 1, + dots: showDots, + dotsContainer: showDots ? '#wk-header-owl-dots' : false, + nav: false, + autoplay: autoPlay, + autoplayTimeout: interval, + autoplaySpeed: 800, + autoplayHoverPause: false, + animateOut: 'wk' + animLabel + 'Out', + animateIn: 'wk' + animLabel + 'In', + responsiveClass: true, + rtl: (typeof language_is_rtl !== 'undefined' ? language_is_rtl : false) + }); + + var owl = $imgCarousel.data('owl.carousel'); + + var _origLeave = $.proxy(owl.leave, owl); + owl.leave = function (name) { + _origLeave(name); + var cur = owl._states.current; + for (var k in cur) { + if (cur[k] < 0) { cur[k] = 0; } + } + }; + + + var $taglineEl = $('.js-header-tagline'); + var _fadeTimer = null; + var _shownIndex = -1; + + var $slides = $imgCarousel.find('.owl-item:not(.cloned) .header-slide-img'); + var taglines = $slides.map(function () { return $(this).data('tagline') || ''; }).get(); + var tlStyles = $slides.map(function () { + return { + color: $(this).data('tl-color') || '#ffffff', + fontSize: $(this).data('tl-font-size') || 16, + fontWeight: $(this).data('tl-font-weight') || '400' + }; + }).get(); + + function showTagline(tag, style) { + if (!$taglineEl.length) { return; } + if (_fadeTimer) { clearTimeout(_fadeTimer); _fadeTimer = null; } + + style = style || { color: '#ffffff', fontSize: 16, fontWeight: '400' }; + $taglineEl.css({ + color: style.color, + fontSize: style.fontSize + 'px', + fontWeight: style.fontWeight + }); + + $taglineEl.removeClass('wk-tagline-out wk-tagline-in').text(tag); + if (!tag) { $taglineEl.hide(); return; } + + $taglineEl.show(); + $taglineEl[0].offsetHeight; // force reflow so the transition actually runs + $taglineEl.addClass('wk-tagline-in'); + _fadeTimer = setTimeout(function () { + _fadeTimer = null; + $taglineEl.removeClass('wk-tagline-in'); + }, 400); + } + + _shownIndex = owl.relative(owl.current()); + showTagline(taglines[_shownIndex] || '', tlStyles[_shownIndex]); + + $imgCarousel.on('changed.owl.carousel', function () { + var realIndex = owl.relative(owl.current()); + if (realIndex === _shownIndex) { return; } // loop-snap second fire — no-op + _shownIndex = realIndex; + showTagline(taglines[realIndex] || '', tlStyles[realIndex]); + }); + + + $('.js-header-media-prev').on('click', function () { + $imgCarousel.trigger('prev.owl.carousel'); + }); + $('.js-header-media-next').on('click', function () { + $imgCarousel.trigger('next.owl.carousel'); + }); + + + if (autoPlay) { + var ap = owl._plugins && owl._plugins.autoplay; + + if (ap) { ap.pause = function () {}; } + + $imgCarousel.on('translated.owl.carousel', function () { + if (ap && owl.is('rotating') && !ap._paused) { + ap._setAutoPlayInterval(); + } + }); + + document.addEventListener('visibilitychange', function () { + if (!document.hidden) { + $imgCarousel.trigger('stop.owl.autoplay'); + $imgCarousel.trigger('play.owl.autoplay'); + } + }); + + var _watchdogTimer = setInterval(function () { + if (document.hidden) { return; } + var apNow = owl._plugins && owl._plugins.autoplay; + if (!owl.is('rotating') || (apNow && apNow._paused)) { + $imgCarousel.trigger('stop.owl.autoplay'); + $imgCarousel.trigger('play.owl.autoplay'); + } + }, interval + 1000); + + $(window).one('beforeunload', function () { + clearInterval(_watchdogTimer); + }); + } + +}); diff --git a/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl new file mode 100644 index 0000000000..fc293811d8 --- /dev/null +++ b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl @@ -0,0 +1,613 @@ +{** +* NOTICE OF LICENSE +* +* This source file is subject to the Open Software License version 3.0 +* that is bundled with this package in the file LICENSE.md +* It is also available through the world-wide-web at this URL: +* https://opensource.org/license/osl-3-0-php +* If you did not receive a copy of the license and are unable to +* obtain it through the world-wide-web, please send an email +* to support@qloapps.com so we can send you a copy immediately. +* +* DISCLAIMER +* +* Do not edit or add to this file if you wish to upgrade this module to a newer +* versions in the future. If you wish to customize this module for your needs +* please refer to https://store.webkul.com/customisation-guidelines for more information. +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*} + +
    + + + +
    +
    +  {l s='Header Media Settings' mod='hotelreservationsystem'} +
    +
    + +
    + +
    + +

    {l s='Choose whether the home page header shows a slideshow of images or a background video.' mod='hotelreservationsystem'}

    +
    +
    + + + + +
    + +
    +
    + +{* Confirm switch-to-video modal — triggered when saving with Video selected while 2+ images exist *} + + +{* Confirm bulk-delete images modal — triggered by the "Delete selected" bulk action *} + + + + +{* Bulk tag line modal — triggered by the "Set tag line for all images" button above *} + + + diff --git a/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl b/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl index 9ea527e801..448ed0cb84 100644 --- a/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl +++ b/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl @@ -24,17 +24,17 @@
    -
    +
    -
    -

    {l s='Welcome To' mod='hotelreservationsystem'}

    -
    +
    + {if $QLO_HOTEL_NAME_ENABLE && $QLO_HEADER_MEDIA_TYPE == $QLO_HEADER_MEDIA_TYPE_IMAGE} {block name='header_hotel_chain_name'}

    {$WK_HTL_CHAIN_NAME|escape:'htmlall':'UTF-8'}

    {/block} + {/if} {block name='header_hotel_description'} -

    {$WK_HTL_TAG_LINE|escape:'htmlall':'UTF-8'}

    + {/block}
    diff --git a/themes/hotel-reservation-theme/header.tpl b/themes/hotel-reservation-theme/header.tpl index 8dc419a6bf..6fa210eae0 100644 --- a/themes/hotel-reservation-theme/header.tpl +++ b/themes/hotel-reservation-theme/header.tpl @@ -87,7 +87,62 @@ {/if}
    -