@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,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
+ }