-
-
-
-
{__('Text Color')}
-
- handleChange('hero_text_color', e.target.value)}
- className="w-20 h-10 p-1 cursor-pointer"
- />
- handleChange('hero_text_color', e.target.value)}
- placeholder="#ffffff"
- className="flex-1"
- />
-
-
- {__('Text and heading color for hero cards (usually white)')}
+
+
+ {__('Colors are now unified!')} {' '}
+ {__('Email colors (buttons, gradients) now use the same colors as your storefront for consistent branding.')}
+
+
+ {__('To change colors, go to')}{' '}
+
+ {__('Appearance → General → Colors')}
+
-
-
- {/* Preview */}
-
-
{__('Preview')}
-
{__('This is how your hero cards will look')}
-
-
-
- {/* Button Styling */}
-
-
-
-
{__('Button Text Color')}
-
- handleChange('button_text_color', e.target.value)}
- className="w-20 h-10 p-1 cursor-pointer"
- />
- handleChange('button_text_color', e.target.value)}
- placeholder="#ffffff"
- className="flex-1"
- />
-
-
- {__('Text color for buttons (usually white for dark buttons)')}
-
-
-
- {/* Button Preview */}
-
-
- {__('Primary Button')}
-
-
- {__('Secondary Button')}
-
-
@@ -540,7 +372,7 @@ export default function EmailCustomization() {
{__('Add Social Link')}
-
+
{formData.social_links.length === 0 ? (
{__('No social links added. Click "Add Social Link" to get started.')}
diff --git a/admin-spa/tailwind.config.js b/admin-spa/tailwind.config.js
index 751a1ed..a477b6c 100644
--- a/admin-spa/tailwind.config.js
+++ b/admin-spa/tailwind.config.js
@@ -22,5 +22,5 @@ module.exports = {
borderRadius: { lg: "12px", md: "10px", sm: "8px" }
}
},
- plugins: [require("tailwindcss-animate")]
+ plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")]
};
\ No newline at end of file
diff --git a/customer-spa/src/App.tsx b/customer-spa/src/App.tsx
index f10f559..d46de44 100644
--- a/customer-spa/src/App.tsx
+++ b/customer-spa/src/App.tsx
@@ -56,22 +56,42 @@ const getAppearanceSettings = () => {
return (window as any).woonoowCustomer?.appearanceSettings || {};
};
-// Get initial route from data attribute (set by PHP based on SPA mode)
+// Get initial route from data attribute or derive from SPA mode
const getInitialRoute = () => {
const appEl = document.getElementById('woonoow-customer-app');
const initialRoute = appEl?.getAttribute('data-initial-route');
- return initialRoute || '/shop'; // Default to shop if not specified
+ if (initialRoute) return initialRoute;
+
+ // Derive from SPA mode if no explicit route
+ const spaMode = (window as any).woonoowCustomer?.spaMode || 'full';
+ if (spaMode === 'checkout_only') return '/checkout';
+ return '/shop'; // Default for full mode
+};
+
+// Get front page slug from config
+const getFrontPageSlug = () => {
+ return (window as any).woonoowCustomer?.frontPageSlug || null;
};
// Router wrapper component that uses hooks requiring Router context
function AppRoutes() {
const initialRoute = getInitialRoute();
+ const frontPageSlug = getFrontPageSlug();
return (
- {/* Root route redirects to initial route based on SPA mode */}
- } />
+ {/* Root route: If frontPageSlug exists, render it. Else redirect to initialRoute (e.g. /shop) */}
+
+ ) : (
+
+ )
+ }
+ />
{/* Shop Routes */}
} />
@@ -128,6 +148,24 @@ function App() {
const appearanceSettings = getAppearanceSettings();
const toastPosition = (appearanceSettings?.general?.toast_position || 'top-right') as any;
+ // Inject gradient CSS variables
+ React.useEffect(() => {
+ // appearanceSettings is already the 'data' object from Assets.php injection
+ // Structure: { general: { colors: { primary, secondary, accent, text, background, gradientStart, gradientEnd } } }
+ const colors = appearanceSettings?.general?.colors;
+ if (colors) {
+ const root = document.documentElement;
+ // Inject all color settings as CSS variables
+ if (colors.primary) root.style.setProperty('--wn-primary', colors.primary);
+ if (colors.secondary) root.style.setProperty('--wn-secondary', colors.secondary);
+ if (colors.accent) root.style.setProperty('--wn-accent', colors.accent);
+ if (colors.text) root.style.setProperty('--wn-text', colors.text);
+ if (colors.background) root.style.setProperty('--wn-background', colors.background);
+ if (colors.gradientStart) root.style.setProperty('--wn-gradient-start', colors.gradientStart);
+ if (colors.gradientEnd) root.style.setProperty('--wn-gradient-end', colors.gradientEnd);
+ }
+ }, [appearanceSettings]);
+
return (
diff --git a/customer-spa/src/components/SharedContentLayout.tsx b/customer-spa/src/components/SharedContentLayout.tsx
new file mode 100644
index 0000000..f8ae4fc
--- /dev/null
+++ b/customer-spa/src/components/SharedContentLayout.tsx
@@ -0,0 +1,155 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import * as LucideIcons from 'lucide-react';
+
+interface SharedContentProps {
+ // Content
+ title?: string;
+ text?: string; // HTML content
+
+ // Image
+ image?: string;
+ imagePosition?: 'left' | 'right' | 'top' | 'bottom';
+
+ // Layout
+ containerWidth?: 'full' | 'contained';
+
+ // Styles
+ className?: string;
+ titleStyle?: React.CSSProperties;
+ titleClassName?: string;
+ textStyle?: React.CSSProperties;
+ textClassName?: string;
+ headingStyle?: React.CSSProperties; // For prose headings override
+ imageStyle?: React.CSSProperties;
+
+ // Pro Features (for future)
+ buttons?: Array<{ text: string, url: string }>;
+ buttonStyle?: { classNames?: string; style?: React.CSSProperties };
+}
+
+
+export const SharedContentLayout: React.FC = ({
+ title,
+ text,
+ image,
+ imagePosition = 'left',
+ containerWidth = 'contained',
+ className,
+ titleStyle,
+ titleClassName,
+ textStyle,
+ textClassName,
+ headingStyle,
+ buttons,
+
+ imageStyle,
+ buttonStyle
+}) => {
+
+ const hasImage = !!image;
+ const isImageLeft = imagePosition === 'left';
+ const isImageRight = imagePosition === 'right';
+ const isImageTop = imagePosition === 'top';
+ const isImageBottom = imagePosition === 'bottom';
+
+ // Wrapper classes
+ const containerClasses = cn(
+ 'w-full mx-auto px-4 sm:px-6 lg:px-8',
+ containerWidth === 'contained' ? 'max-w-7xl' : ''
+ );
+
+ const gridClasses = cn(
+ 'mx-auto',
+ hasImage && (isImageLeft || isImageRight) ? 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center' : 'max-w-4xl'
+ );
+
+ const imageWrapperOrder = isImageRight ? 'lg:order-last' : 'lg:order-first';
+
+ const proseStyle = {
+ ...textStyle,
+ '--tw-prose-headings': headingStyle?.color,
+ '--tw-prose-body': textStyle?.color,
+ } as React.CSSProperties;
+
+ return (
+
+
+ {/* Image Side */}
+ {hasImage && (
+
+
+
+ )}
+
+ {/* Content Side */}
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+
+ {text && (
+
+ )}
+
+
+
+ {/* Buttons */}
+ {buttons && buttons.length > 0 && (
+
+ {buttons.map((btn, idx) => (
+ btn.text && btn.url && (
+
+ {btn.text}
+
+ )
+ ))}
+
+ )}
+
+
+
+ );
+};
diff --git a/customer-spa/src/components/ui/searchable-select.tsx b/customer-spa/src/components/ui/searchable-select.tsx
index 74a6afa..59dcf80 100644
--- a/customer-spa/src/components/ui/searchable-select.tsx
+++ b/customer-spa/src/components/ui/searchable-select.tsx
@@ -68,7 +68,7 @@ export function SearchableSelect({
type="button"
role="combobox"
className={cn(
- "w-full flex items-center justify-between border !rounded-lg px-4 py-2 text-left bg-white hover:bg-gray-50 focus:outline-none focus:border-gray-400",
+ "w-full flex items-center justify-between border rounded-lg px-4 py-2 text-left bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary/20",
disabled && "opacity-50 cursor-not-allowed",
className
)}
diff --git a/customer-spa/src/hooks/useAppearanceSettings.ts b/customer-spa/src/hooks/useAppearanceSettings.ts
index 7894cad..86b09e7 100644
--- a/customer-spa/src/hooks/useAppearanceSettings.ts
+++ b/customer-spa/src/hooks/useAppearanceSettings.ts
@@ -37,25 +37,35 @@ interface AppearanceSettings {
thankyou: any;
account: any;
};
+ menus: {
+ primary: Array<{
+ id: string;
+ label: string;
+ type: 'page' | 'custom';
+ value: string;
+ target: '_self' | '_blank';
+ }>;
+ mobile: Array;
+ };
}
export function useAppearanceSettings() {
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
-
+
// Get preloaded settings from window object
const preloadedSettings = (window as any).woonoowCustomer?.appearanceSettings;
-
+
return useQuery({
queryKey: ['appearance-settings'],
queryFn: async () => {
const response = await fetch(`${apiRoot}/appearance/settings`, {
credentials: 'include',
});
-
+
if (!response.ok) {
throw new Error('Failed to fetch appearance settings');
}
-
+
const data = await response.json();
return data.data;
},
@@ -68,7 +78,7 @@ export function useAppearanceSettings() {
export function useShopSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
const defaultSettings = {
layout: {
grid_columns: '3' as string,
@@ -93,7 +103,7 @@ export function useShopSettings() {
show_icon: true,
},
};
-
+
return {
layout: { ...defaultSettings.layout, ...(data?.pages?.shop?.layout || {}) },
elements: { ...defaultSettings.elements, ...(data?.pages?.shop?.elements || {}) },
@@ -105,7 +115,7 @@ export function useShopSettings() {
export function useProductSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
const defaultSettings = {
layout: {
image_position: 'left' as string,
@@ -127,7 +137,7 @@ export function useProductSettings() {
hide_if_empty: true,
},
};
-
+
return {
layout: { ...defaultSettings.layout, ...(data?.pages?.product?.layout || {}) },
elements: { ...defaultSettings.elements, ...(data?.pages?.product?.elements || {}) },
@@ -139,7 +149,7 @@ export function useProductSettings() {
export function useCartSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
const defaultSettings = {
layout: {
style: 'fullwidth' as string,
@@ -152,7 +162,7 @@ export function useCartSettings() {
shipping_calculator: false,
},
};
-
+
return {
layout: { ...defaultSettings.layout, ...(data?.pages?.cart?.layout || {}) },
elements: { ...defaultSettings.elements, ...(data?.pages?.cart?.elements || {}) },
@@ -162,7 +172,7 @@ export function useCartSettings() {
export function useCheckoutSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
const defaultSettings = {
layout: {
style: 'two-column' as string,
@@ -178,7 +188,7 @@ export function useCheckoutSettings() {
payment_icons: true,
},
};
-
+
return {
layout: { ...defaultSettings.layout, ...(data?.pages?.checkout?.layout || {}) },
elements: { ...defaultSettings.elements, ...(data?.pages?.checkout?.elements || {}) },
@@ -188,7 +198,7 @@ export function useCheckoutSettings() {
export function useThankYouSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
const defaultSettings = {
template: 'basic',
header_visibility: 'show',
@@ -201,7 +211,7 @@ export function useThankYouSettings() {
related_products: false,
},
};
-
+
return {
template: data?.pages?.thankyou?.template || defaultSettings.template,
headerVisibility: data?.pages?.thankyou?.header_visibility || defaultSettings.header_visibility,
@@ -215,7 +225,7 @@ export function useThankYouSettings() {
export function useAccountSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
const defaultSettings = {
layout: {
navigation_style: 'sidebar' as string,
@@ -228,7 +238,7 @@ export function useAccountSettings() {
account_details: true,
},
};
-
+
return {
layout: { ...defaultSettings.layout, ...(data?.pages?.account?.layout || {}) },
elements: { ...defaultSettings.elements, ...(data?.pages?.account?.elements || {}) },
@@ -238,7 +248,7 @@ export function useAccountSettings() {
export function useHeaderSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
return {
style: data?.header?.style ?? 'classic',
sticky: data?.header?.sticky ?? true,
@@ -261,7 +271,7 @@ export function useHeaderSettings() {
export function useFooterSettings() {
const { data, isLoading } = useAppearanceSettings();
-
+
return {
columns: data?.footer?.columns ?? '4',
style: data?.footer?.style ?? 'detailed',
@@ -293,4 +303,15 @@ export function useFooterSettings() {
},
isLoading,
};
+};
+
+
+export function useMenuSettings() {
+ const { data, isLoading } = useAppearanceSettings();
+
+ return {
+ primary: data?.menus?.primary ?? [],
+ mobile: data?.menus?.mobile ?? [],
+ isLoading,
+ };
}
diff --git a/customer-spa/src/layouts/BaseLayout.tsx b/customer-spa/src/layouts/BaseLayout.tsx
index c74d6c0..fca0004 100644
--- a/customer-spa/src/layouts/BaseLayout.tsx
+++ b/customer-spa/src/layouts/BaseLayout.tsx
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { Search, ShoppingCart, User, Menu, X, Heart } from 'lucide-react';
import { useLayout } from '../contexts/ThemeContext';
import { useCartStore } from '../lib/cart/store';
-import { useHeaderSettings, useFooterSettings } from '../hooks/useAppearanceSettings';
+import { useHeaderSettings, useFooterSettings, useMenuSettings } from '../hooks/useAppearanceSettings';
import { SearchModal } from '../components/SearchModal';
import { NewsletterForm } from '../components/NewsletterForm';
import { LayoutWrapper } from './LayoutWrapper';
@@ -51,6 +51,7 @@ function ClassicLayout({ children }: BaseLayoutProps) {
const { isEnabled } = useModules();
const { settings: wishlistSettings } = useModuleSettings('wishlist');
const footerSettings = useFooterSettings();
+ const { primary: primaryMenu, mobile: mobileMenu } = useMenuSettings();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
@@ -74,7 +75,7 @@ function ClassicLayout({ children }: BaseLayoutProps) {
{/* Logo */}
{headerSettings.elements.logo && (
-
+
{storeLogo ? (
-
Shop
-
About
-
Contact
+ {primaryMenu.length > 0 ? (
+ primaryMenu.map(item => (
+ item.type === 'page' ? (
+
{item.label}
+ ) : (
+
{item.label}
+ )
+ ))
+ ) : (
+ <>
+ {(window as any).woonoowCustomer?.frontPageSlug && (
+
Home
+ )}
+
Shop
+
About
+
Contact
+ >
+ )}
)}
@@ -177,9 +193,13 @@ function ClassicLayout({ children }: BaseLayoutProps) {
{headerSettings.elements.navigation && (
- Shop
- About
- Contact
+ {(mobileMenu.length > 0 ? mobileMenu : primaryMenu).map(item => (
+ item.type === 'page' ? (
+ {item.label}
+ ) : (
+ {item.label}
+ )
+ ))}
)}
@@ -198,9 +218,13 @@ function ClassicLayout({ children }: BaseLayoutProps) {
{headerSettings.elements.navigation && (
- setMobileMenuOpen(false)} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">Shop
- setMobileMenuOpen(false)} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">About
- setMobileMenuOpen(false)} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">Contact
+ {(mobileMenu.length > 0 ? mobileMenu : primaryMenu).map(item => (
+ item.type === 'page' ? (
+ setMobileMenuOpen(false)} target={item.target} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">{item.label}
+ ) : (
+ setMobileMenuOpen(false)} target={item.target} className="px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded no-underline">{item.label}
+ )
+ ))}
)}