@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,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { signIn, useSession } from "next-auth/react";
|
|
4
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
5
|
+
import { Lock, Mail, ChevronRight, Loader2, ArrowLeft, ShieldCheck, Eye, EyeOff } from "lucide-react";
|
|
6
|
+
import Image from "next/image";
|
|
7
|
+
import { useRouter } from "next/navigation";
|
|
8
|
+
import { useWebsite } from "../../../../lib/website-context";
|
|
9
|
+
|
|
10
|
+
export default function LoginPage() {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const website = useWebsite();
|
|
13
|
+
const { data: session, status } = useSession();
|
|
14
|
+
const [email, setEmail] = useState("");
|
|
15
|
+
const [password, setPassword] = useState("");
|
|
16
|
+
const [showPassword, setShowPassword] = useState(false); // New state for toggle
|
|
17
|
+
const [error, setError] = useState("");
|
|
18
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (status === "authenticated" && session?.user) {
|
|
22
|
+
router.push('/dashboard');
|
|
23
|
+
}
|
|
24
|
+
}, [status, session, router]);
|
|
25
|
+
|
|
26
|
+
if (status === "loading") {
|
|
27
|
+
return (
|
|
28
|
+
<div className="min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950">
|
|
29
|
+
<Loader2 className="size-8 animate-spin text-primary" />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setError("");
|
|
37
|
+
setIsLoading(true);
|
|
38
|
+
|
|
39
|
+
const result = await signIn("credentials", {
|
|
40
|
+
email,
|
|
41
|
+
password,
|
|
42
|
+
redirect: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (result?.ok) {
|
|
46
|
+
router.push('/dashboard');
|
|
47
|
+
} else if (result?.error) {
|
|
48
|
+
setError("Invalid identity or password");
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-950 px-4 relative overflow-hidden font-sans">
|
|
55
|
+
|
|
56
|
+
{/* Return Link */}
|
|
57
|
+
<a
|
|
58
|
+
href={website.homeUrl}
|
|
59
|
+
className="absolute top-8 left-8 z-20 flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-neutral-500 hover:text-primary transition-all group"
|
|
60
|
+
>
|
|
61
|
+
<ArrowLeft className="size-4 group-hover:-translate-x-1 transition-transform" />
|
|
62
|
+
<span>Return to {website.name}</span>
|
|
63
|
+
</a>
|
|
64
|
+
|
|
65
|
+
{/* Background Atmosphere */}
|
|
66
|
+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
67
|
+
<div className="absolute top-0 left-1/4 size-[500px] rounded-full bg-primary/10 blur-[120px]" />
|
|
68
|
+
<div className="absolute bottom-0 right-1/4 size-[500px] rounded-full bg-blue-600/5 blur-[120px]" />
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<motion.div
|
|
72
|
+
initial={{ opacity: 0, y: 20 }}
|
|
73
|
+
animate={{ opacity: 1, y: 0 }}
|
|
74
|
+
className="w-full max-w-lg relative z-10"
|
|
75
|
+
>
|
|
76
|
+
{/* --- BRANDING HEADER --- */}
|
|
77
|
+
<div className="flex flex-col items-center mb-12">
|
|
78
|
+
<div className="flex items-center gap-6">
|
|
79
|
+
|
|
80
|
+
{/* 1. Client Identity */}
|
|
81
|
+
{website.logo && (
|
|
82
|
+
<div className="flex flex-col items-center gap-2">
|
|
83
|
+
<div className="size-14 relative grayscale hover:grayscale-0 transition-all duration-500">
|
|
84
|
+
<Image src={website.logo.light} alt="" fill className="dark:hidden object-contain" />
|
|
85
|
+
<Image src={website.logo.dark} alt="" fill className="hidden dark:block object-contain" />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* 2. JHITS Typographic Block */}
|
|
91
|
+
<div className="flex flex-col border-l-2 border-primary pl-6 py-1">
|
|
92
|
+
<span className="font-pp text-5xl uppercase leading-[0.75] text-foreground font-medium tracking-tighter">
|
|
93
|
+
JHITS
|
|
94
|
+
</span>
|
|
95
|
+
<div className="flex items-center gap-2 mt-2">
|
|
96
|
+
<span className="text-[10px] font-bold tracking-[0.4em] uppercase text-neutral-500">
|
|
97
|
+
Platform
|
|
98
|
+
</span>
|
|
99
|
+
<span className="text-[10px] font-bold text-primary italic">V2</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="mt-8 flex items-center gap-2 px-3 py-1 bg-neutral-200/50 dark:bg-neutral-800/50 rounded-full border border-neutral-300 dark:border-neutral-700">
|
|
105
|
+
<ShieldCheck className="size-3 text-primary" />
|
|
106
|
+
<span className="text-[9px] font-bold uppercase tracking-widest text-neutral-600 dark:text-neutral-400">
|
|
107
|
+
Authorized {website.name} Terminal
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* --- LOGIN FORM CARD --- */}
|
|
113
|
+
<div className="bg-white/80 dark:bg-neutral-900/70 backdrop-blur-xl border border-neutral-200 dark:border-neutral-800 p-8 rounded-[2.5rem] shadow-2xl">
|
|
114
|
+
<form onSubmit={handleLogin} className="space-y-6">
|
|
115
|
+
<AnimatePresence mode="wait">
|
|
116
|
+
{error && (
|
|
117
|
+
<motion.div
|
|
118
|
+
initial={{ opacity: 0, y: -10 }}
|
|
119
|
+
animate={{ opacity: 1, y: 0 }}
|
|
120
|
+
exit={{ opacity: 0, scale: 0.95 }}
|
|
121
|
+
className="bg-red-50 dark:bg-red-950/30 text-red-600 dark:text-red-400 p-4 rounded-2xl text-[11px] font-sans font-bold uppercase tracking-tight border border-red-100 dark:border-red-900/50 flex items-center gap-3"
|
|
122
|
+
>
|
|
123
|
+
<div className="size-2 rounded-full bg-red-600 animate-pulse" />
|
|
124
|
+
{error}
|
|
125
|
+
</motion.div>
|
|
126
|
+
)}
|
|
127
|
+
</AnimatePresence>
|
|
128
|
+
|
|
129
|
+
<div className="space-y-2">
|
|
130
|
+
<label className="block text-[10px] font-sans font-bold uppercase tracking-[0.2em] text-neutral-600 dark:text-neutral-400 ml-1">
|
|
131
|
+
Identity / Email
|
|
132
|
+
</label>
|
|
133
|
+
<div className="relative group">
|
|
134
|
+
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 size-4 text-neutral-400 group-focus-within:text-primary transition-colors" />
|
|
135
|
+
<input
|
|
136
|
+
type="email"
|
|
137
|
+
value={email}
|
|
138
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
139
|
+
className="w-full pl-12 pr-4 py-3.5 bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700/50 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary outline-none transition-all text-sm font-sans font-medium text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500"
|
|
140
|
+
placeholder="admin@jhits.com"
|
|
141
|
+
required
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<div className="flex justify-between items-center ml-1">
|
|
148
|
+
<label className="block text-[10px] font-sans font-bold uppercase tracking-[0.2em] text-neutral-600 dark:text-neutral-400">
|
|
149
|
+
Security / Password
|
|
150
|
+
</label>
|
|
151
|
+
<button type="button" className="text-[10px] font-sans font-bold uppercase tracking-tighter text-primary hover:opacity-70 transition-opacity">
|
|
152
|
+
Forgot?
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="relative group">
|
|
156
|
+
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 size-4 text-neutral-400 group-focus-within:text-primary transition-colors" />
|
|
157
|
+
<input
|
|
158
|
+
type={showPassword ? "text" : "password"}
|
|
159
|
+
value={password}
|
|
160
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
161
|
+
className="w-full pl-12 pr-12 py-3.5 bg-neutral-100 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700/50 rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary outline-none transition-all text-sm font-sans font-medium text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500"
|
|
162
|
+
placeholder="••••••••"
|
|
163
|
+
required
|
|
164
|
+
/>
|
|
165
|
+
{/* Show/Hide Toggle Button */}
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
169
|
+
className="absolute right-4 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-primary transition-colors p-1"
|
|
170
|
+
tabIndex={-1}
|
|
171
|
+
>
|
|
172
|
+
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
type="submit"
|
|
179
|
+
disabled={isLoading}
|
|
180
|
+
className="group relative w-full bg-primary text-white py-4 rounded-2xl font-sans font-bold uppercase tracking-widest text-xs overflow-hidden transition-all hover:shadow-2xl hover:shadow-primary/30 active:scale-[0.98] disabled:opacity-70 shadow-xl shadow-primary/20"
|
|
181
|
+
>
|
|
182
|
+
<span className="relative z-10 flex items-center justify-center gap-2">
|
|
183
|
+
{isLoading ? (
|
|
184
|
+
<Loader2 className="size-4 animate-spin" />
|
|
185
|
+
) : (
|
|
186
|
+
<>
|
|
187
|
+
Authorize Access <ChevronRight className="size-4 group-hover:translate-x-1 transition-transform" />
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
</span>
|
|
191
|
+
</button>
|
|
192
|
+
</form>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<p className="text-center mt-12 text-[9px] font-bold uppercase tracking-[0.5em] text-neutral-500 dark:text-neutral-600">
|
|
196
|
+
Terminal Active <span className="text-primary mx-2">●</span> Secured by JHITS
|
|
197
|
+
</p>
|
|
198
|
+
</motion.div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Default catch-all page export for dashboard routes
|
|
2
|
+
// This can be directly imported by client apps as their catch-all route
|
|
3
|
+
import DashboardCatchAll from '../../../components/DashboardCatchAll';
|
|
4
|
+
|
|
5
|
+
export default async function CatchAllPage(props: {
|
|
6
|
+
params: Promise<{ locale: string; path: string[] }>;
|
|
7
|
+
}) {
|
|
8
|
+
return <DashboardCatchAll params={props.params} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// packages/jhits-dashboard/src/app/[locale]/dashboard/[...pluginRoute]/page.tsx
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import React, { use, useState, useEffect, useRef, useMemo } from 'react';
|
|
5
|
+
import { PluginRegistry } from "../../../../lib/plugin-registry";
|
|
6
|
+
import { PluginNotFound } from "../../../../components/PluginNotFound";
|
|
7
|
+
import { PluginProps } from "../../../../types/plugin";
|
|
8
|
+
|
|
9
|
+
interface ClientPluginProps {
|
|
10
|
+
darkMode?: boolean;
|
|
11
|
+
customBlocks?: unknown[];
|
|
12
|
+
backgroundColors?: { light: string; dark?: string };
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
__JHITS_PLUGIN_PROPS__?: Record<string, ClientPluginProps>;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function DynamicPluginPage({
|
|
23
|
+
params
|
|
24
|
+
}: {
|
|
25
|
+
params: Promise<{ locale: string, pluginRoute: string[] }>
|
|
26
|
+
}) {
|
|
27
|
+
const resolvedParams = use(params);
|
|
28
|
+
const [prefix, ...subPath] = resolvedParams.pluginRoute;
|
|
29
|
+
const locale = resolvedParams.locale;
|
|
30
|
+
const siteId = "current-site-id";
|
|
31
|
+
|
|
32
|
+
const manifest = PluginRegistry.getDefinition(prefix);
|
|
33
|
+
|
|
34
|
+
// FIX: Store the component in useMemo, but type it as 'any' or 'React.ElementType'
|
|
35
|
+
// to prevent the linter from thinking we are creating a component type.
|
|
36
|
+
const PluginComponent = useMemo(() => {
|
|
37
|
+
if (!manifest || !manifest.enabled) return null;
|
|
38
|
+
try {
|
|
39
|
+
// This returns the reference already stored in your Registry Map
|
|
40
|
+
return PluginRegistry.resolveComponent(manifest.repo);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`Failed to resolve component for ${manifest.repo}:`, error);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}, [manifest]);
|
|
46
|
+
|
|
47
|
+
const [clientProps, setClientProps] = useState<ClientPluginProps | null>(() => {
|
|
48
|
+
if (typeof window !== 'undefined' && manifest && window.__JHITS_PLUGIN_PROPS__) {
|
|
49
|
+
return window.__JHITS_PLUGIN_PROPS__[manifest.repo] || null;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const hasUpdatedFromEffect = useRef(false);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (typeof window === 'undefined' || !manifest) return;
|
|
58
|
+
|
|
59
|
+
const checkProps = () => {
|
|
60
|
+
if (window.__JHITS_PLUGIN_PROPS__) {
|
|
61
|
+
const props = window.__JHITS_PLUGIN_PROPS__[manifest.repo];
|
|
62
|
+
if (props && (!clientProps || JSON.stringify(props) !== JSON.stringify(clientProps))) {
|
|
63
|
+
setClientProps(props);
|
|
64
|
+
hasUpdatedFromEffect.current = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
checkProps();
|
|
70
|
+
const timeoutId = setTimeout(() => {
|
|
71
|
+
if (!hasUpdatedFromEffect.current) checkProps();
|
|
72
|
+
}, 0);
|
|
73
|
+
|
|
74
|
+
return () => clearTimeout(timeoutId);
|
|
75
|
+
}, [manifest, clientProps]);
|
|
76
|
+
|
|
77
|
+
if (!manifest || !manifest.enabled || !PluginComponent) {
|
|
78
|
+
return <PluginNotFound requestedPath={prefix} />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pluginProps: PluginProps & ClientPluginProps = {
|
|
82
|
+
subPath,
|
|
83
|
+
siteId,
|
|
84
|
+
locale,
|
|
85
|
+
...(clientProps || {}),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* FIX: Use React.createElement.
|
|
90
|
+
* This bypasses the JSX transformer's check for "creating components during render"
|
|
91
|
+
* because we are explicitly telling React to render a reference we already have.
|
|
92
|
+
*/
|
|
93
|
+
return (
|
|
94
|
+
<div className="plugin-runtime-wrapper h-full w-full overflow-hidden">
|
|
95
|
+
{React.createElement(PluginComponent, pluginProps)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import Sidebar from '../../../components/dashboard/Sidebar';
|
|
2
|
+
import Topbar from '../../../components/dashboard/Topbar';
|
|
3
|
+
import { Providers, AuthGuard } from '../../../components/Providers';
|
|
4
|
+
import '../../../app/globals.css';
|
|
5
|
+
|
|
6
|
+
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<Providers>
|
|
9
|
+
<AuthGuard>
|
|
10
|
+
{/* 1. Main container uses a soft neutral background (slate/neutral) */}
|
|
11
|
+
{/* Dashboard font and styles are applied via globals.css */}
|
|
12
|
+
<div
|
|
13
|
+
data-dashboard="true"
|
|
14
|
+
className="flex h-screen bg-dashboard-bg transition-colors duration-300 font-sans"
|
|
15
|
+
>
|
|
16
|
+
|
|
17
|
+
{/* Sidebar sits on the neutral background */}
|
|
18
|
+
<Sidebar />
|
|
19
|
+
|
|
20
|
+
{/* 2. Content Area Wrapper */}
|
|
21
|
+
<div className="flex-1 flex flex-col min-w-0 overflow-hidden relative z-40">
|
|
22
|
+
<Topbar />
|
|
23
|
+
|
|
24
|
+
{/* 3. The "Stage": This creates the visual separation.
|
|
25
|
+
We add a margin, rounded corners, and a solid background to
|
|
26
|
+
make the content look like it's sitting on a paper/plate.
|
|
27
|
+
|
|
28
|
+
bg-white dark:bg-neutral-900 border border-neutral-200/50 dark:border-neutral-800/50 p-6 md:p-10
|
|
29
|
+
*/}
|
|
30
|
+
<main className="flex-1 overflow-hidden p-2 md:p-4">
|
|
31
|
+
<div className="h-full w-full shadow-sm overflow-hidden">
|
|
32
|
+
<div className="h-full w-full mx-auto overflow-hidden">
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</main>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</AuthGuard>
|
|
40
|
+
</Providers>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTranslations } from 'next-intl';
|
|
3
|
+
import { useSession } from 'next-auth/react';
|
|
4
|
+
import { PLATFORM_MODULES } from '../../../lib/modules-config';
|
|
5
|
+
import { Link } from '@/i18n/navigation';
|
|
6
|
+
import { ArrowRight, Sparkles, LayoutGrid, Activity, Zap, Clock } from 'lucide-react';
|
|
7
|
+
import { motion } from 'framer-motion';
|
|
8
|
+
|
|
9
|
+
export default function DashboardHome() {
|
|
10
|
+
const t = useTranslations('common.dashboard');
|
|
11
|
+
const tSidebar = useTranslations('common.sidebar');
|
|
12
|
+
const { data: session } = useSession();
|
|
13
|
+
const userName = session?.user?.name || 'User';
|
|
14
|
+
|
|
15
|
+
const containerVariants = {
|
|
16
|
+
hidden: { opacity: 0 },
|
|
17
|
+
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const itemVariants = {
|
|
21
|
+
hidden: { y: 20, opacity: 0 },
|
|
22
|
+
visible: { y: 0, opacity: 1 }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<motion.div
|
|
27
|
+
initial="hidden"
|
|
28
|
+
animate="visible"
|
|
29
|
+
variants={containerVariants}
|
|
30
|
+
className="space-y-8 pb-12 h-full overflow-y-scroll scrollbar-hidden"
|
|
31
|
+
>
|
|
32
|
+
{/* 1. HERO SECTION - Your requested style */}
|
|
33
|
+
<header className="relative group p-8 lg:p-12 rounded-[2.5rem] bg-dashboard-card border border-dashboard-border overflow-hidden shadow-xl shadow-neutral-200/20 dark:shadow-neutral-950/30">
|
|
34
|
+
<div className="relative z-10 grid lg:grid-cols-2 gap-8 items-center">
|
|
35
|
+
<div>
|
|
36
|
+
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary font-bold text-[10px] uppercase tracking-widest mb-6">
|
|
37
|
+
<Sparkles className="size-3" />
|
|
38
|
+
<span>{t('status')}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<h1 className="text-4xl lg:text-5xl font-black tracking-tight text-dashboard-text mb-4 leading-[1.1]">
|
|
41
|
+
{t('welcome', { name: userName })}<span className="text-primary">.</span>
|
|
42
|
+
</h1>
|
|
43
|
+
<p className="text-neutral-600 dark:text-neutral-400 max-w-md text-base lg:text-lg leading-relaxed mb-8">
|
|
44
|
+
{t('description')}
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<div className="flex flex-wrap gap-4">
|
|
48
|
+
{[
|
|
49
|
+
{ label: 'System', value: 'Active', icon: Activity },
|
|
50
|
+
{ label: 'Performance', value: '99%', icon: Zap },
|
|
51
|
+
{ label: 'Uptime', value: '24d', icon: Clock },
|
|
52
|
+
].map((stat, i) => (
|
|
53
|
+
<div key={i} className="flex items-center gap-2 px-4 py-2 bg-dashboard-bg rounded-2xl border border-dashboard-border">
|
|
54
|
+
<stat.icon className="size-3.5 text-neutral-500 dark:text-neutral-400" />
|
|
55
|
+
<span className="text-[10px] font-bold uppercase tracking-tight text-neutral-600 dark:text-neutral-500">{stat.label}:</span>
|
|
56
|
+
<span className="text-[10px] font-black uppercase text-dashboard-text">{stat.value}</span>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="hidden lg:flex justify-end relative">
|
|
63
|
+
<div className="size-64 bg-primary/10 rounded-full blur-[80px] absolute top-0 right-0 animate-pulse" />
|
|
64
|
+
<LayoutGrid className="size-48 text-neutral-200 dark:text-neutral-800/40 rotate-12 relative z-10" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
{/* The "Glow" finish */}
|
|
68
|
+
<div className="absolute top-0 right-0 w-1/3 h-full bg-linear-to-l from-primary/5 to-transparent pointer-events-none" />
|
|
69
|
+
</header>
|
|
70
|
+
|
|
71
|
+
{/* 2. MODULAR GRID - Adapting the Hero style to the cards */}
|
|
72
|
+
<section>
|
|
73
|
+
<div className="flex items-center justify-between mb-8 px-2">
|
|
74
|
+
<h2 className="text-2xl font-black text-dashboard-text uppercase tracking-tighter italic">
|
|
75
|
+
{t('your_modules')}
|
|
76
|
+
</h2>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
80
|
+
{PLATFORM_MODULES.map((module) => (
|
|
81
|
+
<motion.div key={module.id} variants={itemVariants}>
|
|
82
|
+
<Link
|
|
83
|
+
href={module.path as Parameters<typeof Link>[0]['href']}
|
|
84
|
+
className="group relative flex flex-col h-full p-8 bg-dashboard-card border border-dashboard-border rounded-[2.5rem] hover:border-primary transition-all duration-500 shadow-sm hover:shadow-2xl hover:shadow-primary/5 overflow-hidden"
|
|
85
|
+
>
|
|
86
|
+
{/* Static "Lift" Background for Dark Mode - ensures card body is visible */}
|
|
87
|
+
<div className="absolute inset-0 bg-linear-to-br from-transparent via-transparent to-neutral-100/50 dark:to-neutral-800/20 pointer-events-none" />
|
|
88
|
+
|
|
89
|
+
<div className="relative z-10">
|
|
90
|
+
<div className="mb-6 size-14 bg-dashboard-bg rounded-2xl flex items-center justify-center group-hover:bg-primary transition-all duration-500 border border-dashboard-border">
|
|
91
|
+
<span className="font-black text-2xl group-hover:text-white text-neutral-500 dark:text-neutral-400">
|
|
92
|
+
{module.id.charAt(0).toUpperCase()}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<h3 className="text-xl font-black text-dashboard-text uppercase tracking-tight mb-3 group-hover:text-primary transition-colors">
|
|
97
|
+
{tSidebar(module.id)}
|
|
98
|
+
</h3>
|
|
99
|
+
|
|
100
|
+
<p className="text-neutral-600 dark:text-neutral-400 text-sm leading-relaxed mb-8 line-clamp-2">
|
|
101
|
+
{t(`module_desc.${module.id}`)}
|
|
102
|
+
</p>
|
|
103
|
+
|
|
104
|
+
<div className="flex items-center justify-between pt-4 border-t border-dashboard-border">
|
|
105
|
+
<span className="text-[10px] font-black uppercase tracking-widest text-neutral-500 group-hover:text-primary transition-colors">
|
|
106
|
+
{t('open_module')}
|
|
107
|
+
</span>
|
|
108
|
+
<ArrowRight className="size-5 text-neutral-400 group-hover:text-primary group-hover:translate-x-1 transition-all" />
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Hover Accent Glow */}
|
|
113
|
+
<div className="absolute -bottom-10 -right-10 size-32 bg-primary/5 rounded-full blur-2xl group-hover:bg-primary/10 transition-all" />
|
|
114
|
+
</Link>
|
|
115
|
+
</motion.div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
</section>
|
|
119
|
+
</motion.div>
|
|
120
|
+
);
|
|
121
|
+
}
|