@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,295 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { useTheme } from "next-themes";
|
|
5
|
+
import { motion } from "framer-motion";
|
|
6
|
+
import { createPortal } from "react-dom";
|
|
7
|
+
import {
|
|
8
|
+
Sun, Globe, Save, Settings, LayoutDashboard,
|
|
9
|
+
Shield, EyeOff, AlertTriangle, Download, Bell, ChevronDown
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
|
|
12
|
+
import { routing } from '@/i18n/routing';
|
|
13
|
+
import { getPreferences, updatePreferences } from "../../../actions/preferences";
|
|
14
|
+
import { UserPreferences } from "../../../../types/preferences";
|
|
15
|
+
import Image from "next/image";
|
|
16
|
+
|
|
17
|
+
// --- LOGIC HELPERS ---
|
|
18
|
+
const getFlagUrl = (localeCode: string) => {
|
|
19
|
+
try {
|
|
20
|
+
const region = new Intl.Locale(localeCode).maximize().region ||
|
|
21
|
+
(localeCode === 'en' ? 'US' : localeCode.toUpperCase());
|
|
22
|
+
return `https://purecatamphetamine.github.io/country-flag-icons/3x2/${region}.svg`;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return `https://purecatamphetamine.github.io/country-flag-icons/3x2/UN.svg`;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getLanguageLabel = (code: string) => {
|
|
29
|
+
try {
|
|
30
|
+
const displayNames = new Intl.DisplayNames([code], { type: 'language' });
|
|
31
|
+
const name = displayNames.of(code);
|
|
32
|
+
return name ? name.charAt(0).toUpperCase() + name.slice(1) : code.toUpperCase();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return code.toUpperCase();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// --- SUB-COMPONENTS ---
|
|
39
|
+
|
|
40
|
+
function LocalizationDropdown({ value, onChange }: { value: string, onChange: (v: string) => void }) {
|
|
41
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
42
|
+
const [mounted, setMounted] = useState(false);
|
|
43
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const [coords, setCoords] = useState({ top: 0, left: 0, width: 0 });
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
setMounted(true);
|
|
49
|
+
}, 0);
|
|
50
|
+
if (isOpen && containerRef.current) {
|
|
51
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
52
|
+
setCoords({ top: rect.bottom + window.scrollY, left: rect.left + window.scrollX, width: rect.width });
|
|
53
|
+
}
|
|
54
|
+
}, [isOpen]);
|
|
55
|
+
|
|
56
|
+
const availableLocales = routing.locales;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="relative" ref={containerRef}>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
63
|
+
className="w-full bg-white dark:bg-neutral-900 border border-neutral-300 dark:border-neutral-700 rounded-2xl p-4 flex items-center justify-between group hover:border-primary transition-all"
|
|
64
|
+
>
|
|
65
|
+
<div className="flex items-center gap-4">
|
|
66
|
+
<div className="w-8 h-6 rounded-md overflow-hidden border border-neutral-200 dark:border-neutral-800 shadow-sm shrink-0">
|
|
67
|
+
<Image src={getFlagUrl(value)} alt={value} className="w-full h-full object-cover" width={32} height={24} />
|
|
68
|
+
</div>
|
|
69
|
+
<span className="text-[10px] font-black uppercase tracking-widest text-neutral-900 dark:text-neutral-100">
|
|
70
|
+
{getLanguageLabel(value)}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
<ChevronDown className={`size-4 text-neutral-400 group-hover:text-primary transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
|
|
74
|
+
</button>
|
|
75
|
+
|
|
76
|
+
{mounted && isOpen && createPortal(
|
|
77
|
+
<>
|
|
78
|
+
<div className="fixed inset-0 z-9998" onClick={() => setIsOpen(false)} />
|
|
79
|
+
<motion.div
|
|
80
|
+
initial={{ opacity: 0, y: 10 }}
|
|
81
|
+
animate={{ opacity: 1, y: 5 }}
|
|
82
|
+
exit={{ opacity: 0, y: 10 }}
|
|
83
|
+
style={{ top: coords.top, left: coords.left, width: coords.width }}
|
|
84
|
+
className="fixed z-9999 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-2xl shadow-2xl p-2 max-h-60 overflow-y-auto custom-scrollbar"
|
|
85
|
+
>
|
|
86
|
+
{availableLocales.map((locale) => (
|
|
87
|
+
<button
|
|
88
|
+
key={locale}
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={() => { onChange(locale); setIsOpen(false); }}
|
|
91
|
+
className={`w-full text-left p-3 rounded-xl flex items-center gap-4 transition-colors ${value === locale ? 'bg-primary text-white' : 'hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-500 dark:text-neutral-400'}`}
|
|
92
|
+
>
|
|
93
|
+
<div className="w-7 h-5 rounded-sm overflow-hidden shrink-0 border border-black/10">
|
|
94
|
+
<Image src={getFlagUrl(locale)} alt={locale} className="w-full h-full object-cover" width={28} height={20} />
|
|
95
|
+
</div>
|
|
96
|
+
<span className="text-[10px] font-black uppercase tracking-widest">{getLanguageLabel(locale)}</span>
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
</motion.div>
|
|
100
|
+
</>,
|
|
101
|
+
document.body
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function PreferenceToggle({ icon, title, description, enabled, onChange }: { icon: React.ReactNode, title: string, description: string, enabled: boolean, onChange: (v: boolean) => void }) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex items-center justify-between p-4 rounded-2xl bg-white/50 dark:bg-neutral-900/40 border border-neutral-200 dark:border-neutral-700/50">
|
|
110
|
+
<div className="flex items-center gap-4">
|
|
111
|
+
<div className="size-10 rounded-xl bg-white dark:bg-neutral-800 flex items-center justify-center border border-neutral-200 dark:border-neutral-700 shadow-sm text-neutral-500">
|
|
112
|
+
{icon}
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<p className="text-xs font-black uppercase text-neutral-900 dark:text-neutral-100">{title}</p>
|
|
116
|
+
<p className="text-[9px] text-neutral-400 font-bold uppercase tracking-tight">{description}</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={() => onChange(!enabled)}
|
|
122
|
+
className={`w-12 h-6 rounded-full transition-colors relative ${enabled ? 'bg-emerald-500' : 'bg-neutral-300 dark:bg-neutral-700'}`}
|
|
123
|
+
>
|
|
124
|
+
<motion.div
|
|
125
|
+
animate={{ x: enabled ? 26 : 4 }}
|
|
126
|
+
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|
127
|
+
className="absolute top-1 size-4 bg-white rounded-full shadow-sm"
|
|
128
|
+
/>
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default function PreferencesPage() {
|
|
135
|
+
const [prefs, setPrefs] = useState<UserPreferences | null>(null);
|
|
136
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
137
|
+
const { theme, setTheme } = useTheme();
|
|
138
|
+
|
|
139
|
+
// 1. Listen for global theme changes (e.g. from Topbar) and sync local state
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (prefs && theme && prefs.theme !== theme) {
|
|
142
|
+
setPrefs({ ...prefs, theme: theme as UserPreferences['theme'] });
|
|
143
|
+
}
|
|
144
|
+
}, [theme, prefs]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => { getPreferences().then(setPrefs); }, []);
|
|
147
|
+
|
|
148
|
+
const handleSave = async () => {
|
|
149
|
+
if (!prefs) return;
|
|
150
|
+
setIsSaving(true);
|
|
151
|
+
try { await updatePreferences(prefs); alert("Preferences Saved"); } finally { setIsSaving(false); }
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (!prefs) return <div className="p-8 text-[10px] font-black uppercase text-neutral-400 animate-pulse">Synchronizing Engine...</div>;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="max-w-6xl mx-auto space-y-8">
|
|
158
|
+
{/* Header */}
|
|
159
|
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
160
|
+
<div>
|
|
161
|
+
<h1 className="text-3xl font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tighter flex items-center gap-3">
|
|
162
|
+
<Settings className="size-8 text-primary" />
|
|
163
|
+
Settings
|
|
164
|
+
</h1>
|
|
165
|
+
<p className="text-xs font-bold text-neutral-400 uppercase tracking-widest mt-1">Environment Configuration</p>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<motion.button
|
|
169
|
+
whileHover={{ scale: 1.02 }}
|
|
170
|
+
whileTap={{ scale: 0.98 }}
|
|
171
|
+
onClick={handleSave}
|
|
172
|
+
disabled={isSaving}
|
|
173
|
+
className="bg-primary text-white px-8 py-3 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-primary/20 flex items-center gap-2"
|
|
174
|
+
>
|
|
175
|
+
{isSaving ? <div className="size-3 border-2 border-white border-t-transparent rounded-full animate-spin" /> : <Save className="size-4" />}
|
|
176
|
+
{isSaving ? "Saving..." : "Save Changes"}
|
|
177
|
+
</motion.button>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
181
|
+
{/* Visual Environment */}
|
|
182
|
+
<section className="p-8 rounded-4xl bg-neutral-100 dark:bg-neutral-700/30 border border-neutral-300 dark:border-neutral-700 shadow-sm overflow-hidden">
|
|
183
|
+
<h2 className="text-lg font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight mb-8 flex items-center gap-2">
|
|
184
|
+
<Sun className="size-5 text-primary" /> Appearance
|
|
185
|
+
</h2>
|
|
186
|
+
<div className="grid grid-cols-3 gap-2 p-1.5 bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800">
|
|
187
|
+
{(['light', 'dark', 'system'] as const).map((t) => (
|
|
188
|
+
<button
|
|
189
|
+
key={t}
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={() => { setPrefs({ ...prefs, theme: t }); setTheme(t); }}
|
|
192
|
+
className={`py-3 text-[10px] font-black uppercase rounded-xl transition-all ${prefs.theme === t
|
|
193
|
+
? 'bg-primary text-white shadow-lg'
|
|
194
|
+
: 'text-neutral-500 hover:text-neutral-900 dark:hover:text-neutral-300'
|
|
195
|
+
}`}
|
|
196
|
+
>
|
|
197
|
+
{t}
|
|
198
|
+
</button>
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
|
|
203
|
+
{/* Regional Settings */}
|
|
204
|
+
<section className="p-8 rounded-4xl bg-neutral-100 dark:bg-neutral-700/30 border border-neutral-300 dark:border-neutral-700 shadow-sm">
|
|
205
|
+
<h2 className="text-lg font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight mb-8 flex items-center gap-2">
|
|
206
|
+
<Globe className="size-5 text-primary" /> Localization
|
|
207
|
+
</h2>
|
|
208
|
+
<LocalizationDropdown
|
|
209
|
+
value={prefs.language}
|
|
210
|
+
onChange={(v) => setPrefs({ ...prefs, language: v })}
|
|
211
|
+
/>
|
|
212
|
+
</section>
|
|
213
|
+
|
|
214
|
+
{/* Workspace Management */}
|
|
215
|
+
<section className="p-8 rounded-4xl bg-neutral-100 dark:bg-neutral-700/30 border border-neutral-300 dark:border-neutral-700 shadow-sm">
|
|
216
|
+
<h2 className="text-lg font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight mb-8 flex items-center gap-2">
|
|
217
|
+
<LayoutDashboard className="size-5 text-primary" /> Workspace
|
|
218
|
+
</h2>
|
|
219
|
+
<div className="flex gap-4">
|
|
220
|
+
{(['grid', 'list'] as const).map((view) => (
|
|
221
|
+
<button
|
|
222
|
+
key={view}
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={() => setPrefs({ ...prefs, dashboardView: view })}
|
|
225
|
+
className={`flex-1 py-4 rounded-2xl border-2 text-[10px] font-black uppercase transition-all ${prefs.dashboardView === view
|
|
226
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
227
|
+
: 'border-white dark:border-neutral-800 bg-white dark:bg-neutral-900 text-neutral-400'
|
|
228
|
+
}`}
|
|
229
|
+
>
|
|
230
|
+
{view} Layout
|
|
231
|
+
</button>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
</section>
|
|
235
|
+
|
|
236
|
+
{/* Privacy & Visibility */}
|
|
237
|
+
<section className="p-8 rounded-4xl bg-neutral-100 dark:bg-neutral-700/30 border border-neutral-300 dark:border-neutral-700 shadow-sm">
|
|
238
|
+
<h2 className="text-lg font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight mb-8 flex items-center gap-2">
|
|
239
|
+
<Shield className="size-5 text-primary" /> Privacy
|
|
240
|
+
</h2>
|
|
241
|
+
<PreferenceToggle
|
|
242
|
+
icon={<EyeOff className="size-4" />}
|
|
243
|
+
title="Stealth Mode"
|
|
244
|
+
description="Hide active status"
|
|
245
|
+
enabled={prefs.privacy.stealthMode}
|
|
246
|
+
onChange={(v: boolean) => setPrefs({ ...prefs, privacy: { ...prefs.privacy, stealthMode: v } })}
|
|
247
|
+
/>
|
|
248
|
+
</section>
|
|
249
|
+
|
|
250
|
+
{/* Notification Hub */}
|
|
251
|
+
<section className="md:col-span-2 p-8 rounded-4xl bg-neutral-100 dark:bg-neutral-700/30 border border-neutral-300 dark:border-neutral-700 shadow-sm">
|
|
252
|
+
<h2 className="text-lg font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight mb-8 flex items-center gap-2">
|
|
253
|
+
<Bell className="size-5 text-primary" /> Notification Hub
|
|
254
|
+
</h2>
|
|
255
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
256
|
+
<PreferenceToggle
|
|
257
|
+
icon={<Shield className="size-4 text-emerald-500" />}
|
|
258
|
+
title="Security Alerts"
|
|
259
|
+
description="Critical login updates"
|
|
260
|
+
enabled={prefs.notifications.security}
|
|
261
|
+
onChange={(v: boolean) => setPrefs({ ...prefs, notifications: { ...prefs.notifications, security: v } })}
|
|
262
|
+
/>
|
|
263
|
+
<PreferenceToggle
|
|
264
|
+
icon={<Bell className="size-4 text-primary" />}
|
|
265
|
+
title="Product Intel"
|
|
266
|
+
description="Feature releases"
|
|
267
|
+
enabled={prefs.notifications.marketing}
|
|
268
|
+
onChange={(v: boolean) => setPrefs({ ...prefs, notifications: { ...prefs.notifications, marketing: v } })}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
</section>
|
|
272
|
+
|
|
273
|
+
{/* Danger Zone */}
|
|
274
|
+
<section className="md:col-span-2 p-8 rounded-4xl bg-red-500/5 dark:bg-red-500/10 border border-red-500/20 shadow-sm">
|
|
275
|
+
<div className="flex flex-col md:flex-row justify-between items-center gap-6 text-center md:text-left">
|
|
276
|
+
<div>
|
|
277
|
+
<h2 className="text-[10px] font-black uppercase text-red-500 flex items-center gap-2 mb-1 justify-center md:justify-start">
|
|
278
|
+
<AlertTriangle className="size-4" /> Danger Zone
|
|
279
|
+
</h2>
|
|
280
|
+
<p className="text-[10px] text-neutral-500 dark:text-neutral-400 font-bold uppercase">Irreversible account & data operations</p>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="flex gap-4 w-full md:w-auto">
|
|
283
|
+
<button className="flex-1 md:flex-none px-6 py-3 rounded-xl bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 text-[10px] font-black uppercase transition-all hover:bg-neutral-50">
|
|
284
|
+
<Download className="size-3 mr-2 inline" /> Export Data
|
|
285
|
+
</button>
|
|
286
|
+
<button className="flex-1 md:flex-none px-6 py-3 rounded-xl bg-red-500 text-white text-[10px] font-black uppercase hover:bg-red-600 transition-all shadow-lg shadow-red-500/20">
|
|
287
|
+
Delete Account
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</section>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|