@riligar/agents-kit 1.12.0 → 1.14.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/.agent/skills/riligar-business-startup/SKILL.md +0 -1
- package/.agent/skills/riligar-design-system/SKILL.md +0 -1
- package/.agent/skills/riligar-dev-architecture/SKILL.md +7 -8
- package/.agent/skills/riligar-dev-auth-elysia/SKILL.md +7 -3
- package/.agent/skills/riligar-dev-auth-react/SKILL.md +5 -3
- package/.agent/skills/riligar-dev-autopilot/SKILL.md +0 -1
- package/.agent/skills/riligar-dev-backend/SKILL.md +0 -1
- package/.agent/skills/riligar-dev-clean-code/SKILL.md +0 -1
- package/.agent/skills/riligar-dev-code-review/SKILL.md +0 -1
- package/.agent/skills/riligar-dev-database/SKILL.md +8 -9
- package/.agent/skills/riligar-dev-frontend/SKILL.md +0 -1
- package/.agent/skills/riligar-dev-landing-page/SKILL.md +0 -1
- package/.agent/skills/riligar-dev-seo/SKILL.md +6 -3
- package/.agent/skills/riligar-dev-stripe/SKILL.md +196 -91
- package/.agent/skills/riligar-dev-stripe/assets/stripe-client.js +422 -0
- package/.agent/skills/riligar-dev-stripe/assets/stripe-server.js +300 -0
- package/.agent/skills/riligar-dev-stripe/references/stripe-database.md +369 -0
- package/.agent/skills/riligar-dev-stripe/references/stripe-elysia.md +342 -0
- package/.agent/skills/riligar-dev-stripe/references/stripe-react.md +478 -0
- package/.agent/skills/riligar-dev-stripe/references/stripe-webhooks.md +376 -0
- package/.agent/skills/riligar-infrastructure/SKILL.md +0 -1
- package/.agent/skills/riligar-marketing-copy/SKILL.md +0 -1
- package/.agent/skills/riligar-marketing-email/SKILL.md +0 -1
- package/.agent/skills/riligar-marketing-seo/SKILL.md +0 -1
- package/.agent/skills/riligar-plan-writing/SKILL.md +0 -1
- package/.agent/skills/riligar-tech-stack/SKILL.md +0 -1
- package/.agent/skills/skill-creator/SKILL.md +0 -2
- package/package.json +1 -1
- /package/.agent/skills/riligar-dev-architecture/{context-discovery.md → references/context-discovery.md} +0 -0
- /package/.agent/skills/riligar-dev-architecture/{examples.md → references/examples.md} +0 -0
- /package/.agent/skills/riligar-dev-architecture/{pattern-selection.md → references/pattern-selection.md} +0 -0
- /package/.agent/skills/riligar-dev-architecture/{patterns-reference.md → references/patterns-reference.md} +0 -0
- /package/.agent/skills/riligar-dev-architecture/{trade-off-analysis.md → references/trade-off-analysis.md} +0 -0
- /package/.agent/skills/riligar-dev-database/{database-selection.md → references/database-selection.md} +0 -0
- /package/.agent/skills/riligar-dev-database/{indexing.md → references/indexing.md} +0 -0
- /package/.agent/skills/riligar-dev-database/{migrations.md → references/migrations.md} +0 -0
- /package/.agent/skills/riligar-dev-database/{optimization.md → references/optimization.md} +0 -0
- /package/.agent/skills/riligar-dev-database/{orm-selection.md → references/orm-selection.md} +0 -0
- /package/.agent/skills/riligar-dev-database/{schema-design.md → references/schema-design.md} +0 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# Stripe Frontend Patterns (React)
|
|
2
|
+
|
|
3
|
+
Complete patterns for Stripe integration in React applications with Mantine UI.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### Stripe Provider
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
// main.jsx
|
|
11
|
+
import { loadStripe } from '@stripe/stripe-js'
|
|
12
|
+
import { Elements } from '@stripe/react-stripe-js'
|
|
13
|
+
|
|
14
|
+
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY)
|
|
15
|
+
|
|
16
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
17
|
+
<Elements stripe={stripePromise}>
|
|
18
|
+
<App />
|
|
19
|
+
</Elements>
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Stripe Configuration
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// services/stripe.js
|
|
27
|
+
import { loadStripe } from '@stripe/stripe-js'
|
|
28
|
+
|
|
29
|
+
export const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY)
|
|
30
|
+
|
|
31
|
+
// Elements appearance (matches Mantine theme)
|
|
32
|
+
export const elementsAppearance = {
|
|
33
|
+
theme: 'stripe',
|
|
34
|
+
variables: {
|
|
35
|
+
colorPrimary: '#228be6',
|
|
36
|
+
colorBackground: '#ffffff',
|
|
37
|
+
colorText: '#1a1b1e',
|
|
38
|
+
colorDanger: '#fa5252',
|
|
39
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
40
|
+
borderRadius: '4px',
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Hosted Checkout (Recommended)
|
|
46
|
+
|
|
47
|
+
### Pricing Page
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// pages/Pricing.jsx
|
|
51
|
+
import { Button, Card, Group, Stack, Text, Title } from '@mantine/core'
|
|
52
|
+
import { IconCheck } from '@tabler/icons-react'
|
|
53
|
+
import { useState } from 'react'
|
|
54
|
+
import ky from 'ky'
|
|
55
|
+
|
|
56
|
+
const plans = [
|
|
57
|
+
{
|
|
58
|
+
name: 'Starter',
|
|
59
|
+
priceId: 'price_starter_monthly',
|
|
60
|
+
price: 'R$ 29',
|
|
61
|
+
features: ['5 projetos', 'Suporte email', '1GB storage']
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Pro',
|
|
65
|
+
priceId: 'price_pro_monthly',
|
|
66
|
+
price: 'R$ 99',
|
|
67
|
+
features: ['Projetos ilimitados', 'Suporte prioritário', '10GB storage', 'API access']
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
export default function Pricing() {
|
|
72
|
+
const [loading, setLoading] = useState(null)
|
|
73
|
+
|
|
74
|
+
const handleCheckout = async (priceId) => {
|
|
75
|
+
setLoading(priceId)
|
|
76
|
+
try {
|
|
77
|
+
const { url } = await ky.post('/api/billing/checkout', {
|
|
78
|
+
json: { priceId, mode: 'subscription' }
|
|
79
|
+
}).json()
|
|
80
|
+
|
|
81
|
+
window.location.href = url
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Checkout error:', error)
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(null)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Group gap="lg" justify="center">
|
|
91
|
+
{plans.map(plan => (
|
|
92
|
+
<Card key={plan.priceId} shadow="sm" padding="lg" radius="md" withBorder w={300}>
|
|
93
|
+
<Stack>
|
|
94
|
+
<Title order={3}>{plan.name}</Title>
|
|
95
|
+
<Text size="xl" fw={700}>{plan.price}<Text span size="sm" c="dimmed">/mês</Text></Text>
|
|
96
|
+
|
|
97
|
+
<Stack gap="xs">
|
|
98
|
+
{plan.features.map(feature => (
|
|
99
|
+
<Group key={feature} gap="xs">
|
|
100
|
+
<IconCheck size={16} color="green" />
|
|
101
|
+
<Text size="sm">{feature}</Text>
|
|
102
|
+
</Group>
|
|
103
|
+
))}
|
|
104
|
+
</Stack>
|
|
105
|
+
|
|
106
|
+
<Button
|
|
107
|
+
fullWidth
|
|
108
|
+
loading={loading === plan.priceId}
|
|
109
|
+
onClick={() => handleCheckout(plan.priceId)}
|
|
110
|
+
>
|
|
111
|
+
Assinar {plan.name}
|
|
112
|
+
</Button>
|
|
113
|
+
</Stack>
|
|
114
|
+
</Card>
|
|
115
|
+
))}
|
|
116
|
+
</Group>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Success Page
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
// pages/Success.jsx
|
|
125
|
+
import { Button, Container, Stack, Text, Title } from '@mantine/core'
|
|
126
|
+
import { IconCircleCheck } from '@tabler/icons-react'
|
|
127
|
+
import { useEffect, useState } from 'react'
|
|
128
|
+
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
129
|
+
import ky from 'ky'
|
|
130
|
+
|
|
131
|
+
export default function Success() {
|
|
132
|
+
const [searchParams] = useSearchParams()
|
|
133
|
+
const navigate = useNavigate()
|
|
134
|
+
const [session, setSession] = useState(null)
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const sessionId = searchParams.get('session_id')
|
|
138
|
+
if (sessionId) {
|
|
139
|
+
ky.get(`/api/billing/session/${sessionId}`).json()
|
|
140
|
+
.then(setSession)
|
|
141
|
+
.catch(console.error)
|
|
142
|
+
}
|
|
143
|
+
}, [searchParams])
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Container size="sm" py="xl">
|
|
147
|
+
<Stack align="center" gap="md">
|
|
148
|
+
<IconCircleCheck size={64} color="green" />
|
|
149
|
+
<Title order={2}>Pagamento confirmado!</Title>
|
|
150
|
+
<Text c="dimmed">
|
|
151
|
+
Obrigado por assinar. Seu acesso já está ativo.
|
|
152
|
+
</Text>
|
|
153
|
+
<Button onClick={() => navigate('/dashboard')}>
|
|
154
|
+
Ir para o Dashboard
|
|
155
|
+
</Button>
|
|
156
|
+
</Stack>
|
|
157
|
+
</Container>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Customer Portal
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
// components/BillingSettings.jsx
|
|
166
|
+
import { Button, Card, Group, Stack, Text, Title, Badge } from '@mantine/core'
|
|
167
|
+
import { useState, useEffect } from 'react'
|
|
168
|
+
import ky from 'ky'
|
|
169
|
+
|
|
170
|
+
export default function BillingSettings() {
|
|
171
|
+
const [subscription, setSubscription] = useState(null)
|
|
172
|
+
const [loading, setLoading] = useState(false)
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
ky.get('/api/billing/subscription').json()
|
|
176
|
+
.then(setSubscription)
|
|
177
|
+
.catch(console.error)
|
|
178
|
+
}, [])
|
|
179
|
+
|
|
180
|
+
const openPortal = async () => {
|
|
181
|
+
setLoading(true)
|
|
182
|
+
try {
|
|
183
|
+
const { url } = await ky.post('/api/billing/portal').json()
|
|
184
|
+
window.location.href = url
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error('Portal error:', error)
|
|
187
|
+
} finally {
|
|
188
|
+
setLoading(false)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const getStatusBadge = (status) => {
|
|
193
|
+
const colors = {
|
|
194
|
+
active: 'green',
|
|
195
|
+
trialing: 'blue',
|
|
196
|
+
past_due: 'yellow',
|
|
197
|
+
canceled: 'red',
|
|
198
|
+
none: 'gray'
|
|
199
|
+
}
|
|
200
|
+
return <Badge color={colors[status] || 'gray'}>{status}</Badge>
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
|
205
|
+
<Stack>
|
|
206
|
+
<Title order={4}>Assinatura</Title>
|
|
207
|
+
|
|
208
|
+
{subscription ? (
|
|
209
|
+
<>
|
|
210
|
+
<Group justify="space-between">
|
|
211
|
+
<Text>Status</Text>
|
|
212
|
+
{getStatusBadge(subscription.status)}
|
|
213
|
+
</Group>
|
|
214
|
+
|
|
215
|
+
{subscription.status !== 'none' && (
|
|
216
|
+
<>
|
|
217
|
+
<Group justify="space-between">
|
|
218
|
+
<Text>Plano</Text>
|
|
219
|
+
<Text fw={500}>{subscription.plan}</Text>
|
|
220
|
+
</Group>
|
|
221
|
+
|
|
222
|
+
{subscription.cancelAtPeriodEnd && (
|
|
223
|
+
<Text size="sm" c="orange">
|
|
224
|
+
Cancela em {new Date(subscription.currentPeriodEnd * 1000).toLocaleDateString()}
|
|
225
|
+
</Text>
|
|
226
|
+
)}
|
|
227
|
+
</>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
<Button variant="outline" onClick={openPortal} loading={loading}>
|
|
231
|
+
Gerenciar Assinatura
|
|
232
|
+
</Button>
|
|
233
|
+
</>
|
|
234
|
+
) : (
|
|
235
|
+
<Text c="dimmed">Carregando...</Text>
|
|
236
|
+
)}
|
|
237
|
+
</Stack>
|
|
238
|
+
</Card>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Embedded Payment Form
|
|
244
|
+
|
|
245
|
+
For custom payment forms (one-time payments):
|
|
246
|
+
|
|
247
|
+
### Payment Form Component
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
// components/PaymentForm.jsx
|
|
251
|
+
import { Button, Stack, Text } from '@mantine/core'
|
|
252
|
+
import { PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js'
|
|
253
|
+
import { useState } from 'react'
|
|
254
|
+
|
|
255
|
+
export default function PaymentForm({ onSuccess }) {
|
|
256
|
+
const stripe = useStripe()
|
|
257
|
+
const elements = useElements()
|
|
258
|
+
const [loading, setLoading] = useState(false)
|
|
259
|
+
const [error, setError] = useState(null)
|
|
260
|
+
|
|
261
|
+
const handleSubmit = async (e) => {
|
|
262
|
+
e.preventDefault()
|
|
263
|
+
if (!stripe || !elements) return
|
|
264
|
+
|
|
265
|
+
setLoading(true)
|
|
266
|
+
setError(null)
|
|
267
|
+
|
|
268
|
+
const { error: submitError, paymentIntent } = await stripe.confirmPayment({
|
|
269
|
+
elements,
|
|
270
|
+
confirmParams: {
|
|
271
|
+
return_url: `${window.location.origin}/payment/success`,
|
|
272
|
+
},
|
|
273
|
+
redirect: 'if_required'
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
if (submitError) {
|
|
277
|
+
setError(submitError.message)
|
|
278
|
+
setLoading(false)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (paymentIntent?.status === 'succeeded') {
|
|
283
|
+
onSuccess?.(paymentIntent)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setLoading(false)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<form onSubmit={handleSubmit}>
|
|
291
|
+
<Stack>
|
|
292
|
+
<PaymentElement />
|
|
293
|
+
|
|
294
|
+
{error && <Text c="red" size="sm">{error}</Text>}
|
|
295
|
+
|
|
296
|
+
<Button type="submit" loading={loading} disabled={!stripe}>
|
|
297
|
+
Pagar
|
|
298
|
+
</Button>
|
|
299
|
+
</Stack>
|
|
300
|
+
</form>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Using Payment Form
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// pages/Checkout.jsx
|
|
309
|
+
import { Container, Card, Title, Text, Stack } from '@mantine/core'
|
|
310
|
+
import { Elements } from '@stripe/react-stripe-js'
|
|
311
|
+
import { useState, useEffect } from 'react'
|
|
312
|
+
import { stripePromise, elementsAppearance } from '../services/stripe'
|
|
313
|
+
import PaymentForm from '../components/PaymentForm'
|
|
314
|
+
import ky from 'ky'
|
|
315
|
+
|
|
316
|
+
export default function Checkout({ productId, amount }) {
|
|
317
|
+
const [clientSecret, setClientSecret] = useState(null)
|
|
318
|
+
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
ky.post('/api/billing/payment/create', {
|
|
321
|
+
json: { productId, amount }
|
|
322
|
+
}).json()
|
|
323
|
+
.then(({ clientSecret }) => setClientSecret(clientSecret))
|
|
324
|
+
.catch(console.error)
|
|
325
|
+
}, [productId, amount])
|
|
326
|
+
|
|
327
|
+
if (!clientSecret) {
|
|
328
|
+
return <Text>Carregando...</Text>
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<Container size="sm">
|
|
333
|
+
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
|
334
|
+
<Stack>
|
|
335
|
+
<Title order={3}>Finalizar Compra</Title>
|
|
336
|
+
<Text c="dimmed">Total: R$ {(amount / 100).toFixed(2)}</Text>
|
|
337
|
+
|
|
338
|
+
<Elements
|
|
339
|
+
stripe={stripePromise}
|
|
340
|
+
options={{
|
|
341
|
+
clientSecret,
|
|
342
|
+
appearance: elementsAppearance
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<PaymentForm onSuccess={() => console.log('Paid!')} />
|
|
346
|
+
</Elements>
|
|
347
|
+
</Stack>
|
|
348
|
+
</Card>
|
|
349
|
+
</Container>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Subscription Hook
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
// hooks/useSubscription.js
|
|
358
|
+
import { useState, useEffect } from 'react'
|
|
359
|
+
import ky from 'ky'
|
|
360
|
+
|
|
361
|
+
export function useSubscription() {
|
|
362
|
+
const [subscription, setSubscription] = useState(null)
|
|
363
|
+
const [loading, setLoading] = useState(true)
|
|
364
|
+
const [error, setError] = useState(null)
|
|
365
|
+
|
|
366
|
+
const fetchSubscription = async () => {
|
|
367
|
+
setLoading(true)
|
|
368
|
+
try {
|
|
369
|
+
const data = await ky.get('/api/billing/subscription').json()
|
|
370
|
+
setSubscription(data)
|
|
371
|
+
setError(null)
|
|
372
|
+
} catch (err) {
|
|
373
|
+
setError(err)
|
|
374
|
+
} finally {
|
|
375
|
+
setLoading(false)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
fetchSubscription()
|
|
381
|
+
}, [])
|
|
382
|
+
|
|
383
|
+
const isActive = subscription?.status === 'active' || subscription?.status === 'trialing'
|
|
384
|
+
const isPro = isActive && subscription?.plan?.includes('pro')
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
subscription,
|
|
388
|
+
loading,
|
|
389
|
+
error,
|
|
390
|
+
isActive,
|
|
391
|
+
isPro,
|
|
392
|
+
refresh: fetchSubscription
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Using the Hook
|
|
398
|
+
|
|
399
|
+
```javascript
|
|
400
|
+
// components/PremiumFeature.jsx
|
|
401
|
+
import { Alert } from '@mantine/core'
|
|
402
|
+
import { useSubscription } from '../hooks/useSubscription'
|
|
403
|
+
|
|
404
|
+
export default function PremiumFeature({ children }) {
|
|
405
|
+
const { isActive, loading } = useSubscription()
|
|
406
|
+
|
|
407
|
+
if (loading) return null
|
|
408
|
+
|
|
409
|
+
if (!isActive) {
|
|
410
|
+
return (
|
|
411
|
+
<Alert color="yellow" title="Recurso Premium">
|
|
412
|
+
Faça upgrade para acessar este recurso.
|
|
413
|
+
</Alert>
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return children
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Invoices List
|
|
422
|
+
|
|
423
|
+
```javascript
|
|
424
|
+
// components/InvoiceList.jsx
|
|
425
|
+
import { Table, Badge, ActionIcon, Text } from '@mantine/core'
|
|
426
|
+
import { IconDownload } from '@tabler/icons-react'
|
|
427
|
+
import { useState, useEffect } from 'react'
|
|
428
|
+
import ky from 'ky'
|
|
429
|
+
|
|
430
|
+
export default function InvoiceList() {
|
|
431
|
+
const [invoices, setInvoices] = useState([])
|
|
432
|
+
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
ky.get('/api/billing/invoices').json()
|
|
435
|
+
.then(({ data }) => setInvoices(data))
|
|
436
|
+
.catch(console.error)
|
|
437
|
+
}, [])
|
|
438
|
+
|
|
439
|
+
const getStatusBadge = (status) => {
|
|
440
|
+
const colors = { paid: 'green', open: 'blue', void: 'gray' }
|
|
441
|
+
return <Badge color={colors[status]}>{status}</Badge>
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<Table>
|
|
446
|
+
<Table.Thead>
|
|
447
|
+
<Table.Tr>
|
|
448
|
+
<Table.Th>Data</Table.Th>
|
|
449
|
+
<Table.Th>Valor</Table.Th>
|
|
450
|
+
<Table.Th>Status</Table.Th>
|
|
451
|
+
<Table.Th></Table.Th>
|
|
452
|
+
</Table.Tr>
|
|
453
|
+
</Table.Thead>
|
|
454
|
+
<Table.Tbody>
|
|
455
|
+
{invoices.map(invoice => (
|
|
456
|
+
<Table.Tr key={invoice.id}>
|
|
457
|
+
<Table.Td>{new Date(invoice.date * 1000).toLocaleDateString()}</Table.Td>
|
|
458
|
+
<Table.Td>R$ {(invoice.amount / 100).toFixed(2)}</Table.Td>
|
|
459
|
+
<Table.Td>{getStatusBadge(invoice.status)}</Table.Td>
|
|
460
|
+
<Table.Td>
|
|
461
|
+
{invoice.pdfUrl && (
|
|
462
|
+
<ActionIcon
|
|
463
|
+
component="a"
|
|
464
|
+
href={invoice.pdfUrl}
|
|
465
|
+
target="_blank"
|
|
466
|
+
variant="subtle"
|
|
467
|
+
>
|
|
468
|
+
<IconDownload size={16} />
|
|
469
|
+
</ActionIcon>
|
|
470
|
+
)}
|
|
471
|
+
</Table.Td>
|
|
472
|
+
</Table.Tr>
|
|
473
|
+
))}
|
|
474
|
+
</Table.Tbody>
|
|
475
|
+
</Table>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
```
|