feat: Add product images support with WP Media Library integration
- Add WP Media Library integration for product and variation images - Support images array (URLs) conversion to attachment IDs - Add images array to API responses (Admin & Customer SPA) - Implement drag-and-drop sortable images in Admin product form - Add image gallery thumbnails in Customer SPA product page - Initialize WooCommerce session for guest cart operations - Fix product variations and attributes display in Customer SPA - Add variation image field in Admin SPA Changes: - includes/Api/ProductsController.php: Handle images array, add to responses - includes/Frontend/ShopController.php: Add images array for customer SPA - includes/Frontend/CartController.php: Initialize WC session for guests - admin-spa/src/lib/wp-media.ts: Add openWPMediaGallery function - admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx: WP Media + sortable images - admin-spa/src/routes/Products/partials/tabs/VariationsTab.tsx: Add variation image field - customer-spa/src/pages/Product/index.tsx: Add gallery thumbnails display
This commit is contained in:
@@ -193,6 +193,8 @@ export function ProductFormTabbed({
|
||||
setDownloadable={setDownloadable}
|
||||
featured={featured}
|
||||
setFeatured={setFeatured}
|
||||
images={images}
|
||||
setImages={setImages}
|
||||
sku={sku}
|
||||
setSku={setSku}
|
||||
regularPrice={regularPrice}
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { DollarSign } from 'lucide-react';
|
||||
import { DollarSign, Upload, X, Image as ImageIcon } from 'lucide-react';
|
||||
import { getStoreCurrency } from '@/lib/currency';
|
||||
import { RichTextEditor } from '@/components/RichTextEditor';
|
||||
import { openWPMediaGallery } from '@/lib/wp-media';
|
||||
|
||||
type GeneralTabProps = {
|
||||
name: string;
|
||||
@@ -28,6 +30,9 @@ type GeneralTabProps = {
|
||||
setDownloadable: (value: boolean) => void;
|
||||
featured: boolean;
|
||||
setFeatured: (value: boolean) => void;
|
||||
// Images
|
||||
images: string[];
|
||||
setImages: (value: string[]) => void;
|
||||
// Pricing props
|
||||
sku: string;
|
||||
setSku: (value: string) => void;
|
||||
@@ -54,6 +59,8 @@ export function GeneralTab({
|
||||
setDownloadable,
|
||||
featured,
|
||||
setFeatured,
|
||||
images,
|
||||
setImages,
|
||||
sku,
|
||||
setSku,
|
||||
regularPrice,
|
||||
@@ -167,6 +174,97 @@ export function GeneralTab({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Product Images */}
|
||||
<Separator />
|
||||
<div>
|
||||
<Label>{__('Product Images')}</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1 mb-3">
|
||||
{__('First image will be the featured image. Drag to reorder.')}
|
||||
</p>
|
||||
|
||||
{/* Image Upload Button */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
openWPMediaGallery((files) => {
|
||||
const newImages = files.map(file => file.url);
|
||||
setImages([...images, ...newImages]);
|
||||
});
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{__('Add Images from Media Library')}
|
||||
</Button>
|
||||
|
||||
{/* Image Preview Grid - Sortable */}
|
||||
{images.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{images.map((image, index) => (
|
||||
<div
|
||||
key={index}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', index.toString());
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'));
|
||||
const toIndex = index;
|
||||
|
||||
if (fromIndex !== toIndex) {
|
||||
const newImages = [...images];
|
||||
const [movedImage] = newImages.splice(fromIndex, 1);
|
||||
newImages.splice(toIndex, 0, movedImage);
|
||||
setImages(newImages);
|
||||
}
|
||||
}}
|
||||
className="relative group aspect-square border rounded-lg overflow-hidden bg-gray-50 cursor-move hover:border-primary transition-colors"
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`Product ${index + 1}`}
|
||||
className="w-full h-full object-cover pointer-events-none"
|
||||
/>
|
||||
{index === 0 && (
|
||||
<div className="absolute top-2 left-2 bg-primary text-primary-foreground text-xs px-2 py-1 rounded font-medium">
|
||||
{__('Featured')}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newImages = images.filter((_, i) => i !== index);
|
||||
setImages(newImages);
|
||||
}}
|
||||
className="absolute top-2 right-2 bg-red-600 text-white p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-700"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
<div className="absolute bottom-2 left-2 bg-black/50 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{__('Drag to reorder')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{images.length === 0 && (
|
||||
<div className="border-2 border-dashed rounded-lg p-8 text-center text-muted-foreground">
|
||||
<ImageIcon className="mx-auto h-12 w-12 mb-2 opacity-50" />
|
||||
<p className="text-sm">{__('No images uploaded yet')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -7,9 +7,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Plus, X, Layers } from 'lucide-react';
|
||||
import { Plus, X, Layers, Image as ImageIcon } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getStoreCurrency } from '@/lib/currency';
|
||||
import { openWPMediaImage } from '@/lib/wp-media';
|
||||
|
||||
export type ProductVariant = {
|
||||
id?: number;
|
||||
@@ -20,6 +21,7 @@ export type ProductVariant = {
|
||||
stock_quantity?: number;
|
||||
manage_stock?: boolean;
|
||||
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
|
||||
image?: string;
|
||||
};
|
||||
|
||||
type VariationsTabProps = {
|
||||
@@ -210,6 +212,44 @@ export function VariationsTab({
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{/* Variation Image */}
|
||||
<div className="mb-3">
|
||||
<Label className="text-xs">{__('Variation Image (Optional)')}</Label>
|
||||
<div className="flex gap-2 mt-1.5">
|
||||
{variation.image ? (
|
||||
<div className="relative w-16 h-16 border rounded overflow-hidden group">
|
||||
<img src={variation.image} alt="Variation" className="w-full h-full object-cover" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const updated = [...variations];
|
||||
updated[index].image = undefined;
|
||||
setVariations(updated);
|
||||
}}
|
||||
className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
||||
>
|
||||
<X className="h-4 w-4 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
openWPMediaImage((file) => {
|
||||
const updated = [...variations];
|
||||
updated[index].image = file.url;
|
||||
setVariations(updated);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ImageIcon className="mr-2 h-3 w-3" />
|
||||
{__('Add Image')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Input
|
||||
placeholder={__('SKU')}
|
||||
|
||||
498
admin-spa/src/routes/Settings/CustomerSPA.tsx
Normal file
498
admin-spa/src/routes/Settings/CustomerSPA.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-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