Files
cannaiq/wordpress-plugin/includes/loop-builder.php
Kelly 214cbaee7d
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: Add Elementor Loop Grid support with Products and Specials sources
- 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>
2025-12-17 15:12:34 -07:00

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');