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

@@ -1,10 +1,209 @@
import React from 'react';
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useCartStore, type CartItem } from '@/lib/cart/store';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} 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 { toast } from 'sonner';
export default function Cart() {
const navigate = useNavigate();
const { cart, removeItem, updateQuantity, clearCart } = useCartStore();
const [showClearDialog, setShowClearDialog] = useState(false);
// Calculate total from items
const total = cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const handleUpdateQuantity = (key: string, newQuantity: number) => {
if (newQuantity < 1) {
handleRemoveItem(key);
return;
}
updateQuantity(key, newQuantity);
};
const handleRemoveItem = (key: string) => {
removeItem(key);
toast.success('Item removed from cart');
};
const handleClearCart = () => {
clearCart();
setShowClearDialog(false);
toast.success('Cart cleared');
};
if (cart.items.length === 0) {
return (
<Container>
<div className="text-center py-16">
<ShoppingBag className="mx-auto h-16 w-16 text-gray-400 mb-4" />
<h2 className="text-2xl font-bold mb-2">Your cart is empty</h2>
<p className="text-gray-600 mb-6">Add some products to get started!</p>
<Button onClick={() => navigate('/shop')}>
<ArrowLeft className="mr-2 h-4 w-4" />
Continue Shopping
</Button>
</div>
</Container>
);
}
return (
<div className="container-safe py-8">
<h1 className="text-3xl font-bold mb-6">Shopping Cart</h1>
<p className="text-muted-foreground">Cart coming soon...</p>
</div>
<Container>
<div className="py-8">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold">Shopping Cart</h1>
<Button variant="outline" onClick={() => setShowClearDialog(true)}>
<Trash2 className="mr-2 h-4 w-4" />
Clear Cart
</Button>
</div>
<div className="grid lg:grid-cols-3 gap-8">
{/* Cart Items */}
<div className="lg:col-span-2 space-y-4">
{cart.items.map((item: CartItem) => (
<div
key={item.key}
className="flex gap-4 p-4 border rounded-lg bg-white"
>
{/* Product Image */}
<div className="relative w-24 h-24 flex-shrink-0 rounded-lg overflow-hidden bg-gray-100">
{item.image ? (
<img
src={item.image}
alt={item.name}
className="block w-full !h-full object-cover object-center"
/>
) : (
<div className="w-full !h-full flex items-center justify-center text-gray-400 text-xs">
No Image
</div>
)}
</div>
{/* Product Info */}
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-lg mb-1 truncate">
{item.name}
</h3>
<p className="text-gray-600 mb-2">
{formatPrice(item.price)}
</p>
{/* Quantity Controls */}
<div className="flex items-center gap-2">
<button
onClick={() => handleUpdateQuantity(item.key, item.quantity - 1)}
className="p-1 hover:bg-gray-100 rounded"
>
<Minus className="h-4 w-4" />
</button>
<input
type="number"
value={item.quantity}
onChange={(e) =>
handleUpdateQuantity(item.key, parseInt(e.target.value) || 1)
}
className="w-16 text-center border rounded py-1"
min="1"
/>
<button
onClick={() => handleUpdateQuantity(item.key, item.quantity + 1)}
className="p-1 hover:bg-gray-100 rounded"
>
<Plus className="h-4 w-4" />
</button>
</div>
</div>
{/* Item Total & Remove */}
<div className="flex flex-col items-end justify-between">
<button
onClick={() => handleRemoveItem(item.key)}
className="text-red-600 hover:text-red-700 p-2"
>
<Trash2 className="h-5 w-5" />
</button>
<p className="font-bold text-lg">
{formatPrice(item.price * item.quantity)}
</p>
</div>
</div>
))}
</div>
{/* Cart Summary */}
<div className="lg:col-span-1">
<div className="border rounded-lg p-6 bg-white sticky top-4">
<h2 className="text-xl font-bold mb-4">Cart Summary</h2>
<div className="space-y-3 mb-6">
<div className="flex justify-between text-gray-600">
<span>Subtotal</span>
<span>{formatPrice(total)}</span>
</div>
<div className="flex justify-between text-gray-600">
<span>Shipping</span>
<span>Calculated at checkout</span>
</div>
<div className="border-t pt-3 flex justify-between text-lg font-bold">
<span>Total</span>
<span>{formatPrice(total)}</span>
</div>
</div>
<Button
onClick={() => navigate('/checkout')}
size="lg"
className="w-full mb-3"
>
Proceed to Checkout
</Button>
<Button
onClick={() => navigate('/shop')}
variant="outline"
className="w-full"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Continue Shopping
</Button>
</div>
</div>
</div>
</div>
{/* Clear Cart Confirmation Dialog */}
<Dialog open={showClearDialog} onOpenChange={setShowClearDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Clear Cart?</DialogTitle>
<DialogDescription>
Are you sure you want to remove all items from your cart? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setShowClearDialog(false)}>
Cancel
</Button>
<Button variant="destructive" onClick={handleClearCart}>
Clear Cart
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Container>
);
}