From 34e4b80ee93f418ee98d16c11764a1700e99c928 Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 16:43:56 +0530 Subject: [PATCH 01/11] feat: implement newsletters dashboard with filtering and detail views - Added main newsletters component with search and month filtering. - Created individual newsletter pages displaying detailed content. - Implemented reusable components for newsletter cards, filters, and empty states. - Introduced utility functions for filtering and grouping newsletters by month. - Added sample newsletter data for testing and display purposes. --- .../(main)/dashboard/newsletters/Content.tsx | 73 ++++++ .../dashboard/newsletters/[id]/page.tsx | 117 ++++++++++ .../newsletters/components/NewsletterCard.tsx | 65 ++++++ .../components/NewsletterContent.tsx | 115 ++++++++++ .../components/NewsletterEmptyState.tsx | 29 +++ .../components/NewsletterFilters.tsx | 83 +++++++ .../newsletters/components/NewsletterList.tsx | 45 ++++ .../dashboard/newsletters/data/newsletters.ts | 212 ++++++++++++++++++ .../app/(main)/dashboard/newsletters/page.tsx | 11 + .../newsletters/utils/newsletter.filters.ts | 60 +++++ .../newsletters/utils/newsletter.utils.ts | 58 +++++ apps/web/src/components/dashboard/Sidebar.tsx | 6 + apps/web/src/components/ui/input.tsx | 22 ++ apps/web/src/components/ui/select.tsx | 160 +++++++++++++ apps/web/src/types/index.ts | 1 + apps/web/src/types/newsletter.ts | 43 ++++ 16 files changed, 1100 insertions(+) create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/Content.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/page.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts create mode 100644 apps/web/src/components/ui/input.tsx create mode 100644 apps/web/src/components/ui/select.tsx create mode 100644 apps/web/src/types/newsletter.ts diff --git a/apps/web/src/app/(main)/dashboard/newsletters/Content.tsx b/apps/web/src/app/(main)/dashboard/newsletters/Content.tsx new file mode 100644 index 0000000..3e67e8b --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/Content.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Newsletter } from "@/types/newsletter"; +import { GeistSans } from "geist/font/sans"; +import { getAvailableMonths } from "./utils/newsletter.utils"; +import { filterNewsletters } from "./utils/newsletter.filters"; +import NewsletterFilters from "./components/NewsletterFilters"; +import NewsletterList from "./components/NewsletterList"; +import NewsletterEmptyState from "./components/NewsletterEmptyState"; + +interface NewslettersProps { + newsletters: Newsletter[]; +} + +export default function Newsletters({ newsletters }: NewslettersProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [selectedMonth, setSelectedMonth] = useState("all"); + + const availableMonths = useMemo( + () => getAvailableMonths(newsletters), + [newsletters] + ); + + const filteredNewsletters = useMemo( + () => filterNewsletters(newsletters, searchQuery, selectedMonth), + [newsletters, searchQuery, selectedMonth] + ); + + const handleClearFilters = () => { + setSearchQuery(""); + setSelectedMonth("all"); + }; + + const hasActiveFilters = + searchQuery.trim() !== "" || selectedMonth !== "all"; + + return ( +
+
+
+

+ Newsletters +

+

+ Stay updated with the latest features, tips, and insights from + opensox.ai +

+
+ + + {filteredNewsletters.length === 0 ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx new file mode 100644 index 0000000..f06c539 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useParams } from "next/navigation"; +import Link from "next/link"; +import { newsletters } from "../data/newsletters"; +import NewsletterContent from "../components/NewsletterContent"; +import { Calendar, Clock, ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import Image from "next/image"; +import { NewsletterContentItem } from "@/types/newsletter"; +import { GeistSans } from "geist/font/sans"; +import { formatNewsletterDate } from "../utils/newsletter.utils"; + +export default function NewsletterPage() { + const params = useParams(); + const id = params.id as string; + const newsletter = newsletters.find((n) => n.id === id); + + if (!newsletter) { + return ( +
+
+

+ Newsletter not found +

+ + + +
+
+ ); + } + + const formattedDate = formatNewsletterDate(newsletter.date); + + return ( +
+
+ {/* back button */} + + + + + {/* newsletter header */} +
+ {newsletter.coverImage && ( +
+ {typeof newsletter.coverImage === "string" ? ( + {newsletter.title} + ) : ( + {newsletter.title} + )} +
+ )} + +

+ {newsletter.title} +

+ +
+
+ + {formattedDate} +
+ {newsletter.readTime && ( +
+ + {newsletter.readTime} +
+ )} + {newsletter.author && by {newsletter.author}} +
+ + {newsletter.excerpt && ( +

+ {newsletter.excerpt} +

+ )} +
+ + {/* divider */} +
+ + {/* newsletter content */} +
+ +
+ + {/* footer */} +
+ + + +
+
+
+ ); +} + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx new file mode 100644 index 0000000..20ed5c0 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx @@ -0,0 +1,65 @@ +import Link from "next/link"; +import { Calendar, Clock } from "lucide-react"; +import { Card } from "@/components/ui/card"; +import { Newsletter } from "@/types/newsletter"; +import Image from "next/image"; +import { GeistSans } from "geist/font/sans"; +import { formatNewsletterDate } from "../utils/newsletter.utils"; + +interface NewsletterCardProps { + newsletter: Newsletter; +} + +export default function NewsletterCard({ newsletter }: NewsletterCardProps) { + const formattedDate = formatNewsletterDate(newsletter.date); + + return ( + + + {newsletter.coverImage && ( +
+ {typeof newsletter.coverImage === "string" ? ( + {newsletter.title} + ) : ( + {newsletter.title} + )} +
+ )} +
+

+ {newsletter.title} +

+ +
+
+ + {formattedDate} +
+ {newsletter.readTime && ( +
+ + {newsletter.readTime} +
+ )} +
+

+ {newsletter.excerpt} +

+ {newsletter.author && ( +

by {newsletter.author}

+ )} +
+
+ + ); +} + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx new file mode 100644 index 0000000..7c4845d --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { NewsletterContentItem } from "@/types/newsletter"; +import Link from "next/link"; +import Image from "next/image"; + +interface NewsletterContentProps { + content: NewsletterContentItem[]; +} + +export default function NewsletterContent({ content }: NewsletterContentProps) { + return ( +
+ {content.map((item, index) => { + switch (item.type) { + case "paragraph": + return ( +

+ {item.content} +

+ ); + + case "heading": + const HeadingTag = `h${item.level}` as keyof JSX.IntrinsicElements; + const headingClasses = { + 1: "text-4xl font-bold mb-4 mt-8", + 2: "text-3xl font-bold mb-4 mt-8", + 3: "text-2xl font-semibold mb-3 mt-6", + }; + return ( + + {item.content} + + ); + + case "bold": + return ( +

+ {item.content} +

+ ); + + case "link": + return ( +
+ + {item.text} + +
+ ); + + case "image": + return ( +
+ {item.alt +
+ ); + + case "list": + const isRightAligned = item.align === "left"; + return ( +
+
    + {item.items.map((listItem, itemIndex) => ( +
  • + {listItem} + {isRightAligned && ( + + • + + )} +
  • + ))} +
+
+ ); + + default: + return null; + } + })} +
+ ); +} + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx new file mode 100644 index 0000000..1cec602 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Button } from "@/components/ui/button"; + +interface NewsletterEmptyStateProps { + hasActiveFilters: boolean; + onClearFilters: () => void; +} + +export default function NewsletterEmptyState({ + hasActiveFilters, + onClearFilters, +}: NewsletterEmptyStateProps) { + return ( +
+

+ {hasActiveFilters + ? "No newsletters match your filters" + : "No newsletters yet. Check back soon!"} +

+ {hasActiveFilters && ( + + )} +
+ ); +} + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx new file mode 100644 index 0000000..9e5fd00 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { Search, X } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface NewsletterFiltersProps { + searchQuery: string; + selectedMonth: string; + availableMonths: string[]; + resultCount: number; + onSearchChange: (query: string) => void; + onMonthChange: (month: string) => void; + onClearFilters: () => void; +} + +export default function NewsletterFilters({ + searchQuery, + selectedMonth, + availableMonths, + resultCount, + onSearchChange, + onMonthChange, + onClearFilters, +}: NewsletterFiltersProps) { + const hasActiveFilters = searchQuery.trim() !== "" || selectedMonth !== "all"; + + return ( +
+
+
+ + onSearchChange(e.target.value)} + className="pl-10 bg-card border-border" + /> +
+ + +
+ + {hasActiveFilters && ( +
+ + {resultCount} result{resultCount !== 1 ? "s" : ""} + + +
+ )} +
+ ); +} + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx new file mode 100644 index 0000000..9c30f9f --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { Newsletter } from "@/types/newsletter"; +import NewsletterCard from "./NewsletterCard"; +import { groupByMonth, sortMonthKeys } from "../utils/newsletter.utils"; +import { GeistSans } from "geist/font/sans"; + +interface NewsletterListProps { + newsletters: Newsletter[]; +} + +export default function NewsletterList({ newsletters }: NewsletterListProps) { + const groupedNewsletters = groupByMonth(newsletters); + const sortedMonths = sortMonthKeys(Object.keys(groupedNewsletters)); + + if (newsletters.length === 0) { + return ( +
+

+ No newsletters yet. Check back soon! +

+
+ ); + } + + return ( +
+ {sortedMonths.map((monthYear) => ( +
+

+ {monthYear} +

+
+ {groupedNewsletters[monthYear].map((newsletter) => ( + + ))} +
+
+ ))} +
+ ); +} + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts new file mode 100644 index 0000000..f24e8b6 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts @@ -0,0 +1,212 @@ + +export const newsletters = [ + { + id: "nov-2024-product-updates", + title: "november product updates: new ai features and performance improvements", + date: "2024-11-15", + excerpt: "exciting new features including enhanced ai capabilities, faster load times, and improved user experience across the platform.", + coverImage: "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=1200&h=600&fit=crop&q=80", + author: "ajeet", + readTime: "5 min read", + content: [ + { + type: "paragraph", + content: "hey opensox community! we've been working hard this month to bring you some incredible updates that will transform how you use our platform." + }, + { + type: "heading", + level: 2, + content: "what's new this month" + }, + { + type: "paragraph", + content: "we're excited to announce several major improvements to opensox.ai that our pro users have been requesting." + }, + { + type: "heading", + level: 3, + content: "enhanced ai models" + }, + { + type: "paragraph", + content: "our ai engine is now 3x faster with improved accuracy. you'll notice significantly better results across all tasks, from content generation to data analysis." + }, + { + type: "bold", + content: "key improvements:" + }, + { + type: "paragraph", + content: "- 70% faster response times\n- improved accuracy on complex queries\n- better context understanding\n- support for longer inputs" + }, + { + type: "heading", + level: 3, + content: "new dashboard interface" + }, + { + type: "paragraph", + content: "we've completely redesigned the dashboard to make navigation more intuitive. the new interface puts your most-used features front and center." + }, + { + type: "link", + text: "check out the new dashboard", + url: "https://opensox.ai/dashboard" + }, + { + type: "heading", + level: 2, + content: "performance improvements" + }, + { + type: "paragraph", + content: "page load times are down by 40% across the platform. we've optimized our infrastructure to ensure you get the fastest possible experience." + }, + { + type: "paragraph", + content: "thanks for being part of the opensox community. stay tuned for more updates next month!" + } + ] + }, + { + id: "oct-2024-community-highlights", + title: "october community highlights: celebrating our pro users", + date: "2024-10-20", + excerpt: "this month we're spotlighting amazing projects built by our community and sharing tips from power users.", + coverImage: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&h=600&fit=crop&q=80", + author: "ajeet", + readTime: "4 min read", + content: [ + { + type: "paragraph", + content: "october has been an incredible month for the opensox community. let's celebrate some amazing achievements!" + }, + { + type: "heading", + level: 2, + content: "community spotlight" + }, + { + type: "paragraph", + content: "we've seen some truly innovative uses of opensox.ai this month. from startups automating their workflows to enterprises scaling their operations." + }, + { + type: "bold", + content: "featured project of the month:" + }, + { + type: "paragraph", + content: "a fintech startup used opensox to automate their customer onboarding process, reducing processing time from 2 hours to just 5 minutes. incredible work!" + }, + { + type: "heading", + level: 3, + content: "power user tips" + }, + { + type: "paragraph", + content: "here are the top 3 tips from our most active users:" + }, + { + type: "list", + items: [ + "use custom templates to save time on repetitive tasks", + "leverage batch processing for handling large datasets", + "set up webhooks for real-time integrations" + ], + align: "right" + }, + { + type: "heading", + level: 2, + content: "upcoming features" + }, + { + type: "paragraph", + content: "we're working on some exciting features for november. expect major updates to our api, new integrations, and enhanced collaboration tools." + }, + { + type: "link", + text: "join our community forum", + url: "https://community.opensox.ai" + }, + { + type: "paragraph", + content: "thank you for making opensox.ai the best ai platform for professionals. see you next month!" + } + ] + }, + { + id: "sep-2024-getting-started", + title: "getting started with opensox.ai: a guide for new pro users", + date: "2024-09-15", + excerpt: "welcome to opensox! this guide will help you make the most of your pro subscription with tips, tricks, and best practices.", + coverImage: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1200&h=600&fit=crop&q=80", + author: "ajeet", + readTime: "6 min read", + content: [ + { + type: "paragraph", + content: "welcome to opensox.ai! we're thrilled to have you as a pro user. this guide will help you unlock the full potential of our platform." + }, + { + type: "heading", + level: 2, + content: "why opensox.ai?" + }, + { + type: "paragraph", + content: "opensox.ai is built for professionals who need reliable, fast, and accurate ai capabilities. whether you're a developer, content creator, or business analyst, we've got you covered." + }, + { + type: "bold", + content: "what makes us different:" + }, + { + type: "paragraph", + content: "- enterprise-grade security and privacy\n- lightning-fast api responses\n- 99.9% uptime guarantee\n- dedicated support for pro users" + }, + { + type: "heading", + level: 3, + content: "getting started in 5 minutes" + }, + { + type: "paragraph", + content: "step 1: complete your profile and verify your email\nstep 2: explore the dashboard and familiarize yourself with key features\nstep 3: try your first api call or use our web interface\nstep 4: check out our documentation for advanced features" + }, + { + type: "link", + text: "view complete documentation", + url: "https://docs.opensox.ai" + }, + { + type: "heading", + level: 2, + content: "pro tips for success" + }, + { + type: "paragraph", + content: "1. start with our templates to save time\n2. use the playground to test before implementing\n3. monitor your usage dashboard to optimize costs\n4. join our slack community for quick help" + }, + { + type: "heading", + level: 3, + content: "need help?" + }, + { + type: "paragraph", + content: "our support team is here for you 24/7. reach out anytime via email, chat, or our community forum." + }, + { + type: "link", + text: "contact support", + url: "https://opensox.ai/support" + }, + { + type: "paragraph", + content: "we can't wait to see what you'll build with opensox.ai. happy coding!" + } + ] + } +]; diff --git a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx new file mode 100644 index 0000000..a249399 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -0,0 +1,11 @@ +import { Newsletter } from "@/types/newsletter"; +import Newsletters from "./Content"; +import { newsletters } from "./data/newsletters"; + +export default function NewslettersPage() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts new file mode 100644 index 0000000..da9fe23 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts @@ -0,0 +1,60 @@ +import { Newsletter, NewsletterContentItem } from "@/types/newsletter"; + +const matchesSearchQuery = (newsletter: Newsletter, query: string): boolean => { + const matchesBasicFields = + newsletter.title.toLowerCase().includes(query) || + newsletter.excerpt.toLowerCase().includes(query) || + newsletter.author?.toLowerCase().includes(query); + + const matchesContent = newsletter.content?.some((item: NewsletterContentItem) => { + if (item.type === "paragraph" || item.type === "heading" || item.type === "bold") { + return item.content?.toLowerCase().includes(query); + } + if (item.type === "link") { + return ( + item.text?.toLowerCase().includes(query) || + item.url?.toLowerCase().includes(query) + ); + } + return false; + }); + + return matchesBasicFields || matchesContent || false; +}; + +const matchesMonthFilter = ( + newsletter: Newsletter, + selectedMonth: string +): boolean => { + if (selectedMonth === "all") return true; + + const date = new Date(newsletter.date); + const monthYear = date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + return monthYear === selectedMonth; +}; + + +export const filterNewsletters = ( + newsletters: Newsletter[], + searchQuery: string, + selectedMonth: string +): Newsletter[] => { + let filtered = newsletters; + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter((newsletter) => + matchesSearchQuery(newsletter, query) + ); + } + + filtered = filtered.filter((newsletter) => + matchesMonthFilter(newsletter, selectedMonth) + ); + + return filtered; +}; + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts new file mode 100644 index 0000000..dd9a39e --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts @@ -0,0 +1,58 @@ +import { Newsletter } from "@/types/newsletter"; + +export const groupByMonth = (newslettersList: Newsletter[]) => { + const groups: { [key: string]: Newsletter[] } = {}; + + newslettersList.forEach((newsletter) => { + const date = new Date(newsletter.date); + const monthYear = date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + + if (!groups[monthYear]) { + groups[monthYear] = []; + } + groups[monthYear].push(newsletter); + }); + + Object.keys(groups).forEach((key) => { + groups[key].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ); + }); + + return groups; +}; + +export const sortMonthKeys = (keys: string[]): string[] => { + return keys.sort((a, b) => { + const dateA = new Date(a); + const dateB = new Date(b); + return dateB.getTime() - dateA.getTime(); + }); +}; + + +export const getAvailableMonths = (newsletters: Newsletter[]): string[] => { + const months = newsletters.map((n) => { + const date = new Date(n.date); + return date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + }); + + const uniqueMonths = Array.from(new Set(months)); + return sortMonthKeys(uniqueMonths); +}; + + +export const formatNewsletterDate = (dateString: string): string => { + return new Date(dateString).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }); +}; + diff --git a/apps/web/src/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx index eabb046..751d436 100644 --- a/apps/web/src/components/dashboard/Sidebar.tsx +++ b/apps/web/src/components/dashboard/Sidebar.tsx @@ -23,6 +23,7 @@ import { signOut } from "next-auth/react"; import { Twitter } from "../icons/icons"; import { ProfilePic } from "./ProfilePic"; import { useFilterStore } from "@/store/useFilterStore"; +import { NewspaperIcon } from "lucide-react"; const SIDEBAR_ROUTES = [ { @@ -35,6 +36,11 @@ const SIDEBAR_ROUTES = [ label: "Projects", icon: , }, + { + path: "/dashboard/newsletters", + label: "News letters", + icon: , + }, ]; const getSidebarLinkClassName = (currentPath: string, routePath: string) => { diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx new file mode 100644 index 0000000..c64ef13 --- /dev/null +++ b/apps/web/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx new file mode 100644 index 0000000..817f3b9 --- /dev/null +++ b/apps/web/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; + diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index 355bc95..9b1b21a 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -1,2 +1,3 @@ export * from "./filter" export * from "./projects" +export * from "./newsletter" diff --git a/apps/web/src/types/newsletter.ts b/apps/web/src/types/newsletter.ts new file mode 100644 index 0000000..474081f --- /dev/null +++ b/apps/web/src/types/newsletter.ts @@ -0,0 +1,43 @@ +import { StaticImageData } from "next/image"; + +export type NewsletterContentItem = + | { + type: "paragraph"; + content: string; + } + | { + type: "heading"; + level: 1 | 2 | 3; + content: string; + } + | { + type: "bold"; + content: string; + } + | { + type: "link"; + text: string; + url: string; + } + | { + type: "image"; + src: string; + alt?: string; + } + | { + type: "list"; + items: string[]; + align?: "left" | "right"; + }; + +export interface Newsletter { + id: string; + title: string; + date: string; + excerpt: string; + coverImage?: StaticImageData | string; + author?: string; + readTime?: string; + content: NewsletterContentItem[]; +} + From 43262eaeb8e1b2a2599c7b5b7a742c8ded7b5764 Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 17:07:42 +0530 Subject: [PATCH 02/11] added the packages --- apps/web/next.config.js | 4 ++++ apps/web/package.json | 1 + 2 files changed, 5 insertions(+) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index fdebc20..784c5a1 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -6,6 +6,10 @@ const nextConfig = { protocol: "https", hostname: "avatars.githubusercontent.com", }, + { + protocol: "https", + hostname: "images.unsplash.com", + }, ], }, }; diff --git a/apps/web/package.json b/apps/web/package.json index 7848f7c..dd95594 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.90.2", "@trpc/client": "^11.6.0", From 33f39dfe721716ff96c6a35275e590d977f7782a Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 17:24:45 +0530 Subject: [PATCH 03/11] minor change --- .../app/(main)/dashboard/newsletters/data/newsletters.ts | 2 +- .../(main)/dashboard/newsletters/utils/newsletter.utils.ts | 7 +++++-- apps/web/src/components/dashboard/Sidebar.tsx | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts index f24e8b6..eb90dd9 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts +++ b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts @@ -114,7 +114,7 @@ export const newsletters = [ "leverage batch processing for handling large datasets", "set up webhooks for real-time integrations" ], - align: "right" + align: "left" }, { type: "heading", diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts index dd9a39e..db8bc42 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts @@ -27,8 +27,11 @@ export const groupByMonth = (newslettersList: Newsletter[]) => { export const sortMonthKeys = (keys: string[]): string[] => { return keys.sort((a, b) => { - const dateA = new Date(a); - const dateB = new Date(b); + // Parse month and year separately for reliable date parsing + const [monthA, yearA] = a.split(" "); + const [monthB, yearB] = b.split(" "); + const dateA = new Date(`${monthA} 1, ${yearA}`); + const dateB = new Date(`${monthB} 1, ${yearB}`); return dateB.getTime() - dateA.getTime(); }); }; diff --git a/apps/web/src/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx index 751d436..5302581 100644 --- a/apps/web/src/components/dashboard/Sidebar.tsx +++ b/apps/web/src/components/dashboard/Sidebar.tsx @@ -17,13 +17,13 @@ import { StarIcon, HeartIcon, EnvelopeIcon, + NewspaperIcon, } from "@heroicons/react/24/outline"; import { useShowSidebar } from "@/store/useShowSidebar"; import { signOut } from "next-auth/react"; import { Twitter } from "../icons/icons"; import { ProfilePic } from "./ProfilePic"; -import { useFilterStore } from "@/store/useFilterStore"; -import { NewspaperIcon } from "lucide-react"; +import { useFilterStore } from "@/store/useFilterStore"; const SIDEBAR_ROUTES = [ { @@ -38,7 +38,7 @@ const SIDEBAR_ROUTES = [ }, { path: "/dashboard/newsletters", - label: "News letters", + label: "Newsletters", icon: , }, ]; From 1af781559edf77aa7cbecaa1869ad2f3a78f676b Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 17:33:05 +0530 Subject: [PATCH 04/11] fix the issue --- .../components/NewsletterContent.tsx | 42 ++++++++----------- .../newsletters/utils/newsletter.filters.ts | 19 +++++++++ .../newsletters/utils/newsletter.utils.ts | 25 +++++++++++ 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx index 7c4845d..555fe8d 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx @@ -70,39 +70,31 @@ export default function NewsletterContent({ content }: NewsletterContentProps) { case "list": const isRightAligned = item.align === "left"; - return ( -
-
    + + if (isRightAligned) { + return ( +
      {item.items.map((listItem, itemIndex) => (
    • {listItem} - {isRightAligned && ( - - • - - )} +
    • ))}
    -
+ ); + } + + return ( +
    + {item.items.map((listItem, itemIndex) => ( +
  • + {listItem} +
  • + ))} +
); default: diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts index da9fe23..5badbe3 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts @@ -1,5 +1,11 @@ import { Newsletter, NewsletterContentItem } from "@/types/newsletter"; +/** + * Checks if a newsletter matches the search query + * @param newsletter - Newsletter to check + * @param query - Search query (lowercase) + * @returns True if newsletter matches the query + */ const matchesSearchQuery = (newsletter: Newsletter, query: string): boolean => { const matchesBasicFields = newsletter.title.toLowerCase().includes(query) || @@ -22,6 +28,12 @@ const matchesSearchQuery = (newsletter: Newsletter, query: string): boolean => { return matchesBasicFields || matchesContent || false; }; +/** + * Checks if a newsletter matches the selected month filter + * @param newsletter - Newsletter to check + * @param selectedMonth - Selected month-year string or "all" + * @returns True if newsletter matches the month filter + */ const matchesMonthFilter = ( newsletter: Newsletter, selectedMonth: string @@ -37,6 +49,13 @@ const matchesMonthFilter = ( }; +/** + * Filters newsletters based on search query and month + * @param newsletters - Array of newsletters to filter + * @param searchQuery - Search query string + * @param selectedMonth - Selected month filter ("all" or month-year string) + * @returns Filtered array of newsletters + */ export const filterNewsletters = ( newsletters: Newsletter[], searchQuery: string, diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts index db8bc42..5376bd0 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts @@ -1,10 +1,19 @@ import { Newsletter } from "@/types/newsletter"; +/** + * Groups newsletters by month and year + * @param newslettersList - Array of newsletters to group + * @returns Object with month-year keys and arrays of newsletters + */ export const groupByMonth = (newslettersList: Newsletter[]) => { const groups: { [key: string]: Newsletter[] } = {}; newslettersList.forEach((newsletter) => { const date = new Date(newsletter.date); + if (isNaN(date.getTime())) { + console.warn(`Invalid date for newsletter ${newsletter.id}: ${newsletter.date}`); + return; + } const monthYear = date.toLocaleDateString("en-US", { month: "long", year: "numeric", @@ -25,6 +34,12 @@ export const groupByMonth = (newslettersList: Newsletter[]) => { return groups; }; +/** + * Sorts month keys by date (newest first) + * Uses reliable date parsing by splitting month and year components + * @param keys - Array of month-year strings (e.g., "November 2024") + * @returns Sorted array of month-year strings + */ export const sortMonthKeys = (keys: string[]): string[] => { return keys.sort((a, b) => { // Parse month and year separately for reliable date parsing @@ -37,6 +52,11 @@ export const sortMonthKeys = (keys: string[]): string[] => { }; +/** + * Gets unique months from newsletters array + * @param newsletters - Array of newsletters + * @returns Sorted array of unique month-year strings + */ export const getAvailableMonths = (newsletters: Newsletter[]): string[] => { const months = newsletters.map((n) => { const date = new Date(n.date); @@ -51,6 +71,11 @@ export const getAvailableMonths = (newsletters: Newsletter[]): string[] => { }; +/** + * Formats a date string to a readable format + * @param dateString - Date string in YYYY-MM-DD format + * @returns Formatted date string (e.g., "November 15, 2024") + */ export const formatNewsletterDate = (dateString: string): string => { return new Date(dateString).toLocaleDateString("en-US", { month: "long", From c9dc69e74d66f3605aa27b2ffc1175c9e4889907 Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 17:43:38 +0530 Subject: [PATCH 05/11] feat: enhance newsletters functionality with premium access and caching - Implemented premium access control for newsletters, displaying a loading state and a premium gate for unpaid users. - Added caching mechanism for newsletters data to improve performance. - Introduced new content types for newsletters, including code snippets and tables. - Created a dedicated component for the premium access gate with upgrade options. --- .../dashboard/newsletters/[id]/page.tsx | 15 +++++ .../components/NewsletterContent.tsx | 49 ++++++++++++++ .../components/NewsletterPremiumGate.tsx | 67 +++++++++++++++++++ .../app/(main)/dashboard/newsletters/page.tsx | 18 +++++ .../newsletters/utils/newsletter.cache.ts | 41 ++++++++++++ apps/web/src/types/newsletter.ts | 10 +++ 6 files changed, 200 insertions(+) create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx create mode 100644 apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx index f06c539..8871874 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx @@ -10,12 +10,27 @@ import Image from "next/image"; import { NewsletterContentItem } from "@/types/newsletter"; import { GeistSans } from "geist/font/sans"; import { formatNewsletterDate } from "../utils/newsletter.utils"; +import { useSubscription } from "@/hooks/useSubscription"; +import NewsletterPremiumGate from "../components/NewsletterPremiumGate"; export default function NewsletterPage() { const params = useParams(); + const { isPaidUser, isLoading } = useSubscription(); const id = params.id as string; const newsletter = newsletters.find((n) => n.id === id); + if (isLoading) { + return ( +
+
Loading...
+
+ ); + } + + if (!isPaidUser) { + return ; + } + if (!newsletter) { return (
diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx index 555fe8d..f276083 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx @@ -97,6 +97,55 @@ export default function NewsletterContent({ content }: NewsletterContentProps) { ); + case "code": + return ( +
+                
+                  {item.content}
+                
+              
+ ); + + case "table": + return ( +
+ + + + {item.headers.map((header, headerIndex) => ( + + ))} + + + + {item.rows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + +
+ {header} +
+ {cell} +
+
+ ); + default: return null; } diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx new file mode 100644 index 0000000..5a42b75 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx @@ -0,0 +1,67 @@ +"use client"; + +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Lock, Sparkles, ArrowRight } from "lucide-react"; +import { GeistSans } from "geist/font/sans"; + +export default function NewsletterPremiumGate() { + return ( +
+ + +
+
+
+
+ +
+
+
+
+ + Premium Required + + + Unlock premium to access exclusive newsletters + +
+
+ +
+

+ Get exclusive access to our premium newsletters featuring product updates, + community highlights, pro tips, and early access to new features. +

+
+ + Premium feature +
+
+
+ + + + + + +
+
+
+
+ ); +} + diff --git a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx index a249399..f8934e8 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -1,8 +1,26 @@ +"use client"; + import { Newsletter } from "@/types/newsletter"; import Newsletters from "./Content"; import { newsletters } from "./data/newsletters"; +import { useSubscription } from "@/hooks/useSubscription"; +import NewsletterPremiumGate from "./components/NewsletterPremiumGate"; export default function NewslettersPage() { + const { isPaidUser, isLoading } = useSubscription(); + + if (isLoading) { + return ( +
+
Loading...
+
+ ); + } + + if (!isPaidUser) { + return ; + } + return (
diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts new file mode 100644 index 0000000..7ddf739 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts @@ -0,0 +1,41 @@ +import { Newsletter } from "@/types/newsletter"; + +interface CacheEntry { + data: Newsletter[]; + timestamp: number; +} + +const CACHE_DURATION = 60 * 1000; // 1 minute in milliseconds +let cache: CacheEntry | null = null; + +/** + * Gets cached newsletters data if available and not expired + * @returns Cached newsletters array or null if cache is expired/missing + */ +export const getCachedNewsletters = (): Newsletter[] | null => { + if (!cache) return null; + + const now = Date.now(); + if (now - cache.timestamp > CACHE_DURATION) { + cache = null; // Clear expired cache + return null; + } + + return cache.data; +}; + +/** + * Sets newsletters data in cache with current timestamp + * @param newsletters - Array of newsletters to cache + */ +export const setCachedNewsletters = (newsletters: Newsletter[]): void => { + cache = { + data: newsletters, + timestamp: Date.now(), + }; + + // In a real implementation, this would use a proper cache store + // For now, this is a placeholder that demonstrates the caching pattern + // The actual caching would be handled at the API/data fetching level +}; + diff --git a/apps/web/src/types/newsletter.ts b/apps/web/src/types/newsletter.ts index 474081f..812b369 100644 --- a/apps/web/src/types/newsletter.ts +++ b/apps/web/src/types/newsletter.ts @@ -28,6 +28,16 @@ export type NewsletterContentItem = type: "list"; items: string[]; align?: "left" | "right"; + } + | { + type: "code"; + language?: string; + content: string; + } + | { + type: "table"; + headers: string[]; + rows: string[][]; }; export interface Newsletter { From 1d6e6b6433a26b13ec91a529ab7a990554e4c5d3 Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 17:59:26 +0530 Subject: [PATCH 06/11] refactor: update newsletter image handling and remove caching utility - Replaced tags with components for better optimization in newsletter pages and cards. - Adjusted image classes to use 'object-contain' for improved layout. - Removed the newsletter caching utility as it is no longer needed. --- .../dashboard/newsletters/[id]/page.tsx | 8 ++-- .../newsletters/components/NewsletterCard.tsx | 8 ++-- .../components/NewsletterContent.tsx | 8 ++-- .../newsletters/utils/newsletter.cache.ts | 41 ------------------- 4 files changed, 15 insertions(+), 50 deletions(-) delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx index 8871874..3943ac8 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx @@ -67,17 +67,19 @@ export default function NewsletterPage() { {newsletter.coverImage && (
{typeof newsletter.coverImage === "string" ? ( - {newsletter.title} ) : ( {newsletter.title} )}
diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx index 20ed5c0..ffba8fd 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx @@ -19,17 +19,19 @@ export default function NewsletterCard({ newsletter }: NewsletterCardProps) { {newsletter.coverImage && (
{typeof newsletter.coverImage === "string" ? ( - {newsletter.title} ) : ( {newsletter.title} )}
diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx index f276083..b705856 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx @@ -59,11 +59,13 @@ export default function NewsletterContent({ content }: NewsletterContentProps) { case "image": return ( -
- + {item.alt
); diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts deleted file mode 100644 index 7ddf739..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Newsletter } from "@/types/newsletter"; - -interface CacheEntry { - data: Newsletter[]; - timestamp: number; -} - -const CACHE_DURATION = 60 * 1000; // 1 minute in milliseconds -let cache: CacheEntry | null = null; - -/** - * Gets cached newsletters data if available and not expired - * @returns Cached newsletters array or null if cache is expired/missing - */ -export const getCachedNewsletters = (): Newsletter[] | null => { - if (!cache) return null; - - const now = Date.now(); - if (now - cache.timestamp > CACHE_DURATION) { - cache = null; // Clear expired cache - return null; - } - - return cache.data; -}; - -/** - * Sets newsletters data in cache with current timestamp - * @param newsletters - Array of newsletters to cache - */ -export const setCachedNewsletters = (newsletters: Newsletter[]): void => { - cache = { - data: newsletters, - timestamp: Date.now(), - }; - - // In a real implementation, this would use a proper cache store - // For now, this is a placeholder that demonstrates the caching pattern - // The actual caching would be handled at the API/data fetching level -}; - From ee314dd830985f7f7e6e4331e871dd825a88fd4f Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 18:27:00 +0530 Subject: [PATCH 07/11] refactor: simplify newsletter access and enhance content rendering - Removed premium access checks and loading states from newsletters and individual newsletter pages. - Updated NewsletterContent component to convert URLs in text to clickable links. - Enhanced list items to support clickable links and adjusted alignment logic. - Removed the NewsletterPremiumGate component as it is no longer needed. --- .../dashboard/newsletters/[id]/page.tsx | 15 ----- .../components/NewsletterContent.tsx | 51 ++++++++++++-- .../components/NewsletterPremiumGate.tsx | 67 ------------------- .../dashboard/newsletters/data/newsletters.ts | 34 +++++++--- .../app/(main)/dashboard/newsletters/page.tsx | 16 ----- 5 files changed, 69 insertions(+), 114 deletions(-) delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx index 3943ac8..cccf9bc 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx @@ -10,27 +10,12 @@ import Image from "next/image"; import { NewsletterContentItem } from "@/types/newsletter"; import { GeistSans } from "geist/font/sans"; import { formatNewsletterDate } from "../utils/newsletter.utils"; -import { useSubscription } from "@/hooks/useSubscription"; -import NewsletterPremiumGate from "../components/NewsletterPremiumGate"; export default function NewsletterPage() { const params = useParams(); - const { isPaidUser, isLoading } = useSubscription(); const id = params.id as string; const newsletter = newsletters.find((n) => n.id === id); - if (isLoading) { - return ( -
-
Loading...
-
- ); - } - - if (!isPaidUser) { - return ; - } - if (!newsletter) { return (
diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx index b705856..999d1c3 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx @@ -9,14 +9,35 @@ interface NewsletterContentProps { } export default function NewsletterContent({ content }: NewsletterContentProps) { + // Regex to detect URLs in text + const urlRegex = /(https?:\/\/[^\s]+)/g; + return (
{content.map((item, index) => { switch (item.type) { case "paragraph": + // Convert URLs in text to clickable links + const parts = item.content.split(urlRegex); + return (

- {item.content} + {parts.map((part, partIndex) => { + if (part.match(urlRegex)) { + return ( + + {part} + + ); + } + return {part}; + })}

); @@ -50,7 +71,7 @@ export default function NewsletterContent({ content }: NewsletterContentProps) { href={item.url} target="_blank" rel="noopener noreferrer" - className="text-primary hover:underline font-medium" + className="text-blue-500 hover:text-blue-600 hover:underline font-medium" > {item.text} @@ -71,7 +92,27 @@ export default function NewsletterContent({ content }: NewsletterContentProps) { ); case "list": - const isRightAligned = item.align === "left"; + const isRightAligned = item.align === "right"; + + const renderListItem = (listItem: string, itemIndex: number) => { + const parts = listItem.split(urlRegex); + return parts.map((part, partIndex) => { + if (part.match(urlRegex)) { + return ( + + {part} + + ); + } + return {part}; + }); + }; if (isRightAligned) { return ( @@ -81,7 +122,7 @@ export default function NewsletterContent({ content }: NewsletterContentProps) { key={itemIndex} className="text-foreground/90 flex items-center justify-end gap-2" > - {listItem} + {renderListItem(listItem, itemIndex)} ))} @@ -93,7 +134,7 @@ export default function NewsletterContent({ content }: NewsletterContentProps) {
    {item.items.map((listItem, itemIndex) => (
  • - {listItem} + {renderListItem(listItem, itemIndex)}
  • ))}
diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx deleted file mode 100644 index 5a42b75..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Lock, Sparkles, ArrowRight } from "lucide-react"; -import { GeistSans } from "geist/font/sans"; - -export default function NewsletterPremiumGate() { - return ( -
- - -
-
-
-
- -
-
-
-
- - Premium Required - - - Unlock premium to access exclusive newsletters - -
-
- -
-

- Get exclusive access to our premium newsletters featuring product updates, - community highlights, pro tips, and early access to new features. -

-
- - Premium feature -
-
-
- - - - - - -
-
-
-
- ); -} - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts index eb90dd9..5f648cf 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts +++ b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts @@ -29,7 +29,7 @@ export const newsletters = [ }, { type: "paragraph", - content: "our ai engine is now 3x faster with improved accuracy. you'll notice significantly better results across all tasks, from content generation to data analysis." + content: "our ai engine is now 3x faster with improved accuracy. you'll notice significantly better results across all tasks, from content generation to data analysis. learn more about our ai capabilities at https://opensox.ai/ai-features" }, { type: "bold", @@ -60,7 +60,7 @@ export const newsletters = [ }, { type: "paragraph", - content: "page load times are down by 40% across the platform. we've optimized our infrastructure to ensure you get the fastest possible experience." + content: "page load times are down by 40% across the platform. we've optimized our infrastructure to ensure you get the fastest possible experience. check out our performance metrics at https://opensox.ai/performance and read our technical blog post at https://blog.opensox.ai/performance-optimization" }, { type: "paragraph", @@ -96,7 +96,7 @@ export const newsletters = [ }, { type: "paragraph", - content: "a fintech startup used opensox to automate their customer onboarding process, reducing processing time from 2 hours to just 5 minutes. incredible work!" + content: "a fintech startup used opensox to automate their customer onboarding process, reducing processing time from 2 hours to just 5 minutes. incredible work! read the full case study at https://opensox.ai/case-studies/fintech-automation" }, { type: "heading", @@ -110,11 +110,11 @@ export const newsletters = [ { type: "list", items: [ - "use custom templates to save time on repetitive tasks", - "leverage batch processing for handling large datasets", - "set up webhooks for real-time integrations" + "use custom templates to save time on repetitive tasks - browse templates at https://opensox.ai/templates", + "leverage batch processing for handling large datasets - see docs at https://docs.opensox.ai/batch-processing", + "set up webhooks for real-time integrations - guide available at https://docs.opensox.ai/webhooks" ], - align: "left" + align: "right" }, { type: "heading", @@ -172,8 +172,14 @@ export const newsletters = [ content: "getting started in 5 minutes" }, { - type: "paragraph", - content: "step 1: complete your profile and verify your email\nstep 2: explore the dashboard and familiarize yourself with key features\nstep 3: try your first api call or use our web interface\nstep 4: check out our documentation for advanced features" + type: "list", + items: [ + "complete your profile at https://opensox.ai/profile and verify your email", + "explore the dashboard at https://opensox.ai/dashboard and familiarize yourself with key features", + "try your first api call at https://opensox.ai/playground or use our web interface", + "check out our documentation at https://docs.opensox.ai for advanced features" + ], + align: "left" }, { type: "link", @@ -186,8 +192,14 @@ export const newsletters = [ content: "pro tips for success" }, { - type: "paragraph", - content: "1. start with our templates to save time\n2. use the playground to test before implementing\n3. monitor your usage dashboard to optimize costs\n4. join our slack community for quick help" + type: "list", + items: [ + "start with our templates at https://opensox.ai/templates to save time", + "use the playground at https://opensox.ai/playground to test before implementing", + "monitor your usage dashboard at https://opensox.ai/usage to optimize costs", + "join our slack community at https://slack.opensox.ai for quick help" + ], + align: "left" }, { type: "heading", diff --git a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx index f8934e8..bc6cce7 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -3,24 +3,8 @@ import { Newsletter } from "@/types/newsletter"; import Newsletters from "./Content"; import { newsletters } from "./data/newsletters"; -import { useSubscription } from "@/hooks/useSubscription"; -import NewsletterPremiumGate from "./components/NewsletterPremiumGate"; export default function NewslettersPage() { - const { isPaidUser, isLoading } = useSubscription(); - - if (isLoading) { - return ( -
-
Loading...
-
- ); - } - - if (!isPaidUser) { - return ; - } - return (
From a123cb7c0ae556364136b293253fcfc5f9c587fb Mon Sep 17 00:00:00 2001 From: prudvinani Date: Sun, 16 Nov 2025 08:39:16 +0530 Subject: [PATCH 08/11] refactor: overhaul newsletters component structure and functionality - Removed the Content component and integrated its functionality directly into the NewslettersPage. - Enhanced the newsletters page with improved filtering, grouping, and rendering of newsletters. - Updated the data structure for newsletters to include new fields such as preview and contentImages. - Eliminated unused components and utility functions to streamline the codebase. - Improved the overall user experience with better layout and filtering options. --- .../(main)/dashboard/newsletters/Content.tsx | 73 ---- .../dashboard/newsletters/[id]/page.tsx | 132 ++++++-- .../newsletters/components/NewsletterCard.tsx | 67 ---- .../components/NewsletterContent.tsx | 199 ----------- .../components/NewsletterEmptyState.tsx | 29 -- .../components/NewsletterFilters.tsx | 83 ----- .../newsletters/components/NewsletterList.tsx | 45 --- .../dashboard/newsletters/data/newsletters.ts | 318 ++++++------------ .../app/(main)/dashboard/newsletters/page.tsx | 308 ++++++++++++++++- .../newsletters/utils/newsletter.filters.ts | 79 ----- .../newsletters/utils/newsletter.utils.ts | 86 ----- apps/web/src/types/newsletter.ts | 56 +-- 12 files changed, 515 insertions(+), 960 deletions(-) delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/Content.tsx delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts delete mode 100644 apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts diff --git a/apps/web/src/app/(main)/dashboard/newsletters/Content.tsx b/apps/web/src/app/(main)/dashboard/newsletters/Content.tsx deleted file mode 100644 index 3e67e8b..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/Content.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import { useState, useMemo } from "react"; -import { Newsletter } from "@/types/newsletter"; -import { GeistSans } from "geist/font/sans"; -import { getAvailableMonths } from "./utils/newsletter.utils"; -import { filterNewsletters } from "./utils/newsletter.filters"; -import NewsletterFilters from "./components/NewsletterFilters"; -import NewsletterList from "./components/NewsletterList"; -import NewsletterEmptyState from "./components/NewsletterEmptyState"; - -interface NewslettersProps { - newsletters: Newsletter[]; -} - -export default function Newsletters({ newsletters }: NewslettersProps) { - const [searchQuery, setSearchQuery] = useState(""); - const [selectedMonth, setSelectedMonth] = useState("all"); - - const availableMonths = useMemo( - () => getAvailableMonths(newsletters), - [newsletters] - ); - - const filteredNewsletters = useMemo( - () => filterNewsletters(newsletters, searchQuery, selectedMonth), - [newsletters, searchQuery, selectedMonth] - ); - - const handleClearFilters = () => { - setSearchQuery(""); - setSelectedMonth("all"); - }; - - const hasActiveFilters = - searchQuery.trim() !== "" || selectedMonth !== "all"; - - return ( -
-
-
-

- Newsletters -

-

- Stay updated with the latest features, tips, and insights from - opensox.ai -

-
- - - {filteredNewsletters.length === 0 ? ( - - ) : ( - - )} -
-
- ); -} diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx index cccf9bc..011cf52 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx @@ -3,17 +3,46 @@ import { useParams } from "next/navigation"; import Link from "next/link"; import { newsletters } from "../data/newsletters"; -import NewsletterContent from "../components/NewsletterContent"; -import { Calendar, Clock, ArrowLeft } from "lucide-react"; +import { Calendar, ArrowLeft } from "lucide-react"; import { Button } from "@/components/ui/button"; import Image from "next/image"; -import { NewsletterContentItem } from "@/types/newsletter"; import { GeistSans } from "geist/font/sans"; -import { formatNewsletterDate } from "../utils/newsletter.utils"; +/** + * Renders content with automatic URL detection and conversion to clickable links + * @param content - Text content that may contain URLs + * @returns Rendered content with clickable links + */ +const renderContent = (content: string) => { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const parts = content.split(urlRegex); + + return parts.map((part, index) => { + if (part.match(urlRegex)) { + return ( + + {part} + + ); + } + return {part}; + }); +}; + +/** + * Individual newsletter page component + * Displays a single newsletter with full content, metadata, and navigation + * @returns Newsletter detail page component + */ export default function NewsletterPage() { const params = useParams(); - const id = params.id as string; + const id = parseInt(params.id as string); const newsletter = newsletters.find((n) => n.id === id); if (!newsletter) { @@ -34,8 +63,6 @@ export default function NewsletterPage() { ); } - const formattedDate = formatNewsletterDate(newsletter.date); - return (
@@ -49,24 +76,15 @@ export default function NewsletterPage() { {/* newsletter header */}
- {newsletter.coverImage && ( + {newsletter.image && (
- {typeof newsletter.coverImage === "string" ? ( - {newsletter.title} - ) : ( - {newsletter.title} - )} + {newsletter.title}
)} @@ -77,20 +95,14 @@ export default function NewsletterPage() {
- {formattedDate} + {newsletter.date}
- {newsletter.readTime && ( -
- - {newsletter.readTime} -
- )} - {newsletter.author && by {newsletter.author}} + by {newsletter.author}
- {newsletter.excerpt && ( + {newsletter.preview && (

- {newsletter.excerpt} + {newsletter.preview}

)}
@@ -99,10 +111,57 @@ export default function NewsletterPage() {
{/* newsletter content */} -
- +
+
+ {newsletter.content.split('\n\n').map((paragraph, index) => ( +
+

+ {renderContent(paragraph)} +

+ {/* Insert images after specific paragraphs */} + {newsletter.contentImages && index === 1 && newsletter.contentImages[0] && ( +
+ {`${newsletter.title} +
+ )} + {newsletter.contentImages && index === 3 && newsletter.contentImages[1] && ( +
+ {`${newsletter.title} +
+ )} +
+ ))} +
+ {/* takeaways */} + {newsletter.takeaways && newsletter.takeaways.length > 0 && ( +
+

+ Key Takeaways +

+
    + {newsletter.takeaways.map((takeaway, index) => ( +
  • + {takeaway} +
  • + ))} +
+
+ )} + {/* footer */}
@@ -116,4 +175,3 @@ export default function NewsletterPage() {
); } - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx deleted file mode 100644 index ffba8fd..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import Link from "next/link"; -import { Calendar, Clock } from "lucide-react"; -import { Card } from "@/components/ui/card"; -import { Newsletter } from "@/types/newsletter"; -import Image from "next/image"; -import { GeistSans } from "geist/font/sans"; -import { formatNewsletterDate } from "../utils/newsletter.utils"; - -interface NewsletterCardProps { - newsletter: Newsletter; -} - -export default function NewsletterCard({ newsletter }: NewsletterCardProps) { - const formattedDate = formatNewsletterDate(newsletter.date); - - return ( - - - {newsletter.coverImage && ( -
- {typeof newsletter.coverImage === "string" ? ( - {newsletter.title} - ) : ( - {newsletter.title} - )} -
- )} -
-

- {newsletter.title} -

- -
-
- - {formattedDate} -
- {newsletter.readTime && ( -
- - {newsletter.readTime} -
- )} -
-

- {newsletter.excerpt} -

- {newsletter.author && ( -

by {newsletter.author}

- )} -
-
- - ); -} - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx deleted file mode 100644 index 999d1c3..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx +++ /dev/null @@ -1,199 +0,0 @@ -"use client"; - -import { NewsletterContentItem } from "@/types/newsletter"; -import Link from "next/link"; -import Image from "next/image"; - -interface NewsletterContentProps { - content: NewsletterContentItem[]; -} - -export default function NewsletterContent({ content }: NewsletterContentProps) { - // Regex to detect URLs in text - const urlRegex = /(https?:\/\/[^\s]+)/g; - - return ( -
- {content.map((item, index) => { - switch (item.type) { - case "paragraph": - // Convert URLs in text to clickable links - const parts = item.content.split(urlRegex); - - return ( -

- {parts.map((part, partIndex) => { - if (part.match(urlRegex)) { - return ( - - {part} - - ); - } - return {part}; - })} -

- ); - - case "heading": - const HeadingTag = `h${item.level}` as keyof JSX.IntrinsicElements; - const headingClasses = { - 1: "text-4xl font-bold mb-4 mt-8", - 2: "text-3xl font-bold mb-4 mt-8", - 3: "text-2xl font-semibold mb-3 mt-6", - }; - return ( - - {item.content} - - ); - - case "bold": - return ( -

- {item.content} -

- ); - - case "link": - return ( -
- - {item.text} - -
- ); - - case "image": - return ( -
- {item.alt -
- ); - - case "list": - const isRightAligned = item.align === "right"; - - const renderListItem = (listItem: string, itemIndex: number) => { - const parts = listItem.split(urlRegex); - return parts.map((part, partIndex) => { - if (part.match(urlRegex)) { - return ( - - {part} - - ); - } - return {part}; - }); - }; - - if (isRightAligned) { - return ( -
    - {item.items.map((listItem, itemIndex) => ( -
  • - {renderListItem(listItem, itemIndex)} - -
  • - ))} -
- ); - } - - return ( -
    - {item.items.map((listItem, itemIndex) => ( -
  • - {renderListItem(listItem, itemIndex)} -
  • - ))} -
- ); - - case "code": - return ( -
-                
-                  {item.content}
-                
-              
- ); - - case "table": - return ( -
- - - - {item.headers.map((header, headerIndex) => ( - - ))} - - - - {item.rows.map((row, rowIndex) => ( - - {row.map((cell, cellIndex) => ( - - ))} - - ))} - -
- {header} -
- {cell} -
-
- ); - - default: - return null; - } - })} -
- ); -} - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx deleted file mode 100644 index 1cec602..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; - -interface NewsletterEmptyStateProps { - hasActiveFilters: boolean; - onClearFilters: () => void; -} - -export default function NewsletterEmptyState({ - hasActiveFilters, - onClearFilters, -}: NewsletterEmptyStateProps) { - return ( -
-

- {hasActiveFilters - ? "No newsletters match your filters" - : "No newsletters yet. Check back soon!"} -

- {hasActiveFilters && ( - - )} -
- ); -} - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx deleted file mode 100644 index 9e5fd00..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import { Search, X } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -interface NewsletterFiltersProps { - searchQuery: string; - selectedMonth: string; - availableMonths: string[]; - resultCount: number; - onSearchChange: (query: string) => void; - onMonthChange: (month: string) => void; - onClearFilters: () => void; -} - -export default function NewsletterFilters({ - searchQuery, - selectedMonth, - availableMonths, - resultCount, - onSearchChange, - onMonthChange, - onClearFilters, -}: NewsletterFiltersProps) { - const hasActiveFilters = searchQuery.trim() !== "" || selectedMonth !== "all"; - - return ( -
-
-
- - onSearchChange(e.target.value)} - className="pl-10 bg-card border-border" - /> -
- - -
- - {hasActiveFilters && ( -
- - {resultCount} result{resultCount !== 1 ? "s" : ""} - - -
- )} -
- ); -} - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx deleted file mode 100644 index 9c30f9f..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { Newsletter } from "@/types/newsletter"; -import NewsletterCard from "./NewsletterCard"; -import { groupByMonth, sortMonthKeys } from "../utils/newsletter.utils"; -import { GeistSans } from "geist/font/sans"; - -interface NewsletterListProps { - newsletters: Newsletter[]; -} - -export default function NewsletterList({ newsletters }: NewsletterListProps) { - const groupedNewsletters = groupByMonth(newsletters); - const sortedMonths = sortMonthKeys(Object.keys(groupedNewsletters)); - - if (newsletters.length === 0) { - return ( -
-

- No newsletters yet. Check back soon! -

-
- ); - } - - return ( -
- {sortedMonths.map((monthYear) => ( -
-

- {monthYear} -

-
- {groupedNewsletters[monthYear].map((newsletter) => ( - - ))} -
-
- ))} -
- ); -} - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts index 5f648cf..2d9dc6a 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts +++ b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts @@ -1,224 +1,128 @@ +export interface Newsletter { + id: number; + title: string; + date: string; + author: string; + preview: string; + content: string; + image: string; + contentImages?: string[]; + takeaways: string[]; +} -export const newsletters = [ +export const newsletters: Newsletter[] = [ { - id: "nov-2024-product-updates", + id: 1, title: "november product updates: new ai features and performance improvements", - date: "2024-11-15", - excerpt: "exciting new features including enhanced ai capabilities, faster load times, and improved user experience across the platform.", - coverImage: "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=1200&h=600&fit=crop&q=80", + date: "Nov 15, 2024", author: "ajeet", - readTime: "5 min read", - content: [ - { - type: "paragraph", - content: "hey opensox community! we've been working hard this month to bring you some incredible updates that will transform how you use our platform." - }, - { - type: "heading", - level: 2, - content: "what's new this month" - }, - { - type: "paragraph", - content: "we're excited to announce several major improvements to opensox.ai that our pro users have been requesting." - }, - { - type: "heading", - level: 3, - content: "enhanced ai models" - }, - { - type: "paragraph", - content: "our ai engine is now 3x faster with improved accuracy. you'll notice significantly better results across all tasks, from content generation to data analysis. learn more about our ai capabilities at https://opensox.ai/ai-features" - }, - { - type: "bold", - content: "key improvements:" - }, - { - type: "paragraph", - content: "- 70% faster response times\n- improved accuracy on complex queries\n- better context understanding\n- support for longer inputs" - }, - { - type: "heading", - level: 3, - content: "new dashboard interface" - }, - { - type: "paragraph", - content: "we've completely redesigned the dashboard to make navigation more intuitive. the new interface puts your most-used features front and center." - }, - { - type: "link", - text: "check out the new dashboard", - url: "https://opensox.ai/dashboard" - }, - { - type: "heading", - level: 2, - content: "performance improvements" - }, - { - type: "paragraph", - content: "page load times are down by 40% across the platform. we've optimized our infrastructure to ensure you get the fastest possible experience. check out our performance metrics at https://opensox.ai/performance and read our technical blog post at https://blog.opensox.ai/performance-optimization" - }, - { - type: "paragraph", - content: "thanks for being part of the opensox community. stay tuned for more updates next month!" - } + preview: "exciting new features including enhanced ai capabilities, faster load times, and improved user experience across the platform.", + content: "hey opensox community! we've been working hard this month to bring you some incredible updates that will transform how you use our platform. we're excited to announce several major improvements to opensox.ai that our pro users have been requesting.\n\nour ai engine is now 3x faster with improved accuracy. you'll notice significantly better results across all tasks, from content generation to data analysis. we've completely rebuilt our neural network architecture from the ground up, implementing state-of-the-art algorithms that reduce latency while maintaining the highest quality outputs. the new system can handle complex queries in milliseconds, making real-time ai interactions seamless. learn more at https://opensox.ai/ai-features.\n\nwe've completely redesigned the dashboard to make navigation more intuitive. the new interface puts your most-used features front and center. we've conducted extensive user research and gathered feedback from thousands of users to create an experience that feels natural and efficient. the new layout reduces the number of clicks needed to access key features by 60%, saving you valuable time. check it out at https://opensox.ai/dashboard.\n\npage load times are down by 40% across the platform. we've optimized our infrastructure to ensure you get the fastest possible experience. our engineering team has implemented advanced caching strategies, optimized database queries, and upgraded our CDN infrastructure. these improvements mean faster page loads, smoother interactions, and a better overall experience. see our performance metrics at https://opensox.ai/performance.\n\nthanks for being part of the opensox community. stay tuned for more updates next month!", + contentImages: [ + "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=1200&h=600&fit=crop&q=80", + "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&h=600&fit=crop&q=80" + ], + image: "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=1200&h=600&fit=crop&q=80", + takeaways: [ + "70% faster response times with improved accuracy", + "completely redesigned dashboard with intuitive navigation", + "40% reduction in page load times across the platform", + "enhanced ai capabilities for better content generation and data analysis" ] }, { - id: "oct-2024-community-highlights", + id: 2, title: "october community highlights: celebrating our pro users", - date: "2024-10-20", - excerpt: "this month we're spotlighting amazing projects built by our community and sharing tips from power users.", - coverImage: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&h=600&fit=crop&q=80", + date: "Oct 20, 2024", author: "ajeet", - readTime: "4 min read", - content: [ - { - type: "paragraph", - content: "october has been an incredible month for the opensox community. let's celebrate some amazing achievements!" - }, - { - type: "heading", - level: 2, - content: "community spotlight" - }, - { - type: "paragraph", - content: "we've seen some truly innovative uses of opensox.ai this month. from startups automating their workflows to enterprises scaling their operations." - }, - { - type: "bold", - content: "featured project of the month:" - }, - { - type: "paragraph", - content: "a fintech startup used opensox to automate their customer onboarding process, reducing processing time from 2 hours to just 5 minutes. incredible work! read the full case study at https://opensox.ai/case-studies/fintech-automation" - }, - { - type: "heading", - level: 3, - content: "power user tips" - }, - { - type: "paragraph", - content: "here are the top 3 tips from our most active users:" - }, - { - type: "list", - items: [ - "use custom templates to save time on repetitive tasks - browse templates at https://opensox.ai/templates", - "leverage batch processing for handling large datasets - see docs at https://docs.opensox.ai/batch-processing", - "set up webhooks for real-time integrations - guide available at https://docs.opensox.ai/webhooks" - ], - align: "right" - }, - { - type: "heading", - level: 2, - content: "upcoming features" - }, - { - type: "paragraph", - content: "we're working on some exciting features for november. expect major updates to our api, new integrations, and enhanced collaboration tools." - }, - { - type: "link", - text: "join our community forum", - url: "https://community.opensox.ai" - }, - { - type: "paragraph", - content: "thank you for making opensox.ai the best ai platform for professionals. see you next month!" - } + preview: "this month we're spotlighting amazing projects built by our community and sharing tips from power users.", + content: "october has been an incredible month for the opensox community. let's celebrate some amazing achievements! we've seen some truly innovative uses of opensox.ai this month. from startups automating their workflows to enterprises scaling their operations.\n\na fintech startup used opensox to automate their customer onboarding process, reducing processing time from 2 hours to just 5 minutes. incredible work! they integrated our api into their existing systems and saw immediate improvements in efficiency. the automation handles document verification, background checks, and account setup seamlessly. read the full case study at https://opensox.ai/case-studies/fintech-automation.\n\nwe've also seen amazing projects from our developer community. one team built a content moderation system that processes thousands of posts per minute with 99.9% accuracy. another created an intelligent customer support bot that reduced response times by 80%. these success stories inspire us to keep building better tools.\n\nwe're working on some exciting features for november. expect major updates to our api, new integrations, and enhanced collaboration tools. we're adding support for more programming languages, expanding our webhook capabilities, and introducing team collaboration features that will make it easier to work together on projects. browse our templates at https://opensox.ai/templates. join our community forum at https://community.opensox.ai.\n\nthank you for making opensox.ai the best ai platform for professionals. see you next month!", + contentImages: [ + "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&h=600&fit=crop&q=80", + "https://images.unsplash.com/photo-1552664730-d307ca884978?w=1200&h=600&fit=crop&q=80" + ], + image: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&h=600&fit=crop&q=80", + takeaways: [ + "use custom templates to save time on repetitive tasks", + "leverage batch processing for handling large datasets", + "set up webhooks for real-time integrations", + "join our community forum for tips and support" ] }, { - id: "sep-2024-getting-started", + id: 3, title: "getting started with opensox.ai: a guide for new pro users", - date: "2024-09-15", - excerpt: "welcome to opensox! this guide will help you make the most of your pro subscription with tips, tricks, and best practices.", - coverImage: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1200&h=600&fit=crop&q=80", + date: "Sep 15, 2024", author: "ajeet", - readTime: "6 min read", - content: [ - { - type: "paragraph", - content: "welcome to opensox.ai! we're thrilled to have you as a pro user. this guide will help you unlock the full potential of our platform." - }, - { - type: "heading", - level: 2, - content: "why opensox.ai?" - }, - { - type: "paragraph", - content: "opensox.ai is built for professionals who need reliable, fast, and accurate ai capabilities. whether you're a developer, content creator, or business analyst, we've got you covered." - }, - { - type: "bold", - content: "what makes us different:" - }, - { - type: "paragraph", - content: "- enterprise-grade security and privacy\n- lightning-fast api responses\n- 99.9% uptime guarantee\n- dedicated support for pro users" - }, - { - type: "heading", - level: 3, - content: "getting started in 5 minutes" - }, - { - type: "list", - items: [ - "complete your profile at https://opensox.ai/profile and verify your email", - "explore the dashboard at https://opensox.ai/dashboard and familiarize yourself with key features", - "try your first api call at https://opensox.ai/playground or use our web interface", - "check out our documentation at https://docs.opensox.ai for advanced features" - ], - align: "left" - }, - { - type: "link", - text: "view complete documentation", - url: "https://docs.opensox.ai" - }, - { - type: "heading", - level: 2, - content: "pro tips for success" - }, - { - type: "list", - items: [ - "start with our templates at https://opensox.ai/templates to save time", - "use the playground at https://opensox.ai/playground to test before implementing", - "monitor your usage dashboard at https://opensox.ai/usage to optimize costs", - "join our slack community at https://slack.opensox.ai for quick help" - ], - align: "left" - }, - { - type: "heading", - level: 3, - content: "need help?" - }, - { - type: "paragraph", - content: "our support team is here for you 24/7. reach out anytime via email, chat, or our community forum." - }, - { - type: "link", - text: "contact support", - url: "https://opensox.ai/support" - }, - { - type: "paragraph", - content: "we can't wait to see what you'll build with opensox.ai. happy coding!" - } + preview: "welcome to opensox! this guide will help you make the most of your pro subscription with tips, tricks, and best practices.", + content: "welcome to opensox.ai! we're thrilled to have you as a pro user. this guide will help you unlock the full potential of our platform. opensox.ai is built for professionals who need reliable, fast, and accurate ai capabilities. whether you're a developer, content creator, or business analyst, we've got you covered.\n\nwe offer enterprise-grade security and privacy, lightning-fast api responses, 99.9% uptime guarantee, and dedicated support for pro users. our infrastructure is built on industry-leading cloud providers with redundant systems across multiple regions. we encrypt all data in transit and at rest, ensuring your information is always protected. our api is designed for scale, handling millions of requests per day with sub-100ms response times.\n\ngetting started is easy. first, complete your profile at https://opensox.ai/profile. this helps us personalize your experience and provide better recommendations. verify your email to unlock all features and ensure account security.\n\nnext, try your first api call at https://opensox.ai/playground. our interactive playground lets you test api endpoints without writing any code. you can experiment with different parameters, see real-time results, and copy code snippets in multiple programming languages. it's the perfect way to learn how our api works.\n\nfor more advanced features, check out our comprehensive documentation at https://docs.opensox.ai. we have detailed guides for every endpoint, code examples in popular languages, and best practices for building production applications. our documentation is constantly updated with the latest features and improvements.\n\nour support team is here for you 24/7. reach out anytime via email, chat, or our community forum. we're committed to helping you succeed with opensox.ai. we can't wait to see what you'll build with opensox.ai. happy coding!", + contentImages: [ + "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1200&h=600&fit=crop&q=80", + "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=1200&h=600&fit=crop&q=80" + ], + image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1200&h=600&fit=crop&q=80", + takeaways: [ + "complete your profile and verify your email", + "explore the dashboard and familiarize yourself with key features", + "try your first api call at the playground or use our web interface", + "check out our documentation for advanced features" + ] + }, + { + id: 4, + title: "december security updates and new api endpoints", + date: "Dec 10, 2024", + author: "ajeet", + preview: "enhanced security features and new api endpoints to make your integrations more powerful and secure.", + content: "we're excited to announce major security enhancements and new api endpoints this december. security is our top priority, and we've implemented advanced encryption protocols to protect your data.\n\nall api communications now use end-to-end encryption with tls 1.3, the latest and most secure protocol available. we've also implemented certificate pinning and perfect forward secrecy to ensure your data remains protected even in the event of a security breach. our security team conducts regular penetration testing and security audits to identify and fix vulnerabilities before they can be exploited.\n\nwe've also added new endpoints for batch processing and real-time analytics. the batch processing endpoint allows you to process thousands of requests efficiently, reducing api calls and improving performance. the real-time analytics endpoint provides instant insights into your data, enabling you to make decisions faster. these updates make it easier to build scalable applications with opensox.ai.\n\nour new api version includes improved error handling, better rate limiting, and enhanced monitoring capabilities. developers can now track their api usage in real-time, set up custom alerts, and receive detailed analytics about their integrations. learn about our security features at https://docs.opensox.ai/security. explore the new api endpoints at https://docs.opensox.ai/api. test them out in our playground at https://opensox.ai/playground.\n\nwe're committed to keeping your data safe while providing powerful tools for your projects. security is not a one-time effort but an ongoing commitment, and we're dedicated to maintaining the highest standards.", + contentImages: [ + "https://images.unsplash.com/photo-1563013544-824ae1b704d3?w=1200&h=600&fit=crop&q=80", + "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1200&h=600&fit=crop&q=80" + ], + image: "https://images.unsplash.com/photo-1563013544-824ae1b704d3?w=1200&h=600&fit=crop&q=80", + takeaways: [ + "advanced encryption protocols for all api communications", + "new batch processing endpoints for scalable applications", + "real-time analytics endpoints for instant insights", + "enhanced security documentation and best practices" + ] + }, + { + id: 5, + title: "august launch: opensox.ai is now live", + date: "Aug 1, 2024", + author: "ajeet", + preview: "we're thrilled to announce the official launch of opensox.ai, your new ai-powered platform for professionals.", + content: "after months of development and testing, we're excited to officially launch opensox.ai! this platform has been built from the ground up to provide professionals with powerful ai capabilities. whether you're building applications, creating content, or analyzing data, opensox.ai has the tools you need.\n\nour journey began with a simple vision: to make ai accessible and powerful for everyone. we've spent countless hours refining our algorithms, optimizing performance, and building an intuitive interface. the result is a platform that combines cutting-edge ai technology with ease of use.\n\nwe've started with a solid foundation featuring core ai capabilities, robust api infrastructure, and comprehensive documentation. but we're just getting started. we have an exciting roadmap ahead with features like custom model training, advanced analytics, and team collaboration tools. our team is constantly working on improvements and new features based on user feedback.\n\nwe're building a community of developers, creators, and innovators who are pushing the boundaries of what's possible with ai. join us on this journey at https://opensox.ai. sign up for your account at https://opensox.ai/signup. read our launch blog post at https://blog.opensox.ai/launch.\n\nwe can't wait to see what you'll build with opensox.ai! the possibilities are endless, and we're here to support you every step of the way.", + contentImages: [ + "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&h=600&fit=crop&q=80", + "https://images.unsplash.com/photo-1552664730-d307ca884978?w=1200&h=600&fit=crop&q=80" + ], + image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=1200&h=600&fit=crop&q=80", + takeaways: [ + "official launch of opensox.ai platform", + "powerful ai capabilities for professionals", + "solid foundation for future growth", + "join us on this exciting journey" + ] + }, + { + id: 6, + title: "july beta program: thank you to our early adopters", + date: "Jul 20, 2024", + author: "ajeet", + preview: "a heartfelt thank you to all our beta testers who helped shape opensox.ai into what it is today.", + content: "we want to extend a huge thank you to all our beta testers! your feedback has been invaluable in shaping opensox.ai. during the beta period, we received thousands of suggestions and implemented many of your ideas. the platform is stronger because of your input.\n\nover the past few months, we've worked closely with our beta community to refine every aspect of the platform. your real-world use cases have revealed insights we never would have discovered on our own. from edge cases to performance optimizations, your feedback has been instrumental in making opensox.ai production-ready.\n\nwe've learned so much from your use cases and have built features specifically based on your needs. many of the features you see today were directly inspired by beta tester requests. the batch processing capabilities, improved error messages, and enhanced documentation all came from your suggestions.\n\nas we move forward, we want to keep this collaborative spirit alive. your voice matters, and we'll continue to listen and iterate based on your feedback. check out what's new at https://opensox.ai/features. share your feedback at https://opensox.ai/feedback. join our beta community at https://community.opensox.ai.\n\nthank you for being part of this journey with us! without you, opensox.ai wouldn't be what it is today.", + contentImages: [ + "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=1200&h=600&fit=crop&q=80", + "https://images.unsplash.com/photo-1552664730-d307ca884978?w=1200&h=600&fit=crop&q=80" + ], + image: "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=1200&h=600&fit=crop&q=80", + takeaways: [ + "thank you to all beta testers for valuable feedback", + "thousands of suggestions implemented", + "features built based on user needs", + "stronger platform thanks to community input" ] } ]; diff --git a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx index bc6cce7..c3eb426 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -1,13 +1,309 @@ "use client"; -import { Newsletter } from "@/types/newsletter"; -import Newsletters from "./Content"; -import { newsletters } from "./data/newsletters"; +import { useState, useMemo } from "react"; +import Link from "next/link"; +import { GeistSans } from "geist/font/sans"; +import { Search, X, Calendar } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import Image from "next/image"; +import { newsletters, Newsletter } from "./data/newsletters"; +/** + * Groups newsletters by month and year + * @param newslettersList - Array of newsletters to group + * @returns Object with month-year keys and arrays of newsletters + */ +const groupByMonth = (newslettersList: Newsletter[]) => { + const groups: { [key: string]: Newsletter[] } = {}; + newslettersList.forEach((newsletter) => { + const date = new Date(newsletter.date); + if (isNaN(date.getTime())) { + console.warn(`Invalid date for newsletter ${newsletter.id}: ${newsletter.date}`); + return; + } + const monthYear = date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + if (!groups[monthYear]) { + groups[monthYear] = []; + } + groups[monthYear].push(newsletter); + }); + Object.keys(groups).forEach((key) => { + groups[key].sort( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() + ); + }); + return groups; +}; + +/** + * Sorts month keys by date (newest first) + * @param keys - Array of month-year strings (e.g., "November 2024") + * @returns Sorted array of month-year strings + */ +const sortMonthKeys = (keys: string[]): string[] => { + return keys.sort((a, b) => { + const [monthA, yearA] = a.split(" "); + const [monthB, yearB] = b.split(" "); + const dateA = new Date(`${monthA} 1, ${yearA}`); + const dateB = new Date(`${monthB} 1, ${yearB}`); + return dateB.getTime() - dateA.getTime(); + }); +}; + +/** + * Gets unique months from newsletters array + * @param newsletters - Array of newsletters + * @returns Sorted array of unique month-year strings + */ +const getAvailableMonths = (newsletters: Newsletter[]): string[] => { + const months = newsletters.map((n) => { + const date = new Date(n.date); + return date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + }); + const uniqueMonths = Array.from(new Set(months)); + return sortMonthKeys(uniqueMonths); +}; + +/** + * Checks if a newsletter matches the search query + * @param newsletter - Newsletter to check + * @param query - Search query (lowercase) + * @returns True if newsletter matches the query + */ +const matchesSearchQuery = (newsletter: Newsletter, query: string): boolean => { + return ( + newsletter.title.toLowerCase().includes(query) || + newsletter.preview.toLowerCase().includes(query) || + newsletter.content.toLowerCase().includes(query) || + newsletter.author.toLowerCase().includes(query) || + newsletter.takeaways.some((takeaway) => takeaway.toLowerCase().includes(query)) + ); +}; + +/** + * Checks if a newsletter matches the selected month filter + * @param newsletter - Newsletter to check + * @param selectedMonth - Selected month-year string or "all" + * @returns True if newsletter matches the month filter + */ +const matchesMonthFilter = ( + newsletter: Newsletter, + selectedMonth: string +): boolean => { + if (selectedMonth === "all") return true; + const date = new Date(newsletter.date); + const monthYear = date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + return monthYear === selectedMonth; +}; + +/** + * Filters newsletters based on search query and month + * @param newsletters - Array of newsletters to filter + * @param searchQuery - Search query string + * @param selectedMonth - Selected month filter ("all" or month-year string) + * @returns Filtered array of newsletters + */ +const filterNewsletters = ( + newsletters: Newsletter[], + searchQuery: string, + selectedMonth: string +): Newsletter[] => { + let filtered = newsletters; + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter((newsletter) => + matchesSearchQuery(newsletter, query) + ); + } + filtered = filtered.filter((newsletter) => + matchesMonthFilter(newsletter, selectedMonth) + ); + return filtered; +}; + +/** + * Newsletter card component for displaying newsletter preview + * @param newsletter - Newsletter object to display + * @returns Newsletter card component + */ +const NewsletterCard = ({ newsletter }: { newsletter: Newsletter }) => { + return ( + + + {newsletter.image && ( +
+ {newsletter.title} +
+ )} +
+

+ {newsletter.title} +

+
+
+ + {newsletter.date} +
+ by {newsletter.author} +
+

+ {newsletter.preview} +

+
+
+ + ); +}; + +/** + * Main newsletters listing page with search and filter functionality + * Displays newsletters grouped by month with search and filtering capabilities + * @returns Newsletters page component + */ export default function NewslettersPage() { + const [searchQuery, setSearchQuery] = useState(""); + const [selectedMonth, setSelectedMonth] = useState("all"); + + const availableMonths = useMemo( + () => getAvailableMonths(newsletters), + [] + ); + + const filteredNewsletters = useMemo( + () => filterNewsletters(newsletters, searchQuery, selectedMonth), + [searchQuery, selectedMonth] + ); + + const handleClearFilters = () => { + setSearchQuery(""); + setSelectedMonth("all"); + }; + + const hasActiveFilters = + searchQuery.trim() !== "" || selectedMonth !== "all"; + + const groupedNewsletters = groupByMonth(filteredNewsletters); + const sortedMonths = sortMonthKeys(Object.keys(groupedNewsletters)); + return ( -
- +
+
+ {/* Header */} +
+

+ Newsletters +

+

+ Stay updated with the latest features, tips, and insights from + opensox.ai +

+
+ + {/* Filters */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-card border-border" + /> +
+ +
+ + {hasActiveFilters && ( +
+ + {filteredNewsletters.length} result{filteredNewsletters.length !== 1 ? "s" : ""} + + +
+ )} +
+ + {/* Newsletter List or Empty State */} + {filteredNewsletters.length === 0 ? ( +
+

+ {hasActiveFilters + ? "No newsletters match your filters" + : "No newsletters yet. Check back soon!"} +

+ {hasActiveFilters && ( + + )} +
+ ) : ( +
+ {sortedMonths.map((monthYear) => ( +
+

+ {monthYear} +

+
+ {groupedNewsletters[monthYear].map((newsletter) => ( + + ))} +
+
+ ))} +
+ )} +
); -} \ No newline at end of file +} diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts deleted file mode 100644 index 5badbe3..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Newsletter, NewsletterContentItem } from "@/types/newsletter"; - -/** - * Checks if a newsletter matches the search query - * @param newsletter - Newsletter to check - * @param query - Search query (lowercase) - * @returns True if newsletter matches the query - */ -const matchesSearchQuery = (newsletter: Newsletter, query: string): boolean => { - const matchesBasicFields = - newsletter.title.toLowerCase().includes(query) || - newsletter.excerpt.toLowerCase().includes(query) || - newsletter.author?.toLowerCase().includes(query); - - const matchesContent = newsletter.content?.some((item: NewsletterContentItem) => { - if (item.type === "paragraph" || item.type === "heading" || item.type === "bold") { - return item.content?.toLowerCase().includes(query); - } - if (item.type === "link") { - return ( - item.text?.toLowerCase().includes(query) || - item.url?.toLowerCase().includes(query) - ); - } - return false; - }); - - return matchesBasicFields || matchesContent || false; -}; - -/** - * Checks if a newsletter matches the selected month filter - * @param newsletter - Newsletter to check - * @param selectedMonth - Selected month-year string or "all" - * @returns True if newsletter matches the month filter - */ -const matchesMonthFilter = ( - newsletter: Newsletter, - selectedMonth: string -): boolean => { - if (selectedMonth === "all") return true; - - const date = new Date(newsletter.date); - const monthYear = date.toLocaleDateString("en-US", { - month: "long", - year: "numeric", - }); - return monthYear === selectedMonth; -}; - - -/** - * Filters newsletters based on search query and month - * @param newsletters - Array of newsletters to filter - * @param searchQuery - Search query string - * @param selectedMonth - Selected month filter ("all" or month-year string) - * @returns Filtered array of newsletters - */ -export const filterNewsletters = ( - newsletters: Newsletter[], - searchQuery: string, - selectedMonth: string -): Newsletter[] => { - let filtered = newsletters; - - if (searchQuery.trim()) { - const query = searchQuery.toLowerCase(); - filtered = filtered.filter((newsletter) => - matchesSearchQuery(newsletter, query) - ); - } - - filtered = filtered.filter((newsletter) => - matchesMonthFilter(newsletter, selectedMonth) - ); - - return filtered; -}; - diff --git a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts deleted file mode 100644 index 5376bd0..0000000 --- a/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Newsletter } from "@/types/newsletter"; - -/** - * Groups newsletters by month and year - * @param newslettersList - Array of newsletters to group - * @returns Object with month-year keys and arrays of newsletters - */ -export const groupByMonth = (newslettersList: Newsletter[]) => { - const groups: { [key: string]: Newsletter[] } = {}; - - newslettersList.forEach((newsletter) => { - const date = new Date(newsletter.date); - if (isNaN(date.getTime())) { - console.warn(`Invalid date for newsletter ${newsletter.id}: ${newsletter.date}`); - return; - } - const monthYear = date.toLocaleDateString("en-US", { - month: "long", - year: "numeric", - }); - - if (!groups[monthYear]) { - groups[monthYear] = []; - } - groups[monthYear].push(newsletter); - }); - - Object.keys(groups).forEach((key) => { - groups[key].sort( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() - ); - }); - - return groups; -}; - -/** - * Sorts month keys by date (newest first) - * Uses reliable date parsing by splitting month and year components - * @param keys - Array of month-year strings (e.g., "November 2024") - * @returns Sorted array of month-year strings - */ -export const sortMonthKeys = (keys: string[]): string[] => { - return keys.sort((a, b) => { - // Parse month and year separately for reliable date parsing - const [monthA, yearA] = a.split(" "); - const [monthB, yearB] = b.split(" "); - const dateA = new Date(`${monthA} 1, ${yearA}`); - const dateB = new Date(`${monthB} 1, ${yearB}`); - return dateB.getTime() - dateA.getTime(); - }); -}; - - -/** - * Gets unique months from newsletters array - * @param newsletters - Array of newsletters - * @returns Sorted array of unique month-year strings - */ -export const getAvailableMonths = (newsletters: Newsletter[]): string[] => { - const months = newsletters.map((n) => { - const date = new Date(n.date); - return date.toLocaleDateString("en-US", { - month: "long", - year: "numeric", - }); - }); - - const uniqueMonths = Array.from(new Set(months)); - return sortMonthKeys(uniqueMonths); -}; - - -/** - * Formats a date string to a readable format - * @param dateString - Date string in YYYY-MM-DD format - * @returns Formatted date string (e.g., "November 15, 2024") - */ -export const formatNewsletterDate = (dateString: string): string => { - return new Date(dateString).toLocaleDateString("en-US", { - month: "long", - day: "numeric", - year: "numeric", - }); -}; - diff --git a/apps/web/src/types/newsletter.ts b/apps/web/src/types/newsletter.ts index 812b369..af1be34 100644 --- a/apps/web/src/types/newsletter.ts +++ b/apps/web/src/types/newsletter.ts @@ -1,53 +1,11 @@ -import { StaticImageData } from "next/image"; - -export type NewsletterContentItem = - | { - type: "paragraph"; - content: string; - } - | { - type: "heading"; - level: 1 | 2 | 3; - content: string; - } - | { - type: "bold"; - content: string; - } - | { - type: "link"; - text: string; - url: string; - } - | { - type: "image"; - src: string; - alt?: string; - } - | { - type: "list"; - items: string[]; - align?: "left" | "right"; - } - | { - type: "code"; - language?: string; - content: string; - } - | { - type: "table"; - headers: string[]; - rows: string[][]; - }; - export interface Newsletter { - id: string; + id: number; title: string; date: string; - excerpt: string; - coverImage?: StaticImageData | string; - author?: string; - readTime?: string; - content: NewsletterContentItem[]; + author: string; + preview: string; + content: string; + image: string; + contentImages?: string[]; + takeaways: string[]; } - From 180bb99920767a493d28d8639318dab4c7d43253 Mon Sep 17 00:00:00 2001 From: prudvinani Date: Sun, 16 Nov 2025 08:48:07 +0530 Subject: [PATCH 09/11] feat: enhance newsletter functionality and improve content rendering - Added support for additional image sources in the Next.js configuration. - Refactored the newsletter page to streamline content rendering and improve readability. - Removed redundant comments and unnecessary code, enhancing overall clarity and maintainability. - Introduced a new ContentImage component for better image handling within newsletters. --- apps/web/next.config.js | 4 ++ .../dashboard/newsletters/[id]/page.tsx | 70 +++++++------------ .../app/(main)/dashboard/newsletters/page.tsx | 47 ------------- 3 files changed, 29 insertions(+), 92 deletions(-) diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 784c5a1..365d7ef 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -6,6 +6,10 @@ const nextConfig = { protocol: "https", hostname: "avatars.githubusercontent.com", }, + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + }, { protocol: "https", hostname: "images.unsplash.com", diff --git a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx index 011cf52..86758a9 100644 --- a/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx +++ b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx @@ -8,20 +8,14 @@ import { Button } from "@/components/ui/button"; import Image from "next/image"; import { GeistSans } from "geist/font/sans"; -/** - * Renders content with automatic URL detection and conversion to clickable links - * @param content - Text content that may contain URLs - * @returns Rendered content with clickable links - */ -const renderContent = (content: string) => { - const urlRegex = /(https?:\/\/[^\s]+)/g; - const parts = content.split(urlRegex); +const renderContent = (text: string) => { + const parts = text.split(/(https?:\/\/[^\s]+)/g); - return parts.map((part, index) => { - if (part.match(urlRegex)) { + return parts.map((part, i) => { + if (part.startsWith('http://') || part.startsWith('https://')) { return ( { ); } - return {part}; + return {part}; }); }; -/** - * Individual newsletter page component - * Displays a single newsletter with full content, metadata, and navigation - * @returns Newsletter detail page component - */ +const ContentImage = ({ src, alt }: { src: string; alt: string }) => ( +
+ {alt} +
+); + export default function NewsletterPage() { const params = useParams(); const id = parseInt(params.id as string); @@ -63,10 +58,11 @@ export default function NewsletterPage() { ); } + const paragraphs = newsletter.content.split('\n\n'); + return (
- {/* back button */} - {/* newsletter header */}
{newsletter.image && (
@@ -107,46 +102,32 @@ export default function NewsletterPage() { )}
- {/* divider */}
- {/* newsletter content */}
- {newsletter.content.split('\n\n').map((paragraph, index) => ( + {paragraphs.map((paragraph, index) => (

{renderContent(paragraph)}

- {/* Insert images after specific paragraphs */} - {newsletter.contentImages && index === 1 && newsletter.contentImages[0] && ( -
- {`${newsletter.title} -
+ {newsletter.contentImages?.[0] && index === 1 && ( + )} - {newsletter.contentImages && index === 3 && newsletter.contentImages[1] && ( -
- {`${newsletter.title} -
+ {newsletter.contentImages?.[1] && index === 3 && ( + )}
))}
- {/* takeaways */} {newsletter.takeaways && newsletter.takeaways.length > 0 && (

@@ -162,7 +143,6 @@ export default function NewsletterPage() {

)} - {/* footer */}