From 0b4ed48d2f153432292d874d0df8e3c864da46b5 Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 17 Dec 2025 02:03:28 -0700 Subject: [PATCH] feat: Add premade card templates and click analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WordPress Plugin v2.0.0: - Add Promo Banner widget (dark banner with deal text) - Add Horizontal Product Row widget (wide list format) - Add Category Card widget (image-based categories) - Add Compact Card widget (dense grid layout) - Add CannaiQAnalytics click tracking (tracks add_to_cart, product_view, promo_click, category_click events) - Register cannaiq-templates Elementor category - Fix branding: CannaiQAnalytics (not CannaIQAnalytics) Backend: - Add POST /api/analytics/click endpoint for WordPress plugin - Accepts API token auth, records to product_click_events table - Stores metadata: product_name, price, category, url, referrer 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../public/downloads/cannaiq-menus-2.0.0.zip | Bin 61812 -> 72554 bytes backend/src/routes/click-analytics.ts | 77 +++- wordpress-plugin/assets/js/cannaiq-menus.js | 110 ++++- wordpress-plugin/cannaiq-menus.php | 26 +- .../widgets/card-template-category.php | 252 +++++++++++ .../widgets/card-template-compact.php | 405 ++++++++++++++++++ .../widgets/card-template-horizontal.php | 368 ++++++++++++++++ .../widgets/card-template-promo-banner.php | 276 ++++++++++++ 8 files changed, 1510 insertions(+), 4 deletions(-) create mode 100644 wordpress-plugin/widgets/card-template-category.php create mode 100644 wordpress-plugin/widgets/card-template-compact.php create mode 100644 wordpress-plugin/widgets/card-template-horizontal.php create mode 100644 wordpress-plugin/widgets/card-template-promo-banner.php diff --git a/backend/public/downloads/cannaiq-menus-2.0.0.zip b/backend/public/downloads/cannaiq-menus-2.0.0.zip index 6402d45c0033b333f4c4a8a4dd868536ebcae391..d1525469fa80f26d9067a19fa9f9a9479172e678 100644 GIT binary patch delta 20754 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 c@CS&)zXlB%SOk*_L { + try { + // Get API token from Authorization header + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Missing API token' }); + } + + const apiToken = authHeader.substring(7); + + // Validate API token and get store_id + const tokenResult = await pool.query( + 'SELECT store_id FROM api_tokens WHERE token = $1 AND is_active = true', + [apiToken] + ); + + if (tokenResult.rows.length === 0) { + return res.status(401).json({ error: 'Invalid API token' }); + } + + const tokenStoreId = tokenResult.rows[0].store_id; + + const { + event_type, + store_id, + product_id, + product_name, + product_price, + category, + url, + referrer, + timestamp + } = req.body; + + // Use store_id from token if not provided in request + const finalStoreId = store_id || tokenStoreId; + + // Insert click event + await pool.query(` + INSERT INTO product_click_events ( + store_id, + product_id, + brand_id, + action, + metadata, + occurred_at + ) VALUES ($1, $2, $3, $4, $5, $6) + `, [ + finalStoreId, + product_id || null, + null, // brand_id will be looked up later if needed + event_type || 'click', + JSON.stringify({ + product_name, + product_price, + category, + url, + referrer, + source: 'wordpress_plugin' + }), + timestamp || new Date().toISOString() + ]); + + res.json({ success: true }); + } catch (error: any) { + console.error('[ClickAnalytics] Error recording click:', error.message); + res.status(500).json({ error: 'Failed to record click' }); + } +}); + +// All other click analytics endpoints require authentication router.use(authMiddleware); /** diff --git a/wordpress-plugin/assets/js/cannaiq-menus.js b/wordpress-plugin/assets/js/cannaiq-menus.js index a4990a70..c8b175c8 100644 --- a/wordpress-plugin/assets/js/cannaiq-menus.js +++ b/wordpress-plugin/assets/js/cannaiq-menus.js @@ -1,15 +1,118 @@ /** * CannaIQ Menus - WordPress Plugin JavaScript - * v1.5.3 + * v2.0.0 */ (function($) { 'use strict'; + /** + * Click Analytics Tracker + */ + var CannaiQAnalytics = { + /** + * Track a click event + * @param {string} eventType - Type of event (add_to_cart, product_view, promo_click, etc) + * @param {object} data - Event data (product_id, store_id, product_name, etc) + */ + track: function(eventType, data) { + if (!window.cannaiqAnalytics || !window.cannaiqAnalytics.enabled) { + return; + } + + var payload = { + event_type: eventType, + store_id: data.store_id || window.cannaiqAnalytics.store_id, + product_id: data.product_id || null, + product_name: data.product_name || null, + product_price: data.product_price || null, + category: data.category || null, + url: data.url || window.location.href, + referrer: document.referrer || null, + timestamp: new Date().toISOString() + }; + + // Send to analytics endpoint + $.ajax({ + url: window.cannaiqAnalytics.api_url + '/analytics/click', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(payload), + headers: { + 'Authorization': 'Bearer ' + window.cannaiqAnalytics.api_token + }, + // Fire and forget - don't block user interaction + async: true + }); + }, + + /** + * Initialize click tracking on all CannaiQ elements + */ + init: function() { + var self = this; + + // Track Add to Cart clicks + $(document).on('click', '.cannaiq-cart-button, .cannaiq-add-to-cart, .cannaiq-hr-add-btn, .cannaiq-cc-button, [class*="cannaiq"][href*="dutchie"], [class*="cannaiq"][href*="iheartjane"]', function(e) { + var $el = $(this); + var $card = $el.closest('[data-product-id], .cannaiq-product-card, .cannaiq-premium-card, .cannaiq-horizontal-row, .cannaiq-compact-card'); + + self.track('add_to_cart', { + product_id: $card.data('product-id') || $el.data('product-id'), + product_name: $card.data('product-name') || $el.data('product-name') || $card.find('.cannaiq-product-name, .cannaiq-premium-name, .cannaiq-hr-name, .cannaiq-cc-name').first().text().trim(), + product_price: $card.data('product-price') || $el.data('product-price'), + store_id: $card.data('store-id') || $el.data('store-id'), + category: $card.data('category') || $el.data('category'), + url: $el.attr('href') + }); + }); + + // Track product card clicks (view intent) + $(document).on('click', '.cannaiq-product-card, .cannaiq-premium-card, .cannaiq-special-card', function(e) { + // Don't double-track if clicking the cart button + if ($(e.target).closest('.cannaiq-cart-button, .cannaiq-add-to-cart').length) { + return; + } + + var $card = $(this); + self.track('product_view', { + product_id: $card.data('product-id'), + product_name: $card.data('product-name') || $card.find('.cannaiq-product-name, .cannaiq-premium-name').first().text().trim(), + product_price: $card.data('product-price'), + store_id: $card.data('store-id'), + category: $card.data('category') + }); + }); + + // Track promo banner clicks + $(document).on('click', '.cannaiq-promo-banner .cannaiq-promo-button, .cannaiq-promo-banner', function(e) { + var $banner = $(this).closest('.cannaiq-promo-banner'); + self.track('promo_click', { + store_id: $banner.data('store-id'), + promo_headline: $banner.find('.cannaiq-promo-headline').text().trim(), + url: $(this).attr('href') || $banner.find('a').first().attr('href') + }); + }); + + // Track category clicks + $(document).on('click', '.cannaiq-category-card, .cannaiq-category-item', function(e) { + var $cat = $(this); + self.track('category_click', { + store_id: $cat.data('store-id'), + category: $cat.data('category') || $cat.find('.cannaiq-cat-name, .cannaiq-category-name').first().text().trim(), + url: $cat.attr('href') + }); + }); + } + }; + /** * Initialize plugin */ $(document).ready(function() { + // Initialize analytics tracking + CannaiQAnalytics.init(); + // Lazy load images if ('IntersectionObserver' in window) { const imageObserver = new IntersectionObserver((entries, observer) => { @@ -49,10 +152,13 @@ threshold: 0.1 }); - document.querySelectorAll('.cannaiq-product-card').forEach(card => { + document.querySelectorAll('.cannaiq-product-card, .cannaiq-premium-card, .cannaiq-compact-card').forEach(card => { cardObserver.observe(card); }); } }); + // Expose for external use + window.CannaiQAnalytics = CannaiQAnalytics; + })(jQuery); diff --git a/wordpress-plugin/cannaiq-menus.php b/wordpress-plugin/cannaiq-menus.php index 5420cedd..e67fbd2b 100644 --- a/wordpress-plugin/cannaiq-menus.php +++ b/wordpress-plugin/cannaiq-menus.php @@ -44,7 +44,7 @@ class CannaIQ_Menus_Plugin { } /** - * Register CannaIQ Elementor Widget Category + * Register CannaIQ Elementor Widget Categories */ public function register_elementor_category($elements_manager) { $elements_manager->add_category( @@ -54,6 +54,13 @@ class CannaIQ_Menus_Plugin { 'icon' => 'fa fa-cannabis', ] ); + $elements_manager->add_category( + 'cannaiq-templates', + [ + 'title' => __('CannaiQ Templates', 'cannaiq-menus'), + 'icon' => 'fa fa-th-large', + ] + ); } public function init() { @@ -116,6 +123,10 @@ class CannaIQ_Menus_Plugin { // Card templates (v2.0) require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-premium.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-promo-banner.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-horizontal.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-category.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-compact.php'; // Register legacy widgets $widgets_manager->register(new \CannaIQ_Menus_Product_Grid_Widget()); @@ -137,6 +148,10 @@ class CannaIQ_Menus_Plugin { // Register card templates (v2.0) $widgets_manager->register(new \CannaIQ_Premium_Card_Widget()); + $widgets_manager->register(new \CannaIQ_Promo_Banner_Widget()); + $widgets_manager->register(new \CannaIQ_Card_Horizontal_Widget()); + $widgets_manager->register(new \CannaIQ_Card_Category_Widget()); + $widgets_manager->register(new \CannaIQ_Card_Compact_Widget()); } /** @@ -166,6 +181,15 @@ class CannaIQ_Menus_Plugin { CANNAIQ_MENUS_VERSION, true ); + + // Pass analytics config to JavaScript + $api_token = get_option('cannaiq_api_token'); + wp_localize_script('cannaiq-menus-script', 'cannaiqAnalytics', [ + 'enabled' => !empty($api_token), + 'api_url' => CANNAIQ_MENUS_API_URL, + 'api_token' => $api_token, + 'store_id' => get_option('cannaiq_default_store_id', 1), + ]); } /** diff --git a/wordpress-plugin/widgets/card-template-category.php b/wordpress-plugin/widgets/card-template-category.php new file mode 100644 index 00000000..7258b72a --- /dev/null +++ b/wordpress-plugin/widgets/card-template-category.php @@ -0,0 +1,252 @@ +start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'category_name', + [ + 'label' => __('Category Name', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'Flower', + 'placeholder' => __('Category name...', 'cannaiq-menus'), + ] + ); + + $this->add_control( + 'category_image', + [ + 'label' => __('Category Image', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::MEDIA, + 'default' => [ + 'url' => '', + ], + ] + ); + + $this->add_control( + 'link_url', + [ + 'label' => __('Link URL', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::URL, + 'placeholder' => __('/products?category=flower', 'cannaiq-menus'), + 'default' => [ + 'url' => '#', + ], + ] + ); + + $this->add_control( + 'show_product_count', + [ + 'label' => __('Show Product Count', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'no', + ] + ); + + $this->add_control( + 'product_count', + [ + 'label' => __('Product Count', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 0, + 'condition' => [ + 'show_product_count' => 'yes', + ], + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'card_size', + [ + 'label' => __('Card Size', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'medium', + 'options' => [ + 'small' => __('Small (120px)', 'cannaiq-menus'), + 'medium' => __('Medium (160px)', 'cannaiq-menus'), + 'large' => __('Large (200px)', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'card_background', + [ + 'label' => __('Card Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + ] + ); + + $this->add_control( + 'border_color', + [ + 'label' => __('Border Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#e5e7eb', + ] + ); + + $this->add_control( + 'hover_border_color', + [ + 'label' => __('Hover Border Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#22c55e', + ] + ); + + $this->add_control( + 'text_color', + [ + 'label' => __('Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1f2937', + ] + ); + + $this->add_control( + 'border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 30, + ], + ], + 'default' => [ + 'size' => 12, + ], + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + $sizes = [ + 'small' => '120px', + 'medium' => '160px', + 'large' => '200px', + ]; + $size = $sizes[$settings['card_size']] ?? '160px'; + + $bg_color = $settings['card_background']; + $border_color = $settings['border_color']; + $hover_border = $settings['hover_border_color']; + $text_color = $settings['text_color']; + $radius = $settings['border_radius']['size'] . 'px'; + + $url = $settings['link_url']['url'] ?? '#'; + $target = !empty($settings['link_url']['is_external']) ? '_blank' : '_self'; + + $widget_id = $this->get_id(); + ?> + + +
+ + 0): ?> + + () + + +
+ + +
+ <?php echo esc_attr($settings['category_name']); ?> +
+ +
+ start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'store_id', + [ + 'label' => __('Store ID', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => get_option('cannaiq_default_store_id', 1), + 'min' => 1, + ] + ); + + $this->add_control( + 'limit', + [ + 'label' => __('Number of Products', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 12, + 'min' => 1, + 'max' => 50, + ] + ); + + $this->add_control( + 'columns', + [ + 'label' => __('Columns', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => '4', + 'options' => [ + '3' => __('3 Columns', 'cannaiq-menus'), + '4' => __('4 Columns', 'cannaiq-menus'), + '5' => __('5 Columns', 'cannaiq-menus'), + '6' => __('6 Columns', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'category', + [ + 'label' => __('Category', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => '', + 'options' => CannaIQ_Menus_Plugin::instance()->get_category_options(), + ] + ); + + $this->add_control( + 'specials_only', + [ + 'label' => __('Specials Only', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'no', + ] + ); + + $this->end_controls_section(); + + // Display Options + $this->start_controls_section( + 'display_section', + [ + 'label' => __('Display Options', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'show_brand', + [ + 'label' => __('Show Brand', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_thc_cbd', + [ + 'label' => __('Show THC/CBD', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_discount_badge', + [ + 'label' => __('Show Discount Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_original_price', + [ + 'label' => __('Show Original Price', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_cart_button', + [ + 'label' => __('Show Add to Cart', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'card_background', + [ + 'label' => __('Card Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + ] + ); + + $this->add_control( + 'border_color', + [ + 'label' => __('Border Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#e5e7eb', + ] + ); + + $this->add_control( + 'discount_badge_color', + [ + 'label' => __('Discount Badge Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#fbbf24', + ] + ); + + $this->add_control( + 'button_color', + [ + 'label' => __('Button Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f97316', + ] + ); + + $this->add_control( + 'border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 20, + ], + ], + 'default' => [ + 'size' => 8, + ], + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + $args = [ + 'store_id' => $settings['store_id'], + 'limit' => $settings['limit'], + ]; + + if (!empty($settings['category'])) { + $args['type'] = $settings['category']; + } + + $plugin = CannaIQ_Menus_Plugin::instance(); + + if ($settings['specials_only'] === 'yes') { + $products = $plugin->fetch_specials($args); + } else { + $products = $plugin->fetch_products($args); + } + + if (!$products) { + echo '

' . __('No products found.', 'cannaiq-menus') . '

'; + return; + } + + $columns = $settings['columns']; + $card_bg = $settings['card_background']; + $border_color = $settings['border_color']; + $discount_color = $settings['discount_badge_color']; + $btn_color = $settings['button_color']; + $radius = $settings['border_radius']['size'] . 'px'; + + $col_widths = [ + '3' => '33.333%', + '4' => '25%', + '5' => '20%', + '6' => '16.666%', + ]; + $col_width = $col_widths[$columns] ?? '25%'; + ?> +
+ 0 && $sale_price < $regular_price; + $discount_percent = $has_discount ? round((($regular_price - $sale_price) / $regular_price) * 100) : 0; + $brand = $product['brand'] ?? ''; + $thc = $product['thc_percentage'] ?? ''; + $cbd = $product['cbd_percentage'] ?? ''; + ?> +
+ +
+ <?php echo esc_attr($product['name']); ?> +
Stock photo. Actual product may vary.
+
+ + +
+ +
+ + +
+ +
+ + + +
+ THC: % + · + CBD: % +
+ + +
+ +
$
+ + +
+ + $ + + + % off + +
+
+ + + ADD TO CART + +
+ +
+ start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'store_id', + [ + 'label' => __('Store ID', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => get_option('cannaiq_default_store_id', 1), + 'min' => 1, + ] + ); + + $this->add_control( + 'limit', + [ + 'label' => __('Number of Products', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 10, + 'min' => 1, + 'max' => 50, + ] + ); + + $this->add_control( + 'category', + [ + 'label' => __('Category', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => '', + 'options' => CannaIQ_Menus_Plugin::instance()->get_category_options(), + ] + ); + + $this->add_control( + 'specials_only', + [ + 'label' => __('Specials Only', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'no', + ] + ); + + $this->end_controls_section(); + + // Display Options + $this->start_controls_section( + 'display_section', + [ + 'label' => __('Display Options', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'show_image', + [ + 'label' => __('Show Image', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_brand', + [ + 'label' => __('Show Brand', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_thc', + [ + 'label' => __('Show THC', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_weight', + [ + 'label' => __('Show Weight', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_special_tag', + [ + 'label' => __('Show Special Tag', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_discount_badge', + [ + 'label' => __('Show Discount Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_add_button', + [ + 'label' => __('Show Add Button', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'row_background', + [ + 'label' => __('Row Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + ] + ); + + $this->add_control( + 'border_color', + [ + 'label' => __('Border Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#e5e7eb', + ] + ); + + $this->add_control( + 'special_tag_color', + [ + 'label' => __('Special Tag Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#22c55e', + ] + ); + + $this->add_control( + 'discount_badge_color', + [ + 'label' => __('Discount Badge Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f97316', + ] + ); + + $this->add_control( + 'add_button_color', + [ + 'label' => __('Add Button Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#22c55e', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + $args = [ + 'store_id' => $settings['store_id'], + 'limit' => $settings['limit'], + ]; + + if (!empty($settings['category'])) { + $args['type'] = $settings['category']; + } + + $plugin = CannaIQ_Menus_Plugin::instance(); + + if ($settings['specials_only'] === 'yes') { + $products = $plugin->fetch_specials($args); + } else { + $products = $plugin->fetch_products($args); + } + + if (!$products) { + echo '

' . __('No products found.', 'cannaiq-menus') . '

'; + return; + } + + $row_bg = $settings['row_background']; + $border_color = $settings['border_color']; + $special_color = $settings['special_tag_color']; + $discount_color = $settings['discount_badge_color']; + $btn_color = $settings['add_button_color']; + ?> +
+ 0 && $sale_price < $regular_price; + $discount_percent = $has_discount ? round((($regular_price - $sale_price) / $regular_price) * 100) : 0; + $brand = $product['brand'] ?? ''; + $thc = $product['thc_percentage'] ?? ''; + $weight = $product['weight'] ?? $product['subcategory'] ?? ''; + $special_name = $product['special_name'] ?? ''; + ?> +
+ +
+ <?php echo esc_attr($product['name']); ?> +
+ + +
+
+ +
+ + +
+ +
+ + +
+ + THC: % + + + + + ● + + +
+
+ +
+ +
+ +
+ + +
+ $ +
+ + +
+ + $ + + + % off + +
+ +
+ + + + + +
+ +
+ start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'headline', + [ + 'label' => __('Headline', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => '2 for $35 | Eighth Flower (3.5g)', + 'placeholder' => __('Deal headline...', 'cannaiq-menus'), + 'label_block' => true, + ] + ); + + $this->add_control( + 'subtext', + [ + 'label' => __('Subtext', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'Lost Dutchmen ($20)', + 'placeholder' => __('Optional subtext...', 'cannaiq-menus'), + 'label_block' => true, + ] + ); + + $this->add_control( + 'image', + [ + 'label' => __('Product Image', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::MEDIA, + 'default' => [ + 'url' => '', + ], + ] + ); + + $this->add_control( + 'button_text', + [ + 'label' => __('Button Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'SHOP', + ] + ); + + $this->add_control( + 'button_url', + [ + 'label' => __('Button URL', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::URL, + 'placeholder' => __('https://...', 'cannaiq-menus'), + 'default' => [ + 'url' => '#', + ], + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'background_color', + [ + 'label' => __('Background Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1a1a2e', + ] + ); + + $this->add_control( + 'text_color', + [ + 'label' => __('Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + ] + ); + + $this->add_control( + 'button_bg_color', + [ + 'label' => __('Button Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#22c55e', + ] + ); + + $this->add_control( + 'button_text_color', + [ + 'label' => __('Button Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + ] + ); + + $this->add_control( + 'border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 30, + ], + ], + 'default' => [ + 'size' => 12, + ], + ] + ); + + $this->add_control( + 'show_watermark', + [ + 'label' => __('Show Watermark Pattern', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + $bg_color = $settings['background_color']; + $text_color = $settings['text_color']; + $btn_bg = $settings['button_bg_color']; + $btn_text = $settings['button_text_color']; + $radius = $settings['border_radius']['size'] . 'px'; + $show_watermark = $settings['show_watermark'] === 'yes'; + + $url = $settings['button_url']['url'] ?? '#'; + $target = !empty($settings['button_url']['is_external']) ? '_blank' : '_self'; + ?> +
+ +
+ + DEAL + +
+ + +
+
+ + +
+ +
+ + + + +
+ + +
+ <?php echo esc_attr($settings['headline']); ?> +
+ +
+