@djangocfg/layouts 1.0.1
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/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +86 -0
- package/src/auth/README.md +962 -0
- package/src/auth/context/AuthContext.tsx +458 -0
- package/src/auth/context/index.ts +2 -0
- package/src/auth/context/types.ts +63 -0
- package/src/auth/hooks/index.ts +6 -0
- package/src/auth/hooks/useAuthForm.ts +329 -0
- package/src/auth/hooks/useAuthGuard.ts +23 -0
- package/src/auth/hooks/useAuthRedirect.ts +51 -0
- package/src/auth/hooks/useAutoAuth.ts +42 -0
- package/src/auth/hooks/useLocalStorage.ts +211 -0
- package/src/auth/hooks/useSessionStorage.ts +186 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/middlewares/index.ts +1 -0
- package/src/auth/middlewares/proxy.ts +24 -0
- package/src/auth/server.ts +6 -0
- package/src/auth/utils/errors.ts +34 -0
- package/src/auth/utils/index.ts +2 -0
- package/src/auth/utils/validation.ts +14 -0
- package/src/index.ts +15 -0
- package/src/layouts/AppLayout/AppLayout.tsx +123 -0
- package/src/layouts/AppLayout/README.md +204 -0
- package/src/layouts/AppLayout/SUMMARY.md +240 -0
- package/src/layouts/AppLayout/USAGE.md +312 -0
- package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
- package/src/layouts/AppLayout/components/Seo.tsx +87 -0
- package/src/layouts/AppLayout/components/index.ts +6 -0
- package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
- package/src/layouts/AppLayout/context/index.ts +5 -0
- package/src/layouts/AppLayout/hooks/index.ts +6 -0
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
- package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
- package/src/layouts/AppLayout/index.ts +31 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/index.ts +7 -0
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
- package/src/layouts/AppLayout/providers/index.ts +5 -0
- package/src/layouts/AppLayout/types/config.ts +40 -0
- package/src/layouts/AppLayout/types/index.ts +10 -0
- package/src/layouts/AppLayout/types/layout.ts +47 -0
- package/src/layouts/AppLayout/types/navigation.ts +41 -0
- package/src/layouts/AppLayout/types/routes.ts +45 -0
- package/src/layouts/AppLayout/utils/index.ts +5 -0
- package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
- package/src/layouts/PaymentsLayout/README.md +133 -0
- package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
- package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
- package/src/layouts/PaymentsLayout/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/events.ts +106 -0
- package/src/layouts/PaymentsLayout/index.ts +20 -0
- package/src/layouts/PaymentsLayout/types.ts +19 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
- package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
- package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
- package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
- package/src/layouts/ProfileLayout/components/index.ts +3 -0
- package/src/layouts/ProfileLayout/index.ts +3 -0
- package/src/layouts/SupportLayout/README.md +91 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
- package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
- package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
- package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
- package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
- package/src/layouts/SupportLayout/components/index.ts +6 -0
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
- package/src/layouts/SupportLayout/context/index.ts +2 -0
- package/src/layouts/SupportLayout/events.ts +31 -0
- package/src/layouts/SupportLayout/hooks/index.ts +2 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
- package/src/layouts/SupportLayout/index.ts +6 -0
- package/src/layouts/SupportLayout/types.ts +23 -0
- package/src/layouts/index.ts +9 -0
- package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
- package/src/snippets/AuthDialog/events.ts +21 -0
- package/src/snippets/AuthDialog/index.ts +3 -0
- package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
- package/src/snippets/Breadcrumbs.tsx +80 -0
- package/src/snippets/Chat/ChatUIContext.tsx +110 -0
- package/src/snippets/Chat/ChatWidget.tsx +476 -0
- package/src/snippets/Chat/README.md +122 -0
- package/src/snippets/Chat/components/MessageInput.tsx +124 -0
- package/src/snippets/Chat/components/MessageList.tsx +168 -0
- package/src/snippets/Chat/components/SessionList.tsx +192 -0
- package/src/snippets/Chat/components/index.ts +9 -0
- package/src/snippets/Chat/hooks/index.ts +6 -0
- package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
- package/src/snippets/Chat/index.tsx +44 -0
- package/src/snippets/Chat/types.ts +79 -0
- package/src/snippets/VideoPlayer/README.md +203 -0
- package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
- package/src/snippets/VideoPlayer/index.ts +8 -0
- package/src/snippets/VideoPlayer/types.ts +61 -0
- package/src/snippets/index.ts +10 -0
- package/src/styles/dashboard.css +41 -0
- package/src/styles/index.css +20 -0
- package/src/styles/sources.css +6 -0
- package/src/types/index.ts +1 -0
- package/src/types/pageConfig.ts +103 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +57 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recent Payments Component
|
|
3
|
+
* Display recent payment transactions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Card,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
Button,
|
|
15
|
+
Badge,
|
|
16
|
+
Skeleton,
|
|
17
|
+
} from '@djangocfg/ui';
|
|
18
|
+
import { History, ExternalLink } from 'lucide-react';
|
|
19
|
+
import { useOverviewContext } from '@djangocfg/api/cfg/contexts';
|
|
20
|
+
import { openPaymentDetailsDialog } from '../../../events';
|
|
21
|
+
|
|
22
|
+
export const RecentPayments: React.FC = () => {
|
|
23
|
+
const { recentPayments, isLoadingOverview } = useOverviewContext();
|
|
24
|
+
|
|
25
|
+
const formatCurrency = (amount?: number | null) => {
|
|
26
|
+
if (amount === null || amount === undefined) return '$0.00';
|
|
27
|
+
return new Intl.NumberFormat('en-US', {
|
|
28
|
+
style: 'currency',
|
|
29
|
+
currency: 'USD',
|
|
30
|
+
minimumFractionDigits: 2,
|
|
31
|
+
}).format(amount);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getRelativeTime = (date: string | null | undefined): string => {
|
|
35
|
+
if (!date) return 'N/A';
|
|
36
|
+
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const target = new Date(date);
|
|
39
|
+
const diffInSeconds = Math.floor((now.getTime() - target.getTime()) / 1000);
|
|
40
|
+
|
|
41
|
+
if (diffInSeconds < 60) return 'Just now';
|
|
42
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
|
|
43
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
44
|
+
return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getStatusVariant = (
|
|
48
|
+
status: string | null | undefined
|
|
49
|
+
): 'default' | 'destructive' | 'outline' | 'secondary' => {
|
|
50
|
+
switch (status?.toLowerCase()) {
|
|
51
|
+
case 'completed':
|
|
52
|
+
case 'success':
|
|
53
|
+
return 'default';
|
|
54
|
+
case 'pending':
|
|
55
|
+
return 'secondary';
|
|
56
|
+
case 'failed':
|
|
57
|
+
case 'error':
|
|
58
|
+
return 'destructive';
|
|
59
|
+
default:
|
|
60
|
+
return 'outline';
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (isLoadingOverview) {
|
|
65
|
+
return (
|
|
66
|
+
<Card>
|
|
67
|
+
<CardHeader>
|
|
68
|
+
<CardTitle className="flex items-center gap-2">
|
|
69
|
+
<History className="h-5 w-5" />
|
|
70
|
+
Recent Payments
|
|
71
|
+
</CardTitle>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardContent className="space-y-3">
|
|
74
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
75
|
+
<div key={i} className="flex items-center justify-between p-3 border rounded-lg">
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<Skeleton className="h-4 w-32" />
|
|
78
|
+
<Skeleton className="h-3 w-24" />
|
|
79
|
+
</div>
|
|
80
|
+
<Skeleton className="h-6 w-16" />
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const payments = recentPayments?.results || [];
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Card>
|
|
92
|
+
<CardHeader>
|
|
93
|
+
<CardTitle className="flex items-center justify-between">
|
|
94
|
+
<div className="flex items-center gap-2">
|
|
95
|
+
<History className="h-5 w-5" />
|
|
96
|
+
Recent Payments
|
|
97
|
+
</div>
|
|
98
|
+
<Button variant="ghost" size="sm">
|
|
99
|
+
View All
|
|
100
|
+
<ExternalLink className="h-4 w-4 ml-2" />
|
|
101
|
+
</Button>
|
|
102
|
+
</CardTitle>
|
|
103
|
+
</CardHeader>
|
|
104
|
+
<CardContent>
|
|
105
|
+
{payments.length === 0 ? (
|
|
106
|
+
<div className="text-center py-8 text-muted-foreground">
|
|
107
|
+
<History className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
108
|
+
<p>No recent payments</p>
|
|
109
|
+
</div>
|
|
110
|
+
) : (
|
|
111
|
+
<div className="space-y-3">
|
|
112
|
+
{payments.map((payment) => (
|
|
113
|
+
<div
|
|
114
|
+
key={payment.id}
|
|
115
|
+
className="flex items-center justify-between p-3 border rounded-lg hover:bg-accent cursor-pointer transition-colors"
|
|
116
|
+
onClick={() => openPaymentDetailsDialog(payment.id)}
|
|
117
|
+
>
|
|
118
|
+
<div className="flex-1">
|
|
119
|
+
<div className="flex items-center gap-2">
|
|
120
|
+
<span className="font-medium">{formatCurrency(payment.amount_usd)}</span>
|
|
121
|
+
<Badge variant={getStatusVariant(payment.status)} className="text-xs">
|
|
122
|
+
{payment.status}
|
|
123
|
+
</Badge>
|
|
124
|
+
</div>
|
|
125
|
+
<p className="text-sm text-muted-foreground">
|
|
126
|
+
{getRelativeTime(payment.created_at)} • {payment.provider || 'Unknown provider'}
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overview View
|
|
3
|
+
* Dashboard with metrics, balance, and recent activity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { MetricsCards, BalanceCard, RecentPayments } from './components';
|
|
10
|
+
|
|
11
|
+
export const OverviewView: React.FC = () => {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-6">
|
|
14
|
+
<MetricsCards />
|
|
15
|
+
|
|
16
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
17
|
+
<BalanceCard />
|
|
18
|
+
<RecentPayments />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payments List Component
|
|
3
|
+
* Display paginated list of payments with filters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Card,
|
|
11
|
+
CardContent,
|
|
12
|
+
CardHeader,
|
|
13
|
+
CardTitle,
|
|
14
|
+
Table,
|
|
15
|
+
TableBody,
|
|
16
|
+
TableCell,
|
|
17
|
+
TableHead,
|
|
18
|
+
TableHeader,
|
|
19
|
+
TableRow,
|
|
20
|
+
Button,
|
|
21
|
+
Badge,
|
|
22
|
+
Input,
|
|
23
|
+
Select,
|
|
24
|
+
SelectContent,
|
|
25
|
+
SelectItem,
|
|
26
|
+
SelectTrigger,
|
|
27
|
+
SelectValue,
|
|
28
|
+
Skeleton,
|
|
29
|
+
} from '@djangocfg/ui';
|
|
30
|
+
import { Plus, Search, Filter, ChevronLeft, ChevronRight, RefreshCw, ExternalLink } from 'lucide-react';
|
|
31
|
+
import { usePaymentsContext } from '@djangocfg/api/cfg/contexts';
|
|
32
|
+
import { openCreatePaymentDialog, openPaymentDetailsDialog } from '../../../events';
|
|
33
|
+
|
|
34
|
+
export const PaymentsList: React.FC = () => {
|
|
35
|
+
const {
|
|
36
|
+
payments,
|
|
37
|
+
isLoadingPayments,
|
|
38
|
+
refreshPayments,
|
|
39
|
+
} = usePaymentsContext();
|
|
40
|
+
|
|
41
|
+
const paymentsList = payments?.results || [];
|
|
42
|
+
const paymentsPage = payments?.page || 1;
|
|
43
|
+
const paymentsPageSize = payments?.page_size || 20;
|
|
44
|
+
const paymentsTotalCount = payments?.count || 0;
|
|
45
|
+
|
|
46
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
47
|
+
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
48
|
+
|
|
49
|
+
const formatCurrency = (amount?: number | null) => {
|
|
50
|
+
if (amount === null || amount === undefined) return '$0.00';
|
|
51
|
+
return new Intl.NumberFormat('en-US', {
|
|
52
|
+
style: 'currency',
|
|
53
|
+
currency: 'USD',
|
|
54
|
+
minimumFractionDigits: 2,
|
|
55
|
+
}).format(amount);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getRelativeTime = (date: string | null | undefined): string => {
|
|
59
|
+
if (!date) return 'N/A';
|
|
60
|
+
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const target = new Date(date);
|
|
63
|
+
const diffInSeconds = Math.floor((now.getTime() - target.getTime()) / 1000);
|
|
64
|
+
|
|
65
|
+
if (diffInSeconds < 60) return 'Just now';
|
|
66
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
|
|
67
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
68
|
+
return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getStatusVariant = (
|
|
72
|
+
status: string | null | undefined
|
|
73
|
+
): 'default' | 'destructive' | 'outline' | 'secondary' => {
|
|
74
|
+
switch (status?.toLowerCase()) {
|
|
75
|
+
case 'completed':
|
|
76
|
+
case 'success':
|
|
77
|
+
return 'default';
|
|
78
|
+
case 'pending':
|
|
79
|
+
return 'secondary';
|
|
80
|
+
case 'failed':
|
|
81
|
+
case 'error':
|
|
82
|
+
return 'destructive';
|
|
83
|
+
default:
|
|
84
|
+
return 'outline';
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleSearch = async (value: string) => {
|
|
89
|
+
setSearchTerm(value);
|
|
90
|
+
// TODO: Implement search/filter in PaymentsContext
|
|
91
|
+
await refreshPayments();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleStatusFilter = async (status: string) => {
|
|
95
|
+
setStatusFilter(status);
|
|
96
|
+
// TODO: Implement search/filter in PaymentsContext
|
|
97
|
+
await refreshPayments();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handlePageChange = async (page: number) => {
|
|
101
|
+
// TODO: Implement pagination in PaymentsContext
|
|
102
|
+
await refreshPayments();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const totalPages = Math.ceil((paymentsTotalCount || 0) / (paymentsPageSize || 20));
|
|
106
|
+
const showingFrom = ((paymentsPage || 1) - 1) * (paymentsPageSize || 20) + 1;
|
|
107
|
+
const showingTo = Math.min((paymentsPage || 1) * (paymentsPageSize || 20), paymentsTotalCount || 0);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Card>
|
|
111
|
+
<CardHeader>
|
|
112
|
+
<CardTitle className="flex items-center justify-between">
|
|
113
|
+
<span>Payment History</span>
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<Button variant="outline" size="sm" onClick={refreshPayments} disabled={isLoadingPayments}>
|
|
116
|
+
<RefreshCw className={`h-4 w-4 mr-2 ${isLoadingPayments ? 'animate-spin' : ''}`} />
|
|
117
|
+
Refresh
|
|
118
|
+
</Button>
|
|
119
|
+
<Button size="sm" onClick={() => openCreatePaymentDialog()}>
|
|
120
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
121
|
+
New Payment
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
</CardTitle>
|
|
125
|
+
</CardHeader>
|
|
126
|
+
|
|
127
|
+
<CardContent className="space-y-4">
|
|
128
|
+
{/* Filters */}
|
|
129
|
+
<div className="flex flex-col sm:flex-row gap-4">
|
|
130
|
+
<div className="relative flex-1">
|
|
131
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
132
|
+
<Input
|
|
133
|
+
placeholder="Search payments..."
|
|
134
|
+
value={searchTerm}
|
|
135
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
136
|
+
className="pl-10"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<Select value={statusFilter} onValueChange={handleStatusFilter}>
|
|
141
|
+
<SelectTrigger className="w-full sm:w-48">
|
|
142
|
+
<Filter className="h-4 w-4 mr-2" />
|
|
143
|
+
<SelectValue placeholder="Filter by status" />
|
|
144
|
+
</SelectTrigger>
|
|
145
|
+
<SelectContent>
|
|
146
|
+
<SelectItem value="all">All Statuses</SelectItem>
|
|
147
|
+
<SelectItem value="completed">Completed</SelectItem>
|
|
148
|
+
<SelectItem value="pending">Pending</SelectItem>
|
|
149
|
+
<SelectItem value="failed">Failed</SelectItem>
|
|
150
|
+
</SelectContent>
|
|
151
|
+
</Select>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Payments Table */}
|
|
155
|
+
{isLoadingPayments ? (
|
|
156
|
+
<div className="space-y-3">
|
|
157
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
158
|
+
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
|
|
159
|
+
<div className="space-y-2">
|
|
160
|
+
<Skeleton className="h-4 w-32" />
|
|
161
|
+
<Skeleton className="h-3 w-24" />
|
|
162
|
+
</div>
|
|
163
|
+
<Skeleton className="h-6 w-16" />
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
) : !paymentsList || paymentsList.length === 0 ? (
|
|
168
|
+
<div className="text-center py-12">
|
|
169
|
+
<div className="w-16 h-16 mx-auto mb-4 bg-muted rounded-full flex items-center justify-center">
|
|
170
|
+
<Search className="w-8 h-8 text-muted-foreground" />
|
|
171
|
+
</div>
|
|
172
|
+
<h3 className="text-lg font-semibold mb-2">No Payments Found</h3>
|
|
173
|
+
<p className="text-muted-foreground mb-4">
|
|
174
|
+
{searchTerm || statusFilter !== 'all'
|
|
175
|
+
? 'No payments match your current filters'
|
|
176
|
+
: "You haven't made any payments yet"}
|
|
177
|
+
</p>
|
|
178
|
+
<Button onClick={() => openCreatePaymentDialog()}>
|
|
179
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
180
|
+
Create Payment
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
) : (
|
|
184
|
+
<>
|
|
185
|
+
<div className="rounded-md border">
|
|
186
|
+
<Table>
|
|
187
|
+
<TableHeader>
|
|
188
|
+
<TableRow>
|
|
189
|
+
<TableHead>Date</TableHead>
|
|
190
|
+
<TableHead>Amount</TableHead>
|
|
191
|
+
<TableHead>Currency</TableHead>
|
|
192
|
+
<TableHead>Status</TableHead>
|
|
193
|
+
<TableHead>Provider</TableHead>
|
|
194
|
+
<TableHead>ID</TableHead>
|
|
195
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
196
|
+
</TableRow>
|
|
197
|
+
</TableHeader>
|
|
198
|
+
<TableBody>
|
|
199
|
+
{paymentsList.map((payment) => (
|
|
200
|
+
<TableRow key={payment.id} className="cursor-pointer hover:bg-accent">
|
|
201
|
+
<TableCell>
|
|
202
|
+
<div>
|
|
203
|
+
<div className="font-medium">
|
|
204
|
+
{payment.created_at
|
|
205
|
+
? new Date(payment.created_at).toLocaleDateString()
|
|
206
|
+
: 'N/A'}
|
|
207
|
+
</div>
|
|
208
|
+
<div className="text-sm text-muted-foreground">
|
|
209
|
+
{getRelativeTime(payment.created_at)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</TableCell>
|
|
213
|
+
<TableCell className="font-mono font-semibold">
|
|
214
|
+
{formatCurrency(payment.amount_usd)}
|
|
215
|
+
</TableCell>
|
|
216
|
+
<TableCell>
|
|
217
|
+
<Badge variant="outline">Currency #{payment.currency}</Badge>
|
|
218
|
+
</TableCell>
|
|
219
|
+
<TableCell>
|
|
220
|
+
<Badge variant={getStatusVariant(payment.status)}>{payment.status}</Badge>
|
|
221
|
+
</TableCell>
|
|
222
|
+
<TableCell>{payment.provider || 'N/A'}</TableCell>
|
|
223
|
+
<TableCell className="font-mono text-sm text-muted-foreground">
|
|
224
|
+
{payment.id.slice(0, 8)}...
|
|
225
|
+
</TableCell>
|
|
226
|
+
<TableCell className="text-right">
|
|
227
|
+
<Button
|
|
228
|
+
variant="ghost"
|
|
229
|
+
size="sm"
|
|
230
|
+
onClick={(e) => {
|
|
231
|
+
e.stopPropagation();
|
|
232
|
+
openPaymentDetailsDialog(payment.id);
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
<ExternalLink className="h-4 w-4" />
|
|
236
|
+
</Button>
|
|
237
|
+
</TableCell>
|
|
238
|
+
</TableRow>
|
|
239
|
+
))}
|
|
240
|
+
</TableBody>
|
|
241
|
+
</Table>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{/* Pagination */}
|
|
245
|
+
{totalPages > 1 && (
|
|
246
|
+
<div className="flex items-center justify-between">
|
|
247
|
+
<div className="text-sm text-muted-foreground">
|
|
248
|
+
Showing {showingFrom} to {showingTo} of {paymentsTotalCount} payments
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<div className="flex items-center gap-2">
|
|
252
|
+
<Button
|
|
253
|
+
variant="outline"
|
|
254
|
+
size="sm"
|
|
255
|
+
onClick={() => handlePageChange((paymentsPage || 1) - 1)}
|
|
256
|
+
disabled={!paymentsPage || paymentsPage <= 1}
|
|
257
|
+
>
|
|
258
|
+
<ChevronLeft className="h-4 w-4" />
|
|
259
|
+
</Button>
|
|
260
|
+
|
|
261
|
+
<span className="text-sm">
|
|
262
|
+
Page {paymentsPage || 1} of {totalPages}
|
|
263
|
+
</span>
|
|
264
|
+
|
|
265
|
+
<Button
|
|
266
|
+
variant="outline"
|
|
267
|
+
size="sm"
|
|
268
|
+
onClick={() => handlePageChange((paymentsPage || 1) + 1)}
|
|
269
|
+
disabled={!paymentsPage || paymentsPage >= totalPages}
|
|
270
|
+
>
|
|
271
|
+
<ChevronRight className="h-4 w-4" />
|
|
272
|
+
</Button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</>
|
|
277
|
+
)}
|
|
278
|
+
</CardContent>
|
|
279
|
+
</Card>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payments View
|
|
3
|
+
* List and manage payment transactions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { PaymentsList } from './components';
|
|
10
|
+
|
|
11
|
+
export const PaymentsView: React.FC = () => {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-6">
|
|
14
|
+
<PaymentsList />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tariffs View
|
|
3
|
+
* View and manage tariff plans
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { Card, CardContent } from '@djangocfg/ui';
|
|
10
|
+
import { Crown } from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
export const TariffsView: React.FC = () => {
|
|
13
|
+
return (
|
|
14
|
+
<Card>
|
|
15
|
+
<CardContent className="py-12">
|
|
16
|
+
<div className="text-center">
|
|
17
|
+
<div className="w-16 h-16 mx-auto mb-4 bg-muted rounded-full flex items-center justify-center">
|
|
18
|
+
<Crown className="w-8 h-8 text-muted-foreground" />
|
|
19
|
+
</div>
|
|
20
|
+
<h3 className="text-lg font-semibold mb-2">Tariff Plans Coming Soon</h3>
|
|
21
|
+
<p className="text-muted-foreground">
|
|
22
|
+
Subscription tiers and pricing plans will be available here
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</CardContent>
|
|
26
|
+
</Card>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transactions View
|
|
3
|
+
* View transaction history and balance changes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { Card, CardContent } from '@djangocfg/ui';
|
|
10
|
+
import { History } from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
export const TransactionsView: React.FC = () => {
|
|
13
|
+
return (
|
|
14
|
+
<Card>
|
|
15
|
+
<CardContent className="py-12">
|
|
16
|
+
<div className="text-center">
|
|
17
|
+
<div className="w-16 h-16 mx-auto mb-4 bg-muted rounded-full flex items-center justify-center">
|
|
18
|
+
<History className="w-8 h-8 text-muted-foreground" />
|
|
19
|
+
</div>
|
|
20
|
+
<h3 className="text-lg font-semibold mb-2">Transactions Coming Soon</h3>
|
|
21
|
+
<p className="text-muted-foreground">
|
|
22
|
+
Transaction history and balance changes will be available here
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</CardContent>
|
|
26
|
+
</Card>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@djangocfg/ui/components';
|
|
12
|
+
import { AccountsProvider } from '@djangocfg/api/cfg/contexts';
|
|
13
|
+
import { useAuth } from '../../auth';
|
|
14
|
+
|
|
15
|
+
import { AvatarSection, ProfileForm } from './components';
|
|
16
|
+
|
|
17
|
+
interface ProfileLayoutProps {
|
|
18
|
+
// Callbacks
|
|
19
|
+
onUnauthenticated?: () => void;
|
|
20
|
+
|
|
21
|
+
// Optional customization
|
|
22
|
+
title?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
showMemberSince?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ProfileContent = ({
|
|
28
|
+
onUnauthenticated,
|
|
29
|
+
title = 'Profile Settings',
|
|
30
|
+
description = 'Manage your account information and preferences',
|
|
31
|
+
showMemberSince = true
|
|
32
|
+
}: ProfileLayoutProps) => {
|
|
33
|
+
const { user, isLoading } = useAuth();
|
|
34
|
+
|
|
35
|
+
const formatDate = (dateString: string) => {
|
|
36
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
37
|
+
year: 'numeric',
|
|
38
|
+
month: 'long',
|
|
39
|
+
day: 'numeric',
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Show auth check if no user
|
|
44
|
+
if (!user && !isLoading) {
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (onUnauthenticated) {
|
|
47
|
+
onUnauthenticated();
|
|
48
|
+
}
|
|
49
|
+
}, [onUnauthenticated]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
53
|
+
<div className="text-center">
|
|
54
|
+
<h1 className="text-2xl font-bold text-foreground mb-4">Not Authenticated</h1>
|
|
55
|
+
<p className="text-muted-foreground mb-4">Please log in to view your profile.</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isLoading) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
64
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="container mx-auto px-4 py-8">
|
|
71
|
+
<div className="max-w-2xl mx-auto space-y-6">
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<div className="text-center space-y-2">
|
|
74
|
+
<h1 className="text-3xl font-bold text-foreground">{title}</h1>
|
|
75
|
+
<p className="text-muted-foreground">{description}</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Main Profile Card */}
|
|
79
|
+
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
|
|
80
|
+
<CardHeader className="text-center pb-6">
|
|
81
|
+
<AvatarSection />
|
|
82
|
+
|
|
83
|
+
<CardTitle className="text-xl">
|
|
84
|
+
{user?.display_username || user?.email}
|
|
85
|
+
</CardTitle>
|
|
86
|
+
{showMemberSince && user?.date_joined && (
|
|
87
|
+
<CardDescription className="text-muted-foreground">
|
|
88
|
+
Member since {formatDate(user.date_joined)}
|
|
89
|
+
</CardDescription>
|
|
90
|
+
)}
|
|
91
|
+
</CardHeader>
|
|
92
|
+
|
|
93
|
+
<CardContent className="space-y-6">
|
|
94
|
+
{/* Profile Form */}
|
|
95
|
+
<ProfileForm />
|
|
96
|
+
</CardContent>
|
|
97
|
+
</Card>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const ProfileLayout: React.FC<ProfileLayoutProps> = (props) => {
|
|
104
|
+
return (
|
|
105
|
+
<AccountsProvider>
|
|
106
|
+
<ProfileContent {...props} />
|
|
107
|
+
</AccountsProvider>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|