@underpostnet/underpost 2.97.0 → 2.97.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.
@@ -2,8 +2,8 @@ import { getId, isValidDate, newInstance } from './CommonJs.js';
2
2
  import { LoadingAnimation } from '../core/LoadingAnimation.js';
3
3
  import { Validator } from '../core/Validator.js';
4
4
  import { Input } from '../core/Input.js';
5
- import { darkTheme, ThemeEvents } from './Css.js';
6
- import { append, copyData, getDataFromInputFile, htmls, s } from './VanillaJs.js';
5
+ import { darkTheme, ThemeEvents, subThemeManager, lightenHex, darkenHex } from './Css.js';
6
+ import { append, copyData, getDataFromInputFile, htmls, s, sa } from './VanillaJs.js';
7
7
  import { BtnIcon } from './BtnIcon.js';
8
8
  import { Translate } from './Translate.js';
9
9
  import { DropDown } from './DropDown.js';
@@ -16,6 +16,7 @@ import { Badge } from './Badge.js';
16
16
  import { Content } from './Content.js';
17
17
  import { DocumentService } from '../../services/document/document.service.js';
18
18
  import { NotificationManager } from './NotificationManager.js';
19
+ import { getApiBaseUrl } from '../../services/core/core.service.js';
19
20
 
20
21
  const logger = loggerFactory(import.meta);
21
22
 
@@ -35,6 +36,7 @@ const Panel = {
35
36
  share: {
36
37
  copyLink: false,
37
38
  },
39
+ showCreatorProfile: false,
38
40
  },
39
41
  ) {
40
42
  const idPanel = options?.idPanel ? options.idPanel : getId(this.Tokens, `${idPanel}-`);
@@ -206,8 +208,49 @@ const Panel = {
206
208
  e.preventDefault();
207
209
  // if (options.onClick) await options.onClick({ payload });
208
210
  };
211
+
212
+ // Add theme change handler for creator profile header
213
+ if (options.showCreatorProfile && obj.userInfo) {
214
+ const updateCreatorProfileTheme = () => {
215
+ const profileHeader = s(`.creator-profile-header-${id}`);
216
+ if (profileHeader) {
217
+ profileHeader.style.borderBottom = `1px solid ${darkTheme ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)'}`;
218
+ profileHeader.style.background = `${darkTheme ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)'}`;
219
+
220
+ // Update avatar border if it's an image
221
+ const avatarImg = profileHeader.querySelector('.creator-avatar');
222
+ if (avatarImg && avatarImg.tagName === 'IMG') {
223
+ avatarImg.style.border = `2px solid ${darkTheme ? 'rgba(102, 126, 234, 0.5)' : 'rgba(102, 126, 234, 0.3)'}`;
224
+ }
225
+
226
+ // Update username color
227
+ const username = profileHeader.querySelector('.creator-username');
228
+ if (username) {
229
+ username.style.color = `${darkTheme ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.85)'}`;
230
+ }
231
+
232
+ // Update "Creator" label color
233
+ const creatorLabel = username?.nextElementSibling;
234
+ if (creatorLabel) {
235
+ creatorLabel.style.color = `${darkTheme ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)'}`;
236
+ }
237
+ }
238
+ };
239
+
240
+ // Register theme change handler
241
+ const profileThemeHandlerId = `${id}-creator-profile-theme`;
242
+ ThemeEvents[profileThemeHandlerId] = updateCreatorProfileTheme;
243
+ }
209
244
  });
210
245
  if (s(`.${idPanel}-${id}`)) s(`.${idPanel}-${id}`).remove();
246
+
247
+ // Check if document is public (from obj.isPublic field)
248
+ const isPublic = obj.isPublic === true;
249
+ // Visibility icon: globe for public, padlock for private
250
+ const visibilityIcon = isPublic
251
+ ? '<i class="fas fa-globe" title="Public document"></i>'
252
+ : '<i class="fas fa-lock" title="Private document"></i>';
253
+
211
254
  return html` <div class="in box-shadow ${idPanel} ${idPanel}-${id}" style="position: relative;">
212
255
  <div class="fl ${idPanel}-tools session-fl-log-in ${obj.tools ? '' : 'hide'}">
213
256
  ${await BtnIcon.Render({
@@ -234,6 +277,51 @@ const Panel = {
234
277
  })}
235
278
  </div>
236
279
  <div class="in container-${idPanel}-${id}">
280
+ <div class="panel-visibility-icon">${visibilityIcon}</div>
281
+ ${options.showCreatorProfile && obj.userInfo
282
+ ? html`<div
283
+ class="creator-profile-header creator-profile-header-${id}"
284
+ style="padding: 10px 12px; margin-bottom: 10px; border-bottom: 1px solid ${darkTheme
285
+ ? 'rgba(255,255,255,0.1)'
286
+ : 'rgba(0,0,0,0.08)'}; display: flex; align-items: center; gap: 10px; background: ${darkTheme
287
+ ? 'rgba(255,255,255,0.02)'
288
+ : 'rgba(0,0,0,0.02)'}; border-radius: 4px 4px 0 0;"
289
+ >
290
+ ${obj.userInfo.profileImageId && obj.userInfo.profileImageId._id
291
+ ? html`<img
292
+ class="creator-avatar"
293
+ src="${getApiBaseUrl({ id: obj.userInfo.profileImageId._id, endpoint: 'file/blob' })}"
294
+ alt="${obj.userInfo.username || obj.userInfo.email}"
295
+ style="width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid ${darkTheme
296
+ ? 'rgba(102, 126, 234, 0.5)'
297
+ : 'rgba(102, 126, 234, 0.3)'}; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"
298
+ title="${obj.userInfo.email || obj.userInfo.username}"
299
+ />`
300
+ : html`<div
301
+ class="creator-avatar"
302
+ style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15);"
303
+ title="${obj.userInfo.email || obj.userInfo.username}"
304
+ >
305
+ ${(obj.userInfo.username || obj.userInfo.email || 'U').charAt(0).toUpperCase()}
306
+ </div>`}
307
+ <div style="display: flex; flex-direction: column; min-width: 0; flex: 1;">
308
+ <span
309
+ class="creator-username"
310
+ style="font-size: 14px; font-weight: 600; color: ${darkTheme
311
+ ? 'rgba(255,255,255,0.9)'
312
+ : 'rgba(0,0,0,0.85)'}; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
313
+ >
314
+ ${obj.userInfo.username || obj.userInfo.email || 'Unknown'}
315
+ </span>
316
+ <span
317
+ style="font-size: 11px; color: ${darkTheme
318
+ ? 'rgba(255,255,255,0.5)'
319
+ : 'rgba(0,0,0,0.45)'}; line-height: 1.3; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500;"
320
+ >Creator</span
321
+ >
322
+ </div>
323
+ </div>`
324
+ : ''}
237
325
  <div class="in ${idPanel}-head">
238
326
  <div class="in ${idPanel}-title">
239
327
  ${options.titleIcon}
@@ -263,18 +351,79 @@ const Panel = {
263
351
  }
264
352
 
265
353
  if (formData.find((f) => f.model === infoKey && f.panel && f.panel.type === 'tags')) {
266
- setTimeout(async () => {
354
+ // Function to render tags with current theme
355
+ const renderTags = async () => {
267
356
  let tagRender = html``;
268
357
  for (const tag of obj[infoKey]) {
358
+ // Use subThemeManager colors for consistent theming
359
+ const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor;
360
+ const hasThemeColor = themeColor && themeColor !== null;
361
+
362
+ let tagBg, tagColor;
363
+ if (darkTheme) {
364
+ tagBg = hasThemeColor ? darkenHex(themeColor, 0.6) : '#4a4a4a';
365
+ tagColor = hasThemeColor ? lightenHex(themeColor, 0.7) : '#ffffff';
366
+ } else {
367
+ tagBg = hasThemeColor ? lightenHex(themeColor, 0.7) : '#a2a2a2';
368
+ tagColor = hasThemeColor ? darkenHex(themeColor, 0.5) : '#ffffff';
369
+ }
370
+
269
371
  tagRender += await Badge.Render({
270
372
  text: tag,
271
- style: { color: 'white' },
272
- classList: 'inl',
273
- style: { margin: '3px', background: `#a2a2a2` },
373
+ style: { color: tagColor },
374
+ classList: 'inl panel-tag-clickable',
375
+ style: {
376
+ margin: '3px',
377
+ background: tagBg,
378
+ color: tagColor,
379
+ cursor: 'pointer',
380
+ transition: 'all 0.2s ease',
381
+ },
274
382
  });
275
383
  }
276
- if (s(`.tag-render-${id}`)) htmls(`.tag-render-${id}`, tagRender);
277
- });
384
+ if (s(`.tag-render-${id}`)) {
385
+ htmls(`.tag-render-${id}`, tagRender);
386
+
387
+ // Add click handlers to tags for search integration
388
+ setTimeout(() => {
389
+ const tagElements = sa(`.tag-render-${id} .panel-tag-clickable`);
390
+ tagElements.forEach((tagEl) => {
391
+ tagEl.onclick = (e) => {
392
+ e.stopPropagation();
393
+ const tagText = tagEl.textContent.trim();
394
+
395
+ // Open search bar if closed
396
+ if (
397
+ !s('.main-body-btn-ui-bar-custom-open').classList.contains('hide') ||
398
+ !s(`.main-body-btn-ui-open`).classList.contains('hide')
399
+ )
400
+ s('.main-body-btn-bar-custom').click();
401
+
402
+ // Find and populate search box if it exists
403
+ const searchBox = s('.top-bar-search-box');
404
+ if (searchBox) {
405
+ searchBox.value = tagText;
406
+ searchBox.focus();
407
+
408
+ // Trigger input event to start search
409
+ const inputEvent = new Event('input', { bubbles: true });
410
+ searchBox.dispatchEvent(inputEvent);
411
+
412
+ logger.info(`Tag clicked: ${tagText} - search triggered`);
413
+ }
414
+ };
415
+ });
416
+ }, 100);
417
+ }
418
+ };
419
+
420
+ // Initial render
421
+ setTimeout(renderTags);
422
+
423
+ // Add theme change handler for this tag set
424
+ const tagThemeHandlerId = `${id}-tags-${infoKey}-theme`;
425
+ ThemeEvents[tagThemeHandlerId] = renderTags;
426
+
278
427
  return html``;
279
428
  }
280
429
  {
@@ -662,6 +811,29 @@ const Panel = {
662
811
  ? getDarkStyles(idPanel, scrollClassContainer)
663
812
  : getLightStyles(idPanel, scrollClassContainer);
664
813
  }
814
+
815
+ // Update tag hover styles
816
+ const tagStyleElement = s(`.${idPanel}-tag-styles`);
817
+ if (tagStyleElement) {
818
+ const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor;
819
+ const hasThemeColor = themeColor && themeColor !== null;
820
+ let hoverBg;
821
+ if (darkTheme) {
822
+ hoverBg = hasThemeColor ? darkenHex(themeColor, 0.5) : '#5a5a5a';
823
+ } else {
824
+ hoverBg = hasThemeColor ? lightenHex(themeColor, 0.6) : '#8a8a8a';
825
+ }
826
+
827
+ tagStyleElement.textContent = css`
828
+ .panel-tag-clickable:hover {
829
+ background: ${hoverBg} !important;
830
+ transform: scale(1.05);
831
+ }
832
+ .panel-tag-clickable:active {
833
+ transform: scale(0.98);
834
+ }
835
+ `;
836
+ }
665
837
  };
666
838
 
667
839
  // Add theme change listener
@@ -680,15 +852,39 @@ const Panel = {
680
852
  width: 100%;
681
853
  }
682
854
  .${idPanel}-title {
683
- color: rgba(109, 104, 255, 1);
855
+ color: ${(() => {
856
+ const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor;
857
+ const hasThemeColor = themeColor && themeColor !== null;
858
+ if (hasThemeColor) {
859
+ return darkTheme ? lightenHex(themeColor, 0.3) : darkenHex(themeColor, 0.2);
860
+ } else {
861
+ return darkTheme ? '#8a85ff' : 'rgba(109, 104, 255, 1)';
862
+ }
863
+ })()};
684
864
  font-size: 24px;
685
865
  padding: 5px;
686
866
  }
687
867
  .a-title-${idPanel} {
688
- color: rgba(109, 104, 255, 1);
868
+ color: ${(() => {
869
+ const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor;
870
+ const hasThemeColor = themeColor && themeColor !== null;
871
+ if (hasThemeColor) {
872
+ return darkTheme ? lightenHex(themeColor, 0.3) : darkenHex(themeColor, 0.2);
873
+ } else {
874
+ return darkTheme ? '#8a85ff' : 'rgba(109, 104, 255, 1)';
875
+ }
876
+ })()};
689
877
  }
690
878
  .a-title-${idPanel}:hover {
691
- color: #e89f4c;
879
+ color: ${(() => {
880
+ const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor;
881
+ const hasThemeColor = themeColor && themeColor !== null;
882
+ if (hasThemeColor) {
883
+ return darkTheme ? lightenHex(themeColor, 0.5) : lightenHex(themeColor, 0.3);
884
+ } else {
885
+ return darkTheme ? '#ffb74d' : '#e89f4c';
886
+ }
887
+ })()};
692
888
  }
693
889
  .${idPanel}-row {
694
890
  padding: 5px;
@@ -705,6 +901,7 @@ const Panel = {
705
901
  margin-left: 10px;
706
902
  top: -7px;
707
903
  }
904
+
708
905
  .${idPanel}-row-key {
709
906
  }
710
907
  .${idPanel}-row-value {
@@ -765,6 +962,23 @@ const Panel = {
765
962
  }
766
963
  </style>
767
964
  <style class="${idPanel}-styles"></style>
965
+ <style class="${idPanel}-tag-styles">
966
+ .panel-tag-clickable:hover {
967
+ background: ${(() => {
968
+ const themeColor = darkTheme ? subThemeManager.darkColor : subThemeManager.lightColor;
969
+ const hasThemeColor = themeColor && themeColor !== null;
970
+ if (darkTheme) {
971
+ return hasThemeColor ? darkenHex(themeColor, 0.5) : '#5a5a5a';
972
+ } else {
973
+ return hasThemeColor ? lightenHex(themeColor, 0.6) : '#8a8a8a';
974
+ }
975
+ })()} !important;
976
+ transform: scale(1.05);
977
+ }
978
+ .panel-tag-clickable:active {
979
+ transform: scale(0.98);
980
+ }
981
+ </style>
768
982
  <div class="${idPanel}-container">
769
983
  <div class="in modal ${idPanel}-form-container ${options.formContainerClass ? options.formContainerClass : ''}">
770
984
  <div class="in ${idPanel}-form-header">
@@ -856,6 +1070,19 @@ function getBaseStyles(idPanel, scrollClassContainer) {
856
1070
  .${idPanel}-dropdown {
857
1071
  min-height: 100px;
858
1072
  }
1073
+ .panel-visibility-icon {
1074
+ position: absolute;
1075
+ top: 34px;
1076
+ left: 0px;
1077
+ font-size: 14px;
1078
+ opacity: 0.7;
1079
+ transition: opacity 0.2s ease;
1080
+ pointer-events: none;
1081
+ z-index: 10;
1082
+ }
1083
+ .${idPanel}:hover .panel-visibility-icon {
1084
+ opacity: 1;
1085
+ }
859
1086
  `;
860
1087
  }
861
1088
 
@@ -872,15 +1099,27 @@ function getLightStyles(idPanel, scrollClassContainer) {
872
1099
  background: #ffffff;
873
1100
  }
874
1101
  .${idPanel}-title {
875
- color: rgba(109, 104, 255, 1);
1102
+ color: ${(() => {
1103
+ const themeColor = subThemeManager.lightColor;
1104
+ const hasThemeColor = themeColor && themeColor !== null;
1105
+ return hasThemeColor ? darkenHex(themeColor, 0.2) : 'rgba(109, 104, 255, 1)';
1106
+ })()};
876
1107
  font-size: 24px;
877
1108
  padding: 5px;
878
1109
  }
879
1110
  .a-title-${idPanel} {
880
- color: rgba(109, 104, 255, 1);
1111
+ color: ${(() => {
1112
+ const themeColor = subThemeManager.lightColor;
1113
+ const hasThemeColor = themeColor && themeColor !== null;
1114
+ return hasThemeColor ? darkenHex(themeColor, 0.2) : 'rgba(109, 104, 255, 1)';
1115
+ })()};
881
1116
  }
882
1117
  .a-title-${idPanel}:hover {
883
- color: #e89f4c;
1118
+ color: ${(() => {
1119
+ const themeColor = subThemeManager.lightColor;
1120
+ const hasThemeColor = themeColor && themeColor !== null;
1121
+ return hasThemeColor ? lightenHex(themeColor, 0.3) : '#e89f4c';
1122
+ })()};
884
1123
  }
885
1124
  .${idPanel}-row-pin-value {
886
1125
  font-size: 20px;
@@ -894,6 +1133,10 @@ function getLightStyles(idPanel, scrollClassContainer) {
894
1133
  color: #000000 !important;
895
1134
  font-size: 17px !important;
896
1135
  }
1136
+ .panel-visibility-icon .fa-globe,
1137
+ .panel-visibility-icon .fa-lock {
1138
+ color: #666;
1139
+ }
897
1140
  `;
898
1141
  }
899
1142
 
@@ -910,15 +1153,27 @@ function getDarkStyles(idPanel, scrollClassContainer) {
910
1153
  background: #3a3a3a;
911
1154
  }
912
1155
  .${idPanel}-title {
913
- color: #8a85ff;
1156
+ color: ${(() => {
1157
+ const themeColor = subThemeManager.darkColor;
1158
+ const hasThemeColor = themeColor && themeColor !== null;
1159
+ return hasThemeColor ? lightenHex(themeColor, 0.3) : '#8a85ff';
1160
+ })()};
914
1161
  font-size: 24px;
915
1162
  padding: 5px;
916
1163
  }
917
1164
  .a-title-${idPanel} {
918
- color: #8a85ff;
1165
+ color: ${(() => {
1166
+ const themeColor = subThemeManager.darkColor;
1167
+ const hasThemeColor = themeColor && themeColor !== null;
1168
+ return hasThemeColor ? lightenHex(themeColor, 0.3) : '#8a85ff';
1169
+ })()};
919
1170
  }
920
1171
  .a-title-${idPanel}:hover {
921
- color: #ffb74d;
1172
+ color: ${(() => {
1173
+ const themeColor = subThemeManager.darkColor;
1174
+ const hasThemeColor = themeColor && themeColor !== null;
1175
+ return hasThemeColor ? lightenHex(themeColor, 0.5) : '#ffb74d';
1176
+ })()};
922
1177
  }
923
1178
  .${idPanel}-row-pin-value {
924
1179
  font-size: 20px;
@@ -932,6 +1187,10 @@ function getDarkStyles(idPanel, scrollClassContainer) {
932
1187
  color: #ffffff !important;
933
1188
  font-size: 17px !important;
934
1189
  }
1190
+ .panel-visibility-icon .fa-globe,
1191
+ .panel-visibility-icon .fa-lock {
1192
+ color: #999;
1193
+ }
935
1194
  `;
936
1195
  }
937
1196
 
@@ -6,7 +6,7 @@ import { NotificationManager } from './NotificationManager.js';
6
6
  import { DocumentService } from '../../services/document/document.service.js';
7
7
  import { FileService } from '../../services/file/file.service.js';
8
8
  import { getSrcFromFileData } from './Input.js';
9
- import { imageShimmer, renderCssAttr } from './Css.js';
9
+ import { imageShimmer, renderCssAttr, darkTheme, ThemeEvents, subThemeManager, lightenHex, darkenHex } from './Css.js';
10
10
  import { Translate } from './Translate.js';
11
11
  import { Modal } from './Modal.js';
12
12
  import { closeModalRouteChangeEvents, listenQueryPathInstance, setQueryPath, getQueryParams } from './Router.js';
@@ -30,11 +30,14 @@ const PanelForm = {
30
30
  share: {
31
31
  copyLink: false,
32
32
  },
33
+ showCreatorProfile: false,
33
34
  },
34
35
  ) {
35
36
  const { idPanel, defaultUrlImage, Elements } = options;
36
37
 
37
- let prefixTags = [idPanel, 'public'];
38
+ // Authenticated users don't need 'public' tag - they see all their own posts
39
+ // Only include 'public' for unauthenticated users (handled by backend)
40
+ let prefixTags = [idPanel];
38
41
  this.Data[idPanel] = {
39
42
  originData: [],
40
43
  data: [],
@@ -112,6 +115,7 @@ const PanelForm = {
112
115
  route: options.route,
113
116
  formContainerClass: 'session-in-log-in',
114
117
  share: options.share,
118
+ showCreatorProfile: options.showCreatorProfile,
115
119
  onClick: async function ({ payload }) {
116
120
  if (options.route) {
117
121
  setQueryPath({ path: options.route, queryPath: payload._id });
@@ -327,6 +331,9 @@ const PanelForm = {
327
331
  const location = `${prefixTags.join('/')}`;
328
332
  const blob = new Blob([data.mdFileId], { type: 'text/markdown' });
329
333
  const md = new File([blob], mdFileName, { type: 'text/markdown' });
334
+ // Parse and normalize tags
335
+ // Note: 'public' tag is automatically extracted by the backend and converted to isPublic field
336
+ // It will be filtered from the tags array to keep visibility control separate from content tags
330
337
  const tags = data.tags
331
338
  ? uniqueArray(
332
339
  data.tags
@@ -397,6 +404,7 @@ const PanelForm = {
397
404
  }
398
405
  }
399
406
  })();
407
+ // Backend will automatically extract 'public' from tags and set isPublic field
400
408
  const body = {
401
409
  location,
402
410
  tags,
@@ -420,6 +428,9 @@ const PanelForm = {
420
428
  _id: documentData._id,
421
429
  id: documentData._id,
422
430
  createdAt: documentData.createdAt,
431
+ // Use server response data - backend has already processed tags and isPublic
432
+ isPublic: documentData.isPublic || false,
433
+ tags: (documentData.tags || []).filter((t) => !prefixTags.includes(t)),
423
434
  };
424
435
 
425
436
  if (documentStatus === 'error') status = 'error';
@@ -560,13 +571,24 @@ const PanelForm = {
560
571
  id: documentObject._id,
561
572
  title: documentObject.title,
562
573
  createdAt: documentObject.createdAt,
574
+ // Backend filters 'public' tag automatically - it's converted to isPublic field
563
575
  tags: documentObject.tags.filter((t) => !prefixTags.includes(t)),
564
576
  mdFileId: marked.parse(mdFileId),
565
577
  userId: documentObject.userId._id,
578
+ userInfo:
579
+ documentObject.userId && typeof documentObject.userId === 'object'
580
+ ? {
581
+ username: documentObject.userId.username,
582
+ email: documentObject.userId.email,
583
+ _id: documentObject.userId._id,
584
+ profileImageId: documentObject.userId.profileImageId,
585
+ }
586
+ : null,
566
587
  fileId,
567
588
  tools: Elements.Data.user.main.model.user._id === documentObject.userId._id,
568
589
  _id: documentObject._id,
569
590
  totalCopyShareLinkCount: documentObject.totalCopyShareLinkCount || 0,
591
+ isPublic: documentObject.isPublic || false,
570
592
  });
571
593
  } catch (fileError) {
572
594
  logger.error('Error fetching files for document:', documentObject._id, fileError);