@magnet-cms/plugin-polar 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 +2494 -0
- package/dist/backend/index.d.cts +435 -0
- package/dist/backend/index.d.ts +435 -0
- package/dist/backend/index.js +22 -0
- package/dist/chunk-7KT7JQIH.js +2474 -0
- package/dist/frontend/bundle.iife.js +23323 -0
- package/dist/frontend/bundle.iife.js.map +1 -0
- package/dist/index.cjs +2474 -0
- package/dist/index.d.cts +140 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +1 -0
- package/package.json +83 -0
- package/src/admin/components/recent-orders.tsx +80 -0
- package/src/admin/components/revenue-chart.tsx +56 -0
- package/src/admin/components/subscription-metrics.tsx +65 -0
- package/src/admin/index.ts +128 -0
- package/src/admin/pages/benefits.tsx +175 -0
- package/src/admin/pages/customers.tsx +187 -0
- package/src/admin/pages/orders.tsx +188 -0
- package/src/admin/pages/polar-dashboard.tsx +90 -0
- package/src/admin/pages/products.tsx +203 -0
- package/src/admin/pages/subscriptions.tsx +245 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { PageHeader, useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Button,
|
|
5
|
+
DataTable,
|
|
6
|
+
type DataTableColumn,
|
|
7
|
+
type DataTableRenderContext,
|
|
8
|
+
Input,
|
|
9
|
+
Skeleton,
|
|
10
|
+
} from '@magnet-cms/ui/components'
|
|
11
|
+
import { Search } from 'lucide-react'
|
|
12
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
interface Benefit {
|
|
15
|
+
id: string
|
|
16
|
+
polarBenefitId: string
|
|
17
|
+
type: string
|
|
18
|
+
description?: string
|
|
19
|
+
organizationId?: string
|
|
20
|
+
selectable: boolean
|
|
21
|
+
createdAt: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const BenefitsPage = () => {
|
|
25
|
+
const adapter = useAdapter()
|
|
26
|
+
const [benefits, setBenefits] = useState<Benefit[]>([])
|
|
27
|
+
const [search, setSearch] = useState('')
|
|
28
|
+
const [loading, setLoading] = useState(true)
|
|
29
|
+
|
|
30
|
+
const fetchBenefits = useCallback(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const data = await adapter.request<Benefit[]>('/polar/admin/benefits')
|
|
33
|
+
setBenefits(data)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('[Polar] Failed to fetch benefits:', error)
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false)
|
|
38
|
+
}
|
|
39
|
+
}, [adapter])
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetchBenefits()
|
|
43
|
+
}, [fetchBenefits])
|
|
44
|
+
|
|
45
|
+
const filtered = benefits.filter(
|
|
46
|
+
(b) =>
|
|
47
|
+
b.type.toLowerCase().includes(search.toLowerCase()) ||
|
|
48
|
+
(b.description ?? '').toLowerCase().includes(search.toLowerCase()),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const columns: DataTableColumn<Benefit>[] = [
|
|
52
|
+
{
|
|
53
|
+
type: 'custom',
|
|
54
|
+
header: 'Type',
|
|
55
|
+
cell: (row) => <Badge variant="outline">{row.original.type}</Badge>,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
header: 'Description',
|
|
60
|
+
accessorKey: 'description',
|
|
61
|
+
format: (value) => (
|
|
62
|
+
<div className="text-sm text-muted-foreground max-w-[400px] truncate">
|
|
63
|
+
{(value as string) ?? '—'}
|
|
64
|
+
</div>
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'custom',
|
|
69
|
+
header: 'Selectable',
|
|
70
|
+
cell: (row) => (
|
|
71
|
+
<Badge variant={row.original.selectable ? 'default' : 'secondary'}>
|
|
72
|
+
{row.original.selectable ? 'Yes' : 'No'}
|
|
73
|
+
</Badge>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
header: 'Created',
|
|
79
|
+
accessorKey: 'createdAt',
|
|
80
|
+
format: (value) => new Date(value as string).toLocaleDateString(),
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
const renderToolbar = () => (
|
|
85
|
+
<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">
|
|
86
|
+
<div className="relative w-full sm:w-80">
|
|
87
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
88
|
+
<Input
|
|
89
|
+
placeholder="Search benefits..."
|
|
90
|
+
value={search}
|
|
91
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
92
|
+
className="pl-9"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
<Button variant="ghost" size="sm" onClick={() => setSearch('')}>
|
|
96
|
+
Clear Filters
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const renderPagination = (table: DataTableRenderContext<Benefit>) => {
|
|
102
|
+
const { pageIndex, pageSize } = table.getState().pagination
|
|
103
|
+
const totalRows = table.getFilteredRowModel().rows.length
|
|
104
|
+
const startRow = pageIndex * pageSize + 1
|
|
105
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows)
|
|
106
|
+
return (
|
|
107
|
+
<div className="flex-none px-6 py-4 border-t border-gray-200 bg-white flex items-center justify-between">
|
|
108
|
+
<div className="text-xs text-gray-500">
|
|
109
|
+
Showing <span className="font-medium text-gray-900">{startRow}</span>{' '}
|
|
110
|
+
to <span className="font-medium text-gray-900">{endRow}</span> of{' '}
|
|
111
|
+
<span className="font-medium text-gray-900">{totalRows}</span> results
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
<Button
|
|
115
|
+
variant="outline"
|
|
116
|
+
size="sm"
|
|
117
|
+
disabled={!table.getCanPreviousPage()}
|
|
118
|
+
onClick={() => table.previousPage()}
|
|
119
|
+
>
|
|
120
|
+
Previous
|
|
121
|
+
</Button>
|
|
122
|
+
<Button
|
|
123
|
+
variant="outline"
|
|
124
|
+
size="sm"
|
|
125
|
+
disabled={!table.getCanNextPage()}
|
|
126
|
+
onClick={() => table.nextPage()}
|
|
127
|
+
>
|
|
128
|
+
Next
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (loading) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
138
|
+
<PageHeader title="Benefits" />
|
|
139
|
+
<div className="flex-1 p-6">
|
|
140
|
+
<Skeleton className="h-96 w-full" />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
148
|
+
<PageHeader
|
|
149
|
+
title="Benefits"
|
|
150
|
+
description={`${benefits.length} benefit(s) total.`}
|
|
151
|
+
/>
|
|
152
|
+
<div className="flex-1 flex flex-col overflow-hidden bg-gray-50">
|
|
153
|
+
<div className="flex-1 overflow-hidden relative">
|
|
154
|
+
<div className="absolute inset-0 overflow-auto">
|
|
155
|
+
<DataTable
|
|
156
|
+
data={filtered}
|
|
157
|
+
columns={columns}
|
|
158
|
+
getRowId={(row) => row.id}
|
|
159
|
+
renderToolbar={renderToolbar}
|
|
160
|
+
renderPagination={renderPagination}
|
|
161
|
+
enablePagination
|
|
162
|
+
pageSizeOptions={[10, 20, 50]}
|
|
163
|
+
initialPagination={{ pageIndex: 0, pageSize: 10 }}
|
|
164
|
+
showCount={false}
|
|
165
|
+
className="h-full flex flex-col"
|
|
166
|
+
variant="content-manager"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default BenefitsPage
|
|
@@ -0,0 +1,187 @@
|
|
|
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 { useCallback, useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
interface Customer {
|
|
14
|
+
id: string
|
|
15
|
+
polarCustomerId: string
|
|
16
|
+
email: string
|
|
17
|
+
name?: string
|
|
18
|
+
userId?: string
|
|
19
|
+
createdAt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CustomersPage = () => {
|
|
23
|
+
const adapter = useAdapter()
|
|
24
|
+
const [customers, setCustomers] = useState<Customer[]>([])
|
|
25
|
+
const [search, setSearch] = useState('')
|
|
26
|
+
const [loading, setLoading] = useState(true)
|
|
27
|
+
|
|
28
|
+
const fetchCustomers = useCallback(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const data = await adapter.request<Customer[]>('/polar/admin/customers')
|
|
31
|
+
setCustomers(data)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('[Polar] Failed to fetch customers:', error)
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false)
|
|
36
|
+
}
|
|
37
|
+
}, [adapter])
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
fetchCustomers()
|
|
41
|
+
}, [fetchCustomers])
|
|
42
|
+
|
|
43
|
+
const filtered = customers.filter(
|
|
44
|
+
(c) =>
|
|
45
|
+
c.email.toLowerCase().includes(search.toLowerCase()) ||
|
|
46
|
+
(c.name ?? '').toLowerCase().includes(search.toLowerCase()),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const columns: DataTableColumn<Customer>[] = [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
header: 'Name',
|
|
53
|
+
accessorKey: 'name',
|
|
54
|
+
format: (value) => (
|
|
55
|
+
<div className="text-sm font-medium text-gray-900">
|
|
56
|
+
{(value as string) ?? '—'}
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
header: 'Email',
|
|
63
|
+
accessorKey: 'email',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
header: 'User ID',
|
|
68
|
+
accessorKey: 'userId',
|
|
69
|
+
format: (value) => (
|
|
70
|
+
<span className="font-mono text-sm">{(value as string) ?? '—'}</span>
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
header: 'Created',
|
|
76
|
+
accessorKey: 'createdAt',
|
|
77
|
+
format: (value) => new Date(value as string).toLocaleDateString(),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'custom',
|
|
81
|
+
header: 'Polar',
|
|
82
|
+
cell: (row) => (
|
|
83
|
+
<a
|
|
84
|
+
href={`https://dashboard.polar.sh/customers/${row.original.polarCustomerId}`}
|
|
85
|
+
target="_blank"
|
|
86
|
+
rel="noopener noreferrer"
|
|
87
|
+
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
|
88
|
+
>
|
|
89
|
+
<ExternalLink className="h-3 w-3" />
|
|
90
|
+
View
|
|
91
|
+
</a>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const renderToolbar = () => (
|
|
97
|
+
<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">
|
|
98
|
+
<div className="relative w-full sm:w-80">
|
|
99
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
100
|
+
<Input
|
|
101
|
+
placeholder="Search customers..."
|
|
102
|
+
value={search}
|
|
103
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
104
|
+
className="pl-9"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<Button variant="ghost" size="sm" onClick={() => setSearch('')}>
|
|
108
|
+
Clear Filters
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const renderPagination = (table: DataTableRenderContext<Customer>) => {
|
|
114
|
+
const { pageIndex, pageSize } = table.getState().pagination
|
|
115
|
+
const totalRows = table.getFilteredRowModel().rows.length
|
|
116
|
+
const startRow = pageIndex * pageSize + 1
|
|
117
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows)
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex-none px-6 py-4 border-t border-gray-200 bg-white flex items-center justify-between">
|
|
120
|
+
<div className="text-xs text-gray-500">
|
|
121
|
+
Showing <span className="font-medium text-gray-900">{startRow}</span>{' '}
|
|
122
|
+
to <span className="font-medium text-gray-900">{endRow}</span> of{' '}
|
|
123
|
+
<span className="font-medium text-gray-900">{totalRows}</span> results
|
|
124
|
+
</div>
|
|
125
|
+
<div className="flex items-center gap-2">
|
|
126
|
+
<Button
|
|
127
|
+
variant="outline"
|
|
128
|
+
size="sm"
|
|
129
|
+
disabled={!table.getCanPreviousPage()}
|
|
130
|
+
onClick={() => table.previousPage()}
|
|
131
|
+
>
|
|
132
|
+
Previous
|
|
133
|
+
</Button>
|
|
134
|
+
<Button
|
|
135
|
+
variant="outline"
|
|
136
|
+
size="sm"
|
|
137
|
+
disabled={!table.getCanNextPage()}
|
|
138
|
+
onClick={() => table.nextPage()}
|
|
139
|
+
>
|
|
140
|
+
Next
|
|
141
|
+
</Button>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (loading) {
|
|
148
|
+
return (
|
|
149
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
150
|
+
<PageHeader title="Customers" />
|
|
151
|
+
<div className="flex-1 p-6">
|
|
152
|
+
<Skeleton className="h-96 w-full" />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
160
|
+
<PageHeader
|
|
161
|
+
title="Customers"
|
|
162
|
+
description={`${customers.length} customer(s) total.`}
|
|
163
|
+
/>
|
|
164
|
+
<div className="flex-1 flex flex-col overflow-hidden bg-gray-50">
|
|
165
|
+
<div className="flex-1 overflow-hidden relative">
|
|
166
|
+
<div className="absolute inset-0 overflow-auto">
|
|
167
|
+
<DataTable
|
|
168
|
+
data={filtered}
|
|
169
|
+
columns={columns}
|
|
170
|
+
getRowId={(row) => row.id}
|
|
171
|
+
renderToolbar={renderToolbar}
|
|
172
|
+
renderPagination={renderPagination}
|
|
173
|
+
enablePagination
|
|
174
|
+
pageSizeOptions={[10, 20, 50]}
|
|
175
|
+
initialPagination={{ pageIndex: 0, pageSize: 10 }}
|
|
176
|
+
showCount={false}
|
|
177
|
+
className="h-full flex flex-col"
|
|
178
|
+
variant="content-manager"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default CustomersPage
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { PageHeader, useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Button,
|
|
5
|
+
DataTable,
|
|
6
|
+
type DataTableColumn,
|
|
7
|
+
type DataTableRenderContext,
|
|
8
|
+
Skeleton,
|
|
9
|
+
} from '@magnet-cms/ui/components'
|
|
10
|
+
import { useEffect, useState } from 'react'
|
|
11
|
+
|
|
12
|
+
interface Order {
|
|
13
|
+
id: string
|
|
14
|
+
polarOrderId: string
|
|
15
|
+
customerId: string
|
|
16
|
+
productId: string
|
|
17
|
+
status: string
|
|
18
|
+
totalAmount: number
|
|
19
|
+
currency: string
|
|
20
|
+
billingReason?: string
|
|
21
|
+
createdAt: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStatusVariant(
|
|
25
|
+
status: string,
|
|
26
|
+
): 'default' | 'secondary' | 'destructive' {
|
|
27
|
+
switch (status) {
|
|
28
|
+
case 'paid':
|
|
29
|
+
return 'default'
|
|
30
|
+
case 'refunded':
|
|
31
|
+
return 'destructive'
|
|
32
|
+
default:
|
|
33
|
+
return 'secondary'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatCurrency(cents: number, currency: string): string {
|
|
38
|
+
return `${currency.toUpperCase()} $${(cents / 100).toFixed(2)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const OrdersPage = () => {
|
|
42
|
+
const adapter = useAdapter()
|
|
43
|
+
const [orders, setOrders] = useState<Order[]>([])
|
|
44
|
+
const [loading, setLoading] = useState(true)
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
async function fetch() {
|
|
48
|
+
try {
|
|
49
|
+
const data = await adapter.request<Order[]>('/polar/admin/orders')
|
|
50
|
+
setOrders(data)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[Polar] Failed to fetch orders:', error)
|
|
53
|
+
} finally {
|
|
54
|
+
setLoading(false)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
fetch()
|
|
58
|
+
}, [adapter])
|
|
59
|
+
|
|
60
|
+
const columns: DataTableColumn<Order>[] = [
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
header: 'Order ID',
|
|
64
|
+
accessorKey: 'polarOrderId',
|
|
65
|
+
format: (value) => (
|
|
66
|
+
<span className="font-mono text-sm">
|
|
67
|
+
{(value as string).slice(0, 12)}...
|
|
68
|
+
</span>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'custom',
|
|
73
|
+
header: 'Amount',
|
|
74
|
+
cell: (row) => (
|
|
75
|
+
<span className="font-medium">
|
|
76
|
+
{formatCurrency(row.original.totalAmount, row.original.currency)}
|
|
77
|
+
</span>
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'custom',
|
|
82
|
+
header: 'Status',
|
|
83
|
+
cell: (row) => (
|
|
84
|
+
<Badge variant={getStatusVariant(row.original.status)}>
|
|
85
|
+
{row.original.status}
|
|
86
|
+
</Badge>
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
header: 'Customer',
|
|
92
|
+
accessorKey: 'customerId',
|
|
93
|
+
format: (value) => (
|
|
94
|
+
<span className="font-mono text-sm">{value as string}</span>
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
header: 'Reason',
|
|
100
|
+
accessorKey: 'billingReason',
|
|
101
|
+
format: (value) => (
|
|
102
|
+
<span className="text-sm text-muted-foreground">
|
|
103
|
+
{(value as string) ?? '—'}
|
|
104
|
+
</span>
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
header: 'Date',
|
|
110
|
+
accessorKey: 'createdAt',
|
|
111
|
+
format: (value) => new Date(value as string).toLocaleDateString(),
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const renderPagination = (table: DataTableRenderContext<Order>) => {
|
|
116
|
+
const { pageIndex, pageSize } = table.getState().pagination
|
|
117
|
+
const totalRows = table.getFilteredRowModel().rows.length
|
|
118
|
+
const startRow = pageIndex * pageSize + 1
|
|
119
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows)
|
|
120
|
+
return (
|
|
121
|
+
<div className="flex-none px-6 py-4 border-t border-gray-200 bg-white flex items-center justify-between">
|
|
122
|
+
<div className="text-xs text-gray-500">
|
|
123
|
+
Showing <span className="font-medium text-gray-900">{startRow}</span>{' '}
|
|
124
|
+
to <span className="font-medium text-gray-900">{endRow}</span> of{' '}
|
|
125
|
+
<span className="font-medium text-gray-900">{totalRows}</span> results
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<Button
|
|
129
|
+
variant="outline"
|
|
130
|
+
size="sm"
|
|
131
|
+
disabled={!table.getCanPreviousPage()}
|
|
132
|
+
onClick={() => table.previousPage()}
|
|
133
|
+
>
|
|
134
|
+
Previous
|
|
135
|
+
</Button>
|
|
136
|
+
<Button
|
|
137
|
+
variant="outline"
|
|
138
|
+
size="sm"
|
|
139
|
+
disabled={!table.getCanNextPage()}
|
|
140
|
+
onClick={() => table.nextPage()}
|
|
141
|
+
>
|
|
142
|
+
Next
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (loading) {
|
|
150
|
+
return (
|
|
151
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
152
|
+
<PageHeader title="Orders" />
|
|
153
|
+
<div className="flex-1 p-6">
|
|
154
|
+
<Skeleton className="h-96 w-full" />
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="flex-1 flex flex-col min-w-0 bg-white h-full relative overflow-hidden">
|
|
162
|
+
<PageHeader
|
|
163
|
+
title="Orders"
|
|
164
|
+
description={`${orders.length} order(s) total.`}
|
|
165
|
+
/>
|
|
166
|
+
<div className="flex-1 flex flex-col overflow-hidden bg-gray-50">
|
|
167
|
+
<div className="flex-1 overflow-hidden relative">
|
|
168
|
+
<div className="absolute inset-0 overflow-auto">
|
|
169
|
+
<DataTable
|
|
170
|
+
data={orders}
|
|
171
|
+
columns={columns}
|
|
172
|
+
getRowId={(row) => row.id}
|
|
173
|
+
renderPagination={renderPagination}
|
|
174
|
+
enablePagination
|
|
175
|
+
pageSizeOptions={[10, 20, 50]}
|
|
176
|
+
initialPagination={{ pageIndex: 0, pageSize: 10 }}
|
|
177
|
+
showCount={false}
|
|
178
|
+
className="h-full flex flex-col"
|
|
179
|
+
variant="content-manager"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default OrdersPage
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PageContent, PageHeader, useAdapter } from '@magnet-cms/admin'
|
|
2
|
+
import { Card, Skeleton } from '@magnet-cms/ui/components'
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { RecentOrders } from '../components/recent-orders'
|
|
5
|
+
import { RevenueChart } from '../components/revenue-chart'
|
|
6
|
+
import { SubscriptionMetrics } from '../components/subscription-metrics'
|
|
7
|
+
|
|
8
|
+
interface MetricsData {
|
|
9
|
+
mrr: number
|
|
10
|
+
revenueThisMonth: number
|
|
11
|
+
activeSubscriptions: number
|
|
12
|
+
churnRate: number
|
|
13
|
+
revenueByMonth: Array<{ month: string; revenue: number }>
|
|
14
|
+
recentOrders: Array<{
|
|
15
|
+
id: string
|
|
16
|
+
totalAmount: number
|
|
17
|
+
currency: string
|
|
18
|
+
status: string
|
|
19
|
+
customerEmail: string
|
|
20
|
+
createdAt: string
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function DashboardSkeleton() {
|
|
25
|
+
return (
|
|
26
|
+
<div className="space-y-6 p-6">
|
|
27
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
28
|
+
{['mrr', 'revenue', 'subscriptions', 'churn'].map((id) => (
|
|
29
|
+
<Skeleton key={id} className="h-24" />
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
<Skeleton className="h-[300px]" />
|
|
33
|
+
<Skeleton className="h-[200px]" />
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PolarDashboard = () => {
|
|
39
|
+
const adapter = useAdapter()
|
|
40
|
+
const [metrics, setMetrics] = useState<MetricsData | null>(null)
|
|
41
|
+
const [loading, setLoading] = useState(true)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
async function fetchMetrics() {
|
|
45
|
+
try {
|
|
46
|
+
const data = await adapter.request<MetricsData>('/polar/admin/metrics')
|
|
47
|
+
setMetrics(data)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('[Polar] Failed to fetch metrics:', error)
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
fetchMetrics()
|
|
55
|
+
}, [adapter])
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<PageHeader title="Polar Dashboard" />
|
|
60
|
+
<PageContent>
|
|
61
|
+
{loading || !metrics ? (
|
|
62
|
+
<DashboardSkeleton />
|
|
63
|
+
) : (
|
|
64
|
+
<div className="space-y-6 p-6">
|
|
65
|
+
<SubscriptionMetrics
|
|
66
|
+
mrr={metrics.mrr}
|
|
67
|
+
revenueThisMonth={metrics.revenueThisMonth}
|
|
68
|
+
activeSubscriptions={metrics.activeSubscriptions}
|
|
69
|
+
churnRate={metrics.churnRate}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<Card className="p-6">
|
|
73
|
+
<h3 className="text-lg font-semibold mb-4">
|
|
74
|
+
Revenue (Last 12 Months)
|
|
75
|
+
</h3>
|
|
76
|
+
<RevenueChart data={metrics.revenueByMonth} />
|
|
77
|
+
</Card>
|
|
78
|
+
|
|
79
|
+
<div>
|
|
80
|
+
<h3 className="text-lg font-semibold mb-4">Recent Orders</h3>
|
|
81
|
+
<RecentOrders orders={metrics.recentOrders} />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</PageContent>
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default PolarDashboard
|