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,207 @@
import React, { createContext, useContext, useEffect, ReactNode } from 'react';
interface ThemeColors {
primary: string;
secondary: string;
accent: string;
background?: string;
text?: string;
}
interface ThemeTypography {
preset: 'professional' | 'modern' | 'elegant' | 'tech' | 'custom';
customFonts?: {
heading: string;
body: string;
};
}
interface ThemeConfig {
mode: 'disabled' | 'full' | 'checkout_only';
layout: 'classic' | 'modern' | 'boutique' | 'launch';
colors: ThemeColors;
typography: ThemeTypography;
}
interface ThemeContextValue {
config: ThemeConfig;
isFullSPA: boolean;
isCheckoutOnly: boolean;
isLaunchLayout: boolean;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
const TYPOGRAPHY_PRESETS = {
professional: {
heading: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
body: "'Lora', Georgia, serif",
headingWeight: 700,
bodyWeight: 400,
},
modern: {
heading: "'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
body: "'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
headingWeight: 600,
bodyWeight: 400,
},
elegant: {
heading: "'Playfair Display', Georgia, serif",
body: "'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
headingWeight: 700,
bodyWeight: 400,
},
tech: {
heading: "'Space Grotesk', monospace",
body: "'IBM Plex Mono', monospace",
headingWeight: 700,
bodyWeight: 400,
},
};
/**
* Load Google Fonts for typography preset
*/
function loadTypography(preset: string, customFonts?: { heading: string; body: string }) {
// Remove existing font link if any
const existingLink = document.getElementById('woonoow-fonts');
if (existingLink) {
existingLink.remove();
}
if (preset === 'custom' && customFonts) {
// TODO: Handle custom fonts
return;
}
const fontMap: Record<string, string[]> = {
professional: ['Inter:400,600,700', 'Lora:400,700'],
modern: ['Poppins:400,600,700', 'Roboto:400,700'],
elegant: ['Playfair+Display:400,700', 'Source+Sans+Pro:400,700'],
tech: ['Space+Grotesk:400,700', 'IBM+Plex+Mono:400,700'],
};
const fonts = fontMap[preset];
if (!fonts) return;
const link = document.createElement('link');
link.id = 'woonoow-fonts';
link.href = `https://fonts.googleapis.com/css2?family=${fonts.join('&family=')}&display=swap`;
link.rel = 'stylesheet';
document.head.appendChild(link);
}
/**
* Generate color shades from base color
*/
function generateColorShades(baseColor: string): Record<number, string> {
// For now, just return the base color
// TODO: Implement proper color shade generation
return {
50: baseColor,
100: baseColor,
200: baseColor,
300: baseColor,
400: baseColor,
500: baseColor,
600: baseColor,
700: baseColor,
800: baseColor,
900: baseColor,
};
}
export function ThemeProvider({
config,
children
}: {
config: ThemeConfig;
children: ReactNode;
}) {
useEffect(() => {
const root = document.documentElement;
// Inject color CSS variables
root.style.setProperty('--color-primary', config.colors.primary);
root.style.setProperty('--color-secondary', config.colors.secondary);
root.style.setProperty('--color-accent', config.colors.accent);
if (config.colors.background) {
root.style.setProperty('--color-background', config.colors.background);
}
if (config.colors.text) {
root.style.setProperty('--color-text', config.colors.text);
}
// Inject typography CSS variables
const typoPreset = TYPOGRAPHY_PRESETS[config.typography.preset as keyof typeof TYPOGRAPHY_PRESETS];
if (typoPreset) {
root.style.setProperty('--font-heading', typoPreset.heading);
root.style.setProperty('--font-body', typoPreset.body);
root.style.setProperty('--font-weight-heading', typoPreset.headingWeight.toString());
root.style.setProperty('--font-weight-body', typoPreset.bodyWeight.toString());
}
// Load Google Fonts
loadTypography(config.typography.preset, config.typography.customFonts);
// Add layout class to body
document.body.classList.remove('layout-classic', 'layout-modern', 'layout-boutique', 'layout-launch');
document.body.classList.add(`layout-${config.layout}`);
// Add mode class to body
document.body.classList.remove('mode-disabled', 'mode-full', 'mode-checkout-only');
document.body.classList.add(`mode-${config.mode}`);
}, [config]);
const contextValue: ThemeContextValue = {
config,
isFullSPA: config.mode === 'full',
isCheckoutOnly: config.mode === 'checkout_only',
isLaunchLayout: config.layout === 'launch',
};
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}
/**
* Hook to access theme configuration
*/
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
/**
* Hook to check if we're in a specific layout
*/
export function useLayout() {
const { config } = useTheme();
return {
isClassic: config.layout === 'classic',
isModern: config.layout === 'modern',
isBoutique: config.layout === 'boutique',
isLaunch: config.layout === 'launch',
layout: config.layout,
};
}
/**
* Hook to check current mode
*/
export function useMode() {
const { config, isFullSPA, isCheckoutOnly } = useTheme();
return {
isFullSPA,
isCheckoutOnly,
isDisabled: config.mode === 'disabled',
mode: config.mode,
};
}