Files
cannaiq/wordpress-plugin/cannaiq-menus.php
Kelly 87da7625cd
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: WordPress plugin v2.0.0 - modular component library
- Add 14 new shortcodes for building custom product cards
- Add visual builder guide with Hot Lava example in admin page
- Add comprehensive component documentation
- Update branding to "CannaiQ" throughout
- Include layout shortcodes: specials, brands, categories
- Include component shortcodes: discount_badge, strain_badge,
  thc, cbd, effects, price, cart_button, stock, terpenes
- Add build steps and instructions for assembling cards

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 01:58:20 -07:00

1094 lines
46 KiB
PHP

<?php
/**
* 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: 2.0.0
* Author: CannaiQ
* Author URI: https://cannaiq.co
* License: GPL v2 or later
* Text Domain: cannaiq-menus
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
define('CANNAIQ_MENUS_VERSION', '2.0.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__));
/**
* Main Plugin Class
*/
class CannaIQ_Menus_Plugin {
private static $instance = null;
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
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 helper functions
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'includes/effects-icons.php';
// Load Elementor Dynamic Tags (if Elementor is active)
if (did_action('elementor/loaded')) {
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags-extended.php';
}
// Register shortcodes - primary CannaiQ shortcodes
add_shortcode('cannaiq_products', [$this, 'products_shortcode']);
add_shortcode('cannaiq_product', [$this, 'single_product_shortcode']);
add_shortcode('cannaiq_specials', [$this, 'specials_shortcode']);
add_shortcode('cannaiq_brands', [$this, 'brands_shortcode']);
add_shortcode('cannaiq_categories', [$this, 'categories_shortcode']);
// Component shortcodes (v2.0)
add_shortcode('cannaiq_discount_badge', [$this, 'discount_badge_shortcode']);
add_shortcode('cannaiq_strain_badge', [$this, 'strain_badge_shortcode']);
add_shortcode('cannaiq_thc', [$this, 'thc_shortcode']);
add_shortcode('cannaiq_cbd', [$this, 'cbd_shortcode']);
add_shortcode('cannaiq_effects', [$this, 'effects_shortcode']);
add_shortcode('cannaiq_price', [$this, 'price_shortcode']);
add_shortcode('cannaiq_cart_button', [$this, 'cart_button_shortcode']);
add_shortcode('cannaiq_stock', [$this, 'stock_shortcode']);
add_shortcode('cannaiq_terpenes', [$this, 'terpenes_shortcode']);
// DEPRECATED: Legacy shortcode alias for backward compatibility only
add_shortcode('crawlsy_products', [$this, 'products_shortcode']);
add_shortcode('crawlsy_product', [$this, 'single_product_shortcode']);
}
/**
* Register Elementor Widgets
*/
public function register_elementor_widgets($widgets_manager) {
// Legacy widgets
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-grid.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/single-product.php';
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';
// Modular component widgets (v2.0)
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/discount-ribbon.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/strain-badge.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/thc-meter.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/effects-display.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/price-block.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/cart-button.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/stock-indicator.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-image-overlay.php';
// Card templates (v2.0)
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-premium.php';
// Register legacy widgets
$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());
// Register modular component widgets (v2.0)
$widgets_manager->register(new \CannaIQ_Discount_Ribbon_Widget());
$widgets_manager->register(new \CannaIQ_Strain_Badge_Widget());
$widgets_manager->register(new \CannaIQ_THC_Meter_Widget());
$widgets_manager->register(new \CannaIQ_Effects_Display_Widget());
$widgets_manager->register(new \CannaIQ_Price_Block_Widget());
$widgets_manager->register(new \CannaIQ_Cart_Button_Widget());
$widgets_manager->register(new \CannaIQ_Stock_Indicator_Widget());
$widgets_manager->register(new \CannaIQ_Product_Image_Overlay_Widget());
// Register card templates (v2.0)
$widgets_manager->register(new \CannaIQ_Premium_Card_Widget());
}
/**
* Enqueue Scripts and Styles
*/
public function enqueue_scripts() {
// Base styles
wp_enqueue_style(
'cannaiq-menus-styles',
CANNAIQ_MENUS_PLUGIN_URL . 'assets/css/cannaiq-menus.css',
[],
CANNAIQ_MENUS_VERSION
);
// Component styles (v2.0 modular components)
wp_enqueue_style(
'cannaiq-components-styles',
CANNAIQ_MENUS_PLUGIN_URL . 'assets/css/components.css',
['cannaiq-menus-styles'],
CANNAIQ_MENUS_VERSION
);
wp_enqueue_script(
'cannaiq-menus-script',
CANNAIQ_MENUS_PLUGIN_URL . 'assets/js/cannaiq-menus.js',
['jquery'],
CANNAIQ_MENUS_VERSION,
true
);
}
/**
* Add Admin Menu
*/
public function add_admin_menu() {
add_menu_page(
'CannaiQ Menus',
'CannaiQ Menus',
'manage_options',
'cannaiq-menus',
[$this, 'admin_page'],
'dashicons-products',
30
);
}
/**
* Register Plugin Settings
*/
public function register_settings() {
register_setting('cannaiq_menus_settings', 'cannaiq_api_token');
// MIGRATION: Auto-migrate API token from old Crawlsy plugin
$old_crawlsy_token = get_option('crawlsy_api_token');
if (!get_option('cannaiq_api_token') && $old_crawlsy_token) {
update_option('cannaiq_api_token', $old_crawlsy_token);
}
}
/**
* Admin Page
*/
public function admin_page() {
?>
<div class="wrap">
<h1>CannaiQ Menus Settings</h1>
<p>Version <?php echo CANNAIQ_MENUS_VERSION; ?> by <a href="https://cannaiq.co" target="_blank">CannaiQ</a></p>
<p class="description">Display real-time cannabis menus with data updated daily from CannaiQ.</p>
<form method="post" action="options.php">
<?php settings_fields('cannaiq_menus_settings'); ?>
<?php do_settings_sections('cannaiq_menus_settings'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="cannaiq_api_token">API Token</label></th>
<td>
<input type="password" id="cannaiq_api_token" name="cannaiq_api_token"
value="<?php echo esc_attr(get_option('cannaiq_api_token')); ?>"
class="regular-text" />
<p class="description">Your authentication token from the CannaiQ admin dashboard. The token includes your store configuration.</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<hr />
<h2>Test Connection</h2>
<button type="button" id="test-api-connection" class="button button-primary">Test API Connection</button>
<div id="api-test-result" style="margin-top: 10px;"></div>
<script>
jQuery(document).ready(function($) {
$('#test-api-connection').on('click', function() {
var apiUrl = '<?php echo CANNAIQ_MENUS_API_URL; ?>';
var apiToken = $('#cannaiq_api_token').val();
if (!apiToken || apiToken.trim() === '') {
$('#api-test-result').html(
'<div class="notice notice-warning"><p><strong>Warning:</strong> Please enter an API token first.</p></div>'
);
return;
}
$('#api-test-result').html('<p><span class="spinner is-active" style="float:none;margin:0 5px 0 0;"></span>Testing connection...</p>');
$.ajax({
url: apiUrl + '/menu',
method: 'GET',
headers: {
'X-API-Key': apiToken
},
timeout: 30000,
success: function(response) {
var html = '<div class="notice notice-success"><p><strong>Success!</strong> Connected to: ' +
response.dispensary + '</p>';
if (response.menu) {
html += '<ul style="margin-left: 20px;">';
html += '<li>Total Products: ' + (response.menu.total_products || 0) + '</li>';
html += '<li>In Stock: ' + (response.menu.in_stock_count || 0) + '</li>';
html += '<li>Brands: ' + (response.menu.brand_count || 0) + '</li>';
html += '<li>Categories: ' + (response.menu.category_count || 0) + '</li>';
if (response.menu.last_updated) {
html += '<li>Last Updated: ' + new Date(response.menu.last_updated).toLocaleString() + '</li>';
}
html += '</ul>';
}
html += '</div>';
$('#api-test-result').html(html);
},
error: function(xhr, textStatus, errorThrown) {
var errorHtml = '<div class="notice notice-error">';
errorHtml += '<p><strong>Connection Failed</strong></p>';
// Show HTTP status
if (xhr.status) {
errorHtml += '<p><strong>HTTP Status:</strong> ' + xhr.status + ' ' + (xhr.statusText || '') + '</p>';
}
// Show error message from API
if (xhr.responseJSON) {
if (xhr.responseJSON.error) {
errorHtml += '<p><strong>Error:</strong> ' + xhr.responseJSON.error + '</p>';
}
if (xhr.responseJSON.message) {
errorHtml += '<p><strong>Details:</strong> ' + xhr.responseJSON.message + '</p>';
}
if (xhr.responseJSON.client_ip) {
errorHtml += '<p><strong>Your IP:</strong> ' + xhr.responseJSON.client_ip + '</p>';
}
if (xhr.responseJSON.origin) {
errorHtml += '<p><strong>Origin:</strong> ' + xhr.responseJSON.origin + '</p>';
}
} else if (textStatus === 'timeout') {
errorHtml += '<p><strong>Error:</strong> Request timed out. The API server may be unavailable.</p>';
} else if (textStatus === 'error' && !xhr.status) {
errorHtml += '<p><strong>Error:</strong> Could not connect to the API. This may be a CORS issue or network problem.</p>';
errorHtml += '<p><em>Note: If testing from wp-admin, try saving the token and testing on the frontend shortcode instead.</em></p>';
} else {
errorHtml += '<p><strong>Error:</strong> ' + (errorThrown || textStatus || 'Unknown error') + '</p>';
}
// Debug info
errorHtml += '<details style="margin-top: 10px;"><summary style="cursor:pointer;"><strong>Debug Info</strong></summary>';
errorHtml += '<pre style="background:#f5f5f5;padding:10px;margin-top:5px;overflow-x:auto;font-size:11px;">';
errorHtml += 'API URL: ' + apiUrl + '/menu\n';
errorHtml += 'Token Length: ' + apiToken.length + ' chars\n';
errorHtml += 'Token Preview: ' + apiToken.substring(0, 8) + '...' + apiToken.substring(apiToken.length - 4) + '\n';
errorHtml += 'Status: ' + xhr.status + '\n';
errorHtml += 'Response: ' + (xhr.responseText ? xhr.responseText.substring(0, 500) : 'none') + '\n';
errorHtml += '</pre></details>';
// Help text based on status
if (xhr.status === 401) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Your API token is invalid or inactive. Please check that you copied the full token from the CannaIQ admin dashboard.</p>';
} else if (xhr.status === 403) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Access denied. Your IP address or domain may not be in the allowed list. Contact support to update your permissions.</p>';
} else if (xhr.status === 500) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Server error. This may be a temporary issue. Try again in a few minutes, or contact support if the problem persists.</p>';
} else if (xhr.status === 503) {
errorHtml += '<p style="margin-top:10px;"><strong>Troubleshooting:</strong> Menu data is not yet available for your dispensary. The data may still be processing.</p>';
}
errorHtml += '</div>';
$('#api-test-result').html(errorHtml);
}
});
});
});
</script>
<hr />
<h2>Usage</h2>
<h3>Shortcodes</h3>
<table class="widefat" style="max-width: 900px;">
<thead>
<tr>
<th>Shortcode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[cannaiq_products]</code></td>
<td>Product grid. Options: <code>category</code>, <code>brand</code>, <code>limit</code>, <code>columns</code>, <code>in_stock</code></td>
</tr>
<tr>
<td><code>[cannaiq_product id="123"]</code></td>
<td>Single product by ID</td>
</tr>
<tr>
<td><code>[cannaiq_specials]</code></td>
<td>Products on sale. Options: <code>limit</code>, <code>columns</code></td>
</tr>
<tr>
<td><code>[cannaiq_brands]</code></td>
<td>Brand grid. Options: <code>limit</code>, <code>columns</code></td>
</tr>
<tr>
<td><code>[cannaiq_categories]</code></td>
<td>Category list. Options: <code>style</code> (list|grid)</td>
</tr>
</tbody>
</table>
<h4 style="margin-top: 20px;">Component Shortcodes <span style="color: #666; font-weight: normal;">(use inside product context)</span></h4>
<table class="widefat" style="max-width: 900px;">
<thead>
<tr>
<th>Shortcode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[cannaiq_discount_badge]</code></td>
<td>Discount ribbon/pill. Options: <code>style</code> (ribbon|pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_strain_badge]</code></td>
<td>Sativa/Indica/Hybrid badge. Options: <code>style</code> (pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_thc]</code></td>
<td>THC percentage. Options: <code>style</code> (meter|badge|pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_cbd]</code></td>
<td>CBD percentage. Options: <code>style</code> (meter|badge|pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_effects]</code></td>
<td>Effect chips with icons. Options: <code>limit</code>, <code>icons</code> (yes|no)</td>
</tr>
<tr>
<td><code>[cannaiq_price]</code></td>
<td>Price display. Options: <code>show_original</code> (yes|no), <code>show_weight</code> (yes|no)</td>
</tr>
<tr>
<td><code>[cannaiq_cart_button]</code></td>
<td>Add to cart button. Options: <code>text</code>, <code>style</code> (solid|outline)</td>
</tr>
<tr>
<td><code>[cannaiq_stock]</code></td>
<td>Stock status. Options: <code>style</code> (badge|text|dot)</td>
</tr>
<tr>
<td><code>[cannaiq_terpenes]</code></td>
<td>Terpene profile. Options: <code>limit</code>, <code>style</code> (chips|list|text)</td>
</tr>
</tbody>
</table>
<h3 style="margin-top: 30px;">Elementor Widgets</h3>
<p>With Elementor installed, find CannaiQ widgets in the editor:</p>
<h4>Layout Widgets</h4>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>Product Grid</strong> - Filterable product grid</li>
<li><strong>Product Loop</strong> - Custom loop for building cards</li>
<li><strong>Single Product</strong> - Display one product</li>
<li><strong>Brand Grid</strong> - Display brands</li>
<li><strong>Category List</strong> - Display categories</li>
<li><strong>Specials/Deals</strong> - Products on sale</li>
</ul>
<h4>Component Widgets <span style="color: #666; font-weight: normal;">(v2.0)</span></h4>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>Discount Ribbon</strong> - Sale percentage badge</li>
<li><strong>Strain Badge</strong> - Sativa/Indica/Hybrid pill</li>
<li><strong>THC/CBD Meter</strong> - Potency display</li>
<li><strong>Effects Display</strong> - Effect chips with icons</li>
<li><strong>Price Block</strong> - Price with sale formatting</li>
<li><strong>Cart Button</strong> - Styled CTA button</li>
<li><strong>Stock Indicator</strong> - Availability badge</li>
<li><strong>Product Image + Badges</strong> - Image with overlays</li>
</ul>
<h4>Card Templates <span style="color: #666; font-weight: normal;">(v2.0)</span></h4>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>Premium Product Card</strong> - Ready-to-use card with all components</li>
</ul>
<h3 style="margin-top: 30px;">Dynamic Tags</h3>
<p>In Elementor, use dynamic tags to insert product data into any widget. Look for "CannaiQ Product" in the dynamic tags menu.</p>
<hr style="margin: 30px 0;" />
<h2>How to Build a Product Card</h2>
<p>Use the modular components to build custom product cards. Here's an example layout:</p>
<div style="display: flex; gap: 30px; flex-wrap: wrap; margin-top: 20px;">
<!-- Visual Example -->
<div style="background: #fff; border: 2px solid #ddd; border-radius: 12px; width: 300px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Image area -->
<div style="background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%); height: 200px; position: relative;">
<span style="position: absolute; top: 0; left: 0; background: #ef4444; color: white; padding: 4px 12px; font-size: 12px; font-weight: bold; border-bottom-right-radius: 8px;">67% OFF</span>
<div style="position: absolute; bottom: 8px; left: 8px; display: flex; gap: 4px;">
<span style="background: #22c55e; color: white; padding: 2px 8px; border-radius: 999px; font-size: 10px; font-weight: bold;">SATIVA</span>
<span style="background: #1f2937; color: white; padding: 2px 8px; border-radius: 999px; font-size: 10px; font-weight: bold;">24.5% THC</span>
</div>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; font-size: 48px;">🌿</div>
</div>
<!-- Body -->
<div style="padding: 16px;">
<h4 style="margin: 0 0 4px 0; font-size: 18px;">Hot Lava</h4>
<p style="margin: 0 0 12px 0; color: #666; font-size: 14px;">by TruInfusion</p>
<div style="display: flex; gap: 6px; margin-bottom: 12px;">
<span style="background: #fef3c7; border: 1px solid #fcd34d; padding: 2px 8px; border-radius: 999px; font-size: 11px;">😴 Sleepy</span>
<span style="background: #dbeafe; border: 1px solid #93c5fd; padding: 2px 8px; border-radius: 999px; font-size: 11px;">😌 Relaxed</span>
<span style="background: #fce7f3; border: 1px solid #f9a8d4; padding: 2px 8px; border-radius: 999px; font-size: 11px;">😊 Happy</span>
</div>
<div style="margin-bottom: 12px;">
<span style="color: #999; font-size: 14px;">1/8 oz</span>
<span style="color: #999; text-decoration: line-through; margin-left: 8px;">$45.00</span>
<span style="color: #dc2626; font-weight: bold; font-size: 18px; margin-left: 8px;">$15.00</span>
</div>
<div style="background: #1f2937; color: white; text-align: center; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px;">ADD TO CART →</div>
</div>
</div>
<!-- Component Labels -->
<div style="flex: 1; min-width: 300px;">
<h4 style="margin-top: 0;">Components Used:</h4>
<table class="widefat" style="max-width: 400px;">
<tr><td style="width: 40%;"><strong>Discount Ribbon</strong></td><td>Top-left corner badge</td></tr>
<tr><td><strong>Product Image</strong></td><td>With badge overlays</td></tr>
<tr><td><strong>Strain Badge</strong></td><td>Green Sativa pill</td></tr>
<tr><td><strong>THC Badge</strong></td><td>Dark potency pill</td></tr>
<tr><td><strong>Product Name</strong></td><td>Dynamic tag</td></tr>
<tr><td><strong>Brand Name</strong></td><td>Dynamic tag</td></tr>
<tr><td><strong>Effects Display</strong></td><td>Colored chips with icons</td></tr>
<tr><td><strong>Price Block</strong></td><td>Weight + strikethrough + sale</td></tr>
<tr><td><strong>Cart Button</strong></td><td>Links to dispensary menu</td></tr>
</table>
<h4 style="margin-top: 20px;">Build Steps:</h4>
<ol style="margin-left: 20px; line-height: 1.8;">
<li>Add a <strong>Product Loop</strong> widget</li>
<li>Inside the loop, add a container</li>
<li>Add <strong>Product Image + Badges</strong> widget</li>
<li>Add heading with <strong>Product Name</strong> dynamic tag</li>
<li>Add text with <strong>Brand Name</strong> dynamic tag</li>
<li>Add <strong>Effects Display</strong> widget</li>
<li>Add <strong>Price Block</strong> widget</li>
<li>Add <strong>Cart Button</strong> widget</li>
</ol>
<p style="margin-top: 20px; padding: 12px; background: #e7f3ff; border-left: 4px solid #2196f3; border-radius: 4px;">
<strong>Tip:</strong> Use the <strong>Premium Product Card</strong> template widget for a ready-to-use version of this layout!
</p>
</div>
</div>
</div>
<?php
}
/**
* Products Shortcode
*/
public function products_shortcode($atts) {
$atts = shortcode_atts([
'category_id' => '',
'limit' => 12,
'columns' => 3,
'in_stock' => 'true'
], $atts);
$products = $this->fetch_products($atts);
if (!$products) {
return '<p>No products found.</p>';
}
ob_start();
include CANNAIQ_MENUS_PLUGIN_DIR . 'templates/product-grid.php';
return ob_get_clean();
}
/**
* Single Product Shortcode
*/
public function single_product_shortcode($atts) {
$atts = shortcode_atts([
'id' => 0
], $atts);
if (!$atts['id']) {
return '<p>Product ID required.</p>';
}
$product = $this->fetch_product($atts['id']);
if (!$product) {
return '<p>Product not found.</p>';
}
ob_start();
include CANNAIQ_MENUS_PLUGIN_DIR . 'templates/single-product.php';
return ob_get_clean();
}
/**
* Specials Shortcode
*/
public function specials_shortcode($atts) {
$atts = shortcode_atts([
'limit' => 12,
'columns' => 3
], $atts);
$products = $this->fetch_specials($atts);
if (!$products) {
return '<p>No specials found.</p>';
}
ob_start();
include CANNAIQ_MENUS_PLUGIN_DIR . 'templates/product-grid.php';
return ob_get_clean();
}
/**
* Brands Shortcode
*/
public function brands_shortcode($atts) {
$atts = shortcode_atts([
'limit' => 20,
'columns' => 4
], $atts);
$brands = $this->fetch_brands($atts);
if (!$brands) {
return '<p>No brands found.</p>';
}
$columns = intval($atts['columns']);
ob_start();
?>
<div class="cannaiq-brands-grid" style="display: grid; grid-template-columns: repeat(<?php echo $columns; ?>, 1fr); gap: 20px;">
<?php foreach ($brands as $brand): ?>
<div class="cannaiq-brand-card" style="text-align: center; padding: 20px; background: #f9fafb; border-radius: 8px;">
<?php if (!empty($brand['logo'])): ?>
<img src="<?php echo esc_url($brand['logo']); ?>" alt="<?php echo esc_attr($brand['brand'] ?? $brand['name']); ?>" style="max-height: 60px; margin-bottom: 10px;" />
<?php endif; ?>
<h4 style="margin: 0;"><?php echo esc_html($brand['brand'] ?? $brand['name']); ?></h4>
<?php if (!empty($brand['product_count'])): ?>
<p style="margin: 5px 0 0; color: #666; font-size: 14px;"><?php echo intval($brand['product_count']); ?> products</p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Categories Shortcode
*/
public function categories_shortcode($atts) {
$atts = shortcode_atts([
'style' => 'list'
], $atts);
$categories = $this->fetch_categories();
if (!$categories) {
return '<p>No categories found.</p>';
}
ob_start();
if ($atts['style'] === 'grid') {
?>
<div class="cannaiq-categories-grid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px;">
<?php foreach ($categories as $cat): ?>
<div class="cannaiq-category-card" style="padding: 15px; background: #f9fafb; border-radius: 8px; text-align: center;">
<h4 style="margin: 0;"><?php echo esc_html(ucwords(str_replace('_', ' ', $cat['type'] ?? $cat['name']))); ?></h4>
<?php if (!empty($cat['count'])): ?>
<p style="margin: 5px 0 0; color: #666; font-size: 14px;"><?php echo intval($cat['count']); ?> products</p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
} else {
?>
<ul class="cannaiq-categories-list" style="list-style: none; padding: 0; margin: 0;">
<?php foreach ($categories as $cat): ?>
<li style="padding: 10px 0; border-bottom: 1px solid #eee;">
<?php echo esc_html(ucwords(str_replace('_', ' ', $cat['type'] ?? $cat['name']))); ?>
<?php if (!empty($cat['count'])): ?>
<span style="color: #666; font-size: 14px;">(<?php echo intval($cat['count']); ?>)</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php
}
return ob_get_clean();
}
/**
* Discount Badge Shortcode
*/
public function discount_badge_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['style' => 'ribbon'], $atts);
$product = $cannaiq_current_product;
$original = $product['Prices'][0] ?? $product['regular_price'] ?? null;
$sale = $product['specialPrice'] ?? $product['sale_price'] ?? null;
if (!$original || !$sale || $original <= $sale) return '';
$percent = round((($original - $sale) / $original) * 100);
$class = 'cannaiq-discount-ribbon cannaiq-discount-ribbon--' . esc_attr($atts['style']);
return sprintf('<span class="%s">%s%% OFF</span>', $class, $percent);
}
/**
* Strain Badge Shortcode
*/
public function strain_badge_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['style' => 'pill'], $atts);
$strain = strtolower($cannaiq_current_product['strainType'] ?? $cannaiq_current_product['strain_type'] ?? '');
if (empty($strain) || !in_array($strain, ['sativa', 'indica', 'hybrid'])) return '';
$colors = ['sativa' => '#22c55e', 'indica' => '#8b5cf6', 'hybrid' => '#f97316'];
$color = $colors[$strain];
$style = $atts['style'] === 'pill' ? "background-color: {$color}; color: white;" : "color: {$color};";
return sprintf('<span class="cannaiq-strain-badge cannaiq-strain-badge--%s" style="%s">%s</span>',
esc_attr($atts['style']), esc_attr($style), esc_html(strtoupper($strain)));
}
/**
* THC Shortcode
*/
public function thc_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['style' => 'badge'], $atts);
$thc = $cannaiq_current_product['THCContent']['range'][0] ?? $cannaiq_current_product['THC'] ?? $cannaiq_current_product['thc_percentage'] ?? null;
if (!$thc || $thc <= 0) return '';
$formatted = number_format((float)$thc, 1) . '% THC';
return sprintf('<span class="cannaiq-potency-badge cannaiq-potency-badge--%s">%s</span>',
esc_attr($atts['style']), esc_html($formatted));
}
/**
* CBD Shortcode
*/
public function cbd_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['style' => 'badge'], $atts);
$cbd = $cannaiq_current_product['CBDContent']['range'][0] ?? $cannaiq_current_product['CBD'] ?? $cannaiq_current_product['cbd_percentage'] ?? null;
if (!$cbd || $cbd <= 0) return '';
$formatted = number_format((float)$cbd, 1) . '% CBD';
return sprintf('<span class="cannaiq-potency-badge cannaiq-potency-badge--%s">%s</span>',
esc_attr($atts['style']), esc_html($formatted));
}
/**
* Effects Shortcode
*/
public function effects_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['limit' => 3, 'icons' => 'yes'], $atts);
$effects = $cannaiq_current_product['effects'] ?? [];
if (empty($effects) || !is_array($effects)) return '';
if (!isset($effects[0])) {
arsort($effects);
$effects = array_keys($effects);
}
$effects = array_slice($effects, 0, intval($atts['limit']));
return cannaiq_render_effects($effects, [
'limit' => intval($atts['limit']),
'show_icon' => $atts['icons'] === 'yes',
'size' => 'medium'
]);
}
/**
* Price Shortcode
*/
public function price_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['show_original' => 'yes', 'show_weight' => 'yes'], $atts);
$product = $cannaiq_current_product;
$original = $product['Prices'][0] ?? $product['regular_price'] ?? null;
$sale = $product['specialPrice'] ?? $product['sale_price'] ?? null;
$weight = $product['Options'][0] ?? $product['weight'] ?? '';
if (!$original || $original <= 0) return '';
$is_sale = $sale && $sale > 0 && $sale < $original;
ob_start();
?>
<span class="cannaiq-price-block">
<?php if ($atts['show_weight'] === 'yes' && !empty($weight)): ?>
<span class="cannaiq-price-block__weight"><?php echo esc_html($weight); ?></span>
<?php endif; ?>
<?php if ($is_sale): ?>
<?php if ($atts['show_original'] === 'yes'): ?>
<span class="cannaiq-price-block__original">$<?php echo number_format((float)$original, 2); ?></span>
<?php endif; ?>
<span class="cannaiq-price-block__sale">$<?php echo number_format((float)$sale, 2); ?></span>
<?php else: ?>
<span class="cannaiq-price-block__regular">$<?php echo number_format((float)$original, 2); ?></span>
<?php endif; ?>
</span>
<?php
return ob_get_clean();
}
/**
* Cart Button Shortcode
*/
public function cart_button_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['text' => 'ADD TO CART', 'style' => 'solid'], $atts);
$url = $cannaiq_current_product['menuUrl'] ?? $cannaiq_current_product['menu_url'] ?? '#';
return sprintf('<a href="%s" class="cannaiq-cart-button cannaiq-cart-button--%s" target="_blank" rel="noopener">%s</a>',
esc_url($url), esc_attr($atts['style']), esc_html($atts['text']));
}
/**
* Stock Shortcode
*/
public function stock_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['style' => 'badge'], $atts);
$status = $cannaiq_current_product['Status'] ?? '';
$in_stock = ($status === 'Active' || $status === 'In Stock' || !empty($cannaiq_current_product['in_stock']));
$text = $in_stock ? 'In Stock' : 'Out of Stock';
$class = 'cannaiq-stock-indicator cannaiq-stock-indicator--' . ($in_stock ? 'in-stock' : 'out-of-stock');
if ($atts['style'] === 'badge') $class .= ' cannaiq-stock-indicator--badge';
$dot = $atts['style'] === 'dot' ? '<span class="cannaiq-stock-indicator__dot"></span>' : '';
return sprintf('<span class="%s">%s%s</span>', esc_attr($class), $dot, esc_html($text));
}
/**
* Terpenes Shortcode
*/
public function terpenes_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['limit' => 3, 'style' => 'chips'], $atts);
$terpenes = $cannaiq_current_product['terpenes'] ?? [];
if (empty($terpenes) || !is_array($terpenes)) return '';
$terpenes = array_slice($terpenes, 0, intval($atts['limit']));
ob_start();
if ($atts['style'] === 'chips') {
echo '<div class="cannaiq-terpenes cannaiq-terpenes--chips">';
foreach ($terpenes as $terp) {
$name = $terp['name'] ?? '';
$percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
printf('<span class="cannaiq-terpene-chip"><span class="cannaiq-terpene-chip__name">%s</span><span class="cannaiq-terpene-chip__percent">%s</span></span>',
esc_html($name), esc_html($percent));
}
echo '</div>';
} elseif ($atts['style'] === 'text') {
$parts = [];
foreach ($terpenes as $terp) {
$name = $terp['name'] ?? '';
$percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
$parts[] = $name . ($percent ? ' ' . $percent : '');
}
echo esc_html(implode(', ', $parts));
} else {
echo '<div class="cannaiq-terpenes cannaiq-terpenes--list">';
foreach ($terpenes as $terp) {
$name = $terp['name'] ?? '';
$percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
printf('<div class="cannaiq-terpene-item"><span>%s</span><span>%s</span></div>',
esc_html($name), esc_html($percent));
}
echo '</div>';
}
return ob_get_clean();
}
/**
* Fetch Products from API
*/
public function fetch_products($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/products?' . $query_args;
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['products'] ?? false;
}
/**
* Fetch Single Product from API
*/
public function fetch_product($id) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$url = CANNAIQ_MENUS_API_URL . '/products/' . intval($id);
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['product'] ?? false;
}
/**
* Fetch Categories from API
*/
public function fetch_categories($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/categories' . ($query_args ? '?' . $query_args : '');
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['categories'] ?? false;
}
/**
* Fetch Brands from API
*/
public function fetch_brands($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/brands' . ($query_args ? '?' . $query_args : '');
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['brands'] ?? false;
}
/**
* Fetch Specials/Deals from API
*/
public function fetch_specials($args = []) {
$api_token = get_option('cannaiq_api_token');
if (!$api_token) {
return false;
}
$query_args = http_build_query($args);
$url = CANNAIQ_MENUS_API_URL . '/specials' . ($query_args ? '?' . $query_args : '');
$response = wp_remote_get($url, [
'headers' => [
'X-API-Key' => $api_token
],
'timeout' => 30
]);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return $data['products'] ?? false;
}
/**
* Get categories as options for Elementor select control
* Returns cached results for performance
*/
public function get_category_options() {
$cache_key = 'cannaiq_category_options';
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$categories = $this->fetch_categories();
$options = ['' => __('All Categories', 'cannaiq-menus')];
if ($categories) {
foreach ($categories as $cat) {
$name = $cat['type'] ?? $cat['name'] ?? '';
if ($name) {
$options[$name] = ucwords(str_replace('_', ' ', $name));
}
}
}
set_transient($cache_key, $options, 5 * MINUTE_IN_SECONDS);
return $options;
}
/**
* Get brands as options for Elementor select control
* Returns cached results for performance
*/
public function get_brand_options() {
$cache_key = 'cannaiq_brand_options';
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached;
}
$brands = $this->fetch_brands(['limit' => 200]);
$options = ['' => __('All Brands', 'cannaiq-menus')];
if ($brands) {
foreach ($brands as $brand) {
$name = $brand['brand'] ?? $brand['brand_name'] ?? '';
if ($name) {
$options[$name] = $name;
}
}
}
set_transient($cache_key, $options, 5 * MINUTE_IN_SECONDS);
return $options;
}
}
// Initialize Plugin
function cannaiq_menus() {
return CannaIQ_Menus_Plugin::instance();
}
cannaiq_menus();