diff --git a/backend/public/downloads/cannaiq-menus-2.0.0.zip b/backend/public/downloads/cannaiq-menus-2.0.0.zip
index 5d6ee49d..6402d45c 100644
Binary files a/backend/public/downloads/cannaiq-menus-2.0.0.zip and b/backend/public/downloads/cannaiq-menus-2.0.0.zip differ
diff --git a/backend/src/utils/payload-storage.ts b/backend/src/utils/payload-storage.ts
index 8abc1a3c..4cfbb013 100644
--- a/backend/src/utils/payload-storage.ts
+++ b/backend/src/utils/payload-storage.ts
@@ -417,23 +417,26 @@ export async function listPayloadMetadata(
sizeBytes: number;
sizeBytesRaw: number;
fetchedAt: Date;
+ dispensary_name: string | null;
+ city: string | null;
+ state: string | null;
}>> {
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (options.dispensaryId) {
- conditions.push(`dispensary_id = $${paramIndex++}`);
+ conditions.push(`rcp.dispensary_id = $${paramIndex++}`);
params.push(options.dispensaryId);
}
if (options.startDate) {
- conditions.push(`fetched_at >= $${paramIndex++}`);
+ conditions.push(`rcp.fetched_at >= $${paramIndex++}`);
params.push(options.startDate);
}
if (options.endDate) {
- conditions.push(`fetched_at <= $${paramIndex++}`);
+ conditions.push(`rcp.fetched_at <= $${paramIndex++}`);
params.push(options.endDate);
}
@@ -445,17 +448,21 @@ export async function listPayloadMetadata(
const result = await pool.query(`
SELECT
- id,
- dispensary_id,
- crawl_run_id,
- storage_path,
- product_count,
- size_bytes,
- size_bytes_raw,
- fetched_at
- FROM raw_crawl_payloads
+ rcp.id,
+ rcp.dispensary_id,
+ rcp.crawl_run_id,
+ rcp.storage_path,
+ rcp.product_count,
+ rcp.size_bytes,
+ rcp.size_bytes_raw,
+ rcp.fetched_at,
+ d.name as dispensary_name,
+ d.city,
+ d.state
+ FROM raw_crawl_payloads rcp
+ LEFT JOIN dispensaries d ON d.id = rcp.dispensary_id
${whereClause}
- ORDER BY fetched_at DESC
+ ORDER BY rcp.fetched_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex}
`, params);
@@ -467,7 +474,10 @@ export async function listPayloadMetadata(
productCount: row.product_count,
sizeBytes: row.size_bytes,
sizeBytesRaw: row.size_bytes_raw,
- fetchedAt: row.fetched_at
+ fetchedAt: row.fetched_at,
+ dispensary_name: row.dispensary_name,
+ city: row.city,
+ state: row.state
}));
}
diff --git a/cannaiq/src/lib/api.ts b/cannaiq/src/lib/api.ts
index a393460c..d1341643 100755
--- a/cannaiq/src/lib/api.ts
+++ b/cannaiq/src/lib/api.ts
@@ -3662,6 +3662,8 @@ export interface PayloadMetadata {
sizeBytesRaw: number;
fetchedAt: string;
dispensary_name?: string;
+ city?: string;
+ state?: string;
}
// Type for high-frequency (per-store) schedules
diff --git a/cannaiq/src/pages/PayloadsDashboard.tsx b/cannaiq/src/pages/PayloadsDashboard.tsx
index 24e5b1b6..890dd954 100644
--- a/cannaiq/src/pages/PayloadsDashboard.tsx
+++ b/cannaiq/src/pages/PayloadsDashboard.tsx
@@ -347,10 +347,17 @@ export function PayloadsDashboard() {
-
-
- {payload.dispensary_name || `Store #${payload.dispensaryId}`}
-
+
+
+
+ {payload.dispensary_name || `Store #${payload.dispensaryId}`}
+
+ {(payload.city || payload.state) && (
+
+ {payload.city}{payload.city && payload.state ? ', ' : ''}{payload.state}
+
+ )}
+
|
diff --git a/wordpress-plugin/cannaiq-menus.php b/wordpress-plugin/cannaiq-menus.php
index b01f46fc..5420cedd 100644
--- a/wordpress-plugin/cannaiq-menus.php
+++ b/wordpress-plugin/cannaiq-menus.php
@@ -69,9 +69,23 @@ class CannaIQ_Menus_Plugin {
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags-extended.php';
}
- // Register shortcodes - primary CannaIQ shortcodes
+ // 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']);
@@ -320,8 +334,9 @@ class CannaIQ_Menus_Plugin {
Usage
+
Shortcodes
-
+
| Shortcode |
@@ -331,21 +346,175 @@ class CannaIQ_Menus_Plugin {
[cannaiq_products] |
- Display a grid of products. Options: category_id, limit, columns, in_stock |
+ Product grid. Options: category, brand, limit, columns, in_stock |
[cannaiq_product id="123"] |
- Display a single product by ID |
+ Single product by ID |
+
+
+ [cannaiq_specials] |
+ Products on sale. Options: limit, columns |
+
+
+ [cannaiq_brands] |
+ Brand grid. Options: limit, columns |
+
+
+ [cannaiq_categories] |
+ Category list. Options: style (list|grid) |
- Elementor Widgets
- If you have Elementor installed, you can use the CannaiQ widgets:
+ Component Shortcodes (use inside product context)
+
+
+
+ | Shortcode |
+ Description |
+
+
+
+
+ [cannaiq_discount_badge] |
+ Discount ribbon/pill. Options: style (ribbon|pill|text) |
+
+
+ [cannaiq_strain_badge] |
+ Sativa/Indica/Hybrid badge. Options: style (pill|text) |
+
+
+ [cannaiq_thc] |
+ THC percentage. Options: style (meter|badge|pill|text) |
+
+
+ [cannaiq_cbd] |
+ CBD percentage. Options: style (meter|badge|pill|text) |
+
+
+ [cannaiq_effects] |
+ Effect chips with icons. Options: limit, icons (yes|no) |
+
+
+ [cannaiq_price] |
+ Price display. Options: show_original (yes|no), show_weight (yes|no) |
+
+
+ [cannaiq_cart_button] |
+ Add to cart button. Options: text, style (solid|outline) |
+
+
+ [cannaiq_stock] |
+ Stock status. Options: style (badge|text|dot) |
+
+
+ [cannaiq_terpenes] |
+ Terpene profile. Options: limit, style (chips|list|text) |
+
+
+
+
+ Elementor Widgets
+ With Elementor installed, find CannaiQ widgets in the editor:
+
+ Layout Widgets
- - CannaiQ Product Grid - Display a grid of products with filtering options
- - CannaiQ Single Product - Display a single product card
+ - Product Grid - Filterable product grid
+ - Product Loop - Custom loop for building cards
+ - Single Product - Display one product
+ - Brand Grid - Display brands
+ - Category List - Display categories
+ - Specials/Deals - Products on sale
+
+ Component Widgets (v2.0)
+
+ - Discount Ribbon - Sale percentage badge
+ - Strain Badge - Sativa/Indica/Hybrid pill
+ - THC/CBD Meter - Potency display
+ - Effects Display - Effect chips with icons
+ - Price Block - Price with sale formatting
+ - Cart Button - Styled CTA button
+ - Stock Indicator - Availability badge
+ - Product Image + Badges - Image with overlays
+
+
+ Card Templates (v2.0)
+
+ - Premium Product Card - Ready-to-use card with all components
+
+
+ Dynamic Tags
+ In Elementor, use dynamic tags to insert product data into any widget. Look for "CannaiQ Product" in the dynamic tags menu.
+
+
+
+ How to Build a Product Card
+ Use the modular components to build custom product cards. Here's an example layout:
+
+
+
+
+
+
+ 67% OFF
+
+ SATIVA
+ 24.5% THC
+
+ 🌿
+
+
+
+ Hot Lava
+ by TruInfusion
+
+ 😴 Sleepy
+ 😌 Relaxed
+ 😊 Happy
+
+
+ 1/8 oz
+ $45.00
+ $15.00
+
+ ADD TO CART →
+
+
+
+
+
+ Components Used:
+
+ | Discount Ribbon | Top-left corner badge |
+ | Product Image | With badge overlays |
+ | Strain Badge | Green Sativa pill |
+ | THC Badge | Dark potency pill |
+ | Product Name | Dynamic tag |
+ | Brand Name | Dynamic tag |
+ | Effects Display | Colored chips with icons |
+ | Price Block | Weight + strikethrough + sale |
+ | Cart Button | Links to dispensary menu |
+
+
+ Build Steps:
+
+ - Add a Product Loop widget
+ - Inside the loop, add a container
+ - Add Product Image + Badges widget
+ - Add heading with Product Name dynamic tag
+ - Add text with Brand Name dynamic tag
+ - Add Effects Display widget
+ - Add Price Block widget
+ - Add Cart Button widget
+
+
+
+ Tip: Use the Premium Product Card template widget for a ready-to-use version of this layout!
+
+
+
12,
+ 'columns' => 3
+ ], $atts);
+
+ $products = $this->fetch_specials($atts);
+
+ if (!$products) {
+ return 'No specials found. ';
+ }
+
+ 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 'No brands found. ';
+ }
+
+ $columns = intval($atts['columns']);
+ ob_start();
+ ?>
+
+
+
+
+ ![<?php echo esc_attr($brand['brand'] ?? $brand['name']); ?>](<?php echo esc_url($brand['logo']); ?>)
+
+
+
+ products
+
+
+
+
+ 'list'
+ ], $atts);
+
+ $categories = $this->fetch_categories();
+
+ if (!$categories) {
+ return 'No categories found. ';
+ }
+
+ ob_start();
+ if ($atts['style'] === 'grid') {
+ ?>
+
+
+
+ '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('%s%% OFF', $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('%s',
+ 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('%s',
+ 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('%s',
+ 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();
+ ?>
+
+
+
+
+
+
+ $
+
+ $
+
+ $
+
+
+ 'ADD TO CART', 'style' => 'solid'], $atts);
+ $url = $cannaiq_current_product['menuUrl'] ?? $cannaiq_current_product['menu_url'] ?? '#';
+
+ return sprintf('%s',
+ 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' ? '' : '';
+ return sprintf('%s%s', 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 '';
+ foreach ($terpenes as $terp) {
+ $name = $terp['name'] ?? '';
+ $percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
+ printf('%s%s',
+ esc_html($name), esc_html($percent));
+ }
+ echo ' ';
+ } 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 '';
+ foreach ($terpenes as $terp) {
+ $name = $terp['name'] ?? '';
+ $percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
+ printf(' %s%s ',
+ esc_html($name), esc_html($percent));
+ }
+ echo ' ';
+ }
+
+ return ob_get_clean();
+ }
+
/**
* Fetch Products from API
*/
|