@okjavis/nodebb-theme-javis 3.0.6 → 3.0.9
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 +1 -1
- package/scss/_cards.scss +14 -3
- package/scss/_composer.scss +20 -0
- package/static/lib/theme.js +209 -0
- package/templates/partials/topics_list.tpl +6 -6
package/package.json
CHANGED
package/scss/_cards.scss
CHANGED
|
@@ -61,25 +61,36 @@
|
|
|
61
61
|
cursor: pointer;
|
|
62
62
|
border-radius: $jv-radius-xs;
|
|
63
63
|
transition: color 0.15s ease, background-color 0.15s ease;
|
|
64
|
+
text-decoration: none;
|
|
64
65
|
|
|
65
66
|
&:hover {
|
|
66
67
|
background: rgba(0, 0, 0, 0.06);
|
|
68
|
+
text-decoration: none;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
// Upvote - orange/red on hover and when active
|
|
72
|
+
&.vote-up:hover,
|
|
73
|
+
&.vote-up.upvoted {
|
|
70
74
|
color: #ff4500; // Reddit orange-red
|
|
75
|
+
background: rgba(255, 69, 0, 0.1);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
// Downvote - blue/periwinkle on hover and when active
|
|
79
|
+
&.vote-down:hover,
|
|
80
|
+
&.vote-down.downvoted {
|
|
81
|
+
color: #7193ff; // Reddit periwinkle/blue
|
|
82
|
+
background: rgba(113, 147, 255, 0.1);
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
// Legacy active class support
|
|
77
86
|
&.active.vote-up {
|
|
78
87
|
color: #ff4500;
|
|
88
|
+
background: rgba(255, 69, 0, 0.1);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
&.active.vote-down {
|
|
82
92
|
color: #7193ff;
|
|
93
|
+
background: rgba(113, 147, 255, 0.1);
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
i {
|
package/scss/_composer.scss
CHANGED
|
@@ -414,10 +414,14 @@ html.composing .composer .formatting-group {
|
|
|
414
414
|
gap: 2px !important;
|
|
415
415
|
flex-wrap: nowrap !important;
|
|
416
416
|
overflow-x: auto !important;
|
|
417
|
+
overflow-y: visible !important; // Allow badges to overflow vertically
|
|
418
|
+
padding-top: 8px !important; // Space for badges that overflow upward
|
|
417
419
|
|
|
418
420
|
li {
|
|
419
421
|
flex-shrink: 0 !important;
|
|
420
422
|
list-style: none !important;
|
|
423
|
+
position: relative !important; // For badge positioning
|
|
424
|
+
overflow: visible !important; // Allow badges to show
|
|
421
425
|
|
|
422
426
|
.btn {
|
|
423
427
|
color: $jv-text-muted !important;
|
|
@@ -430,11 +434,27 @@ html.composing .composer .formatting-group {
|
|
|
430
434
|
justify-content: center !important;
|
|
431
435
|
background: transparent !important;
|
|
432
436
|
border: none !important;
|
|
437
|
+
position: relative !important; // For badge positioning
|
|
438
|
+
overflow: visible !important; // Allow badges to show
|
|
433
439
|
|
|
434
440
|
&:hover {
|
|
435
441
|
background: $jv-hover-bg !important;
|
|
436
442
|
color: $jv-text-main !important;
|
|
437
443
|
}
|
|
444
|
+
|
|
445
|
+
// Badge on buttons (like thumbnail count)
|
|
446
|
+
.badge {
|
|
447
|
+
position: absolute !important;
|
|
448
|
+
top: -4px !important;
|
|
449
|
+
right: -4px !important;
|
|
450
|
+
transform: none !important;
|
|
451
|
+
left: auto !important;
|
|
452
|
+
font-size: 10px !important;
|
|
453
|
+
min-width: 16px !important;
|
|
454
|
+
height: 16px !important;
|
|
455
|
+
padding: 0 4px !important;
|
|
456
|
+
line-height: 16px !important;
|
|
457
|
+
}
|
|
438
458
|
}
|
|
439
459
|
}
|
|
440
460
|
|
package/static/lib/theme.js
CHANGED
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
// Initialize click handler for composer prompt card (rendered server-side in feed.tpl)
|
|
34
34
|
initFeedComposerPromptHandler();
|
|
35
35
|
|
|
36
|
+
// Initialize voting handlers for category/topics listing pages
|
|
37
|
+
initTopicListVoting();
|
|
38
|
+
|
|
39
|
+
// Initialize immediate category filter navigation on feed page
|
|
40
|
+
initFeedCategoryFilter();
|
|
41
|
+
|
|
36
42
|
// Re-initialize carousels when new posts are loaded (infinite scroll, etc.)
|
|
37
43
|
// Also handle post edits by clearing the processed flag
|
|
38
44
|
$(window).on('action:posts.loaded action:topic.loaded action:ajaxify.end', function() {
|
|
@@ -40,6 +46,8 @@
|
|
|
40
46
|
initParentPostNavigation();
|
|
41
47
|
initPostHoverActions();
|
|
42
48
|
initFeedComposerPromptHandler();
|
|
49
|
+
initTopicListVoting();
|
|
50
|
+
initFeedCategoryFilter();
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
// Handle post edits - need to re-process the edited post
|
|
@@ -497,4 +505,205 @@
|
|
|
497
505
|
});
|
|
498
506
|
}
|
|
499
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Initialize voting handlers for topic listing pages (category, recent, etc.)
|
|
510
|
+
* NodeBB's native voting only works on topic detail pages, so we add custom handlers here
|
|
511
|
+
*/
|
|
512
|
+
function initTopicListVoting() {
|
|
513
|
+
// Only run on pages with topic listings (not on topic detail page)
|
|
514
|
+
if ($('[component="topic"]').length) {
|
|
515
|
+
// We're on a topic detail page, NodeBB handles voting natively
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Find vote columns that haven't been initialized
|
|
520
|
+
$('.vote-column:not([data-vote-initialized])').each(function() {
|
|
521
|
+
var $voteColumn = $(this);
|
|
522
|
+
$voteColumn.attr('data-vote-initialized', 'true');
|
|
523
|
+
|
|
524
|
+
var pid = $voteColumn.attr('data-pid');
|
|
525
|
+
if (!pid) return;
|
|
526
|
+
|
|
527
|
+
var $upvoteBtn = $voteColumn.find('[component="post/upvote"]');
|
|
528
|
+
var $downvoteBtn = $voteColumn.find('[component="post/downvote"]');
|
|
529
|
+
var $voteCount = $voteColumn.find('[component="post/vote-count"]');
|
|
530
|
+
|
|
531
|
+
// Upvote click handler
|
|
532
|
+
$upvoteBtn.on('click', function(e) {
|
|
533
|
+
e.preventDefault();
|
|
534
|
+
e.stopPropagation();
|
|
535
|
+
|
|
536
|
+
if (!config.loggedIn) {
|
|
537
|
+
window.location.href = config.relative_path + '/login';
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
var isUpvoted = $upvoteBtn.hasClass('upvoted');
|
|
542
|
+
var method = isUpvoted ? 'del' : 'put';
|
|
543
|
+
|
|
544
|
+
$.ajax({
|
|
545
|
+
url: config.relative_path + '/api/v3/posts/' + pid + '/vote',
|
|
546
|
+
method: method,
|
|
547
|
+
data: JSON.stringify({ delta: 1 }),
|
|
548
|
+
contentType: 'application/json',
|
|
549
|
+
headers: {
|
|
550
|
+
'x-csrf-token': config.csrf_token
|
|
551
|
+
},
|
|
552
|
+
success: function(response) {
|
|
553
|
+
// Toggle upvoted state
|
|
554
|
+
$upvoteBtn.toggleClass('upvoted');
|
|
555
|
+
// Remove downvoted if it was set
|
|
556
|
+
$downvoteBtn.removeClass('downvoted');
|
|
557
|
+
|
|
558
|
+
// Update vote count - handle various response structures
|
|
559
|
+
var votes = null;
|
|
560
|
+
if (response) {
|
|
561
|
+
if (response.response && response.response.post) {
|
|
562
|
+
votes = response.response.post.votes;
|
|
563
|
+
} else if (response.response && response.response.votes !== undefined) {
|
|
564
|
+
votes = response.response.votes;
|
|
565
|
+
} else if (response.post && response.post.votes !== undefined) {
|
|
566
|
+
votes = response.post.votes;
|
|
567
|
+
} else if (response.votes !== undefined) {
|
|
568
|
+
votes = response.votes;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (votes !== null) {
|
|
573
|
+
$voteCount.text(votes);
|
|
574
|
+
$voteCount.attr('data-votes', votes);
|
|
575
|
+
$voteCount.attr('title', votes);
|
|
576
|
+
} else {
|
|
577
|
+
// Fallback: manually calculate from current state
|
|
578
|
+
var currentVotes = parseInt($voteCount.attr('data-votes') || $voteCount.text()) || 0;
|
|
579
|
+
var wasUpvoted = !$upvoteBtn.hasClass('upvoted'); // toggled already
|
|
580
|
+
votes = wasUpvoted ? currentVotes - 1 : currentVotes + 1;
|
|
581
|
+
$voteCount.text(votes);
|
|
582
|
+
$voteCount.attr('data-votes', votes);
|
|
583
|
+
$voteCount.attr('title', votes);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
error: function(xhr) {
|
|
587
|
+
var msg = xhr.responseJSON && xhr.responseJSON.status && xhr.responseJSON.status.message;
|
|
588
|
+
if (msg) {
|
|
589
|
+
alert(msg);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Downvote click handler
|
|
596
|
+
$downvoteBtn.on('click', function(e) {
|
|
597
|
+
e.preventDefault();
|
|
598
|
+
e.stopPropagation();
|
|
599
|
+
|
|
600
|
+
if (!config.loggedIn) {
|
|
601
|
+
window.location.href = config.relative_path + '/login';
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
var isDownvoted = $downvoteBtn.hasClass('downvoted');
|
|
606
|
+
var method = isDownvoted ? 'del' : 'put';
|
|
607
|
+
|
|
608
|
+
$.ajax({
|
|
609
|
+
url: config.relative_path + '/api/v3/posts/' + pid + '/vote',
|
|
610
|
+
method: method,
|
|
611
|
+
data: JSON.stringify({ delta: -1 }),
|
|
612
|
+
contentType: 'application/json',
|
|
613
|
+
headers: {
|
|
614
|
+
'x-csrf-token': config.csrf_token
|
|
615
|
+
},
|
|
616
|
+
success: function(response) {
|
|
617
|
+
// Toggle downvoted state
|
|
618
|
+
$downvoteBtn.toggleClass('downvoted');
|
|
619
|
+
// Remove upvoted if it was set
|
|
620
|
+
$upvoteBtn.removeClass('upvoted');
|
|
621
|
+
|
|
622
|
+
// Update vote count - handle various response structures
|
|
623
|
+
var votes = null;
|
|
624
|
+
if (response) {
|
|
625
|
+
if (response.response && response.response.post) {
|
|
626
|
+
votes = response.response.post.votes;
|
|
627
|
+
} else if (response.response && response.response.votes !== undefined) {
|
|
628
|
+
votes = response.response.votes;
|
|
629
|
+
} else if (response.post && response.post.votes !== undefined) {
|
|
630
|
+
votes = response.post.votes;
|
|
631
|
+
} else if (response.votes !== undefined) {
|
|
632
|
+
votes = response.votes;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (votes !== null) {
|
|
637
|
+
$voteCount.text(votes);
|
|
638
|
+
$voteCount.attr('data-votes', votes);
|
|
639
|
+
$voteCount.attr('title', votes);
|
|
640
|
+
} else {
|
|
641
|
+
// Fallback: manually calculate from current state
|
|
642
|
+
var currentVotes = parseInt($voteCount.attr('data-votes') || $voteCount.text()) || 0;
|
|
643
|
+
var wasDownvoted = !$downvoteBtn.hasClass('downvoted'); // toggled already
|
|
644
|
+
votes = wasDownvoted ? currentVotes + 1 : currentVotes - 1;
|
|
645
|
+
$voteCount.text(votes);
|
|
646
|
+
$voteCount.attr('data-votes', votes);
|
|
647
|
+
$voteCount.attr('title', votes);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
error: function(xhr) {
|
|
651
|
+
var msg = xhr.responseJSON && xhr.responseJSON.status && xhr.responseJSON.status.message;
|
|
652
|
+
if (msg) {
|
|
653
|
+
alert(msg);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Initialize immediate category filter navigation on feed page
|
|
663
|
+
* When a category is selected, navigate immediately instead of waiting for dropdown close
|
|
664
|
+
*/
|
|
665
|
+
function initFeedCategoryFilter() {
|
|
666
|
+
// Only run on feed page
|
|
667
|
+
if (!$('.feed').length) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
var $categoryDropdown = $('.feed-category-filter [component="category/dropdown"]');
|
|
672
|
+
if (!$categoryDropdown.length || $categoryDropdown.attr('data-filter-initialized')) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
$categoryDropdown.attr('data-filter-initialized', 'true');
|
|
676
|
+
|
|
677
|
+
// Handle category selection - navigate immediately
|
|
678
|
+
$categoryDropdown.on('click', '[component="category/list"] [data-cid]', function(e) {
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
e.stopPropagation();
|
|
681
|
+
|
|
682
|
+
var $item = $(this);
|
|
683
|
+
var cid = $item.attr('data-cid');
|
|
684
|
+
var currentParams = utils.params();
|
|
685
|
+
|
|
686
|
+
// Build the new URL
|
|
687
|
+
if (cid === 'all') {
|
|
688
|
+
delete currentParams.cid;
|
|
689
|
+
} else {
|
|
690
|
+
currentParams.cid = cid;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Remove page parameter to start from first page
|
|
694
|
+
delete currentParams.page;
|
|
695
|
+
|
|
696
|
+
var url = '/feed';
|
|
697
|
+
if (Object.keys(currentParams).length) {
|
|
698
|
+
url += '?' + $.param(currentParams);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Close the dropdown
|
|
702
|
+
$categoryDropdown.find('.dropdown-toggle').dropdown('hide');
|
|
703
|
+
|
|
704
|
+
// Navigate to the filtered feed
|
|
705
|
+
ajaxify.go(url);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
500
709
|
})();
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
<!-- Vote Column (Reddit-style) -->
|
|
16
16
|
{{{ if !reputation:disabled }}}
|
|
17
|
-
<div class="vote-column">
|
|
18
|
-
<
|
|
17
|
+
<div class="vote-column" data-pid="{./mainPid}">
|
|
18
|
+
<a component="post/upvote" href="#" class="vote-btn vote-up{{{ if ./upvoted }}} upvoted{{{ end }}}" title="[[topic:upvote]]">
|
|
19
19
|
<i class="fa fa-chevron-up"></i>
|
|
20
|
-
</
|
|
21
|
-
<span class="vote-count" title="{./votes}">{humanReadableNumber(./votes, 0)}</span>
|
|
22
|
-
<
|
|
20
|
+
</a>
|
|
21
|
+
<span component="post/vote-count" class="vote-count" data-votes="{./votes}" title="{./votes}">{humanReadableNumber(./votes, 0)}</span>
|
|
22
|
+
<a component="post/downvote" href="#" class="vote-btn vote-down{{{ if ./downvoted }}} downvoted{{{ end }}}" title="[[topic:downvote]]">
|
|
23
23
|
<i class="fa fa-chevron-down"></i>
|
|
24
|
-
</
|
|
24
|
+
</a>
|
|
25
25
|
</div>
|
|
26
26
|
{{{ end }}}
|
|
27
27
|
|