Witaj @name! Czas stworzyć Crew Council - pirackie forum dyskusyjne jak Reddit. 🗳️
Piraci głosują nad decyzjami, pomysłami i propozycjami. Nauczysz się budować system głosowania i sortowania treści według popularności.
Twitter/Instagram:
Reddit:
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; // Głos aktualnego użytkownika
10 comments: number;
11 timestamp: Date;
12}Kluczowe pola:
upvotes - liczba głosów "za"downvotes - liczba głosów "przeciw"userVote - co użytkownik zagłosował (up, down, lub null)Reddit używa score = upvotes - downvotes:
1function calculateScore(post: Post): number {
2 return post.upvotes - post.downvotes;
3}
4
5// Przykłady:
6// 100 upvotes, 20 downvotes = 80 score ✅
7// 50 upvotes, 60 downvotes = -10 score ❌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 // Cofnij upvote
25 setUpvotes(upvotes - 1);
26 setUserVote(null);
27 } else if (userVote === 'down') {
28 // Zmień z downvote na upvote
29 setDownvotes(downvotes - 1);
30 setUpvotes(upvotes + 1);
31 setUserVote('up');
32 } else {
33 // Nowy upvote
34 setUpvotes(upvotes + 1);
35 setUserVote('up');
36 }
37 };
38
39 const handleDownvote = () => {
40 if (userVote === 'down') {
41 // Cofnij downvote
42 setDownvotes(downvotes - 1);
43 setUserVote(null);
44 } else if (userVote === 'up') {
45 // Zmień z upvote na downvote
46 setUpvotes(upvotes - 1);
47 setDownvotes(downvotes + 1);
48 setUserVote('down');
49 } else {
50 // Nowy 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}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}Reddit ma różne sposoby sortowania:
1function sortByNew(posts: Post[]): Post[] {
2 return [...posts].sort((a, b) =>
3 b.timestamp.getTime() - a.timestamp.getTime()
4 );
5}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}Reddit używa złożonego algorytmu uwzględniającego score i czas. Uproszczona wersja:
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 // Nowsze posty z dobrym score mają wyższy "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}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 >
55 ⭐ Top
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}Reddit pokazuje procent pozytywnych głosów:
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// Przykład użycia:
8const percentage = calculateUpvotePercentage(post.upvotes, post.downvotes);
9// 85 upvotes, 15 downvotes = 85% upvoted✅ Voting system - upvote/downvote z toggle ✅ Score - upvotes - downvotes ✅ userVote state - śledzenie głosu użytkownika ✅ Vote logic - handle upvote/downvote z 3 stanami ✅ Sorting - Hot, New, Top algorithms ✅ Hot algorithm - score + time decay ✅ Percentage - upvote percentage calculation ✅ Reddit UI - vote buttons + post card
W następnym ćwiczeniu dodamy threaded comments (zagnieżdżone komentarze)!
Do zobaczenia! 🚀