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:
145
admin-spa/src/routes/Appearance/Account.tsx
Normal file
145
admin-spa/src/routes/Appearance/Account.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
155
admin-spa/src/routes/Appearance/Cart.tsx
Normal file
155
admin-spa/src/routes/Appearance/Cart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
256
admin-spa/src/routes/Appearance/Checkout.tsx
Normal file
256
admin-spa/src/routes/Appearance/Checkout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
463
admin-spa/src/routes/Appearance/Footer.tsx
Normal file
463
admin-spa/src/routes/Appearance/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
280
admin-spa/src/routes/Appearance/General.tsx
Normal file
280
admin-spa/src/routes/Appearance/General.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
214
admin-spa/src/routes/Appearance/Header.tsx
Normal file
214
admin-spa/src/routes/Appearance/Header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
278
admin-spa/src/routes/Appearance/Product.tsx
Normal file
278
admin-spa/src/routes/Appearance/Product.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
302
admin-spa/src/routes/Appearance/Shop.tsx
Normal file
302
admin-spa/src/routes/Appearance/Shop.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
225
admin-spa/src/routes/Appearance/ThankYou.tsx
Normal file
225
admin-spa/src/routes/Appearance/ThankYou.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
498
admin-spa/src/routes/Appearance/Themes.tsx
Normal file
498
admin-spa/src/routes/Appearance/Themes.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
admin-spa/src/routes/Appearance/index.tsx
Normal file
13
admin-spa/src/routes/Appearance/index.tsx
Normal 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;
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
126
admin-spa/src/routes/Marketing/EmailTemplates.tsx
Normal file
126
admin-spa/src/routes/Marketing/EmailTemplates.tsx
Normal 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! You'll receive updates about our latest products and offers. Best regards, {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>
|
||||
);
|
||||
}
|
||||
201
admin-spa/src/routes/Marketing/Newsletter.tsx
Normal file
201
admin-spa/src/routes/Marketing/Newsletter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
admin-spa/src/routes/Marketing/index.tsx
Normal file
5
admin-spa/src/routes/Marketing/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
export default function Marketing() {
|
||||
return <Navigate to="/marketing/newsletter" replace />;
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user