create-elit 3.2.6 → 3.2.8

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.
@@ -0,0 +1,342 @@
1
+ import { div, h2, h3, p, button, span, input, label } from 'elit/el';
2
+ import { createState, reactive, reactiveAs } from 'elit/state';
3
+ import type { Router } from 'elit';
4
+
5
+ export function ProfilePage(router: Router) {
6
+ const isEditing = createState(false);
7
+ const name = createState('');
8
+ const email = createState('');
9
+ const bio = createState('');
10
+ const location = createState('');
11
+ const website = createState('');
12
+ const isLoading = createState(false);
13
+ const isLoaded = createState(false);
14
+ const error = createState('');
15
+
16
+ const stats = {
17
+ projects: 0,
18
+ followers: 0,
19
+ following: 0,
20
+ stars: 0
21
+ };
22
+
23
+ // Load profile data
24
+ const loadProfile = async () => {
25
+ const token = localStorage.getItem('token');
26
+ if (!token) {
27
+ router.push('/login');
28
+ return;
29
+ }
30
+
31
+ try {
32
+ const response = await fetch('/api/profile', {
33
+ method: 'GET',
34
+ headers: {
35
+ 'Authorization': `Bearer ${token}`
36
+ }
37
+ });
38
+
39
+ if (!response.ok) {
40
+ if (response.status === 401) {
41
+ localStorage.removeItem('token');
42
+ localStorage.removeItem('user');
43
+ router.push('/login');
44
+ return;
45
+ }
46
+ throw new Error('Failed to load profile');
47
+ }
48
+
49
+ const data = await response.json();
50
+ const user = data.user;
51
+
52
+ name.value = user.name;
53
+ email.value = user.email;
54
+ bio.value = user.bio;
55
+ location.value = user.location;
56
+ website.value = user.website;
57
+ stats.projects = user.stats.projects;
58
+ stats.followers = user.stats.followers;
59
+ stats.following = user.stats.following;
60
+ stats.stars = user.stats.stars;
61
+
62
+ isLoaded.value = true;
63
+ } catch (err) {
64
+ error.value = 'Failed to load profile';
65
+ isLoaded.value = true;
66
+ }
67
+ };
68
+
69
+ // Load profile on mount
70
+ loadProfile();
71
+
72
+ const handleSave = async () => {
73
+ isLoading.value = true;
74
+ error.value = '';
75
+
76
+ const token = localStorage.getItem('token');
77
+ if (!token) {
78
+ error.value = 'Not authenticated';
79
+ isLoading.value = false;
80
+ return;
81
+ }
82
+
83
+ try {
84
+ const response = await fetch('/api/profile', {
85
+ method: 'PUT',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ 'Authorization': `Bearer ${token}`
89
+ },
90
+ body: JSON.stringify({
91
+ name: name.value,
92
+ bio: bio.value,
93
+ location: location.value,
94
+ website: website.value
95
+ })
96
+ });
97
+
98
+ if (!response.ok) {
99
+ throw new Error('Failed to update profile');
100
+ }
101
+
102
+ const data = await response.json();
103
+ const updatedUser = data.user;
104
+
105
+ // Update local storage
106
+ localStorage.setItem('user', JSON.stringify(updatedUser));
107
+
108
+ isLoading.value = false;
109
+ isEditing.value = false;
110
+ } catch (err) {
111
+ error.value = 'Failed to save profile';
112
+ isLoading.value = false;
113
+ }
114
+ };
115
+
116
+ const handleCancel = () => {
117
+ // Reset to original values from localStorage
118
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
119
+ if (user.name) {
120
+ name.value = user.name;
121
+ }
122
+ if (user.email) {
123
+ email.value = user.email;
124
+ }
125
+ if (user.bio) {
126
+ bio.value = user.bio;
127
+ }
128
+ if (user.location) {
129
+ location.value = user.location;
130
+ }
131
+ if (user.website) {
132
+ website.value = user.website;
133
+ }
134
+ isEditing.value = false;
135
+ };
136
+
137
+ const handleLogout = () => {
138
+ localStorage.removeItem('token');
139
+ localStorage.removeItem('user');
140
+ // Dispatch custom event to notify Header
141
+ window.dispatchEvent(new Event('elit:storage'));
142
+ router.push('/');
143
+ };
144
+
145
+ return div({ className: 'profile-page' },
146
+ // Error display
147
+ reactive(error, (err) => err ? div({ className: 'auth-error' }, err) : null),
148
+
149
+ // Profile Header
150
+ div({ className: 'profile-header-section' },
151
+ div({ className: 'profile-cover' }),
152
+ div({ className: 'profile-avatar-section' },
153
+ div({ className: 'profile-avatar' },
154
+ reactive(name, (nm) => nm ? nm.charAt(0).toUpperCase() : '?')
155
+ ),
156
+ button({ className: 'avatar-edit-button' }, '📷')
157
+ ),
158
+ ),
159
+
160
+ // Profile Content
161
+ reactive(isLoaded, (loaded) => {
162
+ if (!loaded) {
163
+ return div({ className: 'profile-content' }, p('Loading...'));
164
+ }
165
+
166
+ return div({ className: 'profile-content' },
167
+ // Left column
168
+ div({ className: 'profile-sidebar' },
169
+ div({ className: 'profile-card' },
170
+ reactive(isEditing, (editing) => {
171
+ if (editing) {
172
+ return div({ className: 'edit-form' },
173
+ div({ className: 'form-group' },
174
+ label({ className: 'form-label' }, 'Display Name'),
175
+ input({
176
+ type: 'text',
177
+ className: 'form-input',
178
+ value: name.value,
179
+ oninput: (e: Event) => { name.value = (e.target as HTMLInputElement).value; }
180
+ })
181
+ ),
182
+ div({ className: 'form-group' },
183
+ label({ className: 'form-label' }, 'Email'),
184
+ input({
185
+ type: 'email',
186
+ className: 'form-input',
187
+ value: email.value,
188
+ disabled: true
189
+ })
190
+ )
191
+ );
192
+ }
193
+ return div({ className: 'profile-info-display' },
194
+ reactive(name, (nm) => h3({ className: 'profile-name' }, nm)),
195
+ reactive(email, (em) => p({ className: 'profile-email' }, em))
196
+ );
197
+ }),
198
+
199
+ div({ className: 'profile-meta' },
200
+ div({ className: 'meta-item' },
201
+ span({ className: 'meta-icon' }, '📍'),
202
+ reactive(isEditing, (editing) => {
203
+ if (editing) {
204
+ return input({
205
+ type: 'text',
206
+ className: 'form-input form-input-sm',
207
+ value: location.value,
208
+ oninput: (e: Event) => { location.value = (e.target as HTMLInputElement).value; }
209
+ });
210
+ }
211
+ return span({ className: 'meta-text' }, location.value || 'No location');
212
+ })
213
+ ),
214
+ div({ className: 'meta-item' },
215
+ span({ className: 'meta-icon' }, '🔗'),
216
+ reactive(isEditing, (editing) => {
217
+ if (editing) {
218
+ return input({
219
+ type: 'url',
220
+ className: 'form-input form-input-sm',
221
+ value: website.value,
222
+ oninput: (e: Event) => { website.value = (e.target as HTMLInputElement).value; }
223
+ });
224
+ }
225
+ return span({ className: 'meta-text' }, website.value || 'No website')
226
+ })
227
+ )
228
+ ),
229
+
230
+ // Use reactiveAs with a stable div wrapper to handle structure changes
231
+ reactiveAs('div', isEditing, (editing) => {
232
+ if (editing) {
233
+ return div({ className: 'edit-actions' },
234
+ button({
235
+ className: 'btn btn-secondary',
236
+ onclick: () => {
237
+ console.log('Cancel button clicked');
238
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
239
+ console.log('User from localStorage:', user);
240
+ name.value = user.name || '';
241
+ email.value = user.email || '';
242
+ bio.value = user.bio || '';
243
+ location.value = user.location || '';
244
+ website.value = user.website || '';
245
+ console.log('States reset, setting isEditing to false');
246
+ isEditing.value = false;
247
+ console.log('isEditing.value:', isEditing.value);
248
+ },
249
+ disabled: isLoading.value
250
+ }, 'Cancel'),
251
+ button({
252
+ className: 'btn btn-primary',
253
+ onclick: handleSave,
254
+ disabled: isLoading.value
255
+ }, isLoading.value ? 'Saving...' : 'Save Changes')
256
+ );
257
+ }
258
+ return button({
259
+ className: 'btn btn-primary btn-block',
260
+ onclick: () => isEditing.value = true
261
+ }, 'Edit Profile');
262
+ }, { className: 'profile-action-wrapper' })
263
+ )
264
+ ),
265
+
266
+ // Right column
267
+ div({ className: 'profile-main' },
268
+ // Stats Card
269
+ div({ className: 'stats-grid' },
270
+ div({ className: 'stat-card' },
271
+ span({ className: 'stat-icon' }, '📁'),
272
+ div({ className: 'stat-info' },
273
+ span({ className: 'stat-value' }, stats.projects.toString()),
274
+ span({ className: 'stat-label' }, 'Projects')
275
+ )
276
+ ),
277
+ div({ className: 'stat-card' },
278
+ span({ className: 'stat-icon' }, '⭐'),
279
+ div({ className: 'stat-info' },
280
+ span({ className: 'stat-value' }, stats.stars.toString()),
281
+ span({ className: 'stat-label' }, 'Stars')
282
+ )
283
+ ),
284
+ div({ className: 'stat-card' },
285
+ span({ className: 'stat-icon' }, '👥'),
286
+ div({ className: 'stat-info' },
287
+ span({ className: 'stat-value' }, stats.followers.toString()),
288
+ span({ className: 'stat-label' }, 'Followers')
289
+ )
290
+ ),
291
+ div({ className: 'stat-card' },
292
+ span({ className: 'stat-icon' }, '👣'),
293
+ div({ className: 'stat-info' },
294
+ span({ className: 'stat-value' }, stats.following.toString()),
295
+ span({ className: 'stat-label' }, 'Following')
296
+ )
297
+ )
298
+ ),
299
+
300
+ // About Card
301
+ div({ className: 'profile-card' },
302
+ h3({ className: 'card-title' }, 'About'),
303
+ reactive(isEditing, (editing) => {
304
+ if (editing) {
305
+ return div({ className: 'form-group' },
306
+ label({ className: 'form-label' }, 'Bio'),
307
+ input({
308
+ type: 'text',
309
+ className: 'form-input',
310
+ value: bio.value,
311
+ oninput: (e: Event) => { bio.value = (e.target as HTMLInputElement).value; }
312
+ })
313
+ );
314
+ }
315
+ return reactive(bio, (b) => p({ className: 'profile-bio' }, b || 'No bio yet'));
316
+ })
317
+ ),
318
+
319
+ // Activity Card
320
+ div({ className: 'profile-card' },
321
+ h3({ className: 'card-title' }, 'Recent Activity'),
322
+ div({ className: 'activity-list' },
323
+ div({ className: 'activity-item' },
324
+ span({ className: 'activity-icon' }, '🚀'),
325
+ div({ className: 'activity-content' },
326
+ p({ className: 'activity-title' }, 'Account created'),
327
+ p({ className: 'activity-time' }, 'Just now')
328
+ )
329
+ )
330
+ )
331
+ ),
332
+
333
+ // Actions
334
+ div({ className: 'profile-actions' },
335
+ button({ className: 'btn btn-outline btn-block' }, 'View Public Profile'),
336
+ button({ className: 'btn btn-secondary btn-block', onclick: handleLogout }, 'Logout')
337
+ )
338
+ )
339
+ );
340
+ })
341
+ );
342
+ }
@@ -0,0 +1,230 @@
1
+ import { div, h1, h2, p, input, label, button, form, span } from 'elit/el';
2
+ import { createState, reactive } from 'elit/state';
3
+ import type { Router } from 'elit';
4
+
5
+ export function RegisterPage(router: Router) {
6
+ // Check if already logged in
7
+ const token = localStorage.getItem('token');
8
+ if (token) {
9
+ router.push('/profile');
10
+ }
11
+
12
+ const name = createState('');
13
+ const email = createState('');
14
+ const password = createState('');
15
+ const confirmPassword = createState('');
16
+ const error = createState('');
17
+ const isLoading = createState(false);
18
+
19
+ const handleRegister = async (e: Event) => {
20
+ e.preventDefault();
21
+
22
+ if (!name.value || !email.value || !password.value || !confirmPassword.value) {
23
+ error.value = 'Please fill in all fields';
24
+ return;
25
+ }
26
+
27
+ if (!email.value.includes('@')) {
28
+ error.value = 'Please enter a valid email';
29
+ return;
30
+ }
31
+
32
+ if (password.value.length < 6) {
33
+ error.value = 'Password must be at least 6 characters';
34
+ return;
35
+ }
36
+
37
+ if (password.value !== confirmPassword.value) {
38
+ error.value = 'Passwords do not match';
39
+ return;
40
+ }
41
+
42
+ isLoading.value = true;
43
+ error.value = '';
44
+
45
+ try {
46
+ const response = await fetch('/api/auth/register', {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/json'
50
+ },
51
+ body: JSON.stringify({
52
+ name: name.value,
53
+ email: email.value,
54
+ password: password.value
55
+ })
56
+ });
57
+
58
+ const data = await response.json();
59
+
60
+ if (!response.ok) {
61
+ error.value = data.error || 'Registration failed';
62
+ isLoading.value = false;
63
+ return;
64
+ }
65
+
66
+ // Store token and user data
67
+ localStorage.setItem('token', data.token);
68
+ localStorage.setItem('user', JSON.stringify(data.user));
69
+
70
+ // Dispatch custom event to notify other components (like Header)
71
+ window.dispatchEvent(new Event('elit:storage'));
72
+
73
+ isLoading.value = false;
74
+ router.push('/profile');
75
+ } catch (err) {
76
+ isLoading.value = false;
77
+ error.value = 'Network error. Please try again.';
78
+ }
79
+ };
80
+
81
+ return div({ className: 'auth-page' },
82
+ div({ className: 'auth-container' },
83
+ // Left side - branding
84
+ div({ className: 'auth-branding' },
85
+ div({ className: 'branding-content' },
86
+ h1({ className: 'branding-title' }, 'Join Us Today'),
87
+ p({ className: 'branding-description' },
88
+ 'Create your account and start building amazing applications in minutes.'
89
+ ),
90
+ div({ className: 'branding-features' },
91
+ div({ className: 'branding-feature' },
92
+ span({ className: 'feature-icon' }, '✓'),
93
+ span({ className: 'feature-text' }, 'Free forever for personal projects')
94
+ ),
95
+ div({ className: 'branding-feature' },
96
+ span({ className: 'feature-icon' }, '✓'),
97
+ span({ className: 'feature-text' }, 'No credit card required')
98
+ ),
99
+ div({ className: 'branding-feature' },
100
+ span({ className: 'feature-icon' }, '✓'),
101
+ span({ className: 'feature-text' }, 'Instant setup & deployment')
102
+ )
103
+ )
104
+ )
105
+ ),
106
+
107
+ // Right side - form
108
+ div({ className: 'auth-form-wrapper' },
109
+ div({ className: 'auth-form-card' },
110
+ div({ className: 'auth-header' },
111
+ h2({ className: 'auth-title' }, 'Create Account'),
112
+ p({ className: 'auth-subtitle' }, 'Sign up to get started with your free account')
113
+ ),
114
+
115
+ reactive(error, (err) => err ? div({ className: 'auth-error' }, err) : null),
116
+
117
+ form({ onsubmit: handleRegister },
118
+ div({ className: 'form-group' },
119
+ label({ htmlFor: 'name', className: 'form-label' }, 'Full Name'),
120
+ div({ className: 'input-wrapper' },
121
+ span({ className: 'input-icon' }, '👤'),
122
+ input({
123
+ type: 'text',
124
+ id: 'name',
125
+ className: 'form-input',
126
+ placeholder: 'John Doe',
127
+ value: name.value,
128
+ oninput: (e: Event) => {
129
+ name.value = (e.target as HTMLInputElement).value;
130
+ error.value = '';
131
+ }
132
+ })
133
+ )
134
+ ),
135
+
136
+ div({ className: 'form-group' },
137
+ label({ htmlFor: 'email', className: 'form-label' }, 'Email Address'),
138
+ div({ className: 'input-wrapper' },
139
+ span({ className: 'input-icon' }, '📧'),
140
+ input({
141
+ type: 'email',
142
+ id: 'email',
143
+ className: 'form-input',
144
+ placeholder: 'your@email.com',
145
+ value: email.value,
146
+ oninput: (e: Event) => {
147
+ email.value = (e.target as HTMLInputElement).value;
148
+ error.value = '';
149
+ }
150
+ })
151
+ )
152
+ ),
153
+
154
+ div({ className: 'form-group' },
155
+ label({ htmlFor: 'password', className: 'form-label' }, 'Password'),
156
+ div({ className: 'input-wrapper' },
157
+ span({ className: 'input-icon' }, '🔒'),
158
+ input({
159
+ type: 'password',
160
+ id: 'password',
161
+ className: 'form-input',
162
+ placeholder: '••••••••',
163
+ value: password.value,
164
+ oninput: (e: Event) => {
165
+ password.value = (e.target as HTMLInputElement).value;
166
+ error.value = '';
167
+ }
168
+ })
169
+ )
170
+ ),
171
+
172
+ div({ className: 'form-group' },
173
+ label({ htmlFor: 'confirmPassword', className: 'form-label' }, 'Confirm Password'),
174
+ div({ className: 'input-wrapper' },
175
+ span({ className: 'input-icon' }, '🔒'),
176
+ input({
177
+ type: 'password',
178
+ id: 'confirmPassword',
179
+ className: 'form-input',
180
+ placeholder: '••••••••',
181
+ value: confirmPassword.value,
182
+ oninput: (e: Event) => {
183
+ confirmPassword.value = (e.target as HTMLInputElement).value;
184
+ error.value = '';
185
+ }
186
+ })
187
+ )
188
+ ),
189
+
190
+ div({ className: 'form-options' },
191
+ label({ className: 'checkbox-label' },
192
+ input({ type: 'checkbox', className: 'checkbox' }),
193
+ span({ className: 'checkbox-text' }, 'I agree to the Terms of Service and Privacy Policy')
194
+ )
195
+ ),
196
+
197
+ button({
198
+ type: 'submit',
199
+ className: 'btn btn-primary btn-block btn-lg',
200
+ disabled: isLoading.value
201
+ }, isLoading.value ? 'Creating account...' : 'Create Account')
202
+ ),
203
+
204
+ div({ className: 'auth-divider' }, 'OR'),
205
+
206
+ div({ className: 'social-login' },
207
+ button({ className: 'social-button' },
208
+ span({ className: 'social-icon' }, 'G'),
209
+ 'Sign up with Google'
210
+ ),
211
+ button({ className: 'social-button' },
212
+ span({ className: 'social-icon' }, 'Gh'),
213
+ 'Sign up with GitHub'
214
+ )
215
+ ),
216
+
217
+ div({ className: 'auth-footer' },
218
+ p({ className: 'footer-text' },
219
+ 'Already have an account? ',
220
+ button({
221
+ className: 'link-button-inline',
222
+ onclick: () => router.push('/login')
223
+ }, 'Sign in')
224
+ )
225
+ )
226
+ )
227
+ )
228
+ )
229
+ );
230
+ }
@@ -0,0 +1,30 @@
1
+ import { createRouter, createRouterView, type RouteParams, type Router } from 'elit';
2
+ import { HomePage } from './pages/HomePage';
3
+ import { LoginPage } from './pages/LoginPage';
4
+ import { RegisterPage } from './pages/RegisterPage';
5
+ import { ProfilePage } from './pages/ProfilePage';
6
+ import { ForgotPasswordPage } from './pages/ForgotPasswordPage';
7
+ import { ChatPage } from './pages/ChatPage';
8
+ import { ChatListPage } from './pages/ChatListPage';
9
+ import { PrivateChatPage } from './pages/PrivateChatPage';
10
+
11
+ // Initialize router
12
+ export const router = createRouter({
13
+ mode: 'hash',
14
+ base: '/',
15
+ routes: []
16
+ });
17
+
18
+ // Define routes
19
+ const routes = [
20
+ { path: '/', component: () => HomePage(router) },
21
+ { path: '/login', component: () => LoginPage(router) },
22
+ { path: '/register', component: () => RegisterPage(router) },
23
+ { path: '/profile', component: () => ProfilePage(router) },
24
+ { path: '/forgot-password', component: () => ForgotPasswordPage(router) },
25
+ { path: '/chat', component: () => ChatPage(router) },
26
+ { path: '/chat/list', component: () => ChatListPage(router) },
27
+ { path: '/chat/dm/:userId', component: (params: RouteParams) => PrivateChatPage(router, params.userId as string) }
28
+ ];
29
+
30
+ export const RouterView = createRouterView(router, { mode: 'hash', routes });