feat: CannaiQ Menus WordPress Plugin v2.0.0 - Modular Component Library
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

New modular component widgets:
- Discount Ribbon (ribbon/pill/text styles)
- Strain Badge (Sativa/Indica/Hybrid colored pills)
- THC/CBD Meter (progress bars or badges)
- Effects Display (styled chips with icons)
- Price Block (original + sale price)
- Cart Button (styled CTA linking to menu)
- Stock Indicator (in/out of stock badges)
- Product Image + Badges (image with overlays)

New card template:
- Premium Product Card (ready-to-use template)

Extended dynamic tags (30+ total):
- Discount %, Strain Badge, THC/CBD Badge
- Effects Chips, Terpenes, Price Display
- Menu URL, Stock Status, and more

New files:
- assets/css/components.css
- includes/effects-icons.php (SVG icons)
- 10 new widget files
- dynamic-tags-extended.php

Branding updated to "CannaiQ" throughout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-17 00:21:40 -07:00
parent 38e7980cf4
commit c33ed1cae9
23 changed files with 4609 additions and 24 deletions

Binary file not shown.

View File

@@ -1 +1 @@
cannaiq-menus-1.7.0.zip
cannaiq-menus-2.0.0.zip

View File

@@ -1 +1 @@
1.7.0
2.0.0

View File

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

View File

@@ -1,10 +1,10 @@
<?php
/**
* Plugin Name: CannaIQ Menus
* Plugin Name: CannaiQ Menus
* Plugin URI: https://cannaiq.co
* Description: Display cannabis product menus from CannaIQ with Elementor integration. Real-time menu data updated daily.
* Version: 1.7.0
* Author: CannaIQ
* Description: Display cannabis product menus from CannaiQ with Elementor integration. Real-time menu data updated daily.
* Version: 2.0.0
* Author: CannaiQ
* Author URI: https://cannaiq.co
* License: GPL v2 or later
* Text Domain: cannaiq-menus
@@ -15,7 +15,7 @@ if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
define('CANNAIQ_MENUS_VERSION', '1.7.0');
define('CANNAIQ_MENUS_VERSION', '2.0.0');
define('CANNAIQ_MENUS_API_URL', 'https://cannaiq.co/api/v1');
define('CANNAIQ_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('CANNAIQ_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__));
@@ -50,7 +50,7 @@ class CannaIQ_Menus_Plugin {
$elements_manager->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() {
?>
<div class="wrap">
<h1>CannaIQ Menus Settings</h1>
<p>Version <?php echo CANNAIQ_MENUS_VERSION; ?> by <a href="https://cannaiq.co" target="_blank">CannaIQ</a></p>
<p class="description">Display real-time cannabis menus with data updated daily from CannaIQ.</p>
<h1>CannaiQ Menus Settings</h1>
<p>Version <?php echo CANNAIQ_MENUS_VERSION; ?> by <a href="https://cannaiq.co" target="_blank">CannaiQ</a></p>
<p class="description">Display real-time cannabis menus with data updated daily from CannaiQ.</p>
<form method="post" action="options.php">
<?php settings_fields('cannaiq_menus_settings'); ?>
@@ -162,7 +203,7 @@ class CannaIQ_Menus_Plugin {
<input type="password" id="cannaiq_api_token" name="cannaiq_api_token"
value="<?php echo esc_attr(get_option('cannaiq_api_token')); ?>"
class="regular-text" />
<p class="description">Your authentication token from the CannaIQ admin dashboard. The token includes your store configuration.</p>
<p class="description">Your authentication token from the CannaiQ admin dashboard. The token includes your store configuration.</p>
</td>
</tr>
</table>
@@ -300,10 +341,10 @@ class CannaIQ_Menus_Plugin {
</table>
<h3>Elementor Widgets</h3>
<p>If you have Elementor installed, you can use the CannaIQ widgets:</p>
<p>If you have Elementor installed, you can use the CannaiQ widgets:</p>
<ul style="list-style: disc; margin-left: 20px;">
<li><strong>CannaIQ Product Grid</strong> - Display a grid of products with filtering options</li>
<li><strong>CannaIQ Single Product</strong> - Display a single product card</li>
<li><strong>CannaiQ Product Grid</strong> - Display a grid of products with filtering options</li>
<li><strong>CannaiQ Single Product</strong> - Display a single product card</li>
</ul>
</div>
<?php

View File

@@ -0,0 +1,192 @@
<?php
/**
* Effects Icons Library
*
* SVG icons for cannabis effects display.
* Used by Effects Display widget and dynamic tags.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Get SVG icon for an effect
*
* @param string $effect Effect name (case-insensitive)
* @param array $args Optional args: size, class, color
* @return string SVG HTML or empty string if not found
*/
function cannaiq_get_effect_icon($effect, $args = []) {
$defaults = [
'size' => 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' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>',
'relaxed' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>',
'sleepy' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
'euphoric' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
'creative' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M12 2v4"/><path d="m6.34 6.34 2.83 2.83"/><path d="M2 12h4"/><path d="m6.34 17.66 2.83-2.83"/><path d="M12 18v4"/><path d="m17.66 17.66-2.83-2.83"/><path d="M18 12h4"/><path d="m17.66 6.34-2.83 2.83"/></svg>',
'energetic' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
'focused' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>',
'hungry' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>',
'uplifted' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="m18 15-6-6-6 6"/><path d="m18 9-6-6-6 6"/></svg>',
'talkative' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
'giggly' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><path d="M9 9h.01"/><path d="M15 9h.01"/></svg>',
'aroused' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
'tingly' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="M8 13h2"/><path d="M8 17h2"/><path d="M14 13h2"/><path d="M14 17h2"/></svg>',
'calm' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></svg>',
'sedated' => '<svg width="{SIZE}" height="{SIZE}" viewBox="0 0 24 24" fill="none" stroke="{COLOR}" stroke-width="2" class="{CLASS}"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/><path d="M9 10h.01"/><path d="M15 10h.01"/><path d="M10 16s.5-1 2-1 2 1 2 1"/></svg>',
];
}
/**
* 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(
'<span class="cannaiq-effect-chip %s %s" style="--effect-color: %s">%s<span class="cannaiq-effect-chip__label">%s</span></span>',
$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(
'<div class="cannaiq-effects-container %s">%s</div>',
esc_attr($args['class']),
implode('', $chips)
);
}

View File

@@ -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() {

View File

@@ -0,0 +1,510 @@
<?php
/**
* CannaIQ Premium Card Template Widget
*
* Pre-built product card template showcasing all modular components.
* Includes: discount ribbon, product image with overlays, name, brand,
* effects, price block, and cart button.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
// Ensure effects icons are loaded
require_once dirname(__DIR__) . '/includes/effects-icons.php';
class CannaIQ_Premium_Card_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_premium_card';
}
public function get_title() {
return __('Premium Product Card', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-single-product';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['product', 'card', 'premium', 'template', 'cannaiq'];
}
protected function register_controls() {
// Content Section
$this->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 '<p>' . __('No product context available. Place this widget inside a Product Loop.', 'cannaiq-menus') . '</p>';
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',
];
?>
<div class="cannaiq-product-card">
<!-- Image Section -->
<?php if ($settings['show_image'] === 'yes'): ?>
<div class="cannaiq-product-card__image">
<div class="cannaiq-product-image" style="aspect-ratio: 1;">
<?php if (!empty($image_url)): ?>
<img src="<?php echo esc_url($image_url); ?>" alt="<?php echo esc_attr($name); ?>" />
<?php endif; ?>
<!-- Discount Ribbon -->
<?php if ($settings['show_discount'] === 'yes' && $discount_percent > 0): ?>
<div class="cannaiq-product-image__overlay cannaiq-product-image__overlay--top-left">
<span class="cannaiq-discount-ribbon cannaiq-discount-ribbon--ribbon"><?php echo esc_html($discount_percent); ?>% OFF</span>
</div>
<?php endif; ?>
<!-- Bottom badges -->
<?php if (($settings['show_strain_badge'] === 'yes' && !empty($strain_type)) || ($settings['show_thc_badge'] === 'yes' && $thc > 0)): ?>
<div class="cannaiq-product-image__overlay cannaiq-product-image__overlay--bottom-left">
<div class="cannaiq-product-image__badges">
<?php if ($settings['show_strain_badge'] === 'yes' && !empty($strain_type) && in_array($strain_type, ['sativa', 'indica', 'hybrid'])): ?>
<span class="cannaiq-strain-badge cannaiq-strain-badge--pill" style="background-color: <?php echo esc_attr($strain_colors[$strain_type]); ?>; color: white;"><?php echo esc_html(strtoupper($strain_type)); ?></span>
<?php endif; ?>
<?php if ($settings['show_thc_badge'] === 'yes' && $thc > 0): ?>
<span class="cannaiq-potency-badge cannaiq-potency-badge--pill" style="background-color: #1f2937; color: white;"><?php echo esc_html(number_format((float)$thc, 1)); ?>% THC</span>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Body Section -->
<div class="cannaiq-product-card__body">
<?php if ($settings['show_name'] === 'yes' && !empty($name)): ?>
<h3 class="cannaiq-product-card__title"><?php echo esc_html($name); ?></h3>
<?php endif; ?>
<?php if ($settings['show_brand'] === 'yes' && !empty($brand)): ?>
<p class="cannaiq-product-card__brand">by <?php echo esc_html($brand); ?></p>
<?php endif; ?>
<?php if ($settings['show_effects'] === 'yes' && !empty($effects)): ?>
<div style="margin: 12px 0;">
<?php echo cannaiq_render_effects($effects, ['limit' => $effects_limit, 'show_icon' => true, 'size' => 'small']); ?>
</div>
<?php endif; ?>
<!-- Footer -->
<div class="cannaiq-product-card__footer">
<?php if ($settings['show_price'] === 'yes' && $original_price > 0): ?>
<div class="cannaiq-price-block" style="margin-bottom: 12px;">
<?php if ($settings['show_weight'] === 'yes' && !empty($weight)): ?>
<span class="cannaiq-price-block__weight"><?php echo esc_html($weight); ?></span>
<?php endif; ?>
<?php if ($is_on_sale): ?>
<span class="cannaiq-price-block__original">$<?php echo esc_html(number_format((float)$original_price, 2)); ?></span>
<span class="cannaiq-price-block__sale">$<?php echo esc_html(number_format((float)$sale_price, 2)); ?></span>
<?php else: ?>
<span class="cannaiq-price-block__regular">$<?php echo esc_html(number_format((float)$original_price, 2)); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($settings['show_cart_button'] === 'yes'): ?>
<a href="<?php echo esc_url($menu_url); ?>" class="cannaiq-cart-button cannaiq-cart-button--solid cannaiq-cart-button--full" target="_blank" rel="noopener noreferrer">
<?php echo esc_html($settings['button_text']); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
}

View File

@@ -0,0 +1,303 @@
<?php
/**
* CannaIQ Cart Button Widget
*
* Displays a styled "Add to Cart" button that links to the menu/dispensary.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class CannaIQ_Cart_Button_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_cart_button';
}
public function get_title() {
return __('Cart Button', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-cart';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['cart', 'buy', 'order', 'shop', 'button', 'cannaiq'];
}
protected function register_controls() {
$this->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 = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>';
?>
<a href="<?php echo esc_url($url); ?>" class="<?php echo esc_attr(implode(' ', $classes)); ?>"<?php echo $target; ?>>
<?php if ($settings['show_icon'] === 'yes' && $settings['icon_position'] === 'before'): ?>
<?php echo $icon; ?>
<?php endif; ?>
<?php echo esc_html($settings['button_text']); ?>
<?php if ($settings['show_icon'] === 'yes' && $settings['icon_position'] === 'after'): ?>
<?php echo $icon; ?>
<?php endif; ?>
</a>
<?php
}
}

View File

@@ -14,7 +14,7 @@ class CannaIQ_Menus_Category_List_Widget extends \Elementor\Widget_Base {
}
public function get_title() {
return __('CannaIQ Category List', 'cannaiq-menus');
return __('CannaiQ Category List', 'cannaiq-menus');
}
public function get_icon() {

View File

@@ -0,0 +1,216 @@
<?php
/**
* CannaIQ Discount Ribbon Widget
*
* Displays discount percentage as a positioned badge/ribbon.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class CannaIQ_Discount_Ribbon_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_discount_ribbon';
}
public function get_title() {
return __('Discount Ribbon', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-price-table';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['discount', 'sale', 'ribbon', 'badge', 'cannaiq'];
}
protected function register_controls() {
$this->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(
'<span class="%s">%s</span>',
esc_attr(implode(' ', $classes)),
esc_html($text)
);
}
}

View File

@@ -0,0 +1,793 @@
<?php
/**
* CannaIQ Extended Dynamic Tags
*
* Additional dynamic tags for v2.0 modular component system.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
// Include effects icons helper
require_once dirname(__DIR__) . '/includes/effects-icons.php';
/**
* Register extended CannaIQ dynamic tags
*/
add_action('elementor/dynamic_tags/register', function($dynamic_tags_manager) {
// Register new tags
$dynamic_tags_manager->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(
'<span class="%s">%s%% OFF</span>',
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(
'<span class="%s" style="%s">%s</span>',
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(
'<span class="%s">%s</span>',
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(
'<span class="%s">%s</span>',
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 '<div class="cannaiq-terpenes cannaiq-terpenes--chips">';
foreach ($terpenes as $terp) {
$name = $terp['name'] ?? '';
$percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
printf(
'<span class="cannaiq-terpene-chip"><span class="cannaiq-terpene-chip__name">%s</span><span class="cannaiq-terpene-chip__percent">%s</span></span>',
esc_html($name),
esc_html($percent)
);
}
echo '</div>';
break;
case 'list':
echo '<div class="cannaiq-terpenes cannaiq-terpenes--list">';
foreach ($terpenes as $terp) {
$name = $terp['name'] ?? '';
$percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : '';
printf(
'<div class="cannaiq-terpene-item"><span>%s</span><span>%s</span></div>',
esc_html($name),
esc_html($percent)
);
}
echo '</div>';
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 '<span class="cannaiq-price-block">';
if ($is_on_sale) {
if ($show_original) {
printf(
'<span class="cannaiq-price-block__original">$%s</span>',
esc_html(number_format((float)$original, 2))
);
}
printf(
'<span class="cannaiq-price-block__sale">$%s</span>',
esc_html(number_format((float)$sale, 2))
);
} else {
printf(
'<span class="cannaiq-price-block__regular">$%s</span>',
esc_html(number_format((float)$original, 2))
);
}
echo '</span>';
}
}
/**
* 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('<span class="%s">', esc_attr($class));
if ($style === 'dot') {
echo '<span class="cannaiq-stock-indicator__dot"></span>';
}
echo esc_html($text);
echo '</span>';
}
}

View File

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

View File

@@ -0,0 +1,288 @@
<?php
/**
* CannaIQ Effects Display Widget
*
* Displays product effects as styled chips with optional icons.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
// Include effects icons helper
require_once dirname(__DIR__) . '/includes/effects-icons.php';
class CannaIQ_Effects_Display_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_effects_display';
}
public function get_title() {
return __('Effects Display', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-bullet-list';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['effects', 'happy', 'relaxed', 'sleepy', 'chips', 'cannaiq'];
}
protected function register_controls() {
$this->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);
?>
<div class="cannaiq-effects-container">
<?php foreach ($effects as $effect): ?>
<?php
$effect_name = ucfirst(strtolower(trim($effect)));
$effect_key = strtolower(trim($effect));
// Get color if using colors
$color = '#6B7280'; // Default gray
$style = '';
if ($settings['use_colors'] === 'yes') {
$color = cannaiq_get_effect_color($effect);
$style = sprintf('--effect-color: %s;', esc_attr($color));
} else {
$style = sprintf('background: %s; border-color: %s;',
esc_attr($settings['default_background']),
esc_attr($settings['default_background'])
);
}
// Build classes
$classes = ['cannaiq-effect-chip'];
if ($settings['size'] !== 'medium') {
$classes[] = 'cannaiq-effect-chip--' . $settings['size'];
}
?>
<span class="<?php echo esc_attr(implode(' ', $classes)); ?>" style="<?php echo esc_attr($style); ?>">
<?php if ($settings['show_icons'] === 'yes'): ?>
<?php echo cannaiq_get_effect_icon($effect, [
'size' => $icon_size,
'color' => $settings['use_colors'] === 'yes' ? $color : 'currentColor',
]); ?>
<?php endif; ?>
<span class="cannaiq-effect-chip__label"><?php echo esc_html($effect_name); ?></span>
</span>
<?php endforeach; ?>
</div>
<?php
}
}

View File

@@ -0,0 +1,309 @@
<?php
/**
* CannaIQ Price Block Widget
*
* Displays product price with optional sale price and weight.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class CannaIQ_Price_Block_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_price_block';
}
public function get_title() {
return __('Price Block', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-product-price';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['price', 'sale', 'cost', 'money', 'cannaiq'];
}
protected function register_controls() {
$this->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'];
?>
<div class="<?php echo esc_attr(implode(' ', $classes)); ?>">
<?php if ($settings['show_weight'] === 'yes' && !empty($weight)): ?>
<span class="cannaiq-price-block__weight"><?php echo esc_html($weight); ?></span>
<?php endif; ?>
<?php if ($is_on_sale): ?>
<?php if ($settings['show_original_when_sale'] === 'yes'): ?>
<span class="cannaiq-price-block__original"><?php echo esc_html($currency . number_format($regular_price, 2)); ?></span>
<?php endif; ?>
<span class="cannaiq-price-block__sale"><?php echo esc_html($currency . number_format(floatval($sale_price), 2)); ?></span>
<?php else: ?>
<span class="cannaiq-price-block__regular"><?php echo esc_html($currency . number_format($regular_price, 2)); ?></span>
<?php endif; ?>
</div>
<?php
}
}

View File

@@ -14,7 +14,7 @@ class CannaIQ_Menus_Product_Grid_Widget extends \Elementor\Widget_Base {
}
public function get_title() {
return __('CannaIQ Product Grid', 'cannaiq-menus');
return __('CannaiQ Product Grid', 'cannaiq-menus');
}
public function get_icon() {

View File

@@ -0,0 +1,390 @@
<?php
/**
* CannaIQ Product Image Overlay Widget
*
* Displays product image with positioned badge overlays.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class CannaIQ_Product_Image_Overlay_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_product_image_overlay';
}
public function get_title() {
return __('Product Image + Badges', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-image';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['image', 'product', 'photo', 'overlay', 'badges', 'cannaiq'];
}
protected function register_controls() {
$this->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';
}
?>
<div class="cannaiq-product-image <?php echo esc_attr($hover_class); ?>" style="<?php echo esc_attr($aspect_style); ?>">
<?php if (!empty($image_url)): ?>
<img src="<?php echo esc_url($image_url); ?>" alt="<?php echo esc_attr($alt_text); ?>" />
<?php endif; ?>
<?php foreach ($badges_by_position as $position => $badges): ?>
<div class="cannaiq-product-image__overlay cannaiq-product-image__overlay--<?php echo esc_attr($position); ?>">
<div class="cannaiq-product-image__badges">
<?php foreach ($badges as $badge_type): ?>
<?php $this->render_badge($badge_type); ?>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php
}
private function render_badge($type) {
global $cannaiq_current_product;
switch ($type) {
case 'discount':
if (!isset($cannaiq_current_product)) return;
$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) {
$percent = round((($original - $sale) / $original) * 100);
echo '<span class="cannaiq-discount-ribbon cannaiq-discount-ribbon--pill">' . esc_html($percent) . '% OFF</span>';
}
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 '<span class="cannaiq-strain-badge cannaiq-strain-badge--pill" style="background-color: ' . esc_attr($color) . '; color: white;">' . esc_html(strtoupper($strain)) . '</span>';
}
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 '<span class="cannaiq-potency-badge cannaiq-potency-badge--pill" style="background-color: #1f2937; color: white;">' . esc_html(number_format((float)$thc, 1)) . '% THC</span>';
}
break;
}
}
}

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -0,0 +1,258 @@
<?php
/**
* CannaIQ Stock Indicator Widget
*
* Displays product stock status (In Stock / Out of Stock).
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class CannaIQ_Stock_Indicator_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_stock_indicator';
}
public function get_title() {
return __('Stock Indicator', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-check-circle';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['stock', 'inventory', 'available', 'cannaiq'];
}
protected function register_controls() {
$this->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));
}
?>
<span class="<?php echo esc_attr(implode(' ', $classes)); ?>" style="<?php echo esc_attr($style); ?>">
<?php if ($settings['format'] === 'dot'): ?>
<span class="cannaiq-stock-indicator__dot"></span>
<?php endif; ?>
<?php echo esc_html($text); ?>
</span>
<?php
}
}

View File

@@ -0,0 +1,250 @@
<?php
/**
* CannaIQ Strain Badge Widget
*
* Displays strain type (Sativa/Indica/Hybrid) as a colored badge.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class CannaIQ_Strain_Badge_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_strain_badge';
}
public function get_title() {
return __('Strain Badge', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-tags';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['strain', 'sativa', 'indica', 'hybrid', 'badge', 'cannaiq'];
}
protected function register_controls() {
$this->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 = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>';
}
printf(
'<span class="%s" style="%s">%s%s</span>',
esc_attr(implode(' ', $classes)),
esc_attr($style),
$icon,
esc_html($text)
);
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* CannaIQ THC/CBD Meter Widget
*
* Displays THC or CBD percentage as a visual meter/progress bar.
*
* @package CannaIQ_Menus
* @since 2.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class CannaIQ_THC_Meter_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'cannaiq_thc_meter';
}
public function get_title() {
return __('THC/CBD Meter', 'cannaiq-menus');
}
public function get_icon() {
return 'eicon-skill-bar';
}
public function get_categories() {
return ['cannaiq'];
}
public function get_keywords() {
return ['thc', 'cbd', 'potency', 'meter', 'percentage', 'cannaiq'];
}
protected function register_controls() {
$this->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);
?>
<div class="cannaiq-potency-meter cannaiq-potency-meter--<?php echo esc_attr($type); ?>">
<?php if ($settings['show_label'] === 'yes'): ?>
<div class="cannaiq-potency-meter__header">
<span class="cannaiq-potency-meter__label"><?php echo esc_html($label); ?></span>
<span class="cannaiq-potency-meter__value"><?php echo esc_html($formatted_value); ?></span>
</div>
<?php endif; ?>
<div class="cannaiq-potency-meter__bar">
<div class="cannaiq-potency-meter__fill" style="width: <?php echo esc_attr($fill_percent); ?>%; background: linear-gradient(90deg, <?php echo esc_attr($color); ?> 0%, <?php echo esc_attr($color); ?> 100%);"></div>
</div>
</div>
<?php
break;
case 'badge':
?>
<span class="cannaiq-potency-badge cannaiq-potency-badge--badge">
<?php if ($settings['show_label'] === 'yes'): ?>
<span class="cannaiq-potency-badge__label"><?php echo esc_html($label); ?></span>
<?php endif; ?>
<span class="cannaiq-potency-badge__value"><?php echo esc_html($formatted_value); ?></span>
</span>
<?php
break;
case 'pill':
?>
<span class="cannaiq-potency-badge cannaiq-potency-badge--pill" style="background-color: <?php echo esc_attr($color); ?>;">
<?php if ($settings['show_label'] === 'yes'): ?>
<?php echo esc_html($label); ?>:
<?php endif; ?>
<?php echo esc_html($formatted_value); ?>
</span>
<?php
break;
case 'text':
?>
<span class="cannaiq-potency-badge cannaiq-potency-badge--text">
<?php if ($settings['show_label'] === 'yes'): ?>
<?php echo esc_html($label); ?>:
<?php endif; ?>
<?php echo esc_html($formatted_value); ?>
</span>
<?php
break;
}
}
}