diff --git a/backend/src/index.ts b/backend/src/index.ts index 677a6a80..366d29f1 100755 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -38,42 +38,16 @@ app.use('/images', express.static(LOCAL_IMAGES_PATH)); // Usage: /img/products/az/store/brand/product/image.webp?w=200&h=200 app.use('/img', imageProxyRoutes); -// Serve static downloads (plugin files, etc.) -// Uses ./public/downloads relative to working directory (works for both Docker and local dev) -const LOCAL_DOWNLOADS_PATH = process.env.LOCAL_DOWNLOADS_PATH || './public/downloads'; +// Serve downloads from MinIO/CDN +// Files are stored in MinIO bucket at: cannaiq/downloads/ +const CDN_DOWNLOADS_URL = process.env.CDN_DOWNLOADS_URL || 'https://cdn.cannabrands.app/cannaiq/downloads'; -// Dynamic "latest" redirect for WordPress plugin - finds highest version automatically -app.get('/downloads/cannaiq-menus-latest.zip', (req, res) => { - const fs = require('fs'); - const path = require('path'); - try { - const files = fs.readdirSync(LOCAL_DOWNLOADS_PATH); - const pluginFiles = files - .filter((f: string) => f.match(/^cannaiq-menus-\d+\.\d+\.\d+\.zip$/)) - .sort((a: string, b: string) => { - const vA = a.match(/(\d+)\.(\d+)\.(\d+)/); - const vB = b.match(/(\d+)\.(\d+)\.(\d+)/); - if (!vA || !vB) return 0; - for (let i = 1; i <= 3; i++) { - const diff = parseInt(vB[i]) - parseInt(vA[i]); - if (diff !== 0) return diff; - } - return 0; - }); - - if (pluginFiles.length > 0) { - const latestFile = pluginFiles[0]; - res.redirect(302, `/downloads/${latestFile}`); - } else { - res.status(404).json({ error: 'No plugin versions found' }); - } - } catch (err) { - res.status(500).json({ error: 'Failed to find latest plugin' }); - } +// Redirect all download requests to CDN +app.get('/downloads/:filename', (req, res) => { + const filename = req.params.filename; + res.redirect(302, `${CDN_DOWNLOADS_URL}/${filename}`); }); -app.use('/downloads', express.static(LOCAL_DOWNLOADS_PATH)); - // Simple health check for load balancers/K8s probes app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); diff --git a/wordpress-plugin/VERSION b/wordpress-plugin/VERSION index dc1e644a..bd8bf882 100644 --- a/wordpress-plugin/VERSION +++ b/wordpress-plugin/VERSION @@ -1 +1 @@ -1.6.0 +1.7.0 diff --git a/wordpress-plugin/assets/css/cannaiq-menus.css b/wordpress-plugin/assets/css/cannaiq-menus.css index dfbd7665..a8dad324 100644 --- a/wordpress-plugin/assets/css/cannaiq-menus.css +++ b/wordpress-plugin/assets/css/cannaiq-menus.css @@ -1,6 +1,6 @@ /** * CannaIQ Menus - WordPress Plugin Styles - * v1.5.3 + * v1.7.0 */ /* Product Grid */ @@ -493,3 +493,189 @@ font-size: 14px; color: #999; } + +/* ======================================== + Product Loop Widget + ======================================== */ +.cannaiq-product-loop { + margin: 20px 0; +} + +.cannaiq-product-loop-grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(3, 1fr); +} + +.cannaiq-product-loop-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.cannaiq-product-loop-list .cannaiq-product-card { + flex-direction: row; +} + +.cannaiq-product-loop-list .cannaiq-product-image { + width: 150px; + min-width: 150px; + aspect-ratio: 1; +} + +.cannaiq-product-loop-masonry { + display: grid; + gap: 20px; + grid-template-columns: repeat(3, 1fr); +} + +.cannaiq-product-loop .cannaiq-product-card { + position: relative; +} + +.cannaiq-product-loop .cannaiq-product-name { + font-size: 16px; + font-weight: 600; + margin: 0 0 4px 0; + color: #1a1a1a; + line-height: 1.3; +} + +.cannaiq-product-loop .cannaiq-product-category { + font-size: 12px; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.cannaiq-product-loop .cannaiq-product-weight { + font-size: 13px; + color: #666; + background: #f0f0f0; + padding: 2px 8px; + border-radius: 4px; +} + +/* Strain Badges */ +.cannaiq-strain-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: #fff; + letter-spacing: 0.5px; +} + +/* Cannabinoid Display */ +.cannaiq-cannabinoids { + display: flex; + gap: 12px; + margin: 8px 0; + font-size: 13px; +} + +.cannaiq-cannabinoids .cannaiq-thc, +.cannaiq-cannabinoids .cannaiq-cbd { + padding: 4px 8px; + border-radius: 4px; + font-weight: 500; +} + +/* Effects Badges */ +.cannaiq-effects { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 8px 0; +} + +.cannaiq-effect-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background: #f0f0f0; + border-radius: 16px; + font-size: 11px; + font-weight: 500; + color: #444; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +/* Dynamic Tag Effects Badges */ +.cannaiq-effects-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.cannaiq-effects-badges .cannaiq-effect-badge { + padding: 4px 10px; + background: linear-gradient(135deg, #f5f5f5, #e8e8e8); + border-radius: 16px; + font-size: 11px; + font-weight: 500; + color: #444; +} + +/* Effect-specific colors */ +.cannaiq-effect-happy { background: linear-gradient(135deg, #fff3cd, #ffeaa7); color: #856404; } +.cannaiq-effect-relaxed { background: linear-gradient(135deg, #d4edda, #c3e6cb); color: #155724; } +.cannaiq-effect-sleepy { background: linear-gradient(135deg, #e2e3f3, #c8c9e8); color: #383d6e; } +.cannaiq-effect-focused { background: linear-gradient(135deg, #cce5ff, #b8daff); color: #004085; } +.cannaiq-effect-creative { background: linear-gradient(135deg, #f8d7da, #f5c6cb); color: #721c24; } +.cannaiq-effect-energetic { background: linear-gradient(135deg, #ffecb3, #ffe082); color: #7c5e00; } +.cannaiq-effect-hungry { background: linear-gradient(135deg, #ffe0cc, #ffcc99); color: #8b4513; } +.cannaiq-effect-euphoric { background: linear-gradient(135deg, #e8daef, #d7bde2); color: #5b2c6f; } +.cannaiq-effect-uplifted { background: linear-gradient(135deg, #d1ecf1, #bee5eb); color: #0c5460; } +.cannaiq-effect-talkative { background: linear-gradient(135deg, #cce5ff, #99ccff); color: #004085; } + +/* Sale Badge */ +.cannaiq-sale-badge { + position: absolute; + top: 10px; + left: 10px; + background: #e74c3c; + color: #fff; + font-size: 11px; + font-weight: 700; + padding: 4px 10px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.5px; + z-index: 1; +} + +/* No Products Message */ +.cannaiq-no-products { + text-align: center; + padding: 40px 20px; + color: #666; + font-size: 16px; +} + +/* Responsive */ +@media (max-width: 1024px) { + .cannaiq-product-loop-grid, + .cannaiq-product-loop-masonry { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .cannaiq-product-loop-grid, + .cannaiq-product-loop-masonry { + grid-template-columns: 1fr; + } + + .cannaiq-product-loop-list .cannaiq-product-card { + flex-direction: column; + } + + .cannaiq-product-loop-list .cannaiq-product-image { + width: 100%; + } +} diff --git a/wordpress-plugin/build-plugin.sh b/wordpress-plugin/build-plugin.sh index 9d9c128a..357546b9 100755 --- a/wordpress-plugin/build-plugin.sh +++ b/wordpress-plugin/build-plugin.sh @@ -1,6 +1,18 @@ #!/bin/bash # WordPress Plugin Build Script -# Builds the plugin zip with the correct naming convention: cannaiq-menus-{version}.zip +# Builds the plugin zip and uploads to MinIO CDN +# +# Usage: ./build-plugin.sh +# +# This script: +# 1. Extracts version from cannaiq-menus.php +# 2. Creates ZIP file excluding legacy/dev files +# 3. Uploads to MinIO CDN (cannaiq bucket, downloads/ folder) +# 4. Uploads as both versioned and "latest" files +# +# The plugin is then available at: +# https://cdn.cannabrands.app/cannaiq/downloads/cannaiq-menus-{version}.zip +# https://cdn.cannabrands.app/cannaiq/downloads/cannaiq-menus-latest.zip set -e @@ -36,7 +48,7 @@ zip -r "${OUTPUT_DIR}/${OUTPUT_FILE}" . \ -x "assets/css/crawlsy-menus.css" \ -x "assets/js/crawlsy-menus.js" -# Create/update the "latest" symlink +# Create/update the "latest" symlink (for local reference) cd "${OUTPUT_DIR}" rm -f cannaiq-menus-latest.zip ln -s "${OUTPUT_FILE}" cannaiq-menus-latest.zip @@ -45,7 +57,42 @@ echo "" echo "Build complete!" echo " File: ${OUTPUT_DIR}/${OUTPUT_FILE}" echo " Size: $(ls -lh "${OUTPUT_DIR}/${OUTPUT_FILE}" | awk '{print $5}')" + +# Upload to MinIO CDN echo "" -echo "Download URLs:" -echo " Versioned: https://cannaiq.co/downloads/cannaiq-menus-${VERSION}.zip" -echo " Latest: https://cannaiq.co/downloads/cannaiq-menus-latest.zip" +echo "Uploading to MinIO CDN..." +cd "${PLUGIN_DIR}/../backend" +node -e " +const Minio = require('minio'); +const path = require('path'); + +const client = new Minio.Client({ + endPoint: 'cdn.cannabrands.app', + port: 443, + useSSL: true, + accessKey: 'FLE4dpKrOS2uQM7AFQSY', + secretKey: 'wDU6qkruxoDWftIvM0OIdJgTCleTLr5vhozPuYqF', +}); + +const localFile = 'public/downloads/${OUTPUT_FILE}'; +const version = '${VERSION}'; + +(async () => { + // Upload versioned file + await client.fPutObject('cannaiq', 'downloads/cannaiq-menus-' + version + '.zip', localFile, { + 'Content-Type': 'application/zip', + }); + console.log(' Uploaded: cannaiq-menus-' + version + '.zip'); + + // Upload as latest + await client.fPutObject('cannaiq', 'downloads/cannaiq-menus-latest.zip', localFile, { + 'Content-Type': 'application/zip', + }); + console.log(' Uploaded: cannaiq-menus-latest.zip'); +})(); +" + +echo "" +echo "Done! Plugin is live at:" +echo " https://cdn.cannabrands.app/cannaiq/downloads/cannaiq-menus-${VERSION}.zip" +echo " https://cdn.cannabrands.app/cannaiq/downloads/cannaiq-menus-latest.zip" diff --git a/wordpress-plugin/cannaiq-menus.php b/wordpress-plugin/cannaiq-menus.php index 159eca2e..92ca4579 100644 --- a/wordpress-plugin/cannaiq-menus.php +++ b/wordpress-plugin/cannaiq-menus.php @@ -3,7 +3,7 @@ * Plugin Name: CannaIQ Menus * Plugin URI: https://cannaiq.co * Description: Display cannabis product menus from CannaIQ with Elementor integration. Real-time menu data updated daily. - * Version: 1.6.0 + * Version: 1.7.0 * Author: CannaIQ * Author URI: https://cannaiq.co * License: GPL v2 or later @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; // Exit if accessed directly } -define('CANNAIQ_MENUS_VERSION', '1.6.0'); +define('CANNAIQ_MENUS_VERSION', '1.7.0'); define('CANNAIQ_MENUS_API_URL', 'https://cannaiq.co/api/v1'); define('CANNAIQ_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CANNAIQ_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__)); @@ -36,27 +36,42 @@ class CannaIQ_Menus_Plugin { public function __construct() { add_action('plugins_loaded', [$this, 'init']); + add_action('elementor/elements/categories_registered', [$this, 'register_elementor_category']); add_action('elementor/widgets/register', [$this, 'register_elementor_widgets']); add_action('admin_menu', [$this, 'add_admin_menu']); add_action('admin_init', [$this, 'register_settings']); add_action('wp_enqueue_scripts', [$this, 'enqueue_scripts']); } + /** + * Register CannaIQ Elementor Widget Category + */ + public function register_elementor_category($elements_manager) { + $elements_manager->add_category( + 'cannaiq', + [ + 'title' => __('CannaIQ', 'cannaiq-menus'), + 'icon' => 'fa fa-cannabis', + ] + ); + } + public function init() { // Initialize plugin load_plugin_textdomain('cannaiq-menus', false, dirname(plugin_basename(__FILE__)) . '/languages'); + // Load Elementor Dynamic Tags (if Elementor is active) + if (did_action('elementor/loaded')) { + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags.php'; + } + // Register shortcodes - primary CannaIQ shortcodes add_shortcode('cannaiq_products', [$this, 'products_shortcode']); add_shortcode('cannaiq_product', [$this, 'single_product_shortcode']); - // DEPRECATED: Legacy shortcode aliases for backward compatibility only - // These allow sites that used the old plugin names to continue working - // New implementations should use [cannaiq_products] and [cannaiq_product] - add_shortcode('crawlsy_products', [$this, 'products_shortcode']); // deprecated - add_shortcode('crawlsy_product', [$this, 'single_product_shortcode']); // deprecated - add_shortcode('dutchie_products', [$this, 'products_shortcode']); // deprecated - add_shortcode('dutchie_product', [$this, 'single_product_shortcode']); // deprecated + // DEPRECATED: Legacy shortcode alias for backward compatibility only + add_shortcode('crawlsy_products', [$this, 'products_shortcode']); + add_shortcode('crawlsy_product', [$this, 'single_product_shortcode']); } /** @@ -68,12 +83,14 @@ class CannaIQ_Menus_Plugin { require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/brand-grid.php'; require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/category-list.php'; require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/specials-grid.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-loop.php'; $widgets_manager->register(new \CannaIQ_Menus_Product_Grid_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Single_Product_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Brand_Grid_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Category_List_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Specials_Grid_Widget()); + $widgets_manager->register(new \CannaIQ_Product_Loop_Widget()); } /** @@ -117,18 +134,10 @@ class CannaIQ_Menus_Plugin { public function register_settings() { register_setting('cannaiq_menus_settings', 'cannaiq_api_token'); - // MIGRATION: Auto-migrate API tokens from old plugin versions - // This runs once - if user had crawlsy or dutchie plugin, their token is preserved - // Can be removed in a future major version once all users have migrated + // MIGRATION: Auto-migrate API token from old Crawlsy plugin $old_crawlsy_token = get_option('crawlsy_api_token'); - $old_dutchie_token = get_option('dutchie_api_token'); - - if (!get_option('cannaiq_api_token')) { - if ($old_crawlsy_token) { - update_option('cannaiq_api_token', $old_crawlsy_token); - } elseif ($old_dutchie_token) { - update_option('cannaiq_api_token', $old_dutchie_token); - } + if (!get_option('cannaiq_api_token') && $old_crawlsy_token) { + update_option('cannaiq_api_token', $old_crawlsy_token); } } diff --git a/wordpress-plugin/crawlsy-menus.php b/wordpress-plugin/crawlsy-menus.php index 3cf537d3..fdeece00 100644 --- a/wordpress-plugin/crawlsy-menus.php +++ b/wordpress-plugin/crawlsy-menus.php @@ -46,12 +46,9 @@ class Crawlsy_Menus_Plugin { // Initialize plugin load_plugin_textdomain('crawlsy-menus', false, dirname(plugin_basename(__FILE__)) . '/languages'); - // Register shortcodes (keeping backward compatible shortcode names) + // Register shortcodes add_shortcode('crawlsy_products', [$this, 'products_shortcode']); add_shortcode('crawlsy_product', [$this, 'single_product_shortcode']); - // Legacy shortcode support - add_shortcode('dutchie_products', [$this, 'products_shortcode']); - add_shortcode('dutchie_product', [$this, 'single_product_shortcode']); } /** @@ -105,12 +102,6 @@ class Crawlsy_Menus_Plugin { */ public function register_settings() { register_setting('crawlsy_menus_settings', 'crawlsy_api_token'); - - // Migrate old setting if exists - $old_token = get_option('dutchie_api_token'); - if ($old_token && !get_option('crawlsy_api_token')) { - update_option('crawlsy_api_token', $old_token); - } } /** diff --git a/wordpress-plugin/widgets/dynamic-tags.php b/wordpress-plugin/widgets/dynamic-tags.php new file mode 100644 index 00000000..305f2680 --- /dev/null +++ b/wordpress-plugin/widgets/dynamic-tags.php @@ -0,0 +1,806 @@ +register_group('cannaiq', [ + 'title' => __('CannaIQ Product', 'cannaiq-menus') + ]); + + // Register all tags + $dynamic_tags_manager->register(new CannaIQ_Product_Name_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Product_Description_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Brand_Name_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Brand_Logo_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Brand_Description_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Category_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Price_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Price_Rec_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Price_Med_Tag()); + $dynamic_tags_manager->register(new CannaIQ_THC_Tag()); + $dynamic_tags_manager->register(new CannaIQ_CBD_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Strain_Type_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Effects_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Weight_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Image_Tag()); + $dynamic_tags_manager->register(new CannaIQ_In_Stock_Tag()); + $dynamic_tags_manager->register(new CannaIQ_On_Sale_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Sale_Percent_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Quantity_Tag()); +}); + +/** + * Base class for CannaIQ dynamic tags + */ +abstract class CannaIQ_Dynamic_Tag_Base extends \Elementor\Core\DynamicTags\Tag { + + public function get_group() { + return 'cannaiq'; + } + + /** + * Get the current product from context + */ + protected function get_current_product() { + global $cannaiq_current_product; + return $cannaiq_current_product ?? []; + } + + /** + * Get nested value from array using dot notation + */ + protected function get_nested_value($array, $path, $default = '') { + $keys = explode('.', $path); + $value = $array; + + foreach ($keys as $key) { + // Handle array index like [0] + if (preg_match('/^(\w+)\[(\d+)\]$/', $key, $matches)) { + $key = $matches[1]; + $index = (int)$matches[2]; + if (isset($value[$key][$index])) { + $value = $value[$key][$index]; + } else { + return $default; + } + } else { + if (isset($value[$key])) { + $value = $value[$key]; + } else { + return $default; + } + } + } + + return $value; + } +} + +/** + * Product Name Tag + */ +class CannaIQ_Product_Name_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-product-name'; + } + + public function get_title() { + return __('Product Name', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + echo esc_html($product['Name'] ?? ''); + } +} + +/** + * Product Description Tag + */ +class CannaIQ_Product_Description_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-product-description'; + } + + public function get_title() { + return __('Product Description', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + echo esc_html($product['description'] ?? ''); + } +} + +/** + * Brand Name Tag + */ +class CannaIQ_Brand_Name_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-brand-name'; + } + + public function get_title() { + return __('Brand Name', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + echo esc_html($product['brand']['name'] ?? $product['brandName'] ?? ''); + } +} + +/** + * Brand Logo Tag + */ +class CannaIQ_Brand_Logo_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-brand-logo'; + } + + public function get_title() { + return __('Brand Logo', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::IMAGE_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + $url = $product['brand']['imageUrl'] ?? $product['brandLogo'] ?? ''; + echo esc_url($url); + } + + public function get_value(array $options = []) { + $product = $this->get_current_product(); + $url = $product['brand']['imageUrl'] ?? $product['brandLogo'] ?? ''; + return [ + 'id' => '', + 'url' => $url, + ]; + } +} + +/** + * Brand Description Tag + */ +class CannaIQ_Brand_Description_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-brand-description'; + } + + public function get_title() { + return __('Brand Description', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + echo esc_html($product['brand']['description'] ?? ''); + } +} + +/** + * Category Tag + */ +class CannaIQ_Category_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-category'; + } + + public function get_title() { + return __('Category', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + echo esc_html($product['type'] ?? $product['category'] ?? ''); + } +} + +/** + * Price Tag + */ +class CannaIQ_Price_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-price'; + } + + public function get_title() { + return __('Price', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('format', [ + 'label' => __('Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'currency', + 'options' => [ + 'currency' => '$XX.XX', + 'number' => 'XX.XX', + 'raw' => 'Raw value', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $price = $product['Prices'][0] ?? $product['recPrices'][0] ?? null; + $format = $this->get_settings('format'); + + if ($price === null) { + return; + } + + switch ($format) { + case 'currency': + echo '$' . number_format((float)$price, 2); + break; + case 'number': + echo number_format((float)$price, 2); + break; + default: + echo esc_html($price); + } + } +} + +/** + * Rec Price Tag + */ +class CannaIQ_Price_Rec_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-price-rec'; + } + + public function get_title() { + return __('Rec Price', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + $price = $product['recPrices'][0] ?? $product['Prices'][0] ?? null; + + if ($price !== null) { + echo '$' . number_format((float)$price, 2); + } + } +} + +/** + * Med Price Tag + */ +class CannaIQ_Price_Med_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-price-med'; + } + + public function get_title() { + return __('Med Price', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + $price = $product['medPrices'][0] ?? null; + + // Try POSMetaData + if ($price === null && isset($product['POSMetaData']['children'][0]['medPrice'])) { + $price = $product['POSMetaData']['children'][0]['medPrice']; + } + + if ($price !== null) { + echo '$' . number_format((float)$price, 2); + } + } +} + +/** + * THC Tag + */ +class CannaIQ_THC_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-thc'; + } + + public function get_title() { + return __('THC %', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('format', [ + 'label' => __('Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'percent', + 'options' => [ + 'percent' => 'XX.X%', + 'number' => 'XX.X', + 'badge' => 'THC: XX.X%', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $thc = $product['THCContent']['range'][0] ?? $product['THC'] ?? null; + $format = $this->get_settings('format'); + + if ($thc === null) { + return; + } + + switch ($format) { + case 'percent': + echo number_format((float)$thc, 1) . '%'; + break; + case 'number': + echo number_format((float)$thc, 1); + break; + case 'badge': + echo 'THC: ' . number_format((float)$thc, 1) . '%'; + break; + } + } +} + +/** + * CBD Tag + */ +class CannaIQ_CBD_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-cbd'; + } + + public function get_title() { + return __('CBD %', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + $cbd = $product['CBDContent']['range'][0] ?? $product['CBD'] ?? null; + + if ($cbd !== null) { + echo number_format((float)$cbd, 1) . '%'; + } + } +} + +/** + * Strain Type Tag + */ +class CannaIQ_Strain_Type_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-strain-type'; + } + + public function get_title() { + return __('Strain Type', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('format', [ + 'label' => __('Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'full', + 'options' => [ + 'full' => 'Sativa / Indica / Hybrid', + 'short' => 'S / I / H', + 'uppercase' => 'SATIVA / INDICA / HYBRID', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $strain = $product['strainType'] ?? ''; + $format = $this->get_settings('format'); + + if (empty($strain)) { + return; + } + + switch ($format) { + case 'short': + echo strtoupper(substr($strain, 0, 1)); + break; + case 'uppercase': + echo strtoupper($strain); + break; + default: + echo ucfirst($strain); + } + } +} + +/** + * Effects Tag + */ +class CannaIQ_Effects_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-effects'; + } + + public function get_title() { + return __('Effects', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('format', [ + 'label' => __('Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'badges', + 'options' => [ + 'badges' => 'HTML Badges', + 'list' => 'Comma separated', + 'top3' => 'Top 3 effects', + ], + ]); + + $this->add_control('limit', [ + 'label' => __('Max Effects', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 5, + 'min' => 1, + 'max' => 10, + ]); + } + + public function render() { + $product = $this->get_current_product(); + $effects = $product['effects'] ?? []; + $format = $this->get_settings('format'); + $limit = (int)$this->get_settings('limit') ?: 5; + + if (empty($effects) || !is_array($effects)) { + return; + } + + // Sort by value descending + arsort($effects); + + // Take top N + $effects = array_slice($effects, 0, $limit, true); + + switch ($format) { + case 'badges': + echo '
'; + foreach ($effects as $effect => $score) { + $icon = $this->get_effect_icon($effect); + echo ''; + echo $icon . ' ' . esc_html(strtoupper($effect)); + echo ''; + } + echo '
'; + break; + + case 'list': + $names = array_map('ucfirst', array_keys($effects)); + echo esc_html(implode(', ', $names)); + break; + + case 'top3': + $names = array_slice(array_map('ucfirst', array_keys($effects)), 0, 3); + echo esc_html(implode(', ', $names)); + break; + } + } + + private function get_effect_icon($effect) { + $icons = [ + 'happy' => '😊', + 'relaxed' => '😌', + 'sleepy' => '😴', + 'focused' => '🎯', + 'creative' => '🎨', + 'energetic' => '⚡', + 'hungry' => '🍽️', + 'euphoric' => '✨', + 'uplifted' => '🌟', + 'talkative' => '💬', + 'inspired' => '💡', + 'giggly' => '😄', + ]; + return $icons[strtolower($effect)] ?? '•'; + } +} + +/** + * Weight Tag + */ +class CannaIQ_Weight_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-weight'; + } + + public function get_title() { + return __('Weight', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + $weight = $product['Options'][0] ?? $product['rawOptions'][0] ?? ''; + echo esc_html($weight); + } +} + +/** + * Product Image Tag + */ +class CannaIQ_Image_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-image'; + } + + public function get_title() { + return __('Product Image', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::IMAGE_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + $url = $product['Image'] ?? $product['images'][0]['url'] ?? ''; + echo esc_url($url); + } + + public function get_value(array $options = []) { + $product = $this->get_current_product(); + $url = $product['Image'] ?? $product['images'][0]['url'] ?? ''; + return [ + 'id' => '', + 'url' => $url, + ]; + } +} + +/** + * In Stock Tag + */ +class CannaIQ_In_Stock_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-in-stock'; + } + + public function get_title() { + return __('In Stock', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('in_stock_text', [ + 'label' => __('In Stock Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'In Stock', + ]); + + $this->add_control('out_of_stock_text', [ + 'label' => __('Out of Stock Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'Out of Stock', + ]); + } + + public function render() { + $product = $this->get_current_product(); + $status = $product['Status'] ?? ''; + $in_stock = ($status === 'Active' || $status === 'In Stock'); + + if ($in_stock) { + echo esc_html($this->get_settings('in_stock_text')); + } else { + echo esc_html($this->get_settings('out_of_stock_text')); + } + } +} + +/** + * On Sale Tag + */ +class CannaIQ_On_Sale_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-on-sale'; + } + + public function get_title() { + return __('On Sale', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('sale_text', [ + 'label' => __('Sale Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'SALE', + ]); + } + + public function render() { + $product = $this->get_current_product(); + $on_sale = $product['special'] ?? false; + + if ($on_sale) { + echo esc_html($this->get_settings('sale_text')); + } + } +} + +/** + * Sale Percent Tag + */ +class CannaIQ_Sale_Percent_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-sale-percent'; + } + + public function get_title() { + return __('Sale Percentage', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('format', [ + 'label' => __('Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'badge', + 'options' => [ + 'badge' => 'XX% OFF', + 'number' => 'XX', + 'percent' => 'XX%', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + + // Calculate discount if we have original and sale price + $original = $product['Prices'][0] ?? null; + $sale = $product['specialPrice'] ?? null; + + // Check POSMetaData for discount + if (isset($product['POSMetaData']['children'][0])) { + $child = $product['POSMetaData']['children'][0]; + if (isset($child['price']) && isset($child['specialPrice'])) { + $original = $child['price']; + $sale = $child['specialPrice']; + } + } + + if ($original && $sale && $original > $sale) { + $percent = round((($original - $sale) / $original) * 100); + $format = $this->get_settings('format'); + + switch ($format) { + case 'badge': + echo $percent . '% OFF'; + break; + case 'number': + echo $percent; + break; + case 'percent': + echo $percent . '%'; + break; + } + } + } +} + +/** + * Quantity Tag + */ +class CannaIQ_Quantity_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-quantity'; + } + + public function get_title() { + return __('Stock Quantity', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + $quantity = $product['POSMetaData']['children'][0]['quantity'] ?? null; + + if ($quantity !== null) { + echo (int)$quantity; + } + } +} diff --git a/wordpress-plugin/widgets/product-loop.php b/wordpress-plugin/widgets/product-loop.php new file mode 100644 index 00000000..aeed1606 --- /dev/null +++ b/wordpress-plugin/widgets/product-loop.php @@ -0,0 +1,858 @@ +start_controls_section( + 'section_data_source', + [ + 'label' => __('Data Source', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'dispensary_id', + [ + 'label' => __('Store ID', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => '', + 'description' => __('Enter your store/dispensary ID from CannaIQ', 'cannaiq-menus'), + ] + ); + + $this->add_control( + 'limit', + [ + 'label' => __('Products Limit', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 12, + 'min' => 1, + 'max' => 100, + ] + ); + + $this->end_controls_section(); + + // Content Section - Filters + $this->start_controls_section( + 'section_filters', + [ + 'label' => __('Filters', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'category', + [ + 'label' => __('Category', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => '', + 'options' => [ + '' => __('All Categories', 'cannaiq-menus'), + 'Flower' => __('Flower', 'cannaiq-menus'), + 'Vaporizers' => __('Vaporizers', 'cannaiq-menus'), + 'Concentrates' => __('Concentrates', 'cannaiq-menus'), + 'Edibles' => __('Edibles', 'cannaiq-menus'), + 'Pre-Rolls' => __('Pre-Rolls', 'cannaiq-menus'), + 'Topicals' => __('Topicals', 'cannaiq-menus'), + 'Tinctures' => __('Tinctures', 'cannaiq-menus'), + 'Accessories' => __('Accessories', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'strain_type', + [ + 'label' => __('Strain Type', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => '', + 'options' => [ + '' => __('All Strains', 'cannaiq-menus'), + 'sativa' => __('Sativa', 'cannaiq-menus'), + 'indica' => __('Indica', 'cannaiq-menus'), + 'hybrid' => __('Hybrid', 'cannaiq-menus'), + 'cbd' => __('CBD', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'brand', + [ + 'label' => __('Brand Name', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => '', + 'description' => __('Filter by brand name (exact match)', 'cannaiq-menus'), + ] + ); + + $this->add_control( + 'on_sale', + [ + 'label' => __('On Sale Only', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => '', + ] + ); + + $this->add_control( + 'min_thc', + [ + 'label' => __('Minimum THC %', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => '', + 'min' => 0, + 'max' => 100, + ] + ); + + $this->add_control( + 'max_price', + [ + 'label' => __('Maximum Price', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => '', + 'min' => 0, + ] + ); + + $this->add_control( + 'search', + [ + 'label' => __('Search Term', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => '', + 'description' => __('Search in product name, brand, description', 'cannaiq-menus'), + ] + ); + + $this->add_control( + 'sort_by', + [ + 'label' => __('Sort By', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'name', + 'options' => [ + 'name' => __('Name (A-Z)', 'cannaiq-menus'), + 'name_desc' => __('Name (Z-A)', 'cannaiq-menus'), + 'price' => __('Price (Low to High)', 'cannaiq-menus'), + 'price_desc' => __('Price (High to Low)', 'cannaiq-menus'), + 'thc' => __('THC % (Low to High)', 'cannaiq-menus'), + 'thc_desc' => __('THC % (High to Low)', 'cannaiq-menus'), + 'brand' => __('Brand (A-Z)', 'cannaiq-menus'), + ], + ] + ); + + $this->end_controls_section(); + + // Layout Section + $this->start_controls_section( + 'section_layout', + [ + 'label' => __('Layout', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'layout', + [ + 'label' => __('Layout', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'grid', + 'options' => [ + 'grid' => __('Grid', 'cannaiq-menus'), + 'list' => __('List', 'cannaiq-menus'), + 'masonry' => __('Masonry', 'cannaiq-menus'), + ], + ] + ); + + $this->add_responsive_control( + 'columns', + [ + 'label' => __('Columns', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => '3', + 'tablet_default' => '2', + 'mobile_default' => '1', + 'options' => [ + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + '5' => '5', + '6' => '6', + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-loop-grid' => 'grid-template-columns: repeat({{VALUE}}, 1fr);', + ], + 'condition' => [ + 'layout' => ['grid', 'masonry'], + ], + ] + ); + + $this->add_responsive_control( + 'gap', + [ + 'label' => __('Gap', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px', 'em', 'rem'], + 'range' => [ + 'px' => ['min' => 0, 'max' => 100], + ], + 'default' => ['size' => 20, 'unit' => 'px'], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-loop-grid' => 'gap: {{SIZE}}{{UNIT}};', + '{{WRAPPER}} .cannaiq-product-loop-list' => 'gap: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->end_controls_section(); + + // Card Display Section + $this->start_controls_section( + 'section_card_display', + [ + 'label' => __('Card Display', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'show_image', + [ + 'label' => __('Show Image', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_brand', + [ + 'label' => __('Show Brand', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_name', + [ + 'label' => __('Show Name', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_category', + [ + 'label' => __('Show Category', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_strain_type', + [ + 'label' => __('Show Strain Type', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_thc', + [ + 'label' => __('Show THC %', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_cbd', + [ + 'label' => __('Show CBD %', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => '', + ] + ); + + $this->add_control( + 'show_effects', + [ + 'label' => __('Show Effects', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'effects_limit', + [ + 'label' => __('Effects Limit', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 3, + 'min' => 1, + 'max' => 10, + 'condition' => [ + 'show_effects' => 'yes', + ], + ] + ); + + $this->add_control( + 'show_price', + [ + 'label' => __('Show Price', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_weight', + [ + 'label' => __('Show Weight/Size', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_sale_badge', + [ + 'label' => __('Show Sale Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->end_controls_section(); + + // Style Section - Card + $this->start_controls_section( + 'section_style_card', + [ + 'label' => __('Card', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'card_background', + [ + 'label' => __('Background Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card' => 'background-color: {{VALUE}};', + ], + ] + ); + + $this->add_responsive_control( + 'card_padding', + [ + 'label' => __('Padding', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::DIMENSIONS, + 'size_units' => ['px', 'em', '%'], + 'default' => [ + 'top' => '15', + 'right' => '15', + 'bottom' => '15', + 'left' => '15', + 'unit' => 'px', + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Border::get_type(), + [ + 'name' => 'card_border', + 'selector' => '{{WRAPPER}} .cannaiq-product-card', + ] + ); + + $this->add_control( + 'card_border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::DIMENSIONS, + 'size_units' => ['px', '%'], + 'default' => [ + 'top' => '8', + 'right' => '8', + 'bottom' => '8', + 'left' => '8', + 'unit' => 'px', + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Box_Shadow::get_type(), + [ + 'name' => 'card_shadow', + 'selector' => '{{WRAPPER}} .cannaiq-product-card', + ] + ); + + $this->end_controls_section(); + + // Style Section - Image + $this->start_controls_section( + 'section_style_image', + [ + 'label' => __('Image', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_responsive_control( + 'image_height', + [ + 'label' => __('Height', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px', 'vh'], + 'range' => [ + 'px' => ['min' => 100, 'max' => 500], + ], + 'default' => ['size' => 200, 'unit' => 'px'], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-image' => 'height: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->add_control( + 'image_fit', + [ + 'label' => __('Object Fit', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'contain', + 'options' => [ + 'contain' => __('Contain', 'cannaiq-menus'), + 'cover' => __('Cover', 'cannaiq-menus'), + 'fill' => __('Fill', 'cannaiq-menus'), + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-image img' => 'object-fit: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'image_border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::DIMENSIONS, + 'size_units' => ['px', '%'], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-image' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', + ], + ] + ); + + $this->end_controls_section(); + + // Style Section - Typography + $this->start_controls_section( + 'section_style_typography', + [ + 'label' => __('Typography', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'name_color', + [ + 'label' => __('Name Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1a1a1a', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-name' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'name_typography', + 'label' => __('Name Typography', 'cannaiq-menus'), + 'selector' => '{{WRAPPER}} .cannaiq-product-name', + ] + ); + + $this->add_control( + 'brand_color', + [ + 'label' => __('Brand Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#666666', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-brand' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'price_color', + [ + 'label' => __('Price Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#2ecc71', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-price' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'price_typography', + 'label' => __('Price Typography', 'cannaiq-menus'), + 'selector' => '{{WRAPPER}} .cannaiq-product-price', + ] + ); + + $this->end_controls_section(); + + // Style Section - Badges + $this->start_controls_section( + 'section_style_badges', + [ + 'label' => __('Badges', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'strain_sativa_color', + [ + 'label' => __('Sativa Badge Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f39c12', + ] + ); + + $this->add_control( + 'strain_indica_color', + [ + 'label' => __('Indica Badge Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#9b59b6', + ] + ); + + $this->add_control( + 'strain_hybrid_color', + [ + 'label' => __('Hybrid Badge Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#2ecc71', + ] + ); + + $this->add_control( + 'sale_badge_color', + [ + 'label' => __('Sale Badge Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#e74c3c', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-sale-badge' => 'background-color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'effect_badge_background', + [ + 'label' => __('Effect Badge Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f0f0f0', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-effect-badge' => 'background-color: {{VALUE}};', + ], + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + $products = $this->fetch_products($settings); + + if (empty($products)) { + echo '

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

'; + return; + } + + $layout_class = 'cannaiq-product-loop-' . $settings['layout']; + ?> +
+ render_product_card($product, $settings); + } + + // Clear global + $cannaiq_current_product = null; + ?> +
+ $settings['strain_sativa_color'], + 'indica' => $settings['strain_indica_color'], + 'hybrid' => $settings['strain_hybrid_color'], + ]; + $strain_color = $strain_colors[$strain_type] ?? '#999999'; + + // Get values supporting both formats + $product_id = $product['id'] ?? $product['_id'] ?? ''; + $product_name = $product['name'] ?? $product['Name'] ?? ''; + $brand_name = $product['brand'] ?? $product['brandName'] ?? ''; + if (is_array($brand_name)) { + $brand_name = $brand_name['name'] ?? ''; + } + $category = $product['category'] ?? $product['type'] ?? $product['Category'] ?? ''; + $image_url = $product['image_url'] ?? $product['Image'] ?? $product['images'][0]['url'] ?? ''; + $weight = $product['weight'] ?? $product['Options'][0] ?? $product['rawOptions'][0] ?? ''; + $thc = $product['thc'] ?? $product['THCContent']['range'][0] ?? $product['THC'] ?? null; + $cbd = $product['cbd'] ?? $product['CBDContent']['range'][0] ?? $product['CBD'] ?? null; + $price = $product['price'] ?? $product['Prices'][0] ?? $product['recPrices'][0] ?? null; + if (is_array($price)) { + $price = $price['price'] ?? $price[0] ?? null; + } + $on_sale = $product['special'] ?? $product['on_sale'] ?? false; + $in_stock = $product['in_stock'] ?? ($product['status'] ?? $product['Status']) === 'Active'; + ?> +
+ + +
SALE
+ + + +
+ + <?php echo esc_attr($product_name); ?> + +
+ + +
+ + +
+ +
+ + + +

+ +

+ + +
+ + + + + + + + + + + + + +
+ + +
+ + THC: % + + + + CBD: % + +
+ + + +
+ $score): + ?> + + +
+ + + +
+ +
+ + +
+
+ ['sort' => 'name', 'order' => 'asc'], + 'name_desc' => ['sort' => 'name', 'order' => 'desc'], + 'price' => ['sort' => 'price', 'order' => 'asc'], + 'price_desc' => ['sort' => 'price', 'order' => 'desc'], + 'thc' => ['sort' => 'thc', 'order' => 'asc'], + 'thc_desc' => ['sort' => 'thc', 'order' => 'desc'], + 'brand' => ['sort' => 'brand', 'order' => 'asc'], + ]; + + if (!empty($settings['sort_by']) && isset($sort_map[$settings['sort_by']])) { + $params['sort'] = $sort_map[$settings['sort_by']]['sort']; + $params['order'] = $sort_map[$settings['sort_by']]['order']; + } + + $query_string = http_build_query($params); + $url = $api_url . ($query_string ? '?' . $query_string : ''); + + // Make request + $response = wp_remote_get($url, [ + 'timeout' => 30, + 'headers' => [ + 'Accept' => 'application/json', + ], + ]); + + if (is_wp_error($response)) { + return []; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + return $data['products'] ?? []; + } +}