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,261 @@
import React, { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { useLayout } from '../contexts/ThemeContext';
interface BaseLayoutProps {
children: ReactNode;
}
/**
* Base Layout Component
*
* Renders the appropriate layout based on theme configuration
*/
export function BaseLayout({ children }: BaseLayoutProps) {
const { layout } = useLayout();
// Dynamically import and render the appropriate layout
switch (layout) {
case 'classic':
return <ClassicLayout>{children}</ClassicLayout>;
case 'modern':
return <ModernLayout>{children}</ModernLayout>;
case 'boutique':
return <BoutiqueLayout>{children}</BoutiqueLayout>;
case 'launch':
return <LaunchLayout>{children}</LaunchLayout>;
default:
return <ModernLayout>{children}</ModernLayout>;
}
}
/**
* Classic Layout - Traditional ecommerce
*/
function ClassicLayout({ children }: BaseLayoutProps) {
return (
<div className="classic-layout min-h-screen flex flex-col">
<header className="classic-header bg-white border-b sticky top-0 z-50">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-20">
{/* Logo */}
<div className="flex-shrink-0">
<Link to="/shop" className="text-2xl font-bold" style={{ color: 'var(--color-primary)' }}>
{(window as any).woonoowCustomer?.siteTitle || 'Store Title'}
</Link>
</div>
{/* Navigation */}
<nav className="hidden md:flex items-center space-x-8">
<Link to="/shop" className="hover:text-primary transition-colors">Shop</Link>
<a href="/about" className="hover:text-primary transition-colors">About</a>
<a href="/contact" className="hover:text-primary transition-colors">Contact</a>
</nav>
{/* Actions */}
<div className="flex items-center space-x-4">
<Link to="/my-account" className="hover:text-primary transition-colors">Account</Link>
<Link to="/cart" className="hover:text-primary transition-colors">Cart (0)</Link>
</div>
</div>
</div>
</header>
<main className="classic-main flex-1">
{children}
</main>
<footer className="classic-footer bg-gray-100 border-t mt-auto">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h3 className="font-semibold mb-4">About</h3>
<p className="text-sm text-gray-600">Your store description here.</p>
</div>
<div>
<h3 className="font-semibold mb-4">Quick Links</h3>
<ul className="space-y-2 text-sm">
<li><a href="/shop" className="text-gray-600 hover:text-primary">Shop</a></li>
<li><a href="/about" className="text-gray-600 hover:text-primary">About</a></li>
<li><a href="/contact" className="text-gray-600 hover:text-primary">Contact</a></li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4">Customer Service</h3>
<ul className="space-y-2 text-sm">
<li><a href="/shipping" className="text-gray-600 hover:text-primary">Shipping</a></li>
<li><a href="/returns" className="text-gray-600 hover:text-primary">Returns</a></li>
<li><a href="/faq" className="text-gray-600 hover:text-primary">FAQ</a></li>
</ul>
</div>
<div>
<h3 className="font-semibold mb-4">Newsletter</h3>
<p className="text-sm text-gray-600 mb-4">Subscribe to get updates</p>
<input
type="email"
placeholder="Your email"
className="w-full px-4 py-2 border rounded-md text-sm"
/>
</div>
</div>
<div className="border-t mt-8 pt-8 text-center text-sm text-gray-600">
© 2024 Your Store. All rights reserved.
</div>
</div>
</footer>
</div>
);
}
/**
* Modern Layout - Minimalist, clean
*/
function ModernLayout({ children }: BaseLayoutProps) {
return (
<div className="modern-layout min-h-screen flex flex-col">
<header className="modern-header bg-white border-b sticky top-0 z-50">
<div className="container mx-auto px-4">
<div className="flex flex-col items-center py-6">
{/* Logo - Centered */}
<Link to="/shop" className="text-3xl font-bold mb-4" style={{ color: 'var(--color-primary)' }}>
{(window as any).woonoowCustomer?.siteTitle || 'Store Title'}
</Link>
{/* Navigation - Centered */}
<nav className="flex items-center space-x-8">
<Link to="/shop" className="hover:text-primary transition-colors">Shop</Link>
<a href="/about" className="hover:text-primary transition-colors">About</a>
<a href="/contact" className="hover:text-primary transition-colors">Contact</a>
<Link to="/my-account" className="hover:text-primary transition-colors">Account</Link>
<Link to="/cart" className="hover:text-primary transition-colors">Cart</Link>
</nav>
</div>
</div>
</header>
<main className="modern-main flex-1">
{children}
</main>
<footer className="modern-footer bg-white border-t mt-auto">
<div className="container mx-auto px-4 py-12 text-center">
<div className="mb-6">
<a href="/" className="text-2xl font-bold" style={{ color: 'var(--color-primary)' }}>
Store Logo
</a>
</div>
<nav className="flex justify-center space-x-6 mb-6">
<a href="/shop" className="text-sm text-gray-600 hover:text-primary">Shop</a>
<a href="/about" className="text-sm text-gray-600 hover:text-primary">About</a>
<a href="/contact" className="text-sm text-gray-600 hover:text-primary">Contact</a>
</nav>
<p className="text-sm text-gray-600">
© 2024 Your Store. All rights reserved.
</p>
</div>
</footer>
</div>
);
}
/**
* Boutique Layout - Luxury, elegant
*/
function BoutiqueLayout({ children }: BaseLayoutProps) {
return (
<div className="boutique-layout min-h-screen flex flex-col font-serif">
<header className="boutique-header bg-white border-b sticky top-0 z-50">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-24">
{/* Logo */}
<div className="flex-1"></div>
<div className="flex-shrink-0">
<Link to="/shop" className="text-3xl font-bold tracking-wide" style={{ color: 'var(--color-primary)' }}>
{(window as any).woonoowCustomer?.siteTitle || 'BOUTIQUE'}
</Link>
</div>
<div className="flex-1 flex justify-end">
<nav className="hidden md:flex items-center space-x-8">
<Link to="/shop" className="text-sm uppercase tracking-wider hover:text-primary transition-colors">Shop</Link>
<Link to="/my-account" className="text-sm uppercase tracking-wider hover:text-primary transition-colors">Account</Link>
<Link to="/cart" className="text-sm uppercase tracking-wider hover:text-primary transition-colors">Cart</Link>
</nav>
</div>
</div>
</div>
</header>
<main className="boutique-main flex-1">
{children}
</main>
<footer className="boutique-footer bg-gray-50 border-t mt-auto">
<div className="container mx-auto px-4 py-16 text-center">
<div className="mb-8">
<a href="/" className="text-3xl font-bold tracking-wide" style={{ color: 'var(--color-primary)' }}>
BOUTIQUE
</a>
</div>
<nav className="flex justify-center space-x-8 mb-8">
<a href="/shop" className="text-sm uppercase tracking-wider text-gray-600 hover:text-primary">Shop</a>
<a href="/about" className="text-sm uppercase tracking-wider text-gray-600 hover:text-primary">About</a>
<a href="/contact" className="text-sm uppercase tracking-wider text-gray-600 hover:text-primary">Contact</a>
</nav>
<p className="text-sm text-gray-600 tracking-wide">
© 2024 BOUTIQUE. ALL RIGHTS RESERVED.
</p>
</div>
</footer>
</div>
);
}
/**
* Launch Layout - Single product funnel
* Note: Landing page is custom (user's page builder)
* WooNooW only takes over from checkout onwards
*/
function LaunchLayout({ children }: BaseLayoutProps) {
const isCheckoutFlow = window.location.pathname.includes('/checkout') ||
window.location.pathname.includes('/my-account') ||
window.location.pathname.includes('/order-received');
if (!isCheckoutFlow) {
// For non-checkout pages, use minimal layout
return (
<div className="launch-layout min-h-screen flex flex-col">
{children}
</div>
);
}
// For checkout flow: minimal header, no footer
return (
<div className="launch-layout min-h-screen flex flex-col bg-gray-50">
<header className="launch-header bg-white border-b">
<div className="container mx-auto px-4">
<div className="flex items-center justify-center h-16">
<Link to="/shop" className="text-xl font-bold" style={{ color: 'var(--color-primary)' }}>
{(window as any).woonoowCustomer?.siteTitle || 'Store Title'}
</Link>
</div>
</div>
</header>
<main className="launch-main flex-1 py-8">
<div className="container mx-auto px-4 max-w-2xl">
{children}
</div>
</main>
{/* Minimal footer for checkout */}
<footer className="launch-footer bg-white border-t py-4">
<div className="container mx-auto px-4 text-center text-sm text-gray-600">
© 2024 Your Store. Secure Checkout.
</div>
</footer>
</div>
);
}