feat: Add product images support with WP Media Library integration

- Add WP Media Library integration for product and variation images
- Support images array (URLs) conversion to attachment IDs
- Add images array to API responses (Admin & Customer SPA)
- Implement drag-and-drop sortable images in Admin product form
- Add image gallery thumbnails in Customer SPA product page
- Initialize WooCommerce session for guest cart operations
- Fix product variations and attributes display in Customer SPA
- Add variation image field in Admin SPA

Changes:
- includes/Api/ProductsController.php: Handle images array, add to responses
- includes/Frontend/ShopController.php: Add images array for customer SPA
- includes/Frontend/CartController.php: Initialize WC session for guests
- admin-spa/src/lib/wp-media.ts: Add openWPMediaGallery function
- admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx: WP Media + sortable images
- admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx: Add variation image field
- customer-spa/src/pages/Product/index.tsx: Add gallery thumbnails display
This commit is contained in:
Dwindi Ramadhana
2025-11-26 16:18:43 +07:00
parent 909bddb23d
commit f397ef850f
69 changed files with 12481 additions and 156 deletions

View File

@@ -0,0 +1,224 @@
<?php
namespace WooNooW\Frontend;
/**
* WooCommerce Hook Bridge
* Captures WooCommerce action hook output and makes it available to the SPA
*/
class HookBridge {
/**
* Common WooCommerce hooks to capture
*/
private static $hooks = [
// Single Product Hooks
'woocommerce_before_single_product',
'woocommerce_before_single_product_summary',
'woocommerce_single_product_summary',
'woocommerce_before_add_to_cart_form',
'woocommerce_before_add_to_cart_button',
'woocommerce_after_add_to_cart_button',
'woocommerce_after_add_to_cart_form',
'woocommerce_product_meta_start',
'woocommerce_product_meta_end',
'woocommerce_after_single_product_summary',
'woocommerce_after_single_product',
// Shop/Archive Hooks
'woocommerce_before_shop_loop',
'woocommerce_after_shop_loop',
'woocommerce_before_shop_loop_item',
'woocommerce_after_shop_loop_item',
'woocommerce_before_shop_loop_item_title',
'woocommerce_shop_loop_item_title',
'woocommerce_after_shop_loop_item_title',
// Cart Hooks
'woocommerce_before_cart',
'woocommerce_before_cart_table',
'woocommerce_before_cart_contents',
'woocommerce_cart_contents',
'woocommerce_after_cart_contents',
'woocommerce_after_cart_table',
'woocommerce_cart_collaterals',
'woocommerce_after_cart',
// Checkout Hooks
'woocommerce_before_checkout_form',
'woocommerce_checkout_before_customer_details',
'woocommerce_checkout_after_customer_details',
'woocommerce_checkout_before_order_review',
'woocommerce_checkout_after_order_review',
'woocommerce_after_checkout_form',
];
/**
* Capture hook output for a specific context
*
* @param string $context 'product', 'shop', 'cart', 'checkout'
* @param array $args Context-specific arguments (e.g., product_id)
* @return array Associative array of hook_name => html_output
*/
public static function capture_hooks($context, $args = []) {
$captured = [];
// Filter hooks based on context
$context_hooks = self::get_context_hooks($context);
foreach ($context_hooks as $hook) {
// Start output buffering
ob_start();
// Setup context (e.g., global $product for product hooks)
self::setup_context($context, $args);
// Execute the hook
do_action($hook);
// Capture output
$output = ob_get_clean();
// Only include hooks that have output
if (!empty(trim($output))) {
$captured[$hook] = $output;
}
// Cleanup context
self::cleanup_context($context);
}
return $captured;
}
/**
* Get hooks for a specific context
*/
private static function get_context_hooks($context) {
$context_map = [
'product' => [
'woocommerce_before_single_product',
'woocommerce_before_single_product_summary',
'woocommerce_single_product_summary',
'woocommerce_before_add_to_cart_form',
'woocommerce_before_add_to_cart_button',
'woocommerce_after_add_to_cart_button',
'woocommerce_after_add_to_cart_form',
'woocommerce_product_meta_start',
'woocommerce_product_meta_end',
'woocommerce_after_single_product_summary',
'woocommerce_after_single_product',
],
'shop' => [
'woocommerce_before_shop_loop',
'woocommerce_after_shop_loop',
'woocommerce_before_shop_loop_item',
'woocommerce_after_shop_loop_item',
],
'cart' => [
'woocommerce_before_cart',
'woocommerce_before_cart_table',
'woocommerce_cart_collaterals',
'woocommerce_after_cart',
],
'checkout' => [
'woocommerce_before_checkout_form',
'woocommerce_checkout_before_customer_details',
'woocommerce_checkout_after_customer_details',
'woocommerce_after_checkout_form',
],
];
return $context_map[$context] ?? [];
}
/**
* Setup context for hook execution
*/
private static function setup_context($context, $args) {
global $product, $post;
switch ($context) {
case 'product':
if (isset($args['product_id'])) {
$product = wc_get_product($args['product_id']);
$post = get_post($args['product_id']);
setup_postdata($post);
}
break;
case 'shop':
// Setup shop context if needed
break;
case 'cart':
// Ensure cart is loaded
if (!WC()->cart) {
WC()->cart = new \WC_Cart();
}
break;
case 'checkout':
// Ensure checkout is loaded
if (!WC()->checkout()) {
WC()->checkout = new \WC_Checkout();
}
break;
}
}
/**
* Cleanup context after hook execution
*/
private static function cleanup_context($context) {
global $product, $post;
switch ($context) {
case 'product':
wp_reset_postdata();
$product = null;
break;
}
}
/**
* Register REST API endpoint for hook capture
*/
public static function register_routes() {
register_rest_route('woonoow/v1', '/hooks/(?P<context>[a-z]+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_hooks'],
'permission_callback' => '__return_true',
'args' => [
'context' => [
'required' => true,
'type' => 'string',
'enum' => ['product', 'shop', 'cart', 'checkout'],
],
'product_id' => [
'type' => 'integer',
],
],
]);
}
/**
* REST API callback to get hooks
*/
public static function get_hooks($request) {
$context = $request->get_param('context');
$args = [];
// Get context-specific args
if ($context === 'product') {
$args['product_id'] = $request->get_param('product_id');
}
$hooks = self::capture_hooks($context, $args);
return rest_ensure_response([
'success' => true,
'context' => $context,
'hooks' => $hooks,
]);
}
}