@magnet-cms/plugin-stripe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/index.cjs +2278 -0
- package/dist/backend/index.d.cts +317 -0
- package/dist/backend/index.d.ts +317 -0
- package/dist/backend/index.js +19 -0
- package/dist/chunk-IPPZL6QM.js +2257 -0
- package/dist/frontend/bundle.iife.js +23271 -0
- package/dist/frontend/bundle.iife.js.map +1 -0
- package/dist/index.cjs +2261 -0
- package/dist/index.d.cts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +1 -0
- package/package.json +83 -0
- package/src/admin/components/recent-payments.tsx +106 -0
- package/src/admin/components/revenue-chart.tsx +56 -0
- package/src/admin/components/subscription-metrics.tsx +65 -0
- package/src/admin/index.ts +120 -0
- package/src/admin/pages/customers.tsx +209 -0
- package/src/admin/pages/payments.tsx +251 -0
- package/src/admin/pages/products.tsx +220 -0
- package/src/admin/pages/stripe-dashboard.tsx +90 -0
- package/src/admin/pages/subscriptions.tsx +251 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Plugin - Frontend Entry
|
|
3
|
+
*
|
|
4
|
+
* Self-registers the plugin when loaded via script injection.
|
|
5
|
+
* The admin app loads plugin bundles at runtime and plugins self-register
|
|
6
|
+
* on window.__MAGNET_PLUGINS__.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ComponentType } from 'react'
|
|
10
|
+
|
|
11
|
+
interface FrontendPluginManifest {
|
|
12
|
+
pluginName: string
|
|
13
|
+
routes?: {
|
|
14
|
+
path: string
|
|
15
|
+
componentId: string
|
|
16
|
+
children?: { path: string; componentId: string }[]
|
|
17
|
+
}[]
|
|
18
|
+
sidebar?: {
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
url: string
|
|
22
|
+
icon: string
|
|
23
|
+
order?: number
|
|
24
|
+
items?: { id: string; title: string; url: string; icon: string }[]
|
|
25
|
+
}[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PluginRegistration {
|
|
29
|
+
manifest: FrontendPluginManifest
|
|
30
|
+
components: Record<string, () => Promise<{ default: ComponentType<unknown> }>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const manifest: FrontendPluginManifest = {
|
|
34
|
+
pluginName: 'stripe',
|
|
35
|
+
routes: [
|
|
36
|
+
{
|
|
37
|
+
path: 'stripe',
|
|
38
|
+
componentId: 'StripeDashboard',
|
|
39
|
+
children: [
|
|
40
|
+
{ path: '', componentId: 'StripeDashboard' },
|
|
41
|
+
{ path: 'customers', componentId: 'StripeCustomers' },
|
|
42
|
+
{ path: 'products', componentId: 'StripeProducts' },
|
|
43
|
+
{ path: 'subscriptions', componentId: 'StripeSubscriptions' },
|
|
44
|
+
{ path: 'payments', componentId: 'StripePayments' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
sidebar: [
|
|
49
|
+
{
|
|
50
|
+
id: 'stripe',
|
|
51
|
+
title: 'Stripe Payments',
|
|
52
|
+
url: '/stripe',
|
|
53
|
+
icon: 'CreditCard',
|
|
54
|
+
order: 30,
|
|
55
|
+
items: [
|
|
56
|
+
{
|
|
57
|
+
id: 'stripe-dashboard',
|
|
58
|
+
title: 'Dashboard',
|
|
59
|
+
url: '/stripe',
|
|
60
|
+
icon: 'BarChart3',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'stripe-customers',
|
|
64
|
+
title: 'Customers',
|
|
65
|
+
url: '/stripe/customers',
|
|
66
|
+
icon: 'Users',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'stripe-products',
|
|
70
|
+
title: 'Products',
|
|
71
|
+
url: '/stripe/products',
|
|
72
|
+
icon: 'Package',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'stripe-subscriptions',
|
|
76
|
+
title: 'Subscriptions',
|
|
77
|
+
url: '/stripe/subscriptions',
|
|
78
|
+
icon: 'RefreshCw',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'stripe-payments',
|
|
82
|
+
title: 'Payments',
|
|
83
|
+
url: '/stripe/payments',
|
|
84
|
+
icon: 'Receipt',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const components: Record<
|
|
92
|
+
string,
|
|
93
|
+
() => Promise<{ default: ComponentType<unknown> }>
|
|
94
|
+
> = {
|
|
95
|
+
StripeDashboard: () => import('./pages/stripe-dashboard'),
|
|
96
|
+
StripeCustomers: () => import('./pages/customers'),
|
|
97
|
+
StripeProducts: () => import('./pages/products'),
|
|
98
|
+
StripeSubscriptions: () => import('./pages/subscriptions'),
|
|
99
|
+
StripePayments: () => import('./pages/payments'),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function registerPlugin() {
|
|
103
|
+
if (!window.__MAGNET_PLUGINS__) {
|
|
104
|
+
window.__MAGNET_PLUGINS__ = []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const alreadyRegistered = window.__MAGNET_PLUGINS__.some(
|
|
108
|
+
(p) => p.manifest.pluginName === manifest.pluginName,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (!alreadyRegistered) {
|
|
112
|
+
window.__MAGNET_PLUGINS__.push({ manifest, components })
|
|
113
|
+
console.log(`[Magnet] Plugin registered: ${manifest.pluginName}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
registerPlugin()
|
|
118
|
+
|
|
119
|
+
export const stripePlugin = () => ({ manifest, components })
|
|
120
|
+
export default stripePlugin
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { PageHeader, useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
DataTable,
|
|
5
|
+
type DataTableColumn,
|
|
6
|
+
type DataTableRenderContext,
|
|
7
|
+
Input,
|
|
8
|
+
Skeleton,
|
|
9
|
+
} from '@magnet-cms/ui/components'
|
|
10
|
+
import { ExternalLink, Search } from 'lucide-react'
|
|
11
|
+
import { useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
const contentManagerStyles = `
|
|
14
|
+
.table-row-hover:hover td {
|
|
15
|
+
background-color: #F9FAFB;
|
|
16
|
+
}
|
|
17
|
+
.table-row-hover.group:hover td {
|
|
18
|
+
background-color: #F9FAFB;
|
|
19
|
+
}
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
interface Customer {
|
|
23
|
+
id: string
|
|
24
|
+
stripeCustomerId: string
|
|
25
|
+
email: string
|
|
26
|
+
name?: string
|
|
27
|
+
userId?: string
|
|
28
|
+
createdAt: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CustomersPage = () => {
|
|
32
|
+
const adapter = useAdapter()
|
|
33
|
+
const [customers, setCustomers] = useState<Customer[]>([])
|
|
34
|
+
const [search, setSearch] = useState('')
|
|
35
|
+
const [loading, setLoading] = useState(true)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
async function fetch() {
|
|
39
|
+
try {
|
|
40
|
+
const data = await adapter.request<Customer[]>(
|
|
41
|
+
'/stripe/admin/customers',
|
|
42
|
+
)
|
|
43
|
+
setCustomers(data)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('[Stripe] Failed to fetch customers:', error)
|
|
46
|
+
} finally {
|
|
47
|
+
setLoading(false)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
fetch()
|
|
51
|
+
}, [adapter])
|
|
52
|
+
|
|
53
|
+
const filtered = customers.filter(
|
|
54
|
+
(c) =>
|
|
55
|
+
c.email.toLowerCase().includes(search.toLowerCase()) ||
|
|
56
|
+
(c.name ?? '').toLowerCase().includes(search.toLowerCase()),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const columns: DataTableColumn<Customer>[] = [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
header: 'Name',
|
|
63
|
+
accessorKey: 'name',
|
|
64
|
+
format: (value, row) => (
|
|
65
|
+
<div className="text-sm font-medium text-gray-900">
|
|
66
|
+
{(value as string) ?? row.original.name ?? '—'}
|
|
67
|
+
</div>
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
header: 'Email',
|
|
73
|
+
accessorKey: 'email',
|
|
74
|
+
format: (value) => (
|
|
75
|
+
<div className="text-sm text-gray-600">{value as string}</div>
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'custom',
|
|
80
|
+
header: 'User ID',
|
|
81
|
+
cell: (row) =>
|
|
82
|
+
row.original.userId ? (
|
|
83
|
+
<span className="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-600/20">
|
|
84
|
+
{row.original.userId}
|
|
85
|
+
</span>
|
|
86
|
+
) : (
|
|
87
|
+
<span className="text-muted-foreground">—</span>
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
header: 'Created',
|
|
93
|
+
accessorKey: 'createdAt',
|
|
94
|
+
format: (value) => (
|
|
95
|
+
<span className="text-sm text-gray-500">
|
|
96
|
+
{new Date(value as string).toLocaleDateString()}
|
|
97
|
+
</span>
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'custom',
|
|
102
|
+
header: 'Stripe',
|
|
103
|
+
cell: (row) => (
|
|
104
|
+
<a
|
|
105
|
+
href={`https://dashboard.stripe.com/customers/${row.original.stripeCustomerId}`}
|
|
106
|
+
target="_blank"
|
|
107
|
+
rel="noopener noreferrer"
|
|
108
|
+
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
|
109
|
+
>
|
|
110
|
+
<ExternalLink className="h-3 w-3" />
|
|
111
|
+
View
|
|
112
|
+
</a>
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
const renderToolbar = () => (
|
|
118
|
+
<div className="px-6 py-4 flex flex-col sm:flex-row gap-3 items-center justify-between flex-none bg-white border-b border-gray-200">
|
|
119
|
+
<div className="relative w-full sm:w-80">
|
|
120
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
121
|
+
<Input
|
|
122
|
+
placeholder="Search customers..."
|
|
123
|
+
value={search}
|
|
124
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
125
|
+
className="pl-9"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<Button variant="ghost" size="sm" onClick={() => setSearch('')}>
|
|
129
|
+
Clear Filters
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const renderPagination = (table: DataTableRenderContext<Customer>) => {
|
|
135
|
+
const { pageIndex, pageSize } = table.getState().pagination
|
|
136
|
+
const totalRows = table.getFilteredRowModel().rows.length
|
|
137
|
+
const startRow = pageIndex * pageSize + 1
|
|
138
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows)
|
|
139
|
+
return (
|
|
140
|
+
<div className="flex-none px-6 py-4 border-t border-gray-200 bg-white flex items-center justify-between">
|
|
141
|
+
<div className="text-xs text-gray-500">
|
|
142
|
+
Showing <span className="font-medium text-gray-900">{startRow}</span>{' '}
|
|
143
|
+
to <span className="font-medium text-gray-900">{endRow}</span> of{' '}
|
|
144
|
+
<span className="font-medium text-gray-900">{totalRows}</span> results
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<Button
|
|
148
|
+
variant="outline"
|
|
149
|
+
size="sm"
|
|
150
|
+
disabled={!table.getCanPreviousPage()}
|
|
151
|
+
onClick={() => table.previousPage()}
|
|
152
|
+
>
|
|
153
|
+
Previous
|
|
154
|
+
</Button>
|
|
155
|
+
<Button
|
|
156
|
+
variant="outline"
|
|
157
|
+
size="sm"
|
|
158
|
+
disabled={!table.getCanNextPage()}
|
|
159
|
+
onClick={() => table.nextPage()}
|
|
160
|
+
>
|
|
161
|
+
Next
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (loading) {
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
171
|
+
<PageHeader title="Customers" />
|
|
172
|
+
<div className="flex-1 p-6">
|
|
173
|
+
<Skeleton className="h-96 w-full" />
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
181
|
+
<style>{contentManagerStyles}</style>
|
|
182
|
+
<PageHeader
|
|
183
|
+
title="Customers"
|
|
184
|
+
description={`${customers.length} customer(s) total.`}
|
|
185
|
+
/>
|
|
186
|
+
<div className="flex-1 flex flex-col overflow-hidden bg-gray-50">
|
|
187
|
+
<div className="flex-1 overflow-hidden relative">
|
|
188
|
+
<div className="absolute inset-0 overflow-auto">
|
|
189
|
+
<DataTable
|
|
190
|
+
data={filtered}
|
|
191
|
+
columns={columns}
|
|
192
|
+
getRowId={(row) => row.id}
|
|
193
|
+
renderToolbar={renderToolbar}
|
|
194
|
+
renderPagination={renderPagination}
|
|
195
|
+
enablePagination
|
|
196
|
+
pageSizeOptions={[10, 20, 50]}
|
|
197
|
+
initialPagination={{ pageIndex: 0, pageSize: 10 }}
|
|
198
|
+
showCount={false}
|
|
199
|
+
className="h-full flex flex-col"
|
|
200
|
+
variant="content-manager"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default CustomersPage
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { PageHeader, useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Button,
|
|
5
|
+
DataTable,
|
|
6
|
+
type DataTableColumn,
|
|
7
|
+
type DataTableRenderContext,
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
Skeleton,
|
|
14
|
+
} from '@magnet-cms/ui/components'
|
|
15
|
+
import { useEffect, useState } from 'react'
|
|
16
|
+
|
|
17
|
+
const contentManagerStyles = `
|
|
18
|
+
.table-row-hover:hover td {
|
|
19
|
+
background-color: #F9FAFB;
|
|
20
|
+
}
|
|
21
|
+
.table-row-hover.group:hover td {
|
|
22
|
+
background-color: #F9FAFB;
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
interface Payment {
|
|
27
|
+
id: string
|
|
28
|
+
stripePaymentIntentId: string
|
|
29
|
+
customerId: string
|
|
30
|
+
amount: number
|
|
31
|
+
currency: string
|
|
32
|
+
status: string
|
|
33
|
+
receiptUrl?: string
|
|
34
|
+
invoiceId?: string
|
|
35
|
+
createdAt: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatCurrency(cents: number, currency: string): string {
|
|
39
|
+
return `${currency.toUpperCase()} ${(cents / 100).toFixed(2)}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getStatusVariant(
|
|
43
|
+
status: string,
|
|
44
|
+
): 'default' | 'secondary' | 'destructive' | 'outline' {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case 'succeeded':
|
|
47
|
+
return 'default'
|
|
48
|
+
case 'failed':
|
|
49
|
+
return 'destructive'
|
|
50
|
+
case 'refunded':
|
|
51
|
+
return 'secondary'
|
|
52
|
+
default:
|
|
53
|
+
return 'outline'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PaymentsPage = () => {
|
|
58
|
+
const adapter = useAdapter()
|
|
59
|
+
const [payments, setPayments] = useState<Payment[]>([])
|
|
60
|
+
const [statusFilter, setStatusFilter] = useState<string>('all')
|
|
61
|
+
const [loading, setLoading] = useState(true)
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
async function fetch() {
|
|
65
|
+
try {
|
|
66
|
+
const data = await adapter.request<Payment[]>('/stripe/admin/payments')
|
|
67
|
+
setPayments(data)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('[Stripe] Failed to fetch payments:', error)
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
fetch()
|
|
75
|
+
}, [adapter])
|
|
76
|
+
|
|
77
|
+
const filtered =
|
|
78
|
+
statusFilter === 'all'
|
|
79
|
+
? payments
|
|
80
|
+
: payments.filter((p) => p.status === statusFilter)
|
|
81
|
+
|
|
82
|
+
const handleRefund = async (paymentIntentId: string) => {
|
|
83
|
+
try {
|
|
84
|
+
await adapter.request(
|
|
85
|
+
`/stripe/admin/payments/${paymentIntentId}/refund`,
|
|
86
|
+
{ method: 'POST' },
|
|
87
|
+
)
|
|
88
|
+
setPayments((prev) =>
|
|
89
|
+
prev.map((p) =>
|
|
90
|
+
p.stripePaymentIntentId === paymentIntentId
|
|
91
|
+
? { ...p, status: 'refunded' }
|
|
92
|
+
: p,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('[Stripe] Failed to refund payment:', error)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const columns: DataTableColumn<Payment>[] = [
|
|
101
|
+
{
|
|
102
|
+
type: 'custom',
|
|
103
|
+
header: 'Amount',
|
|
104
|
+
cell: (row) => (
|
|
105
|
+
<span className="font-medium">
|
|
106
|
+
{formatCurrency(row.original.amount, row.original.currency)}
|
|
107
|
+
</span>
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'custom',
|
|
112
|
+
header: 'Status',
|
|
113
|
+
cell: (row) => (
|
|
114
|
+
<Badge variant={getStatusVariant(row.original.status)}>
|
|
115
|
+
{row.original.status}
|
|
116
|
+
</Badge>
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
header: 'Customer',
|
|
122
|
+
accessorKey: 'customerId',
|
|
123
|
+
format: (value) => (
|
|
124
|
+
<span className="font-mono text-sm text-muted-foreground">
|
|
125
|
+
{value as string}
|
|
126
|
+
</span>
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'text',
|
|
131
|
+
header: 'Date',
|
|
132
|
+
accessorKey: 'createdAt',
|
|
133
|
+
format: (value) => (
|
|
134
|
+
<span className="text-muted-foreground">
|
|
135
|
+
{new Date(value as string).toLocaleDateString()}
|
|
136
|
+
</span>
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'custom',
|
|
141
|
+
header: 'Actions',
|
|
142
|
+
cell: (row) =>
|
|
143
|
+
row.original.status === 'succeeded' ? (
|
|
144
|
+
<Button
|
|
145
|
+
variant="outline"
|
|
146
|
+
size="sm"
|
|
147
|
+
onClick={() => handleRefund(row.original.stripePaymentIntentId)}
|
|
148
|
+
>
|
|
149
|
+
Refund
|
|
150
|
+
</Button>
|
|
151
|
+
) : null,
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
const renderToolbar = () => (
|
|
156
|
+
<div className="px-6 py-4 flex flex-col sm:flex-row gap-3 items-center justify-between flex-none bg-white border-b border-gray-200">
|
|
157
|
+
<div className="max-w-[200px]">
|
|
158
|
+
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
159
|
+
<SelectTrigger>
|
|
160
|
+
<SelectValue placeholder="Filter by status" />
|
|
161
|
+
</SelectTrigger>
|
|
162
|
+
<SelectContent>
|
|
163
|
+
<SelectItem value="all">All Statuses</SelectItem>
|
|
164
|
+
<SelectItem value="succeeded">Succeeded</SelectItem>
|
|
165
|
+
<SelectItem value="failed">Failed</SelectItem>
|
|
166
|
+
<SelectItem value="refunded">Refunded</SelectItem>
|
|
167
|
+
</SelectContent>
|
|
168
|
+
</Select>
|
|
169
|
+
</div>
|
|
170
|
+
<Button variant="ghost" size="sm" onClick={() => setStatusFilter('all')}>
|
|
171
|
+
Clear Filters
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const renderPagination = (table: DataTableRenderContext<Payment>) => {
|
|
177
|
+
const { pageIndex, pageSize } = table.getState().pagination
|
|
178
|
+
const totalRows = table.getFilteredRowModel().rows.length
|
|
179
|
+
const startRow = pageIndex * pageSize + 1
|
|
180
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows)
|
|
181
|
+
return (
|
|
182
|
+
<div className="flex-none px-6 py-4 border-t border-gray-200 bg-white flex items-center justify-between">
|
|
183
|
+
<div className="text-xs text-gray-500">
|
|
184
|
+
Showing <span className="font-medium text-gray-900">{startRow}</span>{' '}
|
|
185
|
+
to <span className="font-medium text-gray-900">{endRow}</span> of{' '}
|
|
186
|
+
<span className="font-medium text-gray-900">{totalRows}</span> results
|
|
187
|
+
</div>
|
|
188
|
+
<div className="flex items-center gap-2">
|
|
189
|
+
<Button
|
|
190
|
+
variant="outline"
|
|
191
|
+
size="sm"
|
|
192
|
+
disabled={!table.getCanPreviousPage()}
|
|
193
|
+
onClick={() => table.previousPage()}
|
|
194
|
+
>
|
|
195
|
+
Previous
|
|
196
|
+
</Button>
|
|
197
|
+
<Button
|
|
198
|
+
variant="outline"
|
|
199
|
+
size="sm"
|
|
200
|
+
disabled={!table.getCanNextPage()}
|
|
201
|
+
onClick={() => table.nextPage()}
|
|
202
|
+
>
|
|
203
|
+
Next
|
|
204
|
+
</Button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (loading) {
|
|
211
|
+
return (
|
|
212
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
213
|
+
<PageHeader title="Payments" />
|
|
214
|
+
<div className="flex-1 p-6">
|
|
215
|
+
<Skeleton className="h-96 w-full" />
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
223
|
+
<style>{contentManagerStyles}</style>
|
|
224
|
+
<PageHeader
|
|
225
|
+
title="Payments"
|
|
226
|
+
description={`${payments.length} payment(s) total.`}
|
|
227
|
+
/>
|
|
228
|
+
<div className="flex-1 flex flex-col overflow-hidden bg-gray-50">
|
|
229
|
+
<div className="flex-1 overflow-hidden relative">
|
|
230
|
+
<div className="absolute inset-0 overflow-auto">
|
|
231
|
+
<DataTable
|
|
232
|
+
data={filtered}
|
|
233
|
+
columns={columns}
|
|
234
|
+
getRowId={(row) => row.id}
|
|
235
|
+
renderToolbar={renderToolbar}
|
|
236
|
+
renderPagination={renderPagination}
|
|
237
|
+
enablePagination
|
|
238
|
+
pageSizeOptions={[10, 20, 50]}
|
|
239
|
+
initialPagination={{ pageIndex: 0, pageSize: 10 }}
|
|
240
|
+
showCount={false}
|
|
241
|
+
className="h-full flex flex-col"
|
|
242
|
+
variant="content-manager"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default PaymentsPage
|