@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,491 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import { useTranslations } from 'next-intl';
5
+ import { motion, AnimatePresence } from 'framer-motion';
6
+ import { useSession } from 'next-auth/react';
7
+ import {
8
+ User, Shield, Bell, Camera, Check,
9
+ ChevronRight, Key, CreditCard,
10
+ Activity, Trash2, X, Laptop, Smartphone, Globe
11
+ } from 'lucide-react';
12
+
13
+ import {
14
+ changePassword,
15
+ getUserProfile,
16
+ getUserSessions,
17
+ revokeSession,
18
+ updateProfileFields,
19
+ updateProfileImage
20
+ } from '../../../actions/user';
21
+ import { createPortal } from 'react-dom';
22
+ import Image from 'next/image';
23
+
24
+ // --- TYPES ---
25
+ interface UserData {
26
+ name: string;
27
+ email: string;
28
+ image?: string;
29
+ role?: string;
30
+ }
31
+
32
+ interface SessionLog {
33
+ id: string;
34
+ device: string;
35
+ browser: string;
36
+ location: string;
37
+ isCurrent: boolean;
38
+ lastActive: string;
39
+ }
40
+
41
+ export default function ProfilePage() {
42
+ const t = useTranslations('common.profile');
43
+ const { data: session, update } = useSession();
44
+ const fileInputRef = useRef<HTMLInputElement>(null);
45
+
46
+ // Global States
47
+ const [user, setUser] = useState<UserData | null>(null);
48
+ const [originalUser, setOriginalUser] = useState<UserData | null>(null);
49
+ const [loading, setLoading] = useState(true);
50
+ const [isSaving, setIsSaving] = useState(false);
51
+ const [isUploading, setIsUploading] = useState(false);
52
+ const [sessions, setSessions] = useState<SessionLog[]>([]);
53
+
54
+ // Feature States
55
+ const [showPasswordModal, setShowPasswordModal] = useState(false);
56
+ const [isChangingPwd, setIsChangingPwd] = useState(false);
57
+ const [passwords, setPasswords] = useState({ current: '', new: '', confirm: '' });
58
+
59
+ useEffect(() => { loadData(); }, []);
60
+
61
+ async function loadData() {
62
+ setLoading(true);
63
+ try {
64
+ const [profile, activeSessions] = await Promise.all([
65
+ getUserProfile(),
66
+ getUserSessions()
67
+ ]);
68
+ setUser(profile);
69
+ setOriginalUser(profile);
70
+ setSessions(activeSessions);
71
+ } catch (e) {
72
+ console.error(e);
73
+ } finally {
74
+ setLoading(false);
75
+ }
76
+ }
77
+
78
+ const handleRevokeSession = async (id: string) => {
79
+ try {
80
+ const result = await revokeSession(id);
81
+ if (result.success) {
82
+ setSessions(prev => prev.filter(s => s.id !== id));
83
+ }
84
+ } catch (error: unknown) {
85
+ const errorMessage = error instanceof Error ? error.message : "Failed to revoke session";
86
+ alert(errorMessage);
87
+ }
88
+ };
89
+
90
+ const handleUpdateProfile = async () => {
91
+ if (!user) return;
92
+ setIsSaving(true);
93
+ try {
94
+ const result = await updateProfileFields(user.name, user.email);
95
+ if (result.success) {
96
+ await update({ user: { ...session?.user, name: result.name, email: result.email } });
97
+ setOriginalUser({ ...user });
98
+ alert("Profile updated!");
99
+ }
100
+ } catch (error: unknown) {
101
+ const errorMessage = error instanceof Error ? error.message : "Failed to update profile";
102
+ alert(errorMessage);
103
+ } finally {
104
+ setIsSaving(false);
105
+ }
106
+ };
107
+
108
+ const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
109
+ const file = e.target.files?.[0];
110
+ if (!file) return;
111
+ setIsUploading(true);
112
+ const reader = new FileReader();
113
+ reader.onloadend = async () => {
114
+ try {
115
+ const result = await updateProfileImage(reader.result as string);
116
+ if (result.success) {
117
+ await update({ user: { ...session?.user, image: result.image } });
118
+ await loadData();
119
+ }
120
+ } catch (error) { alert("Upload failed"); } finally { setIsUploading(false); }
121
+ };
122
+ reader.readAsDataURL(file);
123
+ };
124
+
125
+ const handleRemoveImage = async (e: React.MouseEvent) => {
126
+ e.stopPropagation();
127
+ if (!confirm("Remove photo?")) return;
128
+ setIsUploading(true);
129
+ try {
130
+ const result = await updateProfileImage(null);
131
+ if (result.success) {
132
+ await update({ user: { ...session?.user, image: null } });
133
+ await loadData();
134
+ }
135
+ } catch (error) { console.error(error); } finally { setIsUploading(false); }
136
+ };
137
+
138
+ const handlePasswordChange = async (e: React.FormEvent) => {
139
+ e.preventDefault();
140
+ if (passwords.new !== passwords.confirm) return alert("Passwords mismatch");
141
+ setIsChangingPwd(true);
142
+ try {
143
+ const result = await changePassword(passwords.current, passwords.new);
144
+ if (result.success) {
145
+ setShowPasswordModal(false);
146
+ setPasswords({ current: '', new: '', confirm: '' });
147
+ alert("Password updated");
148
+ }
149
+ } catch (error: unknown) {
150
+ const errorMessage = error instanceof Error ? error.message : "Failed to change password";
151
+ alert(errorMessage);
152
+ } finally { setIsChangingPwd(false); }
153
+ };
154
+
155
+ if (loading) return <LoadingSpinner />;
156
+
157
+ return (
158
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-8 pb-12">
159
+
160
+ <ProfileHero
161
+ user={user}
162
+ isUploading={isUploading}
163
+ fileInputRef={fileInputRef as React.RefObject<HTMLInputElement>}
164
+ onImageChange={handleImageChange}
165
+ onRemoveImage={handleRemoveImage}
166
+ />
167
+
168
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
169
+ <div className="lg:col-span-2 space-y-8">
170
+ <PersonalInfoSection
171
+ user={user}
172
+ setUser={setUser}
173
+ originalUser={originalUser}
174
+ isSaving={isSaving}
175
+ onSave={handleUpdateProfile}
176
+ />
177
+ <SessionHistorySection
178
+ sessions={sessions}
179
+ onRevoke={handleRevokeSession}
180
+ />
181
+ </div>
182
+
183
+ <div className="space-y-6">
184
+ <SecurityActions onPasswordClick={() => setShowPasswordModal(true)} />
185
+ <StatusCard />
186
+ </div>
187
+ </div>
188
+
189
+ <AnimatePresence>
190
+ {showPasswordModal && (
191
+ <PasswordModal
192
+ passwords={passwords}
193
+ setPasswords={setPasswords}
194
+ isChanging={isChangingPwd}
195
+ onClose={() => setShowPasswordModal(false)}
196
+ onSubmit={handlePasswordChange}
197
+ />
198
+ )}
199
+ </AnimatePresence>
200
+ </motion.div>
201
+ );
202
+ }
203
+
204
+ // --- MODULAR SUB-COMPONENTS ---
205
+
206
+ function PersonalInfoSection({ user, setUser, originalUser, isSaving, onSave }: { user: UserData | null, setUser: (user: UserData) => void, originalUser: UserData | null, isSaving: boolean, onSave: () => void }) {
207
+ const hasChanges = user?.name !== originalUser?.name || user?.email !== originalUser?.email;
208
+
209
+ return (
210
+ <section className="p-8 rounded-4xl bg-neutral-100 dark:bg-neutral-700/30 border border-neutral-300 dark:border-neutral-700 shadow-sm transition-all overflow-hidden">
211
+ <motion.div layout className="flex items-center justify-between mb-8">
212
+ <h2 className="text-lg font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight flex items-center gap-2">
213
+ <User className="size-5 text-primary" />
214
+ Personal Information
215
+ </h2>
216
+
217
+ <AnimatePresence mode="wait">
218
+ {(hasChanges || isSaving) && (
219
+ <motion.button
220
+ key="save-button"
221
+ layout
222
+ initial={{ opacity: 0, scale: 0.9, x: 20 }}
223
+ animate={{ opacity: 1, scale: 1, x: 0 }}
224
+ exit={{ opacity: 0, scale: 0.9, x: 10 }}
225
+ transition={{ type: "spring", stiffness: 400, damping: 30 }}
226
+ onClick={onSave}
227
+ disabled={isSaving}
228
+ className="relative px-6 py-2.5 bg-primary text-white text-[10px] font-black uppercase tracking-widest rounded-xl hover:scale-[1.05] active:scale-95 shadow-lg shadow-primary/20 flex items-center justify-center min-w-[120px]"
229
+ >
230
+ <AnimatePresence mode="wait">
231
+ {isSaving ? (
232
+ <motion.div key="updating" initial={{ y: 15, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -15, opacity: 0 }} className="flex items-center gap-2">
233
+ <div className="size-3 border-2 border-white border-t-transparent rounded-full animate-spin" />
234
+ <span>Updating</span>
235
+ </motion.div>
236
+ ) : (
237
+ <motion.span key="save" initial={{ y: 15, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -15, opacity: 0 }}>
238
+ Save Changes
239
+ </motion.span>
240
+ )}
241
+ </AnimatePresence>
242
+ </motion.button>
243
+ )}
244
+ </AnimatePresence>
245
+ </motion.div>
246
+
247
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
248
+ <InputGroup
249
+ label="Full Name"
250
+ value={user?.name}
251
+ onChange={(v: string) => setUser({ ...user, name: v } as UserData)}
252
+ />
253
+ <InputGroup
254
+ label="Email Address"
255
+ value={user?.email}
256
+ onChange={(v: string) => setUser({ ...user, email: v } as UserData)}
257
+ />
258
+ </div>
259
+ </section>
260
+ );
261
+ }
262
+
263
+ function ProfileHero({ user, isUploading, fileInputRef, onImageChange, onRemoveImage }: { user: UserData | null, isUploading: boolean, fileInputRef: React.RefObject<HTMLInputElement>, onImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void, onRemoveImage: (e: React.MouseEvent) => void }) {
264
+ const initials = user?.name?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2);
265
+ return (
266
+ <header className="relative p-8 lg:p-10 rounded-[2.5rem] bg-neutral-100 dark:bg-neutral-700/50 border border-neutral-300 dark:border-neutral-700 overflow-hidden shadow-xl">
267
+ <div className="relative z-10 flex flex-col md:flex-row items-center gap-8">
268
+ <div className="relative group">
269
+ <input type="file" ref={fileInputRef} onChange={onImageChange} className="hidden" accept="image/*" />
270
+ <div onClick={() => fileInputRef.current?.click()} className={`size-32 lg:size-40 rounded-[2.5rem] bg-primary flex items-center justify-center text-white text-4xl font-black shadow-2xl overflow-hidden cursor-pointer transition-all ${isUploading ? 'opacity-50 grayscale' : ''}`}>
271
+ {user?.image ? <Image src={user.image} className="w-full h-full object-cover" alt="Profile" width={128} height={128} /> : initials}
272
+ <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"><Camera className="size-8" /></div>
273
+ </div>
274
+ {user?.image && (
275
+ <button onClick={onRemoveImage} className="absolute -top-2 -right-2 size-10 rounded-2xl bg-red-500 border-4 border-neutral-100 dark:border-neutral-800 flex items-center justify-center shadow-lg"><Trash2 className="size-4 text-white" /></button>
276
+ )}
277
+ <div className="absolute -bottom-2 -right-2 size-10 rounded-2xl bg-emerald-500 border-4 border-neutral-100 dark:border-neutral-800 flex items-center justify-center shadow-lg"><Check className="size-5 text-white" strokeWidth={3} /></div>
278
+ </div>
279
+ <div className="text-center md:text-left">
280
+ <h1 className="text-3xl lg:text-4xl font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight">{user?.name || 'User'}<span className="text-primary">.</span></h1>
281
+ <p className="text-neutral-600 dark:text-neutral-400 font-medium mb-3">{user?.email}</p>
282
+ <div className="flex gap-2 justify-center md:justify-start">
283
+ <span className="px-3 py-1 rounded-full bg-neutral-200 dark:bg-neutral-900 text-[10px] font-black uppercase text-neutral-500 dark:text-neutral-400">{user?.role || 'User'}</span>
284
+ <span className="px-3 py-1 rounded-full bg-primary/10 text-primary text-[10px] font-black uppercase tracking-widest">Verified Account</span>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ <div className="absolute top-0 right-0 w-64 h-64 bg-primary/10 rounded-full blur-3xl -mr-20 -mt-20" />
289
+ </header>
290
+ );
291
+ }
292
+
293
+ function SessionHistorySection({ sessions, onRevoke }: { sessions: SessionLog[], onRevoke: (id: string) => void }) {
294
+ const [revokingId, setRevokingId] = useState<string | null>(null);
295
+
296
+ const handleRevoke = async (id: string) => {
297
+ if (!confirm("Are you sure you want to terminate this session?")) return;
298
+ setRevokingId(id);
299
+ try { await onRevoke(id); } finally { setRevokingId(null); }
300
+ };
301
+
302
+ return (
303
+ <section className="p-8 rounded-4xl bg-neutral-100 dark:bg-neutral-700/30 border border-neutral-300 dark:border-neutral-700 shadow-sm">
304
+ <h2 className="text-lg font-black text-neutral-950 dark:text-neutral-50 uppercase tracking-tight mb-6 flex items-center gap-2">
305
+ <Activity className="size-5 text-primary" />
306
+ Login Activity
307
+ </h2>
308
+
309
+ <div className="space-y-4">
310
+ <AnimatePresence mode="popLayout">
311
+ {sessions.map((sess: SessionLog) => (
312
+ <motion.div
313
+ key={sess.id}
314
+ layout
315
+ initial={{ opacity: 0, scale: 0.95 }}
316
+ animate={{ opacity: 1, scale: 1 }}
317
+ exit={{ opacity: 0, scale: 0.95, x: -20 }}
318
+ 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"
319
+ >
320
+ <div className="flex items-center gap-4">
321
+ <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">
322
+ {sess.device.toLowerCase().includes('phone') ? (
323
+ <Smartphone className="size-5 text-neutral-500" />
324
+ ) : (
325
+ <Laptop className="size-5 text-neutral-500" />
326
+ )}
327
+ </div>
328
+ <div>
329
+ <div className="flex items-center gap-2">
330
+ <p className="text-xs font-black uppercase text-neutral-900 dark:text-neutral-100">
331
+ {sess.device} • {sess.browser}
332
+ </p>
333
+ {sess.isCurrent && (
334
+ <span className="text-[8px] bg-emerald-500/10 text-emerald-500 px-2 py-0.5 rounded-full border border-emerald-500/20 font-black uppercase">
335
+ Current
336
+ </span>
337
+ )}
338
+ </div>
339
+ <p className="text-[10px] font-bold text-neutral-400 uppercase flex items-center gap-1 mt-0.5">
340
+ <Globe className="size-3" /> {sess.location} • {sess.lastActive}
341
+ </p>
342
+ </div>
343
+ </div>
344
+
345
+ {!sess.isCurrent && (
346
+ <button
347
+ onClick={() => handleRevoke(sess.id)}
348
+ disabled={revokingId === sess.id}
349
+ className="text-[10px] font-black uppercase text-red-500 px-3 py-1.5 hover:bg-red-500/10 rounded-lg transition-all"
350
+ >
351
+ {revokingId === sess.id ? (
352
+ <div className="size-3 border-2 border-red-500 border-t-transparent rounded-full animate-spin" />
353
+ ) : (
354
+ 'Revoke'
355
+ )}
356
+ </button>
357
+ )}
358
+ </motion.div>
359
+ ))}
360
+ </AnimatePresence>
361
+ </div>
362
+ </section>
363
+ );
364
+ }
365
+
366
+ function SecurityActions({ onPasswordClick }: { onPasswordClick: () => void }) {
367
+ return (
368
+ <section className="p-8 rounded-4xl bg-neutral-200 dark:bg-neutral-700/20 border border-neutral-300 dark:border-neutral-700">
369
+ <h2 className="text-sm font-black text-neutral-950 dark:text-neutral-50 uppercase mb-6">Security & Access</h2>
370
+ <div className="space-y-3">
371
+ <ActionButton label="Change Password" icon={Key} onClick={onPasswordClick} />
372
+ <ActionButton label="Two-Factor Auth" icon={Shield} onClick={() => {}} />
373
+ <ActionButton label="Billing Details" icon={CreditCard} onClick={() => {}} />
374
+ </div>
375
+ </section>
376
+ );
377
+ }
378
+
379
+ function ActionButton({ label, icon: Icon, onClick }: { label: string, icon: React.ElementType, onClick: () => void }) {
380
+ return (
381
+ <button onClick={onClick} className="w-full flex items-center justify-between p-4 rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-300 dark:border-neutral-700/50 hover:border-primary transition-all group">
382
+ <div className="flex items-center gap-3">
383
+ <div className="size-8 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center group-hover:bg-primary/10 transition-colors"><Icon className="size-4 text-neutral-500 group-hover:text-primary" /></div>
384
+ <span className="text-xs font-bold text-neutral-700 dark:text-neutral-300 uppercase">{label}</span>
385
+ </div>
386
+ <ChevronRight className="size-4 text-neutral-300 group-hover:translate-x-1 transition-transform" />
387
+ </button>
388
+ );
389
+ }
390
+
391
+ function InputGroup({ label, value, onChange, type = "text" }: { label: string, value: string | undefined, onChange: (v: string) => void, type?: string }) {
392
+ return (
393
+ <div className="space-y-2">
394
+ <label className="text-[10px] font-black uppercase tracking-widest text-neutral-400">{label}</label>
395
+ <input
396
+ type={type} value={value || ""} onChange={(e) => onChange(e.target.value)}
397
+ className="w-full bg-white dark:bg-neutral-900/50 border border-neutral-300 dark:border-neutral-700 p-4 rounded-2xl text-sm font-bold outline-none focus:border-primary transition-all dark:text-neutral-100"
398
+ />
399
+ </div>
400
+ );
401
+ }
402
+
403
+ function PasswordModal({ passwords, setPasswords, isChanging, onClose, onSubmit }: { passwords: { current: string, new: string, confirm: string }, setPasswords: (passwords: { current: string, new: string, confirm: string }) => void, isChanging: boolean, onClose: () => void, onSubmit: (e: React.FormEvent) => void }) {
404
+ // 1. Ensure this only runs on the client
405
+ const [mounted, setMounted] = useState(false);
406
+ useEffect(() => {
407
+ setTimeout(() => {
408
+ setMounted(true);
409
+ }, 0);
410
+ }, []);
411
+
412
+ if (!mounted) return null;
413
+
414
+ // 2. Use createPortal to mount at the bottom of <body>
415
+ return createPortal(
416
+ <div
417
+ // 3. Handle background click to exit
418
+ onClick={onClose}
419
+ className="fixed inset-0 z-9999 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md cursor-pointer"
420
+ >
421
+ <motion.div
422
+ initial={{ scale: 0.9, opacity: 0, y: 20 }}
423
+ animate={{ scale: 1, opacity: 1, y: 0 }}
424
+ exit={{ scale: 0.9, opacity: 0, y: 20 }}
425
+ // 4. Prevent clicks inside the modal from closing it
426
+ onClick={(e) => e.stopPropagation()}
427
+ className="w-full max-w-md bg-white dark:bg-neutral-800 rounded-[2.5rem] p-8 shadow-2xl relative border border-neutral-300 dark:border-white/10 cursor-default"
428
+ >
429
+ <button
430
+ onClick={onClose}
431
+ className="absolute top-6 right-6 text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
432
+ >
433
+ <X className="size-6" />
434
+ </button>
435
+
436
+ <h3 className="text-xl font-black uppercase mb-6 flex items-center gap-2 dark:text-white text-neutral-950">
437
+ <Key className="size-5 text-primary" />
438
+ Security Update
439
+ </h3>
440
+
441
+ <form onSubmit={onSubmit} className="space-y-4">
442
+ <InputGroup
443
+ label="Current Password"
444
+ type="password"
445
+ value={passwords.current}
446
+ onChange={(v: string) => setPasswords({ ...passwords, current: v })}
447
+ />
448
+ <InputGroup
449
+ label="New Password"
450
+ type="password"
451
+ value={passwords.new}
452
+ onChange={(v: string) => setPasswords({ ...passwords, current: passwords.current, new: v })}
453
+ />
454
+ <InputGroup
455
+ label="Confirm New Password"
456
+ type="password"
457
+ value={passwords.confirm}
458
+ onChange={(v: string) => setPasswords({ ...passwords, confirm: v })}
459
+ />
460
+
461
+ <button
462
+ type="submit"
463
+ disabled={isChanging}
464
+ className="w-full py-4 mt-4 bg-primary text-white text-xs font-black uppercase rounded-xl shadow-lg shadow-primary/20 transition-all hover:scale-[1.02] active:scale-95 disabled:opacity-50"
465
+ >
466
+ {isChanging ? 'Updating...' : 'Update Password'}
467
+ </button>
468
+ </form>
469
+ </motion.div>
470
+ </div>,
471
+ document.body
472
+ );
473
+ }
474
+
475
+ function StatusCard() {
476
+ return (
477
+ <div className="p-6 rounded-4xl bg-primary flex items-center justify-between overflow-hidden relative">
478
+ <div className="relative z-10"><p className="text-[10px] font-black uppercase text-white/60 tracking-widest">Global Status</p><h3 className="text-lg font-black text-white uppercase italic">System Notifications</h3></div>
479
+ <div className="size-12 rounded-2xl bg-white/20 backdrop-blur-md flex items-center justify-center z-10"><Bell className="size-6 text-white animate-ring" /></div>
480
+ <div className="absolute -left-4 -bottom-4 size-24 bg-white/10 rounded-full blur-2xl" />
481
+ </div>
482
+ );
483
+ }
484
+
485
+ function LoadingSpinner() {
486
+ return (
487
+ <div className="flex h-[60vh] items-center justify-center">
488
+ <div className="size-12 border-4 border-primary border-t-transparent rounded-full animate-spin" />
489
+ </div>
490
+ );
491
+ }
@@ -0,0 +1,28 @@
1
+ import { NextIntlClientProvider } from 'next-intl';
2
+ import { getMessages } from 'next-intl/server';
3
+ import { Providers } from "../../components/Providers";
4
+ import "../globals.css";
5
+
6
+ export default async function LocaleLayout(props: {
7
+ children: React.ReactNode;
8
+ params: Promise<{ locale: string }>;
9
+ }) {
10
+ // 1. Unwrapping the params (Required for Next.js 15)
11
+ const { locale } = await props.params;
12
+
13
+ // 2. Extracting children from props
14
+ const children = props.children;
15
+
16
+ // 3. Getting the translations
17
+ const messages = await getMessages();
18
+
19
+ return (
20
+ <html lang={locale} suppressHydrationWarning>
21
+ <body className="antialiased">
22
+ <NextIntlClientProvider messages={messages} locale={locale}>
23
+ <Providers>{children}</Providers>
24
+ </NextIntlClientProvider>
25
+ </body>
26
+ </html>
27
+ );
28
+ }
@@ -0,0 +1,40 @@
1
+ "use server";
2
+
3
+ import { getServerSession } from "next-auth";
4
+ import { authOptions } from "../../lib/auth";
5
+ import clientPromise from "../../lib/mongodb";
6
+ import { UserPreferences } from "../../types/preferences";
7
+
8
+ const DEFAULT_PREFS: UserPreferences = {
9
+ theme: 'system',
10
+ language: 'en',
11
+ dashboardView: 'grid',
12
+ notifications: { marketing: false, security: true },
13
+ privacy: { stealthMode: false, publicSearch: true }
14
+ };
15
+
16
+ export async function getPreferences() {
17
+ const session = await getServerSession(authOptions);
18
+ if (!session?.user?.email) return DEFAULT_PREFS;
19
+
20
+ const client = await clientPromise;
21
+ const db = client.db();
22
+ const user = await db.collection("users").findOne({ email: session.user.email });
23
+
24
+ return (user?.preferences as UserPreferences) || DEFAULT_PREFS;
25
+ }
26
+
27
+ export async function updatePreferences(data: UserPreferences) {
28
+ const session = await getServerSession(authOptions);
29
+ if (!session?.user?.email) throw new Error("Unauthorized");
30
+
31
+ const client = await clientPromise;
32
+ const db = client.db();
33
+
34
+ await db.collection("users").updateOne(
35
+ { email: session.user.email },
36
+ { $set: { preferences: data } }
37
+ );
38
+
39
+ return { success: true };
40
+ }