@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.2.34",
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.34",
67
- "@djangocfg/og-image": "^1.2.34",
68
- "@djangocfg/ui": "^1.2.34",
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.34",
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 { AdminLayout } from './layouts/AdminLayout';
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-11T06:22:05.284Z
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.34"
24
+ "version": "1.2.35"
25
25
  },
26
26
  {
27
27
  "name": "@djangocfg/api",
28
- "version": "1.2.34"
28
+ "version": "1.2.35"
29
29
  },
30
30
  {
31
31
  "name": "@djangocfg/layouts",
32
- "version": "1.2.34"
32
+ "version": "1.2.35"
33
33
  },
34
34
  {
35
35
  "name": "@djangocfg/markdown",
36
- "version": "1.2.34"
36
+ "version": "1.2.35"
37
37
  },
38
38
  {
39
39
  "name": "@djangocfg/og-image",
40
- "version": "1.2.34"
40
+ "version": "1.2.35"
41
41
  },
42
42
  {
43
43
  "name": "@djangocfg/eslint-config",
44
- "version": "1.2.34"
44
+ "version": "1.2.35"
45
45
  },
46
46
  {
47
47
  "name": "@djangocfg/typescript-config",
48
- "version": "1.2.34"
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 { useCfgApp } from './hooks';
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
- // Only run on client side
86
- const [isMounted, setIsMounted] = React.useState(false);
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
- function AdminLayoutClient({
91
+ // Wrapper component that provides CfgAppProvider with auth callback
92
+ function AdminLayoutClientWithProvider({
114
93
  children,
115
94
  config,
116
- enableParentSync = true
95
+ enableParentSync
117
96
  }: AdminLayoutProps) {
118
- const { isAdminUser, user, isLoading, isAuthenticated, loadCurrentProfile } = useAuth();
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
- console.log('[AdminLayout] handleAuthToken called, tokensReceived:', tokensReceivedRef.current, 'profileLoaded:', profileLoadedRef.current);
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
- console.log('[AdminLayout] Tokens already received and processed, ignoring duplicate');
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
- console.log('[AdminLayout] First time receiving tokens, processing...');
146
- console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
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
- console.log('[AdminLayout] Tokens set in API client');
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
- console.log('[AdminLayout] Loading user profile (first time)...');
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
- console.log('[AdminLayout] User profile loaded successfully');
142
+ consola.success('[AdminLayout] User profile loaded successfully');
159
143
  } catch (error) {
160
- console.error('[AdminLayout] Failed to load profile:', error);
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
- console.log('[AdminLayout] Profile already loaded, skipping duplicate call');
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
- console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
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
- // useCfgApp hook is called here to initialize iframe communication
177
- // Automatically sets tokens in API client when received from parent
178
- const { isEmbedded } = useCfgApp({
179
- onAuthTokenReceived: handleAuthToken // Now truly stable - never recreated
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 <LoadingState />;
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 { useCfgApp } from '../hooks/useApp';
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 } = useCfgApp();
55
+ const { isEmbedded, isMounted, parentTheme } = useCfgAppContext();
56
56
 
57
57
  // 1. Sync theme from parent → iframe
58
58
  useEffect(() => {
@@ -1 +1,3 @@
1
1
  export { ParentSync, AuthStatusSync } from './ParentSync';
2
+ export { PagePreloader, PagePreloaderDark } from './PagePreloader';
3
+ export type { PagePreloaderProps } from './PagePreloader';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { CfgAppProvider, useCfgAppContext } from './CfgAppContext';
2
+ export type { CfgAppProviderProps } from './CfgAppContext';
@@ -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
- console.log('[useCfgApp] parent-auth message received, authTokenProcessed:', authTokenProcessed);
145
+ consola.info('[useCfgApp] parent-auth message received', { authTokenProcessed });
142
146
 
143
147
  // Prevent processing if already handled
144
148
  if (authTokenProcessed) {
145
- console.log('[useCfgApp] Auth tokens already processed, ignoring duplicate message');
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
- console.log('[useCfgApp] Auth tokens already processed during debounce, skipping');
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
- console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
165
- console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
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
- console.log('[useCfgApp] onAuthTokenReceived callback completed successfully');
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 since listener is registered
209
- if (inIframe) {
215
+ // Send iframe-ready ONLY ONCE (even if component remounts)
216
+ if (inIframe && !iframeReadySent) {
210
217
  try {
211
- // console.log('[useCfgApp] Sending iframe-ready message to parent');
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
- // authLogger.debug('iframe-ready message sent');
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
- // console.log('[useCfgApp] Not in iframe, skipping iframe-ready message');
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, ChevronLeft, ChevronRight, RefreshCw, ExternalLink } from 'lucide-react';
31
- import { usePaymentsContext } from '@djangocfg/api/cfg/contexts';
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
- isLoadingPayments,
38
- refreshPayments,
39
- } = usePaymentsContext();
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 = async (value: string) => {
96
+ const handleSearch = (value: string) => {
92
97
  setSearchTerm(value);
93
- // TODO: Implement search/filter in PaymentsContext when API supports it
94
- await refreshPayments();
98
+ // Client-side filtering only
95
99
  };
96
100
 
97
- const handleStatusFilter = async (status: string) => {
101
+ const handleStatusFilter = (status: string) => {
98
102
  setStatusFilter(status);
99
- // TODO: Implement status filter in PaymentsContext when API supports it
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
- {totalPages > 1 && (
272
- <div className="flex items-center justify-between">
273
- <div className="text-sm text-muted-foreground">
274
- Showing {showingFrom} to {showingTo} of {totalCount} payments
275
- </div>
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
  ];