@djangocfg/nextjs 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/package.json +133 -0
- package/src/components/HomePage.tsx +73 -0
- package/src/components/index.ts +7 -0
- package/src/config/base-next-config.ts +262 -0
- package/src/config/index.ts +6 -0
- package/src/contact/index.ts +13 -0
- package/src/contact/route.ts +102 -0
- package/src/contact/submit.ts +80 -0
- package/src/errors/ErrorLayout.tsx +228 -0
- package/src/errors/errorConfig.ts +118 -0
- package/src/errors/index.ts +10 -0
- package/src/health/index.ts +7 -0
- package/src/health/route.ts +65 -0
- package/src/health/types.ts +19 -0
- package/src/index.ts +36 -0
- package/src/legal/LegalPage.tsx +85 -0
- package/src/legal/configs.ts +131 -0
- package/src/legal/index.ts +24 -0
- package/src/legal/pages.tsx +58 -0
- package/src/legal/types.ts +15 -0
- package/src/navigation/index.ts +9 -0
- package/src/navigation/types.ts +68 -0
- package/src/navigation/utils.ts +181 -0
- package/src/og-image/README.md +66 -0
- package/src/og-image/components/DefaultTemplate.tsx +369 -0
- package/src/og-image/components/index.ts +9 -0
- package/src/og-image/index.ts +27 -0
- package/src/og-image/route.tsx +253 -0
- package/src/og-image/types.ts +46 -0
- package/src/og-image/utils/fonts.ts +150 -0
- package/src/og-image/utils/index.ts +28 -0
- package/src/og-image/utils/metadata.ts +235 -0
- package/src/og-image/utils/url.ts +327 -0
- package/src/sitemap/generator.ts +64 -0
- package/src/sitemap/index.ts +8 -0
- package/src/sitemap/route.ts +74 -0
- package/src/sitemap/types.ts +20 -0
- package/src/types.ts +35 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Next.js Configuration Factory
|
|
3
|
+
*
|
|
4
|
+
* Universal, reusable Next.js config for all DjangoCFG projects
|
|
5
|
+
* Provides standard settings that can be extended per project
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // In your project's next.config.ts
|
|
10
|
+
* import { createBaseNextConfig } from '@djangocfg/nextjs/config';
|
|
11
|
+
* import bundleAnalyzer from '@next/bundle-analyzer';
|
|
12
|
+
*
|
|
13
|
+
* const withBundleAnalyzer = bundleAnalyzer({
|
|
14
|
+
* enabled: process.env.ANALYZE === 'true',
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* export default withBundleAnalyzer(createBaseNextConfig({
|
|
18
|
+
* // Your project-specific overrides
|
|
19
|
+
* transpilePackages: ['my-custom-package'],
|
|
20
|
+
* }));
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { NextConfig } from 'next';
|
|
25
|
+
import type { Configuration as WebpackConfig } from 'webpack';
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// Standard Environment Variables
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
|
|
32
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Configuration Options
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export interface BaseNextConfigOptions {
|
|
39
|
+
/** Base path for static builds (default: '/cfg/admin') */
|
|
40
|
+
basePath?: string;
|
|
41
|
+
/** Static build path - used when NEXT_PUBLIC_STATIC_BUILD=true (overrides basePath for static builds) */
|
|
42
|
+
staticBuildPath?: string;
|
|
43
|
+
/** Additional transpile packages (merged with defaults) */
|
|
44
|
+
transpilePackages?: string[];
|
|
45
|
+
/** Additional optimize package imports (merged with defaults) */
|
|
46
|
+
optimizePackageImports?: string[];
|
|
47
|
+
/** Custom webpack configuration function (called after base webpack logic) */
|
|
48
|
+
webpack?: (
|
|
49
|
+
config: WebpackConfig,
|
|
50
|
+
options: { isServer: boolean; dev: boolean; [key: string]: any }
|
|
51
|
+
) => WebpackConfig | void;
|
|
52
|
+
/** Custom experimental options (merged with defaults) */
|
|
53
|
+
experimental?: NextConfig['experimental'];
|
|
54
|
+
/** Custom env variables (merged with defaults) */
|
|
55
|
+
env?: Record<string, string | undefined>;
|
|
56
|
+
/** Override any Next.js config option */
|
|
57
|
+
[key: string]: any;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// Deep Merge Helper (simple implementation)
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
|
|
65
|
+
const output = { ...target };
|
|
66
|
+
|
|
67
|
+
for (const key in source) {
|
|
68
|
+
if (source[key] === undefined) continue;
|
|
69
|
+
|
|
70
|
+
// Arrays: replace (don't merge arrays)
|
|
71
|
+
if (Array.isArray(source[key])) {
|
|
72
|
+
output[key] = source[key] as any;
|
|
73
|
+
}
|
|
74
|
+
// Objects: deep merge
|
|
75
|
+
else if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
76
|
+
const targetValue = output[key];
|
|
77
|
+
if (targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) {
|
|
78
|
+
output[key] = deepMerge(targetValue, source[key] as any);
|
|
79
|
+
} else {
|
|
80
|
+
output[key] = source[key] as any;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Primitives: replace
|
|
84
|
+
else {
|
|
85
|
+
output[key] = source[key] as any;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return output;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
93
|
+
// Base Configuration Factory
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create base Next.js configuration with standard DjangoCFG settings
|
|
98
|
+
*
|
|
99
|
+
* @param options - Custom configuration options to merge with base config
|
|
100
|
+
* @returns Next.js configuration function
|
|
101
|
+
*/
|
|
102
|
+
export function createBaseNextConfig(
|
|
103
|
+
options: BaseNextConfigOptions = {}
|
|
104
|
+
): NextConfig {
|
|
105
|
+
// Determine basePath: staticBuildPath takes precedence for static builds, then basePath, then default
|
|
106
|
+
const basePath = isStaticBuild
|
|
107
|
+
? (options.staticBuildPath || options.basePath)
|
|
108
|
+
: (options.basePath || '');
|
|
109
|
+
|
|
110
|
+
// Base configuration
|
|
111
|
+
const baseConfig: NextConfig = {
|
|
112
|
+
reactStrictMode: true,
|
|
113
|
+
trailingSlash: true,
|
|
114
|
+
|
|
115
|
+
// Static export configuration
|
|
116
|
+
...(isStaticBuild && {
|
|
117
|
+
output: 'export' as const,
|
|
118
|
+
distDir: 'out',
|
|
119
|
+
basePath,
|
|
120
|
+
assetPrefix: basePath,
|
|
121
|
+
// Fix for Next.js 15.5.4: prevent 500.html generation issue
|
|
122
|
+
//
|
|
123
|
+
// PROBLEM: In App Router, error.tsx is a client component ('use client')
|
|
124
|
+
// and cannot be statically exported as 500.html. Next.js tries to create
|
|
125
|
+
// and move 500.html during static export, causing ENOENT error.
|
|
126
|
+
//
|
|
127
|
+
// SOLUTION: Use generateBuildId to work around the issue.
|
|
128
|
+
// The error.tsx component will still work in development/runtime mode.
|
|
129
|
+
generateBuildId: async () => {
|
|
130
|
+
return 'static-build';
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
// Standalone output for Docker (only in production, not dev)
|
|
135
|
+
...(!isStaticBuild && !isDev && {
|
|
136
|
+
output: 'standalone' as const,
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
// Environment variables
|
|
140
|
+
env: {
|
|
141
|
+
NEXT_PUBLIC_BASE_PATH: basePath,
|
|
142
|
+
NEXT_PUBLIC_API_URL: isStaticBuild ? '' : process.env.NEXT_PUBLIC_API_URL,
|
|
143
|
+
...options.env,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Images configuration
|
|
147
|
+
images: {
|
|
148
|
+
unoptimized: true,
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// Transpile packages (merge with user-provided)
|
|
152
|
+
transpilePackages: [
|
|
153
|
+
'@djangocfg/ui',
|
|
154
|
+
'@djangocfg/layouts',
|
|
155
|
+
'@djangocfg/nextjs',
|
|
156
|
+
'@djangocfg/api',
|
|
157
|
+
'@djangocfg/centrifugo',
|
|
158
|
+
...(options.transpilePackages || []),
|
|
159
|
+
],
|
|
160
|
+
|
|
161
|
+
// Experimental features
|
|
162
|
+
// Base optimizations first, then user options (user can override base settings)
|
|
163
|
+
experimental: {
|
|
164
|
+
// Optimize package imports (only in production)
|
|
165
|
+
...(!isDev && {
|
|
166
|
+
optimizePackageImports: [
|
|
167
|
+
'@djangocfg/ui',
|
|
168
|
+
'@djangocfg/layouts',
|
|
169
|
+
'lucide-react',
|
|
170
|
+
'recharts',
|
|
171
|
+
...(options.optimizePackageImports || []),
|
|
172
|
+
],
|
|
173
|
+
}),
|
|
174
|
+
// Dev mode optimizations
|
|
175
|
+
...(isDev && {
|
|
176
|
+
optimizeCss: false,
|
|
177
|
+
}),
|
|
178
|
+
// User experimental options applied last (can override base settings)
|
|
179
|
+
...options.experimental,
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Webpack configuration
|
|
183
|
+
webpack: (config: WebpackConfig, webpackOptions: { isServer: boolean; dev: boolean; [key: string]: any }) => {
|
|
184
|
+
const { isServer, dev } = webpackOptions;
|
|
185
|
+
|
|
186
|
+
// Dev mode optimizations
|
|
187
|
+
if (dev) {
|
|
188
|
+
config.optimization = {
|
|
189
|
+
...config.optimization,
|
|
190
|
+
removeAvailableModules: false,
|
|
191
|
+
removeEmptyChunks: false,
|
|
192
|
+
splitChunks: false, // Disable code splitting in dev for faster compilation
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Filesystem cache (dev and production)
|
|
197
|
+
config.cache = {
|
|
198
|
+
type: 'filesystem',
|
|
199
|
+
buildDependencies: {},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Compression plugins (only for static build in production)
|
|
203
|
+
// Note: compression-webpack-plugin should be installed in the consuming project
|
|
204
|
+
if (!isServer && isStaticBuild && !dev) {
|
|
205
|
+
try {
|
|
206
|
+
// Dynamic import to avoid bundling compression-webpack-plugin
|
|
207
|
+
const CompressionPlugin = require('compression-webpack-plugin');
|
|
208
|
+
|
|
209
|
+
// Gzip compression
|
|
210
|
+
config.plugins.push(
|
|
211
|
+
new CompressionPlugin({
|
|
212
|
+
filename: '[path][base].gz',
|
|
213
|
+
algorithm: 'gzip',
|
|
214
|
+
test: /\.(js|css|html|svg|json)$/,
|
|
215
|
+
threshold: 8192,
|
|
216
|
+
minRatio: 0.8,
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Brotli compression (balanced level for speed/size)
|
|
221
|
+
config.plugins.push(
|
|
222
|
+
new CompressionPlugin({
|
|
223
|
+
filename: '[path][base].br',
|
|
224
|
+
algorithm: 'brotliCompress',
|
|
225
|
+
test: /\.(js|css|html|svg|json)$/,
|
|
226
|
+
threshold: 8192,
|
|
227
|
+
minRatio: 0.8,
|
|
228
|
+
compressionOptions: {
|
|
229
|
+
level: 8, // Balanced: good compression without excessive build time
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// compression-webpack-plugin not installed, skip compression
|
|
235
|
+
console.warn('compression-webpack-plugin not found, skipping compression');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Call user's webpack function if provided
|
|
240
|
+
if (options.webpack) {
|
|
241
|
+
return options.webpack(config, webpackOptions);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return config;
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Deep merge user options with base config
|
|
250
|
+
const finalConfig = deepMerge(baseConfig, options);
|
|
251
|
+
|
|
252
|
+
// Cleanup: Remove our custom options that are not part of NextConfig
|
|
253
|
+
// These are internal to BaseNextConfigOptions and should not be in the final config
|
|
254
|
+
delete (finalConfig as any).basePath;
|
|
255
|
+
delete (finalConfig as any).optimizePackageImports;
|
|
256
|
+
// Note: We don't delete transpilePackages, experimental, env, webpack
|
|
257
|
+
// as they are valid NextConfig keys and may have been overridden by user
|
|
258
|
+
|
|
259
|
+
// Return clean NextConfig object (user should wrap with withBundleAnalyzer in their next.config.ts)
|
|
260
|
+
return finalConfig;
|
|
261
|
+
}
|
|
262
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Form Utilities
|
|
3
|
+
*
|
|
4
|
+
* Server-side utilities for contact form submission
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { submitContactForm } from './submit';
|
|
8
|
+
export type { SubmitContactFormOptions, SubmitContactFormResult } from './submit';
|
|
9
|
+
|
|
10
|
+
// Route handler
|
|
11
|
+
export { POST, createContactRoute } from './route';
|
|
12
|
+
export type { ContactRouteOptions } from './route';
|
|
13
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Form API Route Handler
|
|
3
|
+
*
|
|
4
|
+
* Ready-to-use Next.js API route handler for contact form submissions.
|
|
5
|
+
* Proxies requests to backend API to avoid CORS issues.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // In app/api/contact/route.ts
|
|
10
|
+
* export { POST } from '@djangocfg/nextjs/contact/route';
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
15
|
+
import { submitContactForm } from './submit';
|
|
16
|
+
import type { Schemas } from '@djangocfg/api';
|
|
17
|
+
|
|
18
|
+
export interface ContactRouteOptions {
|
|
19
|
+
/** Backend API base URL (defaults to process.env.NEXT_PUBLIC_API_URL) */
|
|
20
|
+
apiUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create contact form route handler with custom options
|
|
25
|
+
*/
|
|
26
|
+
export function createContactRoute(options?: ContactRouteOptions) {
|
|
27
|
+
return async function POST(request: NextRequest) {
|
|
28
|
+
try {
|
|
29
|
+
// Parse request body
|
|
30
|
+
const body: any = await request.json();
|
|
31
|
+
|
|
32
|
+
// Extract apiUrl from request body (passed from ContactFormProvider)
|
|
33
|
+
// or use from options/environment as fallback
|
|
34
|
+
const apiUrl = (body._apiUrl && body._apiUrl !== '')
|
|
35
|
+
? body._apiUrl
|
|
36
|
+
: (options?.apiUrl || process.env.NEXT_PUBLIC_API_URL || '');
|
|
37
|
+
|
|
38
|
+
if (!apiUrl) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{
|
|
41
|
+
success: false,
|
|
42
|
+
message: 'API URL not configured. Set NEXT_PUBLIC_API_URL, provide apiUrl option, or pass _apiUrl in request body.',
|
|
43
|
+
},
|
|
44
|
+
{ status: 500 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Remove _apiUrl from body before submitting
|
|
49
|
+
const { _apiUrl, ...submissionData } = body;
|
|
50
|
+
|
|
51
|
+
// Submit using smart wrapper from @djangocfg/nextjs
|
|
52
|
+
const response = await submitContactForm({
|
|
53
|
+
data: submissionData as Schemas.LeadSubmissionRequest,
|
|
54
|
+
apiUrl,
|
|
55
|
+
siteUrl: request.headers.get('origin') || undefined,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return NextResponse.json(response);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Contact form submission error:', error);
|
|
61
|
+
|
|
62
|
+
// Handle validation errors (400)
|
|
63
|
+
if (error instanceof Error && error.message.includes('Missing required fields')) {
|
|
64
|
+
return NextResponse.json(
|
|
65
|
+
{
|
|
66
|
+
success: false,
|
|
67
|
+
message: error.message,
|
|
68
|
+
},
|
|
69
|
+
{ status: 400 }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle API errors
|
|
74
|
+
if (error instanceof Error) {
|
|
75
|
+
return NextResponse.json(
|
|
76
|
+
{
|
|
77
|
+
success: false,
|
|
78
|
+
message: error.message || 'Failed to submit contact form',
|
|
79
|
+
},
|
|
80
|
+
{ status: 500 }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{
|
|
86
|
+
success: false,
|
|
87
|
+
message: 'An unexpected error occurred',
|
|
88
|
+
},
|
|
89
|
+
{ status: 500 }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Default POST handler (uses process.env.NEXT_PUBLIC_API_URL)
|
|
97
|
+
*/
|
|
98
|
+
export async function POST(request: NextRequest) {
|
|
99
|
+
const handler = createContactRoute();
|
|
100
|
+
return handler(request);
|
|
101
|
+
}
|
|
102
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Form Submission - Server-side function
|
|
3
|
+
*
|
|
4
|
+
* Server-side function to submit contact form data to backend API.
|
|
5
|
+
* Can be used in Next.js API routes to avoid CORS issues.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { submitContactForm } from '@djangocfg/nextjs/contact';
|
|
10
|
+
*
|
|
11
|
+
* const result = await submitContactForm({
|
|
12
|
+
* name: 'John Doe',
|
|
13
|
+
* email: 'john@example.com',
|
|
14
|
+
* message: 'Hello!',
|
|
15
|
+
* apiUrl: 'https://api.example.com'
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { API, MemoryStorageAdapter, Fetchers } from '@djangocfg/api';
|
|
21
|
+
import type { Schemas } from '@djangocfg/api';
|
|
22
|
+
|
|
23
|
+
export interface SubmitContactFormOptions {
|
|
24
|
+
/** Lead submission data */
|
|
25
|
+
data: Schemas.LeadSubmissionRequest;
|
|
26
|
+
/** Backend API base URL */
|
|
27
|
+
apiUrl: string;
|
|
28
|
+
/** Optional site URL (auto-detected from origin if not provided) */
|
|
29
|
+
siteUrl?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SubmitContactFormResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
message: string;
|
|
35
|
+
lead_id?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Submit contact form data to backend API
|
|
40
|
+
*
|
|
41
|
+
* Server-side function that uses the typed fetcher for type safety
|
|
42
|
+
* and runtime validation via Zod schemas.
|
|
43
|
+
*/
|
|
44
|
+
export async function submitContactForm({
|
|
45
|
+
data,
|
|
46
|
+
apiUrl,
|
|
47
|
+
siteUrl,
|
|
48
|
+
}: SubmitContactFormOptions): Promise<SubmitContactFormResult> {
|
|
49
|
+
// Validate required fields
|
|
50
|
+
if (!data.name || !data.email || !data.message) {
|
|
51
|
+
throw new Error('Missing required fields: name, email, message');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!apiUrl) {
|
|
55
|
+
throw new Error('API URL is required');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create server-side API instance with MemoryStorageAdapter
|
|
59
|
+
// This works on the server because MemoryStorageAdapter is not client-only
|
|
60
|
+
const serverApi = new API(apiUrl, {
|
|
61
|
+
storage: new MemoryStorageAdapter(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Prepare submission data with site_url
|
|
65
|
+
const submissionData: Schemas.LeadSubmissionRequest = {
|
|
66
|
+
...data,
|
|
67
|
+
site_url: data.site_url || siteUrl,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Use typed fetcher with server API instance
|
|
71
|
+
// This provides type safety and runtime validation via Zod
|
|
72
|
+
const result = await Fetchers.createLeadsSubmitCreate(submissionData, serverApi);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: result.success,
|
|
76
|
+
message: result.message,
|
|
77
|
+
lead_id: result.lead_id,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorLayout - Universal Error Display
|
|
3
|
+
*
|
|
4
|
+
* Minimalist error page with customizable content
|
|
5
|
+
* Works with Next.js error pages (404.tsx, 500.tsx, error.tsx)
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // app/not-found.tsx
|
|
10
|
+
* import { ErrorLayout } from '@djangocfg/nextjs/errors';
|
|
11
|
+
*
|
|
12
|
+
* export default function NotFound() {
|
|
13
|
+
* return <ErrorLayout code={404} supportEmail={settings.contact.email} />;
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use client';
|
|
19
|
+
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import { Button } from '@djangocfg/ui/components';
|
|
22
|
+
import { getErrorContent } from './errorConfig';
|
|
23
|
+
|
|
24
|
+
export interface ErrorLayoutProps {
|
|
25
|
+
/** Error code (e.g., "404", "500", "403") - if provided, auto-configures title/description/icon */
|
|
26
|
+
code?: string | number;
|
|
27
|
+
/** Error title (auto-generated from code if not provided) */
|
|
28
|
+
title?: string;
|
|
29
|
+
/** Error description (auto-generated from code if not provided) */
|
|
30
|
+
description?: string;
|
|
31
|
+
/** Custom action buttons */
|
|
32
|
+
actions?: React.ReactNode;
|
|
33
|
+
/** Show default actions (back, home) */
|
|
34
|
+
showDefaultActions?: boolean;
|
|
35
|
+
/** Custom illustration/icon (auto-generated from code if not provided) */
|
|
36
|
+
illustration?: React.ReactNode;
|
|
37
|
+
/** Support email for contact link */
|
|
38
|
+
supportEmail?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Local function to select the icon based on the code.
|
|
42
|
+
// This is safe as it's defined and used inside a Client Component.
|
|
43
|
+
function getErrorIcon(code?: string | number): React.ReactNode {
|
|
44
|
+
const c = code ? String(code) : '';
|
|
45
|
+
|
|
46
|
+
// NOTE: You can replace these SVG paths with imported Lucid Icons
|
|
47
|
+
// (e.g., <AlertTriangle />) if you prefer.
|
|
48
|
+
switch (c) {
|
|
49
|
+
case '404':
|
|
50
|
+
return (
|
|
51
|
+
<svg
|
|
52
|
+
className="w-24 h-24 mx-auto text-muted-foreground/50"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
viewBox="0 0 24 24"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
{/* Missing Page Icon */}
|
|
59
|
+
<path
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
strokeWidth={1.5}
|
|
63
|
+
d="M9.343 3.07a7.227 7.227 0 0111.558 0c.806.515 1.393 1.39 1.393 2.37v6.636c0 .98-.587 1.855-1.393 2.37a7.227 7.227 0 01-11.558 0c-.806-.515-1.393-1.39-1.393-2.37V5.44c0-.98.587-1.855 1.393-2.37zM12 13a1 1 0 100-2 1 1 0 000 2z"
|
|
64
|
+
/>
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
case '500':
|
|
68
|
+
return (
|
|
69
|
+
<svg
|
|
70
|
+
className="w-24 h-24 mx-auto text-muted-foreground/50"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
>
|
|
76
|
+
{/* Server Error Icon */}
|
|
77
|
+
<path
|
|
78
|
+
strokeLinecap="round"
|
|
79
|
+
strokeLinejoin="round"
|
|
80
|
+
strokeWidth={1.5}
|
|
81
|
+
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 0l-6.849 6.849m0 0l-6.849-6.849m6.849 6.849V21m0 0h7.5M12 21v-7.5M7.5 21H3m7.5 0h7.5M3 18V9a4.5 4.5 0 014.5-4.5h9A4.5 4.5 0 0121 9v9a4.5 4.5 0 01-4.5 4.5h-9A4.5 4.5 0 013 18z"
|
|
82
|
+
/>
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
85
|
+
case '403':
|
|
86
|
+
return (
|
|
87
|
+
<svg
|
|
88
|
+
className="w-24 h-24 mx-auto text-muted-foreground/50"
|
|
89
|
+
fill="none"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
viewBox="0 0 24 24"
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
>
|
|
94
|
+
{/* Forbidden Icon */}
|
|
95
|
+
<path
|
|
96
|
+
strokeLinecap="round"
|
|
97
|
+
strokeLinejoin="round"
|
|
98
|
+
strokeWidth={1.5}
|
|
99
|
+
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
|
100
|
+
/>
|
|
101
|
+
</svg>
|
|
102
|
+
);
|
|
103
|
+
default:
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* ErrorLayout Component
|
|
110
|
+
*/
|
|
111
|
+
export function ErrorLayout({
|
|
112
|
+
code,
|
|
113
|
+
title,
|
|
114
|
+
description,
|
|
115
|
+
actions,
|
|
116
|
+
showDefaultActions = true,
|
|
117
|
+
illustration,
|
|
118
|
+
supportEmail = 'support@example.com',
|
|
119
|
+
}: ErrorLayoutProps) {
|
|
120
|
+
|
|
121
|
+
// Get content (Title/Description) from config. Note: Illustration check removed.
|
|
122
|
+
// The function getErrorContent MUST NOT return React components/functions.
|
|
123
|
+
const autoContent = code && (!title || !description)
|
|
124
|
+
? getErrorContent(code)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
// Fallback to auto-generated values
|
|
128
|
+
const finalTitle = title || autoContent?.title || 'Error';
|
|
129
|
+
const finalDescription = description || autoContent?.description;
|
|
130
|
+
|
|
131
|
+
// ILLUSTRATION FIX: Use passed prop OR compute the icon locally using getErrorIcon.
|
|
132
|
+
const finalIllustration = illustration ?? getErrorIcon(code);
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
const handleGoBack = () => {
|
|
136
|
+
if (document.referrer && document.referrer !== window.location.href) {
|
|
137
|
+
window.location.href = document.referrer;
|
|
138
|
+
} else if (window.history.length > 1) {
|
|
139
|
+
window.history.back();
|
|
140
|
+
} else {
|
|
141
|
+
window.location.href = '/';
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleGoHome = () => {
|
|
146
|
+
window.location.href = '/';
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="min-h-screen flex items-center justify-center px-4 bg-background">
|
|
151
|
+
<div className="max-w-2xl w-full text-center space-y-8">
|
|
152
|
+
{/* Error Code */}
|
|
153
|
+
{code && (
|
|
154
|
+
<div className="relative">
|
|
155
|
+
<h1
|
|
156
|
+
className="text-[12rem] font-bold leading-none text-muted/20 select-none"
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
>
|
|
159
|
+
{code}
|
|
160
|
+
</h1>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Illustration */}
|
|
165
|
+
{finalIllustration && (
|
|
166
|
+
<div className="flex justify-center py-8">
|
|
167
|
+
{finalIllustration}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Error Content */}
|
|
172
|
+
<div className="space-y-4">
|
|
173
|
+
<h2 className="text-4xl font-bold text-foreground">
|
|
174
|
+
{finalTitle}
|
|
175
|
+
</h2>
|
|
176
|
+
|
|
177
|
+
{finalDescription && (
|
|
178
|
+
<p className="text-lg text-muted-foreground max-w-md mx-auto">
|
|
179
|
+
{finalDescription}
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Actions */}
|
|
185
|
+
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
|
|
186
|
+
{/* Custom actions */}
|
|
187
|
+
{actions}
|
|
188
|
+
|
|
189
|
+
{/* Default actions */}
|
|
190
|
+
{showDefaultActions && !actions && (
|
|
191
|
+
<>
|
|
192
|
+
<Button
|
|
193
|
+
variant="outline"
|
|
194
|
+
size="lg"
|
|
195
|
+
onClick={handleGoBack}
|
|
196
|
+
style={{ minWidth: '140px', padding: '12px 32px' }}
|
|
197
|
+
>
|
|
198
|
+
Go Back
|
|
199
|
+
</Button>
|
|
200
|
+
<Button
|
|
201
|
+
variant="default"
|
|
202
|
+
size="lg"
|
|
203
|
+
onClick={handleGoHome}
|
|
204
|
+
style={{ minWidth: '140px', padding: '12px 32px' }}
|
|
205
|
+
>
|
|
206
|
+
Go Home
|
|
207
|
+
</Button>
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Additional Info */}
|
|
213
|
+
<div className="pt-8 text-sm text-muted-foreground">
|
|
214
|
+
<p>
|
|
215
|
+
Need help? Contact{' '}
|
|
216
|
+
<a
|
|
217
|
+
href={`mailto:${supportEmail}`}
|
|
218
|
+
className="text-primary hover:underline"
|
|
219
|
+
>
|
|
220
|
+
support
|
|
221
|
+
</a>
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|