commit ba8c1e69576890bf4807bd1c8ca463fba08c94ee Author: O K Date: Sun Sep 7 12:50:02 2025 +0300 first commit diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..4d5fb6f --- /dev/null +++ b/README.MD @@ -0,0 +1,191 @@ +

+ Product Countdown Module Logo +

+ +

Product Discount Countdown for PrestaShop

+ +

+ PrestaShop Version + License: MIT + + Donate via WayForPay + +

+ +

+ A free, feature-rich PrestaShop module that displays a real-time countdown timer for products with specific prices (discounts), creating a sense of urgency to boost your sales. +

+ +
+ +

+ English | Українська | Русский +

+ +
+ +## 🇬🇧 English + +Boost your store's conversion rate by visually highlighting limited-time offers. This module adds a customizable countdown timer directly on the product page for any item with an expiring specific price, encouraging customers to make a purchase before the deal is gone. + +### ✨ Key Features + +* **Real-Time Countdown:** Displays days, hours, minutes, and seconds remaining for a special offer. +* **Highly Customizable:** + * Set a **display threshold** (e.g., only show the timer if the offer ends in less than 30 days). + * Choose the **display position** (after price, near add to cart, etc.). + * Customize all **front-end text**. + * Select custom **background and text colors** for the badge to match your theme. + * Option to display the name of the Catalog Price Rule (e.g., "Black Friday Sale"). + * Add your own styles with a **Custom CSS** field. +* **AJAX Compatible:** The timer correctly reloads when a customer changes product combinations (attributes). +* **Smart Logic:** + * Automatically detects the customer's country or uses the store's default. + * Choose what happens when the timer expires: hide, reload the page, or show a message. +* **Modern & Translatable:** Built using PrestaShop's modern translation system. +* **Lightweight:** Clean code ensures minimal impact on your site's performance. + +### 📸 Screenshots + +#### Admin Configuration Interface +*Manage all settings from a clean and intuitive back-office panel.* +![Admin Interface Demo](demo1.jpg) + +#### Front-End Countdown Timer +*A clear and attractive timer displayed on the product page.* +![Front-End Interface Demo](demo2.jpg) + +### ⚙️ Compatibility + +* **PrestaShop:** Version 1.7.8 or newer. + +### 🛠️ Installation + +1. Download the latest release `.zip` file from the [releases page](https://github.com/panariga/productcountdown/releases). +2. In your PrestaShop Back Office, navigate to **Modules > Module Manager**. +3. Click on **"Upload a module"** and select the `.zip` file you downloaded. +4. After installation, click **"Configure"** to set up the module. + +### ❤️ Support & Contribution + +This module is completely free and developed in my spare time. If you find it useful and it helps your business, please consider showing your appreciation with a small donation. Your support helps cover the costs of development and motivates me to continue improving this module and creating new ones. + +

+ + Donate + +

+ +Found a bug or have a feature request? Please [open an issue](https://github.com/panariga/productcountdown/issues) on GitHub. + +### 📄 License + +This module is released under the [MIT License](LICENSE). + +--- + +## 🇺🇦 Українська + +Збільшуйте конверсію вашого магазину, візуально виділяючи обмежені в часі пропозиції. Цей модуль додає таймер зворотного відліку на сторінку товару для будь-якого продукту з акційною ціною, що закінчується, спонукаючи клієнтів зробити покупку, доки пропозиція діє. + +### ✨ Ключові можливості + +* **Таймер у реальному часі:** Показує дні, години, хвилини та секунди, що залишилися до кінця акції. +* **Гнучке налаштування:** + * Встановіть **поріг відображення** (наприклад, показувати таймер, лише якщо акція закінчується менш ніж за 30 днів). + * Виберіть **позицію для відображення** (після ціни, біля кнопки "Додати в кошик" тощо). + * Налаштуйте всі **тексти, що бачить користувач**. + * Виберіть **колір фону та тексту** для таймера, щоб він пасував до вашої теми. + * Можливість показувати назву правила каталогу цін (напр., "Чорна п'ятниця"). + * Додайте власні стилі за допомогою поля **Custom CSS**. +* **Сумісність з AJAX:** Таймер коректно перезавантажується, коли клієнт змінює комбінації товару (атрибути). +* **Розумна логіка:** + * Автоматично визначає країну клієнта або використовує країну магазину за замовчуванням. + * Виберіть дію після закінчення таймера: приховати, перезавантажити сторінку або показати повідомлення. +* **Сучасний та перекладний:** Створений з використанням сучасної системи перекладів PrestaShop. +* **Легкий:** Чистий код забезпечує мінімальний вплив на продуктивність вашого сайту. + +### 📸 Скріншоти + +*(Дивіться скріншоти в англійській секції вище)* + +### ⚙️ Сумісність + +* **PrestaShop:** Версія 1.7.8 або новіша. + +### 🛠️ Встановлення + +1. Завантажте останню версію `.zip` файлу зі [сторінки релізів](https://github.com/panariga/productcountdown/releases). +2. У вашій адмін-панелі PrestaShop перейдіть до **Модулі > Менеджер модулів**. +3. Натисніть **"Завантажити модуль"** та виберіть завантажений `.zip` файл. +4. Після встановлення натисніть **"Налаштувати"**, щоб сконфігурувати модуль. + +### ❤️ Підтримка та внесок + +Цей модуль є повністю безкоштовним і розроблений у мій вільний час. Якщо він вам подобається і допомагає вашому бізнесу, будь ласка, підтримайте його невеликим внеском. Ваша підтримка допомагає покрити витрати на розробку та мотивує мене продовжувати вдосконалювати цей модуль та створювати нові. + +

+ + Підтримати + +

+ +Знайшли помилку або маєте ідею? Будь ласка, [створіть issue](https://github.com/panariga/productcountdown/issues) на GitHub. + +### 📄 Ліцензія + +Цей модуль випущено під [ліцензією MIT](LICENSE). + +--- + +## 🇷🇺 Русский + +Увеличивайте конверсию вашего магазина, визуально выделяя ограниченные по времени предложения. Этот модуль добавляет таймер обратного отсчета на страницу товара для любого продукта с истекающей специальной ценой, побуждая клиентов совершить покупку, пока предложение действует. + +### ✨ Ключевые возможности + +* **Таймер в реальном времени:** Показывает дни, часы, минуты и секунды, оставшиеся до конца акции. +* **Гибкая настройка:** + * Установите **порог отображения** (например, показывать таймер, только если акция заканчивается менее чем через 30 дней). + * Выберите **позицию для отображения** (после цены, возле кнопки "Добавить в корзину" и т.д.). + * Настройте все **тексты, которые видит пользователь**. + * Выберите **цвет фона и текста** для таймера, чтобы он соответствовал вашей теме. + * Возможность отображать название правила каталога цен (напр., "Черная пятница"). + * Добавьте собственные стили с помощью поля **Custom CSS**. +* **Совместимость с AJAX:** Таймер корректно перезагружается, когда клиент меняет комбинации товара (атрибуты). +* **Умная логика:** + * Автоматически определяет страну клиента или использует страну магазина по умолчанию. + * Выберите действие после окончания таймера: скрыть, перезагрузить страницу или показать сообщение. +* **Современный и переводимый:** Создан с использованием современной системы переводов PrestaShop. +* **Легковесный:** Чистый код обеспечивает минимальное влияние на производительность вашего сайта. + +### 📸 Скриншоты + +*(Смотрите скриншоты в английской секции выше)* + +### ⚙️ Совместимость + +* **PrestaShop:** Версия 1.7.8 или новее. + +### 🛠️ Установка + +1. Скачайте последнюю версию `.zip` файла со [страницы релизов](https://github.com/panariga/productcountdown/releases). +2. В вашей админ-панели PrestaShop перейдите в **Модули > Менеджер модулей**. +3. Нажмите **"Загрузить модуль"** и выберите скачанный `.zip` файл. +4. После установки нажмите **"Настроить"**, чтобы сконфигурировать модуль. + +### ❤️ Поддержка и вклад + +Этот модуль является полностью бесплатным и разработан в мое свободное время. Если он вам нравится и помогает вашему бизнесу, пожалуйста, поддержите его небольшим пожертвованием. Ваша поддержка помогает покрыть расходы на разработку и мотивирует меня продолжать улучшать этот модуль и создавать новые. + +

+ + Поддержать + +

+ +Нашли ошибку или есть идея? Пожалуйста, [создайте issue](https://github.com/panariga/productcountdown/issues) на GitHub. + +### 📄 Лицензия + +Этот модуль выпущен под [лицензией MIT](LICENSE). \ No newline at end of file diff --git a/demo1.jpg b/demo1.jpg new file mode 100644 index 0000000..c0faf66 Binary files /dev/null and b/demo1.jpg differ diff --git a/demo2.jpg b/demo2.jpg new file mode 100644 index 0000000..0456b07 Binary files /dev/null and b/demo2.jpg differ diff --git a/logo.jpg b/logo.jpg new file mode 100644 index 0000000..3a25ef1 Binary files /dev/null and b/logo.jpg differ diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..95cbf58 Binary files /dev/null and b/logo.png differ diff --git a/productcountdown.php b/productcountdown.php new file mode 100644 index 0000000..39e00e2 --- /dev/null +++ b/productcountdown.php @@ -0,0 +1,472 @@ +name = 'productcountdown'; + $this->tab = 'front_office_features'; + $this->version = '1.0.0'; + $this->author = 'Panariga'; + $this->need_instance = 0; + $this->ps_versions_compliancy = ['min' => '1.7.8', 'max' => _PS_VERSION_]; + $this->bootstrap = true; + + parent::__construct(); + + $this->displayName = $this->trans('Product Discount Countdown', [], 'Modules.Productcountdown.Admin'); + $this->description = $this->trans('Displays a countdown for products with a limited-time discount.', [], 'Modules.Productcountdown.Admin'); + } + + public function isUsingNewTranslationSystem() + { + return true; + } + + public function install() + { + if (!parent::install()) { + return false; + } + + // Set default configuration values + Configuration::updateValue('PDC_ENABLED', 1); + Configuration::updateValue('PDC_MAX_DAYS', 30); + // Default hook is 'displayProductPriceBlock', but we register all of them. + Configuration::updateValue('PDC_HOOK_POSITION', 'displayProductPriceBlock'); + Configuration::updateValue('PDC_PREFIX_TEXT', ['en' => 'Special offer ends in:', 'fr' => 'Offre spéciale se termine dans :']); + Configuration::updateValue('PDC_SHOW_NAME', 0); + Configuration::updateValue('PDC_ON_EXPIRE', 'hide'); + Configuration::updateValue('PDC_EXPIRED_TEXT', ['en' => 'This special offer has expired.']); + Configuration::updateValue('PDC_BADGE_BG_COLOR', '#D9534F'); // A nice default red + Configuration::updateValue('PDC_BADGE_TEXT_COLOR', '#FFFFFF'); // White text + Configuration::updateValue('PDC_CUSTOM_CSS', ''); + + // Register ALL possible hooks. The logic to display will be inside each hook method. + return $this->registerHook('displayHeader') + && $this->registerHook('displayProductPriceBlock') + && $this->registerHook('displayProductActions') + && $this->registerHook('displayProductAdditionalInfo'); + } + + // ... uninstall() remains the same ... + public function uninstall() + { + // Delete all configuration values + $configKeys = [ + 'PDC_ENABLED', + 'PDC_MAX_DAYS', + 'PDC_HOOK_POSITION', + 'PDC_PREFIX_TEXT', + 'PDC_SHOW_NAME', + 'PDC_ON_EXPIRE', + 'PDC_EXPIRED_TEXT', + 'PDC_CUSTOM_CSS', + + 'PDC_BADGE_BG_COLOR', + 'PDC_BADGE_TEXT_COLOR' + ]; + foreach ($configKeys as $key) { + Configuration::deleteByName($key); + } + + return parent::uninstall(); + } + + public function getContent() + { + $output = ''; + if (Tools::isSubmit('submit' . $this->name)) { + $this->postProcess(); + $output .= $this->displayConfirmation($this->trans('Settings updated', [], 'Admin.Notifications.Success')); + } + // Generate the HTML for the form and the support panel + $formHtml = $this->renderForm(); + $supportPanelHtml = $this->renderSupportPanel(); + + // Combine them into a two-column layout using Bootstrap's grid system + $finalHtml = ' +
+
' . $formHtml . '
+
' . $supportPanelHtml . '
+
+ '; + + return $output . $finalHtml; + } + + /** + * SIMPLIFIED postProcess() method + */ + protected function postProcess() + { + $form_values = $this->getConfigFormValues(); + $languages = Language::getLanguages(false); + + foreach (array_keys($form_values) as $key) { + if (in_array($key, ['PDC_PREFIX_TEXT', 'PDC_EXPIRED_TEXT'])) { + $values = []; + foreach ($languages as $lang) { + $values[$lang['id_lang']] = Tools::getValue($key . '_' . $lang['id_lang']); + } + Configuration::updateValue($key, $values); + } else { + // The complex hook registration logic is no longer needed here. + // We just save the value. + Configuration::updateValue($key, Tools::getValue($key)); + } + } + } + + // ... renderForm(), getSettingsForm(), getStylingForm(), getConfigFormValues() remain the same ... + + + // --- HOOKS AND FRONT OFFICE LOGIC --- + + /** + * NEW: Dedicated hook method for displayProductPriceBlock + * This correctly handles the 'type' parameter. + */ + public function hookDisplayProductPriceBlock($params) + { + // 1. Check if this is the hook selected by the user in the configuration. + if (Configuration::get('PDC_HOOK_POSITION') === 'displayProductPriceBlock') { + // 2. Check for the specific 'type' to prevent multiple displays. + if (isset($params['type']) && $params['type'] === 'after_price') { + return $this->renderCountdown($params); + } + } + } + + /** + * NEW: Dedicated hook method for displayProductActions + */ + public function hookDisplayProductActions($params) + { + if (Configuration::get('PDC_HOOK_POSITION') === 'displayProductActions') { + return $this->renderCountdown($params); + } + } + + /** + * NEW: Dedicated hook method for displayProductAdditionalInfo + */ + public function hookDisplayProductAdditionalInfo($params) + { + if (Configuration::get('PDC_HOOK_POSITION') === 'displayProductAdditionalInfo') { + return $this->renderCountdown($params); + } + } + + // ... hookDisplayHeader(), renderCountdown(), and getActiveDiscount() remain the same ... + public function hookDisplayHeader() + { + if ($this->context->controller instanceof ProductController) { + $this->context->controller->registerJavascript( + 'module-productcountdown-js', + 'modules/' . $this->name . '/views/js/front.js', + ['position' => 'bottom', 'priority' => 150] + ); + $this->context->controller->registerStylesheet( + 'module-productcountdown-css', + 'modules/' . $this->name . '/views/css/front.css', + ['media' => 'all', 'priority' => 150] + ); + + $customCss = Configuration::get('PDC_CUSTOM_CSS'); + if (!empty($customCss)) { + Media::addInlineStyle($customCss); + } + } + } + + public function renderCountdown($params) + { + if (!(bool)Configuration::get('PDC_ENABLED')) { + return; + } + + $product = isset($params['product']) ? $params['product'] : null; + if (!$product || !is_object($product) || !isset($product->id)) { + return; + } + + $activeDiscount = $this->getActiveDiscount($product); + + if (!$activeDiscount) { + return; + } + + $activeDiscountTime = strtotime($activeDiscount['to']); + $maxDays = (int)Configuration::get('PDC_MAX_DAYS', null, null, null, 30); + $maxTime = ($maxDays > 0) ? (time() + 60 * 60 * 24 * $maxDays) : 0; + + if ($activeDiscountTime > time() && ($maxTime === 0 || $activeDiscountTime < $maxTime)) { + + $discountName = null; + if ((bool)Configuration::get('PDC_SHOW_NAME') && (int)$activeDiscount['id_specific_price_rule'] > 0) { + $rule = new SpecificPriceRule($activeDiscount['id_specific_price_rule']); + if (Validate::isLoadedObject($rule)) { + $discountName = $rule->name[$this->context->language->id] ?? current($rule->name); + } + } + + $prefixText = Configuration::get('PDC_PREFIX_TEXT', $this->context->language->id, null, null, 'Special offer ends in:'); + $expiredText = Configuration::get('PDC_EXPIRED_TEXT', $this->context->language->id, null, null, 'This special offer has expired.'); + + $this->context->smarty->assign([ + 'countdown_timestamp' => $activeDiscountTime, + 'countdown_prefix' => $prefixText, + 'countdown_discount_name' => $discountName, + 'countdown_on_expire_action' => Configuration::get('PDC_ON_EXPIRE', null, null, null, 'hide'), + 'countdown_expired_text' => $expiredText, + // NEW: Assign colors to Smarty variables + 'countdown_bg_color' => Configuration::get('PDC_BADGE_BG_COLOR'), + 'countdown_text_color' => Configuration::get('PDC_BADGE_TEXT_COLOR'), + ]); + + return $this->display(__FILE__, 'views/templates/hook/countdown.tpl'); + } + } + + public function getActiveDiscount($product) + { + $id_country = (int)($this->context->country->id ?? Configuration::get('PS_COUNTRY_DEFAULT')); + + return SpecificPrice::getSpecificPrice( + (int)$product->id, + $this->context->shop->id, + $this->context->currency->id, + $id_country, + $this->context->customer->id_default_group, + 1, + null, + null + ); + } + + + + + /** + * Builds the configuration form using HelperForm. + */ + public function renderForm() + { + $helper = new HelperForm(); + $helper->show_toolbar = false; + $helper->table = $this->table; + $helper->module = $this; + $helper->default_form_language = $this->context->language->id; + $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0); + $helper->identifier = $this->identifier; + $helper->submit_action = 'submit' . $this->name; + $helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false) + . '&configure=' . $this->name . '&tab_module=' . $this->tab . '&module_name=' . $this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + + $helper->tpl_vars = [ + 'fields_value' => $this->getConfigFormValues(), + 'languages' => $this->context->controller->getLanguages(), + 'id_language' => $this->context->language->id, + ]; + + return $helper->generateForm([$this->getSettingsForm(), $this->getStylingForm()]); + } + + /** + * Defines the "Settings" section of the form. + */ + protected function getSettingsForm() + { + $hookOptions = [ + ['id_option' => 'displayProductPriceBlock', 'name' => $this->trans('After Price (Recommended)', [], 'Modules.Productcountdown.Admin')], + ['id_option' => 'displayProductActions', 'name' => $this->trans('Product Actions (Near Add to Cart)', [], 'Modules.Productcountdown.Admin')], + ['id_option' => 'displayProductAdditionalInfo', 'name' => $this->trans('Product Additional Info (Tabs)', [], 'Modules.Productcountdown.Admin')], + ]; + + $expireOptions = [ + ['id_option' => 'hide', 'name' => $this->trans('Hide the countdown timer', [], 'Modules.Productcountdown.Admin')], + ['id_option' => 'reload', 'name' => $this->trans('Reload the page', [], 'Modules.Productcountdown.Admin')], + ['id_option' => 'message', 'name' => $this->trans('Show an "Expired" message', [], 'Modules.Productcountdown.Admin')], + ]; + + return [ + 'form' => [ + 'legend' => [ + 'title' => $this->trans('General Settings', [], 'Modules.Productcountdown.Admin'), + 'icon' => 'icon-cogs', + ], + 'input' => [ + [ + 'type' => 'switch', + 'label' => $this->trans('Enable Countdown', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_ENABLED', + 'is_bool' => true, + 'desc' => $this->trans('Globally enable or disable the countdown timer.', [], 'Modules.Productcountdown.Admin'), + 'values' => [['id' => 'active_on', 'value' => 1, 'label' => $this->trans('Enabled', [], 'Admin.Global')], ['id' => 'active_off', 'value' => 0, 'label' => $this->trans('Disabled', [], 'Admin.Global')]], + ], + [ + 'type' => 'text', + 'label' => $this->trans('Display Threshold', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_MAX_DAYS', + 'class' => 'fixed-width-sm', + 'suffix' => $this->trans('days', [], 'Modules.Productcountdown.Admin'), + 'desc' => $this->trans('Only show the timer if the discount expires within this many days. Set to 0 to always show.', [], 'Modules.Productcountdown.Admin'), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Display Position (Hook)', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_HOOK_POSITION', + 'options' => ['query' => $hookOptions, 'id' => 'id_option', 'name' => 'name'], + 'desc' => $this->trans('Choose where the countdown timer should appear.', [], 'Modules.Productcountdown.Admin'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Countdown Prefix Text', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_PREFIX_TEXT', + 'lang' => true, + 'desc' => $this->trans('This text appears before the timer (e.g., "Offer ends in:").', [], 'Modules.Productcountdown.Admin'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Display Discount Name', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_SHOW_NAME', + 'is_bool' => true, + 'desc' => $this->trans('If available, show the name of the Catalog Price Rule.', [], 'Modules.Productcountdown.Admin'), + 'values' => [['id' => 'active_on', 'value' => 1, 'label' => $this->trans('Yes', [], 'Admin.Global')], ['id' => 'active_off', 'value' => 0, 'label' => $this->trans('No', [], 'Admin.Global')]], + ], + [ + 'type' => 'select', + 'label' => $this->trans('Action on Expiry', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_ON_EXPIRE', + 'options' => ['query' => $expireOptions, 'id' => 'id_option', 'name' => 'name'], + 'desc' => $this->trans('What should happen when the timer reaches zero?', [], 'Modules.Productcountdown.Admin'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Expired Message Text', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_EXPIRED_TEXT', + 'lang' => true, + 'desc' => $this->trans('This message is shown only if "Action on Expiry" is set to "Show an Expired message".', [], 'Modules.Productcountdown.Admin'), + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ], + ]; + } + + /** + * Renders the support and donation panel for the configuration page. + */ + protected function renderSupportPanel() + { + // Define your donation link here + $donationLink = 'https://secure.wayforpay.com/donate/dd579282b23b4'; // <--- CHANGE THIS LINK + + $this->context->smarty->assign([ + 'panel_title' => $this->trans('Enjoying this module?', [], 'Modules.Productcountdown.Admin'), + 'panel_body_text_1' => $this->trans('This module is offered for free on GitHub.', [], 'Modules.Productcountdown.Admin'), + 'panel_body_text_2' => $this->trans('If it helps your business, please consider supporting its future development by making a small donation.', [], 'Modules.Productcountdown.Admin'), + 'button_text' => $this->trans('Buy me a donut', [], 'Modules.Productcountdown.Admin'), + 'donation_link' => $donationLink, + ]); + + return $this->display(__FILE__, 'views/templates/admin/support_panel.tpl'); + } + + /** + * Defines the "Styling" section of the form. + */ + protected function getStylingForm() + { + return [ + 'form' => [ + 'legend' => [ + 'title' => $this->trans('Styling', [], 'Modules.Productcountdown.Admin'), + 'icon' => 'icon-paint-brush', + ], + 'input' => [ + // NEW: Color pickers + [ + 'type' => 'color', + 'label' => $this->trans('Badge Background Color', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_BADGE_BG_COLOR', + 'desc' => $this->trans('Choose the background color for the countdown badge.', [], 'Modules.Productcountdown.Admin'), + ], + [ + 'type' => 'color', + 'label' => $this->trans('Badge Text Color', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_BADGE_TEXT_COLOR', + 'desc' => $this->trans('Choose the text color for the countdown badge.', [], 'Modules.Productcountdown.Admin'), + ], + // END NEW + [ + 'type' => 'textarea', + 'label' => $this->trans('Custom CSS', [], 'Modules.Productcountdown.Admin'), + 'name' => 'PDC_CUSTOM_CSS', + 'desc' => $this->trans('Add your custom CSS rules here. The main container has the class ".product-countdown-container".', [], 'Modules.Productcountdown.Admin'), + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ], + ]; + } + + + /** + * Retrieves current configuration values to populate the form. + */ + protected function getConfigFormValues() + { + $languages = Language::getLanguages(false); + $fields = []; + + $fields['PDC_ENABLED'] = Configuration::get('PDC_ENABLED'); + $fields['PDC_MAX_DAYS'] = Configuration::get('PDC_MAX_DAYS'); + $fields['PDC_HOOK_POSITION'] = Configuration::get('PDC_HOOK_POSITION'); + $fields['PDC_SHOW_NAME'] = Configuration::get('PDC_SHOW_NAME'); + $fields['PDC_ON_EXPIRE'] = Configuration::get('PDC_ON_EXPIRE'); + $fields['PDC_CUSTOM_CSS'] = Configuration::get('PDC_CUSTOM_CSS'); + $fields['PDC_BADGE_BG_COLOR'] = Configuration::get('PDC_BADGE_BG_COLOR'); + $fields['PDC_BADGE_TEXT_COLOR'] = Configuration::get('PDC_BADGE_TEXT_COLOR'); + + foreach ($languages as $lang) { + $fields['PDC_PREFIX_TEXT'][$lang['id_lang']] = Tools::getValue( + 'PDC_PREFIX_TEXT_' . $lang['id_lang'], + Configuration::get('PDC_PREFIX_TEXT', $lang['id_lang']) + ); + $fields['PDC_EXPIRED_TEXT'][$lang['id_lang']] = Tools::getValue( + 'PDC_EXPIRED_TEXT_' . $lang['id_lang'], + Configuration::get('PDC_EXPIRED_TEXT', $lang['id_lang']) + ); + } + + return $fields; + } + + // --- HOOKS AND FRONT OFFICE LOGIC --- + + public function __call($method, $args) + { + if (strpos($method, 'hook') === 0) { + $configuredHook = Configuration::get('PDC_HOOK_POSITION', null, null, null, 'displayProductPriceBlock'); + if (strtolower($method) === 'hook' . strtolower($configuredHook)) { + echo ''; + + return $this->renderCountdown($args[0]); + } + } + return parent::__call($method, $args); + } +} diff --git a/views/css/front.css b/views/css/front.css new file mode 100644 index 0000000..acb8c1e --- /dev/null +++ b/views/css/front.css @@ -0,0 +1,27 @@ +.product-countdown-container { + margin-top: 10px; + margin-bottom: 10px; + clear: both; +} + +.product-countdown-container .countdown-name { + font-weight: bold; + margin-bottom: 5px; + font-size: 0.9rem; + color: #333; +} + +/* MODIFIED: Changed selector from .badge to .countdown-badge */ +.product-countdown-container .countdown-wrapper .countdown-badge { + padding: 0.5em 0.75em; + font-size: 1rem; + font-weight: 600; + /* Colors are now handled by inline styles, so they are not needed here */ +} + +.product-countdown-container .countdown-timer { + display: inline-block; + margin-left: 5px; + min-width: 120px; + text-align: left; +} \ No newline at end of file diff --git a/views/js/front.js b/views/js/front.js new file mode 100644 index 0000000..94132f6 --- /dev/null +++ b/views/js/front.js @@ -0,0 +1,117 @@ +/** + * Product Discount Countdown Module + * + * This script initializes countdown timers on the product page. + * It handles the initial page load and the dynamic updates that occur + * when a customer changes a product combination (attribute). + */ + +// A global array to keep track of our active timer intervals. +// This is crucial for cleanup when the product block is updated via AJAX. +let productCountdownIntervals = []; + +/** + * Scans the DOM for countdown containers and initializes them. + * This function is designed to be called on page load and after AJAX updates. + */ +function initializeProductCountdowns() { + // 1. Clear any previously running timers. + // This prevents multiple timers from running after a product combination change. + productCountdownIntervals.forEach(intervalId => clearInterval(intervalId)); + productCountdownIntervals = []; + + // 2. Find all countdown containers on the page that need to be processed. + const countdownContainers = document.querySelectorAll('.product-countdown-container'); + + countdownContainers.forEach(container => { + // Check if this specific container has already been initialized to avoid race conditions. + if (container.dataset.initialized === 'true') { + return; + } + + const timerElement = container.querySelector('.countdown-timer'); + if (!timerElement) return; + + // Mark as initialized + container.dataset.initialized = 'true'; + + const targetTimestamp = parseInt(container.dataset.timestamp, 10); + const onExpireAction = container.dataset.onExpire; + const expiredText = container.dataset.expiredText; + + // Get translated units from hidden spans + const units = { + day: container.querySelector('[data-unit="day"]').textContent, + days: container.querySelector('[data-unit="days"]').textContent, + hr: container.querySelector('[data-unit="hr"]').textContent, + min: container.querySelector('[data-unit="min"]').textContent, + sec: container.querySelector('[data-unit="sec"]').textContent + }; + + function updateTimer() { + const now = Math.floor(new Date().getTime() / 1000); + const diff = targetTimestamp - now; + + if (diff <= 0) { + clearInterval(interval); + handleExpiry(); + return; + } + + const d = Math.floor(diff / (60 * 60 * 24)); + const h = Math.floor((diff % (60 * 60 * 24)) / (60 * 60)); + const m = Math.floor((diff % (60 * 60)) / 60); + const s = Math.floor(diff % 60); + + const parts = []; + if (d > 0) { + parts.push(`${d} ${d > 1 ? units.days : units.day}`); + } + // Always show hours, minutes, and seconds for a better countdown experience + parts.push(`${String(h).padStart(2, '0')}${units.hr}`); + parts.push(`${String(m).padStart(2, '0')}${units.min}`); + parts.push(`${String(s).padStart(2, '0')}${units.sec}`); + + timerElement.textContent = parts.join(' '); + } + + function handleExpiry() { + switch (onExpireAction) { + case 'reload': + location.reload(); + break; + case 'message': + container.querySelector('.countdown-wrapper').innerHTML = `${expiredText}`; + break; + case 'hide': + default: + container.style.display = 'none'; + break; + } + } + + const interval = setInterval(updateTimer, 1000); + // Add the new interval ID to our global array for tracking. + productCountdownIntervals.push(interval); + updateTimer(); // Initial call to display the timer immediately + }); +} + +// --- Event Listeners --- + +// 1. Run the initializer on the initial page load. +document.addEventListener('DOMContentLoaded', () => { + initializeProductCountdowns(); +}); + +// 2. Run the initializer whenever PrestaShop updates the product block via AJAX. +// The `prestashop` object is globally available in modern themes. +if (typeof prestashop !== 'undefined') { + prestashop.on('updateProduct', (data) => { + // We use a small timeout to ensure the DOM has been fully updated by PrestaShop's scripts + // before our script runs and looks for the new container. 100ms is usually safe. + setTimeout(() => { + initializeProductCountdowns(); + }, 100); + }); +} \ No newline at end of file diff --git a/views/templates/admin/support_panel.tpl b/views/templates/admin/support_panel.tpl new file mode 100644 index 0000000..03a6293 --- /dev/null +++ b/views/templates/admin/support_panel.tpl @@ -0,0 +1,12 @@ +
+
+ {$panel_title} +
+
+

{$panel_body_text_1}

+

{$panel_body_text_2}

+ + {$button_text} + +
+
\ No newline at end of file diff --git a/views/templates/hook/countdown.tpl b/views/templates/hook/countdown.tpl new file mode 100644 index 0000000..2a7f1df --- /dev/null +++ b/views/templates/hook/countdown.tpl @@ -0,0 +1,24 @@ +
+ + {if !empty($countdown_discount_name)} +

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

+ {/if} + +
+ {* MODIFIED: Added style attribute and changed class *} + + {l s=$countdown_prefix d='Modules.Productcountdown.Shop'} + + +
+ + {* These hidden spans provide translated units for the javascript, no changes here *} + {l s='day' d='Modules.Productcountdown.Shop'} + {l s='days' d='Modules.Productcountdown.Shop'} + {l s='hr' d='Modules.Productcountdown.Shop'} + {l s='min' d='Modules.Productcountdown.Shop'} + {l s='sec' d='Modules.Productcountdown.Shop'} +
\ No newline at end of file