fix(monitor): remove non-existent worker columns from job_run_logs query

The job_run_logs table tracks scheduled job orchestration, not individual
worker jobs. Worker info (worker_id, worker_hostname) belongs on
dispensary_crawl_jobs, not job_run_logs.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-03 18:45:05 -07:00
parent 54f40d26bb
commit 66e07b2009
466 changed files with 84988 additions and 9226 deletions

View File

@@ -3,7 +3,7 @@
* Plugin Name: Crawlsy Menus
* Plugin URI: https://creationshop.io
* Description: Display cannabis product menus from Crawlsy with Elementor integration
* Version: 1.4.0
* Version: 1.5.0
* Author: Creationshop
* Author URI: https://creationshop.io
* License: GPL v2 or later
@@ -15,7 +15,7 @@ if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
define('CRAWLSY_MENUS_VERSION', '1.4.0');
define('CRAWLSY_MENUS_VERSION', '1.5.0');
define('CRAWLSY_MENUS_API_URL', 'https://dispos.crawlsy.com/api/v1');
define('CRAWLSY_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('CRAWLSY_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__));

View File

@@ -1,196 +0,0 @@
# Dispensary Analytics WordPress Plugin
Beautiful WordPress plugin for displaying Arizona dispensary product data with Elementor widgets and shortcodes.
**By CreationShop** - [https://creationshop.io](https://creationshop.io)
## Features
- **Elementor Widgets**: Drag-and-drop widgets for products, stores, brands, and specials
- **Beautiful Carousels**: Auto-playing product sliders with Swiper.js
- **Flexible Shortcodes**: Easy-to-use shortcodes for any page builder
- **Field Selection**: Choose which data fields to display
- **Multiple Styles**: Modern, Minimal, and Bold card designs
- **Responsive Design**: Perfect on all devices
- **High-Performance**: Lazy loading and optimized API calls
- **SEO Friendly**: Semantic HTML and proper image alt tags
## Installation
1. Upload the `dutchie-analytics` folder to `/wp-content/plugins/`
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Go to Settings → Dutchie Analytics
4. Enter your API URL and authentication token
5. Test the connection
## Configuration
### API Settings
Navigate to **Settings → Dutchie Analytics**:
- **API URL**: Your Dutchie Analytics API endpoint (e.g., `https://api.dutchieanalytics.com/api`)
- **API Token**: Your JWT authentication token
## Shortcodes
### Products Grid
Display a grid of products:
```
[dutchie_products store_id="1" limit="12" columns="4"]
```
**Parameters:**
- `store_id` (required): Store ID
- `limit` (optional): Number of products (default: 12)
- `columns` (optional): Grid columns (default: 4)
- `category_id` (optional): Filter by category
- `in_stock` (optional): Show only in-stock products (default: true)
- `search` (optional): Search term
- `fields` (optional): Comma-separated fields to display
- `style` (optional): Card style - modern, minimal, bold (default: modern)
### Products Carousel
Display a sliding carousel:
```
[dutchie_carousel store_id="1" limit="20" slides_per_view="4"]
```
**Parameters:**
- `store_id` (required): Store ID
- `limit` (optional): Number of products (default: 20)
- `slides_per_view` (optional): Slides visible at once (default: 4)
- `category_id` (optional): Filter by category
- `autoplay` (optional): Enable autoplay (default: true)
- `style` (optional): Card style (default: modern)
### Stores List
Display all stores:
```
[dutchie_stores]
```
### Brands List
Display brands for a store:
```
[dutchie_brands store_id="1"]
```
### Daily Specials
Display store specials:
```
[dutchie_specials store_id="1" date="2025-01-15"]
```
**Parameters:**
- `store_id` (required): Store ID
- `date` (optional): Date for specials (default: today)
## Field Selection
Control which fields to display using the `fields` parameter:
```
[dutchie_products store_id="1" fields="id,name,price,brand,thc_percentage,in_stock"]
```
**Available Fields:**
- `id` - Product ID
- `name` - Product name
- `brand` - Brand name
- `price` - Price
- `description` - Description
- `thc_percentage` - THC %
- `cbd_percentage` - CBD %
- `weight` - Weight
- `strain_type` - Strain type
- `in_stock` - Stock status
- `image_url_full` - High-resolution image (2000x2000)
## Elementor Widgets
Find all widgets in the **Dutchie Analytics** category in Elementor:
1. **Products Grid** - Customizable product grid
2. **Products Carousel** - Auto-playing slider
3. **Stores List** - Store directory
4. **Brands List** - Brand badges
5. **Specials** - Daily deals
### Widget Controls
Each widget provides:
- **Content Tab**: Data source, filters, display options
- **Style Tab**: Colors, spacing, card styles
- **Advanced Tab**: CSS classes, animations
## Card Styles
### Modern (Default)
- Clean white cards
- Subtle shadows
- Smooth hover effects
- Blue accent colors
### Minimal
- Simple borders
- No shadows
- Hover border highlights
- Ultra-clean design
### Bold
- Gradient backgrounds
- Vibrant colors
- Large shadows
- Eye-catching design
## Customization
### Custom CSS
Add custom styles in your theme's CSS:
```css
.dutchie-product-card-modern {
border-radius: 20px;
/* Your custom styles */
}
```
### Hooks and Filters
Coming soon: WordPress hooks for advanced customization.
## Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
- Elementor (optional, for widgets)
## Support
For support, visit: [https://dutchieanalytics.com/support](https://dutchieanalytics.com/support)
## Changelog
### 1.0.0
- Initial release
- Elementor widgets
- Shortcodes
- Field selection
- Multiple card styles
- Carousel support
## License
GPL v2 or later

View File

@@ -1,326 +0,0 @@
# Dispensary Analytics - User Guide
## Table of Contents
1. [Installation & Setup](#installation--setup)
2. [Plugin Configuration](#plugin-configuration)
3. [Using Elementor Widgets](#using-elementor-widgets)
4. [Using Shortcodes](#using-shortcodes)
5. [Widget Reference](#widget-reference)
6. [Shortcode Reference](#shortcode-reference)
7. [Troubleshooting](#troubleshooting)
---
## Installation & Setup
### Requirements
- WordPress 5.0 or higher
- PHP 7.4 or higher
- Elementor plugin (for widget functionality)
### Installation Steps
1. Download the `dutchie-analytics.zip` file
2. Go to WordPress Admin → Plugins → Add New
3. Click "Upload Plugin" and select the zip file
4. Click "Install Now" and then "Activate"
---
## Plugin Configuration
### Setting Up API Connection
1. Go to **WordPress Admin → Settings → Dispensary Analytics**
2. Configure the following settings:
- **API URL**: Your backend API URL (e.g., `http://localhost:3010/api` or `https://yourdomain.com/api`)
- **API Token**: Your authentication token (if required)
3. Click "Save Changes"
---
## Using Elementor Widgets
### Finding the Widgets
1. Edit any page with **Elementor**
2. Look for the **"Dutchie Analytics"** category in the widgets panel
3. You'll find 5 widgets available:
- Products Grid
- Products Carousel
- Stores List
- Brands List
- Specials
### Adding a Widget to Your Page
1. Open a page in **Elementor Editor**
2. Drag the desired widget from the sidebar to your page
3. Configure the widget settings in the left panel
4. Click "Update" to save your changes
---
## Using Shortcodes
Shortcodes allow you to display dispensary data in any post, page, or widget area without using Elementor.
### How to Use Shortcodes
1. Edit any post or page
2. Add a shortcode block (or paste directly in the classic editor)
3. Copy and paste one of the shortcodes below
4. Customize the parameters as needed
5. Publish or update your page
---
## Widget Reference
### 1. Products Grid Widget
**Purpose**: Display products in a responsive grid layout
**Widget Settings**:
- **Store ID** (required): The dispensary store ID
- **Category ID** (optional): Filter by specific category
- **Limit**: Number of products to show (default: 12)
- **Columns**: Number of columns (default: 4)
- **In Stock Only**: Show only available products (default: Yes)
- **Search**: Filter by keyword
- **Style**: Choose visual style (modern, classic, minimal)
- **Fields**: Customize which product fields to display
**Elementor Location**: Dutchie Analytics → Products Grid
---
### 2. Products Carousel Widget
**Purpose**: Display products in a sliding carousel/slider
**Widget Settings**:
- **Store ID** (required): The dispensary store ID
- **Category ID** (optional): Filter by specific category
- **Limit**: Number of products to show (default: 12)
- **Slides Per View**: How many products visible at once (default: 4)
- **Autoplay**: Enable automatic sliding (default: Yes)
- **Loop**: Enable infinite loop (default: Yes)
- **Navigation**: Show prev/next arrows (default: Yes)
- **Pagination**: Show dot indicators (default: Yes)
**Elementor Location**: Dutchie Analytics → Products Carousel
---
### 3. Stores List Widget
**Purpose**: Display a list of dispensary locations
**Widget Settings**:
- **Limit**: Number of stores to show (default: 10)
- **State**: Filter by state (e.g., "Arizona")
- **City**: Filter by city
- **Style**: List style (cards, list, table)
- **Show Address**: Display store address (default: Yes)
- **Show Phone**: Display phone number (default: Yes)
- **Show Hours**: Display business hours (default: Yes)
**Elementor Location**: Dutchie Analytics → Stores List
---
### 4. Brands List Widget
**Purpose**: Display available brands
**Widget Settings**:
- **Store ID** (optional): Filter brands by store
- **Limit**: Number of brands to show (default: 20)
- **Style**: Display style (grid, list, carousel)
- **Show Logo**: Display brand logos (default: Yes)
- **Show Product Count**: Show number of products per brand (default: Yes)
**Elementor Location**: Dutchie Analytics → Brands List
---
### 5. Specials Widget
**Purpose**: Display current deals and promotions
**Widget Settings**:
- **Store ID** (required): The dispensary store ID
- **Limit**: Number of specials to show (default: 6)
- **Active Only**: Show only active promotions (default: Yes)
- **Style**: Display style (banner, card, minimal)
- **Show Expiration**: Display when deal expires (default: Yes)
**Elementor Location**: Dutchie Analytics → Specials
---
## Shortcode Reference
### 1. Products Grid Shortcode
Display products in a grid layout.
**Basic Usage**:
```
[dutchie_products store_id="1"]
```
**Full Example**:
```
[dutchie_products store_id="1" limit="12" category_id="5" columns="4" in_stock="true" style="modern" search="indica"]
```
**Parameters**:
- `store_id` (required) - Store ID number
- `limit` - Number of products (default: 12)
- `category_id` - Filter by category ID
- `columns` - Grid columns: 2, 3, 4, or 6 (default: 4)
- `in_stock` - Show only in-stock: "true" or "false" (default: true)
- `search` - Filter by keyword
- `style` - Visual style: "modern", "classic", "minimal"
- `fields` - Comma-separated fields: "name,price,brand,thc,cbd"
---
### 2. Products Carousel Shortcode
Display products in a carousel/slider.
**Basic Usage**:
```
[dutchie_carousel store_id="1"]
```
**Full Example**:
```
[dutchie_carousel store_id="1" limit="16" slides_per_view="4" autoplay="true" loop="true"]
```
**Parameters**:
- `store_id` (required) - Store ID number
- `limit` - Number of products (default: 12)
- `category_id` - Filter by category ID
- `slides_per_view` - Products visible at once (default: 4)
- `autoplay` - Auto-slide: "true" or "false" (default: true)
- `loop` - Infinite loop: "true" or "false" (default: true)
- `navigation` - Show arrows: "true" or "false" (default: true)
- `pagination` - Show dots: "true" or "false" (default: true)
---
### 3. Stores List Shortcode
Display dispensary locations.
**Basic Usage**:
```
[dutchie_stores]
```
**Full Example**:
```
[dutchie_stores limit="10" state="Arizona" city="Phoenix" style="cards"]
```
**Parameters**:
- `limit` - Number of stores (default: 10)
- `state` - Filter by state
- `city` - Filter by city
- `style` - Display style: "cards", "list", "table"
---
### 4. Brands List Shortcode
Display available brands.
**Basic Usage**:
```
[dutchie_brands]
```
**Full Example**:
```
[dutchie_brands store_id="1" limit="20" style="grid"]
```
**Parameters**:
- `store_id` - Filter by store ID
- `limit` - Number of brands (default: 20)
- `style` - Display style: "grid", "list", "carousel"
---
### 5. Specials Shortcode
Display current deals and promotions.
**Basic Usage**:
```
[dutchie_specials store_id="1"]
```
**Full Example**:
```
[dutchie_specials store_id="1" limit="6" active_only="true" style="banner"]
```
**Parameters**:
- `store_id` (required) - Store ID number
- `limit` - Number of specials (default: 6)
- `active_only` - Show only active: "true" or "false" (default: true)
- `style` - Display style: "banner", "card", "minimal"
---
## Troubleshooting
### Widget Not Appearing
- Make sure Elementor is installed and activated
- Clear Elementor cache: Elementor → Tools → Regenerate CSS & Data
- Check if plugin is activated
### No Data Displaying
- Verify API URL in Settings → Dispensary Analytics
- Check that store_id is correct
- Test API connection in browser
- Check browser console for errors (F12)
### Shortcode Shows as Text
- Make sure you're using the correct shortcode format: `[shortcode_name]`
- Verify the plugin is activated
- Check that the post/page content is not in HTML mode
### Styling Issues
- Clear WordPress cache
- Clear browser cache (Ctrl+F5)
- Check for theme CSS conflicts
- Try switching to a default WordPress theme temporarily
### API Connection Issues
- Verify API URL is correct and accessible
- Check if API token is required and configured
- Ensure backend server is running
- Check server firewall settings
---
## Support
For additional support:
- Check plugin settings: WordPress Admin → Settings → Dispensary Analytics
- Review backend API logs
- Contact: creationshop.io
---
## Version Information
**Current Version**: 1.0.2
**Author**: Creationshop LLC
**License**: GPL v2 or later

View File

@@ -1,456 +0,0 @@
/**
* Dutchie Analytics - Beautiful Styles
* Modern, clean, and professional design
*/
/* Products Grid */
.dutchie-products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin: 24px 0;
}
.dutchie-products-grid[data-columns="3"] {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.dutchie-products-grid[data-columns="4"] {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.dutchie-products-grid[data-columns="5"] {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
/* Product Cards - Modern Style */
.dutchie-product-card-modern {
background: #ffffff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid #e5e7eb;
}
.dutchie-product-card-modern:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
}
/* Product Cards - Minimal Style */
.dutchie-product-card-minimal {
background: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: none;
transition: all 0.2s ease;
border: 1px solid #e5e7eb;
}
.dutchie-product-card-minimal:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
}
/* Product Cards - Bold Style */
.dutchie-product-card-bold {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
color: #ffffff;
}
.dutchie-product-card-bold:hover {
transform: scale(1.05);
box-shadow: 0 12px 28px rgba(102, 126, 234, 0.4);
}
.dutchie-product-card-bold .dutchie-product-name,
.dutchie-product-card-bold .dutchie-product-brand,
.dutchie-product-card-bold .dutchie-product-description {
color: #ffffff;
}
/* Product Image */
.dutchie-product-image {
position: relative;
width: 100%;
aspect-ratio: 1;
background: #f9fafb;
overflow: hidden;
}
.dutchie-product-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.dutchie-product-card:hover .dutchie-product-image img {
transform: scale(1.05);
}
/* Stock Badge */
.dutchie-stock-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
backdrop-filter: blur(10px);
}
.dutchie-stock-badge.in-stock {
background: rgba(16, 185, 129, 0.9);
color: #ffffff;
}
.dutchie-stock-badge.out-of-stock {
background: rgba(239, 68, 68, 0.9);
color: #ffffff;
}
/* Product Content */
.dutchie-product-content {
padding: 20px;
}
.dutchie-product-name {
font-size: 16px;
font-weight: 600;
color: #111827;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.dutchie-product-brand {
font-size: 13px;
color: #6b7280;
margin: 0 0 12px 0;
font-weight: 500;
}
.dutchie-product-description {
font-size: 13px;
color: #6b7280;
line-height: 1.5;
margin: 0 0 16px 0;
}
/* Product Meta */
.dutchie-product-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.dutchie-product-price {
font-size: 20px;
font-weight: 700;
color: #3b82f6;
}
.dutchie-thc-badge,
.dutchie-cbd-badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.dutchie-thc-badge {
background: #ecfdf5;
color: #059669;
}
.dutchie-cbd-badge {
background: #eff6ff;
color: #2563eb;
}
.dutchie-product-weight {
font-size: 12px;
color: #9ca3af;
margin: 8px 0 0 0;
}
/* Carousel Wrapper */
.dutchie-products-carousel-wrapper {
margin: 24px 0;
position: relative;
}
.dutchie-products-carousel-wrapper .swiper {
padding: 0 50px 50px 50px;
}
.dutchie-products-carousel-wrapper .swiper-button-prev,
.dutchie-products-carousel-wrapper .swiper-button-next {
width: 44px;
height: 44px;
background: #ffffff;
border-radius: 50%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.dutchie-products-carousel-wrapper .swiper-button-prev:hover,
.dutchie-products-carousel-wrapper .swiper-button-next:hover {
background: #3b82f6;
color: #ffffff;
transform: scale(1.1);
}
.dutchie-products-carousel-wrapper .swiper-button-prev::after,
.dutchie-products-carousel-wrapper .swiper-button-next::after {
font-size: 18px;
}
.dutchie-products-carousel-wrapper .swiper-pagination-bullet {
width: 10px;
height: 10px;
background: #d1d5db;
opacity: 1;
transition: all 0.2s ease;
}
.dutchie-products-carousel-wrapper .swiper-pagination-bullet-active {
background: #3b82f6;
width: 28px;
border-radius: 5px;
}
/* Stores Grid */
.dutchie-stores-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
margin: 24px 0;
}
.dutchie-store-card {
background: #ffffff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
text-align: center;
border: 1px solid #e5e7eb;
}
.dutchie-store-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
}
.dutchie-store-logo {
width: 80px;
height: 80px;
object-fit: contain;
margin: 0 auto 16px;
display: block;
}
.dutchie-store-card h3 {
font-size: 18px;
font-weight: 600;
color: #111827;
margin: 0 0 12px 0;
}
.dutchie-store-stats {
font-size: 14px;
color: #6b7280;
margin: 0 0 16px 0;
}
.dutchie-store-link {
display: inline-block;
padding: 10px 20px;
background: #3b82f6;
color: #ffffff;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
font-size: 14px;
transition: all 0.2s ease;
}
.dutchie-store-link:hover {
background: #2563eb;
transform: scale(1.05);
color: #ffffff;
}
/* Brands Grid */
.dutchie-brands-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 24px 0;
}
.dutchie-brand-badge {
display: inline-flex;
align-items: center;
padding: 10px 18px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 24px;
font-size: 14px;
font-weight: 500;
color: #374151;
transition: all 0.2s ease;
}
.dutchie-brand-badge:hover {
background: #3b82f6;
color: #ffffff;
border-color: #3b82f6;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
/* Specials Grid */
.dutchie-specials-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
margin: 24px 0;
}
.dutchie-special-card {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 16px rgba(251, 191, 36, 0.3);
color: #ffffff;
position: relative;
overflow: hidden;
}
.dutchie-special-card::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
pointer-events: none;
}
.dutchie-special-card img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 16px;
}
.dutchie-special-card h3 {
font-size: 20px;
font-weight: 700;
color: #ffffff;
margin: 0 0 12px 0;
}
.dutchie-special-card p {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
margin: 0 0 16px 0;
}
.dutchie-discount-badge {
display: inline-block;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.4);
border-radius: 20px;
font-size: 16px;
font-weight: 700;
color: #ffffff;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Responsive Design */
@media (max-width: 768px) {
.dutchie-products-grid,
.dutchie-stores-grid,
.dutchie-specials-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.dutchie-products-carousel-wrapper .swiper {
padding: 0 0 40px 0;
}
.dutchie-product-name {
font-size: 15px;
}
.dutchie-product-price {
font-size: 18px;
}
}
@media (max-width: 480px) {
.dutchie-products-grid,
.dutchie-stores-grid,
.dutchie-specials-grid {
grid-template-columns: 1fr;
}
}
/* Loading State */
.dutchie-loading {
text-align: center;
padding: 40px;
color: #6b7280;
}
.dutchie-loading::after {
content: '';
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: dutchie-spin 0.8s linear infinite;
}
@keyframes dutchie-spin {
to { transform: rotate(360deg); }
}
/* Error State */
.dutchie-error {
background: #fee2e2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 16px;
color: #991b1b;
margin: 16px 0;
}

View File

@@ -1,45 +0,0 @@
/**
* Dutchie Analytics - Frontend JavaScript
*/
(function($) {
'use strict';
// Lazy load images
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
}
});
});
$('.dutchie-product-image img[data-src]').each(function() {
imageObserver.observe(this);
});
}
// Add hover effects
$('.dutchie-product-card').on('mouseenter', function() {
$(this).addClass('dutchie-hover');
}).on('mouseleave', function() {
$(this).removeClass('dutchie-hover');
});
// Optional: Track product views for analytics
$('.dutchie-product-card').on('click', function() {
const productId = $(this).data('product-id');
if (productId && typeof gtag !== 'undefined') {
gtag('event', 'view_item', {
'item_id': productId
});
}
});
})(jQuery);

View File

@@ -1,56 +0,0 @@
#!/bin/bash
# WordPress Plugin Version Bump Script
# Usage: ./bump-version.sh [major|minor|patch]
PLUGIN_FILE="dutchie-analytics.php"
BUMP_TYPE=${1:-patch} # Default to patch if not specified
# Get current version
CURRENT_VERSION=$(grep "^ \* Version:" "$PLUGIN_FILE" | sed 's/.*Version: \(.*\)/\1/')
if [ -z "$CURRENT_VERSION" ]; then
echo "Error: Could not find current version"
exit 1
fi
echo "Current version: $CURRENT_VERSION"
# Split version into parts
IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION"
MAJOR="${VERSION_PARTS[0]}"
MINOR="${VERSION_PARTS[1]}"
PATCH="${VERSION_PARTS[2]}"
# Bump version based on type
case $BUMP_TYPE in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
*)
echo "Error: Invalid bump type. Use: major, minor, or patch"
exit 1
;;
esac
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
echo "New version: $NEW_VERSION"
# Update version in plugin file
sed -i "s/^ \* Version: .*/ * Version: $NEW_VERSION/" "$PLUGIN_FILE"
sed -i "s/define('DUTCHIE_ANALYTICS_VERSION', '.*');/define('DUTCHIE_ANALYTICS_VERSION', '$NEW_VERSION');/" "$PLUGIN_FILE"
echo "✓ Version bumped from $CURRENT_VERSION to $NEW_VERSION"
echo ""
echo "Changes made to: $PLUGIN_FILE"
echo " - Plugin header: Version: $NEW_VERSION"
echo " - Constant: DUTCHIE_ANALYTICS_VERSION = '$NEW_VERSION'"

View File

@@ -1,130 +0,0 @@
<?php
/**
* Plugin Name: Dispensary Analytics
* Plugin URI: https://creationshop.io
* Description: Display Arizona dispensary product data, brands, and specials with beautiful Elementor widgets and shortcodes
* Version: 1.0.2
* Author: Creationshop LLC
* Author URI: creationshop.io
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: dutchie-analytics
* Requires at least: 5.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
define('DUTCHIE_ANALYTICS_VERSION', '1.0.2');
define('DUTCHIE_ANALYTICS_PATH', plugin_dir_path(__FILE__));
define('DUTCHIE_ANALYTICS_URL', plugin_dir_url(__FILE__));
// Autoloader
spl_autoload_register(function ($class) {
$prefix = 'DutchieAnalytics\\';
$base_dir = DUTCHIE_ANALYTICS_PATH . 'includes/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Initialize plugin
function dutchie_analytics_init() {
// Load API Client
require_once DUTCHIE_ANALYTICS_PATH . 'includes/API_Client.php';
// Load Shortcodes
require_once DUTCHIE_ANALYTICS_PATH . 'includes/Shortcodes.php';
new DutchieAnalytics\Shortcodes();
// Load Admin Settings
if (is_admin()) {
require_once DUTCHIE_ANALYTICS_PATH . 'includes/Admin_Settings.php';
new DutchieAnalytics\Admin_Settings();
}
// Register Elementor Widgets
add_action('elementor/widgets/register', function($widgets_manager) {
require_once DUTCHIE_ANALYTICS_PATH . 'includes/elementor/Products_Grid_Widget.php';
require_once DUTCHIE_ANALYTICS_PATH . 'includes/elementor/Products_Carousel_Widget.php';
require_once DUTCHIE_ANALYTICS_PATH . 'includes/elementor/Stores_List_Widget.php';
require_once DUTCHIE_ANALYTICS_PATH . 'includes/elementor/Brands_List_Widget.php';
require_once DUTCHIE_ANALYTICS_PATH . 'includes/elementor/Specials_Widget.php';
$widgets_manager->register(new \DutchieAnalytics\Elementor\Products_Grid_Widget());
$widgets_manager->register(new \DutchieAnalytics\Elementor\Products_Carousel_Widget());
$widgets_manager->register(new \DutchieAnalytics\Elementor\Stores_List_Widget());
$widgets_manager->register(new \DutchieAnalytics\Elementor\Brands_List_Widget());
$widgets_manager->register(new \DutchieAnalytics\Elementor\Specials_Widget());
});
// Register Elementor Widget Category
add_action('elementor/elements/categories_registered', function($elements_manager) {
$elements_manager->add_category(
'dutchie-analytics',
[
'title' => __('Dutchie Analytics', 'dutchie-analytics'),
'icon' => 'fa fa-plug',
]
);
});
}
add_action('plugins_loaded', 'dutchie_analytics_init');
// Enqueue styles and scripts
function dutchie_analytics_enqueue_assets() {
wp_enqueue_style(
'dutchie-analytics-styles',
DUTCHIE_ANALYTICS_URL . 'assets/css/dutchie-analytics.css',
[],
DUTCHIE_ANALYTICS_VERSION
);
wp_enqueue_script(
'dutchie-analytics-scripts',
DUTCHIE_ANALYTICS_URL . 'assets/js/dutchie-analytics.js',
['jquery'],
DUTCHIE_ANALYTICS_VERSION,
true
);
// Enqueue Swiper for carousels
wp_enqueue_style(
'swiper-css',
'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css',
[],
'11.0.0'
);
wp_enqueue_script(
'swiper-js',
'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js',
[],
'11.0.0',
true
);
}
add_action('wp_enqueue_scripts', 'dutchie_analytics_enqueue_assets');
// Activation hook
register_activation_hook(__FILE__, function() {
// Set default options
add_option('dutchie_analytics_api_url', 'http://localhost:3010/api');
add_option('dutchie_analytics_api_token', '');
});
// Deactivation hook
register_deactivation_hook(__FILE__, function() {
// Cleanup if needed
});

View File

@@ -1,121 +0,0 @@
<?php
namespace DutchieAnalytics;
class API_Client {
private $api_url;
private $api_token;
public function __construct() {
$this->api_url = get_option('dutchie_analytics_api_url', 'http://localhost:3010/api');
// Try to get API key from permissions first, fallback to manual token
$this->api_token = get_option('dutchie_analytics_api_token', '');
if (empty($this->api_token) && class_exists('DutchieAnalytics\API_Permissions')) {
$this->api_token = API_Permissions::get_site_api_key();
}
}
/**
* Make API request
*/
private function request($endpoint, $params = []) {
$url = $this->api_url . $endpoint;
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
$args = [
'headers' => [
'X-API-Key' => $this->api_token,
'Content-Type' => 'application/json',
],
'timeout' => 30,
];
$response = wp_remote_get($url, $args);
if (is_wp_error($response)) {
error_log('Dutchie Analytics API Error: ' . $response->get_error_message());
return null;
}
$body = wp_remote_retrieve_body($response);
return json_decode($body, true);
}
/**
* Get all stores
*/
public function get_stores() {
return $this->request('/stores');
}
/**
* Get single store
*/
public function get_store($id) {
return $this->request('/stores/' . $id);
}
/**
* Get store brands
*/
public function get_store_brands($store_id) {
return $this->request('/stores/' . $store_id . '/brands');
}
/**
* Get store specials
*/
public function get_store_specials($store_id, $date = null) {
$params = $date ? ['date' => $date] : [];
return $this->request('/stores/' . $store_id . '/specials', $params);
}
/**
* Get products
*
* @param array $args {
* @type int $store_id Filter by store ID
* @type int $category_id Filter by category ID
* @type bool $in_stock Filter by stock availability
* @type string $search Search query
* @type int $limit Results per page (default: 50, max: 1000)
* @type int $offset Pagination offset
* @type string $fields Comma-separated list of fields to return
* }
*/
public function get_products($args = []) {
$defaults = [
'limit' => 50,
'offset' => 0,
];
$params = wp_parse_args($args, $defaults);
return $this->request('/products', $params);
}
/**
* Get single product
*/
public function get_product($id) {
return $this->request('/products/' . $id);
}
/**
* Get categories
*/
public function get_categories($store_id = null) {
$params = $store_id ? ['store_id' => $store_id] : [];
return $this->request('/categories', $params);
}
/**
* Get category tree
*/
public function get_category_tree($store_id) {
return $this->request('/categories/tree', ['store_id' => $store_id]);
}
}

View File

@@ -1,113 +0,0 @@
<?php
namespace DutchieAnalytics;
class Admin_Settings {
public function __construct() {
add_action('admin_menu', [$this, 'add_settings_page']);
add_action('admin_init', [$this, 'register_settings']);
}
public function add_settings_page() {
add_options_page(
__('Dutchie Analytics Settings', 'dutchie-analytics'),
__('Dutchie Analytics', 'dutchie-analytics'),
'manage_options',
'dutchie-analytics',
[$this, 'render_settings_page']
);
}
public function register_settings() {
register_setting('dutchie_analytics_settings', 'dutchie_analytics_api_url');
register_setting('dutchie_analytics_settings', 'dutchie_analytics_api_token');
add_settings_section(
'dutchie_analytics_main',
__('API Configuration', 'dutchie-analytics'),
[$this, 'render_section_description'],
'dutchie-analytics'
);
add_settings_field(
'dutchie_analytics_api_url',
__('API URL', 'dutchie-analytics'),
[$this, 'render_api_url_field'],
'dutchie-analytics',
'dutchie_analytics_main'
);
add_settings_field(
'dutchie_analytics_api_token',
__('API Token', 'dutchie-analytics'),
[$this, 'render_api_token_field'],
'dutchie-analytics',
'dutchie_analytics_main'
);
}
public function render_settings_page() {
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields('dutchie_analytics_settings');
do_settings_sections('dutchie-analytics');
submit_button(__('Save Settings', 'dutchie-analytics'));
?>
</form>
<hr>
<h2><?php _e('Test Connection', 'dutchie-analytics'); ?></h2>
<?php
$api = new API_Client();
$stores = $api->get_stores();
if ($stores && isset($stores['stores'])) {
echo '<p style="color: green;"><strong>✓ Connection successful!</strong></p>';
echo '<p>' . sprintf(__('Found %d stores', 'dutchie-analytics'), count($stores['stores'])) . '</p>';
} else {
echo '<p style="color: red;"><strong>✗ Connection failed</strong></p>';
echo '<p>' . __('Please check your API URL and Token', 'dutchie-analytics') . '</p>';
}
?>
<hr>
<h2><?php _e('Documentation', 'dutchie-analytics'); ?></h2>
<h3><?php _e('Shortcodes', 'dutchie-analytics'); ?></h3>
<ul>
<li><code>[dutchie_products store_id="1" limit="12"]</code> - Display products grid</li>
<li><code>[dutchie_carousel store_id="1" limit="20"]</code> - Display products carousel</li>
<li><code>[dutchie_stores]</code> - Display all stores</li>
<li><code>[dutchie_brands store_id="1"]</code> - Display store brands</li>
<li><code>[dutchie_specials store_id="1"]</code> - Display today's specials</li>
</ul>
<h3><?php _e('Field Selection', 'dutchie-analytics'); ?></h3>
<p>You can select which fields to display using the <code>fields</code> parameter:</p>
<code>[dutchie_products store_id="1" fields="id,name,price,brand,in_stock"]</code>
<h3><?php _e('Elementor Widgets', 'dutchie-analytics'); ?></h3>
<p>Find all widgets in the <strong>Dutchie Analytics</strong> category in Elementor.</p>
</div>
<?php
}
public function render_section_description() {
echo '<p>' . __('Configure your Dutchie Analytics API connection', 'dutchie-analytics') . '</p>';
}
public function render_api_url_field() {
$value = get_option('dutchie_analytics_api_url', 'http://localhost:3010/api');
echo '<input type="text" name="dutchie_analytics_api_url" value="' . esc_attr($value) . '" class="regular-text">';
echo '<p class="description">' . __('Example: https://api.dutchieanalytics.com/api', 'dutchie-analytics') . '</p>';
}
public function render_api_token_field() {
$value = get_option('dutchie_analytics_api_token', '');
echo '<input type="password" name="dutchie_analytics_api_token" value="' . esc_attr($value) . '" class="regular-text">';
echo '<p class="description">' . __('Your JWT authentication token', 'dutchie-analytics') . '</p>';
}
}

View File

@@ -1,349 +0,0 @@
<?php
namespace DutchieAnalytics;
class Shortcodes {
public function __construct() {
add_shortcode('dutchie_products', [$this, 'products_grid']);
add_shortcode('dutchie_carousel', [$this, 'products_carousel']);
add_shortcode('dutchie_stores', [$this, 'stores_list']);
add_shortcode('dutchie_brands', [$this, 'brands_list']);
add_shortcode('dutchie_specials', [$this, 'specials']);
}
/**
* Products Grid Shortcode
* [dutchie_products store_id="1" limit="12" category_id="5" fields="name,price,brand"]
*/
public function products_grid($atts) {
$atts = shortcode_atts([
'store_id' => '',
'category_id' => '',
'limit' => 12,
'in_stock' => 'true',
'search' => '',
'columns' => 4,
'fields' => '',
'style' => 'modern',
], $atts);
if (empty($atts['store_id'])) {
return '<p>' . __('Please specify a store_id', 'dutchie-analytics') . '</p>';
}
$api = new API_Client();
$args = [
'store_id' => $atts['store_id'],
'limit' => (int) $atts['limit'],
];
if (!empty($atts['category_id'])) {
$args['category_id'] = $atts['category_id'];
}
if (!empty($atts['search'])) {
$args['search'] = $atts['search'];
}
if ($atts['in_stock'] === 'true') {
$args['in_stock'] = true;
}
if (!empty($atts['fields'])) {
$args['fields'] = $atts['fields'];
}
$data = $api->get_products($args);
$products = $data['products'] ?? [];
if (empty($products)) {
return '<p>' . __('No products found', 'dutchie-analytics') . '</p>';
}
ob_start();
?>
<div class="dutchie-products-grid" data-columns="<?php echo esc_attr($atts['columns']); ?>">
<?php foreach ($products as $product): ?>
<div class="dutchie-product-card dutchie-product-card-<?php echo esc_attr($atts['style']); ?>">
<?php if (!empty($product['image_url_full'])): ?>
<div class="dutchie-product-image">
<img src="<?php echo esc_url($product['image_url_full']); ?>" alt="<?php echo esc_attr($product['name']); ?>" loading="lazy">
<?php if ($product['in_stock']): ?>
<span class="dutchie-stock-badge in-stock"><?php _e('In Stock', 'dutchie-analytics'); ?></span>
<?php else: ?>
<span class="dutchie-stock-badge out-of-stock"><?php _e('Out of Stock', 'dutchie-analytics'); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="dutchie-product-content">
<h3 class="dutchie-product-name"><?php echo esc_html($product['name']); ?></h3>
<?php if (!empty($product['brand'])): ?>
<p class="dutchie-product-brand"><?php echo esc_html($product['brand']); ?></p>
<?php endif; ?>
<?php if (!empty($product['description'])): ?>
<p class="dutchie-product-description"><?php echo esc_html(wp_trim_words($product['description'], 20)); ?></p>
<?php endif; ?>
<div class="dutchie-product-meta">
<?php if (!empty($product['price'])): ?>
<span class="dutchie-product-price">$<?php echo number_format($product['price'], 2); ?></span>
<?php endif; ?>
<?php if (!empty($product['thc_percentage'])): ?>
<span class="dutchie-thc-badge"><?php echo esc_html($product['thc_percentage']); ?>% THC</span>
<?php endif; ?>
<?php if (!empty($product['cbd_percentage'])): ?>
<span class="dutchie-cbd-badge"><?php echo esc_html($product['cbd_percentage']); ?>% CBD</span>
<?php endif; ?>
</div>
<?php if (!empty($product['weight'])): ?>
<p class="dutchie-product-weight"><?php echo esc_html($product['weight']); ?></p>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Products Carousel Shortcode
* [dutchie_carousel store_id="1" limit="20"]
*/
public function products_carousel($atts) {
$atts = shortcode_atts([
'store_id' => '',
'category_id' => '',
'limit' => 20,
'in_stock' => 'true',
'slides_per_view' => 4,
'autoplay' => 'true',
'style' => 'modern',
], $atts);
if (empty($atts['store_id'])) {
return '<p>' . __('Please specify a store_id', 'dutchie-analytics') . '</p>';
}
$api = new API_Client();
$args = [
'store_id' => $atts['store_id'],
'limit' => (int) $atts['limit'],
];
if (!empty($atts['category_id'])) {
$args['category_id'] = $atts['category_id'];
}
if ($atts['in_stock'] === 'true') {
$args['in_stock'] = true;
}
$data = $api->get_products($args);
$products = $data['products'] ?? [];
if (empty($products)) {
return '<p>' . __('No products found', 'dutchie-analytics') . '</p>';
}
$carousel_id = 'dutchie-carousel-' . uniqid();
ob_start();
?>
<div class="dutchie-products-carousel-wrapper">
<div class="swiper <?php echo esc_attr($carousel_id); ?>">
<div class="swiper-wrapper">
<?php foreach ($products as $product): ?>
<div class="swiper-slide">
<div class="dutchie-product-card dutchie-product-card-<?php echo esc_attr($atts['style']); ?>">
<?php if (!empty($product['image_url_full'])): ?>
<div class="dutchie-product-image">
<img src="<?php echo esc_url($product['image_url_full']); ?>" alt="<?php echo esc_attr($product['name']); ?>" loading="lazy">
<?php if ($product['in_stock']): ?>
<span class="dutchie-stock-badge in-stock"><?php _e('In Stock', 'dutchie-analytics'); ?></span>
<?php else: ?>
<span class="dutchie-stock-badge out-of-stock"><?php _e('Out of Stock', 'dutchie-analytics'); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="dutchie-product-content">
<h3 class="dutchie-product-name"><?php echo esc_html($product['name']); ?></h3>
<?php if (!empty($product['brand'])): ?>
<p class="dutchie-product-brand"><?php echo esc_html($product['brand']); ?></p>
<?php endif; ?>
<div class="dutchie-product-meta">
<?php if (!empty($product['price'])): ?>
<span class="dutchie-product-price">$<?php echo number_format($product['price'], 2); ?></span>
<?php endif; ?>
<?php if (!empty($product['thc_percentage'])): ?>
<span class="dutchie-thc-badge"><?php echo esc_html($product['thc_percentage']); ?>% THC</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<div class="swiper-pagination"></div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
new Swiper('.<?php echo esc_js($carousel_id); ?>', {
slidesPerView: 1,
spaceBetween: 24,
loop: true,
<?php if ($atts['autoplay'] === 'true'): ?>
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
<?php endif; ?>
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
pagination: {
el: '.swiper-pagination',
clickable: true,
},
breakpoints: {
640: { slidesPerView: 2 },
768: { slidesPerView: 3 },
1024: { slidesPerView: <?php echo (int) $atts['slides_per_view']; ?> },
}
});
});
</script>
<?php
return ob_get_clean();
}
/**
* Stores List Shortcode
* [dutchie_stores]
*/
public function stores_list($atts) {
$api = new API_Client();
$data = $api->get_stores();
$stores = $data['stores'] ?? [];
if (empty($stores)) {
return '<p>' . __('No stores found', 'dutchie-analytics') . '</p>';
}
ob_start();
?>
<div class="dutchie-stores-grid">
<?php foreach ($stores as $store): ?>
<div class="dutchie-store-card">
<?php if (!empty($store['logo_url'])): ?>
<img src="<?php echo esc_url($store['logo_url']); ?>" alt="<?php echo esc_attr($store['name']); ?>" class="dutchie-store-logo">
<?php endif; ?>
<h3><?php echo esc_html($store['name']); ?></h3>
<p class="dutchie-store-stats">
<?php printf(__('%d Products', 'dutchie-analytics'), $store['product_count']); ?>
<?php printf(__('%d Categories', 'dutchie-analytics'), $store['category_count']); ?>
</p>
<?php if ($store['dutchie_url']): ?>
<a href="<?php echo esc_url($store['dutchie_url']); ?>" target="_blank" class="dutchie-store-link">
<?php _e('View Store', 'dutchie-analytics'); ?>
</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Brands List Shortcode
* [dutchie_brands store_id="1"]
*/
public function brands_list($atts) {
$atts = shortcode_atts(['store_id' => ''], $atts);
if (empty($atts['store_id'])) {
return '<p>' . __('Please specify a store_id', 'dutchie-analytics') . '</p>';
}
$api = new API_Client();
$data = $api->get_store_brands($atts['store_id']);
$brands = $data['brands'] ?? [];
if (empty($brands)) {
return '<p>' . __('No brands found', 'dutchie-analytics') . '</p>';
}
ob_start();
?>
<div class="dutchie-brands-grid">
<?php foreach ($brands as $brand): ?>
<div class="dutchie-brand-badge"><?php echo esc_html($brand); ?></div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Specials Shortcode
* [dutchie_specials store_id="1" date="2025-01-15"]
*/
public function specials($atts) {
$atts = shortcode_atts([
'store_id' => '',
'date' => '',
], $atts);
if (empty($atts['store_id'])) {
return '<p>' . __('Please specify a store_id', 'dutchie-analytics') . '</p>';
}
$api = new API_Client();
$data = $api->get_store_specials($atts['store_id'], $atts['date'] ?: null);
$specials = $data['specials'] ?? [];
if (empty($specials)) {
return '<p>' . __('No specials for this date', 'dutchie-analytics') . '</p>';
}
ob_start();
?>
<div class="dutchie-specials-grid">
<?php foreach ($specials as $special): ?>
<div class="dutchie-special-card">
<?php if (!empty($special['product_image'])): ?>
<img src="<?php echo esc_url($special['product_image']); ?>" alt="<?php echo esc_attr($special['name']); ?>">
<?php endif; ?>
<h3><?php echo esc_html($special['name']); ?></h3>
<?php if (!empty($special['description'])): ?>
<p><?php echo esc_html($special['description']); ?></p>
<?php endif; ?>
<?php if (!empty($special['discount_percentage'])): ?>
<span class="dutchie-discount-badge"><?php echo esc_html($special['discount_percentage']); ?>% OFF</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -1,82 +0,0 @@
<?php
namespace DutchieAnalytics\Elementor;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
use DutchieAnalytics\API_Client;
class Brands_List_Widget extends Widget_Base {
public function get_name() {
return 'dutchie_brands_list';
}
public function get_title() {
return __('Brands List', 'dutchie-analytics');
}
public function get_icon() {
return 'eicon-tags';
}
public function get_categories() {
return ['dutchie-analytics'];
}
protected function register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => __('Content', 'dutchie-analytics'),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
$api = new API_Client();
$stores_data = $api->get_stores();
$stores = [];
if ($stores_data && isset($stores_data['stores'])) {
foreach ($stores_data['stores'] as $store) {
$stores[$store['id']] = $store['name'];
}
}
$this->add_control(
'store_id',
[
'label' => __('Store', 'dutchie-analytics'),
'type' => Controls_Manager::SELECT,
'options' => $stores,
'default' => array_key_first($stores) ?: '',
]
);
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
if (empty($settings['store_id'])) {
echo '<p>' . __('Please select a store', 'dutchie-analytics') . '</p>';
return;
}
$api = new API_Client();
$data = $api->get_store_brands($settings['store_id']);
$brands = $data['brands'] ?? [];
if (empty($brands)) {
echo '<p>' . __('No brands found', 'dutchie-analytics') . '</p>';
return;
}
?>
<div class="dutchie-brands-grid">
<?php foreach ($brands as $brand): ?>
<div class="dutchie-brand-badge"><?php echo esc_html($brand); ?></div>
<?php endforeach; ?>
</div>
<?php
}
}

View File

@@ -1,336 +0,0 @@
<?php
namespace DutchieAnalytics\Elementor;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
use DutchieAnalytics\API_Client;
class Products_Carousel_Widget extends Widget_Base {
public function get_name() {
return 'dutchie_products_carousel';
}
public function get_title() {
return __('Products Carousel', 'dutchie-analytics');
}
public function get_icon() {
return 'eicon-slider-push';
}
public function get_categories() {
return ['dutchie-analytics'];
}
protected function register_controls() {
// Content Section
$this->start_controls_section(
'content_section',
[
'label' => __('Content', 'dutchie-analytics'),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
// Store selection
$api = new API_Client();
$stores_data = $api->get_stores();
$stores = [];
if ($stores_data && isset($stores_data['stores'])) {
foreach ($stores_data['stores'] as $store) {
$stores[$store['id']] = $store['name'];
}
}
$this->add_control(
'store_id',
[
'label' => __('Store', 'dutchie-analytics'),
'type' => Controls_Manager::SELECT,
'options' => $stores,
'default' => array_key_first($stores) ?: '',
]
);
$this->add_control(
'category_id',
[
'label' => __('Category', 'dutchie-analytics'),
'type' => Controls_Manager::TEXT,
'placeholder' => __('Leave empty for all categories', 'dutchie-analytics'),
]
);
$this->add_control(
'limit',
[
'label' => __('Number of Products', 'dutchie-analytics'),
'type' => Controls_Manager::NUMBER,
'default' => 20,
'min' => 1,
'max' => 100,
]
);
$this->add_control(
'in_stock_only',
[
'label' => __('In Stock Only', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->add_control(
'slides_per_view',
[
'label' => __('Slides Per View', 'dutchie-analytics'),
'type' => Controls_Manager::NUMBER,
'default' => 4,
'min' => 1,
'max' => 6,
]
);
$this->add_control(
'space_between',
[
'label' => __('Space Between Slides', 'dutchie-analytics'),
'type' => Controls_Manager::NUMBER,
'default' => 24,
'min' => 0,
'max' => 100,
]
);
$this->add_control(
'autoplay',
[
'label' => __('Autoplay', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->add_control(
'loop',
[
'label' => __('Loop', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'yes',
]
);
// Field selection
$this->add_control(
'show_brand',
[
'label' => __('Show Brand', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->add_control(
'show_description',
[
'label' => __('Show Description', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'no',
]
);
$this->add_control(
'show_thc',
[
'label' => __('Show THC %', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->add_control(
'show_price',
[
'label' => __('Show Price', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->end_controls_section();
// Style Section
$this->start_controls_section(
'style_section',
[
'label' => __('Style', 'dutchie-analytics'),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$this->add_control(
'card_style',
[
'label' => __('Card Style', 'dutchie-analytics'),
'type' => Controls_Manager::SELECT,
'options' => [
'modern' => __('Modern', 'dutchie-analytics'),
'minimal' => __('Minimal', 'dutchie-analytics'),
'bold' => __('Bold', 'dutchie-analytics'),
],
'default' => 'modern',
]
);
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
if (empty($settings['store_id'])) {
echo '<p>' . __('Please select a store', 'dutchie-analytics') . '</p>';
return;
}
$api = new API_Client();
$args = [
'store_id' => $settings['store_id'],
'limit' => $settings['limit'],
];
if (!empty($settings['category_id'])) {
$args['category_id'] = $settings['category_id'];
}
if ($settings['in_stock_only'] === 'yes') {
$args['in_stock'] = true;
}
$data = $api->get_products($args);
$products = $data['products'] ?? [];
if (empty($products)) {
echo '<p>' . __('No products found', 'dutchie-analytics') . '</p>';
return;
}
$carousel_id = 'dutchie-carousel-' . uniqid();
$card_class = 'dutchie-product-card-' . $settings['card_style'];
?>
<div class="dutchie-products-carousel-wrapper">
<div class="swiper <?php echo esc_attr($carousel_id); ?>">
<div class="swiper-wrapper">
<?php foreach ($products as $product): ?>
<div class="swiper-slide">
<div class="dutchie-product-card <?php echo esc_attr($card_class); ?>">
<?php if (!empty($product['image_url_full'])): ?>
<div class="dutchie-product-image">
<img src="<?php echo esc_url($product['image_url_full']); ?>" alt="<?php echo esc_attr($product['name']); ?>">
<?php if ($product['in_stock']): ?>
<span class="dutchie-stock-badge in-stock"><?php _e('In Stock', 'dutchie-analytics'); ?></span>
<?php else: ?>
<span class="dutchie-stock-badge out-of-stock"><?php _e('Out of Stock', 'dutchie-analytics'); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="dutchie-product-content">
<h3 class="dutchie-product-name"><?php echo esc_html($product['name']); ?></h3>
<?php if ($settings['show_brand'] === 'yes' && !empty($product['brand'])): ?>
<p class="dutchie-product-brand"><?php echo esc_html($product['brand']); ?></p>
<?php endif; ?>
<?php if ($settings['show_description'] === 'yes' && !empty($product['description'])): ?>
<p class="dutchie-product-description"><?php echo esc_html(wp_trim_words($product['description'], 15)); ?></p>
<?php endif; ?>
<div class="dutchie-product-meta">
<?php if ($settings['show_price'] === 'yes' && !empty($product['price'])): ?>
<span class="dutchie-product-price">$<?php echo number_format($product['price'], 2); ?></span>
<?php endif; ?>
<?php if ($settings['show_thc'] === 'yes' && !empty($product['thc_percentage'])): ?>
<span class="dutchie-thc-badge"><?php echo esc_html($product['thc_percentage']); ?>% THC</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Navigation -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- Pagination -->
<div class="swiper-pagination"></div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
new Swiper('.<?php echo esc_js($carousel_id); ?>', {
slidesPerView: 1,
spaceBetween: <?php echo (int) $settings['space_between']; ?>,
<?php if ($settings['loop'] === 'yes'): ?>
loop: true,
<?php endif; ?>
<?php if ($settings['autoplay'] === 'yes'): ?>
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
<?php endif; ?>
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
pagination: {
el: '.swiper-pagination',
clickable: true,
},
breakpoints: {
640: {
slidesPerView: 2,
},
768: {
slidesPerView: 3,
},
1024: {
slidesPerView: <?php echo (int) $settings['slides_per_view']; ?>,
},
}
});
});
</script>
<?php
}
}

View File

@@ -1,194 +0,0 @@
<?php
namespace DutchieAnalytics\Elementor;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
use DutchieAnalytics\API_Client;
class Products_Grid_Widget extends Widget_Base {
public function get_name() {
return 'dutchie_products_grid';
}
public function get_title() {
return __('Products Grid', 'dutchie-analytics');
}
public function get_icon() {
return 'eicon-gallery-grid';
}
public function get_categories() {
return ['dutchie-analytics'];
}
protected function register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => __('Content', 'dutchie-analytics'),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
$api = new API_Client();
$stores_data = $api->get_stores();
$stores = [];
if ($stores_data && isset($stores_data['stores'])) {
foreach ($stores_data['stores'] as $store) {
$stores[$store['id']] = $store['name'];
}
}
$this->add_control(
'store_id',
[
'label' => __('Store', 'dutchie-analytics'),
'type' => Controls_Manager::SELECT,
'options' => $stores,
'default' => array_key_first($stores) ?: '',
]
);
$this->add_control(
'category_id',
[
'label' => __('Category', 'dutchie-analytics'),
'type' => Controls_Manager::TEXT,
'placeholder' => __('Leave empty for all categories', 'dutchie-analytics'),
]
);
$this->add_control(
'limit',
[
'label' => __('Number of Products', 'dutchie-analytics'),
'type' => Controls_Manager::NUMBER,
'default' => 12,
'min' => 1,
'max' => 100,
]
);
$this->add_control(
'columns',
[
'label' => __('Columns', 'dutchie-analytics'),
'type' => Controls_Manager::NUMBER,
'default' => 4,
'min' => 1,
'max' => 6,
]
);
$this->add_control(
'in_stock_only',
[
'label' => __('In Stock Only', 'dutchie-analytics'),
'type' => Controls_Manager::SWITCHER,
'label_on' => __('Yes', 'dutchie-analytics'),
'label_off' => __('No', 'dutchie-analytics'),
'return_value' => 'yes',
'default' => 'yes',
]
);
$this->add_control(
'card_style',
[
'label' => __('Card Style', 'dutchie-analytics'),
'type' => Controls_Manager::SELECT,
'options' => [
'modern' => __('Modern', 'dutchie-analytics'),
'minimal' => __('Minimal', 'dutchie-analytics'),
'bold' => __('Bold', 'dutchie-analytics'),
],
'default' => 'modern',
]
);
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
if (empty($settings['store_id'])) {
echo '<p>' . __('Please select a store', 'dutchie-analytics') . '</p>';
return;
}
$api = new API_Client();
$args = [
'store_id' => $settings['store_id'],
'limit' => $settings['limit'],
];
if (!empty($settings['category_id'])) {
$args['category_id'] = $settings['category_id'];
}
if ($settings['in_stock_only'] === 'yes') {
$args['in_stock'] = true;
}
$data = $api->get_products($args);
$products = $data['products'] ?? [];
if (empty($products)) {
echo '<p>' . __('No products found', 'dutchie-analytics') . '</p>';
return;
}
?>
<div class="dutchie-products-grid" data-columns="<?php echo esc_attr($settings['columns']); ?>">
<?php foreach ($products as $product): ?>
<div class="dutchie-product-card dutchie-product-card-<?php echo esc_attr($settings['card_style']); ?>">
<?php if (!empty($product['image_url_full'])): ?>
<div class="dutchie-product-image">
<img src="<?php echo esc_url($product['image_url_full']); ?>" alt="<?php echo esc_attr($product['name']); ?>" loading="lazy">
<?php if ($product['in_stock']): ?>
<span class="dutchie-stock-badge in-stock"><?php _e('In Stock', 'dutchie-analytics'); ?></span>
<?php else: ?>
<span class="dutchie-stock-badge out-of-stock"><?php _e('Out of Stock', 'dutchie-analytics'); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="dutchie-product-content">
<h3 class="dutchie-product-name"><?php echo esc_html($product['name']); ?></h3>
<?php if (!empty($product['brand'])): ?>
<p class="dutchie-product-brand"><?php echo esc_html($product['brand']); ?></p>
<?php endif; ?>
<?php if (!empty($product['description'])): ?>
<p class="dutchie-product-description"><?php echo esc_html(wp_trim_words($product['description'], 20)); ?></p>
<?php endif; ?>
<div class="dutchie-product-meta">
<?php if (!empty($product['price'])): ?>
<span class="dutchie-product-price">$<?php echo number_format($product['price'], 2); ?></span>
<?php endif; ?>
<?php if (!empty($product['thc_percentage'])): ?>
<span class="dutchie-thc-badge"><?php echo esc_html($product['thc_percentage']); ?>% THC</span>
<?php endif; ?>
<?php if (!empty($product['cbd_percentage'])): ?>
<span class="dutchie-cbd-badge"><?php echo esc_html($product['cbd_percentage']); ?>% CBD</span>
<?php endif; ?>
</div>
<?php if (!empty($product['weight'])): ?>
<p class="dutchie-product-weight"><?php echo esc_html($product['weight']); ?></p>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php
}
}

View File

@@ -1,105 +0,0 @@
<?php
namespace DutchieAnalytics\Elementor;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
use DutchieAnalytics\API_Client;
class Specials_Widget extends Widget_Base {
public function get_name() {
return 'dutchie_specials';
}
public function get_title() {
return __('Daily Specials', 'dutchie-analytics');
}
public function get_icon() {
return 'eicon-flash';
}
public function get_categories() {
return ['dutchie-analytics'];
}
protected function register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => __('Content', 'dutchie-analytics'),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
$api = new API_Client();
$stores_data = $api->get_stores();
$stores = [];
if ($stores_data && isset($stores_data['stores'])) {
foreach ($stores_data['stores'] as $store) {
$stores[$store['id']] = $store['name'];
}
}
$this->add_control(
'store_id',
[
'label' => __('Store', 'dutchie-analytics'),
'type' => Controls_Manager::SELECT,
'options' => $stores,
'default' => array_key_first($stores) ?: '',
]
);
$this->add_control(
'date',
[
'label' => __('Date', 'dutchie-analytics'),
'type' => Controls_Manager::DATE_TIME,
'picker_options' => [
'enableTime' => false,
],
]
);
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
if (empty($settings['store_id'])) {
echo '<p>' . __('Please select a store', 'dutchie-analytics') . '</p>';
return;
}
$api = new API_Client();
$date = !empty($settings['date']) ? date('Y-m-d', strtotime($settings['date'])) : null;
$data = $api->get_store_specials($settings['store_id'], $date);
$specials = $data['specials'] ?? [];
if (empty($specials)) {
echo '<p>' . __('No specials for this date', 'dutchie-analytics') . '</p>';
return;
}
?>
<div class="dutchie-specials-grid">
<?php foreach ($specials as $special): ?>
<div class="dutchie-special-card">
<?php if (!empty($special['product_image'])): ?>
<img src="<?php echo esc_url($special['product_image']); ?>" alt="<?php echo esc_attr($special['name']); ?>" loading="lazy">
<?php endif; ?>
<h3><?php echo esc_html($special['name']); ?></h3>
<?php if (!empty($special['description'])): ?>
<p><?php echo esc_html($special['description']); ?></p>
<?php endif; ?>
<?php if (!empty($special['discount_percentage'])): ?>
<span class="dutchie-discount-badge"><?php echo esc_html($special['discount_percentage']); ?>% OFF</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
}
}

View File

@@ -1,82 +0,0 @@
<?php
namespace DutchieAnalytics\Elementor;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
use DutchieAnalytics\API_Client;
class Stores_List_Widget extends Widget_Base {
public function get_name() {
return 'dutchie_stores_list';
}
public function get_title() {
return __('Stores List', 'dutchie-analytics');
}
public function get_icon() {
return 'eicon-products';
}
public function get_categories() {
return ['dutchie-analytics'];
}
protected function register_controls() {
$this->start_controls_section(
'content_section',
[
'label' => __('Content', 'dutchie-analytics'),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
$this->add_control(
'columns',
[
'label' => __('Columns', 'dutchie-analytics'),
'type' => Controls_Manager::NUMBER,
'default' => 3,
'min' => 1,
'max' => 4,
]
);
$this->end_controls_section();
}
protected function render() {
$api = new API_Client();
$data = $api->get_stores();
$stores = $data['stores'] ?? [];
if (empty($stores)) {
echo '<p>' . __('No stores found', 'dutchie-analytics') . '</p>';
return;
}
?>
<div class="dutchie-stores-grid">
<?php foreach ($stores as $store): ?>
<div class="dutchie-store-card">
<?php if (!empty($store['logo_url'])): ?>
<img src="<?php echo esc_url($store['logo_url']); ?>" alt="<?php echo esc_attr($store['name']); ?>" class="dutchie-store-logo">
<?php endif; ?>
<h3><?php echo esc_html($store['name']); ?></h3>
<p class="dutchie-store-stats">
<?php printf(__('%d Products', 'dutchie-analytics'), $store['product_count']); ?>
<?php printf(__('%d Categories', 'dutchie-analytics'), $store['category_count']); ?>
</p>
<?php if ($store['dutchie_url']): ?>
<a href="<?php echo esc_url($store['dutchie_url']); ?>" target="_blank" class="dutchie-store-link">
<?php _e('View Store', 'dutchie-analytics'); ?>
</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
}
}

View File

@@ -11,11 +11,16 @@ $columns = isset($atts['columns']) ? $atts['columns'] : 3;
?>
<div class="crawlsy-product-grid crawlsy-grid-cols-<?php echo esc_attr($columns); ?>">
<?php foreach ($products as $product): ?>
<div class="crawlsy-product-card">
<?php if (!empty($product['image_url_full'])): ?>
<?php foreach ($products as $product):
$image_url = $product['image_url'] ?? $product['primary_image_url'] ?? $product['image_url_full'] ?? '';
$product_url = !empty($product['dutchie_url']) ? $product['dutchie_url'] : '#';
?>
<div class="crawlsy-product-card"
<?php if ($product_url !== '#'): ?>onclick="window.open('<?php echo esc_url($product_url); ?>', '_blank')"<?php endif; ?>
style="cursor: <?php echo ($product_url !== '#') ? 'pointer' : 'default'; ?>;">
<?php if (!empty($image_url)): ?>
<div class="crawlsy-product-image">
<img src="<?php echo esc_url($product['image_url_full']); ?>"
<img src="<?php echo esc_url($image_url); ?>"
alt="<?php echo esc_attr($product['name']); ?>"
loading="lazy" />
</div>

View File

@@ -262,11 +262,16 @@ class Crawlsy_Menus_Product_Grid_Widget extends \Elementor\Widget_Base {
$columns = $settings['columns'];
?>
<div class="crawlsy-product-grid crawlsy-grid-cols-<?php echo esc_attr($columns); ?>">
<?php foreach ($products as $product): ?>
<div class="crawlsy-product-card">
<?php if ($settings['show_image'] === 'yes' && !empty($product['image_url_full'])): ?>
<?php foreach ($products as $product):
$image_url = $product['image_url'] ?? $product['primary_image_url'] ?? $product['image_url_full'] ?? '';
$product_url = !empty($product['dutchie_url']) ? $product['dutchie_url'] : '#';
?>
<div class="crawlsy-product-card"
<?php if ($product_url !== '#'): ?>onclick="window.open('<?php echo esc_url($product_url); ?>', '_blank')"<?php endif; ?>
style="cursor: <?php echo ($product_url !== '#') ? 'pointer' : 'default'; ?>;">
<?php if ($settings['show_image'] === 'yes' && !empty($image_url)): ?>
<div class="crawlsy-product-image">
<img src="<?php echo esc_url($product['image_url_full']); ?>"
<img src="<?php echo esc_url($image_url); ?>"
alt="<?php echo esc_attr($product['name']); ?>"
loading="lazy" />
</div>

View File

@@ -25,6 +25,33 @@ class Crawlsy_Menus_Single_Product_Widget extends \Elementor\Widget_Base {
return ['general'];
}
/**
* Get products for the SELECT2 dropdown
*
* @return array Associative array of product_id => product_name
*/
protected function get_products_for_select() {
$options = ['' => __('-- Select a Product --', 'crawlsy-menus')];
$plugin = Crawlsy_Menus_Plugin::instance();
$products = $plugin->fetch_products(['limit' => 500]);
if ($products && is_array($products)) {
foreach ($products as $product) {
$label = $product['name'];
if (!empty($product['brand'])) {
$label = $product['brand'] . ' - ' . $label;
}
if (!empty($product['category'])) {
$label .= ' (' . $product['category'] . ')';
}
$options[$product['id']] = $label;
}
}
return $options;
}
protected function register_controls() {
// Content Section
@@ -36,13 +63,31 @@ class Crawlsy_Menus_Single_Product_Widget extends \Elementor\Widget_Base {
]
);
// Get products for the select dropdown
$products_options = $this->get_products_for_select();
$this->add_control(
'product_id',
[
'label' => __('Product ID', 'crawlsy-menus'),
'label' => __('Select Product', 'crawlsy-menus'),
'type' => \Elementor\Controls_Manager::SELECT2,
'options' => $products_options,
'default' => '',
'label_block' => true,
'description' => __('Search and select a product to display', 'crawlsy-menus'),
]
);
$this->add_control(
'product_id_manual',
[
'label' => __('Or Enter Product ID', 'crawlsy-menus'),
'type' => \Elementor\Controls_Manager::NUMBER,
'default' => '',
'description' => __('Enter the product ID to display', 'crawlsy-menus'),
'description' => __('Manually enter a product ID if not found in dropdown', 'crawlsy-menus'),
'condition' => [
'product_id' => '',
],
]
);
@@ -228,13 +273,16 @@ class Crawlsy_Menus_Single_Product_Widget extends \Elementor\Widget_Base {
protected function render() {
$settings = $this->get_settings_for_display();
if (empty($settings['product_id'])) {
echo '<p>' . __('Please enter a product ID.', 'crawlsy-menus') . '</p>';
// Use dropdown selection, or fall back to manual ID
$product_id = !empty($settings['product_id']) ? $settings['product_id'] : $settings['product_id_manual'];
if (empty($product_id)) {
echo '<p>' . __('Please select or enter a product ID.', 'crawlsy-menus') . '</p>';
return;
}
$plugin = Crawlsy_Menus_Plugin::instance();
$product = $plugin->fetch_product($settings['product_id']);
$product = $plugin->fetch_product($product_id);
if (!$product) {
echo '<p>' . __('Product not found.', 'crawlsy-menus') . '</p>';