@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,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,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
|
+
|