create-pxlr 1.0.0

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 (153) hide show
  1. package/README.md +160 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +264 -0
  4. package/package.json +51 -0
  5. package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
  6. package/templates/blog/frontend/app/blog/page.tsx +102 -0
  7. package/templates/blog/frontend/app/components/footer.tsx +21 -0
  8. package/templates/blog/frontend/app/components/header.tsx +45 -0
  9. package/templates/blog/frontend/app/globals.css +30 -0
  10. package/templates/blog/frontend/app/layout.tsx +38 -0
  11. package/templates/blog/frontend/app/lib/cms.ts +71 -0
  12. package/templates/blog/frontend/app/page.tsx +155 -0
  13. package/templates/blog/frontend/next.config.ts +16 -0
  14. package/templates/blog/frontend/package.json +24 -0
  15. package/templates/blog/frontend/postcss.config.mjs +7 -0
  16. package/templates/blog/frontend/tsconfig.json +23 -0
  17. package/templates/blog/pxlr-cms/README.md +188 -0
  18. package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
  19. package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
  20. package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
  21. package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
  22. package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
  23. package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  24. package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
  25. package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
  26. package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  27. package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  28. package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  29. package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  30. package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  31. package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  32. package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  33. package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  34. package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  35. package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  36. package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  37. package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  38. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  39. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  40. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  41. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  42. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  43. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  44. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  45. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  46. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  47. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  48. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  49. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  50. package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  51. package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  52. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  53. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  54. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  55. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  56. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  57. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  58. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  59. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  60. package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  61. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  62. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  63. package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  64. package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  65. package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  66. package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
  67. package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
  68. package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
  69. package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
  70. package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
  71. package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
  72. package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
  73. package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  74. package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
  75. package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
  76. package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  77. package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  78. package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  79. package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  80. package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  81. package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  82. package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
  83. package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
  84. package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  85. package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
  86. package/templates/clean/pxlr-cms/README.md +188 -0
  87. package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
  88. package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
  89. package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
  90. package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
  91. package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
  92. package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  93. package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
  94. package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
  95. package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  96. package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  97. package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  98. package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  99. package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  100. package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  101. package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  102. package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  103. package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  104. package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  105. package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  106. package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  107. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  108. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  109. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  110. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  111. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  112. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  113. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  114. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  115. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  116. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  117. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  118. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  119. package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  120. package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  121. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  122. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  123. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  124. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  125. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  126. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  127. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  128. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  129. package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  130. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  131. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  132. package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  133. package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  134. package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  135. package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
  136. package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
  137. package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
  138. package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
  139. package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
  140. package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
  141. package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
  142. package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  143. package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
  144. package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  145. package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  146. package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  147. package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  148. package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  149. package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  150. package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
  151. package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
  152. package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  153. package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
@@ -0,0 +1,184 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useQuery } from '@tanstack/react-query';
6
+ import { useAuthStore } from '@/lib/store/auth';
7
+ import { api } from '@/lib/api';
8
+ import { Sidebar } from '@/components/layout/sidebar';
9
+ import { Header } from '@/components/layout/header';
10
+ import { FileText, Images, Database, Users, Loader2 } from 'lucide-react';
11
+
12
+ interface StatsCard {
13
+ title: string;
14
+ value: string | number;
15
+ icon: React.ReactNode;
16
+ description: string;
17
+ }
18
+
19
+ function DashboardContent() {
20
+ const { data: schemas } = useQuery({
21
+ queryKey: ['schemas'],
22
+ queryFn: () => api.get('/schemas'),
23
+ });
24
+
25
+ const { data: documents } = useQuery({
26
+ queryKey: ['documents'],
27
+ queryFn: () => api.get('/content?limit=1'),
28
+ });
29
+
30
+ const { data: media } = useQuery({
31
+ queryKey: ['media'],
32
+ queryFn: () => api.get('/media?limit=1'),
33
+ });
34
+
35
+ const stats: StatsCard[] = [
36
+ {
37
+ title: 'Content Types',
38
+ value: schemas?.schemas?.length || 0,
39
+ icon: <Database className="h-5 w-5" />,
40
+ description: 'Defined schemas',
41
+ },
42
+ {
43
+ title: 'Documents',
44
+ value: documents?.pagination?.total || 0,
45
+ icon: <FileText className="h-5 w-5" />,
46
+ description: 'Total content items',
47
+ },
48
+ {
49
+ title: 'Media Files',
50
+ value: media?.pagination?.total || 0,
51
+ icon: <Images className="h-5 w-5" />,
52
+ description: 'Uploaded files',
53
+ },
54
+ {
55
+ title: 'System Status',
56
+ value: 'Online',
57
+ icon: <Users className="h-5 w-5" />,
58
+ description: 'All services running',
59
+ },
60
+ ];
61
+
62
+ return (
63
+ <div className="space-y-6">
64
+ <div>
65
+ <h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
66
+ <p className="text-muted-foreground">
67
+ Welcome to PXLR CMS. Manage your content from here.
68
+ </p>
69
+ </div>
70
+
71
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
72
+ {stats.map((stat) => (
73
+ <div
74
+ key={stat.title}
75
+ className="rounded-lg border bg-card p-6 shadow-sm"
76
+ >
77
+ <div className="flex items-center justify-between">
78
+ <span className="text-sm font-medium text-muted-foreground">
79
+ {stat.title}
80
+ </span>
81
+ <span className="text-muted-foreground">{stat.icon}</span>
82
+ </div>
83
+ <div className="mt-2">
84
+ <span className="text-3xl font-bold">{stat.value}</span>
85
+ </div>
86
+ <p className="mt-1 text-sm text-muted-foreground">
87
+ {stat.description}
88
+ </p>
89
+ </div>
90
+ ))}
91
+ </div>
92
+
93
+ <div className="grid gap-6 md:grid-cols-2">
94
+ <div className="rounded-lg border bg-card p-6 shadow-sm">
95
+ <h2 className="text-lg font-semibold">Quick Start</h2>
96
+ <ul className="mt-4 space-y-3 text-sm">
97
+ <li className="flex items-center gap-2">
98
+ <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
99
+ 1
100
+ </span>
101
+ <span>Create a content schema in the Schemas section</span>
102
+ </li>
103
+ <li className="flex items-center gap-2">
104
+ <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
105
+ 2
106
+ </span>
107
+ <span>Add content documents based on your schemas</span>
108
+ </li>
109
+ <li className="flex items-center gap-2">
110
+ <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
111
+ 3
112
+ </span>
113
+ <span>Upload media files to the Media Library</span>
114
+ </li>
115
+ <li className="flex items-center gap-2">
116
+ <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
117
+ 4
118
+ </span>
119
+ <span>Access your content via the REST API</span>
120
+ </li>
121
+ </ul>
122
+ </div>
123
+
124
+ <div className="rounded-lg border bg-card p-6 shadow-sm">
125
+ <h2 className="text-lg font-semibold">API Access</h2>
126
+ <div className="mt-4 space-y-3">
127
+ <div>
128
+ <p className="text-sm text-muted-foreground">API Endpoint</p>
129
+ <code className="mt-1 block rounded bg-muted px-2 py-1 text-sm">
130
+ http://localhost:4000
131
+ </code>
132
+ </div>
133
+ <div>
134
+ <p className="text-sm text-muted-foreground">Documentation</p>
135
+ <a
136
+ href="http://localhost:4000/docs"
137
+ target="_blank"
138
+ rel="noopener noreferrer"
139
+ className="mt-1 block text-sm text-primary hover:underline"
140
+ >
141
+ View Swagger API Docs
142
+ </a>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ );
149
+ }
150
+
151
+ export default function HomePage() {
152
+ const router = useRouter();
153
+ const { isAuthenticated, isLoading } = useAuthStore();
154
+
155
+ useEffect(() => {
156
+ if (!isLoading && !isAuthenticated) {
157
+ router.replace('/login');
158
+ }
159
+ }, [isAuthenticated, isLoading, router]);
160
+
161
+ if (isLoading) {
162
+ return (
163
+ <div className="flex items-center justify-center min-h-screen">
164
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
165
+ </div>
166
+ );
167
+ }
168
+
169
+ if (!isAuthenticated) {
170
+ return null;
171
+ }
172
+
173
+ return (
174
+ <div className="flex h-screen overflow-hidden">
175
+ <Sidebar />
176
+ <div className="flex flex-1 flex-col overflow-hidden">
177
+ <Header />
178
+ <main className="flex-1 overflow-auto bg-muted/30 p-6">
179
+ <DashboardContent />
180
+ </main>
181
+ </div>
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+
3
+ import { AuthLayout } from '@/components/layout/auth-layout';
4
+
5
+ export default function ProfileLayout({ children }: { children: React.ReactNode }) {
6
+ return <AuthLayout>{children}</AuthLayout>;
7
+ }
@@ -0,0 +1,206 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useMutation } from '@tanstack/react-query';
5
+ import { useAuthStore } from '@/lib/store/auth';
6
+ import { api } from '@/lib/api';
7
+ import { Button } from '@/components/ui/button';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Label } from '@/components/ui/label';
10
+ import { toast } from '@/components/ui/use-toast';
11
+ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
12
+ import { Loader2, Save, User, Mail, Shield, Calendar } from 'lucide-react';
13
+
14
+ export default function ProfilePage() {
15
+ const { user, setUser } = useAuthStore();
16
+ const [name, setName] = useState(user?.name || '');
17
+ const [currentPassword, setCurrentPassword] = useState('');
18
+ const [newPassword, setNewPassword] = useState('');
19
+ const [confirmPassword, setConfirmPassword] = useState('');
20
+
21
+ const updateProfileMutation = useMutation({
22
+ mutationFn: (data: { name: string }) => api.put('/auth/profile', data),
23
+ onSuccess: (data) => {
24
+ if (data.user) {
25
+ setUser(data.user);
26
+ }
27
+ toast({ title: 'Profile updated successfully' });
28
+ },
29
+ onError: (error: any) => {
30
+ toast({ title: 'Error', description: error.message, variant: 'destructive' });
31
+ },
32
+ });
33
+
34
+ const changePasswordMutation = useMutation({
35
+ mutationFn: (data: { currentPassword: string; newPassword: string }) =>
36
+ api.put('/auth/password', data),
37
+ onSuccess: () => {
38
+ toast({ title: 'Password changed successfully' });
39
+ setCurrentPassword('');
40
+ setNewPassword('');
41
+ setConfirmPassword('');
42
+ },
43
+ onError: (error: any) => {
44
+ toast({ title: 'Error', description: error.message, variant: 'destructive' });
45
+ },
46
+ });
47
+
48
+ const handleUpdateProfile = () => {
49
+ if (!name.trim()) {
50
+ toast({ title: 'Name is required', variant: 'destructive' });
51
+ return;
52
+ }
53
+ updateProfileMutation.mutate({ name });
54
+ };
55
+
56
+ const handleChangePassword = () => {
57
+ if (!currentPassword || !newPassword) {
58
+ toast({ title: 'Please fill in all password fields', variant: 'destructive' });
59
+ return;
60
+ }
61
+ if (newPassword !== confirmPassword) {
62
+ toast({ title: 'New passwords do not match', variant: 'destructive' });
63
+ return;
64
+ }
65
+ if (newPassword.length < 6) {
66
+ toast({ title: 'Password must be at least 6 characters', variant: 'destructive' });
67
+ return;
68
+ }
69
+ changePasswordMutation.mutate({ currentPassword, newPassword });
70
+ };
71
+
72
+ const initials = user?.name
73
+ ? user.name
74
+ .split(' ')
75
+ .map((n) => n[0])
76
+ .join('')
77
+ .toUpperCase()
78
+ : 'U';
79
+
80
+ return (
81
+ <div className="space-y-6 max-w-2xl">
82
+ <div>
83
+ <h1 className="text-2xl font-bold tracking-tight">Profile</h1>
84
+ <p className="text-muted-foreground">
85
+ Manage your account settings
86
+ </p>
87
+ </div>
88
+
89
+ {/* User Info Card */}
90
+ <div className="rounded-lg border bg-card p-6">
91
+ <div className="flex items-center gap-4 mb-6">
92
+ <Avatar className="h-20 w-20">
93
+ <AvatarFallback className="text-2xl">{initials}</AvatarFallback>
94
+ </Avatar>
95
+ <div>
96
+ <h2 className="text-xl font-semibold">{user?.name}</h2>
97
+ <p className="text-muted-foreground">{user?.email}</p>
98
+ <span className="inline-flex items-center gap-1 mt-1 text-xs bg-primary/10 text-primary px-2 py-0.5 rounded">
99
+ <Shield className="h-3 w-3" />
100
+ {user?.role || 'admin'}
101
+ </span>
102
+ </div>
103
+ </div>
104
+
105
+ <div className="grid gap-4 text-sm">
106
+ <div className="flex items-center gap-2 text-muted-foreground">
107
+ <Mail className="h-4 w-4" />
108
+ <span>{user?.email}</span>
109
+ </div>
110
+ <div className="flex items-center gap-2 text-muted-foreground">
111
+ <Calendar className="h-4 w-4" />
112
+ <span>Member since {new Date().toLocaleDateString()}</span>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ {/* Update Profile */}
118
+ <div className="rounded-lg border bg-card p-6">
119
+ <h2 className="text-lg font-semibold mb-4">Update Profile</h2>
120
+ <div className="space-y-4">
121
+ <div className="space-y-2">
122
+ <Label htmlFor="name">Display Name</Label>
123
+ <Input
124
+ id="name"
125
+ value={name}
126
+ onChange={(e) => setName(e.target.value)}
127
+ placeholder="Your name"
128
+ />
129
+ </div>
130
+ <div className="space-y-2">
131
+ <Label htmlFor="email">Email</Label>
132
+ <Input
133
+ id="email"
134
+ value={user?.email || ''}
135
+ disabled
136
+ className="bg-muted"
137
+ />
138
+ <p className="text-xs text-muted-foreground">
139
+ Email cannot be changed
140
+ </p>
141
+ </div>
142
+ <Button
143
+ onClick={handleUpdateProfile}
144
+ disabled={updateProfileMutation.isPending}
145
+ >
146
+ {updateProfileMutation.isPending ? (
147
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
148
+ ) : (
149
+ <Save className="mr-2 h-4 w-4" />
150
+ )}
151
+ Save Changes
152
+ </Button>
153
+ </div>
154
+ </div>
155
+
156
+ {/* Change Password */}
157
+ <div className="rounded-lg border bg-card p-6">
158
+ <h2 className="text-lg font-semibold mb-4">Change Password</h2>
159
+ <div className="space-y-4">
160
+ <div className="space-y-2">
161
+ <Label htmlFor="currentPassword">Current Password</Label>
162
+ <Input
163
+ id="currentPassword"
164
+ type="password"
165
+ value={currentPassword}
166
+ onChange={(e) => setCurrentPassword(e.target.value)}
167
+ placeholder="Enter current password"
168
+ />
169
+ </div>
170
+ <div className="space-y-2">
171
+ <Label htmlFor="newPassword">New Password</Label>
172
+ <Input
173
+ id="newPassword"
174
+ type="password"
175
+ value={newPassword}
176
+ onChange={(e) => setNewPassword(e.target.value)}
177
+ placeholder="Enter new password"
178
+ />
179
+ </div>
180
+ <div className="space-y-2">
181
+ <Label htmlFor="confirmPassword">Confirm New Password</Label>
182
+ <Input
183
+ id="confirmPassword"
184
+ type="password"
185
+ value={confirmPassword}
186
+ onChange={(e) => setConfirmPassword(e.target.value)}
187
+ placeholder="Confirm new password"
188
+ />
189
+ </div>
190
+ <Button
191
+ onClick={handleChangePassword}
192
+ disabled={changePasswordMutation.isPending}
193
+ variant="outline"
194
+ >
195
+ {changePasswordMutation.isPending ? (
196
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
197
+ ) : (
198
+ <Shield className="mr-2 h-4 w-4" />
199
+ )}
200
+ Change Password
201
+ </Button>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ );
206
+ }