Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Threaded Comments - Zagnieżdżone komentarze

Reddit ma threaded comments - komentarze mogą mieć odpowiedzi, które z kolei mają swoje odpowiedzi. To hierarchiczna struktura drzewa.

Struktura danych - Rekurencyjna

1interface Comment {
2  id: number;
3  author: string;
4  content: string;
5  upvotes: number;
6  downvotes: number;
7  userVote: 'up' | 'down' | null;
8  timestamp: Date;
9  replies: Comment[]; // 🔑 Rekurencja - komentarze zawierają komentarze!
10}

Kluczowa koncepcja:

replies
to tablica komentarzy, które same mogą mieć
replies
.

Przykład danych:

1const exampleComment: Comment = {
2  id: 1,
3  author: 'Redbeard',
4  content: 'Znalazłem mapę do wyspy skarbów!',
5  upvotes: 42,
6  downvotes: 3,
7  userVote: null,
8  timestamp: new Date(),
9  replies: [
10    {
11      id: 2,
12      author: 'Morgan',
13      content: 'Gdzie dokładnie?',
14      upvotes: 15,
15      downvotes: 0,
16      userVote: null,
17      timestamp: new Date(),
18      replies: [
19        {
20          id: 3,
21          author: 'Redbeard',
22          content: 'Na północ od Tortuga!',
23          upvotes: 8,
24          downvotes: 0,
25          userVote: null,
26          timestamp: new Date(),
27          replies: [], // Brak dalszych odpowiedzi
28        },
29      ],
30    },
31  ],
32};

Wizualizacja:

1Comment 1 (Redbeard)
2└── Comment 2 (Morgan)
3    └── Comment 3 (Redbeard)

Recursive Component - Renderowanie drzewa

Aby wyświetlić zagnieżdżone komentarze, komponent musi renderować sam siebie:

1'use client';
2
3import { useState } from 'react';
4import VoteButtons from './VoteButtons';
5
6interface CommentProps {
7  comment: Comment;
8  depth?: number; // Głębokość zagnieżdżenia (dla wcięć)
9}
10
11export default function CommentThread({ comment, depth = 0 }: CommentProps) {
12  const [isCollapsed, setIsCollapsed] = useState(false);
13  const [showReplyForm, setShowReplyForm] = useState(false);
14
15  const score = comment.upvotes - comment.downvotes;
16
17  return (
18    <div className={`${depth > 0 ? 'ml-8 border-l-2 border-gray-200 pl-4' : ''}`}>
19      {/* Comment Header */}
20      <div className="flex gap-3 py-3">
21        <VoteButtons
22          initialUpvotes={comment.upvotes}
23          initialDownvotes={comment.downvotes}
24          initialUserVote={comment.userVote}
25        />
26
27        <div className="flex-1">
28          {/* Author & Time */}
29          <div className="flex items-center gap-2 text-xs text-gray-600 mb-1">
30            <span className="font-bold text-gray-900">{comment.author}</span>
31            <span></span>
32            <span>{formatTimeAgo(comment.timestamp)}</span>
33            {isCollapsed && (
34              <span className="text-blue-500">
35                [{comment.replies.length} children]
36              </span>
37            )}
38          </div>
39
40          {/* Content */}
41          {!isCollapsed && (
42            <>
43              <p className="text-gray-800 text-sm mb-2">{comment.content}</p>
44
45              {/* Actions */}
46              <div className="flex items-center gap-3 text-xs text-gray-500">
47                <button
48                  onClick={() => setShowReplyForm(!showReplyForm)}
49                  className="hover:underline"
50                >
51                  💬 Reply
52                </button>
53                <button className="hover:underline">🔗 Share</button>
54                <button className="hover:underline">🏴 Report</button>
55                {comment.replies.length > 0 && (
56                  <button
57                    onClick={() => setIsCollapsed(true)}
58                    className="hover:underline"
59                  >
60                    [] Collapse
61                  </button>
62                )}
63              </div>
64
65              {/* Reply Form */}
66              {showReplyForm && (
67                <div className="mt-3 bg-gray-50 p-3 rounded">
68                  <textarea
69                    placeholder="Napisz swoją odpowiedź..."
70                    className="w-full p-2 border rounded text-sm"
71                    rows={3}
72                  />
73                  <div className="flex gap-2 mt-2">
74                    <button className="px-3 py-1 bg-blue-500 text-white rounded text-sm">
75                      Reply
76                    </button>
77                    <button
78                      onClick={() => setShowReplyForm(false)}
79                      className="px-3 py-1 bg-gray-200 rounded text-sm"
80                    >
81                      Cancel
82                    </button>
83                  </div>
84                </div>
85              )}
86            </>
87          )}
88
89          {/* Collapsed State */}
90          {isCollapsed && (
91            <button
92              onClick={() => setIsCollapsed(false)}
93              className="text-xs text-blue-500 hover:underline"
94            >
95              [+] Expand
96            </button>
97          )}
98        </div>
99      </div>
100
101      {/* 🔑 REKURENCJA - Renderuj replies używając tego samego komponentu */}
102      {!isCollapsed && comment.replies.length > 0 && (
103        <div className="mt-2">
104          {comment.replies.map(reply => (
105            <CommentThread
106              key={reply.id}
107              comment={reply}
108              depth={depth + 1} // Zwiększ głębokość dla wcięć
109            />
110          ))}
111        </div>
112      )}
113    </div>
114  );
115}

Depth Limiting - Ograniczenie głębokości

Reddit ogranicza głębokość do ~10 poziomów i dodaje "Continue this thread →":

1const MAX_DEPTH = 10;
2
3export default function CommentThread({ comment, depth = 0 }: CommentProps) {
4  // ... kod
5  // Jeśli głębokość zbyt duża, pokaż link zamiast renderować replies
6  if (depth >= MAX_DEPTH && comment.replies.length > 0) {
7    return (
8      <div className="ml-8 pl-4 border-l-2 border-gray-200">
9        <a
10          href={`/comment/${comment.id}`}
11          className="text-blue-500 text-sm hover:underline"
12        >
13          Continue this thread →
14        </a>
15      </div>
16    );
17  }
18
19  // Reszta kodu...
20}

Sorting Replies

Komentarze też mogą być sortowane:

1function sortComments(comments: Comment[], sortMode: 'best' | 'new' | 'top'): Comment[] {
2  const sorted = [...comments];
3
4  switch (sortMode) {
5    case 'best':
6      // Wilson score algorithm (uproszczony)
7      sorted.sort((a, b) => {
8        const scoreA = a.upvotes - a.downvotes;
9        const scoreB = b.upvotes - b.downvotes;
10        return scoreB - scoreA;
11      });
12      break;
13    case 'new':
14      sorted.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
15      break;
16    case 'top':
17      sorted.sort((a, b) => {
18        const scoreA = a.upvotes - a.downvotes;
19        const scoreB = b.upvotes - b.downvotes;
20        return scoreB - scoreA;
21      });
22      break;
23  }
24
25  // Rekurencyjnie sortuj replies
26  return sorted.map(comment => ({
27    ...comment,
28    replies: sortComments(comment.replies, sortMode),
29  }));
30}

Adding Replies - Dodawanie odpowiedzi

Aby dodać odpowiedź do konkretnego komentarza w drzewie, potrzebujemy rekurencyjnej funkcji:

1function addReplyToComment(
2  comments: Comment[],
3  parentId: number,
4  newReply: Comment
5): Comment[] {
6  return comments.map(comment => {
7    if (comment.id === parentId) {
8      // Znaleziono rodzica - dodaj reply
9      return {
10        ...comment,
11        replies: [...comment.replies, newReply],
12      };
13    }
14
15    // Szukaj głębiej w replies
16    return {
17      ...comment,
18      replies: addReplyToComment(comment.replies, parentId, newReply),
19    };
20  });
21}
22
23// Użycie:
24const [comments, setComments] = useState<Comment[]>(initialComments);
25
26const handleAddReply = (parentId: number, content: string) => {
27  const newReply: Comment = {
28    id: Date.now(),
29    author: 'You',
30    content,
31    upvotes: 1,
32    downvotes: 0,
33    userVote: null,
34    timestamp: new Date(),
35    replies: [],
36  };
37
38  setComments(addReplyToComment(comments, parentId, newReply));
39};

Comment Count - Liczenie wszystkich komentarzy

Rekurencyjna funkcja do liczenia komentarzy z replies:

1function countTotalComments(comments: Comment[]): number {
2  return comments.reduce((total, comment) => {
3    return total + 1 + countTotalComments(comment.replies);
4  }, 0);
5}
6
7// Przykład:
8const total = countTotalComments(post.comments);
9// "142 comments"

Highlighting New Comments

Reddit podświetla nowe komentarze (od ostatniej wizyty):

1interface Comment {
2  // ... inne pola
3  isNew?: boolean; // Dodany od ostatniej wizyty
4}
5
6// W komponencie:
7<div className={`${comment.isNew ? 'bg-blue-50 border-l-4 border-l-blue-500' : ''}`}>
8  {/* Treść komentarza */}
9</div>

Podsumowanie 🎓

Recursive data - Comment zawiera replies: Comment[] ✅ Recursive component - renderuje sam siebie dla replies ✅ Depth tracking - wcięcia bazowane na głębokości ✅ Collapse/Expand - zwijanie wątków ✅ Depth limiting - "Continue thread" dla głębokich wątków ✅ Sorting replies - rekurencyjne sortowanie ✅ Adding replies - rekurencyjna funkcja update ✅ Comment count - rekurencyjne liczenie ✅ Visual hierarchy - border-l, margin-left

W następnym ćwiczeniu stworzymy Communities (subreddity)!

Do zobaczenia! 🚀

Przejdź do CodeWorlds