From c33ed1cae9f6a2a26d61ff510e3111ce418f7266 Mon Sep 17 00:00:00 2001 From: Kelly Date: Wed, 17 Dec 2025 00:21:40 -0700 Subject: [PATCH] feat: CannaiQ Menus WordPress Plugin v2.0.0 - Modular Component Library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../public/downloads/cannaiq-menus-2.0.0.zip | Bin 0 -> 57738 bytes .../public/downloads/cannaiq-menus-latest.zip | 2 +- wordpress-plugin/VERSION | 2 +- wordpress-plugin/assets/css/components.css | 740 ++++++++++++++++ wordpress-plugin/cannaiq-menus.php | 71 +- wordpress-plugin/includes/effects-icons.php | 192 +++++ wordpress-plugin/widgets/brand-grid.php | 2 +- .../widgets/card-template-premium.php | 510 +++++++++++ wordpress-plugin/widgets/cart-button.php | 303 +++++++ wordpress-plugin/widgets/category-list.php | 2 +- wordpress-plugin/widgets/discount-ribbon.php | 216 +++++ .../widgets/dynamic-tags-extended.php | 793 ++++++++++++++++++ wordpress-plugin/widgets/dynamic-tags.php | 2 +- wordpress-plugin/widgets/effects-display.php | 288 +++++++ wordpress-plugin/widgets/price-block.php | 309 +++++++ wordpress-plugin/widgets/product-grid.php | 2 +- .../widgets/product-image-overlay.php | 390 +++++++++ wordpress-plugin/widgets/product-loop.php | 2 +- wordpress-plugin/widgets/single-product.php | 2 +- wordpress-plugin/widgets/specials-grid.php | 2 +- wordpress-plugin/widgets/stock-indicator.php | 258 ++++++ wordpress-plugin/widgets/strain-badge.php | 250 ++++++ wordpress-plugin/widgets/thc-meter.php | 295 +++++++ 23 files changed, 4609 insertions(+), 24 deletions(-) create mode 100644 backend/public/downloads/cannaiq-menus-2.0.0.zip create mode 100644 wordpress-plugin/assets/css/components.css create mode 100644 wordpress-plugin/includes/effects-icons.php create mode 100644 wordpress-plugin/widgets/card-template-premium.php create mode 100644 wordpress-plugin/widgets/cart-button.php create mode 100644 wordpress-plugin/widgets/discount-ribbon.php create mode 100644 wordpress-plugin/widgets/dynamic-tags-extended.php create mode 100644 wordpress-plugin/widgets/effects-display.php create mode 100644 wordpress-plugin/widgets/price-block.php create mode 100644 wordpress-plugin/widgets/product-image-overlay.php create mode 100644 wordpress-plugin/widgets/stock-indicator.php create mode 100644 wordpress-plugin/widgets/strain-badge.php create mode 100644 wordpress-plugin/widgets/thc-meter.php diff --git a/backend/public/downloads/cannaiq-menus-2.0.0.zip b/backend/public/downloads/cannaiq-menus-2.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..5d6ee49d793ae9f21dde2d53092632e442d9be99 GIT binary patch literal 57738 zcmafaV~}NS)@<3fZQHhO+qP}nt}ffQ-DR`OwyW#*J2T(temCxkSUYx{h?9TL%ANV- zT3bOH7z7I7uTK_Gg!bQV{&9fQ&6@T!tSr)ZsFSV&!?Uq7uIsjG~FmKvilzzPg7KVTz zQ!GqMliYTk{Lz^P6tM>)PbT0J4 z+v^UXp@|OL%K^bq3JWye-Qwfh;@RGO;QVYT@VG@+mAtvOv9CWzMfGS5NAZJ=ll1M9E_38vrTrR~$c!?dMnw0_D zJUo`(BIlfq^{9q`hI0F57t2yU0B*#pLN%b-Ta=GbrlcIH~*@tnAv;j7C)oBSZpd@A`s8` z{Qc6O2SJKDxH(FjpcvGvSg}2pti*{NioubtB$mv8xi>??&h<+UvξEr*aX_aPAU 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

sQgW{3y$&O^Ua_x%s{O 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 literal 0 HcmV?d00001 diff --git a/backend/public/downloads/cannaiq-menus-latest.zip b/backend/public/downloads/cannaiq-menus-latest.zip index 026c7acd..7c310084 120000 --- a/backend/public/downloads/cannaiq-menus-latest.zip +++ b/backend/public/downloads/cannaiq-menus-latest.zip @@ -1 +1 @@ -cannaiq-menus-1.7.0.zip \ No newline at end of file +cannaiq-menus-2.0.0.zip \ No newline at end of file diff --git a/wordpress-plugin/VERSION b/wordpress-plugin/VERSION index bd8bf882..227cea21 100644 --- a/wordpress-plugin/VERSION +++ b/wordpress-plugin/VERSION @@ -1 +1 @@ -1.7.0 +2.0.0 diff --git a/wordpress-plugin/assets/css/components.css b/wordpress-plugin/assets/css/components.css new file mode 100644 index 00000000..fbbf0826 --- /dev/null +++ b/wordpress-plugin/assets/css/components.css @@ -0,0 +1,740 @@ +/** + * CannaIQ Modular Components CSS + * + * Styles for the modular component library. + * Each component is independently styled and composable. + * + * @package CannaIQ_Menus + * @since 2.0.0 + */ + +/* ========================================================================== + CSS Variables (Design Tokens) + ========================================================================== */ + +:root { + /* Strain Colors */ + --cannaiq-sativa: #22c55e; + --cannaiq-indica: #8b5cf6; + --cannaiq-hybrid: #f97316; + + /* UI Colors */ + --cannaiq-discount: #ef4444; + --cannaiq-discount-bg: #fef2f2; + --cannaiq-sale: #dc2626; + --cannaiq-stock-in: #16a34a; + --cannaiq-stock-out: #9ca3af; + --cannaiq-price-original: #9ca3af; + --cannaiq-price-sale: #dc2626; + + /* Neutrals */ + --cannaiq-text-primary: #1f2937; + --cannaiq-text-secondary: #6b7280; + --cannaiq-text-muted: #9ca3af; + --cannaiq-border: #e5e7eb; + --cannaiq-bg-light: #f9fafb; + + /* Spacing */ + --cannaiq-space-xs: 0.25rem; + --cannaiq-space-sm: 0.5rem; + --cannaiq-space-md: 0.75rem; + --cannaiq-space-lg: 1rem; + --cannaiq-space-xl: 1.5rem; + + /* Border Radius */ + --cannaiq-radius-sm: 0.25rem; + --cannaiq-radius-md: 0.375rem; + --cannaiq-radius-lg: 0.5rem; + --cannaiq-radius-xl: 0.75rem; + --cannaiq-radius-full: 9999px; + + /* Shadows */ + --cannaiq-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --cannaiq-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --cannaiq-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); +} + +/* ========================================================================== + Discount Ribbon + ========================================================================== */ + +.cannaiq-discount-ribbon { + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.025em; + white-space: nowrap; +} + +/* Ribbon Style - Corner positioned */ +.cannaiq-discount-ribbon--ribbon { + position: absolute; + top: 0; + left: 0; + background: var(--cannaiq-discount); + color: white; + padding: var(--cannaiq-space-xs) var(--cannaiq-space-md); + font-size: 0.75rem; + border-bottom-right-radius: var(--cannaiq-radius-md); + z-index: 10; +} + +/* Pill Style */ +.cannaiq-discount-ribbon--pill { + background: var(--cannaiq-discount); + color: white; + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + font-size: 0.75rem; + border-radius: var(--cannaiq-radius-full); +} + +/* Text Style */ +.cannaiq-discount-ribbon--text { + color: var(--cannaiq-discount); + font-size: 0.875rem; +} + +/* Sizes */ +.cannaiq-discount-ribbon--small { + font-size: 0.625rem; + padding: 2px var(--cannaiq-space-xs); +} + +.cannaiq-discount-ribbon--large { + font-size: 0.875rem; + padding: var(--cannaiq-space-sm) var(--cannaiq-space-lg); +} + +/* ========================================================================== + Strain Badge + ========================================================================== */ + +.cannaiq-strain-badge { + display: inline-flex; + align-items: center; + gap: var(--cannaiq-space-xs); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + border-radius: var(--cannaiq-radius-full); +} + +/* Pill Style */ +.cannaiq-strain-badge--pill { + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + font-size: 0.625rem; +} + +/* Text Style */ +.cannaiq-strain-badge--text { + padding: 0; + font-size: 0.75rem; +} + +/* Strain Type Colors */ +.cannaiq-strain-badge--sativa { + background: var(--cannaiq-sativa); + color: white; +} + +.cannaiq-strain-badge--sativa.cannaiq-strain-badge--text { + background: transparent; + color: var(--cannaiq-sativa); +} + +.cannaiq-strain-badge--indica { + background: var(--cannaiq-indica); + color: white; +} + +.cannaiq-strain-badge--indica.cannaiq-strain-badge--text { + background: transparent; + color: var(--cannaiq-indica); +} + +.cannaiq-strain-badge--hybrid { + background: var(--cannaiq-hybrid); + color: white; +} + +.cannaiq-strain-badge--hybrid.cannaiq-strain-badge--text { + background: transparent; + color: var(--cannaiq-hybrid); +} + +/* Sizes */ +.cannaiq-strain-badge--small { + font-size: 0.5rem; + padding: 2px var(--cannaiq-space-xs); +} + +.cannaiq-strain-badge--large { + font-size: 0.75rem; + padding: var(--cannaiq-space-sm) var(--cannaiq-space-md); +} + +/* ========================================================================== + THC/CBD Badge + ========================================================================== */ + +.cannaiq-potency-badge { + display: inline-flex; + align-items: center; + gap: var(--cannaiq-space-xs); + font-weight: 600; +} + +/* Badge Style */ +.cannaiq-potency-badge--badge { + background: var(--cannaiq-bg-light); + border: 1px solid var(--cannaiq-border); + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + border-radius: var(--cannaiq-radius-md); + font-size: 0.75rem; +} + +/* Pill Style */ +.cannaiq-potency-badge--pill { + background: var(--cannaiq-text-primary); + color: white; + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + border-radius: var(--cannaiq-radius-full); + font-size: 0.75rem; +} + +/* Text Style */ +.cannaiq-potency-badge--text { + color: var(--cannaiq-text-secondary); + font-size: 0.875rem; +} + +.cannaiq-potency-badge__label { + color: var(--cannaiq-text-muted); + font-size: 0.625rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.cannaiq-potency-badge__value { + font-weight: 700; +} + +/* ========================================================================== + THC/CBD Meter (Visual Progress Bar) + ========================================================================== */ + +.cannaiq-potency-meter { + display: flex; + flex-direction: column; + gap: var(--cannaiq-space-xs); +} + +.cannaiq-potency-meter__header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; +} + +.cannaiq-potency-meter__label { + font-weight: 600; + color: var(--cannaiq-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.cannaiq-potency-meter__value { + font-weight: 700; + color: var(--cannaiq-text-primary); +} + +.cannaiq-potency-meter__bar { + height: 6px; + background: var(--cannaiq-border); + border-radius: var(--cannaiq-radius-full); + overflow: hidden; +} + +.cannaiq-potency-meter__fill { + height: 100%; + border-radius: var(--cannaiq-radius-full); + transition: width 0.3s ease; +} + +.cannaiq-potency-meter--thc .cannaiq-potency-meter__fill { + background: linear-gradient(90deg, #22c55e 0%, #16a34a 100%); +} + +.cannaiq-potency-meter--cbd .cannaiq-potency-meter__fill { + background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%); +} + +/* ========================================================================== + Effects Display + ========================================================================== */ + +.cannaiq-effects-container { + display: flex; + flex-wrap: wrap; + gap: var(--cannaiq-space-sm); +} + +.cannaiq-effect-chip { + display: inline-flex; + align-items: center; + gap: var(--cannaiq-space-xs); + background: color-mix(in srgb, var(--effect-color, #6b7280) 15%, white); + border: 1px solid color-mix(in srgb, var(--effect-color, #6b7280) 30%, white); + border-radius: var(--cannaiq-radius-full); + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + font-size: 0.75rem; + font-weight: 500; + color: var(--cannaiq-text-primary); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.cannaiq-effect-chip:hover { + transform: translateY(-1px); + box-shadow: var(--cannaiq-shadow-sm); +} + +.cannaiq-effect-chip svg { + flex-shrink: 0; +} + +.cannaiq-effect-chip__label { + white-space: nowrap; +} + +/* Effect Chip Sizes */ +.cannaiq-effect-chip--small { + padding: 2px var(--cannaiq-space-xs); + font-size: 0.625rem; + gap: 2px; +} + +.cannaiq-effect-chip--large { + padding: var(--cannaiq-space-sm) var(--cannaiq-space-md); + font-size: 0.875rem; +} + +/* ========================================================================== + Terpene Profile + ========================================================================== */ + +.cannaiq-terpenes { + display: flex; + flex-direction: column; + gap: var(--cannaiq-space-sm); +} + +.cannaiq-terpenes--chips { + flex-direction: row; + flex-wrap: wrap; +} + +.cannaiq-terpene-chip { + display: inline-flex; + align-items: center; + gap: var(--cannaiq-space-xs); + background: var(--cannaiq-bg-light); + border: 1px solid var(--cannaiq-border); + border-radius: var(--cannaiq-radius-full); + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + font-size: 0.75rem; +} + +.cannaiq-terpene-chip__name { + font-weight: 500; + color: var(--cannaiq-text-primary); +} + +.cannaiq-terpene-chip__percent { + color: var(--cannaiq-text-secondary); +} + +/* Terpene List Style */ +.cannaiq-terpenes--list .cannaiq-terpene-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--cannaiq-space-xs) 0; + border-bottom: 1px solid var(--cannaiq-border); +} + +.cannaiq-terpenes--list .cannaiq-terpene-item:last-child { + border-bottom: none; +} + +/* ========================================================================== + Price Block + ========================================================================== */ + +.cannaiq-price-block { + display: flex; + align-items: baseline; + gap: var(--cannaiq-space-sm); + flex-wrap: wrap; +} + +.cannaiq-price-block--stacked { + flex-direction: column; + align-items: flex-start; + gap: var(--cannaiq-space-xs); +} + +.cannaiq-price-block__original { + color: var(--cannaiq-price-original); + text-decoration: line-through; + font-size: 0.875rem; +} + +.cannaiq-price-block__sale { + color: var(--cannaiq-price-sale); + font-weight: 700; + font-size: 1.25rem; +} + +.cannaiq-price-block__regular { + color: var(--cannaiq-text-primary); + font-weight: 700; + font-size: 1.25rem; +} + +.cannaiq-price-block__weight { + color: var(--cannaiq-text-muted); + font-size: 0.875rem; +} + +/* Price Sizes */ +.cannaiq-price-block--small .cannaiq-price-block__sale, +.cannaiq-price-block--small .cannaiq-price-block__regular { + font-size: 1rem; +} + +.cannaiq-price-block--small .cannaiq-price-block__original { + font-size: 0.75rem; +} + +.cannaiq-price-block--large .cannaiq-price-block__sale, +.cannaiq-price-block--large .cannaiq-price-block__regular { + font-size: 1.5rem; +} + +/* ========================================================================== + Cart Button + ========================================================================== */ + +.cannaiq-cart-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--cannaiq-space-sm); + padding: var(--cannaiq-space-md) var(--cannaiq-space-xl); + font-weight: 600; + font-size: 0.875rem; + text-decoration: none; + border-radius: var(--cannaiq-radius-md); + cursor: pointer; + transition: all 0.15s ease; + border: 2px solid transparent; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +/* Solid Style */ +.cannaiq-cart-button--solid { + background: var(--cannaiq-text-primary); + color: white; + border-color: var(--cannaiq-text-primary); +} + +.cannaiq-cart-button--solid:hover { + background: #374151; + border-color: #374151; + transform: translateY(-1px); + box-shadow: var(--cannaiq-shadow-md); +} + +/* Outline Style */ +.cannaiq-cart-button--outline { + background: transparent; + color: var(--cannaiq-text-primary); + border-color: var(--cannaiq-text-primary); +} + +.cannaiq-cart-button--outline:hover { + background: var(--cannaiq-text-primary); + color: white; +} + +/* Full Width */ +.cannaiq-cart-button--full { + width: 100%; +} + +/* Sizes */ +.cannaiq-cart-button--small { + padding: var(--cannaiq-space-sm) var(--cannaiq-space-md); + font-size: 0.75rem; +} + +.cannaiq-cart-button--large { + padding: var(--cannaiq-space-lg) var(--cannaiq-space-xl); + font-size: 1rem; +} + +/* ========================================================================== + Stock Indicator + ========================================================================== */ + +.cannaiq-stock-indicator { + display: inline-flex; + align-items: center; + gap: var(--cannaiq-space-xs); + font-size: 0.75rem; + font-weight: 500; +} + +.cannaiq-stock-indicator--in-stock { + color: var(--cannaiq-stock-in); +} + +.cannaiq-stock-indicator--out-of-stock { + color: var(--cannaiq-stock-out); +} + +/* Badge Style */ +.cannaiq-stock-indicator--badge { + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + border-radius: var(--cannaiq-radius-md); +} + +.cannaiq-stock-indicator--badge.cannaiq-stock-indicator--in-stock { + background: #dcfce7; +} + +.cannaiq-stock-indicator--badge.cannaiq-stock-indicator--out-of-stock { + background: #f3f4f6; +} + +/* Dot Indicator */ +.cannaiq-stock-indicator__dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; +} + +/* ========================================================================== + Product Image with Overlays + ========================================================================== */ + +.cannaiq-product-image { + position: relative; + overflow: hidden; + border-radius: var(--cannaiq-radius-lg); + background: var(--cannaiq-bg-light); +} + +.cannaiq-product-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.cannaiq-product-image:hover img { + transform: scale(1.05); +} + +.cannaiq-product-image__overlay { + position: absolute; + padding: var(--cannaiq-space-sm); +} + +.cannaiq-product-image__overlay--top-left { + top: 0; + left: 0; +} + +.cannaiq-product-image__overlay--top-right { + top: 0; + right: 0; +} + +.cannaiq-product-image__overlay--bottom-left { + bottom: 0; + left: 0; +} + +.cannaiq-product-image__overlay--bottom-right { + bottom: 0; + right: 0; +} + +/* Badge Stack in Overlays */ +.cannaiq-product-image__badges { + display: flex; + gap: var(--cannaiq-space-xs); + flex-wrap: wrap; +} + +/* ========================================================================== + Weight Options Selector + ========================================================================== */ + +.cannaiq-weight-options { + display: flex; + gap: var(--cannaiq-space-xs); + flex-wrap: wrap; +} + +.cannaiq-weight-option { + padding: var(--cannaiq-space-xs) var(--cannaiq-space-sm); + border: 1px solid var(--cannaiq-border); + border-radius: var(--cannaiq-radius-md); + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + background: white; +} + +.cannaiq-weight-option:hover { + border-color: var(--cannaiq-text-primary); +} + +.cannaiq-weight-option--selected { + background: var(--cannaiq-text-primary); + color: white; + border-color: var(--cannaiq-text-primary); +} + +.cannaiq-weight-option__price { + color: var(--cannaiq-text-secondary); + margin-left: var(--cannaiq-space-xs); +} + +.cannaiq-weight-option--selected .cannaiq-weight-option__price { + color: rgba(255, 255, 255, 0.8); +} + +/* Dropdown Style */ +.cannaiq-weight-options--dropdown { + display: block; +} + +.cannaiq-weight-options--dropdown select { + width: 100%; + padding: var(--cannaiq-space-sm) var(--cannaiq-space-md); + border: 1px solid var(--cannaiq-border); + border-radius: var(--cannaiq-radius-md); + font-size: 0.875rem; + background: white; + cursor: pointer; +} + +/* ========================================================================== + Card Container (for premade templates) + ========================================================================== */ + +.cannaiq-product-card { + display: flex; + flex-direction: column; + background: white; + border-radius: var(--cannaiq-radius-xl); + overflow: hidden; + box-shadow: var(--cannaiq-shadow-sm); + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.cannaiq-product-card:hover { + box-shadow: var(--cannaiq-shadow-lg); + transform: translateY(-2px); +} + +.cannaiq-product-card__image { + position: relative; + aspect-ratio: 1; + overflow: hidden; +} + +.cannaiq-product-card__body { + padding: var(--cannaiq-space-lg); + display: flex; + flex-direction: column; + gap: var(--cannaiq-space-sm); + flex: 1; +} + +.cannaiq-product-card__title { + font-size: 1rem; + font-weight: 700; + color: var(--cannaiq-text-primary); + margin: 0; + line-height: 1.3; +} + +.cannaiq-product-card__brand { + font-size: 0.875rem; + color: var(--cannaiq-text-secondary); + margin: 0; +} + +.cannaiq-product-card__footer { + margin-top: auto; + padding-top: var(--cannaiq-space-md); +} + +/* ========================================================================== + Utility Classes + ========================================================================== */ + +.cannaiq-flex { + display: flex; +} + +.cannaiq-flex-wrap { + flex-wrap: wrap; +} + +.cannaiq-items-center { + align-items: center; +} + +.cannaiq-justify-between { + justify-content: space-between; +} + +.cannaiq-gap-xs { + gap: var(--cannaiq-space-xs); +} + +.cannaiq-gap-sm { + gap: var(--cannaiq-space-sm); +} + +.cannaiq-gap-md { + gap: var(--cannaiq-space-md); +} + +.cannaiq-gap-lg { + gap: var(--cannaiq-space-lg); +} + +.cannaiq-mt-auto { + margin-top: auto; +} + +.cannaiq-text-center { + text-align: center; +} + +.cannaiq-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/wordpress-plugin/cannaiq-menus.php b/wordpress-plugin/cannaiq-menus.php index 92ca4579..b01f46fc 100644 --- a/wordpress-plugin/cannaiq-menus.php +++ b/wordpress-plugin/cannaiq-menus.php @@ -1,10 +1,10 @@ add_category( 'cannaiq', [ - 'title' => __('CannaIQ', 'cannaiq-menus'), + 'title' => __('CannaiQ', 'cannaiq-menus'), 'icon' => 'fa fa-cannabis', ] ); @@ -60,9 +60,13 @@ class CannaIQ_Menus_Plugin { // Initialize plugin load_plugin_textdomain('cannaiq-menus', false, dirname(plugin_basename(__FILE__)) . '/languages'); + // Load helper functions + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'includes/effects-icons.php'; + // Load Elementor Dynamic Tags (if Elementor is active) if (did_action('elementor/loaded')) { require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/dynamic-tags-extended.php'; } // Register shortcodes - primary CannaIQ shortcodes @@ -78,6 +82,7 @@ class CannaIQ_Menus_Plugin { * Register Elementor Widgets */ public function register_elementor_widgets($widgets_manager) { + // Legacy widgets require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-grid.php'; require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/single-product.php'; require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/brand-grid.php'; @@ -85,18 +90,46 @@ class CannaIQ_Menus_Plugin { require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/specials-grid.php'; require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-loop.php'; + // Modular component widgets (v2.0) + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/discount-ribbon.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/strain-badge.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/thc-meter.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/effects-display.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/price-block.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/cart-button.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/stock-indicator.php'; + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/product-image-overlay.php'; + + // Card templates (v2.0) + require_once CANNAIQ_MENUS_PLUGIN_DIR . 'widgets/card-template-premium.php'; + + // Register legacy widgets $widgets_manager->register(new \CannaIQ_Menus_Product_Grid_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Single_Product_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Brand_Grid_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Category_List_Widget()); $widgets_manager->register(new \CannaIQ_Menus_Specials_Grid_Widget()); $widgets_manager->register(new \CannaIQ_Product_Loop_Widget()); + + // Register modular component widgets (v2.0) + $widgets_manager->register(new \CannaIQ_Discount_Ribbon_Widget()); + $widgets_manager->register(new \CannaIQ_Strain_Badge_Widget()); + $widgets_manager->register(new \CannaIQ_THC_Meter_Widget()); + $widgets_manager->register(new \CannaIQ_Effects_Display_Widget()); + $widgets_manager->register(new \CannaIQ_Price_Block_Widget()); + $widgets_manager->register(new \CannaIQ_Cart_Button_Widget()); + $widgets_manager->register(new \CannaIQ_Stock_Indicator_Widget()); + $widgets_manager->register(new \CannaIQ_Product_Image_Overlay_Widget()); + + // Register card templates (v2.0) + $widgets_manager->register(new \CannaIQ_Premium_Card_Widget()); } /** * Enqueue Scripts and Styles */ public function enqueue_scripts() { + // Base styles wp_enqueue_style( 'cannaiq-menus-styles', CANNAIQ_MENUS_PLUGIN_URL . 'assets/css/cannaiq-menus.css', @@ -104,6 +137,14 @@ class CannaIQ_Menus_Plugin { CANNAIQ_MENUS_VERSION ); + // Component styles (v2.0 modular components) + wp_enqueue_style( + 'cannaiq-components-styles', + CANNAIQ_MENUS_PLUGIN_URL . 'assets/css/components.css', + ['cannaiq-menus-styles'], + CANNAIQ_MENUS_VERSION + ); + wp_enqueue_script( 'cannaiq-menus-script', CANNAIQ_MENUS_PLUGIN_URL . 'assets/js/cannaiq-menus.js', @@ -118,8 +159,8 @@ class CannaIQ_Menus_Plugin { */ public function add_admin_menu() { add_menu_page( - 'CannaIQ Menus', - 'CannaIQ Menus', + 'CannaiQ Menus', + 'CannaiQ Menus', 'manage_options', 'cannaiq-menus', [$this, 'admin_page'], @@ -147,9 +188,9 @@ class CannaIQ_Menus_Plugin { public function admin_page() { ?>

-

CannaIQ Menus Settings

-

Version by CannaIQ

-

Display real-time cannabis menus with data updated daily from CannaIQ.

+

CannaiQ Menus Settings

+

Version by CannaiQ

+

Display real-time cannabis menus with data updated daily from CannaiQ.

@@ -162,7 +203,7 @@ class CannaIQ_Menus_Plugin { -

Your authentication token from the CannaIQ admin dashboard. The token includes your store configuration.

+

Your authentication token from the CannaiQ admin dashboard. The token includes your store configuration.

@@ -300,10 +341,10 @@ class CannaIQ_Menus_Plugin {

Elementor Widgets

-

If you have Elementor installed, you can use the CannaIQ widgets:

+

If you have Elementor installed, you can use the CannaiQ widgets:

    -
  • CannaIQ Product Grid - Display a grid of products with filtering options
  • -
  • CannaIQ Single Product - Display a single product card
  • +
  • CannaiQ Product Grid - Display a grid of products with filtering options
  • +
  • CannaiQ Single Product - Display a single product card
16, + 'class' => '', + 'color' => 'currentColor', + ]; + $args = wp_parse_args($args, $defaults); + + $effect_key = strtolower(trim($effect)); + $icons = cannaiq_get_effect_icons(); + + if (!isset($icons[$effect_key])) { + return ''; + } + + $svg = $icons[$effect_key]; + $size = intval($args['size']); + $class = esc_attr($args['class']); + $color = esc_attr($args['color']); + + // Replace placeholders in SVG + $svg = str_replace( + ['{SIZE}', '{CLASS}', '{COLOR}'], + [$size, $class, $color], + $svg + ); + + return $svg; +} + +/** + * Get all effect icons + * + * @return array Associative array of effect => SVG + */ +function cannaiq_get_effect_icons() { + return [ + 'happy' => '', + + 'relaxed' => '', + + 'sleepy' => '', + + 'euphoric' => '', + + 'creative' => '', + + 'energetic' => '', + + 'focused' => '', + + 'hungry' => '', + + 'uplifted' => '', + + 'talkative' => '', + + 'giggly' => '', + + 'aroused' => '', + + 'tingly' => '', + + 'calm' => '', + + 'sedated' => '', + ]; +} + +/** + * Get effect color + * + * @param string $effect Effect name + * @return string Hex color code + */ +function cannaiq_get_effect_color($effect) { + $colors = [ + 'happy' => '#FFD700', // Gold + 'relaxed' => '#87CEEB', // Sky blue + 'sleepy' => '#9370DB', // Medium purple + 'euphoric' => '#FF69B4', // Hot pink + 'creative' => '#FF8C00', // Dark orange + 'energetic' => '#32CD32', // Lime green + 'focused' => '#4169E1', // Royal blue + 'hungry' => '#FF6347', // Tomato + 'uplifted' => '#00CED1', // Dark turquoise + 'talkative' => '#DDA0DD', // Plum + 'giggly' => '#FFB6C1', // Light pink + 'aroused' => '#DC143C', // Crimson + 'tingly' => '#8A2BE2', // Blue violet + 'calm' => '#98FB98', // Pale green + 'sedated' => '#708090', // Slate gray + ]; + + $key = strtolower(trim($effect)); + return isset($colors[$key]) ? $colors[$key] : '#6B7280'; // Default gray +} + +/** + * Render effect chip HTML + * + * @param string $effect Effect name + * @param array $args Optional args: show_icon, size, class + * @return string HTML for effect chip + */ +function cannaiq_render_effect_chip($effect, $args = []) { + $defaults = [ + 'show_icon' => true, + 'size' => 'medium', + 'class' => '', + ]; + $args = wp_parse_args($args, $defaults); + + $effect_name = ucfirst(strtolower(trim($effect))); + $color = cannaiq_get_effect_color($effect); + $size_class = 'cannaiq-effect-chip--' . esc_attr($args['size']); + $extra_class = esc_attr($args['class']); + + $icon_html = ''; + if ($args['show_icon']) { + $icon_html = cannaiq_get_effect_icon($effect, [ + 'size' => $args['size'] === 'small' ? 12 : ($args['size'] === 'large' ? 20 : 16), + 'color' => $color, + ]); + } + + return sprintf( + '%s%s', + $size_class, + $extra_class, + esc_attr($color), + $icon_html, + esc_html($effect_name) + ); +} + +/** + * Render multiple effect chips + * + * @param array $effects Array of effect names + * @param array $args Optional args: limit, show_icon, size + * @return string HTML for all effect chips + */ +function cannaiq_render_effects($effects, $args = []) { + $defaults = [ + 'limit' => 3, + 'show_icon' => true, + 'size' => 'medium', + 'class' => '', + ]; + $args = wp_parse_args($args, $defaults); + + if (!is_array($effects)) { + return ''; + } + + $effects = array_slice($effects, 0, intval($args['limit'])); + $chips = array_map(function($effect) use ($args) { + return cannaiq_render_effect_chip($effect, [ + 'show_icon' => $args['show_icon'], + 'size' => $args['size'], + ]); + }, $effects); + + return sprintf( + '
%s
', + esc_attr($args['class']), + implode('', $chips) + ); +} diff --git a/wordpress-plugin/widgets/brand-grid.php b/wordpress-plugin/widgets/brand-grid.php index d16dbe74..9acc722f 100644 --- a/wordpress-plugin/widgets/brand-grid.php +++ b/wordpress-plugin/widgets/brand-grid.php @@ -14,7 +14,7 @@ class CannaIQ_Menus_Brand_Grid_Widget extends \Elementor\Widget_Base { } public function get_title() { - return __('CannaIQ Brand Grid', 'cannaiq-menus'); + return __('CannaiQ Brand Grid', 'cannaiq-menus'); } public function get_icon() { diff --git a/wordpress-plugin/widgets/card-template-premium.php b/wordpress-plugin/widgets/card-template-premium.php new file mode 100644 index 00000000..63e2881e --- /dev/null +++ b/wordpress-plugin/widgets/card-template-premium.php @@ -0,0 +1,510 @@ +start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'content_note', + [ + 'type' => \Elementor\Controls_Manager::RAW_HTML, + 'raw' => __('This card uses the current product context from Product Loop or Product Grid. Place it inside a CannaiQ Product Loop widget.', 'cannaiq-menus'), + 'content_classes' => 'elementor-panel-alert elementor-panel-alert-info', + ] + ); + + $this->end_controls_section(); + + // Components Section + $this->start_controls_section( + 'components_section', + [ + 'label' => __('Components', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'show_discount', + [ + 'label' => __('Show Discount Ribbon', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_image', + [ + 'label' => __('Show Product Image', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_strain_badge', + [ + 'label' => __('Show Strain Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_thc_badge', + [ + 'label' => __('Show THC Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_name', + [ + 'label' => __('Show Product Name', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_brand', + [ + 'label' => __('Show Brand', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_effects', + [ + 'label' => __('Show Effects', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'effects_limit', + [ + 'label' => __('Effects Limit', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 3, + 'min' => 1, + 'max' => 5, + 'condition' => [ + 'show_effects' => 'yes', + ], + ] + ); + + $this->add_control( + 'show_price', + [ + 'label' => __('Show Price', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_weight', + [ + 'label' => __('Show Weight', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_cart_button', + [ + 'label' => __('Show Cart Button', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'button_text', + [ + 'label' => __('Button Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'ADD TO CART', + 'condition' => [ + 'show_cart_button' => 'yes', + ], + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'card_style_section', + [ + 'label' => __('Card Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'card_background', + [ + 'label' => __('Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card' => 'background-color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'card_border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 30, + ], + ], + 'default' => [ + 'size' => 12, + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card' => 'border-radius: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->add_control( + 'card_padding', + [ + 'label' => __('Padding', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 40, + ], + ], + 'default' => [ + 'size' => 16, + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card__body' => 'padding: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Box_Shadow::get_type(), + [ + 'name' => 'card_shadow', + 'selector' => '{{WRAPPER}} .cannaiq-product-card', + ] + ); + + $this->end_controls_section(); + + // Typography Section + $this->start_controls_section( + 'typography_section', + [ + 'label' => __('Typography', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'title_typography', + 'label' => __('Title', 'cannaiq-menus'), + 'selector' => '{{WRAPPER}} .cannaiq-product-card__title', + ] + ); + + $this->add_control( + 'title_color', + [ + 'label' => __('Title Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1f2937', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card__title' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'brand_color', + [ + 'label' => __('Brand Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#6b7280', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-card__brand' => 'color: {{VALUE}};', + ], + ] + ); + + $this->end_controls_section(); + + // Button Style Section + $this->start_controls_section( + 'button_style_section', + [ + 'label' => __('Button Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + 'condition' => [ + 'show_cart_button' => 'yes', + ], + ] + ); + + $this->add_control( + 'button_background', + [ + 'label' => __('Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1f2937', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button' => 'background-color: {{VALUE}}; border-color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'button_text_color', + [ + 'label' => __('Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'button_hover_background', + [ + 'label' => __('Hover Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#374151', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button:hover' => 'background-color: {{VALUE}}; border-color: {{VALUE}};', + ], + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + global $cannaiq_current_product; + $product = $cannaiq_current_product ?? []; + + if (empty($product)) { + echo '

' . __('No product context available. Place this widget inside a Product Loop.', 'cannaiq-menus') . '

'; + return; + } + + // Extract product data + $name = $product['Name'] ?? $product['name'] ?? ''; + $brand = $product['brand']['name'] ?? $product['brandName'] ?? $product['brand'] ?? ''; + $image_url = $product['Image'] ?? $product['images'][0]['url'] ?? $product['image_url'] ?? ''; + $strain_type = strtolower($product['strainType'] ?? $product['strain_type'] ?? ''); + $thc = $product['THCContent']['range'][0] ?? $product['THC'] ?? $product['thc_percentage'] ?? null; + $weight = $product['Options'][0] ?? $product['rawOptions'][0] ?? $product['weight'] ?? ''; + $menu_url = $product['menuUrl'] ?? $product['menu_url'] ?? $product['productUrl'] ?? '#'; + + // Price + $original_price = $product['Prices'][0] ?? $product['regular_price'] ?? null; + $sale_price = $product['specialPrice'] ?? $product['sale_price'] ?? null; + $is_on_sale = $sale_price && $sale_price > 0 && $sale_price < $original_price; + $discount_percent = 0; + if ($is_on_sale && $original_price > 0) { + $discount_percent = round((($original_price - $sale_price) / $original_price) * 100); + } + + // Effects + $effects = $product['effects'] ?? []; + if (!empty($effects) && !isset($effects[0])) { + arsort($effects); + $effects = array_keys($effects); + } + $effects_limit = intval($settings['effects_limit']) ?: 3; + $effects = array_slice($effects, 0, $effects_limit); + + // Strain colors + $strain_colors = [ + 'sativa' => '#22c55e', + 'indica' => '#8b5cf6', + 'hybrid' => '#f97316', + ]; + + ?> +
+ + +
+
+ + <?php echo esc_attr($name); ?> + + + + 0): ?> +
+ % OFF +
+ + + + 0)): ?> +
+
+ + + + 0): ?> + % THC + +
+
+ +
+
+ + + +
+ +

+ + + +

by

+ + + +
+ $effects_limit, 'show_icon' => true, 'size' => 'small']); ?> +
+ + + + +
+
+ start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'button_text', + [ + 'label' => __('Button Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'ADD TO CART', + ] + ); + + $this->add_control( + 'link_source', + [ + 'label' => __('Link Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom URL', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_url', + [ + 'label' => __('Custom URL', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::URL, + 'placeholder' => 'https://dutchie.com/store/...', + 'condition' => [ + 'link_source' => 'custom', + ], + ] + ); + + $this->add_control( + 'open_in_new_tab', + [ + 'label' => __('Open in New Tab', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_icon', + [ + 'label' => __('Show Icon', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => '', + ] + ); + + $this->add_control( + 'icon_position', + [ + 'label' => __('Icon Position', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'after', + 'options' => [ + 'before' => __('Before Text', 'cannaiq-menus'), + 'after' => __('After Text', 'cannaiq-menus'), + ], + 'condition' => [ + 'show_icon' => 'yes', + ], + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'button_style', + [ + 'label' => __('Button Style', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'solid', + 'options' => [ + 'solid' => __('Solid', 'cannaiq-menus'), + 'outline' => __('Outline', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'full_width', + [ + 'label' => __('Full Width', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => '', + ] + ); + + $this->add_control( + 'size', + [ + 'label' => __('Size', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'medium', + 'options' => [ + 'small' => __('Small', 'cannaiq-menus'), + 'medium' => __('Medium', 'cannaiq-menus'), + 'large' => __('Large', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'background_color', + [ + 'label' => __('Background Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1f2937', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button--solid' => 'background-color: {{VALUE}}; border-color: {{VALUE}};', + '{{WRAPPER}} .cannaiq-cart-button--outline' => 'border-color: {{VALUE}};', + '{{WRAPPER}} .cannaiq-cart-button--outline:hover' => 'background-color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'text_color', + [ + 'label' => __('Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button--solid' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'outline_text_color', + [ + 'label' => __('Outline Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1f2937', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button--outline' => 'color: {{VALUE}};', + ], + 'condition' => [ + 'button_style' => 'outline', + ], + ] + ); + + $this->add_control( + 'hover_background_color', + [ + 'label' => __('Hover Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#374151', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button--solid:hover' => 'background-color: {{VALUE}}; border-color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 50, + ], + ], + 'default' => [ + 'size' => 6, + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-cart-button' => 'border-radius: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'typography', + 'selector' => '{{WRAPPER}} .cannaiq-cart-button', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + // Get URL + $url = '#'; + if ($settings['link_source'] === 'custom' && !empty($settings['custom_url']['url'])) { + $url = $settings['custom_url']['url']; + } else { + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $url = $cannaiq_current_product['menuUrl'] + ?? $cannaiq_current_product['menu_url'] + ?? $cannaiq_current_product['productUrl'] + ?? '#'; + } + } + + // Build classes + $classes = [ + 'cannaiq-cart-button', + 'cannaiq-cart-button--' . $settings['button_style'], + ]; + if ($settings['full_width'] === 'yes') { + $classes[] = 'cannaiq-cart-button--full'; + } + if ($settings['size'] !== 'medium') { + $classes[] = 'cannaiq-cart-button--' . $settings['size']; + } + + // Target attribute + $target = $settings['open_in_new_tab'] === 'yes' ? ' target="_blank" rel="noopener noreferrer"' : ''; + + // Icon SVG (arrow right) + $icon = ''; + + ?> + > + + + + + + + + + start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'source', + [ + 'label' => __('Discount Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom value', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_discount', + [ + 'label' => __('Discount Percentage', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 25, + 'min' => 1, + 'max' => 99, + 'condition' => [ + 'source' => 'custom', + ], + ] + ); + + $this->add_control( + 'format', + [ + 'label' => __('Display Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'ribbon', + 'options' => [ + 'ribbon' => __('Ribbon', 'cannaiq-menus'), + 'pill' => __('Pill', 'cannaiq-menus'), + 'text' => __('Text Only', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'text_template', + [ + 'label' => __('Text Template', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => '{percent}% OFF', + 'description' => __('Use {percent} as placeholder', 'cannaiq-menus'), + ] + ); + + $this->add_control( + 'hide_if_no_discount', + [ + 'label' => __('Hide if No Discount', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'size', + [ + 'label' => __('Size', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'medium', + 'options' => [ + 'small' => __('Small', 'cannaiq-menus'), + 'medium' => __('Medium', 'cannaiq-menus'), + 'large' => __('Large', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'background_color', + [ + 'label' => __('Background Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ef4444', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-discount-ribbon' => 'background-color: {{VALUE}};', + ], + 'condition' => [ + 'format!' => 'text', + ], + ] + ); + + $this->add_control( + 'text_color', + [ + 'label' => __('Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#ffffff', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-discount-ribbon' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'typography', + 'selector' => '{{WRAPPER}} .cannaiq-discount-ribbon', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + // Get discount percentage + $discount = 0; + if ($settings['source'] === 'custom') { + $discount = intval($settings['custom_discount']); + } else { + // Get from product context + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $original = $cannaiq_current_product['Prices'][0] ?? $cannaiq_current_product['regular_price'] ?? null; + $sale = $cannaiq_current_product['specialPrice'] ?? $cannaiq_current_product['sale_price'] ?? null; + + if ($original && $sale && $original > $sale) { + $discount = round((($original - $sale) / $original) * 100); + } + } + } + + // Hide if no discount and setting enabled + if ($discount <= 0 && $settings['hide_if_no_discount'] === 'yes') { + return; + } + + // Build display text + $text = str_replace('{percent}', $discount, $settings['text_template']); + + // Build classes + $classes = [ + 'cannaiq-discount-ribbon', + 'cannaiq-discount-ribbon--' . $settings['format'], + ]; + if ($settings['size'] !== 'medium') { + $classes[] = 'cannaiq-discount-ribbon--' . $settings['size']; + } + + printf( + '%s', + esc_attr(implode(' ', $classes)), + esc_html($text) + ); + } +} diff --git a/wordpress-plugin/widgets/dynamic-tags-extended.php b/wordpress-plugin/widgets/dynamic-tags-extended.php new file mode 100644 index 00000000..f30b7d37 --- /dev/null +++ b/wordpress-plugin/widgets/dynamic-tags-extended.php @@ -0,0 +1,793 @@ +register(new CannaIQ_Discount_Percent_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Discount_Badge_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Strain_Badge_Tag()); + $dynamic_tags_manager->register(new CannaIQ_THC_Badge_Tag()); + $dynamic_tags_manager->register(new CannaIQ_CBD_Badge_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Effects_Chips_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Single_Effect_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Terpenes_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Price_Display_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Sale_Price_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Original_Price_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Menu_URL_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Subcategory_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Stock_Quantity_Tag()); + $dynamic_tags_manager->register(new CannaIQ_Stock_Status_Tag()); +}, 20); // Priority 20 to run after base tags + +/** + * Discount Percentage Tag + */ +class CannaIQ_Discount_Percent_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-discount-percent'; + } + + public function get_title() { + return __('Discount %', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('format', [ + 'label' => __('Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'off', + 'options' => [ + 'off' => 'XX% OFF', + 'percent' => 'XX%', + 'number' => 'XX', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $format = $this->get_settings('format'); + + $original = $product['Prices'][0] ?? $product['regular_price'] ?? null; + $sale = $product['specialPrice'] ?? $product['sale_price'] ?? null; + + if (!$original || !$sale || $original <= $sale) { + return; + } + + $percent = round((($original - $sale) / $original) * 100); + + switch ($format) { + case 'off': + echo esc_html($percent . '% OFF'); + break; + case 'percent': + echo esc_html($percent . '%'); + break; + case 'number': + echo esc_html($percent); + break; + } + } +} + +/** + * Discount Badge Tag (HTML) + */ +class CannaIQ_Discount_Badge_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-discount-badge'; + } + + public function get_title() { + return __('Discount Badge', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('style', [ + 'label' => __('Style', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'ribbon', + 'options' => [ + 'ribbon' => 'Ribbon', + 'pill' => 'Pill', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $style = $this->get_settings('style'); + + $original = $product['Prices'][0] ?? $product['regular_price'] ?? null; + $sale = $product['specialPrice'] ?? $product['sale_price'] ?? null; + + if (!$original || !$sale || $original <= $sale) { + return; + } + + $percent = round((($original - $sale) / $original) * 100); + $class = 'cannaiq-discount-ribbon cannaiq-discount-ribbon--' . $style; + + printf( + '%s%% OFF', + esc_attr($class), + esc_html($percent) + ); + } +} + +/** + * Strain Badge Tag (HTML) + */ +class CannaIQ_Strain_Badge_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-strain-badge'; + } + + public function get_title() { + return __('Strain Badge', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('style', [ + 'label' => __('Style', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'pill', + 'options' => [ + 'pill' => 'Pill', + 'text' => 'Text Only', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $style = $this->get_settings('style'); + + $strain = strtolower($product['strainType'] ?? $product['strain_type'] ?? ''); + if (empty($strain) || !in_array($strain, ['sativa', 'indica', 'hybrid'])) { + return; + } + + $colors = [ + 'sativa' => '#22c55e', + 'indica' => '#8b5cf6', + 'hybrid' => '#f97316', + ]; + $color = $colors[$strain]; + + $class = 'cannaiq-strain-badge cannaiq-strain-badge--' . $style . ' cannaiq-strain-badge--' . $strain; + $css_style = $style === 'pill' + ? sprintf('background-color: %s; color: white;', $color) + : sprintf('color: %s;', $color); + + printf( + '%s', + esc_attr($class), + esc_attr($css_style), + esc_html(strtoupper($strain)) + ); + } +} + +/** + * THC Badge Tag (HTML) + */ +class CannaIQ_THC_Badge_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-thc-badge'; + } + + public function get_title() { + return __('THC Badge', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('style', [ + 'label' => __('Style', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'badge', + 'options' => [ + 'badge' => 'Badge', + 'pill' => 'Pill', + 'text' => 'Text Only', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $style = $this->get_settings('style'); + + $thc = $product['THCContent']['range'][0] + ?? $product['THC'] + ?? $product['thc_percentage'] + ?? null; + + if (!$thc || $thc <= 0) { + return; + } + + $class = 'cannaiq-potency-badge cannaiq-potency-badge--' . $style; + $formatted = number_format((float)$thc, 1) . '% THC'; + + printf( + '%s', + esc_attr($class), + esc_html($formatted) + ); + } +} + +/** + * CBD Badge Tag (HTML) + */ +class CannaIQ_CBD_Badge_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-cbd-badge'; + } + + public function get_title() { + return __('CBD Badge', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('style', [ + 'label' => __('Style', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'badge', + 'options' => [ + 'badge' => 'Badge', + 'pill' => 'Pill', + 'text' => 'Text Only', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $style = $this->get_settings('style'); + + $cbd = $product['CBDContent']['range'][0] + ?? $product['CBD'] + ?? $product['cbd_percentage'] + ?? null; + + if (!$cbd || $cbd <= 0) { + return; + } + + $class = 'cannaiq-potency-badge cannaiq-potency-badge--' . $style; + $formatted = number_format((float)$cbd, 1) . '% CBD'; + + printf( + '%s', + esc_attr($class), + esc_html($formatted) + ); + } +} + +/** + * Effects Chips Tag (HTML) + */ +class CannaIQ_Effects_Chips_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-effects-chips'; + } + + public function get_title() { + return __('Effects Chips', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('limit', [ + 'label' => __('Max Effects', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 3, + 'min' => 1, + 'max' => 10, + ]); + + $this->add_control('show_icons', [ + 'label' => __('Show Icons', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ]); + } + + public function render() { + $product = $this->get_current_product(); + $limit = (int)$this->get_settings('limit') ?: 3; + $show_icons = $this->get_settings('show_icons') === 'yes'; + + $effects = $product['effects'] ?? []; + if (empty($effects) || !is_array($effects)) { + return; + } + + // If associative array with scores, sort by score + if (!isset($effects[0])) { + arsort($effects); + $effects = array_keys($effects); + } + + $effects = array_slice($effects, 0, $limit); + + echo cannaiq_render_effects($effects, [ + 'limit' => $limit, + 'show_icon' => $show_icons, + 'size' => 'medium', + ]); + } +} + +/** + * Single Effect Tag + */ +class CannaIQ_Single_Effect_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-single-effect'; + } + + public function get_title() { + return __('Single Effect', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('effect_index', [ + 'label' => __('Effect Index', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 1, + 'min' => 1, + 'max' => 10, + 'description' => __('1 = first effect, 2 = second, etc.', 'cannaiq-menus'), + ]); + + $this->add_control('show_icon', [ + 'label' => __('Show Icon', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ]); + } + + public function render() { + $product = $this->get_current_product(); + $index = (int)$this->get_settings('effect_index') - 1; // Convert to 0-based + $show_icon = $this->get_settings('show_icon') === 'yes'; + + $effects = $product['effects'] ?? []; + if (empty($effects) || !is_array($effects)) { + return; + } + + // If associative array with scores, sort by score and get keys + if (!isset($effects[0])) { + arsort($effects); + $effects = array_keys($effects); + } + + if (!isset($effects[$index])) { + return; + } + + $effect = $effects[$index]; + echo cannaiq_render_effect_chip($effect, [ + 'show_icon' => $show_icon, + 'size' => 'medium', + ]); + } +} + +/** + * Terpenes Tag (HTML) + */ +class CannaIQ_Terpenes_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-terpenes'; + } + + public function get_title() { + return __('Terpenes', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('format', [ + 'label' => __('Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'chips', + 'options' => [ + 'chips' => 'Chips', + 'list' => 'List', + 'text' => 'Text', + ], + ]); + + $this->add_control('limit', [ + 'label' => __('Max Terpenes', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 3, + 'min' => 1, + 'max' => 10, + ]); + } + + public function render() { + $product = $this->get_current_product(); + $format = $this->get_settings('format'); + $limit = (int)$this->get_settings('limit') ?: 3; + + $terpenes = $product['terpenes'] ?? []; + if (empty($terpenes) || !is_array($terpenes)) { + return; + } + + $terpenes = array_slice($terpenes, 0, $limit); + + switch ($format) { + case 'chips': + echo '
'; + foreach ($terpenes as $terp) { + $name = $terp['name'] ?? ''; + $percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : ''; + printf( + '%s%s', + esc_html($name), + esc_html($percent) + ); + } + echo '
'; + break; + + case 'list': + echo '
'; + foreach ($terpenes as $terp) { + $name = $terp['name'] ?? ''; + $percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : ''; + printf( + '
%s%s
', + esc_html($name), + esc_html($percent) + ); + } + echo '
'; + break; + + case 'text': + $parts = []; + foreach ($terpenes as $terp) { + $name = $terp['name'] ?? ''; + $percent = isset($terp['percent']) ? number_format((float)$terp['percent'], 2) . '%' : ''; + $parts[] = $name . ($percent ? ' ' . $percent : ''); + } + echo esc_html(implode(', ', $parts)); + break; + } + } +} + +/** + * Price Display Tag (with sale handling) + */ +class CannaIQ_Price_Display_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-price-display'; + } + + public function get_title() { + return __('Price Display', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('show_original', [ + 'label' => __('Show Original on Sale', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ]); + } + + public function render() { + $product = $this->get_current_product(); + $show_original = $this->get_settings('show_original') === 'yes'; + + $original = $product['Prices'][0] ?? $product['regular_price'] ?? null; + $sale = $product['specialPrice'] ?? $product['sale_price'] ?? null; + + if (!$original || $original <= 0) { + return; + } + + $is_on_sale = $sale && $sale > 0 && $sale < $original; + + echo ''; + if ($is_on_sale) { + if ($show_original) { + printf( + '$%s', + esc_html(number_format((float)$original, 2)) + ); + } + printf( + '$%s', + esc_html(number_format((float)$sale, 2)) + ); + } else { + printf( + '$%s', + esc_html(number_format((float)$original, 2)) + ); + } + echo ''; + } +} + +/** + * Sale Price Tag + */ +class CannaIQ_Sale_Price_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-price-sale'; + } + + public function get_title() { + return __('Sale Price', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + + $sale = $product['specialPrice'] ?? $product['sale_price'] ?? null; + + if ($sale && $sale > 0) { + echo '$' . number_format((float)$sale, 2); + } + } +} + +/** + * Original Price Tag + */ +class CannaIQ_Original_Price_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-price-original'; + } + + public function get_title() { + return __('Original Price', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + + $original = $product['Prices'][0] ?? $product['regular_price'] ?? null; + + if ($original && $original > 0) { + echo '$' . number_format((float)$original, 2); + } + } +} + +/** + * Menu URL Tag + */ +class CannaIQ_Menu_URL_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-menu-url'; + } + + public function get_title() { + return __('Menu URL', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::URL_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + + $url = $product['menuUrl'] + ?? $product['menu_url'] + ?? $product['productUrl'] + ?? ''; + + echo esc_url($url); + } +} + +/** + * Subcategory Tag + */ +class CannaIQ_Subcategory_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-subcategory'; + } + + public function get_title() { + return __('Subcategory', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + + $subcategory = $product['subcategory'] + ?? $product['subCategory'] + ?? ''; + + echo esc_html($subcategory); + } +} + +/** + * Stock Quantity Tag + */ +class CannaIQ_Stock_Quantity_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-stock-qty'; + } + + public function get_title() { + return __('Stock Quantity', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + public function render() { + $product = $this->get_current_product(); + + $qty = $product['POSMetaData']['children'][0]['quantity'] + ?? $product['quantity'] + ?? null; + + if ($qty !== null) { + echo (int)$qty; + } + } +} + +/** + * Stock Status Tag (HTML badge) + */ +class CannaIQ_Stock_Status_Tag extends CannaIQ_Dynamic_Tag_Base { + + public function get_name() { + return 'cannaiq-stock-status'; + } + + public function get_title() { + return __('Stock Status Badge', 'cannaiq-menus'); + } + + public function get_categories() { + return [\Elementor\Modules\DynamicTags\Module::TEXT_CATEGORY]; + } + + protected function register_controls() { + $this->add_control('style', [ + 'label' => __('Style', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'badge', + 'options' => [ + 'badge' => 'Badge', + 'text' => 'Text', + 'dot' => 'Dot + Text', + ], + ]); + } + + public function render() { + $product = $this->get_current_product(); + $style = $this->get_settings('style'); + + $status = $product['Status'] ?? ''; + $in_stock = ($status === 'Active' || $status === 'In Stock' || !empty($product['in_stock'])); + + $text = $in_stock ? 'In Stock' : 'Out of Stock'; + $class = 'cannaiq-stock-indicator cannaiq-stock-indicator--' . ($in_stock ? 'in-stock' : 'out-of-stock'); + + if ($style === 'badge') { + $class .= ' cannaiq-stock-indicator--badge'; + } + + printf('', esc_attr($class)); + + if ($style === 'dot') { + echo ''; + } + + echo esc_html($text); + echo ''; + } +} diff --git a/wordpress-plugin/widgets/dynamic-tags.php b/wordpress-plugin/widgets/dynamic-tags.php index 305f2680..641de373 100644 --- a/wordpress-plugin/widgets/dynamic-tags.php +++ b/wordpress-plugin/widgets/dynamic-tags.php @@ -17,7 +17,7 @@ add_action('elementor/dynamic_tags/register', function($dynamic_tags_manager) { // Register CannaIQ group $dynamic_tags_manager->register_group('cannaiq', [ - 'title' => __('CannaIQ Product', 'cannaiq-menus') + 'title' => __('CannaiQ Product', 'cannaiq-menus') ]); // Register all tags diff --git a/wordpress-plugin/widgets/effects-display.php b/wordpress-plugin/widgets/effects-display.php new file mode 100644 index 00000000..781d9f30 --- /dev/null +++ b/wordpress-plugin/widgets/effects-display.php @@ -0,0 +1,288 @@ +start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'source', + [ + 'label' => __('Effects Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom values', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_effects', + [ + 'label' => __('Custom Effects', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'Happy, Relaxed, Creative', + 'description' => __('Comma-separated list of effects', 'cannaiq-menus'), + 'condition' => [ + 'source' => 'custom', + ], + ] + ); + + $this->add_control( + 'limit', + [ + 'label' => __('Max Effects', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 3, + 'min' => 1, + 'max' => 10, + ] + ); + + $this->add_control( + 'show_icons', + [ + 'label' => __('Show Icons', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'use_colors', + [ + 'label' => __('Colored Chips', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + 'description' => __('Use effect-specific colors', 'cannaiq-menus'), + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'size', + [ + 'label' => __('Size', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'medium', + 'options' => [ + 'small' => __('Small', 'cannaiq-menus'), + 'medium' => __('Medium', 'cannaiq-menus'), + 'large' => __('Large', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'gap', + [ + 'label' => __('Gap', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 2, + 'max' => 20, + ], + ], + 'default' => [ + 'size' => 8, + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-effects-container' => 'gap: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->add_control( + 'default_background', + [ + 'label' => __('Default Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f3f4f6', + 'condition' => [ + 'use_colors!' => 'yes', + ], + ] + ); + + $this->add_control( + 'text_color', + [ + 'label' => __('Text Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1f2937', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-effect-chip' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'typography', + 'selector' => '{{WRAPPER}} .cannaiq-effect-chip', + ] + ); + + $this->add_control( + 'border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 50, + ], + ], + 'default' => [ + 'size' => 999, + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-effect-chip' => 'border-radius: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + // Get effects + $effects = []; + if ($settings['source'] === 'custom') { + $effects_string = $settings['custom_effects']; + $effects = array_map('trim', explode(',', $effects_string)); + } else { + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $raw_effects = $cannaiq_current_product['effects'] ?? []; + if (is_array($raw_effects)) { + // If effects is associative array with scores, sort by score + if (isset($raw_effects[0]) && !is_array($raw_effects[0])) { + $effects = $raw_effects; + } else { + // Sort by value descending and get keys + arsort($raw_effects); + $effects = array_keys($raw_effects); + } + } + } + } + + if (empty($effects)) { + return; + } + + // Apply limit + $limit = intval($settings['limit']); + $effects = array_slice($effects, 0, $limit); + + // Determine icon size + $icon_size = $settings['size'] === 'small' ? 12 : ($settings['size'] === 'large' ? 20 : 16); + ?> +
+ + + + + $icon_size, + 'color' => $settings['use_colors'] === 'yes' ? $color : 'currentColor', + ]); ?> + + + + +
+ start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'source', + [ + 'label' => __('Price Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom values', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_price', + [ + 'label' => __('Regular Price', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 45, + 'min' => 0, + 'step' => 0.01, + 'condition' => [ + 'source' => 'custom', + ], + ] + ); + + $this->add_control( + 'custom_sale_price', + [ + 'label' => __('Sale Price (optional)', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => '', + 'min' => 0, + 'step' => 0.01, + 'condition' => [ + 'source' => 'custom', + ], + ] + ); + + $this->add_control( + 'show_original_when_sale', + [ + 'label' => __('Show Original Price on Sale', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'show_weight', + [ + 'label' => __('Show Weight', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'custom_weight', + [ + 'label' => __('Weight Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => '1/8 oz', + 'condition' => [ + 'source' => 'custom', + 'show_weight' => 'yes', + ], + ] + ); + + $this->add_control( + 'layout', + [ + 'label' => __('Layout', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'inline', + 'options' => [ + 'inline' => __('Inline', 'cannaiq-menus'), + 'stacked' => __('Stacked', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'currency_symbol', + [ + 'label' => __('Currency Symbol', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => '$', + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'size', + [ + 'label' => __('Size', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'medium', + 'options' => [ + 'small' => __('Small', 'cannaiq-menus'), + 'medium' => __('Medium', 'cannaiq-menus'), + 'large' => __('Large', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'price_color', + [ + 'label' => __('Price Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#1f2937', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-price-block__regular' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'sale_color', + [ + 'label' => __('Sale Price Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#dc2626', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-price-block__sale' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'original_color', + [ + 'label' => __('Original Price Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#9ca3af', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-price-block__original' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_control( + 'weight_color', + [ + 'label' => __('Weight Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#9ca3af', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-price-block__weight' => 'color: {{VALUE}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'price_typography', + 'label' => __('Price Typography', 'cannaiq-menus'), + 'selector' => '{{WRAPPER}} .cannaiq-price-block__sale, {{WRAPPER}} .cannaiq-price-block__regular', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + // Get price values + $regular_price = 0; + $sale_price = null; + $weight = ''; + + if ($settings['source'] === 'custom') { + $regular_price = floatval($settings['custom_price']); + $sale_price = !empty($settings['custom_sale_price']) ? floatval($settings['custom_sale_price']) : null; + $weight = $settings['custom_weight']; + } else { + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $regular_price = $cannaiq_current_product['Prices'][0] + ?? $cannaiq_current_product['recPrices'][0] + ?? $cannaiq_current_product['regular_price'] + ?? 0; + + $sale_price = $cannaiq_current_product['specialPrice'] + ?? $cannaiq_current_product['sale_price'] + ?? null; + + // Check POSMetaData for prices + if (isset($cannaiq_current_product['POSMetaData']['children'][0])) { + $child = $cannaiq_current_product['POSMetaData']['children'][0]; + if (isset($child['price'])) { + $regular_price = $child['price']; + } + if (isset($child['specialPrice']) && $child['specialPrice'] > 0) { + $sale_price = $child['specialPrice']; + } + } + + $weight = $cannaiq_current_product['Options'][0] + ?? $cannaiq_current_product['rawOptions'][0] + ?? $cannaiq_current_product['weight'] + ?? ''; + } + } + + $regular_price = floatval($regular_price); + if ($regular_price <= 0) { + return; + } + + // Determine if on sale + $is_on_sale = $sale_price !== null && floatval($sale_price) > 0 && floatval($sale_price) < $regular_price; + + // Build classes + $classes = ['cannaiq-price-block']; + if ($settings['layout'] === 'stacked') { + $classes[] = 'cannaiq-price-block--stacked'; + } + if ($settings['size'] !== 'medium') { + $classes[] = 'cannaiq-price-block--' . $settings['size']; + } + + $currency = $settings['currency_symbol']; + ?> +
+ + + + + + + + + + + + +
+ start_controls_section( + 'content_section', + [ + 'label' => __('Image', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'image_source', + [ + 'label' => __('Image Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom image', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_image', + [ + 'label' => __('Choose Image', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::MEDIA, + 'default' => [ + 'url' => \Elementor\Utils::get_placeholder_image_src(), + ], + 'condition' => [ + 'image_source' => 'custom', + ], + ] + ); + + $this->add_control( + 'fallback_image', + [ + 'label' => __('Fallback Image', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::MEDIA, + 'description' => __('Shown if product has no image', 'cannaiq-menus'), + ] + ); + + $this->add_control( + 'aspect_ratio', + [ + 'label' => __('Aspect Ratio', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => '1/1', + 'options' => [ + '1/1' => __('Square (1:1)', 'cannaiq-menus'), + '4/3' => __('4:3', 'cannaiq-menus'), + '3/4' => __('3:4', 'cannaiq-menus'), + '16/9' => __('16:9', 'cannaiq-menus'), + 'auto' => __('Auto', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'hover_effect', + [ + 'label' => __('Hover Effect', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'zoom', + 'options' => [ + 'none' => __('None', 'cannaiq-menus'), + 'zoom' => __('Zoom', 'cannaiq-menus'), + ], + ] + ); + + $this->end_controls_section(); + + // Overlay Badges Section + $this->start_controls_section( + 'overlays_section', + [ + 'label' => __('Badge Overlays', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'show_discount_badge', + [ + 'label' => __('Show Discount Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'discount_position', + [ + 'label' => __('Discount Position', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'top-left', + 'options' => [ + 'top-left' => __('Top Left', 'cannaiq-menus'), + 'top-right' => __('Top Right', 'cannaiq-menus'), + 'bottom-left' => __('Bottom Left', 'cannaiq-menus'), + 'bottom-right' => __('Bottom Right', 'cannaiq-menus'), + ], + 'condition' => [ + 'show_discount_badge' => 'yes', + ], + ] + ); + + $this->add_control( + 'show_strain_badge', + [ + 'label' => __('Show Strain Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'strain_position', + [ + 'label' => __('Strain Position', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'bottom-left', + 'options' => [ + 'top-left' => __('Top Left', 'cannaiq-menus'), + 'top-right' => __('Top Right', 'cannaiq-menus'), + 'bottom-left' => __('Bottom Left', 'cannaiq-menus'), + 'bottom-right' => __('Bottom Right', 'cannaiq-menus'), + ], + 'condition' => [ + 'show_strain_badge' => 'yes', + ], + ] + ); + + $this->add_control( + 'show_thc_badge', + [ + 'label' => __('Show THC Badge', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => '', + ] + ); + + $this->add_control( + 'thc_position', + [ + 'label' => __('THC Position', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'bottom-right', + 'options' => [ + 'top-left' => __('Top Left', 'cannaiq-menus'), + 'top-right' => __('Top Right', 'cannaiq-menus'), + 'bottom-left' => __('Bottom Left', 'cannaiq-menus'), + 'bottom-right' => __('Bottom Right', 'cannaiq-menus'), + ], + 'condition' => [ + 'show_thc_badge' => 'yes', + ], + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'border_radius', + [ + 'label' => __('Border Radius', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 0, + 'max' => 50, + ], + ], + 'default' => [ + 'size' => 8, + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-image' => 'border-radius: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->add_control( + 'background_color', + [ + 'label' => __('Background Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f9fafb', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-product-image' => 'background-color: {{VALUE}};', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Box_Shadow::get_type(), + [ + 'name' => 'box_shadow', + 'selector' => '{{WRAPPER}} .cannaiq-product-image', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + // Get image URL + $image_url = ''; + if ($settings['image_source'] === 'custom') { + $image_url = $settings['custom_image']['url'] ?? ''; + } else { + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $image_url = $cannaiq_current_product['Image'] + ?? $cannaiq_current_product['images'][0]['url'] + ?? $cannaiq_current_product['image_url'] + ?? ''; + } + } + + // Use fallback if no image + if (empty($image_url) && !empty($settings['fallback_image']['url'])) { + $image_url = $settings['fallback_image']['url']; + } + + // Get product name for alt text + $alt_text = ''; + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $alt_text = $cannaiq_current_product['Name'] ?? $cannaiq_current_product['name'] ?? ''; + } + + // Aspect ratio style + $aspect_style = ''; + if ($settings['aspect_ratio'] !== 'auto') { + $aspect_style = 'aspect-ratio: ' . $settings['aspect_ratio'] . ';'; + } + + // Hover class + $hover_class = $settings['hover_effect'] === 'zoom' ? 'cannaiq-product-image--hover-zoom' : ''; + + // Group badges by position + $badges_by_position = []; + + if ($settings['show_discount_badge'] === 'yes') { + $badges_by_position[$settings['discount_position']][] = 'discount'; + } + if ($settings['show_strain_badge'] === 'yes') { + $badges_by_position[$settings['strain_position']][] = 'strain'; + } + if ($settings['show_thc_badge'] === 'yes') { + $badges_by_position[$settings['thc_position']][] = 'thc'; + } + + ?> +
+ + <?php echo esc_attr($alt_text); ?> + + + $badges): ?> +
+
+ + render_badge($badge_type); ?> + +
+
+ +
+ $sale) { + $percent = round((($original - $sale) / $original) * 100); + echo '' . esc_html($percent) . '% OFF'; + } + break; + + case 'strain': + if (!isset($cannaiq_current_product)) return; + + $strain = strtolower($cannaiq_current_product['strainType'] + ?? $cannaiq_current_product['strain_type'] + ?? ''); + + if (!empty($strain) && in_array($strain, ['sativa', 'indica', 'hybrid'])) { + $colors = [ + 'sativa' => '#22c55e', + 'indica' => '#8b5cf6', + 'hybrid' => '#f97316', + ]; + $color = $colors[$strain]; + echo '' . esc_html(strtoupper($strain)) . ''; + } + break; + + case 'thc': + if (!isset($cannaiq_current_product)) return; + + $thc = $cannaiq_current_product['THCContent']['range'][0] + ?? $cannaiq_current_product['THC'] + ?? $cannaiq_current_product['thc_percentage'] + ?? null; + + if ($thc !== null && $thc > 0) { + echo '' . esc_html(number_format((float)$thc, 1)) . '% THC'; + } + break; + } + } +} diff --git a/wordpress-plugin/widgets/product-loop.php b/wordpress-plugin/widgets/product-loop.php index aeed1606..8085cfdd 100644 --- a/wordpress-plugin/widgets/product-loop.php +++ b/wordpress-plugin/widgets/product-loop.php @@ -17,7 +17,7 @@ class CannaIQ_Product_Loop_Widget extends \Elementor\Widget_Base { } public function get_title() { - return __('CannaIQ Product Loop', 'cannaiq-menus'); + return __('CannaiQ Product Loop', 'cannaiq-menus'); } public function get_icon() { diff --git a/wordpress-plugin/widgets/single-product.php b/wordpress-plugin/widgets/single-product.php index 4a23e8d4..bde3ccf2 100644 --- a/wordpress-plugin/widgets/single-product.php +++ b/wordpress-plugin/widgets/single-product.php @@ -14,7 +14,7 @@ class CannaIQ_Menus_Single_Product_Widget extends \Elementor\Widget_Base { } public function get_title() { - return __('CannaIQ Single Product', 'cannaiq-menus'); + return __('CannaiQ Single Product', 'cannaiq-menus'); } public function get_icon() { diff --git a/wordpress-plugin/widgets/specials-grid.php b/wordpress-plugin/widgets/specials-grid.php index 0f1d5536..cf7e9f76 100644 --- a/wordpress-plugin/widgets/specials-grid.php +++ b/wordpress-plugin/widgets/specials-grid.php @@ -14,7 +14,7 @@ class CannaIQ_Menus_Specials_Grid_Widget extends \Elementor\Widget_Base { } public function get_title() { - return __('CannaIQ Specials/Deals', 'cannaiq-menus'); + return __('CannaiQ Specials/Deals', 'cannaiq-menus'); } public function get_icon() { diff --git a/wordpress-plugin/widgets/stock-indicator.php b/wordpress-plugin/widgets/stock-indicator.php new file mode 100644 index 00000000..f3ff8753 --- /dev/null +++ b/wordpress-plugin/widgets/stock-indicator.php @@ -0,0 +1,258 @@ +start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'source', + [ + 'label' => __('Stock Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom value', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_in_stock', + [ + 'label' => __('In Stock', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + 'condition' => [ + 'source' => 'custom', + ], + ] + ); + + $this->add_control( + 'format', + [ + 'label' => __('Display Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'badge', + 'options' => [ + 'badge' => __('Badge', 'cannaiq-menus'), + 'text' => __('Text', 'cannaiq-menus'), + 'dot' => __('Dot + Text', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'in_stock_text', + [ + 'label' => __('In Stock Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'In Stock', + ] + ); + + $this->add_control( + 'out_of_stock_text', + [ + 'label' => __('Out of Stock Text', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'Out of Stock', + ] + ); + + $this->add_control( + 'show_quantity', + [ + 'label' => __('Show Quantity', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => '', + 'description' => __('Show quantity if available', 'cannaiq-menus'), + ] + ); + + $this->add_control( + 'hide_if_in_stock', + [ + 'label' => __('Hide if In Stock', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => '', + 'description' => __('Only show when out of stock', 'cannaiq-menus'), + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'in_stock_color', + [ + 'label' => __('In Stock Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#16a34a', + ] + ); + + $this->add_control( + 'out_of_stock_color', + [ + 'label' => __('Out of Stock Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#9ca3af', + ] + ); + + $this->add_control( + 'in_stock_bg', + [ + 'label' => __('In Stock Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#dcfce7', + 'condition' => [ + 'format' => 'badge', + ], + ] + ); + + $this->add_control( + 'out_of_stock_bg', + [ + 'label' => __('Out of Stock Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f3f4f6', + 'condition' => [ + 'format' => 'badge', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'typography', + 'selector' => '{{WRAPPER}} .cannaiq-stock-indicator', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + // Get stock status + $in_stock = true; + $quantity = null; + + if ($settings['source'] === 'custom') { + $in_stock = $settings['custom_in_stock'] === 'yes'; + } else { + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $status = $cannaiq_current_product['Status'] ?? ''; + $in_stock = ($status === 'Active' || $status === 'In Stock' || !empty($cannaiq_current_product['in_stock'])); + + // Get quantity if available + $quantity = $cannaiq_current_product['POSMetaData']['children'][0]['quantity'] + ?? $cannaiq_current_product['quantity'] + ?? null; + } + } + + // Hide if in stock and setting enabled + if ($in_stock && $settings['hide_if_in_stock'] === 'yes') { + return; + } + + // Determine display text + $text = $in_stock ? $settings['in_stock_text'] : $settings['out_of_stock_text']; + if ($in_stock && $settings['show_quantity'] === 'yes' && $quantity !== null) { + $text .= ' (' . intval($quantity) . ')'; + } + + // Colors + $color = $in_stock ? $settings['in_stock_color'] : $settings['out_of_stock_color']; + $bg_color = $in_stock ? $settings['in_stock_bg'] : $settings['out_of_stock_bg']; + + // Build classes + $classes = [ + 'cannaiq-stock-indicator', + $in_stock ? 'cannaiq-stock-indicator--in-stock' : 'cannaiq-stock-indicator--out-of-stock', + ]; + if ($settings['format'] === 'badge') { + $classes[] = 'cannaiq-stock-indicator--badge'; + } + + // Build style + $style = sprintf('color: %s;', esc_attr($color)); + if ($settings['format'] === 'badge') { + $style .= sprintf(' background-color: %s;', esc_attr($bg_color)); + } + + ?> + + + + + + + start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'source', + [ + 'label' => __('Strain Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom value', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_strain', + [ + 'label' => __('Strain Type', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'hybrid', + 'options' => [ + 'sativa' => __('Sativa', 'cannaiq-menus'), + 'indica' => __('Indica', 'cannaiq-menus'), + 'hybrid' => __('Hybrid', 'cannaiq-menus'), + ], + 'condition' => [ + 'source' => 'custom', + ], + ] + ); + + $this->add_control( + 'format', + [ + 'label' => __('Display Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'pill', + 'options' => [ + 'pill' => __('Pill', 'cannaiq-menus'), + 'text' => __('Text Only', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'text_format', + [ + 'label' => __('Text Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'uppercase', + 'options' => [ + 'uppercase' => __('UPPERCASE', 'cannaiq-menus'), + 'capitalize' => __('Capitalize', 'cannaiq-menus'), + 'lowercase' => __('lowercase', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'show_icon', + [ + 'label' => __('Show Icon', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => '', + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'size', + [ + 'label' => __('Size', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'medium', + 'options' => [ + 'small' => __('Small', 'cannaiq-menus'), + 'medium' => __('Medium', 'cannaiq-menus'), + 'large' => __('Large', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'sativa_color', + [ + 'label' => __('Sativa Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#22c55e', + ] + ); + + $this->add_control( + 'indica_color', + [ + 'label' => __('Indica Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#8b5cf6', + ] + ); + + $this->add_control( + 'hybrid_color', + [ + 'label' => __('Hybrid Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#f97316', + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'typography', + 'selector' => '{{WRAPPER}} .cannaiq-strain-badge', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + // Get strain type + $strain = ''; + if ($settings['source'] === 'custom') { + $strain = $settings['custom_strain']; + } else { + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + $strain = strtolower($cannaiq_current_product['strainType'] ?? $cannaiq_current_product['strain_type'] ?? ''); + } + } + + if (empty($strain)) { + return; + } + + // Normalize strain type + $strain_key = strtolower($strain); + if (!in_array($strain_key, ['sativa', 'indica', 'hybrid'])) { + return; + } + + // Format text + switch ($settings['text_format']) { + case 'uppercase': + $text = strtoupper($strain); + break; + case 'lowercase': + $text = strtolower($strain); + break; + default: + $text = ucfirst($strain); + } + + // Get color based on strain type + $color = $settings[$strain_key . '_color']; + + // Build classes + $classes = [ + 'cannaiq-strain-badge', + 'cannaiq-strain-badge--' . $settings['format'], + 'cannaiq-strain-badge--' . $strain_key, + ]; + if ($settings['size'] !== 'medium') { + $classes[] = 'cannaiq-strain-badge--' . $settings['size']; + } + + // Build style + $style = ''; + if ($settings['format'] === 'pill') { + $style = sprintf('background-color: %s; color: white;', esc_attr($color)); + } else { + $style = sprintf('color: %s;', esc_attr($color)); + } + + // Icon SVG (leaf icon) + $icon = ''; + if ($settings['show_icon'] === 'yes') { + $icon = ''; + } + + printf( + '%s%s', + esc_attr(implode(' ', $classes)), + esc_attr($style), + $icon, + esc_html($text) + ); + } +} diff --git a/wordpress-plugin/widgets/thc-meter.php b/wordpress-plugin/widgets/thc-meter.php new file mode 100644 index 00000000..83df8146 --- /dev/null +++ b/wordpress-plugin/widgets/thc-meter.php @@ -0,0 +1,295 @@ +start_controls_section( + 'content_section', + [ + 'label' => __('Content', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, + ] + ); + + $this->add_control( + 'type', + [ + 'label' => __('Type', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'thc', + 'options' => [ + 'thc' => __('THC', 'cannaiq-menus'), + 'cbd' => __('CBD', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'source', + [ + 'label' => __('Value Source', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'auto', + 'options' => [ + 'auto' => __('Auto (from product)', 'cannaiq-menus'), + 'custom' => __('Custom value', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'custom_value', + [ + 'label' => __('Percentage', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 20, + 'min' => 0, + 'max' => 100, + 'step' => 0.1, + 'condition' => [ + 'source' => 'custom', + ], + ] + ); + + $this->add_control( + 'display_format', + [ + 'label' => __('Display Format', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'meter', + 'options' => [ + 'meter' => __('Meter (progress bar)', 'cannaiq-menus'), + 'badge' => __('Badge', 'cannaiq-menus'), + 'pill' => __('Pill', 'cannaiq-menus'), + 'text' => __('Text Only', 'cannaiq-menus'), + ], + ] + ); + + $this->add_control( + 'show_label', + [ + 'label' => __('Show Label', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'label_on' => __('Yes', 'cannaiq-menus'), + 'label_off' => __('No', 'cannaiq-menus'), + 'return_value' => 'yes', + 'default' => 'yes', + ] + ); + + $this->add_control( + 'max_percentage', + [ + 'label' => __('Max Percentage (for meter)', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::NUMBER, + 'default' => 35, + 'min' => 10, + 'max' => 100, + 'description' => __('Used to calculate bar fill percentage', 'cannaiq-menus'), + 'condition' => [ + 'display_format' => 'meter', + ], + ] + ); + + $this->end_controls_section(); + + // Style Section + $this->start_controls_section( + 'style_section', + [ + 'label' => __('Style', 'cannaiq-menus'), + 'tab' => \Elementor\Controls_Manager::TAB_STYLE, + ] + ); + + $this->add_control( + 'thc_color', + [ + 'label' => __('THC Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#22c55e', + ] + ); + + $this->add_control( + 'cbd_color', + [ + 'label' => __('CBD Color', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#3b82f6', + ] + ); + + $this->add_control( + 'bar_height', + [ + 'label' => __('Bar Height', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => [ + 'px' => [ + 'min' => 4, + 'max' => 20, + ], + ], + 'default' => [ + 'size' => 6, + ], + 'selectors' => [ + '{{WRAPPER}} .cannaiq-potency-meter__bar' => 'height: {{SIZE}}{{UNIT}};', + ], + 'condition' => [ + 'display_format' => 'meter', + ], + ] + ); + + $this->add_control( + 'bar_background', + [ + 'label' => __('Bar Background', 'cannaiq-menus'), + 'type' => \Elementor\Controls_Manager::COLOR, + 'default' => '#e5e7eb', + 'selectors' => [ + '{{WRAPPER}} .cannaiq-potency-meter__bar' => 'background-color: {{VALUE}};', + ], + 'condition' => [ + 'display_format' => 'meter', + ], + ] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'typography', + 'selector' => '{{WRAPPER}} .cannaiq-potency-meter, {{WRAPPER}} .cannaiq-potency-badge', + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + $type = $settings['type']; + + // Get percentage value + $percentage = 0; + if ($settings['source'] === 'custom') { + $percentage = floatval($settings['custom_value']); + } else { + global $cannaiq_current_product; + if (isset($cannaiq_current_product)) { + if ($type === 'thc') { + $percentage = $cannaiq_current_product['THCContent']['range'][0] + ?? $cannaiq_current_product['THC'] + ?? $cannaiq_current_product['thc_percentage'] + ?? 0; + } else { + $percentage = $cannaiq_current_product['CBDContent']['range'][0] + ?? $cannaiq_current_product['CBD'] + ?? $cannaiq_current_product['cbd_percentage'] + ?? 0; + } + } + } + + $percentage = floatval($percentage); + if ($percentage <= 0) { + return; + } + + $label = strtoupper($type); + $color = $type === 'thc' ? $settings['thc_color'] : $settings['cbd_color']; + $formatted_value = number_format($percentage, 1) . '%'; + + switch ($settings['display_format']) { + case 'meter': + $fill_percent = min(100, ($percentage / floatval($settings['max_percentage'])) * 100); + ?> +
+ +
+ + +
+ +
+
+
+
+ + + + + + + + + + + : + + + + + + + : + + + +