@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/package.json +133 -0
  4. package/src/components/HomePage.tsx +73 -0
  5. package/src/components/index.ts +7 -0
  6. package/src/config/base-next-config.ts +262 -0
  7. package/src/config/index.ts +6 -0
  8. package/src/contact/index.ts +13 -0
  9. package/src/contact/route.ts +102 -0
  10. package/src/contact/submit.ts +80 -0
  11. package/src/errors/ErrorLayout.tsx +228 -0
  12. package/src/errors/errorConfig.ts +118 -0
  13. package/src/errors/index.ts +10 -0
  14. package/src/health/index.ts +7 -0
  15. package/src/health/route.ts +65 -0
  16. package/src/health/types.ts +19 -0
  17. package/src/index.ts +36 -0
  18. package/src/legal/LegalPage.tsx +85 -0
  19. package/src/legal/configs.ts +131 -0
  20. package/src/legal/index.ts +24 -0
  21. package/src/legal/pages.tsx +58 -0
  22. package/src/legal/types.ts +15 -0
  23. package/src/navigation/index.ts +9 -0
  24. package/src/navigation/types.ts +68 -0
  25. package/src/navigation/utils.ts +181 -0
  26. package/src/og-image/README.md +66 -0
  27. package/src/og-image/components/DefaultTemplate.tsx +369 -0
  28. package/src/og-image/components/index.ts +9 -0
  29. package/src/og-image/index.ts +27 -0
  30. package/src/og-image/route.tsx +253 -0
  31. package/src/og-image/types.ts +46 -0
  32. package/src/og-image/utils/fonts.ts +150 -0
  33. package/src/og-image/utils/index.ts +28 -0
  34. package/src/og-image/utils/metadata.ts +235 -0
  35. package/src/og-image/utils/url.ts +327 -0
  36. package/src/sitemap/generator.ts +64 -0
  37. package/src/sitemap/index.ts +8 -0
  38. package/src/sitemap/route.ts +74 -0
  39. package/src/sitemap/types.ts +20 -0
  40. 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,6 @@
1
+ /**
2
+ * Next.js Configuration Utilities
3
+ */
4
+
5
+ export { createBaseNextConfig, type BaseNextConfigOptions } from './base-next-config';
6
+
@@ -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
+