We use cookies to enhance your experience on the site
CodeWorlds

Voting System - Upvotes and Downvotes

Welcome @name! Time to build Crew Council - a pirate discussion forum like Reddit. 🗳️

Pirates vote on decisions, ideas, and proposals. You'll learn to build a voting system and sort content by popularity.

How Does Reddit Differ from Twitter/Instagram?

Twitter/Instagram:

  • Chronological feed (newest on top)
  • Likes (positive only)
  • Flat comment structure

Reddit:

  • Sorted by votes (most popular on top)
  • Upvote ⬆️ and Downvote ⬇️ (positive and negative)
  • Threaded comments (nested replies)
  • Communities (subreddits)

Voting System - Data Structure

1interface Post {
2  id: number;
3  title: string;
4  content: string;
5  author: string;
6  community: string;
7  upvotes: number;
8  downvotes: number;
9  userVote: 'up' | 'down' | null; // Current user's vote
10  comments: number;
11  timestamp: Date;
12}

Key fields:

  • upvotes
    - number of "for" votes
  • downvotes
    - number of "against" votes
  • userVote
    - what the user voted (up, down, or null)

Score Calculation

Reddit uses score = upvotes - downvotes:

1function calculateScore(post: Post): number {
2  return post.upvotes - post.downvotes;
3}
4
5// Examples:
6// 100 upvotes, 20 downvotes = 80 score ✅
7// 50 upvotes, 60 downvotes = -10 score ❌

Vote Component - Interactive Voting System

1'use client';
2
3import { useState } from 'react';
4
5interface VoteProps {
6  initialUpvotes: number;
7  initialDownvotes: number;
8  initialUserVote: 'up' | 'down' | null;
9}
10
11export default function VoteButtons({
12  initialUpvotes,
13  initialDownvotes,
14  initialUserVote
15}: VoteProps) {
16  const [upvotes, setUpvotes] = useState(initialUpvotes);
17  const [downvotes, setDownvotes] = useState(initialDownvotes);
18  const [userVote, setUserVote] = useState<'up' | 'down' | null>(initialUserVote);
19
20  const score = upvotes - downvotes;
21
22  const handleUpvote = () => {
23    if (userVote === 'up') {
24      // Undo upvote
25      setUpvotes(upvotes - 1);
26      setUserVote(null);
27    } else if (userVote === 'down') {
28      // Switch from downvote to upvote
29      setDownvotes(downvotes - 1);
30      setUpvotes(upvotes + 1);
31      setUserVote('up');
32    } else {
33      // New upvote
34      setUpvotes(upvotes + 1);
35      setUserVote('up');
36    }
37  };
38
39  const handleDownvote = () => {
40    if (userVote === 'down') {
41      // Undo downvote
42      setDownvotes(downvotes - 1);
43      setUserVote(null);
44    } else if (userVote === 'up') {
45      // Switch from upvote to downvote
46      setUpvotes(upvotes - 1);
47      setDownvotes(downvotes + 1);
48      setUserVote('down');
49    } else {
50      // New downvote
51      setDownvotes(downvotes + 1);
52      setUserVote('down');
53    }
54  };
55
56  return (
57    <div className="flex flex-col items-center gap-1">
58      <button
59        onClick={handleUpvote}
60        className={`text-2xl transition-colors ${
61          userVote === 'up' ? 'text-orange-500' : 'text-gray-400 hover:text-orange-500'
62        }`}
63      >
64        ⬆️
65      </button>
66
67      <span className={`font-bold text-sm ${
68        score > 0 ? 'text-orange-500' : score < 0 ? 'text-blue-500' : 'text-gray-600'
69      }`}>
70        {score}
71      </span>
72
73      <button
74        onClick={handleDownvote}
75        className={`text-2xl transition-colors ${
76          userVote === 'down' ? 'text-blue-500' : 'text-gray-400 hover:text-blue-500'
77        }`}
78      >
79        ⬇️
80      </button>
81    </div>
82  );
83}

Reddit Post Card

1'use client';
2
3import VoteButtons from './VoteButtons';
4
5interface PostCardProps {
6  post: Post;
7}
8
9export default function PostCard({ post }: PostCardProps) {
10  const score = post.upvotes - post.downvotes;
11
12  return (
13    <div className="bg-white rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200">
14      <div className="flex gap-3 p-4">
15        {/* Vote Section */}
16        <VoteButtons
17          initialUpvotes={post.upvotes}
18          initialDownvotes={post.downvotes}
19          initialUserVote={post.userVote}
20        />
21
22        {/* Content Section */}
23        <div className="flex-1">
24          {/* Community & Author */}
25          <div className="flex items-center gap-2 text-xs text-gray-600 mb-2">
26            <span className="font-bold text-gray-900">r/{post.community}</span>
27            <span></span>
28            <span>Posted by u/{post.author}</span>
29            <span></span>
30            <span>{formatTimeAgo(post.timestamp)}</span>
31          </div>
32
33          {/* Title */}
34          <h2 className="text-xl font-bold text-gray-900 mb-2 hover:underline cursor-pointer">
35            {post.title}
36          </h2>
37
38          {/* Content Preview */}
39          <p className="text-gray-700 text-sm mb-3 line-clamp-3">
40            {post.content}
41          </p>
42
43          {/* Actions */}
44          <div className="flex items-center gap-4 text-gray-500 text-sm">
45            <button className="flex items-center gap-1 hover:bg-gray-100 px-2 py-1 rounded">
46              💬 {post.comments} Comments
47            </button>
48            <button className="flex items-center gap-1 hover:bg-gray-100 px-2 py-1 rounded">
49              🔗 Share
50            </button>
51            <button className="flex items-center gap-1 hover:bg-gray-100 px-2 py-1 rounded">
52              🔖 Save
53            </button>
54          </div>
55        </div>
56      </div>
57    </div>
58  );
59}
60
61function formatTimeAgo(date: Date): string {
62  const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
63
64  if (seconds < 60) return `${seconds}s ago`;
65  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
66  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
67  return `${Math.floor(seconds / 86400)}d ago`;
68}

Sorting Posts - Hot, New, Top

Reddit has different ways to sort:

1. New (Newest):

1function sortByNew(posts: Post[]): Post[] {
2  return [...posts].sort((a, b) =>
3    b.timestamp.getTime() - a.timestamp.getTime()
4  );
5}

2. Top (Best by score):

1function sortByTop(posts: Post[]): Post[] {
2  return [...posts].sort((a, b) => {
3    const scoreA = a.upvotes - a.downvotes;
4    const scoreB = b.upvotes - b.downvotes;
5    return scoreB - scoreA;
6  });
7}

3. Hot (Popularity algorithm):

Reddit uses a complex algorithm accounting for score and time. Simplified version:

1function calculateHotScore(post: Post): number {
2  const score = post.upvotes - post.downvotes;
3  const hoursSincePost = (new Date().getTime() - post.timestamp.getTime()) / (1000 * 60 * 60);
4
5  // Newer posts with good score have a higher "hot score"
6  return score / Math.pow(hoursSincePost + 2, 1.5);
7}
8
9function sortByHot(posts: Post[]): Post[] {
10  return [...posts].sort((a, b) =>
11    calculateHotScore(b) - calculateHotScore(a)
12  );
13}

Feed Component with Sorting

1'use client';
2
3import { useState } from 'react';
4import PostCard from './PostCard';
5
6type SortMode = 'hot' | 'new' | 'top';
7
8export default function Feed({ initialPosts }: { initialPosts: Post[] }) {
9  const [sortMode, setSortMode] = useState<SortMode>('hot');
10
11  const sortedPosts = () => {
12    switch (sortMode) {
13      case 'new':
14        return sortByNew(initialPosts);
15      case 'top':
16        return sortByTop(initialPosts);
17      case 'hot':
18      default:
19        return sortByHot(initialPosts);
20    }
21  };
22
23  return (
24    <div className="max-w-4xl mx-auto p-4">
25      {/* Sort Tabs */}
26      <div className="flex gap-2 mb-4 bg-white rounded-lg p-2 shadow">
27        <button
28          onClick={() => setSortMode('hot')}
29          className={`px-4 py-2 rounded transition ${
30            sortMode === 'hot'
31              ? 'bg-orange-500 text-white'
32              : 'hover:bg-gray-100 text-gray-700'
33          }`}
34        >
35          🔥 Hot
36        </button>
37        <button
38          onClick={() => setSortMode('new')}
39          className={`px-4 py-2 rounded transition ${
40            sortMode === 'new'
41              ? 'bg-orange-500 text-white'
42              : 'hover:bg-gray-100 text-gray-700'
43          }`}
44        >
45          🆕 New
46        </button>
47        <button
48          onClick={() => setSortMode('top')}
49          className={`px-4 py-2 rounded transition ${
50            sortMode === 'top'
51              ? 'bg-orange-500 text-white'
52              : 'hover:bg-gray-100 text-gray-700'
53          }`}
54        >
55Top
56        </button>
57      </div>
58
59      {/* Posts */}
60      <div className="space-y-3">
61        {sortedPosts().map(post => (
62          <PostCard key={post.id} post={post} />
63        ))}
64      </div>
65    </div>
66  );
67}

Percentage Upvoted

Reddit shows the percentage of positive votes:

1function calculateUpvotePercentage(upvotes: number, downvotes: number): number {
2  const total = upvotes + downvotes;
3  if (total === 0) return 0;
4  return Math.round((upvotes / total) * 100);
5}
6
7// Example usage:
8const percentage = calculateUpvotePercentage(post.upvotes, post.downvotes);
9// 85 upvotes, 15 downvotes = 85% upvoted

Summary 🎓

Voting system - upvote/downvote with toggle ✅ Score - upvotes - downvotes ✅ userVote state - tracking user's vote ✅ Vote logic - handle upvote/downvote with 3 states ✅ Sorting - Hot, New, Top algorithms ✅ Hot algorithm - score + time decay ✅ Percentage - upvote percentage calculation ✅ Reddit UI - vote buttons + post card

In the next exercise we'll add threaded comments (nested replies)!

See you there! 🚀

Go to CodeWorlds