@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.
- package/README.md +36 -0
- package/next.config.ts +32 -0
- package/package.json +79 -0
- package/postcss.config.mjs +7 -0
- package/src/api/README.md +72 -0
- package/src/api/masterRouter.ts +150 -0
- package/src/api/pluginRouter.ts +135 -0
- package/src/app/[locale]/(auth)/layout.tsx +30 -0
- package/src/app/[locale]/(auth)/login/page.tsx +201 -0
- package/src/app/[locale]/catch-all/page.tsx +10 -0
- package/src/app/[locale]/dashboard/[...pluginRoute]/page.tsx +98 -0
- package/src/app/[locale]/dashboard/layout.tsx +42 -0
- package/src/app/[locale]/dashboard/page.tsx +121 -0
- package/src/app/[locale]/dashboard/preferences/page.tsx +295 -0
- package/src/app/[locale]/dashboard/profile/page.tsx +491 -0
- package/src/app/[locale]/layout.tsx +28 -0
- package/src/app/actions/preferences.ts +40 -0
- package/src/app/actions/user.ts +191 -0
- package/src/app/api/auth/[...nextauth]/route.ts +6 -0
- package/src/app/api/plugin-images/list/route.ts +96 -0
- package/src/app/api/plugin-images/upload/route.ts +88 -0
- package/src/app/api/telemetry/log/route.ts +10 -0
- package/src/app/api/telemetry/route.ts +12 -0
- package/src/app/api/uploads/[filename]/route.ts +33 -0
- package/src/app/globals.css +181 -0
- package/src/app/layout.tsx +4 -0
- package/src/assets/locales/en/common.json +47 -0
- package/src/assets/locales/nl/common.json +48 -0
- package/src/assets/locales/sv/common.json +48 -0
- package/src/assets/plugins.json +42 -0
- 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/animated-logo-white.svg +5 -0
- package/src/assets/public/logo_black.svg +5 -0
- package/src/assets/public/logo_white.svg +5 -0
- package/src/assets/public/noimagefound.jpg +0 -0
- package/src/components/DashboardCatchAll.tsx +95 -0
- package/src/components/DashboardRootLayout.tsx +37 -0
- package/src/components/PluginNotFound.tsx +24 -0
- package/src/components/Providers.tsx +59 -0
- package/src/components/dashboard/Sidebar.tsx +263 -0
- package/src/components/dashboard/Topbar.tsx +363 -0
- package/src/components/page.tsx +130 -0
- package/src/config.ts +230 -0
- package/src/i18n/navigation.ts +7 -0
- package/src/i18n/request.ts +41 -0
- package/src/i18n/routing.ts +35 -0
- package/src/i18n/translations.ts +20 -0
- package/src/index.tsx +69 -0
- package/src/lib/auth.ts +159 -0
- package/src/lib/db.ts +11 -0
- package/src/lib/get-website-info.ts +78 -0
- package/src/lib/modules-config.ts +68 -0
- package/src/lib/mongodb.ts +32 -0
- package/src/lib/plugin-registry.tsx +77 -0
- package/src/lib/website-context.tsx +39 -0
- package/src/proxy.ts +55 -0
- package/src/router.tsx +45 -0
- package/src/routes.tsx +3 -0
- package/src/server.ts +8 -0
- package/src/types/plugin.ts +24 -0
- package/src/types/preferences.ts +13 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/lib/modules-config.ts
|
|
2
|
+
import { BarChart3, Newspaper, Globe, Users, Mail, type LucideIcon } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface Module {
|
|
5
|
+
id: string;
|
|
6
|
+
namespace: string;
|
|
7
|
+
path: string;
|
|
8
|
+
aliases: Record<string, string>;
|
|
9
|
+
icon: LucideIcon;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const PLATFORM_MODULES: Module[] = [
|
|
13
|
+
{
|
|
14
|
+
id: 'users',
|
|
15
|
+
namespace: 'users',
|
|
16
|
+
path: '/dashboard/users',
|
|
17
|
+
icon: Users,
|
|
18
|
+
aliases: {
|
|
19
|
+
en: '/dashboard/users',
|
|
20
|
+
nl: '/dashboard/gebruikers',
|
|
21
|
+
sv: '/dashboard/användare'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'website',
|
|
26
|
+
namespace: 'website',
|
|
27
|
+
path: '/dashboard/website',
|
|
28
|
+
icon: Globe,
|
|
29
|
+
aliases: {
|
|
30
|
+
en: '/dashboard/site-settings',
|
|
31
|
+
nl: '/dashboard/website-instellingen',
|
|
32
|
+
sv: '/dashboard/webbplatsinstallningar'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'blog',
|
|
37
|
+
namespace: 'blog',
|
|
38
|
+
path: '/dashboard/blog',
|
|
39
|
+
icon: Newspaper,
|
|
40
|
+
aliases: {
|
|
41
|
+
en: '/dashboard/content-manager',
|
|
42
|
+
nl: '/dashboard/blog-beheer',
|
|
43
|
+
sv: '/dashboard/blogghantering'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'newsletter',
|
|
48
|
+
namespace: 'newsletter',
|
|
49
|
+
path: '/dashboard/newsletter',
|
|
50
|
+
icon: Mail,
|
|
51
|
+
aliases: {
|
|
52
|
+
en: '/dashboard/newsletter',
|
|
53
|
+
nl: '/dashboard/nieuwsbrief',
|
|
54
|
+
sv: '/dashboard/nyhetsbrev'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'stats',
|
|
59
|
+
namespace: 'stats',
|
|
60
|
+
path: '/dashboard/stats',
|
|
61
|
+
icon: BarChart3,
|
|
62
|
+
aliases: {
|
|
63
|
+
en: '/dashboard/statistics',
|
|
64
|
+
nl: '/dashboard/statistieken',
|
|
65
|
+
sv: '/dashboard/statistik'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MongoClient } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
// Support both DATABASE_URL (dashboard) and MONGODB_URI (client app)
|
|
4
|
+
const uri = process.env.DATABASE_URL || process.env.MONGODB_URI;
|
|
5
|
+
|
|
6
|
+
if (!uri) {
|
|
7
|
+
throw new Error('Invalid/Missing environment variable: "DATABASE_URL" or "MONGODB_URI"');
|
|
8
|
+
}
|
|
9
|
+
const options = {};
|
|
10
|
+
|
|
11
|
+
let client: MongoClient;
|
|
12
|
+
let clientPromise: Promise<MongoClient>;
|
|
13
|
+
|
|
14
|
+
if (process.env.NODE_ENV === 'development') {
|
|
15
|
+
// In development mode, use a global variable so the value
|
|
16
|
+
// is preserved across module reloads caused by HMR.
|
|
17
|
+
const globalWithMongo = global as typeof globalThis & {
|
|
18
|
+
_mongoClientPromise?: Promise<MongoClient>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (!globalWithMongo._mongoClientPromise) {
|
|
22
|
+
client = new MongoClient(uri, options);
|
|
23
|
+
globalWithMongo._mongoClientPromise = client.connect();
|
|
24
|
+
}
|
|
25
|
+
clientPromise = globalWithMongo._mongoClientPromise;
|
|
26
|
+
} else {
|
|
27
|
+
// In production mode, it's best to not use a global variable.
|
|
28
|
+
client = new MongoClient(uri, options);
|
|
29
|
+
clientPromise = client.connect();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default clientPromise;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// packages/jhits-dashboard/src/lib/plugin-registry.tsx
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import dynamic from 'next/dynamic';
|
|
5
|
+
import pluginData from "../assets/plugins.json";
|
|
6
|
+
import { PluginProps, PluginManifest } from "../types/plugin";
|
|
7
|
+
|
|
8
|
+
// Type the imported JSON safely
|
|
9
|
+
const library = pluginData as PluginManifest[];
|
|
10
|
+
|
|
11
|
+
// Cache for resolved components to avoid recreating them
|
|
12
|
+
const componentCache = new Map<string, React.ComponentType<PluginProps>>();
|
|
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
|
+
}
|
|
31
|
+
|
|
32
|
+
export const PluginRegistry = {
|
|
33
|
+
// Returns the static data from JSON
|
|
34
|
+
getDefinition: (prefix: string): PluginManifest | undefined => {
|
|
35
|
+
return library.find(p => p.routePrefix === prefix);
|
|
36
|
+
},
|
|
37
|
+
|
|
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
|
|
41
|
+
resolveComponent: (repoName: string) => {
|
|
42
|
+
// Check cache first
|
|
43
|
+
if (componentCache.has(repoName)) {
|
|
44
|
+
return componentCache.get(repoName)!;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Verify the plugin exists in the JSON configuration
|
|
48
|
+
const pluginManifest = library.find(p => p.repo === repoName);
|
|
49
|
+
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
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!pluginManifest.enabled) {
|
|
57
|
+
throw new Error(`Plugin "${repoName}" is disabled in plugins.json`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create dynamic loader for this plugin
|
|
61
|
+
const loader = createDynamicPluginLoader(repoName);
|
|
62
|
+
|
|
63
|
+
// Create and cache the component using dynamic import
|
|
64
|
+
const Component = dynamic<PluginProps>(
|
|
65
|
+
loader,
|
|
66
|
+
{
|
|
67
|
+
loading: () => <div className="p-10 animate-pulse font-black uppercase">Mounting_Module...</div>,
|
|
68
|
+
ssr: false
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
componentCache.set(repoName, Component);
|
|
73
|
+
return Component;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
getAll: () => library
|
|
77
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface WebsiteInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
tagline?: string;
|
|
8
|
+
homeUrl: string; // Base URL to navigate back to the main site
|
|
9
|
+
locale?: string;
|
|
10
|
+
logo?: {
|
|
11
|
+
light: string; // Path to light mode logo
|
|
12
|
+
dark: string; // Path to dark mode logo
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const WebsiteContext = createContext<WebsiteInfo | null>(null);
|
|
17
|
+
|
|
18
|
+
export function WebsiteProvider({ children, website }: { children: ReactNode; website: WebsiteInfo }) {
|
|
19
|
+
return (
|
|
20
|
+
<WebsiteContext.Provider value={website}>
|
|
21
|
+
{children}
|
|
22
|
+
</WebsiteContext.Provider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useWebsite() {
|
|
27
|
+
const context = useContext(WebsiteContext);
|
|
28
|
+
if (!context) {
|
|
29
|
+
// Fallback values if context is not provided
|
|
30
|
+
return {
|
|
31
|
+
name: 'Website',
|
|
32
|
+
tagline: '',
|
|
33
|
+
homeUrl: '/',
|
|
34
|
+
locale: 'en'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return context;
|
|
38
|
+
}
|
|
39
|
+
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import type { NextRequest } from 'next/server';
|
|
4
|
+
import { getToken } from "next-auth/jwt";
|
|
5
|
+
import createIntlMiddleware from 'next-intl/middleware';
|
|
6
|
+
import { routing } from './i18n/routing';
|
|
7
|
+
|
|
8
|
+
// 1. Initialize the Internationalization middleware
|
|
9
|
+
const handleI18nRouting = createIntlMiddleware(routing);
|
|
10
|
+
|
|
11
|
+
export default async function middleware(req: NextRequest) {
|
|
12
|
+
const { pathname } = req.nextUrl;
|
|
13
|
+
|
|
14
|
+
// 2. Run i18n middleware first (handles /en, /nl, /sv prefixes)
|
|
15
|
+
const response = handleI18nRouting(req);
|
|
16
|
+
|
|
17
|
+
// 3. Auth Logic
|
|
18
|
+
const token = await getToken({ req });
|
|
19
|
+
const isAuth = !!token;
|
|
20
|
+
|
|
21
|
+
// Check if we are on a login page (any language version like /en/login)
|
|
22
|
+
const isAuthPage = pathname.match(/\/(en|nl|sv)\/login$/) || pathname === '/login';
|
|
23
|
+
|
|
24
|
+
// Check if we are on a protected dashboard route
|
|
25
|
+
const isDashboardPage = pathname.includes('/stats') ||
|
|
26
|
+
pathname.includes('/blog') ||
|
|
27
|
+
pathname.includes('/dashboard');
|
|
28
|
+
|
|
29
|
+
// Logic A: Already logged in? Redirect away from Login to Stats
|
|
30
|
+
if (isAuthPage) {
|
|
31
|
+
if (isAuth) {
|
|
32
|
+
return NextResponse.redirect(new URL('/dashboard', req.url)); // Default to /en/stats or detect locale
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Logic B: Not logged in? Redirect from Dashboard to Login
|
|
38
|
+
if (isDashboardPage && !isAuth) {
|
|
39
|
+
let from = pathname;
|
|
40
|
+
if (req.nextUrl.search) from += req.nextUrl.search;
|
|
41
|
+
|
|
42
|
+
// We redirect to the /login page, preserving the current locale if possible
|
|
43
|
+
const locale = pathname.split('/')[1] || 'en';
|
|
44
|
+
return NextResponse.redirect(
|
|
45
|
+
new URL(`/${locale}/login?from=${encodeURIComponent(from)}`, req.url)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const config = {
|
|
53
|
+
// Match all routes except static files and API
|
|
54
|
+
matcher: ['/((?!api|_next|_static|_vercel|[\\w-]+\\.\\w+).*)']
|
|
55
|
+
};
|
package/src/router.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Main router component that handles all dashboard routes
|
|
2
|
+
// This is the single entry point that the host app uses
|
|
3
|
+
|
|
4
|
+
import DashboardLayout from './app/[locale]/dashboard/layout';
|
|
5
|
+
import DashboardHome from './app/[locale]/dashboard/page';
|
|
6
|
+
import DashboardPreferences from './app/[locale]/dashboard/preferences/page';
|
|
7
|
+
import DashboardProfile from './app/[locale]/dashboard/profile/page';
|
|
8
|
+
import DashboardPluginRoute from './app/[locale]/dashboard/[...pluginRoute]/page';
|
|
9
|
+
|
|
10
|
+
export async function DashboardRouter({
|
|
11
|
+
path,
|
|
12
|
+
params
|
|
13
|
+
}: {
|
|
14
|
+
path: string[];
|
|
15
|
+
params: Promise<{ locale: string }>;
|
|
16
|
+
}) {
|
|
17
|
+
const resolvedParams = await params;
|
|
18
|
+
const locale = resolvedParams.locale;
|
|
19
|
+
|
|
20
|
+
// Get the actual route (first segment of path)
|
|
21
|
+
const route = path.length === 0 ? 'home' : path[0];
|
|
22
|
+
|
|
23
|
+
// Handle different dashboard routes
|
|
24
|
+
switch (route) {
|
|
25
|
+
case 'preferences':
|
|
26
|
+
return <DashboardPreferences />;
|
|
27
|
+
case 'profile':
|
|
28
|
+
return <DashboardProfile />;
|
|
29
|
+
case 'home':
|
|
30
|
+
case '':
|
|
31
|
+
return <DashboardHome />;
|
|
32
|
+
default:
|
|
33
|
+
// This is a plugin route - pass the full path as pluginRoute
|
|
34
|
+
return <DashboardPluginRoute params={Promise.resolve({
|
|
35
|
+
locale,
|
|
36
|
+
pluginRoute: path
|
|
37
|
+
})} />;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Layout wrapper
|
|
42
|
+
export function DashboardRouterLayout({ children }: { children: React.ReactNode }) {
|
|
43
|
+
return <DashboardLayout>{children}</DashboardLayout>;
|
|
44
|
+
}
|
|
45
|
+
|
package/src/routes.tsx
ADDED
package/src/server.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only exports for @jhits/dashboard
|
|
3
|
+
* These exports should only be used in server-side code (API routes, server components)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { handleDashboardApi, createNextRequestFromRequest } from './api/masterRouter';
|
|
7
|
+
export { handlePluginApi, type PluginRouterConfig } from './api/pluginRouter';
|
|
8
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// types/plugin.ts
|
|
2
|
+
|
|
3
|
+
// This represents what is in your JSON file
|
|
4
|
+
export interface PluginManifest {
|
|
5
|
+
id: string;
|
|
6
|
+
repo: string;
|
|
7
|
+
routePrefix: string;
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PluginProps {
|
|
14
|
+
subPath: string[];
|
|
15
|
+
siteId: string;
|
|
16
|
+
locale: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// This represents the actual loaded code
|
|
20
|
+
export interface SitePlugin extends PluginManifest {
|
|
21
|
+
MainRouter: React.ComponentType<PluginProps>;
|
|
22
|
+
HeaderComponent?: React.ComponentType;
|
|
23
|
+
SettingsPanel?: React.ComponentType;
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface UserPreferences {
|
|
2
|
+
theme: 'light' | 'dark' | 'system';
|
|
3
|
+
language: string;
|
|
4
|
+
dashboardView: 'grid' | 'list';
|
|
5
|
+
notifications: {
|
|
6
|
+
marketing: boolean;
|
|
7
|
+
security: boolean;
|
|
8
|
+
};
|
|
9
|
+
privacy: {
|
|
10
|
+
stealthMode: boolean;
|
|
11
|
+
publicSearch: boolean;
|
|
12
|
+
};
|
|
13
|
+
}
|