@djangocfg/layouts 2.1.20 → 2.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AppLayout/AppLayout.tsx +29 -27
  3. package/src/layouts/AppLayout/BaseApp.tsx +36 -38
  4. package/src/layouts/PublicLayout/PublicLayout.tsx +9 -43
  5. package/src/layouts/PublicLayout/components/PublicFooter/DjangoCFGLogo.tsx +45 -0
  6. package/src/layouts/PublicLayout/components/PublicFooter/FooterBottom.tsx +114 -0
  7. package/src/layouts/PublicLayout/components/PublicFooter/FooterMenuSections.tsx +53 -0
  8. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +77 -0
  9. package/src/layouts/PublicLayout/components/PublicFooter/FooterSocialLinks.tsx +82 -0
  10. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +129 -0
  11. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +17 -0
  12. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +57 -0
  13. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +3 -6
  14. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +3 -6
  15. package/src/layouts/PublicLayout/index.ts +12 -1
  16. package/src/layouts/_components/UserMenu.tsx +159 -38
  17. package/src/layouts/index.ts +4 -1
  18. package/src/layouts/shared/README.md +86 -0
  19. package/src/layouts/shared/index.ts +21 -0
  20. package/src/layouts/shared/types.ts +215 -0
  21. package/src/snippets/McpChat/components/AIChatWidget.tsx +150 -53
  22. package/src/snippets/McpChat/components/AskAIButton.tsx +2 -5
  23. package/src/snippets/McpChat/components/ChatMessages.tsx +30 -9
  24. package/src/snippets/McpChat/components/ChatPanel.tsx +1 -1
  25. package/src/snippets/McpChat/components/ChatSidebar.tsx +1 -1
  26. package/src/snippets/McpChat/components/MessageBubble.tsx +46 -34
  27. package/src/snippets/McpChat/context/AIChatContext.tsx +23 -6
  28. package/src/layouts/PublicLayout/components/PublicFooter.tsx +0 -190
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.20",
3
+ "version": "2.1.21",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -92,9 +92,9 @@
92
92
  "check": "tsc --noEmit"
93
93
  },
94
94
  "peerDependencies": {
95
- "@djangocfg/api": "^2.1.20",
96
- "@djangocfg/centrifugo": "^2.1.20",
97
- "@djangocfg/ui-nextjs": "^2.1.20",
95
+ "@djangocfg/api": "^2.1.21",
96
+ "@djangocfg/centrifugo": "^2.1.21",
97
+ "@djangocfg/ui-nextjs": "^2.1.21",
98
98
  "@hookform/resolvers": "^5.2.0",
99
99
  "consola": "^3.4.2",
100
100
  "lucide-react": "^0.545.0",
@@ -114,7 +114,7 @@
114
114
  "uuid": "^11.1.0"
115
115
  },
116
116
  "devDependencies": {
117
- "@djangocfg/typescript-config": "^2.1.20",
117
+ "@djangocfg/typescript-config": "^2.1.21",
118
118
  "@types/node": "^24.7.2",
119
119
  "@types/react": "^19.1.0",
120
120
  "@types/react-dom": "^19.1.0",
@@ -39,11 +39,17 @@
39
39
  import React, { ReactNode, useMemo } from 'react';
40
40
  import { usePathname } from 'next/navigation';
41
41
  import { CentrifugoProvider } from '@djangocfg/centrifugo';
42
- import { type AuthConfig } from '@djangocfg/api/auth';
43
- import { type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/errors/ErrorsTracker';
44
42
  import { AnalyticsProvider } from '../../snippets/Analytics';
45
43
  import { Suspense, ClientOnly } from '../../components/core';
46
44
  import { BaseApp } from './BaseApp';
45
+ import type {
46
+ ThemeConfig,
47
+ LayoutErrorTrackingConfig,
48
+ ErrorBoundaryConfig,
49
+ SWRConfigOptions,
50
+ McpChatConfig,
51
+ } from '../shared/types';
52
+ import type { AuthConfig } from '@djangocfg/api/auth';
47
53
 
48
54
  export type LayoutMode = 'public' | 'private' | 'admin';
49
55
 
@@ -81,60 +87,54 @@ function determineLayoutMode(
81
87
 
82
88
  export interface AppLayoutProps {
83
89
  children: ReactNode;
84
-
90
+
85
91
  /** Public layout component with enabled paths */
86
92
  publicLayout?: {
87
93
  component: React.ComponentType<{ children: ReactNode }>;
88
94
  enabledPath?: string | string[];
89
95
  };
90
-
96
+
91
97
  /** Private layout component with enabled paths */
92
98
  privateLayout?: {
93
99
  component: React.ComponentType<{ children: ReactNode }>;
94
100
  enabledPath?: string | string[];
95
101
  };
96
-
102
+
97
103
  /** Admin layout component with enabled paths */
98
104
  adminLayout?: {
99
105
  component: React.ComponentType<{ children: ReactNode }>;
100
106
  enabledPath?: string | string[];
101
107
  };
102
-
108
+
103
109
  /** Theme configuration */
104
- theme?: {
105
- defaultTheme?: 'light' | 'dark' | 'system';
106
- storageKey?: string;
107
- };
108
-
110
+ theme?: ThemeConfig;
111
+
109
112
  /** Auth configuration */
110
113
  auth?: AuthConfig;
111
-
114
+
112
115
  /** Analytics configuration */
113
116
  analytics?: {
114
117
  googleTrackingId?: string;
115
118
  };
116
-
119
+
117
120
  /** Centrifugo configuration */
118
121
  centrifugo?: {
119
122
  enabled?: boolean;
120
123
  autoConnect?: boolean;
121
124
  url?: string;
122
125
  };
123
-
126
+
124
127
  /** Error tracking configuration */
125
- errorTracking?: {
126
- validation?: Partial<ValidationErrorConfig>;
127
- cors?: Partial<CORSErrorConfig>;
128
- network?: Partial<NetworkErrorConfig>;
129
- onError?: (error: any) => boolean | void;
130
- };
131
-
128
+ errorTracking?: LayoutErrorTrackingConfig;
129
+
130
+ /** SWR configuration */
131
+ swr?: SWRConfigOptions;
132
+
132
133
  /** Error boundary configuration */
133
- errorBoundary?: {
134
- enabled?: boolean;
135
- supportEmail?: string;
136
- onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
137
- };
134
+ errorBoundary?: ErrorBoundaryConfig;
135
+
136
+ /** MCP Chat configuration */
137
+ mcpChat?: McpChatConfig;
138
138
  }
139
139
 
140
140
  /**
@@ -244,7 +244,7 @@ function AppLayoutContent({
244
244
  * AppLayout - Main Component with All Providers
245
245
  */
246
246
  export function AppLayout(props: AppLayoutProps) {
247
- const { theme, auth, errorTracking, errorBoundary } = props;
247
+ const { theme, auth, errorTracking, errorBoundary, swr, mcpChat } = props;
248
248
 
249
249
  return (
250
250
  <BaseApp
@@ -252,6 +252,8 @@ export function AppLayout(props: AppLayoutProps) {
252
252
  auth={auth}
253
253
  errorTracking={errorTracking}
254
254
  errorBoundary={errorBoundary}
255
+ swr={swr}
256
+ mcpChat={mcpChat}
255
257
  >
256
258
  <AppLayoutContent {...props} />
257
259
  </BaseApp>
@@ -8,16 +8,17 @@
8
8
  * - AuthProvider (authentication context)
9
9
  * - ErrorTrackingProvider (error handling)
10
10
  * - ErrorBoundary (React error boundary, enabled by default)
11
+ * - MCP Chat Widget (optional, lazy loaded)
11
12
  *
12
13
  * @example
13
14
  * ```tsx
14
15
  * import { BaseApp } from '@djangocfg/layouts';
15
16
  *
16
- * // ErrorBoundary enabled by default
17
+ * // With MCP Chat enabled
17
18
  * <BaseApp
18
19
  * theme={{ defaultTheme: 'system' }}
19
20
  * auth={{ loginPath: '/auth/login' }}
20
- * errorBoundary={{ supportEmail: 'support@example.com' }}
21
+ * mcpChat={{ enabled: true, autoDetectEnvironment: true }}
21
22
  * >
22
23
  * {children}
23
24
  * </BaseApp>
@@ -32,52 +33,28 @@
32
33
  'use client';
33
34
 
34
35
  import { ReactNode } from 'react';
36
+ import dynamic from 'next/dynamic';
35
37
  import { SWRConfig } from 'swr';
36
38
  import { ThemeProvider, Toaster, TooltipProvider } from '@djangocfg/ui-nextjs';
37
- import { AuthProvider, type AuthConfig } from '@djangocfg/api/auth';
38
- import { ErrorTrackingProvider, type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/errors/ErrorsTracker';
39
+ import { AuthProvider } from '@djangocfg/api/auth';
40
+ import { ErrorTrackingProvider } from '../../components/errors/ErrorsTracker';
39
41
  import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
40
42
  import { PageProgress } from '../../components/core/PageProgress';
43
+ import type { BaseLayoutProps } from '../shared/types';
41
44
 
42
- export interface BaseAppProps {
43
- children: ReactNode;
45
+ // Lazy load MCP Chat Widget with dynamic import
46
+ const AIChatWidget = dynamic(
47
+ () => import('../../snippets/McpChat/components/AIChatWidget').then(mod => ({ default: mod.AIChatWidget })),
48
+ { ssr: false }
49
+ );
44
50
 
45
- /** Theme configuration */
46
- theme?: {
47
- defaultTheme?: 'light' | 'dark' | 'system';
48
- storageKey?: string;
49
- };
50
-
51
- /** Auth configuration */
52
- auth?: AuthConfig;
53
-
54
- /** Error tracking configuration */
55
- errorTracking?: {
56
- validation?: Partial<ValidationErrorConfig>;
57
- cors?: Partial<CORSErrorConfig>;
58
- network?: Partial<NetworkErrorConfig>;
59
- onError?: (error: any) => boolean | void;
60
- };
61
-
62
- /** SWR configuration */
63
- swr?: {
64
- revalidateOnFocus?: boolean;
65
- revalidateOnReconnect?: boolean;
66
- dedupingInterval?: number;
67
- };
68
-
69
- /** Error boundary configuration (enabled by default) */
70
- errorBoundary?: {
71
- enabled?: boolean;
72
- supportEmail?: string;
73
- onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
74
- };
75
- }
51
+ // For backwards compatibility, re-export as BaseAppProps
52
+ export type BaseAppProps = BaseLayoutProps;
76
53
 
77
54
  /**
78
55
  * BaseApp - Core providers wrapper for any React/Next.js app
79
56
  *
80
- * Includes: ThemeProvider, TooltipProvider, SWRConfig, AuthProvider, ErrorTrackingProvider, ErrorBoundary (optional)
57
+ * Includes: ThemeProvider, TooltipProvider, SWRConfig, AuthProvider, ErrorTrackingProvider, ErrorBoundary (optional), MCP Chat (optional)
81
58
  * Also renders global Toaster and PageProgress components
82
59
  */
83
60
  export function BaseApp({
@@ -87,10 +64,16 @@ export function BaseApp({
87
64
  errorTracking,
88
65
  errorBoundary,
89
66
  swr,
67
+ mcpChat,
90
68
  }: BaseAppProps) {
91
69
  // ErrorBoundary is enabled by default
92
70
  const enableErrorBoundary = errorBoundary?.enabled !== false;
93
71
 
72
+ // Debug: log mcpChat config
73
+ if (typeof window !== 'undefined') {
74
+ console.log('[BaseApp] mcpChat config:', mcpChat);
75
+ }
76
+
94
77
  const content = (
95
78
  <ThemeProvider
96
79
  defaultTheme={theme?.defaultTheme || 'system'}
@@ -114,6 +97,21 @@ export function BaseApp({
114
97
  {children}
115
98
  <PageProgress />
116
99
  <Toaster />
100
+
101
+ {/* MCP Chat Widget - lazy loaded */}
102
+ {mcpChat?.enabled && (
103
+ <AIChatWidget
104
+ apiEndpoint={mcpChat.apiEndpoint}
105
+ title={mcpChat.title}
106
+ placeholder={mcpChat.placeholder}
107
+ greeting={mcpChat.greeting}
108
+ position={mcpChat.position}
109
+ variant={mcpChat.variant}
110
+ enableStreaming={mcpChat.enableStreaming}
111
+ autoDetectEnvironment={mcpChat.autoDetectEnvironment}
112
+ className={mcpChat.className}
113
+ />
114
+ )}
117
115
  </ErrorTrackingProvider>
118
116
  </AuthProvider>
119
117
  </SWRConfig>
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Public Layout
3
- *
3
+ *
4
4
  * Simple layout for public pages (home, docs, contact, legal pages)
5
5
  * Import and use directly with props - no complex configs needed!
6
- *
6
+ *
7
7
  * Features:
8
8
  * - Responsive navigation with mobile drawer
9
- * - Footer with links and copyright
10
9
  * - User menu integration
11
- *
10
+ * - No footer (add your own custom footer component)
11
+ *
12
12
  * @example
13
13
  * ```tsx
14
14
  * import { PublicLayout } from '@djangocfg/layouts';
15
- *
15
+ *
16
16
  * <PublicLayout
17
17
  * logo="/logo.svg"
18
18
  * siteName="My App"
@@ -20,9 +20,6 @@
20
20
  * { label: 'Home', href: '/' },
21
21
  * { label: 'Docs', href: '/docs' }
22
22
  * ]}
23
- * footer={{
24
- * links: { privacy: '/privacy', terms: '/terms' }
25
- * }}
26
23
  * >
27
24
  * {children}
28
25
  * </PublicLayout>
@@ -32,30 +29,12 @@
32
29
  'use client';
33
30
 
34
31
  import { ReactNode, useState } from 'react';
32
+ import type { NavigationItem, UserMenuConfig } from '../shared/types';
35
33
  import {
36
34
  PublicNavigation,
37
35
  PublicMobileDrawer,
38
- PublicFooter,
39
36
  } from './components';
40
37
 
41
- export interface NavigationItem {
42
- label: string;
43
- href: string;
44
- icon?: string;
45
- }
46
-
47
- export interface FooterConfig {
48
- links?: {
49
- privacy?: string;
50
- terms?: string;
51
- security?: string;
52
- cookies?: string;
53
- docs?: string;
54
- };
55
- /** Copyright text (optional, auto-generated from siteName if not provided) */
56
- copyright?: string;
57
- }
58
-
59
38
  export interface PublicLayoutProps {
60
39
  children: ReactNode;
61
40
  /** Logo path or URL */
@@ -64,17 +43,8 @@ export interface PublicLayoutProps {
64
43
  siteName?: string;
65
44
  /** Navigation items */
66
45
  navigation?: NavigationItem[];
67
- /** Footer configuration */
68
- footer?: FooterConfig;
69
- /** User menu paths (optional, uses useAuth() for authentication state) */
70
- userMenu?: {
71
- /** Profile page path */
72
- profilePath?: string;
73
- /** Dashboard page path */
74
- dashboardPath?: string;
75
- /** Auth page path (for sign in button) */
76
- authPath?: string;
77
- };
46
+ /** User menu configuration (optional, uses useAuth() for authentication state) */
47
+ userMenu?: UserMenuConfig;
78
48
  }
79
49
 
80
50
  export function PublicLayout({
@@ -82,7 +52,6 @@ export function PublicLayout({
82
52
  logo,
83
53
  siteName = 'App',
84
54
  navigation = [],
85
- footer,
86
55
  userMenu,
87
56
  }: PublicLayoutProps) {
88
57
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
@@ -111,10 +80,7 @@ export function PublicLayout({
111
80
  {/* Main Content */}
112
81
  <main className="flex-1">{children}</main>
113
82
 
114
- {/* Footer */}
115
- {footer && (
116
- <PublicFooter logo={logo} siteName={siteName} footer={footer} />
117
- )}
83
+ {/* Footer - Add your own custom footer component here if needed */}
118
84
  </div>
119
85
  );
120
86
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * DjangoCFG Logo Component
3
+ *
4
+ * Default SVG logo for DjangoCFG
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import React from 'react';
10
+
11
+ export interface DjangoCFGLogoProps {
12
+ className?: string;
13
+ size?: number;
14
+ }
15
+
16
+ export function DjangoCFGLogo({ className, size }: DjangoCFGLogoProps) {
17
+ return (
18
+ <svg
19
+ width={size}
20
+ height={size}
21
+ viewBox="0 0 541 536"
22
+ fill="none"
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ className={className}
25
+ >
26
+ <path
27
+ d="M159.958 408.555C129.071 408.555 102.501 403.407 80.2482 393.111C57.9958 382.483 40.8913 366.707 28.9348 345.783C16.9783 324.527 11 297.791 11 265.575C11 236.348 16.9783 211.272 28.9348 190.348C40.8913 169.092 57.6637 152.818 79.2519 141.526C100.84 130.233 126.248 124.421 155.475 124.089C167.431 124.089 177.893 125.085 186.861 127.078C196.16 128.739 202.969 130.566 207.286 132.558L206.29 185.864C201.972 184.204 196.492 182.543 189.85 180.883C183.207 179.222 174.904 178.392 164.94 178.392C137.374 178.392 116.616 186.031 102.667 201.308C89.0496 216.586 82.241 238.008 82.241 265.575C82.241 284.506 85.0641 300.614 90.7102 313.899C96.6885 327.184 105.656 337.314 117.612 344.289C129.569 350.931 144.847 354.252 163.446 354.252C173.41 354.252 182.045 353.422 189.352 351.761C196.991 350.101 203.633 348.108 209.279 345.783L202.803 364.216C200.146 358.57 198.485 351.761 197.821 343.79C197.157 335.819 196.824 327.184 196.824 317.885V113.627C196.824 105.656 196.658 96.0244 196.326 84.7322C195.994 73.4399 195.496 62.4797 194.832 51.8517C194.167 41.2237 193.171 33.2526 191.843 27.9386L193.337 23.9531H266.571V304.433C266.571 311.076 266.571 319.711 266.571 330.339C266.903 340.635 267.235 350.765 267.567 360.729C268.231 370.693 269.228 378.664 270.556 384.642L269.062 388.627C255.112 394.273 239.004 398.923 220.737 402.577C202.803 406.562 182.543 408.555 159.958 408.555Z"
28
+ fill="currentColor"
29
+ />
30
+ <path
31
+ d="M193.337 23.9531L191.843 27.9385C193.171 33.2525 194.168 41.2236 194.832 51.8516C195.496 62.4796 195.994 73.4401 196.326 84.7324C196.658 96.0245 196.824 105.656 196.824 113.627V129.152C193.901 128.435 190.58 127.742 186.86 127.078C177.893 125.085 167.431 124.089 155.475 124.089C126.248 124.421 100.84 130.233 79.252 141.525L78.2432 142.059C57.1405 153.328 40.7042 169.425 28.9346 190.349C16.9782 211.272 11 236.348 11 265.575L11.0039 267.081C11.1878 298.625 17.1649 324.859 28.9346 345.783C40.8911 366.707 57.9956 382.483 80.248 393.111C102.5 403.407 129.07 408.555 159.958 408.555C182.543 408.555 202.803 406.562 220.737 402.576C239.004 398.923 255.112 394.273 269.062 388.627L270.557 384.642C269.228 378.663 268.232 370.692 267.567 360.729C267.235 350.765 266.903 340.635 266.571 330.339V23.9531H193.337ZM82.2412 265.575C82.2412 238.009 89.0498 216.586 102.667 201.309C116.616 186.031 137.374 178.392 164.94 178.392L166.789 178.401C175.935 178.499 183.622 179.326 189.85 180.883C192.337 181.505 194.663 182.126 196.824 182.748V317.885L196.828 319.62C196.867 328.261 197.199 336.317 197.821 343.79C197.985 345.757 198.21 347.653 198.495 349.478C195.625 350.297 192.577 351.06 189.352 351.762C182.045 353.422 173.409 354.252 163.445 354.252V343.252C172.677 343.252 180.348 342.49 186.584 341.106C186.076 333.821 185.824 326.078 185.824 317.885V191.237C180.514 190.065 173.614 189.392 164.94 189.392C139.406 189.392 122.05 196.41 110.814 208.698C99.5767 221.349 93.2412 239.809 93.2412 265.575C93.2413 283.41 95.8996 297.933 100.788 309.487C105.861 320.71 113.267 328.992 123.06 334.729C132.888 340.16 146.134 343.252 163.445 343.252V354.252C144.846 354.252 129.569 350.931 117.612 344.288C105.656 337.313 96.6882 327.184 90.71 313.899C85.2402 301.029 82.4199 285.51 82.249 267.341L82.2412 265.575ZM277.571 330.152C277.899 340.322 278.227 350.329 278.555 360.174C279.202 369.794 280.145 377.085 281.294 382.256L282.003 385.445L277.666 397.012L273.188 398.824C258.493 404.772 241.727 409.592 222.969 413.348C204.136 417.518 183.107 419.555 159.958 419.555C127.877 419.555 99.6539 414.211 75.6289 403.095L75.5684 403.066L75.5078 403.037C51.2848 391.468 32.4787 374.156 19.3838 351.24L19.3652 351.208L19.3477 351.176C6.21456 327.828 6.03515e-05 299.093 0 265.575C0 234.84 6.29459 207.797 19.3838 184.891C32.3805 161.813 50.6989 144.047 74.1533 131.778C97.5551 119.537 124.732 113.438 155.35 113.09L155.412 113.089H155.475C166.591 113.089 176.734 113.908 185.824 115.636V113.627C185.824 105.798 185.661 96.2805 185.331 85.0557C185.002 73.8772 184.51 63.0382 183.854 52.5381C183.2 42.0782 182.249 34.9186 181.171 30.6064L180.34 27.2832L185.714 12.9531H277.571V330.152Z"
32
+ fill="currentColor"
33
+ opacity="0.2"
34
+ />
35
+ <path
36
+ d="M326.086 273.578L489.086 2.57812L415.586 204.578H532.086L333.586 533.078L415.586 273.578H326.086Z"
37
+ fill="#FFEF0B"
38
+ />
39
+ <path
40
+ d="M493.784 4.28711L422.726 199.577H540.949L337.865 535.663L328.818 531.57L408.763 278.577H317.244L484.801 0L493.784 4.28711ZM334.929 268.577H422.409L350.98 494.621L523.223 209.577H408.446L466.683 49.5254L334.929 268.577Z"
41
+ fill="currentColor"
42
+ />
43
+ </svg>
44
+ );
45
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Footer Bottom Section Component
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import React from 'react';
8
+ import Link from 'next/link';
9
+ import { DjangoCFGLogo } from './DjangoCFGLogo';
10
+ import type { FooterLink } from './types';
11
+
12
+ export interface FooterBottomProps {
13
+ copyright: string;
14
+ credits?: {
15
+ text: string;
16
+ url?: string;
17
+ };
18
+ links?: FooterLink[];
19
+ variant?: 'mobile' | 'desktop';
20
+ }
21
+
22
+ export function FooterBottom({
23
+ copyright,
24
+ credits,
25
+ links = [],
26
+ variant = 'desktop',
27
+ }: FooterBottomProps) {
28
+ const isMobile = variant === 'mobile';
29
+
30
+ if (isMobile) {
31
+ return (
32
+ <div className="border-t border-border pt-4">
33
+ <div className="text-center space-y-2">
34
+ <div className="text-xs text-muted-foreground">{copyright}</div>
35
+ {credits && (
36
+ <div className="text-xs text-muted-foreground flex items-center justify-center gap-1.5">
37
+ {credits.url ? (
38
+ <a
39
+ href={credits.url}
40
+ target="_blank"
41
+ rel="noopener noreferrer"
42
+ className="hover:text-primary transition-colors flex items-center gap-1.5"
43
+ >
44
+ <DjangoCFGLogo size={12} className="text-foreground" />
45
+ {credits.text}
46
+ </a>
47
+ ) : (
48
+ <>
49
+ <DjangoCFGLogo size={12} className="text-foreground" />
50
+ {credits.text}
51
+ </>
52
+ )}
53
+ </div>
54
+ )}
55
+ </div>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <div className="border-t border-border mt-8 pt-6">
62
+ <div className="flex flex-col md:flex-row justify-between items-center space-y-3 md:space-y-0 gap-4">
63
+ <div className="text-xs text-muted-foreground">{copyright}</div>
64
+
65
+ {credits && (
66
+ <div className="text-xs text-muted-foreground flex items-center gap-1.5">
67
+ {credits.url ? (
68
+ <a
69
+ href={credits.url}
70
+ target="_blank"
71
+ rel="noopener noreferrer"
72
+ className="hover:text-primary transition-colors flex items-center gap-1.5"
73
+ >
74
+ <DjangoCFGLogo size={14} className="text-foreground" />
75
+ {credits.text}
76
+ </a>
77
+ ) : (
78
+ <>
79
+ <DjangoCFGLogo size={14} className="text-foreground" />
80
+ {credits.text}
81
+ </>
82
+ )}
83
+ </div>
84
+ )}
85
+
86
+ {links.length > 0 && (
87
+ <div className="flex flex-wrap items-center gap-3 md:gap-4 justify-center md:justify-end">
88
+ {links.map((link) =>
89
+ link.external ? (
90
+ <a
91
+ key={link.path}
92
+ href={link.path}
93
+ target="_blank"
94
+ rel="noopener noreferrer"
95
+ className="text-xs text-muted-foreground hover:text-primary transition-colors"
96
+ >
97
+ {link.label}
98
+ </a>
99
+ ) : (
100
+ <Link
101
+ key={link.path}
102
+ href={link.path}
103
+ className="text-xs text-muted-foreground hover:text-primary transition-colors"
104
+ >
105
+ {link.label}
106
+ </Link>
107
+ )
108
+ )}
109
+ </div>
110
+ )}
111
+ </div>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Footer Menu Sections Component
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import React from 'react';
8
+ import Link from 'next/link';
9
+ import type { FooterMenuSection } from './types';
10
+
11
+ export interface FooterMenuSectionsProps {
12
+ menuSections: FooterMenuSection[];
13
+ }
14
+
15
+ export function FooterMenuSections({ menuSections }: FooterMenuSectionsProps) {
16
+ if (menuSections.length === 0) return null;
17
+
18
+ // Gap in pixels (lg:gap-x-12 = 3rem = 48px)
19
+ const gapPx = 48;
20
+ const sectionCount = menuSections.length;
21
+ const totalGap = (sectionCount - 1) * gapPx;
22
+
23
+ // Each section = 25% of full width, minus proportional part of gap
24
+ const sectionWidth = `calc(25% - ${totalGap / sectionCount}px)`;
25
+
26
+ return (
27
+ <div className="flex flex-1 gap-8 lg:gap-x-12 justify-end">
28
+ {menuSections.map((section) => (
29
+ <div
30
+ key={section.title}
31
+ className="flex-shrink-0 min-w-0"
32
+ style={{ width: sectionWidth }}
33
+ >
34
+ <h3 className="text-base font-semibold text-foreground mb-3">
35
+ {section.title}
36
+ </h3>
37
+ <ul className="space-y-2">
38
+ {section.items.map((item) => (
39
+ <li key={item.path}>
40
+ <Link
41
+ href={item.path}
42
+ className="text-muted-foreground hover:text-primary text-sm transition-colors"
43
+ >
44
+ {item.label}
45
+ </Link>
46
+ </li>
47
+ ))}
48
+ </ul>
49
+ </div>
50
+ ))}
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Footer Project Info Component
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import React from 'react';
8
+ import type { LucideIcon } from 'lucide-react';
9
+ import { DjangoCFGLogo } from './DjangoCFGLogo';
10
+ import { FooterSocialLinksComponent } from './FooterSocialLinks';
11
+ import type { FooterSocialLinks } from './types';
12
+
13
+ export interface FooterProjectInfoProps {
14
+ siteName: string;
15
+ description?: string;
16
+ logo?: string;
17
+ badge?: {
18
+ icon: LucideIcon;
19
+ text: string;
20
+ };
21
+ socialLinks?: FooterSocialLinks;
22
+ variant?: 'mobile' | 'desktop';
23
+ }
24
+
25
+ export function FooterProjectInfo({
26
+ siteName,
27
+ description,
28
+ logo,
29
+ badge,
30
+ socialLinks,
31
+ variant = 'desktop',
32
+ }: FooterProjectInfoProps) {
33
+ const isMobile = variant === 'mobile';
34
+
35
+ return (
36
+ <div className={isMobile ? 'text-center space-y-4 mb-6' : 'space-y-4 lg:flex-shrink-0 lg:w-80'}>
37
+ <div className={isMobile ? 'flex items-center justify-center gap-2' : 'flex items-center gap-2'}>
38
+ {logo ? (
39
+ <div className={isMobile ? 'w-6 h-6 flex items-center justify-center' : 'w-8 h-8 flex items-center justify-center'}>
40
+ <img
41
+ src={logo}
42
+ alt={`${siteName} Logo`}
43
+ className="w-full h-full object-contain"
44
+ />
45
+ </div>
46
+ ) : (
47
+ <DjangoCFGLogo size={isMobile ? 24 : 32} className="text-foreground" />
48
+ )}
49
+ <span className={isMobile ? 'text-lg font-bold text-foreground' : 'text-xl font-bold text-foreground'}>
50
+ {siteName}
51
+ </span>
52
+ </div>
53
+
54
+ {description && (
55
+ <p className={isMobile ? 'text-muted-foreground text-sm leading-relaxed max-w-md mx-auto' : 'text-muted-foreground text-sm leading-relaxed'}>
56
+ {description}
57
+ </p>
58
+ )}
59
+
60
+ {badge && !isMobile && (
61
+ <div className="pt-2">
62
+ <span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-gradient-to-r from-primary/80 to-secondary/60 border border-primary/30 shadow-brand text-xs font-semibold text-primary-foreground">
63
+ <badge.icon className="w-4 h-4" />
64
+ {badge.text}
65
+ </span>
66
+ </div>
67
+ )}
68
+
69
+ {socialLinks && (
70
+ <FooterSocialLinksComponent
71
+ socialLinks={socialLinks}
72
+ className={isMobile ? 'flex justify-center space-x-6' : 'flex space-x-4 pt-4'}
73
+ />
74
+ )}
75
+ </div>
76
+ );
77
+ }