WordPress Plugin: - Add dynamic tags for all product payload fields (name, brand, price, THC, effects, etc.) - Add Product Loop widget with filtering, sorting, and layout options - Register CannaIQ widget category in Elementor - Update build script to auto-upload to MinIO CDN - Remove legacy dutchie references - Bump version to 1.7.0 Backend: - Redirect /downloads/* to CDN instead of serving from local filesystem 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
355 lines
14 KiB
PHP
355 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Crawlsy Menus
|
|
* Plugin URI: https://creationshop.io
|
|
* Description: Display cannabis product menus from Crawlsy with Elementor integration
|
|
* Version: 1.5.4
|
|
* Author: Creationshop
|
|
* Author URI: https://creationshop.io
|
|
* License: GPL v2 or later
|
|
* Text Domain: crawlsy-menus
|
|
* Requires PHP: 7.4
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit; // Exit if accessed directly
|
|
}
|
|
|
|
define('CRAWLSY_MENUS_VERSION', '1.5.4');
|
|
define('CRAWLSY_MENUS_API_URL', 'https://cannaiq.co/api/v1');
|
|
define('CRAWLSY_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
|
define('CRAWLSY_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__));
|
|
|
|
/**
|
|
* Main Plugin Class
|
|
*/
|
|
class Crawlsy_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/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']);
|
|
}
|
|
|
|
public function init() {
|
|
// Initialize plugin
|
|
load_plugin_textdomain('crawlsy-menus', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
|
|
|
// Register shortcodes
|
|
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) {
|
|
require_once CRAWLSY_MENUS_PLUGIN_DIR . 'widgets/product-grid.php';
|
|
require_once CRAWLSY_MENUS_PLUGIN_DIR . 'widgets/single-product.php';
|
|
|
|
$widgets_manager->register(new \Crawlsy_Menus_Product_Grid_Widget());
|
|
$widgets_manager->register(new \Crawlsy_Menus_Single_Product_Widget());
|
|
}
|
|
|
|
/**
|
|
* Enqueue Scripts and Styles
|
|
*/
|
|
public function enqueue_scripts() {
|
|
wp_enqueue_style(
|
|
'crawlsy-menus-styles',
|
|
CRAWLSY_MENUS_PLUGIN_URL . 'assets/css/crawlsy-menus.css',
|
|
[],
|
|
CRAWLSY_MENUS_VERSION
|
|
);
|
|
|
|
wp_enqueue_script(
|
|
'crawlsy-menus-script',
|
|
CRAWLSY_MENUS_PLUGIN_URL . 'assets/js/crawlsy-menus.js',
|
|
['jquery'],
|
|
CRAWLSY_MENUS_VERSION,
|
|
true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add Admin Menu
|
|
*/
|
|
public function add_admin_menu() {
|
|
add_menu_page(
|
|
'Crawlsy Menus',
|
|
'Crawlsy Menus',
|
|
'manage_options',
|
|
'crawlsy-menus',
|
|
[$this, 'admin_page'],
|
|
'dashicons-products',
|
|
30
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register Plugin Settings
|
|
*/
|
|
public function register_settings() {
|
|
register_setting('crawlsy_menus_settings', 'crawlsy_api_token');
|
|
}
|
|
|
|
/**
|
|
* Admin Page
|
|
*/
|
|
public function admin_page() {
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Crawlsy Menus Settings</h1>
|
|
<p>Version <?php echo CRAWLSY_MENUS_VERSION; ?> by <a href="https://creationshop.io" target="_blank">Creationshop</a></p>
|
|
|
|
<form method="post" action="options.php">
|
|
<?php settings_fields('crawlsy_menus_settings'); ?>
|
|
<?php do_settings_sections('crawlsy_menus_settings'); ?>
|
|
|
|
<table class="form-table">
|
|
<tr>
|
|
<th scope="row"><label for="crawlsy_api_token">API Token</label></th>
|
|
<td>
|
|
<input type="password" id="crawlsy_api_token" name="crawlsy_api_token"
|
|
value="<?php echo esc_attr(get_option('crawlsy_api_token')); ?>"
|
|
class="regular-text" />
|
|
<p class="description">Your authentication token from the 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 CRAWLSY_MENUS_API_URL; ?>';
|
|
var apiToken = $('#crawlsy_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 to ' + apiUrl + '...</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 Crawlsy 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 pipeline may still be processing.</p>';
|
|
}
|
|
|
|
errorHtml += '</div>';
|
|
$('#api-test-result').html(errorHtml);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
</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 CRAWLSY_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 CRAWLSY_MENUS_PLUGIN_DIR . 'templates/single-product.php';
|
|
return ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Fetch Products from API
|
|
*/
|
|
public function fetch_products($args = []) {
|
|
$api_token = get_option('crawlsy_api_token');
|
|
|
|
if (!$api_token) {
|
|
return false;
|
|
}
|
|
|
|
$query_args = http_build_query($args);
|
|
$url = CRAWLSY_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('crawlsy_api_token');
|
|
|
|
if (!$api_token) {
|
|
return false;
|
|
}
|
|
|
|
$url = CRAWLSY_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;
|
|
}
|
|
}
|
|
|
|
// Initialize Plugin
|
|
function crawlsy_menus() {
|
|
return Crawlsy_Menus_Plugin::instance();
|
|
}
|
|
|
|
crawlsy_menus();
|