feat: add WPCFTO-inspired design system and React navigation
- Add WPCFTO-inspired design system CSS (colors, spacing, typography) - Add reusable React components matching WPCFTO visual language - Implement client-side navigation with hash-based routing - Add NavigationMenu component for on-page navigation (no SSR) - Update WordPress submenu highlighting based on current React page - Add Refresh button to DataTable toolbar - Fix icon imports in VariationPricingTable - Remove page headers from all table pages (consistent UX) - Convert Orders page to use DataTable for consistency Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,8 @@ class ReactAdmin {
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'restUrl' => rest_url( 'formipay/v1' ),
|
||||
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
||||
'pluginUrl' => FORMIPAY_URL,
|
||||
'siteUrl' => site_url(),
|
||||
] );
|
||||
|
||||
// Debug logging
|
||||
@@ -149,7 +151,7 @@ class ReactAdmin {
|
||||
public static function render_mount_point( $page ) {
|
||||
|
||||
printf(
|
||||
'<div id="formipay-admin-root" data-formipay-mount="%s">Loading %s...</div>',
|
||||
'<div class="wrap"><div id="formipay-admin-root" data-formipay-mount="%s">Loading %s...</div></div>',
|
||||
esc_attr( $page ),
|
||||
esc_html( ucfirst( $page ) )
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* App Component - Main admin application shell
|
||||
* App Component - Main admin application shell with client-side routing
|
||||
*/
|
||||
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import OrdersPage from '../pages/Orders';
|
||||
import CustomersPage from '../pages/Customers';
|
||||
import ProductsPage from '../pages/Products';
|
||||
@@ -10,6 +10,8 @@ import FormsPage from '../pages/Forms';
|
||||
import CouponsPage from '../pages/Coupons';
|
||||
import AccessPage from '../pages/Access';
|
||||
import LicensesPage from '../pages/Licenses';
|
||||
import NavigationMenu from './NavigationMenu';
|
||||
import '../pages/AdminPages.css';
|
||||
|
||||
const pageComponents = {
|
||||
orders: OrdersPage,
|
||||
@@ -21,23 +23,88 @@ const pageComponents = {
|
||||
licenses: LicensesPage,
|
||||
};
|
||||
|
||||
export default function App({ page, initialData }) {
|
||||
useEffect(() => {
|
||||
console.log('[Formipay App] Rendering page:', page, 'with data:', initialData);
|
||||
}, [page, initialData]);
|
||||
export default function App({ page: initialPage, initialData }) {
|
||||
// Use state for client-side routing
|
||||
// Prioritize hash over initial page for reload support
|
||||
const [currentPage, setCurrentPage] = useState(() => {
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
return (pageComponents[hash]) ? hash : initialPage;
|
||||
});
|
||||
|
||||
const PageComponent = pageComponents[page];
|
||||
// Update WordPress submenu active state based on current page
|
||||
useEffect(() => {
|
||||
// Remove active class from all submenu items
|
||||
document.querySelectorAll('li.wp-first-item.current, li.wp-first-item .current').forEach(el => {
|
||||
el.classList.remove('current', 'wp-first-item');
|
||||
});
|
||||
|
||||
// Add active class to current page's submenu item
|
||||
const pageUrls = {
|
||||
forms: 'admin.php?page=formipay',
|
||||
products: 'admin.php?page=formipay-products',
|
||||
coupons: 'admin.php?page=formipay-coupons',
|
||||
orders: 'admin.php?page=formipay-orders',
|
||||
customers: 'admin.php?page=formipay-customers',
|
||||
access: 'admin.php?page=formipay-access',
|
||||
licenses: 'admin.php?page=formipay-licenses',
|
||||
};
|
||||
|
||||
const targetUrl = pageUrls[currentPage];
|
||||
if (targetUrl) {
|
||||
const submenuLinks = document.querySelectorAll('#toplevel_page_formipay .wp-submenu a');
|
||||
submenuLinks.forEach(link => {
|
||||
link.parentElement.classList.remove('current');
|
||||
link.classList.remove('current');
|
||||
if (link.getAttribute('href')?.includes(targetUrl)) {
|
||||
if(currentPage === 'forms') {
|
||||
document.querySelectorAll('#toplevel_page_formipay .wp-submenu li:nth-child(2) a').forEach(ahref => {
|
||||
ahref.parentElement.classList.add('current');
|
||||
ahref.classList.add('current');
|
||||
});
|
||||
}else{
|
||||
link.parentElement.classList.add('current');
|
||||
link.classList.add('current');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
// Listen for hash changes for browser back/forward support
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
if (pageComponents[hash]) {
|
||||
setCurrentPage(hash);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
||||
}, []);
|
||||
|
||||
// Update URL when page changes via navigation
|
||||
const handlePageNavigate = (pageKey) => {
|
||||
setCurrentPage(pageKey);
|
||||
window.location.hash = pageKey;
|
||||
};
|
||||
|
||||
const PageComponent = pageComponents[currentPage];
|
||||
|
||||
if (!PageComponent) {
|
||||
return (
|
||||
<div className="formipay-error">
|
||||
<p>Unknown page: {page}</p>
|
||||
<p>Unknown page: {currentPage}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="formipay-admin-wrap">
|
||||
<NavigationMenu
|
||||
currentPage={currentPage}
|
||||
onPageNavigate={handlePageNavigate}
|
||||
/>
|
||||
<PageComponent initialData={initialData} />
|
||||
</div>
|
||||
);
|
||||
|
||||
49
src/admin/components/NavigationMenu.js
Normal file
49
src/admin/components/NavigationMenu.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Navigation Menu Component - Sticky header with logo and page navigation
|
||||
* Used for React on-page navigation (no page reload)
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export default function NavigationMenu({ currentPage, onPageNavigate }) {
|
||||
const navItems = [
|
||||
{ key: 'forms', label: __('Forms', 'formipay') },
|
||||
{ key: 'products', label: __('Products', 'formipay') },
|
||||
{ key: 'coupons', label: __('Coupons', 'formipay') },
|
||||
{ key: 'orders', label: __('Orders', 'formipay') },
|
||||
{ key: 'customers', label: __('Customers', 'formipay') },
|
||||
{ key: 'access', label: __('Access', 'formipay') },
|
||||
{ key: 'licenses', label: __('Licenses', 'formipay') },
|
||||
];
|
||||
|
||||
const handleNavClick = (e, itemKey) => {
|
||||
e.preventDefault();
|
||||
if (onPageNavigate) {
|
||||
onPageNavigate(itemKey);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="formipay-navigation-menu">
|
||||
<img
|
||||
src={`${window.formipayAdmin?.pluginUrl || ''}/admin/assets/img/formipay-logo-circle-white_256.png`}
|
||||
alt="Formipay"
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
|
||||
<nav className="navigation-links">
|
||||
{navItems.map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={`#${item.key}`}
|
||||
className={`nav-link ${currentPage === item.key ? 'active' : ''}`}
|
||||
onClick={(e) => handleNavClick(e, item.key)}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,7 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
|
||||
import { TextControl, Button } from '@wordpress/components';
|
||||
import { Icon } from '@wordpress/icons';
|
||||
import minus from '@wordpress/icons/build/minus';
|
||||
import eyeClosed from '@wordpress/icons/build/eye-closed';
|
||||
import eyeOpened from '@wordpress/icons/build/eye-opened';
|
||||
import * as Icons from '@wordpress/icons';
|
||||
import './VariationPricingTable.css';
|
||||
|
||||
export default function VariationPricingTable({ productId, productDetails }) {
|
||||
@@ -421,12 +418,12 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
||||
className="toggle-expand"
|
||||
onClick={onToggleExpanded}
|
||||
>
|
||||
<Icon icon={row.expanded ? eyeOpened : eyeClosed} size={16} />
|
||||
{row.expanded ? '▼' : '▶'}
|
||||
</button>
|
||||
<strong>{ row.name }</strong>
|
||||
</td>
|
||||
|
||||
{showFlatPricing ? (
|
||||
{showFlatPricing && row.prices?.[0] ? (
|
||||
<>
|
||||
<PriceCell
|
||||
price={row.prices[0]}
|
||||
@@ -465,7 +462,7 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
||||
size="small"
|
||||
isDestructive
|
||||
onClick={onDelete}
|
||||
icon={minus()}
|
||||
icon={Icons.trash}
|
||||
>
|
||||
{ __('Delete', 'formipay') }
|
||||
</Button>
|
||||
@@ -477,7 +474,7 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
||||
<td colSpan="5">
|
||||
<table className="inner-table">
|
||||
<tbody>
|
||||
{row.prices.map((price, currencyIndex) => {
|
||||
{(row.prices || []).map((price, currencyIndex) => {
|
||||
const code = String(price.currency).split(':::')[0];
|
||||
const isDefault = code === defaultCurrencyCode;
|
||||
const step = price.currency_decimal_digits
|
||||
@@ -523,6 +520,10 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
||||
|
||||
// Price input cell component
|
||||
function PriceCell({ price, field, onChange }) {
|
||||
if (!price) {
|
||||
return <td className="price-cell">-</td>;
|
||||
}
|
||||
|
||||
const step = price.currency_decimal_digits
|
||||
? 1 / Math.pow(10, price.currency_decimal_digits)
|
||||
: 0.01;
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.formipay-table-toolbar *:is(button,input,select) {
|
||||
height: 40px!important;
|
||||
}
|
||||
|
||||
.formipay-table-toolbar .components-base-control__field {
|
||||
margin-bottom: unset!important;
|
||||
}
|
||||
|
||||
.formipay-table-toolbar *:is(button, input, select, .components-input-control__backdrop){
|
||||
border-radius: 4px!important;
|
||||
}
|
||||
|
||||
/* Filter Tabs */
|
||||
.formipay-filter-tabs {
|
||||
display: flex;
|
||||
@@ -112,6 +124,14 @@
|
||||
background-color: #f0f0f1;
|
||||
}
|
||||
|
||||
.formipay-table *:is(td, th):first-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formipay-table th.column-select > input {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Checkbox Column */
|
||||
.formipay-table .column-select {
|
||||
width: 40px;
|
||||
|
||||
@@ -60,6 +60,9 @@ export default function DataTable({
|
||||
tableAction, // e.g., 'formipay-tabledata-forms'
|
||||
deleteAction,
|
||||
duplicateAction,
|
||||
|
||||
// Selection callback
|
||||
onSelectionChange,
|
||||
}) {
|
||||
// State
|
||||
const [data, setData] = useState(initialData);
|
||||
@@ -82,6 +85,13 @@ export default function DataTable({
|
||||
const [selectedRows, setSelectedRows] = useState(new Set());
|
||||
const [selectAll, setSelectAll] = useState(false);
|
||||
|
||||
// Notify parent of selection changes
|
||||
useEffect(() => {
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange(selectedRows);
|
||||
}
|
||||
}, [selectedRows, onSelectionChange]);
|
||||
|
||||
// Add New Modal
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [newItemTitle, setNewItemTitle] = useState('');
|
||||
@@ -89,8 +99,6 @@ export default function DataTable({
|
||||
// Derive action names from tableAction
|
||||
const baseActionName = tableAction.replace('formipay-tabledata-', '');
|
||||
const bulkDeleteAction = actions.bulkDelete?.action || `formipay-bulk-delete-${baseActionName}`;
|
||||
const deleteActionName = deleteAction || `formipay-delete-${baseActionName}`;
|
||||
const duplicateActionName = duplicateAction || `formipay-duplicate-${baseActionName}`;
|
||||
|
||||
// Load data
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -213,56 +221,6 @@ export default function DataTable({
|
||||
}
|
||||
};
|
||||
|
||||
// Handle inline delete
|
||||
const handleDelete = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to delete this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=${deleteActionName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
|
||||
loadData();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle duplicate
|
||||
const handleDuplicate = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Confirm', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=${duplicateActionName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
|
||||
loadData();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Add New
|
||||
const handleAddNew = async () => {
|
||||
if (!newItemTitle.trim()) {
|
||||
@@ -356,6 +314,15 @@ export default function DataTable({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Refresh Button */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={loadData}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? __('Refreshing...', 'formipay') : __('Refresh', 'formipay')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
@@ -408,10 +375,6 @@ export default function DataTable({
|
||||
{column.label}
|
||||
</th>
|
||||
))}
|
||||
{/* Actions column */}
|
||||
{actions.inline && (
|
||||
<th className="column-actions">{__('Actions', 'formipay')}</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -435,41 +398,6 @@ export default function DataTable({
|
||||
{column.render ? column.render(row) : row[column.key]}
|
||||
</td>
|
||||
))}
|
||||
{/* Actions */}
|
||||
{actions.inline && (
|
||||
<td className="column-actions">
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${rowId}&action=edit`}>
|
||||
{__('Edit', 'formipay')}
|
||||
</a>
|
||||
{' | '}
|
||||
<span
|
||||
className="row-actions"
|
||||
style={{ visibility: 'hidden' }}
|
||||
>
|
||||
<button
|
||||
className="button-link delete"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDelete(rowId);
|
||||
}}
|
||||
>
|
||||
{__('Delete', 'formipay')}
|
||||
</button>
|
||||
{' | '}
|
||||
<button
|
||||
className="button-link duplicate"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDuplicate(rowId);
|
||||
}}
|
||||
>
|
||||
{__('Duplicate', 'formipay')}
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -531,7 +459,7 @@ export default function DataTable({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add New Modal - only render when actually open */}
|
||||
{/* Add New Modal - only render when open */}
|
||||
{actions.addNew && isAddModalOpen && (
|
||||
<Modal
|
||||
title={actions.addNew.label || __('Add New', 'formipay')}
|
||||
@@ -539,7 +467,6 @@ export default function DataTable({
|
||||
setIsAddModalOpen(false);
|
||||
setNewItemTitle('');
|
||||
}}
|
||||
isDismissible
|
||||
>
|
||||
<TextControl
|
||||
__next40pxDefaultSize
|
||||
|
||||
35
src/admin/components/shared/ScreenMenu.js
Normal file
35
src/admin/components/shared/ScreenMenu.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Screen Menu Component - Page title and bulk delete action
|
||||
* Navigation is handled by NavigationMenu component
|
||||
* Add New button is in DataTable toolbar
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export default function ScreenMenu({
|
||||
title,
|
||||
showBulkDelete = false,
|
||||
bulkDeleteCount = 0,
|
||||
onBulkDelete,
|
||||
}) {
|
||||
// Derive bulk delete visibility from count
|
||||
const bulkDeleteVisible = showBulkDelete || bulkDeleteCount > 0;
|
||||
|
||||
return (
|
||||
<div className="formipay-screen-menu">
|
||||
<h1>{title}</h1>
|
||||
|
||||
{bulkDeleteVisible && onBulkDelete && (
|
||||
<button
|
||||
className="button button-secondary"
|
||||
onClick={onBulkDelete}
|
||||
>
|
||||
{__('Delete Selected', 'formipay')} ({bulkDeleteCount})
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 6a1 1 0 0 1 .117 1.993L20 8h-.081L19 19a3 3 0 0 1-2.824 2.995L16 22H8c-1.598 0-2.904-1.249-2.992-2.75l-.005-.167L4.08 8H4a1 1 0 0 1-.117-1.993L4 6zm-9.489 5.14a1 1 0 0 0-1.218 1.567L10.585 14l-1.292 1.293l-.083.094a1 1 0 0 0 1.497 1.32L12 15.415l1.293 1.292l.094.083a1 1 0 0 0 1.32-1.497L13.415 14l1.292-1.293l.083-.094a1 1 0 0 0-1.497-1.32L12 12.585l-1.293-1.292l-.094-.083zM14 2a2 2 0 0 1 2 2a1 1 0 0 1-1.993.117L14 4h-4l-.007.117A1 1 0 0 1 8 4a2 2 0 0 1 1.85-1.995L10 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
224
src/admin/design-system/README.md
Normal file
224
src/admin/design-system/README.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Formipay Design System - WPCFTO-Inspired
|
||||
|
||||
A comprehensive React component library that matches the visual language of WPCFTO Vue components.
|
||||
|
||||
## Installation
|
||||
|
||||
Import the CSS in your main React entry point:
|
||||
|
||||
```javascript
|
||||
import './design-system/WpcftoDesign.css';
|
||||
```
|
||||
|
||||
Import components:
|
||||
|
||||
```javascript
|
||||
import { Box, Field, Input, Button, Repeater, TabNav } from './design-system';
|
||||
// OR
|
||||
import * as DS from './design-system';
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### Box & BoxChild
|
||||
Container components with WPCFTO styling.
|
||||
|
||||
```jsx
|
||||
<Box>
|
||||
<BoxChild>
|
||||
<h2>Section Title</h2>
|
||||
<p>Content goes here...</p>
|
||||
</BoxChild>
|
||||
</Box>
|
||||
```
|
||||
|
||||
### TabNav & TabPanel
|
||||
Vertical/horizontal tab navigation matching WPCFTO sidebar.
|
||||
|
||||
```jsx
|
||||
function MyComponent() {
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', label: 'General', icon: '⚙' },
|
||||
{ id: 'settings', label: 'Settings', icon: '🔧' },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabNav
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
<TabPanel tabs={tabs} activeTab={activeTab}>
|
||||
{({ id }) => (
|
||||
id === 'general' && <GeneralContent />
|
||||
)}
|
||||
</TabPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Field, Input, Textarea, Select
|
||||
Form field components with consistent styling.
|
||||
|
||||
```jsx
|
||||
<Field
|
||||
label="Product Name"
|
||||
description="Enter the product name"
|
||||
required
|
||||
>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Product Name"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Textarea
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={5}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Status"
|
||||
options={[
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'inactive', label: 'Inactive' },
|
||||
]}
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Button
|
||||
Button with variants and sizes.
|
||||
|
||||
```jsx
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button variant="danger" icon={() => <span>🗑</span>}>
|
||||
Delete
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Repeater
|
||||
Expandable/collapsible repeater component matching WPCFTO repeater.
|
||||
|
||||
```jsx
|
||||
const [items, setItems] = useState([
|
||||
{ id: 1, title: 'Item 1', collapsed: false },
|
||||
]);
|
||||
|
||||
<Repeater
|
||||
items={items}
|
||||
renderItem={(item, index) => (
|
||||
<Field label="Item Name">
|
||||
<Input
|
||||
value={item.title}
|
||||
onChange={(e) => updateItem(index, 'title', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
onAdd={() => setItems([...items, { id: Date.now(), title: 'New Item', collapsed: false }])}
|
||||
onRemove={(id, index) => setItems(items.filter((_, i) => i !== index))}
|
||||
addLabel="Add New Item"
|
||||
/>
|
||||
```
|
||||
|
||||
### Notice
|
||||
Alert/notice component for messages.
|
||||
|
||||
```jsx
|
||||
<Notice type="success" title="Saved!">
|
||||
Your changes have been successfully saved.
|
||||
</Notice>
|
||||
|
||||
<Notice type="error" title="Error" onDismiss={() => setError(null)}>
|
||||
{errorMessage}
|
||||
</Notice>
|
||||
```
|
||||
|
||||
### Badge
|
||||
Status badge component.
|
||||
|
||||
```jsx
|
||||
<Badge variant="success">Active</Badge>
|
||||
<Badge variant="warning">Pending</Badge>
|
||||
<Badge variant="danger">Expired</Badge>
|
||||
```
|
||||
|
||||
### Table
|
||||
Styled table component.
|
||||
|
||||
```jsx
|
||||
<Table
|
||||
columns={[
|
||||
{ key: 'id', label: 'ID' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'status', label: 'Status', render: (row) => (
|
||||
<Badge variant={row.status === 'active' ? 'success' : 'default'}>
|
||||
{row.status}
|
||||
</Badge>
|
||||
)},
|
||||
]}
|
||||
data={tableData}
|
||||
emptyMessage="No items found"
|
||||
/>
|
||||
```
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### Colors
|
||||
- Primary: `#2985f7` (Blue)
|
||||
- Sidebar: `#1e2a36` (Dark)
|
||||
- Success: `#00a32a` (Green)
|
||||
- Warning: `#dba617` (Yellow)
|
||||
- Danger: `#d63638` (Red)
|
||||
|
||||
### Spacing
|
||||
- xs: 4px, sm: 8px, md: 12px, lg: 16px, xl: 20px, xxl: 24px
|
||||
|
||||
### Border Radius
|
||||
- sm: 4px, md: 10px, lg: 30px, full: 50%
|
||||
|
||||
### Typography
|
||||
- Font: Roboto
|
||||
- Sizes: 13px (sm), 14px (base), 15px (md), 18px (lg)
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
1. **Use Box/BoxChild** for content sections with proper spacing and borders
|
||||
2. **Use TabNav/TabPanel** for settings pages with multiple sections
|
||||
3. **Use Field** wrapper for all form inputs to get consistent labels and descriptions
|
||||
4. **Use Repeater** for repeatable content like variations, attributes, etc.
|
||||
5. **Use Notice** for success/error/warning messages
|
||||
6. **Use Badge** for status indicators
|
||||
|
||||
## Migration Notes
|
||||
|
||||
When migrating from WPCFTO Vue to React:
|
||||
|
||||
1. **Structure**: Match the `.wpcfto-box` → `.formipay-box` class structure
|
||||
2. **Colors**: Use CSS custom properties for consistent theming
|
||||
3. **Interactions**: Maintain same hover states and transitions
|
||||
4. **Validation**: Reuse WPCFTO validation patterns where possible
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Add Sortable for drag-drop reordering
|
||||
- [ ] Add ColorPicker for color inputs
|
||||
- [ ] Add DatePicker for date/time selection
|
||||
- [ ] Add RichText for WYSIWYG editing
|
||||
- [ ] Add FileUpload for media handling
|
||||
- [ ] Add IconPicker for icon selection
|
||||
384
src/admin/design-system/WpcftoComponents.js
Normal file
384
src/admin/design-system/WpcftoComponents.js
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* Formipay Design System - WPCFTO-inspired React components
|
||||
* Reusable components matching WPCFTO visual language
|
||||
*/
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import './WpcftoDesign.css';
|
||||
|
||||
// Box Component
|
||||
export function Box({ children, className = '', ...props }) {
|
||||
return (
|
||||
<div className={`formipay-box ${className}`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Box Child Component
|
||||
export function BoxChild({ children, className = '', ...props }) {
|
||||
return (
|
||||
<div className={`formipay-box-child ${className}`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Tab Navigation Component
|
||||
export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical' }) {
|
||||
return (
|
||||
<div className={`formipay-tab-nav formipay-tab-nav-${orientation}`}>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`formipay-nav-item ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
>
|
||||
<div className="formipay-nav-title">
|
||||
{tab.icon && <span className="formipay-nav-icon">{tab.icon}</span>}
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Tab Panel Component
|
||||
export function TabPanel({ tabs, activeTab, children }) {
|
||||
return (
|
||||
<div className="formipay-tabs">
|
||||
{tabs.map((tab, index) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`formipay-tab ${tab.id === activeTab ? 'active' : ''}`}
|
||||
>
|
||||
<div className="formipay-tab-content">
|
||||
{typeof children === 'function' ? children(tab, index) : children}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Field Component
|
||||
export function Field({
|
||||
label,
|
||||
description,
|
||||
required = false,
|
||||
children,
|
||||
className = '',
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<div className={`formipay-field ${className}`} {...props}>
|
||||
{label && (
|
||||
<div className={`formipay-field-label ${required ? 'required' : ''}`}>
|
||||
<span className="formipay-field-label-text">{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="formipay-field-content">
|
||||
{children}
|
||||
</div>
|
||||
{description && (
|
||||
<div className="formipay-field-description">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Input Component
|
||||
export function Input({
|
||||
label,
|
||||
description,
|
||||
required = false,
|
||||
className = '',
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Field label={label} description={description} required={required}>
|
||||
<input className={`formipay-input ${className}`} {...props} />
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
// Textarea Component
|
||||
export function Textarea({
|
||||
label,
|
||||
description,
|
||||
required = false,
|
||||
rows = 4,
|
||||
className = '',
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Field label={label} description={description} required={required}>
|
||||
<textarea
|
||||
className={`formipay-textarea ${className}`}
|
||||
rows={rows}
|
||||
{...props}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
// Select Component
|
||||
export function Select({
|
||||
label,
|
||||
description,
|
||||
required = false,
|
||||
options = [],
|
||||
className = '',
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Field label={label} description={description} required={required}>
|
||||
<select className={`formipay-select ${className}`} {...props}>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
// Checkbox Component
|
||||
export function Checkbox({
|
||||
label,
|
||||
checked,
|
||||
onChange,
|
||||
className = '',
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<label className={`formipay-checkbox ${className}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// Button Component
|
||||
export function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
icon: Icon,
|
||||
children,
|
||||
className = '',
|
||||
disabled = false,
|
||||
onClick,
|
||||
...props
|
||||
}) {
|
||||
const sizeClass = size !== 'md' ? `formipay-btn-${size}` : '';
|
||||
const variantClass = `formipay-btn-${variant}`;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`formipay-btn ${variantClass} ${sizeClass} ${className}`}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{Icon && <span className="formipay-btn-icon"><Icon /></span>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Repeater Component
|
||||
export function Repeater({
|
||||
items,
|
||||
renderItem,
|
||||
onAdd,
|
||||
onRemove,
|
||||
addLabel = 'Add Item',
|
||||
className = '',
|
||||
}) {
|
||||
return (
|
||||
<div className={`formipay-repeater ${className}`}>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id || index} className={`formipay-repeater-item ${item.collapsed ? 'collapsed' : ''}`}>
|
||||
<div
|
||||
className="formipay-repeater-header"
|
||||
onClick={() => onToggle?.(item.id)}
|
||||
>
|
||||
<div className="formipay-repeater-title">
|
||||
<span className="formipay-repeater-toggle">▼</span>
|
||||
<span>{item.title || `Item ${index + 1}`}</span>
|
||||
</div>
|
||||
<div className="formipay-repeater-actions">
|
||||
<span
|
||||
className="formipay-repeater-delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove?.(item.id, index);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="formipay-repeater-body">
|
||||
{renderItem(item, index)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button className="formipay-repeater-add" onClick={onAdd}>
|
||||
<span>+</span>
|
||||
<span>{addLabel}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Notice Component
|
||||
export function Notice({
|
||||
type = 'info',
|
||||
title,
|
||||
children,
|
||||
onDismiss,
|
||||
className = '',
|
||||
}) {
|
||||
return (
|
||||
<div className={`formipay-notice formipay-notice-${type} ${className}`}>
|
||||
<div className="formipay-notice-icon">
|
||||
{type === 'success' && '✓'}
|
||||
{type === 'warning' && '⚠'}
|
||||
{type === 'error' && '✕'}
|
||||
{type === 'info' && 'ℹ'}
|
||||
</div>
|
||||
<div className="formipay-notice-content">
|
||||
{title && <div className="formipay-notice-title">{title}</div>}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="formipay-notice-dismiss"
|
||||
onClick={onDismiss}
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading Spinner Component
|
||||
export function Spinner({ size = 'md', className = '' }) {
|
||||
const sizeClass = size !== 'md' ? `formipay-spinner-${size}` : '';
|
||||
return (
|
||||
<div className={`formipay-loading ${className}`}>
|
||||
<div className={`formipay-spinner ${sizeClass}`}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty State Component
|
||||
export function EmptyState({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
actionLabel,
|
||||
onAction,
|
||||
className = '',
|
||||
}) {
|
||||
return (
|
||||
<div className={`formipay-empty-state ${className}`}>
|
||||
{icon && <div className="formipay-empty-icon">{icon}</div>}
|
||||
{title && <div className="formipay-empty-title">{title}</div>}
|
||||
{description && (
|
||||
<div className="formipay-empty-description">{description}</div>
|
||||
)}
|
||||
{action && (
|
||||
<Button onClick={onAction}>
|
||||
{actionLabel || __('Take Action', 'formipay')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Status Badge Component
|
||||
export function Badge({
|
||||
variant = 'default',
|
||||
children,
|
||||
className = '',
|
||||
}) {
|
||||
return (
|
||||
<span className={`formipay-badge formipay-badge-${variant} ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Table Component
|
||||
export function Table({
|
||||
columns,
|
||||
data,
|
||||
emptyMessage = __('No items found', 'formipay'),
|
||||
className = '',
|
||||
}) {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className={`formipay-table-wrapper ${className}`}>
|
||||
<EmptyState
|
||||
title={emptyMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`formipay-table-wrapper ${className}`}>
|
||||
<table className="formipay-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th key={column.key}>{column.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{columns.map((column) => (
|
||||
<td key={column.key}>
|
||||
{column.render ? column.render(row, rowIndex) : row[column.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
Box,
|
||||
BoxChild,
|
||||
TabNav,
|
||||
TabPanel,
|
||||
Field,
|
||||
Input,
|
||||
Textarea,
|
||||
Select,
|
||||
Checkbox,
|
||||
Button,
|
||||
Repeater,
|
||||
Notice,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
Badge,
|
||||
Table,
|
||||
};
|
||||
645
src/admin/design-system/WpcftoDesign.css
Normal file
645
src/admin/design-system/WpcftoDesign.css
Normal file
@@ -0,0 +1,645 @@
|
||||
/**
|
||||
* Formipay Design System - WPCFTO-inspired component styles
|
||||
* Matches the visual language of WPCFTO Vue components
|
||||
* Use these classes in React components for consistency
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
DESIGN TOKENS
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--formipay-color-primary: #2985f7;
|
||||
--formipay-color-sidebar-bg: #1e2a36;
|
||||
--formipay-color-sidebar-text: #fff;
|
||||
--formipay-color-content-bg: #fff;
|
||||
--formipay-color-border: #f0f0f1;
|
||||
--formipay-color-border-dark: #8c99a5;
|
||||
--formipay-color-input-bg: #f6f9fc;
|
||||
--formipay-color-text: #1d2327;
|
||||
--formipay-color-text-muted: #646970;
|
||||
--formipay-color-danger: #d63638;
|
||||
--formipay-color-success: #00a32a;
|
||||
--formipay-color-warning: #dba617;
|
||||
|
||||
/* Spacing */
|
||||
--formipay-spacing-xs: 4px;
|
||||
--formipay-spacing-sm: 8px;
|
||||
--formipay-spacing-md: 12px;
|
||||
--formipay-spacing-lg: 16px;
|
||||
--formipay-spacing-xl: 20px;
|
||||
--formipay-spacing-xxl: 24px;
|
||||
|
||||
/* Border Radius */
|
||||
--formipay-radius-sm: 4px;
|
||||
--formipay-radius-md: 10px;
|
||||
--formipay-radius-lg: 30px;
|
||||
--formipay-radius-full: 50%;
|
||||
|
||||
/* Typography */
|
||||
--formipay-font-family: 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--formipay-font-size-sm: 13px;
|
||||
--formipay-font-size-base: 14px;
|
||||
--formipay-font-size-md: 15px;
|
||||
--formipay-font-size-lg: 18px;
|
||||
--formipay-font-weight-normal: 400;
|
||||
--formipay-font-weight-medium: 500;
|
||||
--formipay-font-weight-semibold: 600;
|
||||
--formipay-font-weight-bold: 700;
|
||||
|
||||
/* Shadows */
|
||||
--formipay-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
--formipay-shadow-md: -2px 2px 5px rgba(0, 0, 0, 0.08);
|
||||
--formipay-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Transitions */
|
||||
--formipay-transition-fast: 0.15s ease;
|
||||
--formipay-transition-base: 0.3s ease;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BASE STYLES
|
||||
============================================ */
|
||||
|
||||
.formipay-design-system * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.formipay-design-system {
|
||||
font-family: var(--formipay-font-family);
|
||||
color: var(--formipay-color-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BOX COMPONENT
|
||||
============================================ */
|
||||
|
||||
.formipay-box {
|
||||
background-color: var(--formipay-color-content-bg);
|
||||
border-radius: var(--formipay-radius-md);
|
||||
margin: 0 0 10px;
|
||||
min-height: 80px;
|
||||
position: relative;
|
||||
box-shadow: var(--formipay-shadow-sm);
|
||||
}
|
||||
|
||||
.formipay-box-child {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.formipay-box-child + .formipay-box-child {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.formipay-box a {
|
||||
color: var(--formipay-color-primary);
|
||||
text-decoration: none;
|
||||
transition: color var(--formipay-transition-base);
|
||||
}
|
||||
|
||||
.formipay-box a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TAB NAVIGATION (Vertical)
|
||||
============================================ */
|
||||
|
||||
.formipay-tab-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--formipay-spacing-xs);
|
||||
}
|
||||
|
||||
.formipay-nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--formipay-spacing-md) var(--formipay-spacing-lg);
|
||||
border-radius: var(--formipay-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--formipay-transition-fast);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.formipay-nav-item:hover {
|
||||
background-color: rgba(41, 133, 247, 0.05);
|
||||
}
|
||||
|
||||
.formipay-nav-item.active {
|
||||
background-color: rgba(41, 133, 247, 0.1);
|
||||
color: var(--formipay-color-primary);
|
||||
}
|
||||
|
||||
.formipay-nav-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--formipay-spacing-sm);
|
||||
font-size: var(--formipay-font-size-base);
|
||||
font-weight: var(--formipay-font-weight-medium);
|
||||
}
|
||||
|
||||
.formipay-nav-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TAB CONTENT PANELS
|
||||
============================================ */
|
||||
|
||||
.formipay-tab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.formipay-tab.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.formipay-tab-content {
|
||||
padding: var(--formipay-spacing-lg);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FIELD COMPONENT
|
||||
============================================ */
|
||||
|
||||
.formipay-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--formipay-spacing-lg) 0;
|
||||
border-bottom: 1px solid var(--formipay-color-border);
|
||||
}
|
||||
|
||||
.formipay-field:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.formipay-field-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--formipay-spacing-xs);
|
||||
margin-bottom: var(--formipay-spacing-sm);
|
||||
font-size: var(--formipay-font-size-base);
|
||||
font-weight: var(--formipay-font-weight-medium);
|
||||
}
|
||||
|
||||
.formipay-field-label.required .formipay-field-label-text::after {
|
||||
content: '*';
|
||||
color: var(--formipay-color-danger);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.formipay-field-description {
|
||||
font-size: var(--formipay-font-size-sm);
|
||||
color: var(--formipay-color-text-muted);
|
||||
margin-top: var(--formipay-spacing-xs);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
INPUTS
|
||||
============================================ */
|
||||
|
||||
.formipay-input,
|
||||
.formipay-select,
|
||||
.formipay-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--formipay-color-border-dark);
|
||||
border-radius: var(--formipay-radius-lg);
|
||||
background-color: var(--formipay-color-input-bg);
|
||||
font-family: inherit;
|
||||
font-size: var(--formipay-font-size-base);
|
||||
color: var(--formipay-color-text);
|
||||
transition: border-color var(--formipay-transition-fast), box-shadow var(--formipay-transition-fast);
|
||||
}
|
||||
|
||||
.formipay-input:focus,
|
||||
.formipay-select:focus,
|
||||
.formipay-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--formipay-color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(41, 133, 247, 0.1);
|
||||
}
|
||||
|
||||
.formipay-input:disabled,
|
||||
.formipay-select:disabled,
|
||||
.formipay-textarea:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.formipay-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CHECKBOX & RADIO
|
||||
============================================ */
|
||||
|
||||
.formipay-checkbox,
|
||||
.formipay-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--formipay-spacing-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.formipay-checkbox input,
|
||||
.formipay-radio input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BUTTONS
|
||||
============================================ */
|
||||
|
||||
.formipay-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--formipay-spacing-sm);
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: var(--formipay-radius-sm);
|
||||
font-family: inherit;
|
||||
font-size: var(--formipay-font-size-base);
|
||||
font-weight: var(--formipay-font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--formipay-transition-fast);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.formipay-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.formipay-btn-primary {
|
||||
background-color: var(--formipay-color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.formipay-btn-primary:hover:not(:disabled) {
|
||||
background-color: #1e6ae6;
|
||||
}
|
||||
|
||||
.formipay-btn-secondary {
|
||||
background-color: #f0f0f1;
|
||||
color: var(--formipay-color-text);
|
||||
}
|
||||
|
||||
.formipay-btn-secondary:hover:not(:disabled) {
|
||||
background-color: #e0e0e1;
|
||||
}
|
||||
|
||||
.formipay-btn-danger {
|
||||
background-color: var(--formipay-color-danger);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.formipay-btn-danger:hover:not(:disabled) {
|
||||
background-color: #b32d2e;
|
||||
}
|
||||
|
||||
.formipay-btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: var(--formipay-font-size-sm);
|
||||
}
|
||||
|
||||
.formipay-btn-icon {
|
||||
padding: 8px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
REPEATER COMPONENT
|
||||
============================================ */
|
||||
|
||||
.formipay-repeater {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--formipay-spacing-md);
|
||||
}
|
||||
|
||||
.formipay-repeater-item {
|
||||
background-color: #fafafa;
|
||||
border: 1px solid var(--formipay-color-border);
|
||||
border-radius: var(--formipay-radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formipay-repeater-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--formipay-spacing-md) var(--formipay-spacing-lg);
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid var(--formipay-color-border);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.formipay-repeater-header:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.formipay-repeater-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--formipay-spacing-sm);
|
||||
font-weight: var(--formipay-font-weight-medium);
|
||||
}
|
||||
|
||||
.formipay-repeater-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform var(--formipay-transition-fast);
|
||||
}
|
||||
|
||||
.formipay-repeater-item.collapsed .formipay-repeater-toggle {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.formipay-repeater-body {
|
||||
padding: var(--formipay-spacing-lg);
|
||||
}
|
||||
|
||||
.formipay-repeater-item.collapsed .formipay-repeater-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.formipay-repeater-actions {
|
||||
display: flex;
|
||||
gap: var(--formipay-spacing-sm);
|
||||
}
|
||||
|
||||
.formipay-repeater-delete {
|
||||
color: var(--formipay-color-danger);
|
||||
cursor: pointer;
|
||||
transition: color var(--formipay-transition-fast);
|
||||
}
|
||||
|
||||
.formipay-repeater-delete:hover {
|
||||
color: #b32d2e;
|
||||
}
|
||||
|
||||
.formipay-repeater-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--formipay-spacing-sm);
|
||||
padding: var(--formipay-spacing-md) var(--formipay-spacing-lg);
|
||||
background-color: transparent;
|
||||
border: 1px dashed var(--formipay-color-border-dark);
|
||||
border-radius: var(--formipay-radius-sm);
|
||||
color: var(--formipay-color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all var(--formipay-transition-fast);
|
||||
}
|
||||
|
||||
.formipay-repeater-add:hover {
|
||||
border-color: var(--formipay-color-primary);
|
||||
color: var(--formipay-color-primary);
|
||||
background-color: rgba(41, 133, 247, 0.05);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TABLE STYLES (WPCFTO-style)
|
||||
============================================ */
|
||||
|
||||
.formipay-table-wrapper {
|
||||
background-color: var(--formipay-color-content-bg);
|
||||
border-radius: var(--formipay-radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--formipay-shadow-sm);
|
||||
}
|
||||
|
||||
.formipay-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.formipay-table thead {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.formipay-table th {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-size: var(--formipay-font-size-sm);
|
||||
font-weight: var(--formipay-font-weight-semibold);
|
||||
color: var(--formipay-color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--formipay-color-border);
|
||||
}
|
||||
|
||||
.formipay-table td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--formipay-color-border);
|
||||
}
|
||||
|
||||
.formipay-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.formipay-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
STATUS BADGES
|
||||
============================================ */
|
||||
|
||||
.formipay-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--formipay-radius-full);
|
||||
font-size: var(--formipay-font-size-sm);
|
||||
font-weight: var(--formipay-font-weight-medium);
|
||||
}
|
||||
|
||||
.formipay-badge-success {
|
||||
background-color: #edfaef;
|
||||
color: var(--formipay-color-success);
|
||||
}
|
||||
|
||||
.formipay-badge-warning {
|
||||
background-color: #fff8e5;
|
||||
color: var(--formipay-color-warning);
|
||||
}
|
||||
|
||||
.formipay-badge-danger {
|
||||
background-color: #fce8e6;
|
||||
color: var(--formipay-color-danger);
|
||||
}
|
||||
|
||||
.formipay-badge-info {
|
||||
background-color: #e7f3ff;
|
||||
color: var(--formipay-color-primary);
|
||||
}
|
||||
|
||||
.formipay-badge-default {
|
||||
background-color: #f0f0f1;
|
||||
color: var(--formipay-color-text-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
NOTICE / ALERT COMPONENTS
|
||||
============================================ */
|
||||
|
||||
.formipay-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--formipay-spacing-md);
|
||||
padding: var(--formipay-spacing-md) var(--formipay-spacing-lg);
|
||||
border-radius: var(--formipay-radius-sm);
|
||||
margin: var(--formipay-spacing-md) 0;
|
||||
}
|
||||
|
||||
.formipay-notice-icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.formipay-notice-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.formipay-notice-title {
|
||||
font-weight: var(--formipay-font-weight-semibold);
|
||||
margin-bottom: var(--formipay-spacing-xs);
|
||||
}
|
||||
|
||||
.formipay-notice-success {
|
||||
background-color: #edfaef;
|
||||
border-left: 4px solid var(--formipay-color-success);
|
||||
}
|
||||
|
||||
.formipay-notice-warning {
|
||||
background-color: #fff8e5;
|
||||
border-left: 4px solid var(--formipay-color-warning);
|
||||
}
|
||||
|
||||
.formipay-notice-error {
|
||||
background-color: #fce8e6;
|
||||
border-left: 4px solid var(--formipay-color-danger);
|
||||
}
|
||||
|
||||
.formipay-notice-info {
|
||||
background-color: #e7f3ff;
|
||||
border-left: 4px solid var(--formipay-color-primary);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LOADING & EMPTY STATES
|
||||
============================================ */
|
||||
|
||||
.formipay-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.formipay-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--formipay-color-border);
|
||||
border-top-color: var(--formipay-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: formipay-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes formipay-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.formipay-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.formipay-empty-icon {
|
||||
font-size: 48px;
|
||||
color: var(--formipay-color-border-dark);
|
||||
margin-bottom: var(--formipay-spacing-md);
|
||||
}
|
||||
|
||||
.formipay-empty-title {
|
||||
font-size: var(--formipay-font-size-lg);
|
||||
font-weight: var(--formipay-font-weight-semibold);
|
||||
margin-bottom: var(--formipay-spacing-sm);
|
||||
}
|
||||
|
||||
.formipay-empty-description {
|
||||
color: var(--formipay-color-text-muted);
|
||||
margin-bottom: var(--formipay-spacing-lg);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITIES
|
||||
============================================ */
|
||||
|
||||
.formipay-text-center { text-align: center; }
|
||||
.formipay-text-right { text-align: right; }
|
||||
.formipay-text-muted { color: var(--formipay-color-text-muted); }
|
||||
.formipay-text-small { font-size: var(--formipay-font-size-sm); }
|
||||
.formipay-text-large { font-size: var(--formipay-font-size-lg); }
|
||||
|
||||
.formipay-mt-0 { margin-top: 0; }
|
||||
.formipay-mt-1 { margin-top: var(--formipay-spacing-xs); }
|
||||
.formipay-mt-2 { margin-top: var(--formipay-spacing-sm); }
|
||||
.formipay-mt-3 { margin-top: var(--formipay-spacing-md); }
|
||||
.formipay-mt-4 { margin-top: var(--formipay-spacing-lg); }
|
||||
|
||||
.formipay-mb-0 { margin-bottom: 0; }
|
||||
.formipay-mb-1 { margin-bottom: var(--formipay-spacing-xs); }
|
||||
.formipay-mb-2 { margin-bottom: var(--formipay-spacing-sm); }
|
||||
.formipay-mb-3 { margin-bottom: var(--formipay-spacing-md); }
|
||||
.formipay-mb-4 { margin-bottom: var(--formipay-spacing-lg); }
|
||||
|
||||
.formipay-flex { display: flex; }
|
||||
.formipay-flex-center { display: flex; align-items: center; justify-content: center; }
|
||||
.formipay-flex-between { display: flex; align-items: center; justify-content: space-between; }
|
||||
.formipay-flex-column { display: flex; flex-direction: column; }
|
||||
|
||||
.formipay-gap-1 { gap: var(--formipay-spacing-xs); }
|
||||
.formipay-gap-2 { gap: var(--formipay-spacing-sm); }
|
||||
.formipay-gap-3 { gap: var(--formipay-spacing-md); }
|
||||
.formipay-gap-4 { gap: var(--formipay-spacing-lg); }
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.formipay-tab-nav {
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.formipay-nav-item {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.formipay-field {
|
||||
padding: var(--formipay-spacing-md) 0;
|
||||
}
|
||||
}
|
||||
129
src/admin/design-system/index.js
Normal file
129
src/admin/design-system/index.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Formipay Design System - WPCFTO-inspired
|
||||
* Exports design system CSS and React components
|
||||
*/
|
||||
|
||||
// Export components
|
||||
export { default } from './WpcftoComponents';
|
||||
export {
|
||||
Box,
|
||||
BoxChild,
|
||||
TabNav,
|
||||
TabPanel,
|
||||
Field,
|
||||
Input,
|
||||
Textarea,
|
||||
Select,
|
||||
Checkbox,
|
||||
Button,
|
||||
Repeater,
|
||||
Notice,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
Badge,
|
||||
Table,
|
||||
} from './WpcftoComponents';
|
||||
|
||||
// Export CSS import helper
|
||||
export const WpcftoDesign = {
|
||||
css: './WpcftoDesign.css',
|
||||
description: 'WPCFTO-inspired design system for React components',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
// Design tokens documentation
|
||||
export const tokens = {
|
||||
colors: {
|
||||
primary: '#2985f7',
|
||||
sidebarBg: '#1e2a36',
|
||||
sidebarText: '#fff',
|
||||
contentBg: '#fff',
|
||||
border: '#f0f0f1',
|
||||
borderDark: '#8c99a5',
|
||||
inputBg: '#f6f9fc',
|
||||
text: '#1d2327',
|
||||
textMuted: '#646970',
|
||||
danger: '#d63638',
|
||||
success: '#00a32a',
|
||||
warning: '#dba617',
|
||||
},
|
||||
spacing: {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '20px',
|
||||
xxl: '24px',
|
||||
},
|
||||
borderRadius: {
|
||||
sm: '4px',
|
||||
md: '10px',
|
||||
lg: '30px',
|
||||
full: '50%',
|
||||
},
|
||||
fontSize: {
|
||||
sm: '13px',
|
||||
base: '14px',
|
||||
md: '15px',
|
||||
lg: '18px',
|
||||
},
|
||||
fontFamily: "'Roboto', -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
};
|
||||
|
||||
// Component usage examples
|
||||
export const usage = {
|
||||
Box: `
|
||||
<Box>
|
||||
<BoxChild>
|
||||
Content goes here
|
||||
</BoxChild>
|
||||
</Box>
|
||||
`,
|
||||
|
||||
TabNav: `
|
||||
<TabNav
|
||||
tabs={[
|
||||
{ id: 'general', label: 'General', icon: '⚙' },
|
||||
{ id: 'settings', label: 'Settings', icon: '🔧' }
|
||||
]}
|
||||
activeTab="general"
|
||||
onTabChange={(tabId) => console.log(tabId)}
|
||||
/>
|
||||
`,
|
||||
|
||||
Field: `
|
||||
<Field
|
||||
label="Field Label"
|
||||
description="Field description"
|
||||
required
|
||||
>
|
||||
<Input value={value} onChange={setValue} />
|
||||
</Field>
|
||||
`,
|
||||
|
||||
Button: `
|
||||
<Button variant="primary" onClick={handleClick}>
|
||||
Click Me
|
||||
</Button>
|
||||
`,
|
||||
|
||||
Repeater: `
|
||||
<Repeater
|
||||
items={items}
|
||||
renderItem={(item, index) => <YourComponent item={item} />}
|
||||
onAdd={handleAdd}
|
||||
onRemove={handleRemove}
|
||||
addLabel="Add Item"
|
||||
/>
|
||||
`,
|
||||
|
||||
Notice: `
|
||||
<Notice type="success" title="Success!">
|
||||
Your changes have been saved.
|
||||
</Notice>
|
||||
`,
|
||||
|
||||
Badge: `
|
||||
<Badge variant="success">Active</Badge>
|
||||
`,
|
||||
};
|
||||
@@ -6,7 +6,59 @@ import { __ } from '@wordpress/i18n';
|
||||
import DataTable from '../components/shared/DataTable';
|
||||
import './AdminPages.css';
|
||||
|
||||
// SweetAlert2 is loaded via WordPress (global scope)
|
||||
const Swal = window.Swal;
|
||||
|
||||
export default function AccessPage() {
|
||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||
const nonce = window.formipayAdmin?.nonce || '';
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to delete this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-delete-access-item`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Confirm', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-access-item`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'ID',
|
||||
@@ -16,6 +68,38 @@ export default function AccessPage() {
|
||||
{
|
||||
key: 'title',
|
||||
label: __('Title', 'formipay'),
|
||||
render: (row) => (
|
||||
<>
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>
|
||||
<strong>{row.title || row.post_title || __('Untitled', 'formipay')}</strong>
|
||||
</a>
|
||||
<span className="row-actions">
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>{__('edit', 'formipay')}</a>
|
||||
{' | '}
|
||||
<button
|
||||
className="button-link delete"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDelete(row.ID);
|
||||
}}
|
||||
>
|
||||
{__('delete', 'formipay')}
|
||||
</button>
|
||||
{' | '}
|
||||
<button
|
||||
className="button-link duplicate"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDuplicate(row.ID);
|
||||
}}
|
||||
>
|
||||
{__('duplicate', 'formipay')}
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'product_name',
|
||||
@@ -50,10 +134,6 @@ export default function AccessPage() {
|
||||
|
||||
return (
|
||||
<div className="formipay-page-access">
|
||||
<div className="formipay-page-header">
|
||||
<h1>{ __('Access Items', 'formipay') }</h1>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||
|
||||
@@ -11,6 +11,55 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/** Navigation Menu (App-level) **/
|
||||
.formipay-navigation-menu {
|
||||
margin-left: -20px;
|
||||
padding: 12px 20px 12px 50px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
gap: 2em;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 32px;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px #d2d2d2;
|
||||
width: calc(100% + 20px);
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.formipay-navigation-menu > img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navigation-links {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navigation-links .nav-link {
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
color: #646970;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navigation-links .nav-link:hover {
|
||||
background-color: #f6f7f7;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.navigation-links .nav-link.active {
|
||||
background-color: #2271b1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
|
||||
@@ -6,7 +6,59 @@ import { __ } from '@wordpress/i18n';
|
||||
import DataTable from '../components/shared/DataTable';
|
||||
import './AdminPages.css';
|
||||
|
||||
// SweetAlert2 is loaded via WordPress (global scope)
|
||||
const Swal = window.Swal;
|
||||
|
||||
export default function CouponsPage() {
|
||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||
const nonce = window.formipayAdmin?.nonce || '';
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to delete this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-delete-coupon`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Confirm', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-coupon`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'ID',
|
||||
@@ -16,7 +68,36 @@ export default function CouponsPage() {
|
||||
{
|
||||
key: 'code',
|
||||
label: __('Coupon Code', 'formipay'),
|
||||
render: (row) => <strong>{row.code || row.post_title}</strong>
|
||||
render: (row) => (
|
||||
<>
|
||||
<strong>{row.code || row.post_title}</strong>
|
||||
<span className="row-actions">
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>{__('edit', 'formipay')}</a>
|
||||
{' | '}
|
||||
<button
|
||||
className="button-link delete"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDelete(row.ID);
|
||||
}}
|
||||
>
|
||||
{__('delete', 'formipay')}
|
||||
</button>
|
||||
{' | '}
|
||||
<button
|
||||
className="button-link duplicate"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDuplicate(row.ID);
|
||||
}}
|
||||
>
|
||||
{__('duplicate', 'formipay')}
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
@@ -30,8 +111,21 @@ export default function CouponsPage() {
|
||||
key: 'amount',
|
||||
label: __('Amount', 'formipay'),
|
||||
render: (row) => {
|
||||
// Multi-currency display would go here
|
||||
return row.amount || '-';
|
||||
const amount = row.amount;
|
||||
// Handle multi-currency amounts (array of objects)
|
||||
if (Array.isArray(amount)) {
|
||||
return amount.map((a) => (
|
||||
<div key={a.raw} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
{a.flag && <img src={a.flag} alt="" height="14" style={{ verticalAlign: 'middle' }} />}
|
||||
<span>{a.amount}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
// Handle single currency amount (number or string)
|
||||
if (typeof amount === 'number') {
|
||||
return amount + '%';
|
||||
}
|
||||
return amount || '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -65,10 +159,6 @@ export default function CouponsPage() {
|
||||
|
||||
return (
|
||||
<div className="formipay-page-coupons">
|
||||
<div className="formipay-page-header">
|
||||
<h1>{ __('Coupons', 'formipay') }</h1>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||
|
||||
@@ -9,14 +9,21 @@ import './AdminPages.css';
|
||||
export default function CustomersPage() {
|
||||
const columns = [
|
||||
{
|
||||
key: 'id',
|
||||
key: 'ID',
|
||||
label: __('ID', 'formipay'),
|
||||
render: (row) => <strong>#{row.id}</strong>
|
||||
render: (row) => <strong>#{row.ID}</strong>
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: __('Name', 'formipay'),
|
||||
render: (row) => row.name || row.full_name || '-'
|
||||
render: (row) => (
|
||||
<>
|
||||
<strong>{row.name || row.full_name || '-'}</strong>
|
||||
<span className="row-actions">
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/admin.php?page=formipay-customers&customer_id=${row.ID}`}>{__('view', 'formipay')}</a>
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
@@ -32,23 +39,10 @@ export default function CustomersPage() {
|
||||
label: __('Total Orders', 'formipay'),
|
||||
render: (row) => row.total_order || row.total_orders || 0
|
||||
},
|
||||
{
|
||||
key: 'created_date',
|
||||
label: __('Date', 'formipay'),
|
||||
render: (row) => {
|
||||
const date = row.created_date || row.date;
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="formipay-page-customers">
|
||||
<div className="formipay-page-header">
|
||||
<h1>{ __('Customers', 'formipay') }</h1>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||
|
||||
@@ -4,12 +4,60 @@
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import DataTable from '../components/shared/DataTable';
|
||||
import './AdminPages.css';
|
||||
|
||||
// SweetAlert2 is loaded via WordPress (global scope)
|
||||
const Swal = window.Swal;
|
||||
|
||||
export default function FormsPage() {
|
||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||
const nonce = window.formipayAdmin?.nonce || '';
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to delete this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-delete-form`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Confirm', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-form`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'ID',
|
||||
@@ -20,9 +68,36 @@ export default function FormsPage() {
|
||||
key: 'title',
|
||||
label: __('Title', 'formipay'),
|
||||
render: (row) => (
|
||||
<>
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>
|
||||
<strong>{row.title}</strong>
|
||||
</a>
|
||||
<span className="row-actions">
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>{__('edit', 'formipay')}</a>
|
||||
{' | '}
|
||||
<button
|
||||
className="button-link delete"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDelete(row.ID);
|
||||
}}
|
||||
>
|
||||
{__('delete', 'formipay')}
|
||||
</button>
|
||||
{' | '}
|
||||
<button
|
||||
className="button-link duplicate"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDuplicate(row.ID);
|
||||
}}
|
||||
>
|
||||
{__('duplicate', 'formipay')}
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -75,7 +150,7 @@ export default function FormsPage() {
|
||||
const text = e.currentTarget.dataset.copy;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const originalHTML = e.currentTarget.innerHTML;
|
||||
e.currentTarget.innerHTML = '✓ Copied';
|
||||
e.currentTarget.innerHTML = '[Copied]';
|
||||
setTimeout(() => {
|
||||
e.currentTarget.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
@@ -92,7 +167,7 @@ export default function FormsPage() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
📋 {__('Copy', 'formipay')}
|
||||
{__('Copy', 'formipay')}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
@@ -100,11 +175,6 @@ export default function FormsPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="formipay-page-forms">
|
||||
<div className="formipay-page-header">
|
||||
<h1>{ __('Forms', 'formipay') }</h1>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||
@@ -132,6 +202,5 @@ export default function FormsPage() {
|
||||
}}
|
||||
emptyMessage={__('No forms found', 'formipay')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,36 @@ import { __ } from '@wordpress/i18n';
|
||||
import DataTable from '../components/shared/DataTable';
|
||||
import './AdminPages.css';
|
||||
|
||||
// SweetAlert2 is loaded via WordPress (global scope)
|
||||
const Swal = window.Swal;
|
||||
|
||||
export default function LicensesPage() {
|
||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||
const nonce = window.formipayAdmin?.nonce || '';
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to delete this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-delete-license`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'id',
|
||||
@@ -16,7 +45,23 @@ export default function LicensesPage() {
|
||||
{
|
||||
key: 'license_key',
|
||||
label: __('License Key', 'formipay'),
|
||||
render: (row) => <code>{row.license_key || '-'}</code>
|
||||
render: (row) => (
|
||||
<>
|
||||
<code>{row.license_key || '-'}</code>
|
||||
<span className="row-actions">
|
||||
<button
|
||||
className="button-link delete"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDelete(row.id);
|
||||
}}
|
||||
>
|
||||
{__('delete', 'formipay')}
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'product',
|
||||
@@ -54,10 +99,6 @@ export default function LicensesPage() {
|
||||
|
||||
return (
|
||||
<div className="formipay-page-licenses">
|
||||
<div className="formipay-page-header">
|
||||
<h1>{ __('Licenses', 'formipay') }</h1>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import OrderList from '../components/orders/OrderList';
|
||||
import DataTable from '../components/shared/DataTable';
|
||||
import OrderDetail from '../components/orders/OrderDetail';
|
||||
|
||||
export default function OrdersPage({ initialData }) {
|
||||
export default function OrdersPage() {
|
||||
const [selectedOrderId, setSelectedOrderId] = useState(null);
|
||||
|
||||
if (selectedOrderId) {
|
||||
@@ -19,9 +19,85 @@ export default function OrdersPage({ initialData }) {
|
||||
);
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'ID',
|
||||
label: __('ID', 'formipay'),
|
||||
render: (row) => <strong>#{row.id}</strong>
|
||||
},
|
||||
{
|
||||
key: 'created_date',
|
||||
label: __('Date', 'formipay'),
|
||||
render: (row) => {
|
||||
const date = row.created_date || row.date;
|
||||
if (!date) return '-';
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'customer',
|
||||
label: __('Customer', 'formipay'),
|
||||
render: (row) => {
|
||||
// Extract customer info from form_data
|
||||
if (row.form_data && Array.isArray(row.form_data)) {
|
||||
const name = row.form_data.find(f => f.name === 'name')?.value;
|
||||
const email = row.form_data.find(f => f.name === 'email')?.value;
|
||||
return name || email || '-';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
label: __('Total', 'formipay'),
|
||||
render: (row) => row.total_formatted || row.total || '-'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: __('Status', 'formipay'),
|
||||
render: (row) => {
|
||||
const statusLabels = {
|
||||
'on-hold': __('On Hold', 'formipay'),
|
||||
'payment-confirm': __('Payment Confirmed', 'formipay'),
|
||||
'in-progress': __('In Progress', 'formipay'),
|
||||
'shipping': __('Shipping', 'formipay'),
|
||||
'completed': __('Completed', 'formipay'),
|
||||
'failed': __('Failed', 'formipay'),
|
||||
'refunded': __('Refunded', 'formipay'),
|
||||
'cancelled': __('Cancelled', 'formipay'),
|
||||
};
|
||||
const status = row.status || 'unknown';
|
||||
return (
|
||||
<OrderList
|
||||
onSelectOrder={(orderId) => setSelectedOrderId(orderId)}
|
||||
<span className={`status-label status-${status}`}>
|
||||
{statusLabels[status] || status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: __('Actions', 'formipay'),
|
||||
render: (row) => (
|
||||
<button
|
||||
className="button button-small"
|
||||
onClick={() => setSelectedOrderId(row.id)}
|
||||
>
|
||||
{__('View', 'formipay')}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||
nonce={window.formipayAdmin?.nonce || ''}
|
||||
tableAction="formipay-tabledata-orders"
|
||||
selectable={false}
|
||||
inline={false}
|
||||
emptyMessage={__('No orders found', 'formipay')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,62 @@ import DataTable from '../components/shared/DataTable';
|
||||
import VariationPricingTable from '../components/products/VariationPricingTable';
|
||||
import './AdminPages.css';
|
||||
|
||||
// SweetAlert2 is loaded via WordPress (global scope)
|
||||
const Swal = window.Swal;
|
||||
|
||||
export default function ProductsPage() {
|
||||
const [isEditor, setIsEditor] = useState(false);
|
||||
const [selectedProductId, setSelectedProductId] = useState(null);
|
||||
|
||||
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
|
||||
const nonce = window.formipayAdmin?.nonce || '';
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to delete this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Delete Permanently', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-delete-product`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = async (id) => {
|
||||
const result = await Swal.fire({
|
||||
icon: 'info',
|
||||
html: __('Do you want to duplicate this item?', 'formipay'),
|
||||
showCancelButton: true,
|
||||
confirmButtonText: __('Confirm', 'formipay'),
|
||||
cancelButtonText: __('Cancel', 'formipay'),
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await fetch(`${ajaxUrl}?action=formipay-duplicate-product`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
id,
|
||||
_wpnonce: nonce,
|
||||
}),
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// If in editor mode, show the variation pricing table
|
||||
if (isEditor && selectedProductId) {
|
||||
return (
|
||||
@@ -48,32 +100,33 @@ export default function ProductsPage() {
|
||||
label: __('Title', 'formipay'),
|
||||
render: (row) => (
|
||||
<>
|
||||
<strong>
|
||||
<button
|
||||
className="button-link"
|
||||
onClick={() => {
|
||||
setSelectedProductId(row.ID);
|
||||
setIsEditor(true);
|
||||
}}
|
||||
>
|
||||
{row.post_title || row.title || __('Untitled', 'formipay')}
|
||||
</button>
|
||||
</strong>
|
||||
<br />
|
||||
<span className="row-actions" style={{ display: 'none', visibility: 'hidden' }}>
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>
|
||||
<strong>{row.post_title || row.title || __('Untitled', 'formipay')}</strong>
|
||||
</a>
|
||||
<span className="row-actions">
|
||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>{__('edit', 'formipay')}</a>
|
||||
{' | '}
|
||||
<button className="button-link" onClick={() => {
|
||||
setSelectedProductId(row.ID);
|
||||
setIsEditor(true);
|
||||
}}>
|
||||
{__('Edit Variations', 'formipay')}
|
||||
{__('edit variations', 'formipay')}
|
||||
</button>
|
||||
{' | '}
|
||||
<button className="button-link delete" data-id={row.ID}>
|
||||
{__('Delete', 'formipay')}
|
||||
<button className="button-link delete" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDelete(row.ID);
|
||||
}}>
|
||||
{__('delete', 'formipay')}
|
||||
</button>
|
||||
{' | '}
|
||||
<button className="button-link duplicate" data-id={row.ID}>
|
||||
{__('Duplicate', 'formipay')}
|
||||
<button className="button-link duplicate" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDuplicate(row.ID);
|
||||
}}>
|
||||
{__('duplicate', 'formipay')}
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
@@ -83,16 +136,29 @@ export default function ProductsPage() {
|
||||
key: 'price',
|
||||
label: __('Price', 'formipay'),
|
||||
render: (row) => {
|
||||
// Multi-currency price display
|
||||
const prices = row.prices || row.price;
|
||||
if (typeof prices === 'object' && prices !== null) {
|
||||
return Object.entries(prices).map(([currency, price]) => (
|
||||
<div key={currency}>
|
||||
{currency}: {price}
|
||||
if (Array.isArray(prices) && prices.length > 0) {
|
||||
return prices.map((p) => (
|
||||
<div key={p.currency} style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '2px' }}>
|
||||
{p.flag && <img src={p.flag} alt="" height="14" style={{ verticalAlign: 'middle' }} />}
|
||||
{p.has_sale ? (
|
||||
<>
|
||||
<span style={{ fontSize: '12px', textDecoration: 'line-through', opacity: '0.7', marginRight: '4px' }}>
|
||||
{p.regular_price}
|
||||
</span>
|
||||
<span style={{ fontSize: '13px', fontWeight: '500' }}>
|
||||
{p.sale_price}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontSize: '13px' }}>
|
||||
{p.regular_price}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
return prices || '-';
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -132,10 +198,6 @@ export default function ProductsPage() {
|
||||
|
||||
return (
|
||||
<div className="formipay-page-products">
|
||||
<div className="formipay-page-header">
|
||||
<h1>{ __('Products', 'formipay') }</h1>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||
|
||||
Reference in New Issue
Block a user