From 057611ef401ebd7c1c06be47a272321c33c5f73a Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 19 Apr 2026 05:58:44 +0700 Subject: [PATCH] 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 --- includes/Admin/ReactAdmin.php | 4 +- src/admin/components/App.js | 83 ++- src/admin/components/NavigationMenu.js | 49 ++ .../products/VariationPricingTable.js | 17 +- src/admin/components/shared/DataTable.css | 20 + src/admin/components/shared/DataTable.js | 113 +-- src/admin/components/shared/ScreenMenu.js | 35 + src/admin/design-system/README.md | 224 ++++++ src/admin/design-system/WpcftoComponents.js | 384 +++++++++++ src/admin/design-system/WpcftoDesign.css | 645 ++++++++++++++++++ src/admin/design-system/index.js | 129 ++++ src/admin/pages/Access.js | 88 ++- src/admin/pages/AdminPages.css | 51 +- src/admin/pages/Coupons.js | 104 ++- src/admin/pages/Customers.js | 26 +- src/admin/pages/Forms.js | 147 ++-- src/admin/pages/Licenses.js | 51 +- src/admin/pages/Orders.js | 84 ++- src/admin/pages/Products.js | 118 +++- 19 files changed, 2158 insertions(+), 214 deletions(-) create mode 100644 src/admin/components/NavigationMenu.js create mode 100644 src/admin/components/shared/ScreenMenu.js create mode 100644 src/admin/design-system/README.md create mode 100644 src/admin/design-system/WpcftoComponents.js create mode 100644 src/admin/design-system/WpcftoDesign.css create mode 100644 src/admin/design-system/index.js diff --git a/includes/Admin/ReactAdmin.php b/includes/Admin/ReactAdmin.php index c7bd80de4..07cfafd6b 100644 --- a/includes/Admin/ReactAdmin.php +++ b/includes/Admin/ReactAdmin.php @@ -65,6 +65,8 @@ class ReactAdmin { 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'restUrl' => rest_url( 'formipay/v1' ), 'nonce' => wp_create_nonce( 'formipay-admin' ), + 'pluginUrl' => FORMIPAY_URL, + 'siteUrl' => site_url(), ] ); // Debug logging @@ -149,7 +151,7 @@ class ReactAdmin { public static function render_mount_point( $page ) { printf( - '
Loading %s...
', + '
Loading %s...
', esc_attr( $page ), esc_html( ucfirst( $page ) ) ); diff --git a/src/admin/components/App.js b/src/admin/components/App.js index 76d7678a5..accd3b2f9 100644 --- a/src/admin/components/App.js +++ b/src/admin/components/App.js @@ -1,8 +1,8 @@ /** - * App Component - Main admin application shell + * App Component - Main admin application shell with client-side routing */ -import { useEffect } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; import OrdersPage from '../pages/Orders'; import CustomersPage from '../pages/Customers'; import ProductsPage from '../pages/Products'; @@ -10,6 +10,8 @@ import FormsPage from '../pages/Forms'; import CouponsPage from '../pages/Coupons'; import AccessPage from '../pages/Access'; import LicensesPage from '../pages/Licenses'; +import NavigationMenu from './NavigationMenu'; +import '../pages/AdminPages.css'; const pageComponents = { orders: OrdersPage, @@ -21,23 +23,88 @@ const pageComponents = { licenses: LicensesPage, }; -export default function App({ page, initialData }) { - useEffect(() => { - console.log('[Formipay App] Rendering page:', page, 'with data:', initialData); - }, [page, initialData]); +export default function App({ page: initialPage, initialData }) { + // Use state for client-side routing + // Prioritize hash over initial page for reload support + const [currentPage, setCurrentPage] = useState(() => { + const hash = window.location.hash.replace('#', ''); + return (pageComponents[hash]) ? hash : initialPage; + }); - const PageComponent = pageComponents[page]; + // Update WordPress submenu active state based on current page + useEffect(() => { + // Remove active class from all submenu items + document.querySelectorAll('li.wp-first-item.current, li.wp-first-item .current').forEach(el => { + el.classList.remove('current', 'wp-first-item'); + }); + + // Add active class to current page's submenu item + const pageUrls = { + forms: 'admin.php?page=formipay', + products: 'admin.php?page=formipay-products', + coupons: 'admin.php?page=formipay-coupons', + orders: 'admin.php?page=formipay-orders', + customers: 'admin.php?page=formipay-customers', + access: 'admin.php?page=formipay-access', + licenses: 'admin.php?page=formipay-licenses', + }; + + const targetUrl = pageUrls[currentPage]; + if (targetUrl) { + const submenuLinks = document.querySelectorAll('#toplevel_page_formipay .wp-submenu a'); + submenuLinks.forEach(link => { + link.parentElement.classList.remove('current'); + link.classList.remove('current'); + if (link.getAttribute('href')?.includes(targetUrl)) { + if(currentPage === 'forms') { + document.querySelectorAll('#toplevel_page_formipay .wp-submenu li:nth-child(2) a').forEach(ahref => { + ahref.parentElement.classList.add('current'); + ahref.classList.add('current'); + }); + }else{ + link.parentElement.classList.add('current'); + link.classList.add('current'); + } + } + }); + } + }, [currentPage]); + + // Listen for hash changes for browser back/forward support + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.replace('#', ''); + if (pageComponents[hash]) { + setCurrentPage(hash); + } + }; + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, []); + + // Update URL when page changes via navigation + const handlePageNavigate = (pageKey) => { + setCurrentPage(pageKey); + window.location.hash = pageKey; + }; + + const PageComponent = pageComponents[currentPage]; if (!PageComponent) { return (
-

Unknown page: {page}

+

Unknown page: {currentPage}

); } return (
+
); diff --git a/src/admin/components/NavigationMenu.js b/src/admin/components/NavigationMenu.js new file mode 100644 index 000000000..257b3cc24 --- /dev/null +++ b/src/admin/components/NavigationMenu.js @@ -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 ( +
+ Formipay + + +
+ ); +} diff --git a/src/admin/components/products/VariationPricingTable.js b/src/admin/components/products/VariationPricingTable.js index 18ad279ec..c7b942ce1 100644 --- a/src/admin/components/products/VariationPricingTable.js +++ b/src/admin/components/products/VariationPricingTable.js @@ -6,10 +6,7 @@ import { __ } from '@wordpress/i18n'; import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; import { TextControl, Button } from '@wordpress/components'; -import { Icon } from '@wordpress/icons'; -import minus from '@wordpress/icons/build/minus'; -import eyeClosed from '@wordpress/icons/build/eye-closed'; -import eyeOpened from '@wordpress/icons/build/eye-opened'; +import * as Icons from '@wordpress/icons'; import './VariationPricingTable.css'; export default function VariationPricingTable({ productId, productDetails }) { @@ -421,12 +418,12 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode, className="toggle-expand" onClick={onToggleExpanded} > - + {row.expanded ? '▼' : '▶'} { row.name } - {showFlatPricing ? ( + {showFlatPricing && row.prices?.[0] ? ( <> { __('Delete', 'formipay') } @@ -477,7 +474,7 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode, - {row.prices.map((price, currencyIndex) => { + {(row.prices || []).map((price, currencyIndex) => { const code = String(price.currency).split(':::')[0]; const isDefault = code === defaultCurrencyCode; const step = price.currency_decimal_digits @@ -523,6 +520,10 @@ function VariationRow({ row, rowIndex, showFlatPricing, defaultCurrencyCode, // Price input cell component function PriceCell({ price, field, onChange }) { + if (!price) { + return ; + } + const step = price.currency_decimal_digits ? 1 / Math.pow(10, price.currency_decimal_digits) : 0.01; diff --git a/src/admin/components/shared/DataTable.css b/src/admin/components/shared/DataTable.css index bafd3c92f..077a14ad0 100644 --- a/src/admin/components/shared/DataTable.css +++ b/src/admin/components/shared/DataTable.css @@ -21,6 +21,18 @@ min-width: 150px; } +.formipay-table-toolbar *:is(button,input,select) { + height: 40px!important; +} + +.formipay-table-toolbar .components-base-control__field { + margin-bottom: unset!important; +} + +.formipay-table-toolbar *:is(button, input, select, .components-input-control__backdrop){ + border-radius: 4px!important; +} + /* Filter Tabs */ .formipay-filter-tabs { display: flex; @@ -112,6 +124,14 @@ background-color: #f0f0f1; } +.formipay-table *:is(td, th):first-child { + text-align: center; +} + +.formipay-table th.column-select > input { + margin-left: 0; +} + /* Checkbox Column */ .formipay-table .column-select { width: 40px; diff --git a/src/admin/components/shared/DataTable.js b/src/admin/components/shared/DataTable.js index 07be80d2f..7d23ee35b 100644 --- a/src/admin/components/shared/DataTable.js +++ b/src/admin/components/shared/DataTable.js @@ -60,6 +60,9 @@ export default function DataTable({ tableAction, // e.g., 'formipay-tabledata-forms' deleteAction, duplicateAction, + + // Selection callback + onSelectionChange, }) { // State const [data, setData] = useState(initialData); @@ -82,6 +85,13 @@ export default function DataTable({ const [selectedRows, setSelectedRows] = useState(new Set()); const [selectAll, setSelectAll] = useState(false); + // Notify parent of selection changes + useEffect(() => { + if (onSelectionChange) { + onSelectionChange(selectedRows); + } + }, [selectedRows, onSelectionChange]); + // Add New Modal const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [newItemTitle, setNewItemTitle] = useState(''); @@ -89,8 +99,6 @@ export default function DataTable({ // Derive action names from tableAction const baseActionName = tableAction.replace('formipay-tabledata-', ''); const bulkDeleteAction = actions.bulkDelete?.action || `formipay-bulk-delete-${baseActionName}`; - const deleteActionName = deleteAction || `formipay-delete-${baseActionName}`; - const duplicateActionName = duplicateAction || `formipay-duplicate-${baseActionName}`; // Load data const loadData = useCallback(async () => { @@ -213,56 +221,6 @@ export default function DataTable({ } }; - // Handle inline delete - const handleDelete = async (id) => { - const result = await Swal.fire({ - icon: 'info', - html: __('Do you want to delete this item?', 'formipay'), - showCancelButton: true, - confirmButtonText: __('Delete Permanently', 'formipay'), - cancelButtonText: __('Cancel', 'formipay'), - }); - - if (result.isConfirmed) { - await fetch(`${ajaxUrl}?action=${deleteActionName}`, { - method: 'POST', - credentials: 'same-origin', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - id, - _wpnonce: nonce, - }), - }); - - loadData(); - } - }; - - // Handle duplicate - const handleDuplicate = async (id) => { - const result = await Swal.fire({ - icon: 'info', - html: __('Do you want to duplicate this item?', 'formipay'), - showCancelButton: true, - confirmButtonText: __('Confirm', 'formipay'), - cancelButtonText: __('Cancel', 'formipay'), - }); - - if (result.isConfirmed) { - await fetch(`${ajaxUrl}?action=${duplicateActionName}`, { - method: 'POST', - credentials: 'same-origin', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - id, - _wpnonce: nonce, - }), - }); - - loadData(); - } - }; - // Handle Add New const handleAddNew = async () => { if (!newItemTitle.trim()) { @@ -356,6 +314,15 @@ export default function DataTable({ }} /> )} + + {/* Refresh Button */} + {/* Filter Tabs */} @@ -408,10 +375,6 @@ export default function DataTable({ {column.label} ))} - {/* Actions column */} - {actions.inline && ( - - )} @@ -435,41 +398,6 @@ export default function DataTable({ {column.render ? column.render(row) : row[column.key]} ))} - {/* Actions */} - {actions.inline && ( - - )} ); })} @@ -531,7 +459,7 @@ export default function DataTable({ )} - {/* Add New Modal - only render when actually open */} + {/* Add New Modal - only render when open */} {actions.addNew && isAddModalOpen && ( 0; + + return ( +
+

{title}

+ + {bulkDeleteVisible && onBulkDelete && ( + + )} +
+ ); +} diff --git a/src/admin/design-system/README.md b/src/admin/design-system/README.md new file mode 100644 index 000000000..e341995d5 --- /dev/null +++ b/src/admin/design-system/README.md @@ -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 + + +

Section Title

+

Content goes here...

+
+
+``` + +### 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 ( + <> + + + {({ id }) => ( + id === 'general' && + )} + + + ); +} +``` + +### Field, Input, Textarea, Select +Form field components with consistent styling. + +```jsx + + setName(e.target.value)} + placeholder="Product Name" + /> + + +
- {__('Actions', 'formipay')}
- - {__('Edit', 'formipay')} - - {' | '} - - - {' | '} - - -