feat: rewrite DataTable + design system with shadcn/ui, replace SweetAlert2

- Rewrite DataTable using shadcn Table, Button, Dialog, Input, Select, Checkbox, Skeleton
- Replace all SweetAlert2 calls with shadcn Dialog (confirm) + sonner toast
- Rewrite design system internals to use shadcn Label, Input, Switch, Button, Alert, Badge, Tabs
- Add Toaster to App.js for global toast support
- Remove all @wordpress/components imports from DataTable
- Remove WpcftoDesign.css import from design system
- Replace Swal.fire() in Coupons, Products, Forms, Access, Licenses pages
This commit is contained in:
dwindown
2026-04-19 13:25:42 +07:00
parent 862abc8d74
commit 99912a9335
12 changed files with 947 additions and 2928 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => '9a1a0e2d03b8d775e648'); <?php return array('dependencies' => array('react', 'react-dom', 'wp-components', 'wp-element', 'wp-i18n', 'wp-icons/build/arrow-left', 'wp-icons/build/bell', 'wp-icons/build/message', 'wp-icons/build/trash', 'wp-primitives'), 'version' => '6bf4643f2ea15ecdf0b7');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,7 @@ 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 NavigationMenu from './NavigationMenu';
import { Toaster } from '@/components/ui/sonner';
import '../pages/AdminPages.css'; import '../pages/AdminPages.css';
const pageComponents = { const pageComponents = {
@@ -101,6 +102,7 @@ export default function App({ page: initialPage, initialData }) {
return ( return (
<div className="formipay-admin-wrap"> <div className="formipay-admin-wrap">
<Toaster />
<NavigationMenu <NavigationMenu
currentPage={currentPage} currentPage={currentPage}
onPageNavigate={handlePageNavigate} onPageNavigate={handlePageNavigate}

View File

@@ -1,500 +1,555 @@
/** /**
* Full-featured DataTable component * Full-featured DataTable component
* Supports: selection, filtering, search, sort, pagination, actions * Supports: selection, filtering, search, sort, pagination, actions
*
* Built with shadcn/ui components + Tailwind CSS.
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useState, useCallback, useEffect } from '@wordpress/element'; import { useState, useCallback, useEffect } from '@wordpress/element';
import { import { cn } from '@/lib/utils';
Button, import { confirm } from '@/lib/confirm';
Modal, import { toast } from '@/lib/toast';
TextControl,
SelectControl,
Spinner,
} from '@wordpress/components';
import './DataTable.css';
// SweetAlert2 is loaded via WordPress (global scope) // shadcn/ui components
const Swal = window.Swal; import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
export default function DataTable({ export default function DataTable({
// Data fetching // Data fetching
initialData = [], initialData = [],
// Columns definition // Columns definition
columns, columns,
// Filtering // Filtering
filterOptions = null, // { key: 'post_status', options: [{value, label}] } filterOptions = null, // { key: 'post_status', options: [{value, label}] }
statusCounts = null, // { all: 10, publish: 5, draft: 5 } statusCounts = null, // { all: 10, publish: 5, draft: 5 }
// Search // Search
searchable = true, searchable = true,
searchPlaceholder = __('Search...', 'formipay'), searchPlaceholder = __('Search...', 'formipay'),
// Sorting // Sorting
sortable = true, sortable = true,
defaultSort = { id: 'ID', desc: true }, defaultSort = { id: 'ID', desc: true },
// Selection // Selection
selectable = true, selectable = true,
// Pagination // Pagination
pagination = true, pagination = true,
pageSize = 10, pageSize = 10,
pageSizeOptions = [10, 20, 50, 100], pageSizeOptions = [10, 20, 50, 100],
// Actions // Actions
actions = { actions = {
addNew: false, // { label, action: 'formipay-create-form-post' } addNew: false, // { label, action: 'formipay-create-form-post' }
bulkDelete: true, // { action: 'formipay-bulk-delete-form' } bulkDelete: true, // { action: 'formipay-bulk-delete-form' }
inline: true, // edit, delete, duplicate inline: true, // edit, delete, duplicate
}, },
// Empty state // Empty state
emptyMessage = __('No items found', 'formipay'), emptyMessage = __('No items found', 'formipay'),
// AJAX config // AJAX config
ajaxUrl, ajaxUrl,
nonce, nonce,
tableAction, // e.g., 'formipay-tabledata-forms' tableAction, // e.g., 'formipay-tabledata-forms'
deleteAction, deleteAction,
duplicateAction, duplicateAction,
// Selection callback // Selection callback
onSelectionChange, onSelectionChange,
}) { }) {
// State // State
const [data, setData] = useState(initialData); const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
// Filters // Filters
const [activeFilter, setActiveFilter] = useState('all'); const [activeFilter, setActiveFilter] = useState('all');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
// Sorting // Sorting
const [sortBy, setSortBy] = useState(defaultSort.id || 'ID'); const [sortBy, setSortBy] = useState(defaultSort.id || 'ID');
const [sortOrder, setSortOrder] = useState(defaultSort.desc ? 'desc' : 'asc'); const [sortOrder, setSortOrder] = useState(defaultSort.desc ? 'desc' : 'asc');
// Pagination // Pagination
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [currentPageSize, setCurrentPageSize] = useState(pageSize); const [currentPageSize, setCurrentPageSize] = useState(pageSize);
// Selection // Selection
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 // Notify parent of selection changes
useEffect(() => { useEffect(() => {
if (onSelectionChange) { if (onSelectionChange) {
onSelectionChange(selectedRows); onSelectionChange(selectedRows);
} }
}, [selectedRows, onSelectionChange]); }, [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('');
// 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}`;
// Load data // Load data
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
const params = new URLSearchParams({ const params = new URLSearchParams({
action: tableAction, action: tableAction,
_wpnonce: nonce, _wpnonce: nonce,
limit: currentPageSize.toString(), limit: currentPageSize.toString(),
offset: ((currentPage - 1) * currentPageSize).toString(), offset: ((currentPage - 1) * currentPageSize).toString(),
}); });
// Add filter // Add filter
if (filterOptions && activeFilter !== 'all') { if (filterOptions && activeFilter !== 'all') {
params.append(filterOptions.key, activeFilter); params.append(filterOptions.key, activeFilter);
} }
// Add search // Add search
if (searchQuery) { if (searchQuery) {
params.append('search', searchQuery); params.append('search', searchQuery);
} }
// Add sort // Add sort
params.append('orderby', sortBy); params.append('orderby', sortBy);
params.append('sort', sortOrder); params.append('sort', sortOrder);
try { try {
const response = await fetch(`${ajaxUrl}?${params.toString()}`, { const response = await fetch(`${ajaxUrl}?${params.toString()}`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params, body: params,
}); });
const result = await response.json(); const result = await response.json();
const items = result.data?.results || result.results || result.data || []; const items = result.data?.results || result.results || result.data || [];
setData(items); setData(items);
setTotal(result.total || items.length); setTotal(result.total || items.length);
} catch (error) { } catch (error) {
console.error('Load data error:', error); console.error('Load data error:', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [ajaxUrl, nonce, tableAction, currentPageSize, currentPage, activeFilter, searchQuery, sortBy, sortOrder, filterOptions]); }, [ajaxUrl, nonce, tableAction, currentPageSize, currentPage, activeFilter, searchQuery, sortBy, sortOrder, filterOptions]);
// Initial load and refresh on filter/sort/page change // Initial load and refresh on filter/sort/page change
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [loadData]); }, [loadData]);
// Handle filter change // Handle filter change
const handleFilterChange = (value) => { const handleFilterChange = (value) => {
setActiveFilter(value); setActiveFilter(value);
setCurrentPage(1); setCurrentPage(1);
}; };
// Handle search (debounced) // Handle search (debounced)
useEffect(() => { useEffect(() => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
if (searchQuery !== null) { if (searchQuery !== null) {
setCurrentPage(1); setCurrentPage(1);
} }
}, 500); }, 500);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [searchQuery]); }, [searchQuery]);
// Handle selection // Handle selection
const handleRowSelect = (id) => { const handleRowSelect = (id) => {
const newSelected = new Set(selectedRows); const newSelected = new Set(selectedRows);
if (newSelected.has(id)) { if (newSelected.has(id)) {
newSelected.delete(id); newSelected.delete(id);
} else { } else {
newSelected.add(id); newSelected.add(id);
} }
setSelectedRows(newSelected); setSelectedRows(newSelected);
setSelectAll(false); setSelectAll(false);
}; };
const handleSelectAll = () => { const handleSelectAll = () => {
if (selectAll) { if (selectAll) {
setSelectedRows(new Set()); setSelectedRows(new Set());
} else { } else {
setSelectedRows(new Set(data.map(row => row.ID || row.id))); setSelectedRows(new Set(data.map(row => row.ID || row.id)));
} }
setSelectAll(!selectAll); setSelectAll(!selectAll);
}; };
// Handle bulk delete // Handle bulk delete
const handleBulkDelete = async () => { const handleBulkDelete = async () => {
if (selectedRows.size === 0) return; if (selectedRows.size === 0) return;
const result = await Swal.fire({ const confirmed = await confirm({
icon: 'info', title: __('Delete Selected', 'formipay'),
html: __('Do you want to delete the selected item(s)?', 'formipay'), message: __('Do you want to delete the selected item(s)?', 'formipay'),
showCancelButton: true, confirmText: __('Confirm', 'formipay'),
confirmButtonText: __('Confirm', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'), variant: 'destructive',
}); });
if (result.isConfirmed) { if (confirmed) {
await fetch(`${ajaxUrl}?action=${bulkDeleteAction}`, { await fetch(`${ajaxUrl}?action=${bulkDeleteAction}`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ body: new URLSearchParams({
ids: Array.from(selectedRows), ids: Array.from(selectedRows),
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
setSelectedRows(new Set()); setSelectedRows(new Set());
setSelectAll(false); setSelectAll(false);
loadData(); loadData();
Swal.fire({ toast.success(__('Items deleted successfully.', 'formipay'));
title: __('Done!', 'formipay'), }
html: __('Items deleted successfully.', 'formipay'), };
icon: 'success',
});
}
};
// Handle Add New // Handle Add New
const handleAddNew = async () => { const handleAddNew = async () => {
if (!newItemTitle.trim()) { if (!newItemTitle.trim()) {
Swal.fire({ toast.error(__('Title is required.', 'formipay'));
html: __('Title is required.', 'formipay'), return;
icon: 'error', }
});
return;
}
const createAction = actions.addNew.action; const createAction = actions.addNew.action;
const result = await fetch(`${ajaxUrl}?action=${createAction}`, { const result = await fetch(`${ajaxUrl}?action=${createAction}`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ body: new URLSearchParams({
title: newItemTitle, title: newItemTitle,
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
const response = await result.json(); const response = await result.json();
if (response.success) { if (response.success) {
setIsAddModalOpen(false); setIsAddModalOpen(false);
setNewItemTitle(''); setNewItemTitle('');
if (response.data.edit_post_url) { if (response.data.edit_post_url) {
window.location.href = response.data.edit_post_url; window.location.href = response.data.edit_post_url;
} else { } else {
loadData(); loadData();
} }
} else { } else {
Swal.fire({ toast.error(response.data.message || __('Error creating item.', 'formipay'));
html: response.data.message || __('Error creating item.', 'formipay'), }
icon: 'error', };
});
}
};
return ( // Compute total pages
<div className="formipay-data-table-wrapper"> const totalPages = Math.ceil(total / currentPageSize);
{/* Toolbar */}
<div className="formipay-table-toolbar">
{/* Add New Button */}
{actions.addNew && (
<Button
variant="primary"
onClick={() => setIsAddModalOpen(true)}
>
{actions.addNew.label || __('+ Add New', 'formipay')}
</Button>
)}
{/* Bulk Delete Button */} return (
{actions.bulkDelete && selectable && selectedRows.size > 0 && ( <div className="formipay-design-system">
<Button {/* Toolbar */}
variant="secondary" <div className="flex items-center gap-3 mb-4 flex-wrap">
isDestructive {/* Add New Button */}
onClick={handleBulkDelete} {actions.addNew && (
> <Button
{__('Delete Selected', 'formipay')} ({selectedRows.size}) onClick={() => setIsAddModalOpen(true)}
</Button> >
)} {actions.addNew.label || __('+ Add New', 'formipay')}
</Button>
)}
{/* Search */} {/* Bulk Delete Button */}
{searchable && ( {actions.bulkDelete && selectable && selectedRows.size > 0 && (
<TextControl <Button
placeholder={searchPlaceholder} variant="destructive"
value={searchQuery} onClick={handleBulkDelete}
onChange={setSearchQuery} >
className="formipay-table-search" {__('Delete Selected', 'formipay')} ({selectedRows.size})
/> </Button>
)} )}
{/* Sort */} {/* Search */}
{sortable && ( {searchable && (
<SelectControl <Input
value={`${sortBy}-${sortOrder}`} placeholder={searchPlaceholder}
options={[ value={searchQuery}
{ label: __('ID ↓', 'formipay'), value: 'ID-desc' }, onChange={(e) => setSearchQuery(e.target.value)}
{ label: __('ID ↑', 'formipay'), value: 'ID-asc' }, className="max-w-75 grow"
{ label: __('Date ↓', 'formipay'), value: 'date-desc' }, />
{ label: __('Date ↑', 'formipay'), value: 'date-asc' }, )}
{ label: __('Title A-Z', 'formipay'), value: 'title-asc' },
{ label: __('Title Z-A', 'formipay'), value: 'title-desc' },
]}
onChange={(value) => {
const [id, sort] = value.split('-');
setSortBy(id);
setSortOrder(sort);
}}
/>
)}
{/* Refresh Button */} {/* Sort */}
<Button {sortable && (
variant="secondary" <Select
onClick={loadData} value={`${sortBy}-${sortOrder}`}
disabled={loading} onValueChange={(value) => {
> const [id, sort] = value.split('-');
{loading ? __('Refreshing...', 'formipay') : __('Refresh', 'formipay')} setSortBy(id);
</Button> setSortOrder(sort);
</div> }}
>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ID-desc">{__('ID \u2193', 'formipay')}</SelectItem>
<SelectItem value="ID-asc">{__('ID \u2191', 'formipay')}</SelectItem>
<SelectItem value="date-desc">{__('Date \u2193', 'formipay')}</SelectItem>
<SelectItem value="date-asc">{__('Date \u2191', 'formipay')}</SelectItem>
<SelectItem value="title-asc">{__('Title A-Z', 'formipay')}</SelectItem>
<SelectItem value="title-desc">{__('Title Z-A', 'formipay')}</SelectItem>
</SelectContent>
</Select>
)}
{/* Filter Tabs */} {/* Refresh Button */}
{filterOptions && ( <Button
<div className="formipay-filter-tabs"> variant="outline"
{filterOptions.options.map(option => ( onClick={loadData}
<button disabled={loading}
key={option.value} >
className={`filter-tab ${activeFilter === option.value ? 'active' : ''}`} {loading ? __('Refreshing...', 'formipay') : __('Refresh', 'formipay')}
onClick={() => handleFilterChange(option.value)} </Button>
> </div>
{option.label}
{statusCounts && (
<span className="count">
{statusCounts[option.value] || 0}
</span>
)}
</button>
))}
</div>
)}
{/* Table */} {/* Filter Tabs */}
<div className="formipay-table-container"> {filterOptions && (
{loading ? ( <div className="flex gap-2 mb-4">
<div className="formipay-table-loading"> {filterOptions.options.map(option => (
<Spinner /> <button
</div> key={option.value}
) : data.length === 0 ? ( className={cn(
<div className="formipay-table-empty"> 'px-3 py-1.5 rounded-md text-sm font-medium transition-colors',
{emptyMessage} activeFilter === option.value
</div> ? 'bg-primary text-primary-foreground'
) : ( : 'bg-muted text-muted-foreground hover:bg-muted/80'
<table className="formipay-table wp-list-table widefat fixed striped"> )}
<thead> onClick={() => handleFilterChange(option.value)}
<tr> >
{/* Checkbox column */} {option.label}
{selectable && ( {statusCounts && (
<th className="column-select"> <span
<input className={cn(
type="checkbox" 'inline-block min-w-4.5 px-1.5 py-0.5 ml-1.5 rounded-full text-[11px] leading-none',
checked={selectAll} activeFilter === option.value
onChange={handleSelectAll} ? 'bg-primary-foreground/20 text-primary-foreground'
/> : 'bg-muted-foreground/10 text-muted-foreground'
</th> )}
)} >
{/* Data columns */} {statusCounts[option.value] || 0}
{columns.map((column) => ( </span>
<th key={column.key} className={`column-${column.key}`}> )}
{column.label} </button>
</th> ))}
))} </div>
</tr> )}
</thead>
<tbody>
{data.map((row, rowIndex) => {
const rowId = row.ID || row.id;
return (
<tr key={rowIndex} className="formipay-table-row">
{/* Checkbox */}
{selectable && (
<td>
<input
type="checkbox"
checked={selectedRows.has(rowId)}
onChange={() => handleRowSelect(rowId)}
/>
</td>
)}
{/* Data columns */}
{columns.map((column) => (
<td key={column.key}>
{column.render ? column.render(row) : row[column.key]}
</td>
))}
</tr>
);
})}
</tbody>
</table>
)}
</div>
{/* Pagination */} {/* Table */}
{pagination && total > currentPageSize && ( <div className="rounded-lg border bg-card">
<div className="formipay-table-pagination"> {loading ? (
<div className="pagination-info"> <div className="flex flex-col items-center justify-center py-12">
{__('Showing', 'formipay')} {((currentPage - 1) * currentPageSize) + 1} - {Math.min(currentPage * currentPageSize, total)} {__('of', 'formipay')} {total} <div className="w-full space-y-3 px-4">
</div> {Array.from({ length: 5 }).map((_, i) => (
<div className="pagination-controls"> <div key={i} className="flex items-center gap-4">
<Button <Skeleton className="h-4 w-8" />
variant="secondary" <Skeleton className="h-4 flex-1" />
disabled={currentPage === 1} <Skeleton className="h-4 flex-1" />
onClick={() => setCurrentPage(1)} <Skeleton className="h-4 w-24" />
> </div>
{'««'} ))}
</Button> </div>
<Button </div>
variant="secondary" ) : data.length === 0 ? (
disabled={currentPage === 1} <div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
onClick={() => setCurrentPage(currentPage - 1)} <p>{emptyMessage}</p>
> </div>
{''} ) : (
</Button> <Table>
<span className="page-info"> <TableHeader>
{__('Page', 'formipay')} {currentPage} {__('of', 'formipay')} {Math.ceil(total / currentPageSize)} <TableRow>
</span> {/* Checkbox column */}
<Button {selectable && (
variant="secondary" <TableHead className="w-10 text-center">
disabled={currentPage >= Math.ceil(total / currentPageSize)} <Checkbox
onClick={() => setCurrentPage(currentPage + 1)} checked={selectAll}
> onCheckedChange={handleSelectAll}
{''} />
</Button> </TableHead>
<Button )}
variant="secondary" {/* Data columns */}
disabled={currentPage >= Math.ceil(total / currentPageSize)} {columns.map((column) => (
onClick={() => setCurrentPage(Math.ceil(total / currentPageSize))} <TableHead key={column.key}>
> {column.label}
{'»'} </TableHead>
</Button> ))}
<SelectControl </TableRow>
value={currentPageSize.toString()} </TableHeader>
options={pageSizeOptions.map(size => ({ <TableBody>
label: size.toString(), {data.map((row, rowIndex) => {
value: size.toString(), const rowId = row.ID || row.id;
}))} return (
onChange={(value) => { <TableRow key={rowIndex}>
setCurrentPageSize(parseInt(value)); {/* Checkbox */}
setCurrentPage(1); {selectable && (
}} <TableCell className="w-10 text-center">
/> <Checkbox
</div> checked={selectedRows.has(rowId)}
</div> onCheckedChange={() => handleRowSelect(rowId)}
)} />
</TableCell>
)}
{/* Data columns */}
{columns.map((column) => (
<TableCell key={column.key}>
{column.render ? column.render(row) : row[column.key]}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
)}
</div>
{/* Add New Modal - only render when open */} {/* Pagination */}
{actions.addNew && isAddModalOpen && ( {pagination && total > currentPageSize && (
<Modal <div className="flex items-center justify-between mt-4">
title={actions.addNew.label || __('Add New', 'formipay')} <div className="text-sm text-muted-foreground">
onRequestClose={() => { {__('Showing', 'formipay')} {((currentPage - 1) * currentPageSize) + 1} - {Math.min(currentPage * currentPageSize, total)} {__('of', 'formipay')} {total}
setIsAddModalOpen(false); </div>
setNewItemTitle(''); <div className="flex items-center gap-2">
}} <Button
> variant="outline"
<TextControl size="icon"
__next40pxDefaultSize disabled={currentPage === 1}
__nextHasNoMarginBottom onClick={() => setCurrentPage(1)}
label={__('Title', 'formipay')} >
value={newItemTitle} {'\u00AB'}
onChange={setNewItemTitle} </Button>
autoFocus <Button
/> variant="outline"
<div className="formipay-modal-actions"> size="icon"
<Button disabled={currentPage === 1}
variant="secondary" onClick={() => setCurrentPage(currentPage - 1)}
onClick={() => { >
setIsAddModalOpen(false); {'\u2039'}
setNewItemTitle(''); </Button>
}} <span className="px-2 text-sm text-muted-foreground">
> {__('Page', 'formipay')} {currentPage} {__('of', 'formipay')} {totalPages}
{__('Cancel', 'formipay')} </span>
</Button> <Button
<Button variant="outline"
variant="primary" size="icon"
onClick={handleAddNew} disabled={currentPage >= totalPages}
> onClick={() => setCurrentPage(currentPage + 1)}
{__('Create', 'formipay')} >
</Button> {'\u203A'}
</div> </Button>
</Modal> <Button
)} variant="outline"
</div> size="icon"
); disabled={currentPage >= totalPages}
onClick={() => setCurrentPage(totalPages)}
>
{'\u00BB'}
</Button>
<Select
value={currentPageSize.toString()}
onValueChange={(value) => {
setCurrentPageSize(parseInt(value));
setCurrentPage(1);
}}
>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
{pageSizeOptions.map(size => (
<SelectItem key={size} value={size.toString()}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
{/* Add New Dialog */}
{actions.addNew && (
<Dialog open={isAddModalOpen} onOpenChange={(open) => {
if (!open) {
setIsAddModalOpen(false);
setNewItemTitle('');
}
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{actions.addNew.label || __('Add New', 'formipay')}
</DialogTitle>
</DialogHeader>
<div className="py-4">
<label className="text-sm font-medium mb-1.5 block">
{__('Title', 'formipay')}
</label>
<Input
placeholder={__('Enter title...', 'formipay')}
value={newItemTitle}
onChange={(e) => setNewItemTitle(e.target.value)}
autoFocus
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setIsAddModalOpen(false);
setNewItemTitle('');
}}
>
{__('Cancel', 'formipay')}
</Button>
<Button
onClick={handleAddNew}
>
{__('Create', 'formipay')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
);
} }

View File

@@ -1,30 +1,76 @@
/** /**
* Formipay Design System - WPCFTO-inspired React components * Formipay Design System - WPCFTO-inspired React components
* Reusable components matching WPCFTO visual language * Built on shadcn/ui primitives + Tailwind CSS
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import './WpcftoDesign.css'; import { cn } from '@/lib/utils';
// Box Component // shadcn/ui primitives
export function Box({ children, className = '', ...props }) { import { Label } from '@/components/ui/label';
import { Input as ShadcnInput } from '@/components/ui/input';
import { Textarea as ShadcnTextarea } from '@/components/ui/textarea';
import {
Select as ShadcnSelect,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Button as ShadcnButton } from '@/components/ui/button';
import {
Alert,
AlertTitle,
AlertDescription,
} from '@/components/ui/alert';
import { Badge as ShadcnBadge } from '@/components/ui/badge';
import {
Table as ShadcnTable,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from '@/components/ui/table';
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from '@/components/ui/tabs';
// ---------------------------------------------------------------------------
// Box
// ---------------------------------------------------------------------------
export function Box({ children, className, ...props }) {
return ( return (
<div className={`formipay-box ${className}`} {...props}> <div
className={cn(
'bg-[var(--formipay-color-content-bg,#f0f3f5)] rounded-[10px] mb-2.5 min-h-[80px] shadow-sm',
className
)}
{...props}
>
{children} {children}
</div> </div>
); );
} }
// Box Child Component // ---------------------------------------------------------------------------
export function BoxChild({ children, className = '', ...props }) { // BoxChild
// ---------------------------------------------------------------------------
export function BoxChild({ children, className, ...props }) {
return ( return (
<div className={`formipay-box-child ${className}`} {...props}> <div className={cn(className)} {...props}>
{children} {children}
</div> </div>
); );
} }
// Tab Navigation Component - WPCFTO sidebar style // ---------------------------------------------------------------------------
// TabNav (WPCFTO sidebar style)
// ---------------------------------------------------------------------------
export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical' }) { export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical' }) {
if (!tabs || !Array.isArray(tabs)) { if (!tabs || !Array.isArray(tabs)) {
console.warn('[Formipay] TabNav: tabs is not an array', tabs); console.warn('[Formipay] TabNav: tabs is not an array', tabs);
@@ -32,32 +78,45 @@ export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical'
} }
return ( return (
<div className={`formipay-wpcfto-tab-nav ${orientation === 'vertical' ? 'formipay-wpcfto-sidebar' : ''}`}> <div
<div className="formipay-wpcfto-tab-nav-inner"> className={cn(
'flex-col w-[273px] h-auto bg-[#2c3e50] rounded-none p-5',
orientation !== 'vertical' && 'flex-row w-auto'
)}
>
<div className="flex flex-col gap-1">
{tabs.map((tab) => ( {tabs.map((tab) => (
<div <div key={tab.id}>
key={tab.id} <TabsTrigger
className={`formipay-wpcfto-nav ${activeTab === tab.id ? 'active' : ''} ${tab.submenu ? 'has-submenu' : ''} ${!tab.icon ? 'no-icon' : ''}`} value={tab.id}
onClick={() => onTabChange(tab.id)} className={cn(
> 'w-full justify-start text-[#bec5cb] uppercase text-sm',
<div className="formipay-wpcfto-nav-title"> 'data-[state=active]:bg-[#2985f7] data-[state=active]:text-white rounded-none',
{tab.icon && <i className={tab.icon}></i>} activeTab === tab.id && 'bg-[#2985f7] text-white'
)}
onClick={() => onTabChange(tab.id)}
>
{tab.icon && <i className={tab.icon} />}
<span>{tab.label}</span> <span>{tab.label}</span>
</div> </TabsTrigger>
{tab.submenu && ( {tab.submenu && (
<div className="formipay-wpcfto-submenus"> <div className="flex flex-col mt-1">
{tab.submenu.map((sub) => ( {tab.submenu.map((sub) => (
<div <div
key={`${tab.id}_${sub.id}`} key={`${tab.id}_${sub.id}`}
className={`formipay-wpcfto-submenu-item ${activeTab === `${tab.id}_${sub.id}` ? 'active' : ''}`} className={cn(
'px-4 py-2 text-sm cursor-pointer text-[#bec5cb] hover:text-white',
activeTab === `${tab.id}_${sub.id}` &&
'text-white bg-[#2985f7]'
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onTabChange(`${tab.id}_${sub.id}`); onTabChange(`${tab.id}_${sub.id}`);
}} }}
> >
{sub.label} {sub.label}
<i className="fa fa-chevron-right"></i> <i className="fa fa-chevron-right ml-auto" />
</div> </div>
))} ))}
</div> </div>
@@ -69,7 +128,9 @@ export function TabNav({ tabs, activeTab, onTabChange, orientation = 'vertical'
); );
} }
// Tab Panel Component // ---------------------------------------------------------------------------
// TabPanel
// ---------------------------------------------------------------------------
export function TabPanel({ tabs, activeTab, children }) { export function TabPanel({ tabs, activeTab, children }) {
if (!tabs || !Array.isArray(tabs)) { if (!tabs || !Array.isArray(tabs)) {
console.warn('[Formipay] TabPanel: tabs is not an array', tabs); console.warn('[Formipay] TabPanel: tabs is not an array', tabs);
@@ -77,262 +138,352 @@ export function TabPanel({ tabs, activeTab, children }) {
} }
return ( return (
<div className="formipay-tabs"> <div className="flex-1">
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
<div <div
key={tab.id} key={tab.id}
className={`formipay-tab ${tab.id === activeTab ? 'active' : ''}`} className={cn(tab.id !== activeTab && 'hidden')}
> >
<div className="formipay-tab-content"> {typeof children === 'function' ? children(tab, index) : children}
{typeof children === 'function' ? children(tab, index) : children}
</div>
</div> </div>
))} ))}
</div> </div>
); );
} }
// Field Component - WPCFTO-style 2-column layout // ---------------------------------------------------------------------------
// Field (2-column WPCFTO layout)
// ---------------------------------------------------------------------------
export function Field({ export function Field({
label, label,
description, description,
required = false, required = false,
children, children,
className = '', className,
...props ...props
}) { }) {
return ( return (
<div className={`formipay-generic-field ${className}`} {...props}> <div
<div className="formipay-field-aside"> className={cn(
'flex flex-wrap justify-between p-[1.8rem_1rem_0] w-full rounded-[10px] bg-card mb-2.5',
className
)}
{...props}
>
<aside className="w-[40%] pr-8">
{label && ( {label && (
<div className={`formipay-field-label ${required ? 'required' : ''}`}> <Label className={cn(required && "after:content-['*'] after:ml-0.5 after:text-destructive")}>
<span className="formipay-field-label-text">{label}</span> {label}
</div> </Label>
)} )}
{description && ( {description && (
<div className="formipay-field-description">{description}</div> <p className="mt-2 text-[13px] text-muted-foreground">{description}</p>
)} )}
</div> </aside>
<div className="formipay-field-content"> <div className="w-[60%]">
{children} {children}
</div> </div>
</div> </div>
); );
} }
// Input Component // ---------------------------------------------------------------------------
// Input
// ---------------------------------------------------------------------------
export function Input({ export function Input({
label, label,
description, description,
required = false, required = false,
className = '', className,
...props ...props
}) { }) {
return ( return (
<Field label={label} description={description} required={required}> <Field label={label} description={description} required={required}>
<input className={`formipay-input ${className}`} {...props} /> <ShadcnInput className={cn(className)} {...props} />
</Field> </Field>
); );
} }
// Textarea Component // ---------------------------------------------------------------------------
// Textarea
// ---------------------------------------------------------------------------
export function Textarea({ export function Textarea({
label, label,
description, description,
required = false, required = false,
rows = 4, rows = 4,
className = '', className,
...props ...props
}) { }) {
return ( return (
<Field label={label} description={description} required={required}> <Field label={label} description={description} required={required}>
<textarea <ShadcnTextarea rows={rows} className={cn(className)} {...props} />
className={`formipay-textarea ${className}`}
rows={rows}
{...props}
/>
</Field> </Field>
); );
} }
// Select Component // ---------------------------------------------------------------------------
// Select
// ---------------------------------------------------------------------------
export function Select({ export function Select({
label, label,
description, description,
required = false, required = false,
options = [], options = [],
className = '', className,
...props ...props
}) { }) {
// Derive a current value from props for the Radix select
const selectValue = props.value ?? props.defaultValue ?? undefined;
return ( return (
<Field label={label} description={description} required={required}> <Field label={label} description={description} required={required}>
<select className={`formipay-select ${className}`} {...props}> <ShadcnSelect
{options.map((option) => ( value={selectValue}
<option key={option.value} value={option.value}> onValueChange={(val) => {
{option.label} if (props.onChange) {
</option> props.onChange({ target: { value: val } });
))} }
</select> }}
>
<SelectTrigger className={cn('w-full', className)}>
<SelectValue placeholder={props.placeholder || __('Select...', 'formipay')} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={String(option.value)}>
{option.label}
</SelectItem>
))}
</SelectContent>
</ShadcnSelect>
</Field> </Field>
); );
} }
// Checkbox Component - WPCFTO toggle switch style // ---------------------------------------------------------------------------
// Checkbox (toggle-switch style)
// ---------------------------------------------------------------------------
export function Checkbox({ export function Checkbox({
label, label,
checked, checked,
onChange, onChange,
className = '', className,
isToggle = true, isToggle = true,
...props ...props
}) { }) {
return ( return (
<label className={`formipay-admin-checkbox ${checked ? 'active' : ''} ${className}`}> <label
<div className={`formipay-admin-checkbox-wrapper ${isToggle ? 'is_toggle' : ''}`}> className={cn(
<div className="formipay-checkbox-switcher"></div> 'inline-flex items-center gap-2 cursor-pointer',
<input className
type="checkbox" )}
checked={checked} >
onChange={onChange} <Switch
{...props} checked={checked}
/> onCheckedChange={(val) =>
</div> onChange?.({ target: { checked: val } })
<span>{label}</span> }
{...props}
/>
{label && <span className="text-sm">{label}</span>}
</label> </label>
); );
} }
// Button Component // ---------------------------------------------------------------------------
// Button
// ---------------------------------------------------------------------------
const variantMap = {
primary: 'default',
secondary: 'secondary',
danger: 'destructive',
};
const sizeMap = {
sm: 'sm',
md: 'default',
lg: 'lg',
};
export function Button({ export function Button({
variant = 'primary', variant = 'primary',
size = 'md', size = 'md',
icon: Icon, icon: Icon,
children, children,
className = '', className,
disabled = false, disabled = false,
onClick, onClick,
...props ...props
}) { }) {
const sizeClass = size !== 'md' ? `formipay-btn-${size}` : '';
const variantClass = `formipay-btn-${variant}`;
return ( return (
<button <ShadcnButton
className={`formipay-btn ${variantClass} ${sizeClass} ${className}`} variant={variantMap[variant] || variant}
size={sizeMap[size] || size}
className={cn(className)}
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
{...props} {...props}
> >
{Icon && <span className="formipay-btn-icon"><Icon /></span>} {Icon && <Icon />}
{children} {children}
</button> </ShadcnButton>
); );
} }
// Repeater Component // ---------------------------------------------------------------------------
// Repeater
// ---------------------------------------------------------------------------
export function Repeater({ export function Repeater({
items, items,
renderItem, renderItem,
onAdd, onAdd,
onRemove, onRemove,
addLabel = 'Add Item', addLabel = 'Add Item',
className = '', className,
}) { }) {
return ( return (
<div className={`formipay-repeater ${className}`}> <div className={cn('flex flex-col gap-3', className)}>
{items.map((item, index) => ( {items.map((item, index) => (
<div key={item.id || index} className={`formipay-repeater-item ${item.collapsed ? 'collapsed' : ''}`}> <div
key={item.id || index}
className={cn(
'border rounded-lg overflow-hidden',
item.collapsed && 'border-transparent'
)}
>
<div <div
className="formipay-repeater-header" className="flex items-center justify-between px-4 py-3 bg-muted/50 cursor-pointer select-none"
onClick={() => onToggle?.(item.id)} onClick={() => onToggle?.(item.id)}
> >
<div className="formipay-repeater-title"> <div className="flex items-center gap-2 text-sm font-medium">
<span className="formipay-repeater-toggle"></span> <span
className={cn(
'transition-transform text-xs',
item.collapsed && '-rotate-90'
)}
>
&#9660;
</span>
<span>{item.title || `Item ${index + 1}`}</span> <span>{item.title || `Item ${index + 1}`}</span>
</div> </div>
<div className="formipay-repeater-actions"> <button
<span type="button"
className="formipay-repeater-delete" className="text-muted-foreground hover:text-destructive text-sm px-1"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onRemove?.(item.id, index); onRemove?.(item.id, index);
}} }}
> >
&#10005;
</span> </button>
</div>
</div>
<div className="formipay-repeater-body">
{renderItem(item, index)}
</div> </div>
{!item.collapsed && (
<div className="p-4">{renderItem(item, index)}</div>
)}
</div> </div>
))} ))}
<button className="formipay-repeater-add" onClick={onAdd}> <ShadcnButton
<span>+</span> variant="outline"
<span>{addLabel}</span> size="sm"
</button> className="self-start"
onClick={onAdd}
>
<span className="mr-1">+</span>
{addLabel}
</ShadcnButton>
</div> </div>
); );
} }
// Notice Component // ---------------------------------------------------------------------------
// Notice
// ---------------------------------------------------------------------------
const noticeStyles = {
success: 'border-l-4 border-l-green-500 bg-green-50 text-green-900',
warning: 'border-l-4 border-l-yellow-500 bg-yellow-50 text-yellow-900',
error: 'border-l-4 border-l-red-500 bg-red-50 text-red-900',
info: 'border-l-4 border-l-blue-500 bg-blue-50 text-blue-900',
};
const noticeIcons = {
success: '\u2713',
warning: '\u26A0',
error: '\u2715',
info: '\u2139',
};
export function Notice({ export function Notice({
type = 'info', type = 'info',
title, title,
children, children,
onDismiss, onDismiss,
className = '', className,
}) { }) {
return ( return (
<div className={`formipay-notice formipay-notice-${type} ${className}`}> <Alert className={cn(noticeStyles[type] || noticeStyles.info, className)}>
<div className="formipay-notice-icon"> <span className="absolute left-4 top-4 text-base font-bold">
{type === 'success' && '✓'} {noticeIcons[type]}
{type === 'warning' && '⚠'} </span>
{type === 'error' && '✕'} <div className="pl-6">
{type === 'info' && ''} {title && <AlertTitle>{title}</AlertTitle>}
</div> <AlertDescription>{children}</AlertDescription>
<div className="formipay-notice-content">
{title && <div className="formipay-notice-title">{title}</div>}
<div>{children}</div>
</div> </div>
{onDismiss && ( {onDismiss && (
<button <button
className="formipay-notice-dismiss" type="button"
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground"
onClick={onDismiss} onClick={onDismiss}
aria-label="Dismiss" aria-label="Dismiss"
> >
&#10005;
</button> </button>
)} )}
</div> </Alert>
); );
} }
// Group Title Component - WPCFTO section divider (visual, not accordion) // ---------------------------------------------------------------------------
export function GroupTitle({ // GroupTitle
title, // ---------------------------------------------------------------------------
icon, export function GroupTitle({ title, icon, className }) {
className = '',
}) {
return ( return (
<div className={`formipay-group-title ${className}`}> <div
{icon && <i className={icon}></i>} className={cn(
'w-full pb-3 text-muted-foreground text-sm uppercase tracking-wider border-b flex items-center gap-2 mb-4',
className
)}
>
{icon && <i className={icon} />}
{title} {title}
</div> </div>
); );
} }
// Loading Spinner Component // ---------------------------------------------------------------------------
export function Spinner({ size = 'md', className = '' }) { // Spinner
const sizeClass = size !== 'md' ? `formipay-spinner-${size}` : ''; // ---------------------------------------------------------------------------
export function Spinner({ size = 'md', className }) {
const sizeClass = {
sm: 'h-4 w-4',
md: 'h-6 w-6',
lg: 'h-8 w-8',
};
return ( return (
<div className={`formipay-loading ${className}`}> <div className={cn('flex items-center justify-center', className)}>
<div className={`formipay-spinner ${sizeClass}`}></div> <div
className={cn(
'animate-spin rounded-full border-2 border-muted border-t-primary',
sizeClass[size] || sizeClass.md
)}
/>
</div> </div>
); );
} }
// Empty State Component // ---------------------------------------------------------------------------
// EmptyState
// ---------------------------------------------------------------------------
export function EmptyState({ export function EmptyState({
icon, icon,
title, title,
@@ -340,96 +491,133 @@ export function EmptyState({
action, action,
actionLabel, actionLabel,
onAction, onAction,
className = '', className,
}) { }) {
return ( return (
<div className={`formipay-empty-state ${className}`}> <div
{icon && <div className="formipay-empty-icon">{icon}</div>} className={cn(
{title && <div className="formipay-empty-title">{title}</div>} 'flex flex-col items-center justify-center py-12 text-muted-foreground',
className
)}
>
{icon && <div className="mb-4 text-4xl">{icon}</div>}
{title && <h3 className="text-lg font-semibold mb-1">{title}</h3>}
{description && ( {description && (
<div className="formipay-empty-description">{description}</div> <p className="text-sm text-muted-foreground mb-4">{description}</p>
)} )}
{action && ( {action && (
<Button onClick={onAction}> <ShadcnButton onClick={onAction}>
{actionLabel || __('Take Action', 'formipay')} {actionLabel || __('Take Action', 'formipay')}
</Button> </ShadcnButton>
)} )}
</div> </div>
); );
} }
// Status Badge Component // ---------------------------------------------------------------------------
export function Badge({ // Badge
variant = 'default', // ---------------------------------------------------------------------------
children, export function Badge({ variant = 'default', children, className }) {
className = '',
}) {
return ( return (
<span className={`formipay-badge formipay-badge-${variant} ${className}`}> <ShadcnBadge variant={variant} className={cn(className)}>
{children} {children}
</span> </ShadcnBadge>
); );
} }
// Table Component // ---------------------------------------------------------------------------
// Table
// ---------------------------------------------------------------------------
export function Table({ export function Table({
columns, columns,
data, data,
emptyMessage = __('No items found', 'formipay'), emptyMessage = __('No items found', 'formipay'),
className = '', className,
}) { }) {
if (!data || data.length === 0) { if (!data || data.length === 0) {
return ( return (
<div className={`formipay-table-wrapper ${className}`}> <div className={cn('flex flex-col items-center justify-center py-12 text-muted-foreground', className)}>
<EmptyState <EmptyState title={emptyMessage} />
title={emptyMessage}
/>
</div> </div>
); );
} }
return ( return (
<div className={`formipay-table-wrapper ${className}`}> <div className={cn('rounded-lg border', className)}>
<table className="formipay-table"> <ShadcnTable>
<thead> <TableHeader>
<tr> <TableRow>
{columns.map((column) => ( {columns.map((column) => (
<th key={column.key}>{column.label}</th> <TableHead key={column.key}>{column.label}</TableHead>
))} ))}
</tr> </TableRow>
</thead> </TableHeader>
<tbody> <TableBody>
{data.map((row, rowIndex) => ( {data.map((row, rowIndex) => (
<tr key={rowIndex}> <TableRow key={rowIndex}>
{columns.map((column) => ( {columns.map((column) => (
<td key={column.key}> <TableCell key={column.key}>
{column.render ? column.render(row, rowIndex) : row[column.key]} {column.render
</td> ? column.render(row, rowIndex)
: row[column.key]}
</TableCell>
))} ))}
</tr> </TableRow>
))} ))}
</tbody> </TableBody>
</table> </ShadcnTable>
</div> </div>
); );
} }
// Metabox Layout Component - WPCFTO 2-column wrapper for React metabox islands // ---------------------------------------------------------------------------
// MetaboxLayout
// ---------------------------------------------------------------------------
export function MetaboxLayout({ tabs, activeTab, onTabChange, children }) { export function MetaboxLayout({ tabs, activeTab, onTabChange, children }) {
return ( return (
<div className="formipay-wpcfto-metabox"> <div className="bg-white rounded-[10px] overflow-hidden shadow-sm">
<div className="formipay-wpcfto-metabox-inner"> <Tabs
<div className="formipay-wpcfto-container"> value={activeTab}
<TabNav tabs={tabs} activeTab={activeTab} onTabChange={onTabChange} /> onValueChange={onTabChange}
<div className="formipay-wpcfto-tabs"> className="flex flex-row"
{children} >
</div> <TabsList className="flex-col w-[273px] h-auto bg-[#2c3e50] rounded-none p-5">
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
className={cn(
'w-full justify-start text-[#bec5cb] uppercase text-sm',
'data-[state=active]:bg-[#2985f7] data-[state=active]:text-white rounded-none'
)}
>
{tab.icon && <i className={tab.icon} />}
<span>{tab.label}</span>
</TabsTrigger>
))}
</TabsList>
<div className="flex-1">
{tabs.map((tab) => (
<TabsContent
key={tab.id}
value={tab.id}
className="p-6 mt-0"
>
{typeof children === 'function'
? children(tab)
: children}
</TabsContent>
))}
</div> </div>
</div> </Tabs>
</div> </div>
); );
} }
// ---------------------------------------------------------------------------
// Default export (aggregate)
// ---------------------------------------------------------------------------
export default { export default {
Box, Box,
BoxChild, BoxChild,

View File

@@ -4,25 +4,24 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import DataTable from '../components/shared/DataTable'; import DataTable from '../components/shared/DataTable';
import { confirm } from '@/lib/confirm';
import { toast } from '@/lib/toast';
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 ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
const nonce = window.formipayAdmin?.nonce || ''; const nonce = window.formipayAdmin?.nonce || '';
const handleDelete = async (id) => { const handleDelete = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Delete Item', 'formipay'),
html: __('Do you want to delete this item?', 'formipay'), message: __('Do you want to delete this item?', 'formipay'),
showCancelButton: true, confirmText: __('Delete Permanently', 'formipay'),
confirmButtonText: __('Delete Permanently', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'), variant: 'destructive',
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-delete-access-item`, { await fetch(`${ajaxUrl}?action=formipay-delete-access-item`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -32,20 +31,20 @@ export default function AccessPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item deleted successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };
const handleDuplicate = async (id) => { const handleDuplicate = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Duplicate Item', 'formipay'),
html: __('Do you want to duplicate this item?', 'formipay'), message: __('Do you want to duplicate this item?', 'formipay'),
showCancelButton: true, confirmText: __('Confirm', 'formipay'),
confirmButtonText: __('Confirm', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'),
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-duplicate-access-item`, { await fetch(`${ajaxUrl}?action=formipay-duplicate-access-item`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -55,6 +54,7 @@ export default function AccessPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item duplicated successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };

View File

@@ -4,11 +4,10 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import DataTable from '../components/shared/DataTable'; import DataTable from '../components/shared/DataTable';
import { confirm } from '@/lib/confirm';
import { toast } from '@/lib/toast';
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 ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
const nonce = window.formipayAdmin?.nonce || ''; const nonce = window.formipayAdmin?.nonce || '';
@@ -19,15 +18,15 @@ export default function CouponsPage() {
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Delete Item', 'formipay'),
html: __('Do you want to delete this item?', 'formipay'), message: __('Do you want to delete this item?', 'formipay'),
showCancelButton: true, confirmText: __('Delete Permanently', 'formipay'),
confirmButtonText: __('Delete Permanently', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'), variant: 'destructive',
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-delete-coupon`, { await fetch(`${ajaxUrl}?action=formipay-delete-coupon`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -37,20 +36,20 @@ export default function CouponsPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item deleted successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };
const handleDuplicate = async (id) => { const handleDuplicate = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Duplicate Item', 'formipay'),
html: __('Do you want to duplicate this item?', 'formipay'), message: __('Do you want to duplicate this item?', 'formipay'),
showCancelButton: true, confirmText: __('Confirm', 'formipay'),
confirmButtonText: __('Confirm', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'),
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-duplicate-coupon`, { await fetch(`${ajaxUrl}?action=formipay-duplicate-coupon`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -60,6 +59,7 @@ export default function CouponsPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item duplicated successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };

View File

@@ -4,24 +4,23 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import DataTable from '../components/shared/DataTable'; import DataTable from '../components/shared/DataTable';
import { confirm } from '@/lib/confirm';
// SweetAlert2 is loaded via WordPress (global scope) import { toast } from '@/lib/toast';
const Swal = window.Swal;
export default function FormsPage() { export default function FormsPage() {
const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php'; const ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
const nonce = window.formipayAdmin?.nonce || ''; const nonce = window.formipayAdmin?.nonce || '';
const handleDelete = async (id) => { const handleDelete = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Delete Item', 'formipay'),
html: __('Do you want to delete this item?', 'formipay'), message: __('Do you want to delete this item?', 'formipay'),
showCancelButton: true, confirmText: __('Delete Permanently', 'formipay'),
confirmButtonText: __('Delete Permanently', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'), variant: 'destructive',
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-delete-form`, { await fetch(`${ajaxUrl}?action=formipay-delete-form`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -31,20 +30,20 @@ export default function FormsPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item deleted successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };
const handleDuplicate = async (id) => { const handleDuplicate = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Duplicate Item', 'formipay'),
html: __('Do you want to duplicate this item?', 'formipay'), message: __('Do you want to duplicate this item?', 'formipay'),
showCancelButton: true, confirmText: __('Confirm', 'formipay'),
confirmButtonText: __('Confirm', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'),
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-duplicate-form`, { await fetch(`${ajaxUrl}?action=formipay-duplicate-form`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -54,6 +53,7 @@ export default function FormsPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item duplicated successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };
@@ -155,15 +155,7 @@ export default function FormsPage() {
e.currentTarget.innerHTML = originalHTML; e.currentTarget.innerHTML = originalHTML;
}, 2000); }, 2000);
Swal.fire({ toast.success(__('Shortcode copied!', 'formipay'));
icon: 'success',
title: __('Shortcode copied!', 'formipay'),
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
});
}); });
}} }}
> >

View File

@@ -4,25 +4,24 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import DataTable from '../components/shared/DataTable'; import DataTable from '../components/shared/DataTable';
import { confirm } from '@/lib/confirm';
import { toast } from '@/lib/toast';
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 ajaxUrl = window.formipayAdmin?.ajaxUrl || '/wp-admin/admin-ajax.php';
const nonce = window.formipayAdmin?.nonce || ''; const nonce = window.formipayAdmin?.nonce || '';
const handleDelete = async (id) => { const handleDelete = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Delete Item', 'formipay'),
html: __('Do you want to delete this item?', 'formipay'), message: __('Do you want to delete this item?', 'formipay'),
showCancelButton: true, confirmText: __('Delete Permanently', 'formipay'),
confirmButtonText: __('Delete Permanently', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'), variant: 'destructive',
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-delete-license`, { await fetch(`${ajaxUrl}?action=formipay-delete-license`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -32,6 +31,7 @@ export default function LicensesPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item deleted successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };

View File

@@ -6,11 +6,10 @@ import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element'; import { useState } from '@wordpress/element';
import DataTable from '../components/shared/DataTable'; import DataTable from '../components/shared/DataTable';
import VariationPricingTable from '../components/products/VariationPricingTable'; import VariationPricingTable from '../components/products/VariationPricingTable';
import { confirm } from '@/lib/confirm';
import { toast } from '@/lib/toast';
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);
@@ -19,15 +18,15 @@ export default function ProductsPage() {
const nonce = window.formipayAdmin?.nonce || ''; const nonce = window.formipayAdmin?.nonce || '';
const handleDelete = async (id) => { const handleDelete = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Delete Item', 'formipay'),
html: __('Do you want to delete this item?', 'formipay'), message: __('Do you want to delete this item?', 'formipay'),
showCancelButton: true, confirmText: __('Delete Permanently', 'formipay'),
confirmButtonText: __('Delete Permanently', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'), variant: 'destructive',
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-delete-product`, { await fetch(`${ajaxUrl}?action=formipay-delete-product`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -37,20 +36,20 @@ export default function ProductsPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item deleted successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };
const handleDuplicate = async (id) => { const handleDuplicate = async (id) => {
const result = await Swal.fire({ const result = await confirm({
icon: 'info', title: __('Duplicate Item', 'formipay'),
html: __('Do you want to duplicate this item?', 'formipay'), message: __('Do you want to duplicate this item?', 'formipay'),
showCancelButton: true, confirmText: __('Confirm', 'formipay'),
confirmButtonText: __('Confirm', 'formipay'), cancelText: __('Cancel', 'formipay'),
cancelButtonText: __('Cancel', 'formipay'),
}); });
if (result.isConfirmed) { if (result) {
await fetch(`${ajaxUrl}?action=formipay-duplicate-product`, { await fetch(`${ajaxUrl}?action=formipay-duplicate-product`, {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
@@ -60,6 +59,7 @@ export default function ProductsPage() {
_wpnonce: nonce, _wpnonce: nonce,
}), }),
}); });
toast.success(__('Item duplicated successfully.', 'formipay'));
window.location.reload(); window.location.reload();
} }
}; };