diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 0e61e0a..a013bd7 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -184,6 +184,10 @@ const config: Config = { label: "🎙️ Podcast", to: "/podcasts/", }, + { + label: "📰 Newsletter", + to: "/newsletter/", + }, ], }, { diff --git a/package.json b/package.json index 9d6f245..5cfead0 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/components/newsletter/NewsletterCard.tsx b/src/components/newsletter/NewsletterCard.tsx new file mode 100644 index 0000000..4fd53e1 --- /dev/null +++ b/src/components/newsletter/NewsletterCard.tsx @@ -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 = ({ + 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 ( +
+ + {post.title} +
+ {/*clickable title*/} + +

{post.title}

+
+

+ {new Date(post.date).toLocaleDateString()} • {post.author} +

+

{post.summary}

+ +
+ {post.tags.map((tg) => ( + + {tg} + + ))} +
+ +
+
+ + + + + +
+ + + +
+
+
+ ); +}; + +export default NewsletterCard; diff --git a/src/components/newsletter/NewsletterSidebar.tsx b/src/components/newsletter/NewsletterSidebar.tsx new file mode 100644 index 0000000..2814f68 --- /dev/null +++ b/src/components/newsletter/NewsletterSidebar.tsx @@ -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>; + sortBy: "Latest" | "Popular" | "Oldest"; + setSortBy: React.Dispatch>; + clearFilters: () => void; + showSavedOnly: boolean; + setShowSavedOnly: React.Dispatch>; + savedIds: number[]; + onHideSidebar: () => void; + tagCounts: { [tag: string]: number }; +} + +const MAX_VISIBLE_TAGS = 5; + +const NewsletterSidebar: React.FC = ({ + 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: , + Popular: , + Oldest: , + }; + + return ( + + ); +}; + +export default NewsletterSidebar; diff --git a/src/data/posts.ts b/src/data/posts.ts new file mode 100644 index 0000000..a41964a --- /dev/null +++ b/src/data/posts.ts @@ -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; diff --git a/src/pages/newsletter/index.tsx b/src/pages/newsletter/index.tsx new file mode 100644 index 0000000..d2dddc4 --- /dev/null +++ b/src/pages/newsletter/index.tsx @@ -0,0 +1,163 @@ +import React, { useState } from "react"; +import NewsletterSidebar from "@site/src/components/newsletter/NewsletterSidebar"; +import NewsletterCard from "@site/src/components/newsletter/NewsletterCard"; +import posts from "@site/src/data/posts"; +import { useLocalStorage } from "@site/src/utils/useLocalStorage"; + +const ITEMS_PER_LOAD = 6; + +const NewsletterPage: React.FC = () => { + const [displayCount, setDisplayCount] = useState(ITEMS_PER_LOAD); + const [selectedTags, setSelectedTags] = useState([]); + const [sortBy, setSortBy] = useState<"Latest" | "Popular" | "Oldest">("Latest"); + const [likedIds, setLikedIds] = useLocalStorage("likedPosts", []); + const [savedIds, setSavedIds] = useLocalStorage("savedPosts", []); + const [showSavedOnly, setShowSavedOnly] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [isSidebarVisible, setIsSidebarVisible] = useState(true); + const [showSuggestions, setShowSuggestions] = useState(false); + + const filtered = posts + .filter((p) => + selectedTags.length + ? selectedTags.every((tg) => p.tags.includes(tg)) + : true + ) + .filter((p) => (showSavedOnly ? savedIds.includes(p.id) : true)) + .filter((p) => + p.title.toLowerCase().includes(searchTerm.toLowerCase()) || + p.tags.some((tag) => tag.toLowerCase().includes(searchTerm.toLowerCase())) || + p.author.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .sort((a, b) => { + if (sortBy === "Popular") return b.likes - a.likes; + if (sortBy === "Oldest") return new Date(a.date).getTime() - new Date(b.date).getTime(); + return new Date(b.date).getTime() - new Date(a.date).getTime(); // Latest + }); + + const visible = filtered.slice(0, displayCount); + + const toggleLike = (id: number) => + setLikedIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + ); + + const toggleSave = (id: number) => + setSavedIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + ); + const tagCounts = posts.reduce((acc: { [tag: string]: number }, post) => { + post.tags.forEach((tag) => { + acc[tag] = (acc[tag] || 0) + 1; + }); + return acc; +}, {}); + + + return ( +
+ {/* Sidebar Section */} +
+ {isSidebarVisible && ( + { + setSelectedTags([]); + setSortBy("Latest"); + setSearchTerm(""); + setShowSavedOnly(false); + }} + onHideSidebar={() => setIsSidebarVisible(false)} + /> + )} +
+ {/* Main Content Section */} +
+ {!isSidebarVisible && ( +
+ +
+ )} + +
+

📰 RecodeHive Newsletter

+

+ Discover insightful updates, tech stories, and community highlights — all in one place. +

+ {showSavedOnly && ( +

+ Showing only saved newsletters 💾 +

+)} + + +
+ setSearchTerm(e.target.value)} + /> +
+ + + {/* Newsletter Crads */} + {filtered.length === 0 ? ( +
+

😕 No newsletters match your search or filters.

+

+ Try changing the search term or clearing filters. +

+
+ ) : ( + +
+ {visible.map((p) => ( + toggleLike(p.id)} + onSaveToggle={() => toggleSave(p.id)} + /> + ))} +
+ )} + + {displayCount < filtered.length && ( +
+ +
+ )} +
+
+
+ ); +}; + +export default NewsletterPage; diff --git a/src/utils/useLocalStorage.ts b/src/utils/useLocalStorage.ts new file mode 100644 index 0000000..02a69fe --- /dev/null +++ b/src/utils/useLocalStorage.ts @@ -0,0 +1,19 @@ +import { useState, useEffect } from "react"; + +export function useLocalStorage(key: string, initialValue: T) { + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === "undefined") return initialValue; + try { + const item = window.localStorage.getItem(key); + return item ? (JSON.parse(item) as T) : initialValue; + } catch { + return initialValue; + } + }); + + useEffect(() => { + window.localStorage.setItem(key, JSON.stringify(storedValue)); + }, [key, storedValue]); + + return [storedValue, setStoredValue] as const; +} diff --git a/static/img/posts/ai-education.jpeg b/static/img/posts/ai-education.jpeg new file mode 100644 index 0000000..6a03cc9 Binary files /dev/null and b/static/img/posts/ai-education.jpeg differ diff --git a/static/img/posts/cybersecurity.jpeg b/static/img/posts/cybersecurity.jpeg new file mode 100644 index 0000000..7e4fad3 Binary files /dev/null and b/static/img/posts/cybersecurity.jpeg differ diff --git a/static/img/posts/design-thinking.jpeg b/static/img/posts/design-thinking.jpeg new file mode 100644 index 0000000..0ba4cec Binary files /dev/null and b/static/img/posts/design-thinking.jpeg differ diff --git a/static/img/posts/future-ai.jpeg b/static/img/posts/future-ai.jpeg new file mode 100644 index 0000000..776043e Binary files /dev/null and b/static/img/posts/future-ai.jpeg differ diff --git a/static/img/posts/open-source-community.jpeg b/static/img/posts/open-source-community.jpeg new file mode 100644 index 0000000..ea934bf Binary files /dev/null and b/static/img/posts/open-source-community.jpeg differ diff --git a/static/img/posts/remote-work.jpeg b/static/img/posts/remote-work.jpeg new file mode 100644 index 0000000..2381941 Binary files /dev/null and b/static/img/posts/remote-work.jpeg differ