From 34e4b80ee93f418ee98d16c11764a1700e99c928 Mon Sep 17 00:00:00 2001 From: prudvinani Date: Fri, 14 Nov 2025 16:43:56 +0530 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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 (