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 '
' . __('No products found.', 'cannaiq-menus') . '
'; + return; + } + + $layout_class = 'cannaiq-product-loop-' . $settings['layout']; + ?> +