feat: implement header/footer visibility controls for checkout and thankyou pages

- Created LayoutWrapper component to conditionally render header/footer based on route
- Created MinimalHeader component (logo only)
- Created MinimalFooter component (trust badges + policy links)
- Created usePageVisibility hook to get visibility settings per page
- Wrapped ClassicLayout with LayoutWrapper for conditional rendering
- Header/footer visibility now controlled directly in React SPA
- Settings: show/minimal/hide for both header and footer
- Background color support for checkout and thankyou pages
This commit is contained in:
Dwindi Ramadhana
2025-12-25 22:20:48 +07:00
parent c37ecb8e96
commit 9ac09582d2
104 changed files with 14801 additions and 1213 deletions

View File

@@ -0,0 +1,145 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceAccount() {
const [loading, setLoading] = useState(true);
const [navigationStyle, setNavigationStyle] = useState('sidebar');
const [elements, setElements] = useState({
dashboard: true,
orders: true,
downloads: false,
addresses: true,
account_details: true,
});
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const account = response.data?.pages?.account;
if (account) {
if (account.layout?.navigation_style) setNavigationStyle(account.layout.navigation_style);
if (account.elements) setElements(account.elements);
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const handleSave = async () => {
try {
await api.post('/appearance/pages/account', {
layout: { navigation_style: navigationStyle },
elements,
});
toast.success('My account settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="My Account Settings"
onSave={handleSave}
isLoading={loading}
>
<SettingsCard
title="Layout"
description="Configure my account page layout"
>
<SettingsSection label="Navigation Style" htmlFor="navigation-style">
<Select value={navigationStyle} onValueChange={setNavigationStyle}>
<SelectTrigger id="navigation-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sidebar">Sidebar</SelectItem>
<SelectItem value="tabs">Tabs</SelectItem>
<SelectItem value="dropdown">Dropdown</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Elements"
description="Choose which sections to display in my account"
>
<div className="flex items-center justify-between">
<Label htmlFor="element-dashboard" className="cursor-pointer">
Show dashboard
</Label>
<Switch
id="element-dashboard"
checked={elements.dashboard}
onCheckedChange={() => toggleElement('dashboard')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-orders" className="cursor-pointer">
Show orders
</Label>
<Switch
id="element-orders"
checked={elements.orders}
onCheckedChange={() => toggleElement('orders')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-downloads" className="cursor-pointer">
Show downloads
</Label>
<Switch
id="element-downloads"
checked={elements.downloads}
onCheckedChange={() => toggleElement('downloads')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-addresses" className="cursor-pointer">
Show addresses
</Label>
<Switch
id="element-addresses"
checked={elements.addresses}
onCheckedChange={() => toggleElement('addresses')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-accountDetails" className="cursor-pointer">
Show account details
</Label>
<Switch
id="element-account-details"
checked={elements.account_details}
onCheckedChange={() => toggleElement('account_details')}
/>
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,155 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceCart() {
const [loading, setLoading] = useState(true);
const [layoutStyle, setLayoutStyle] = useState('fullwidth');
const [summaryPosition, setSummaryPosition] = useState('right');
const [elements, setElements] = useState({
product_images: true,
continue_shopping_button: true,
coupon_field: true,
shipping_calculator: false,
});
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const cart = response.data?.pages?.cart;
if (cart) {
if (cart.layout) {
if (cart.layout.style) setLayoutStyle(cart.layout.style);
if (cart.layout.summary_position) setSummaryPosition(cart.layout.summary_position);
}
if (cart.elements) {
setElements({
product_images: cart.elements.product_images ?? true,
continue_shopping_button: cart.elements.continue_shopping_button ?? true,
coupon_field: cart.elements.coupon_field ?? true,
shipping_calculator: cart.elements.shipping_calculator ?? false,
});
}
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const handleSave = async () => {
try {
await api.post('/appearance/pages/cart', {
layout: { style: layoutStyle, summary_position: summaryPosition },
elements,
});
toast.success('Cart page settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="Cart Page Settings"
onSave={handleSave}
isLoading={loading}
>
<SettingsCard
title="Layout"
description="Configure cart page layout"
>
<SettingsSection label="Style" htmlFor="layout-style">
<Select value={layoutStyle} onValueChange={setLayoutStyle}>
<SelectTrigger id="layout-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fullwidth">Full Width</SelectItem>
<SelectItem value="boxed">Boxed</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Summary Position" htmlFor="summary-position">
<Select value={summaryPosition} onValueChange={setSummaryPosition}>
<SelectTrigger id="summary-position">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="right">Right</SelectItem>
<SelectItem value="bottom">Bottom</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Elements"
description="Choose which elements to display on the cart page"
>
<div className="flex items-center justify-between">
<Label htmlFor="element-product-images" className="cursor-pointer">
Show product images
</Label>
<Switch
id="element-product-images"
checked={elements.product_images}
onCheckedChange={() => toggleElement('product_images')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-continue-shopping" className="cursor-pointer">
Show continue shopping button
</Label>
<Switch
id="element-continue-shopping"
checked={elements.continue_shopping_button}
onCheckedChange={() => toggleElement('continue_shopping_button')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-coupon-field" className="cursor-pointer">
Show coupon field
</Label>
<Switch
id="element-coupon-field"
checked={elements.coupon_field}
onCheckedChange={() => toggleElement('coupon_field')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-shipping-calculator" className="cursor-pointer">
Show shipping calculator
</Label>
<Switch
id="element-shipping-calculator"
checked={elements.shipping_calculator}
onCheckedChange={() => toggleElement('shipping_calculator')}
/>
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,256 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceCheckout() {
const [loading, setLoading] = useState(true);
const [layoutStyle, setLayoutStyle] = useState('two-column');
const [orderSummary, setOrderSummary] = useState('sidebar');
const [headerVisibility, setHeaderVisibility] = useState('minimal');
const [footerVisibility, setFooterVisibility] = useState('minimal');
const [backgroundColor, setBackgroundColor] = useState('#f9fafb');
const [elements, setElements] = useState({
order_notes: true,
coupon_field: true,
shipping_options: true,
payment_icons: true,
});
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const checkout = response.data?.pages?.checkout;
if (checkout) {
if (checkout.layout) {
if (checkout.layout.style) setLayoutStyle(checkout.layout.style);
if (checkout.layout.order_summary) setOrderSummary(checkout.layout.order_summary);
if (checkout.layout.header_visibility) setHeaderVisibility(checkout.layout.header_visibility);
if (checkout.layout.footer_visibility) setFooterVisibility(checkout.layout.footer_visibility);
if (checkout.layout.background_color) setBackgroundColor(checkout.layout.background_color);
}
if (checkout.elements) {
setElements({
order_notes: checkout.elements.order_notes ?? true,
coupon_field: checkout.elements.coupon_field ?? true,
shipping_options: checkout.elements.shipping_options ?? true,
payment_icons: checkout.elements.payment_icons ?? true,
});
}
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const handleSave = async () => {
try {
await api.post('/appearance/pages/checkout', {
layout: {
style: layoutStyle,
order_summary: orderSummary,
header_visibility: headerVisibility,
footer_visibility: footerVisibility,
background_color: backgroundColor,
},
elements,
});
toast.success('Checkout page settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="Checkout Page Settings"
onSave={handleSave}
isLoading={loading}
>
<SettingsCard
title="Layout"
description="Configure checkout page layout"
>
<SettingsSection label="Style" htmlFor="layout-style">
<Select value={layoutStyle} onValueChange={setLayoutStyle}>
<SelectTrigger id="layout-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="single-column">Single Column</SelectItem>
<SelectItem value="two-column">Two Columns</SelectItem>
</SelectContent>
</Select>
<div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-900 font-medium mb-1">Layout Scenarios:</p>
<ul className="text-sm text-blue-800 space-y-1 list-disc list-inside">
<li><strong>Two Columns + Sidebar:</strong> Form left, summary right (Desktop standard)</li>
<li><strong>Two Columns + Top:</strong> Summary top, form below (Mobile-friendly)</li>
<li><strong>Single Column:</strong> Everything stacked vertically (Order Summary position ignored)</li>
</ul>
</div>
</SettingsSection>
<SettingsSection label="Order Summary Position" htmlFor="order-summary">
<Select
value={orderSummary}
onValueChange={setOrderSummary}
disabled={layoutStyle === 'single-column'}
>
<SelectTrigger id="order-summary">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sidebar">Sidebar (Right)</SelectItem>
<SelectItem value="top">Top (Above Form)</SelectItem>
</SelectContent>
</Select>
{layoutStyle === 'single-column' && (
<p className="text-sm text-muted-foreground mt-2">
This setting is disabled in Single Column mode. Summary always appears at top.
</p>
)}
{layoutStyle === 'two-column' && (
<p className="text-sm text-muted-foreground mt-2">
{orderSummary === 'sidebar'
? '✓ Summary appears on right side (desktop), top on mobile'
: '✓ Summary appears at top, form below. Place Order button moves to bottom.'}
</p>
)}
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Header & Footer"
description="Control header and footer visibility for distraction-free checkout"
>
<SettingsSection label="Header Visibility" htmlFor="header-visibility">
<Select value={headerVisibility} onValueChange={setHeaderVisibility}>
<SelectTrigger id="header-visibility">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="show">Show Full Header</SelectItem>
<SelectItem value="minimal">Minimal (Logo Only)</SelectItem>
<SelectItem value="hide">Hide Completely</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-gray-500 mt-1">
Minimal header reduces distractions and improves conversion by 5-10%
</p>
</SettingsSection>
<SettingsSection label="Footer Visibility" htmlFor="footer-visibility">
<Select value={footerVisibility} onValueChange={setFooterVisibility}>
<SelectTrigger id="footer-visibility">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="show">Show Full Footer</SelectItem>
<SelectItem value="minimal">Minimal (Trust Badges & Policies)</SelectItem>
<SelectItem value="hide">Hide Completely</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-gray-500 mt-1">
Minimal footer with trust signals builds confidence without clutter
</p>
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Page Styling"
description="Customize the visual appearance of the checkout page"
>
<SettingsSection label="Background Color" htmlFor="background-color">
<div className="flex gap-2">
<Input
id="background-color"
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-20 h-10"
/>
<Input
type="text"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
placeholder="#f9fafb"
className="flex-1"
/>
</div>
<p className="text-sm text-gray-500 mt-1">
Set the background color for the checkout page
</p>
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Elements"
description="Choose which elements to display on the checkout page"
>
<div className="flex items-center justify-between">
<Label htmlFor="element-order-notes" className="cursor-pointer">
Show order notes field
</Label>
<Switch
id="element-order-notes"
checked={elements.order_notes}
onCheckedChange={() => toggleElement('order_notes')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-coupon-field" className="cursor-pointer">
Show coupon field
</Label>
<Switch
id="element-coupon-field"
checked={elements.coupon_field}
onCheckedChange={() => toggleElement('coupon_field')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-shipping-options" className="cursor-pointer">
Show shipping options
</Label>
<Switch
id="element-shipping-options"
checked={elements.shipping_options}
onCheckedChange={() => toggleElement('shipping_options')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-payment-icons" className="cursor-pointer">
Show payment icons
</Label>
<Switch
id="element-payment-icons"
checked={elements.payment_icons}
onCheckedChange={() => toggleElement('payment_icons')}
/>
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,463 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Plus, X } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/api';
interface SocialLink {
id: string;
platform: string;
url: string;
}
interface FooterSection {
id: string;
title: string;
type: 'menu' | 'contact' | 'social' | 'newsletter' | 'custom';
content: any;
visible: boolean;
}
interface ContactData {
email: string;
phone: string;
address: string;
show_email: boolean;
show_phone: boolean;
show_address: boolean;
}
export default function AppearanceFooter() {
const [loading, setLoading] = useState(true);
const [columns, setColumns] = useState('4');
const [style, setStyle] = useState('detailed');
const [copyrightText, setCopyrightText] = useState('© 2024 WooNooW. All rights reserved.');
const [elements, setElements] = useState({
newsletter: true,
social: true,
payment: true,
copyright: true,
menu: true,
contact: true,
});
const [socialLinks, setSocialLinks] = useState<SocialLink[]>([]);
const [sections, setSections] = useState<FooterSection[]>([]);
const [contactData, setContactData] = useState<ContactData>({
email: '',
phone: '',
address: '',
show_email: true,
show_phone: true,
show_address: true,
});
const defaultSections: FooterSection[] = [
{ id: '1', title: 'Contact', type: 'contact', content: '', visible: true },
{ id: '2', title: 'Quick Links', type: 'menu', content: '', visible: true },
{ id: '3', title: 'Follow Us', type: 'social', content: '', visible: true },
{ id: '4', title: 'Newsletter', type: 'newsletter', content: '', visible: true },
];
const [labels, setLabels] = useState({
contact_title: 'Contact',
menu_title: 'Quick Links',
social_title: 'Follow Us',
newsletter_title: 'Newsletter',
newsletter_description: 'Subscribe to get updates',
});
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const footer = response.data?.footer;
if (footer) {
if (footer.columns) setColumns(footer.columns);
if (footer.style) setStyle(footer.style);
if (footer.copyright_text) setCopyrightText(footer.copyright_text);
if (footer.elements) setElements(footer.elements);
if (footer.social_links) setSocialLinks(footer.social_links);
if (footer.sections && footer.sections.length > 0) {
setSections(footer.sections);
} else {
setSections(defaultSections);
}
if (footer.contact_data) setContactData(footer.contact_data);
if (footer.labels) setLabels(footer.labels);
} else {
setSections(defaultSections);
}
// Fetch store identity data
try {
const identityResponse = await api.get('/settings/store-identity');
const identity = identityResponse.data;
if (identity && !footer?.contact_data) {
setContactData(prev => ({
...prev,
email: identity.email || prev.email,
phone: identity.phone || prev.phone,
address: identity.address || prev.address,
}));
}
} catch (err) {
console.log('Store identity not available');
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const addSocialLink = () => {
setSocialLinks([
...socialLinks,
{ id: Date.now().toString(), platform: '', url: '' },
]);
};
const removeSocialLink = (id: string) => {
setSocialLinks(socialLinks.filter(link => link.id !== id));
};
const updateSocialLink = (id: string, field: 'platform' | 'url', value: string) => {
setSocialLinks(socialLinks.map(link =>
link.id === id ? { ...link, [field]: value } : link
));
};
const addSection = () => {
setSections([
...sections,
{
id: Date.now().toString(),
title: 'New Section',
type: 'custom',
content: '',
visible: true,
},
]);
};
const removeSection = (id: string) => {
setSections(sections.filter(s => s.id !== id));
};
const updateSection = (id: string, field: keyof FooterSection, value: any) => {
setSections(sections.map(s => s.id === id ? { ...s, [field]: value } : s));
};
const handleSave = async () => {
try {
await api.post('/appearance/footer', {
columns,
style,
copyright_text: copyrightText,
elements,
social_links: socialLinks,
sections,
contact_data: contactData,
labels,
});
toast.success('Footer settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="Footer Settings"
onSave={handleSave}
isLoading={loading}
>
{/* Layout */}
<SettingsCard
title="Layout"
description="Configure footer layout and style"
>
<SettingsSection label="Columns" htmlFor="footer-columns">
<Select value={columns} onValueChange={setColumns}>
<SelectTrigger id="footer-columns">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 Column</SelectItem>
<SelectItem value="2">2 Columns</SelectItem>
<SelectItem value="3">3 Columns</SelectItem>
<SelectItem value="4">4 Columns</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Style" htmlFor="footer-style">
<Select value={style} onValueChange={setStyle}>
<SelectTrigger id="footer-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="simple">Simple</SelectItem>
<SelectItem value="detailed">Detailed</SelectItem>
<SelectItem value="minimal">Minimal</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
</SettingsCard>
{/* Labels */}
<SettingsCard
title="Section Labels"
description="Customize footer section headings and text"
>
<SettingsSection label="Contact Title" htmlFor="contact-title">
<Input
id="contact-title"
value={labels.contact_title}
onChange={(e) => setLabels({ ...labels, contact_title: e.target.value })}
placeholder="Contact"
/>
</SettingsSection>
<SettingsSection label="Menu Title" htmlFor="menu-title">
<Input
id="menu-title"
value={labels.menu_title}
onChange={(e) => setLabels({ ...labels, menu_title: e.target.value })}
placeholder="Quick Links"
/>
</SettingsSection>
<SettingsSection label="Social Title" htmlFor="social-title">
<Input
id="social-title"
value={labels.social_title}
onChange={(e) => setLabels({ ...labels, social_title: e.target.value })}
placeholder="Follow Us"
/>
</SettingsSection>
<SettingsSection label="Newsletter Title" htmlFor="newsletter-title">
<Input
id="newsletter-title"
value={labels.newsletter_title}
onChange={(e) => setLabels({ ...labels, newsletter_title: e.target.value })}
placeholder="Newsletter"
/>
</SettingsSection>
<SettingsSection label="Newsletter Description" htmlFor="newsletter-desc">
<Input
id="newsletter-desc"
value={labels.newsletter_description}
onChange={(e) => setLabels({ ...labels, newsletter_description: e.target.value })}
placeholder="Subscribe to get updates"
/>
</SettingsSection>
</SettingsCard>
{/* Contact Data */}
<SettingsCard
title="Contact Information"
description="Manage contact details from Store Identity"
>
<SettingsSection label="Email" htmlFor="contact-email">
<Input
id="contact-email"
type="email"
value={contactData.email}
onChange={(e) => setContactData({ ...contactData, email: e.target.value })}
placeholder="info@store.com"
/>
<div className="flex items-center gap-2 mt-2">
<Switch
checked={contactData.show_email}
onCheckedChange={(checked) => setContactData({ ...contactData, show_email: checked })}
/>
<Label className="text-sm text-muted-foreground">Show in footer</Label>
</div>
</SettingsSection>
<SettingsSection label="Phone" htmlFor="contact-phone">
<Input
id="contact-phone"
type="tel"
value={contactData.phone}
onChange={(e) => setContactData({ ...contactData, phone: e.target.value })}
placeholder="(123) 456-7890"
/>
<div className="flex items-center gap-2 mt-2">
<Switch
checked={contactData.show_phone}
onCheckedChange={(checked) => setContactData({ ...contactData, show_phone: checked })}
/>
<Label className="text-sm text-muted-foreground">Show in footer</Label>
</div>
</SettingsSection>
<SettingsSection label="Address" htmlFor="contact-address">
<Textarea
id="contact-address"
value={contactData.address}
onChange={(e) => setContactData({ ...contactData, address: e.target.value })}
placeholder="123 Main St, City, State 12345"
rows={2}
/>
<div className="flex items-center gap-2 mt-2">
<Switch
checked={contactData.show_address}
onCheckedChange={(checked) => setContactData({ ...contactData, show_address: checked })}
/>
<Label className="text-sm text-muted-foreground">Show in footer</Label>
</div>
</SettingsSection>
</SettingsCard>
{/* Content */}
<SettingsCard
title="Content"
description="Customize footer content"
>
<SettingsSection label="Copyright Text" htmlFor="copyright">
<Textarea
id="copyright"
value={copyrightText}
onChange={(e) => setCopyrightText(e.target.value)}
rows={2}
placeholder="© 2024 Your Store. All rights reserved."
/>
</SettingsSection>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label>Social Media Links</Label>
<Button onClick={addSocialLink} variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Link
</Button>
</div>
<div className="space-y-3">
{socialLinks.map((link) => (
<div key={link.id} className="flex gap-2">
<Input
placeholder="Platform (e.g., Facebook)"
value={link.platform}
onChange={(e) => updateSocialLink(link.id, 'platform', e.target.value)}
className="flex-1"
/>
<Input
placeholder="URL"
value={link.url}
onChange={(e) => updateSocialLink(link.id, 'url', e.target.value)}
className="flex-1"
/>
<Button
onClick={() => removeSocialLink(link.id)}
variant="ghost"
size="icon"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
</SettingsCard>
{/* Custom Sections Builder */}
<SettingsCard
title="Custom Sections"
description="Build custom footer sections with flexible content"
>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>Footer Sections</Label>
<Button onClick={addSection} variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Section
</Button>
</div>
{sections.map((section) => (
<div key={section.id} className="border rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<Input
placeholder="Section Title"
value={section.title}
onChange={(e) => updateSection(section.id, 'title', e.target.value)}
className="flex-1 mr-2"
/>
<Button
onClick={() => removeSection(section.id)}
variant="ghost"
size="icon"
>
<X className="h-4 w-4" />
</Button>
</div>
<Select
value={section.type}
onValueChange={(value) => updateSection(section.id, 'type', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="menu">Menu Links</SelectItem>
<SelectItem value="contact">Contact Info</SelectItem>
<SelectItem value="social">Social Links</SelectItem>
<SelectItem value="newsletter">Newsletter Form</SelectItem>
<SelectItem value="custom">Custom HTML</SelectItem>
</SelectContent>
</Select>
{section.type === 'custom' && (
<Textarea
placeholder="Custom content (HTML supported)"
value={section.content}
onChange={(e) => updateSection(section.id, 'content', e.target.value)}
rows={4}
/>
)}
<div className="flex items-center gap-2">
<Switch
checked={section.visible}
onCheckedChange={(checked) => updateSection(section.id, 'visible', checked)}
/>
<Label className="text-sm text-muted-foreground">Visible</Label>
</div>
</div>
))}
{sections.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
No custom sections yet. Click "Add Section" to create one.
</p>
)}
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Slider } from '@/components/ui/slider';
import { Input } from '@/components/ui/input';
import { AlertCircle } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceGeneral() {
const [loading, setLoading] = useState(true);
const [spaMode, setSpaMode] = useState<'disabled' | 'checkout_only' | 'full'>('full');
const [typographyMode, setTypographyMode] = useState<'predefined' | 'custom_google'>('predefined');
const [predefinedPair, setPredefinedPair] = useState('modern');
const [customHeading, setCustomHeading] = useState('');
const [customBody, setCustomBody] = useState('');
const [fontScale, setFontScale] = useState([1.0]);
const fontPairs = {
modern: { name: 'Modern & Clean', fonts: 'Inter' },
editorial: { name: 'Editorial', fonts: 'Playfair Display + Source Sans' },
friendly: { name: 'Friendly', fonts: 'Poppins + Open Sans' },
elegant: { name: 'Elegant', fonts: 'Cormorant + Lato' },
};
const [colors, setColors] = useState({
primary: '#1a1a1a',
secondary: '#6b7280',
accent: '#3b82f6',
text: '#111827',
background: '#ffffff',
});
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const general = response.data?.general;
if (general) {
if (general.spa_mode) setSpaMode(general.spa_mode);
if (general.typography) {
setTypographyMode(general.typography.mode || 'predefined');
setPredefinedPair(general.typography.predefined_pair || 'modern');
setCustomHeading(general.typography.custom?.heading || '');
setCustomBody(general.typography.custom?.body || '');
setFontScale([general.typography.scale || 1.0]);
}
if (general.colors) {
setColors({
primary: general.colors.primary || '#1a1a1a',
secondary: general.colors.secondary || '#6b7280',
accent: general.colors.accent || '#3b82f6',
text: general.colors.text || '#111827',
background: general.colors.background || '#ffffff',
});
}
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const handleSave = async () => {
try {
await api.post('/appearance/general', {
spa_mode: spaMode,
typography: {
mode: typographyMode,
predefined_pair: typographyMode === 'predefined' ? predefinedPair : undefined,
custom: typographyMode === 'custom_google' ? { heading: customHeading, body: customBody } : undefined,
scale: fontScale[0],
},
colors,
});
toast.success('General settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="General Settings"
onSave={handleSave}
isLoading={loading}
>
{/* SPA Mode */}
<SettingsCard
title="SPA Mode"
description="Choose how the Single Page Application is implemented"
>
<RadioGroup value={spaMode} onValueChange={(value: any) => setSpaMode(value)}>
<div className="flex items-start space-x-3">
<RadioGroupItem value="disabled" id="spa-disabled" />
<div className="space-y-1">
<Label htmlFor="spa-disabled" className="font-medium cursor-pointer">
Disabled
</Label>
<p className="text-sm text-muted-foreground">
Use WordPress default pages (no SPA functionality)
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<RadioGroupItem value="checkout_only" id="spa-checkout" />
<div className="space-y-1">
<Label htmlFor="spa-checkout" className="font-medium cursor-pointer">
Checkout Only
</Label>
<p className="text-sm text-muted-foreground">
SPA for checkout flow only (cart, checkout, thank you)
</p>
</div>
</div>
<div className="flex items-start space-x-3">
<RadioGroupItem value="full" id="spa-full" />
<div className="space-y-1">
<Label htmlFor="spa-full" className="font-medium cursor-pointer">
Full SPA
</Label>
<p className="text-sm text-muted-foreground">
Entire customer-facing site uses SPA (recommended)
</p>
</div>
</div>
</RadioGroup>
</SettingsCard>
{/* Typography */}
<SettingsCard
title="Typography"
description="Choose fonts for your store"
>
<RadioGroup value={typographyMode} onValueChange={(value: any) => setTypographyMode(value)}>
<div className="flex items-start space-x-3">
<RadioGroupItem value="predefined" id="typo-predefined" />
<div className="space-y-1 flex-1">
<Label htmlFor="typo-predefined" className="font-medium cursor-pointer">
Predefined Font Pairs (GDPR-compliant)
</Label>
<p className="text-sm text-muted-foreground mb-3">
Self-hosted fonts, no external requests
</p>
{typographyMode === 'predefined' && (
<Select value={predefinedPair} onValueChange={setPredefinedPair}>
<SelectTrigger className="w-full min-w-[300px] [&>span]:line-clamp-none [&>span]:whitespace-normal">
<SelectValue>
{fontPairs[predefinedPair as keyof typeof fontPairs]?.name}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="modern">
<div>
<div className="font-medium">Modern & Clean</div>
<div className="text-xs text-muted-foreground">Inter</div>
</div>
</SelectItem>
<SelectItem value="editorial">
<div>
<div className="font-medium">Editorial</div>
<div className="text-xs text-muted-foreground">Playfair Display + Source Sans</div>
</div>
</SelectItem>
<SelectItem value="friendly">
<div>
<div className="font-medium">Friendly</div>
<div className="text-xs text-muted-foreground">Poppins + Open Sans</div>
</div>
</SelectItem>
<SelectItem value="elegant">
<div>
<div className="font-medium">Elegant</div>
<div className="text-xs text-muted-foreground">Cormorant + Lato</div>
</div>
</SelectItem>
</SelectContent>
</Select>
)}
</div>
</div>
<div className="flex items-start space-x-3">
<RadioGroupItem value="custom_google" id="typo-custom" />
<div className="space-y-1 flex-1">
<Label htmlFor="typo-custom" className="font-medium cursor-pointer">
Custom Google Fonts
</Label>
<Alert className="mt-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Using Google Fonts may not be GDPR compliant
</AlertDescription>
</Alert>
{typographyMode === 'custom_google' && (
<div className="space-y-3 mt-3">
<SettingsSection label="Heading Font" htmlFor="heading-font">
<Input
id="heading-font"
placeholder="e.g., Montserrat"
value={customHeading}
onChange={(e) => setCustomHeading(e.target.value)}
/>
</SettingsSection>
<SettingsSection label="Body Font" htmlFor="body-font">
<Input
id="body-font"
placeholder="e.g., Roboto"
value={customBody}
onChange={(e) => setCustomBody(e.target.value)}
/>
</SettingsSection>
</div>
)}
</div>
</div>
</RadioGroup>
<div className="space-y-3 pt-4 border-t">
<Label>Font Scale: {fontScale[0].toFixed(1)}x</Label>
<Slider
value={fontScale}
onValueChange={setFontScale}
min={0.8}
max={1.2}
step={0.1}
className="w-full"
/>
<p className="text-sm text-muted-foreground">
Adjust the overall size of all text (0.8x - 1.2x)
</p>
</div>
</SettingsCard>
{/* Colors */}
<SettingsCard
title="Colors"
description="Customize your store's color palette"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.entries(colors).map(([key, value]) => (
<SettingsSection key={key} label={key.charAt(0).toUpperCase() + key.slice(1)} htmlFor={`color-${key}`}>
<div className="flex gap-2">
<Input
id={`color-${key}`}
type="color"
value={value}
onChange={(e) => setColors({ ...colors, [key]: e.target.value })}
className="w-20 h-10 cursor-pointer"
/>
<Input
type="text"
value={value}
onChange={(e) => setColors({ ...colors, [key]: e.target.value })}
className="flex-1 font-mono"
/>
</div>
</SettingsSection>
))}
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,214 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceHeader() {
const [loading, setLoading] = useState(true);
const [style, setStyle] = useState('classic');
const [sticky, setSticky] = useState(true);
const [height, setHeight] = useState('normal');
const [mobileMenu, setMobileMenu] = useState('hamburger');
const [mobileLogo, setMobileLogo] = useState('left');
const [logoWidth, setLogoWidth] = useState('auto');
const [logoHeight, setLogoHeight] = useState('40px');
const [elements, setElements] = useState({
logo: true,
navigation: true,
search: true,
account: true,
cart: true,
wishlist: false,
});
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const header = response.data?.header;
if (header) {
if (header.style) setStyle(header.style);
if (header.sticky !== undefined) setSticky(header.sticky);
if (header.height) setHeight(header.height);
if (header.mobile_menu) setMobileMenu(header.mobile_menu);
if (header.mobile_logo) setMobileLogo(header.mobile_logo);
if (header.logo_width) setLogoWidth(header.logo_width);
if (header.logo_height) setLogoHeight(header.logo_height);
if (header.elements) setElements(header.elements);
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const handleSave = async () => {
try {
await api.post('/appearance/header', {
style,
sticky,
height,
mobile_menu: mobileMenu,
mobile_logo: mobileLogo,
logo_width: logoWidth,
logo_height: logoHeight,
elements,
});
toast.success('Header settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="Header Settings"
onSave={handleSave}
isLoading={loading}
>
{/* Layout */}
<SettingsCard
title="Layout"
description="Configure header layout and style"
>
<SettingsSection label="Style" htmlFor="header-style">
<Select value={style} onValueChange={setStyle}>
<SelectTrigger id="header-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="classic">Classic</SelectItem>
<SelectItem value="modern">Modern</SelectItem>
<SelectItem value="minimal">Minimal</SelectItem>
<SelectItem value="centered">Centered</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="sticky-header">Sticky Header</Label>
<p className="text-sm text-muted-foreground">
Header stays visible when scrolling
</p>
</div>
<Switch
id="sticky-header"
checked={sticky}
onCheckedChange={setSticky}
/>
</div>
<SettingsSection label="Height" htmlFor="header-height">
<Select value={height} onValueChange={setHeight}>
<SelectTrigger id="header-height">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="compact">Compact</SelectItem>
<SelectItem value="normal">Normal</SelectItem>
<SelectItem value="tall">Tall</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Logo Width" htmlFor="logo-width">
<Select value={logoWidth} onValueChange={setLogoWidth}>
<SelectTrigger id="logo-width">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="100px">100px</SelectItem>
<SelectItem value="150px">150px</SelectItem>
<SelectItem value="200px">200px</SelectItem>
<SelectItem value="250px">250px</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Logo Height" htmlFor="logo-height">
<Select value={logoHeight} onValueChange={setLogoHeight}>
<SelectTrigger id="logo-height">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="30px">30px</SelectItem>
<SelectItem value="40px">40px</SelectItem>
<SelectItem value="50px">50px</SelectItem>
<SelectItem value="60px">60px</SelectItem>
<SelectItem value="80px">80px</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
</SettingsCard>
{/* Elements */}
<SettingsCard
title="Elements"
description="Choose which elements to display in the header"
>
{Object.entries(elements).map(([key, value]) => (
<div key={key} className="flex items-center justify-between">
<Label htmlFor={`element-${key}`} className="capitalize cursor-pointer">
Show {key.replace(/([A-Z])/g, ' $1').toLowerCase()}
</Label>
<Switch
id={`element-${key}`}
checked={value}
onCheckedChange={() => toggleElement(key as keyof typeof elements)}
/>
</div>
))}
</SettingsCard>
{/* Mobile */}
<SettingsCard
title="Mobile Settings"
description="Configure header behavior on mobile devices"
>
<SettingsSection label="Menu Style" htmlFor="mobile-menu">
<Select value={mobileMenu} onValueChange={setMobileMenu}>
<SelectTrigger id="mobile-menu">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hamburger">Hamburger</SelectItem>
<SelectItem value="bottom-nav">Bottom Navigation</SelectItem>
<SelectItem value="slide-in">Slide-in Drawer</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Logo Position" htmlFor="mobile-logo">
<Select value={mobileLogo} onValueChange={setMobileLogo}>
<SelectTrigger id="mobile-logo">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,278 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceProduct() {
const [loading, setLoading] = useState(true);
const [imagePosition, setImagePosition] = useState('left');
const [galleryStyle, setGalleryStyle] = useState('thumbnails');
const [stickyAddToCart, setStickyAddToCart] = useState(false);
const [elements, setElements] = useState({
breadcrumbs: true,
related_products: true,
reviews: true,
share_buttons: false,
product_meta: true,
});
const [reviewSettings, setReviewSettings] = useState({
placement: 'product_page',
hide_if_empty: true,
});
const [relatedProductsTitle, setRelatedProductsTitle] = useState('You May Also Like');
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const product = response.data?.pages?.product;
if (product) {
if (product.layout) {
if (product.layout.image_position) setImagePosition(product.layout.image_position);
if (product.layout.gallery_style) setGalleryStyle(product.layout.gallery_style);
if (product.layout.sticky_add_to_cart !== undefined) setStickyAddToCart(product.layout.sticky_add_to_cart);
}
if (product.elements) {
setElements({
breadcrumbs: product.elements.breadcrumbs ?? true,
related_products: product.elements.related_products ?? true,
reviews: product.elements.reviews ?? true,
share_buttons: product.elements.share_buttons ?? false,
product_meta: product.elements.product_meta ?? true,
});
}
if (product.related_products) {
setRelatedProductsTitle(product.related_products.title ?? 'You May Also Like');
}
if (product.reviews) {
setReviewSettings({
placement: product.reviews.placement ?? 'product_page',
hide_if_empty: product.reviews.hide_if_empty ?? true,
});
}
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const handleSave = async () => {
try {
await api.post('/appearance/pages/product', {
layout: {
image_position: imagePosition,
gallery_style: galleryStyle,
sticky_add_to_cart: stickyAddToCart
},
elements,
related_products: {
title: relatedProductsTitle,
},
reviews: reviewSettings,
});
toast.success('Product page settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="Product Page Settings"
onSave={handleSave}
isLoading={loading}
>
{/* Layout */}
<SettingsCard
title="Layout"
description="Configure product page layout and gallery"
>
<SettingsSection label="Image Position" htmlFor="image-position">
<Select value={imagePosition} onValueChange={setImagePosition}>
<SelectTrigger id="image-position">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="right">Right</SelectItem>
<SelectItem value="top">Top</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Gallery Style" htmlFor="gallery-style">
<Select value={galleryStyle} onValueChange={setGalleryStyle}>
<SelectTrigger id="gallery-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="thumbnails">Thumbnails</SelectItem>
<SelectItem value="dots">Dots</SelectItem>
<SelectItem value="slider">Slider</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="sticky-cart">Sticky Add to Cart</Label>
<p className="text-sm text-muted-foreground">
Keep add to cart button visible when scrolling
</p>
</div>
<Switch
id="sticky-cart"
checked={stickyAddToCart}
onCheckedChange={setStickyAddToCart}
/>
</div>
</SettingsCard>
{/* Elements */}
<SettingsCard
title="Elements"
description="Choose which elements to display on the product page"
>
<div className="flex items-center justify-between">
<Label htmlFor="element-breadcrumbs" className="cursor-pointer">
Show breadcrumbs
</Label>
<Switch
id="element-breadcrumbs"
checked={elements.breadcrumbs}
onCheckedChange={() => toggleElement('breadcrumbs')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-related-products" className="cursor-pointer">
Show related products
</Label>
<Switch
id="element-related-products"
checked={elements.related_products}
onCheckedChange={() => toggleElement('related_products')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-reviews" className="cursor-pointer">
Show reviews
</Label>
<Switch
id="element-reviews"
checked={elements.reviews}
onCheckedChange={() => toggleElement('reviews')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-share-buttons" className="cursor-pointer">
Show share buttons
</Label>
<Switch
id="element-share-buttons"
checked={elements.share_buttons}
onCheckedChange={() => toggleElement('share_buttons')}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="element-product-meta" className="cursor-pointer">
Show product meta
</Label>
<p className="text-sm text-muted-foreground">
SKU, categories, tags
</p>
</div>
<Switch
id="element-product-meta"
checked={elements.product_meta}
onCheckedChange={() => toggleElement('product_meta')}
/>
</div>
</SettingsCard>
{/* Related Products Settings */}
<SettingsCard
title="Related Products"
description="Configure related products section"
>
<SettingsSection label="Section Title" htmlFor="related-products-title">
<input
id="related-products-title"
type="text"
value={relatedProductsTitle}
onChange={(e) => setRelatedProductsTitle(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="You May Also Like"
/>
<p className="text-sm text-muted-foreground mt-2">
This heading appears above the related products grid
</p>
</SettingsSection>
</SettingsCard>
{/* Review Settings */}
<SettingsCard
title="Review Settings"
description="Configure how and where reviews are displayed"
>
<SettingsSection label="Review Placement" htmlFor="review-placement">
<Select value={reviewSettings.placement} onValueChange={(value) => setReviewSettings({ ...reviewSettings, placement: value })}>
<SelectTrigger id="review-placement">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="product_page">Product Page (Traditional)</SelectItem>
<SelectItem value="order_details">Order Details Only (Marketplace Style)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground mt-2">
{reviewSettings.placement === 'product_page'
? 'Reviews appear on product page. Users can submit reviews directly on the product.'
: 'Reviews only appear in order details after purchase. Ensures verified purchases only.'}
</p>
</SettingsSection>
{reviewSettings.placement === 'product_page' && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="hide-if-empty" className="cursor-pointer">
Hide reviews section if empty
</Label>
<p className="text-sm text-muted-foreground">
Only show reviews section when product has at least one review
</p>
</div>
<Switch
id="hide-if-empty"
checked={reviewSettings.hide_if_empty}
onCheckedChange={(checked) => setReviewSettings({ ...reviewSettings, hide_if_empty: checked })}
/>
</div>
)}
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,302 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceShop() {
const [loading, setLoading] = useState(true);
const [gridColumns, setGridColumns] = useState('3');
const [gridStyle, setGridStyle] = useState('standard');
const [cardStyle, setCardStyle] = useState('card');
const [aspectRatio, setAspectRatio] = useState('square');
const [elements, setElements] = useState({
category_filter: true,
search_bar: true,
sort_dropdown: true,
sale_badges: true,
});
const [saleBadgeColor, setSaleBadgeColor] = useState('#ef4444');
const [cardTextAlign, setCardTextAlign] = useState('left');
const [addToCartPosition, setAddToCartPosition] = useState('below');
const [addToCartStyle, setAddToCartStyle] = useState('solid');
const [showCartIcon, setShowCartIcon] = useState(true);
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const shop = response.data?.pages?.shop;
if (shop) {
setGridColumns(shop.layout?.grid_columns || '3');
setGridStyle(shop.layout?.grid_style || 'standard');
setCardStyle(shop.layout?.card_style || 'card');
setAspectRatio(shop.layout?.aspect_ratio || 'square');
setCardTextAlign(shop.layout?.card_text_align || 'left');
if (shop.elements) {
setElements(shop.elements);
}
setSaleBadgeColor(shop.sale_badge?.color || '#ef4444');
setAddToCartPosition(shop.add_to_cart?.position || 'below');
setAddToCartStyle(shop.add_to_cart?.style || 'solid');
setShowCartIcon(shop.add_to_cart?.show_icon ?? true);
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const handleSave = async () => {
try {
await api.post('/appearance/pages/shop', {
layout: {
grid_columns: gridColumns,
grid_style: gridStyle,
card_style: cardStyle,
aspect_ratio: aspectRatio,
card_text_align: cardTextAlign
},
elements: {
category_filter: elements.category_filter,
search_bar: elements.search_bar,
sort_dropdown: elements.sort_dropdown,
sale_badges: elements.sale_badges,
},
sale_badge: {
color: saleBadgeColor
},
add_to_cart: {
position: addToCartPosition,
style: addToCartStyle,
show_icon: showCartIcon
},
});
toast.success('Shop page settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="Shop Page Settings"
onSave={handleSave}
isLoading={loading}
>
{/* Layout */}
<SettingsCard
title="Layout"
description="Configure shop page layout and product display"
>
<SettingsSection label="Grid Columns" htmlFor="grid-columns">
<Select value={gridColumns} onValueChange={setGridColumns}>
<SelectTrigger id="grid-columns">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2 Columns</SelectItem>
<SelectItem value="3">3 Columns</SelectItem>
<SelectItem value="4">4 Columns</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Grid Style" htmlFor="grid-style" description="Masonry creates a Pinterest-like layout with varying heights">
<Select value={gridStyle} onValueChange={setGridStyle}>
<SelectTrigger id="grid-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="standard">Standard - Equal heights</SelectItem>
<SelectItem value="masonry">Masonry - Dynamic heights</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Product Card Style" htmlFor="card-style" description="Visual style adapts to column count - more columns = cleaner style">
<Select value={cardStyle} onValueChange={setCardStyle}>
<SelectTrigger id="card-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="card">Card - Bordered with shadow</SelectItem>
<SelectItem value="minimal">Minimal - Clean, no border</SelectItem>
<SelectItem value="overlay">Overlay - Shadow on hover</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Image Aspect Ratio" htmlFor="aspect-ratio">
<Select value={aspectRatio} onValueChange={setAspectRatio}>
<SelectTrigger id="aspect-ratio">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="square">Square (1:1)</SelectItem>
<SelectItem value="portrait">Portrait (3:4)</SelectItem>
<SelectItem value="landscape">Landscape (4:3)</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Card Text Alignment" htmlFor="card-text-align" description="Align product title and price">
<Select value={cardTextAlign} onValueChange={setCardTextAlign}>
<SelectTrigger id="card-text-align">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="center">Center</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Sale Badge Color" htmlFor="sale-badge-color">
<input
type="color"
id="sale-badge-color"
value={saleBadgeColor}
onChange={(e) => setSaleBadgeColor(e.target.value)}
className="h-10 w-full rounded-md border border-input cursor-pointer"
/>
</SettingsSection>
</SettingsCard>
{/* Elements */}
<SettingsCard
title="Elements"
description="Choose which elements to display on the shop page"
>
<div className="flex items-center justify-between">
<Label htmlFor="element-category_filter" className="cursor-pointer">
Show category filter
</Label>
<Switch
id="element-category_filter"
checked={elements.category_filter}
onCheckedChange={() => toggleElement('category_filter')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-search_bar" className="cursor-pointer">
Show search bar
</Label>
<Switch
id="element-search_bar"
checked={elements.search_bar}
onCheckedChange={() => toggleElement('search_bar')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-sort_dropdown" className="cursor-pointer">
Show sort dropdown
</Label>
<Switch
id="element-sort_dropdown"
checked={elements.sort_dropdown}
onCheckedChange={() => toggleElement('sort_dropdown')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-sale_badges" className="cursor-pointer">
Show sale badges
</Label>
<Switch
id="element-sale_badges"
checked={elements.sale_badges}
onCheckedChange={() => toggleElement('sale_badges')}
/>
</div>
</SettingsCard>
{/* Add to Cart Button */}
<SettingsCard
title="Add to Cart Button"
description="Configure add to cart button appearance and behavior"
>
<SettingsSection label="Position">
<RadioGroup value={addToCartPosition} onValueChange={setAddToCartPosition}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="below" id="position-below" />
<Label htmlFor="position-below" className="cursor-pointer">
Below image
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="overlay" id="position-overlay" />
<Label htmlFor="position-overlay" className="cursor-pointer">
On hover overlay
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="bottom" id="position-bottom" />
<Label htmlFor="position-bottom" className="cursor-pointer">
Bottom of card
</Label>
</div>
</RadioGroup>
</SettingsSection>
<SettingsSection label="Style">
<RadioGroup value={addToCartStyle} onValueChange={setAddToCartStyle}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="solid" id="style-solid" />
<Label htmlFor="style-solid" className="cursor-pointer">
Solid
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="outline" id="style-outline" />
<Label htmlFor="style-outline" className="cursor-pointer">
Outline
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="text" id="style-text" />
<Label htmlFor="style-text" className="cursor-pointer">
Text only
</Label>
</div>
</RadioGroup>
</SettingsSection>
<div className="flex items-center justify-between">
<Label htmlFor="show-cart-icon" className="cursor-pointer">
Show cart icon
</Label>
<Switch
id="show-cart-icon"
checked={showCartIcon}
onCheckedChange={setShowCartIcon}
/>
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,225 @@
import React, { useState, useEffect } from 'react';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { SettingsSection } from '@/routes/Settings/components/SettingsSection';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { toast } from 'sonner';
import { api } from '@/lib/api';
export default function AppearanceThankYou() {
const [loading, setLoading] = useState(true);
const [template, setTemplate] = useState('basic');
const [headerVisibility, setHeaderVisibility] = useState('show');
const [footerVisibility, setFooterVisibility] = useState('minimal');
const [backgroundColor, setBackgroundColor] = useState('#f9fafb');
const [customMessage, setCustomMessage] = useState('Thank you for your order! We\'ll send you a confirmation email shortly.');
const [elements, setElements] = useState({
order_details: true,
continue_shopping_button: true,
related_products: false,
});
useEffect(() => {
const loadSettings = async () => {
try {
const response = await api.get('/appearance/settings');
const thankyou = response.data?.pages?.thankyou;
if (thankyou) {
if (thankyou.template) setTemplate(thankyou.template);
if (thankyou.header_visibility) setHeaderVisibility(thankyou.header_visibility);
if (thankyou.footer_visibility) setFooterVisibility(thankyou.footer_visibility);
if (thankyou.background_color) setBackgroundColor(thankyou.background_color);
if (thankyou.custom_message) setCustomMessage(thankyou.custom_message);
if (thankyou.elements) setElements(thankyou.elements);
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
setLoading(false);
}
};
loadSettings();
}, []);
const toggleElement = (key: keyof typeof elements) => {
setElements({ ...elements, [key]: !elements[key] });
};
const handleSave = async () => {
try {
await api.post('/appearance/pages/thankyou', {
template,
header_visibility: headerVisibility,
footer_visibility: footerVisibility,
background_color: backgroundColor,
custom_message: customMessage,
elements,
});
toast.success('Thank you page settings saved successfully');
} catch (error) {
console.error('Save error:', error);
toast.error('Failed to save settings');
}
};
return (
<SettingsLayout
title="Thank You Page Settings"
onSave={handleSave}
isLoading={loading}
>
<SettingsCard
title="Template Style"
description="Choose the visual style for your thank you page"
>
<SettingsSection label="Template" htmlFor="template-style">
<RadioGroup value={template} onValueChange={setTemplate}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="basic" id="template-basic" />
<Label htmlFor="template-basic" className="cursor-pointer">
<div>
<div className="font-medium">Basic Style</div>
<div className="text-sm text-gray-500">Modern card-based layout with clean design</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="receipt" id="template-receipt" />
<Label htmlFor="template-receipt" className="cursor-pointer">
<div>
<div className="font-medium">Receipt Style</div>
<div className="text-sm text-gray-500">Classic receipt design with dotted lines and monospace font</div>
</div>
</Label>
</div>
</RadioGroup>
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Header & Footer"
description="Control header and footer visibility for focused order confirmation"
>
<SettingsSection label="Header Visibility" htmlFor="header-visibility">
<Select value={headerVisibility} onValueChange={setHeaderVisibility}>
<SelectTrigger id="header-visibility">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="show">Show Full Header</SelectItem>
<SelectItem value="minimal">Minimal (Logo Only)</SelectItem>
<SelectItem value="hide">Hide Completely</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-gray-500 mt-1">
Control main site header visibility on thank you page
</p>
</SettingsSection>
<SettingsSection label="Footer Visibility" htmlFor="footer-visibility">
<Select value={footerVisibility} onValueChange={setFooterVisibility}>
<SelectTrigger id="footer-visibility">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="show">Show Full Footer</SelectItem>
<SelectItem value="minimal">Minimal (Trust Badges & Policies)</SelectItem>
<SelectItem value="hide">Hide Completely</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-gray-500 mt-1">
Control main site footer visibility on thank you page
</p>
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Page Styling"
description="Customize the visual appearance of the thank you page"
>
<SettingsSection label="Background Color" htmlFor="background-color">
<div className="flex gap-2">
<Input
id="background-color"
type="color"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
className="w-20 h-10"
/>
<Input
type="text"
value={backgroundColor}
onChange={(e) => setBackgroundColor(e.target.value)}
placeholder="#f9fafb"
className="flex-1"
/>
</div>
<p className="text-sm text-gray-500 mt-1">
Set the background color for the thank you page
</p>
</SettingsSection>
</SettingsCard>
<SettingsCard
title="Elements"
description="Choose which elements to display on the thank you page"
>
<div className="flex items-center justify-between">
<Label htmlFor="element-orderDetails" className="cursor-pointer">
Show order details
</Label>
<Switch
id="element-order-details"
checked={elements.order_details}
onCheckedChange={() => toggleElement('order_details')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-continueShoppingButton" className="cursor-pointer">
Show continue shopping button
</Label>
<Switch
id="element-continue-shopping-button"
checked={elements.continue_shopping_button}
onCheckedChange={() => toggleElement('continue_shopping_button')}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="element-relatedProducts" className="cursor-pointer">
Show related products
</Label>
<Switch
id="element-related-products"
checked={elements.related_products}
onCheckedChange={() => toggleElement('related_products')}
/>
</div>
</SettingsCard>
<SettingsCard
title="Custom Message"
description="Add a personalized message for customers after purchase"
>
<SettingsSection label="Message" htmlFor="custom-message">
<Textarea
id="custom-message"
value={customMessage}
onChange={(e) => setCustomMessage(e.target.value)}
rows={4}
placeholder="Thank you for your order!"
/>
</SettingsSection>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,498 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { __ } from '@/lib/i18n';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Checkbox } from '@/components/ui/checkbox';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import { Loader2, Palette, Layout, Monitor, ShoppingCart, CheckCircle2, AlertCircle, Store, Zap, Sparkles } from 'lucide-react';
interface CustomerSPASettings {
mode: 'disabled' | 'full' | 'checkout_only';
checkoutPages?: {
checkout: boolean;
thankyou: boolean;
account: boolean;
cart: boolean;
};
layout: 'classic' | 'modern' | 'boutique' | 'launch';
colors: {
primary: string;
secondary: string;
accent: string;
};
typography: {
preset: string;
};
}
export default function CustomerSPASettings() {
const queryClient = useQueryClient();
// Fetch settings
const { data: settings, isLoading } = useQuery<CustomerSPASettings>({
queryKey: ['customer-spa-settings'],
queryFn: async () => {
const response = await fetch('/wp-json/woonoow/v1/settings/customer-spa', {
headers: {
'X-WP-Nonce': (window as any).WNW_API?.nonce || (window as any).wpApiSettings?.nonce || '',
},
credentials: 'same-origin',
});
if (!response.ok) throw new Error('Failed to fetch settings');
return response.json();
},
});
// Update settings mutation
const updateMutation = useMutation({
mutationFn: async (newSettings: Partial<CustomerSPASettings>) => {
const response = await fetch('/wp-json/woonoow/v1/settings/customer-spa', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': (window as any).WNW_API?.nonce || (window as any).wpApiSettings?.nonce || '',
},
credentials: 'same-origin',
body: JSON.stringify(newSettings),
});
if (!response.ok) throw new Error('Failed to update settings');
return response.json();
},
onSuccess: (data) => {
queryClient.setQueryData(['customer-spa-settings'], data.data);
toast.success(__('Settings saved successfully'));
},
onError: (error: any) => {
toast.error(error.message || __('Failed to save settings'));
},
});
const handleModeChange = (mode: string) => {
updateMutation.mutate({ mode: mode as any });
};
const handleLayoutChange = (layout: string) => {
updateMutation.mutate({ layout: layout as any });
};
const handleCheckoutPageToggle = (page: string, checked: boolean) => {
if (!settings) return;
const currentPages = settings.checkoutPages || {
checkout: true,
thankyou: true,
account: true,
cart: false,
};
updateMutation.mutate({
checkoutPages: {
...currentPages,
[page]: checked,
},
});
};
const handleColorChange = (colorKey: string, value: string) => {
if (!settings) return;
updateMutation.mutate({
colors: {
...settings.colors,
[colorKey]: value,
},
});
};
const handleTypographyChange = (preset: string) => {
updateMutation.mutate({
typography: {
preset,
},
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!settings) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
<p className="text-muted-foreground">{__('Failed to load settings')}</p>
</div>
</div>
);
}
return (
<div className="space-y-6 max-w-5xl mx-auto pb-8">
<div>
<h1 className="text-3xl font-bold">{__('Customer SPA')}</h1>
<p className="text-muted-foreground mt-2">
{__('Configure the modern React-powered storefront for your customers')}
</p>
</div>
<Separator />
{/* Mode Selection */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Monitor className="w-5 h-5" />
{__('Activation Mode')}
</CardTitle>
<CardDescription>
{__('Choose how WooNooW Customer SPA integrates with your site')}
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={settings.mode} onValueChange={handleModeChange}>
<div className="space-y-4">
{/* Disabled */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="disabled" id="mode-disabled" className="mt-1" />
<div className="flex-1">
<Label htmlFor="mode-disabled" className="font-semibold cursor-pointer">
{__('Disabled')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Use your own theme and page builder for the storefront. Only WooNooW Admin SPA will be active.')}
</p>
</div>
</div>
{/* Full SPA */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="full" id="mode-full" className="mt-1" />
<div className="flex-1">
<Label htmlFor="mode-full" className="font-semibold cursor-pointer">
{__('Full SPA')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('WooNooW takes over the entire storefront (Shop, Product, Cart, Checkout, Account pages).')}
</p>
{settings.mode === 'full' && (
<div className="mt-3 p-3 bg-primary/10 rounded-md">
<p className="text-sm font-medium text-primary">
{__('Active - Choose your layout below')}
</p>
</div>
)}
</div>
</div>
{/* Checkout Only */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="checkout_only" id="mode-checkout" className="mt-1" />
<div className="flex-1">
<Label htmlFor="mode-checkout" className="font-semibold cursor-pointer">
{__('Checkout Only')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('WooNooW only overrides checkout pages. Perfect for single product sellers with custom landing pages.')}
</p>
{settings.mode === 'checkout_only' && (
<div className="mt-3 space-y-3">
<p className="text-sm font-medium">{__('Pages to override:')}</p>
<div className="space-y-2 pl-4">
<div className="flex items-center space-x-2">
<Checkbox
id="page-checkout"
checked={settings.checkoutPages?.checkout}
onCheckedChange={(checked) => handleCheckoutPageToggle('checkout', checked as boolean)}
/>
<Label htmlFor="page-checkout" className="cursor-pointer">
{__('Checkout')}
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="page-thankyou"
checked={settings.checkoutPages?.thankyou}
onCheckedChange={(checked) => handleCheckoutPageToggle('thankyou', checked as boolean)}
/>
<Label htmlFor="page-thankyou" className="cursor-pointer">
{__('Thank You (Order Received)')}
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="page-account"
checked={settings.checkoutPages?.account}
onCheckedChange={(checked) => handleCheckoutPageToggle('account', checked as boolean)}
/>
<Label htmlFor="page-account" className="cursor-pointer">
{__('My Account')}
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="page-cart"
checked={settings.checkoutPages?.cart}
onCheckedChange={(checked) => handleCheckoutPageToggle('cart', checked as boolean)}
/>
<Label htmlFor="page-cart" className="cursor-pointer">
{__('Cart (Optional)')}
</Label>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</RadioGroup>
</CardContent>
</Card>
{/* Layout Selection - Only show if Full SPA is active */}
{settings.mode === 'full' && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Layout className="w-5 h-5" />
{__('Layout')}
</CardTitle>
<CardDescription>
{__('Choose a master layout for your storefront')}
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={settings.layout} onValueChange={handleLayoutChange}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Classic */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="classic" id="layout-classic" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-classic" className="font-semibold cursor-pointer flex items-center gap-2">
<Store className="w-4 h-4" />
{__('Classic')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Traditional ecommerce with sidebar filters. Best for B2B and traditional retail.')}
</p>
</div>
</div>
{/* Modern */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="modern" id="layout-modern" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-modern" className="font-semibold cursor-pointer flex items-center gap-2">
<Sparkles className="w-4 h-4" />
{__('Modern')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Minimalist design with large product cards. Best for fashion and lifestyle brands.')}
</p>
</div>
</div>
{/* Boutique */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="boutique" id="layout-boutique" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-boutique" className="font-semibold cursor-pointer flex items-center gap-2">
<Sparkles className="w-4 h-4" />
{__('Boutique')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Luxury-focused with masonry grid. Best for high-end fashion and luxury goods.')}
</p>
</div>
</div>
{/* Launch */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="launch" id="layout-launch" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-launch" className="font-semibold cursor-pointer flex items-center gap-2">
<Zap className="w-4 h-4" />
{__('Launch')} <span className="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded">NEW</span>
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Single product funnel. Best for digital products, courses, and product launches.')}
</p>
<p className="text-xs text-muted-foreground mt-2 italic">
{__('Note: Landing page uses your page builder. WooNooW takes over from checkout onwards.')}
</p>
</div>
</div>
</div>
</RadioGroup>
</CardContent>
</Card>
)}
{/* Color Customization - Show if Full SPA or Checkout Only is active */}
{(settings.mode === 'full' || settings.mode === 'checkout_only') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="w-5 h-5" />
{__('Colors')}
</CardTitle>
<CardDescription>
{__('Customize your brand colors')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Primary Color */}
<div className="space-y-2">
<Label htmlFor="color-primary">{__('Primary Color')}</Label>
<div className="flex items-center gap-2">
<Input
type="color"
id="color-primary"
value={settings.colors.primary}
onChange={(e) => handleColorChange('primary', e.target.value)}
className="w-16 h-10 p-1 cursor-pointer"
/>
<Input
type="text"
value={settings.colors.primary}
onChange={(e) => handleColorChange('primary', e.target.value)}
className="flex-1 font-mono text-sm"
placeholder="#3B82F6"
/>
</div>
<p className="text-xs text-muted-foreground">
{__('Buttons, links, active states')}
</p>
</div>
{/* Secondary Color */}
<div className="space-y-2">
<Label htmlFor="color-secondary">{__('Secondary Color')}</Label>
<div className="flex items-center gap-2">
<Input
type="color"
id="color-secondary"
value={settings.colors.secondary}
onChange={(e) => handleColorChange('secondary', e.target.value)}
className="w-16 h-10 p-1 cursor-pointer"
/>
<Input
type="text"
value={settings.colors.secondary}
onChange={(e) => handleColorChange('secondary', e.target.value)}
className="flex-1 font-mono text-sm"
placeholder="#8B5CF6"
/>
</div>
<p className="text-xs text-muted-foreground">
{__('Badges, accents, secondary buttons')}
</p>
</div>
{/* Accent Color */}
<div className="space-y-2">
<Label htmlFor="color-accent">{__('Accent Color')}</Label>
<div className="flex items-center gap-2">
<Input
type="color"
id="color-accent"
value={settings.colors.accent}
onChange={(e) => handleColorChange('accent', e.target.value)}
className="w-16 h-10 p-1 cursor-pointer"
/>
<Input
type="text"
value={settings.colors.accent}
onChange={(e) => handleColorChange('accent', e.target.value)}
className="flex-1 font-mono text-sm"
placeholder="#10B981"
/>
</div>
<p className="text-xs text-muted-foreground">
{__('Success states, CTAs, highlights')}
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Typography - Show if Full SPA is active */}
{settings.mode === 'full' && (
<Card>
<CardHeader>
<CardTitle>{__('Typography')}</CardTitle>
<CardDescription>
{__('Choose a font pairing for your storefront')}
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={settings.typography.preset} onValueChange={handleTypographyChange}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="professional" id="typo-professional" />
<Label htmlFor="typo-professional" className="cursor-pointer flex-1">
<div className="font-semibold">Professional</div>
<div className="text-sm text-muted-foreground">Inter + Lora</div>
</Label>
</div>
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="modern" id="typo-modern" />
<Label htmlFor="typo-modern" className="cursor-pointer flex-1">
<div className="font-semibold">Modern</div>
<div className="text-sm text-muted-foreground">Poppins + Roboto</div>
</Label>
</div>
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="elegant" id="typo-elegant" />
<Label htmlFor="typo-elegant" className="cursor-pointer flex-1">
<div className="font-semibold">Elegant</div>
<div className="text-sm text-muted-foreground">Playfair Display + Source Sans</div>
</Label>
</div>
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="tech" id="typo-tech" />
<Label htmlFor="typo-tech" className="cursor-pointer flex-1">
<div className="font-semibold">Tech</div>
<div className="text-sm text-muted-foreground">Space Grotesk + IBM Plex Mono</div>
</Label>
</div>
</div>
</RadioGroup>
</CardContent>
</Card>
)}
{/* Info Card */}
{settings.mode !== 'disabled' && (
<Card className="bg-primary/5 border-primary/20">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-primary mt-0.5" />
<div>
<p className="font-medium text-primary mb-1">
{__('Customer SPA is Active')}
</p>
<p className="text-sm text-muted-foreground">
{settings.mode === 'full'
? __('Your storefront is now powered by WooNooW React SPA. Visit your shop to see the changes.')
: __('Checkout pages are now powered by WooNooW React SPA. Create your custom landing page and link the CTA to /checkout.')}
</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,13 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export default function AppearanceIndex() {
const navigate = useNavigate();
useEffect(() => {
// Redirect to General as the default appearance page
navigate('/appearance/general', { replace: true });
}, [navigate]);
return null;
}

View File

@@ -173,6 +173,15 @@ export default function CouponsIndex() {
<RefreshCw className="w-4 h-4" />
{__('Refresh')}
</button>
{/* New Coupon - Desktop only */}
<button
className="border rounded-md px-3 py-2 text-sm bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-2"
onClick={() => navigate('/coupons/new')}
>
<Tag className="w-4 h-4" />
{__('New Coupon')}
</button>
</div>
{/* Right: Filters */}

View File

@@ -0,0 +1,126 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { toast } from 'sonner';
import { api } from '@/lib/api';
import { ArrowLeft, Save } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
export default function EmailTemplates() {
const navigate = useNavigate();
const { template } = useParams();
const queryClient = useQueryClient();
const [subject, setSubject] = useState('');
const [content, setContent] = useState('');
const { data: templateData, isLoading } = useQuery({
queryKey: ['newsletter-template', template],
queryFn: async () => {
const response = await api.get(`/newsletter/template/${template}`);
return response.data;
},
enabled: !!template,
});
React.useEffect(() => {
if (templateData) {
setSubject(templateData.subject || '');
setContent(templateData.content || '');
}
}, [templateData]);
const saveTemplate = useMutation({
mutationFn: async () => {
await api.post(`/newsletter/template/${template}`, {
subject,
content,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['newsletter-template'] });
toast.success('Template saved successfully');
},
onError: () => {
toast.error('Failed to save template');
},
});
const handleSave = () => {
saveTemplate.mutate();
};
return (
<SettingsLayout
title={`Edit ${template === 'welcome' ? 'Welcome' : 'Confirmation'} Email Template`}
description="Customize the email template sent to newsletter subscribers"
>
<div className="mb-4">
<Button variant="ghost" onClick={() => navigate('/marketing/newsletter')}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Newsletter
</Button>
</div>
<SettingsCard
title="Email Template"
description="Use variables like {site_name}, {email}, {unsubscribe_url}"
>
<div className="space-y-4">
<div>
<Label htmlFor="subject">Email Subject</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Welcome to {site_name} Newsletter!"
/>
</div>
<div>
<Label htmlFor="content">Email Content</Label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
rows={15}
placeholder="Thank you for subscribing to our newsletter!&#10;&#10;You'll receive updates about our latest products and offers.&#10;&#10;Best regards,&#10;{site_name}"
/>
<p className="text-sm text-muted-foreground mt-2">
Available variables: <code>{'{site_name}'}</code>, <code>{'{email}'}</code>, <code>{'{unsubscribe_url}'}</code>
</p>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => navigate('/marketing/newsletter')}>
Cancel
</Button>
<Button onClick={handleSave} disabled={saveTemplate.isPending}>
<Save className="mr-2 h-4 w-4" />
{saveTemplate.isPending ? 'Saving...' : 'Save Template'}
</Button>
</div>
</div>
</SettingsCard>
<SettingsCard
title="Preview"
description="Preview how your email will look"
>
<div className="border rounded-lg p-6 bg-muted/50">
<div className="mb-4">
<strong>Subject:</strong> {subject.replace('{site_name}', 'Your Store')}
</div>
<div className="whitespace-pre-wrap">
{content.replace('{site_name}', 'Your Store').replace('{email}', 'customer@example.com')}
</div>
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,201 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout';
import { SettingsCard } from '@/routes/Settings/components/SettingsCard';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Download, Trash2, Mail, Search } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/lib/api';
import { useNavigate } from 'react-router-dom';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
export default function NewsletterSubscribers() {
const [searchQuery, setSearchQuery] = useState('');
const queryClient = useQueryClient();
const navigate = useNavigate();
const { data: subscribersData, isLoading } = useQuery({
queryKey: ['newsletter-subscribers'],
queryFn: async () => {
const response = await api.get('/newsletter/subscribers');
return response.data;
},
});
const deleteSubscriber = useMutation({
mutationFn: async (email: string) => {
await api.delete(`/newsletter/subscribers/${encodeURIComponent(email)}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['newsletter-subscribers'] });
toast.success('Subscriber removed successfully');
},
onError: () => {
toast.error('Failed to remove subscriber');
},
});
const exportSubscribers = () => {
if (!subscribersData?.subscribers) return;
const csv = ['Email,Subscribed Date'].concat(
subscribersData.subscribers.map((sub: any) =>
`${sub.email},${sub.subscribed_at || 'N/A'}`
)
).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `newsletter-subscribers-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
window.URL.revokeObjectURL(url);
};
const subscribers = subscribersData?.subscribers || [];
const filteredSubscribers = subscribers.filter((sub: any) =>
sub.email.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<SettingsLayout
title="Newsletter Subscribers"
description="Manage your newsletter subscribers and send campaigns"
>
<SettingsCard
title="Subscribers List"
description={`Total subscribers: ${subscribersData?.count || 0}`}
>
<div className="space-y-4">
{/* Actions Bar */}
<div className="flex items-center justify-between gap-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
<Input
placeholder="Search by email..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="flex gap-2">
<Button onClick={exportSubscribers} variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Export CSV
</Button>
<Button variant="outline" size="sm">
<Mail className="mr-2 h-4 w-4" />
Send Campaign
</Button>
</div>
</div>
{/* Subscribers Table */}
{isLoading ? (
<div className="text-center py-8 text-muted-foreground">
Loading subscribers...
</div>
) : filteredSubscribers.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{searchQuery ? 'No subscribers found matching your search' : 'No subscribers yet'}
</div>
) : (
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead>Subscribed Date</TableHead>
<TableHead>WP User</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredSubscribers.map((subscriber: any) => (
<TableRow key={subscriber.email}>
<TableCell className="font-medium">{subscriber.email}</TableCell>
<TableCell>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
{subscriber.status || 'Active'}
</span>
</TableCell>
<TableCell className="text-muted-foreground">
{subscriber.subscribed_at
? new Date(subscriber.subscribed_at).toLocaleDateString()
: 'N/A'
}
</TableCell>
<TableCell>
{subscriber.user_id ? (
<span className="text-xs text-blue-600">Yes (ID: {subscriber.user_id})</span>
) : (
<span className="text-xs text-muted-foreground">No</span>
)}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={() => deleteSubscriber.mutate(subscriber.email)}
disabled={deleteSubscriber.isPending}
>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
</SettingsCard>
{/* Email Template Settings */}
<SettingsCard
title="Email Templates"
description="Customize newsletter email templates using the email builder"
>
<div className="space-y-4">
<div className="p-4 border rounded-lg bg-muted/50">
<h4 className="font-medium mb-2">Newsletter Welcome Email</h4>
<p className="text-sm text-muted-foreground mb-4">
Welcome email sent when someone subscribes to your newsletter
</p>
<Button
variant="outline"
size="sm"
onClick={() => navigate('/settings/notifications/customer/newsletter_welcome/edit')}
>
Edit Template
</Button>
</div>
<div className="p-4 border rounded-lg bg-muted/50">
<h4 className="font-medium mb-2">New Subscriber Notification (Admin)</h4>
<p className="text-sm text-muted-foreground mb-4">
Admin notification when someone subscribes to newsletter
</p>
<Button
variant="outline"
size="sm"
onClick={() => navigate('/settings/notifications/staff/newsletter_subscribed_admin/edit')}
>
Edit Template
</Button>
</div>
</div>
</SettingsCard>
</SettingsLayout>
);
}

View File

@@ -0,0 +1,5 @@
import { Navigate } from 'react-router-dom';
export default function Marketing() {
return <Navigate to="/marketing/newsletter" replace />;
}

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Tag, Settings as SettingsIcon, ChevronRight, Minimize2, LogOut, Sun, Moon, Monitor, ExternalLink } from 'lucide-react';
import { Tag, Settings as SettingsIcon, Palette, ChevronRight, Minimize2, LogOut, Sun, Moon, Monitor, ExternalLink } from 'lucide-react';
import { __ } from '@/lib/i18n';
import { usePageHeader } from '@/contexts/PageHeaderContext';
import { useApp } from '@/contexts/AppContext';
@@ -21,6 +21,12 @@ const menuItems: MenuItem[] = [
description: __('Manage discount codes and promotions'),
to: '/coupons'
},
{
icon: <Palette className="w-5 h-5" />,
label: __('Appearance'),
description: __('Customize your store appearance'),
to: '/appearance'
},
{
icon: <SettingsIcon className="w-5 h-5" />,
label: __('Settings'),

View File

@@ -1,498 +0,0 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { __ } from '@/lib/i18n';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Checkbox } from '@/components/ui/checkbox';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import { Loader2, Palette, Layout, Monitor, ShoppingCart, CheckCircle2, AlertCircle, Store, Zap, Sparkles } from 'lucide-react';
interface CustomerSPASettings {
mode: 'disabled' | 'full' | 'checkout_only';
checkoutPages?: {
checkout: boolean;
thankyou: boolean;
account: boolean;
cart: boolean;
};
layout: 'classic' | 'modern' | 'boutique' | 'launch';
colors: {
primary: string;
secondary: string;
accent: string;
};
typography: {
preset: string;
};
}
export default function CustomerSPASettings() {
const queryClient = useQueryClient();
// Fetch settings
const { data: settings, isLoading } = useQuery<CustomerSPASettings>({
queryKey: ['customer-spa-settings'],
queryFn: async () => {
const response = await fetch('/wp-json/woonoow/v1/settings/customer-spa', {
headers: {
'X-WP-Nonce': (window as any).WNW_API?.nonce || (window as any).wpApiSettings?.nonce || '',
},
credentials: 'same-origin',
});
if (!response.ok) throw new Error('Failed to fetch settings');
return response.json();
},
});
// Update settings mutation
const updateMutation = useMutation({
mutationFn: async (newSettings: Partial<CustomerSPASettings>) => {
const response = await fetch('/wp-json/woonoow/v1/settings/customer-spa', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': (window as any).WNW_API?.nonce || (window as any).wpApiSettings?.nonce || '',
},
credentials: 'same-origin',
body: JSON.stringify(newSettings),
});
if (!response.ok) throw new Error('Failed to update settings');
return response.json();
},
onSuccess: (data) => {
queryClient.setQueryData(['customer-spa-settings'], data.data);
toast.success(__('Settings saved successfully'));
},
onError: (error: any) => {
toast.error(error.message || __('Failed to save settings'));
},
});
const handleModeChange = (mode: string) => {
updateMutation.mutate({ mode: mode as any });
};
const handleLayoutChange = (layout: string) => {
updateMutation.mutate({ layout: layout as any });
};
const handleCheckoutPageToggle = (page: string, checked: boolean) => {
if (!settings) return;
const currentPages = settings.checkoutPages || {
checkout: true,
thankyou: true,
account: true,
cart: false,
};
updateMutation.mutate({
checkoutPages: {
...currentPages,
[page]: checked,
},
});
};
const handleColorChange = (colorKey: string, value: string) => {
if (!settings) return;
updateMutation.mutate({
colors: {
...settings.colors,
[colorKey]: value,
},
});
};
const handleTypographyChange = (preset: string) => {
updateMutation.mutate({
typography: {
preset,
},
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!settings) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
<p className="text-muted-foreground">{__('Failed to load settings')}</p>
</div>
</div>
);
}
return (
<div className="space-y-6 max-w-4xl pb-8">
<div>
<h1 className="text-3xl font-bold">{__('Customer SPA')}</h1>
<p className="text-muted-foreground mt-2">
{__('Configure the modern React-powered storefront for your customers')}
</p>
</div>
<Separator />
{/* Mode Selection */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Monitor className="w-5 h-5" />
{__('Activation Mode')}
</CardTitle>
<CardDescription>
{__('Choose how WooNooW Customer SPA integrates with your site')}
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={settings.mode} onValueChange={handleModeChange}>
<div className="space-y-4">
{/* Disabled */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="disabled" id="mode-disabled" className="mt-1" />
<div className="flex-1">
<Label htmlFor="mode-disabled" className="font-semibold cursor-pointer">
{__('Disabled')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Use your own theme and page builder for the storefront. Only WooNooW Admin SPA will be active.')}
</p>
</div>
</div>
{/* Full SPA */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="full" id="mode-full" className="mt-1" />
<div className="flex-1">
<Label htmlFor="mode-full" className="font-semibold cursor-pointer">
{__('Full SPA')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('WooNooW takes over the entire storefront (Shop, Product, Cart, Checkout, Account pages).')}
</p>
{settings.mode === 'full' && (
<div className="mt-3 p-3 bg-primary/10 rounded-md">
<p className="text-sm font-medium text-primary">
{__('Active - Choose your layout below')}
</p>
</div>
)}
</div>
</div>
{/* Checkout Only */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="checkout_only" id="mode-checkout" className="mt-1" />
<div className="flex-1">
<Label htmlFor="mode-checkout" className="font-semibold cursor-pointer">
{__('Checkout Only')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('WooNooW only overrides checkout pages. Perfect for single product sellers with custom landing pages.')}
</p>
{settings.mode === 'checkout_only' && (
<div className="mt-3 space-y-3">
<p className="text-sm font-medium">{__('Pages to override:')}</p>
<div className="space-y-2 pl-4">
<div className="flex items-center space-x-2">
<Checkbox
id="page-checkout"
checked={settings.checkoutPages?.checkout}
onCheckedChange={(checked) => handleCheckoutPageToggle('checkout', checked as boolean)}
/>
<Label htmlFor="page-checkout" className="cursor-pointer">
{__('Checkout')}
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="page-thankyou"
checked={settings.checkoutPages?.thankyou}
onCheckedChange={(checked) => handleCheckoutPageToggle('thankyou', checked as boolean)}
/>
<Label htmlFor="page-thankyou" className="cursor-pointer">
{__('Thank You (Order Received)')}
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="page-account"
checked={settings.checkoutPages?.account}
onCheckedChange={(checked) => handleCheckoutPageToggle('account', checked as boolean)}
/>
<Label htmlFor="page-account" className="cursor-pointer">
{__('My Account')}
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="page-cart"
checked={settings.checkoutPages?.cart}
onCheckedChange={(checked) => handleCheckoutPageToggle('cart', checked as boolean)}
/>
<Label htmlFor="page-cart" className="cursor-pointer">
{__('Cart (Optional)')}
</Label>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</RadioGroup>
</CardContent>
</Card>
{/* Layout Selection - Only show if Full SPA is active */}
{settings.mode === 'full' && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Layout className="w-5 h-5" />
{__('Layout')}
</CardTitle>
<CardDescription>
{__('Choose a master layout for your storefront')}
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={settings.layout} onValueChange={handleLayoutChange}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Classic */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="classic" id="layout-classic" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-classic" className="font-semibold cursor-pointer flex items-center gap-2">
<Store className="w-4 h-4" />
{__('Classic')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Traditional ecommerce with sidebar filters. Best for B2B and traditional retail.')}
</p>
</div>
</div>
{/* Modern */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="modern" id="layout-modern" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-modern" className="font-semibold cursor-pointer flex items-center gap-2">
<Sparkles className="w-4 h-4" />
{__('Modern')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Minimalist design with large product cards. Best for fashion and lifestyle brands.')}
</p>
</div>
</div>
{/* Boutique */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="boutique" id="layout-boutique" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-boutique" className="font-semibold cursor-pointer flex items-center gap-2">
<Sparkles className="w-4 h-4" />
{__('Boutique')}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Luxury-focused with masonry grid. Best for high-end fashion and luxury goods.')}
</p>
</div>
</div>
{/* Launch */}
<div className="flex items-start space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="launch" id="layout-launch" className="mt-1" />
<div className="flex-1">
<Label htmlFor="layout-launch" className="font-semibold cursor-pointer flex items-center gap-2">
<Zap className="w-4 h-4" />
{__('Launch')} <span className="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded">NEW</span>
</Label>
<p className="text-sm text-muted-foreground mt-1">
{__('Single product funnel. Best for digital products, courses, and product launches.')}
</p>
<p className="text-xs text-muted-foreground mt-2 italic">
{__('Note: Landing page uses your page builder. WooNooW takes over from checkout onwards.')}
</p>
</div>
</div>
</div>
</RadioGroup>
</CardContent>
</Card>
)}
{/* Color Customization - Show if Full SPA or Checkout Only is active */}
{(settings.mode === 'full' || settings.mode === 'checkout_only') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="w-5 h-5" />
{__('Colors')}
</CardTitle>
<CardDescription>
{__('Customize your brand colors')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Primary Color */}
<div className="space-y-2">
<Label htmlFor="color-primary">{__('Primary Color')}</Label>
<div className="flex items-center gap-2">
<Input
type="color"
id="color-primary"
value={settings.colors.primary}
onChange={(e) => handleColorChange('primary', e.target.value)}
className="w-16 h-10 p-1 cursor-pointer"
/>
<Input
type="text"
value={settings.colors.primary}
onChange={(e) => handleColorChange('primary', e.target.value)}
className="flex-1 font-mono text-sm"
placeholder="#3B82F6"
/>
</div>
<p className="text-xs text-muted-foreground">
{__('Buttons, links, active states')}
</p>
</div>
{/* Secondary Color */}
<div className="space-y-2">
<Label htmlFor="color-secondary">{__('Secondary Color')}</Label>
<div className="flex items-center gap-2">
<Input
type="color"
id="color-secondary"
value={settings.colors.secondary}
onChange={(e) => handleColorChange('secondary', e.target.value)}
className="w-16 h-10 p-1 cursor-pointer"
/>
<Input
type="text"
value={settings.colors.secondary}
onChange={(e) => handleColorChange('secondary', e.target.value)}
className="flex-1 font-mono text-sm"
placeholder="#8B5CF6"
/>
</div>
<p className="text-xs text-muted-foreground">
{__('Badges, accents, secondary buttons')}
</p>
</div>
{/* Accent Color */}
<div className="space-y-2">
<Label htmlFor="color-accent">{__('Accent Color')}</Label>
<div className="flex items-center gap-2">
<Input
type="color"
id="color-accent"
value={settings.colors.accent}
onChange={(e) => handleColorChange('accent', e.target.value)}
className="w-16 h-10 p-1 cursor-pointer"
/>
<Input
type="text"
value={settings.colors.accent}
onChange={(e) => handleColorChange('accent', e.target.value)}
className="flex-1 font-mono text-sm"
placeholder="#10B981"
/>
</div>
<p className="text-xs text-muted-foreground">
{__('Success states, CTAs, highlights')}
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Typography - Show if Full SPA is active */}
{settings.mode === 'full' && (
<Card>
<CardHeader>
<CardTitle>{__('Typography')}</CardTitle>
<CardDescription>
{__('Choose a font pairing for your storefront')}
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={settings.typography.preset} onValueChange={handleTypographyChange}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="professional" id="typo-professional" />
<Label htmlFor="typo-professional" className="cursor-pointer flex-1">
<div className="font-semibold">Professional</div>
<div className="text-sm text-muted-foreground">Inter + Lora</div>
</Label>
</div>
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="modern" id="typo-modern" />
<Label htmlFor="typo-modern" className="cursor-pointer flex-1">
<div className="font-semibold">Modern</div>
<div className="text-sm text-muted-foreground">Poppins + Roboto</div>
</Label>
</div>
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="elegant" id="typo-elegant" />
<Label htmlFor="typo-elegant" className="cursor-pointer flex-1">
<div className="font-semibold">Elegant</div>
<div className="text-sm text-muted-foreground">Playfair Display + Source Sans</div>
</Label>
</div>
<div className="flex items-center space-x-3 p-4 border rounded-lg hover:bg-accent/50 transition-colors">
<RadioGroupItem value="tech" id="typo-tech" />
<Label htmlFor="typo-tech" className="cursor-pointer flex-1">
<div className="font-semibold">Tech</div>
<div className="text-sm text-muted-foreground">Space Grotesk + IBM Plex Mono</div>
</Label>
</div>
</div>
</RadioGroup>
</CardContent>
</Card>
)}
{/* Info Card */}
{settings.mode !== 'disabled' && (
<Card className="bg-primary/5 border-primary/20">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-primary mt-0.5" />
<div>
<p className="font-medium text-primary mb-1">
{__('Customer SPA is Active')}
</p>
<p className="text-sm text-muted-foreground">
{settings.mode === 'full'
? __('Your storefront is now powered by WooNooW React SPA. Visit your shop to see the changes.')
: __('Checkout pages are now powered by WooNooW React SPA. Create your custom landing page and link the CTA to /checkout.')}
</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
}