@djangocfg/ext-payments 1.0.14 → 1.0.17
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/config.cjs +5 -8
- package/dist/config.js +5 -8
- package/dist/index.cjs +1085 -1107
- package/dist/index.d.cts +480 -41
- package/dist/index.d.ts +480 -41
- package/dist/index.js +1037 -1093
- package/package.json +13 -16
- package/src/api/generated/ext_payments/CLAUDE.md +7 -3
- package/src/api/generated/ext_payments/_utils/fetchers/ext_payments__payments.ts +237 -5
- package/src/api/generated/ext_payments/_utils/hooks/ext_payments__payments.ts +71 -3
- package/src/api/generated/ext_payments/_utils/schemas/PaginatedWithdrawalListList.schema.ts +24 -0
- package/src/api/generated/ext_payments/_utils/schemas/PaymentCreateRequest.schema.ts +21 -0
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCreateRequest.schema.ts +21 -0
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalDetail.schema.ts +42 -0
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalList.schema.ts +29 -0
- package/src/api/generated/ext_payments/_utils/schemas/index.ts +5 -0
- package/src/api/generated/ext_payments/enums.ts +36 -0
- package/src/api/generated/ext_payments/ext_payments__payments/client.ts +58 -5
- package/src/api/generated/ext_payments/ext_payments__payments/models.ts +141 -0
- package/src/api/generated/ext_payments/schema.json +579 -3
- package/src/components/ActivityItem.tsx +118 -0
- package/src/components/ActivityList.tsx +93 -0
- package/src/components/AddFundsSheet.tsx +258 -0
- package/src/components/BalanceHero.tsx +102 -0
- package/src/components/PaymentSheet.tsx +290 -0
- package/src/components/ResponsiveSheet.tsx +151 -0
- package/src/components/WithdrawSheet.tsx +329 -0
- package/src/components/index.ts +18 -0
- package/src/contexts/WalletContext.tsx +355 -0
- package/src/contexts/index.ts +12 -45
- package/src/index.ts +6 -18
- package/src/contexts/BalancesContext.tsx +0 -63
- package/src/contexts/CurrenciesContext.tsx +0 -64
- package/src/contexts/OverviewContext.tsx +0 -173
- package/src/contexts/PaymentsContext.tsx +0 -122
- package/src/contexts/PaymentsExtensionProvider.tsx +0 -56
- package/src/contexts/README.md +0 -201
- package/src/contexts/RootPaymentsContext.tsx +0 -66
- package/src/contexts/types.ts +0 -40
- package/src/hooks/index.ts +0 -20
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +0 -90
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +0 -274
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +0 -287
- package/src/layouts/PaymentsLayout/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/events.ts +0 -47
- package/src/layouts/PaymentsLayout/index.ts +0 -16
- package/src/layouts/PaymentsLayout/types.ts +0 -6
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +0 -121
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +0 -139
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +0 -21
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +0 -279
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +0 -18
- package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +0 -260
- package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +0 -18
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Item (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* Single row in the activity list
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { ArrowDownLeft, ArrowUpRight, Clock, CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react';
|
|
10
|
+
import moment from 'moment';
|
|
11
|
+
|
|
12
|
+
import { TokenIcon } from '@djangocfg/ui-core';
|
|
13
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
|
+
|
|
15
|
+
import type { ActivityItem as ActivityItemType } from '../contexts/WalletContext';
|
|
16
|
+
|
|
17
|
+
interface ActivityItemProps {
|
|
18
|
+
item: ActivityItemType;
|
|
19
|
+
onClick?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const statusConfig: Record<string, { icon: any; color: string; bg: string; animate?: boolean }> = {
|
|
23
|
+
pending: {
|
|
24
|
+
icon: Clock,
|
|
25
|
+
color: 'text-yellow-500',
|
|
26
|
+
bg: 'bg-yellow-500/10',
|
|
27
|
+
},
|
|
28
|
+
confirming: {
|
|
29
|
+
icon: Loader2,
|
|
30
|
+
color: 'text-blue-500',
|
|
31
|
+
bg: 'bg-blue-500/10',
|
|
32
|
+
animate: true,
|
|
33
|
+
},
|
|
34
|
+
completed: {
|
|
35
|
+
icon: CheckCircle2,
|
|
36
|
+
color: 'text-green-500',
|
|
37
|
+
bg: 'bg-green-500/10',
|
|
38
|
+
},
|
|
39
|
+
failed: {
|
|
40
|
+
icon: XCircle,
|
|
41
|
+
color: 'text-red-500',
|
|
42
|
+
bg: 'bg-red-500/10',
|
|
43
|
+
},
|
|
44
|
+
expired: {
|
|
45
|
+
icon: AlertCircle,
|
|
46
|
+
color: 'text-muted-foreground',
|
|
47
|
+
bg: 'bg-muted',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function ActivityItem({ item, onClick }: ActivityItemProps) {
|
|
52
|
+
const config = statusConfig[item.status];
|
|
53
|
+
const StatusIcon = config.icon;
|
|
54
|
+
|
|
55
|
+
const isPositive = item.type === 'payment' || item.type === 'deposit';
|
|
56
|
+
const DirectionIcon = isPositive ? ArrowDownLeft : ArrowUpRight;
|
|
57
|
+
|
|
58
|
+
const relativeTime = moment(item.createdAt).fromNow();
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
onClick={onClick}
|
|
63
|
+
className={cn(
|
|
64
|
+
'w-full flex items-center gap-4 p-4 rounded-xl',
|
|
65
|
+
'cursor-pointer',
|
|
66
|
+
'hover:bg-accent active:bg-accent/80 transition-colors',
|
|
67
|
+
'text-left'
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{/* Icon */}
|
|
71
|
+
<div className={cn(
|
|
72
|
+
'flex items-center justify-center w-10 h-10 rounded-full',
|
|
73
|
+
isPositive ? 'bg-green-500/10' : 'bg-red-500/10'
|
|
74
|
+
)}>
|
|
75
|
+
{item.currency ? (
|
|
76
|
+
<TokenIcon symbol={item.currency} size={24} />
|
|
77
|
+
) : (
|
|
78
|
+
<DirectionIcon className={cn(
|
|
79
|
+
'h-5 w-5',
|
|
80
|
+
isPositive ? 'text-green-500' : 'text-red-500'
|
|
81
|
+
)} />
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Content */}
|
|
86
|
+
<div className="flex-1 min-w-0">
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<span className="font-medium truncate">{item.description}</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
91
|
+
<span>{relativeTime}</span>
|
|
92
|
+
{item.status !== 'completed' && (
|
|
93
|
+
<>
|
|
94
|
+
<span>·</span>
|
|
95
|
+
<span className={cn('flex items-center gap-1', config.color)}>
|
|
96
|
+
<StatusIcon className={cn('h-3 w-3', config.animate && 'animate-spin')} />
|
|
97
|
+
{item.statusDisplay}
|
|
98
|
+
</span>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Amount */}
|
|
105
|
+
<div className="text-right">
|
|
106
|
+
<span className={cn(
|
|
107
|
+
'font-semibold tabular-nums',
|
|
108
|
+
isPositive ? 'text-green-600 dark:text-green-400' : 'text-foreground'
|
|
109
|
+
)}>
|
|
110
|
+
{item.amountDisplay}
|
|
111
|
+
</span>
|
|
112
|
+
{item.status === 'completed' && (
|
|
113
|
+
<CheckCircle2 className="h-4 w-4 text-green-500 ml-2 inline-block" />
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity List (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* List of recent activity (payments + transactions)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { History, ChevronRight } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Button, Skeleton } from '@djangocfg/ui-core';
|
|
12
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
+
|
|
14
|
+
import { useWallet, type ActivityItem as ActivityItemType } from '../contexts/WalletContext';
|
|
15
|
+
import { ActivityItem } from './ActivityItem';
|
|
16
|
+
|
|
17
|
+
interface ActivityListProps {
|
|
18
|
+
onItemClick?: (item: ActivityItemType) => void;
|
|
19
|
+
onViewAll?: () => void;
|
|
20
|
+
limit?: number;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ActivityList({
|
|
25
|
+
onItemClick,
|
|
26
|
+
onViewAll,
|
|
27
|
+
limit = 10,
|
|
28
|
+
className,
|
|
29
|
+
}: ActivityListProps) {
|
|
30
|
+
const { activity, isLoadingActivity, hasMoreActivity } = useWallet();
|
|
31
|
+
|
|
32
|
+
const displayedActivity = limit ? activity.slice(0, limit) : activity;
|
|
33
|
+
|
|
34
|
+
if (isLoadingActivity) {
|
|
35
|
+
return (
|
|
36
|
+
<div className={cn('space-y-2', className)}>
|
|
37
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
38
|
+
<Skeleton className="h-5 w-32" />
|
|
39
|
+
</div>
|
|
40
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
41
|
+
<div key={i} className="flex items-center gap-4 p-4">
|
|
42
|
+
<Skeleton className="h-10 w-10 rounded-full" />
|
|
43
|
+
<div className="flex-1 space-y-2">
|
|
44
|
+
<Skeleton className="h-4 w-32" />
|
|
45
|
+
<Skeleton className="h-3 w-24" />
|
|
46
|
+
</div>
|
|
47
|
+
<Skeleton className="h-5 w-16" />
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (activity.length === 0) {
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn('text-center py-12', className)}>
|
|
57
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-muted mb-4">
|
|
58
|
+
<History className="h-8 w-8 text-muted-foreground" />
|
|
59
|
+
</div>
|
|
60
|
+
<h3 className="font-semibold mb-1">No Activity Yet</h3>
|
|
61
|
+
<p className="text-sm text-muted-foreground">
|
|
62
|
+
Your transactions will appear here
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={cn('pt-6', className)}>
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
72
|
+
<h2 className="font-semibold text-lg">Recent Activity</h2>
|
|
73
|
+
{hasMoreActivity && onViewAll && (
|
|
74
|
+
<Button variant="ghost" size="sm" onClick={onViewAll} className="text-primary">
|
|
75
|
+
View All
|
|
76
|
+
<ChevronRight className="h-4 w-4 ml-1" />
|
|
77
|
+
</Button>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* List */}
|
|
82
|
+
<div className="divide-y divide-border/50">
|
|
83
|
+
{displayedActivity.map((item) => (
|
|
84
|
+
<ActivityItem
|
|
85
|
+
key={item.id}
|
|
86
|
+
item={item}
|
|
87
|
+
onClick={() => onItemClick?.(item)}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Funds Sheet (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* Responsive: Dialog on desktop, Drawer on mobile
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
10
|
+
import { RefreshCw } from 'lucide-react';
|
|
11
|
+
import { useForm } from 'react-hook-form';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
Alert,
|
|
17
|
+
AlertDescription,
|
|
18
|
+
Button,
|
|
19
|
+
Combobox,
|
|
20
|
+
Form,
|
|
21
|
+
FormControl,
|
|
22
|
+
FormField,
|
|
23
|
+
FormItem,
|
|
24
|
+
FormLabel,
|
|
25
|
+
FormMessage,
|
|
26
|
+
Input,
|
|
27
|
+
TokenIcon,
|
|
28
|
+
ResponsiveSheet,
|
|
29
|
+
ResponsiveSheetContent,
|
|
30
|
+
ResponsiveSheetDescription,
|
|
31
|
+
ResponsiveSheetHeader,
|
|
32
|
+
ResponsiveSheetTitle,
|
|
33
|
+
} from '@djangocfg/ui-core';
|
|
34
|
+
|
|
35
|
+
import { useWallet } from '../contexts/WalletContext';
|
|
36
|
+
import type { PaymentDetail } from '../api/generated/ext_payments/_utils/schemas';
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
// Schema
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const AddFundsSchema = z.object({
|
|
43
|
+
amount: z.number().min(1, 'Minimum $1.00'),
|
|
44
|
+
currency: z.string().min(1, 'Select a currency'),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type AddFundsForm = z.infer<typeof AddFundsSchema>;
|
|
48
|
+
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// Props
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
interface AddFundsSheetProps {
|
|
54
|
+
open: boolean;
|
|
55
|
+
onOpenChange: (open: boolean) => void;
|
|
56
|
+
onSuccess?: (payment: PaymentDetail) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
// Component
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetProps) {
|
|
64
|
+
const { currencies, isLoadingCurrencies, addFunds } = useWallet();
|
|
65
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
|
|
68
|
+
const form = useForm<AddFundsForm>({
|
|
69
|
+
resolver: zodResolver(AddFundsSchema),
|
|
70
|
+
defaultValues: {
|
|
71
|
+
amount: 100,
|
|
72
|
+
currency: '',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Currency options for combobox
|
|
77
|
+
const currencyOptions = useMemo(() => {
|
|
78
|
+
return currencies.map((c) => ({
|
|
79
|
+
value: c.code,
|
|
80
|
+
label: c.network ? `${c.code} (${c.network})` : c.code,
|
|
81
|
+
rate: c.rate,
|
|
82
|
+
network: c.network,
|
|
83
|
+
}));
|
|
84
|
+
}, [currencies]);
|
|
85
|
+
|
|
86
|
+
// Set default currency when loaded
|
|
87
|
+
useMemo(() => {
|
|
88
|
+
if (currencyOptions.length > 0 && !form.getValues('currency')) {
|
|
89
|
+
const usdt = currencyOptions.find(c => c.value.includes('USDT'));
|
|
90
|
+
form.setValue('currency', usdt?.value || currencyOptions[0].value);
|
|
91
|
+
}
|
|
92
|
+
}, [currencyOptions, form]);
|
|
93
|
+
|
|
94
|
+
// Calculate crypto amount
|
|
95
|
+
const selectedCurrency = currencyOptions.find(c => c.value === form.watch('currency'));
|
|
96
|
+
const cryptoAmount = useMemo(() => {
|
|
97
|
+
const amount = form.watch('amount');
|
|
98
|
+
if (!selectedCurrency?.rate || !amount) return null;
|
|
99
|
+
return amount / selectedCurrency.rate;
|
|
100
|
+
}, [form.watch('amount'), selectedCurrency]);
|
|
101
|
+
|
|
102
|
+
// Handle submit
|
|
103
|
+
const handleSubmit = useCallback(async (data: AddFundsForm) => {
|
|
104
|
+
try {
|
|
105
|
+
setIsSubmitting(true);
|
|
106
|
+
setError(null);
|
|
107
|
+
|
|
108
|
+
const result = await addFunds({
|
|
109
|
+
amount_usd: String(data.amount),
|
|
110
|
+
currency_code: data.currency,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
form.reset();
|
|
114
|
+
onOpenChange(false);
|
|
115
|
+
onSuccess?.(result);
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
const message = err?.response?.data?.message
|
|
118
|
+
|| err?.response?.data?.detail
|
|
119
|
+
|| err?.message
|
|
120
|
+
|| 'Failed to create payment';
|
|
121
|
+
setError(message);
|
|
122
|
+
} finally {
|
|
123
|
+
setIsSubmitting(false);
|
|
124
|
+
}
|
|
125
|
+
}, [addFunds, form, onOpenChange, onSuccess]);
|
|
126
|
+
|
|
127
|
+
// Reset on close
|
|
128
|
+
const handleOpenChange = useCallback((open: boolean) => {
|
|
129
|
+
if (!open) {
|
|
130
|
+
setError(null);
|
|
131
|
+
form.reset();
|
|
132
|
+
}
|
|
133
|
+
onOpenChange(open);
|
|
134
|
+
}, [form, onOpenChange]);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ResponsiveSheet open={open} onOpenChange={handleOpenChange}>
|
|
138
|
+
<ResponsiveSheetContent className="sm:max-w-md">
|
|
139
|
+
<ResponsiveSheetHeader>
|
|
140
|
+
<ResponsiveSheetTitle>Add Funds</ResponsiveSheetTitle>
|
|
141
|
+
<ResponsiveSheetDescription>
|
|
142
|
+
Add funds to your wallet using cryptocurrency
|
|
143
|
+
</ResponsiveSheetDescription>
|
|
144
|
+
</ResponsiveSheetHeader>
|
|
145
|
+
|
|
146
|
+
<Form {...form}>
|
|
147
|
+
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 p-4 sm:p-0 sm:mt-4">
|
|
148
|
+
{/* Amount Input */}
|
|
149
|
+
<FormField
|
|
150
|
+
control={form.control}
|
|
151
|
+
name="amount"
|
|
152
|
+
render={({ field }) => (
|
|
153
|
+
<FormItem>
|
|
154
|
+
<FormLabel>Amount (USD)</FormLabel>
|
|
155
|
+
<FormControl>
|
|
156
|
+
<div className="relative">
|
|
157
|
+
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground text-lg">
|
|
158
|
+
$
|
|
159
|
+
</span>
|
|
160
|
+
<Input
|
|
161
|
+
type="number"
|
|
162
|
+
step="0.01"
|
|
163
|
+
min="1"
|
|
164
|
+
placeholder="100.00"
|
|
165
|
+
className="pl-8 text-2xl h-14 font-semibold"
|
|
166
|
+
{...field}
|
|
167
|
+
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
</FormControl>
|
|
171
|
+
<FormMessage />
|
|
172
|
+
</FormItem>
|
|
173
|
+
)}
|
|
174
|
+
/>
|
|
175
|
+
|
|
176
|
+
{/* Currency Selection */}
|
|
177
|
+
<FormField
|
|
178
|
+
control={form.control}
|
|
179
|
+
name="currency"
|
|
180
|
+
render={({ field }) => (
|
|
181
|
+
<FormItem>
|
|
182
|
+
<FormLabel>Pay with</FormLabel>
|
|
183
|
+
<FormControl>
|
|
184
|
+
<Combobox
|
|
185
|
+
options={currencyOptions}
|
|
186
|
+
value={field.value}
|
|
187
|
+
onValueChange={field.onChange}
|
|
188
|
+
placeholder="Select currency..."
|
|
189
|
+
searchPlaceholder="Search..."
|
|
190
|
+
disabled={isLoadingCurrencies}
|
|
191
|
+
className="h-14"
|
|
192
|
+
renderOption={(option) => (
|
|
193
|
+
<div className="flex items-center gap-3 flex-1">
|
|
194
|
+
<TokenIcon symbol={option.value} size={24} />
|
|
195
|
+
<span className="font-medium">{option.label}</span>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
renderValue={(option) => option && (
|
|
199
|
+
<div className="flex items-center gap-3">
|
|
200
|
+
<TokenIcon symbol={option.value} size={24} />
|
|
201
|
+
<span className="font-medium">{option.label}</span>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
/>
|
|
205
|
+
</FormControl>
|
|
206
|
+
<FormMessage />
|
|
207
|
+
</FormItem>
|
|
208
|
+
)}
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
{/* Conversion Preview */}
|
|
212
|
+
{cryptoAmount !== null && selectedCurrency && (
|
|
213
|
+
<div className="bg-muted rounded-xl p-4 space-y-2">
|
|
214
|
+
<div className="flex items-center justify-between">
|
|
215
|
+
<span className="text-muted-foreground">You will send</span>
|
|
216
|
+
<div className="flex items-center gap-2">
|
|
217
|
+
<TokenIcon symbol={selectedCurrency.value} size={20} />
|
|
218
|
+
<span className="font-mono font-semibold">
|
|
219
|
+
{cryptoAmount.toFixed(8)} {selectedCurrency.value}
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
224
|
+
<span>Rate</span>
|
|
225
|
+
<span>1 {selectedCurrency.value} = ${selectedCurrency.rate.toFixed(2)}</span>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Error */}
|
|
231
|
+
{error && (
|
|
232
|
+
<Alert variant="destructive">
|
|
233
|
+
<AlertDescription>{error}</AlertDescription>
|
|
234
|
+
</Alert>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{/* Submit Button */}
|
|
238
|
+
<Button
|
|
239
|
+
type="submit"
|
|
240
|
+
size="lg"
|
|
241
|
+
className="w-full h-14 text-lg rounded-xl"
|
|
242
|
+
disabled={isSubmitting || currencyOptions.length === 0}
|
|
243
|
+
>
|
|
244
|
+
{isSubmitting ? (
|
|
245
|
+
<>
|
|
246
|
+
<RefreshCw className="h-5 w-5 mr-2 animate-spin" />
|
|
247
|
+
Creating...
|
|
248
|
+
</>
|
|
249
|
+
) : (
|
|
250
|
+
'Continue'
|
|
251
|
+
)}
|
|
252
|
+
</Button>
|
|
253
|
+
</form>
|
|
254
|
+
</Form>
|
|
255
|
+
</ResponsiveSheetContent>
|
|
256
|
+
</ResponsiveSheet>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance Hero (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* Large centered balance display with action buttons
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { Plus, ArrowUpRight, RefreshCw } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Button, Skeleton } from '@djangocfg/ui-core';
|
|
12
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
+
|
|
14
|
+
import { useWallet } from '../contexts/WalletContext';
|
|
15
|
+
|
|
16
|
+
interface BalanceHeroProps {
|
|
17
|
+
onAddFunds?: () => void;
|
|
18
|
+
onWithdraw?: () => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function BalanceHero({ onAddFunds, onWithdraw, className }: BalanceHeroProps) {
|
|
23
|
+
const { balance, balanceAmount, isLoadingBalance, refreshWallet } = useWallet();
|
|
24
|
+
|
|
25
|
+
const formattedBalance = new Intl.NumberFormat('en-US', {
|
|
26
|
+
style: 'currency',
|
|
27
|
+
currency: 'USD',
|
|
28
|
+
minimumFractionDigits: 2,
|
|
29
|
+
maximumFractionDigits: 2,
|
|
30
|
+
}).format(balanceAmount);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn('flex flex-col items-center py-16 px-4', className)}>
|
|
34
|
+
{/* Balance Display */}
|
|
35
|
+
<div className="text-center mb-6">
|
|
36
|
+
{isLoadingBalance ? (
|
|
37
|
+
<>
|
|
38
|
+
<Skeleton className="h-12 w-48 mx-auto mb-2" />
|
|
39
|
+
<Skeleton className="h-4 w-24 mx-auto" />
|
|
40
|
+
</>
|
|
41
|
+
) : (
|
|
42
|
+
<>
|
|
43
|
+
<h1 className="text-5xl font-bold tracking-tight tabular-nums">
|
|
44
|
+
{formattedBalance}
|
|
45
|
+
</h1>
|
|
46
|
+
<p className="text-muted-foreground mt-1">Available Balance</p>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Action Buttons */}
|
|
52
|
+
<div className="flex items-center gap-3">
|
|
53
|
+
<Button
|
|
54
|
+
size="lg"
|
|
55
|
+
onClick={onAddFunds}
|
|
56
|
+
className="rounded-full px-6"
|
|
57
|
+
>
|
|
58
|
+
<Plus className="h-5 w-5 mr-2" />
|
|
59
|
+
Add Funds
|
|
60
|
+
</Button>
|
|
61
|
+
|
|
62
|
+
<Button
|
|
63
|
+
size="lg"
|
|
64
|
+
variant="outline"
|
|
65
|
+
onClick={onWithdraw}
|
|
66
|
+
className="rounded-full px-6"
|
|
67
|
+
>
|
|
68
|
+
<ArrowUpRight className="h-5 w-5 mr-2" />
|
|
69
|
+
Withdraw
|
|
70
|
+
</Button>
|
|
71
|
+
|
|
72
|
+
<Button
|
|
73
|
+
size="icon"
|
|
74
|
+
variant="ghost"
|
|
75
|
+
onClick={() => refreshWallet()}
|
|
76
|
+
className="rounded-full"
|
|
77
|
+
>
|
|
78
|
+
<RefreshCw className="h-4 w-4" />
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Stats (subtle) */}
|
|
83
|
+
{balance && !isLoadingBalance && (
|
|
84
|
+
<div className="flex items-center gap-6 mt-6 text-sm text-muted-foreground">
|
|
85
|
+
<div className="text-center">
|
|
86
|
+
<span className="block font-medium text-foreground">
|
|
87
|
+
${parseFloat(balance.total_deposited || '0').toFixed(2)}
|
|
88
|
+
</span>
|
|
89
|
+
<span>Total Deposited</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="h-8 w-px bg-border" />
|
|
92
|
+
<div className="text-center">
|
|
93
|
+
<span className="block font-medium text-foreground">
|
|
94
|
+
${parseFloat(balance.total_withdrawn || '0').toFixed(2)}
|
|
95
|
+
</span>
|
|
96
|
+
<span>Total Withdrawn</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|