diff --git a/backend/public/downloads/cannaiq-menus-2.0.0.zip b/backend/public/downloads/cannaiq-menus-2.0.0.zip
new file mode 100644
index 00000000..5d6ee49d
Binary files /dev/null and b/backend/public/downloads/cannaiq-menus-2.0.0.zip differ
diff --git a/backend/public/downloads/cannaiq-menus-latest.zip b/backend/public/downloads/cannaiq-menus-latest.zip
index 026c7acd..7c310084 120000
--- a/backend/public/downloads/cannaiq-menus-latest.zip
+++ b/backend/public/downloads/cannaiq-menus-latest.zip
@@ -1 +1 @@
-cannaiq-menus-1.7.0.zip
\ No newline at end of file
+cannaiq-menus-2.0.0.zip
\ No newline at end of file
diff --git a/wordpress-plugin/VERSION b/wordpress-plugin/VERSION
index bd8bf882..227cea21 100644
--- a/wordpress-plugin/VERSION
+++ b/wordpress-plugin/VERSION
@@ -1 +1 @@
-1.7.0
+2.0.0
diff --git a/wordpress-plugin/assets/css/components.css b/wordpress-plugin/assets/css/components.css
new file mode 100644
index 00000000..fbbf0826
--- /dev/null
+++ b/wordpress-plugin/assets/css/components.css
@@ -0,0 +1,740 @@
+/**
+ * CannaIQ Modular Components CSS
+ *
+ * Styles for the modular component library.
+ * Each component is independently styled and composable.
+ *
+ * @package CannaIQ_Menus
+ * @since 2.0.0
+ */
+
+/* ==========================================================================
+ CSS Variables (Design Tokens)
+ ========================================================================== */
+
+:root {
+ /* Strain Colors */
+ --cannaiq-sativa: #22c55e;
+ --cannaiq-indica: #8b5cf6;
+ --cannaiq-hybrid: #f97316;
+
+ /* UI Colors */
+ --cannaiq-discount: #ef4444;
+ --cannaiq-discount-bg: #fef2f2;
+ --cannaiq-sale: #dc2626;
+ --cannaiq-stock-in: #16a34a;
+ --cannaiq-stock-out: #9ca3af;
+ --cannaiq-price-original: #9ca3af;
+ --cannaiq-price-sale: #dc2626;
+
+ /* Neutrals */
+ --cannaiq-text-primary: #1f2937;
+ --cannaiq-text-secondary: #6b7280;
+ --cannaiq-text-muted: #9ca3af;
+ --cannaiq-border: #e5e7eb;
+ --cannaiq-bg-light: #f9fafb;
+
+ /* Spacing */
+ --cannaiq-space-xs: 0.25rem;
+ --cannaiq-space-sm: 0.5rem;
+ --cannaiq-space-md: 0.75rem;
+ --cannaiq-space-lg: 1rem;
+ --cannaiq-space-xl: 1.5rem;
+
+ /* Border Radius */
+ --cannaiq-radius-sm: 0.25rem;
+ --cannaiq-radius-md: 0.375rem;
+ --cannaiq-radius-lg: 0.5rem;
+ --cannaiq-radius-xl: 0.75rem;
+ --cannaiq-radius-full: 9999px;
+
+ /* Shadows */
+ --cannaiq-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --cannaiq-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
+ --cannaiq-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
+}
+
+/* ==========================================================================
+ Discount Ribbon
+ ========================================================================== */
+
+.cannaiq-discount-ribbon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+ white-space: nowrap;
+}
+
+/* Ribbon Style - Corner positioned */
+.cannaiq-discount-ribbon--ribbon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: var(--cannaiq-discount);
+ color: white;
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-md);
+ font-size: 0.75rem;
+ border-bottom-right-radius: var(--cannaiq-radius-md);
+ z-index: 10;
+}
+
+/* Pill Style */
+.cannaiq-discount-ribbon--pill {
+ background: var(--cannaiq-discount);
+ color: white;
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ font-size: 0.75rem;
+ border-radius: var(--cannaiq-radius-full);
+}
+
+/* Text Style */
+.cannaiq-discount-ribbon--text {
+ color: var(--cannaiq-discount);
+ font-size: 0.875rem;
+}
+
+/* Sizes */
+.cannaiq-discount-ribbon--small {
+ font-size: 0.625rem;
+ padding: 2px var(--cannaiq-space-xs);
+}
+
+.cannaiq-discount-ribbon--large {
+ font-size: 0.875rem;
+ padding: var(--cannaiq-space-sm) var(--cannaiq-space-lg);
+}
+
+/* ==========================================================================
+ Strain Badge
+ ========================================================================== */
+
+.cannaiq-strain-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--cannaiq-space-xs);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ border-radius: var(--cannaiq-radius-full);
+}
+
+/* Pill Style */
+.cannaiq-strain-badge--pill {
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ font-size: 0.625rem;
+}
+
+/* Text Style */
+.cannaiq-strain-badge--text {
+ padding: 0;
+ font-size: 0.75rem;
+}
+
+/* Strain Type Colors */
+.cannaiq-strain-badge--sativa {
+ background: var(--cannaiq-sativa);
+ color: white;
+}
+
+.cannaiq-strain-badge--sativa.cannaiq-strain-badge--text {
+ background: transparent;
+ color: var(--cannaiq-sativa);
+}
+
+.cannaiq-strain-badge--indica {
+ background: var(--cannaiq-indica);
+ color: white;
+}
+
+.cannaiq-strain-badge--indica.cannaiq-strain-badge--text {
+ background: transparent;
+ color: var(--cannaiq-indica);
+}
+
+.cannaiq-strain-badge--hybrid {
+ background: var(--cannaiq-hybrid);
+ color: white;
+}
+
+.cannaiq-strain-badge--hybrid.cannaiq-strain-badge--text {
+ background: transparent;
+ color: var(--cannaiq-hybrid);
+}
+
+/* Sizes */
+.cannaiq-strain-badge--small {
+ font-size: 0.5rem;
+ padding: 2px var(--cannaiq-space-xs);
+}
+
+.cannaiq-strain-badge--large {
+ font-size: 0.75rem;
+ padding: var(--cannaiq-space-sm) var(--cannaiq-space-md);
+}
+
+/* ==========================================================================
+ THC/CBD Badge
+ ========================================================================== */
+
+.cannaiq-potency-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--cannaiq-space-xs);
+ font-weight: 600;
+}
+
+/* Badge Style */
+.cannaiq-potency-badge--badge {
+ background: var(--cannaiq-bg-light);
+ border: 1px solid var(--cannaiq-border);
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ border-radius: var(--cannaiq-radius-md);
+ font-size: 0.75rem;
+}
+
+/* Pill Style */
+.cannaiq-potency-badge--pill {
+ background: var(--cannaiq-text-primary);
+ color: white;
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ border-radius: var(--cannaiq-radius-full);
+ font-size: 0.75rem;
+}
+
+/* Text Style */
+.cannaiq-potency-badge--text {
+ color: var(--cannaiq-text-secondary);
+ font-size: 0.875rem;
+}
+
+.cannaiq-potency-badge__label {
+ color: var(--cannaiq-text-muted);
+ font-size: 0.625rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.cannaiq-potency-badge__value {
+ font-weight: 700;
+}
+
+/* ==========================================================================
+ THC/CBD Meter (Visual Progress Bar)
+ ========================================================================== */
+
+.cannaiq-potency-meter {
+ display: flex;
+ flex-direction: column;
+ gap: var(--cannaiq-space-xs);
+}
+
+.cannaiq-potency-meter__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.75rem;
+}
+
+.cannaiq-potency-meter__label {
+ font-weight: 600;
+ color: var(--cannaiq-text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.cannaiq-potency-meter__value {
+ font-weight: 700;
+ color: var(--cannaiq-text-primary);
+}
+
+.cannaiq-potency-meter__bar {
+ height: 6px;
+ background: var(--cannaiq-border);
+ border-radius: var(--cannaiq-radius-full);
+ overflow: hidden;
+}
+
+.cannaiq-potency-meter__fill {
+ height: 100%;
+ border-radius: var(--cannaiq-radius-full);
+ transition: width 0.3s ease;
+}
+
+.cannaiq-potency-meter--thc .cannaiq-potency-meter__fill {
+ background: linear-gradient(90deg, #22c55e 0%, #16a34a 100%);
+}
+
+.cannaiq-potency-meter--cbd .cannaiq-potency-meter__fill {
+ background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
+}
+
+/* ==========================================================================
+ Effects Display
+ ========================================================================== */
+
+.cannaiq-effects-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--cannaiq-space-sm);
+}
+
+.cannaiq-effect-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--cannaiq-space-xs);
+ background: color-mix(in srgb, var(--effect-color, #6b7280) 15%, white);
+ border: 1px solid color-mix(in srgb, var(--effect-color, #6b7280) 30%, white);
+ border-radius: var(--cannaiq-radius-full);
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: var(--cannaiq-text-primary);
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+
+.cannaiq-effect-chip:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--cannaiq-shadow-sm);
+}
+
+.cannaiq-effect-chip svg {
+ flex-shrink: 0;
+}
+
+.cannaiq-effect-chip__label {
+ white-space: nowrap;
+}
+
+/* Effect Chip Sizes */
+.cannaiq-effect-chip--small {
+ padding: 2px var(--cannaiq-space-xs);
+ font-size: 0.625rem;
+ gap: 2px;
+}
+
+.cannaiq-effect-chip--large {
+ padding: var(--cannaiq-space-sm) var(--cannaiq-space-md);
+ font-size: 0.875rem;
+}
+
+/* ==========================================================================
+ Terpene Profile
+ ========================================================================== */
+
+.cannaiq-terpenes {
+ display: flex;
+ flex-direction: column;
+ gap: var(--cannaiq-space-sm);
+}
+
+.cannaiq-terpenes--chips {
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.cannaiq-terpene-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--cannaiq-space-xs);
+ background: var(--cannaiq-bg-light);
+ border: 1px solid var(--cannaiq-border);
+ border-radius: var(--cannaiq-radius-full);
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ font-size: 0.75rem;
+}
+
+.cannaiq-terpene-chip__name {
+ font-weight: 500;
+ color: var(--cannaiq-text-primary);
+}
+
+.cannaiq-terpene-chip__percent {
+ color: var(--cannaiq-text-secondary);
+}
+
+/* Terpene List Style */
+.cannaiq-terpenes--list .cannaiq-terpene-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--cannaiq-space-xs) 0;
+ border-bottom: 1px solid var(--cannaiq-border);
+}
+
+.cannaiq-terpenes--list .cannaiq-terpene-item:last-child {
+ border-bottom: none;
+}
+
+/* ==========================================================================
+ Price Block
+ ========================================================================== */
+
+.cannaiq-price-block {
+ display: flex;
+ align-items: baseline;
+ gap: var(--cannaiq-space-sm);
+ flex-wrap: wrap;
+}
+
+.cannaiq-price-block--stacked {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--cannaiq-space-xs);
+}
+
+.cannaiq-price-block__original {
+ color: var(--cannaiq-price-original);
+ text-decoration: line-through;
+ font-size: 0.875rem;
+}
+
+.cannaiq-price-block__sale {
+ color: var(--cannaiq-price-sale);
+ font-weight: 700;
+ font-size: 1.25rem;
+}
+
+.cannaiq-price-block__regular {
+ color: var(--cannaiq-text-primary);
+ font-weight: 700;
+ font-size: 1.25rem;
+}
+
+.cannaiq-price-block__weight {
+ color: var(--cannaiq-text-muted);
+ font-size: 0.875rem;
+}
+
+/* Price Sizes */
+.cannaiq-price-block--small .cannaiq-price-block__sale,
+.cannaiq-price-block--small .cannaiq-price-block__regular {
+ font-size: 1rem;
+}
+
+.cannaiq-price-block--small .cannaiq-price-block__original {
+ font-size: 0.75rem;
+}
+
+.cannaiq-price-block--large .cannaiq-price-block__sale,
+.cannaiq-price-block--large .cannaiq-price-block__regular {
+ font-size: 1.5rem;
+}
+
+/* ==========================================================================
+ Cart Button
+ ========================================================================== */
+
+.cannaiq-cart-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--cannaiq-space-sm);
+ padding: var(--cannaiq-space-md) var(--cannaiq-space-xl);
+ font-weight: 600;
+ font-size: 0.875rem;
+ text-decoration: none;
+ border-radius: var(--cannaiq-radius-md);
+ cursor: pointer;
+ transition: all 0.15s ease;
+ border: 2px solid transparent;
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+}
+
+/* Solid Style */
+.cannaiq-cart-button--solid {
+ background: var(--cannaiq-text-primary);
+ color: white;
+ border-color: var(--cannaiq-text-primary);
+}
+
+.cannaiq-cart-button--solid:hover {
+ background: #374151;
+ border-color: #374151;
+ transform: translateY(-1px);
+ box-shadow: var(--cannaiq-shadow-md);
+}
+
+/* Outline Style */
+.cannaiq-cart-button--outline {
+ background: transparent;
+ color: var(--cannaiq-text-primary);
+ border-color: var(--cannaiq-text-primary);
+}
+
+.cannaiq-cart-button--outline:hover {
+ background: var(--cannaiq-text-primary);
+ color: white;
+}
+
+/* Full Width */
+.cannaiq-cart-button--full {
+ width: 100%;
+}
+
+/* Sizes */
+.cannaiq-cart-button--small {
+ padding: var(--cannaiq-space-sm) var(--cannaiq-space-md);
+ font-size: 0.75rem;
+}
+
+.cannaiq-cart-button--large {
+ padding: var(--cannaiq-space-lg) var(--cannaiq-space-xl);
+ font-size: 1rem;
+}
+
+/* ==========================================================================
+ Stock Indicator
+ ========================================================================== */
+
+.cannaiq-stock-indicator {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--cannaiq-space-xs);
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.cannaiq-stock-indicator--in-stock {
+ color: var(--cannaiq-stock-in);
+}
+
+.cannaiq-stock-indicator--out-of-stock {
+ color: var(--cannaiq-stock-out);
+}
+
+/* Badge Style */
+.cannaiq-stock-indicator--badge {
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ border-radius: var(--cannaiq-radius-md);
+}
+
+.cannaiq-stock-indicator--badge.cannaiq-stock-indicator--in-stock {
+ background: #dcfce7;
+}
+
+.cannaiq-stock-indicator--badge.cannaiq-stock-indicator--out-of-stock {
+ background: #f3f4f6;
+}
+
+/* Dot Indicator */
+.cannaiq-stock-indicator__dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+/* ==========================================================================
+ Product Image with Overlays
+ ========================================================================== */
+
+.cannaiq-product-image {
+ position: relative;
+ overflow: hidden;
+ border-radius: var(--cannaiq-radius-lg);
+ background: var(--cannaiq-bg-light);
+}
+
+.cannaiq-product-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.3s ease;
+}
+
+.cannaiq-product-image:hover img {
+ transform: scale(1.05);
+}
+
+.cannaiq-product-image__overlay {
+ position: absolute;
+ padding: var(--cannaiq-space-sm);
+}
+
+.cannaiq-product-image__overlay--top-left {
+ top: 0;
+ left: 0;
+}
+
+.cannaiq-product-image__overlay--top-right {
+ top: 0;
+ right: 0;
+}
+
+.cannaiq-product-image__overlay--bottom-left {
+ bottom: 0;
+ left: 0;
+}
+
+.cannaiq-product-image__overlay--bottom-right {
+ bottom: 0;
+ right: 0;
+}
+
+/* Badge Stack in Overlays */
+.cannaiq-product-image__badges {
+ display: flex;
+ gap: var(--cannaiq-space-xs);
+ flex-wrap: wrap;
+}
+
+/* ==========================================================================
+ Weight Options Selector
+ ========================================================================== */
+
+.cannaiq-weight-options {
+ display: flex;
+ gap: var(--cannaiq-space-xs);
+ flex-wrap: wrap;
+}
+
+.cannaiq-weight-option {
+ padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm);
+ border: 1px solid var(--cannaiq-border);
+ border-radius: var(--cannaiq-radius-md);
+ font-size: 0.75rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ background: white;
+}
+
+.cannaiq-weight-option:hover {
+ border-color: var(--cannaiq-text-primary);
+}
+
+.cannaiq-weight-option--selected {
+ background: var(--cannaiq-text-primary);
+ color: white;
+ border-color: var(--cannaiq-text-primary);
+}
+
+.cannaiq-weight-option__price {
+ color: var(--cannaiq-text-secondary);
+ margin-left: var(--cannaiq-space-xs);
+}
+
+.cannaiq-weight-option--selected .cannaiq-weight-option__price {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* Dropdown Style */
+.cannaiq-weight-options--dropdown {
+ display: block;
+}
+
+.cannaiq-weight-options--dropdown select {
+ width: 100%;
+ padding: var(--cannaiq-space-sm) var(--cannaiq-space-md);
+ border: 1px solid var(--cannaiq-border);
+ border-radius: var(--cannaiq-radius-md);
+ font-size: 0.875rem;
+ background: white;
+ cursor: pointer;
+}
+
+/* ==========================================================================
+ Card Container (for premade templates)
+ ========================================================================== */
+
+.cannaiq-product-card {
+ display: flex;
+ flex-direction: column;
+ background: white;
+ border-radius: var(--cannaiq-radius-xl);
+ overflow: hidden;
+ box-shadow: var(--cannaiq-shadow-sm);
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
+}
+
+.cannaiq-product-card:hover {
+ box-shadow: var(--cannaiq-shadow-lg);
+ transform: translateY(-2px);
+}
+
+.cannaiq-product-card__image {
+ position: relative;
+ aspect-ratio: 1;
+ overflow: hidden;
+}
+
+.cannaiq-product-card__body {
+ padding: var(--cannaiq-space-lg);
+ display: flex;
+ flex-direction: column;
+ gap: var(--cannaiq-space-sm);
+ flex: 1;
+}
+
+.cannaiq-product-card__title {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--cannaiq-text-primary);
+ margin: 0;
+ line-height: 1.3;
+}
+
+.cannaiq-product-card__brand {
+ font-size: 0.875rem;
+ color: var(--cannaiq-text-secondary);
+ margin: 0;
+}
+
+.cannaiq-product-card__footer {
+ margin-top: auto;
+ padding-top: var(--cannaiq-space-md);
+}
+
+/* ==========================================================================
+ Utility Classes
+ ========================================================================== */
+
+.cannaiq-flex {
+ display: flex;
+}
+
+.cannaiq-flex-wrap {
+ flex-wrap: wrap;
+}
+
+.cannaiq-items-center {
+ align-items: center;
+}
+
+.cannaiq-justify-between {
+ justify-content: space-between;
+}
+
+.cannaiq-gap-xs {
+ gap: var(--cannaiq-space-xs);
+}
+
+.cannaiq-gap-sm {
+ gap: var(--cannaiq-space-sm);
+}
+
+.cannaiq-gap-md {
+ gap: var(--cannaiq-space-md);
+}
+
+.cannaiq-gap-lg {
+ gap: var(--cannaiq-space-lg);
+}
+
+.cannaiq-mt-auto {
+ margin-top: auto;
+}
+
+.cannaiq-text-center {
+ text-align: center;
+}
+
+.cannaiq-truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/wordpress-plugin/cannaiq-menus.php b/wordpress-plugin/cannaiq-menus.php
index 92ca4579..b01f46fc 100644
--- a/wordpress-plugin/cannaiq-menus.php
+++ b/wordpress-plugin/cannaiq-menus.php
@@ -1,10 +1,10 @@
add_category(
'cannaiq',
[
- 'title' => __('CannaIQ', 'cannaiq-menus'),
+ 'title' => __('CannaiQ', 'cannaiq-menus'),
'icon' => 'fa fa-cannabis',
]
);
@@ -60,9 +60,13 @@ class CannaIQ_Menus_Plugin {
// Initialize plugin
load_plugin_textdomain('cannaiq-menus', false, dirname(plugin_basename(__FILE__)) . '/languages');
+ // Load helper functions
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'includes/effects-icons.php';
+
// Load Elementor Dynamic Tags (if Elementor is active)
if (did_action('elementor/loaded')) {
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags-extended.php';
}
// Register shortcodes - primary CannaIQ shortcodes
@@ -78,6 +82,7 @@ class CannaIQ_Menus_Plugin {
* Register Elementor Widgets
*/
public function register_elementor_widgets($widgets_manager) {
+ // Legacy widgets
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-grid.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/single-product.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/brand-grid.php';
@@ -85,18 +90,46 @@ class CannaIQ_Menus_Plugin {
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/specials-grid.php';
require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-loop.php';
+ // Modular component widgets (v2.0)
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/discount-ribbon.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/strain-badge.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/thc-meter.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/effects-display.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/price-block.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/cart-button.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/stock-indicator.php';
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-image-overlay.php';
+
+ // Card templates (v2.0)
+ require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-premium.php';
+
+ // Register legacy widgets
$widgets_manager->register(new \CannaIQ_Menus_Product_Grid_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Single_Product_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Brand_Grid_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Category_List_Widget());
$widgets_manager->register(new \CannaIQ_Menus_Specials_Grid_Widget());
$widgets_manager->register(new \CannaIQ_Product_Loop_Widget());
+
+ // Register modular component widgets (v2.0)
+ $widgets_manager->register(new \CannaIQ_Discount_Ribbon_Widget());
+ $widgets_manager->register(new \CannaIQ_Strain_Badge_Widget());
+ $widgets_manager->register(new \CannaIQ_THC_Meter_Widget());
+ $widgets_manager->register(new \CannaIQ_Effects_Display_Widget());
+ $widgets_manager->register(new \CannaIQ_Price_Block_Widget());
+ $widgets_manager->register(new \CannaIQ_Cart_Button_Widget());
+ $widgets_manager->register(new \CannaIQ_Stock_Indicator_Widget());
+ $widgets_manager->register(new \CannaIQ_Product_Image_Overlay_Widget());
+
+ // Register card templates (v2.0)
+ $widgets_manager->register(new \CannaIQ_Premium_Card_Widget());
}
/**
* Enqueue Scripts and Styles
*/
public function enqueue_scripts() {
+ // Base styles
wp_enqueue_style(
'cannaiq-menus-styles',
CANNAIQ_MENUS_PLUGIN_URL . 'assets/css/cannaiq-menus.css',
@@ -104,6 +137,14 @@ class CannaIQ_Menus_Plugin {
CANNAIQ_MENUS_VERSION
);
+ // Component styles (v2.0 modular components)
+ wp_enqueue_style(
+ 'cannaiq-components-styles',
+ CANNAIQ_MENUS_PLUGIN_URL . 'assets/css/components.css',
+ ['cannaiq-menus-styles'],
+ CANNAIQ_MENUS_VERSION
+ );
+
wp_enqueue_script(
'cannaiq-menus-script',
CANNAIQ_MENUS_PLUGIN_URL . 'assets/js/cannaiq-menus.js',
@@ -118,8 +159,8 @@ class CannaIQ_Menus_Plugin {
*/
public function add_admin_menu() {
add_menu_page(
- 'CannaIQ Menus',
- 'CannaIQ Menus',
+ 'CannaiQ Menus',
+ 'CannaiQ Menus',
'manage_options',
'cannaiq-menus',
[$this, 'admin_page'],
@@ -147,9 +188,9 @@ class CannaIQ_Menus_Plugin {
public function admin_page() {
?>
-
CannaIQ Menus Settings
-
Version by CannaIQ
-
Display real-time cannabis menus with data updated daily from CannaIQ.
+
CannaiQ Menus Settings
+
Version by CannaiQ
+
Display real-time cannabis menus with data updated daily from CannaiQ.
16,
+ 'class' => '',
+ 'color' => 'currentColor',
+ ];
+ $args = wp_parse_args($args, $defaults);
+
+ $effect_key = strtolower(trim($effect));
+ $icons = cannaiq_get_effect_icons();
+
+ if (!isset($icons[$effect_key])) {
+ return '';
+ }
+
+ $svg = $icons[$effect_key];
+ $size = intval($args['size']);
+ $class = esc_attr($args['class']);
+ $color = esc_attr($args['color']);
+
+ // Replace placeholders in SVG
+ $svg = str_replace(
+ ['{SIZE}', '{CLASS}', '{COLOR}'],
+ [$size, $class, $color],
+ $svg
+ );
+
+ return $svg;
+}
+
+/**
+ * Get all effect icons
+ *
+ * @return array Associative array of effect => SVG
+ */
+function cannaiq_get_effect_icons() {
+ return [
+ 'happy' => '',
+
+ 'relaxed' => '',
+
+ 'sleepy' => '',
+
+ 'euphoric' => '',
+
+ 'creative' => '',
+
+ 'energetic' => '',
+
+ 'focused' => '',
+
+ 'hungry' => '',
+
+ 'uplifted' => '',
+
+ 'talkative' => '',
+
+ 'giggly' => '',
+
+ 'aroused' => '',
+
+ 'tingly' => '',
+
+ 'calm' => '',
+
+ 'sedated' => '',
+ ];
+}
+
+/**
+ * Get effect color
+ *
+ * @param string $effect Effect name
+ * @return string Hex color code
+ */
+function cannaiq_get_effect_color($effect) {
+ $colors = [
+ 'happy' => '#FFD700', // Gold
+ 'relaxed' => '#87CEEB', // Sky blue
+ 'sleepy' => '#9370DB', // Medium purple
+ 'euphoric' => '#FF69B4', // Hot pink
+ 'creative' => '#FF8C00', // Dark orange
+ 'energetic' => '#32CD32', // Lime green
+ 'focused' => '#4169E1', // Royal blue
+ 'hungry' => '#FF6347', // Tomato
+ 'uplifted' => '#00CED1', // Dark turquoise
+ 'talkative' => '#DDA0DD', // Plum
+ 'giggly' => '#FFB6C1', // Light pink
+ 'aroused' => '#DC143C', // Crimson
+ 'tingly' => '#8A2BE2', // Blue violet
+ 'calm' => '#98FB98', // Pale green
+ 'sedated' => '#708090', // Slate gray
+ ];
+
+ $key = strtolower(trim($effect));
+ return isset($colors[$key]) ? $colors[$key] : '#6B7280'; // Default gray
+}
+
+/**
+ * Render effect chip HTML
+ *
+ * @param string $effect Effect name
+ * @param array $args Optional args: show_icon, size, class
+ * @return string HTML for effect chip
+ */
+function cannaiq_render_effect_chip($effect, $args = []) {
+ $defaults = [
+ 'show_icon' => true,
+ 'size' => 'medium',
+ 'class' => '',
+ ];
+ $args = wp_parse_args($args, $defaults);
+
+ $effect_name = ucfirst(strtolower(trim($effect)));
+ $color = cannaiq_get_effect_color($effect);
+ $size_class = 'cannaiq-effect-chip--' . esc_attr($args['size']);
+ $extra_class = esc_attr($args['class']);
+
+ $icon_html = '';
+ if ($args['show_icon']) {
+ $icon_html = cannaiq_get_effect_icon($effect, [
+ 'size' => $args['size'] === 'small' ? 12 : ($args['size'] === 'large' ? 20 : 16),
+ 'color' => $color,
+ ]);
+ }
+
+ return sprintf(
+ '%s%s',
+ $size_class,
+ $extra_class,
+ esc_attr($color),
+ $icon_html,
+ esc_html($effect_name)
+ );
+}
+
+/**
+ * Render multiple effect chips
+ *
+ * @param array $effects Array of effect names
+ * @param array $args Optional args: limit, show_icon, size
+ * @return string HTML for all effect chips
+ */
+function cannaiq_render_effects($effects, $args = []) {
+ $defaults = [
+ 'limit' => 3,
+ 'show_icon' => true,
+ 'size' => 'medium',
+ 'class' => '',
+ ];
+ $args = wp_parse_args($args, $defaults);
+
+ if (!is_array($effects)) {
+ return '';
+ }
+
+ $effects = array_slice($effects, 0, intval($args['limit']));
+ $chips = array_map(function($effect) use ($args) {
+ return cannaiq_render_effect_chip($effect, [
+ 'show_icon' => $args['show_icon'],
+ 'size' => $args['size'],
+ ]);
+ }, $effects);
+
+ return sprintf(
+ '%s
',
+ esc_attr($args['class']),
+ implode('', $chips)
+ );
+}
diff --git a/wordpress-plugin/widgets/brand-grid.php b/wordpress-plugin/widgets/brand-grid.php
index d16dbe74..9acc722f 100644
--- a/wordpress-plugin/widgets/brand-grid.php
+++ b/wordpress-plugin/widgets/brand-grid.php
@@ -14,7 +14,7 @@ class CannaIQ_Menus_Brand_Grid_Widget extends \Elementor\Widget_Base {
}
public function get_title() {
- return __('CannaIQ Brand Grid', 'cannaiq-menus');
+ return __('CannaiQ Brand Grid', 'cannaiq-menus');
}
public function get_icon() {
diff --git a/wordpress-plugin/widgets/card-template-premium.php b/wordpress-plugin/widgets/card-template-premium.php
new file mode 100644
index 00000000..63e2881e
--- /dev/null
+++ b/wordpress-plugin/widgets/card-template-premium.php
@@ -0,0 +1,510 @@
+start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'content_note',
+ [
+ 'type' => \Elementor\Controls_Manager::RAW_HTML,
+ 'raw' => __('This card uses the current product context from Product Loop or Product Grid. Place it inside a CannaiQ Product Loop widget.', 'cannaiq-menus'),
+ 'content_classes' => 'elementor-panel-alert elementor-panel-alert-info',
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Components Section
+ $this->start_controls_section(
+ 'components_section',
+ [
+ 'label' => __('Components', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'show_discount',
+ [
+ 'label' => __('Show Discount Ribbon', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_image',
+ [
+ 'label' => __('Show Product Image', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_strain_badge',
+ [
+ 'label' => __('Show Strain Badge', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_thc_badge',
+ [
+ 'label' => __('Show THC Badge', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_name',
+ [
+ 'label' => __('Show Product Name', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_brand',
+ [
+ 'label' => __('Show Brand', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_effects',
+ [
+ 'label' => __('Show Effects', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'effects_limit',
+ [
+ 'label' => __('Effects Limit', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 3,
+ 'min' => 1,
+ 'max' => 5,
+ 'condition' => [
+ 'show_effects' => 'yes',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'show_price',
+ [
+ 'label' => __('Show Price', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_weight',
+ [
+ 'label' => __('Show Weight', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_cart_button',
+ [
+ 'label' => __('Show Cart Button', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'button_text',
+ [
+ 'label' => __('Button Text', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => 'ADD TO CART',
+ 'condition' => [
+ 'show_cart_button' => 'yes',
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'card_style_section',
+ [
+ 'label' => __('Card Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'card_background',
+ [
+ 'label' => __('Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#ffffff',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-product-card' => 'background-color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'card_border_radius',
+ [
+ 'label' => __('Border Radius', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SLIDER,
+ 'size_units' => ['px'],
+ 'range' => [
+ 'px' => [
+ 'min' => 0,
+ 'max' => 30,
+ ],
+ ],
+ 'default' => [
+ 'size' => 12,
+ ],
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-product-card' => 'border-radius: {{SIZE}}{{UNIT}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'card_padding',
+ [
+ 'label' => __('Padding', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SLIDER,
+ 'size_units' => ['px'],
+ 'range' => [
+ 'px' => [
+ 'min' => 0,
+ 'max' => 40,
+ ],
+ ],
+ 'default' => [
+ 'size' => 16,
+ ],
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-product-card__body' => 'padding: {{SIZE}}{{UNIT}};',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Box_Shadow::get_type(),
+ [
+ 'name' => 'card_shadow',
+ 'selector' => '{{WRAPPER}} .cannaiq-product-card',
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Typography Section
+ $this->start_controls_section(
+ 'typography_section',
+ [
+ 'label' => __('Typography', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'title_typography',
+ 'label' => __('Title', 'cannaiq-menus'),
+ 'selector' => '{{WRAPPER}} .cannaiq-product-card__title',
+ ]
+ );
+
+ $this->add_control(
+ 'title_color',
+ [
+ 'label' => __('Title Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#1f2937',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-product-card__title' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'brand_color',
+ [
+ 'label' => __('Brand Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#6b7280',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-product-card__brand' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Button Style Section
+ $this->start_controls_section(
+ 'button_style_section',
+ [
+ 'label' => __('Button Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ 'condition' => [
+ 'show_cart_button' => 'yes',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'button_background',
+ [
+ 'label' => __('Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#1f2937',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button' => 'background-color: {{VALUE}}; border-color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'button_text_color',
+ [
+ 'label' => __('Text Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#ffffff',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'button_hover_background',
+ [
+ 'label' => __('Hover Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#374151',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button:hover' => 'background-color: {{VALUE}}; border-color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ global $cannaiq_current_product;
+ $product = $cannaiq_current_product ?? [];
+
+ if (empty($product)) {
+ echo '' . __('No product context available. Place this widget inside a Product Loop.', 'cannaiq-menus') . '
';
+ return;
+ }
+
+ // Extract product data
+ $name = $product['Name'] ?? $product['name'] ?? '';
+ $brand = $product['brand']['name'] ?? $product['brandName'] ?? $product['brand'] ?? '';
+ $image_url = $product['Image'] ?? $product['images'][0]['url'] ?? $product['image_url'] ?? '';
+ $strain_type = strtolower($product['strainType'] ?? $product['strain_type'] ?? '');
+ $thc = $product['THCContent']['range'][0] ?? $product['THC'] ?? $product['thc_percentage'] ?? null;
+ $weight = $product['Options'][0] ?? $product['rawOptions'][0] ?? $product['weight'] ?? '';
+ $menu_url = $product['menuUrl'] ?? $product['menu_url'] ?? $product['productUrl'] ?? '#';
+
+ // Price
+ $original_price = $product['Prices'][0] ?? $product['regular_price'] ?? null;
+ $sale_price = $product['specialPrice'] ?? $product['sale_price'] ?? null;
+ $is_on_sale = $sale_price && $sale_price > 0 && $sale_price < $original_price;
+ $discount_percent = 0;
+ if ($is_on_sale && $original_price > 0) {
+ $discount_percent = round((($original_price - $sale_price) / $original_price) * 100);
+ }
+
+ // Effects
+ $effects = $product['effects'] ?? [];
+ if (!empty($effects) && !isset($effects[0])) {
+ arsort($effects);
+ $effects = array_keys($effects);
+ }
+ $effects_limit = intval($settings['effects_limit']) ?: 3;
+ $effects = array_slice($effects, 0, $effects_limit);
+
+ // Strain colors
+ $strain_colors = [
+ 'sativa' => '#22c55e',
+ 'indica' => '#8b5cf6',
+ 'hybrid' => '#f97316',
+ ];
+
+ ?>
+
+
+
+
+
+
+
; ?>)
+
+
+
+ 0): ?>
+
+ % OFF
+
+
+
+
+ 0)): ?>
+
+
+
+
+
+ 0): ?>
+ % THC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
by
+
+
+
+
+ $effects_limit, 'show_icon' => true, 'size' => 'small']); ?>
+
+
+
+
+
+
+
+ start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'button_text',
+ [
+ 'label' => __('Button Text', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => 'ADD TO CART',
+ ]
+ );
+
+ $this->add_control(
+ 'link_source',
+ [
+ 'label' => __('Link Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom URL', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_url',
+ [
+ 'label' => __('Custom URL', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::URL,
+ 'placeholder' => 'https://dutchie.com/store/...',
+ 'condition' => [
+ 'link_source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'open_in_new_tab',
+ [
+ 'label' => __('Open in New Tab', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_icon',
+ [
+ 'label' => __('Show Icon', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => '',
+ ]
+ );
+
+ $this->add_control(
+ 'icon_position',
+ [
+ 'label' => __('Icon Position', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'after',
+ 'options' => [
+ 'before' => __('Before Text', 'cannaiq-menus'),
+ 'after' => __('After Text', 'cannaiq-menus'),
+ ],
+ 'condition' => [
+ 'show_icon' => 'yes',
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'button_style',
+ [
+ 'label' => __('Button Style', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'solid',
+ 'options' => [
+ 'solid' => __('Solid', 'cannaiq-menus'),
+ 'outline' => __('Outline', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'full_width',
+ [
+ 'label' => __('Full Width', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => '',
+ ]
+ );
+
+ $this->add_control(
+ 'size',
+ [
+ 'label' => __('Size', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'medium',
+ 'options' => [
+ 'small' => __('Small', 'cannaiq-menus'),
+ 'medium' => __('Medium', 'cannaiq-menus'),
+ 'large' => __('Large', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'background_color',
+ [
+ 'label' => __('Background Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#1f2937',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button--solid' => 'background-color: {{VALUE}}; border-color: {{VALUE}};',
+ '{{WRAPPER}} .cannaiq-cart-button--outline' => 'border-color: {{VALUE}};',
+ '{{WRAPPER}} .cannaiq-cart-button--outline:hover' => 'background-color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'text_color',
+ [
+ 'label' => __('Text Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#ffffff',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button--solid' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'outline_text_color',
+ [
+ 'label' => __('Outline Text Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#1f2937',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button--outline' => 'color: {{VALUE}};',
+ ],
+ 'condition' => [
+ 'button_style' => 'outline',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'hover_background_color',
+ [
+ 'label' => __('Hover Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#374151',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button--solid:hover' => 'background-color: {{VALUE}}; border-color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'border_radius',
+ [
+ 'label' => __('Border Radius', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SLIDER,
+ 'size_units' => ['px'],
+ 'range' => [
+ 'px' => [
+ 'min' => 0,
+ 'max' => 50,
+ ],
+ ],
+ 'default' => [
+ 'size' => 6,
+ ],
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-cart-button' => 'border-radius: {{SIZE}}{{UNIT}};',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'typography',
+ 'selector' => '{{WRAPPER}} .cannaiq-cart-button',
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ // Get URL
+ $url = '#';
+ if ($settings['link_source'] === 'custom' && !empty($settings['custom_url']['url'])) {
+ $url = $settings['custom_url']['url'];
+ } else {
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $url = $cannaiq_current_product['menuUrl']
+ ?? $cannaiq_current_product['menu_url']
+ ?? $cannaiq_current_product['productUrl']
+ ?? '#';
+ }
+ }
+
+ // Build classes
+ $classes = [
+ 'cannaiq-cart-button',
+ 'cannaiq-cart-button--' . $settings['button_style'],
+ ];
+ if ($settings['full_width'] === 'yes') {
+ $classes[] = 'cannaiq-cart-button--full';
+ }
+ if ($settings['size'] !== 'medium') {
+ $classes[] = 'cannaiq-cart-button--' . $settings['size'];
+ }
+
+ // Target attribute
+ $target = $settings['open_in_new_tab'] === 'yes' ? ' target="_blank" rel="noopener noreferrer"' : '';
+
+ // Icon SVG (arrow right)
+ $icon = '';
+
+ ?>
+ >
+
+
+
+
+
+
+
+
+ start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'source',
+ [
+ 'label' => __('Discount Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom value', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_discount',
+ [
+ 'label' => __('Discount Percentage', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 25,
+ 'min' => 1,
+ 'max' => 99,
+ 'condition' => [
+ 'source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'format',
+ [
+ 'label' => __('Display Format', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'ribbon',
+ 'options' => [
+ 'ribbon' => __('Ribbon', 'cannaiq-menus'),
+ 'pill' => __('Pill', 'cannaiq-menus'),
+ 'text' => __('Text Only', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'text_template',
+ [
+ 'label' => __('Text Template', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => '{percent}% OFF',
+ 'description' => __('Use {percent} as placeholder', 'cannaiq-menus'),
+ ]
+ );
+
+ $this->add_control(
+ 'hide_if_no_discount',
+ [
+ 'label' => __('Hide if No Discount', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'size',
+ [
+ 'label' => __('Size', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'medium',
+ 'options' => [
+ 'small' => __('Small', 'cannaiq-menus'),
+ 'medium' => __('Medium', 'cannaiq-menus'),
+ 'large' => __('Large', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'background_color',
+ [
+ 'label' => __('Background Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#ef4444',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-discount-ribbon' => 'background-color: {{VALUE}};',
+ ],
+ 'condition' => [
+ 'format!' => 'text',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'text_color',
+ [
+ 'label' => __('Text Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#ffffff',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-discount-ribbon' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'typography',
+ 'selector' => '{{WRAPPER}} .cannaiq-discount-ribbon',
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ // Get discount percentage
+ $discount = 0;
+ if ($settings['source'] === 'custom') {
+ $discount = intval($settings['custom_discount']);
+ } else {
+ // Get from product context
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $original = $cannaiq_current_product['Prices'][0] ?? $cannaiq_current_product['regular_price'] ?? null;
+ $sale = $cannaiq_current_product['specialPrice'] ?? $cannaiq_current_product['sale_price'] ?? null;
+
+ if ($original && $sale && $original > $sale) {
+ $discount = round((($original - $sale) / $original) * 100);
+ }
+ }
+ }
+
+ // Hide if no discount and setting enabled
+ if ($discount <= 0 && $settings['hide_if_no_discount'] === 'yes') {
+ return;
+ }
+
+ // Build display text
+ $text = str_replace('{percent}', $discount, $settings['text_template']);
+
+ // Build classes
+ $classes = [
+ 'cannaiq-discount-ribbon',
+ 'cannaiq-discount-ribbon--' . $settings['format'],
+ ];
+ if ($settings['size'] !== 'medium') {
+ $classes[] = 'cannaiq-discount-ribbon--' . $settings['size'];
+ }
+
+ printf(
+ '%s',
+ esc_attr(implode(' ', $classes)),
+ esc_html($text)
+ );
+ }
+}
diff --git a/wordpress-plugin/widgets/dynamic-tags-extended.php b/wordpress-plugin/widgets/dynamic-tags-extended.php
new file mode 100644
index 00000000..f30b7d37
--- /dev/null
+++ b/wordpress-plugin/widgets/dynamic-tags-extended.php
@@ -0,0 +1,793 @@
+register(new CannaIQ_Discount_Percent_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Discount_Badge_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Strain_Badge_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_THC_Badge_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_CBD_Badge_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Effects_Chips_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Single_Effect_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Terpenes_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Price_Display_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Sale_Price_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Original_Price_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Menu_URL_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Subcategory_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Stock_Quantity_Tag());
+ $dynamic_tags_manager->register(new CannaIQ_Stock_Status_Tag());
+}, 20); // Priority 20 to run after base tags
+
+/**
+ * Discount Percentage Tag
+ */
+class CannaIQ_Discount_Percent_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-discount-percent';
+ }
+
+ public function get_title() {
+ return __('Discount %', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('format', [
+ 'label' => __('Format', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'off',
+ 'options' => [
+ 'off' => 'XX% OFF',
+ 'percent' => 'XX%',
+ 'number' => 'XX',
+ ],
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $format = $this->get_settings('format');
+
+ $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);
+
+ switch ($format) {
+ case 'off':
+ echo esc_html($percent . '% OFF');
+ break;
+ case 'percent':
+ echo esc_html($percent . '%');
+ break;
+ case 'number':
+ echo esc_html($percent);
+ break;
+ }
+ }
+}
+
+/**
+ * Discount Badge Tag (HTML)
+ */
+class CannaIQ_Discount_Badge_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-discount-badge';
+ }
+
+ public function get_title() {
+ return __('Discount Badge', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('style', [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'ribbon',
+ 'options' => [
+ 'ribbon' => 'Ribbon',
+ 'pill' => 'Pill',
+ ],
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $style = $this->get_settings('style');
+
+ $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--' . $style;
+
+ printf(
+ '%s%% OFF',
+ esc_attr($class),
+ esc_html($percent)
+ );
+ }
+}
+
+/**
+ * Strain Badge Tag (HTML)
+ */
+class CannaIQ_Strain_Badge_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-strain-badge';
+ }
+
+ public function get_title() {
+ return __('Strain Badge', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('style', [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'pill',
+ 'options' => [
+ 'pill' => 'Pill',
+ 'text' => 'Text Only',
+ ],
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $style = $this->get_settings('style');
+
+ $strain = strtolower($product['strainType'] ?? $product['strain_type'] ?? '');
+ if (empty($strain) || !in_array($strain, ['sativa', 'indica', 'hybrid'])) {
+ return;
+ }
+
+ $colors = [
+ 'sativa' => '#22c55e',
+ 'indica' => '#8b5cf6',
+ 'hybrid' => '#f97316',
+ ];
+ $color = $colors[$strain];
+
+ $class = 'cannaiq-strain-badge cannaiq-strain-badge--' . $style . ' cannaiq-strain-badge--' . $strain;
+ $css_style = $style === 'pill'
+ ? sprintf('background-color: %s; color: white;', $color)
+ : sprintf('color: %s;', $color);
+
+ printf(
+ '%s',
+ esc_attr($class),
+ esc_attr($css_style),
+ esc_html(strtoupper($strain))
+ );
+ }
+}
+
+/**
+ * THC Badge Tag (HTML)
+ */
+class CannaIQ_THC_Badge_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-thc-badge';
+ }
+
+ public function get_title() {
+ return __('THC Badge', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('style', [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'badge',
+ 'options' => [
+ 'badge' => 'Badge',
+ 'pill' => 'Pill',
+ 'text' => 'Text Only',
+ ],
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $style = $this->get_settings('style');
+
+ $thc = $product['THCContent']['range'][0]
+ ?? $product['THC']
+ ?? $product['thc_percentage']
+ ?? null;
+
+ if (!$thc || $thc <= 0) {
+ return;
+ }
+
+ $class = 'cannaiq-potency-badge cannaiq-potency-badge--' . $style;
+ $formatted = number_format((float)$thc, 1) . '% THC';
+
+ printf(
+ '%s',
+ esc_attr($class),
+ esc_html($formatted)
+ );
+ }
+}
+
+/**
+ * CBD Badge Tag (HTML)
+ */
+class CannaIQ_CBD_Badge_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-cbd-badge';
+ }
+
+ public function get_title() {
+ return __('CBD Badge', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('style', [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'badge',
+ 'options' => [
+ 'badge' => 'Badge',
+ 'pill' => 'Pill',
+ 'text' => 'Text Only',
+ ],
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $style = $this->get_settings('style');
+
+ $cbd = $product['CBDContent']['range'][0]
+ ?? $product['CBD']
+ ?? $product['cbd_percentage']
+ ?? null;
+
+ if (!$cbd || $cbd <= 0) {
+ return;
+ }
+
+ $class = 'cannaiq-potency-badge cannaiq-potency-badge--' . $style;
+ $formatted = number_format((float)$cbd, 1) . '% CBD';
+
+ printf(
+ '%s',
+ esc_attr($class),
+ esc_html($formatted)
+ );
+ }
+}
+
+/**
+ * Effects Chips Tag (HTML)
+ */
+class CannaIQ_Effects_Chips_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-effects-chips';
+ }
+
+ public function get_title() {
+ return __('Effects Chips', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('limit', [
+ 'label' => __('Max Effects', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 3,
+ 'min' => 1,
+ 'max' => 10,
+ ]);
+
+ $this->add_control('show_icons', [
+ 'label' => __('Show Icons', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $limit = (int)$this->get_settings('limit') ?: 3;
+ $show_icons = $this->get_settings('show_icons') === 'yes';
+
+ $effects = $product['effects'] ?? [];
+ if (empty($effects) || !is_array($effects)) {
+ return;
+ }
+
+ // If associative array with scores, sort by score
+ if (!isset($effects[0])) {
+ arsort($effects);
+ $effects = array_keys($effects);
+ }
+
+ $effects = array_slice($effects, 0, $limit);
+
+ echo cannaiq_render_effects($effects, [
+ 'limit' => $limit,
+ 'show_icon' => $show_icons,
+ 'size' => 'medium',
+ ]);
+ }
+}
+
+/**
+ * Single Effect Tag
+ */
+class CannaIQ_Single_Effect_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-single-effect';
+ }
+
+ public function get_title() {
+ return __('Single Effect', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('effect_index', [
+ 'label' => __('Effect Index', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 1,
+ 'min' => 1,
+ 'max' => 10,
+ 'description' => __('1 = first effect, 2 = second, etc.', 'cannaiq-menus'),
+ ]);
+
+ $this->add_control('show_icon', [
+ 'label' => __('Show Icon', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $index = (int)$this->get_settings('effect_index') - 1; // Convert to 0-based
+ $show_icon = $this->get_settings('show_icon') === 'yes';
+
+ $effects = $product['effects'] ?? [];
+ if (empty($effects) || !is_array($effects)) {
+ return;
+ }
+
+ // If associative array with scores, sort by score and get keys
+ if (!isset($effects[0])) {
+ arsort($effects);
+ $effects = array_keys($effects);
+ }
+
+ if (!isset($effects[$index])) {
+ return;
+ }
+
+ $effect = $effects[$index];
+ echo cannaiq_render_effect_chip($effect, [
+ 'show_icon' => $show_icon,
+ 'size' => 'medium',
+ ]);
+ }
+}
+
+/**
+ * Terpenes Tag (HTML)
+ */
+class CannaIQ_Terpenes_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-terpenes';
+ }
+
+ public function get_title() {
+ return __('Terpenes', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('format', [
+ 'label' => __('Format', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'chips',
+ 'options' => [
+ 'chips' => 'Chips',
+ 'list' => 'List',
+ 'text' => 'Text',
+ ],
+ ]);
+
+ $this->add_control('limit', [
+ 'label' => __('Max Terpenes', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 3,
+ 'min' => 1,
+ 'max' => 10,
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $format = $this->get_settings('format');
+ $limit = (int)$this->get_settings('limit') ?: 3;
+
+ $terpenes = $product['terpenes'] ?? [];
+ if (empty($terpenes) || !is_array($terpenes)) {
+ return;
+ }
+
+ $terpenes = array_slice($terpenes, 0, $limit);
+
+ switch ($format) {
+ case '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 '
';
+ break;
+
+ case 'list':
+ 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 '
';
+ break;
+
+ case '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));
+ break;
+ }
+ }
+}
+
+/**
+ * Price Display Tag (with sale handling)
+ */
+class CannaIQ_Price_Display_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-price-display';
+ }
+
+ public function get_title() {
+ return __('Price Display', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('show_original', [
+ 'label' => __('Show Original on Sale', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $show_original = $this->get_settings('show_original') === 'yes';
+
+ $original = $product['Prices'][0] ?? $product['regular_price'] ?? null;
+ $sale = $product['specialPrice'] ?? $product['sale_price'] ?? null;
+
+ if (!$original || $original <= 0) {
+ return;
+ }
+
+ $is_on_sale = $sale && $sale > 0 && $sale < $original;
+
+ echo '';
+ if ($is_on_sale) {
+ if ($show_original) {
+ printf(
+ '$%s',
+ esc_html(number_format((float)$original, 2))
+ );
+ }
+ printf(
+ '$%s',
+ esc_html(number_format((float)$sale, 2))
+ );
+ } else {
+ printf(
+ '$%s',
+ esc_html(number_format((float)$original, 2))
+ );
+ }
+ echo '';
+ }
+}
+
+/**
+ * Sale Price Tag
+ */
+class CannaIQ_Sale_Price_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-price-sale';
+ }
+
+ public function get_title() {
+ return __('Sale Price', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+
+ $sale = $product['specialPrice'] ?? $product['sale_price'] ?? null;
+
+ if ($sale && $sale > 0) {
+ echo '$' . number_format((float)$sale, 2);
+ }
+ }
+}
+
+/**
+ * Original Price Tag
+ */
+class CannaIQ_Original_Price_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-price-original';
+ }
+
+ public function get_title() {
+ return __('Original Price', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+
+ $original = $product['Prices'][0] ?? $product['regular_price'] ?? null;
+
+ if ($original && $original > 0) {
+ echo '$' . number_format((float)$original, 2);
+ }
+ }
+}
+
+/**
+ * Menu URL Tag
+ */
+class CannaIQ_Menu_URL_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-menu-url';
+ }
+
+ public function get_title() {
+ return __('Menu URL', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::URL_CATEGORY];
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+
+ $url = $product['menuUrl']
+ ?? $product['menu_url']
+ ?? $product['productUrl']
+ ?? '';
+
+ echo esc_url($url);
+ }
+}
+
+/**
+ * Subcategory Tag
+ */
+class CannaIQ_Subcategory_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-subcategory';
+ }
+
+ public function get_title() {
+ return __('Subcategory', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+
+ $subcategory = $product['subcategory']
+ ?? $product['subCategory']
+ ?? '';
+
+ echo esc_html($subcategory);
+ }
+}
+
+/**
+ * Stock Quantity Tag
+ */
+class CannaIQ_Stock_Quantity_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-stock-qty';
+ }
+
+ public function get_title() {
+ return __('Stock Quantity', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+
+ $qty = $product['POSMetaData']['children'][0]['quantity']
+ ?? $product['quantity']
+ ?? null;
+
+ if ($qty !== null) {
+ echo (int)$qty;
+ }
+ }
+}
+
+/**
+ * Stock Status Tag (HTML badge)
+ */
+class CannaIQ_Stock_Status_Tag extends CannaIQ_Dynamic_Tag_Base {
+
+ public function get_name() {
+ return 'cannaiq-stock-status';
+ }
+
+ public function get_title() {
+ return __('Stock Status Badge', 'cannaiq-menus');
+ }
+
+ public function get_categories() {
+ return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY];
+ }
+
+ protected function register_controls() {
+ $this->add_control('style', [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'badge',
+ 'options' => [
+ 'badge' => 'Badge',
+ 'text' => 'Text',
+ 'dot' => 'Dot + Text',
+ ],
+ ]);
+ }
+
+ public function render() {
+ $product = $this->get_current_product();
+ $style = $this->get_settings('style');
+
+ $status = $product['Status'] ?? '';
+ $in_stock = ($status === 'Active' || $status === 'In Stock' || !empty($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 ($style === 'badge') {
+ $class .= ' cannaiq-stock-indicator--badge';
+ }
+
+ printf('', esc_attr($class));
+
+ if ($style === 'dot') {
+ echo '';
+ }
+
+ echo esc_html($text);
+ echo '';
+ }
+}
diff --git a/wordpress-plugin/widgets/dynamic-tags.php b/wordpress-plugin/widgets/dynamic-tags.php
index 305f2680..641de373 100644
--- a/wordpress-plugin/widgets/dynamic-tags.php
+++ b/wordpress-plugin/widgets/dynamic-tags.php
@@ -17,7 +17,7 @@ add_action('elementor/dynamic_tags/register', function($dynamic_tags_manager) {
// Register CannaIQ group
$dynamic_tags_manager->register_group('cannaiq', [
- 'title' => __('CannaIQ Product', 'cannaiq-menus')
+ 'title' => __('CannaiQ Product', 'cannaiq-menus')
]);
// Register all tags
diff --git a/wordpress-plugin/widgets/effects-display.php b/wordpress-plugin/widgets/effects-display.php
new file mode 100644
index 00000000..781d9f30
--- /dev/null
+++ b/wordpress-plugin/widgets/effects-display.php
@@ -0,0 +1,288 @@
+start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'source',
+ [
+ 'label' => __('Effects Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom values', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_effects',
+ [
+ 'label' => __('Custom Effects', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => 'Happy, Relaxed, Creative',
+ 'description' => __('Comma-separated list of effects', 'cannaiq-menus'),
+ 'condition' => [
+ 'source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'limit',
+ [
+ 'label' => __('Max Effects', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 3,
+ 'min' => 1,
+ 'max' => 10,
+ ]
+ );
+
+ $this->add_control(
+ 'show_icons',
+ [
+ 'label' => __('Show Icons', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'use_colors',
+ [
+ 'label' => __('Colored Chips', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ 'description' => __('Use effect-specific colors', 'cannaiq-menus'),
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'size',
+ [
+ 'label' => __('Size', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'medium',
+ 'options' => [
+ 'small' => __('Small', 'cannaiq-menus'),
+ 'medium' => __('Medium', 'cannaiq-menus'),
+ 'large' => __('Large', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'gap',
+ [
+ 'label' => __('Gap', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SLIDER,
+ 'size_units' => ['px'],
+ 'range' => [
+ 'px' => [
+ 'min' => 2,
+ 'max' => 20,
+ ],
+ ],
+ 'default' => [
+ 'size' => 8,
+ ],
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-effects-container' => 'gap: {{SIZE}}{{UNIT}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'default_background',
+ [
+ 'label' => __('Default Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#f3f4f6',
+ 'condition' => [
+ 'use_colors!' => 'yes',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'text_color',
+ [
+ 'label' => __('Text Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#1f2937',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-effect-chip' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'typography',
+ 'selector' => '{{WRAPPER}} .cannaiq-effect-chip',
+ ]
+ );
+
+ $this->add_control(
+ 'border_radius',
+ [
+ 'label' => __('Border Radius', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SLIDER,
+ 'size_units' => ['px'],
+ 'range' => [
+ 'px' => [
+ 'min' => 0,
+ 'max' => 50,
+ ],
+ ],
+ 'default' => [
+ 'size' => 999,
+ ],
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-effect-chip' => 'border-radius: {{SIZE}}{{UNIT}};',
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ // Get effects
+ $effects = [];
+ if ($settings['source'] === 'custom') {
+ $effects_string = $settings['custom_effects'];
+ $effects = array_map('trim', explode(',', $effects_string));
+ } else {
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $raw_effects = $cannaiq_current_product['effects'] ?? [];
+ if (is_array($raw_effects)) {
+ // If effects is associative array with scores, sort by score
+ if (isset($raw_effects[0]) && !is_array($raw_effects[0])) {
+ $effects = $raw_effects;
+ } else {
+ // Sort by value descending and get keys
+ arsort($raw_effects);
+ $effects = array_keys($raw_effects);
+ }
+ }
+ }
+ }
+
+ if (empty($effects)) {
+ return;
+ }
+
+ // Apply limit
+ $limit = intval($settings['limit']);
+ $effects = array_slice($effects, 0, $limit);
+
+ // Determine icon size
+ $icon_size = $settings['size'] === 'small' ? 12 : ($settings['size'] === 'large' ? 20 : 16);
+ ?>
+
+
+
+
+
+ $icon_size,
+ 'color' => $settings['use_colors'] === 'yes' ? $color : 'currentColor',
+ ]); ?>
+
+
+
+
+
+ start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'source',
+ [
+ 'label' => __('Price Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom values', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_price',
+ [
+ 'label' => __('Regular Price', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 45,
+ 'min' => 0,
+ 'step' => 0.01,
+ 'condition' => [
+ 'source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_sale_price',
+ [
+ 'label' => __('Sale Price (optional)', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => '',
+ 'min' => 0,
+ 'step' => 0.01,
+ 'condition' => [
+ 'source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'show_original_when_sale',
+ [
+ 'label' => __('Show Original Price on Sale', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'show_weight',
+ [
+ 'label' => __('Show Weight', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'custom_weight',
+ [
+ 'label' => __('Weight Text', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => '1/8 oz',
+ 'condition' => [
+ 'source' => 'custom',
+ 'show_weight' => 'yes',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'layout',
+ [
+ 'label' => __('Layout', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'inline',
+ 'options' => [
+ 'inline' => __('Inline', 'cannaiq-menus'),
+ 'stacked' => __('Stacked', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'currency_symbol',
+ [
+ 'label' => __('Currency Symbol', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => '$',
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'size',
+ [
+ 'label' => __('Size', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'medium',
+ 'options' => [
+ 'small' => __('Small', 'cannaiq-menus'),
+ 'medium' => __('Medium', 'cannaiq-menus'),
+ 'large' => __('Large', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'price_color',
+ [
+ 'label' => __('Price Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#1f2937',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-price-block__regular' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'sale_color',
+ [
+ 'label' => __('Sale Price Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#dc2626',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-price-block__sale' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'original_color',
+ [
+ 'label' => __('Original Price Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#9ca3af',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-price-block__original' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'weight_color',
+ [
+ 'label' => __('Weight Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#9ca3af',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-price-block__weight' => 'color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'price_typography',
+ 'label' => __('Price Typography', 'cannaiq-menus'),
+ 'selector' => '{{WRAPPER}} .cannaiq-price-block__sale, {{WRAPPER}} .cannaiq-price-block__regular',
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ // Get price values
+ $regular_price = 0;
+ $sale_price = null;
+ $weight = '';
+
+ if ($settings['source'] === 'custom') {
+ $regular_price = floatval($settings['custom_price']);
+ $sale_price = !empty($settings['custom_sale_price']) ? floatval($settings['custom_sale_price']) : null;
+ $weight = $settings['custom_weight'];
+ } else {
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $regular_price = $cannaiq_current_product['Prices'][0]
+ ?? $cannaiq_current_product['recPrices'][0]
+ ?? $cannaiq_current_product['regular_price']
+ ?? 0;
+
+ $sale_price = $cannaiq_current_product['specialPrice']
+ ?? $cannaiq_current_product['sale_price']
+ ?? null;
+
+ // Check POSMetaData for prices
+ if (isset($cannaiq_current_product['POSMetaData']['children'][0])) {
+ $child = $cannaiq_current_product['POSMetaData']['children'][0];
+ if (isset($child['price'])) {
+ $regular_price = $child['price'];
+ }
+ if (isset($child['specialPrice']) && $child['specialPrice'] > 0) {
+ $sale_price = $child['specialPrice'];
+ }
+ }
+
+ $weight = $cannaiq_current_product['Options'][0]
+ ?? $cannaiq_current_product['rawOptions'][0]
+ ?? $cannaiq_current_product['weight']
+ ?? '';
+ }
+ }
+
+ $regular_price = floatval($regular_price);
+ if ($regular_price <= 0) {
+ return;
+ }
+
+ // Determine if on sale
+ $is_on_sale = $sale_price !== null && floatval($sale_price) > 0 && floatval($sale_price) < $regular_price;
+
+ // Build classes
+ $classes = ['cannaiq-price-block'];
+ if ($settings['layout'] === 'stacked') {
+ $classes[] = 'cannaiq-price-block--stacked';
+ }
+ if ($settings['size'] !== 'medium') {
+ $classes[] = 'cannaiq-price-block--' . $settings['size'];
+ }
+
+ $currency = $settings['currency_symbol'];
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Image', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'image_source',
+ [
+ 'label' => __('Image Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom image', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_image',
+ [
+ 'label' => __('Choose Image', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::MEDIA,
+ 'default' => [
+ 'url' => \Elementor\Utils::get_placeholder_image_src(),
+ ],
+ 'condition' => [
+ 'image_source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'fallback_image',
+ [
+ 'label' => __('Fallback Image', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::MEDIA,
+ 'description' => __('Shown if product has no image', 'cannaiq-menus'),
+ ]
+ );
+
+ $this->add_control(
+ 'aspect_ratio',
+ [
+ 'label' => __('Aspect Ratio', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => '1/1',
+ 'options' => [
+ '1/1' => __('Square (1:1)', 'cannaiq-menus'),
+ '4/3' => __('4:3', 'cannaiq-menus'),
+ '3/4' => __('3:4', 'cannaiq-menus'),
+ '16/9' => __('16:9', 'cannaiq-menus'),
+ 'auto' => __('Auto', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'hover_effect',
+ [
+ 'label' => __('Hover Effect', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'zoom',
+ 'options' => [
+ 'none' => __('None', 'cannaiq-menus'),
+ 'zoom' => __('Zoom', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Overlay Badges Section
+ $this->start_controls_section(
+ 'overlays_section',
+ [
+ 'label' => __('Badge Overlays', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'show_discount_badge',
+ [
+ 'label' => __('Show Discount Badge', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'discount_position',
+ [
+ 'label' => __('Discount Position', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'top-left',
+ 'options' => [
+ 'top-left' => __('Top Left', 'cannaiq-menus'),
+ 'top-right' => __('Top Right', 'cannaiq-menus'),
+ 'bottom-left' => __('Bottom Left', 'cannaiq-menus'),
+ 'bottom-right' => __('Bottom Right', 'cannaiq-menus'),
+ ],
+ 'condition' => [
+ 'show_discount_badge' => 'yes',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'show_strain_badge',
+ [
+ 'label' => __('Show Strain Badge', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'strain_position',
+ [
+ 'label' => __('Strain Position', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'bottom-left',
+ 'options' => [
+ 'top-left' => __('Top Left', 'cannaiq-menus'),
+ 'top-right' => __('Top Right', 'cannaiq-menus'),
+ 'bottom-left' => __('Bottom Left', 'cannaiq-menus'),
+ 'bottom-right' => __('Bottom Right', 'cannaiq-menus'),
+ ],
+ 'condition' => [
+ 'show_strain_badge' => 'yes',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'show_thc_badge',
+ [
+ 'label' => __('Show THC Badge', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => '',
+ ]
+ );
+
+ $this->add_control(
+ 'thc_position',
+ [
+ 'label' => __('THC Position', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'bottom-right',
+ 'options' => [
+ 'top-left' => __('Top Left', 'cannaiq-menus'),
+ 'top-right' => __('Top Right', 'cannaiq-menus'),
+ 'bottom-left' => __('Bottom Left', 'cannaiq-menus'),
+ 'bottom-right' => __('Bottom Right', 'cannaiq-menus'),
+ ],
+ 'condition' => [
+ 'show_thc_badge' => 'yes',
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'border_radius',
+ [
+ 'label' => __('Border Radius', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SLIDER,
+ 'size_units' => ['px'],
+ 'range' => [
+ 'px' => [
+ 'min' => 0,
+ 'max' => 50,
+ ],
+ ],
+ 'default' => [
+ 'size' => 8,
+ ],
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-product-image' => 'border-radius: {{SIZE}}{{UNIT}};',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'background_color',
+ [
+ 'label' => __('Background Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#f9fafb',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-product-image' => 'background-color: {{VALUE}};',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Box_Shadow::get_type(),
+ [
+ 'name' => 'box_shadow',
+ 'selector' => '{{WRAPPER}} .cannaiq-product-image',
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ // Get image URL
+ $image_url = '';
+ if ($settings['image_source'] === 'custom') {
+ $image_url = $settings['custom_image']['url'] ?? '';
+ } else {
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $image_url = $cannaiq_current_product['Image']
+ ?? $cannaiq_current_product['images'][0]['url']
+ ?? $cannaiq_current_product['image_url']
+ ?? '';
+ }
+ }
+
+ // Use fallback if no image
+ if (empty($image_url) && !empty($settings['fallback_image']['url'])) {
+ $image_url = $settings['fallback_image']['url'];
+ }
+
+ // Get product name for alt text
+ $alt_text = '';
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $alt_text = $cannaiq_current_product['Name'] ?? $cannaiq_current_product['name'] ?? '';
+ }
+
+ // Aspect ratio style
+ $aspect_style = '';
+ if ($settings['aspect_ratio'] !== 'auto') {
+ $aspect_style = 'aspect-ratio: ' . $settings['aspect_ratio'] . ';';
+ }
+
+ // Hover class
+ $hover_class = $settings['hover_effect'] === 'zoom' ? 'cannaiq-product-image--hover-zoom' : '';
+
+ // Group badges by position
+ $badges_by_position = [];
+
+ if ($settings['show_discount_badge'] === 'yes') {
+ $badges_by_position[$settings['discount_position']][] = 'discount';
+ }
+ if ($settings['show_strain_badge'] === 'yes') {
+ $badges_by_position[$settings['strain_position']][] = 'strain';
+ }
+ if ($settings['show_thc_badge'] === 'yes') {
+ $badges_by_position[$settings['thc_position']][] = 'thc';
+ }
+
+ ?>
+
+
+
; ?>)
+
+
+ $badges): ?>
+
+
+
+ render_badge($badge_type); ?>
+
+
+
+
+
+ $sale) {
+ $percent = round((($original - $sale) / $original) * 100);
+ echo '' . esc_html($percent) . '% OFF';
+ }
+ break;
+
+ case 'strain':
+ if (!isset($cannaiq_current_product)) return;
+
+ $strain = strtolower($cannaiq_current_product['strainType']
+ ?? $cannaiq_current_product['strain_type']
+ ?? '');
+
+ if (!empty($strain) && in_array($strain, ['sativa', 'indica', 'hybrid'])) {
+ $colors = [
+ 'sativa' => '#22c55e',
+ 'indica' => '#8b5cf6',
+ 'hybrid' => '#f97316',
+ ];
+ $color = $colors[$strain];
+ echo '' . esc_html(strtoupper($strain)) . '';
+ }
+ break;
+
+ case 'thc':
+ if (!isset($cannaiq_current_product)) return;
+
+ $thc = $cannaiq_current_product['THCContent']['range'][0]
+ ?? $cannaiq_current_product['THC']
+ ?? $cannaiq_current_product['thc_percentage']
+ ?? null;
+
+ if ($thc !== null && $thc > 0) {
+ echo '' . esc_html(number_format((float)$thc, 1)) . '% THC';
+ }
+ break;
+ }
+ }
+}
diff --git a/wordpress-plugin/widgets/product-loop.php b/wordpress-plugin/widgets/product-loop.php
index aeed1606..8085cfdd 100644
--- a/wordpress-plugin/widgets/product-loop.php
+++ b/wordpress-plugin/widgets/product-loop.php
@@ -17,7 +17,7 @@ class CannaIQ_Product_Loop_Widget extends \Elementor\Widget_Base {
}
public function get_title() {
- return __('CannaIQ Product Loop', 'cannaiq-menus');
+ return __('CannaiQ Product Loop', 'cannaiq-menus');
}
public function get_icon() {
diff --git a/wordpress-plugin/widgets/single-product.php b/wordpress-plugin/widgets/single-product.php
index 4a23e8d4..bde3ccf2 100644
--- a/wordpress-plugin/widgets/single-product.php
+++ b/wordpress-plugin/widgets/single-product.php
@@ -14,7 +14,7 @@ class CannaIQ_Menus_Single_Product_Widget extends \Elementor\Widget_Base {
}
public function get_title() {
- return __('CannaIQ Single Product', 'cannaiq-menus');
+ return __('CannaiQ Single Product', 'cannaiq-menus');
}
public function get_icon() {
diff --git a/wordpress-plugin/widgets/specials-grid.php b/wordpress-plugin/widgets/specials-grid.php
index 0f1d5536..cf7e9f76 100644
--- a/wordpress-plugin/widgets/specials-grid.php
+++ b/wordpress-plugin/widgets/specials-grid.php
@@ -14,7 +14,7 @@ class CannaIQ_Menus_Specials_Grid_Widget extends \Elementor\Widget_Base {
}
public function get_title() {
- return __('CannaIQ Specials/Deals', 'cannaiq-menus');
+ return __('CannaiQ Specials/Deals', 'cannaiq-menus');
}
public function get_icon() {
diff --git a/wordpress-plugin/widgets/stock-indicator.php b/wordpress-plugin/widgets/stock-indicator.php
new file mode 100644
index 00000000..f3ff8753
--- /dev/null
+++ b/wordpress-plugin/widgets/stock-indicator.php
@@ -0,0 +1,258 @@
+start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'source',
+ [
+ 'label' => __('Stock Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom value', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_in_stock',
+ [
+ 'label' => __('In Stock', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ 'condition' => [
+ 'source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'format',
+ [
+ 'label' => __('Display Format', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'badge',
+ 'options' => [
+ 'badge' => __('Badge', 'cannaiq-menus'),
+ 'text' => __('Text', 'cannaiq-menus'),
+ 'dot' => __('Dot + Text', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'in_stock_text',
+ [
+ 'label' => __('In Stock Text', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => 'In Stock',
+ ]
+ );
+
+ $this->add_control(
+ 'out_of_stock_text',
+ [
+ 'label' => __('Out of Stock Text', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::TEXT,
+ 'default' => 'Out of Stock',
+ ]
+ );
+
+ $this->add_control(
+ 'show_quantity',
+ [
+ 'label' => __('Show Quantity', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => '',
+ 'description' => __('Show quantity if available', 'cannaiq-menus'),
+ ]
+ );
+
+ $this->add_control(
+ 'hide_if_in_stock',
+ [
+ 'label' => __('Hide if In Stock', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => '',
+ 'description' => __('Only show when out of stock', 'cannaiq-menus'),
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'in_stock_color',
+ [
+ 'label' => __('In Stock Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#16a34a',
+ ]
+ );
+
+ $this->add_control(
+ 'out_of_stock_color',
+ [
+ 'label' => __('Out of Stock Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#9ca3af',
+ ]
+ );
+
+ $this->add_control(
+ 'in_stock_bg',
+ [
+ 'label' => __('In Stock Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#dcfce7',
+ 'condition' => [
+ 'format' => 'badge',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'out_of_stock_bg',
+ [
+ 'label' => __('Out of Stock Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#f3f4f6',
+ 'condition' => [
+ 'format' => 'badge',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'typography',
+ 'selector' => '{{WRAPPER}} .cannaiq-stock-indicator',
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ // Get stock status
+ $in_stock = true;
+ $quantity = null;
+
+ if ($settings['source'] === 'custom') {
+ $in_stock = $settings['custom_in_stock'] === 'yes';
+ } else {
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $status = $cannaiq_current_product['Status'] ?? '';
+ $in_stock = ($status === 'Active' || $status === 'In Stock' || !empty($cannaiq_current_product['in_stock']));
+
+ // Get quantity if available
+ $quantity = $cannaiq_current_product['POSMetaData']['children'][0]['quantity']
+ ?? $cannaiq_current_product['quantity']
+ ?? null;
+ }
+ }
+
+ // Hide if in stock and setting enabled
+ if ($in_stock && $settings['hide_if_in_stock'] === 'yes') {
+ return;
+ }
+
+ // Determine display text
+ $text = $in_stock ? $settings['in_stock_text'] : $settings['out_of_stock_text'];
+ if ($in_stock && $settings['show_quantity'] === 'yes' && $quantity !== null) {
+ $text .= ' (' . intval($quantity) . ')';
+ }
+
+ // Colors
+ $color = $in_stock ? $settings['in_stock_color'] : $settings['out_of_stock_color'];
+ $bg_color = $in_stock ? $settings['in_stock_bg'] : $settings['out_of_stock_bg'];
+
+ // Build classes
+ $classes = [
+ 'cannaiq-stock-indicator',
+ $in_stock ? 'cannaiq-stock-indicator--in-stock' : 'cannaiq-stock-indicator--out-of-stock',
+ ];
+ if ($settings['format'] === 'badge') {
+ $classes[] = 'cannaiq-stock-indicator--badge';
+ }
+
+ // Build style
+ $style = sprintf('color: %s;', esc_attr($color));
+ if ($settings['format'] === 'badge') {
+ $style .= sprintf(' background-color: %s;', esc_attr($bg_color));
+ }
+
+ ?>
+
+
+
+
+
+
+ start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'source',
+ [
+ 'label' => __('Strain Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom value', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_strain',
+ [
+ 'label' => __('Strain Type', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'hybrid',
+ 'options' => [
+ 'sativa' => __('Sativa', 'cannaiq-menus'),
+ 'indica' => __('Indica', 'cannaiq-menus'),
+ 'hybrid' => __('Hybrid', 'cannaiq-menus'),
+ ],
+ 'condition' => [
+ 'source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'format',
+ [
+ 'label' => __('Display Format', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'pill',
+ 'options' => [
+ 'pill' => __('Pill', 'cannaiq-menus'),
+ 'text' => __('Text Only', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'text_format',
+ [
+ 'label' => __('Text Format', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'uppercase',
+ 'options' => [
+ 'uppercase' => __('UPPERCASE', 'cannaiq-menus'),
+ 'capitalize' => __('Capitalize', 'cannaiq-menus'),
+ 'lowercase' => __('lowercase', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'show_icon',
+ [
+ 'label' => __('Show Icon', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => '',
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'size',
+ [
+ 'label' => __('Size', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'medium',
+ 'options' => [
+ 'small' => __('Small', 'cannaiq-menus'),
+ 'medium' => __('Medium', 'cannaiq-menus'),
+ 'large' => __('Large', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'sativa_color',
+ [
+ 'label' => __('Sativa Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#22c55e',
+ ]
+ );
+
+ $this->add_control(
+ 'indica_color',
+ [
+ 'label' => __('Indica Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#8b5cf6',
+ ]
+ );
+
+ $this->add_control(
+ 'hybrid_color',
+ [
+ 'label' => __('Hybrid Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#f97316',
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'typography',
+ 'selector' => '{{WRAPPER}} .cannaiq-strain-badge',
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+
+ // Get strain type
+ $strain = '';
+ if ($settings['source'] === 'custom') {
+ $strain = $settings['custom_strain'];
+ } else {
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ $strain = strtolower($cannaiq_current_product['strainType'] ?? $cannaiq_current_product['strain_type'] ?? '');
+ }
+ }
+
+ if (empty($strain)) {
+ return;
+ }
+
+ // Normalize strain type
+ $strain_key = strtolower($strain);
+ if (!in_array($strain_key, ['sativa', 'indica', 'hybrid'])) {
+ return;
+ }
+
+ // Format text
+ switch ($settings['text_format']) {
+ case 'uppercase':
+ $text = strtoupper($strain);
+ break;
+ case 'lowercase':
+ $text = strtolower($strain);
+ break;
+ default:
+ $text = ucfirst($strain);
+ }
+
+ // Get color based on strain type
+ $color = $settings[$strain_key . '_color'];
+
+ // Build classes
+ $classes = [
+ 'cannaiq-strain-badge',
+ 'cannaiq-strain-badge--' . $settings['format'],
+ 'cannaiq-strain-badge--' . $strain_key,
+ ];
+ if ($settings['size'] !== 'medium') {
+ $classes[] = 'cannaiq-strain-badge--' . $settings['size'];
+ }
+
+ // Build style
+ $style = '';
+ if ($settings['format'] === 'pill') {
+ $style = sprintf('background-color: %s; color: white;', esc_attr($color));
+ } else {
+ $style = sprintf('color: %s;', esc_attr($color));
+ }
+
+ // Icon SVG (leaf icon)
+ $icon = '';
+ if ($settings['show_icon'] === 'yes') {
+ $icon = '';
+ }
+
+ printf(
+ '%s%s',
+ esc_attr(implode(' ', $classes)),
+ esc_attr($style),
+ $icon,
+ esc_html($text)
+ );
+ }
+}
diff --git a/wordpress-plugin/widgets/thc-meter.php b/wordpress-plugin/widgets/thc-meter.php
new file mode 100644
index 00000000..83df8146
--- /dev/null
+++ b/wordpress-plugin/widgets/thc-meter.php
@@ -0,0 +1,295 @@
+start_controls_section(
+ 'content_section',
+ [
+ 'label' => __('Content', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
+ ]
+ );
+
+ $this->add_control(
+ 'type',
+ [
+ 'label' => __('Type', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'thc',
+ 'options' => [
+ 'thc' => __('THC', 'cannaiq-menus'),
+ 'cbd' => __('CBD', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'source',
+ [
+ 'label' => __('Value Source', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'auto',
+ 'options' => [
+ 'auto' => __('Auto (from product)', 'cannaiq-menus'),
+ 'custom' => __('Custom value', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'custom_value',
+ [
+ 'label' => __('Percentage', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 20,
+ 'min' => 0,
+ 'max' => 100,
+ 'step' => 0.1,
+ 'condition' => [
+ 'source' => 'custom',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'display_format',
+ [
+ 'label' => __('Display Format', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SELECT,
+ 'default' => 'meter',
+ 'options' => [
+ 'meter' => __('Meter (progress bar)', 'cannaiq-menus'),
+ 'badge' => __('Badge', 'cannaiq-menus'),
+ 'pill' => __('Pill', 'cannaiq-menus'),
+ 'text' => __('Text Only', 'cannaiq-menus'),
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'show_label',
+ [
+ 'label' => __('Show Label', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SWITCHER,
+ 'label_on' => __('Yes', 'cannaiq-menus'),
+ 'label_off' => __('No', 'cannaiq-menus'),
+ 'return_value' => 'yes',
+ 'default' => 'yes',
+ ]
+ );
+
+ $this->add_control(
+ 'max_percentage',
+ [
+ 'label' => __('Max Percentage (for meter)', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::NUMBER,
+ 'default' => 35,
+ 'min' => 10,
+ 'max' => 100,
+ 'description' => __('Used to calculate bar fill percentage', 'cannaiq-menus'),
+ 'condition' => [
+ 'display_format' => 'meter',
+ ],
+ ]
+ );
+
+ $this->end_controls_section();
+
+ // Style Section
+ $this->start_controls_section(
+ 'style_section',
+ [
+ 'label' => __('Style', 'cannaiq-menus'),
+ 'tab' => \Elementor\Controls_Manager::TAB_STYLE,
+ ]
+ );
+
+ $this->add_control(
+ 'thc_color',
+ [
+ 'label' => __('THC Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#22c55e',
+ ]
+ );
+
+ $this->add_control(
+ 'cbd_color',
+ [
+ 'label' => __('CBD Color', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#3b82f6',
+ ]
+ );
+
+ $this->add_control(
+ 'bar_height',
+ [
+ 'label' => __('Bar Height', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::SLIDER,
+ 'size_units' => ['px'],
+ 'range' => [
+ 'px' => [
+ 'min' => 4,
+ 'max' => 20,
+ ],
+ ],
+ 'default' => [
+ 'size' => 6,
+ ],
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-potency-meter__bar' => 'height: {{SIZE}}{{UNIT}};',
+ ],
+ 'condition' => [
+ 'display_format' => 'meter',
+ ],
+ ]
+ );
+
+ $this->add_control(
+ 'bar_background',
+ [
+ 'label' => __('Bar Background', 'cannaiq-menus'),
+ 'type' => \Elementor\Controls_Manager::COLOR,
+ 'default' => '#e5e7eb',
+ 'selectors' => [
+ '{{WRAPPER}} .cannaiq-potency-meter__bar' => 'background-color: {{VALUE}};',
+ ],
+ 'condition' => [
+ 'display_format' => 'meter',
+ ],
+ ]
+ );
+
+ $this->add_group_control(
+ \Elementor\Group_Control_Typography::get_type(),
+ [
+ 'name' => 'typography',
+ 'selector' => '{{WRAPPER}} .cannaiq-potency-meter, {{WRAPPER}} .cannaiq-potency-badge',
+ ]
+ );
+
+ $this->end_controls_section();
+ }
+
+ protected function render() {
+ $settings = $this->get_settings_for_display();
+ $type = $settings['type'];
+
+ // Get percentage value
+ $percentage = 0;
+ if ($settings['source'] === 'custom') {
+ $percentage = floatval($settings['custom_value']);
+ } else {
+ global $cannaiq_current_product;
+ if (isset($cannaiq_current_product)) {
+ if ($type === 'thc') {
+ $percentage = $cannaiq_current_product['THCContent']['range'][0]
+ ?? $cannaiq_current_product['THC']
+ ?? $cannaiq_current_product['thc_percentage']
+ ?? 0;
+ } else {
+ $percentage = $cannaiq_current_product['CBDContent']['range'][0]
+ ?? $cannaiq_current_product['CBD']
+ ?? $cannaiq_current_product['cbd_percentage']
+ ?? 0;
+ }
+ }
+ }
+
+ $percentage = floatval($percentage);
+ if ($percentage <= 0) {
+ return;
+ }
+
+ $label = strtoupper($type);
+ $color = $type === 'thc' ? $settings['thc_color'] : $settings['cbd_color'];
+ $formatted_value = number_format($percentage, 1) . '%';
+
+ switch ($settings['display_format']) {
+ case 'meter':
+ $fill_percent = min(100, ($percentage / floatval($settings['max_percentage'])) * 100);
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ :
+
+
+
+
+
+
+ :
+
+
+
+