You've already forked wc-composable-product
Initial implementation of WooCommerce Composable Products plugin
Implemented custom WooCommerce product type allowing customers to build their own product bundles by selecting from predefined sets of products. Features: - Custom "Composable Product" type with admin interface - Product selection by category, tag, or SKU - Configurable selection limits (global and per-product) - Dual pricing modes: fixed price or sum of selected products - Modern responsive frontend with Twig templates - AJAX add-to-cart functionality - Full internationalization support (.pot file) - WooCommerce settings integration - Comprehensive documentation Technical implementation: - PHP 8.3+ with PSR-4 autoloading - Twig 3.0 templating engine via Composer - Vanilla JavaScript with jQuery for frontend interactions - WordPress and WooCommerce hooks for seamless integration - Security: input sanitization, validation, and output escaping - Translation-ready with text domain 'wc-composable-product' Documentation: - README.md: Project overview and features - INSTALL.md: Installation and usage guide - IMPLEMENTATION.md: Technical architecture - CHANGELOG.md: Version history 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Linked sources
|
||||
wp-core
|
||||
wp-plugins
|
||||
|
||||
# Editor swap files
|
||||
*.*swp
|
||||
|
||||
# Composer
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# Cache
|
||||
cache/
|
||||
|
||||
# Development files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Release files
|
||||
*.zip
|
||||
releases/
|
||||
34
CHANGELOG.md
Normal file
34
CHANGELOG.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2024-12-31
|
||||
|
||||
### Added
|
||||
- Initial release
|
||||
- Composable product type for WooCommerce
|
||||
- Product selection by category, tag, or SKU
|
||||
- Configurable selection limits (global and per-product)
|
||||
- Two pricing modes: fixed price or sum of selected products
|
||||
- Admin settings page
|
||||
- Frontend product selector with grid layout
|
||||
- AJAX add-to-cart functionality
|
||||
- Twig template engine integration
|
||||
- Full internationalization support
|
||||
- Responsive design
|
||||
- WooCommerce cart integration
|
||||
- Product data validation
|
||||
|
||||
### Features
|
||||
- Select products from predefined categories, tags, or SKUs
|
||||
- Limit number of items customers can select
|
||||
- Visual product selector with images and prices
|
||||
- Real-time price calculation
|
||||
- Clean, modern UI
|
||||
- Mobile responsive
|
||||
- Translation ready
|
||||
|
||||
[1.0.0]: https://github.com/magdev/wc-composable-product/releases/tag/v1.0.0
|
||||
92
CLAUDE.md
Normal file
92
CLAUDE.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# WooCommerce plugin for user composable products - AI Context Document
|
||||
|
||||
**Author:** Marco Graetsch
|
||||
|
||||
## Project Overview
|
||||
|
||||
This plugin implements a special product type, for which users can select a limited number of product from a configurable set of simple or variable products. The limit of selectable products should be a global and per-product setting, for which global is the fallback. The set of selectable products can be defined per category, tag or SKU. The price is either a fixed price or the sum of the prices of the selected products. Think of a package of stickers as composable product, where each package can contain $limit number of stickers.
|
||||
|
||||
### Key Fact: 100% AI-Generated
|
||||
|
||||
This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase was created through AI assistance.
|
||||
|
||||
## Technical Stack
|
||||
|
||||
- **Language:** PHP 8.3+
|
||||
- **Framework:** Latest WordPress Plugin API
|
||||
- **E-commerce:** WooCommerce 10.0+
|
||||
- **Template Engine:** Twig 3.0 (via Composer)
|
||||
- **Frontend:** Vanilla JavaScript + jQuery
|
||||
- **Styling:** Custom CSS
|
||||
- **Dependency Management:** Composer
|
||||
- **Internationalization:** WordPress i18n (.pot/.po/.mo files)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- All user inputs are sanitized (integers for quantities/prices)
|
||||
- Nonce verification on form submissions
|
||||
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
|
||||
- Direct file access prevention via `ABSPATH` check
|
||||
|
||||
### Translation Ready
|
||||
|
||||
All user-facing strings use:
|
||||
|
||||
```php
|
||||
__('Text to translate', 'wc-composable-product')
|
||||
_e('Text to translate', 'wc-composable-product')
|
||||
```
|
||||
|
||||
Text domain: `wc-composable-product`
|
||||
|
||||
**Available Translations (as of v1.1.22):**
|
||||
|
||||
- `en_US` - English (United States)
|
||||
- `de_DE` - German (Germany, formal)
|
||||
- `de_DE_informal` - German (Germany, informal "du")
|
||||
- `de_CH` - German (Switzerland, formal "Sie")
|
||||
- `de_CH_informal` - German (Switzerland, informal "du")
|
||||
- `fr_CH` - French (Switzerland)
|
||||
- `it_CH` - Italian (Switzerland)
|
||||
|
||||
Note: Swiss locales use CHF currency formatting in examples (e.g., "CHF 50.-")
|
||||
|
||||
### Create releases
|
||||
|
||||
**Important Notes**
|
||||
|
||||
- The `vendor/` directory MUST be included in releases (Twig dependency required for runtime)
|
||||
- Running zip from wrong directory creates empty or malformed archives
|
||||
- Exclusion patterns must match the relative path structure used in zip command
|
||||
- Always verify the package with `unzip -l` and test extraction before committing
|
||||
- The `wp-core/` and `wp-plugins/` directories MUST NOT be included in releases!
|
||||
|
||||
**Important Git Notes:**
|
||||
|
||||
- Always commit from `dev` branch first
|
||||
- Tags should use format `vX.X.X` (e.g., `v1.1.22`)
|
||||
- Use annotated tags (`-a`) not lightweight tags
|
||||
- Commit messages should follow the established format with Claude Code attribution
|
||||
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
|
||||
|
||||
#### What Gets Released
|
||||
|
||||
- All plugin source files
|
||||
- Compiled vendor dependencies
|
||||
- Translation files (.mo compiled from .po)
|
||||
- Assets (CSS, JS)
|
||||
- Documentation (README, CHANGELOG, etc.)
|
||||
|
||||
#### What's Excluded
|
||||
|
||||
- Git metadata (`.git/`)
|
||||
- Development files (`.vscode/`, `.claude/`, `CLAUDE.md`)
|
||||
- Logs and cache files
|
||||
- Previous releases
|
||||
- `composer.lock` (but `vendor/` is included)
|
||||
|
||||
---
|
||||
|
||||
Always refer to this document when starting work on this project. Good luck!
|
||||
406
IMPLEMENTATION.md
Normal file
406
IMPLEMENTATION.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# WooCommerce Composable Products - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a technical overview of the WooCommerce Composable Products plugin implementation.
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Created:** 2024-12-31
|
||||
**AI-Generated:** 100% created with Claude.AI assistance
|
||||
|
||||
## Architecture
|
||||
|
||||
### Plugin Structure
|
||||
|
||||
```
|
||||
wc-composable-product/
|
||||
├── assets/ # Frontend assets
|
||||
│ ├── css/
|
||||
│ │ ├── admin.css # Admin styles
|
||||
│ │ └── frontend.css # Frontend styles
|
||||
│ └── js/
|
||||
│ ├── admin.js # Admin JavaScript
|
||||
│ └── frontend.js # Frontend JavaScript
|
||||
├── cache/ # Twig template cache
|
||||
├── includes/ # PHP classes
|
||||
│ ├── Admin/
|
||||
│ │ ├── Product_Data.php # Product data tab
|
||||
│ │ └── Settings.php # Settings page
|
||||
│ ├── Cart_Handler.php # Cart integration
|
||||
│ ├── Plugin.php # Main plugin class
|
||||
│ ├── Product_Selector.php # Frontend selector
|
||||
│ └── Product_Type.php # Custom product type
|
||||
├── languages/ # Translation files
|
||||
│ └── wc-composable-product.pot
|
||||
├── templates/ # Twig templates
|
||||
│ └── product-selector.twig
|
||||
└── wc-composable-product.php # Main plugin file
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Main Plugin Class (`Plugin.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
- Singleton pattern implementation
|
||||
- Twig template engine initialization
|
||||
- Hook registration
|
||||
- Component initialization
|
||||
- Asset enqueuing
|
||||
|
||||
**Key Methods:**
|
||||
- `instance()`: Get singleton instance
|
||||
- `init_twig()`: Initialize Twig with WordPress functions
|
||||
- `render_template()`: Render Twig templates
|
||||
- `add_product_type()`: Register composable product type
|
||||
|
||||
### 2. Product Type (`Product_Type.php`)
|
||||
|
||||
**Extends:** `WC_Product`
|
||||
|
||||
**Key Features:**
|
||||
- Custom product type: `composable`
|
||||
- Selection limit management (per-product or global)
|
||||
- Pricing mode (fixed or sum)
|
||||
- Product selection criteria (category/tag/SKU)
|
||||
- Dynamic product availability
|
||||
- Price calculation
|
||||
|
||||
**Key Methods:**
|
||||
- `get_selection_limit()`: Get max selectable items
|
||||
- `get_pricing_mode()`: Get pricing calculation mode
|
||||
- `get_available_products()`: Query available products
|
||||
- `calculate_composed_price()`: Calculate final price
|
||||
|
||||
### 3. Admin Settings (`Admin/Settings.php`)
|
||||
|
||||
**Extends:** `WC_Settings_Page`
|
||||
|
||||
**Global Settings:**
|
||||
- Default selection limit
|
||||
- Default pricing mode
|
||||
- Display options (images, prices, total)
|
||||
|
||||
**Integration:** Adds tab to WooCommerce Settings
|
||||
|
||||
### 4. Product Data Tab (`Admin/Product_Data.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
- Add "Composable Options" tab to product edit page
|
||||
- Render selection criteria fields
|
||||
- Save product meta data
|
||||
- Dynamic field visibility based on criteria type
|
||||
|
||||
**Saved Meta:**
|
||||
- `_composable_selection_limit`: Item limit
|
||||
- `_composable_pricing_mode`: Pricing calculation
|
||||
- `_composable_criteria_type`: Selection method
|
||||
- `_composable_categories`: Selected categories
|
||||
- `_composable_tags`: Selected tags
|
||||
- `_composable_skus`: SKU list
|
||||
|
||||
### 5. Product Selector (`Product_Selector.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
- Render frontend product selection interface
|
||||
- Prepare data for Twig template
|
||||
- Apply display settings
|
||||
|
||||
**Template Variables:**
|
||||
- `products`: Available products array
|
||||
- `selection_limit`: Max selections
|
||||
- `pricing_mode`: Pricing calculation
|
||||
- `show_images/prices/total`: Display flags
|
||||
|
||||
### 6. Cart Handler (`Cart_Handler.php`)
|
||||
|
||||
**Responsibilities:**
|
||||
- Validate product selection
|
||||
- Add selected products to cart data
|
||||
- Calculate dynamic pricing
|
||||
- Display selected products in cart
|
||||
|
||||
**Hooks:**
|
||||
- `woocommerce_add_to_cart_validation`: Validate selections
|
||||
- `woocommerce_add_cart_item_data`: Store selections
|
||||
- `woocommerce_before_calculate_totals`: Update prices
|
||||
- `woocommerce_get_item_data`: Display in cart
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Product Selector Template (`product-selector.twig`)
|
||||
|
||||
**Features:**
|
||||
- Responsive grid layout
|
||||
- Checkbox-based selection
|
||||
- Product images and prices
|
||||
- Real-time total calculation
|
||||
- AJAX add-to-cart
|
||||
|
||||
**Data Attributes:**
|
||||
- `data-product-id`: Composable product ID
|
||||
- `data-selection-limit`: Max selections
|
||||
- `data-pricing-mode`: Pricing mode
|
||||
- `data-price`: Individual product prices
|
||||
|
||||
### JavaScript (`frontend.js`)
|
||||
|
||||
**Functionality:**
|
||||
- Selection limit enforcement
|
||||
- Visual feedback on selection
|
||||
- Real-time price updates (sum mode)
|
||||
- AJAX cart operations
|
||||
- Error/success messages
|
||||
|
||||
**Key Functions:**
|
||||
- `handleCheckboxChange()`: Selection logic
|
||||
- `updateTotalPrice()`: Calculate total
|
||||
- `addToCart()`: AJAX add-to-cart
|
||||
- `showMessage()`: User feedback
|
||||
|
||||
### CSS Styling
|
||||
|
||||
**Approach:**
|
||||
- Grid-based layout (responsive)
|
||||
- Card-style product items
|
||||
- Visual selection states
|
||||
- Mobile-first design
|
||||
- Breakpoints: 768px, 480px
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Admin selects "Composable product" type
|
||||
2. Configure selection limit and pricing mode
|
||||
3. Choose selection criteria (category/tag/SKU)
|
||||
4. Save product metadata
|
||||
5. WooCommerce registers product with custom type
|
||||
|
||||
### Frontend Display
|
||||
|
||||
1. Customer visits product page
|
||||
2. `Cart_Handler` renders `Product_Selector`
|
||||
3. `Product_Type::get_available_products()` queries products
|
||||
4. Twig template renders grid with products
|
||||
5. JavaScript handles interactions
|
||||
|
||||
### Adding to Cart
|
||||
|
||||
1. Customer selects products (JavaScript validation)
|
||||
2. Click "Add to Cart" button
|
||||
3. AJAX request with selected product IDs
|
||||
4. `Cart_Handler::validate_add_to_cart()` validates
|
||||
5. `Cart_Handler::add_cart_item_data()` stores selections
|
||||
6. `Cart_Handler::calculate_cart_item_price()` updates price
|
||||
7. Product added to cart with custom data
|
||||
|
||||
### Cart Display
|
||||
|
||||
1. WooCommerce loads cart
|
||||
2. `Cart_Handler::get_cart_item_from_session()` restores data
|
||||
3. `Cart_Handler::display_cart_item_data()` shows selections
|
||||
4. Price calculated dynamically on each cart load
|
||||
|
||||
## Security Implementation
|
||||
|
||||
### Input Sanitization
|
||||
|
||||
- **Integers:** `absint()` for IDs and limits
|
||||
- **Text:** `sanitize_text_field()` for modes and types
|
||||
- **Textarea:** `sanitize_textarea_field()` for SKUs
|
||||
- **Arrays:** `array_map()` with sanitization functions
|
||||
|
||||
### Output Escaping
|
||||
|
||||
- **HTML:** `esc_html()`, `esc_html_e()`
|
||||
- **Attributes:** `esc_attr()`
|
||||
- **URLs:** `esc_url()`
|
||||
- **JavaScript:** Localized scripts with escaped data
|
||||
|
||||
### Validation
|
||||
|
||||
- Selection limit enforcement
|
||||
- Product availability verification
|
||||
- Cart data validation
|
||||
- Nonce verification (via WooCommerce)
|
||||
|
||||
## Internationalization
|
||||
|
||||
### Text Domain
|
||||
|
||||
`wc-composable-product`
|
||||
|
||||
### Translation Functions
|
||||
|
||||
- `__()`: Return translated string
|
||||
- `_e()`: Echo translated string
|
||||
- `sprintf()` with `__()`: Variable substitution
|
||||
|
||||
### POT File
|
||||
|
||||
Generated template: `languages/wc-composable-product.pot`
|
||||
|
||||
**Supported Locales (per CLAUDE.md):**
|
||||
- en_US (English)
|
||||
- de_DE, de_DE_informal (German - Germany)
|
||||
- de_CH, de_CH_informal (German - Switzerland)
|
||||
- fr_CH (French - Switzerland)
|
||||
- it_CH (Italian - Switzerland)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching
|
||||
|
||||
- Twig templates cached in `cache/` directory
|
||||
- Auto-reload enabled in debug mode
|
||||
- Optimized Composer autoloader
|
||||
|
||||
### Database Queries
|
||||
|
||||
- Efficient `WP_Query` for product selection
|
||||
- Meta queries for SKU filtering
|
||||
- Taxonomy queries for category/tag filtering
|
||||
|
||||
### Asset Loading
|
||||
|
||||
- Scripts only on relevant pages
|
||||
- Minification ready (use build tools)
|
||||
- Conditional enqueuing
|
||||
|
||||
## Extensibility
|
||||
|
||||
### Hooks & Filters
|
||||
|
||||
**Available Filters:**
|
||||
- `wc_composable_settings`: Modify settings array
|
||||
- `woocommerce_product_class`: Custom product class
|
||||
- `product_type_selector`: Product type registration
|
||||
|
||||
**Customization Points:**
|
||||
- Twig templates (override in theme)
|
||||
- CSS styling (enqueue custom styles)
|
||||
- JavaScript behavior (extend object)
|
||||
|
||||
### Developer API
|
||||
|
||||
```php
|
||||
// Get composable product
|
||||
$product = wc_get_product($product_id);
|
||||
|
||||
// Check if composable
|
||||
if ($product->get_type() === 'composable') {
|
||||
// Get available products
|
||||
$products = $product->get_available_products();
|
||||
|
||||
// Get selection limit
|
||||
$limit = $product->get_selection_limit();
|
||||
|
||||
// Calculate price
|
||||
$price = $product->calculate_composed_price($selected_ids);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Admin Testing
|
||||
- [ ] Product type appears in dropdown
|
||||
- [ ] Composable Options tab displays
|
||||
- [ ] Selection criteria toggle works
|
||||
- [ ] Meta data saves correctly
|
||||
- [ ] Settings page accessible
|
||||
- [ ] Global defaults apply
|
||||
|
||||
### Frontend Testing
|
||||
- [ ] Product selector renders
|
||||
- [ ] Selection limit enforced
|
||||
- [ ] Price calculation accurate (both modes)
|
||||
- [ ] AJAX add-to-cart works
|
||||
- [ ] Cart displays selections
|
||||
- [ ] Checkout processes correctly
|
||||
|
||||
### Edge Cases
|
||||
- [ ] Empty criteria (no products)
|
||||
- [ ] Out of stock products excluded
|
||||
- [ ] Invalid product selections rejected
|
||||
- [ ] Multiple cart items unique
|
||||
- [ ] Session persistence
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Variable Products:** Currently supports simple products in selection
|
||||
2. **Grouped Products:** Cannot be used as selectable items
|
||||
3. **Stock Management:** No automatic stock reduction for selected items
|
||||
4. **Caching:** Template cache needs manual clearing after updates
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential features for future versions:
|
||||
|
||||
- Variable product support in selection
|
||||
- Quantity selection per item (not just presence)
|
||||
- Visual bundle previews
|
||||
- Advanced pricing rules
|
||||
- Stock management integration
|
||||
- Product recommendations
|
||||
- Selection templates/presets
|
||||
- Multi-currency support enhancements
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Runtime
|
||||
- PHP 8.3+
|
||||
- WordPress 6.0+
|
||||
- WooCommerce 8.0+
|
||||
- Twig 3.0 (via Composer)
|
||||
|
||||
### Development
|
||||
- Composer for dependency management
|
||||
- WP-CLI for i18n operations (optional)
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production Checklist
|
||||
|
||||
1. Run `composer install --no-dev --optimize-autoloader`
|
||||
2. Ensure `vendor/` directory is included
|
||||
3. Ensure `cache/` directory is writable
|
||||
4. Test on staging environment
|
||||
5. Clear all caches after activation
|
||||
6. Verify WooCommerce compatibility
|
||||
|
||||
### Release Package
|
||||
|
||||
Must include:
|
||||
- All PHP files
|
||||
- `vendor/` directory
|
||||
- Assets (CSS, JS)
|
||||
- Templates
|
||||
- Language files
|
||||
- Documentation
|
||||
|
||||
Must exclude:
|
||||
- `.git/` directory
|
||||
- `composer.lock`
|
||||
- Development files
|
||||
- `wp-core/`, `wp-plugins/` symlinks
|
||||
|
||||
## Support & Maintenance
|
||||
|
||||
### Code Standards
|
||||
- WordPress Coding Standards
|
||||
- WooCommerce best practices
|
||||
- PSR-4 autoloading
|
||||
- Inline documentation
|
||||
|
||||
### Version Control
|
||||
- Semantic versioning (MAJOR.MINOR.PATCH)
|
||||
- Changelog maintained
|
||||
- Annotated git tags
|
||||
- Development on `dev` branch
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2024-12-31
|
||||
**Maintainer:** Marco Graetsch
|
||||
**AI Assistant:** Claude.AI (Anthropic)
|
||||
150
INSTALL.md
Normal file
150
INSTALL.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Installation Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
Before installing the WooCommerce Composable Products plugin, ensure your system meets these requirements:
|
||||
|
||||
- **PHP**: 8.3 or higher
|
||||
- **WordPress**: 6.0 or higher
|
||||
- **WooCommerce**: 8.0 or higher
|
||||
- **Composer**: For dependency management
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Upload Plugin Files
|
||||
|
||||
Upload the plugin directory to your WordPress installation:
|
||||
|
||||
```bash
|
||||
/wp-content/plugins/wc-composable-product/
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
Navigate to the plugin directory and install dependencies:
|
||||
|
||||
```bash
|
||||
cd /wp-content/plugins/wc-composable-product/
|
||||
composer install --no-dev --optimize-autoloader
|
||||
```
|
||||
|
||||
### 3. Activate Plugin
|
||||
|
||||
1. Log in to your WordPress admin panel
|
||||
2. Navigate to **Plugins > Installed Plugins**
|
||||
3. Find "WooCommerce Composable Products"
|
||||
4. Click **Activate**
|
||||
|
||||
### 4. Configure Settings
|
||||
|
||||
After activation, configure the plugin:
|
||||
|
||||
1. Navigate to **WooCommerce > Settings**
|
||||
2. Click on the **Composable Products** tab
|
||||
3. Configure default settings:
|
||||
- **Default Selection Limit**: Number of items customers can select (default: 5)
|
||||
- **Default Pricing Mode**: Choose between "Sum of selected products" or "Fixed price"
|
||||
- **Display Options**: Toggle product images, prices, and totals
|
||||
|
||||
## Creating Your First Composable Product
|
||||
|
||||
### Step 1: Create a New Product
|
||||
|
||||
1. Go to **Products > Add New**
|
||||
2. Enter a product name (e.g., "Custom Sticker Pack")
|
||||
|
||||
### Step 2: Set Product Type
|
||||
|
||||
1. In the **Product Data** panel, select **Composable product** from the dropdown
|
||||
|
||||
### Step 3: Configure General Settings
|
||||
|
||||
In the **General** tab:
|
||||
- Set a **Regular price** (used if pricing mode is "Fixed")
|
||||
- Configure **Selection Limit** (leave empty to use global default)
|
||||
- Choose **Pricing Mode** (leave empty to use global default)
|
||||
|
||||
### Step 4: Configure Composable Options
|
||||
|
||||
Click on the **Composable Options** tab:
|
||||
|
||||
1. **Selection Criteria**: Choose how to select available products
|
||||
- **By Category**: Select product categories
|
||||
- **By Tag**: Select product tags
|
||||
- **By SKU**: Enter comma-separated SKUs
|
||||
|
||||
2. Based on your selection:
|
||||
- **Categories**: Select one or more categories from the dropdown
|
||||
- **Tags**: Select one or more tags from the dropdown
|
||||
- **SKUs**: Enter SKUs like: `STICKER-01, STICKER-02, STICKER-03`
|
||||
|
||||
### Step 5: Publish
|
||||
|
||||
Click **Publish** to make your composable product live.
|
||||
|
||||
## Frontend Usage
|
||||
|
||||
When customers visit your composable product:
|
||||
|
||||
1. They see a grid of available products based on your criteria
|
||||
2. They can select up to the configured limit
|
||||
3. The total price updates in real-time (if using sum pricing mode)
|
||||
4. Click "Add to Cart" to add the composition to their cart
|
||||
5. Selected products are displayed in the cart
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Won't Activate
|
||||
|
||||
- Ensure WooCommerce is installed and activated first
|
||||
- Check PHP version (must be 8.3+)
|
||||
- Verify Composer dependencies are installed
|
||||
|
||||
### Products Not Showing in Selector
|
||||
|
||||
- Check that products are published and in stock
|
||||
- Verify the selection criteria (category/tag/SKU) is correct
|
||||
- Ensure products match the criteria you configured
|
||||
|
||||
### Twig Template Errors
|
||||
|
||||
- Ensure the `vendor/` directory exists and contains Twig
|
||||
- Run `composer install` again
|
||||
- Check that the `cache/` directory is writable
|
||||
|
||||
### JavaScript Not Working
|
||||
|
||||
- Clear browser cache
|
||||
- Check browser console for errors
|
||||
- Ensure jQuery is loaded (WooCommerce includes it)
|
||||
|
||||
## Updating
|
||||
|
||||
When updating the plugin:
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Replace plugin files
|
||||
3. Run `composer install --no-dev --optimize-autoloader`
|
||||
4. Reactivate the plugin
|
||||
5. Clear all caches (WordPress, browser, CDN)
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To completely remove the plugin:
|
||||
|
||||
1. Deactivate the plugin
|
||||
2. Delete the plugin from the Plugins page
|
||||
3. Optionally clean up database entries (WooCommerce will handle this automatically)
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests:
|
||||
- GitHub: https://github.com/magdev/wc-composable-product/issues
|
||||
- Documentation: See README.md
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Customize the template by editing `templates/product-selector.twig`
|
||||
- Modify styles in `assets/css/frontend.css`
|
||||
- Translate the plugin using the provided `.pot` file
|
||||
- Create categories/tags for easier product organization
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2024 Marco Graetsch
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
|
||||
For the full text of the GPL v3 license, visit:
|
||||
https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
77
README.md
Normal file
77
README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# WooCommerce Composable Products
|
||||
|
||||
Create composable products where customers can select a limited number of items from a configurable set of products.
|
||||
|
||||
## Description
|
||||
|
||||
This plugin adds a new product type to WooCommerce that allows customers to build their own product bundles by selecting from a predefined set of simple or variable products. Think of it as a "build your own gift box" or "create your sticker pack" feature.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Custom Product Type**: New "Composable Product" type in WooCommerce
|
||||
- **Flexible Selection**: Define available products by category, tag, or SKU
|
||||
- **Configurable Limits**: Set global or per-product selection limits
|
||||
- **Pricing Options**: Fixed price or sum of selected products
|
||||
- **Multi-language Support**: Fully translatable with i18n support
|
||||
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.3 or higher
|
||||
- WordPress 6.0 or higher
|
||||
- WooCommerce 8.0 or higher
|
||||
|
||||
## Installation
|
||||
|
||||
1. Upload the plugin files to `/wp-content/plugins/wc-composable-product/`
|
||||
2. Run `composer install --no-dev` in the plugin directory
|
||||
3. Activate the plugin through the 'Plugins' menu in WordPress
|
||||
4. Configure global settings under WooCommerce > Settings > Composable Products
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Composable Product
|
||||
|
||||
1. Go to Products > Add New
|
||||
2. Select "Composable Product" as the product type
|
||||
3. Configure product details:
|
||||
- Set the selection limit (or use global default)
|
||||
- Choose pricing mode (fixed or sum)
|
||||
- Define available products by category, tag, or SKU
|
||||
4. Publish the product
|
||||
|
||||
### Global Settings
|
||||
|
||||
Navigate to WooCommerce > Settings > Composable Products to configure:
|
||||
- Default selection limit
|
||||
- Default pricing mode
|
||||
- Display options
|
||||
|
||||
## Development
|
||||
|
||||
This project was created with AI assistance (Claude.AI) and follows WordPress and WooCommerce best practices.
|
||||
|
||||
### Building from Source
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
### Translation
|
||||
|
||||
Generate POT file:
|
||||
```bash
|
||||
wp i18n make-pot . languages/wc-composable-product.pot
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
GPL v3 or later - see LICENSE file for details
|
||||
|
||||
## Author
|
||||
|
||||
Marco Graetsch
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please use the GitHub issue tracker.
|
||||
43
assets/css/admin.css
Normal file
43
assets/css/admin.css
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Admin Styles for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
#composable_product_data {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.composable_criteria_group {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
#_composable_categories,
|
||||
#_composable_tags {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.show_if_composable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-type-composable .show_if_composable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.composable_options.composable_tab a::before {
|
||||
content: '\f323';
|
||||
font-family: 'Dashicons';
|
||||
}
|
||||
|
||||
/* Enhanced select styling */
|
||||
.wc-composable-product-admin .select2-container {
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
/* Help tips */
|
||||
.woocommerce-help-tip {
|
||||
color: #666;
|
||||
}
|
||||
201
assets/css/frontend.css
Normal file
201
assets/css/frontend.css
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Frontend Styles for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
.wc-composable-product-selector {
|
||||
margin: 2rem 0;
|
||||
padding: 2rem;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.composable-header {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.composable-header h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.selection-info {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.composable-products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.composable-product-item {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.composable-product-item:hover {
|
||||
border-color: #999;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.composable-product-item.selected {
|
||||
border-color: #2c3e50;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
.product-item-label {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.composable-product-checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.product-item-image {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-item-image img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.product-item-details {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-item-name {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-item-price {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.product-item-checkmark {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #fff;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.composable-product-item.selected .product-item-checkmark::after {
|
||||
content: '✓';
|
||||
color: #2c3e50;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.composable-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.composable-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.composable-add-to-cart {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.composable-add-to-cart.loading {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.composable-messages {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.composable-message {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.composable-message-error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
border: 1px solid #fcc;
|
||||
}
|
||||
|
||||
.composable-message-success {
|
||||
background: #efe;
|
||||
color: #060;
|
||||
border: 1px solid #cfc;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.composable-products-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.wc-composable-product-selector {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.composable-total {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.composable-products-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
50
assets/js/admin.js
Normal file
50
assets/js/admin.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Admin JavaScript for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
/**
|
||||
* Show/hide product data tab for composable products
|
||||
*/
|
||||
$('select#product-type').on('change', function() {
|
||||
const productType = $(this).val();
|
||||
|
||||
if (productType === 'composable') {
|
||||
$('.show_if_composable').show();
|
||||
$('.hide_if_composable').hide();
|
||||
$('#composable_product_data').show();
|
||||
$('.product_data_tabs .composable_options a').show();
|
||||
} else {
|
||||
$('.show_if_composable').hide();
|
||||
$('#composable_product_data').hide();
|
||||
$('.product_data_tabs .composable_options a').hide();
|
||||
}
|
||||
}).trigger('change');
|
||||
|
||||
/**
|
||||
* Toggle criteria groups based on selected type
|
||||
*/
|
||||
$('#_composable_criteria_type').on('change', function() {
|
||||
const criteriaType = $(this).val();
|
||||
|
||||
$('.composable_criteria_group').hide();
|
||||
$('#composable_criteria_' + criteriaType).show();
|
||||
}).trigger('change');
|
||||
|
||||
/**
|
||||
* Initialize enhanced select for categories and tags
|
||||
*/
|
||||
if ($.fn.selectWoo) {
|
||||
$('#_composable_categories, #_composable_tags').selectWoo({
|
||||
placeholder: 'Select options...',
|
||||
allowClear: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
186
assets/js/frontend.js
Normal file
186
assets/js/frontend.js
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Frontend JavaScript for Composable Products
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
const ComposableProduct = {
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind events
|
||||
*/
|
||||
bindEvents: function() {
|
||||
const self = this;
|
||||
|
||||
// Handle checkbox changes
|
||||
$(document).on('change', '.composable-product-checkbox', function() {
|
||||
self.handleCheckboxChange($(this));
|
||||
});
|
||||
|
||||
// Handle add to cart button
|
||||
$(document).on('click', '.composable-add-to-cart', function(e) {
|
||||
e.preventDefault();
|
||||
self.addToCart($(this));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle checkbox change
|
||||
*
|
||||
* @param {jQuery} $checkbox Checkbox element
|
||||
*/
|
||||
handleCheckboxChange: function($checkbox) {
|
||||
const $container = $checkbox.closest('.wc-composable-product-selector');
|
||||
const selectionLimit = parseInt($container.data('selection-limit'));
|
||||
const pricingMode = $container.data('pricing-mode');
|
||||
const $checked = $container.find('.composable-product-checkbox:checked');
|
||||
|
||||
// Enforce selection limit
|
||||
if ($checked.length > selectionLimit) {
|
||||
$checkbox.prop('checked', false);
|
||||
this.showMessage($container, wcComposableProduct.i18n.max_items, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update visual state
|
||||
$checkbox.closest('.composable-product-item').toggleClass('selected', $checkbox.is(':checked'));
|
||||
|
||||
// Update total price if in sum mode
|
||||
if (pricingMode === 'sum') {
|
||||
this.updateTotalPrice($container);
|
||||
}
|
||||
|
||||
// Clear messages
|
||||
this.clearMessages($container);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update total price
|
||||
*
|
||||
* @param {jQuery} $container Container element
|
||||
*/
|
||||
updateTotalPrice: function($container) {
|
||||
const $checked = $container.find('.composable-product-checkbox:checked');
|
||||
let total = 0;
|
||||
|
||||
$checked.each(function() {
|
||||
const price = parseFloat($(this).data('price'));
|
||||
if (!isNaN(price)) {
|
||||
total += price;
|
||||
}
|
||||
});
|
||||
|
||||
const currencySymbol = $container.find('.total-price').data('currency');
|
||||
$container.find('.calculated-total').text(currencySymbol + total.toFixed(2));
|
||||
},
|
||||
|
||||
/**
|
||||
* Add to cart
|
||||
*
|
||||
* @param {jQuery} $button Button element
|
||||
*/
|
||||
addToCart: function($button) {
|
||||
const $container = $button.closest('.wc-composable-product-selector');
|
||||
const $checked = $container.find('.composable-product-checkbox:checked');
|
||||
const productId = $button.data('product-id');
|
||||
|
||||
// Validate selection
|
||||
if ($checked.length === 0) {
|
||||
this.showMessage($container, wcComposableProduct.i18n.min_items, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect selected product IDs
|
||||
const selectedProducts = [];
|
||||
$checked.each(function() {
|
||||
selectedProducts.push($(this).val());
|
||||
});
|
||||
|
||||
// Disable button
|
||||
$button.prop('disabled', true).addClass('loading');
|
||||
|
||||
// Add to cart via AJAX
|
||||
$.ajax({
|
||||
url: wc_add_to_cart_params.wc_ajax_url.toString().replace('%%endpoint%%', 'add_to_cart'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
product_id: productId,
|
||||
quantity: 1,
|
||||
composable_products: selectedProducts
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.error) {
|
||||
this.showMessage($container, response.error, 'error');
|
||||
} else {
|
||||
// Trigger WooCommerce event
|
||||
$(document.body).trigger('added_to_cart', [response.fragments, response.cart_hash, $button]);
|
||||
|
||||
// Show success message
|
||||
this.showMessage($container, 'Product added to cart!', 'success');
|
||||
|
||||
// Reset selection
|
||||
setTimeout(function() {
|
||||
$checked.prop('checked', false);
|
||||
$container.find('.composable-product-item').removeClass('selected');
|
||||
this.updateTotalPrice($container);
|
||||
}.bind(this), 1500);
|
||||
}
|
||||
}.bind(this),
|
||||
error: function() {
|
||||
this.showMessage($container, 'An error occurred. Please try again.', 'error');
|
||||
}.bind(this),
|
||||
complete: function() {
|
||||
$button.prop('disabled', false).removeClass('loading');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show message
|
||||
*
|
||||
* @param {jQuery} $container Container element
|
||||
* @param {string} message Message text
|
||||
* @param {string} type Message type (error, success)
|
||||
*/
|
||||
showMessage: function($container, message, type) {
|
||||
const $messages = $container.find('.composable-messages');
|
||||
const $message = $('<div>')
|
||||
.addClass('composable-message')
|
||||
.addClass('composable-message-' + type)
|
||||
.text(message);
|
||||
|
||||
$messages.empty().append($message);
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(function() {
|
||||
$message.fadeOut(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear messages
|
||||
*
|
||||
* @param {jQuery} $container Container element
|
||||
*/
|
||||
clearMessages: function($container) {
|
||||
$container.find('.composable-messages').empty();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready
|
||||
$(document).ready(function() {
|
||||
ComposableProduct.init();
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
25
composer.json
Normal file
25
composer.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "magdev/wc-composable-product",
|
||||
"description": "WooCommerce plugin for user composable products",
|
||||
"type": "wordpress-plugin",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marco Graetsch",
|
||||
"email": "marco@example.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WC_Composable_Product\\": "includes/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
190
includes/Admin/Product_Data.php
Normal file
190
includes/Admin/Product_Data.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Data Tab
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Product Data Tab Class
|
||||
*/
|
||||
class Product_Data {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter('woocommerce_product_data_tabs', [$this, 'add_product_data_tab']);
|
||||
add_action('woocommerce_product_data_panels', [$this, 'add_product_data_panel']);
|
||||
add_action('woocommerce_process_product_meta_composable', [$this, 'save_product_data']);
|
||||
add_action('woocommerce_product_options_general_product_data', [$this, 'add_general_fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add composable products tab
|
||||
*
|
||||
* @param array $tabs Product data tabs
|
||||
* @return array
|
||||
*/
|
||||
public function add_product_data_tab($tabs) {
|
||||
$tabs['composable'] = [
|
||||
'label' => __('Composable Options', 'wc-composable-product'),
|
||||
'target' => 'composable_product_data',
|
||||
'class' => ['show_if_composable'],
|
||||
'priority' => 21,
|
||||
];
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add fields to general tab
|
||||
*/
|
||||
public function add_general_fields() {
|
||||
global $product_object;
|
||||
|
||||
if ($product_object && $product_object->get_type() === 'composable') {
|
||||
echo '<div class="options_group show_if_composable">';
|
||||
|
||||
woocommerce_wp_text_input([
|
||||
'id' => '_composable_selection_limit',
|
||||
'label' => __('Selection Limit', 'wc-composable-product'),
|
||||
'description' => __('Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'type' => 'number',
|
||||
'custom_attributes' => [
|
||||
'min' => '1',
|
||||
'step' => '1',
|
||||
],
|
||||
]);
|
||||
|
||||
woocommerce_wp_select([
|
||||
'id' => '_composable_pricing_mode',
|
||||
'label' => __('Pricing Mode', 'wc-composable-product'),
|
||||
'description' => __('How to calculate the price. Leave empty to use global default.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'options' => [
|
||||
'' => __('Use global default', 'wc-composable-product'),
|
||||
'sum' => __('Sum of selected products', 'wc-composable-product'),
|
||||
'fixed' => __('Fixed price', 'wc-composable-product'),
|
||||
],
|
||||
]);
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data panel
|
||||
*/
|
||||
public function add_product_data_panel() {
|
||||
global $post;
|
||||
?>
|
||||
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_select([
|
||||
'id' => '_composable_criteria_type',
|
||||
'label' => __('Selection Criteria', 'wc-composable-product'),
|
||||
'description' => __('How to select available products.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'options' => [
|
||||
'category' => __('By Category', 'wc-composable-product'),
|
||||
'tag' => __('By Tag', 'wc-composable-product'),
|
||||
'sku' => __('By SKU', 'wc-composable-product'),
|
||||
],
|
||||
'value' => get_post_meta($post->ID, '_composable_criteria_type', true) ?: 'category',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_category">
|
||||
<p class="form-field">
|
||||
<label for="_composable_categories"><?php esc_html_e('Select Categories', 'wc-composable-product'); ?></label>
|
||||
<select id="_composable_categories" name="_composable_categories[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||
<?php
|
||||
$selected_categories = get_post_meta($post->ID, '_composable_categories', true) ?: [];
|
||||
$categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]);
|
||||
foreach ($categories as $category) {
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr($category->term_id),
|
||||
selected(in_array($category->term_id, (array) $selected_categories), true, false),
|
||||
esc_html($category->name)
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<span class="description"><?php esc_html_e('Select product categories to include.', 'wc-composable-product'); ?></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_tag" style="display: none;">
|
||||
<p class="form-field">
|
||||
<label for="_composable_tags"><?php esc_html_e('Select Tags', 'wc-composable-product'); ?></label>
|
||||
<select id="_composable_tags" name="_composable_tags[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||
<?php
|
||||
$selected_tags = get_post_meta($post->ID, '_composable_tags', true) ?: [];
|
||||
$tags = get_terms(['taxonomy' => 'product_tag', 'hide_empty' => false]);
|
||||
foreach ($tags as $tag) {
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr($tag->term_id),
|
||||
selected(in_array($tag->term_id, (array) $selected_tags), true, false),
|
||||
esc_html($tag->name)
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<span class="description"><?php esc_html_e('Select product tags to include.', 'wc-composable-product'); ?></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group composable_criteria_group" id="composable_criteria_sku" style="display: none;">
|
||||
<?php
|
||||
woocommerce_wp_textarea_input([
|
||||
'id' => '_composable_skus',
|
||||
'label' => __('Product SKUs', 'wc-composable-product'),
|
||||
'description' => __('Enter product SKUs separated by commas.', 'wc-composable-product'),
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __('SKU-1, SKU-2, SKU-3', 'wc-composable-product'),
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product data
|
||||
*
|
||||
* @param int $post_id Post ID
|
||||
*/
|
||||
public function save_product_data($post_id) {
|
||||
// Save selection limit
|
||||
$selection_limit = isset($_POST['_composable_selection_limit']) ? absint($_POST['_composable_selection_limit']) : '';
|
||||
update_post_meta($post_id, '_composable_selection_limit', $selection_limit);
|
||||
|
||||
// Save pricing mode
|
||||
$pricing_mode = isset($_POST['_composable_pricing_mode']) ? sanitize_text_field($_POST['_composable_pricing_mode']) : '';
|
||||
update_post_meta($post_id, '_composable_pricing_mode', $pricing_mode);
|
||||
|
||||
// Save criteria type
|
||||
$criteria_type = isset($_POST['_composable_criteria_type']) ? sanitize_text_field($_POST['_composable_criteria_type']) : 'category';
|
||||
update_post_meta($post_id, '_composable_criteria_type', $criteria_type);
|
||||
|
||||
// Save categories
|
||||
$categories = isset($_POST['_composable_categories']) ? array_map('absint', $_POST['_composable_categories']) : [];
|
||||
update_post_meta($post_id, '_composable_categories', $categories);
|
||||
|
||||
// Save tags
|
||||
$tags = isset($_POST['_composable_tags']) ? array_map('absint', $_POST['_composable_tags']) : [];
|
||||
update_post_meta($post_id, '_composable_tags', $tags);
|
||||
|
||||
// Save SKUs
|
||||
$skus = isset($_POST['_composable_skus']) ? sanitize_textarea_field($_POST['_composable_skus']) : '';
|
||||
update_post_meta($post_id, '_composable_skus', $skus);
|
||||
}
|
||||
}
|
||||
108
includes/Admin/Settings.php
Normal file
108
includes/Admin/Settings.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Settings
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product\Admin;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Settings class
|
||||
*/
|
||||
class Settings extends \WC_Settings_Page {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->id = 'composable_products';
|
||||
$this->label = __('Composable Products', 'wc-composable-product');
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_settings() {
|
||||
$settings = [
|
||||
[
|
||||
'title' => __('Composable Products Settings', 'wc-composable-product'),
|
||||
'type' => 'title',
|
||||
'desc' => __('Configure default settings for composable products.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_settings',
|
||||
],
|
||||
[
|
||||
'title' => __('Default Selection Limit', 'wc-composable-product'),
|
||||
'desc' => __('Default number of items customers can select.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_default_limit',
|
||||
'type' => 'number',
|
||||
'default' => '5',
|
||||
'custom_attributes' => [
|
||||
'min' => '1',
|
||||
'step' => '1',
|
||||
],
|
||||
'desc_tip' => true,
|
||||
],
|
||||
[
|
||||
'title' => __('Default Pricing Mode', 'wc-composable-product'),
|
||||
'desc' => __('How to calculate the price of composable products.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_default_pricing',
|
||||
'type' => 'select',
|
||||
'default' => 'sum',
|
||||
'options' => [
|
||||
'sum' => __('Sum of selected products', 'wc-composable-product'),
|
||||
'fixed' => __('Fixed price', 'wc-composable-product'),
|
||||
],
|
||||
'desc_tip' => true,
|
||||
],
|
||||
[
|
||||
'title' => __('Show Product Images', 'wc-composable-product'),
|
||||
'desc' => __('Display product images in the selection interface.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_show_images',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
],
|
||||
[
|
||||
'title' => __('Show Product Prices', 'wc-composable-product'),
|
||||
'desc' => __('Display individual product prices in the selection interface.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_show_prices',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
],
|
||||
[
|
||||
'title' => __('Show Total Price', 'wc-composable-product'),
|
||||
'desc' => __('Display the total price as customers make selections.', 'wc-composable-product'),
|
||||
'id' => 'wc_composable_show_total',
|
||||
'type' => 'checkbox',
|
||||
'default' => 'yes',
|
||||
],
|
||||
[
|
||||
'type' => 'sectionend',
|
||||
'id' => 'wc_composable_settings',
|
||||
],
|
||||
];
|
||||
|
||||
return apply_filters('wc_composable_settings', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the settings
|
||||
*/
|
||||
public function output() {
|
||||
$settings = $this->get_settings();
|
||||
\WC_Admin_Settings::output_fields($settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings
|
||||
*/
|
||||
public function save() {
|
||||
$settings = $this->get_settings();
|
||||
\WC_Admin_Settings::save_fields($settings);
|
||||
}
|
||||
}
|
||||
181
includes/Cart_Handler.php
Normal file
181
includes/Cart_Handler.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
/**
|
||||
* Cart Handler
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Cart Handler Class
|
||||
*
|
||||
* Handles adding composable products to cart and calculating prices
|
||||
*/
|
||||
class Cart_Handler {
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3);
|
||||
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2);
|
||||
add_filter('woocommerce_get_cart_item_from_session', [$this, 'get_cart_item_from_session'], 10, 2);
|
||||
add_filter('woocommerce_get_item_data', [$this, 'display_cart_item_data'], 10, 2);
|
||||
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
||||
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render product selector on product page
|
||||
*/
|
||||
public function render_product_selector() {
|
||||
global $product;
|
||||
|
||||
if ($product && $product->get_type() === 'composable') {
|
||||
Product_Selector::render($product);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate add to cart
|
||||
*
|
||||
* @param bool $passed Validation status
|
||||
* @param int $product_id Product ID
|
||||
* @param int $quantity Quantity
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_add_to_cart($passed, $product_id, $quantity) {
|
||||
$product = wc_get_product($product_id);
|
||||
|
||||
if (!$product || $product->get_type() !== 'composable') {
|
||||
return $passed;
|
||||
}
|
||||
|
||||
// Check if selected products are provided
|
||||
if (!isset($_POST['composable_products']) || empty($_POST['composable_products'])) {
|
||||
wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
$selected_products = array_map('absint', $_POST['composable_products']);
|
||||
$selection_limit = $product->get_selection_limit();
|
||||
|
||||
// Validate selection limit
|
||||
if (count($selected_products) > $selection_limit) {
|
||||
/* translators: %d: selection limit */
|
||||
wc_add_notice(sprintf(__('You can select a maximum of %d products.', 'wc-composable-product'), $selection_limit), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($selected_products) === 0) {
|
||||
wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate that selected products are valid
|
||||
$available_products = $product->get_available_products();
|
||||
$available_ids = array_map(function($p) {
|
||||
return $p->get_id();
|
||||
}, $available_products);
|
||||
|
||||
foreach ($selected_products as $selected_id) {
|
||||
if (!in_array($selected_id, $available_ids)) {
|
||||
wc_add_notice(__('One or more selected products are not available.', 'wc-composable-product'), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $passed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add cart item data
|
||||
*
|
||||
* @param array $cart_item_data Cart item data
|
||||
* @param int $product_id Product ID
|
||||
* @return array
|
||||
*/
|
||||
public function add_cart_item_data($cart_item_data, $product_id) {
|
||||
$product = wc_get_product($product_id);
|
||||
|
||||
if (!$product || $product->get_type() !== 'composable') {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
if (isset($_POST['composable_products']) && !empty($_POST['composable_products'])) {
|
||||
$selected_products = array_map('absint', $_POST['composable_products']);
|
||||
$cart_item_data['composable_products'] = $selected_products;
|
||||
|
||||
// Make cart item unique
|
||||
$cart_item_data['unique_key'] = md5(json_encode($selected_products) . time());
|
||||
}
|
||||
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cart item from session
|
||||
*
|
||||
* @param array $cart_item Cart item
|
||||
* @param array $values Values from session
|
||||
* @return array
|
||||
*/
|
||||
public function get_cart_item_from_session($cart_item, $values) {
|
||||
if (isset($values['composable_products'])) {
|
||||
$cart_item['composable_products'] = $values['composable_products'];
|
||||
}
|
||||
|
||||
return $cart_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display cart item data
|
||||
*
|
||||
* @param array $item_data Item data
|
||||
* @param array $cart_item Cart item
|
||||
* @return array
|
||||
*/
|
||||
public function display_cart_item_data($item_data, $cart_item) {
|
||||
if (isset($cart_item['composable_products']) && !empty($cart_item['composable_products'])) {
|
||||
$product_names = [];
|
||||
foreach ($cart_item['composable_products'] as $product_id) {
|
||||
$product = wc_get_product($product_id);
|
||||
if ($product) {
|
||||
$product_names[] = $product->get_name();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($product_names)) {
|
||||
$item_data[] = [
|
||||
'key' => __('Selected Products', 'wc-composable-product'),
|
||||
'value' => implode(', ', $product_names),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $item_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cart item price
|
||||
*
|
||||
* @param \WC_Cart $cart Cart object
|
||||
*/
|
||||
public function calculate_cart_item_price($cart) {
|
||||
if (is_admin() && !defined('DOING_AJAX')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
||||
if (isset($cart_item['composable_products'])) {
|
||||
$product = $cart_item['data'];
|
||||
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
||||
$cart_item['data']->set_price($price);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
216
includes/Plugin.php
Normal file
216
includes/Plugin.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* Main Plugin Class
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Main plugin class - Singleton pattern
|
||||
*/
|
||||
class Plugin {
|
||||
/**
|
||||
* The single instance of the class
|
||||
*
|
||||
* @var Plugin
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Twig environment
|
||||
*
|
||||
* @var \Twig\Environment
|
||||
*/
|
||||
private $twig = null;
|
||||
|
||||
/**
|
||||
* Main Plugin Instance
|
||||
*
|
||||
* Ensures only one instance is loaded or can be loaded.
|
||||
*
|
||||
* @return Plugin
|
||||
*/
|
||||
public static function instance() {
|
||||
if (is_null(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
$this->init_twig();
|
||||
$this->includes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WordPress and WooCommerce
|
||||
*/
|
||||
private function init_hooks() {
|
||||
// Register product type
|
||||
add_filter('product_type_selector', [$this, 'add_product_type']);
|
||||
add_filter('woocommerce_product_class', [$this, 'product_class'], 10, 2);
|
||||
|
||||
// Enqueue scripts and styles
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_scripts']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
|
||||
|
||||
// Admin settings
|
||||
add_filter('woocommerce_get_settings_pages', [$this, 'add_settings_page']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Twig template engine
|
||||
*/
|
||||
private function init_twig() {
|
||||
$loader = new \Twig\Loader\FilesystemLoader(WC_COMPOSABLE_PRODUCT_PATH . 'templates');
|
||||
$this->twig = new \Twig\Environment($loader, [
|
||||
'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache',
|
||||
'auto_reload' => true,
|
||||
'debug' => defined('WP_DEBUG') && WP_DEBUG,
|
||||
]);
|
||||
|
||||
// Add WordPress functions to Twig
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('__', function($text) {
|
||||
return __($text, 'wc-composable-product');
|
||||
}));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_html', 'esc_html'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_attr', 'esc_attr'));
|
||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_url', 'esc_url'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Include required files
|
||||
*/
|
||||
private function includes() {
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Product_Data.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Type.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Cart_Handler.php';
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Selector.php';
|
||||
|
||||
// Initialize components
|
||||
new Admin\Product_Data();
|
||||
new Cart_Handler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add composable product type to selector
|
||||
*
|
||||
* @param array $types Product types
|
||||
* @return array
|
||||
*/
|
||||
public function add_product_type($types) {
|
||||
$types['composable'] = __('Composable product', 'wc-composable-product');
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use custom product class for composable products
|
||||
*
|
||||
* @param string $classname Product class name
|
||||
* @param string $product_type Product type
|
||||
* @return string
|
||||
*/
|
||||
public function product_class($classname, $product_type) {
|
||||
if ($product_type === 'composable') {
|
||||
$classname = 'WC_Composable_Product\Product_Type';
|
||||
}
|
||||
return $classname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend scripts and styles
|
||||
*/
|
||||
public function enqueue_frontend_scripts() {
|
||||
if (is_product()) {
|
||||
wp_enqueue_style(
|
||||
'wc-composable-product',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css',
|
||||
[],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wc-composable-product',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js',
|
||||
['jquery'],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script('wc-composable-product', 'wcComposableProduct', [
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wc_composable_product_nonce'),
|
||||
'i18n' => [
|
||||
'select_items' => __('Please select items', 'wc-composable-product'),
|
||||
'max_items' => __('Maximum items selected', 'wc-composable-product'),
|
||||
'min_items' => __('Please select at least one item', 'wc-composable-product'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts and styles
|
||||
*/
|
||||
public function enqueue_admin_scripts($hook) {
|
||||
if ('post.php' === $hook || 'post-new.php' === $hook) {
|
||||
global $post_type;
|
||||
if ('product' === $post_type) {
|
||||
wp_enqueue_style(
|
||||
'wc-composable-product-admin',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css',
|
||||
[],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wc-composable-product-admin',
|
||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js',
|
||||
['jquery', 'wc-admin-product-meta-boxes'],
|
||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings page to WooCommerce
|
||||
*
|
||||
* @param array $settings WooCommerce settings pages
|
||||
* @return array
|
||||
*/
|
||||
public function add_settings_page($settings) {
|
||||
$settings[] = new Admin\Settings();
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twig environment
|
||||
*
|
||||
* @return \Twig\Environment
|
||||
*/
|
||||
public function get_twig() {
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Twig template
|
||||
*
|
||||
* @param string $template Template name
|
||||
* @param array $context Template variables
|
||||
* @return string
|
||||
*/
|
||||
public function render_template($template, $context = []) {
|
||||
return $this->twig->render($template, $context);
|
||||
}
|
||||
}
|
||||
65
includes/Product_Selector.php
Normal file
65
includes/Product_Selector.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* Product Selector
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Product Selector Class
|
||||
*
|
||||
* Handles rendering the product selection interface
|
||||
*/
|
||||
class Product_Selector {
|
||||
/**
|
||||
* Render product selector
|
||||
*
|
||||
* @param Product_Type $product Composable product
|
||||
*/
|
||||
public static function render($product) {
|
||||
if (!$product || $product->get_type() !== 'composable') {
|
||||
return;
|
||||
}
|
||||
|
||||
$available_products = $product->get_available_products();
|
||||
$selection_limit = $product->get_selection_limit();
|
||||
$pricing_mode = $product->get_pricing_mode();
|
||||
|
||||
$show_images = get_option('wc_composable_show_images', 'yes') === 'yes';
|
||||
$show_prices = get_option('wc_composable_show_prices', 'yes') === 'yes';
|
||||
$show_total = get_option('wc_composable_show_total', 'yes') === 'yes';
|
||||
|
||||
// Prepare product data for template
|
||||
$products_data = [];
|
||||
foreach ($available_products as $available_product) {
|
||||
$products_data[] = [
|
||||
'id' => $available_product->get_id(),
|
||||
'name' => $available_product->get_name(),
|
||||
'price' => $available_product->get_price(),
|
||||
'price_html' => $available_product->get_price_html(),
|
||||
'image_url' => wp_get_attachment_image_url($available_product->get_image_id(), 'thumbnail'),
|
||||
'permalink' => $available_product->get_permalink(),
|
||||
];
|
||||
}
|
||||
|
||||
$context = [
|
||||
'product_id' => $product->get_id(),
|
||||
'products' => $products_data,
|
||||
'selection_limit' => $selection_limit,
|
||||
'pricing_mode' => $pricing_mode,
|
||||
'show_images' => $show_images,
|
||||
'show_prices' => $show_prices,
|
||||
'show_total' => $show_total,
|
||||
'fixed_price' => $product->get_price(),
|
||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||
];
|
||||
|
||||
// Render template
|
||||
$plugin = Plugin::instance();
|
||||
echo $plugin->render_template('product-selector.twig', $context);
|
||||
}
|
||||
}
|
||||
213
includes/Product_Type.php
Normal file
213
includes/Product_Type.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* Composable Product Type
|
||||
*
|
||||
* @package WC_Composable_Product
|
||||
*/
|
||||
|
||||
namespace WC_Composable_Product;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Composable Product Type Class
|
||||
*/
|
||||
class Product_Type extends \WC_Product {
|
||||
/**
|
||||
* Product type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'composable';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param mixed $product Product ID or object
|
||||
*/
|
||||
public function __construct($product = 0) {
|
||||
$this->supports[] = 'ajax_add_to_cart';
|
||||
parent::__construct($product);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type() {
|
||||
return 'composable';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selection limit
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_selection_limit() {
|
||||
$limit = $this->get_meta('_composable_selection_limit', true);
|
||||
if (empty($limit)) {
|
||||
$limit = get_option('wc_composable_default_limit', 5);
|
||||
}
|
||||
return absint($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pricing mode
|
||||
*
|
||||
* @return string 'fixed' or 'sum'
|
||||
*/
|
||||
public function get_pricing_mode() {
|
||||
$mode = $this->get_meta('_composable_pricing_mode', true);
|
||||
if (empty($mode)) {
|
||||
$mode = get_option('wc_composable_default_pricing', 'sum');
|
||||
}
|
||||
return $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product selection criteria
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_selection_criteria() {
|
||||
return [
|
||||
'type' => $this->get_meta('_composable_criteria_type', true) ?: 'category',
|
||||
'categories' => $this->get_meta('_composable_categories', true) ?: [],
|
||||
'tags' => $this->get_meta('_composable_tags', true) ?: [],
|
||||
'skus' => $this->get_meta('_composable_skus', true) ?: '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if product is purchasable
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if product is sold individually
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_sold_individually() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available products based on criteria
|
||||
*
|
||||
* @return array Array of WC_Product objects
|
||||
*/
|
||||
public function get_available_products() {
|
||||
$criteria = $this->get_selection_criteria();
|
||||
$args = [
|
||||
'post_type' => 'product',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'tax_query' => [],
|
||||
];
|
||||
|
||||
// Exclude composable products from selection
|
||||
$args['meta_query'] = [
|
||||
[
|
||||
'key' => '_product_type',
|
||||
'value' => 'composable',
|
||||
'compare' => '!=',
|
||||
],
|
||||
];
|
||||
|
||||
switch ($criteria['type']) {
|
||||
case 'category':
|
||||
if (!empty($criteria['categories'])) {
|
||||
$args['tax_query'][] = [
|
||||
'taxonomy' => 'product_cat',
|
||||
'field' => 'term_id',
|
||||
'terms' => $criteria['categories'],
|
||||
'operator' => 'IN',
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tag':
|
||||
if (!empty($criteria['tags'])) {
|
||||
$args['tax_query'][] = [
|
||||
'taxonomy' => 'product_tag',
|
||||
'field' => 'term_id',
|
||||
'terms' => $criteria['tags'],
|
||||
'operator' => 'IN',
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sku':
|
||||
if (!empty($criteria['skus'])) {
|
||||
$skus = array_map('trim', explode(',', $criteria['skus']));
|
||||
$args['meta_query'][] = [
|
||||
'key' => '_sku',
|
||||
'value' => $skus,
|
||||
'compare' => 'IN',
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$query = new \WP_Query($args);
|
||||
$products = [];
|
||||
|
||||
if ($query->have_posts()) {
|
||||
foreach ($query->posts as $post) {
|
||||
$product = wc_get_product($post->ID);
|
||||
if ($product && $product->is_in_stock() && $product->is_purchasable()) {
|
||||
$products[] = $product;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate price based on selected products
|
||||
*
|
||||
* @param array $selected_products Array of product IDs
|
||||
* @return float
|
||||
*/
|
||||
public function calculate_composed_price($selected_products) {
|
||||
$pricing_mode = $this->get_pricing_mode();
|
||||
|
||||
if ($pricing_mode === 'fixed') {
|
||||
return floatval($this->get_regular_price());
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
foreach ($selected_products as $product_id) {
|
||||
$product = wc_get_product($product_id);
|
||||
if ($product) {
|
||||
$total += floatval($product->get_price());
|
||||
}
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart validation
|
||||
*
|
||||
* @param int $product_id Product ID
|
||||
* @param int $quantity Quantity
|
||||
* @param int $variation_id Variation ID
|
||||
* @param array $variations Variations
|
||||
* @param array $cart_item_data Cart item data
|
||||
* @return bool
|
||||
*/
|
||||
public function add_to_cart_validation($product_id, $quantity, $variation_id = 0, $variations = [], $cart_item_data = []) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
199
languages/wc-composable-product.pot
Normal file
199
languages/wc-composable-product.pot
Normal file
@@ -0,0 +1,199 @@
|
||||
# Copyright (C) 2024 Marco Graetsch
|
||||
# This file is distributed under the GPL v3 or later.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: WP-CLI\n"
|
||||
|
||||
#: wc-composable-product.php
|
||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
||||
msgstr ""
|
||||
|
||||
#: wc-composable-product.php
|
||||
msgid "This plugin requires WooCommerce to be installed and active."
|
||||
msgstr ""
|
||||
|
||||
#: wc-composable-product.php
|
||||
msgid "Plugin Activation Error"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Composable Products"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Composable Products Settings"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Configure default settings for composable products."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Default Selection Limit"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Default number of items customers can select."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Default Pricing Mode"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "How to calculate the price of composable products."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Sum of selected products"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Fixed price"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Show Product Images"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Display product images in the selection interface."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Show Product Prices"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Display individual product prices in the selection interface."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Show Total Price"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Settings.php
|
||||
msgid "Display the total price as customers make selections."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Plugin.php
|
||||
msgid "Composable product"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
||||
msgid "Select Your Products"
|
||||
msgstr ""
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "Choose up to"
|
||||
msgstr ""
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "items from the selection below."
|
||||
msgstr ""
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "Total Price:"
|
||||
msgstr ""
|
||||
|
||||
#: templates/product-selector.twig
|
||||
msgid "Add to Cart"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
msgid "Please select at least one product."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
msgid "You can select a maximum of %d products."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
msgid "One or more selected products are not available."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Cart_Handler.php
|
||||
msgid "Selected Products"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Composable Options"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Selection Limit"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Pricing Mode"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "How to calculate the price. Leave empty to use global default."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Use global default"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Selection Criteria"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "How to select available products."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "By Category"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "By Tag"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "By SKU"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Select Categories"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Select product categories to include."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Select Tags"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Select product tags to include."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Product SKUs"
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "Enter product SKUs separated by commas."
|
||||
msgstr ""
|
||||
|
||||
#: includes/Admin/Product_Data.php
|
||||
msgid "SKU-1, SKU-2, SKU-3"
|
||||
msgstr ""
|
||||
65
templates/product-selector.twig
Normal file
65
templates/product-selector.twig
Normal file
@@ -0,0 +1,65 @@
|
||||
{# Product Selector Template #}
|
||||
<div class="wc-composable-product-selector" data-product-id="{{ product_id }}" data-selection-limit="{{ selection_limit }}" data-pricing-mode="{{ pricing_mode }}" data-fixed-price="{{ fixed_price }}">
|
||||
|
||||
<div class="composable-header">
|
||||
<h3>{{ __('Select Your Products') }}</h3>
|
||||
<p class="selection-info">
|
||||
{{ __('Choose up to') }} <strong>{{ selection_limit }}</strong> {{ __('items from the selection below.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="composable-products-grid">
|
||||
{% for product in products %}
|
||||
<div class="composable-product-item" data-product-id="{{ product.id }}" data-price="{{ product.price }}">
|
||||
<div class="product-item-inner">
|
||||
<label class="product-item-label">
|
||||
<input type="checkbox"
|
||||
name="composable_products[]"
|
||||
value="{{ product.id }}"
|
||||
class="composable-product-checkbox"
|
||||
data-product-id="{{ product.id }}"
|
||||
data-price="{{ product.price }}">
|
||||
|
||||
{% if show_images and product.image_url %}
|
||||
<div class="product-item-image">
|
||||
<img src="{{ product.image_url }}" alt="{{ product.name|esc_attr }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="product-item-details">
|
||||
<h4 class="product-item-name">{{ product.name|esc_html }}</h4>
|
||||
|
||||
{% if show_prices %}
|
||||
<div class="product-item-price">
|
||||
{{ product.price_html|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<span class="product-item-checkmark"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if show_total %}
|
||||
<div class="composable-total">
|
||||
<div class="total-label">{{ __('Total Price:') }}</div>
|
||||
<div class="total-price" data-currency="{{ currency_symbol }}">
|
||||
{% if pricing_mode == 'fixed' %}
|
||||
{{ currency_symbol }}{{ fixed_price }}
|
||||
{% else %}
|
||||
<span class="calculated-total">{{ currency_symbol }}0.00</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="composable-actions">
|
||||
<button type="button" class="button alt composable-add-to-cart" data-product-id="{{ product_id }}">
|
||||
{{ __('Add to Cart') }}
|
||||
</button>
|
||||
<div class="composable-messages"></div>
|
||||
</div>
|
||||
</div>
|
||||
79
wc-composable-product.php
Normal file
79
wc-composable-product.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WooCommerce Composable Products
|
||||
* Plugin URI: https://github.com/magdev/wc-composable-product
|
||||
* Description: Create composable products where customers select a limited number of items from a configurable set
|
||||
* Version: 1.0.0
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://example.com
|
||||
* License: GPL v3 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
* Text Domain: wc-composable-product
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.3
|
||||
* WC requires at least: 8.0
|
||||
* WC tested up to: 10.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// Define plugin constants
|
||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.0.0');
|
||||
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
||||
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
||||
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
||||
define('WC_COMPOSABLE_PRODUCT_BASENAME', plugin_basename(__FILE__));
|
||||
|
||||
// Load Composer autoloader
|
||||
if (file_exists(WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php')) {
|
||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active
|
||||
*/
|
||||
function wc_composable_product_check_woocommerce() {
|
||||
if (!class_exists('WooCommerce')) {
|
||||
add_action('admin_notices', function() {
|
||||
?>
|
||||
<div class="notice notice-error">
|
||||
<p><?php esc_html_e('WooCommerce Composable Products requires WooCommerce to be installed and active.', 'wc-composable-product'); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*/
|
||||
function wc_composable_product_init() {
|
||||
if (!wc_composable_product_check_woocommerce()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load text domain
|
||||
load_plugin_textdomain('wc-composable-product', false, dirname(WC_COMPOSABLE_PRODUCT_BASENAME) . '/languages');
|
||||
|
||||
// Initialize main plugin class
|
||||
WC_Composable_Product\Plugin::instance();
|
||||
}
|
||||
add_action('plugins_loaded', 'wc_composable_product_init');
|
||||
|
||||
/**
|
||||
* Activation hook
|
||||
*/
|
||||
function wc_composable_product_activate() {
|
||||
if (!class_exists('WooCommerce')) {
|
||||
deactivate_plugins(WC_COMPOSABLE_PRODUCT_BASENAME);
|
||||
wp_die(
|
||||
esc_html__('This plugin requires WooCommerce to be installed and active.', 'wc-composable-product'),
|
||||
esc_html__('Plugin Activation Error', 'wc-composable-product'),
|
||||
array('back_link' => true)
|
||||
);
|
||||
}
|
||||
}
|
||||
register_activation_hook(__FILE__, 'wc_composable_product_activate');
|
||||
Reference in New Issue
Block a user