Skip to content
Closed
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 @@ -193,6 +193,10 @@ const config: Config = {
label: "🎙️ Podcast",
to: "/podcasts/",
},
{
label: "📰 Newsletter",
to: "/newsletter/",
},
],
},
{
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"build": "cross-env NODE_OPTIONS=${NODE_OPTIONS:---max_old_space_size=4096} docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
Expand Down Expand Up @@ -47,17 +47,19 @@
"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",
"tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.8",
"vanilla-tilt": "^1.8.1"
"vanilla-tilt": "^1.8.1",
"cross-env": "^7.0.3"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/tsconfig": "3.8.1",
"@docusaurus/types": "3.8.1"
"@tailwindcss/postcss": "^4.1.4",
"@types/canvas-confetti": "^1.9.0",
"@types/react": "^19.1.9",
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;
Loading
Loading