@underpostnet/underpost 2.97.1 → 2.97.5

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 (59) hide show
  1. package/README.md +2 -2
  2. package/cli.md +3 -1
  3. package/conf.js +2 -0
  4. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  5. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  6. package/package.json +1 -1
  7. package/src/api/core/core.service.js +0 -5
  8. package/src/api/default/default.service.js +7 -5
  9. package/src/api/document/document.model.js +1 -1
  10. package/src/api/document/document.router.js +5 -0
  11. package/src/api/document/document.service.js +105 -47
  12. package/src/api/file/file.model.js +112 -4
  13. package/src/api/file/file.ref.json +42 -0
  14. package/src/api/file/file.service.js +380 -32
  15. package/src/api/user/user.model.js +38 -1
  16. package/src/api/user/user.router.js +96 -63
  17. package/src/api/user/user.service.js +81 -48
  18. package/src/cli/db.js +424 -166
  19. package/src/cli/index.js +8 -0
  20. package/src/cli/repository.js +1 -1
  21. package/src/cli/run.js +1 -0
  22. package/src/cli/ssh.js +10 -10
  23. package/src/client/components/core/Account.js +327 -36
  24. package/src/client/components/core/AgGrid.js +3 -0
  25. package/src/client/components/core/Auth.js +9 -3
  26. package/src/client/components/core/Chat.js +2 -2
  27. package/src/client/components/core/Content.js +159 -78
  28. package/src/client/components/core/CssCore.js +16 -12
  29. package/src/client/components/core/FileExplorer.js +115 -8
  30. package/src/client/components/core/Input.js +204 -11
  31. package/src/client/components/core/LogIn.js +42 -20
  32. package/src/client/components/core/Modal.js +138 -24
  33. package/src/client/components/core/Panel.js +69 -31
  34. package/src/client/components/core/PanelForm.js +262 -77
  35. package/src/client/components/core/PublicProfile.js +888 -0
  36. package/src/client/components/core/Router.js +117 -15
  37. package/src/client/components/core/SearchBox.js +329 -13
  38. package/src/client/components/core/SignUp.js +26 -7
  39. package/src/client/components/core/SocketIo.js +6 -3
  40. package/src/client/components/core/Translate.js +98 -0
  41. package/src/client/components/core/Validator.js +15 -0
  42. package/src/client/components/core/windowGetDimensions.js +6 -6
  43. package/src/client/components/default/MenuDefault.js +59 -12
  44. package/src/client/components/default/RoutesDefault.js +1 -0
  45. package/src/client/services/core/core.service.js +163 -1
  46. package/src/client/services/default/default.management.js +451 -64
  47. package/src/client/services/default/default.service.js +13 -6
  48. package/src/client/services/file/file.service.js +43 -16
  49. package/src/client/services/user/user.service.js +13 -9
  50. package/src/db/DataBaseProvider.js +1 -1
  51. package/src/db/mongo/MongooseDB.js +1 -1
  52. package/src/index.js +1 -1
  53. package/src/mailer/MailerProvider.js +4 -4
  54. package/src/runtime/express/Express.js +2 -1
  55. package/src/runtime/lampp/Lampp.js +2 -2
  56. package/src/server/auth.js +3 -6
  57. package/src/server/data-query.js +449 -0
  58. package/src/server/object-layer.js +0 -3
  59. package/src/ws/IoInterface.js +2 -2
@@ -0,0 +1,888 @@
1
+ import { renderWave } from './Css.js';
2
+ import { Translate } from './Translate.js';
3
+ import { s, htmls } from './VanillaJs.js';
4
+ import { UserService } from '../../services/user/user.service.js';
5
+ import { ThemeEvents, darkTheme, subThemeManager, lightenHex, darkenHex } from './Css.js';
6
+ import { Modal } from './Modal.js';
7
+ import { getId } from './CommonJs.js';
8
+ import { setPath, getProxyPath, getQueryParams, extractUsernameFromPath, RouterEvents } from './Router.js';
9
+
10
+ const PublicProfile = {
11
+ Data: {},
12
+ currentUsername: null, // Track the currently displayed username for back/forward navigation
13
+
14
+ Update: async function (options = { idModal: '', user: {} }) {
15
+ const { idModal, user } = options;
16
+ const username = user.username || 'Unknown User';
17
+
18
+ // Check if modal exists and is registered in Modal.Data
19
+ if (!Modal.Data[idModal]) {
20
+ return await this.Render(options);
21
+ }
22
+
23
+ try {
24
+ // Track the username we're updating to
25
+ this.currentUsername = username;
26
+
27
+ // Ensure modal is in correct state
28
+ this._ensureModalState(idModal);
29
+
30
+ // Show loading state using Modal.writeHTML
31
+ const loadingHtml = this._getLoadingHtml(username);
32
+ Modal.writeHTML({ idModal, html: loadingHtml });
33
+
34
+ // Clean up existing profile data to avoid conflicts
35
+ this._cleanupProfileData({ idModal });
36
+
37
+ // Re-render the profile content with new user
38
+ const newContent = await this.Render({ ...options, disableUpdate: true });
39
+
40
+ // Update modal content using Modal.writeHTML with smooth transition
41
+ Modal.writeHTML({ idModal, html: newContent });
42
+ Modal.zIndexSync({ idModal });
43
+
44
+ if (Modal.mobileModal()) setTimeout(() => s(`.btn-close-modal-menu`).click());
45
+
46
+ this._addTransitionEffect(idModal);
47
+ return newContent;
48
+ } catch (error) {
49
+ console.error('Error updating profile:', error);
50
+
51
+ // Show error state using Modal.writeHTML
52
+ const errorHtml = this._getErrorHtml(username, error.message);
53
+ Modal.writeHTML({ idModal, html: errorHtml });
54
+
55
+ throw error;
56
+ }
57
+ },
58
+
59
+ _getLoadingHtml: function (username) {
60
+ return html`
61
+ <div class="profile-loading-container">
62
+ <div class="profile-loading-spinner"></div>
63
+ <p class="profile-loading-text">Loading profile for @${username}...</p>
64
+ <style>
65
+ .profile-loading-container {
66
+ display: flex;
67
+ justify-content: center;
68
+ align-items: center;
69
+ height: 400px;
70
+ flex-direction: column;
71
+ gap: 20px;
72
+ }
73
+ .profile-loading-spinner {
74
+ width: 40px;
75
+ height: 40px;
76
+ border: 4px solid #f3f3f3;
77
+ border-top: 4px solid #3498db;
78
+ border-radius: 50%;
79
+ animation: profile-spin 1s linear infinite;
80
+ }
81
+ .profile-loading-text {
82
+ margin: 0;
83
+ color: #666;
84
+ font-size: 14px;
85
+ }
86
+ @keyframes profile-spin {
87
+ 0% {
88
+ transform: rotate(0deg);
89
+ }
90
+ 100% {
91
+ transform: rotate(360deg);
92
+ }
93
+ }
94
+ </style>
95
+ </div>
96
+ `;
97
+ },
98
+
99
+ _addTransitionEffect: function (idModal) {
100
+ // Add smooth transition effect to modal content
101
+ const modalContent = s(`.html-${idModal}`);
102
+ if (modalContent) {
103
+ modalContent.style.transition = 'opacity 0.3s ease-in-out';
104
+ modalContent.style.opacity = '0';
105
+
106
+ // Fade in after a brief delay
107
+ setTimeout(() => {
108
+ modalContent.style.opacity = '1';
109
+ }, 50);
110
+ }
111
+ },
112
+
113
+ _getErrorHtml: function (username, errorMessage) {
114
+ return html`
115
+ <div class="profile-error-container">
116
+ <div class="profile-error-icon">
117
+ <i class="fas fa-exclamation-triangle"></i>
118
+ </div>
119
+ <h3 class="profile-error-title">Failed to load profile</h3>
120
+ <p class="profile-error-message">Could not load profile for @${username}</p>
121
+ <p class="profile-error-details">${errorMessage}</p>
122
+ <button class="profile-error-retry btn-retry-profile" onclick="location.reload()">
123
+ <i class="fas fa-redo"></i> Retry
124
+ </button>
125
+ <style>
126
+ .profile-error-container {
127
+ display: flex;
128
+ justify-content: center;
129
+ align-items: center;
130
+ height: 400px;
131
+ flex-direction: column;
132
+ gap: 15px;
133
+ text-align: center;
134
+ padding: 20px;
135
+ }
136
+ .profile-error-icon {
137
+ font-size: 48px;
138
+ color: #e74c3c;
139
+ }
140
+ .profile-error-title {
141
+ margin: 0;
142
+ color: #333;
143
+ font-size: 18px;
144
+ }
145
+ .profile-error-message {
146
+ margin: 0;
147
+ color: #666;
148
+ font-size: 14px;
149
+ }
150
+ .profile-error-details {
151
+ margin: 0;
152
+ color: #999;
153
+ font-size: 12px;
154
+ font-style: italic;
155
+ }
156
+ .profile-error-retry {
157
+ padding: 8px 16px;
158
+ background: #3498db;
159
+ color: white;
160
+ border: none;
161
+ border-radius: 4px;
162
+ cursor: pointer;
163
+ font-size: 14px;
164
+ transition: background-color 0.3s;
165
+ }
166
+ .profile-error-retry:hover {
167
+ background: #2980b9;
168
+ }
169
+ </style>
170
+ </div>
171
+ `;
172
+ },
173
+
174
+ _cleanupProfileData: function ({ idModal }) {
175
+ delete ThemeEvents[`error-state-${idModal}`];
176
+ delete ThemeEvents[`profile-${idModal}`];
177
+ delete this.Data[idModal];
178
+ },
179
+
180
+ _ensureModalState: function (idModal) {
181
+ // Ensure modal is in the correct state for content updates
182
+ if (Modal.Data[idModal]) {
183
+ // Reset any modal-specific states that might interfere
184
+ Modal.Data[idModal].updated = true;
185
+ Modal.Data[idModal].lastUpdated = Date.now();
186
+ }
187
+ },
188
+
189
+ Render: async function (
190
+ options = {
191
+ idModal: '',
192
+ user: {},
193
+ },
194
+ ) {
195
+ let {
196
+ user: { _id: userId, username },
197
+ } = options;
198
+ const idModal = options.idModal || getId();
199
+ const profileId = `public-profile-${userId}`;
200
+ const waveAnimationId = `${profileId}-wave`;
201
+ const profileImageClass = `${profileId}-image`;
202
+ const profileContainerId = `${profileId}-container`;
203
+ const cardId = `${profileId}-card`;
204
+
205
+ if (!options.disableUpdate) {
206
+ const queryParams = getQueryParams();
207
+ const usernameFromPath = extractUsernameFromPath();
208
+ const cid = usernameFromPath || queryParams.cid || username;
209
+ const existingModal = s(`.${idModal}`);
210
+ if (existingModal && Modal.Data[idModal]) {
211
+ await PublicProfile.Update({
212
+ idModal,
213
+ user: { username: cid },
214
+ });
215
+ return;
216
+ } else username = cid;
217
+ }
218
+
219
+ // Initialize data structure (Modal.Data pattern)
220
+ if (!PublicProfile.Data[profileId]) {
221
+ PublicProfile.Data[profileId] = {
222
+ userData: null,
223
+ colors: {},
224
+ updated: true,
225
+ lastUpdated: Date.now(),
226
+ };
227
+ } // Setup observer callback using Modal.Data pattern
228
+
229
+ // Get primary theme color for secondary colors
230
+ const getPrimaryThemeColor = () => {
231
+ const primaryColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor;
232
+ return primaryColor || (darkTheme ? '#3498db' : '#3498db');
233
+ };
234
+
235
+ // Theme-aware color palette with subtheme secondary colors
236
+ const getThemeColors = () => {
237
+ const isDark = darkTheme;
238
+ const primaryColor = getPrimaryThemeColor();
239
+ const primaryColorLight = lightenHex(primaryColor, 0.8);
240
+ const primaryColorDark = darkenHex(primaryColor, 0.75);
241
+
242
+ return {
243
+ // Background colors
244
+ bgPrimary: isDark ? 'rgba(25, 25, 30, 0.95)' : 'rgba(255, 255, 255, 0.95)',
245
+ bgSecondary: isDark ? 'rgba(35, 35, 40, 0.95)' : 'rgba(248, 249, 250, 0.95)',
246
+ bgGradient: isDark
247
+ ? `linear-gradient(135deg, ${darkenHex(primaryColor, 0.9)}, ${darkenHex(primaryColor, 0.95)})`
248
+ : `linear-gradient(135deg, ${lightenHex(primaryColor, 0.92)}, ${lightenHex(primaryColor, 0.88)})`,
249
+
250
+ // Text colors
251
+ textPrimary: isDark ? '#e8e8e8' : '#1a1a1a',
252
+ textSecondary: isDark ? '#b0b0b0' : '#555555',
253
+ textTertiary: isDark ? '#808080' : '#999999',
254
+
255
+ // Border and divider
256
+ border: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.08)',
257
+ divider: isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)',
258
+
259
+ // Badge with subtheme
260
+ badgeBg: isDark ? `${darkenHex(primaryColor, 0.85)}33` : `${lightenHex(primaryColor, 0.92)}66`,
261
+ badgeBorder: isDark ? `${primaryColorLight}4d` : `${primaryColor}26`,
262
+ badgeText: isDark ? primaryColorLight : primaryColor,
263
+
264
+ // Button with subtheme gradient
265
+ buttonGradient: isDark
266
+ ? `linear-gradient(135deg, ${primaryColorLight}, ${primaryColor})`
267
+ : `linear-gradient(135deg, ${primaryColor}, ${primaryColorDark})`,
268
+ buttonShadow: isDark ? `${primaryColor}40` : `${primaryColor}4d`,
269
+ buttonShadowHover: isDark ? `${primaryColor}66` : `${primaryColor}80`,
270
+
271
+ // Error
272
+ error: isDark ? '#ff6b6b' : '#e74c3c',
273
+
274
+ // Shadows
275
+ shadowSmall: isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.08)',
276
+ shadowMedium: isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(0, 0, 0, 0.12)',
277
+ shadowLarge: isDark ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.15)',
278
+
279
+ // Primary colors
280
+ primaryColor,
281
+ primaryColorLight,
282
+ primaryColorDark,
283
+ };
284
+ };
285
+
286
+ // Update error state theme colors dynamically
287
+ const updateErrorStateTheme = () => {
288
+ const errorStateEl = s(`.${profileContainerId}-error-state`);
289
+ if (!errorStateEl) return;
290
+
291
+ const colors = getThemeColors();
292
+ const primaryColor = getPrimaryThemeColor();
293
+
294
+ errorStateEl.style.background = `linear-gradient(135deg, ${colors.bgPrimary} 0%, ${colors.bgSecondary} 100%)`;
295
+
296
+ // Update icon container
297
+ const iconContainer = errorStateEl.querySelector('div[style*="border-radius: 50%"]');
298
+ if (iconContainer) {
299
+ iconContainer.style.background = `${colors.error}15`;
300
+ iconContainer.style.borderColor = `${colors.error}30`;
301
+ }
302
+
303
+ // Update icon
304
+ const icon = errorStateEl.querySelector('i');
305
+ if (icon) {
306
+ icon.style.color = colors.error;
307
+ }
308
+
309
+ // Update heading
310
+ const heading = errorStateEl.querySelector('h2');
311
+ if (heading) {
312
+ heading.style.color = colors.textPrimary;
313
+ }
314
+
315
+ // Update description
316
+ const description = errorStateEl.querySelector('p');
317
+ if (description) {
318
+ description.style.color = colors.textSecondary;
319
+ }
320
+
321
+ // Update buttons
322
+ const buttons = errorStateEl.querySelectorAll('a');
323
+ buttons.forEach((btn, index) => {
324
+ if (index === 0) {
325
+ // Home button
326
+ btn.style.background = colors.buttonGradient;
327
+ btn.style.boxShadow = `0 4px 12px ${colors.buttonShadow}`;
328
+ } else {
329
+ // Go Back button
330
+ btn.style.color = colors.primaryColor;
331
+ btn.style.borderColor = `${colors.primaryColor}40`;
332
+ }
333
+ });
334
+ };
335
+
336
+ // Register theme change handler
337
+ ThemeEvents[`error-state-${idModal}`] = updateErrorStateTheme;
338
+ setTimeout(updateErrorStateTheme);
339
+
340
+ const renderErrorState = (message, icon = 'fa-exclamation-circle', description = '') => {
341
+ const colors = getThemeColors();
342
+ return html`
343
+ <div
344
+ class="${profileContainerId}-error-state"
345
+ style="
346
+ display: flex;
347
+ flex-direction: column;
348
+ align-items: center;
349
+ justify-content: center;
350
+ padding: 60px 40px;
351
+ background: linear-gradient(135deg, ${colors.bgPrimary} 0%, ${colors.bgSecondary} 100%);
352
+ border-radius: 20px;
353
+ text-align: center;
354
+ "
355
+ >
356
+ <!-- Icon Container -->
357
+ <div
358
+ style="
359
+ width: 120px;
360
+ height: 120px;
361
+ border-radius: 50%;
362
+ background: ${colors.error}15;
363
+ border: 2px solid ${colors.error}30;
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: center;
367
+ margin-bottom: 32px;
368
+ "
369
+ >
370
+ <i
371
+ class="fa-solid ${icon}"
372
+ style="
373
+ font-size: 56px;
374
+ color: ${colors.error};
375
+ opacity: 0.9;
376
+ "
377
+ ></i>
378
+ </div>
379
+
380
+ <!-- Main Message -->
381
+ <h2
382
+ style="
383
+ margin: 0 0 16px 0;
384
+ font-size: 24px;
385
+ font-weight: 600;
386
+ color: ${colors.textPrimary};
387
+ letter-spacing: -0.5px;
388
+ "
389
+ >
390
+ ${message}
391
+ </h2>
392
+
393
+ <!-- Description/Documentation -->
394
+ ${description
395
+ ? html`<p
396
+ style="
397
+ margin: 0 0 32px 0;
398
+ font-size: 16px;
399
+ color: ${colors.textSecondary};
400
+ line-height: 1.6;
401
+ max-width: 500px;
402
+ "
403
+ >
404
+ ${description}
405
+ </p>`
406
+ : ''}
407
+
408
+ <!-- Helpful Actions -->
409
+ <div
410
+ style="
411
+ display: flex;
412
+ gap: 12px;
413
+ flex-wrap: wrap;
414
+ justify-content: center;
415
+ margin-top: 24px;
416
+ "
417
+ >
418
+ <a
419
+ href="/"
420
+ style="
421
+ padding: 12px 28px;
422
+ background: ${colors.buttonGradient};
423
+ color: white;
424
+ text-decoration: none;
425
+ border-radius: 8px;
426
+ font-size: 14px;
427
+ font-weight: 500;
428
+ transition: all 0.3s ease;
429
+ box-shadow: 0 4px 12px ${colors.buttonShadow};
430
+ cursor: pointer;
431
+ display: inline-flex;
432
+ align-items: center;
433
+ gap: 8px;
434
+ "
435
+ onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px ${colors.buttonShadowHover}';"
436
+ onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px ${colors.buttonShadow}';"
437
+ >
438
+ <i class="fa-solid fa-home"></i>
439
+ ${Translate.Render('go-home')}
440
+ </a>
441
+ <a
442
+ href="javascript:history.back()"
443
+ style="
444
+ padding: 12px 28px;
445
+ background: transparent;
446
+ color: ${colors.primaryColor};
447
+ text-decoration: none;
448
+ border: 2px solid ${colors.primaryColor}40;
449
+ border-radius: 8px;
450
+ font-size: 14px;
451
+ font-weight: 500;
452
+ transition: all 0.3s ease;
453
+ cursor: pointer;
454
+ display: inline-flex;
455
+ align-items: center;
456
+ gap: 8px;
457
+ "
458
+ onmouseover="this.style.background='${colors.primaryColor}08'; this.style.borderColor='${colors.primaryColor}60';"
459
+ onmouseout="this.style.background='transparent'; this.style.borderColor='${colors.primaryColor}40';"
460
+ >
461
+ <i class="fa-solid fa-arrow-left"></i>
462
+ ${Translate.Render('go-back')}
463
+ </a>
464
+ </div>
465
+ </div>
466
+ `;
467
+ };
468
+
469
+ if (!userId && !username) {
470
+ return renderErrorState(
471
+ Translate.Render('user-not-found'),
472
+ 'fa-user-slash',
473
+ "The user you're looking for could not be found. Please check the username and try again.",
474
+ );
475
+ }
476
+
477
+ // Fetch public user data
478
+ let userData = null;
479
+ try {
480
+ const result = await UserService.get({ id: `u/${username}` });
481
+ setTimeout(() => {
482
+ Modal.Data[idModal].onObserverListener['profile-card-observer'] = () => {
483
+ const modalHeight = s(`.${idModal}`).offsetHeight;
484
+
485
+ if (s(`.${profileContainerId}`)) s(`.${profileContainerId}`).style.height = `${modalHeight - 110}px`;
486
+ if (s(`.${profileContainerId}-error-state`))
487
+ s(`.${profileContainerId}-error-state`).style.height = `${modalHeight - 160}px`;
488
+ };
489
+ Modal.Data[idModal].onObserverListener['profile-card-observer']();
490
+ });
491
+ if (result.status === 'success') {
492
+ userData = result.data;
493
+ PublicProfile.Data[profileId].userData = userData;
494
+
495
+ // Track the currently displayed username for back/forward navigation
496
+ PublicProfile.currentUsername = userData.username || username;
497
+
498
+ // Update browser history to show clean URL after successful data fetch
499
+ if (userData.username) {
500
+ const cleanPath = `${getProxyPath()}u/${username}`;
501
+ setPath(cleanPath, { replace: true });
502
+ }
503
+ } else {
504
+ if (result.message && result.message.toLowerCase().match('private'))
505
+ return renderErrorState(
506
+ Translate.Render('profile-is-private'),
507
+ 'fa-lock',
508
+ 'This user has chosen to keep their profile private. Respect their privacy and check back later if they change their settings.',
509
+ );
510
+ else
511
+ return renderErrorState(
512
+ Translate.Render('user-not-found'),
513
+ 'fa-user-slash',
514
+ 'This user profile does not exist or has been removed. Please verify the username.',
515
+ );
516
+ }
517
+ } catch (error) {
518
+ console.error('Error fetching public profile:', error);
519
+ return renderErrorState(
520
+ Translate.Render('error-loading-profile'),
521
+ 'fa-circle-exclamation',
522
+ 'We encountered an issue loading this profile. Please try again in a moment or contact support if the problem persists.',
523
+ );
524
+ }
525
+
526
+ // If user doesn't have public profile enabled
527
+ if (!userData.publicProfile) {
528
+ return renderErrorState(
529
+ Translate.Render('profile-is-private'),
530
+ 'fa-lock',
531
+ 'This user has chosen to keep their profile private. Respect their privacy and check back later if they change their settings.',
532
+ );
533
+ }
534
+
535
+ // Function to update profile card styles based on theme changes
536
+ const updateProfileCardTheme = () => {
537
+ const container = s(`.${profileContainerId}`);
538
+ if (!container) return;
539
+
540
+ const colors = getThemeColors();
541
+ PublicProfile.Data[profileId].colors = colors;
542
+
543
+ // Update container background
544
+ container.style.background = colors.bgGradient;
545
+
546
+ // Update card
547
+ const card = s(`.${cardId}`);
548
+ if (card) {
549
+ card.style.backgroundColor = colors.bgPrimary;
550
+ card.style.borderColor = colors.border;
551
+ card.style.boxShadow = `0 12px 48px ${colors.shadowMedium}`;
552
+ }
553
+
554
+ // Update text colors
555
+ const username_el = s(`.${profileId}-username`);
556
+ if (username_el) username_el.style.color = colors.textPrimary;
557
+
558
+ const description_els = document.querySelectorAll(`.${profileId}-description`);
559
+ description_els.forEach((el) => {
560
+ el.style.color = colors.textSecondary;
561
+ });
562
+
563
+ const badge_el = s(`.${profileId}-badge`);
564
+ if (badge_el) {
565
+ badge_el.style.backgroundColor = colors.badgeBg;
566
+ badge_el.style.borderColor = colors.badgeBorder;
567
+ const badgeText = badge_el.querySelector('span');
568
+ if (badgeText) badgeText.style.color = colors.badgeText;
569
+ }
570
+
571
+ const divider = s(`.${profileId}-divider`);
572
+ if (divider) {
573
+ divider.style.background = `linear-gradient(90deg, transparent, ${colors.divider}, transparent)`;
574
+ }
575
+
576
+ const button = s(`.${profileId}-button`);
577
+ if (button) {
578
+ button.style.background = colors.buttonGradient;
579
+ button.style.boxShadow = `0 6px 16px ${colors.buttonShadow}`;
580
+ }
581
+
582
+ const imageContainer = s(`.${profileId}-image-container`);
583
+ if (imageContainer) {
584
+ imageContainer.style.borderColor = colors.bgPrimary;
585
+ imageContainer.style.backgroundColor = colors.bgSecondary;
586
+ }
587
+ };
588
+
589
+ // Register theme change listener
590
+ ThemeEvents[`profile-${idModal}`] = updateProfileCardTheme;
591
+
592
+ // Schedule rendering of profile image after DOM is ready
593
+ setTimeout(async () => {
594
+ if (userData.profileImageId) {
595
+ try {
596
+ const imageSrc = `${getProxyPath()}api/file/blob/${userData.profileImageId}`;
597
+ const imgElement = s(`.${profileImageClass}`);
598
+ if (imgElement) {
599
+ imgElement.src = imageSrc;
600
+ imgElement.style.opacity = '1';
601
+ const loadingEl = s(`.${profileImageClass}-loading`);
602
+ if (loadingEl) {
603
+ loadingEl.style.display = 'none';
604
+ }
605
+ }
606
+ } catch (error) {
607
+ console.warn('Could not load profile image:', error);
608
+ }
609
+ }
610
+ });
611
+
612
+ const colors = getThemeColors();
613
+ PublicProfile.Data[profileId].colors = colors;
614
+
615
+ return html`
616
+ <!-- Hero Section with Wave Background -->
617
+ <div
618
+ class="${profileContainerId}"
619
+ style="
620
+ position: relative;
621
+ background: ${colors.bgGradient};
622
+ overflow: visible;
623
+ display: flex;
624
+ align-items: flex-start;
625
+ justify-content: center;
626
+ padding-top: 40px;
627
+ padding-bottom: 20px;
628
+ padding-left: 20px;
629
+ padding-right: 20px;
630
+ transition: background 0.3s ease-in-out;
631
+ "
632
+ >
633
+ <!-- Wave Animation Background - Fixed Height -->
634
+ <div
635
+ class="${waveAnimationId}"
636
+ style="
637
+ position: absolute;
638
+ top: 0;
639
+ left: 0;
640
+ width: 100%;
641
+ height: 100%;
642
+ opacity: 0.5;
643
+ overflow: hidden;
644
+ "
645
+ >
646
+ ${renderWave({ id: waveAnimationId })}
647
+ </div>
648
+
649
+ <!-- Profile Card Container - Half Overlapping -->
650
+ <div
651
+ class="${cardId}"
652
+ style="
653
+ position: relative;
654
+ top: 60px;
655
+ background: ${colors.bgPrimary};
656
+ backdrop-filter: blur(10px);
657
+ border-radius: 20px;
658
+ padding: 48px 32px 40px;
659
+ max-width: 520px;
660
+ width: 100%;
661
+ box-shadow: 0 12px 48px ${colors.shadowMedium};
662
+ border: 1px solid ${colors.border};
663
+ transition: all 0.3s ease-in-out;
664
+ margin-top: -40px;
665
+ "
666
+ >
667
+ <!-- Profile Image Container -->
668
+ <div
669
+ style="
670
+ display: flex;
671
+ justify-content: center;
672
+ margin-bottom: 36px;
673
+ "
674
+ >
675
+ <div
676
+ class="${profileId}-image-container"
677
+ style="
678
+ position: relative;
679
+ width: 160px;
680
+ height: 160px;
681
+ flex-shrink: 0;
682
+ border-radius: 50%;
683
+ border: 6px solid ${colors.bgPrimary};
684
+ background-color: ${colors.bgSecondary};
685
+ transition: all 0.3s ease-in-out;
686
+ overflow: hidden;
687
+ box-shadow: 0 12px 40px ${colors.shadowLarge};
688
+ "
689
+ >
690
+ <img
691
+ class="${profileImageClass}"
692
+ style="
693
+ width: 100%;
694
+ height: 100%;
695
+ border-radius: 50%;
696
+ opacity: 0;
697
+ transition: opacity 0.4s ease-in-out;
698
+ object-fit: cover;
699
+ "
700
+ alt="${userData.username}"
701
+ />
702
+ <div
703
+ class="${profileImageClass}-loading"
704
+ style="
705
+ position: absolute;
706
+ top: 0;
707
+ left: 0;
708
+ width: 100%;
709
+ height: 100%;
710
+ border-radius: 50%;
711
+ display: flex;
712
+ align-items: center;
713
+ justify-content: center;
714
+ color: ${colors.textTertiary};
715
+ font-size: 48px;
716
+ transition: display 0.3s ease-in-out;
717
+ background: linear-gradient(135deg, ${colors.bgSecondary}, ${colors.bgPrimary});
718
+ "
719
+ >
720
+ <i class="fa-solid fa-user" style="opacity: 0.4;"></i>
721
+ </div>
722
+ </div>
723
+ </div>
724
+
725
+ <!-- Content Section -->
726
+ <div style="text-align: center;">
727
+ <!-- Username -->
728
+ <h1
729
+ class="${profileId}-username"
730
+ style="
731
+ margin: 0 0 12px 0;
732
+ font-size: 28px;
733
+ font-weight: 700;
734
+ color: ${colors.textPrimary};
735
+ letter-spacing: -0.5px;
736
+ transition: color 0.3s ease-in-out;
737
+ "
738
+ >
739
+ ${userData.username}
740
+ </h1>
741
+
742
+ <!-- Brief Description / Bio -->
743
+ <div style="margin: 16px 0 28px 0;">
744
+ ${userData.briefDescription
745
+ ? html`<p
746
+ class="${profileId}-description"
747
+ style="
748
+ margin: 0;
749
+ font-size: 14px;
750
+ color: ${colors.textSecondary};
751
+ line-height: 1.6;
752
+ font-weight: 500;
753
+ transition: color 0.3s ease-in-out;
754
+ "
755
+ >
756
+ ${userData.briefDescription}
757
+ </p>`
758
+ : html`<p
759
+ class="${profileId}-description"
760
+ style="
761
+ margin: 0;
762
+ font-size: 14px;
763
+ color: ${colors.textTertiary};
764
+ font-style: italic;
765
+ transition: color 0.3s ease-in-out;
766
+ "
767
+ >
768
+ ${Translate.Render('no-description')}
769
+ </p>`}
770
+ </div>
771
+
772
+ <!-- Member Since Badge -->
773
+ <div
774
+ class="${profileId}-badge"
775
+ style="
776
+ display: inline-block;
777
+ background: ${colors.badgeBg};
778
+ border: 1px solid ${colors.badgeBorder};
779
+ border-radius: 24px;
780
+ padding: 8px 16px;
781
+ margin-bottom: 28px;
782
+ transition: all 0.3s ease-in-out;
783
+ "
784
+ >
785
+ <span
786
+ style="
787
+ font-size: 12px;
788
+ color: ${colors.badgeText};
789
+ font-weight: 600;
790
+ letter-spacing: 0.5px;
791
+ transition: color 0.3s ease-in-out;
792
+ "
793
+ >
794
+ <i class="fa-solid fa-calendar" style="margin-right: 6px;"></i>
795
+ ${Translate.Render('member-since')}: ${new Date(userData.createdAt).toLocaleDateString()}
796
+ </span>
797
+ </div>
798
+
799
+ <!-- Divider -->
800
+ <div
801
+ class="${profileId}-divider"
802
+ style="
803
+ margin: 24px 0;
804
+ height: 1px;
805
+ background: linear-gradient(
806
+ 90deg,
807
+ transparent,
808
+ ${colors.divider},
809
+ transparent
810
+ );
811
+ transition: background 0.3s ease-in-out;
812
+ "
813
+ ></div>
814
+
815
+ <!-- Action Links -->
816
+ <div style="display: flex; justify-content: center; gap: 12px; flex-wrap: wrap;">
817
+ <a
818
+ class="${profileId}-button"
819
+ href="javascript:void(0)"
820
+ title="${Translate.Render('view-profile')}"
821
+ style="
822
+ display: inline-flex;
823
+ align-items: center;
824
+ justify-content: center;
825
+ width: 50px;
826
+ height: 50px;
827
+ border-radius: 50%;
828
+ background: ${colors.buttonGradient};
829
+ color: white;
830
+ text-decoration: none;
831
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
832
+ cursor: pointer;
833
+ box-shadow: 0 6px 16px ${colors.buttonShadow};
834
+ font-size: 18px;
835
+ border: none;
836
+ "
837
+ onmouseover="
838
+ this.style.transform = 'translateY(-4px) scale(1.1)';
839
+ this.style.boxShadow = '0 10px 24px ${colors.buttonShadowHover}';
840
+ "
841
+ onmouseout="
842
+ this.style.transform = 'translateY(0) scale(1)';
843
+ this.style.boxShadow = '0 6px 16px ${colors.buttonShadow}';
844
+ "
845
+ >
846
+ <i class="fa-solid fa-user"></i>
847
+ </a>
848
+ </div>
849
+ </div>
850
+ </div>
851
+ </div>
852
+ `;
853
+ },
854
+
855
+ Router: async function (options = { idModal: '' }) {
856
+ const idModal = options.idModal || 'modal-public-profile';
857
+ // Register RouterEvents listener for back/forward navigation between profiles
858
+ // This ensures the profile updates when the user navigates through browser history
859
+ // Note: route id is 'u', modal id is 'modal-public-profile', button class is 'main-btn-public-profile'
860
+ RouterEvents[`${idModal}-navigation`] = async ({ route }) => {
861
+ if (route === 'u') {
862
+ const usernameFromPath = extractUsernameFromPath();
863
+ const queryParams = getQueryParams();
864
+ const cid = usernameFromPath || queryParams.cid;
865
+
866
+ if (!cid) return;
867
+
868
+ // Check if modal exists (could be behind another view modal like settings)
869
+ if (s(`.${idModal}`) && Modal.Data[idModal]) {
870
+ // Modal exists - bring to front and update if username changed
871
+ const currentUsername = PublicProfile.currentUsername;
872
+ if (currentUsername !== cid)
873
+ await PublicProfile.Update({
874
+ idModal,
875
+ user: { username: cid },
876
+ });
877
+ } else {
878
+ // Modal doesn't exist - open it
879
+ if (s('.main-btn-public-profile')) {
880
+ s('.main-btn-public-profile').click();
881
+ }
882
+ }
883
+ }
884
+ };
885
+ },
886
+ };
887
+
888
+ export { PublicProfile };