@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,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sidebar": {
|
|
3
|
+
"users": "Gebruikers",
|
|
4
|
+
"website": "Site",
|
|
5
|
+
"stats": "Statistieken",
|
|
6
|
+
"blog": "Content Beheer",
|
|
7
|
+
"newsletter": "Nieuwsbrief",
|
|
8
|
+
"settings": "Instellingen",
|
|
9
|
+
"logout": "Uitloggen",
|
|
10
|
+
"logging_out": "Uitloggen..."
|
|
11
|
+
},
|
|
12
|
+
"dashboard": {
|
|
13
|
+
"status": "PLATFORM ACTIEF",
|
|
14
|
+
"welcome": "Welkom terug, {name}",
|
|
15
|
+
"description": "Uw modulair ecosysteem is gereed. Bekijk uw statistieken, update uw content of beheer uw nieuwsbrieven vanaf één centrale plek.",
|
|
16
|
+
"your_modules": "Geïnstalleerde Modules",
|
|
17
|
+
"open_module": "Module Openen",
|
|
18
|
+
"module_desc": {
|
|
19
|
+
"users": "Beheer gebruikersaccounts, rollen en rechten.",
|
|
20
|
+
"website": "Configureer site-instellingen, pagina's en navigatie.",
|
|
21
|
+
"stats": "Bekijk realtime verkeer en betrokkenheidsgegevens.",
|
|
22
|
+
"blog": "Publiceer en bewerk uw nieuwste artikelen.",
|
|
23
|
+
"newsletter": "Beheer uw abonnees en campagnes."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"profile": {
|
|
27
|
+
"personal_information": "Persoonlijke Informatie",
|
|
28
|
+
"full_name": "Volledige Naam",
|
|
29
|
+
"email_address": "E-mailadres",
|
|
30
|
+
"save_changes": "Wijzigingen Opslaan",
|
|
31
|
+
"updating": "Bijwerken",
|
|
32
|
+
"verified_account": "Geverifieerd Account",
|
|
33
|
+
"login_activity": "Inlogactiviteit",
|
|
34
|
+
"current": "Huidig",
|
|
35
|
+
"revoke": "Intrekken",
|
|
36
|
+
"revoking": "Intrekken...",
|
|
37
|
+
"security_access": "Beveiliging & Toegang",
|
|
38
|
+
"change_password": "Wachtwoord Wijzigen",
|
|
39
|
+
"two_factor_auth": "Twee-Factor Authenticatie",
|
|
40
|
+
"billing_details": "Factureringsgegevens",
|
|
41
|
+
"security_update": "Beveiligingsupdate",
|
|
42
|
+
"new_password": "Nieuw Wachtwoord",
|
|
43
|
+
"confirm_new_password": "Bevestig Nieuw Wachtwoord",
|
|
44
|
+
"update_password": "Wachtwoord Bijwerken",
|
|
45
|
+
"updating_password": "Bijwerken...",
|
|
46
|
+
"terminate_session_confirm": "Weet u zeker dat u deze sessie wilt beëindigen?"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sidebar": {
|
|
3
|
+
"users": "Användare",
|
|
4
|
+
"website": "Site",
|
|
5
|
+
"stats": "Statistik",
|
|
6
|
+
"blog": "Innehållshantering",
|
|
7
|
+
"newsletter": "Nyhetsbrev",
|
|
8
|
+
"settings": "Inställningar",
|
|
9
|
+
"logout": "Logga ut",
|
|
10
|
+
"logging_out": "Loggar ut..."
|
|
11
|
+
},
|
|
12
|
+
"dashboard": {
|
|
13
|
+
"status": "PLATTFORM AKTIV",
|
|
14
|
+
"welcome": "Välkommen tillbaka, {name}",
|
|
15
|
+
"description": "Ditt modulära ekosystem är redo. Övervaka din statistik, uppdatera ditt innehåll eller hantera dina nyhetsbrev från en och samma plats.",
|
|
16
|
+
"your_modules": "Installerade Moduler",
|
|
17
|
+
"open_module": "Öppna Modul",
|
|
18
|
+
"module_desc": {
|
|
19
|
+
"users": "Hantera användarkonton, roller och behörigheter.",
|
|
20
|
+
"website": "Konfigurera webbplatsinställningar, sidor och navigering.",
|
|
21
|
+
"stats": "Visa trafik och engagemangsdata i realtid.",
|
|
22
|
+
"blog": "Publicera och redigera dina senaste artiklar.",
|
|
23
|
+
"newsletter": "Hantera dina prenumeranter och kampanjer."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"profile": {
|
|
27
|
+
"personal_information": "Personlig Information",
|
|
28
|
+
"full_name": "Fullständigt Namn",
|
|
29
|
+
"email_address": "E-postadress",
|
|
30
|
+
"save_changes": "Spara Ändringar",
|
|
31
|
+
"updating": "Uppdaterar",
|
|
32
|
+
"verified_account": "Verifierat Konto",
|
|
33
|
+
"login_activity": "Inloggningsaktivitet",
|
|
34
|
+
"current": "Nuvarande",
|
|
35
|
+
"revoke": "Återkalla",
|
|
36
|
+
"revoking": "Återkallar...",
|
|
37
|
+
"security_access": "Säkerhet & Åtkomst",
|
|
38
|
+
"change_password": "Ändra Lösenord",
|
|
39
|
+
"two_factor_auth": "Tvåfaktorsautentisering",
|
|
40
|
+
"billing_details": "Faktureringsuppgifter",
|
|
41
|
+
"security_update": "Säkerhetsuppdatering",
|
|
42
|
+
"new_password": "Nytt Lösenord",
|
|
43
|
+
"confirm_new_password": "Bekräfta Nytt Lösenord",
|
|
44
|
+
"update_password": "Uppdatera Lösenord",
|
|
45
|
+
"updating_password": "Uppdaterar...",
|
|
46
|
+
"terminate_session_confirm": "Är du säker på att du vill avsluta denna session?"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "user-management",
|
|
4
|
+
"repo": "plugin-users",
|
|
5
|
+
"routePrefix": "users",
|
|
6
|
+
"name": "User Management",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"enabled": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "website-settings",
|
|
12
|
+
"repo": "plugin-website",
|
|
13
|
+
"routePrefix": "website",
|
|
14
|
+
"name": "Website Settings",
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"enabled": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "blog-system",
|
|
20
|
+
"repo": "plugin-blog",
|
|
21
|
+
"routePrefix": "blog",
|
|
22
|
+
"name": "Blog Management Pro",
|
|
23
|
+
"version": "1.0.0",
|
|
24
|
+
"enabled": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "image-management",
|
|
28
|
+
"repo": "plugin-images",
|
|
29
|
+
"routePrefix": "images",
|
|
30
|
+
"name": "Image Manager",
|
|
31
|
+
"version": "1.0.0",
|
|
32
|
+
"enabled": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "newsletter-management",
|
|
36
|
+
"repo": "plugin-newsletter",
|
|
37
|
+
"routePrefix": "newsletter",
|
|
38
|
+
"name": "Newsletter Manager",
|
|
39
|
+
"version": "1.0.0",
|
|
40
|
+
"enabled": true
|
|
41
|
+
}
|
|
42
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" class="animate-svg"><defs><style>.animate-svg path{-webkit-animation:draw ease-in-out forwards;animation:draw ease-in-out forwards;stroke:#000}.animate-svg{-webkit-animation:fillopacity 3 ease-in-out forwards;animation:fillopacity 3s ease-in-out forwards}@-webkit-keyframes draw{100%{stroke-dashoffset:0}}@keyframes draw{100%{stroke-dashoffset:0}}@-webkit-keyframes fillopacity{0%,70%{fill-opacity:0;stroke-width:0.5%}100%{fill-opacity:1;stroke-width:0}}@keyframes fillopacity{0%,70%{fill-opacity:0;stroke-width:0.5%}100%{fill-opacity:1;stroke-width:0}}</style></defs>
|
|
2
|
+
<path d="M107.428 29.7635L107.426 5.23329C103.06 4.31776 98.5891 3.69177 94.0308 3.37646V25.4817V84.743C98.1903 84.1056 102.543 83.5975 107.428 83.2324V29.7635Z" fill="#F3F3F3" style="stroke-dasharray: 186.385px; stroke-dashoffset: 186.385px; animation-delay: 0s; animation-duration: 1.24993s;"></path>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.6358 199.538C48.3919 197.43 20.3896 179.775 4.05225 153.999L4.28131 153.647C6.87071 149.667 10.5908 143.953 13.2505 140.741C13.4148 140.522 13.5753 140.309 13.7352 140.097C14.9218 138.517 16.0705 136.988 18.3439 134.332C27.3553 124.301 47.3779 105.717 76.9136 98.86C94.6562 94.7392 106.311 92.2758 132.545 92.2758C142.75 92.2758 153.293 92.5643 162.925 92.9553C169.007 93.2026 174.557 93.509 179.949 93.8073C181.746 93.9051 183.524 94.003 185.3 94.0983C194.196 94.6438 199.897 95.097 199.897 95.097C199.897 95.097 194.278 95.3336 185.295 95.8898L163.144 97.4804C146.094 98.8783 125.621 100.976 107.428 103.985V161.309C103.104 162.514 98.6251 163.346 94.0307 163.76V106.472C89.2068 107.48 84.6949 108.572 80.6358 109.748V199.538ZM67.2383 175.09C49.7919 170.385 34.8192 159.644 24.701 145.248C33.1855 136.527 48.1885 123.153 67.2383 114.643V175.09Z" fill="#F3F3F3" style="stroke-dasharray: 800.894px; stroke-dashoffset: 800.894px; animation-delay: 0.333333s; animation-duration: 2.27715s;"></path>
|
|
4
|
+
<path d="M67.2383 90.5051C68.9502 90.0055 70.6876 89.5502 72.4481 89.1408C75.3076 88.4766 78.0076 87.8566 80.6357 87.2825V39.4291C76.0413 39.8456 71.5625 40.6752 67.2383 41.8806V90.5051Z" fill="#F3F3F3" style="stroke-dasharray: 123.899px; stroke-dashoffset: 123.899px; animation-delay: 0.666667s; animation-duration: 1.47s;"></path>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="200" height="200" viewBox="0 0 200 200" fill="currentColor" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
|
|
2
|
+
<path d="M107.428 29.7635L107.426 5.23329C103.06 4.31776 98.5891 3.69177 94.0308 3.37646V25.4817V84.743C98.1903 84.1056 102.543 83.5975 107.428 83.2324V29.7635Z"></path>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.6358 199.538C48.3919 197.43 20.3896 179.775 4.05225 153.999L4.28131 153.647C6.87071 149.667 10.5908 143.953 13.2505 140.741C13.4148 140.522 13.5753 140.309 13.7352 140.097C14.9218 138.517 16.0705 136.988 18.3439 134.332C27.3553 124.301 47.3779 105.717 76.9136 98.86C94.6562 94.7392 106.311 92.2758 132.545 92.2758C142.75 92.2758 153.293 92.5643 162.925 92.9553C169.007 93.2026 174.557 93.509 179.949 93.8073C181.746 93.9051 183.524 94.003 185.3 94.0983C194.196 94.6438 199.897 95.097 199.897 95.097C199.897 95.097 194.278 95.3336 185.295 95.8898L163.144 97.4804C146.094 98.8783 125.621 100.976 107.428 103.985V161.309C103.104 162.514 98.6251 163.346 94.0307 163.76V106.472C89.2068 107.48 84.6949 108.572 80.6358 109.748V199.538ZM67.2383 175.09C49.7919 170.385 34.8192 159.644 24.701 145.248C33.1855 136.527 48.1885 123.153 67.2383 114.643V175.09Z"></path>
|
|
4
|
+
<path d="M67.2383 90.5051C68.9502 90.0055 70.6876 89.5502 72.4481 89.1408C75.3076 88.4766 78.0076 87.8566 80.6357 87.2825V39.4291C76.0413 39.8456 71.5625 40.6752 67.2383 41.8806V90.5051Z"></path>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="200" height="200" view-box="0 0 200 200" fill="white" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M107.428 29.7635L107.426 5.23329C103.06 4.31776 98.5891 3.69177 94.0308 3.37646V25.4817V84.743C98.1903 84.1056 102.543 83.5975 107.428 83.2324V29.7635Z"></path>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.6358 199.538C48.3919 197.43 20.3896 179.775 4.05225 153.999L4.28131 153.647C6.87071 149.667 10.5908 143.953 13.2505 140.741C13.4148 140.522 13.5753 140.309 13.7352 140.097C14.9218 138.517 16.0705 136.988 18.3439 134.332C27.3553 124.301 47.3779 105.717 76.9136 98.86C94.6562 94.7392 106.311 92.2758 132.545 92.2758C142.75 92.2758 153.293 92.5643 162.925 92.9553C169.007 93.2026 174.557 93.509 179.949 93.8073C181.746 93.9051 183.524 94.003 185.3 94.0983C194.196 94.6438 199.897 95.097 199.897 95.097C199.897 95.097 194.278 95.3336 185.295 95.8898L163.144 97.4804C146.094 98.8783 125.621 100.976 107.428 103.985V161.309C103.104 162.514 98.6251 163.346 94.0307 163.76V106.472C89.2068 107.48 84.6949 108.572 80.6358 109.748V199.538ZM67.2383 175.09C49.7919 170.385 34.8192 159.644 24.701 145.248C33.1855 136.527 48.1885 123.153 67.2383 114.643V175.09Z"></path>
|
|
4
|
+
<path d="M67.2383 90.5051C68.9502 90.0055 70.6876 89.5502 72.4481 89.1408C75.3076 88.4766 78.0076 87.8566 80.6357 87.2825V39.4291C76.0413 39.8456 71.5625 40.6752 67.2383 41.8806V90.5051Z"></path>
|
|
5
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Dashboard catch-all route handler
|
|
2
|
+
// This component handles all routes and delegates dashboard routes to the dashboard router
|
|
3
|
+
import DashboardLayout from '../app/[locale]/dashboard/layout';
|
|
4
|
+
import DashboardHome from '../app/[locale]/dashboard/page';
|
|
5
|
+
import DashboardPreferences from '../app/[locale]/dashboard/preferences/page';
|
|
6
|
+
import DashboardProfile from '../app/[locale]/dashboard/profile/page';
|
|
7
|
+
import DashboardPluginRoute from '../app/[locale]/dashboard/[...pluginRoute]/page';
|
|
8
|
+
import { WebsiteProvider } from '../lib/website-context';
|
|
9
|
+
import { getWebsiteInfo } from '../lib/get-website-info';
|
|
10
|
+
import { notFound } from 'next/navigation';
|
|
11
|
+
|
|
12
|
+
export interface DashboardCatchAllProps {
|
|
13
|
+
params: Promise<{ locale: string; path: string[] }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* DashboardCatchAll - Handles routing for dashboard routes
|
|
18
|
+
*
|
|
19
|
+
* This component checks if the current path is a dashboard route.
|
|
20
|
+
* If it is, it delegates to the DashboardRouter.
|
|
21
|
+
* If it's not, it returns 404.
|
|
22
|
+
*
|
|
23
|
+
* Usage in client app:
|
|
24
|
+
* export default async function CatchAllPage(props: { params: Promise<{ locale: string; path: string[] }> }) {
|
|
25
|
+
* return <DashboardCatchAll params={props.params} />;
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export default async function DashboardCatchAll({ params }: DashboardCatchAllProps) {
|
|
30
|
+
const resolvedParams = await params;
|
|
31
|
+
const path = resolvedParams.path;
|
|
32
|
+
const locale = resolvedParams.locale;
|
|
33
|
+
|
|
34
|
+
// Handle login routes first (these should be accessible without auth)
|
|
35
|
+
if (path.length > 0 && path[0] === 'login') {
|
|
36
|
+
const { default: LoginPage } = await import('../app/[locale]/(auth)/login/page');
|
|
37
|
+
const { Providers } = await import('../components/Providers');
|
|
38
|
+
const { WebsiteProvider } = await import('../lib/website-context');
|
|
39
|
+
const websiteInfo = await getWebsiteInfo(locale);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Providers>
|
|
43
|
+
<WebsiteProvider website={websiteInfo}>
|
|
44
|
+
<LoginPage />
|
|
45
|
+
</WebsiteProvider>
|
|
46
|
+
</Providers>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle dashboard routes (these require authentication)
|
|
51
|
+
// For /dashboard, path will be ['dashboard']
|
|
52
|
+
// For /dashboard/something, path will be ['dashboard', 'something']
|
|
53
|
+
if (path.length > 0 && path[0] === 'dashboard') {
|
|
54
|
+
// Remove 'dashboard' prefix and pass the rest to dashboard router
|
|
55
|
+
const dashboardPath = path.slice(1);
|
|
56
|
+
|
|
57
|
+
// Get website info from host app
|
|
58
|
+
const websiteInfo = await getWebsiteInfo(locale);
|
|
59
|
+
|
|
60
|
+
// Route to the appropriate dashboard page
|
|
61
|
+
const route = dashboardPath.length === 0 ? 'home' : dashboardPath[0];
|
|
62
|
+
|
|
63
|
+
let dashboardContent;
|
|
64
|
+
switch (route) {
|
|
65
|
+
case 'preferences':
|
|
66
|
+
dashboardContent = <DashboardPreferences />;
|
|
67
|
+
break;
|
|
68
|
+
case 'profile':
|
|
69
|
+
dashboardContent = <DashboardProfile />;
|
|
70
|
+
break;
|
|
71
|
+
case 'home':
|
|
72
|
+
case '':
|
|
73
|
+
dashboardContent = <DashboardHome />;
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
// This is a plugin route - pass the full path as pluginRoute
|
|
77
|
+
dashboardContent = <DashboardPluginRoute params={Promise.resolve({
|
|
78
|
+
locale,
|
|
79
|
+
pluginRoute: dashboardPath
|
|
80
|
+
})} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<WebsiteProvider website={websiteInfo}>
|
|
85
|
+
<DashboardLayout>
|
|
86
|
+
{dashboardContent}
|
|
87
|
+
</DashboardLayout>
|
|
88
|
+
</WebsiteProvider>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Not a dashboard route - return 404
|
|
93
|
+
notFound();
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Dashboard-specific root layout wrapper
|
|
2
|
+
// This provides complete isolation from client app with dashboard metadata, fonts, and styles
|
|
3
|
+
|
|
4
|
+
import type { Metadata } from "next";
|
|
5
|
+
import '../app/globals.css';
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: "JHITS Dashboard | Platform V2",
|
|
9
|
+
description: "Your modular ecosystem dashboard. Monitor your stats, update your content, or manage your newsletters from one place.",
|
|
10
|
+
icons: {
|
|
11
|
+
icon: [
|
|
12
|
+
{ url: '/logo_black.svg', media: '(prefers-color-scheme: light)' },
|
|
13
|
+
{ url: '/logo_white.svg', media: '(prefers-color-scheme: dark)' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function DashboardRootLayout({
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{/* Dashboard font preload */}
|
|
26
|
+
<link
|
|
27
|
+
rel="preload"
|
|
28
|
+
href="https://cdn.prod.website-files.com/673af51dea86ab95d124c3ee/673b0f5784f7060c0ac05534_PPNeueCorp-TightUltrabold.otf"
|
|
29
|
+
as="font"
|
|
30
|
+
type="font/otf"
|
|
31
|
+
crossOrigin="anonymous"
|
|
32
|
+
/>
|
|
33
|
+
{children}
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// apps/dashboard/components/PluginNotFound.tsx
|
|
2
|
+
import { AlertTriangle } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export function PluginNotFound({ requestedPath }: { requestedPath: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="min-h-[70vh] flex flex-col items-center justify-center p-8 text-center">
|
|
7
|
+
<div className="size-20 bg-red-50 dark:bg-red-950/20 rounded-full flex items-center justify-center mb-6">
|
|
8
|
+
<AlertTriangle className="size-10 text-red-500" />
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<h1 className="text-2xl font-black uppercase italic tracking-tighter text-neutral-950 dark:text-white mb-2">
|
|
12
|
+
Plugin Inactive
|
|
13
|
+
</h1>
|
|
14
|
+
|
|
15
|
+
<p className="text-[10px] font-bold text-neutral-400 uppercase tracking-[0.2em] max-w-xs mb-8">
|
|
16
|
+
The module <span className="text-primary">"/{requestedPath}"</span> is either not in your library or hasn't been initialized for this workspace.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<button className="bg-neutral-950 dark:bg-white dark:text-neutral-950 text-white px-8 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:scale-105 transition-transform">
|
|
20
|
+
Visit Plugin Marketplace
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { SessionProvider } from "next-auth/react";
|
|
4
|
+
import { ThemeProvider } from "next-themes";
|
|
5
|
+
import { useSession } from "next-auth/react";
|
|
6
|
+
import { useEffect } from "react";
|
|
7
|
+
import { useRouter } from "@/i18n/navigation";
|
|
8
|
+
|
|
9
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<SessionProvider>
|
|
12
|
+
<ThemeProvider
|
|
13
|
+
attribute="data-theme"
|
|
14
|
+
defaultTheme="system"
|
|
15
|
+
enableSystem
|
|
16
|
+
enableColorScheme={false}
|
|
17
|
+
disableTransitionOnChange={false}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</ThemeProvider>
|
|
21
|
+
</SessionProvider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
|
26
|
+
const { data: session, status } = useSession();
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// If the session exists but the background check failed (returned null)
|
|
31
|
+
// status becomes "unauthenticated"
|
|
32
|
+
if (status === "unauthenticated") {
|
|
33
|
+
// Redirect to login page
|
|
34
|
+
// Using type assertion since the route is dynamically constructed
|
|
35
|
+
router.push('/login' as Parameters<typeof router.push>[0]);
|
|
36
|
+
}
|
|
37
|
+
}, [status, router]);
|
|
38
|
+
|
|
39
|
+
// Show loading state while checking authentication
|
|
40
|
+
if (status === "loading") {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex items-center justify-center min-h-screen bg-neutral-100 dark:bg-neutral-950">
|
|
43
|
+
<div className="text-center">
|
|
44
|
+
<div className="size-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
45
|
+
<p className="text-sm font-bold uppercase tracking-wider text-neutral-600 dark:text-neutral-400">
|
|
46
|
+
Verifying Access...
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If not authenticated, don't render children (redirect will happen in useEffect)
|
|
54
|
+
if (status === "unauthenticated") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return <>{children}</>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Link, usePathname } from '@/i18n/navigation';
|
|
4
|
+
import { PLATFORM_MODULES } from '../../lib/modules-config';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
6
|
+
import Image from 'next/image';
|
|
7
|
+
import { ChevronLeft, Menu, X, LogOut, Loader2, Home, ExternalLink } from 'lucide-react';
|
|
8
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
9
|
+
import { signOut } from 'next-auth/react';
|
|
10
|
+
import { useWebsite } from '../../lib/website-context';
|
|
11
|
+
|
|
12
|
+
/* --- HELPER: TOOLTIP COMPONENT --- */
|
|
13
|
+
function SidebarTooltip({ text, isVisible }: { text: string; isVisible: boolean }) {
|
|
14
|
+
if (!isVisible) return null;
|
|
15
|
+
return (
|
|
16
|
+
<div className="fixed left-[80px] px-3 py-1.5 bg-neutral-900 dark:bg-neutral-800 text-white text-[10px] rounded-md opacity-0 group-hover:opacity-100 pointer-events-none transition-all duration-200 uppercase font-bold tracking-widest z-9999 whitespace-nowrap border border-neutral-700 shadow-2xl translate-x-[-10px] group-hover:translate-x-2">
|
|
17
|
+
{text}
|
|
18
|
+
<div className="absolute left-[-4px] top-1/2 -translate-y-1/2 w-2 h-2 bg-neutral-900 dark:bg-neutral-800 rotate-45 border-l border-b border-neutral-700" />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* --- SUB-COMPONENT: HEADER --- */
|
|
24
|
+
function SidebarHeader({ isCollapsed }: { isCollapsed: boolean }) {
|
|
25
|
+
const website = useWebsite();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="relative pt-4 pb-10 px-3">
|
|
29
|
+
<Link href="/dashboard" className="flex items-center gap-4 overflow-hidden mb-2">
|
|
30
|
+
<motion.div
|
|
31
|
+
layout
|
|
32
|
+
className="relative size-14 shrink-0 flex items-center justify-center rounded-xl bg-dashboard-card border border-dashboard-border"
|
|
33
|
+
>
|
|
34
|
+
<Image src="/logo_black.svg" alt="L" width={32} height={32} className="dark:hidden" />
|
|
35
|
+
<Image src="/logo_white.svg" alt="L" width={32} height={32} className="hidden dark:block" />
|
|
36
|
+
</motion.div>
|
|
37
|
+
|
|
38
|
+
<AnimatePresence mode="popLayout">
|
|
39
|
+
{!isCollapsed && (
|
|
40
|
+
<motion.div
|
|
41
|
+
initial={{ opacity: 0, x: -20 }}
|
|
42
|
+
animate={{ opacity: 1, x: 0 }}
|
|
43
|
+
exit={{ opacity: 0, x: -20 }}
|
|
44
|
+
className="flex flex-col border-l-2 border-primary pl-4 py-0.5 whitespace-nowrap"
|
|
45
|
+
>
|
|
46
|
+
<span className="font-pp text-4xl uppercase leading-[0.8] text-neutral-900 dark:text-neutral-100 font-medium">JHITS</span>
|
|
47
|
+
<div className="flex items-center gap-2 mt-2">
|
|
48
|
+
<span className="text-[9px] font-bold tracking-[0.4em] uppercase text-neutral-500 dark:text-neutral-400">Platform</span>
|
|
49
|
+
<span className="text-[9px] font-bold text-primary italic">V2</span>
|
|
50
|
+
</div>
|
|
51
|
+
</motion.div>
|
|
52
|
+
)}
|
|
53
|
+
</AnimatePresence>
|
|
54
|
+
</Link>
|
|
55
|
+
|
|
56
|
+
<motion.div
|
|
57
|
+
initial={false}
|
|
58
|
+
animate={{
|
|
59
|
+
height: isCollapsed ? 0 : 40,
|
|
60
|
+
opacity: isCollapsed ? 0 : 1,
|
|
61
|
+
marginTop: isCollapsed ? 0 : 16,
|
|
62
|
+
marginBottom: isCollapsed ? 0 : 8
|
|
63
|
+
}}
|
|
64
|
+
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
65
|
+
className="overflow-hidden"
|
|
66
|
+
>
|
|
67
|
+
<a
|
|
68
|
+
href={website.homeUrl}
|
|
69
|
+
className="flex items-center gap-2 px-3 py-2 rounded-xl bg-dashboard-card hover:bg-dashboard-bg transition-colors group whitespace-nowrap"
|
|
70
|
+
>
|
|
71
|
+
<Home className="size-4 text-neutral-600 dark:text-neutral-400 group-hover:text-primary transition-colors shrink-0" />
|
|
72
|
+
<span className="text-[10px] font-bold uppercase tracking-wider text-neutral-600 dark:text-neutral-400 group-hover:text-primary transition-colors">
|
|
73
|
+
{website.name}
|
|
74
|
+
</span>
|
|
75
|
+
<ExternalLink className="size-3 text-neutral-500 dark:text-neutral-500 group-hover:text-primary transition-colors ml-auto shrink-0" />
|
|
76
|
+
</a>
|
|
77
|
+
</motion.div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* --- SUB-COMPONENT: NAVIGATION --- */
|
|
83
|
+
function SidebarNav({ isCollapsed, pathname, t, closeMobile }: { isCollapsed: boolean, pathname: string, t: ReturnType<typeof useTranslations>, closeMobile: () => void }) {
|
|
84
|
+
return (
|
|
85
|
+
<nav className="flex-1 flex flex-col gap-2 px-3">
|
|
86
|
+
{PLATFORM_MODULES.map((module) => {
|
|
87
|
+
const isActive = pathname === module.path;
|
|
88
|
+
const Icon = module.icon;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Link key={module.id} href={module.path as Parameters<typeof Link>[0]['href']} onClick={closeMobile} className="relative group flex items-center h-12 outline-none">
|
|
92
|
+
<motion.div
|
|
93
|
+
whileHover={{ y: -1 }}
|
|
94
|
+
whileTap={{ scale: 0.98 }}
|
|
95
|
+
className={`relative flex items-center h-full w-full rounded-xl transition-all duration-300 ${isActive
|
|
96
|
+
? 'bg-dashboard-card shadow-[0_4px_12px_rgba(0,0,0,0.05)] dark:shadow-[0_4px_20px_rgba(0,0,0,0.3)] border border-dashboard-border'
|
|
97
|
+
: 'hover:bg-dashboard-card border border-transparent'
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
<div className="w-[56px] flex items-center justify-center shrink-0 relative">
|
|
101
|
+
{Icon && <Icon className={`size-5 transition-all duration-300 z-10 ${isActive ? 'text-primary scale-110' : 'text-neutral-500 group-hover:text-neutral-900 dark:group-hover:text-neutral-200'}`} />}
|
|
102
|
+
{isActive && <motion.div layoutId="activeDot" className="absolute size-1.5 bg-primary rounded-full -left-1 shadow-[0_0_8px_#3b82f6]" />}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="flex-1 overflow-hidden">
|
|
106
|
+
<AnimatePresence mode="wait">
|
|
107
|
+
{!isCollapsed && (
|
|
108
|
+
<motion.span
|
|
109
|
+
initial={{ opacity: 0, x: -10 }}
|
|
110
|
+
animate={{ opacity: 1, x: 0 }}
|
|
111
|
+
exit={{ opacity: 0, x: -10 }}
|
|
112
|
+
className={`text-[15px] font-bold whitespace-nowrap transition-colors ${isActive ? 'text-dashboard-text' : 'text-neutral-500 dark:text-neutral-400 group-hover:text-dashboard-text'}`}
|
|
113
|
+
>
|
|
114
|
+
{t(module.id)}
|
|
115
|
+
</motion.span>
|
|
116
|
+
)}
|
|
117
|
+
</AnimatePresence>
|
|
118
|
+
</div>
|
|
119
|
+
</motion.div>
|
|
120
|
+
<SidebarTooltip text={t(module.id)} isVisible={isCollapsed} />
|
|
121
|
+
</Link>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</nav>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* --- SUB-COMPONENT: FOOTER --- */
|
|
129
|
+
function SidebarFooter({ isCollapsed, t }: { isCollapsed: boolean, t: ReturnType<typeof useTranslations> }) {
|
|
130
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
131
|
+
|
|
132
|
+
const handleLogout = async () => {
|
|
133
|
+
try {
|
|
134
|
+
setIsLoading(true);
|
|
135
|
+
await signOut({ callbackUrl: '/', redirect: true });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Logout error:', error);
|
|
138
|
+
setIsLoading(false);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="mt-auto border-t border-neutral-200 dark:border-neutral-800 group relative">
|
|
144
|
+
<button
|
|
145
|
+
onClick={handleLogout}
|
|
146
|
+
disabled={isLoading}
|
|
147
|
+
className="flex cursor-pointer items-center h-16 w-full text-neutral-600 dark:text-neutral-400 hover:text-red-500 transition-colors relative disabled:opacity-50"
|
|
148
|
+
>
|
|
149
|
+
<div className="min-w-[80px] flex items-center justify-center shrink-0 z-10">
|
|
150
|
+
{isLoading ? <Loader2 className="size-5 animate-spin" /> : <LogOut className="size-5 transition-transform group-hover:-translate-x-1" />}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
154
|
+
{!isCollapsed && (
|
|
155
|
+
<motion.div
|
|
156
|
+
initial={{ opacity: 0, width: 0 }}
|
|
157
|
+
animate={{ opacity: 1, width: "auto" }}
|
|
158
|
+
exit={{ opacity: 0, width: 0 }}
|
|
159
|
+
className="overflow-hidden whitespace-nowrap"
|
|
160
|
+
>
|
|
161
|
+
<span className="font-pp text-xl uppercase font-bold pr-6">
|
|
162
|
+
{isLoading ? t('logging_out') : t('logout')}
|
|
163
|
+
</span>
|
|
164
|
+
</motion.div>
|
|
165
|
+
)}
|
|
166
|
+
</AnimatePresence>
|
|
167
|
+
</button>
|
|
168
|
+
<SidebarTooltip text={t('logout')} isVisible={isCollapsed} />
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* --- MAIN EXPORT --- */
|
|
174
|
+
export default function Sidebar() {
|
|
175
|
+
const t = useTranslations('common.sidebar');
|
|
176
|
+
const pathname = usePathname();
|
|
177
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
178
|
+
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
setIsMobileOpen(false);
|
|
183
|
+
}, 0);
|
|
184
|
+
}, [pathname]);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
188
|
+
// Don't handle Ctrl+B if user is typing in an input, textarea, or contentEditable
|
|
189
|
+
const target = e.target as HTMLElement;
|
|
190
|
+
const isEditable = target && (
|
|
191
|
+
target.tagName === 'INPUT' ||
|
|
192
|
+
target.tagName === 'TEXTAREA' ||
|
|
193
|
+
target.contentEditable === 'true' ||
|
|
194
|
+
target.closest('[contenteditable="true"]') !== null
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'b') {
|
|
198
|
+
// Only handle if not in an editable element
|
|
199
|
+
if (!isEditable) {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
setIsCollapsed((prev) => !prev);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
206
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
<AnimatePresence>
|
|
212
|
+
{!isMobileOpen && (
|
|
213
|
+
<motion.button
|
|
214
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
215
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
216
|
+
exit={{ opacity: 0, scale: 0 }}
|
|
217
|
+
onClick={() => setIsMobileOpen(true)}
|
|
218
|
+
className="lg:hidden fixed top-4 left-5 z-50 size-11 flex items-center justify-center bg-dashboard-card backdrop-blur-xl border border-dashboard-border rounded-2xl shadow-2xl active:scale-95 transition-all"
|
|
219
|
+
>
|
|
220
|
+
<Menu className="size-6 text-neutral-600 dark:text-neutral-300" />
|
|
221
|
+
</motion.button>
|
|
222
|
+
)}
|
|
223
|
+
</AnimatePresence>
|
|
224
|
+
|
|
225
|
+
<AnimatePresence>
|
|
226
|
+
{isMobileOpen && (
|
|
227
|
+
<motion.div
|
|
228
|
+
initial={{ opacity: 0 }}
|
|
229
|
+
animate={{ opacity: 1 }}
|
|
230
|
+
exit={{ opacity: 0 }}
|
|
231
|
+
onClick={() => setIsMobileOpen(false)}
|
|
232
|
+
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-48 lg:hidden"
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
</AnimatePresence>
|
|
236
|
+
|
|
237
|
+
<motion.aside
|
|
238
|
+
initial={false}
|
|
239
|
+
animate={{
|
|
240
|
+
width: isCollapsed ? 80 : 280,
|
|
241
|
+
x: isMobileOpen ? 0 : (typeof window !== 'undefined' && window.innerWidth < 1024 ? -280 : 0)
|
|
242
|
+
}}
|
|
243
|
+
className="fixed inset-y-0 left-0 z-50 flex flex-col bg-dashboard-sidebar border-r border-dashboard-border h-dvh lg:sticky top-0 transition-colors duration-300"
|
|
244
|
+
>
|
|
245
|
+
<button onClick={() => setIsMobileOpen(false)} className="lg:hidden absolute right-4 top-5 p-2 bg-dashboard-card rounded-lg text-neutral-500 dark:text-neutral-400">
|
|
246
|
+
<X className="size-5" />
|
|
247
|
+
</button>
|
|
248
|
+
|
|
249
|
+
<div className="flex flex-col h-full w-full relative">
|
|
250
|
+
<SidebarHeader isCollapsed={isCollapsed} />
|
|
251
|
+
<div className="flex-1 overflow-y-auto overflow-x-visible custom-scrollbar">
|
|
252
|
+
<SidebarNav isCollapsed={isCollapsed} pathname={pathname} t={t} closeMobile={() => setIsMobileOpen(false)} />
|
|
253
|
+
</div>
|
|
254
|
+
<SidebarFooter isCollapsed={isCollapsed} t={t} />
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<button onClick={() => setIsCollapsed(!isCollapsed)} className="hidden lg:flex absolute -right-3 top-14 size-6 bg-dashboard-card border border-dashboard-border rounded-full items-center justify-center shadow-md z-50 hover:scale-110 transition-transform">
|
|
258
|
+
<ChevronLeft className={`size-3 text-neutral-500 dark:text-neutral-400 transition-transform duration-500 ${isCollapsed ? 'rotate-180' : ''}`} />
|
|
259
|
+
</button>
|
|
260
|
+
</motion.aside>
|
|
261
|
+
</>
|
|
262
|
+
);
|
|
263
|
+
}
|