Skip to content

Newsletter page #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ const config: Config = {
label: "🎙️ Podcast",
to: "/podcasts/",
},
{
label: "📰 Newsletter",
to: "/newsletter/",
},
],
},
{
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@
"lucide-react": "^0.503.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.3.1",
"react-dom": "^18.0.0",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"react-slot-counter": "^3.3.1",
"react-tooltip": "^5.29.1",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"styled-components": "^6.1.18",
Expand All @@ -60,6 +61,8 @@
"@docusaurus/types": "3.7.0",
"@tailwindcss/postcss": "^4.1.4",
"@types/canvas-confetti": "^1.9.0",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.4",
Expand Down
99 changes: 99 additions & 0 deletions src/components/newsletter/NewsletterCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// NewsletterCard.tsx
import React from "react";
import { FaHeart, FaShare, FaBookmark, FaRegBookmark } from "react-icons/fa";
import { Tooltip } from "react-tooltip";
import { Post } from "@site/src/data/posts";

interface CardProps {
post: Post;
isLiked: boolean;
isSaved: boolean;
onLikeToggle: () => void;
onSaveToggle: () => void;
}

const NewsletterCard: React.FC<CardProps> = ({
post,
isLiked,
isSaved,
onLikeToggle,
onSaveToggle,
}) => {
const handleShare = () => {

const shareUrl = `${window.location.href}?id=${post.id}`;
if (navigator.share) {
navigator.share({ title: post.title, text: post.summary, url: shareUrl });
} else {
navigator.clipboard.writeText(shareUrl);
alert("Link copied to clipboard!");
}
};

return (
<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">

<img
src={post.image}
alt={post.title}
className="w-full h-48 object-cover rounded-t-lg"
/>
<div className="p-4">
{/*clickable title*/}
<a
href={post.link}
target="_balnk"
rel="noopener noreferrer"
className="block hover:scale-105 transition-transform duration-300"
>
<h3 className="text-xl font-semibold">{post.title}</h3>
</a>
<p className="flex justify-between text-sm text-gray-500 mb-2">
{new Date(post.date).toLocaleDateString()} • {post.author}
</p>
<p className="text-gray-700 mb-2">{post.summary}</p>

<div className="flex flex-wrap gap-1 mb-3">
{post.tags.map((tg) => (
<span key={tg} className="text-xs bg-gray-100 px-2 py-1 rounded-full">
{tg}
</span>
))}
</div>

<div className="flex justify-between items-center mt-2">
<div className="flex items-center gap-3">
<button
data-tooltip-id={`like-${post.id}`}
onClick={onLikeToggle}
className={`text-lg transition ${isLiked ? "text-red-500" : "text-gray-500"}`}
>
<FaHeart />
</button>
<Tooltip id={`like-${post.id}`} content="Like" />

<button
data-tooltip-id={`save-${post.id}`}
onClick={onSaveToggle}
className="text-lg text-gray-500 hover:text-blue-600"
>
{isSaved ? <FaBookmark /> : <FaRegBookmark />}
</button>
<Tooltip id={`save-${post.id}`} content={isSaved ? "Unsave" : "Save for later"} />
</div>

<button
data-tooltip-id={`share-${post.id}`}
onClick={handleShare}
className="text-lg text-gray-500 hover:text-gray-700 transition"
>
<FaShare />
</button>
<Tooltip id={`share-${post.id}`} content="Share" />
</div>
</div>
</div>
);
};

export default NewsletterCard;
177 changes: 177 additions & 0 deletions src/components/newsletter/NewsletterSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React, { ReactNode, useState } from "react";
import { Post } from "@site/src/data/posts";
import { FaBookmark ,FaClock,FaFire,FaHourglassStart} from "react-icons/fa";
import { HiOutlineAdjustments } from "react-icons/hi";
import { Tooltip } from "react-tooltip";

interface SidebarProps {
posts: Post[];
selectedTags: string[];
setSelectedTags: React.Dispatch<React.SetStateAction<string[]>>;
sortBy: "Latest" | "Popular" | "Oldest";
setSortBy: React.Dispatch<React.SetStateAction<"Latest" | "Popular" | "Oldest">>;
clearFilters: () => void;
showSavedOnly: boolean;
setShowSavedOnly: React.Dispatch<React.SetStateAction<boolean>>;
savedIds: number[];
onHideSidebar: () => void;
tagCounts: { [tag: string]: number };
}

const MAX_VISIBLE_TAGS = 5;

const NewsletterSidebar: React.FC<SidebarProps> = ({
posts,
selectedTags,
setSelectedTags,
sortBy,
setSortBy,
clearFilters,
showSavedOnly,
setShowSavedOnly,
savedIds,
onHideSidebar,
}) => {
const [collapsed, setCollapsed] = useState(false);

const allTags = Array.from(new Set(posts.flatMap((p) => p.tags)));
const visibleTags = allTags.slice(0, MAX_VISIBLE_TAGS);
const hiddenTags = allTags.slice(MAX_VISIBLE_TAGS);

const toggleTag = (tag: string) =>
setSelectedTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
);

const tagCounts = posts.reduce((acc: { [tag: string]: number }, post) => {
post.tags.forEach((tag) => {
acc[tag] = (acc[tag] || 0) + 1;
});
return acc;
}, {});

const sortIcons: { [key: string]: ReactNode } = {
Latest: <FaClock className="text-sm mr-1" />,
Popular: <FaFire className="text-sm mr-1" />,
Oldest: <FaHourglassStart className="text-sm mr-1" />,
};

return (
<aside className="w-full md:w-64 p-4 sticky top-0 backdrop-blur-lg bg-white/70 shadow-xl borderborder-gray-200 min-h-screen transition-all duration-300">
<div className="w-full mb-6">
<h3 className="text-sm font-semibold text-gray-600 mb-2 pl-1 tracking-wide">Sort By</h3>
{/* Sort by */}
<div className="flex flex-wrap items-center gap-2 bg-gray-100 p-2 rounded-xl shadow-inner border border-gray-200">
{["Latest", "Popular", "Oldest"].map((option) => (
<button
key={option}
onClick={() => setSortBy(option as "Latest" | "Popular" | "Oldest")}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-sm transition-all duration-200 font-medium shadow-sm border whitespace-nowrap ${
sortBy === option ? "bg-gradient-to-r from-black to-gray-800 text-white" : "bg-white text-gray-700 border-gray-300 hover:bg-gray-50"
}`}
>
{sortIcons[option]}
{option}
</button>
))}
</div>

{/* Tags */}
<div>
<label className="font-semibold mb-1 block text-gray-800">Tags</label>
<div className="flex flex-wrap gap-2">
{visibleTags.map((tag) => (
<button
key={tag}
className={`flex items-center justify-between px-3 py-1.5 rounded-md text-sm transition ${
selectedTags.includes(tag) ? "bg-black text-white" : "hover:bg-gray-100"
}`}
onClick={() => toggleTag(tag)}
>
<span>{tag}</span>
<span className="text-xs text-gray-500 ml-2">({tagCounts[tag]})</span>
</button>
))}

{hiddenTags.length > 0 && (
<>
<div className="relative group">
<button
className="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-md text-sm text-gray-700 relative z-10"
data-tooltip-id="tag-tooltip"
data-tooltip-html={hiddenTags
.map(
(tag) =>
`<div style="cursor:pointer; margin:4px 0;" onclick="document.getElementById('tag-${tag}').click()">
${tag} (${tagCounts[tag]})
</div>`
)
.join("")}
>
+{hiddenTags.length} more
</button>
<Tooltip
id="tag-tooltip"
place="top"
className="backdrop-blur-md bg-white/80 border border-gray-300 shadow-lg rounded-lg text-gray-800 text-sm p-2 z-50 transition-all duration-300"
clickable
/>
</div>

{/* Hidden buttons (to trigger via onclick in tooltip) */}
{hiddenTags.map((tag) => (
<button
key={tag}
id={`tag-${tag}`}
className="hidden"
onClick={() => toggleTag(tag)}
/>
))}
</>
)}
</div>
</div>

{/* Show Saved Only Toggle */}
<div className="pt-2">
<button
onClick={() => setShowSavedOnly(!showSavedOnly)}
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 ${
showSavedOnly
? "bg-green-100 text-green-800 border-green-300"
: "bg-white text-gray-700 border-gray-300 hover:bg-gray-100"
}`}
>
<FaBookmark className={`transition-transform duration-200 ${showSavedOnly ? 'scale-110 text-green-600' : 'text-gray-500 hover:text-black'}`}
/>
{showSavedOnly ? "Viewing Saved" : "View Saved"}
</button>
</div>

{/* Hide Sidebar */}
<div className="pt-2">
<button
onClick={onHideSidebar}
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"
>
<HiOutlineAdjustments className="text-pink-500" />
Hide Sidebar (Full Screen)
</button>
</div>

{/* Clear Filters */}
{(selectedTags.length > 0 || sortBy !== "Latest" || showSavedOnly) && (
<button
onClick={clearFilters}
className="text-red-500 text-sm hover:text-red-700 transition animate-shake hover:animate-shake"
>
Clear Filters
</button>
)}
</div>

</aside>
);
};

export default NewsletterSidebar;
84 changes: 84 additions & 0 deletions src/data/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// posts object array with strict typing
export interface Post {
id: number;
title: string;
summary: string;
date: string; // YYYY‑MM‑DD
author: string;
tags: string[];
image: string;
likes: number;
link:string;
}

const posts: Post[] = [
{
id: 1,
title: "Building an Open Source Community",
summary: "Learn how to grow a healthy developer community.",
date: "2025-08-01",
author: "Sanjay Vishwanatham",
tags: ["Open Source", "Community"],
image: "/img/posts/open-source-community.jpeg",
likes: 120,
link: "https://example.com/open-source-community",
},
{
id: 2,
title: "AI in Education: The Future",
summary: "Exploring how AI will reshape learning experiences.",
date: "2025-07-30",
author: "Sanjay Vishwanatham",
tags: ["AI", "Education"],
image: "/img/posts/ai-education.jpeg",
likes: 210,
link: "https://example.com/ai-education",
},
{
id: 3,
title: "The Future of AI: 2025 and Beyond",
summary: "A deep dive into how AI will evolve in the near future and impact industries worldwide.",
date: "2024-10-1",
author: "Sanjay Vishwanatham",
tags: ["AI", "Education","Technology", "Trends"],
image: "/img/posts/future-ai.jpeg",
likes: 20,
link: "https://example.com/future-ai",
},
{
id: 4,
title: "Design Thinking in Modern UX",
summary: "Understanding how design thinking is transforming user experience strategies",
date: "2025-06-18",
author: "Sanjay Vishwanatham",
tags: ["UX", "Design", "Innovation"],
image: "/img/posts/design-thinking.jpeg",
likes: 20,
link: "https://example.com/design-thinking",
},
{
id: 5,
title: "Remote Work and Digital Nomadism",
summary: "Exploring how remote work is changing lifestyles and productivity across the globe.",
date: "2024-10-1",
author: "Sanjay Vishwanatham",
tags: ["Remote Work", "Lifestyle", "Productivity"],
image: "/img/posts/remote-work.jpeg",
likes: 20,
link: "https://example.com/remote-work",
},
{
id: 6,
title: "Cybersecurity Threats You Should Know",
summary: "An overview of emerging cybersecurity threats and how to protect against them.",
date: "2024-10-1",
author: "Sanjay Vishwanatham",
tags: ["Cybersecurity", "Technology", "Safety"],
image: "/img/posts/cybersecurity.jpeg",
likes: 20,
link: "https://example.com/cybersecurity",
},
// …add more posts
];

export default posts;
Loading
Loading