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' ),
|
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||||
'restUrl' => rest_url( 'formipay/v1' ),
|
'restUrl' => rest_url( 'formipay/v1' ),
|
||||||
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
'nonce' => wp_create_nonce( 'formipay-admin' ),
|
||||||
|
'pluginUrl' => FORMIPAY_URL,
|
||||||
|
'siteUrl' => site_url(),
|
||||||
] );
|
] );
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
@@ -149,7 +151,7 @@ class ReactAdmin {
|
|||||||
public static function render_mount_point( $page ) {
|
public static function render_mount_point( $page ) {
|
||||||
|
|
||||||
printf(
|
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_attr( $page ),
|
||||||
esc_html( ucfirst( $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 OrdersPage from '../pages/Orders';
|
||||||
import CustomersPage from '../pages/Customers';
|
import CustomersPage from '../pages/Customers';
|
||||||
import ProductsPage from '../pages/Products';
|
import ProductsPage from '../pages/Products';
|
||||||
@@ -10,6 +10,8 @@ import FormsPage from '../pages/Forms';
|
|||||||
import CouponsPage from '../pages/Coupons';
|
import CouponsPage from '../pages/Coupons';
|
||||||
import AccessPage from '../pages/Access';
|
import AccessPage from '../pages/Access';
|
||||||
import LicensesPage from '../pages/Licenses';
|
import LicensesPage from '../pages/Licenses';
|
||||||
|
import NavigationMenu from './NavigationMenu';
|
||||||
|
import '../pages/AdminPages.css';
|
||||||
|
|
||||||
const pageComponents = {
|
const pageComponents = {
|
||||||
orders: OrdersPage,
|
orders: OrdersPage,
|
||||||
@@ -21,23 +23,88 @@ const pageComponents = {
|
|||||||
licenses: LicensesPage,
|
licenses: LicensesPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App({ page, initialData }) {
|
export default function App({ page: initialPage, initialData }) {
|
||||||
useEffect(() => {
|
// Use state for client-side routing
|
||||||
console.log('[Formipay App] Rendering page:', page, 'with data:', initialData);
|
// Prioritize hash over initial page for reload support
|
||||||
}, [page, initialData]);
|
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) {
|
if (!PageComponent) {
|
||||||
return (
|
return (
|
||||||
<div className="formipay-error">
|
<div className="formipay-error">
|
||||||
<p>Unknown page: {page}</p>
|
<p>Unknown page: {currentPage}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="formipay-admin-wrap">
|
<div className="formipay-admin-wrap">
|
||||||
|
<NavigationMenu
|
||||||
|
currentPage={currentPage}
|
||||||
|
onPageNavigate={handlePageNavigate}
|
||||||
|
/>
|
||||||
<PageComponent initialData={initialData} />
|
<PageComponent initialData={initialData} />
|
||||||
</div>
|
</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 { __ } from '@wordpress/i18n';
|
||||||
import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
|
import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
|
||||||
import { TextControl, Button } from '@wordpress/components';
|
import { TextControl, Button } from '@wordpress/components';
|
||||||
import { Icon } from '@wordpress/icons';
|
import * as Icons 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 './VariationPricingTable.css';
|
import './VariationPricingTable.css';
|
||||||
|
|
||||||
export default function VariationPricingTable({ productId, productDetails }) {
|
export default function VariationPricingTable({ productId, productDetails }) {
|
||||||
@@ -421,12 +418,12 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
|||||||
className="toggle-expand"
|
className="toggle-expand"
|
||||||
onClick={onToggleExpanded}
|
onClick={onToggleExpanded}
|
||||||
>
|
>
|
||||||
<Icon icon={row.expanded ? eyeOpened : eyeClosed} size={16} />
|
{row.expanded ? '▼' : '▶'}
|
||||||
</button>
|
</button>
|
||||||
<strong>{ row.name }</strong>
|
<strong>{ row.name }</strong>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{showFlatPricing ? (
|
{showFlatPricing && row.prices?.[0] ? (
|
||||||
<>
|
<>
|
||||||
<PriceCell
|
<PriceCell
|
||||||
price={row.prices[0]}
|
price={row.prices[0]}
|
||||||
@@ -465,7 +462,7 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
|||||||
size="small"
|
size="small"
|
||||||
isDestructive
|
isDestructive
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
icon={minus()}
|
icon={Icons.trash}
|
||||||
>
|
>
|
||||||
{ __('Delete', 'formipay') }
|
{ __('Delete', 'formipay') }
|
||||||
</Button>
|
</Button>
|
||||||
@@ -477,7 +474,7 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
|||||||
<td colSpan="5">
|
<td colSpan="5">
|
||||||
<table className="inner-table">
|
<table className="inner-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{row.prices.map((price, currencyIndex) => {
|
{(row.prices || []).map((price, currencyIndex) => {
|
||||||
const code = String(price.currency).split(':::')[0];
|
const code = String(price.currency).split(':::')[0];
|
||||||
const isDefault = code === defaultCurrencyCode;
|
const isDefault = code === defaultCurrencyCode;
|
||||||
const step = price.currency_decimal_digits
|
const step = price.currency_decimal_digits
|
||||||
@@ -523,6 +520,10 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode,
|
|||||||
|
|
||||||
// Price input cell component
|
// Price input cell component
|
||||||
function PriceCell({ price, field, onChange }) {
|
function PriceCell({ price, field, onChange }) {
|
||||||
|
if (!price) {
|
||||||
|
return <td className="price-cell">-</td>;
|
||||||
|
}
|
||||||
|
|
||||||
const step = price.currency_decimal_digits
|
const step = price.currency_decimal_digits
|
||||||
? 1 / Math.pow(10, price.currency_decimal_digits)
|
? 1 / Math.pow(10, price.currency_decimal_digits)
|
||||||
: 0.01;
|
: 0.01;
|
||||||
|
|||||||
@@ -21,6 +21,18 @@
|
|||||||
min-width: 150px;
|
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 */
|
/* Filter Tabs */
|
||||||
.formipay-filter-tabs {
|
.formipay-filter-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -112,6 +124,14 @@
|
|||||||
background-color: #f0f0f1;
|
background-color: #f0f0f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formipay-table *:is(td, th):first-child {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formipay-table th.column-select > input {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Checkbox Column */
|
/* Checkbox Column */
|
||||||
.formipay-table .column-select {
|
.formipay-table .column-select {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export default function DataTable({
|
|||||||
tableAction, // e.g., 'formipay-tabledata-forms'
|
tableAction, // e.g., 'formipay-tabledata-forms'
|
||||||
deleteAction,
|
deleteAction,
|
||||||
duplicateAction,
|
duplicateAction,
|
||||||
|
|
||||||
|
// Selection callback
|
||||||
|
onSelectionChange,
|
||||||
}) {
|
}) {
|
||||||
// State
|
// State
|
||||||
const [data, setData] = useState(initialData);
|
const [data, setData] = useState(initialData);
|
||||||
@@ -82,6 +85,13 @@ export default function DataTable({
|
|||||||
const [selectedRows, setSelectedRows] = useState(new Set());
|
const [selectedRows, setSelectedRows] = useState(new Set());
|
||||||
const [selectAll, setSelectAll] = useState(false);
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
|
||||||
|
// Notify parent of selection changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (onSelectionChange) {
|
||||||
|
onSelectionChange(selectedRows);
|
||||||
|
}
|
||||||
|
}, [selectedRows, onSelectionChange]);
|
||||||
|
|
||||||
// Add New Modal
|
// Add New Modal
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [newItemTitle, setNewItemTitle] = useState('');
|
const [newItemTitle, setNewItemTitle] = useState('');
|
||||||
@@ -89,8 +99,6 @@ export default function DataTable({
|
|||||||
// Derive action names from tableAction
|
// Derive action names from tableAction
|
||||||
const baseActionName = tableAction.replace('formipay-tabledata-', '');
|
const baseActionName = tableAction.replace('formipay-tabledata-', '');
|
||||||
const bulkDeleteAction = actions.bulkDelete?.action || `formipay-bulk-delete-${baseActionName}`;
|
const bulkDeleteAction = actions.bulkDelete?.action || `formipay-bulk-delete-${baseActionName}`;
|
||||||
const deleteActionName = deleteAction || `formipay-delete-${baseActionName}`;
|
|
||||||
const duplicateActionName = duplicateAction || `formipay-duplicate-${baseActionName}`;
|
|
||||||
|
|
||||||
// Load data
|
// Load data
|
||||||
const loadData = useCallback(async () => {
|
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
|
// Handle Add New
|
||||||
const handleAddNew = async () => {
|
const handleAddNew = async () => {
|
||||||
if (!newItemTitle.trim()) {
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
{/* Filter Tabs */}
|
||||||
@@ -408,10 +375,6 @@ export default function DataTable({
|
|||||||
{column.label}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{/* Actions column */}
|
|
||||||
{actions.inline && (
|
|
||||||
<th className="column-actions">{__('Actions', 'formipay')}</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -435,41 +398,6 @@ export default function DataTable({
|
|||||||
{column.render ? column.render(row) : row[column.key]}
|
{column.render ? column.render(row) : row[column.key]}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -531,7 +459,7 @@ export default function DataTable({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add New Modal - only render when actually open */}
|
{/* Add New Modal - only render when open */}
|
||||||
{actions.addNew && isAddModalOpen && (
|
{actions.addNew && isAddModalOpen && (
|
||||||
<Modal
|
<Modal
|
||||||
title={actions.addNew.label || __('Add New', 'formipay')}
|
title={actions.addNew.label || __('Add New', 'formipay')}
|
||||||
@@ -539,7 +467,6 @@ export default function DataTable({
|
|||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
setNewItemTitle('');
|
setNewItemTitle('');
|
||||||
}}
|
}}
|
||||||
isDismissible
|
|
||||||
>
|
>
|
||||||
<TextControl
|
<TextControl
|
||||||
__next40pxDefaultSize
|
__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 DataTable from '../components/shared/DataTable';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
|
// SweetAlert2 is loaded via WordPress (global scope)
|
||||||
|
const Swal = window.Swal;
|
||||||
|
|
||||||
export default function AccessPage() {
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: 'ID',
|
key: 'ID',
|
||||||
@@ -16,6 +68,38 @@ export default function AccessPage() {
|
|||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
label: __('Title', 'formipay'),
|
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',
|
key: 'product_name',
|
||||||
@@ -50,10 +134,6 @@ export default function AccessPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="formipay-page-access">
|
<div className="formipay-page-access">
|
||||||
<div className="formipay-page-header">
|
|
||||||
<h1>{ __('Access Items', 'formipay') }</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||||
|
|||||||
@@ -11,6 +11,55 @@
|
|||||||
font-weight: 400;
|
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 {
|
.status-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
@@ -42,4 +91,4 @@ code {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,59 @@ import { __ } from '@wordpress/i18n';
|
|||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
|
// SweetAlert2 is loaded via WordPress (global scope)
|
||||||
|
const Swal = window.Swal;
|
||||||
|
|
||||||
export default function CouponsPage() {
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: 'ID',
|
key: 'ID',
|
||||||
@@ -16,7 +68,36 @@ export default function CouponsPage() {
|
|||||||
{
|
{
|
||||||
key: 'code',
|
key: 'code',
|
||||||
label: __('Coupon Code', 'formipay'),
|
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',
|
key: 'type',
|
||||||
@@ -30,8 +111,21 @@ export default function CouponsPage() {
|
|||||||
key: 'amount',
|
key: 'amount',
|
||||||
label: __('Amount', 'formipay'),
|
label: __('Amount', 'formipay'),
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
// Multi-currency display would go here
|
const amount = row.amount;
|
||||||
return 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 (
|
return (
|
||||||
<div className="formipay-page-coupons">
|
<div className="formipay-page-coupons">
|
||||||
<div className="formipay-page-header">
|
|
||||||
<h1>{ __('Coupons', 'formipay') }</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||||
|
|||||||
@@ -9,14 +9,21 @@ import './AdminPages.css';
|
|||||||
export default function CustomersPage() {
|
export default function CustomersPage() {
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: 'id',
|
key: 'ID',
|
||||||
label: __('ID', 'formipay'),
|
label: __('ID', 'formipay'),
|
||||||
render: (row) => <strong>#{row.id}</strong>
|
render: (row) => <strong>#{row.ID}</strong>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
label: __('Name', 'formipay'),
|
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',
|
key: 'email',
|
||||||
@@ -32,23 +39,10 @@ export default function CustomersPage() {
|
|||||||
label: __('Total Orders', 'formipay'),
|
label: __('Total Orders', 'formipay'),
|
||||||
render: (row) => row.total_order || row.total_orders || 0
|
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 (
|
return (
|
||||||
<div className="formipay-page-customers">
|
<div className="formipay-page-customers">
|
||||||
<div className="formipay-page-header">
|
|
||||||
<h1>{ __('Customers', 'formipay') }</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||||
|
|||||||
@@ -4,12 +4,60 @@
|
|||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
import './AdminPages.css';
|
|
||||||
|
|
||||||
// SweetAlert2 is loaded via WordPress (global scope)
|
// SweetAlert2 is loaded via WordPress (global scope)
|
||||||
const Swal = window.Swal;
|
const Swal = window.Swal;
|
||||||
|
|
||||||
export default function FormsPage() {
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: 'ID',
|
key: 'ID',
|
||||||
@@ -20,9 +68,36 @@ export default function FormsPage() {
|
|||||||
key: 'title',
|
key: 'title',
|
||||||
label: __('Title', 'formipay'),
|
label: __('Title', 'formipay'),
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>
|
<>
|
||||||
<strong>{row.title}</strong>
|
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>
|
||||||
</a>
|
<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;
|
const text = e.currentTarget.dataset.copy;
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
const originalHTML = e.currentTarget.innerHTML;
|
const originalHTML = e.currentTarget.innerHTML;
|
||||||
e.currentTarget.innerHTML = '✓ Copied';
|
e.currentTarget.innerHTML = '[Copied]';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
e.currentTarget.innerHTML = originalHTML;
|
e.currentTarget.innerHTML = originalHTML;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -92,7 +167,7 @@ export default function FormsPage() {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
📋 {__('Copy', 'formipay')}
|
{__('Copy', 'formipay')}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -100,38 +175,32 @@ export default function FormsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="formipay-page-forms">
|
<DataTable
|
||||||
<div className="formipay-page-header">
|
columns={columns}
|
||||||
<h1>{ __('Forms', 'formipay') }</h1>
|
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||||
</div>
|
nonce={window.formipayAdmin?.nonce || ''}
|
||||||
|
tableAction="formipay-tabledata-forms"
|
||||||
<DataTable
|
deleteAction="formipay-delete-form"
|
||||||
columns={columns}
|
duplicateAction="formipay-duplicate-form"
|
||||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
filterOptions={{
|
||||||
nonce={window.formipayAdmin?.nonce || ''}
|
key: 'post_status',
|
||||||
tableAction="formipay-tabledata-forms"
|
options: [
|
||||||
deleteAction="formipay-delete-form"
|
{ value: 'all', label: __('All', 'formipay') },
|
||||||
duplicateAction="formipay-duplicate-form"
|
{ value: 'publish', label: __('Published', 'formipay') },
|
||||||
filterOptions={{
|
{ value: 'draft', label: __('Draft', 'formipay') },
|
||||||
key: 'post_status',
|
]
|
||||||
options: [
|
}}
|
||||||
{ value: 'all', label: __('All', 'formipay') },
|
actions={{
|
||||||
{ value: 'publish', label: __('Published', 'formipay') },
|
addNew: {
|
||||||
{ value: 'draft', label: __('Draft', 'formipay') },
|
label: __('+ Add New Form', 'formipay'),
|
||||||
]
|
action: 'formipay-create-form-post',
|
||||||
}}
|
},
|
||||||
actions={{
|
bulkDelete: {
|
||||||
addNew: {
|
action: 'formipay-bulk-delete-form',
|
||||||
label: __('+ Add New Form', 'formipay'),
|
},
|
||||||
action: 'formipay-create-form-post',
|
inline: true,
|
||||||
},
|
}}
|
||||||
bulkDelete: {
|
emptyMessage={__('No forms found', 'formipay')}
|
||||||
action: 'formipay-bulk-delete-form',
|
/>
|
||||||
},
|
|
||||||
inline: true,
|
|
||||||
}}
|
|
||||||
emptyMessage={__('No forms found', 'formipay')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,36 @@ import { __ } from '@wordpress/i18n';
|
|||||||
import DataTable from '../components/shared/DataTable';
|
import DataTable from '../components/shared/DataTable';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
|
// SweetAlert2 is loaded via WordPress (global scope)
|
||||||
|
const Swal = window.Swal;
|
||||||
|
|
||||||
export default function LicensesPage() {
|
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 = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: 'id',
|
key: 'id',
|
||||||
@@ -16,7 +45,23 @@ export default function LicensesPage() {
|
|||||||
{
|
{
|
||||||
key: 'license_key',
|
key: 'license_key',
|
||||||
label: __('License Key', 'formipay'),
|
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',
|
key: 'product',
|
||||||
@@ -54,10 +99,6 @@ export default function LicensesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="formipay-page-licenses">
|
<div className="formipay-page-licenses">
|
||||||
<div className="formipay-page-header">
|
|
||||||
<h1>{ __('Licenses', 'formipay') }</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import OrderList from '../components/orders/OrderList';
|
import DataTable from '../components/shared/DataTable';
|
||||||
import OrderDetail from '../components/orders/OrderDetail';
|
import OrderDetail from '../components/orders/OrderDetail';
|
||||||
|
|
||||||
export default function OrdersPage({ initialData }) {
|
export default function OrdersPage() {
|
||||||
const [selectedOrderId, setSelectedOrderId] = useState(null);
|
const [selectedOrderId, setSelectedOrderId] = useState(null);
|
||||||
|
|
||||||
if (selectedOrderId) {
|
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 (
|
||||||
|
<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 (
|
return (
|
||||||
<OrderList
|
<DataTable
|
||||||
onSelectOrder={(orderId) => setSelectedOrderId(orderId)}
|
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 VariationPricingTable from '../components/products/VariationPricingTable';
|
||||||
import './AdminPages.css';
|
import './AdminPages.css';
|
||||||
|
|
||||||
|
// SweetAlert2 is loaded via WordPress (global scope)
|
||||||
|
const Swal = window.Swal;
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
const [isEditor, setIsEditor] = useState(false);
|
const [isEditor, setIsEditor] = useState(false);
|
||||||
const [selectedProductId, setSelectedProductId] = useState(null);
|
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 in editor mode, show the variation pricing table
|
||||||
if (isEditor && selectedProductId) {
|
if (isEditor && selectedProductId) {
|
||||||
return (
|
return (
|
||||||
@@ -48,32 +100,33 @@ export default function ProductsPage() {
|
|||||||
label: __('Title', 'formipay'),
|
label: __('Title', 'formipay'),
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<>
|
<>
|
||||||
<strong>
|
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>
|
||||||
<button
|
<strong>{row.post_title || row.title || __('Untitled', 'formipay')}</strong>
|
||||||
className="button-link"
|
</a>
|
||||||
onClick={() => {
|
<span className="row-actions">
|
||||||
setSelectedProductId(row.ID);
|
<a href={`${window.formipayAdmin?.siteUrl || ''}/wp-admin/post.php?post=${row.ID}&action=edit`}>{__('edit', 'formipay')}</a>
|
||||||
setIsEditor(true);
|
{' | '}
|
||||||
}}
|
|
||||||
>
|
|
||||||
{row.post_title || row.title || __('Untitled', 'formipay')}
|
|
||||||
</button>
|
|
||||||
</strong>
|
|
||||||
<br />
|
|
||||||
<span className="row-actions" style={{ display: 'none', visibility: 'hidden' }}>
|
|
||||||
<button className="button-link" onClick={() => {
|
<button className="button-link" onClick={() => {
|
||||||
setSelectedProductId(row.ID);
|
setSelectedProductId(row.ID);
|
||||||
setIsEditor(true);
|
setIsEditor(true);
|
||||||
}}>
|
}}>
|
||||||
{__('Edit Variations', 'formipay')}
|
{__('edit variations', 'formipay')}
|
||||||
</button>
|
</button>
|
||||||
{' | '}
|
{' | '}
|
||||||
<button className="button-link delete" data-id={row.ID}>
|
<button className="button-link delete" onClick={(e) => {
|
||||||
{__('Delete', 'formipay')}
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(row.ID);
|
||||||
|
}}>
|
||||||
|
{__('delete', 'formipay')}
|
||||||
</button>
|
</button>
|
||||||
{' | '}
|
{' | '}
|
||||||
<button className="button-link duplicate" data-id={row.ID}>
|
<button className="button-link duplicate" onClick={(e) => {
|
||||||
{__('Duplicate', 'formipay')}
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDuplicate(row.ID);
|
||||||
|
}}>
|
||||||
|
{__('duplicate', 'formipay')}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
@@ -83,16 +136,29 @@ export default function ProductsPage() {
|
|||||||
key: 'price',
|
key: 'price',
|
||||||
label: __('Price', 'formipay'),
|
label: __('Price', 'formipay'),
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
// Multi-currency price display
|
|
||||||
const prices = row.prices || row.price;
|
const prices = row.prices || row.price;
|
||||||
if (typeof prices === 'object' && prices !== null) {
|
if (Array.isArray(prices) && prices.length > 0) {
|
||||||
return Object.entries(prices).map(([currency, price]) => (
|
return prices.map((p) => (
|
||||||
<div key={currency}>
|
<div key={p.currency} style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '2px' }}>
|
||||||
{currency}: {price}
|
{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>
|
</div>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return prices || '-';
|
return '-';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -132,10 +198,6 @@ export default function ProductsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="formipay-page-products">
|
<div className="formipay-page-products">
|
||||||
<div className="formipay-page-header">
|
|
||||||
<h1>{ __('Products', 'formipay') }</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
ajaxUrl={window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'}
|
||||||
|
|||||||
Reference in New Issue
Block a user