@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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/package.json +86 -0
  4. package/src/auth/README.md +962 -0
  5. package/src/auth/context/AuthContext.tsx +458 -0
  6. package/src/auth/context/index.ts +2 -0
  7. package/src/auth/context/types.ts +63 -0
  8. package/src/auth/hooks/index.ts +6 -0
  9. package/src/auth/hooks/useAuthForm.ts +329 -0
  10. package/src/auth/hooks/useAuthGuard.ts +23 -0
  11. package/src/auth/hooks/useAuthRedirect.ts +51 -0
  12. package/src/auth/hooks/useAutoAuth.ts +42 -0
  13. package/src/auth/hooks/useLocalStorage.ts +211 -0
  14. package/src/auth/hooks/useSessionStorage.ts +186 -0
  15. package/src/auth/index.ts +10 -0
  16. package/src/auth/middlewares/index.ts +1 -0
  17. package/src/auth/middlewares/proxy.ts +24 -0
  18. package/src/auth/server.ts +6 -0
  19. package/src/auth/utils/errors.ts +34 -0
  20. package/src/auth/utils/index.ts +2 -0
  21. package/src/auth/utils/validation.ts +14 -0
  22. package/src/index.ts +15 -0
  23. package/src/layouts/AppLayout/AppLayout.tsx +123 -0
  24. package/src/layouts/AppLayout/README.md +204 -0
  25. package/src/layouts/AppLayout/SUMMARY.md +240 -0
  26. package/src/layouts/AppLayout/USAGE.md +312 -0
  27. package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
  28. package/src/layouts/AppLayout/components/Seo.tsx +87 -0
  29. package/src/layouts/AppLayout/components/index.ts +6 -0
  30. package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
  31. package/src/layouts/AppLayout/context/index.ts +5 -0
  32. package/src/layouts/AppLayout/hooks/index.ts +6 -0
  33. package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
  34. package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
  35. package/src/layouts/AppLayout/index.ts +31 -0
  36. package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
  37. package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
  38. package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
  39. package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
  40. package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
  41. package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
  42. package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
  43. package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
  44. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
  45. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
  46. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
  47. package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
  48. package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
  49. package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
  50. package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
  51. package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
  52. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
  53. package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
  54. package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
  55. package/src/layouts/AppLayout/layouts/index.ts +7 -0
  56. package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
  57. package/src/layouts/AppLayout/providers/index.ts +5 -0
  58. package/src/layouts/AppLayout/types/config.ts +40 -0
  59. package/src/layouts/AppLayout/types/index.ts +10 -0
  60. package/src/layouts/AppLayout/types/layout.ts +47 -0
  61. package/src/layouts/AppLayout/types/navigation.ts +41 -0
  62. package/src/layouts/AppLayout/types/routes.ts +45 -0
  63. package/src/layouts/AppLayout/utils/index.ts +5 -0
  64. package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
  65. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
  66. package/src/layouts/PaymentsLayout/README.md +133 -0
  67. package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
  68. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
  69. package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
  70. package/src/layouts/PaymentsLayout/components/index.ts +4 -0
  71. package/src/layouts/PaymentsLayout/events.ts +106 -0
  72. package/src/layouts/PaymentsLayout/index.ts +20 -0
  73. package/src/layouts/PaymentsLayout/types.ts +19 -0
  74. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
  75. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
  76. package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
  77. package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
  78. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
  79. package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
  80. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
  81. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
  82. package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
  83. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
  84. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
  85. package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
  86. package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
  87. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
  88. package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
  89. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
  90. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
  91. package/src/layouts/ProfileLayout/components/index.ts +3 -0
  92. package/src/layouts/ProfileLayout/index.ts +3 -0
  93. package/src/layouts/SupportLayout/README.md +91 -0
  94. package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
  95. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
  96. package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
  97. package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
  98. package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
  99. package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
  100. package/src/layouts/SupportLayout/components/index.ts +6 -0
  101. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
  102. package/src/layouts/SupportLayout/context/index.ts +2 -0
  103. package/src/layouts/SupportLayout/events.ts +31 -0
  104. package/src/layouts/SupportLayout/hooks/index.ts +2 -0
  105. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
  106. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
  107. package/src/layouts/SupportLayout/index.ts +6 -0
  108. package/src/layouts/SupportLayout/types.ts +23 -0
  109. package/src/layouts/index.ts +9 -0
  110. package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
  111. package/src/snippets/AuthDialog/events.ts +21 -0
  112. package/src/snippets/AuthDialog/index.ts +3 -0
  113. package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
  114. package/src/snippets/Breadcrumbs.tsx +80 -0
  115. package/src/snippets/Chat/ChatUIContext.tsx +110 -0
  116. package/src/snippets/Chat/ChatWidget.tsx +476 -0
  117. package/src/snippets/Chat/README.md +122 -0
  118. package/src/snippets/Chat/components/MessageInput.tsx +124 -0
  119. package/src/snippets/Chat/components/MessageList.tsx +168 -0
  120. package/src/snippets/Chat/components/SessionList.tsx +192 -0
  121. package/src/snippets/Chat/components/index.ts +9 -0
  122. package/src/snippets/Chat/hooks/index.ts +6 -0
  123. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
  124. package/src/snippets/Chat/index.tsx +44 -0
  125. package/src/snippets/Chat/types.ts +79 -0
  126. package/src/snippets/VideoPlayer/README.md +203 -0
  127. package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
  128. package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
  129. package/src/snippets/VideoPlayer/index.ts +8 -0
  130. package/src/snippets/VideoPlayer/types.ts +61 -0
  131. package/src/snippets/index.ts +10 -0
  132. package/src/styles/dashboard.css +41 -0
  133. package/src/styles/index.css +20 -0
  134. package/src/styles/sources.css +6 -0
  135. package/src/types/index.ts +1 -0
  136. package/src/types/pageConfig.ts +103 -0
  137. package/src/utils/index.ts +6 -0
  138. package/src/utils/logger.ts +57 -0
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Payments Layout Events
3
+ * Event-based communication for dialogs and actions
4
+ */
5
+
6
+ import { events } from '@djangocfg/ui';
7
+ import type { APIKeyDetail, Payment } from './types';
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────
10
+ // Dialog Events
11
+ // ─────────────────────────────────────────────────────────────────────────
12
+
13
+ export const PAYMENTS_DIALOG_EVENTS = {
14
+ // API Keys
15
+ OPEN_CREATE_APIKEY_DIALOG: 'OPEN_CREATE_APIKEY_DIALOG',
16
+ OPEN_EDIT_APIKEY_DIALOG: 'OPEN_EDIT_APIKEY_DIALOG',
17
+ OPEN_DELETE_APIKEY_DIALOG: 'OPEN_DELETE_APIKEY_DIALOG',
18
+
19
+ // Payments
20
+ OPEN_CREATE_PAYMENT_DIALOG: 'OPEN_CREATE_PAYMENT_DIALOG',
21
+ OPEN_PAYMENT_DETAILS_DIALOG: 'OPEN_PAYMENT_DETAILS_DIALOG',
22
+ OPEN_CANCEL_PAYMENT_DIALOG: 'OPEN_CANCEL_PAYMENT_DIALOG',
23
+
24
+ // Close
25
+ CLOSE_PAYMENTS_DIALOG: 'CLOSE_PAYMENTS_DIALOG',
26
+ } as const;
27
+
28
+ // ─────────────────────────────────────────────────────────────────────────
29
+ // Event Types
30
+ // ─────────────────────────────────────────────────────────────────────────
31
+
32
+ export interface PaymentsDialogEvent {
33
+ type: typeof PAYMENTS_DIALOG_EVENTS[keyof typeof PAYMENTS_DIALOG_EVENTS];
34
+ payload?: {
35
+ // API Keys
36
+ keyId?: string;
37
+ keyData?: APIKeyDetail;
38
+ initialKeyData?: Partial<APIKeyDetail>;
39
+
40
+ // Payments
41
+ paymentId?: string;
42
+ paymentData?: Payment;
43
+ initialPaymentData?: Partial<Payment>;
44
+ };
45
+ }
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────
48
+ // API Keys Events
49
+ // ─────────────────────────────────────────────────────────────────────────
50
+
51
+ export const openCreateApiKeyDialog = (initialData?: Partial<APIKeyDetail>) => {
52
+ events.publish({
53
+ type: PAYMENTS_DIALOG_EVENTS.OPEN_CREATE_APIKEY_DIALOG,
54
+ payload: { initialKeyData: initialData },
55
+ });
56
+ };
57
+
58
+ export const openEditApiKeyDialog = (keyId: string, keyData?: APIKeyDetail) => {
59
+ events.publish({
60
+ type: PAYMENTS_DIALOG_EVENTS.OPEN_EDIT_APIKEY_DIALOG,
61
+ payload: { keyId, keyData },
62
+ });
63
+ };
64
+
65
+ export const openDeleteApiKeyDialog = (keyId: string, keyData?: APIKeyDetail) => {
66
+ events.publish({
67
+ type: PAYMENTS_DIALOG_EVENTS.OPEN_DELETE_APIKEY_DIALOG,
68
+ payload: { keyId, keyData },
69
+ });
70
+ };
71
+
72
+ // ─────────────────────────────────────────────────────────────────────────
73
+ // Payments Events
74
+ // ─────────────────────────────────────────────────────────────────────────
75
+
76
+ export const openCreatePaymentDialog = (initialData?: Partial<Payment>) => {
77
+ events.publish({
78
+ type: PAYMENTS_DIALOG_EVENTS.OPEN_CREATE_PAYMENT_DIALOG,
79
+ payload: { initialPaymentData: initialData },
80
+ });
81
+ };
82
+
83
+ export const openPaymentDetailsDialog = (paymentId: string, paymentData?: Payment) => {
84
+ events.publish({
85
+ type: PAYMENTS_DIALOG_EVENTS.OPEN_PAYMENT_DETAILS_DIALOG,
86
+ payload: { paymentId, paymentData },
87
+ });
88
+ };
89
+
90
+ export const openCancelPaymentDialog = (paymentId: string, paymentData?: Payment) => {
91
+ events.publish({
92
+ type: PAYMENTS_DIALOG_EVENTS.OPEN_CANCEL_PAYMENT_DIALOG,
93
+ payload: { paymentId, paymentData },
94
+ });
95
+ };
96
+
97
+ // ─────────────────────────────────────────────────────────────────────────
98
+ // Close Events
99
+ // ─────────────────────────────────────────────────────────────────────────
100
+
101
+ export const closePaymentsDialog = () => {
102
+ events.publish({
103
+ type: PAYMENTS_DIALOG_EVENTS.CLOSE_PAYMENTS_DIALOG,
104
+ });
105
+ };
106
+
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Payments Layout
3
+ * Export main layout and types
4
+ */
5
+
6
+ export { PaymentsLayout } from './PaymentsLayout';
7
+ export type { PaymentsLayoutProps } from './PaymentsLayout';
8
+ export type { PaymentTab } from './types';
9
+
10
+ // Re-export events for convenience
11
+ export {
12
+ openCreateApiKeyDialog,
13
+ openEditApiKeyDialog,
14
+ openDeleteApiKeyDialog,
15
+ openCreatePaymentDialog,
16
+ openPaymentDetailsDialog,
17
+ openCancelPaymentDialog,
18
+ closePaymentsDialog,
19
+ } from './events';
20
+
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Payments Layout Types
3
+ */
4
+
5
+ // Tab types for navigation
6
+ export type PaymentTab = 'overview' | 'payments' | 'transactions' | 'apikeys' | 'tariffs';
7
+
8
+ // Re-export API types for convenience
9
+ export type {
10
+ Payment,
11
+ UserBalance,
12
+ Currency,
13
+ APIKeyDetail,
14
+ PaginatedPaymentListList,
15
+ PaginatedUserBalanceList,
16
+ PaginatedCurrencyListList,
17
+ PaginatedAPIKeyListList,
18
+ } from '@djangocfg/api/cfg/contexts';
19
+
@@ -0,0 +1,109 @@
1
+ /**
2
+ * API Key Metrics Component
3
+ * Display API key usage statistics
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ CardHeader,
13
+ CardTitle,
14
+ Skeleton,
15
+ } from '@djangocfg/ui';
16
+ import { Key, CheckCircle2, XCircle, Activity } from 'lucide-react';
17
+ import { useApiKeysContext } from '@djangocfg/api/cfg/contexts';
18
+
19
+ export const ApiKeyMetrics: React.FC = () => {
20
+ const { apiKeys, isLoadingApiKeys } = useApiKeysContext();
21
+
22
+ const keysList = apiKeys?.results || [];
23
+
24
+ const metrics = React.useMemo(() => {
25
+ const total = keysList.length;
26
+ const active = keysList.filter(key => key.is_active).length;
27
+ const inactive = total - active;
28
+ const recentlyUsed = keysList.filter(key => {
29
+ if (!key.last_used_at) return false;
30
+ const lastUsed = new Date(key.last_used_at);
31
+ const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
32
+ return lastUsed > dayAgo;
33
+ }).length;
34
+
35
+ return { total, active, inactive, recentlyUsed };
36
+ }, [keysList]);
37
+
38
+ if (isLoadingApiKeys) {
39
+ return (
40
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
41
+ {Array.from({ length: 4 }).map((_, i) => (
42
+ <Card key={i}>
43
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
44
+ <Skeleton className="h-4 w-24" />
45
+ <Skeleton className="h-4 w-4 rounded-full" />
46
+ </CardHeader>
47
+ <CardContent>
48
+ <Skeleton className="h-7 w-12 mb-1" />
49
+ <Skeleton className="h-3 w-32" />
50
+ </CardContent>
51
+ </Card>
52
+ ))}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ const metricsData = [
58
+ {
59
+ title: 'Total Keys',
60
+ value: metrics.total.toString(),
61
+ description: 'All API keys',
62
+ icon: Key,
63
+ },
64
+ {
65
+ title: 'Active Keys',
66
+ value: metrics.active.toString(),
67
+ description: 'Currently active',
68
+ icon: CheckCircle2,
69
+ iconColor: 'text-green-600',
70
+ },
71
+ {
72
+ title: 'Inactive Keys',
73
+ value: metrics.inactive.toString(),
74
+ description: 'Not in use',
75
+ icon: XCircle,
76
+ iconColor: 'text-muted-foreground',
77
+ },
78
+ {
79
+ title: 'Recently Used',
80
+ value: metrics.recentlyUsed.toString(),
81
+ description: 'Last 24 hours',
82
+ icon: Activity,
83
+ iconColor: 'text-blue-600',
84
+ },
85
+ ];
86
+
87
+ return (
88
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
89
+ {metricsData.map((metric, index) => {
90
+ const Icon = metric.icon;
91
+ return (
92
+ <Card key={index}>
93
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
94
+ <CardTitle className="text-sm font-medium">{metric.title}</CardTitle>
95
+ <Icon className={`h-4 w-4 ${metric.iconColor || 'text-muted-foreground'}`} />
96
+ </CardHeader>
97
+ <CardContent>
98
+ <div className="text-2xl font-bold">{metric.value}</div>
99
+ <p className="text-xs text-muted-foreground mt-1">
100
+ {metric.description}
101
+ </p>
102
+ </CardContent>
103
+ </Card>
104
+ );
105
+ })}
106
+ </div>
107
+ );
108
+ };
109
+
@@ -0,0 +1,194 @@
1
+ /**
2
+ * API Keys List Component
3
+ * Display and manage API keys
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
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuItem,
19
+ DropdownMenuTrigger,
20
+ Skeleton,
21
+ useCopy,
22
+ } from '@djangocfg/ui';
23
+ import { Plus, MoreHorizontal, Edit, Trash2, Key, Copy, CheckCircle2 } from 'lucide-react';
24
+ import { useApiKeysContext } from '@djangocfg/api/cfg/contexts';
25
+ import { paymentsLogger } from '../../../../../utils/logger';
26
+ import { openCreateApiKeyDialog, openEditApiKeyDialog, openDeleteApiKeyDialog } from '../../../events';
27
+
28
+ export const ApiKeysList: React.FC = () => {
29
+ const { apiKeys, isLoadingApiKeys, deleteApiKey } = useApiKeysContext();
30
+ const { copyToClipboard } = useCopy({
31
+ successMessage: 'API key copied to clipboard',
32
+ errorMessage: 'Failed to copy API key',
33
+ });
34
+
35
+ const keysList = apiKeys?.results || [];
36
+
37
+ const getStatusVariant = (isActive?: boolean | null) => {
38
+ return isActive ? 'default' : 'secondary';
39
+ };
40
+
41
+ const getStatusLabel = (isActive?: boolean | null) => {
42
+ return isActive ? 'Active' : 'Inactive';
43
+ };
44
+
45
+ const getRelativeTime = (date: string | null | undefined): string => {
46
+ if (!date) return 'N/A';
47
+
48
+ const now = new Date();
49
+ const target = new Date(date);
50
+ const diffInSeconds = Math.floor((now.getTime() - target.getTime()) / 1000);
51
+
52
+ if (diffInSeconds < 60) return 'Just now';
53
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
54
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
55
+ return `${Math.floor(diffInSeconds / 86400)}d ago`;
56
+ };
57
+
58
+ const formatApiKey = (key: string) => {
59
+ if (!key) return '';
60
+ if (key.length <= 12) return key;
61
+ return `${key.slice(0, 8)}...${key.slice(-4)}`;
62
+ };
63
+
64
+ const handleCopyKey = async (keyValue: string) => {
65
+ await copyToClipboard(keyValue);
66
+ };
67
+
68
+ const handleDelete = async (keyId: string) => {
69
+ try {
70
+ await deleteApiKey(keyId);
71
+ } catch (error) {
72
+ paymentsLogger.error('Failed to delete key:', error);
73
+ }
74
+ };
75
+
76
+ if (isLoadingApiKeys) {
77
+ return (
78
+ <Card>
79
+ <CardHeader>
80
+ <CardTitle className="flex items-center justify-between">
81
+ <span>API Keys</span>
82
+ <Button disabled>
83
+ <Plus className="h-4 w-4 mr-2" />
84
+ Create Key
85
+ </Button>
86
+ </CardTitle>
87
+ </CardHeader>
88
+ <CardContent>
89
+ <div className="space-y-4">
90
+ {Array.from({ length: 3 }).map((_, i) => (
91
+ <div key={i} className="flex items-center justify-between p-4 border rounded-lg">
92
+ <div className="space-y-2 flex-1">
93
+ <Skeleton className="h-4 w-32" />
94
+ <Skeleton className="h-3 w-48" />
95
+ </div>
96
+ <Skeleton className="h-8 w-8 rounded-full" />
97
+ </div>
98
+ ))}
99
+ </div>
100
+ </CardContent>
101
+ </Card>
102
+ );
103
+ }
104
+
105
+ return (
106
+ <Card>
107
+ <CardHeader>
108
+ <CardTitle className="flex items-center justify-between">
109
+ <span>API Keys</span>
110
+ <Button onClick={() => openCreateApiKeyDialog()}>
111
+ <Plus className="h-4 w-4 mr-2" />
112
+ Create Key
113
+ </Button>
114
+ </CardTitle>
115
+ </CardHeader>
116
+ <CardContent>
117
+ {keysList.length === 0 ? (
118
+ <div className="text-center py-12">
119
+ <Key className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
120
+ <h3 className="text-lg font-semibold mb-2">No API Keys</h3>
121
+ <p className="text-muted-foreground mb-4">
122
+ Create your first API key to get started
123
+ </p>
124
+ <Button onClick={() => openCreateApiKeyDialog()}>
125
+ <Plus className="h-4 w-4 mr-2" />
126
+ Create Key
127
+ </Button>
128
+ </div>
129
+ ) : (
130
+ <div className="space-y-4">
131
+ {keysList.map((key) => (
132
+ <div
133
+ key={key.id}
134
+ className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent/50 transition-colors"
135
+ >
136
+ <div className="flex-1 min-w-0">
137
+ <div className="flex items-center gap-3 mb-2">
138
+ <h3 className="font-medium truncate">{key.name || 'Unnamed Key'}</h3>
139
+ <Badge variant={getStatusVariant(key.is_active)}>
140
+ {getStatusLabel(key.is_active)}
141
+ </Badge>
142
+ </div>
143
+
144
+ <div className="space-y-1">
145
+ <div className="flex items-center gap-2">
146
+ <code className="text-sm bg-muted px-2 py-1 rounded font-mono">
147
+ {formatApiKey(key.id)}
148
+ </code>
149
+ <Button
150
+ variant="ghost"
151
+ size="sm"
152
+ onClick={() => handleCopyKey(key.id)}
153
+ >
154
+ <Copy className="h-4 w-4" />
155
+ </Button>
156
+ </div>
157
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
158
+ <span>Created {getRelativeTime(key.created_at)}</span>
159
+ {key.last_used_at && (
160
+ <span>Last used {getRelativeTime(key.last_used_at)}</span>
161
+ )}
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <DropdownMenu>
167
+ <DropdownMenuTrigger asChild>
168
+ <Button variant="ghost" size="sm">
169
+ <MoreHorizontal className="h-4 w-4" />
170
+ </Button>
171
+ </DropdownMenuTrigger>
172
+ <DropdownMenuContent align="end">
173
+ <DropdownMenuItem onClick={() => openEditApiKeyDialog(key.id)}>
174
+ <Edit className="h-4 w-4 mr-2" />
175
+ Edit
176
+ </DropdownMenuItem>
177
+ <DropdownMenuItem
178
+ onClick={() => openDeleteApiKeyDialog(key.id)}
179
+ className="text-destructive"
180
+ >
181
+ <Trash2 className="h-4 w-4 mr-2" />
182
+ Delete
183
+ </DropdownMenuItem>
184
+ </DropdownMenuContent>
185
+ </DropdownMenu>
186
+ </div>
187
+ ))}
188
+ </div>
189
+ )}
190
+ </CardContent>
191
+ </Card>
192
+ );
193
+ };
194
+
@@ -0,0 +1,3 @@
1
+ export { ApiKeysList } from './ApiKeysList';
2
+ export { ApiKeyMetrics } from './ApiKeyMetrics';
3
+
@@ -0,0 +1,19 @@
1
+ /**
2
+ * API Keys View
3
+ * Manage API keys for secure platform access
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import { ApiKeyMetrics, ApiKeysList } from './components';
10
+
11
+ export const ApiKeysView: React.FC = () => {
12
+ return (
13
+ <div className="space-y-6">
14
+ <ApiKeyMetrics />
15
+ <ApiKeysList />
16
+ </div>
17
+ );
18
+ };
19
+
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Balance Card Component
3
+ * Display user balance with quick actions
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 { Wallet, RefreshCw, Plus } from 'lucide-react';
19
+ import { useBalancesContext } from '@djangocfg/api/cfg/contexts';
20
+ import { openCreatePaymentDialog } from '../../../events';
21
+
22
+ export const BalanceCard: React.FC = () => {
23
+ const {
24
+ balanceSummary,
25
+ isLoadingSummary,
26
+ refreshSummary
27
+ } = useBalancesContext();
28
+
29
+ const formatCurrency = (amount?: number | null) => {
30
+ if (amount === null || amount === undefined) return '$0.00';
31
+ return new Intl.NumberFormat('en-US', {
32
+ style: 'currency',
33
+ currency: 'USD',
34
+ minimumFractionDigits: 2,
35
+ }).format(amount);
36
+ };
37
+
38
+ if (isLoadingSummary) {
39
+ return (
40
+ <Card>
41
+ <CardHeader>
42
+ <CardTitle className="flex items-center justify-between">
43
+ <div className="flex items-center gap-2">
44
+ <Wallet className="h-5 w-5" />
45
+ Account Balance
46
+ </div>
47
+ <Skeleton className="h-8 w-20" />
48
+ </CardTitle>
49
+ </CardHeader>
50
+ <CardContent className="space-y-4">
51
+ <Skeleton className="h-10 w-32" />
52
+ <Skeleton className="h-4 w-48" />
53
+ </CardContent>
54
+ </Card>
55
+ );
56
+ }
57
+
58
+ return (
59
+ <Card>
60
+ <CardHeader>
61
+ <CardTitle className="flex items-center justify-between">
62
+ <div className="flex items-center gap-2">
63
+ <Wallet className="h-5 w-5" />
64
+ Account Balance
65
+ </div>
66
+ <div className="flex items-center gap-2">
67
+ <Button variant="ghost" size="sm" onClick={refreshSummary}>
68
+ <RefreshCw className="h-4 w-4" />
69
+ </Button>
70
+ <Button size="sm" onClick={() => openCreatePaymentDialog()}>
71
+ <Plus className="h-4 w-4 mr-2" />
72
+ Add Funds
73
+ </Button>
74
+ </div>
75
+ </CardTitle>
76
+ </CardHeader>
77
+ <CardContent className="space-y-4">
78
+ <div>
79
+ <div className="text-4xl font-bold">
80
+ {balanceSummary ? formatCurrency(balanceSummary.balance_usd) : '$0.00'}
81
+ </div>
82
+ <p className="text-sm text-muted-foreground mt-1">
83
+ {balanceSummary?.balance_display || 'No balance available'}
84
+ </p>
85
+ </div>
86
+
87
+ {balanceSummary && (
88
+ <div className="flex items-center gap-2">
89
+ <Badge variant={balanceSummary.has_transactions ? 'default' : 'secondary'}>
90
+ {balanceSummary.has_transactions ? 'Active' : 'New Account'}
91
+ </Badge>
92
+ {balanceSummary.is_empty && <Badge variant="outline">Empty Balance</Badge>}
93
+ </div>
94
+ )}
95
+ </CardContent>
96
+ </Card>
97
+ );
98
+ };
99
+
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Metrics Cards Component
3
+ * Display key payment metrics in cards
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ CardHeader,
13
+ CardTitle,
14
+ Skeleton,
15
+ } from '@djangocfg/ui';
16
+ import { Wallet, TrendingUp, CreditCard, DollarSign } from 'lucide-react';
17
+ import { useOverviewContext } from '@djangocfg/api/cfg/contexts';
18
+
19
+ export const MetricsCards: React.FC = () => {
20
+ const { metrics, isLoadingOverview } = useOverviewContext();
21
+
22
+ const formatCurrency = (amount?: number | null) => {
23
+ if (amount === null || amount === undefined) return '$0.00';
24
+ return new Intl.NumberFormat('en-US', {
25
+ style: 'currency',
26
+ currency: 'USD',
27
+ minimumFractionDigits: 2,
28
+ }).format(amount);
29
+ };
30
+
31
+ if (isLoadingOverview) {
32
+ return (
33
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
34
+ {Array.from({ length: 4 }).map((_, i) => (
35
+ <Card key={i}>
36
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
37
+ <Skeleton className="h-4 w-24" />
38
+ <Skeleton className="h-4 w-4 rounded-full" />
39
+ </CardHeader>
40
+ <CardContent>
41
+ <Skeleton className="h-7 w-20 mb-1" />
42
+ <Skeleton className="h-3 w-32" />
43
+ </CardContent>
44
+ </Card>
45
+ ))}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ const metricsData = [
51
+ {
52
+ title: 'Current Balance',
53
+ value: formatCurrency(metrics?.balance?.current_balance),
54
+ description: 'Available funds',
55
+ icon: Wallet,
56
+ trend: null,
57
+ },
58
+ {
59
+ title: 'Total Payments',
60
+ value: metrics?.payments?.total_payments?.toString() || '0',
61
+ description: 'All-time payments',
62
+ icon: CreditCard,
63
+ trend: null,
64
+ },
65
+ {
66
+ title: 'Total Spent',
67
+ value: formatCurrency(metrics?.balance?.total_spent),
68
+ description: 'Lifetime spending',
69
+ icon: DollarSign,
70
+ trend: null,
71
+ },
72
+ {
73
+ title: 'Active API Keys',
74
+ value: metrics?.api_keys?.active_keys?.toString() || '0',
75
+ description: 'Currently active',
76
+ icon: TrendingUp,
77
+ trend: null,
78
+ },
79
+ ];
80
+
81
+ return (
82
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
83
+ {metricsData.map((metric, index) => {
84
+ const Icon = metric.icon;
85
+ return (
86
+ <Card key={index}>
87
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
88
+ <CardTitle className="text-sm font-medium">{metric.title}</CardTitle>
89
+ <Icon className="h-4 w-4 text-muted-foreground" />
90
+ </CardHeader>
91
+ <CardContent>
92
+ <div className="text-2xl font-bold">{metric.value}</div>
93
+ <p className="text-xs text-muted-foreground mt-1">
94
+ {metric.description}
95
+ </p>
96
+ </CardContent>
97
+ </Card>
98
+ );
99
+ })}
100
+ </div>
101
+ );
102
+ };
103
+