@jhits/dashboard 0.0.6 → 0.0.8

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 (38) hide show
  1. package/package.json +55 -29
  2. package/src/api/pluginRouter.ts +15 -6
  3. package/src/app/[locale]/dashboard/[...pluginRoute]/page.tsx +25 -11
  4. package/src/app/[locale]/dashboard/layout.tsx +30 -30
  5. package/src/app/[locale]/dashboard/preferences/page.tsx +18 -2
  6. package/src/app/[locale]/dashboard/profile/page.tsx +50 -7
  7. package/src/app/globals.css +50 -22
  8. package/src/assets/public/animated-logo-white.svg +0 -0
  9. package/src/assets/public/logo_black.svg +0 -0
  10. package/src/assets/public/logo_white.svg +0 -0
  11. package/src/components/DashboardCatchAll.tsx +16 -8
  12. package/src/components/DashboardRootWrapper.tsx +59 -0
  13. package/src/components/Providers.tsx +56 -22
  14. package/src/components/dashboard/Sidebar.tsx +147 -60
  15. package/src/components/dashboard/Topbar.tsx +56 -25
  16. package/src/config.ts +180 -186
  17. package/src/empty-loader.js +4 -0
  18. package/src/empty.js +5 -0
  19. package/src/index.client.tsx +16 -0
  20. package/src/index.server.tsx +66 -0
  21. package/src/index.tsx +14 -67
  22. package/src/lib/generate-dashboard-metadata.ts +53 -0
  23. package/src/lib/modules-config.ts +0 -2
  24. package/src/lib/plugin-registry.tsx +54 -32
  25. package/src/server.ts +1 -0
  26. package/src/api/README.md +0 -72
  27. package/src/app/[locale]/layout.tsx +0 -28
  28. package/src/app/api/auth/[...nextauth]/route.ts +0 -6
  29. package/src/app/api/plugin-images/list/route.ts +0 -96
  30. package/src/app/api/plugin-images/upload/route.ts +0 -88
  31. package/src/app/api/telemetry/log/route.ts +0 -10
  32. package/src/app/api/telemetry/route.ts +0 -12
  33. package/src/app/api/uploads/[filename]/route.ts +0 -33
  34. package/src/app/layout.tsx +0 -4
  35. package/src/assets/public/Logo_JH_black.jpg +0 -0
  36. package/src/assets/public/Logo_JH_black.png +0 -0
  37. package/src/assets/public/Logo_JH_white.png +0 -0
  38. package/src/assets/public/noimagefound.jpg +0 -0
@@ -0,0 +1,53 @@
1
+ import type { Metadata } from "next";
2
+ import { headers } from "next/headers";
3
+ import { getWebsiteInfo } from "./get-website-info";
4
+
5
+ /**
6
+ * Generates metadata for dashboard and login routes
7
+ * This function should be used in the client app's layout.tsx
8
+ *
9
+ * @param locale - The locale string
10
+ * @returns Metadata object for dashboard/login routes, or null if not a dashboard route
11
+ */
12
+ export async function generateDashboardMetadata(locale: string): Promise<Metadata | null> {
13
+ // Check if this is a dashboard or login route
14
+ const headersList = await headers();
15
+ const pathname = headersList.get('x-pathname') || headersList.get('x-url') || '';
16
+ const isDashboardRoute = pathname.includes('/dashboard');
17
+ const isLoginRoute = pathname.includes('/login');
18
+
19
+ // Return null if not a dashboard/login route (client app should handle its own metadata)
20
+ if (!isDashboardRoute && !isLoginRoute) {
21
+ return null;
22
+ }
23
+
24
+ // Get website info for dashboard metadata
25
+ try {
26
+ const websiteInfo = await getWebsiteInfo(locale);
27
+ const siteName = websiteInfo.name || "Website";
28
+ const siteTagline = websiteInfo.tagline || "";
29
+
30
+ return {
31
+ title: `${siteName} Dashboard | ${siteTagline || "Platform V2"}`,
32
+ description: `Manage ${siteName}. Monitor your stats, update your content, or manage your newsletters from one place.`,
33
+ icons: {
34
+ icon: [
35
+ { url: '/logo_black.svg', media: '(prefers-color-scheme: light)' },
36
+ { url: '/logo_white.svg', media: '(prefers-color-scheme: dark)' },
37
+ ],
38
+ },
39
+ };
40
+ } catch (e) {
41
+ // Fallback if DB is down or website info unavailable
42
+ return {
43
+ title: "JHITS Dashboard | Platform V2",
44
+ description: "Your modular ecosystem dashboard. Monitor your stats, update your content, or manage your newsletters from one place.",
45
+ icons: {
46
+ icon: [
47
+ { url: '/logo_black.svg', media: '(prefers-color-scheme: light)' },
48
+ { url: '/logo_white.svg', media: '(prefers-color-scheme: dark)' },
49
+ ],
50
+ },
51
+ };
52
+ }
53
+ }
@@ -1,5 +1,3 @@
1
- import 'server-only';
2
-
3
1
  // src/lib/modules-config.ts
4
2
  import { BarChart3, Newspaper, Globe, Users, Mail, type LucideIcon } from 'lucide-react';
5
3
 
@@ -11,23 +11,24 @@ const library = pluginData as PluginManifest[];
11
11
  // Cache for resolved components to avoid recreating them
12
12
  const componentCache = new Map<string, React.ComponentType<PluginProps>>();
13
13
 
14
- // Dynamic plugin loader factory
15
- // This creates a loader function that dynamically imports plugins based on the JSON configuration
16
- // Plugins are loaded from @jhits/{repoName} where repoName comes from plugins.json
17
- function createDynamicPluginLoader(repoName: string): () => Promise<React.ComponentType<PluginProps>> {
18
- return async () => {
19
- try {
20
- // Dynamically import the plugin package
21
- // The package name follows the pattern: @jhits/{repoName}
22
- const pluginModule = await import(`@jhits/${repoName}`);
23
- // Return the default export or Index export
24
- return pluginModule.default || pluginModule.Index;
25
- } catch (error) {
26
- console.error(`Failed to load plugin "${repoName}":`, error);
27
- throw new Error(`Plugin "${repoName}" could not be loaded. Make sure it's installed and the package name matches @jhits/${repoName}`);
28
- }
29
- };
30
- }
14
+ /**
15
+ * EXPLICIT LOADER MAP
16
+ * This is the fix for the "Module Not Found" (tls, net, fs) errors.
17
+ * By explicitly defining the imports, Turbopack/Webpack knows exactly
18
+ * which files to bundle and can safely ignore the /server.ts files.
19
+ */
20
+ const pluginLoaders: Record<string, () => Promise<any>> = {
21
+ 'plugin-blog': () => import('@jhits/plugin-blog'),
22
+ 'plugin-users': () => import('@jhits/plugin-users'),
23
+ // Note: plugin-dep is server-only and has no client UI, so it's not included here
24
+ // It's only used for server-side API routes via @jhits/plugin-dep/server
25
+ 'plugin-telemetry': () => import('@jhits/plugin-telemetry'),
26
+ 'plugin-website': () => import('@jhits/plugin-website'),
27
+ 'plugin-images': () => import('@jhits/plugin-images'),
28
+ 'plugin-content': () => import('@jhits/plugin-content'),
29
+ 'plugin-newsletter': () => import('@jhits/plugin-newsletter'),
30
+ // Add any other plugins that appear in your plugins.json here
31
+ };
31
32
 
32
33
  export const PluginRegistry = {
33
34
  // Returns the static data from JSON
@@ -35,37 +36,58 @@ export const PluginRegistry = {
35
36
  return library.find(p => p.routePrefix === prefix);
36
37
  },
37
38
 
38
- // Resolves the actual React Component with caching
39
- // Dynamically loads plugins based on the JSON configuration
40
- // No hardcoded plugin list - everything comes from plugins.json
39
+ /**
40
+ * Resolves the actual React Component with caching
41
+ * Uses the explicit loader map to ensure clean bundling
42
+ */
41
43
  resolveComponent: (repoName: string) => {
42
- // Check cache first
44
+ // 1. Check cache first
43
45
  if (componentCache.has(repoName)) {
44
46
  return componentCache.get(repoName)!;
45
47
  }
46
48
 
47
- // Verify the plugin exists in the JSON configuration
49
+ // 2. Verify the plugin exists in the JSON configuration
48
50
  const pluginManifest = library.find(p => p.repo === repoName);
49
51
  if (!pluginManifest) {
50
- throw new Error(
51
- `Plugin "${repoName}" not found in plugins.json. ` +
52
- `Available plugins: ${library.map(p => `${p.repo} (${p.id})`).join(', ')}`
53
- );
52
+ throw new Error(`Plugin "${repoName}" not found in plugins.json.`);
54
53
  }
55
54
 
56
55
  if (!pluginManifest.enabled) {
57
56
  throw new Error(`Plugin "${repoName}" is disabled in plugins.json`);
58
57
  }
59
58
 
60
- // Create dynamic loader for this plugin
61
- const loader = createDynamicPluginLoader(repoName);
59
+ // 3. Get the explicit loader
60
+ const loader = pluginLoaders[repoName];
61
+
62
+ if (!loader) {
63
+ console.error(`[PluginRegistry] No loader found for "${repoName}". Add it to the pluginLoaders map.`);
64
+ // Fallback component so the whole app doesn't crash
65
+ return () => (
66
+ <div className="p-10 border-2 border-dashed border-red-500 text-red-500">
67
+ Configuration Error: <b>{repoName}</b> is not registered in the loader map.
68
+ </div>
69
+ );
70
+ }
62
71
 
63
- // Create and cache the component using dynamic import
72
+ // 4. Create and cache the component using dynamic import
64
73
  const Component = dynamic<PluginProps>(
65
- loader,
74
+ async () => {
75
+ try {
76
+ const pluginModule = await loader();
77
+ // Supports both 'export default' and 'export const Index'
78
+ return pluginModule.default || pluginModule.Index;
79
+ } catch (error) {
80
+ console.error(`Failed to load plugin "${repoName}":`, error);
81
+ throw error;
82
+ }
83
+ },
66
84
  {
67
- loading: () => <div className="p-10 animate-pulse font-black uppercase">Mounting_Module...</div>,
68
- ssr: false
85
+ loading: () => (
86
+ <div className="p-10 animate-pulse font-black uppercase text-zinc-500">
87
+ Mounting_{repoName.replace('-', '_')}...
88
+ </div>
89
+ ),
90
+ ssr: false // Prevents server-side Node.js errors during hydration
69
91
  }
70
92
  );
71
93
 
package/src/server.ts CHANGED
@@ -7,4 +7,5 @@ import 'server-only';
7
7
 
8
8
  export { handleDashboardApi, createNextRequestFromRequest } from './api/masterRouter';
9
9
  export { handlePluginApi, type PluginRouterConfig } from './api/pluginRouter';
10
+ export { generateDashboardMetadata } from './lib/generate-dashboard-metadata';
10
11
 
package/src/api/README.md DELETED
@@ -1,72 +0,0 @@
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
-
@@ -1,28 +0,0 @@
1
- import { NextIntlClientProvider } from 'next-intl';
2
- import { getMessages } from 'next-intl/server';
3
- import { Providers } from "../../components/Providers";
4
- import "../globals.css";
5
-
6
- export default async function LocaleLayout(props: {
7
- children: React.ReactNode;
8
- params: Promise<{ locale: string }>;
9
- }) {
10
- // 1. Unwrapping the params (Required for Next.js 15)
11
- const { locale } = await props.params;
12
-
13
- // 2. Extracting children from props
14
- const children = props.children;
15
-
16
- // 3. Getting the translations
17
- const messages = await getMessages();
18
-
19
- return (
20
- <html lang={locale} suppressHydrationWarning>
21
- <body className="antialiased">
22
- <NextIntlClientProvider messages={messages} locale={locale}>
23
- <Providers>{children}</Providers>
24
- </NextIntlClientProvider>
25
- </body>
26
- </html>
27
- );
28
- }
@@ -1,6 +0,0 @@
1
- import NextAuth from "next-auth";
2
- import { authOptions } from "../../../../lib/auth";
3
-
4
- const handler = NextAuth(authOptions);
5
-
6
- export { handler as GET, handler as POST };
@@ -1,96 +0,0 @@
1
- /**
2
- * Image List API Route
3
- * Returns paginated list of uploaded images for plugin-images
4
- */
5
-
6
- import { NextRequest, NextResponse } from 'next/server';
7
- import { readdir, stat } from 'fs/promises';
8
- import path from 'path';
9
-
10
- const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
11
-
12
- export async function GET(request: NextRequest) {
13
- try {
14
- const { searchParams } = new URL(request.url);
15
- const page = parseInt(searchParams.get('page') || '1');
16
- const limit = parseInt(searchParams.get('limit') || '20');
17
- const search = searchParams.get('search') || '';
18
-
19
- // Read uploads directory
20
- let files: string[] = [];
21
- try {
22
- files = await readdir(uploadsDir);
23
- } catch (error) {
24
- // Directory doesn't exist yet
25
- return NextResponse.json({
26
- images: [],
27
- total: 0,
28
- page: 1,
29
- limit,
30
- });
31
- }
32
-
33
- // Filter image files and get metadata
34
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];
35
- const imageFiles = files.filter(file => {
36
- const ext = path.extname(file).toLowerCase();
37
- return imageExtensions.includes(ext);
38
- });
39
-
40
- // Apply search filter if provided
41
- const filteredFiles = search
42
- ? imageFiles.filter(file => file.toLowerCase().includes(search.toLowerCase()))
43
- : imageFiles;
44
-
45
- // Sort by modification time (newest first)
46
- const filesWithStats = await Promise.all(
47
- filteredFiles.map(async (filename) => {
48
- const filePath = path.join(uploadsDir, filename);
49
- const stats = await stat(filePath);
50
- return {
51
- filename,
52
- mtime: stats.mtime,
53
- size: stats.size,
54
- };
55
- })
56
- );
57
-
58
- filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
59
-
60
- // Paginate
61
- const start = (page - 1) * limit;
62
- const end = start + limit;
63
- const paginatedFiles = filesWithStats.slice(start, end);
64
-
65
- // Build image metadata
66
- const images = paginatedFiles.map(({ filename, mtime, size }) => {
67
- const ext = path.extname(filename).toLowerCase();
68
- const mimeType = ext === '.png' ? 'image/png' :
69
- ext === '.webp' ? 'image/webp' :
70
- ext === '.gif' ? 'image/gif' : 'image/jpeg';
71
-
72
- return {
73
- id: filename,
74
- filename,
75
- url: `/api/uploads/${filename}`,
76
- size,
77
- mimeType,
78
- uploadedAt: mtime.toISOString(),
79
- };
80
- });
81
-
82
- return NextResponse.json({
83
- images,
84
- total: filteredFiles.length,
85
- page,
86
- limit,
87
- });
88
- } catch (error) {
89
- console.error('List images error:', error);
90
- return NextResponse.json(
91
- { error: 'Failed to list images' },
92
- { status: 500 }
93
- );
94
- }
95
- }
96
-
@@ -1,88 +0,0 @@
1
- /**
2
- * Image Upload API Route
3
- * Handles image file uploads for plugin-images
4
- */
5
-
6
- import { NextRequest, NextResponse } from 'next/server';
7
- import { writeFile, mkdir } from 'fs/promises';
8
- import path from 'path';
9
- import { randomBytes } from 'crypto';
10
-
11
- // Ensure uploads directory exists
12
- const uploadsDir = path.join(process.cwd(), 'data', 'uploads');
13
-
14
- async function ensureUploadsDir() {
15
- try {
16
- await mkdir(uploadsDir, { recursive: true });
17
- } catch (error) {
18
- // Directory might already exist
19
- }
20
- }
21
-
22
- export async function POST(request: NextRequest) {
23
- try {
24
- await ensureUploadsDir();
25
-
26
- const formData = await request.formData();
27
- const file = formData.get('file') as File;
28
-
29
- if (!file) {
30
- return NextResponse.json(
31
- { success: false, error: 'No file provided' },
32
- { status: 400 }
33
- );
34
- }
35
-
36
- // Validate file type
37
- const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
38
- if (!allowedTypes.includes(file.type)) {
39
- return NextResponse.json(
40
- { success: false, error: 'Invalid file type. Only images are allowed.' },
41
- { status: 400 }
42
- );
43
- }
44
-
45
- // Validate file size (max 10MB)
46
- const maxSize = 10 * 1024 * 1024; // 10MB
47
- if (file.size > maxSize) {
48
- return NextResponse.json(
49
- { success: false, error: 'File size exceeds 10MB limit' },
50
- { status: 400 }
51
- );
52
- }
53
-
54
- // Generate unique filename
55
- const ext = path.extname(file.name);
56
- const uniqueId = randomBytes(16).toString('hex');
57
- const timestamp = Date.now();
58
- const uniqueFilename = `${timestamp}-${uniqueId}${ext}`;
59
- const filePath = path.join(uploadsDir, uniqueFilename);
60
-
61
- // Save file
62
- const bytes = await file.arrayBuffer();
63
- const buffer = Buffer.from(bytes);
64
- await writeFile(filePath, buffer);
65
-
66
- // Get image dimensions (basic - could use sharp for better handling)
67
- const imageMetadata = {
68
- id: uniqueFilename,
69
- filename: file.name,
70
- url: `/api/uploads/${uniqueFilename}`,
71
- size: file.size,
72
- mimeType: file.type,
73
- uploadedAt: new Date().toISOString(),
74
- };
75
-
76
- return NextResponse.json({
77
- success: true,
78
- image: imageMetadata,
79
- });
80
- } catch (error) {
81
- console.error('Upload error:', error);
82
- return NextResponse.json(
83
- { success: false, error: 'Failed to upload image' },
84
- { status: 500 }
85
- );
86
- }
87
- }
88
-
@@ -1,10 +0,0 @@
1
- /**
2
- * Telemetry API Route (Legacy)
3
- * This route is kept for backward compatibility but redirects to /api/telemetry
4
- * Plugin-mounted API route using the telemetry handler
5
- */
6
- import { POST } from '@jhits/plugin-telemetry/api/route';
7
-
8
- // Re-export the POST handler from the plugin
9
- export { POST };
10
-
@@ -1,12 +0,0 @@
1
- /**
2
- * Telemetry API Route
3
- * Plugin-mounted API route using the telemetry handler
4
- * Mounted at /api/telemetry
5
- *
6
- * IMPORTANT: This route file is server-only and should never be imported in client code.
7
- */
8
- import { POST } from '@jhits/plugin-telemetry/api/route';
9
-
10
- // Re-export the POST handler from the plugin
11
- export { POST };
12
-
@@ -1,33 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
-
5
- export async function GET(
6
- request: NextRequest,
7
- { params }: { params: Promise<{ filename: string }> } // 1. Define params as a Promise
8
- ) {
9
- // 2. Await the params to unwrap them
10
- const { filename } = await params;
11
-
12
- // 3. Security: Prevent directory traversal (only allow the filename)
13
- const sanitizedFilename = path.basename(filename);
14
- const filePath = path.join(process.cwd(), "data/uploads", sanitizedFilename);
15
-
16
- try {
17
- const fileBuffer = await fs.readFile(filePath);
18
-
19
- // Determine content type based on extension
20
- const ext = path.extname(sanitizedFilename).toLowerCase();
21
- const contentType = ext === '.png' ? 'image/png' : 'image/jpeg';
22
-
23
- return new NextResponse(fileBuffer, {
24
- headers: {
25
- 'Content-Type': contentType,
26
- 'Cache-Control': 'public, max-age=31536000, immutable',
27
- },
28
- });
29
- } catch (e) {
30
- console.error("File serving error:", e);
31
- return new NextResponse("File not found", { status: 404 });
32
- }
33
- }
@@ -1,4 +0,0 @@
1
- // This file just passes through to the [locale] layout
2
- export default function RootLayout({ children }: { children: React.ReactNode }) {
3
- return children;
4
- }
Binary file
Binary file
Binary file
Binary file