@okjavis/nodebb-theme-javis 6.0.0 → 6.0.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okjavis/nodebb-theme-javis",
3
- "version": "6.0.0",
3
+ "version": "6.0.3",
4
4
  "description": "Modern, premium NodeBB theme for JAVIS Community - Extends Harmony with custom styling",
5
5
  "main": "theme.js",
6
6
  "scripts": {
package/scss/_feed.scss CHANGED
@@ -1138,6 +1138,104 @@ ul.categories-list {
1138
1138
  }
1139
1139
  }
1140
1140
 
1141
+ // ===========================================================
1142
+ // SUGGESTED USERS – Following Empty State
1143
+ // ===========================================================
1144
+ .javis-suggested-users {
1145
+ background: $jv-surface;
1146
+ border: 1px solid $jv-border-subtle;
1147
+ border-radius: $jv-radius-lg;
1148
+ padding: $jv-space-4 $jv-space-6;
1149
+ margin-bottom: $jv-space-4;
1150
+ box-shadow: $jv-shadow-sm;
1151
+ }
1152
+
1153
+ .javis-suggested-users-title {
1154
+ font-size: $jv-font-size-xs;
1155
+ font-weight: 700;
1156
+ text-transform: uppercase;
1157
+ letter-spacing: 0.6px;
1158
+ color: $jv-text-muted;
1159
+ padding: $jv-space-3 0 $jv-space-2;
1160
+ margin: 0;
1161
+ }
1162
+
1163
+ .javis-sug-card {
1164
+ display: flex !important;
1165
+ flex-direction: row !important;
1166
+ align-items: center !important;
1167
+ gap: $jv-space-3;
1168
+ padding: $jv-space-3 0;
1169
+ border-bottom: 1px solid $jv-border-subtle;
1170
+
1171
+ &:last-child {
1172
+ border-bottom: none;
1173
+ padding-bottom: $jv-space-2;
1174
+ }
1175
+
1176
+ &:first-child {
1177
+ padding-top: $jv-space-2;
1178
+ }
1179
+
1180
+ // Anchor wrapping the avatar — must be inline-flex
1181
+ > a:first-child {
1182
+ display: inline-flex !important;
1183
+ flex-shrink: 0;
1184
+ line-height: 0;
1185
+ }
1186
+ }
1187
+
1188
+ .javis-sug-avatar {
1189
+ width: 40px !important;
1190
+ height: 40px !important;
1191
+ min-width: 40px;
1192
+ min-height: 40px;
1193
+ border-radius: 50% !important;
1194
+ flex-shrink: 0;
1195
+ display: block;
1196
+ object-fit: cover;
1197
+ }
1198
+
1199
+ .javis-sug-avatar-icon {
1200
+ color: #fff;
1201
+ display: inline-flex !important;
1202
+ align-items: center !important;
1203
+ justify-content: center !important;
1204
+ font-size: 16px;
1205
+ font-weight: 700;
1206
+ font-family: $jv-font-sans;
1207
+ line-height: 1;
1208
+ }
1209
+
1210
+ .javis-sug-info {
1211
+ flex: 1 1 0;
1212
+ min-width: 0;
1213
+ display: flex;
1214
+ flex-direction: column;
1215
+ gap: 2px;
1216
+ overflow: hidden;
1217
+ }
1218
+
1219
+ .javis-sug-name {
1220
+ font-size: $jv-font-size-sm;
1221
+ font-weight: 600;
1222
+ color: $jv-text-main;
1223
+ text-decoration: none;
1224
+ white-space: nowrap;
1225
+ overflow: hidden;
1226
+ text-overflow: ellipsis;
1227
+ display: block;
1228
+
1229
+ &:hover {
1230
+ color: $jv-primary;
1231
+ }
1232
+ }
1233
+
1234
+ .javis-sug-meta {
1235
+ font-size: $jv-font-size-xs;
1236
+ color: $jv-text-soft;
1237
+ }
1238
+
1141
1239
  // ===========================================================
1142
1240
  // MOBILE FIX - Category dropdown positioning
1143
1241
  // Fix dropdown opening from bottom left corner on mobile
@@ -26,20 +26,34 @@
26
26
  <div class="col-lg-6 col-sm-12 ms-auto">
27
27
  {{{ end }}}
28
28
 
29
- <!-- Controls row for composer prompt injection -->
30
- <div class="d-flex justify-content-between py-2 mb-0 gap-1">
31
- {{{ if canPost }}}
32
- <button id="new_topic" class="btn btn-primary btn-sm d-none">[[category:new-topic-button]]</button>
29
+ <!-- For You / Following tabs + filter -->
30
+ <div class="d-flex justify-content-between align-items-center py-2 mb-0 gap-1">
31
+ {{{ if loggedIn }}}
32
+ <div class="d-flex gap-1">
33
+ <a href="{config.relative_path}/feed" class="btn btn-ghost btn-sm ff-secondary fw-semibold {{{ if !showFollowed }}}active{{{ end }}}">For You</a>
34
+ <a href="{config.relative_path}/feed?users=followed" class="btn btn-ghost btn-sm ff-secondary fw-semibold {{{ if showFollowed }}}active{{{ end }}}">Following</a>
35
+ </div>
33
36
  {{{ end }}}
37
+ <div class="d-flex align-items-center gap-1 ms-auto">
38
+ <!-- IMPORT partials/category/filter-dropdown-right.tpl -->
39
+ {{{ if canPost }}}
40
+ <button id="new_topic" class="btn btn-primary btn-sm d-none">[[category:new-topic-button]]</button>
41
+ {{{ end }}}
42
+ </div>
34
43
  </div>
35
44
 
36
- <!-- Category filter - LinkedIn style, right aligned, tight spacing -->
37
- <div class="d-flex justify-content-end py-1 feed-category-filter">
38
- <!-- IMPORT partials/category/filter-dropdown-right.tpl -->
45
+ {{{ if !posts.length }}}
46
+ {{{ if showFollowed }}}
47
+ <div class="alert alert-info text-center mb-3">Follow new members to see their posts here.</div>
48
+ <div class="javis-suggested-users">
49
+ <p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.6px;color:#6b7280;margin:0 0 4px;">People you should follow</p>
50
+ <div id="javis-suggested-users-list">
51
+ <div class="text-center text-muted py-4"><i class="fa fa-spinner fa-spin me-2"></i></div>
52
+ </div>
39
53
  </div>
40
-
41
- {{{ if !posts.length }}}
42
- <div class="alert alert-warning text-center">[[feed:no-posts-found]] {{{ if !following.length }}}[[feed:are-you-following-anyone]] {{{ end }}}</div>
54
+ {{{ else }}}
55
+ <div class="alert alert-warning text-center">[[feed:no-posts-found]]</div>
56
+ {{{ end }}}
43
57
  {{{ end }}}
44
58
 
45
59
  <ul component="posts" class="list-unstyled" data-nextstart="{nextStart}">
@@ -126,9 +140,92 @@ $(document).ready(function() {
126
140
  require(['share'], function(share) {
127
141
  share.addShareHandlers('{title}');
128
142
  });
143
+
129
144
  });
130
145
  </script>
131
146
 
147
+ <script>
148
+ // Suggested users – standalone, same pattern as video embed
149
+ (function() {
150
+ function loadSuggestedUsers() {
151
+ var $list = $('#javis-suggested-users-list');
152
+ if (!$list.length) return;
153
+
154
+ fetch((config.relative_path || '') + '/api/users?section=top', {
155
+ credentials: 'same-origin',
156
+ headers: { 'Accept': 'application/json' }
157
+ })
158
+ .then(function(r) { return r.json(); })
159
+ .then(function(data) {
160
+ var users = (data.users || []).filter(function(u) {
161
+ return u.uid && String(u.uid) !== String(config.uid);
162
+ }).slice(0, 6);
163
+
164
+ if (!users.length) {
165
+ $list.html('<p class="text-muted text-center py-2 mb-0">No members to suggest yet.</p>');
166
+ return;
167
+ }
168
+
169
+ $list.html(users.map(function(u, i) {
170
+ var bg = u['icon:bgColor'] || '#0051ff';
171
+ var icon = u['icon:text'] || (u.displayname || 'U')[0].toUpperCase();
172
+ var isLast = i === users.length - 1;
173
+ var avatarHtml = u.picture
174
+ ? '<img src="' + u.picture + '" style="width:40px;height:40px;min-width:40px;border-radius:50%;object-fit:cover;display:block;" data-bg="' + bg + '" data-icon="' + icon + '" class="javis-sug-avatar-img">'
175
+ : '<span style="width:40px;height:40px;min-width:40px;border-radius:50%;background:' + bg + ';color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:15px;font-weight:700;font-family:system-ui,sans-serif;flex-shrink:0;">' + icon + '</span>';
176
+ return '<div style="display:flex;align-items:center;gap:12px;padding:10px 0;' + (isLast ? '' : 'border-bottom:1px solid rgba(0,0,0,0.07);') + '">' +
177
+ '<a href="' + (config.relative_path || '') + '/user/' + u.userslug + '" style="flex-shrink:0;display:inline-flex;line-height:0;">' + avatarHtml + '</a>' +
178
+ '<div style="flex:1;min-width:0;">' +
179
+ '<a href="' + (config.relative_path || '') + '/user/' + u.userslug + '" style="font-size:13px;font-weight:600;color:#111;text-decoration:none;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + (u.displayname || u.username) + '</a>' +
180
+ '<span style="font-size:12px;color:#9ca3af;">' + (u.postcount || 0) + ' posts</span>' +
181
+ '</div>' +
182
+ '<button class="btn btn-sm rounded-pill javis-follow-btn" style="flex-shrink:0;font-size:12px;font-weight:600;padding:4px 14px;border:1.5px solid #0051ff;color:#0051ff;background:transparent;white-space:nowrap;" data-userslug="' + u.userslug + '">Follow</button>' +
183
+ '</div>';
184
+ }).join(''));
185
+
186
+ $list.find('img.javis-sug-avatar-img').on('error', function() {
187
+ var $img = $(this);
188
+ $img.replaceWith('<span class="javis-sug-avatar javis-sug-avatar-icon" style="background:' + ($img.data('bg') || '#0051ff') + '">' + ($img.data('icon') || 'U') + '</span>');
189
+ });
190
+ })
191
+ .catch(function() {
192
+ $list.closest('.javis-suggested-users').remove();
193
+ });
194
+ }
195
+
196
+ // Initial load
197
+ if (document.readyState === 'loading') {
198
+ document.addEventListener('DOMContentLoaded', function() { setTimeout(loadSuggestedUsers, 100); });
199
+ } else {
200
+ setTimeout(loadSuggestedUsers, 100);
201
+ }
202
+
203
+ // SPA navigation
204
+ $(window).on('action:ajaxify.end', function() {
205
+ setTimeout(loadSuggestedUsers, 100);
206
+ });
207
+
208
+ // Follow button
209
+ $(document).on('click', '.javis-follow-btn', function() {
210
+ var $btn = $(this);
211
+ var userslug = $btn.data('userslug');
212
+ $btn.prop('disabled', true).text('...');
213
+ fetch((config.relative_path || '') + '/api/v3/users/' + userslug + '/follow', {
214
+ method: 'PUT',
215
+ credentials: 'same-origin',
216
+ headers: { 'Content-Type': 'application/json', 'x-csrf-token': config.csrf_token },
217
+ body: JSON.stringify({})
218
+ }).then(function(r) {
219
+ if (r.ok) {
220
+ $btn.text('Following').css({'background':'#0051ff','color':'#fff','border-color':'#0051ff'}).prop('disabled',true);
221
+ } else { throw new Error(); }
222
+ }).catch(function() {
223
+ $btn.prop('disabled', false).text('Follow');
224
+ });
225
+ });
226
+ })();
227
+ </script>
228
+
132
229
  <script>
133
230
  // Embed videos on feed page - runs independently
134
231
  (function() {
@@ -0,0 +1,45 @@
1
+ {{{ if !notifications.length }}}
2
+ <div class="no-notifs text-center p-4 d-flex flex-column">
3
+ <div class="p-4"><i class="fa-solid fa-wind fs-2 text-muted"></i></div>
4
+ <div class="text-xs fw-semibold text-muted">[[notifications:no-notifs]]</div>
5
+ </div>
6
+ {{{ end }}}
7
+
8
+ {{{ each notifications }}}
9
+ <div class="{./readClass}" data-nid="{./nid}" data-path="{./path}" {{{ if ./pid }}}data-pid="{./pid}"{{{ end }}}{{{ if ./tid }}}data-tid="{./tid}"{{{ end }}}>
10
+ <div class="d-flex gap-1 justify-content-between">
11
+ <div class="btn btn-ghost btn-sm d-flex gap-2 flex-grow-1 text-start align-items-start">
12
+ <a class="flex-grow-0 flex-shrink-0" href="{{{ if ./user.userslug}}}{config.relative_path}/user/{./user.userslug}{{{ else }}}#{{{ end }}}">
13
+ {{{ if (./image && ./from) }}}
14
+ <img class="avatar avatar-rounded" style="--avatar-size: 32px;" src="{./image}" onerror="this.onerror=null;this.style.display='none';this.nextElementSibling.classList.remove('hidden');" />
15
+ <div class="avatar avatar-rounded hidden" style="--avatar-size: 32px; background-color: {./user.icon:bgColor};">{./user.icon:text}</div>
16
+ {{{ else }}}
17
+ {{{ if ./icon }}}
18
+ <div class="avatar avatar-rounded" style="--avatar-size: 32px;"><i class="text-secondary fa {./icon}"></i></div>
19
+ {{{ else }}}
20
+ <div class="avatar avatar-rounded" style="--avatar-size: 32px; background-color: {./user.icon:bgColor};">{./user.icon:text}</div>
21
+ {{{ end }}}
22
+ {{{ end }}}
23
+ </a>
24
+
25
+ <div class="d-flex flex-grow-1 flex-column align-items-start position-relative">
26
+ <a href="{./path}" class="text-decoration-none d-inline-block text-reset text-break text-sm ff-sans stretched-link" component="notifications/item/link">
27
+ {./bodyShort}
28
+ </a>
29
+ <div class="text-xs text-muted">{{{ if ./timeagoLong }}}{./timeagoLong}{{{ else }}}<span class="timeago" title="{./datetimeISO}"></span>{{{ end }}}</div>
30
+ </div>
31
+ </div>
32
+ <div>
33
+ {{{ if ./nid }}}
34
+ <button class="mark-read btn btn-ghost btn-sm d-flex align-items-center justify-content-center flex-grow-0 flex-shrink-0 p-1" style="width: 1.5rem; height: 1.5rem;">
35
+ <i class="unread fa fa-2xs fa-circle text-primary {{{ if ./read }}}hidden{{{ end }}}" aria-label="[[unread:mark-as-read]]"></i>
36
+ <i class="read fa fa-2xs fa-circle-o text-secondary {{{ if !./read }}}hidden{{{ end }}}" aria-label="[[unread:mark-as-unread]]"></i>
37
+ </button>
38
+ {{{ end }}}
39
+ </div>
40
+ </div>
41
+ </div>
42
+ {{{ if !@last }}}
43
+ <hr class="my-1" />
44
+ {{{ end }}}
45
+ {{{ end }}}