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:
207
customer-spa/src/contexts/ThemeContext.tsx
Normal file
207
customer-spa/src/contexts/ThemeContext.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user