@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
@@ -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
@@ -0,0 +1,3 @@
1
+ // Legacy exports - use router.tsx instead
2
+ export { DashboardRouter, DashboardRouterLayout } from './router';
3
+
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
+ }