@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.
- package/package.json +55 -29
- package/src/api/pluginRouter.ts +15 -6
- package/src/app/[locale]/dashboard/[...pluginRoute]/page.tsx +25 -11
- package/src/app/[locale]/dashboard/layout.tsx +30 -30
- package/src/app/[locale]/dashboard/preferences/page.tsx +18 -2
- package/src/app/[locale]/dashboard/profile/page.tsx +50 -7
- package/src/app/globals.css +50 -22
- package/src/assets/public/animated-logo-white.svg +0 -0
- package/src/assets/public/logo_black.svg +0 -0
- package/src/assets/public/logo_white.svg +0 -0
- package/src/components/DashboardCatchAll.tsx +16 -8
- package/src/components/DashboardRootWrapper.tsx +59 -0
- package/src/components/Providers.tsx +56 -22
- package/src/components/dashboard/Sidebar.tsx +147 -60
- package/src/components/dashboard/Topbar.tsx +56 -25
- package/src/config.ts +180 -186
- package/src/empty-loader.js +4 -0
- package/src/empty.js +5 -0
- package/src/index.client.tsx +16 -0
- package/src/index.server.tsx +66 -0
- package/src/index.tsx +14 -67
- package/src/lib/generate-dashboard-metadata.ts +53 -0
- package/src/lib/modules-config.ts +0 -2
- package/src/lib/plugin-registry.tsx +54 -32
- package/src/server.ts +1 -0
- package/src/api/README.md +0 -72
- package/src/app/[locale]/layout.tsx +0 -28
- package/src/app/api/auth/[...nextauth]/route.ts +0 -6
- package/src/app/api/plugin-images/list/route.ts +0 -96
- package/src/app/api/plugin-images/upload/route.ts +0 -88
- package/src/app/api/telemetry/log/route.ts +0 -10
- package/src/app/api/telemetry/route.ts +0 -12
- package/src/app/api/uploads/[filename]/route.ts +0 -33
- package/src/app/layout.tsx +0 -4
- package/src/assets/public/Logo_JH_black.jpg +0 -0
- package/src/assets/public/Logo_JH_black.png +0 -0
- package/src/assets/public/Logo_JH_white.png +0 -0
- 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
|
+
}
|
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
61
|
-
const loader =
|
|
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
|
-
|
|
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: () =>
|
|
68
|
-
|
|
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,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
|
-
}
|
package/src/app/layout.tsx
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|