From 87da7625cdefd8db39475335dcd50975e3ed35ee Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 17 Dec 2025 01:58:20 -0700 Subject: [PATCH] feat: WordPress plugin v2.0.0 - modular component library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../public/downloads/cannaiq-menus-2.0.0.zip | Bin 57738 -> 61812 bytes backend/src/utils/payload-storage.ts | 38 +- cannaiq/src/lib/api.ts | 2 + cannaiq/src/pages/PayloadsDashboard.tsx | 15 +- wordpress-plugin/cannaiq-menus.php | 500 +++++++++++++++++- 5 files changed, 529 insertions(+), 26 deletions(-) diff --git a/backend/public/downloads/cannaiq-menus-2.0.0.zip b/backend/public/downloads/cannaiq-menus-2.0.0.zip index 5d6ee49d793ae9f21dde2d53092632e442d9be99..6402d45c0033b333f4c4a8a4dd868536ebcae391 100644 GIT binary patch delta 9215 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 delta 5097 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 LL> { 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

- +
@@ -331,21 +346,175 @@ class CannaIQ_Menus_Plugin { - + - + + + + + + + + + + + + +
Shortcode
[cannaiq_products]Display a grid of products. Options: category_id, limit, columns, in_stockProduct grid. Options: category, brand, limit, columns, in_stock
[cannaiq_product id="123"]Display a single product by IDSingle 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)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ShortcodeDescription
[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 RibbonTop-left corner badge
Product ImageWith badge overlays
Strain BadgeGreen Sativa pill
THC BadgeDark potency pill
Product NameDynamic tag
Brand NameDynamic tag
Effects DisplayColored chips with icons
Price BlockWeight + strikethrough + sale
Cart ButtonLinks to dispensary menu
+ +

Build Steps:

+
    +
  1. Add a Product Loop widget
  2. +
  3. Inside the loop, add a container
  4. +
  5. Add Product Image + Badges widget
  6. +
  7. Add heading with Product Name dynamic tag
  8. +
  9. Add text with Brand Name dynamic tag
  10. +
  11. Add Effects Display widget
  12. +
  13. Add Price Block widget
  14. +
  15. Add Cart Button widget
  16. +
+ +

+ 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']); ?> + +

+ +

products

+ +
+ +
+ 'list' + ], $atts); + + $categories = $this->fetch_categories(); + + if (!$categories) { + return '

No categories found.

'; + } + + ob_start(); + if ($atts['style'] === 'grid') { + ?> +
+ +
+

+ +

products

+ +
+ +
+ +
    + +
  • + + + () + +
  • + +
+ '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 */