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:
273
customer-spa/src/components/ProductCard.tsx
Normal file
273
customer-spa/src/components/ProductCard.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ShoppingCart, Heart } from 'lucide-react';
|
||||
import { formatPrice, formatDiscount } from '@/lib/currency';
|
||||
import { Button } from './ui/button';
|
||||
import { useLayout } from '@/contexts/ThemeContext';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
price: string;
|
||||
regular_price?: string;
|
||||
sale_price?: string;
|
||||
image?: string;
|
||||
on_sale?: boolean;
|
||||
stock_status?: string;
|
||||
};
|
||||
onAddToCart?: (product: any) => void;
|
||||
}
|
||||
|
||||
export function ProductCard({ product, onAddToCart }: ProductCardProps) {
|
||||
const { isClassic, isModern, isBoutique, isLaunch } = useLayout();
|
||||
|
||||
const handleAddToCart = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onAddToCart?.(product);
|
||||
};
|
||||
|
||||
// Calculate discount if on sale
|
||||
const discount = product.on_sale && product.regular_price && product.sale_price
|
||||
? formatDiscount(parseFloat(product.regular_price), parseFloat(product.sale_price))
|
||||
: null;
|
||||
|
||||
// Classic Layout - Traditional card with border
|
||||
if (isClassic) {
|
||||
return (
|
||||
<Link to={`/product/${product.slug}`} className="group">
|
||||
<div className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow bg-white">
|
||||
{/* Image */}
|
||||
<div className="relative w-full h-64 overflow-hidden bg-gray-100" style={{ fontSize: 0 }}>
|
||||
{product.image ? (
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="block w-full !h-full object-cover object-center group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full !h-full flex items-center justify-center text-gray-400" style={{ fontSize: '1rem' }}>
|
||||
No Image
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sale Badge */}
|
||||
{product.on_sale && discount && (
|
||||
<div className="absolute top-2 right-2 bg-red-500 text-white text-xs font-bold px-2 py-1 rounded">
|
||||
{discount}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button className="p-2 bg-white rounded-full shadow-md hover:bg-gray-50 flex items-center justify-center">
|
||||
<Heart className="w-4 h-4 block" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2 line-clamp-2 group-hover:text-primary transition-colors">
|
||||
{product.name}
|
||||
</h3>
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{product.on_sale && product.regular_price ? (
|
||||
<>
|
||||
<span className="text-lg font-bold" style={{ color: 'var(--color-primary)' }}>
|
||||
{formatPrice(product.sale_price || product.price)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 line-through">
|
||||
{formatPrice(product.regular_price)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-lg font-bold text-gray-900">
|
||||
{formatPrice(product.price)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
className="w-full"
|
||||
disabled={product.stock_status === 'outofstock'}
|
||||
>
|
||||
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||
{product.stock_status === 'outofstock' ? 'Out of Stock' : 'Add to Cart'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Modern Layout - Minimalist, clean
|
||||
if (isModern) {
|
||||
return (
|
||||
<Link to={`/product/${product.slug}`} className="group">
|
||||
<div className="overflow-hidden">
|
||||
{/* Image */}
|
||||
<div className="relative w-full h-64 mb-4 overflow-hidden bg-gray-50" style={{ fontSize: 0 }}>
|
||||
{product.image ? (
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="block w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-300" style={{ fontSize: '1rem' }}>
|
||||
No Image
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sale Badge */}
|
||||
{product.on_sale && discount && (
|
||||
<div className="absolute top-4 left-4 bg-black text-white text-xs font-medium px-3 py-1">
|
||||
{discount}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hover Overlay */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-300 flex items-center justify-center">
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
disabled={product.stock_status === 'outofstock'}
|
||||
>
|
||||
{product.stock_status === 'outofstock' ? 'Out of Stock' : 'Add to Cart'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="text-center">
|
||||
<h3 className="font-medium text-gray-900 mb-2 group-hover:text-primary transition-colors">
|
||||
{product.name}
|
||||
</h3>
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{product.on_sale && product.regular_price ? (
|
||||
<>
|
||||
<span className="font-semibold" style={{ color: 'var(--color-primary)' }}>
|
||||
{formatPrice(product.sale_price || product.price)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-400 line-through">
|
||||
{formatPrice(product.regular_price)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="font-semibold text-gray-900">
|
||||
{formatPrice(product.price)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Boutique Layout - Luxury, elegant
|
||||
if (isBoutique) {
|
||||
return (
|
||||
<Link to={`/product/${product.slug}`} className="group">
|
||||
<div className="overflow-hidden">
|
||||
{/* Image */}
|
||||
<div className="relative w-full h-80 mb-6 overflow-hidden bg-gray-50" style={{ fontSize: 0 }}>
|
||||
{product.image ? (
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="block w-full h-full object-cover object-center group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-300 font-serif" style={{ fontSize: '1rem' }}>
|
||||
No Image
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sale Badge */}
|
||||
{product.on_sale && discount && (
|
||||
<div className="absolute top-6 right-6 bg-white text-black text-xs font-medium px-4 py-2 tracking-wider">
|
||||
{discount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="text-center font-serif">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3 tracking-wide group-hover:text-primary transition-colors">
|
||||
{product.name}
|
||||
</h3>
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
{product.on_sale && product.regular_price ? (
|
||||
<>
|
||||
<span className="text-xl font-medium" style={{ color: 'var(--color-primary)' }}>
|
||||
{formatPrice(product.sale_price || product.price)}
|
||||
</span>
|
||||
<span className="text-gray-400 line-through">
|
||||
{formatPrice(product.regular_price)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xl font-medium text-gray-900">
|
||||
{formatPrice(product.price)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
variant="outline"
|
||||
className="w-full font-serif tracking-wider"
|
||||
disabled={product.stock_status === 'outofstock'}
|
||||
>
|
||||
{product.stock_status === 'outofstock' ? 'OUT OF STOCK' : 'ADD TO CART'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Launch Layout - Funnel optimized (shouldn't show product grid, but just in case)
|
||||
return (
|
||||
<Link to={`/product/${product.slug}`} className="group">
|
||||
<div className="border rounded-lg overflow-hidden hover:shadow-md transition-shadow bg-white">
|
||||
<div className="relative w-full h-64 overflow-hidden bg-gray-100" style={{ fontSize: 0 }}>
|
||||
{product.image ? (
|
||||
<img
|
||||
src={product.image}
|
||||
alt={product.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" style={{ fontSize: '1rem' }}>
|
||||
No Image
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 text-center">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">{product.name}</h3>
|
||||
<div className="text-xl font-bold mb-3" style={{ color: 'var(--color-primary)' }}>
|
||||
{formatPrice(product.price)}
|
||||
</div>
|
||||
<Button onClick={handleAddToCart} className="w-full" size="lg">
|
||||
Buy Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user