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,373 @@
<?php
namespace WooNooW\Api\Controllers;
use WP_REST_Controller;
use WP_REST_Response;
use WP_Error;
/**
* Cart Controller
* Handles cart operations via REST API
*/
class CartController extends WP_REST_Controller {
/**
* Register routes
*/
public function register_routes() {
$namespace = 'woonoow/v1';
// Get cart
register_rest_route($namespace, '/cart', [
'methods' => 'GET',
'callback' => [$this, 'get_cart'],
'permission_callback' => '__return_true',
]);
// Add to cart
register_rest_route($namespace, '/cart/add', [
'methods' => 'POST',
'callback' => [$this, 'add_to_cart'],
'permission_callback' => '__return_true',
'args' => [
'product_id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
],
'quantity' => [
'required' => false,
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
],
'variation_id' => [
'required' => false,
'type' => 'integer',
'sanitize_callback' => 'absint',
],
],
]);
// Update cart item
register_rest_route($namespace, '/cart/update', [
'methods' => 'POST',
'callback' => [$this, 'update_cart_item'],
'permission_callback' => '__return_true',
'args' => [
'cart_item_key' => [
'required' => true,
'type' => 'string',
],
'quantity' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
],
],
]);
// Remove from cart
register_rest_route($namespace, '/cart/remove', [
'methods' => 'POST',
'callback' => [$this, 'remove_from_cart'],
'permission_callback' => '__return_true',
'args' => [
'cart_item_key' => [
'required' => true,
'type' => 'string',
],
],
]);
// Clear cart
register_rest_route($namespace, '/cart/clear', [
'methods' => 'POST',
'callback' => [$this, 'clear_cart'],
'permission_callback' => '__return_true',
]);
// Apply coupon
register_rest_route($namespace, '/cart/apply-coupon', [
'methods' => 'POST',
'callback' => [$this, 'apply_coupon'],
'permission_callback' => '__return_true',
'args' => [
'coupon_code' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Remove coupon
register_rest_route($namespace, '/cart/remove-coupon', [
'methods' => 'POST',
'callback' => [$this, 'remove_coupon'],
'permission_callback' => '__return_true',
'args' => [
'coupon_code' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
}
/**
* Get cart contents
*/
public function get_cart($request) {
if (!function_exists('WC')) {
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
}
// Ensure cart is initialized
if (is_null(WC()->cart)) {
wc_load_cart();
}
$cart = WC()->cart;
return new WP_REST_Response([
'items' => $this->format_cart_items($cart->get_cart()),
'totals' => [
'subtotal' => $cart->get_subtotal(),
'subtotal_tax' => $cart->get_subtotal_tax(),
'discount_total' => $cart->get_discount_total(),
'discount_tax' => $cart->get_discount_tax(),
'shipping_total' => $cart->get_shipping_total(),
'shipping_tax' => $cart->get_shipping_tax(),
'fee_total' => $cart->get_fee_total(),
'fee_tax' => $cart->get_fee_tax(),
'total' => $cart->get_total(''),
'total_tax' => $cart->get_total_tax(),
],
'coupons' => $cart->get_applied_coupons(),
'needs_shipping' => $cart->needs_shipping(),
'needs_payment' => $cart->needs_payment(),
'item_count' => $cart->get_cart_contents_count(),
], 200);
}
/**
* Add product to cart
*/
public function add_to_cart($request) {
if (!function_exists('WC')) {
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
}
// Ensure cart is initialized
if (is_null(WC()->cart)) {
wc_load_cart();
}
$product_id = $request->get_param('product_id');
$quantity = $request->get_param('quantity') ?: 1;
$variation_id = $request->get_param('variation_id') ?: 0;
// Validate product
$product = wc_get_product($product_id);
if (!$product) {
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
}
// Check stock
if (!$product->is_in_stock()) {
return new WP_Error('out_of_stock', 'Product is out of stock', ['status' => 400]);
}
// Add to cart
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, $variation_id);
if (!$cart_item_key) {
return new WP_Error('add_to_cart_failed', 'Failed to add product to cart', ['status' => 400]);
}
return new WP_REST_Response([
'success' => true,
'cart_item_key' => $cart_item_key,
'message' => sprintf('%s has been added to your cart.', $product->get_name()),
'cart' => $this->get_cart($request)->data,
], 200);
}
/**
* Update cart item quantity
*/
public function update_cart_item($request) {
if (!function_exists('WC')) {
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
}
// Ensure cart is initialized
if (is_null(WC()->cart)) {
wc_load_cart();
}
$cart_item_key = $request->get_param('cart_item_key');
$quantity = $request->get_param('quantity');
// Validate cart item
$cart = WC()->cart->get_cart();
if (!isset($cart[$cart_item_key])) {
return new WP_Error('invalid_cart_item', 'Cart item not found', ['status' => 404]);
}
// Update quantity
$updated = WC()->cart->set_quantity($cart_item_key, $quantity);
if (!$updated) {
return new WP_Error('update_failed', 'Failed to update cart item', ['status' => 400]);
}
return new WP_REST_Response([
'success' => true,
'message' => 'Cart updated successfully',
'cart' => $this->get_cart($request)->data,
], 200);
}
/**
* Remove item from cart
*/
public function remove_from_cart($request) {
if (!function_exists('WC')) {
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
}
// Ensure cart is initialized
if (is_null(WC()->cart)) {
wc_load_cart();
}
$cart_item_key = $request->get_param('cart_item_key');
// Validate cart item
$cart = WC()->cart->get_cart();
if (!isset($cart[$cart_item_key])) {
return new WP_Error('invalid_cart_item', 'Cart item not found', ['status' => 404]);
}
// Remove item
$removed = WC()->cart->remove_cart_item($cart_item_key);
if (!$removed) {
return new WP_Error('remove_failed', 'Failed to remove cart item', ['status' => 400]);
}
return new WP_REST_Response([
'success' => true,
'message' => 'Item removed from cart',
'cart' => $this->get_cart($request)->data,
], 200);
}
/**
* Clear cart
*/
public function clear_cart($request) {
if (!function_exists('WC')) {
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
}
// Ensure cart is initialized
if (is_null(WC()->cart)) {
wc_load_cart();
}
WC()->cart->empty_cart();
return new WP_REST_Response([
'success' => true,
'message' => 'Cart cleared successfully',
], 200);
}
/**
* Apply coupon
*/
public function apply_coupon($request) {
if (!function_exists('WC')) {
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
}
// Ensure cart is initialized
if (is_null(WC()->cart)) {
wc_load_cart();
}
$coupon_code = $request->get_param('coupon_code');
// Apply coupon
$applied = WC()->cart->apply_coupon($coupon_code);
if (!$applied) {
return new WP_Error('coupon_failed', 'Failed to apply coupon', ['status' => 400]);
}
return new WP_REST_Response([
'success' => true,
'message' => 'Coupon applied successfully',
'cart' => $this->get_cart($request)->data,
], 200);
}
/**
* Remove coupon
*/
public function remove_coupon($request) {
if (!function_exists('WC')) {
return new WP_Error('woocommerce_not_active', 'WooCommerce is not active', ['status' => 500]);
}
// Ensure cart is initialized
if (is_null(WC()->cart)) {
wc_load_cart();
}
$coupon_code = $request->get_param('coupon_code');
// Remove coupon
$removed = WC()->cart->remove_coupon($coupon_code);
if (!$removed) {
return new WP_Error('coupon_remove_failed', 'Failed to remove coupon', ['status' => 400]);
}
return new WP_REST_Response([
'success' => true,
'message' => 'Coupon removed successfully',
'cart' => $this->get_cart($request)->data,
], 200);
}
/**
* Format cart items for response
*/
private function format_cart_items($cart_items) {
$formatted = [];
foreach ($cart_items as $cart_item_key => $cart_item) {
$product = $cart_item['data'];
$formatted[] = [
'key' => $cart_item_key,
'product_id' => $cart_item['product_id'],
'variation_id' => $cart_item['variation_id'],
'quantity' => $cart_item['quantity'],
'name' => $product->get_name(),
'price' => $product->get_price(),
'subtotal' => $cart_item['line_subtotal'],
'total' => $cart_item['line_total'],
'image' => wp_get_attachment_image_url($product->get_image_id(), 'thumbnail'),
'permalink' => $product->get_permalink(),
];
}
return $formatted;
}
}

View File

@@ -0,0 +1,317 @@
<?php
namespace WooNooW\Api\Controllers;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Settings Controller
* Handles Customer SPA settings via REST API
*/
class SettingsController extends WP_REST_Controller {
/**
* Namespace
*/
protected $namespace = 'woonoow/v1';
/**
* Rest base
*/
protected $rest_base = 'settings';
/**
* Register routes
*/
public function register_routes() {
// Get/Update Customer SPA settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-spa', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_customer_spa_settings'],
'permission_callback' => [$this, 'get_settings_permissions_check'],
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'update_customer_spa_settings'],
'permission_callback' => [$this, 'update_settings_permissions_check'],
'args' => $this->get_customer_spa_schema(),
],
]);
}
/**
* Get Customer SPA settings
*/
public function get_customer_spa_settings(WP_REST_Request $request) {
$settings = $this->get_default_settings();
$saved_settings = get_option('woonoow_customer_spa_settings', []);
// Merge with saved settings
if (!empty($saved_settings)) {
$settings = array_replace_recursive($settings, $saved_settings);
}
// Add enabled flag
$settings['enabled'] = get_option('woonoow_customer_spa_enabled', false);
return new WP_REST_Response($settings, 200);
}
/**
* Update Customer SPA settings
*/
public function update_customer_spa_settings(WP_REST_Request $request) {
$params = $request->get_json_params();
// Extract enabled flag
if (isset($params['enabled'])) {
update_option('woonoow_customer_spa_enabled', (bool) $params['enabled']);
unset($params['enabled']);
}
// Get current settings
$current_settings = get_option('woonoow_customer_spa_settings', $this->get_default_settings());
// Merge with new settings
$new_settings = array_replace_recursive($current_settings, $params);
// Validate settings
$validated = $this->validate_settings($new_settings);
if (is_wp_error($validated)) {
return $validated;
}
// Save settings
update_option('woonoow_customer_spa_settings', $new_settings);
// Return updated settings
$new_settings['enabled'] = get_option('woonoow_customer_spa_enabled', false);
return new WP_REST_Response([
'success' => true,
'data' => $new_settings,
], 200);
}
/**
* Get default settings
*/
private function get_default_settings() {
return [
'mode' => 'disabled',
'checkoutPages' => [
'checkout' => true,
'thankyou' => true,
'account' => true,
'cart' => false,
],
'layout' => 'modern',
'branding' => [
'logo' => '',
'favicon' => '',
'siteName' => get_bloginfo('name'),
],
'colors' => [
'primary' => '#3B82F6',
'secondary' => '#8B5CF6',
'accent' => '#10B981',
'background' => '#FFFFFF',
'text' => '#1F2937',
],
'typography' => [
'preset' => 'professional',
'customFonts' => null,
],
'menus' => [
'primary' => 0,
'footer' => 0,
],
'homepage' => [
'sections' => [
[
'id' => 'hero-1',
'type' => 'hero',
'enabled' => true,
'order' => 0,
'config' => [],
],
[
'id' => 'featured-1',
'type' => 'featured',
'enabled' => true,
'order' => 1,
'config' => [],
],
[
'id' => 'categories-1',
'type' => 'categories',
'enabled' => true,
'order' => 2,
'config' => [],
],
],
],
'product' => [
'layout' => 'standard',
'showRelatedProducts' => true,
'showReviews' => true,
],
'checkout' => [
'style' => 'onepage',
'enableGuestCheckout' => true,
'showTrustBadges' => true,
'showOrderSummary' => 'sidebar',
],
];
}
/**
* Validate settings
*/
private function validate_settings($settings) {
// Validate mode
if (isset($settings['mode']) && !in_array($settings['mode'], ['disabled', 'full', 'checkout_only'])) {
return new WP_Error(
'invalid_mode',
__('Invalid mode. Must be disabled, full, or checkout_only.', 'woonoow'),
['status' => 400]
);
}
// Validate layout
if (isset($settings['layout']) && !in_array($settings['layout'], ['classic', 'modern', 'boutique', 'launch'])) {
return new WP_Error(
'invalid_layout',
__('Invalid layout. Must be classic, modern, boutique, or launch.', 'woonoow'),
['status' => 400]
);
}
// Validate colors (hex format)
if (isset($settings['colors'])) {
foreach ($settings['colors'] as $key => $color) {
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) {
return new WP_Error(
'invalid_color',
sprintf(__('Invalid color format for %s. Must be hex format (#RRGGBB).', 'woonoow'), $key),
['status' => 400]
);
}
}
}
// Validate typography preset
if (isset($settings['typography']['preset'])) {
$valid_presets = ['professional', 'modern', 'elegant', 'tech', 'custom'];
if (!in_array($settings['typography']['preset'], $valid_presets)) {
return new WP_Error(
'invalid_typography',
__('Invalid typography preset.', 'woonoow'),
['status' => 400]
);
}
}
return true;
}
/**
* Get Customer SPA settings schema
*/
private function get_customer_spa_schema() {
return [
'mode' => [
'type' => 'string',
'enum' => ['disabled', 'full', 'checkout_only'],
'description' => __('Customer SPA mode', 'woonoow'),
],
'checkoutPages' => [
'type' => 'object',
'properties' => [
'checkout' => ['type' => 'boolean'],
'thankyou' => ['type' => 'boolean'],
'account' => ['type' => 'boolean'],
'cart' => ['type' => 'boolean'],
],
],
'layout' => [
'type' => 'string',
'enum' => ['classic', 'modern', 'boutique', 'launch'],
'description' => __('Master layout', 'woonoow'),
],
'branding' => [
'type' => 'object',
'properties' => [
'logo' => ['type' => 'string'],
'favicon' => ['type' => 'string'],
'siteName' => ['type' => 'string'],
],
],
'colors' => [
'type' => 'object',
'properties' => [
'primary' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
'secondary' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
'accent' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
'background' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
'text' => ['type' => 'string', 'pattern' => '^#[a-fA-F0-9]{6}$'],
],
],
'typography' => [
'type' => 'object',
'properties' => [
'preset' => ['type' => 'string', 'enum' => ['professional', 'modern', 'elegant', 'tech', 'custom']],
'customFonts' => ['type' => ['object', 'null']],
],
],
'menus' => [
'type' => 'object',
'properties' => [
'primary' => ['type' => 'integer'],
'footer' => ['type' => 'integer'],
],
],
'homepage' => [
'type' => 'object',
'properties' => [
'sections' => ['type' => 'array'],
],
],
'product' => [
'type' => 'object',
'properties' => [
'layout' => ['type' => 'string'],
'showRelatedProducts' => ['type' => 'boolean'],
'showReviews' => ['type' => 'boolean'],
],
],
'checkout' => [
'type' => 'object',
'properties' => [
'style' => ['type' => 'string'],
'enableGuestCheckout' => ['type' => 'boolean'],
'showTrustBadges' => ['type' => 'boolean'],
'showOrderSummary' => ['type' => 'string'],
],
],
];
}
/**
* Check permissions for getting settings
*/
public function get_settings_permissions_check() {
return current_user_can('manage_woocommerce');
}
/**
* Check permissions for updating settings
*/
public function update_settings_permissions_check() {
return current_user_can('manage_woocommerce');
}
}

View File

@@ -337,12 +337,33 @@ class ProductsController {
$product->set_tag_ids($data['tags']);
}
// Images
if (!empty($data['image_id'])) {
$product->set_image_id($data['image_id']);
}
if (!empty($data['gallery_image_ids']) && is_array($data['gallery_image_ids'])) {
$product->set_gallery_image_ids($data['gallery_image_ids']);
// Images - support both image_id/gallery_image_ids and images array
if (!empty($data['images']) && is_array($data['images'])) {
// Convert URLs to attachment IDs
$image_ids = [];
foreach ($data['images'] as $image_url) {
$attachment_id = attachment_url_to_postid($image_url);
if ($attachment_id) {
$image_ids[] = $attachment_id;
}
}
if (!empty($image_ids)) {
// First image is featured
$product->set_image_id($image_ids[0]);
// Rest are gallery
if (count($image_ids) > 1) {
$product->set_gallery_image_ids(array_slice($image_ids, 1));
}
}
} else {
// Legacy support for direct IDs
if (!empty($data['image_id'])) {
$product->set_image_id($data['image_id']);
}
if (!empty($data['gallery_image_ids']) && is_array($data['gallery_image_ids'])) {
$product->set_gallery_image_ids($data['gallery_image_ids']);
}
}
$product->save();
@@ -407,12 +428,35 @@ class ProductsController {
$product->set_tag_ids($data['tags']);
}
// Images
if (isset($data['image_id'])) {
$product->set_image_id($data['image_id']);
}
if (isset($data['gallery_image_ids'])) {
$product->set_gallery_image_ids($data['gallery_image_ids']);
// Images - support both image_id/gallery_image_ids and images array
if (isset($data['images']) && is_array($data['images']) && !empty($data['images'])) {
// Convert URLs to attachment IDs
$image_ids = [];
foreach ($data['images'] as $image_url) {
$attachment_id = attachment_url_to_postid($image_url);
if ($attachment_id) {
$image_ids[] = $attachment_id;
}
}
if (!empty($image_ids)) {
// First image is featured
$product->set_image_id($image_ids[0]);
// Rest are gallery
if (count($image_ids) > 1) {
$product->set_gallery_image_ids(array_slice($image_ids, 1));
} else {
$product->set_gallery_image_ids([]);
}
}
} else {
// Legacy support for direct IDs
if (isset($data['image_id'])) {
$product->set_image_id($data['image_id']);
}
if (isset($data['gallery_image_ids'])) {
$product->set_gallery_image_ids($data['gallery_image_ids']);
}
}
// Update custom meta fields (Level 1 compatibility)
@@ -596,7 +640,24 @@ class ProductsController {
$data['downloadable'] = $product->is_downloadable();
$data['featured'] = $product->is_featured();
// Gallery images
// Images array (URLs) for frontend - featured + gallery
$images = [];
$featured_image_id = $product->get_image_id();
if ($featured_image_id) {
$featured_url = wp_get_attachment_url($featured_image_id);
if ($featured_url) {
$images[] = $featured_url;
}
}
foreach ($product->get_gallery_image_ids() as $image_id) {
$url = wp_get_attachment_url($image_id);
if ($url) {
$images[] = $url;
}
}
$data['images'] = $images;
// Gallery images (detailed info)
$gallery = [];
foreach ($product->get_gallery_image_ids() as $image_id) {
$image = wp_get_attachment_image_src($image_id, 'full');
@@ -691,6 +752,11 @@ class ProductsController {
$formatted_attributes[$clean_name] = $value;
}
$image_url = $image ? $image[0] : '';
if (!$image_url && $variation->get_image_id()) {
$image_url = wp_get_attachment_url($variation->get_image_id());
}
$variations[] = [
'id' => $variation->get_id(),
'sku' => $variation->get_sku(),
@@ -702,7 +768,8 @@ class ProductsController {
'manage_stock' => $variation->get_manage_stock(),
'attributes' => $formatted_attributes,
'image_id' => $variation->get_image_id(),
'image_url' => $image ? $image[0] : '',
'image_url' => $image_url,
'image' => $image_url, // For form compatibility
];
}
}
@@ -749,7 +816,16 @@ class ProductsController {
if (isset($var_data['manage_stock'])) $variation->set_manage_stock($var_data['manage_stock']);
if (isset($var_data['stock_quantity'])) $variation->set_stock_quantity($var_data['stock_quantity']);
if (isset($var_data['attributes'])) $variation->set_attributes($var_data['attributes']);
if (isset($var_data['image_id'])) $variation->set_image_id($var_data['image_id']);
// Handle image - support both image_id and image URL
if (isset($var_data['image']) && !empty($var_data['image'])) {
$image_id = attachment_url_to_postid($var_data['image']);
if ($image_id) {
$variation->set_image_id($image_id);
}
} elseif (isset($var_data['image_id'])) {
$variation->set_image_id($var_data['image_id']);
}
$variation->save();
}

View File

@@ -20,6 +20,12 @@ use WooNooW\Api\ActivityLogController;
use WooNooW\Api\ProductsController;
use WooNooW\Api\CouponsController;
use WooNooW\Api\CustomersController;
use WooNooW\Frontend\ShopController;
use WooNooW\Frontend\CartController as FrontendCartController;
use WooNooW\Frontend\AccountController;
use WooNooW\Frontend\HookBridge;
use WooNooW\Api\Controllers\SettingsController;
use WooNooW\Api\Controllers\CartController as ApiCartController;
class Routes {
public static function init() {
@@ -66,6 +72,14 @@ class Routes {
OrdersController::register();
AnalyticsController::register_routes();
// Settings controller
$settings_controller = new SettingsController();
$settings_controller->register_routes();
// Cart controller (API)
$api_cart_controller = new ApiCartController();
$api_cart_controller->register_routes();
// Payments controller
$payments_controller = new PaymentsController();
$payments_controller->register_routes();
@@ -116,6 +130,14 @@ class Routes {
// Customers controller
CustomersController::register_routes();
// Frontend controllers (customer-facing)
error_log('WooNooW Routes: Registering Frontend controllers');
ShopController::register_routes();
FrontendCartController::register_routes();
AccountController::register_routes();
HookBridge::register_routes();
error_log('WooNooW Routes: Frontend controllers registered');
});
}
}

View File

@@ -13,7 +13,7 @@ if ( ! defined('ABSPATH') ) exit;
*/
class NavigationRegistry {
const NAV_OPTION = 'wnw_nav_tree';
const NAV_VERSION = '1.0.0';
const NAV_VERSION = '1.0.1'; // Bumped to add Customer SPA settings
/**
* Initialize hooks
@@ -29,6 +29,15 @@ class NavigationRegistry {
* Build the complete navigation tree
*/
public static function build_nav_tree() {
// Check if we need to rebuild (version mismatch)
$cached = get_option(self::NAV_OPTION, []);
$cached_version = $cached['version'] ?? '';
if ($cached_version === self::NAV_VERSION && !empty($cached['tree'])) {
// Cache is valid, no need to rebuild
return;
}
// Base navigation tree (core WooNooW sections)
$tree = self::get_base_tree();
@@ -182,6 +191,7 @@ class NavigationRegistry {
['label' => __('Tax', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/tax'],
['label' => __('Customers', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/customers'],
['label' => __('Notifications', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/notifications'],
['label' => __('Customer SPA', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/customer-spa'],
['label' => __('Developer', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/developer'],
];

View File

@@ -24,6 +24,9 @@ use WooNooW\Core\Notifications\PushNotificationHandler;
use WooNooW\Core\Notifications\EmailManager;
use WooNooW\Core\ActivityLog\ActivityLogTable;
use WooNooW\Branding;
use WooNooW\Frontend\Assets as FrontendAssets;
use WooNooW\Frontend\Shortcodes;
use WooNooW\Frontend\TemplateOverride;
class Bootstrap {
public static function init() {
@@ -37,6 +40,11 @@ class Bootstrap {
PushNotificationHandler::init();
EmailManager::instance(); // Initialize custom email system
// Frontend (customer-spa)
FrontendAssets::init();
Shortcodes::init();
TemplateOverride::init();
// Activity Log
ActivityLogTable::create_table();

207
includes/Core/Installer.php Normal file
View File

@@ -0,0 +1,207 @@
<?php
namespace WooNooW\Core;
/**
* Plugin Installer
* Handles plugin activation tasks
*/
class Installer {
/**
* Run on plugin activation
*/
public static function activate() {
// Create WooNooW pages
self::create_pages();
// Set WooCommerce to use HPOS
update_option('woocommerce_custom_orders_table_enabled', 'yes');
update_option('woocommerce_custom_orders_table_migration_enabled', 'yes');
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Create or update WooNooW pages
* Smart detection: reuses existing WooCommerce pages if they exist
*/
private static function create_pages() {
$pages = [
'shop' => [
'title' => 'Shop',
'content' => '[woonoow_shop]',
'wc_option' => 'woocommerce_shop_page_id',
],
'cart' => [
'title' => 'Cart',
'content' => '[woonoow_cart]',
'wc_option' => 'woocommerce_cart_page_id',
],
'checkout' => [
'title' => 'Checkout',
'content' => '[woonoow_checkout]',
'wc_option' => 'woocommerce_checkout_page_id',
],
'account' => [
'title' => 'My Account',
'content' => '[woonoow_account]',
'wc_option' => 'woocommerce_myaccount_page_id',
],
];
foreach ($pages as $key => $page_data) {
$page_id = null;
// Strategy 1: Check if WooCommerce already has a page set
if (isset($page_data['wc_option'])) {
$wc_page_id = get_option($page_data['wc_option']);
if ($wc_page_id && get_post($wc_page_id)) {
$page_id = $wc_page_id;
error_log("WooNooW: Found existing WooCommerce {$page_data['title']} page (ID: {$page_id})");
}
}
// Strategy 2: Check if WooNooW already created a page
if (!$page_id) {
$woonoow_page_id = get_option('woonoow_' . $key . '_page_id');
if ($woonoow_page_id && get_post($woonoow_page_id)) {
$page_id = $woonoow_page_id;
error_log("WooNooW: Found existing WooNooW {$page_data['title']} page (ID: {$page_id})");
}
}
// Strategy 3: Search for page by title
if (!$page_id) {
$existing_page = get_page_by_title($page_data['title'], OBJECT, 'page');
if ($existing_page) {
$page_id = $existing_page->ID;
error_log("WooNooW: Found existing {$page_data['title']} page by title (ID: {$page_id})");
}
}
// If page exists, update its content with our shortcode
if ($page_id) {
$current_post = get_post($page_id);
// Only update if it doesn't already have our shortcode
if (!has_shortcode($current_post->post_content, 'woonoow_' . $key)) {
// Backup original content
update_post_meta($page_id, '_woonoow_original_content', $current_post->post_content);
// Update with our shortcode
wp_update_post([
'ID' => $page_id,
'post_content' => $page_data['content'],
]);
error_log("WooNooW: Updated {$page_data['title']} page with WooNooW shortcode");
} else {
error_log("WooNooW: {$page_data['title']} page already has WooNooW shortcode");
}
} else {
// No existing page found, create new one
$page_id = wp_insert_post([
'post_title' => $page_data['title'],
'post_content' => $page_data['content'],
'post_status' => 'publish',
'post_type' => 'page',
'post_author' => get_current_user_id(),
'comment_status' => 'closed',
]);
if ($page_id && !is_wp_error($page_id)) {
error_log("WooNooW: Created new {$page_data['title']} page (ID: {$page_id})");
}
}
// Store page ID and update WooCommerce settings
if ($page_id && !is_wp_error($page_id)) {
update_option('woonoow_' . $key . '_page_id', $page_id);
if (isset($page_data['wc_option'])) {
update_option($page_data['wc_option'], $page_id);
}
}
}
}
/**
* Run on plugin deactivation
*/
public static function deactivate() {
// Restore original page content
self::restore_original_content();
// Flush rewrite rules
flush_rewrite_rules();
// Note: We don't delete pages on deactivation
// Users might have content on those pages
}
/**
* Restore original content to pages that were modified
*/
private static function restore_original_content() {
$page_keys = ['shop', 'cart', 'checkout', 'account'];
foreach ($page_keys as $key) {
$page_id = get_option('woonoow_' . $key . '_page_id');
if ($page_id) {
$original_content = get_post_meta($page_id, '_woonoow_original_content', true);
if ($original_content) {
// Restore original content
wp_update_post([
'ID' => $page_id,
'post_content' => $original_content,
]);
// Remove backup
delete_post_meta($page_id, '_woonoow_original_content');
error_log("WooNooW: Restored original content for page ID: {$page_id}");
}
}
}
}
/**
* Run on plugin uninstall
*/
public static function uninstall() {
// Only delete if user explicitly wants to remove all data
if (get_option('woonoow_remove_data_on_uninstall', false)) {
self::delete_pages();
self::delete_options();
}
}
/**
* Delete WooNooW pages
*/
private static function delete_pages() {
$page_keys = ['shop', 'cart', 'checkout', 'account'];
foreach ($page_keys as $key) {
$page_id = get_option('woonoow_' . $key . '_page_id');
if ($page_id) {
wp_delete_post($page_id, true); // Force delete
delete_option('woonoow_' . $key . '_page_id');
}
}
}
/**
* Delete WooNooW options
*/
private static function delete_options() {
global $wpdb;
// Delete all options starting with 'woonoow_'
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'woonoow_%'");
}
}

View File

@@ -0,0 +1,365 @@
<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Account Controller - Customer account API
* Handles customer account operations for customer-spa
*/
class AccountController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get customer orders
register_rest_route($namespace, '/account/orders', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_orders'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'page' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'per_page' => [
'default' => 10,
'sanitize_callback' => 'absint',
],
],
]);
// Get single order
register_rest_route($namespace, '/account/orders/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_order'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
},
],
],
]);
// Get customer profile
register_rest_route($namespace, '/account/profile', [
[
'methods' => 'GET',
'callback' => [__CLASS__, 'get_profile'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
[
'methods' => 'POST',
'callback' => [__CLASS__, 'update_profile'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
]);
// Update password
register_rest_route($namespace, '/account/password', [
'methods' => 'POST',
'callback' => [__CLASS__, 'update_password'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'current_password' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'new_password' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Get addresses
register_rest_route($namespace, '/account/addresses', [
[
'methods' => 'GET',
'callback' => [__CLASS__, 'get_addresses'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
[
'methods' => 'POST',
'callback' => [__CLASS__, 'update_addresses'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
]);
// Get downloads (for digital products)
register_rest_route($namespace, '/account/downloads', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_downloads'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
]);
}
/**
* Check if user is logged in
*/
public static function check_customer_permission() {
return is_user_logged_in();
}
/**
* Get customer orders
*/
public static function get_orders(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$page = $request->get_param('page');
$per_page = $request->get_param('per_page');
$args = [
'customer_id' => $customer_id,
'limit' => $per_page,
'page' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
$orders = wc_get_orders($args);
$formatted_orders = array_map(function($order) {
return self::format_order($order);
}, $orders);
// Get total count
$total_args = [
'customer_id' => $customer_id,
'return' => 'ids',
];
$total = count(wc_get_orders($total_args));
return new WP_REST_Response([
'orders' => $formatted_orders,
'total' => $total,
'total_pages' => ceil($total / $per_page),
'page' => $page,
'per_page' => $per_page,
], 200);
}
/**
* Get single order
*/
public static function get_order(WP_REST_Request $request) {
$order_id = $request->get_param('id');
$customer_id = get_current_user_id();
$order = wc_get_order($order_id);
if (!$order) {
return new WP_Error('order_not_found', 'Order not found', ['status' => 404]);
}
// Check if order belongs to customer
if ($order->get_customer_id() !== $customer_id) {
return new WP_Error('forbidden', 'You do not have permission to view this order', ['status' => 403]);
}
return new WP_REST_Response(self::format_order($order, true), 200);
}
/**
* Get customer profile
*/
public static function get_profile(WP_REST_Request $request) {
$user_id = get_current_user_id();
$user = get_userdata($user_id);
if (!$user) {
return new WP_Error('user_not_found', 'User not found', ['status' => 404]);
}
return new WP_REST_Response([
'id' => $user->ID,
'email' => $user->user_email,
'first_name' => get_user_meta($user_id, 'first_name', true),
'last_name' => get_user_meta($user_id, 'last_name', true),
'username' => $user->user_login,
], 200);
}
/**
* Update customer profile
*/
public static function update_profile(WP_REST_Request $request) {
$user_id = get_current_user_id();
$first_name = $request->get_param('first_name');
$last_name = $request->get_param('last_name');
$email = $request->get_param('email');
// Update user meta
if ($first_name !== null) {
update_user_meta($user_id, 'first_name', sanitize_text_field($first_name));
}
if ($last_name !== null) {
update_user_meta($user_id, 'last_name', sanitize_text_field($last_name));
}
// Update email if changed
if ($email !== null && is_email($email)) {
$user = get_userdata($user_id);
if ($user->user_email !== $email) {
wp_update_user([
'ID' => $user_id,
'user_email' => $email,
]);
}
}
return new WP_REST_Response([
'message' => 'Profile updated successfully',
], 200);
}
/**
* Update password
*/
public static function update_password(WP_REST_Request $request) {
$user_id = get_current_user_id();
$current_password = $request->get_param('current_password');
$new_password = $request->get_param('new_password');
$user = get_userdata($user_id);
// Verify current password
if (!wp_check_password($current_password, $user->user_pass, $user_id)) {
return new WP_Error('invalid_password', 'Current password is incorrect', ['status' => 400]);
}
// Update password
wp_set_password($new_password, $user_id);
return new WP_REST_Response([
'message' => 'Password updated successfully',
], 200);
}
/**
* Get customer addresses
*/
public static function get_addresses(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$customer = new \WC_Customer($customer_id);
return new WP_REST_Response([
'billing' => [
'first_name' => $customer->get_billing_first_name(),
'last_name' => $customer->get_billing_last_name(),
'company' => $customer->get_billing_company(),
'address_1' => $customer->get_billing_address_1(),
'address_2' => $customer->get_billing_address_2(),
'city' => $customer->get_billing_city(),
'state' => $customer->get_billing_state(),
'postcode' => $customer->get_billing_postcode(),
'country' => $customer->get_billing_country(),
'email' => $customer->get_billing_email(),
'phone' => $customer->get_billing_phone(),
],
'shipping' => [
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
'address_1' => $customer->get_shipping_address_1(),
'address_2' => $customer->get_shipping_address_2(),
'city' => $customer->get_shipping_city(),
'state' => $customer->get_shipping_state(),
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
],
], 200);
}
/**
* Update customer addresses
*/
public static function update_addresses(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$customer = new \WC_Customer($customer_id);
$billing = $request->get_param('billing');
$shipping = $request->get_param('shipping');
// Update billing address
if ($billing) {
foreach ($billing as $key => $value) {
$method = 'set_billing_' . $key;
if (method_exists($customer, $method)) {
$customer->$method(sanitize_text_field($value));
}
}
}
// Update shipping address
if ($shipping) {
foreach ($shipping as $key => $value) {
$method = 'set_shipping_' . $key;
if (method_exists($customer, $method)) {
$customer->$method(sanitize_text_field($value));
}
}
}
$customer->save();
return new WP_REST_Response([
'message' => 'Addresses updated successfully',
], 200);
}
/**
* Get customer downloads
*/
public static function get_downloads(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$downloads = wc_get_customer_available_downloads($customer_id);
return new WP_REST_Response($downloads, 200);
}
/**
* Format order data for API response
*/
private static function format_order($order, $detailed = false) {
$data = [
'id' => $order->get_id(),
'order_number' => $order->get_order_number(),
'status' => $order->get_status(),
'date_created' => $order->get_date_created()->date('Y-m-d H:i:s'),
'total' => $order->get_total(),
'currency' => $order->get_currency(),
'payment_method' => $order->get_payment_method_title(),
];
if ($detailed) {
$data['items'] = array_map(function($item) {
$product = $item->get_product();
return [
'id' => $item->get_id(),
'name' => $item->get_name(),
'quantity' => $item->get_quantity(),
'total' => $item->get_total(),
'image' => $product ? wp_get_attachment_url($product->get_image_id()) : '',
];
}, $order->get_items());
$data['billing'] = $order->get_address('billing');
$data['shipping'] = $order->get_address('shipping');
$data['subtotal'] = $order->get_subtotal();
$data['shipping_total'] = $order->get_shipping_total();
$data['tax_total'] = $order->get_total_tax();
$data['discount_total'] = $order->get_discount_total();
}
return $data;
}
}

View File

@@ -0,0 +1,304 @@
<?php
namespace WooNooW\Frontend;
/**
* Frontend Assets Manager
* Handles loading of customer-spa assets
*/
class Assets {
/**
* Initialize
*/
public static function init() {
add_action('wp_enqueue_scripts', [self::class, 'enqueue_assets'], 20);
add_action('wp_head', [self::class, 'add_inline_config'], 5);
add_action('wp_enqueue_scripts', [self::class, 'dequeue_conflicting_scripts'], 100);
add_filter('script_loader_tag', [self::class, 'add_module_type'], 10, 3);
}
/**
* Add type="module" to customer-spa scripts
*/
public static function add_module_type($tag, $handle, $src) {
// Add type="module" to our Vite scripts
if (strpos($handle, 'woonoow-customer') !== false) {
$tag = str_replace('<script ', '<script type="module" ', $tag);
}
return $tag;
}
/**
* Enqueue customer-spa assets
*/
public static function enqueue_assets() {
// Only load on pages with WooNooW shortcodes or in full SPA mode
if (!self::should_load_assets()) {
return;
}
// Check if dev mode is enabled
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
if ($is_dev) {
// Dev mode: Load from Vite dev server
$dev_server = 'https://woonoow.local:5174';
// Vite client for HMR
wp_enqueue_script(
'woonoow-customer-vite',
$dev_server . '/@vite/client',
[],
null,
false // Load in header
);
// Main entry point
wp_enqueue_script(
'woonoow-customer-spa',
$dev_server . '/src/main.tsx',
['woonoow-customer-vite'],
null,
false // Load in header
);
error_log('WooNooW Customer: Loading from Vite dev server at ' . $dev_server);
error_log('WooNooW Customer: Scripts enqueued - vite client and main.tsx');
} else {
// Production mode: Load from build
$plugin_url = plugin_dir_url(dirname(dirname(__FILE__)));
$dist_path = plugin_dir_path(dirname(dirname(__FILE__))) . 'customer-spa/dist/';
// Check if build exists
if (!file_exists($dist_path)) {
error_log('WooNooW: customer-spa build not found. Run: cd customer-spa && npm run build');
return;
}
// Load manifest to get hashed filenames
$manifest_file = $dist_path . 'manifest.json';
if (file_exists($manifest_file)) {
$manifest = json_decode(file_get_contents($manifest_file), true);
// Enqueue main JS
if (isset($manifest['src/main.tsx'])) {
$main_js = $manifest['src/main.tsx']['file'];
wp_enqueue_script(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/' . $main_js,
[],
null,
true
);
}
// Enqueue main CSS
if (isset($manifest['src/main.tsx']['css'])) {
foreach ($manifest['src/main.tsx']['css'] as $css_file) {
wp_enqueue_style(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/' . $css_file,
[],
null
);
}
}
} else {
// Fallback for production build without manifest
wp_enqueue_script(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/app.js',
[],
null,
true
);
wp_enqueue_style(
'woonoow-customer-spa',
$plugin_url . 'customer-spa/dist/app.css',
[],
null
);
}
}
}
/**
* Add inline config and scripts to page head
*/
public static function add_inline_config() {
if (!self::should_load_assets()) {
return;
}
// Get Customer SPA settings
$spa_settings = get_option('woonoow_customer_spa_settings', []);
$default_settings = [
'mode' => 'disabled',
'layout' => 'modern',
'colors' => [
'primary' => '#3B82F6',
'secondary' => '#8B5CF6',
'accent' => '#10B981',
],
'typography' => [
'preset' => 'professional',
],
];
$theme_settings = array_replace_recursive($default_settings, $spa_settings);
// Get WooCommerce currency settings
$currency_settings = [
'code' => get_woocommerce_currency(),
'symbol' => get_woocommerce_currency_symbol(),
'position' => get_option('woocommerce_currency_pos', 'left'),
'thousandSeparator' => wc_get_price_thousand_separator(),
'decimalSeparator' => wc_get_price_decimal_separator(),
'decimals' => wc_get_price_decimals(),
];
$config = [
'apiUrl' => rest_url('woonoow/v1'),
'nonce' => wp_create_nonce('wp_rest'),
'siteUrl' => get_site_url(),
'siteTitle' => get_bloginfo('name'),
'user' => [
'isLoggedIn' => is_user_logged_in(),
'id' => get_current_user_id(),
],
'theme' => $theme_settings,
'currency' => $currency_settings,
];
?>
<script type="text/javascript">
window.woonoowCustomer = <?php echo wp_json_encode($config); ?>;
</script>
<?php
// If dev mode, output scripts directly
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
if ($is_dev) {
$dev_server = 'https://woonoow.local:5174';
?>
<script type="module">
import RefreshRuntime from '<?php echo $dev_server; ?>/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" crossorigin src="<?php echo $dev_server; ?>/@vite/client"></script>
<script type="module" crossorigin src="<?php echo $dev_server; ?>/src/main.tsx"></script>
<?php
error_log('WooNooW Customer: Scripts output directly in head with React Refresh preamble');
}
}
/**
* Check if we should load customer-spa assets
*/
private static function should_load_assets() {
global $post;
// Get Customer SPA settings
$spa_settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
// If disabled, don't load
if ($mode === 'disabled') {
// Still check for shortcodes
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_cart')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_checkout')) {
return true;
}
return false;
}
// Full SPA mode - load on all WooCommerce pages
if ($mode === 'full') {
if (function_exists('is_shop') && is_shop()) {
return true;
}
if (function_exists('is_product') && is_product()) {
return true;
}
if (function_exists('is_cart') && is_cart()) {
return true;
}
if (function_exists('is_checkout') && is_checkout()) {
return true;
}
if (function_exists('is_account_page') && is_account_page()) {
return true;
}
return false;
}
// Checkout-Only mode - load only on specific pages
if ($mode === 'checkout_only') {
$checkout_pages = isset($spa_settings['checkoutPages']) ? $spa_settings['checkoutPages'] : [];
if (!empty($checkout_pages['checkout']) && function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
return true;
}
if (!empty($checkout_pages['thankyou']) && function_exists('is_order_received_page') && is_order_received_page()) {
return true;
}
if (!empty($checkout_pages['account']) && function_exists('is_account_page') && is_account_page()) {
return true;
}
if (!empty($checkout_pages['cart']) && function_exists('is_cart') && is_cart()) {
return true;
}
return false;
}
// Check if current page has WooNooW shortcodes
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_cart')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_checkout')) {
return true;
}
if ($post && has_shortcode($post->post_content, 'woonoow_account')) {
return true;
}
return false;
}
/**
* Dequeue conflicting scripts when SPA is active
*/
public static function dequeue_conflicting_scripts() {
if (!self::should_load_assets()) {
return;
}
// Dequeue WooCommerce scripts that conflict with SPA
wp_dequeue_script('wc-cart-fragments');
wp_dequeue_script('woocommerce');
wp_dequeue_script('wc-add-to-cart');
wp_dequeue_script('wc-add-to-cart-variation');
// Dequeue WordPress block scripts that cause errors in SPA
wp_dequeue_script('wp-block-library');
wp_dequeue_script('wp-block-navigation');
wp_dequeue_script('wp-interactivity');
wp_dequeue_script('wp-interactivity-router');
// Keep only essential WooCommerce styles, dequeue others if needed
// wp_dequeue_style('woocommerce-general');
// wp_dequeue_style('woocommerce-layout');
// wp_dequeue_style('woocommerce-smallscreen');
}
}

View File

@@ -0,0 +1,306 @@
<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Cart Controller - Customer-facing cart API
* Handles cart operations for customer-spa
*/
class CartController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get cart
register_rest_route($namespace, '/cart', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_cart'],
'permission_callback' => '__return_true',
]);
// Add to cart
register_rest_route($namespace, '/cart/add', [
'methods' => 'POST',
'callback' => [__CLASS__, 'add_to_cart'],
'permission_callback' => '__return_true',
'args' => [
'product_id' => [
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param);
},
],
'quantity' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'variation_id' => [
'default' => 0,
'sanitize_callback' => 'absint',
],
],
]);
// Update cart item
register_rest_route($namespace, '/cart/update', [
'methods' => 'POST',
'callback' => [__CLASS__, 'update_cart'],
'permission_callback' => '__return_true',
'args' => [
'cart_item_key' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'quantity' => [
'required' => true,
'sanitize_callback' => 'absint',
],
],
]);
// Remove from cart
register_rest_route($namespace, '/cart/remove', [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_from_cart'],
'permission_callback' => '__return_true',
'args' => [
'cart_item_key' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Apply coupon
register_rest_route($namespace, '/cart/apply-coupon', [
'methods' => 'POST',
'callback' => [__CLASS__, 'apply_coupon'],
'permission_callback' => '__return_true',
'args' => [
'coupon_code' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Remove coupon
register_rest_route($namespace, '/cart/remove-coupon', [
'methods' => 'POST',
'callback' => [__CLASS__, 'remove_coupon'],
'permission_callback' => '__return_true',
'args' => [
'coupon_code' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
}
/**
* Get cart contents
*/
public static function get_cart(WP_REST_Request $request) {
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
return new WP_REST_Response(self::format_cart(), 200);
}
/**
* Add item to cart
*/
public static function add_to_cart(WP_REST_Request $request) {
$product_id = $request->get_param('product_id');
$quantity = $request->get_param('quantity');
$variation_id = $request->get_param('variation_id');
// Initialize WooCommerce session for guest users
if (!WC()->session->has_session()) {
WC()->session->set_customer_session_cookie(true);
}
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Validate product
$product = wc_get_product($product_id);
if (!$product) {
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
}
// Add to cart
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity, $variation_id);
if (!$cart_item_key) {
return new WP_Error('add_to_cart_failed', 'Failed to add product to cart', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Product added to cart',
'cart_item_key' => $cart_item_key,
'cart' => self::format_cart(),
], 200);
}
/**
* Update cart item quantity
*/
public static function update_cart(WP_REST_Request $request) {
$cart_item_key = $request->get_param('cart_item_key');
$quantity = $request->get_param('quantity');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Update quantity
$updated = WC()->cart->set_quantity($cart_item_key, $quantity);
if (!$updated) {
return new WP_Error('update_failed', 'Failed to update cart item', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Cart updated',
'cart' => self::format_cart(),
], 200);
}
/**
* Remove item from cart
*/
public static function remove_from_cart(WP_REST_Request $request) {
$cart_item_key = $request->get_param('cart_item_key');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Remove item
$removed = WC()->cart->remove_cart_item($cart_item_key);
if (!$removed) {
return new WP_Error('remove_failed', 'Failed to remove cart item', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Item removed from cart',
'cart' => self::format_cart(),
], 200);
}
/**
* Apply coupon to cart
*/
public static function apply_coupon(WP_REST_Request $request) {
$coupon_code = $request->get_param('coupon_code');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Apply coupon
$applied = WC()->cart->apply_coupon($coupon_code);
if (!$applied) {
return new WP_Error('coupon_failed', 'Failed to apply coupon', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Coupon applied',
'cart' => self::format_cart(),
], 200);
}
/**
* Remove coupon from cart
*/
public static function remove_coupon(WP_REST_Request $request) {
$coupon_code = $request->get_param('coupon_code');
if (!WC()->cart) {
return new WP_Error('cart_error', 'Cart not initialized', ['status' => 500]);
}
// Remove coupon
$removed = WC()->cart->remove_coupon($coupon_code);
if (!$removed) {
return new WP_Error('remove_coupon_failed', 'Failed to remove coupon', ['status' => 400]);
}
return new WP_REST_Response([
'message' => 'Coupon removed',
'cart' => self::format_cart(),
], 200);
}
/**
* Format cart data for API response
*/
private static function format_cart() {
$cart = WC()->cart;
if (!$cart) {
return null;
}
$items = [];
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
$product = $cart_item['data'];
$items[] = [
'key' => $cart_item_key,
'product_id' => $cart_item['product_id'],
'variation_id' => $cart_item['variation_id'] ?? 0,
'quantity' => $cart_item['quantity'],
'name' => $product->get_name(),
'price' => $product->get_price(),
'subtotal' => $cart_item['line_subtotal'],
'total' => $cart_item['line_total'],
'image' => wp_get_attachment_url($product->get_image_id()),
'permalink' => get_permalink($cart_item['product_id']),
'attributes' => $cart_item['variation'] ?? [],
];
}
// Get applied coupons
$coupons = [];
foreach ($cart->get_applied_coupons() as $coupon_code) {
$coupon = new \WC_Coupon($coupon_code);
$coupons[] = [
'code' => $coupon_code,
'discount' => $cart->get_coupon_discount_amount($coupon_code),
'type' => $coupon->get_discount_type(),
];
}
return [
'items' => $items,
'subtotal' => $cart->get_subtotal(),
'subtotal_tax' => $cart->get_subtotal_tax(),
'discount_total' => $cart->get_discount_total(),
'discount_tax' => $cart->get_discount_tax(),
'shipping_total' => $cart->get_shipping_total(),
'shipping_tax' => $cart->get_shipping_tax(),
'cart_contents_tax' => $cart->get_cart_contents_tax(),
'fee_total' => $cart->get_fee_total(),
'fee_tax' => $cart->get_fee_tax(),
'total' => $cart->get_total('edit'),
'total_tax' => $cart->get_total_tax(),
'coupons' => $coupons,
'needs_shipping' => $cart->needs_shipping(),
'needs_payment' => $cart->needs_payment(),
];
}
}

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,
]);
}
}

View File

@@ -0,0 +1,347 @@
<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Shop Controller - Customer-facing product catalog API
* Handles product listing, search, and categories for customer-spa
*/
class ShopController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get products (public)
register_rest_route($namespace, '/shop/products', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_products'],
'permission_callback' => '__return_true',
'args' => [
'page' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'per_page' => [
'default' => 12,
'sanitize_callback' => 'absint',
],
'category' => [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
'search' => [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
'orderby' => [
'default' => 'date',
'sanitize_callback' => 'sanitize_text_field',
],
'order' => [
'default' => 'DESC',
'sanitize_callback' => 'sanitize_text_field',
],
'slug' => [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Get single product (public)
register_rest_route($namespace, '/shop/products/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_product'],
'permission_callback' => '__return_true',
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
},
],
],
]);
// Get categories (public)
register_rest_route($namespace, '/shop/categories', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_categories'],
'permission_callback' => '__return_true',
]);
// Search products (public)
register_rest_route($namespace, '/shop/search', [
'methods' => 'GET',
'callback' => [__CLASS__, 'search_products'],
'permission_callback' => '__return_true',
'args' => [
's' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
}
/**
* Get products list
*/
public static function get_products(WP_REST_Request $request) {
$page = $request->get_param('page');
$per_page = $request->get_param('per_page');
$category = $request->get_param('category');
$search = $request->get_param('search');
$orderby = $request->get_param('orderby');
$order = $request->get_param('order');
$slug = $request->get_param('slug');
$args = [
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => $orderby,
'order' => $order,
];
// Add slug filter (for single product lookup)
if (!empty($slug)) {
$args['name'] = $slug;
}
// Add category filter
if (!empty($category)) {
$args['tax_query'] = [
[
'taxonomy' => 'product_cat',
'field' => 'slug',
'terms' => $category,
],
];
}
// Add search
if (!empty($search)) {
$args['s'] = $search;
}
$query = new \WP_Query($args);
// Check if this is a single product request (by slug)
$is_single = !empty($slug);
$products = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$product = wc_get_product(get_the_ID());
if ($product) {
// Return detailed data for single product requests
$products[] = self::format_product($product, $is_single);
}
}
wp_reset_postdata();
}
return new WP_REST_Response([
'products' => $products,
'total' => $query->found_posts,
'total_pages' => $query->max_num_pages,
'page' => $page,
'per_page' => $per_page,
], 200);
}
/**
* Get single product
*/
public static function get_product(WP_REST_Request $request) {
$product_id = $request->get_param('id');
$product = wc_get_product($product_id);
if (!$product) {
return new WP_Error('product_not_found', 'Product not found', ['status' => 404]);
}
return new WP_REST_Response(self::format_product($product, true), 200);
}
/**
* Get categories
*/
public static function get_categories(WP_REST_Request $request) {
$terms = get_terms([
'taxonomy' => 'product_cat',
'hide_empty' => true,
]);
if (is_wp_error($terms)) {
return new WP_Error('categories_error', 'Failed to get categories', ['status' => 500]);
}
$categories = [];
foreach ($terms as $term) {
$thumbnail_id = get_term_meta($term->term_id, 'thumbnail_id', true);
$categories[] = [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'count' => $term->count,
'image' => $thumbnail_id ? wp_get_attachment_url($thumbnail_id) : '',
];
}
return new WP_REST_Response($categories, 200);
}
/**
* Search products
*/
public static function search_products(WP_REST_Request $request) {
$search = $request->get_param('s');
$args = [
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => 10,
's' => $search,
];
$query = new \WP_Query($args);
$products = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$product = wc_get_product(get_the_ID());
if ($product) {
$products[] = self::format_product($product);
}
}
wp_reset_postdata();
}
return new WP_REST_Response($products, 200);
}
/**
* Format product data for API response
*/
private static function format_product($product, $detailed = false) {
$data = [
'id' => $product->get_id(),
'name' => $product->get_name(),
'slug' => $product->get_slug(),
'price' => $product->get_price(),
'regular_price' => $product->get_regular_price(),
'sale_price' => $product->get_sale_price(),
'price_html' => $product->get_price_html(),
'on_sale' => $product->is_on_sale(),
'in_stock' => $product->is_in_stock(),
'stock_status' => $product->get_stock_status(),
'stock_quantity' => $product->get_stock_quantity(),
'type' => $product->get_type(),
'image' => wp_get_attachment_url($product->get_image_id()),
'permalink' => get_permalink($product->get_id()),
];
// Add detailed info if requested
if ($detailed) {
$data['description'] = $product->get_description();
$data['short_description'] = $product->get_short_description();
$data['sku'] = $product->get_sku();
$data['categories'] = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'all']);
$data['tags'] = wp_get_post_terms($product->get_id(), 'product_tag', ['fields' => 'names']);
// Gallery images
$gallery_ids = $product->get_gallery_image_ids();
$data['gallery'] = array_map('wp_get_attachment_url', $gallery_ids);
// Images array (featured + gallery) for frontend
$images = [];
if ($data['image']) {
$images[] = $data['image'];
}
$images = array_merge($images, $data['gallery']);
$data['images'] = $images;
// Attributes and Variations for variable products
if ($product->is_type('variable')) {
$data['attributes'] = self::get_product_attributes($product);
$data['variations'] = self::get_product_variations($product);
}
// Related products
$related_ids = wc_get_related_products($product->get_id(), 4);
$data['related_products'] = array_map(function($id) {
$related = wc_get_product($id);
return $related ? self::format_product($related) : null;
}, $related_ids);
$data['related_products'] = array_filter($data['related_products']);
}
return $data;
}
/**
* Get product attributes
*/
private static function get_product_attributes($product) {
$attributes = [];
foreach ($product->get_attributes() as $attribute) {
$attribute_data = [
'name' => wc_attribute_label($attribute->get_name()),
'options' => [],
'visible' => $attribute->get_visible(),
'variation' => $attribute->get_variation(),
];
// Get attribute options
if ($attribute->is_taxonomy()) {
$terms = wc_get_product_terms($product->get_id(), $attribute->get_name(), ['fields' => 'names']);
$attribute_data['options'] = $terms;
} else {
$attribute_data['options'] = $attribute->get_options();
}
$attributes[] = $attribute_data;
}
return $attributes;
}
/**
* Get product variations
*/
private static function get_product_variations($product) {
$variations = [];
foreach ($product->get_available_variations() as $variation) {
$variation_obj = wc_get_product($variation['variation_id']);
if ($variation_obj) {
$variations[] = [
'id' => $variation['variation_id'],
'attributes' => $variation['attributes'],
'price' => $variation_obj->get_price(),
'regular_price' => $variation_obj->get_regular_price(),
'sale_price' => $variation_obj->get_sale_price(),
'in_stock' => $variation_obj->is_in_stock(),
'stock_quantity' => $variation_obj->get_stock_quantity(),
'image' => wp_get_attachment_url($variation_obj->get_image_id()),
];
}
}
return $variations;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace WooNooW\Frontend;
/**
* Shortcodes Manager
* Handles WooNooW customer-facing shortcodes
*/
class Shortcodes {
/**
* Initialize
*/
public static function init() {
add_shortcode('woonoow_shop', [__CLASS__, 'shop_shortcode']);
add_shortcode('woonoow_cart', [__CLASS__, 'cart_shortcode']);
add_shortcode('woonoow_checkout', [__CLASS__, 'checkout_shortcode']);
add_shortcode('woonoow_account', [__CLASS__, 'account_shortcode']);
}
/**
* Shop shortcode
* Usage: [woonoow_shop]
*/
public static function shop_shortcode($atts) {
$atts = shortcode_atts([
'category' => '',
'per_page' => 12,
], $atts);
ob_start();
?>
<div id="woonoow-customer-app" data-page="shop" data-category="<?php echo esc_attr($atts['category']); ?>" data-per-page="<?php echo esc_attr($atts['per_page']); ?>">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading shop...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Cart shortcode
* Usage: [woonoow_cart]
*/
public static function cart_shortcode($atts) {
ob_start();
?>
<div id="woonoow-customer-app" data-page="cart">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading cart...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Checkout shortcode
* Usage: [woonoow_checkout]
*/
public static function checkout_shortcode($atts) {
// Require user to be logged in for checkout
if (!is_user_logged_in()) {
return '<div class="woonoow-notice">' .
'<p>' . esc_html__('Please log in to proceed to checkout.', 'woonoow') . '</p>' .
'<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="button">' .
esc_html__('Log In', 'woonoow') . '</a>' .
'</div>';
}
ob_start();
?>
<div id="woonoow-customer-app" data-page="checkout">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading checkout...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Account shortcode
* Usage: [woonoow_account]
*/
public static function account_shortcode($atts) {
// Require user to be logged in
if (!is_user_logged_in()) {
return '<div class="woonoow-notice">' .
'<p>' . esc_html__('Please log in to view your account.', 'woonoow') . '</p>' .
'<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="button">' .
esc_html__('Log In', 'woonoow') . '</a>' .
'</div>';
}
ob_start();
?>
<div id="woonoow-customer-app" data-page="account">
<!-- Customer SPA will mount here -->
<div class="woonoow-loading">
<p><?php esc_html_e('Loading account...', 'woonoow'); ?></p>
</div>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace WooNooW\Frontend;
/**
* Template Override
* Overrides WooCommerce templates to use WooNooW SPA
*/
class TemplateOverride {
/**
* Initialize
*/
public static function init() {
// Use blank template for full-page SPA
add_filter('template_include', [__CLASS__, 'use_spa_template'], 999);
// Disable canonical redirects for SPA routes
add_filter('redirect_canonical', [__CLASS__, 'disable_canonical_redirect'], 10, 2);
// Override WooCommerce shop page
add_filter('woocommerce_show_page_title', '__return_false');
// Replace WooCommerce content with our SPA
add_action('woocommerce_before_main_content', [__CLASS__, 'start_spa_wrapper'], 5);
add_action('woocommerce_after_main_content', [__CLASS__, 'end_spa_wrapper'], 999);
// Remove WooCommerce default content
remove_action('woocommerce_before_shop_loop', 'woocommerce_result_count', 20);
remove_action('woocommerce_before_shop_loop', 'woocommerce_catalog_ordering', 30);
remove_action('woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10);
remove_action('woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10);
// Override single product template
add_filter('woocommerce_locate_template', [__CLASS__, 'override_template'], 10, 3);
}
/**
* Disable canonical redirects for SPA routes
* This prevents WordPress from redirecting /product/slug URLs
*/
public static function disable_canonical_redirect($redirect_url, $requested_url) {
$settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
// Only disable redirects in full SPA mode
if ($mode !== 'full') {
return $redirect_url;
}
// Check if this is a SPA route
$spa_routes = ['/product/', '/cart', '/checkout', '/my-account'];
foreach ($spa_routes as $route) {
if (strpos($requested_url, $route) !== false) {
// This is a SPA route, disable WordPress redirect
return false;
}
}
return $redirect_url;
}
/**
* Use SPA template (blank page)
*/
public static function use_spa_template($template) {
$settings = get_option('woonoow_customer_spa_settings', []);
$mode = isset($settings['mode']) ? $settings['mode'] : 'disabled';
// Mode 1: Disabled
if ($mode === 'disabled') {
return $template;
}
// Check if current URL is a SPA route (for direct access)
$request_uri = $_SERVER['REQUEST_URI'];
$spa_routes = ['/shop', '/product/', '/cart', '/checkout', '/my-account'];
$is_spa_route = false;
foreach ($spa_routes as $route) {
if (strpos($request_uri, $route) !== false) {
$is_spa_route = true;
break;
}
}
// If it's a SPA route in full mode, use SPA template
if ($mode === 'full' && $is_spa_route) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
// Set status to 200 to prevent 404
status_header(200);
return $spa_template;
}
}
// Mode 3: Checkout-Only (partial SPA)
if ($mode === 'checkout_only') {
$checkout_pages = isset($settings['checkoutPages']) ? $settings['checkoutPages'] : [
'checkout' => true,
'thankyou' => true,
'account' => true,
'cart' => false,
];
$should_override = false;
if (!empty($checkout_pages['checkout']) && is_checkout() && !is_order_received_page()) {
$should_override = true;
}
if (!empty($checkout_pages['thankyou']) && is_order_received_page()) {
$should_override = true;
}
if (!empty($checkout_pages['account']) && is_account_page()) {
$should_override = true;
}
if (!empty($checkout_pages['cart']) && is_cart()) {
$should_override = true;
}
if ($should_override) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
return $spa_template;
}
}
return $template;
}
// Mode 2: Full SPA
if ($mode === 'full') {
// Override all WooCommerce pages
if (is_woocommerce() || is_product() || is_cart() || is_checkout() || is_account_page()) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
return $spa_template;
}
}
}
return $template;
}
/**
* Start SPA wrapper
*/
public static function start_spa_wrapper() {
// Check if we should use SPA
if (!self::should_use_spa()) {
return;
}
// Determine page type
$page_type = 'shop';
$data_attrs = 'data-page="shop"';
if (is_product()) {
$page_type = 'product';
global $post;
$data_attrs = 'data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
} elseif (is_cart()) {
$page_type = 'cart';
$data_attrs = 'data-page="cart"';
} elseif (is_checkout()) {
$page_type = 'checkout';
$data_attrs = 'data-page="checkout"';
} elseif (is_account_page()) {
$page_type = 'account';
$data_attrs = 'data-page="account"';
}
// Output SPA mount point
echo '<div id="woonoow-customer-app" ' . $data_attrs . '>';
echo '<div class="woonoow-loading">';
echo '<p>' . esc_html__('Loading...', 'woonoow') . '</p>';
echo '</div>';
echo '</div>';
// Hide WooCommerce content
echo '<div style="display: none;">';
}
/**
* End SPA wrapper
*/
public static function end_spa_wrapper() {
if (!self::should_use_spa()) {
return;
}
// Close hidden wrapper
echo '</div>';
}
/**
* Check if we should use SPA
*/
private static function should_use_spa() {
// Check if frontend mode is enabled
$mode = get_option('woonoow_frontend_mode', 'shortcodes');
if ($mode === 'disabled') {
return false;
}
// For full SPA mode, always use SPA
if ($mode === 'full_spa') {
return true;
}
// For shortcode mode, check if we're on WooCommerce pages
if (is_shop() || is_product() || is_cart() || is_checkout() || is_account_page()) {
return true;
}
return false;
}
/**
* Override WooCommerce templates
*/
public static function override_template($template, $template_name, $template_path) {
// Only override if SPA is enabled
if (!self::should_use_spa()) {
return $template;
}
// Templates to override
$override_templates = [
'archive-product.php',
'single-product.php',
'cart/cart.php',
'checkout/form-checkout.php',
];
// Check if this template should be overridden
foreach ($override_templates as $override) {
if (strpos($template_name, $override) !== false) {
// Return empty template (SPA will handle rendering)
return plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-wrapper.php';
}
}
return $template;
}
}