@okjavis/nodebb-theme-javis 4.0.4 → 5.0.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.
package/package.json
CHANGED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
define('forum/search', [
|
|
5
|
+
'search',
|
|
6
|
+
'storage',
|
|
7
|
+
'hooks',
|
|
8
|
+
'alerts',
|
|
9
|
+
'api',
|
|
10
|
+
'translator',
|
|
11
|
+
'categoryFilter',
|
|
12
|
+
'userFilter',
|
|
13
|
+
], function (searchModule, storage, hooks, alerts, api, translator, categoryFilter, userFilter) {
|
|
14
|
+
const Search = {};
|
|
15
|
+
let selectedUsers = [];
|
|
16
|
+
let selectedTags = [];
|
|
17
|
+
let selectedCids = [];
|
|
18
|
+
let searchFilters = {};
|
|
19
|
+
Search.init = function () {
|
|
20
|
+
const searchIn = $('#search-in');
|
|
21
|
+
searchIn.on('change', function () {
|
|
22
|
+
updateFormItemVisiblity(searchIn.val());
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const searchQuery = $('#results').attr('data-search-query');
|
|
26
|
+
searchModule.highlightMatches(
|
|
27
|
+
searchQuery,
|
|
28
|
+
$('.search-results .content p, .search-results .topic-title')
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
$('#advanced-search form').off('submit').on('submit', function (e) {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
searchModule.query(getSearchDataFromDOM());
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
handleSavePreferences();
|
|
38
|
+
|
|
39
|
+
categoryFilterDropdown(ajaxify.data.selectedCids);
|
|
40
|
+
userFilterDropdown($('[component="user/filter"]'), ajaxify.data.userFilterSelected);
|
|
41
|
+
tagFilterDropdown($('[component="tag/filter"]'), ajaxify.data.tagFilterSelected);
|
|
42
|
+
|
|
43
|
+
$('[component="search/filters"]').on('hidden.bs.dropdown', '.dropdown', function () {
|
|
44
|
+
const updateFns = {
|
|
45
|
+
replies: updateReplyCountFilter,
|
|
46
|
+
time: updateTimeFilter,
|
|
47
|
+
sort: updateSortFilter,
|
|
48
|
+
tag: updateTagFilter,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (updateFns[$(this).attr('data-filter-name')]) {
|
|
52
|
+
updateFns[$(this).attr('data-filter-name')]();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const searchFiltersNew = getSearchDataFromDOM();
|
|
56
|
+
if (JSON.stringify(searchFilters) !== JSON.stringify(searchFiltersNew)) {
|
|
57
|
+
searchFilters = searchFiltersNew;
|
|
58
|
+
searchModule.query(searchFilters);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
fillOutForm();
|
|
63
|
+
updateTimeFilter();
|
|
64
|
+
updateReplyCountFilter();
|
|
65
|
+
updateSortFilter();
|
|
66
|
+
|
|
67
|
+
searchFilters = getSearchDataFromDOM();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function updateTagFilter() {
|
|
71
|
+
const isActive = selectedTags.length > 0;
|
|
72
|
+
let labelText = '[[search:tags]]';
|
|
73
|
+
if (selectedTags.length) {
|
|
74
|
+
labelText = translator.compile(
|
|
75
|
+
'search:tags-x', selectedTags.map(u => u.value).join(', ')
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
$('[component="tag/filter/button"]').toggleClass(
|
|
79
|
+
'active-filter', isActive
|
|
80
|
+
).find('.filter-label').translateHtml(labelText);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function updateTimeFilter() {
|
|
84
|
+
const isActive = $('#post-time-range').val() > 0;
|
|
85
|
+
$('#post-time-button').toggleClass(
|
|
86
|
+
'active-filter', isActive
|
|
87
|
+
).find('.filter-label').translateText(
|
|
88
|
+
isActive ?
|
|
89
|
+
`[[search:time-${$('#post-time-filter').val()}-than-${$('#post-time-range').val()}]]` :
|
|
90
|
+
`[[search:time]]`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateSortFilter() {
|
|
95
|
+
const isActive = $('#post-sort-by').val() !== 'relevance' || $('#post-sort-direction').val() !== 'desc';
|
|
96
|
+
$('#sort-by-button').toggleClass(
|
|
97
|
+
'active-filter', isActive
|
|
98
|
+
).find('.filter-label').translateText(
|
|
99
|
+
isActive ?
|
|
100
|
+
`[[search:sort-by-${$('#post-sort-by').val()}-${$('#post-sort-direction').val()}]]` :
|
|
101
|
+
`[[search:sort]]`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateReplyCountFilter() {
|
|
106
|
+
const isActive = $('#reply-count').val() > 0;
|
|
107
|
+
$('#reply-count-button').toggleClass(
|
|
108
|
+
'active-filter', isActive
|
|
109
|
+
).find('.filter-label').translateText(
|
|
110
|
+
isActive ?
|
|
111
|
+
`[[search:replies-${$('#reply-count-filter').val()}-count, ${$('#reply-count').val()}]]` :
|
|
112
|
+
`[[search:replies]]`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getSearchDataFromDOM() {
|
|
117
|
+
const form = $('#advanced-search');
|
|
118
|
+
const searchData = {
|
|
119
|
+
in: $('#search-in').val(),
|
|
120
|
+
};
|
|
121
|
+
searchData.term = $('#search-input').val();
|
|
122
|
+
if (['posts', 'titlesposts', 'titles', 'bookmarks'].includes(searchData.in)) {
|
|
123
|
+
searchData.matchWords = form.find('#match-words-filter').val();
|
|
124
|
+
searchData.by = selectedUsers.length ? selectedUsers.map(u => u.username) : undefined;
|
|
125
|
+
searchData.categories = selectedCids.length ? selectedCids : undefined;
|
|
126
|
+
searchData.searchChildren = form.find('#search-children').is(':checked');
|
|
127
|
+
searchData.hasTags = selectedTags.length ? selectedTags.map(t => t.value) : undefined;
|
|
128
|
+
searchData.replies = form.find('#reply-count').val();
|
|
129
|
+
searchData.repliesFilter = form.find('#reply-count-filter').val();
|
|
130
|
+
searchData.timeFilter = form.find('#post-time-filter').val();
|
|
131
|
+
searchData.timeRange = form.find('#post-time-range').val();
|
|
132
|
+
searchData.sortBy = form.find('#post-sort-by').val();
|
|
133
|
+
searchData.sortDirection = form.find('#post-sort-direction').val();
|
|
134
|
+
searchData.showAs = form.find('#show-results-as').val();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
hooks.fire('action:search.getSearchDataFromDOM', {
|
|
138
|
+
form: form,
|
|
139
|
+
data: searchData,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return searchData;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function updateFormItemVisiblity(searchIn) {
|
|
146
|
+
const hideTitlePostFilters = !['posts', 'titles', 'bookmarks'].some(token => searchIn.includes(token));
|
|
147
|
+
$('.post-search-item').toggleClass('hidden', hideTitlePostFilters);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function fillOutForm() {
|
|
151
|
+
const params = utils.params({
|
|
152
|
+
disableToType: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const searchData = searchModule.getSearchPreferences();
|
|
156
|
+
const formData = utils.merge(searchData, params);
|
|
157
|
+
|
|
158
|
+
if (formData) {
|
|
159
|
+
if (ajaxify.data.term) {
|
|
160
|
+
$('#search-input').val(ajaxify.data.term);
|
|
161
|
+
}
|
|
162
|
+
formData.in = formData.in || ajaxify.data.searchDefaultIn;
|
|
163
|
+
$('#search-in').val(formData.in);
|
|
164
|
+
updateFormItemVisiblity(formData.in);
|
|
165
|
+
|
|
166
|
+
if (formData.matchWords) {
|
|
167
|
+
$('#match-words-filter').val(formData.matchWords);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (formData.showAs) {
|
|
171
|
+
$('#show-results-as').val(formData.showAs);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (formData.by) {
|
|
175
|
+
formData.by = Array.isArray(formData.by) ? formData.by : [formData.by];
|
|
176
|
+
formData.by.forEach(function (by) {
|
|
177
|
+
$('#posted-by-user').tagsinput('add', by);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (formData.categories) {
|
|
182
|
+
$('#posted-in-categories').val(formData.categories);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (formData.searchChildren) {
|
|
186
|
+
$('#search-children').prop('checked', true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (formData.hasTags) {
|
|
190
|
+
formData.hasTags = Array.isArray(formData.hasTags) ? formData.hasTags : [formData.hasTags];
|
|
191
|
+
formData.hasTags.forEach(function (tag) {
|
|
192
|
+
$('#has-tags').tagsinput('add', tag);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (formData.replies) {
|
|
197
|
+
$('#reply-count').val(formData.replies);
|
|
198
|
+
$('#reply-count-filter').val(formData.repliesFilter);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (formData.timeRange) {
|
|
202
|
+
$('#post-time-range').val(formData.timeRange);
|
|
203
|
+
$('#post-time-filter').val(formData.timeFilter);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (formData.sortBy || ajaxify.data.searchDefaultSortBy) {
|
|
207
|
+
$('#post-sort-by').val(formData.sortBy || ajaxify.data.searchDefaultSortBy);
|
|
208
|
+
}
|
|
209
|
+
$('#post-sort-direction').val(formData.sortDirection || 'desc');
|
|
210
|
+
|
|
211
|
+
hooks.fire('action:search.fillOutForm', {
|
|
212
|
+
form: formData,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function handleSavePreferences() {
|
|
218
|
+
$('#save-preferences').on('click', function () {
|
|
219
|
+
const data = getSearchDataFromDOM();
|
|
220
|
+
const fieldsToSave = [
|
|
221
|
+
'matchWords', 'in', 'showAs',
|
|
222
|
+
'replies', 'repliesFilter',
|
|
223
|
+
'timeFilter', 'timeRange',
|
|
224
|
+
'sortBy', 'sortDirection',
|
|
225
|
+
];
|
|
226
|
+
const saveData = {};
|
|
227
|
+
fieldsToSave.forEach((key) => {
|
|
228
|
+
saveData[key] = data[key];
|
|
229
|
+
});
|
|
230
|
+
storage.setItem('search-preferences', JSON.stringify(saveData));
|
|
231
|
+
alerts.success('[[search:search-preferences-saved]]');
|
|
232
|
+
return false;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
$('#clear-preferences').on('click', async function () {
|
|
236
|
+
storage.removeItem('search-preferences');
|
|
237
|
+
const html = await app.parseAndTranslate('partials/search-filters', {});
|
|
238
|
+
$('[component="search/filters"]').replaceWith(html);
|
|
239
|
+
$('#search-in').val(ajaxify.data.searchDefaultIn);
|
|
240
|
+
$('#post-sort-by').val(ajaxify.data.searchDefaultSortBy);
|
|
241
|
+
$('#match-words-filter').val('all');
|
|
242
|
+
$('#show-results-as').val('posts');
|
|
243
|
+
// clearing dom removes all event handlers, reinitialize
|
|
244
|
+
userFilterDropdown($('[component="user/filter"]'), []);
|
|
245
|
+
tagFilterDropdown($('[component="tag/filter"]'), []);
|
|
246
|
+
categoryFilterDropdown([]);
|
|
247
|
+
alerts.success('[[search:search-preferences-cleared]]');
|
|
248
|
+
return false;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
function categoryFilterDropdown(_selectedCids) {
|
|
254
|
+
ajaxify.data.allCategoriesUrl = '';
|
|
255
|
+
selectedCids = _selectedCids || [];
|
|
256
|
+
const dropdownEl = $('[component="category/filter"]');
|
|
257
|
+
categoryFilter.init(dropdownEl, {
|
|
258
|
+
selectedCids: _selectedCids,
|
|
259
|
+
updateButton: false, // prevent categoryFilter module from updating the button
|
|
260
|
+
onSelect: function (data) {
|
|
261
|
+
// Update selectedCids immediately when a category is clicked
|
|
262
|
+
ajaxify.data.selectedCids = data.selectedCids;
|
|
263
|
+
selectedCids = data.selectedCids;
|
|
264
|
+
|
|
265
|
+
// Trigger search immediately
|
|
266
|
+
const searchFiltersNew = getSearchDataFromDOM();
|
|
267
|
+
if (JSON.stringify(searchFilters) !== JSON.stringify(searchFiltersNew)) {
|
|
268
|
+
searchFilters = searchFiltersNew;
|
|
269
|
+
searchModule.query(searchFilters);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
onHidden: async function (data) {
|
|
273
|
+
const isActive = data.selectedCids.length > 0 && data.selectedCids[0] !== 'all';
|
|
274
|
+
let labelText = '[[search:categories]]';
|
|
275
|
+
ajaxify.data.selectedCids = data.selectedCids;
|
|
276
|
+
selectedCids = data.selectedCids;
|
|
277
|
+
if (data.selectedCids.length === 1 && data.selectedCids[0] === 'all') {
|
|
278
|
+
ajaxify.data.selectedCategory = null;
|
|
279
|
+
} else if (data.selectedCids.length > 0) {
|
|
280
|
+
const categoryData = await api.get(`/categories/${data.selectedCids[0]}`);
|
|
281
|
+
ajaxify.data.selectedCategory = categoryData;
|
|
282
|
+
labelText = `[[search:categories-x, ${categoryData.name}]]`;
|
|
283
|
+
}
|
|
284
|
+
if (data.selectedCids.length > 1) {
|
|
285
|
+
labelText = `[[search:categories-x, ${data.selectedCids.length}]]`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
$('[component="category/filter/button"]').toggleClass(
|
|
289
|
+
'active-filter', isActive
|
|
290
|
+
).find('.filter-label').translateText(labelText);
|
|
291
|
+
},
|
|
292
|
+
localCategories: [],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function userFilterDropdown(el, _selectedUsers) {
|
|
297
|
+
selectedUsers = _selectedUsers || [];
|
|
298
|
+
userFilter.init(el, {
|
|
299
|
+
selectedUsers: _selectedUsers,
|
|
300
|
+
template: 'partials/search-filters',
|
|
301
|
+
onSelect: function (_selectedUsers) {
|
|
302
|
+
selectedUsers = _selectedUsers;
|
|
303
|
+
},
|
|
304
|
+
onHidden: function (_selectedUsers) {
|
|
305
|
+
const isActive = _selectedUsers.length > 0;
|
|
306
|
+
let labelText = '[[search:posted-by]]';
|
|
307
|
+
if (isActive) {
|
|
308
|
+
labelText = translator.compile(
|
|
309
|
+
'search:posted-by-usernames', selectedUsers.map(u => u.username).join(', ')
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
el.find('[component="user/filter/button"]').toggleClass(
|
|
313
|
+
'active-filter', isActive
|
|
314
|
+
).find('.filter-label').translateText(labelText);
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function tagFilterDropdown(el, _selectedTags) {
|
|
320
|
+
selectedTags = _selectedTags;
|
|
321
|
+
async function renderSelectedTags() {
|
|
322
|
+
const html = await app.parseAndTranslate('partials/search-filters', 'tagFilterSelected', {
|
|
323
|
+
tagFilterSelected: selectedTags,
|
|
324
|
+
});
|
|
325
|
+
el.find('[component="tag/filter/selected"]').html(html);
|
|
326
|
+
}
|
|
327
|
+
function tagValueToObject(value) {
|
|
328
|
+
const escapedTag = utils.escapeHTML(value);
|
|
329
|
+
return {
|
|
330
|
+
value: value,
|
|
331
|
+
valueEscaped: escapedTag,
|
|
332
|
+
valueEncoded: encodeURIComponent(value),
|
|
333
|
+
class: escapedTag.replace(/\s/g, '-'),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function doSearch() {
|
|
338
|
+
let result = { tags: [] };
|
|
339
|
+
const query = el.find('[component="tag/filter/search"]').val();
|
|
340
|
+
if (query && query.length > 1) {
|
|
341
|
+
if (app.user.privileges['search:tags']) {
|
|
342
|
+
result = await socket.emit('topics.searchAndLoadTags', { query: query });
|
|
343
|
+
} else {
|
|
344
|
+
result = {
|
|
345
|
+
tags: [tagValueToObject(query)],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!result.tags.length) {
|
|
351
|
+
el.find('[component="tag/filter/results"]').translateHtml(
|
|
352
|
+
'[[tags:no-tags-found]]'
|
|
353
|
+
);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
result.tags = result.tags.slice(0, 20);
|
|
357
|
+
const tagMap = {};
|
|
358
|
+
result.tags.forEach((tag) => {
|
|
359
|
+
tagMap[tag.value] = tag;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const html = await app.parseAndTranslate('partials/search-filters', 'tagFilterResults', {
|
|
363
|
+
tagFilterResults: result.tags,
|
|
364
|
+
});
|
|
365
|
+
el.find('[component="tag/filter/results"]').html(html);
|
|
366
|
+
el.find('[component="tag/filter/results"] [data-tag]').on('click', async function () {
|
|
367
|
+
selectedTags.push(tagMap[$(this).attr('data-tag')]);
|
|
368
|
+
renderSelectedTags();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
el.find('[component="tag/filter/search"]').on('keyup', utils.debounce(function () {
|
|
373
|
+
if (app.user.privileges['search:tags']) {
|
|
374
|
+
doSearch();
|
|
375
|
+
}
|
|
376
|
+
}, 1000));
|
|
377
|
+
|
|
378
|
+
el.on('click', '[component="tag/filter/delete"]', function () {
|
|
379
|
+
const deleteTag = $(this).attr('data-tag');
|
|
380
|
+
selectedTags = selectedTags.filter(tag => tag.value !== deleteTag);
|
|
381
|
+
renderSelectedTags();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
el.find('[component="tag/filter/search"]').on('keyup', (e) => {
|
|
385
|
+
if (e.key === 'Enter' && !app.user.privileges['search:tags']) {
|
|
386
|
+
const value = el.find('[component="tag/filter/search"]').val();
|
|
387
|
+
if (value && selectedTags.every(tag => tag.value !== value)) {
|
|
388
|
+
selectedTags.push(tagValueToObject(value));
|
|
389
|
+
renderSelectedTags();
|
|
390
|
+
}
|
|
391
|
+
el.find('[component="tag/filter/search"]').val('');
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
el.on('shown.bs.dropdown', function () {
|
|
396
|
+
el.find('[component="tag/filter/search"]').trigger('focus');
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return Search;
|
|
401
|
+
});
|
package/scss/_base.scss
CHANGED
|
@@ -79,7 +79,8 @@ body {
|
|
|
79
79
|
font-size: $jv-font-size-base;
|
|
80
80
|
line-height: $jv-line-height-base;
|
|
81
81
|
color: $jv-text-main;
|
|
82
|
-
background
|
|
82
|
+
background: linear-gradient(180deg, #ffffff 0%, #e0f0ff 100%);
|
|
83
|
+
background-attachment: fixed;
|
|
83
84
|
-webkit-font-smoothing: antialiased;
|
|
84
85
|
-moz-osx-font-smoothing: grayscale;
|
|
85
86
|
}
|
package/scss/_buttons.scss
CHANGED
|
@@ -378,3 +378,55 @@
|
|
|
378
378
|
text-align: left;
|
|
379
379
|
justify-content: flex-start;
|
|
380
380
|
}
|
|
381
|
+
|
|
382
|
+
// ===========================================================
|
|
383
|
+
// LINKEDIN SSO BUTTON
|
|
384
|
+
// ===========================================================
|
|
385
|
+
.linkedin-sso-btn {
|
|
386
|
+
background-color: #0077B5 !important;
|
|
387
|
+
border-color: #0077B5 !important;
|
|
388
|
+
color: #fff !important;
|
|
389
|
+
font-size: 16px !important;
|
|
390
|
+
font-weight: 600 !important;
|
|
391
|
+
border-radius: $jv-radius-md !important;
|
|
392
|
+
box-shadow: 0 4px 14px rgba(0, 119, 181, 0.35) !important;
|
|
393
|
+
transition: all 0.2s ease !important;
|
|
394
|
+
|
|
395
|
+
&:hover {
|
|
396
|
+
background-color: #005885 !important;
|
|
397
|
+
border-color: #005885 !important;
|
|
398
|
+
box-shadow: 0 6px 20px rgba(0, 119, 181, 0.45) !important;
|
|
399
|
+
transform: translateY(-1px);
|
|
400
|
+
color: #fff !important;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
&:active {
|
|
404
|
+
transform: translateY(0);
|
|
405
|
+
box-shadow: 0 2px 8px rgba(0, 119, 181, 0.3) !important;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
i {
|
|
409
|
+
font-size: 20px !important;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// LinkedIn login block styling
|
|
414
|
+
.linkedin-login-block {
|
|
415
|
+
max-width: 400px;
|
|
416
|
+
margin: 0 auto;
|
|
417
|
+
padding: $jv-space-6;
|
|
418
|
+
background: $jv-surface;
|
|
419
|
+
border-radius: $jv-radius-lg;
|
|
420
|
+
border: 1px solid $jv-border-subtle;
|
|
421
|
+
box-shadow: $jv-shadow-md;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Hide admin login section by default (shown via JS when ?admin=true)
|
|
425
|
+
.admin-login-section {
|
|
426
|
+
display: none !important;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// When admin login is shown
|
|
430
|
+
.admin-login-section[style*="display: block"] {
|
|
431
|
+
display: block !important;
|
|
432
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<div data-widget-area="header">
|
|
2
|
+
{{{each widgets.header}}}
|
|
3
|
+
{{widgets.header.html}}
|
|
4
|
+
{{{end}}}
|
|
5
|
+
</div>
|
|
6
|
+
<div class="row login flex-fill align-items-center justify-content-center" style="min-height: calc(100vh - 200px);">
|
|
7
|
+
<div class="d-flex flex-column gap-2 {{{ if widgets.sidebar.length }}}col-lg-9 col-sm-12{{{ else }}}col-lg-12{{{ end }}}">
|
|
8
|
+
<div class="row justify-content-center align-items-center">
|
|
9
|
+
<!-- LinkedIn Primary Login (hidden when ?admin=true) -->
|
|
10
|
+
{{{ if alternate_logins }}}
|
|
11
|
+
<div class="col-12 col-md-6 col-lg-4 px-md-0 linkedin-section">
|
|
12
|
+
<div class="linkedin-login-block d-flex flex-column align-items-center gap-4 py-5">
|
|
13
|
+
<h2 class="tracking-tight fw-semibold text-center mb-2">Welcome to JAVIS Community</h2>
|
|
14
|
+
<p class="text-muted text-center mb-4">Sign in with your LinkedIn account to continue</p>
|
|
15
|
+
|
|
16
|
+
<ul class="alt-logins list-unstyled w-100" style="max-width: 320px;">
|
|
17
|
+
{{{ each authentication }}}
|
|
18
|
+
<li class="{./name} mb-2">
|
|
19
|
+
<a class="btn linkedin-sso-btn d-flex align-items-center justify-content-center gap-3 w-100 py-3" rel="nofollow noopener noreferrer" target="_top" href="{config.relative_path}{./url}">
|
|
20
|
+
<i class="fa-brands fa-linkedin fa-lg"></i>
|
|
21
|
+
<span class="fw-semibold">Sign in with LinkedIn</span>
|
|
22
|
+
</a>
|
|
23
|
+
</li>
|
|
24
|
+
{{{ end }}}
|
|
25
|
+
</ul>
|
|
26
|
+
|
|
27
|
+
<p class="text-muted text-center text-sm mt-4">
|
|
28
|
+
By signing in, you agree to our Terms of Service and Privacy Policy
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
{{{ end }}}
|
|
33
|
+
|
|
34
|
+
<!-- Admin Login Form -->
|
|
35
|
+
{{{ if allowLocalLogin }}}
|
|
36
|
+
<div class="col-12 col-md-6 col-lg-4 px-md-0 admin-login-section">
|
|
37
|
+
<div class="admin-login-block d-flex flex-column gap-3 py-5">
|
|
38
|
+
<h2 class="tracking-tight fw-semibold text-center mb-2">Admin Login</h2>
|
|
39
|
+
<p class="text-muted text-center mb-3">Sign in with your administrator credentials</p>
|
|
40
|
+
<form class="d-flex flex-column gap-3" role="form" method="post" id="login-form">
|
|
41
|
+
<div class="mb-2 d-flex flex-column gap-2">
|
|
42
|
+
<label for="username">{allowLoginWith}</label>
|
|
43
|
+
<input class="form-control" type="text" placeholder="{allowLoginWith}" name="username" id="username" autocorrect="off" autocapitalize="off" autocomplete="nickname" value="{username}" aria-required="true"/>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="mb-2 d-flex flex-column gap-2">
|
|
47
|
+
<label for="password">[[user:password]]</label>
|
|
48
|
+
<div>
|
|
49
|
+
<input class="form-control" type="password" placeholder="[[user:password]]" name="password" id="password" autocomplete="current-password" autocapitalize="off" aria-required="true"/>
|
|
50
|
+
<p id="caps-lock-warning" class="text-danger hidden text-sm mb-0 form-text" aria-live="polite" role="alert" aria-atomic="true">
|
|
51
|
+
<i class="fa fa-exclamation-triangle"></i> [[login:caps-lock-enabled]]
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
{{{ if allowPasswordReset }}}
|
|
55
|
+
<div>
|
|
56
|
+
<a id="reset-link" class="text-sm text-reset text-decoration-underline" href="{config.relative_path}/reset">[[login:forgot-password]]</a>
|
|
57
|
+
</div>
|
|
58
|
+
{{{ end }}}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{{{ each loginFormEntry }}}
|
|
62
|
+
<div class="mb-2 loginFormEntry d-flex flex-column gap-2 {./styleName}">
|
|
63
|
+
<label for="{./inputId}">{./label}</label>
|
|
64
|
+
<div>{{./html}}</div>
|
|
65
|
+
</div>
|
|
66
|
+
{{{ end }}}
|
|
67
|
+
|
|
68
|
+
<input type="hidden" name="_csrf" value="{config.csrf_token}" />
|
|
69
|
+
<input type="hidden" name="noscript" id="noscript" value="true" />
|
|
70
|
+
|
|
71
|
+
<button class="btn btn-primary" id="login" type="submit">[[global:login]]</button>
|
|
72
|
+
|
|
73
|
+
<div class="form-check mb-2">
|
|
74
|
+
<input class="form-check-input" type="checkbox" name="remember" id="remember" checked />
|
|
75
|
+
<label class="form-check-label" for="remember">[[login:remember-me]]</label>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="alert alert-danger {{{ if !error }}} hidden{{{ end }}}" id="login-error-notify" role="alert" aria-atomic="true">
|
|
79
|
+
<strong>[[login:failed-login-attempt]]</strong>
|
|
80
|
+
<p class="mb-0">{error}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</form>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
{{{ end }}}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div data-widget-area="sidebar" class="col-lg-3 col-sm-12 {{{ if !widgets.sidebar.length }}}hidden{{{ end }}}">
|
|
89
|
+
{{{each widgets.sidebar}}}
|
|
90
|
+
{{widgets.sidebar.html}}
|
|
91
|
+
{{{end}}}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div data-widget-area="footer">
|
|
95
|
+
{{{each widgets.footer}}}
|
|
96
|
+
{{widgets.footer.html}}
|
|
97
|
+
{{{end}}}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
// Control visibility based on ?admin=true parameter
|
|
102
|
+
(function() {
|
|
103
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
104
|
+
const isAdminMode = urlParams.get('admin') === 'true';
|
|
105
|
+
const adminSection = document.querySelector('.admin-login-section');
|
|
106
|
+
const linkedinSection = document.querySelector('.linkedin-section');
|
|
107
|
+
|
|
108
|
+
if (isAdminMode) {
|
|
109
|
+
// Admin mode: show admin form, hide LinkedIn
|
|
110
|
+
if (adminSection) {
|
|
111
|
+
adminSection.style.display = 'block';
|
|
112
|
+
}
|
|
113
|
+
if (linkedinSection) {
|
|
114
|
+
linkedinSection.style.display = 'none';
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
// Normal mode: show LinkedIn (if exists), hide admin form
|
|
118
|
+
if (linkedinSection) {
|
|
119
|
+
linkedinSection.style.display = 'block';
|
|
120
|
+
if (adminSection) {
|
|
121
|
+
adminSection.style.display = 'none';
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// No LinkedIn configured - show admin form as fallback
|
|
125
|
+
if (adminSection) {
|
|
126
|
+
adminSection.style.display = 'block';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<button type="button" class="btn btn-ghost btn-sm d-flex align-items-center ff-secondary d-flex gap-2 dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
|
2
|
+
{{{ if selectedCategory }}}
|
|
3
|
+
<span class="category-item d-inline-flex align-items-center gap-1">
|
|
4
|
+
{buildCategoryIcon(selectedCategory, "18px", "rounded-circle")}
|
|
5
|
+
<span class="d-none d-md-inline fw-semibold">{selectedCategory.name}</span>
|
|
6
|
+
</span>
|
|
7
|
+
{{{ else }}}
|
|
8
|
+
<i class="fa fa-fw fa-list text-primary"></i>
|
|
9
|
+
<span class="d-none d-md-inline fw-semibold">[[unread:all-categories]]</span>{{{ end }}}
|
|
10
|
+
</button>
|
|
11
|
+
|
|
12
|
+
<div class="dropdown-menu p-1">
|
|
13
|
+
<div component="category-selector-search" class="p-1 hidden">
|
|
14
|
+
<input type="text" class="form-control form-control-sm" placeholder="[[search:type-to-search]]" autocomplete="off">
|
|
15
|
+
<hr class="mt-2 mb-0"/>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<ul component="category/list" class="list-unstyled mb-0 text-sm category-dropdown-menu ghost-scrollbar" role="menu">
|
|
19
|
+
<li role="presentation" class="category" data-cid="all">
|
|
20
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" role="menuitem" href="{{{ if allCategoriesUrl }}}{config.relative_path}/{allCategoriesUrl}{{{ else }}}#{{{ end }}}">
|
|
21
|
+
<div class="flex-grow-1">[[unread:all-categories]]</div>
|
|
22
|
+
<i component="category/select/icon" class="flex-shrink-0 fa fa-fw fa-check {{{if selectedCategory}}}invisible{{{end}}}"></i>
|
|
23
|
+
</a>
|
|
24
|
+
</li>
|
|
25
|
+
{{{each categoryItems}}}
|
|
26
|
+
<li role="presentation" class="category {{{ if ./disabledClass }}}disabled{{{ end }}}" data-cid="{./cid}" data-parent-cid="{./parentCid}" data-name="{./name}">
|
|
27
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2 {{{ if ./disabledClass }}}disabled{{{ end }}}" role="menuitem" href="#">
|
|
28
|
+
{./level}
|
|
29
|
+
<span component="category-markup" class="flex-grow-1" style="{{{ if ./match }}}font-weight: bold;{{{end}}}">
|
|
30
|
+
{{{ if ./icon }}}
|
|
31
|
+
<div class="category-item d-inline-flex align-items-center gap-1">
|
|
32
|
+
{buildCategoryIcon(@value, "24px", "rounded-circle")}
|
|
33
|
+
{./name}
|
|
34
|
+
</div>
|
|
35
|
+
{{{ else }}}
|
|
36
|
+
{./name}
|
|
37
|
+
{{{ end }}}
|
|
38
|
+
</span>
|
|
39
|
+
<i component="category/select/icon" class="flex-shrink-0 fa fa-fw fa-check {{{ if !./selected }}}invisible{{{ end }}}"></i>
|
|
40
|
+
</a>
|
|
41
|
+
</li>
|
|
42
|
+
{{{end}}}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<a component="header/avatar" id="user_dropdown" href="#" role="button" class="nav-link d-flex gap-2 align-items-center text-truncate" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="[[user:user-menu]]">
|
|
2
|
+
{buildAvatar(user, "20px", true)}
|
|
3
|
+
<span id="user-header-name" class="nav-text small visible-open fw-semibold">{user.username}</span>
|
|
4
|
+
</a>
|
|
5
|
+
<ul id="user-control-list" component="header/usercontrol" class="overscroll-behavior-contain user-dropdown dropdown-menu shadow p-1 text-sm ff-base" role="menu">
|
|
6
|
+
<li>
|
|
7
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" component="header/profilelink" href="{relative_path}/user/{user.userslug}" role="menuitem" aria-label="[[user:profile]]">
|
|
8
|
+
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status {user.status}"><span class="visually-hidden">[[global:{user.status}]]</span></span>
|
|
9
|
+
<span class="fw-semibold" component="header/username">{user.username}</span>
|
|
10
|
+
</a>
|
|
11
|
+
</li>
|
|
12
|
+
<li role="presentation" class="dropdown-divider"></li>
|
|
13
|
+
<li><h6 class="dropdown-header text-xs">[[global:status]]</h6></li>
|
|
14
|
+
<li>
|
|
15
|
+
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.online }}}selected{{{ end }}}" data-status="online" role="menuitem">
|
|
16
|
+
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status online"></span>
|
|
17
|
+
<span class="flex-grow-1">[[global:online]]</span>
|
|
18
|
+
<i class="fa-solid fa-check text-secondary flex-shrink-0" aria-label="[[global:selected]]"></i>
|
|
19
|
+
</a>
|
|
20
|
+
</li>
|
|
21
|
+
<li>
|
|
22
|
+
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.away }}}selected{{{ end }}}" data-status="away" role="menuitem">
|
|
23
|
+
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status away"></span>
|
|
24
|
+
<span class="flex-grow-1">[[global:away]]</span>
|
|
25
|
+
<i class="fa-solid fa-check text-secondary flex-shrink-0"><span class="visually-hidden">[[global:selected]]</span></i>
|
|
26
|
+
</a>
|
|
27
|
+
</li>
|
|
28
|
+
<li>
|
|
29
|
+
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.dnd }}}selected{{{ end }}}" data-status="dnd" role="menuitem">
|
|
30
|
+
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status dnd"></span>
|
|
31
|
+
<span class="flex-grow-1">[[global:dnd]]</span>
|
|
32
|
+
<i class="fa-solid fa-check text-secondary flex-shrink-0"></i>
|
|
33
|
+
</a>
|
|
34
|
+
</li>
|
|
35
|
+
<li>
|
|
36
|
+
<a href="#" class="dropdown-item rounded-1 user-status d-flex align-items-center gap-2 {{{ if user.offline }}}selected{{{ end }}}" data-status="offline" role="menuitem">
|
|
37
|
+
<span component="user/status" class="flex-shrink-0 border border-white border-2 rounded-circle status offline"></span>
|
|
38
|
+
<span class="flex-grow-1">[[global:invisible]]</span>
|
|
39
|
+
<i class="fa-solid fa-check text-secondary flex-shrink-0"></i>
|
|
40
|
+
</a>
|
|
41
|
+
</li>
|
|
42
|
+
<li role="presentation" class="dropdown-divider"></li>
|
|
43
|
+
<li>
|
|
44
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="{relative_path}/user/{user.userslug}/bookmarks" role="menuitem">
|
|
45
|
+
<i class="fa fa-fw fa-bookmark text-secondary"></i> <span>[[user:bookmarks]]</span>
|
|
46
|
+
</a>
|
|
47
|
+
</li>
|
|
48
|
+
<li>
|
|
49
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" component="header/profilelink/edit" href="{relative_path}/user/{user.userslug}/edit" role="menuitem">
|
|
50
|
+
<i class="fa fa-fw fa-edit text-secondary"></i> <span>[[user:edit-profile]]</span>
|
|
51
|
+
</a>
|
|
52
|
+
</li>
|
|
53
|
+
{{{ if showModMenu }}}
|
|
54
|
+
<li role="presentation" class="dropdown-divider"></li>
|
|
55
|
+
<li><h6 class="dropdown-header text-xs">[[pages:moderator-tools]]</h6></li>
|
|
56
|
+
<li>
|
|
57
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="{relative_path}/flags" role="menuitem">
|
|
58
|
+
<i class="fa fa-fw fa-flag text-secondary"></i> <span>[[pages:flagged-content]]</span>
|
|
59
|
+
</a>
|
|
60
|
+
</li>
|
|
61
|
+
<li>
|
|
62
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="{relative_path}/post-queue" role="menuitem">
|
|
63
|
+
<i class="fa fa-fw fa-list-alt text-secondary"></i> <span>[[pages:post-queue]]</span>
|
|
64
|
+
</a>
|
|
65
|
+
</li>
|
|
66
|
+
{{{ if registrationQueueEnabled }}}
|
|
67
|
+
<li>
|
|
68
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="{relative_path}/registration-queue" role="menuitem">
|
|
69
|
+
<i class="fa fa-fw fa-list-alt text-secondary"></i> <span>[[pages:registration-queue]]</span>
|
|
70
|
+
</a>
|
|
71
|
+
</li>
|
|
72
|
+
{{{ end }}}
|
|
73
|
+
<li>
|
|
74
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="{relative_path}/ip-blacklist" role="menuitem">
|
|
75
|
+
<i class="fa fa-fw fa-ban text-secondary"></i> <span>[[pages:ip-blacklist]]</span>
|
|
76
|
+
</a>
|
|
77
|
+
</li>
|
|
78
|
+
{{{ else }}}
|
|
79
|
+
{{{ if postQueueEnabled }}}
|
|
80
|
+
<li>
|
|
81
|
+
<a class="dropdown-item rounded-1 d-flex align-items-center gap-2" href="{relative_path}/post-queue" role="menuitem">
|
|
82
|
+
<i class="fa fa-fw fa-list-alt text-secondary"></i> <span>[[pages:post-queue]]</span>
|
|
83
|
+
</a>
|
|
84
|
+
</li>
|
|
85
|
+
{{{ end }}}
|
|
86
|
+
{{{ end }}}
|
|
87
|
+
|
|
88
|
+
<li role="presentation" class="dropdown-divider"></li>
|
|
89
|
+
<li component="user/logout">
|
|
90
|
+
<form method="post" action="{relative_path}/logout" role="menuitem">
|
|
91
|
+
<input type="hidden" name="_csrf" value="{config.csrf_token}">
|
|
92
|
+
<input type="hidden" name="noscript" value="true">
|
|
93
|
+
<button type="submit" class="dropdown-item rounded-1 d-flex align-items-center gap-2">
|
|
94
|
+
<i class="fa fa-fw fa-sign-out text-secondary"></i><span>[[global:logout]]</span>
|
|
95
|
+
</button>
|
|
96
|
+
</form>
|
|
97
|
+
</li>
|
|
98
|
+
</ul>
|