Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Add loop-builder.php for Loop Grid integration
- Register "Products" and "Specials" as custom query sources
- Show "{Dispensary Name} Products/Specials" in query dropdown
- Auto-save dispensary name when API key is saved
- Specials source auto-filters to on_special=true
- Support all CannaiQ filters (category, brand, strain, etc.)
- Set $cannaiq_current_product for dynamic tags in loop items
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
417 lines
14 KiB
PHP
417 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* CannaiQ Loop Builder Integration
|
|
*
|
|
* Adds CannaiQ Products as a custom query source for Elementor's Loop Grid widget.
|
|
* Users can create Loop Item templates and use CannaiQ dynamic tags inside them.
|
|
*
|
|
* @package CannaIQ_Menus
|
|
* @since 2.2.0
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* CannaiQ Loop Builder Class
|
|
*/
|
|
class CannaiQ_Loop_Builder {
|
|
|
|
/**
|
|
* Instance
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Current products for the loop
|
|
*/
|
|
private $products = [];
|
|
|
|
/**
|
|
* Current product index
|
|
*/
|
|
private $current_index = 0;
|
|
|
|
/**
|
|
* Get instance
|
|
*/
|
|
public static function instance() {
|
|
if (is_null(self::$instance)) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
// Register custom query source
|
|
add_action('elementor/query/register', [$this, 'register_query_source']);
|
|
|
|
// Add CannaiQ to Loop Grid query options
|
|
add_filter('elementor/loop/query_control_options', [$this, 'add_query_options']);
|
|
|
|
// Handle the custom query
|
|
add_action('elementor/query/cannaiq_products', [$this, 'handle_query'], 10, 2);
|
|
|
|
// Filter loop data
|
|
add_filter('elementor/loop_builder/query_data', [$this, 'filter_query_data'], 10, 2);
|
|
|
|
// Set product context before rendering loop item
|
|
add_action('elementor/loop_builder/before_render_item', [$this, 'before_render_item'], 10, 2);
|
|
|
|
// Clear product context after rendering
|
|
add_action('elementor/loop_builder/after_render_item', [$this, 'after_render_item']);
|
|
|
|
// Register controls for CannaiQ query settings
|
|
add_action('elementor/element/loop-grid/section_query/before_section_end', [$this, 'add_query_controls'], 10, 2);
|
|
}
|
|
|
|
/**
|
|
* Register query source
|
|
*/
|
|
public function register_query_source($query_manager) {
|
|
// Register CannaiQ as a query source
|
|
if (method_exists($query_manager, 'register')) {
|
|
// For newer Elementor versions
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add CannaiQ to query options dropdown
|
|
* Shows the dispensary name tied to the API key
|
|
*/
|
|
public function add_query_options($options) {
|
|
$dispensary_name = get_option('cannaiq_dispensary_name', '');
|
|
|
|
if (!empty($dispensary_name)) {
|
|
$options['cannaiq_products'] = sprintf(__('%s Products', 'cannaiq-menus'), $dispensary_name);
|
|
$options['cannaiq_specials'] = sprintf(__('%s Specials', 'cannaiq-menus'), $dispensary_name);
|
|
} else {
|
|
$options['cannaiq_products'] = __('CannaiQ Products', 'cannaiq-menus');
|
|
$options['cannaiq_specials'] = __('CannaiQ Specials', 'cannaiq-menus');
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Add query controls to Loop Grid widget
|
|
*/
|
|
public function add_query_controls($widget, $args) {
|
|
// Controls apply to both products and specials sources
|
|
$cannaiq_sources = ['cannaiq_products', 'cannaiq_specials'];
|
|
|
|
$widget->add_control('cannaiq_store_id', [
|
|
'label' => __('CannaiQ Store ID', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::NUMBER,
|
|
'default' => get_option('cannaiq_default_store_id', ''),
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
'description' => __('Enter your CannaiQ dispensary/store ID', 'cannaiq-menus'),
|
|
]);
|
|
|
|
$widget->add_control('cannaiq_category', [
|
|
'label' => __('Category', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::SELECT,
|
|
'default' => '',
|
|
'options' => [
|
|
'' => __('All Categories', 'cannaiq-menus'),
|
|
'Flower' => __('Flower', 'cannaiq-menus'),
|
|
'Vape' => __('Vape', 'cannaiq-menus'),
|
|
'Edibles' => __('Edibles', 'cannaiq-menus'),
|
|
'Concentrates' => __('Concentrates', 'cannaiq-menus'),
|
|
'Pre-Rolls' => __('Pre-Rolls', 'cannaiq-menus'),
|
|
'Tinctures' => __('Tinctures', 'cannaiq-menus'),
|
|
'Topicals' => __('Topicals', 'cannaiq-menus'),
|
|
'Accessories' => __('Accessories', 'cannaiq-menus'),
|
|
],
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$widget->add_control('cannaiq_brand', [
|
|
'label' => __('Brand', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::TEXT,
|
|
'default' => '',
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
'description' => __('Filter by brand name', 'cannaiq-menus'),
|
|
]);
|
|
|
|
$widget->add_control('cannaiq_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'),
|
|
],
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
// On Special toggle only for products (specials always has it on)
|
|
$widget->add_control('cannaiq_on_special', [
|
|
'label' => __('On Special Only', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::SWITCHER,
|
|
'label_on' => __('Yes', 'cannaiq-menus'),
|
|
'label_off' => __('No', 'cannaiq-menus'),
|
|
'return_value' => 'yes',
|
|
'default' => '',
|
|
'condition' => [
|
|
'query_source' => 'cannaiq_products',
|
|
],
|
|
]);
|
|
|
|
$widget->add_control('cannaiq_in_stock_only', [
|
|
'label' => __('In Stock Only', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::SWITCHER,
|
|
'label_on' => __('Yes', 'cannaiq-menus'),
|
|
'label_off' => __('No', 'cannaiq-menus'),
|
|
'return_value' => 'yes',
|
|
'default' => 'yes',
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$widget->add_control('cannaiq_sort_by', [
|
|
'label' => __('Sort By', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::SELECT,
|
|
'default' => 'name',
|
|
'options' => [
|
|
'name' => __('Name', 'cannaiq-menus'),
|
|
'price' => __('Price', 'cannaiq-menus'),
|
|
'thc' => __('THC %', 'cannaiq-menus'),
|
|
'updated' => __('Recently Updated', 'cannaiq-menus'),
|
|
],
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$widget->add_control('cannaiq_sort_dir', [
|
|
'label' => __('Sort Direction', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::SELECT,
|
|
'default' => 'asc',
|
|
'options' => [
|
|
'asc' => __('Ascending', 'cannaiq-menus'),
|
|
'desc' => __('Descending', 'cannaiq-menus'),
|
|
],
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$widget->add_control('cannaiq_limit', [
|
|
'label' => __('Products Limit', 'cannaiq-menus'),
|
|
'type' => \Elementor\Controls_Manager::NUMBER,
|
|
'default' => 12,
|
|
'min' => 1,
|
|
'max' => 100,
|
|
'conditions' => [
|
|
'relation' => 'or',
|
|
'terms' => [
|
|
['name' => 'query_source', 'value' => 'cannaiq_products'],
|
|
['name' => 'query_source', 'value' => 'cannaiq_specials'],
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle the CannaiQ products query
|
|
*/
|
|
public function handle_query($query, $widget) {
|
|
// This modifies the WP_Query but we're using external data
|
|
// We'll handle this in filter_query_data instead
|
|
}
|
|
|
|
/**
|
|
* Filter query data to inject CannaiQ products
|
|
*/
|
|
public function filter_query_data($query_data, $widget) {
|
|
$settings = $widget->get_settings_for_display();
|
|
|
|
// Check if this is a CannaiQ query (products or specials)
|
|
$query_source = $settings['query_source'] ?? '';
|
|
$is_cannaiq = in_array($query_source, ['cannaiq_products', 'cannaiq_specials'], true);
|
|
|
|
if (!$is_cannaiq) {
|
|
return $query_data;
|
|
}
|
|
|
|
// For specials source, force on_special parameter
|
|
$is_specials = ($query_source === 'cannaiq_specials');
|
|
|
|
// Fetch products from CannaiQ API
|
|
$products = $this->fetch_products($settings, $is_specials);
|
|
|
|
if (empty($products)) {
|
|
return $query_data;
|
|
}
|
|
|
|
// Store products for later use
|
|
$this->products = $products;
|
|
$this->current_index = 0;
|
|
|
|
// Create fake post objects for Elementor to iterate
|
|
$fake_posts = [];
|
|
foreach ($products as $index => $product) {
|
|
$fake_post = new stdClass();
|
|
$fake_post->ID = 'cannaiq_' . ($product['id'] ?? $index);
|
|
$fake_post->post_title = $product['name'] ?? '';
|
|
$fake_post->post_type = 'cannaiq_product';
|
|
$fake_post->cannaiq_product = $product;
|
|
$fake_post->cannaiq_index = $index;
|
|
$fake_posts[] = $fake_post;
|
|
}
|
|
|
|
// Override query data
|
|
$query_data['posts'] = $fake_posts;
|
|
$query_data['total'] = count($products);
|
|
|
|
return $query_data;
|
|
}
|
|
|
|
/**
|
|
* Fetch products from CannaiQ API
|
|
*
|
|
* @param array $settings Widget settings
|
|
* @param bool $is_specials Whether to force on_special filter (for Specials source)
|
|
*/
|
|
private function fetch_products($settings, $is_specials = false) {
|
|
$api_url = CANNAIQ_MENUS_API_URL;
|
|
$api_token = get_option('cannaiq_api_token');
|
|
|
|
// Build query params
|
|
$params = [
|
|
'limit' => $settings['cannaiq_limit'] ?? 12,
|
|
'sort_by' => $settings['cannaiq_sort_by'] ?? 'name',
|
|
'sort_dir' => $settings['cannaiq_sort_dir'] ?? 'asc',
|
|
];
|
|
|
|
if (!empty($settings['cannaiq_store_id'])) {
|
|
$params['dispensary_id'] = $settings['cannaiq_store_id'];
|
|
}
|
|
|
|
if (!empty($settings['cannaiq_category'])) {
|
|
$params['category'] = $settings['cannaiq_category'];
|
|
}
|
|
|
|
if (!empty($settings['cannaiq_brand'])) {
|
|
$params['brand'] = $settings['cannaiq_brand'];
|
|
}
|
|
|
|
if (!empty($settings['cannaiq_strain_type'])) {
|
|
$params['strain_type'] = $settings['cannaiq_strain_type'];
|
|
}
|
|
|
|
// Force on_special for Specials source, otherwise use toggle setting
|
|
if ($is_specials || $settings['cannaiq_on_special'] === 'yes') {
|
|
$params['on_special'] = 'true';
|
|
}
|
|
|
|
if ($settings['cannaiq_in_stock_only'] === 'yes') {
|
|
$params['in_stock_only'] = 'true';
|
|
}
|
|
|
|
// Make API request
|
|
$url = $api_url . '/products?' . http_build_query($params);
|
|
|
|
$args = [
|
|
'headers' => [
|
|
'Authorization' => 'Bearer ' . $api_token,
|
|
'Content-Type' => 'application/json',
|
|
],
|
|
'timeout' => 30,
|
|
];
|
|
|
|
$response = wp_remote_get($url, $args);
|
|
|
|
if (is_wp_error($response)) {
|
|
error_log('CannaiQ Loop Builder: API error - ' . $response->get_error_message());
|
|
return [];
|
|
}
|
|
|
|
$body = wp_remote_retrieve_body($response);
|
|
$data = json_decode($body, true);
|
|
|
|
if (!isset($data['products']) || !is_array($data['products'])) {
|
|
error_log('CannaiQ Loop Builder: Invalid API response');
|
|
return [];
|
|
}
|
|
|
|
return $data['products'];
|
|
}
|
|
|
|
/**
|
|
* Set product context before rendering each loop item
|
|
*/
|
|
public function before_render_item($post, $widget) {
|
|
// Check if this is a CannaiQ product
|
|
if (isset($post->cannaiq_product)) {
|
|
global $cannaiq_current_product;
|
|
$cannaiq_current_product = $post->cannaiq_product;
|
|
} elseif (isset($post->cannaiq_index) && isset($this->products[$post->cannaiq_index])) {
|
|
global $cannaiq_current_product;
|
|
$cannaiq_current_product = $this->products[$post->cannaiq_index];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear product context after rendering
|
|
*/
|
|
public function after_render_item() {
|
|
global $cannaiq_current_product;
|
|
$cannaiq_current_product = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize Loop Builder integration
|
|
*/
|
|
function cannaiq_init_loop_builder() {
|
|
if (did_action('elementor/loaded')) {
|
|
CannaiQ_Loop_Builder::instance();
|
|
}
|
|
}
|
|
add_action('init', 'cannaiq_init_loop_builder');
|