Skip to content

Commit ff7da8d

Browse files
committed
Create Newsletter page
1 parent 64ba4c0 commit ff7da8d

File tree

7 files changed

+540
-0
lines changed

7 files changed

+540
-0
lines changed

docusaurus.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ const config: Config = {
184184
label: "🎙️ Podcast",
185185
to: "/podcasts/",
186186
},
187+
{
188+
label: "📰 Newsletter",
189+
to: "/newsletter/",
190+
},
187191
],
188192
},
189193
{

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"react-dom": "^18.0.0",
4848
"react-icons": "^5.5.0",
4949
"react-slot-counter": "^3.3.1",
50+
"react-tooltip": "^5.29.1",
5051
"rehype-katex": "^7.0.1",
5152
"remark-math": "^6.0.0",
5253
"styled-components": "^6.1.18",
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// NewsletterCard.tsx
2+
import React from "react";
3+
import { FaHeart, FaShare, FaBookmark, FaRegBookmark } from "react-icons/fa";
4+
import { Tooltip } from "react-tooltip";
5+
import { Post } from "@site/src/data/posts";
6+
7+
interface CardProps {
8+
post: Post;
9+
isLiked: boolean;
10+
isSaved: boolean;
11+
onLikeToggle: () => void;
12+
onSaveToggle: () => void;
13+
}
14+
15+
const NewsletterCard: React.FC<CardProps> = ({
16+
post,
17+
isLiked,
18+
isSaved,
19+
onLikeToggle,
20+
onSaveToggle,
21+
}) => {
22+
const handleShare = () => {
23+
const shareUrl = `${window.location.href}?id=${post.id}`;
24+
if (navigator.share) {
25+
navigator.share({ title: post.title, text: post.summary, url: shareUrl });
26+
} else {
27+
navigator.clipboard.writeText(shareUrl);
28+
alert("Link copied to clipboard!");
29+
}
30+
};
31+
32+
return (
33+
<div className="rounded-xl shadow-md p-4 bg-white hover:shadow-xl transform hover:scale-[1.02] transition duration-300 bg-white/80 backdrop-blur-md rounded-lg p-6 shadow">
34+
<img
35+
src={post.image}
36+
alt={post.title}
37+
className="w-full h-48 object-cover rounded-t-lg"
38+
/>
39+
<div className="p-4">
40+
<h3 className="text-xl font-semibold">{post.title}</h3>
41+
<p className="flex justify-between text-sm text-gray-500 mb-2">
42+
{new Date(post.date).toLocaleDateString()}{post.author}
43+
</p>
44+
<p className="text-gray-700 mb-2">{post.summary}</p>
45+
<div className="flex flex-wrap gap-1 mb-3">
46+
{post.tags.map((tg) => (
47+
<span key={tg} className="text-xs bg-gray-100 px-2 py-1 rounded-full">
48+
{tg}
49+
</span>
50+
))}
51+
</div>
52+
53+
<div className="flex justify-between items-center mt-2">
54+
<div className="flex items-center gap-3">
55+
<button
56+
data-tooltip-id={`like-${post.id}`}
57+
onClick={onLikeToggle}
58+
className={`text-lg transition ${isLiked ? "text-red-500" : "text-gray-500"}`}
59+
>
60+
<FaHeart />
61+
</button>
62+
<Tooltip id={`like-${post.id}`} content="Like" />
63+
64+
<button
65+
data-tooltip-id={`save-${post.id}`}
66+
onClick={onSaveToggle}
67+
className="text-lg text-gray-500 hover:text-blue-600"
68+
>
69+
{isSaved ? <FaBookmark /> : <FaRegBookmark />}
70+
</button>
71+
<Tooltip id={`save-${post.id}`} content={isSaved ? "Unsave" : "Save for later"} />
72+
</div>
73+
74+
<button
75+
data-tooltip-id={`share-${post.id}`}
76+
onClick={handleShare}
77+
className="text-lg text-gray-500 hover:text-gray-700 transition"
78+
>
79+
<FaShare />
80+
</button>
81+
<Tooltip id={`share-${post.id}`} content="Share" />
82+
</div>
83+
</div>
84+
</div>
85+
);
86+
};
87+
88+
export default NewsletterCard;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React, { useState } from "react";
2+
import { Post } from "@site/src/data/posts";
3+
import { FaBookmark } from "react-icons/fa";
4+
import { HiOutlineAdjustments } from "react-icons/hi";
5+
import { Tooltip } from "react-tooltip";
6+
7+
interface SidebarProps {
8+
posts: Post[];
9+
selectedTags: string[];
10+
setSelectedTags: React.Dispatch<React.SetStateAction<string[]>>;
11+
sortBy: "Latest" | "Popular" | "Oldest";
12+
setSortBy: React.Dispatch<React.SetStateAction<"Latest" | "Popular" | "Oldest">>;
13+
clearFilters: () => void;
14+
showSavedOnly: boolean;
15+
setShowSavedOnly: React.Dispatch<React.SetStateAction<boolean>>;
16+
savedIds: number[];
17+
onHideSidebar: () => void;
18+
tagCounts: { [tag: string]: number };
19+
}
20+
21+
const MAX_VISIBLE_TAGS = 5;
22+
23+
const NewsletterSidebar: React.FC<SidebarProps> = ({
24+
posts,
25+
selectedTags,
26+
setSelectedTags,
27+
sortBy,
28+
setSortBy,
29+
clearFilters,
30+
showSavedOnly,
31+
setShowSavedOnly,
32+
savedIds,
33+
onHideSidebar,
34+
}) => {
35+
const [collapsed, setCollapsed] = useState(false);
36+
37+
const allTags = Array.from(new Set(posts.flatMap((p) => p.tags)));
38+
const visibleTags = allTags.slice(0, MAX_VISIBLE_TAGS);
39+
const hiddenTags = allTags.slice(MAX_VISIBLE_TAGS);
40+
41+
const toggleTag = (tag: string) =>
42+
setSelectedTags((prev) =>
43+
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
44+
);
45+
46+
const tagCounts = posts.reduce((acc: { [tag: string]: number }, post) => {
47+
post.tags.forEach((tag) => {
48+
acc[tag] = (acc[tag] || 0) + 1;
49+
});
50+
return acc;
51+
}, {});
52+
53+
return (
54+
<aside className="w-full md:w-64 p-4 sticky top-0 bg-white border-r border-gray-200 min-h-screen transition-all duration-300">
55+
{/* Collapse Toggle */}
56+
<button
57+
onClick={() => setCollapsed((c) => !c)}
58+
className="w-full flex justify-between items-center px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-purple-600 to-indigo-600 rounded-md shadow hover:opacity-90 transition mb-4"
59+
>
60+
<span>{collapsed ? "Show Filters" : "Hide Filters"}</span>
61+
<svg
62+
className={`w-4 h-4 transform transition-transform duration-200 ${
63+
collapsed ? "rotate-0" : "rotate-180"
64+
}`}
65+
fill="none"
66+
stroke="currentColor"
67+
strokeWidth={2}
68+
viewBox="0 0 24 24"
69+
>
70+
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
71+
</svg>
72+
</button>
73+
74+
{!collapsed && (
75+
<div className="space-y-5">
76+
{/* Sort by */}
77+
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-full shadow-inner border border-gray-200">
78+
{["Latest", "Popular", "Oldest"].map((option) => (
79+
<button
80+
key={option}
81+
onClick={() => setSortBy(option as "Latest" | "Popular" | "Oldest")}
82+
className={`px-3 py-2 rounded-full text-sm font-medium transition-all duration-300 ${
83+
sortBy === option ? "bg-black text-white shadow" : "text-gray-600 hover:bg-white"
84+
}`}
85+
>
86+
{option}
87+
</button>
88+
))}
89+
</div>
90+
91+
{/* Tags */}
92+
<div>
93+
<label className="font-semibold mb-1 block text-gray-800">Tags</label>
94+
<div className="flex flex-wrap gap-2">
95+
{visibleTags.map((tag) => (
96+
<button
97+
key={tag}
98+
className={`flex items-center justify-between px-3 py-1.5 rounded-md text-sm transition ${
99+
selectedTags.includes(tag) ? "bg-black text-white" : "hover:bg-gray-100"
100+
}`}
101+
onClick={() => toggleTag(tag)}
102+
>
103+
<span>{tag}</span>
104+
<span className="text-xs text-gray-500 ml-2">({tagCounts[tag]})</span>
105+
</button>
106+
))}
107+
108+
{hiddenTags.length > 0 && (
109+
<>
110+
<div className="relative group">
111+
<button
112+
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-md text-sm text-gray-700"
113+
data-tooltip-id="tag-tooltip"
114+
data-tooltip-html={hiddenTags
115+
.map(
116+
(tag) =>
117+
`<div style="cursor:pointer; margin:4px 0;" onclick="document.getElementById('tag-${tag}').click()">
118+
${tag} (${tagCounts[tag]})
119+
</div>`
120+
)
121+
.join("")}
122+
>
123+
+{hiddenTags.length} more
124+
</button>
125+
<Tooltip
126+
id="tag-tooltip"
127+
place="top"
128+
className="whitespace-pre-line z-50 max-w-xs text-sm"
129+
clickable
130+
/>
131+
</div>
132+
133+
{/* Hidden buttons (to trigger via onclick in tooltip) */}
134+
{hiddenTags.map((tag) => (
135+
<button
136+
key={tag}
137+
id={`tag-${tag}`}
138+
className="hidden"
139+
onClick={() => toggleTag(tag)}
140+
/>
141+
))}
142+
</>
143+
)}
144+
</div>
145+
</div>
146+
147+
{/* Show Saved Only Toggle */}
148+
<div className="pt-2">
149+
<button
150+
onClick={() => setShowSavedOnly(!showSavedOnly)}
151+
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-full border shadow-sm transition-all w-full justify-center ${
152+
showSavedOnly
153+
? "bg-green-100 text-green-800 border-green-300"
154+
: "bg-white text-gray-700 border-gray-300 hover:bg-gray-100"
155+
}`}
156+
>
157+
<FaBookmark className={showSavedOnly ? "text-green-600" : "text-gray-500"} />
158+
{showSavedOnly ? "Showing Saved" : "Show Saved Only"}
159+
</button>
160+
</div>
161+
162+
{/* Hide Sidebar */}
163+
<div className="pt-2">
164+
<button
165+
onClick={onHideSidebar}
166+
className="w-full flex items-center justify-center gap-2 text-sm font-medium px-4 py-2 bg-pink-100 text-pink-700 border border-pink-300 rounded-full shadow-sm hover:bg-pink-200 transition-all"
167+
>
168+
<HiOutlineAdjustments className="text-pink-500" />
169+
Hide Sidebar (Full Screen)
170+
</button>
171+
</div>
172+
173+
{/* Clear Filters */}
174+
{(selectedTags.length > 0 || sortBy !== "Latest" || showSavedOnly) && (
175+
<button
176+
onClick={clearFilters}
177+
className="text-red-500 text-sm hover:underline hover:text-red-700 transition"
178+
>
179+
Clear Filters
180+
</button>
181+
)}
182+
</div>
183+
)}
184+
</aside>
185+
);
186+
};
187+
188+
export default NewsletterSidebar;

src/data/posts.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// posts object array with strict typing
2+
export interface Post {
3+
id: number;
4+
title: string;
5+
summary: string;
6+
date: string; // YYYY‑MM‑DD
7+
author: string;
8+
tags: string[];
9+
image: string;
10+
likes: number;
11+
}
12+
13+
const posts: Post[] = [
14+
{
15+
id: 1,
16+
title: "Building an Open Source Community",
17+
summary: "Learn how to grow a healthy developer community.",
18+
date: "2025-08-01",
19+
author: "Sanjay Vishwanatham",
20+
tags: ["Open Source", "Community"],
21+
image: "https://source.unsplash.com/featured/?ai,technology",
22+
likes: 120,
23+
},
24+
{
25+
id: 2,
26+
title: "AI in Education: The Future",
27+
summary: "Exploring how AI will reshape learning experiences.",
28+
date: "2025-07-30",
29+
author: "Sanjay Vishwanatham",
30+
tags: ["AI", "Education"],
31+
image: "https://source.unsplash.com/featured/?ai,technology",
32+
likes: 210,
33+
},
34+
{
35+
id: 3,
36+
title: "The Future of AI: 2025 and Beyond",
37+
summary: "A deep dive into how AI will evolve in the near future and impact industries worldwide.",
38+
date: "2025-07-0",
39+
author: "Sanjay Vishwanatham",
40+
tags: ["AI", "Education","Technology", "Trends"],
41+
image: "https://source.unsplash.com/featured/?ai,technology",
42+
likes: 20,
43+
},
44+
{
45+
id: 4,
46+
title: "Design Thinking in Modern UX",
47+
summary: "Understanding how design thinking is transforming user experience strategies",
48+
date: "2025-06-18",
49+
author: "Sanjay Vishwanatham",
50+
tags: ["UX", "Design", "Innovation"],
51+
image: "https://source.unsplash.com/featured/?ai,technology",
52+
likes: 20,
53+
},
54+
{
55+
id: 5,
56+
title: "Remote Work and Digital Nomadism",
57+
summary: "Exploring how remote work is changing lifestyles and productivity across the globe.",
58+
date: "2025-07-0",
59+
author: "Sanjay Vishwanatham",
60+
tags: ["Remote Work", "Lifestyle", "Productivity"],
61+
image: "https://source.unsplash.com/featured/?ai,technology",
62+
likes: 20,
63+
},
64+
{
65+
id: 6,
66+
title: "Cybersecurity Threats You Should Know",
67+
summary: "An overview of emerging cybersecurity threats and how to protect against them.",
68+
date: "2025-07-0",
69+
author: "Sanjay Vishwanatham",
70+
tags: ["Cybersecurity", "Technology", "Safety"],
71+
image: "https://source.unsplash.com/featured/?ai,technology",
72+
likes: 20,
73+
},
74+
// …add more posts
75+
];
76+
77+
export default posts;

0 commit comments

Comments
 (0)