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:
@@ -1,26 +1,26 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZTCCAs2gAwIBAgIQF1GMfemibsRXEX4zKsPLuTANBgkqhkiG9w0BAQsFADCB
|
||||
lzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTYwNAYDVQQLDC1kd2lu
|
||||
ZG93bkBvYWppc2RoYS1pby5sb2NhbCAoRHdpbmRpIFJhbWFkaGFuYSkxPTA7BgNV
|
||||
BAMMNG1rY2VydCBkd2luZG93bkBvYWppc2RoYS1pby5sb2NhbCAoRHdpbmRpIFJh
|
||||
bWFkaGFuYSkwHhcNMjUxMDI0MTAzMTMxWhcNMjgwMTI0MTAzMTMxWjBhMScwJQYD
|
||||
VQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxNjA0BgNVBAsMLWR3
|
||||
aW5kb3duQG9hamlzZGhhLWlvLmxvY2FsIChEd2luZGkgUmFtYWRoYW5hKTCCASIw
|
||||
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALt22AwSay07IFZanpCHO418klWC
|
||||
KWnQw4iIrGW81hFQMCHsplDlweAN4mIO7qJsP/wtpTKDg7/h1oXLDOkvdYOwgVIq
|
||||
4dZZ0YUXe7UC8dJvFD4Y9/BBRTQoJGcErKYF8yq8Sc8suGfwo0C15oeb4Nsh/U9c
|
||||
bCNvCHWowyF0VGY/r0rNg88xeVPZbfvlaEaGCiH4D3BO+h8h9E7qtUMTRGNEnA/0
|
||||
4jNs2S7QWmjaFobYAv2PmU5LBWYjTIoCW8v/5yRU5lVyuI9YFhtqekGR3b9OJVgG
|
||||
ijqIJevC28+7/EmZXBUthwJksQFyb60WCnd8LpVrLIqkEfa5M4B23ovqnPsCAwEA
|
||||
AaNiMGAwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1Ud
|
||||
IwQYMBaAFMm7kFGBpyWbJhnY+lPOXiQ0q9c3MBgGA1UdEQQRMA+CDXdvb25vb3cu
|
||||
bG9jYWwwDQYJKoZIhvcNAQELBQADggGBAHcW6Z5kGZEhNOI+ZwadClsSW+00FfSs
|
||||
uwzaShUuPZpRC9Hmcvnc3+E+9dVuupzBULq9oTrDA2yVIhD9aHC7a7Vha/VDZubo
|
||||
2tTp+z71T/eXXph6q40D+beI9dw2oes9gQsZ+b9sbkH/9lVyeTTz3Oc06TYNwrK3
|
||||
X5CHn3pt76urHfxCMK1485goacqD+ju4yEI0UX+rnGJHPHJjpS7vZ5+FAGAG7+r3
|
||||
H1UPz94ITomyYzj0ED1v54e3lcxus/4CkiVWuh/VJYxBdoptT8RDt1eP8CD3NTOM
|
||||
P0jxDKbjBBCCCdGoGU7n1FFfpG882SLiW8fsaLf45kVYRTWnk2r16y6AU5pQe3xX
|
||||
8L6DuPo+xPlthxxSpX6ppbuA/O/KQ1qc3iDt8VNmQxffKiBt3zTW/ba3bgf92EAm
|
||||
CZyZyE7GLxQ1X+J6VMM9zDBVSM8suu5IPXEsEepeVk8xDKmoTdJs3ZIBXm538AD/
|
||||
WoI8zeb6KaJ3G8wCkEIHhxxoSmWSt2ez1Q==
|
||||
MIIEdTCCAt2gAwIBAgIRAKO2NWnRuWeb2C/NQ/Teuu0wDQYJKoZIhvcNAQELBQAw
|
||||
gaExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE7MDkGA1UECwwyZHdp
|
||||
bmRvd25ARHdpbmRpcy1NYWMtbWluaS5sb2NhbCAoRHdpbmRpIFJhbWFkaGFuYSkx
|
||||
QjBABgNVBAMMOW1rY2VydCBkd2luZG93bkBEd2luZGlzLU1hYy1taW5pLmxvY2Fs
|
||||
IChEd2luZGkgUmFtYWRoYW5hKTAeFw0yNTExMjIwOTM2NTdaFw0yODAyMjIwOTM2
|
||||
NTdaMGYxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTE7
|
||||
MDkGA1UECwwyZHdpbmRvd25ARHdpbmRpcy1NYWMtbWluaS5sb2NhbCAoRHdpbmRp
|
||||
IFJhbWFkaGFuYSkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwGedS
|
||||
6QfL/vMzFktKhqvIVGAvgpuNJO2r1Mf9oHlmwSryqjYn5/zp82RhgYLIW3w3sH6x
|
||||
1V5AkwiHBoaSh+CZ+CHUOvDw5+noyjaGrlW1lj42VAOH3cxSrtc1scjiP2Cph/jY
|
||||
qZEWZb4iq2J+GSkpbJHUbcqtbUw0XaC8OXg0aRR5ELmRQ2VNs7cqSw1xODvBuOak
|
||||
6650r5YfoR8MPj0sz5a16notcUXwT627HduyA7RAs8oWKn/96ZPBo7kPVCL/JowG
|
||||
tdtIka+ESMRu1qsdu1ZtcSVbove/wTNFV9akfKRymI0J2rcTWPpz4lVfvIBhQz0J
|
||||
bnFqSZeDE3pLLfg1AgMBAAGjYjBgMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAK
|
||||
BggrBgEFBQcDATAfBgNVHSMEGDAWgBSsL6TlzA65pzrFGTrL97kt0FlZJzAYBgNV
|
||||
HREEETAPgg13b29ub293LmxvY2FsMA0GCSqGSIb3DQEBCwUAA4IBgQBkvgb0Gp50
|
||||
VW2Y7wQntNivPcDWJuDjbK1waqUqpSVUkDx2R+i6UPSloNoSLkgBLz6rn4Jt4Hzu
|
||||
cLP+iuZql3KC/+G9Alr6cn/UnG++jGekcO7m/sQYYen+SzdmVYNe4BSJOeJvLe1A
|
||||
Km10372m5nVd5iGRnZ+n5CprWOCymkC1Hg7xiqGOuldDu/yRcyHgdQ3a0y4nK91B
|
||||
TQJzt9Ux/50E12WkPeKXDmD7MSHobQmrrtosMU5aeDwmEZm3FTItLEtXqKuiu7fG
|
||||
V8gOPdL69Da0ttN2XUC0WRCtLcuRfxvi90Tkjo1JHo8586V0bjZZl4JguJwCTn78
|
||||
EdZRwzLUrdvgfAL/TyN/meJgBBfVnTBviUp2OMKH+0VLtk7RNHNYiEnwk7vjIQYR
|
||||
lFBdVKcqDH5yx6QsmdkhExE5/AyYbVh147JXlcTTiEJpD0Nm8m4WCIwRR81HEvKN
|
||||
emjbk+5vcx0ja+jj+TM2Aofv/rdOllfjsv26PJix+jJgn0cJ6F+7gKA=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7dtgMEmstOyBW
|
||||
Wp6QhzuNfJJVgilp0MOIiKxlvNYRUDAh7KZQ5cHgDeJiDu6ibD/8LaUyg4O/4daF
|
||||
ywzpL3WDsIFSKuHWWdGFF3u1AvHSbxQ+GPfwQUU0KCRnBKymBfMqvEnPLLhn8KNA
|
||||
teaHm+DbIf1PXGwjbwh1qMMhdFRmP69KzYPPMXlT2W375WhGhgoh+A9wTvofIfRO
|
||||
6rVDE0RjRJwP9OIzbNku0Fpo2haG2AL9j5lOSwVmI0yKAlvL/+ckVOZVcriPWBYb
|
||||
anpBkd2/TiVYBoo6iCXrwtvPu/xJmVwVLYcCZLEBcm+tFgp3fC6VayyKpBH2uTOA
|
||||
dt6L6pz7AgMBAAECggEAZeT1Daq9QrqOmyFqaph20DLTv1Kee/uTLJVNT4dSu9pg
|
||||
LzBYPkSEGuqxECeZogNAzCtrTYeahyOT3Ok/PUgkkc3QnP7d/gqYDcVz4jGVi5IA
|
||||
6LfdnGN94Bmpn600wpEdWS861zcxjJ2JvtSgVzltAO76prZPuPrTGFEAryBx95jb
|
||||
3p08nAVT3Skw95bz56DBnfT/egqySmKhLRvKgey2ttGkB1WEjqY8YlQch9yy6uV7
|
||||
2iEUwbGY6mbAepFv+KGdOmrGZ/kLktI90PgR1g8E4KOrhk+AfBjN9XgZP2t+yO8x
|
||||
Cwh/owmn5J6s0EKFFEFBQrrbiu2PaZLZ9IEQmcEwEQKBgQDdppwaOYpfXPAfRIMq
|
||||
XlGjQb+3GtFuARqSuGcCl0LxMHUqcBtSI/Ua4z0hJY2kaiomgltEqadhMJR0sWum
|
||||
FXhGh6uhINn9o4Oumu9CySiq1RocR+w4/b15ggDWm60zV8t5v0+jM+R5CqTQPUTv
|
||||
Fd77QZnxspmJyB7M2+jXqoHCrwKBgQDYg/mQYg25+ibwR3mdvjOd5CALTQJPRJ01
|
||||
wHLE5fkcgxTukChbaRBvp9yI7vK8xN7pUbsv/G2FrkBqvpLtAYglVVPJj/TLGzgi
|
||||
i5QE2ORE9KJcyV193nOWE0Y4JS0cXPh1IG5DZDAU5+/zLq67LSKk6x9cO/g7hZ3A
|
||||
1sC6NVJNdQKBgQCLEh6f1bqcWxPOio5B5ywR4w8HNCxzeP3TUSBQ39eAvYbGOdDq
|
||||
mOURGcMhKQ7WOkZ4IxJg4pHCyVhcX3XLn2z30+g8EQC1xAK7azr0DIMXrN3VIMt2
|
||||
dr6LnqYoAUWLEWr52K9/FvAjgiom/kpiOLbPrzmIDSeI66dnohNWPgVswQKBgCDi
|
||||
mqslWXRf3D4ufPhKhUh796n/vlQP1djuLABf9aAxAKLjXl3T7V0oH8TklhW5ySmi
|
||||
8k1th60ANGSCIYrB6s3Q0fMRXFrk/Xexv3+k+bbHeUmihAK0INYwgz/P1bQzIsGX
|
||||
dWfi9bKXL8i91Gg1iMeHtrGpoiBYQQejFo6xvphpAoGAEomDPyuRIA2oYZWtaeIp
|
||||
yghLR0ixbnsZz2oA1MuR4A++iwzspUww/T5cFfI4xthk7FOxy3CK7nDL96rzhHf3
|
||||
EER4qOOxP+kAAs8Ozd4ERkUSuaDkrRsaUhr8CYF5AQajPQWKMEVcCK1G+WqHGNYg
|
||||
GzoAyax8kSdmzv6fMPouiGI=
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwGedS6QfL/vMz
|
||||
FktKhqvIVGAvgpuNJO2r1Mf9oHlmwSryqjYn5/zp82RhgYLIW3w3sH6x1V5AkwiH
|
||||
BoaSh+CZ+CHUOvDw5+noyjaGrlW1lj42VAOH3cxSrtc1scjiP2Cph/jYqZEWZb4i
|
||||
q2J+GSkpbJHUbcqtbUw0XaC8OXg0aRR5ELmRQ2VNs7cqSw1xODvBuOak6650r5Yf
|
||||
oR8MPj0sz5a16notcUXwT627HduyA7RAs8oWKn/96ZPBo7kPVCL/JowGtdtIka+E
|
||||
SMRu1qsdu1ZtcSVbove/wTNFV9akfKRymI0J2rcTWPpz4lVfvIBhQz0JbnFqSZeD
|
||||
E3pLLfg1AgMBAAECggEBAKVoH0xUD3u/w8VHen7M0ct/3Tyi6+J+PjN40ERdF8q5
|
||||
Q9Lcp7OCBp/kenPPhv0UWS+hus7kf/wdXxQcwAggUomsdHH4ztkorB942BBW7bB7
|
||||
J4I2FX7niQRcr04C6JICP5PdYJJ5awrjk9zSp9eTYINFNBCY85dEIyDIlLJXNJ3c
|
||||
SkjmJlCAvJXYZcJ1/UaitBNFxiPWd0Abpr2kEvIbN9ipLP336FzTcp+KwxInMI5p
|
||||
s/vwXDkzlUr/4azE0DlXU4WiFLCOfCiL0+gX128+fugmYimig5eRSbpZDWXPl6b7
|
||||
BnbKLy1ak53qm7Otz2e/K0sgSUnMXX12tY1BGgg+kL0CgYEA2z/usrjLUu8tnvvn
|
||||
XU7ULmEOUsOVh8NmW4jkVgd4Aok+zRxmstA0c+ZcIEr/0g4ad/9OQnI7miGTSdaC
|
||||
1e8cDmR1D7DtyxuwhNDGN73yjWjT+4gAba087J/+JPKky3MNV5fISgRi1he5Jqfp
|
||||
aPZDsf4+cAmI0DQm+TnIDBaXt0cCgYEAzZ50b4KdmqURlruDbK1GxH7jeMVdzpl8
|
||||
ZyLXnXJbTK8qCv2/0kYR6r3raDjAN7AFMFaFh93j6q/DTJb/x4pNYMSKTxbkZu5J
|
||||
S7jUfcgRbMp2ItLjtLc5Ve/yEUa9JtaL8778Efd5oTot5EflkG0v+3ISLYDC6Uu1
|
||||
wTUcClX4iqMCgYEAovB7c8UUDhmEfQ/WnSiVVbZ5j5adDR1xd3tfvnOkg7X9vy9p
|
||||
P2Cuaqf7NWCniDNFBoLtZUJB+0USkiBicZ1W63dK7BNgVb7JS5tghFKc7OzIBbnI
|
||||
H7pMecpZdJoDUNO7Saqahi+GSHeu+QR22bOTEbfSLS9YxurLQBLqEdnEfMcCgYAW
|
||||
0ZPoYB1vcQwvpyWhpOUqn05NM9ICQIROyc4V2gAJ1ZKb36cvBbmtTGBYk5u5Ul5x
|
||||
C9kLx/MoM1NAJ63BDjciGw2iU08LoTwfHCbwwog0g49ys+azQnYpdFRv2GLbcYnc
|
||||
hgBhWg50dwlqwRPX4FYn2HPt+tEmpNFJ3MP83aeUcwKBgCG4FmPe+a7gRZ/uqoNx
|
||||
bIyNSKQw6O/RSP3rOcqeZjVxYwBYuqaMIr8TZj5NTePR1kZsuJ0Lo02h6NOMAP0B
|
||||
UtHulMHf83AXySHt8J907fhdvCotOi6E/94ziTTmU0bNsuWE2/FYe34LrYlcoVbi
|
||||
QPo8USOGPS9H/OTR3tTAPdSG
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -214,6 +214,7 @@ import PushConfiguration from '@/routes/Settings/Notifications/PushConfiguration
|
||||
import EmailCustomization from '@/routes/Settings/Notifications/EmailCustomization';
|
||||
import EditTemplate from '@/routes/Settings/Notifications/EditTemplate';
|
||||
import SettingsDeveloper from '@/routes/Settings/Developer';
|
||||
import SettingsCustomerSPA from '@/routes/Settings/CustomerSPA';
|
||||
import MorePage from '@/routes/More';
|
||||
|
||||
// Addon Route Component - Dynamically loads addon components
|
||||
@@ -511,6 +512,7 @@ function AppRoutes() {
|
||||
<Route path="/settings/notifications/email-customization" element={<EmailCustomization />} />
|
||||
<Route path="/settings/notifications/edit-template" element={<EditTemplate />} />
|
||||
<Route path="/settings/brand" element={<SettingsIndex />} />
|
||||
<Route path="/settings/customer-spa" element={<SettingsCustomerSPA />} />
|
||||
<Route path="/settings/developer" element={<SettingsDeveloper />} />
|
||||
|
||||
{/* Dynamic Addon Routes */}
|
||||
|
||||
@@ -163,3 +163,52 @@ export function openWPMediaFavicon(onSelect: (file: WPMediaFile) => void): void
|
||||
onSelect
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open WordPress Media Modal for Multiple Images (Product Gallery)
|
||||
*/
|
||||
export function openWPMediaGallery(onSelect: (files: WPMediaFile[]) => void): void {
|
||||
// Check if WordPress media is available
|
||||
if (typeof window.wp === 'undefined' || typeof window.wp.media === 'undefined') {
|
||||
console.error('WordPress media library is not available');
|
||||
alert('WordPress Media library is not loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create media frame with multiple selection
|
||||
const frame = window.wp.media({
|
||||
title: 'Select or Upload Product Images',
|
||||
button: {
|
||||
text: 'Add to Gallery',
|
||||
},
|
||||
multiple: true,
|
||||
library: {
|
||||
type: 'image',
|
||||
},
|
||||
});
|
||||
|
||||
// Handle selection
|
||||
frame.on('select', () => {
|
||||
const selection = frame.state().get('selection') as any;
|
||||
const files: WPMediaFile[] = [];
|
||||
|
||||
selection.map((attachment: any) => {
|
||||
const data = attachment.toJSON();
|
||||
files.push({
|
||||
url: data.url,
|
||||
id: data.id,
|
||||
title: data.title || data.filename,
|
||||
filename: data.filename,
|
||||
alt: data.alt || '',
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
});
|
||||
return attachment;
|
||||
});
|
||||
|
||||
onSelect(files);
|
||||
});
|
||||
|
||||
// Open modal
|
||||
frame.open();
|
||||
}
|
||||
|
||||
@@ -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