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

Publications i Content Curation

Medium ma publications - multi-author blogs z editorial teams.

Publication Structure

1interface Publication {
2  id: string;
3  name: string;
4  slug: string;
5  description: string;
6  logo: string;
7  banner?: string;
8
9  // Team
10  editors: Editor[];
11  writers: Writer[];
12  followers: number;
13
14  // Content
15  articles: Article[];
16  topics: string[];
17
18  // Settings
19  isAcceptingSubmissions: boolean;
20  submissionGuidelines?: string;
21
22  createdAt: Date;
23}
24
25interface Editor {
26  userId: string;
27  role: 'owner' | 'editor' | 'writer';
28  permissions: {
29    canPublish: boolean;
30    canEdit: boolean;
31    canInvite: boolean;
32    canManageSettings: boolean;
33  };
34  joinedAt: Date;
35}
36
37interface SubmissionRequest {
38  id: string;
39  articleId: string;
40  publicationId: string;
41  author: Author;
42  status: 'pending' | 'approved' | 'rejected';
43  note?: string;
44  submittedAt: Date;
45  reviewedAt?: Date;
46  reviewedBy?: string;
47}

Publication Page

1'use client';
2
3interface PublicationPageProps {
4  publication: Publication;
5  articles: Article[];
6  isFollowing: boolean;
7}
8
9export default function PublicationPage({
10  publication,
11  articles,
12  isFollowing
13}: PublicationPageProps) {
14  const [activeTab, setActiveTab] = useState<'latest' | 'top' | 'about'>('latest');
15
16  return (
17    <div className="min-h-screen bg-white">
18      {/* Header */}
19      <div className="border-b border-gray-200">
20        <div className="max-w-5xl mx-auto px-8 py-12">
21          <div className="flex items-start gap-6">
22            <img
23              src={publication.logo}
24              alt={publication.name}
25              className="w-24 h-24 rounded"
26            />
27            <div className="flex-1">
28              <h1 className="text-4xl font-bold mb-2">{publication.name}</h1>
29              <p className="text-xl text-gray-600 mb-4">
30                {publication.description}
31              </p>
32              <div className="flex items-center gap-4 text-sm text-gray-500">
33                <span>{publication.followers.toLocaleString()} Followers</span>
34                <span></span>
35                <span>{publication.articles.length} Stories</span>
36              </div>
37            </div>
38            <button
39              className={`
40                px-6 py-2 rounded-full font-semibold
41                ${isFollowing
42                  ? 'border border-green-600 text-green-600 hover:bg-green-50'
43                  : 'bg-green-600 text-white hover:bg-green-700'
44                }
45              `}
46            >
47              {isFollowing ? 'Following' : 'Follow'}
48            </button>
49          </div>
50        </div>
51
52        {/* Navigation */}
53        <div className="max-w-5xl mx-auto px-8">
54          <div className="flex gap-8 border-b border-gray-200">
55            {[
56              { key: 'latest', label: 'Latest' },
57              { key: 'top', label: 'Top' },
58              { key: 'about', label: 'About' }
59            ].map(tab => (
60              <button
61                key={tab.key}
62                onClick={() => setActiveTab(tab.key as any)}
63                className={`
64                  pb-3 font-semibold
65                  ${activeTab === tab.key
66                    ? 'border-b-2 border-black'
67                    : 'text-gray-500 hover:text-black'
68                  }
69                `}
70              >
71                {tab.label}
72              </button>
73            ))}
74          </div>
75        </div>
76      </div>
77
78      {/* Content */}
79      <div className="max-w-5xl mx-auto px-8 py-8">
80        {activeTab === 'latest' && (
81          <div className="space-y-8">
82            {articles.map(article => (
83              <ArticlePreview key={article.id} article={article} />
84            ))}
85          </div>
86        )}
87
88        {activeTab === 'about' && (
89          <div className="max-w-2xl">
90            <h2 className="text-2xl font-bold mb-4">About</h2>
91            <p className="text-lg text-gray-700 leading-relaxed mb-8">
92              {publication.description}
93            </p>
94
95            <h3 className="text-xl font-bold mb-4">Editors</h3>
96            <div className="grid grid-cols-2 gap-4">
97              {publication.editors.map(editor => (
98                <EditorCard key={editor.userId} editor={editor} />
99              ))}
100            </div>
101          </div>
102        )}
103      </div>
104    </div>
105  );
106}
107
108function ArticlePreview({ article }: { article: Article }) {
109  return (
110    <div className="flex gap-6 pb-8 border-b border-gray-200">
111      <div className="flex-1">
112        <div className="flex items-center gap-2 mb-3">
113          <img
114            src={article.author.avatar}
115            alt={article.author.name}
116            className="w-6 h-6 rounded-full"
117          />
118          <span className="text-sm font-semibold">{article.author.name}</span>
119        </div>
120
121        <h2 className="text-2xl font-bold mb-2 hover:underline cursor-pointer">
122          {article.title}
123        </h2>
124        {article.subtitle && (
125          <p className="text-gray-600 mb-3">{article.subtitle}</p>
126        )}
127
128        <div className="flex items-center gap-3 text-sm text-gray-500">
129          <span>{new Date(article.publishedAt!).toLocaleDateString()}</span>
130          <span></span>
131          <span>{article.readingTime} min read</span>
132          <span></span>
133          <span>{article.claps} claps</span>
134        </div>
135      </div>
136
137      {article.coverImage && (
138        <img
139          src={article.coverImage}
140          alt={article.title}
141          className="w-40 h-40 object-cover"
142        />
143      )}
144    </div>
145  );
146}

Topics and Discovery

1interface Topic {
2  id: string;
3  name: string;
4  slug: string;
5  description: string;
6  followers: number;
7  relatedTopics: string[];
8}
9
10// Recommendation engine
11function getRecommendedArticles(
12  userId: string,
13  readingHistory: Article[],
14  followedTopics: Topic[],
15  followedAuthors: Author[]
16): Article[] {
17  // Algorithm:
18  // 1. Analyze user's reading history for topics
19  // 2. Find articles from followed topics
20  // 3. Include articles from followed authors
21  // 4. Add trending articles in related topics
22  // 5. Weight by recency and engagement
23
24  const recommendations: Article[] = [];
25
26  // ... implementation
27
28  return recommendations
29    .sort((a, b) => b.claps - a.claps)
30    .slice(0, 20);
31}
32
33// Home feed
34'use client';
35
36export default function HomeFeed() {
37  const [activeTab, setActiveTab] = useState<'for-you' | 'following'>('for-you');
38  const [articles, setArticles] = useState<Article[]>([]);
39
40  useEffect(() => {
41    if (activeTab === 'for-you') {
42      loadRecommendations();
43    } else {
44      loadFollowingFeed();
45    }
46  }, [activeTab]);
47
48  return (
49    <div className="min-h-screen bg-white">
50      <div className="max-w-6xl mx-auto px-8 py-8">
51        <div className="flex gap-12">
52          {/* Main Feed */}
53          <div className="flex-1">
54            {/* Tabs */}
55            <div className="flex gap-8 mb-8 border-b border-gray-200">
56              <button
57                onClick={() => setActiveTab('for-you')}
58                className={`
59                  pb-3 font-semibold
60                  ${activeTab === 'for-you'
61                    ? 'border-b-2 border-black'
62                    : 'text-gray-500'
63                  }
64                `}
65              >
66                For you
67              </button>
68              <button
69                onClick={() => setActiveTab('following')}
70                className={`
71                  pb-3 font-semibold
72                  ${activeTab === 'following'
73                    ? 'border-b-2 border-black'
74                    : 'text-gray-500'
75                  }
76                `}
77              >
78                Following
79              </button>
80            </div>
81
82            {/* Articles */}
83            <div className="space-y-8">
84              {articles.map(article => (
85                <ArticleCard key={article.id} article={article} />
86              ))}
87            </div>
88          </div>
89
90          {/* Sidebar */}
91          <div className="w-80">
92            <DiscoverySidebar />
93          </div>
94        </div>
95      </div>
96    </div>
97  );
98}
99
100function DiscoverySidebar() {
101  return (
102    <div className="space-y-8">
103      {/* Topics */}
104      <div>
105        <h3 className="font-bold mb-4">Discover more of what matters to you</h3>
106        <div className="flex flex-wrap gap-2">
107          {['Programming', 'Design', 'Data Science', 'Self Improvement', 'Writing', 'Technology'].map(topic => (
108            <button
109              key={topic}
110              className="px-4 py-2 bg-gray-100 rounded-full text-sm hover:bg-gray-200"
111            >
112              {topic}
113            </button>
114          ))}
115        </div>
116      </div>
117
118      {/* Trending */}
119      <div>
120        <h3 className="font-bold mb-4">Trending on Captain's Chronicle</h3>
121        <div className="space-y-4">
122          {[1, 2, 3, 4, 5].map(i => (
123            <TrendingArticle key={i} rank={i} />
124          ))}
125        </div>
126      </div>
127    </div>
128  );
129}

Podsumowanie 🎓

Publications - multi-author blogs ✅ Editorial team - editors, writers, permissions ✅ Submission system - writers submit to publications ✅ Publication page - logo, description, articles ✅ Topics - categorize content ✅ Recommendations - personalized feed (For You) ✅ Following feed - from followed authors/publications ✅ Trending sidebar - discover popular content ✅ Topic following - follow topics of interest

Masz teraz kompletny Medium clone z editor, claps, highlights, responses i publications! 📝

Do zobaczenia! 🚀

Przejdź do CodeWorlds