@jhits/dashboard 0.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 (63) hide show
  1. package/README.md +36 -0
  2. package/next.config.ts +32 -0
  3. package/package.json +79 -0
  4. package/postcss.config.mjs +7 -0
  5. package/src/api/README.md +72 -0
  6. package/src/api/masterRouter.ts +150 -0
  7. package/src/api/pluginRouter.ts +135 -0
  8. package/src/app/[locale]/(auth)/layout.tsx +30 -0
  9. package/src/app/[locale]/(auth)/login/page.tsx +201 -0
  10. package/src/app/[locale]/catch-all/page.tsx +10 -0
  11. package/src/app/[locale]/dashboard/[...pluginRoute]/page.tsx +98 -0
  12. package/src/app/[locale]/dashboard/layout.tsx +42 -0
  13. package/src/app/[locale]/dashboard/page.tsx +121 -0
  14. package/src/app/[locale]/dashboard/preferences/page.tsx +295 -0
  15. package/src/app/[locale]/dashboard/profile/page.tsx +491 -0
  16. package/src/app/[locale]/layout.tsx +28 -0
  17. package/src/app/actions/preferences.ts +40 -0
  18. package/src/app/actions/user.ts +191 -0
  19. package/src/app/api/auth/[...nextauth]/route.ts +6 -0
  20. package/src/app/api/plugin-images/list/route.ts +96 -0
  21. package/src/app/api/plugin-images/upload/route.ts +88 -0
  22. package/src/app/api/telemetry/log/route.ts +10 -0
  23. package/src/app/api/telemetry/route.ts +12 -0
  24. package/src/app/api/uploads/[filename]/route.ts +33 -0
  25. package/src/app/globals.css +181 -0
  26. package/src/app/layout.tsx +4 -0
  27. package/src/assets/locales/en/common.json +47 -0
  28. package/src/assets/locales/nl/common.json +48 -0
  29. package/src/assets/locales/sv/common.json +48 -0
  30. package/src/assets/plugins.json +42 -0
  31. package/src/assets/public/Logo_JH_black.jpg +0 -0
  32. package/src/assets/public/Logo_JH_black.png +0 -0
  33. package/src/assets/public/Logo_JH_white.png +0 -0
  34. package/src/assets/public/animated-logo-white.svg +5 -0
  35. package/src/assets/public/logo_black.svg +5 -0
  36. package/src/assets/public/logo_white.svg +5 -0
  37. package/src/assets/public/noimagefound.jpg +0 -0
  38. package/src/components/DashboardCatchAll.tsx +95 -0
  39. package/src/components/DashboardRootLayout.tsx +37 -0
  40. package/src/components/PluginNotFound.tsx +24 -0
  41. package/src/components/Providers.tsx +59 -0
  42. package/src/components/dashboard/Sidebar.tsx +263 -0
  43. package/src/components/dashboard/Topbar.tsx +363 -0
  44. package/src/components/page.tsx +130 -0
  45. package/src/config.ts +230 -0
  46. package/src/i18n/navigation.ts +7 -0
  47. package/src/i18n/request.ts +41 -0
  48. package/src/i18n/routing.ts +35 -0
  49. package/src/i18n/translations.ts +20 -0
  50. package/src/index.tsx +69 -0
  51. package/src/lib/auth.ts +159 -0
  52. package/src/lib/db.ts +11 -0
  53. package/src/lib/get-website-info.ts +78 -0
  54. package/src/lib/modules-config.ts +68 -0
  55. package/src/lib/mongodb.ts +32 -0
  56. package/src/lib/plugin-registry.tsx +77 -0
  57. package/src/lib/website-context.tsx +39 -0
  58. package/src/proxy.ts +55 -0
  59. package/src/router.tsx +45 -0
  60. package/src/routes.tsx +3 -0
  61. package/src/server.ts +8 -0
  62. package/src/types/plugin.ts +24 -0
  63. package/src/types/preferences.ts +13 -0
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
package/next.config.ts ADDED
@@ -0,0 +1,32 @@
1
+ import createNextIntlPlugin from 'next-intl/plugin';
2
+
3
+ const withNextIntl = createNextIntlPlugin(
4
+ './src/i18n/request.ts' // Path to your config file
5
+ );
6
+
7
+ /** @type {import('next').NextConfig} */
8
+ const nextConfig = {
9
+ logging: {
10
+ fetches: {
11
+ fullUrl: true,
12
+ },
13
+ },
14
+ allowedDevOrigins: ["localhost:3000", "192.168.68.104:3000"],
15
+ // Exclude server-only modules from client bundles
16
+ serverComponentsExternalPackages: [
17
+ '@jhits/plugin-telemetry/api/handler'
18
+ ],
19
+ webpack: (config, { isServer }) => {
20
+ // Ensure handler is only bundled on server
21
+ if (!isServer) {
22
+ config.resolve.fallback = {
23
+ ...config.resolve.fallback,
24
+ fs: false,
25
+ path: false,
26
+ };
27
+ }
28
+ return config;
29
+ },
30
+ };
31
+
32
+ export default withNextIntl(nextConfig);
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@jhits/dashboard",
3
+ "version": "0.0.1",
4
+ "description": "A comprehensive dashboard system built to manage custom built websites - plugin based SaaS system.",
5
+ "main": "./src/index.tsx",
6
+ "types": "./src/index.tsx",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.tsx",
10
+ "default": "./src/index.tsx"
11
+ },
12
+ "./config": {
13
+ "types": "./src/config.ts",
14
+ "default": "./src/config.ts"
15
+ },
16
+ "./server": {
17
+ "types": "./src/server.ts",
18
+ "default": "./src/server.ts",
19
+ "node": "./src/server.ts"
20
+ },
21
+ "./catch-all": {
22
+ "types": "./src/app/[locale]/catch-all/page.tsx",
23
+ "default": "./src/app/[locale]/catch-all/page.tsx"
24
+ },
25
+ "./components/*": "./src/components/*",
26
+ "./lib/*": "./src/lib/*",
27
+ "./lib/auth": {
28
+ "types": "./src/lib/auth.ts",
29
+ "default": "./src/lib/auth.ts"
30
+ }
31
+ },
32
+ "scripts": {
33
+ "lint": "eslint"
34
+ },
35
+ "dependencies": {
36
+ "@jhits/plugin-blog": "^0.0.1",
37
+ "@jhits/plugin-content": "^0.0.1",
38
+ "@jhits/plugin-dep": "^0.0.1",
39
+ "@jhits/plugin-images": "^0.0.1",
40
+ "@jhits/plugin-telemetry": "^0.0.1",
41
+ "@jhits/plugin-users": "^0.0.1",
42
+ "@jhits/plugin-website": "^0.0.1",
43
+ "@jhits/plugin-newsletter": "^0.0.1",
44
+ "@types/jsonwebtoken": "^9.0.10",
45
+ "bcrypt": "^6.0.0",
46
+ "framer-motion": "^12.23.26",
47
+ "jsonwebtoken": "^9.0.3",
48
+ "lucide-react": "^0.562.0",
49
+ "mongodb": "^7.0.0",
50
+ "next-auth": "^4.24.13",
51
+ "next-intl": "^4.6.1",
52
+ "next-themes": "^0.4.6"
53
+ },
54
+ "peerDependencies": {
55
+ "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
56
+ "react": "^18.0.0 || ^19.0.0",
57
+ "react-dom": "^18.0.0 || ^19.0.0"
58
+ },
59
+ "devDependencies": {
60
+ "@tailwindcss/postcss": "^4",
61
+ "@types/bcrypt": "^6.0.0",
62
+ "@types/node": "^20.19.27",
63
+ "@types/react": "^19",
64
+ "@types/react-dom": "^19",
65
+ "eslint": "^9",
66
+ "eslint-config-next": "16.1.1",
67
+ "next": "16.1.1",
68
+ "react": "19.2.3",
69
+ "react-dom": "19.2.3",
70
+ "tailwindcss": "^4",
71
+ "typescript": "^5"
72
+ },
73
+ "files": [
74
+ "src",
75
+ "next.config.ts",
76
+ "postcss.config.mjs",
77
+ "tailwind.config.ts"
78
+ ]
79
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,72 @@
1
+ # Dashboard API Master Router
2
+
3
+ Centralized API routing system for all dashboard plugin endpoints.
4
+
5
+ ## Overview
6
+
7
+ The master router automatically routes API requests to the appropriate plugin handler based on the URL path prefix. This eliminates the need for the client app to know about individual plugin endpoints.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ Client App
13
+ └── /api/dashboard/[...path]/route.ts (catch-all)
14
+ └── handleDashboardApi() (master router)
15
+ ├── /telemetry → @jhits/plugin-telemetry/api/route
16
+ ├── /blog → @jhits/plugin-blog/api/route (TODO)
17
+ └── /users → @jhits/plugin-users/api/route (TODO)
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### In Client App
23
+
24
+ Create a catch-all route at `src/app/api/dashboard/[...path]/route.ts`:
25
+
26
+ ```typescript
27
+ import { NextRequest } from 'next/server';
28
+ import { handleDashboardApi } from '@jhits/dashboard/server';
29
+
30
+ export async function POST(
31
+ req: NextRequest,
32
+ { params }: { params: Promise<{ path: string[] }> }
33
+ ) {
34
+ const { path } = await params;
35
+ return handleDashboardApi(req, path);
36
+ }
37
+
38
+ // Add GET, PUT, DELETE, PATCH as needed
39
+ ```
40
+
41
+ ### Adding New Plugin Handlers
42
+
43
+ 1. Create your plugin handler in `@jhits/plugin-{name}/api/route.ts`:
44
+ ```typescript
45
+ import { NextRequest, NextResponse } from 'next/server';
46
+
47
+ export async function POST(req: NextRequest): Promise<NextResponse> {
48
+ // Your handler logic
49
+ }
50
+ ```
51
+
52
+ 2. Add routing logic in `masterRouter.ts`:
53
+ ```typescript
54
+ if (pluginId === 'your-plugin') {
55
+ const { POST: yourPluginPOST } = await import('@jhits/plugin-your-plugin/api/route');
56
+ return await yourPluginPOST(req);
57
+ }
58
+ ```
59
+
60
+ ## Endpoints
61
+
62
+ - `POST /api/dashboard/telemetry` - Telemetry logging
63
+ - `POST /api/dashboard/blog` - Blog API (TODO)
64
+ - `POST /api/dashboard/users` - Users API (TODO)
65
+
66
+ ## Benefits
67
+
68
+ 1. **Centralized Routing**: All plugin APIs go through one entry point
69
+ 2. **Easy Plugin Addition**: Add new plugins without modifying client app
70
+ 3. **Type Safety**: Full TypeScript support
71
+ 4. **Server-Only**: Handlers are only imported on the server side
72
+
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Master Dashboard API Router
3
+ * Centralized router for all plugin API endpoints
4
+ *
5
+ * This router automatically routes requests to the appropriate plugin handler
6
+ * based on the path prefix (e.g., /api/dashboard/telemetry -> telemetry handler)
7
+ */
8
+
9
+ import { NextRequest, NextResponse } from 'next/server';
10
+
11
+ /**
12
+ * Extract user/session info from request headers or cookies
13
+ * Helper function for extracting context from NextRequest
14
+ */
15
+ function extractContext(request: NextRequest): { userId?: string; sessionId?: string } {
16
+ // Try to get user ID from headers (custom header)
17
+ const userId = request.headers.get('x-user-id') ||
18
+ request.headers.get('x-userid') ||
19
+ undefined;
20
+
21
+ // Try to get session ID from headers or cookies
22
+ const sessionId = request.headers.get('x-session-id') ||
23
+ request.headers.get('x-sessionid') ||
24
+ request.cookies.get('sessionId')?.value ||
25
+ request.cookies.get('session')?.value ||
26
+ undefined;
27
+
28
+ return { userId, sessionId };
29
+ }
30
+
31
+ /**
32
+ * Handle dashboard API requests
33
+ * Routes requests to appropriate plugin handlers based on path prefix
34
+ *
35
+ * @param req - The incoming request
36
+ * @param path - Array of path segments from the catch-all route
37
+ * @returns Response from the appropriate plugin handler
38
+ */
39
+ export async function handleDashboardApi(
40
+ req: NextRequest,
41
+ path: string[]
42
+ ): Promise<NextResponse> {
43
+ // Extract the first path segment as the plugin identifier
44
+ const pluginId = path[0] || '';
45
+
46
+ try {
47
+ // Route to telemetry handler
48
+ if (pluginId === 'telemetry') {
49
+ const { telemetryHandler } = await import('@jhits/plugin-telemetry/server');
50
+
51
+ // Extract context from request
52
+ const { userId, sessionId } = extractContext(req);
53
+
54
+ // Parse request body (handle empty bodies for GET requests)
55
+ let data: unknown;
56
+ try {
57
+ // Try to parse JSON body, but handle cases where body might be empty
58
+ const text = await req.text();
59
+ data = text ? JSON.parse(text) : null;
60
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
61
+ } catch (error) {
62
+ // If parsing fails, return error
63
+ return NextResponse.json(
64
+ { success: false, error: 'Invalid JSON in request body' },
65
+ { status: 400 }
66
+ );
67
+ }
68
+
69
+ // Call telemetry handler with data and context
70
+ const result = await telemetryHandler(data, { userId, sessionId });
71
+
72
+ // Return appropriate response based on result
73
+ if (result.success) {
74
+ return NextResponse.json(result);
75
+ } else {
76
+ return NextResponse.json(result, { status: 400 });
77
+ }
78
+ }
79
+
80
+ // Route to blog handler
81
+ if (pluginId === 'blog') {
82
+ // TODO: Import and call blog API handler when available
83
+ // For now, return a placeholder
84
+ return NextResponse.json(
85
+ {
86
+ success: false,
87
+ error: 'Blog API handler not yet implemented',
88
+ path: path.join('/')
89
+ },
90
+ { status: 501 }
91
+ );
92
+ }
93
+
94
+ // Route to users handler (if needed)
95
+ if (pluginId === 'users') {
96
+ // Users API is already handled by separate routes
97
+ // This could be used for plugin-specific user operations
98
+ return NextResponse.json(
99
+ {
100
+ success: false,
101
+ error: 'Users API handler not yet implemented',
102
+ path: path.join('/')
103
+ },
104
+ { status: 501 }
105
+ );
106
+ }
107
+
108
+ // Unknown plugin
109
+ return NextResponse.json(
110
+ {
111
+ success: false,
112
+ error: `Unknown plugin: ${pluginId}`,
113
+ availablePlugins: ['telemetry', 'blog', 'users'],
114
+ path: path.join('/')
115
+ },
116
+ { status: 404 }
117
+ );
118
+ } catch (error) {
119
+ console.error('[Master Router] Error routing request:', error);
120
+ return NextResponse.json(
121
+ {
122
+ success: false,
123
+ error: 'Internal server error',
124
+ message: error instanceof Error ? error.message : String(error)
125
+ },
126
+ { status: 500 }
127
+ );
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Convert standard Request to NextRequest for compatibility
133
+ * This is a helper for when the router is called from non-Next.js contexts
134
+ */
135
+ export function createNextRequestFromRequest(
136
+ req: Request,
137
+ path: string[]
138
+ ): NextRequest {
139
+ // Create a new URL with the path
140
+ const url = new URL(req.url);
141
+ url.pathname = `/api/dashboard/${path.join('/')}`;
142
+
143
+ // Create NextRequest from the modified request
144
+ return new NextRequest(url, {
145
+ method: req.method,
146
+ headers: req.headers,
147
+ body: req.body,
148
+ });
149
+ }
150
+
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Unified Plugin API Router (Core Engine)
3
+ * Location: packages/jhits-dashboard/src/api/pluginRouter.ts
4
+ */
5
+ import { NextRequest, NextResponse } from 'next/server';
6
+ import { getToken } from 'next-auth/jwt';
7
+
8
+ export interface PluginRouterConfig {
9
+ mongoClient?: Promise<MongoClient>;
10
+ authOptions?: AuthOptions;
11
+ jwtSecret?: string;
12
+ baseUrl?: string;
13
+ localesDir?: string;
14
+ uploadsDir?: string;
15
+ emailConfig?: {
16
+ host: string; port: number; user: string; password: string; from: string;
17
+ };
18
+ deploymentPaths?: { flagPath: string; scriptPath: string; };
19
+ githubWebhookSecret?: string;
20
+ plugins?: Record<string, unknown>;
21
+ }
22
+
23
+ /**
24
+ * Helper function to get user ID from request
25
+ * Uses NextAuth's getToken to extract user ID from session
26
+ */
27
+ async function getUserIdFromRequest(req: NextRequest, jwtSecret?: string): Promise<string | null> {
28
+ try {
29
+ // Use NextAuth's getToken to get the session token
30
+ // This works with NextAuth's cookie-based sessions
31
+ const token = await getToken({
32
+ req,
33
+ secret: process.env.NEXTAUTH_SECRET || jwtSecret
34
+ });
35
+
36
+ if (!token || !token.sub) return null;
37
+
38
+ // NextAuth stores user ID in token.sub
39
+ return token.sub;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Helper function to create getDb function from mongoClient
47
+ */
48
+ import type { MongoClient, Db } from 'mongodb';
49
+ import { AuthOptions } from 'next-auth';
50
+
51
+ function createGetDb(mongoClient?: Promise<MongoClient>) {
52
+ return async () => {
53
+ if (!mongoClient) {
54
+ throw new Error('MongoDB client not configured');
55
+ }
56
+ const client = await mongoClient;
57
+ return { db: (dbName?: string): Db => client.db(dbName) };
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Static import map for Turbopack compatibility
63
+ *
64
+ * NOTE: Turbopack has a known limitation with subpath exports and relative imports
65
+ * in workspace packages. If /server imports fail, we fall back to root imports.
66
+ */
67
+ const PLUGIN_SERVER_MAP: Record<string, () => Promise<unknown>> = {
68
+ 'plugin-blog': () => import('@jhits/plugin-blog/server'),
69
+ 'plugin-dep': () => import('@jhits/plugin-dep/server'),
70
+ 'plugin-images': () => import('@jhits/plugin-images/server'),
71
+ 'plugin-users': () => import('@jhits/plugin-users/server'),
72
+ 'plugin-content': () => import('@jhits/plugin-content/server'),
73
+ 'plugin-website': () => import('@jhits/plugin-website/server'),
74
+ 'plugin-newsletter': () => import('@jhits/plugin-newsletter/server'),
75
+ };
76
+
77
+ /**
78
+ * Main Handler
79
+ * Expects normalized pluginIds (e.g., 'plugin-blog')
80
+ */
81
+ export async function handlePluginApi(
82
+ req: NextRequest,
83
+ pluginId: string,
84
+ path: string[],
85
+ config: PluginRouterConfig
86
+ ): Promise<NextResponse> {
87
+ try {
88
+ const normalizedId = pluginId.startsWith('plugin-') ? pluginId : `plugin-${pluginId}`;
89
+
90
+ // Try server subpath export first
91
+ const importFn = PLUGIN_SERVER_MAP[normalizedId];
92
+
93
+ if (!importFn) {
94
+ console.error(`[PluginRouter] Unknown plugin: ${normalizedId}`);
95
+ return NextResponse.json({ error: `Plugin ${normalizedId} not found` }, { status: 404 });
96
+ }
97
+
98
+ let pluginModule;
99
+ try {
100
+ pluginModule = await importFn();
101
+ } catch (serverImportError: unknown) {
102
+ const message = serverImportError instanceof Error ? serverImportError.message : String(serverImportError);
103
+ // Turbopack workaround: If /server import fails, try root import
104
+ // This happens because Turbopack can't resolve relative imports in subpath exports
105
+ console.warn(`[PluginRouter] Server import failed for ${normalizedId}, trying root import:`, message);
106
+ pluginModule = await import(`@jhits/${normalizedId}`);
107
+ }
108
+
109
+ const handler = pluginModule.handleApi;
110
+
111
+ if (typeof handler !== 'function') {
112
+ return NextResponse.json({ error: "Handler not found" }, { status: 500 });
113
+ }
114
+
115
+ // Adapt config for plugins that need getDb, getUserId, authOptions, emailConfig, or baseUrl
116
+ const adaptedConfig = {
117
+ ...config,
118
+ // Add getDb if mongoClient is available (for plugin-blog, plugin-users, plugin-newsletter)
119
+ getDb: config.mongoClient ? createGetDb(config.mongoClient) : undefined,
120
+ // Add getUserId - uses NextAuth session token (works with or without jwtSecret)
121
+ getUserId: (req: NextRequest) => getUserIdFromRequest(req, config.jwtSecret),
122
+ // authOptions is already in config, but ensure it's passed through for plugin-users
123
+ // emailConfig and baseUrl are already in config, but ensure they're passed through for plugin-newsletter
124
+ emailConfig: config.emailConfig,
125
+ baseUrl: config.baseUrl,
126
+ };
127
+
128
+ return await handler(req, path, adaptedConfig);
129
+
130
+ } catch (error: unknown) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ console.error(`[PluginRouter] Resolution Error:`, message);
133
+ return NextResponse.json({ error: "Plugin load failure" }, { status: 404 });
134
+ }
135
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import React from 'react';
3
+ import { motion } from 'framer-motion';
4
+ import '../../globals.css';
5
+
6
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
7
+ return (
8
+ <div className="min-h-screen bg-neutral-50 dark:bg-neutral-950 flex items-center justify-center p-4 relative overflow-hidden font-sans">
9
+
10
+ {/* 1. Ambient Background Glows */}
11
+ <div className="absolute inset-0 pointer-events-none">
12
+ <div className="absolute -top-[10%] -left-[10%] size-[50%] rounded-full bg-primary/5 blur-[120px] dark:bg-primary/10" />
13
+ <div className="absolute -bottom-[10%] -right-[10%] size-[50%] rounded-full bg-purple-500/5 blur-[120px] dark:bg-purple-500/10" />
14
+ </div>
15
+
16
+ {/* 2. Structural Wrapper */}
17
+ <motion.div
18
+ initial={{ opacity: 0 }}
19
+ animate={{ opacity: 1 }}
20
+ transition={{ duration: 0.5 }}
21
+ className="w-full relative z-10"
22
+ >
23
+ {children}
24
+ </motion.div>
25
+
26
+ {/* 3. Subtle Grid Overlay (Optional - adds a technical "blueprint" feel) */}
27
+ <div className="absolute inset-0 bg-[url('/grid.svg')] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))] pointer-events-none opacity-[0.03] dark:opacity-[0.05]" />
28
+ </div>
29
+ );
30
+ }