fix: WP-Admin CSS conflicts and add-to-cart redirect
- Fix CSS conflicts between WP-Admin and SPA (radio buttons, chart text) - Add Tailwind important selector scoped to #woonoow-admin-app - Remove overly aggressive inline SVG styles from Assets.php - Add targeted WordPress admin CSS overrides in index.css - Fix add-to-cart redirect to use woocommerce_add_to_cart_redirect filter - Let WooCommerce handle cart operations natively for proper session management - Remove duplicate tailwind.config.cjs
This commit is contained in:
@@ -6,7 +6,6 @@ import { Toaster } from 'sonner';
|
||||
// Theme
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { BaseLayout } from './layouts/BaseLayout';
|
||||
import { useAddToCartFromUrl } from './hooks/useAddToCartFromUrl';
|
||||
|
||||
// Pages
|
||||
import Shop from './pages/Shop';
|
||||
@@ -52,55 +51,59 @@ const getAppearanceSettings = () => {
|
||||
return (window as any).woonoowCustomer?.appearanceSettings || {};
|
||||
};
|
||||
|
||||
// Get initial route from data attribute (set by PHP based on SPA mode)
|
||||
const getInitialRoute = () => {
|
||||
const appEl = document.getElementById('woonoow-customer-app');
|
||||
const initialRoute = appEl?.getAttribute('data-initial-route');
|
||||
console.log('[WooNooW Customer] Initial route from data attribute:', initialRoute);
|
||||
console.log('[WooNooW Customer] App element:', appEl);
|
||||
console.log('[WooNooW Customer] All data attributes:', appEl?.dataset);
|
||||
return initialRoute || '/shop'; // Default to shop if not specified
|
||||
};
|
||||
|
||||
// Router wrapper component that uses hooks requiring Router context
|
||||
function AppRoutes() {
|
||||
const initialRoute = getInitialRoute();
|
||||
console.log('[WooNooW Customer] Using initial route:', initialRoute);
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Routes>
|
||||
{/* Root route redirects to initial route based on SPA mode */}
|
||||
<Route path="/" element={<Navigate to={initialRoute} replace />} />
|
||||
|
||||
{/* Shop Routes */}
|
||||
<Route path="/shop" element={<Shop />} />
|
||||
<Route path="/product/:slug" element={<Product />} />
|
||||
|
||||
{/* Cart & Checkout */}
|
||||
<Route path="/cart" element={<Cart />} />
|
||||
<Route path="/checkout" element={<Checkout />} />
|
||||
<Route path="/order-received/:orderId" element={<ThankYou />} />
|
||||
|
||||
{/* Wishlist - Public route accessible to guests */}
|
||||
<Route path="/wishlist" element={<Wishlist />} />
|
||||
|
||||
{/* My Account */}
|
||||
<Route path="/my-account/*" element={<Account />} />
|
||||
|
||||
{/* Fallback to initial route */}
|
||||
<Route path="*" element={<Navigate to={initialRoute} replace />} />
|
||||
</Routes>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const themeConfig = getThemeConfig();
|
||||
const appearanceSettings = getAppearanceSettings();
|
||||
const toastPosition = (appearanceSettings?.general?.toast_position || 'top-right') as any;
|
||||
|
||||
// Handle add-to-cart from URL parameters
|
||||
useAddToCartFromUrl();
|
||||
|
||||
// Get initial route from data attribute (set by PHP based on SPA mode)
|
||||
const getInitialRoute = () => {
|
||||
const appEl = document.getElementById('woonoow-customer-app');
|
||||
const initialRoute = appEl?.getAttribute('data-initial-route');
|
||||
console.log('[WooNooW Customer] Initial route from data attribute:', initialRoute);
|
||||
console.log('[WooNooW Customer] App element:', appEl);
|
||||
console.log('[WooNooW Customer] All data attributes:', appEl?.dataset);
|
||||
return initialRoute || '/shop'; // Default to shop if not specified
|
||||
};
|
||||
|
||||
const initialRoute = getInitialRoute();
|
||||
console.log('[WooNooW Customer] Using initial route:', initialRoute);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider config={themeConfig}>
|
||||
<HashRouter>
|
||||
<BaseLayout>
|
||||
<Routes>
|
||||
{/* Root route redirects to initial route based on SPA mode */}
|
||||
<Route path="/" element={<Navigate to={initialRoute} replace />} />
|
||||
|
||||
{/* Shop Routes */}
|
||||
<Route path="/shop" element={<Shop />} />
|
||||
<Route path="/product/:slug" element={<Product />} />
|
||||
|
||||
{/* Cart & Checkout */}
|
||||
<Route path="/cart" element={<Cart />} />
|
||||
<Route path="/checkout" element={<Checkout />} />
|
||||
<Route path="/order-received/:orderId" element={<ThankYou />} />
|
||||
|
||||
{/* Wishlist - Public route accessible to guests */}
|
||||
<Route path="/wishlist" element={<Wishlist />} />
|
||||
|
||||
{/* My Account */}
|
||||
<Route path="/my-account/*" element={<Account />} />
|
||||
|
||||
{/* Fallback to initial route */}
|
||||
<Route path="*" element={<Navigate to={initialRoute} replace />} />
|
||||
</Routes>
|
||||
</BaseLayout>
|
||||
<AppRoutes />
|
||||
</HashRouter>
|
||||
|
||||
{/* Toast notifications - position from settings */}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { useCartStore } from '@/lib/cart/store';
|
||||
|
||||
/**
|
||||
* Hook to handle add-to-cart from URL parameters
|
||||
@@ -10,51 +11,81 @@ import { toast } from 'sonner';
|
||||
* - Simple product: ?add-to-cart=123
|
||||
* - Variable product: ?add-to-cart=123&variation_id=456
|
||||
* - With quantity: ?add-to-cart=123&quantity=2
|
||||
* - Direct to checkout: ?add-to-cart=123&redirect=checkout
|
||||
* - Stay on cart (default): ?add-to-cart=123&redirect=cart
|
||||
*/
|
||||
export function useAddToCartFromUrl() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { setCart } = useCartStore();
|
||||
const processedRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const productId = params.get('add-to-cart');
|
||||
// Check hash route for add-to-cart parameters
|
||||
const hash = window.location.hash;
|
||||
const hashParams = new URLSearchParams(hash.split('?')[1] || '');
|
||||
const productId = hashParams.get('add-to-cart');
|
||||
|
||||
if (!productId) return;
|
||||
|
||||
const variationId = params.get('variation_id');
|
||||
const quantity = parseInt(params.get('quantity') || '1', 10);
|
||||
const variationId = hashParams.get('variation_id');
|
||||
const quantity = parseInt(hashParams.get('quantity') || '1', 10);
|
||||
const redirect = hashParams.get('redirect') || 'cart';
|
||||
|
||||
// Create unique key for this add-to-cart request
|
||||
const requestKey = `${productId}-${variationId || 'none'}-${quantity}`;
|
||||
|
||||
// Skip if already processed
|
||||
if (processedRef.current.has(requestKey)) {
|
||||
console.log('[WooNooW] Skipping duplicate add-to-cart:', requestKey);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[WooNooW] Add to cart from URL:', {
|
||||
productId,
|
||||
variationId,
|
||||
quantity,
|
||||
redirect,
|
||||
fullUrl: window.location.href,
|
||||
requestKey,
|
||||
});
|
||||
|
||||
// Add product to cart
|
||||
// Mark as processed
|
||||
processedRef.current.add(requestKey);
|
||||
|
||||
addToCart(productId, variationId, quantity)
|
||||
.then(() => {
|
||||
// Remove URL parameters after adding to cart
|
||||
const cleanUrl = window.location.pathname + window.location.hash;
|
||||
window.history.replaceState({}, '', cleanUrl);
|
||||
.then((cartData) => {
|
||||
// Update cart store with fresh data from API
|
||||
if (cartData) {
|
||||
setCart(cartData);
|
||||
console.log('[WooNooW] Cart updated with fresh data:', cartData);
|
||||
}
|
||||
|
||||
// Navigate to cart if not already there
|
||||
if (!location.pathname.includes('/cart')) {
|
||||
navigate('/cart');
|
||||
// Remove URL parameters after adding to cart
|
||||
const currentPath = window.location.hash.split('?')[0];
|
||||
window.location.hash = currentPath;
|
||||
|
||||
// Navigate based on redirect parameter
|
||||
const targetPage = redirect === 'checkout' ? '/checkout' : '/cart';
|
||||
if (!location.pathname.includes(targetPage)) {
|
||||
console.log(`[WooNooW] Navigating to ${targetPage}`);
|
||||
navigate(targetPage);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[WooNooW] Failed to add product to cart:', error);
|
||||
toast.error('Failed to add product to cart');
|
||||
// Remove from processed set on error so it can be retried
|
||||
processedRef.current.delete(requestKey);
|
||||
});
|
||||
}, []); // Run once on mount
|
||||
}, [location.hash, navigate, setCart]); // Include all dependencies
|
||||
}
|
||||
|
||||
async function addToCart(
|
||||
productId: string,
|
||||
variationId: string | null,
|
||||
quantity: number
|
||||
): Promise<void> {
|
||||
): Promise<any> {
|
||||
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
|
||||
const nonce = (window as any).woonoowCustomer?.nonce || '';
|
||||
|
||||
@@ -85,11 +116,13 @@ async function addToCart(
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[WooNooW] Product added to cart:', data);
|
||||
|
||||
if (!data.success) {
|
||||
// API returns {message, cart_item_key, cart} on success
|
||||
if (data.cart_item_key && data.cart) {
|
||||
toast.success(data.message || 'Product added to cart');
|
||||
return data.cart; // Return cart data to update store
|
||||
} else {
|
||||
throw new Error(data.message || 'Failed to add to cart');
|
||||
}
|
||||
|
||||
console.log('[WooNooW] Product added to cart:', data);
|
||||
toast.success('Product added to cart');
|
||||
}
|
||||
|
||||
111
customer-spa/src/lib/cart/api.ts
Normal file
111
customer-spa/src/lib/cart/api.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Cart } from './store';
|
||||
|
||||
const getApiConfig = () => {
|
||||
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
|
||||
const nonce = (window as any).woonoowCustomer?.nonce || '';
|
||||
return { apiRoot, nonce };
|
||||
};
|
||||
|
||||
/**
|
||||
* Update cart item quantity via API
|
||||
*/
|
||||
export async function updateCartItemQuantity(
|
||||
cartItemKey: string,
|
||||
quantity: number
|
||||
): Promise<Cart> {
|
||||
const { apiRoot, nonce } = getApiConfig();
|
||||
|
||||
const response = await fetch(`${apiRoot}/cart/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': nonce,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
cart_item_key: cartItemKey,
|
||||
quantity,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || 'Failed to update cart');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from cart via API
|
||||
*/
|
||||
export async function removeCartItem(cartItemKey: string): Promise<Cart> {
|
||||
const { apiRoot, nonce } = getApiConfig();
|
||||
|
||||
const response = await fetch(`${apiRoot}/cart/remove`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': nonce,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
cart_item_key: cartItemKey,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || 'Failed to remove item');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear entire cart via API
|
||||
*/
|
||||
export async function clearCartAPI(): Promise<Cart> {
|
||||
const { apiRoot, nonce } = getApiConfig();
|
||||
|
||||
const response = await fetch(`${apiRoot}/cart/clear`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': nonce,
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || 'Failed to clear cart');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch current cart from API
|
||||
*/
|
||||
export async function fetchCart(): Promise<Cart> {
|
||||
const { apiRoot, nonce } = getApiConfig();
|
||||
|
||||
const response = await fetch(`${apiRoot}/cart`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-WP-Nonce': nonce,
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch cart');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useCartStore, type CartItem } from '@/lib/cart/store';
|
||||
import { useCartSettings } from '@/hooks/useAppearanceSettings';
|
||||
import { updateCartItemQuantity, removeCartItem, clearCartAPI, fetchCart } from '@/lib/cart/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -13,37 +14,96 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import Container from '@/components/Layout/Container';
|
||||
import { formatPrice } from '@/lib/currency';
|
||||
import { Trash2, Plus, Minus, ShoppingBag, ArrowLeft } from 'lucide-react';
|
||||
import { Trash2, Plus, Minus, ShoppingBag, ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Cart() {
|
||||
const navigate = useNavigate();
|
||||
const { cart, removeItem, updateQuantity, clearCart } = useCartStore();
|
||||
const { cart, setCart } = useCartStore();
|
||||
const { layout, elements } = useCartSettings();
|
||||
const [showClearDialog, setShowClearDialog] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Fetch cart from server on mount to sync with WooCommerce
|
||||
useEffect(() => {
|
||||
const loadCart = async () => {
|
||||
try {
|
||||
const serverCart = await fetchCart();
|
||||
setCart(serverCart);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch cart:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadCart();
|
||||
}, [setCart]);
|
||||
|
||||
// Calculate total from items
|
||||
const total = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
|
||||
const handleUpdateQuantity = (key: string, newQuantity: number) => {
|
||||
const handleUpdateQuantity = async (key: string, newQuantity: number) => {
|
||||
if (newQuantity < 1) {
|
||||
handleRemoveItem(key);
|
||||
return;
|
||||
}
|
||||
updateQuantity(key, newQuantity);
|
||||
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const updatedCart = await updateCartItemQuantity(key, newQuantity);
|
||||
setCart(updatedCart);
|
||||
} catch (error) {
|
||||
console.error('Failed to update quantity:', error);
|
||||
toast.error('Failed to update quantity');
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveItem = (key: string) => {
|
||||
removeItem(key);
|
||||
toast.success('Item removed from cart');
|
||||
const handleRemoveItem = async (key: string) => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const updatedCart = await removeCartItem(key);
|
||||
setCart(updatedCart);
|
||||
toast.success('Item removed from cart');
|
||||
} catch (error) {
|
||||
console.error('Failed to remove item:', error);
|
||||
toast.error('Failed to remove item');
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCart = () => {
|
||||
clearCart();
|
||||
setShowClearDialog(false);
|
||||
toast.success('Cart cleared');
|
||||
const handleClearCart = async () => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const updatedCart = await clearCartAPI();
|
||||
setCart(updatedCart);
|
||||
setShowClearDialog(false);
|
||||
toast.success('Cart cleared');
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cart:', error);
|
||||
toast.error('Failed to clear cart');
|
||||
setShowClearDialog(false);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading state while fetching cart
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Container>
|
||||
<div className="text-center py-16">
|
||||
<Loader2 className="mx-auto h-16 w-16 text-gray-400 mb-4 animate-spin" />
|
||||
<p className="text-gray-600">Loading cart...</p>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (cart.items.length === 0) {
|
||||
return (
|
||||
<Container>
|
||||
|
||||
Reference in New Issue
Block a user