@djangocfg/layouts 1.2.34 → 1.2.35
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/package.json +5 -5
- package/src/layouts/AppLayout/AppLayout.tsx +10 -11
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +45 -59
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.example.tsx +98 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.tsx +149 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +2 -2
- package/src/layouts/AppLayout/layouts/AdminLayout/components/index.ts +2 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/context/CfgAppContext.tsx +48 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/context/index.ts +2 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +20 -12
- package/src/layouts/AppLayout/layouts/AdminLayout/index.ts +4 -0
- package/src/layouts/AppLayout/layouts/AdminLayout/lottie/energizing.json +1 -0
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +24 -55
- package/src/layouts/UILayout/config/components/tools.config.tsx +31 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.35",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"check": "tsc --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@djangocfg/api": "^1.2.
|
|
67
|
-
"@djangocfg/og-image": "^1.2.
|
|
68
|
-
"@djangocfg/ui": "^1.2.
|
|
66
|
+
"@djangocfg/api": "^1.2.35",
|
|
67
|
+
"@djangocfg/og-image": "^1.2.35",
|
|
68
|
+
"@djangocfg/ui": "^1.2.35",
|
|
69
69
|
"@hookform/resolvers": "^5.2.0",
|
|
70
70
|
"consola": "^3.4.2",
|
|
71
71
|
"lucide-react": "^0.468.0",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vidstack": "0.6.15"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
89
|
+
"@djangocfg/typescript-config": "^1.2.35",
|
|
90
90
|
"@types/node": "^24.7.2",
|
|
91
91
|
"@types/react": "19.2.2",
|
|
92
92
|
"@types/react-dom": "19.2.1",
|
|
@@ -26,18 +26,25 @@
|
|
|
26
26
|
|
|
27
27
|
import React, { ReactNode, useEffect, useState } from 'react';
|
|
28
28
|
import { useRouter } from 'next/router';
|
|
29
|
+
import dynamic from 'next/dynamic';
|
|
29
30
|
import { AppContextProvider } from './context';
|
|
30
31
|
import { CoreProviders } from './providers';
|
|
31
32
|
import { Seo, PageProgress, ErrorBoundary } from './components';
|
|
32
33
|
import { PublicLayout } from './layouts/PublicLayout';
|
|
33
34
|
import { PrivateLayout } from './layouts/PrivateLayout';
|
|
34
35
|
import { AuthLayout } from './layouts/AuthLayout';
|
|
35
|
-
import {
|
|
36
|
+
import { PagePreloader } from './layouts/AdminLayout/components';
|
|
36
37
|
import { determineLayoutMode, getRedirectUrl } from './utils';
|
|
37
38
|
import { useAuth } from '../../auth';
|
|
38
39
|
import type { AppLayoutConfig } from './types';
|
|
39
40
|
import type { ValidationErrorConfig } from '../../validation';
|
|
40
41
|
|
|
42
|
+
// Dynamic import for AdminLayout to prevent SSR hydration issues
|
|
43
|
+
const AdminLayout = dynamic(
|
|
44
|
+
() => import('./layouts/AdminLayout').then((mod) => ({ default: mod.AdminLayout })),
|
|
45
|
+
{ ssr: false }
|
|
46
|
+
);
|
|
47
|
+
|
|
41
48
|
export interface AppLayoutProps {
|
|
42
49
|
children: ReactNode;
|
|
43
50
|
config: AppLayoutConfig;
|
|
@@ -131,11 +138,7 @@ function LayoutRouter({
|
|
|
131
138
|
|
|
132
139
|
// Standalone mode: show loading during initialization
|
|
133
140
|
if (!isMounted || isLoading) {
|
|
134
|
-
return
|
|
135
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
136
|
-
<div className="text-muted-foreground">Loading...</div>
|
|
137
|
-
</div>
|
|
138
|
-
);
|
|
141
|
+
return <PagePreloader text="Loading admin..." />;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
// After mount: check authentication
|
|
@@ -163,11 +166,7 @@ function LayoutRouter({
|
|
|
163
166
|
// This prevents hydration mismatch when isAuthenticated differs between server/client
|
|
164
167
|
if (isPrivateRoute && !forceLayout) {
|
|
165
168
|
if (!isMounted || isLoading) {
|
|
166
|
-
return
|
|
167
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
168
|
-
<div className="text-muted-foreground">Loading...</div>
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
169
|
+
return <PagePreloader />;
|
|
171
170
|
}
|
|
172
171
|
|
|
173
172
|
// After mount: check authentication
|
|
@@ -16,36 +16,36 @@ export interface PackageInfo {
|
|
|
16
16
|
/**
|
|
17
17
|
* Package versions registry
|
|
18
18
|
* Auto-synced from package.json files
|
|
19
|
-
* Last updated: 2025-11-
|
|
19
|
+
* Last updated: 2025-11-12T09:15:11.957Z
|
|
20
20
|
*/
|
|
21
21
|
const PACKAGE_VERSIONS: PackageInfo[] = [
|
|
22
22
|
{
|
|
23
23
|
"name": "@djangocfg/ui",
|
|
24
|
-
"version": "1.2.
|
|
24
|
+
"version": "1.2.35"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "@djangocfg/api",
|
|
28
|
-
"version": "1.2.
|
|
28
|
+
"version": "1.2.35"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"name": "@djangocfg/layouts",
|
|
32
|
-
"version": "1.2.
|
|
32
|
+
"version": "1.2.35"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "@djangocfg/markdown",
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.35"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "@djangocfg/og-image",
|
|
40
|
-
"version": "1.2.
|
|
40
|
+
"version": "1.2.35"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "@djangocfg/eslint-config",
|
|
44
|
-
"version": "1.2.
|
|
44
|
+
"version": "1.2.35"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "@djangocfg/typescript-config",
|
|
48
|
-
"version": "1.2.
|
|
48
|
+
"version": "1.2.35"
|
|
49
49
|
}
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import React, { ReactNode } from 'react';
|
|
18
18
|
import { ShieldAlert } from 'lucide-react';
|
|
19
|
-
import { ParentSync } from './components';
|
|
20
|
-
import {
|
|
19
|
+
import { ParentSync, PagePreloader } from './components';
|
|
20
|
+
import { CfgAppProvider, useCfgAppContext } from './context';
|
|
21
21
|
import type { AdminLayoutConfig } from './types';
|
|
22
22
|
import { api } from '@djangocfg/api';
|
|
23
23
|
import { useAuth } from '../../../../auth';
|
|
24
|
+
import { consola } from 'consola';
|
|
24
25
|
|
|
25
26
|
export interface AdminLayoutProps {
|
|
26
27
|
children: ReactNode;
|
|
@@ -82,41 +83,18 @@ export function AdminLayout({
|
|
|
82
83
|
config,
|
|
83
84
|
enableParentSync = true
|
|
84
85
|
}: AdminLayoutProps) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Track mount state to prevent hydration mismatch
|
|
89
|
-
React.useEffect(() => {
|
|
90
|
-
setIsMounted(true);
|
|
91
|
-
}, []);
|
|
92
|
-
|
|
93
|
-
// Minimalist loading component
|
|
94
|
-
const LoadingState = () => (
|
|
95
|
-
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
96
|
-
<div className="flex items-center gap-2 text-muted-foreground">
|
|
97
|
-
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '0ms' }} />
|
|
98
|
-
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '150ms' }} />
|
|
99
|
-
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '300ms' }} />
|
|
100
|
-
<span className="ml-2">Loading...</span>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
86
|
+
return (
|
|
87
|
+
<AdminLayoutClientWithProvider config={config} enableParentSync={enableParentSync}>{children}</AdminLayoutClientWithProvider>
|
|
103
88
|
);
|
|
104
|
-
|
|
105
|
-
// During SSR and initial render, show loading to prevent hydration mismatch
|
|
106
|
-
if (!isMounted) {
|
|
107
|
-
return <LoadingState />;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return <AdminLayoutClient config={config} enableParentSync={enableParentSync}>{children}</AdminLayoutClient>;
|
|
111
89
|
}
|
|
112
90
|
|
|
113
|
-
|
|
91
|
+
// Wrapper component that provides CfgAppProvider with auth callback
|
|
92
|
+
function AdminLayoutClientWithProvider({
|
|
114
93
|
children,
|
|
115
94
|
config,
|
|
116
|
-
enableParentSync
|
|
95
|
+
enableParentSync
|
|
117
96
|
}: AdminLayoutProps) {
|
|
118
|
-
const {
|
|
119
|
-
// console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
|
|
97
|
+
const { loadCurrentProfile } = useAuth();
|
|
120
98
|
|
|
121
99
|
// Use refs to prevent re-renders and maintain state across renders
|
|
122
100
|
const profileLoadedRef = React.useRef(false);
|
|
@@ -131,74 +109,82 @@ function AdminLayoutClient({
|
|
|
131
109
|
// Create a STABLE callback that NEVER changes (no dependencies)
|
|
132
110
|
// Use refs to access latest values without recreating the callback
|
|
133
111
|
const handleAuthToken = React.useCallback(async (authToken: string, refreshToken?: string) => {
|
|
134
|
-
|
|
112
|
+
consola.info('[AdminLayout] handleAuthToken called', {
|
|
113
|
+
tokensReceived: tokensReceivedRef.current,
|
|
114
|
+
profileLoaded: profileLoadedRef.current
|
|
115
|
+
});
|
|
135
116
|
|
|
136
117
|
// Prevent duplicate token processing
|
|
137
118
|
if (tokensReceivedRef.current) {
|
|
138
|
-
|
|
119
|
+
consola.warn('[AdminLayout] Tokens already received and processed, ignoring duplicate');
|
|
139
120
|
return;
|
|
140
121
|
}
|
|
141
122
|
|
|
142
123
|
// Mark tokens as received IMMEDIATELY to prevent race conditions
|
|
143
124
|
tokensReceivedRef.current = true;
|
|
144
125
|
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
consola.start('[AdminLayout] First time receiving tokens, processing...');
|
|
127
|
+
consola.debug('[AdminLayout] Tokens:', {
|
|
128
|
+
authToken: authToken.substring(0, 20) + '...',
|
|
129
|
+
refreshToken: refreshToken ? refreshToken.substring(0, 20) + '...' : 'null'
|
|
130
|
+
});
|
|
147
131
|
|
|
148
132
|
// Always set tokens in API client
|
|
149
133
|
api.setToken(authToken, refreshToken);
|
|
150
|
-
|
|
134
|
+
consola.success('[AdminLayout] Tokens set in API client');
|
|
151
135
|
|
|
152
136
|
// Load user profile after setting tokens - ONLY ONCE per session
|
|
153
137
|
if (!profileLoadedRef.current) {
|
|
154
|
-
|
|
138
|
+
consola.start('[AdminLayout] Loading user profile (first time)...');
|
|
155
139
|
try {
|
|
156
140
|
await loadCurrentProfileRef.current('AdminLayout.onAuthTokenReceived');
|
|
157
141
|
profileLoadedRef.current = true;
|
|
158
|
-
|
|
142
|
+
consola.success('[AdminLayout] User profile loaded successfully');
|
|
159
143
|
} catch (error) {
|
|
160
|
-
|
|
144
|
+
consola.error('[AdminLayout] Failed to load profile:', error);
|
|
161
145
|
// Reset flags on error so user can retry
|
|
162
146
|
tokensReceivedRef.current = false;
|
|
163
147
|
profileLoadedRef.current = false;
|
|
164
148
|
}
|
|
165
149
|
} else {
|
|
166
|
-
|
|
150
|
+
consola.info('[AdminLayout] Profile already loaded, skipping duplicate call');
|
|
167
151
|
}
|
|
168
152
|
|
|
169
153
|
// Call custom handler if provided
|
|
170
154
|
if (config?.onAuthTokenReceived) {
|
|
171
|
-
|
|
155
|
+
consola.info('[AdminLayout] Calling custom onAuthTokenReceived handler');
|
|
172
156
|
config.onAuthTokenReceived(authToken, refreshToken);
|
|
173
157
|
}
|
|
174
158
|
}, []); // ← EMPTY DEPS - callback NEVER changes
|
|
175
159
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
160
|
+
return (
|
|
161
|
+
<CfgAppProvider options={{
|
|
162
|
+
onAuthTokenReceived: handleAuthToken
|
|
163
|
+
}}>
|
|
164
|
+
<AdminLayoutClient config={config} enableParentSync={enableParentSync}>{children}</AdminLayoutClient>
|
|
165
|
+
</CfgAppProvider>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function AdminLayoutClient({
|
|
170
|
+
children,
|
|
171
|
+
config,
|
|
172
|
+
enableParentSync = true
|
|
173
|
+
}: AdminLayoutProps) {
|
|
174
|
+
const { isAdminUser, user, isLoading, isAuthenticated } = useAuth();
|
|
175
|
+
// console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
|
|
176
|
+
|
|
177
|
+
// Get embedding state from context (provided by CfgAppProvider wrapper)
|
|
178
|
+
const { isEmbedded } = useCfgAppContext();
|
|
181
179
|
|
|
182
180
|
// console.log('[AdminLayout] isEmbedded:', isEmbedded);
|
|
183
181
|
|
|
184
|
-
// Minimalist loading component
|
|
185
|
-
const LoadingState = () => (
|
|
186
|
-
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
187
|
-
<div className="flex items-center gap-2 text-muted-foreground">
|
|
188
|
-
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '0ms' }} />
|
|
189
|
-
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '150ms' }} />
|
|
190
|
-
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '300ms' }} />
|
|
191
|
-
<span className="ml-2">Loading...</span>
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
);
|
|
195
|
-
|
|
196
182
|
// Show loading while auth is initializing (waiting for tokens from parent or profile loading)
|
|
197
183
|
// OR if user object is not loaded yet (null/undefined)
|
|
198
184
|
// OR if authentication status is not yet determined
|
|
199
185
|
if (isLoading || !user || !isAuthenticated) {
|
|
200
186
|
// console.log('[AdminLayout] Showing loading state - isLoading:', isLoading, 'user:', user, 'isAuthenticated:', isAuthenticated);
|
|
201
|
-
return <
|
|
187
|
+
return <PagePreloader text="Authenticating..." size="lg" />;
|
|
202
188
|
}
|
|
203
189
|
|
|
204
190
|
// Only show "Access Denied" when we are CERTAIN user doesn't have permissions
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PagePreloader - Usage Examples
|
|
3
|
+
*
|
|
4
|
+
* This file contains examples of how to use the PagePreloader component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { PagePreloader, PagePreloaderDark } from './PagePreloader';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Example 1: Basic Usage
|
|
11
|
+
// ============================================================================
|
|
12
|
+
export function Example1_Basic() {
|
|
13
|
+
return <PagePreloader />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Example 2: Custom Text
|
|
18
|
+
// ============================================================================
|
|
19
|
+
export function Example2_CustomText() {
|
|
20
|
+
return <PagePreloader text="Loading your dashboard..." />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Example 3: Different Sizes
|
|
25
|
+
// ============================================================================
|
|
26
|
+
export function Example3_DifferentSizes() {
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<PagePreloader size="sm" text="Small" />
|
|
30
|
+
<PagePreloader size="md" text="Medium" />
|
|
31
|
+
<PagePreloader size="lg" text="Large" />
|
|
32
|
+
<PagePreloader size="xl" text="Extra Large" />
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Example 4: Dark Variant
|
|
39
|
+
// ============================================================================
|
|
40
|
+
export function Example4_Dark() {
|
|
41
|
+
return <PagePreloaderDark text="Loading..." />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Example 5: No Text
|
|
46
|
+
// ============================================================================
|
|
47
|
+
export function Example5_NoText() {
|
|
48
|
+
return <PagePreloader showText={false} />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Example 6: No Backdrop
|
|
53
|
+
// ============================================================================
|
|
54
|
+
export function Example6_NoBackdrop() {
|
|
55
|
+
return <PagePreloader backdrop={false} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Example 7: Custom Backdrop Opacity
|
|
60
|
+
// ============================================================================
|
|
61
|
+
export function Example7_CustomBackdropOpacity() {
|
|
62
|
+
return <PagePreloader backdropOpacity={50} text="50% opacity backdrop" />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Example 8: Conditional Loading
|
|
67
|
+
// ============================================================================
|
|
68
|
+
export function Example8_ConditionalLoading({ isLoading }: { isLoading: boolean }) {
|
|
69
|
+
if (!isLoading) return null;
|
|
70
|
+
return <PagePreloader text="Loading data..." />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Example 9: With Custom Styling
|
|
75
|
+
// ============================================================================
|
|
76
|
+
export function Example9_CustomStyling() {
|
|
77
|
+
return (
|
|
78
|
+
<PagePreloader
|
|
79
|
+
className="bg-gradient-to-br from-blue-500 to-purple-600"
|
|
80
|
+
text="Loading..."
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Example 10: In AdminLayout
|
|
87
|
+
// ============================================================================
|
|
88
|
+
export function Example10_InAdminLayout() {
|
|
89
|
+
// Usage in AdminLayout component
|
|
90
|
+
const isLoading = true; // Replace with actual loading state
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
{isLoading && <PagePreloader text="Loading application..." />}
|
|
95
|
+
{/* Rest of your app */}
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PagePreloader Component
|
|
3
|
+
*
|
|
4
|
+
* Full-page loading indicator with Lottie animation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { LottiePlayer } from '@djangocfg/ui/tools';
|
|
11
|
+
import energizingAnimation from '../lottie/energizing.json';
|
|
12
|
+
|
|
13
|
+
export interface PagePreloaderProps {
|
|
14
|
+
/**
|
|
15
|
+
* Custom loading text
|
|
16
|
+
* @default 'Loading...'
|
|
17
|
+
*/
|
|
18
|
+
text?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Show loading text
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
showText?: boolean;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Animation size
|
|
28
|
+
* @default 'lg'
|
|
29
|
+
*/
|
|
30
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether to show backdrop
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
backdrop?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Backdrop opacity (0-100)
|
|
40
|
+
* @default 80
|
|
41
|
+
*/
|
|
42
|
+
backdropOpacity?: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Additional CSS classes
|
|
46
|
+
*/
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PagePreloader - Full-page loading indicator with energizing animation
|
|
52
|
+
*
|
|
53
|
+
* Features:
|
|
54
|
+
* - Lottie animation with energizing effect
|
|
55
|
+
* - Customizable text and size
|
|
56
|
+
* - Optional backdrop
|
|
57
|
+
* - Smooth fade-in animation
|
|
58
|
+
*
|
|
59
|
+
* Usage:
|
|
60
|
+
* ```tsx
|
|
61
|
+
* // Basic usage
|
|
62
|
+
* <PagePreloader />
|
|
63
|
+
*
|
|
64
|
+
* // With custom text
|
|
65
|
+
* <PagePreloader text="Loading your data..." />
|
|
66
|
+
*
|
|
67
|
+
* // Different size
|
|
68
|
+
* <PagePreloader size="xl" />
|
|
69
|
+
*
|
|
70
|
+
* // Without backdrop
|
|
71
|
+
* <PagePreloader backdrop={false} />
|
|
72
|
+
*
|
|
73
|
+
* // Custom styling
|
|
74
|
+
* <PagePreloader className="bg-gradient-to-br from-blue-500 to-purple-600" />
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function PagePreloader({
|
|
78
|
+
text = 'Loading...',
|
|
79
|
+
showText = true,
|
|
80
|
+
size = 'lg',
|
|
81
|
+
backdrop = true,
|
|
82
|
+
backdropOpacity = 80,
|
|
83
|
+
className,
|
|
84
|
+
}: PagePreloaderProps) {
|
|
85
|
+
// Generate backdrop opacity classes based on opacity value with explicit light/dark variants
|
|
86
|
+
const getBackdropClasses = () => {
|
|
87
|
+
if (!backdrop) return 'bg-transparent';
|
|
88
|
+
|
|
89
|
+
// Use explicit light/dark classes for reliable theme support
|
|
90
|
+
if (backdropOpacity >= 90) {
|
|
91
|
+
return 'bg-white dark:bg-gray-950'; // Full opacity
|
|
92
|
+
} else if (backdropOpacity >= 70) {
|
|
93
|
+
return 'bg-white/90 dark:bg-gray-950/90'; // 90% opacity
|
|
94
|
+
} else if (backdropOpacity >= 50) {
|
|
95
|
+
return 'bg-white/80 dark:bg-gray-950/80'; // 80% opacity
|
|
96
|
+
} else {
|
|
97
|
+
return 'bg-white/60 dark:bg-gray-950/60'; // 60% opacity
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className={`fixed inset-0 z-50 flex items-center justify-center ${getBackdropClasses()} ${className || ''}`}
|
|
104
|
+
>
|
|
105
|
+
<div className="flex flex-col items-center gap-6 animate-in fade-in duration-300">
|
|
106
|
+
{/* Lottie Animation */}
|
|
107
|
+
<div className="relative">
|
|
108
|
+
<LottiePlayer
|
|
109
|
+
src={energizingAnimation}
|
|
110
|
+
size={size}
|
|
111
|
+
autoplay
|
|
112
|
+
loop
|
|
113
|
+
speed={1}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Loading Text */}
|
|
118
|
+
{showText && (
|
|
119
|
+
<div className="flex flex-col items-center gap-2">
|
|
120
|
+
<p className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
|
121
|
+
{text}
|
|
122
|
+
</p>
|
|
123
|
+
<div className="flex gap-1">
|
|
124
|
+
<span className="h-2 w-2 animate-bounce rounded-full bg-blue-600 dark:bg-blue-400" style={{ animationDelay: '0ms' }} />
|
|
125
|
+
<span className="h-2 w-2 animate-bounce rounded-full bg-blue-600 dark:bg-blue-400" style={{ animationDelay: '150ms' }} />
|
|
126
|
+
<span className="h-2 w-2 animate-bounce rounded-full bg-blue-600 dark:bg-blue-400" style={{ animationDelay: '300ms' }} />
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* PagePreloaderDark - Dark variant of PagePreloader
|
|
137
|
+
*
|
|
138
|
+
* Same as PagePreloader but forces dark backdrop
|
|
139
|
+
* Note: Regular PagePreloader now uses semantic theme colors,
|
|
140
|
+
* so this is only needed for specific dark-only use cases
|
|
141
|
+
*/
|
|
142
|
+
export function PagePreloaderDark(props: Omit<PagePreloaderProps, 'className'>) {
|
|
143
|
+
return (
|
|
144
|
+
<PagePreloader
|
|
145
|
+
{...props}
|
|
146
|
+
className="bg-gray-950/95"
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -12,7 +12,7 @@ import { useEffect, useState } from 'react';
|
|
|
12
12
|
import { useRouter } from 'next/router';
|
|
13
13
|
import { useAuth } from '../../../../../auth';
|
|
14
14
|
import { useThemeContext } from '@djangocfg/ui';
|
|
15
|
-
import {
|
|
15
|
+
import { useCfgAppContext } from '../context';
|
|
16
16
|
import { authLogger } from '../../../../../utils/logger';
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -52,7 +52,7 @@ function ParentSyncClient() {
|
|
|
52
52
|
const router = useRouter();
|
|
53
53
|
const auth = useAuth();
|
|
54
54
|
const { setTheme } = useThemeContext();
|
|
55
|
-
const { isEmbedded, isMounted, parentTheme } =
|
|
55
|
+
const { isEmbedded, isMounted, parentTheme } = useCfgAppContext();
|
|
56
56
|
|
|
57
57
|
// 1. Sync theme from parent → iframe
|
|
58
58
|
useEffect(() => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// CfgApp Context - Shared App State
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Provides useCfgApp state to child components without duplicate hook calls
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { createContext, useContext, ReactNode } from 'react';
|
|
9
|
+
import { UseCfgAppReturn, useCfgApp, UseCfgAppOptions } from '../hooks/useApp';
|
|
10
|
+
|
|
11
|
+
const CfgAppContext = createContext<UseCfgAppReturn | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface CfgAppProviderProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
options?: UseCfgAppOptions;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Provider for CfgApp state
|
|
20
|
+
*
|
|
21
|
+
* Should be used once at the top level (e.g., AdminLayout)
|
|
22
|
+
* to avoid multiple useCfgApp() calls which cause duplicate
|
|
23
|
+
* message listeners and iframe-ready signals
|
|
24
|
+
*/
|
|
25
|
+
export function CfgAppProvider({ children, options }: CfgAppProviderProps) {
|
|
26
|
+
const cfgApp = useCfgApp(options);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<CfgAppContext.Provider value={cfgApp}>
|
|
30
|
+
{children}
|
|
31
|
+
</CfgAppContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hook to consume CfgApp state
|
|
37
|
+
*
|
|
38
|
+
* @throws Error if used outside CfgAppProvider
|
|
39
|
+
*/
|
|
40
|
+
export function useCfgAppContext(): UseCfgAppReturn {
|
|
41
|
+
const context = useContext(CfgAppContext);
|
|
42
|
+
|
|
43
|
+
if (!context) {
|
|
44
|
+
throw new Error('useCfgAppContext must be used within CfgAppProvider');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return context;
|
|
48
|
+
}
|
|
@@ -8,6 +8,7 @@ import React from 'react';
|
|
|
8
8
|
import { useState, useEffect } from 'react';
|
|
9
9
|
import { useRouter } from 'next/router';
|
|
10
10
|
import { authLogger } from '../../../../../utils/logger';
|
|
11
|
+
import { consola } from 'consola';
|
|
11
12
|
|
|
12
13
|
export interface UseCfgAppReturn {
|
|
13
14
|
/**
|
|
@@ -86,6 +87,9 @@ export interface UseCfgAppOptions {
|
|
|
86
87
|
* );
|
|
87
88
|
* ```
|
|
88
89
|
*/
|
|
90
|
+
// Global flag to track if iframe-ready was sent (persists across component remounts)
|
|
91
|
+
let iframeReadySent = false;
|
|
92
|
+
|
|
89
93
|
export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
90
94
|
const router = useRouter();
|
|
91
95
|
const [isMounted, setIsMounted] = useState(false);
|
|
@@ -138,11 +142,11 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
138
142
|
|
|
139
143
|
switch (type) {
|
|
140
144
|
case 'parent-auth':
|
|
141
|
-
|
|
145
|
+
consola.info('[useCfgApp] parent-auth message received', { authTokenProcessed });
|
|
142
146
|
|
|
143
147
|
// Prevent processing if already handled
|
|
144
148
|
if (authTokenProcessed) {
|
|
145
|
-
|
|
149
|
+
consola.warn('[useCfgApp] Auth tokens already processed, ignoring duplicate message');
|
|
146
150
|
return;
|
|
147
151
|
}
|
|
148
152
|
|
|
@@ -155,21 +159,24 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
155
159
|
authTokenTimeout = setTimeout(() => {
|
|
156
160
|
// Double-check still not processed (race condition protection)
|
|
157
161
|
if (authTokenProcessed) {
|
|
158
|
-
|
|
162
|
+
consola.warn('[useCfgApp] Auth tokens already processed during debounce, skipping');
|
|
159
163
|
return;
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
// Receive authentication tokens from parent
|
|
163
167
|
if (data?.authToken && callbackRef.current) {
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
consola.success('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
|
|
169
|
+
consola.debug('[useCfgApp] Tokens:', {
|
|
170
|
+
authToken: data.authToken.substring(0, 20) + '...',
|
|
171
|
+
refreshToken: data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null'
|
|
172
|
+
});
|
|
166
173
|
|
|
167
174
|
// Mark as processed BEFORE calling callback
|
|
168
175
|
authTokenProcessed = true;
|
|
169
176
|
|
|
170
177
|
try {
|
|
171
178
|
callbackRef.current(data.authToken, data.refreshToken);
|
|
172
|
-
|
|
179
|
+
consola.success('[useCfgApp] onAuthTokenReceived callback completed successfully');
|
|
173
180
|
} catch (e) {
|
|
174
181
|
authLogger.error('Failed to process auth tokens:', e);
|
|
175
182
|
// Reset on error to allow retry
|
|
@@ -205,10 +212,10 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
205
212
|
window.addEventListener('message', handleMessage);
|
|
206
213
|
// console.log('[useCfgApp] Message listener registered, isEmbedded:', inIframe);
|
|
207
214
|
|
|
208
|
-
// Send iframe-ready
|
|
209
|
-
if (inIframe) {
|
|
215
|
+
// Send iframe-ready ONLY ONCE (even if component remounts)
|
|
216
|
+
if (inIframe && !iframeReadySent) {
|
|
210
217
|
try {
|
|
211
|
-
|
|
218
|
+
consola.start('[useCfgApp] Sending iframe-ready message to parent (FIRST TIME)');
|
|
212
219
|
window.parent.postMessage({
|
|
213
220
|
type: 'iframe-ready',
|
|
214
221
|
data: {
|
|
@@ -216,12 +223,13 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
216
223
|
referrer: document.referrer
|
|
217
224
|
}
|
|
218
225
|
}, '*');
|
|
219
|
-
//
|
|
226
|
+
iframeReadySent = true; // Mark as sent to prevent duplicates
|
|
227
|
+
consola.success('[useCfgApp] iframe-ready message sent');
|
|
220
228
|
} catch (e) {
|
|
221
229
|
authLogger.error('Failed to notify parent about ready state:', e);
|
|
222
230
|
}
|
|
223
|
-
} else {
|
|
224
|
-
|
|
231
|
+
} else if (inIframe && iframeReadySent) {
|
|
232
|
+
consola.info('[useCfgApp] iframe-ready already sent, skipping to prevent loop');
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
return () => {
|
|
@@ -13,6 +13,10 @@ export type { AdminLayoutProps } from './AdminLayout';
|
|
|
13
13
|
export { useCfgApp, useApp } from './hooks';
|
|
14
14
|
export type { UseCfgAppReturn, UseCfgAppOptions, UseAppReturn, UseAppOptions } from './hooks';
|
|
15
15
|
|
|
16
|
+
// Context
|
|
17
|
+
export { CfgAppProvider, useCfgAppContext } from './context';
|
|
18
|
+
export type { CfgAppProviderProps } from './context';
|
|
19
|
+
|
|
16
20
|
// Components
|
|
17
21
|
export { ParentSync, AuthStatusSync } from './components';
|
|
18
22
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"nm":"Rander","ddd":0,"h":512,"w":512,"meta":{"g":"LottieFiles AE 2.0.4"},"layers":[{"ty":4,"nm":"Capa 2","sr":1,"st":0,"op":46,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[256.002,256,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.956,"y":0},"i":{"x":0.237,"y":1},"s":[120,120,100],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.395,"y":1},"s":[60,60,100],"t":40},{"s":[120,120,100],"t":46}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[256.002,256,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.638,-9.168],[0,0],[0,0],[-8.437,0],[0,0],[-5.944,5.985],[0,0]],"o":[[0,0],[0,0],[-5.944,5.985],[0,0],[-8.437,0],[0,0],[6.562,-6.613]],"v":[[75.715,-65.189],[73.984,-55.526],[-40.87,60.214],[-34.133,76.388],[-66.367,76.388],[-73.104,60.214],[59.634,-73.544]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.8196,0.3569],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[226.809,194.352],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.061,-5.831],[0,0],[0,0],[-1.679,9.179],[0,0],[5.924,0],[0,0]],"o":[[0,0],[0,0],[-6.562,6.624],[0,0],[1.061,-5.831],[0,0],[5.934,0]],"v":[[25.334,-50.448],[8.686,40.722],[-9.249,58.802],[-25.32,50.416],[-6.9,-50.448],[-16.244,-61.646],[15.99,-61.646]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.8196,0.3569],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[233.82,332.386],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tr","a":{"a":0,"k":[233.82,332.386],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[233.82,332.386],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":2,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[6.558,-6.608],[0,0],[-8.439,0],[0,0],[1.064,-5.829],[0,0],[-6.57,6.624],[0,0],[8.2,0.413],[0,0],[-1.005,5.629],[0,0]],"o":[[0,0],[-5.945,5.99],[0,0],[5.925,0],[0,0],[-1.676,9.178],[0,0],[5.782,-5.829],[0,0],[-5.71,-0.288],[0,0],[1.639,-9.166]],"v":[[30.44,-135.194],[-102.297,-1.434],[-95.559,14.745],[-38.427,14.745],[-29.089,25.943],[-47.507,126.802],[-31.43,135.192],[102.301,0.367],[96.039,-15.798],[38.063,-18.72],[29.196,-29.87],[46.521,-126.837]]},"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.9961,0.8941,0.3529],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[256.002,256],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 1","sr":1,"st":-41,"op":46,"ip":-39,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[6,22,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0,0,100],"t":13},{"s":[800,800,100],"t":30}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[252.5,252.5,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":13},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":23},{"s":[0],"t":30}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[30,30],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[0],"t":13},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[3],"t":23},{"s":[0],"t":30}],"ix":5},"c":{"a":0,"k":[1,0.9647,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[6,22],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2}],"v":"4.8.0","fr":30,"op":46,"ip":0,"assets":[]}
|
|
@@ -26,21 +26,26 @@ import {
|
|
|
26
26
|
SelectTrigger,
|
|
27
27
|
SelectValue,
|
|
28
28
|
Skeleton,
|
|
29
|
+
useDRFPagination,
|
|
30
|
+
StaticPagination,
|
|
29
31
|
} from '@djangocfg/ui';
|
|
30
|
-
import { Plus, Search, Filter,
|
|
31
|
-
import {
|
|
32
|
+
import { Plus, Search, Filter, RefreshCw, ExternalLink } from 'lucide-react';
|
|
33
|
+
import { api, Hooks } from '@djangocfg/api';
|
|
32
34
|
import { openCreatePaymentDialog, openPaymentDetailsDialog } from '../../../events';
|
|
33
35
|
|
|
34
36
|
export const PaymentsList: React.FC = () => {
|
|
37
|
+
// Local pagination state
|
|
38
|
+
const pagination = useDRFPagination(1, 20);
|
|
39
|
+
|
|
40
|
+
// Fetch payments with pagination
|
|
35
41
|
const {
|
|
36
|
-
payments,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
data: payments,
|
|
43
|
+
error,
|
|
44
|
+
isLoading: isLoadingPayments,
|
|
45
|
+
mutate: refreshPayments,
|
|
46
|
+
} = Hooks.usePaymentsPaymentsList(pagination.params, api as any);
|
|
40
47
|
|
|
41
48
|
const paymentsList = payments?.results || [];
|
|
42
|
-
const currentPage = payments?.page || 1;
|
|
43
|
-
const pageSize = payments?.page_size || 20;
|
|
44
49
|
const totalCount = payments?.count || 0;
|
|
45
50
|
|
|
46
51
|
const [searchTerm, setSearchTerm] = useState('');
|
|
@@ -88,22 +93,16 @@ export const PaymentsList: React.FC = () => {
|
|
|
88
93
|
}
|
|
89
94
|
};
|
|
90
95
|
|
|
91
|
-
const handleSearch =
|
|
96
|
+
const handleSearch = (value: string) => {
|
|
92
97
|
setSearchTerm(value);
|
|
93
|
-
//
|
|
94
|
-
await refreshPayments();
|
|
98
|
+
// Client-side filtering only
|
|
95
99
|
};
|
|
96
100
|
|
|
97
|
-
const handleStatusFilter =
|
|
101
|
+
const handleStatusFilter = (status: string) => {
|
|
98
102
|
setStatusFilter(status);
|
|
99
|
-
//
|
|
100
|
-
await refreshPayments();
|
|
103
|
+
// Client-side filtering only
|
|
101
104
|
};
|
|
102
105
|
|
|
103
|
-
const handlePageChange = async (page: number) => {
|
|
104
|
-
// TODO: Implement pagination in PaymentsContext
|
|
105
|
-
await refreshPayments();
|
|
106
|
-
};
|
|
107
106
|
|
|
108
107
|
// Filter payments client-side for now (until API supports filtering)
|
|
109
108
|
const filteredPayments = paymentsList.filter((payment) => {
|
|
@@ -120,17 +119,13 @@ export const PaymentsList: React.FC = () => {
|
|
|
120
119
|
return matchesSearch && matchesStatus;
|
|
121
120
|
});
|
|
122
121
|
|
|
123
|
-
const totalPages = Math.ceil((totalCount || 0) / (pageSize || 20));
|
|
124
|
-
const showingFrom = ((currentPage || 1) - 1) * (pageSize || 20) + 1;
|
|
125
|
-
const showingTo = Math.min((currentPage || 1) * (pageSize || 20), totalCount || 0);
|
|
126
|
-
|
|
127
122
|
return (
|
|
128
123
|
<Card>
|
|
129
124
|
<CardHeader>
|
|
130
125
|
<CardTitle className="flex items-center justify-between">
|
|
131
126
|
<span>Payment History</span>
|
|
132
127
|
<div className="flex items-center gap-2">
|
|
133
|
-
<Button variant="outline" size="sm" onClick={refreshPayments} disabled={isLoadingPayments}>
|
|
128
|
+
<Button variant="outline" size="sm" onClick={() => refreshPayments()} disabled={isLoadingPayments}>
|
|
134
129
|
<RefreshCw className={`h-4 w-4 mr-2 ${isLoadingPayments ? 'animate-spin' : ''}`} />
|
|
135
130
|
Refresh
|
|
136
131
|
</Button>
|
|
@@ -267,38 +262,12 @@ export const PaymentsList: React.FC = () => {
|
|
|
267
262
|
</Table>
|
|
268
263
|
</div>
|
|
269
264
|
|
|
270
|
-
{/* Pagination */}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<div className="flex items-center gap-2">
|
|
278
|
-
<Button
|
|
279
|
-
variant="outline"
|
|
280
|
-
size="sm"
|
|
281
|
-
onClick={() => handlePageChange((currentPage || 1) - 1)}
|
|
282
|
-
disabled={!currentPage || currentPage <= 1}
|
|
283
|
-
>
|
|
284
|
-
<ChevronLeft className="h-4 w-4" />
|
|
285
|
-
</Button>
|
|
286
|
-
|
|
287
|
-
<span className="text-sm">
|
|
288
|
-
Page {currentPage || 1} of {totalPages}
|
|
289
|
-
</span>
|
|
290
|
-
|
|
291
|
-
<Button
|
|
292
|
-
variant="outline"
|
|
293
|
-
size="sm"
|
|
294
|
-
onClick={() => handlePageChange((currentPage || 1) + 1)}
|
|
295
|
-
disabled={!currentPage || currentPage >= totalPages}
|
|
296
|
-
>
|
|
297
|
-
<ChevronRight className="h-4 w-4" />
|
|
298
|
-
</Button>
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
)}
|
|
265
|
+
{/* DRF Pagination */}
|
|
266
|
+
<StaticPagination
|
|
267
|
+
data={payments}
|
|
268
|
+
onPageChange={pagination.setPage}
|
|
269
|
+
className="mt-4"
|
|
270
|
+
/>
|
|
302
271
|
</>
|
|
303
272
|
)}
|
|
304
273
|
</CardContent>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
|
-
import { JsonTree, PrettyCode, Mermaid } from '@djangocfg/ui';
|
|
6
|
+
import { JsonTree, PrettyCode, Mermaid, LottiePlayer } from '@djangocfg/ui';
|
|
7
7
|
import type { ComponentConfig } from './types';
|
|
8
8
|
|
|
9
9
|
// Sample data for demos
|
|
@@ -201,4 +201,34 @@ export const TOOLS_COMPONENTS: ComponentConfig[] = [
|
|
|
201
201
|
</div>
|
|
202
202
|
),
|
|
203
203
|
},
|
|
204
|
+
{
|
|
205
|
+
name: 'LottiePlayer',
|
|
206
|
+
category: 'tools',
|
|
207
|
+
description: 'Lottie animation player with size presets and playback controls',
|
|
208
|
+
importPath: `import { LottiePlayer } from '@djangocfg/ui';`,
|
|
209
|
+
example: `<LottiePlayer
|
|
210
|
+
src="https://lottie.host/embed/a0eb3923-2f93-4a2e-9c91-3e0b0f6f3b3e/WHJEbMDJLn.json"
|
|
211
|
+
size="md"
|
|
212
|
+
autoplay
|
|
213
|
+
loop
|
|
214
|
+
/>
|
|
215
|
+
|
|
216
|
+
// Custom size and speed
|
|
217
|
+
<LottiePlayer
|
|
218
|
+
src={animationData}
|
|
219
|
+
width={300}
|
|
220
|
+
height={300}
|
|
221
|
+
speed={1.5}
|
|
222
|
+
/>`,
|
|
223
|
+
preview: (
|
|
224
|
+
<div className="flex items-center justify-center p-8">
|
|
225
|
+
<LottiePlayer
|
|
226
|
+
src="https://lottie.host/embed/a0eb3923-2f93-4a2e-9c91-3e0b0f6f3b3e/WHJEbMDJLn.json"
|
|
227
|
+
size="md"
|
|
228
|
+
autoplay
|
|
229
|
+
loop
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
),
|
|
233
|
+
},
|
|
204
234
|
];
|