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:
Dwindi Ramadhana
2025-12-31 14:06:04 +07:00
parent 93523a74ac
commit 82399d4ddf
20 changed files with 1272 additions and 571 deletions

View File

@@ -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 */}

View File

@@ -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');
}

View 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;
}

View File

@@ -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>