Medium has publications - multi-author blogs with editorial teams.
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}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}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}✅ 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
You now have a complete Medium clone with editor, claps, highlights, responses, and publications! 📝
See you soon! 🚀