feat: WordPress plugin v2.0.0 - modular component library
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

- 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 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-17 01:58:20 -07:00
parent 9f3bc8a843
commit 87da7625cd
5 changed files with 529 additions and 26 deletions

View File

@@ -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
}));
}

View File

@@ -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

View File

@@ -347,10 +347,17 @@ export function PayloadsDashboard() {
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Store className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium truncate max-w-[200px]">
{payload.dispensary_name || `Store #${payload.dispensaryId}`}
</span>
<Store className="w-4 h-4 text-gray-400 flex-shrink-0" />
<div className="min-w-0">
<div className="text-sm font-medium truncate max-w-[200px]">
{payload.dispensary_name || `Store #${payload.dispensaryId}`}
</div>
{(payload.city || payload.state) && (
<div className="text-xs text-gray-500 truncate">
{payload.city}{payload.city && payload.state ? ', ' : ''}{payload.state}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">

View File

@@ -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 {
<hr />
<h2>Usage</h2>
<h3>Shortcodes</h3>
<table class="widefat" style="max-width: 800px;">
<table class="widefat" style="max-width: 900px;">
<thead>
<tr>
<th>Shortcode</th>
@@ -331,21 +346,175 @@ class CannaIQ_Menus_Plugin {
<tbody>
<tr>
<td><code>[cannaiq_products]</code></td>
<td>Display a grid of products. Options: <code>category_id</code>, <code>limit</code>, <code>columns</code>, <code>in_stock</code></td>
<td>Product grid. Options: <code>category</code>, <code>brand</code>, <code>limit</code>, <code>columns</code>, <code>in_stock</code></td>
</tr>
<tr>
<td><code>[cannaiq_product id="123"]</code></td>
<td>Display a single product by ID</td>
<td>Single product by ID</td>
</tr>
<tr>
<td><code>[cannaiq_specials]</code></td>
<td>Products on sale. Options: <code>limit</code>, <code>columns</code></td>
</tr>
<tr>
<td><code>[cannaiq_brands]</code></td>
<td>Brand grid. Options: <code>limit</code>, <code>columns</code></td>
</tr>
<tr>
<td><code>[cannaiq_categories]</code></td>
<td>Category list. Options: <code>style</code> (list|grid)</td>
</tr>
</tbody>
</table>
<h3>Elementor Widgets</h3>
<p>If you have Elementor installed, you can use the CannaiQ widgets:</p>
<h4 style="margin-top: 20px;">Component Shortcodes <span style="color: #666; font-weight: normal;">(use inside product context)</span></h4>
<table class="widefat" style="max-width: 900px;">
<thead>
<tr>
<th>Shortcode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>[cannaiq_discount_badge]</code></td>
<td>Discount ribbon/pill. Options: <code>style</code> (ribbon|pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_strain_badge]</code></td>
<td>Sativa/Indica/Hybrid badge. Options: <code>style</code> (pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_thc]</code></td>
<td>THC percentage. Options: <code>style</code> (meter|badge|pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_cbd]</code></td>
<td>CBD percentage. Options: <code>style</code> (meter|badge|pill|text)</td>
</tr>
<tr>
<td><code>[cannaiq_effects]</code></td>
<td>Effect chips with icons. Options: <code>limit</code>, <code>icons</code> (yes|no)</td>
</tr>
<tr>
<td><code>[cannaiq_price]</code></td>
<td>Price display. Options: <code>show_original</code> (yes|no), <code>show_weight</code> (yes|no)</td>
</tr>
<tr>
<td><code>[cannaiq_cart_button]</code></td>
<td>Add to cart button. Options: <code>text</code>, <code>style</code> (solid|outline)</td>
</tr>
<tr>
<td><code>[cannaiq_stock]</code></td>
<td>Stock status. Options: <code>style</code> (badge|text|dot)</td>
</tr>
<tr>
<td><code>[cannaiq_terpenes]</code></td>
<td>Terpene profile. Options: <code>limit</code>, <code>style</code> (chips|list|text)</td>
</tr>
</tbody>
</table>
<h3 style="margin-top: 30px;">Elementor Widgets</h3>
<p>With Elementor installed, find CannaiQ widgets in the editor:</p>
<h4>Layout Widgets</h4>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>CannaiQ Product Grid</strong> - Display a grid of products with filtering options</li>
<li><strong>CannaiQ Single Product</strong> - Display a single product card</li>
<li><strong>Product Grid</strong> - Filterable product grid</li>
<li><strong>Product Loop</strong> - Custom loop for building cards</li>
<li><strong>Single Product</strong> - Display one product</li>
<li><strong>Brand Grid</strong> - Display brands</li>
<li><strong>Category List</strong> - Display categories</li>
<li><strong>Specials/Deals</strong> - Products on sale</li>
</ul>
<h4>Component Widgets <span style="color: #666; font-weight: normal;">(v2.0)</span></h4>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>Discount Ribbon</strong> - Sale percentage badge</li>
<li><strong>Strain Badge</strong> - Sativa/Indica/Hybrid pill</li>
<li><strong>THC/CBD Meter</strong> - Potency display</li>
<li><strong>Effects Display</strong> - Effect chips with icons</li>
<li><strong>Price Block</strong> - Price with sale formatting</li>
<li><strong>Cart Button</strong> - Styled CTA button</li>
<li><strong>Stock Indicator</strong> - Availability badge</li>
<li><strong>Product Image + Badges</strong> - Image with overlays</li>
</ul>
<h4>Card Templates <span style="color: #666; font-weight: normal;">(v2.0)</span></h4>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>Premium Product Card</strong> - Ready-to-use card with all components</li>
</ul>
<h3 style="margin-top: 30px;">Dynamic Tags</h3>
<p>In Elementor, use dynamic tags to insert product data into any widget. Look for "CannaiQ Product" in the dynamic tags menu.</p>
<hr style="margin: 30px 0;" />
<h2>How to Build a Product Card</h2>
<p>Use the modular components to build custom product cards. Here's an example layout:</p>
<div style="display: flex; gap: 30px; flex-wrap: wrap; margin-top: 20px;">
<!-- Visual Example -->
<div style="background: #fff; border: 2px solid #ddd; border-radius: 12px; width: 300px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Image area -->
<div style="background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%); height: 200px; position: relative;">
<span style="position: absolute; top: 0; left: 0; background: #ef4444; color: white; padding: 4px 12px; font-size: 12px; font-weight: bold; border-bottom-right-radius: 8px;">67% OFF</span>
<div style="position: absolute; bottom: 8px; left: 8px; display: flex; gap: 4px;">
<span style="background: #22c55e; color: white; padding: 2px 8px; border-radius: 999px; font-size: 10px; font-weight: bold;">SATIVA</span>
<span style="background: #1f2937; color: white; padding: 2px 8px; border-radius: 999px; font-size: 10px; font-weight: bold;">24.5% THC</span>
</div>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; font-size: 48px;">🌿</div>
</div>
<!-- Body -->
<div style="padding: 16px;">
<h4 style="margin: 0 0 4px 0; font-size: 18px;">Hot Lava</h4>
<p style="margin: 0 0 12px 0; color: #666; font-size: 14px;">by TruInfusion</p>
<div style="display: flex; gap: 6px; margin-bottom: 12px;">
<span style="background: #fef3c7; border: 1px solid #fcd34d; padding: 2px 8px; border-radius: 999px; font-size: 11px;">😴 Sleepy</span>
<span style="background: #dbeafe; border: 1px solid #93c5fd; padding: 2px 8px; border-radius: 999px; font-size: 11px;">😌 Relaxed</span>
<span style="background: #fce7f3; border: 1px solid #f9a8d4; padding: 2px 8px; border-radius: 999px; font-size: 11px;">😊 Happy</span>
</div>
<div style="margin-bottom: 12px;">
<span style="color: #999; font-size: 14px;">1/8 oz</span>
<span style="color: #999; text-decoration: line-through; margin-left: 8px;">$45.00</span>
<span style="color: #dc2626; font-weight: bold; font-size: 18px; margin-left: 8px;">$15.00</span>
</div>
<div style="background: #1f2937; color: white; text-align: center; padding: 10px; border-radius: 6px; font-weight: bold; font-size: 14px;">ADD TO CART →</div>
</div>
</div>
<!-- Component Labels -->
<div style="flex: 1; min-width: 300px;">
<h4 style="margin-top: 0;">Components Used:</h4>
<table class="widefat" style="max-width: 400px;">
<tr><td style="width: 40%;"><strong>Discount Ribbon</strong></td><td>Top-left corner badge</td></tr>
<tr><td><strong>Product Image</strong></td><td>With badge overlays</td></tr>
<tr><td><strong>Strain Badge</strong></td><td>Green Sativa pill</td></tr>
<tr><td><strong>THC Badge</strong></td><td>Dark potency pill</td></tr>
<tr><td><strong>Product Name</strong></td><td>Dynamic tag</td></tr>
<tr><td><strong>Brand Name</strong></td><td>Dynamic tag</td></tr>
<tr><td><strong>Effects Display</strong></td><td>Colored chips with icons</td></tr>
<tr><td><strong>Price Block</strong></td><td>Weight + strikethrough + sale</td></tr>
<tr><td><strong>Cart Button</strong></td><td>Links to dispensary menu</td></tr>
</table>
<h4 style="margin-top: 20px;">Build Steps:</h4>
<ol style="margin-left: 20px; line-height: 1.8;">
<li>Add a <strong>Product Loop</strong> widget</li>
<li>Inside the loop, add a container</li>
<li>Add <strong>Product Image + Badges</strong> widget</li>
<li>Add heading with <strong>Product Name</strong> dynamic tag</li>
<li>Add text with <strong>Brand Name</strong> dynamic tag</li>
<li>Add <strong>Effects Display</strong> widget</li>
<li>Add <strong>Price Block</strong> widget</li>
<li>Add <strong>Cart Button</strong> widget</li>
</ol>
<p style="margin-top: 20px; padding: 12px; background: #e7f3ff; border-left: 4px solid #2196f3; border-radius: 4px;">
<strong>Tip:</strong> Use the <strong>Premium Product Card</strong> template widget for a ready-to-use version of this layout!
</p>
</div>
</div>
</div>
<?php
}
@@ -395,6 +564,321 @@ class CannaIQ_Menus_Plugin {
return ob_get_clean();
}
/**
* Specials Shortcode
*/
public function specials_shortcode($atts) {
$atts = shortcode_atts([
'limit' => 12,
'columns' => 3
], $atts);
$products = $this->fetch_specials($atts);
if (!$products) {
return '<p>No specials found.</p>';
}
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 '<p>No brands found.</p>';
}
$columns = intval($atts['columns']);
ob_start();
?>
<div class="cannaiq-brands-grid" style="display: grid; grid-template-columns: repeat(<?php echo $columns; ?>, 1fr); gap: 20px;">
<?php foreach ($brands as $brand): ?>
<div class="cannaiq-brand-card" style="text-align: center; padding: 20px; background: #f9fafb; border-radius: 8px;">
<?php if (!empty($brand['logo'])): ?>
<img src="<?php echo esc_url($brand['logo']); ?>" alt="<?php echo esc_attr($brand['brand'] ?? $brand['name']); ?>" style="max-height: 60px; margin-bottom: 10px;" />
<?php endif; ?>
<h4 style="margin: 0;"><?php echo esc_html($brand['brand'] ?? $brand['name']); ?></h4>
<?php if (!empty($brand['product_count'])): ?>
<p style="margin: 5px 0 0; color: #666; font-size: 14px;"><?php echo intval($brand['product_count']); ?> products</p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Categories Shortcode
*/
public function categories_shortcode($atts) {
$atts = shortcode_atts([
'style' => 'list'
], $atts);
$categories = $this->fetch_categories();
if (!$categories) {
return '<p>No categories found.</p>';
}
ob_start();
if ($atts['style'] === 'grid') {
?>
<div class="cannaiq-categories-grid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px;">
<?php foreach ($categories as $cat): ?>
<div class="cannaiq-category-card" style="padding: 15px; background: #f9fafb; border-radius: 8px; text-align: center;">
<h4 style="margin: 0;"><?php echo esc_html(ucwords(str_replace('_', ' ', $cat['type'] ?? $cat['name']))); ?></h4>
<?php if (!empty($cat['count'])): ?>
<p style="margin: 5px 0 0; color: #666; font-size: 14px;"><?php echo intval($cat['count']); ?> products</p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
} else {
?>
<ul class="cannaiq-categories-list" style="list-style: none; padding: 0; margin: 0;">
<?php foreach ($categories as $cat): ?>
<li style="padding: 10px 0; border-bottom: 1px solid #eee;">
<?php echo esc_html(ucwords(str_replace('_', ' ', $cat['type'] ?? $cat['name']))); ?>
<?php if (!empty($cat['count'])): ?>
<span style="color: #666; font-size: 14px;">(<?php echo intval($cat['count']); ?>)</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php
}
return ob_get_clean();
}
/**
* Discount Badge Shortcode
*/
public function discount_badge_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['style' => '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('<span class="%s">%s%% OFF</span>', $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('<span class="cannaiq-strain-badge cannaiq-strain-badge--%s" style="%s">%s</span>',
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('<span class="cannaiq-potency-badge cannaiq-potency-badge--%s">%s</span>',
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('<span class="cannaiq-potency-badge cannaiq-potency-badge--%s">%s</span>',
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();
?>
<span class="cannaiq-price-block">
<?php if ($atts['show_weight'] === 'yes' && !empty($weight)): ?>
<span class="cannaiq-price-block__weight"><?php echo esc_html($weight); ?></span>
<?php endif; ?>
<?php if ($is_sale): ?>
<?php if ($atts['show_original'] === 'yes'): ?>
<span class="cannaiq-price-block__original">$<?php echo number_format((float)$original, 2); ?></span>
<?php endif; ?>
<span class="cannaiq-price-block__sale">$<?php echo number_format((float)$sale, 2); ?></span>
<?php else: ?>
<span class="cannaiq-price-block__regular">$<?php echo number_format((float)$original, 2); ?></span>
<?php endif; ?>
</span>
<?php
return ob_get_clean();
}
/**
* Cart Button Shortcode
*/
public function cart_button_shortcode($atts) {
global $cannaiq_current_product;
if (!$cannaiq_current_product) return '';
$atts = shortcode_atts(['text' => 'ADD TO CART', 'style' => 'solid'], $atts);
$url = $cannaiq_current_product['menuUrl'] ?? $cannaiq_current_product['menu_url'] ?? '#';
return sprintf('<a href="%s" class="cannaiq-cart-button cannaiq-cart-button--%s" target="_blank" rel="noopener">%s</a>',
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' ? '<span class="cannaiq-stock-indicator__dot"></span>' : '';
return sprintf('<span class="%s">%s%s</span>', 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 '<div class="cannaiq-terpenes cannaiq-terpenes--chips">';
foreach ($terpenes as $terp) {
$name = $terp['name'] ?? '';
$percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
printf('<span class="cannaiq-terpene-chip"><span class="cannaiq-terpene-chip__name">%s</span><span class="cannaiq-terpene-chip__percent">%s</span></span>',
esc_html($name), esc_html($percent));
}
echo '</div>';
} 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 '<div class="cannaiq-terpenes cannaiq-terpenes--list">';
foreach ($terpenes as $terp) {
$name = $terp['name'] ?? '';
$percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
printf('<div class="cannaiq-terpene-item"><span>%s</span><span>%s</span></div>',
esc_html($name), esc_html($percent));
}
echo '</div>';
}
return ob_get_clean();
}
/**
* Fetch Products from API
*/