@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okjavis/nodebb-theme-javis",
3
- "version": "3.0.6",
3
+ "version": "3.0.9",
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/_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
- &.vote-up:hover {
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
- &.vote-down:hover {
74
- color: #7193ff; // Reddit periwinkle
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 {
@@ -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
 
@@ -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
- <button class="vote-btn vote-up" title="[[topic:upvote]]">
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
- </button>
21
- <span class="vote-count" title="{./votes}">{humanReadableNumber(./votes, 0)}</span>
22
- <button class="vote-btn vote-down" title="[[topic:downvote]]">
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
- </button>
24
+ </a>
25
25
  </div>
26
26
  {{{ end }}}
27
27