-
Notifications
You must be signed in to change notification settings - Fork 145
feat: implement newsletters dashboard with filtering and detail views #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 2 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
34e4b80
feat: implement newsletters dashboard with filtering and detail views
prudvinani 43262ea
added the packages
prudvinani 33f39df
minor change
prudvinani 1af7815
fix the issue
prudvinani c9dc69e
feat: enhance newsletters functionality with premium access and caching
prudvinani 1d6e6b6
refactor: update newsletter image handling and remove caching utility
prudvinani ee314dd
refactor: simplify newsletter access and enhance content rendering
prudvinani 28c9261
Merge branch 'main' into added-new-letteres
prudvinani a123cb7
refactor: overhaul newsletters component structure and functionality
prudvinani 180bb99
feat: enhance newsletter functionality and improve content rendering
prudvinani 8c4c308
refactor: update newsletter data handling and type imports
prudvinani 98e5815
feat: improve accessibility for newsletter search input
prudvinani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string>("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 ( | ||
| <div className="min-h-screen bg-background font-sans"> | ||
| <div className="max-w-4xl mx-auto px-4 py-12 sm:px-6 lg:px-8"> | ||
| <div className="mb-8 text-center"> | ||
| <h1 | ||
| className={`text-4xl font-bold text-foreground mb-4 ${GeistSans.className}`} | ||
| > | ||
| Newsletters | ||
| </h1> | ||
| <p className="text-lg text-muted-foreground max-w-2xl mx-auto"> | ||
| Stay updated with the latest features, tips, and insights from | ||
| opensox.ai | ||
| </p> | ||
| </div> | ||
| <NewsletterFilters | ||
| searchQuery={searchQuery} | ||
| selectedMonth={selectedMonth} | ||
| availableMonths={availableMonths} | ||
| resultCount={filteredNewsletters.length} | ||
| onSearchChange={setSearchQuery} | ||
| onMonthChange={setSelectedMonth} | ||
| onClearFilters={handleClearFilters} | ||
| /> | ||
|
|
||
| {filteredNewsletters.length === 0 ? ( | ||
| <NewsletterEmptyState | ||
| hasActiveFilters={hasActiveFilters} | ||
| onClearFilters={handleClearFilters} | ||
| /> | ||
| ) : ( | ||
| <NewsletterList newsletters={filteredNewsletters} /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
117 changes: 117 additions & 0 deletions
117
apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="min-h-screen bg-background flex items-center justify-center"> | ||
| <div className="text-center"> | ||
| <h1 className="text-2xl font-bold text-foreground mb-4"> | ||
| Newsletter not found | ||
| </h1> | ||
| <Link href="/dashboard/newsletters"> | ||
| <Button variant="outline"> | ||
| <ArrowLeft className="h-4 w-4 mr-2" /> | ||
| Back to newsletters | ||
| </Button> | ||
| </Link> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| const formattedDate = formatNewsletterDate(newsletter.date); | ||
|
|
||
| return ( | ||
| <div className="min-h-screen bg-background font-sans"> | ||
| <div className="max-w-3xl mx-auto px-4 py-12 sm:px-6 lg:px-8"> | ||
| {/* back button */} | ||
| <Link href="/dashboard/newsletters"> | ||
| <Button variant="ghost" className="mb-8 -ml-2 hover:bg-secondary"> | ||
| <ArrowLeft className="h-4 w-4 mr-2" /> | ||
| All newsletters | ||
| </Button> | ||
| </Link> | ||
|
|
||
| {/* newsletter header */} | ||
| <header className="mb-12"> | ||
| {newsletter.coverImage && ( | ||
| <div className="relative h-[400px] w-full overflow-hidden rounded-lg mb-8 bg-muted"> | ||
| {typeof newsletter.coverImage === "string" ? ( | ||
| <img | ||
| src={newsletter.coverImage} | ||
| alt={newsletter.title} | ||
| className="w-full h-full object-cover" | ||
| /> | ||
| ) : ( | ||
| <Image | ||
| src={newsletter.coverImage} | ||
| alt={newsletter.title} | ||
| fill | ||
| className="object-cover" | ||
| /> | ||
| )} | ||
| </div> | ||
| )} | ||
|
|
||
| <h1 className={`text-2xl md:text-4xl font-bold text-foreground mb-6 ${GeistSans.className}`}> | ||
| {newsletter.title} | ||
| </h1> | ||
|
|
||
| <div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground mb-4"> | ||
| <div className="flex items-center gap-1.5"> | ||
| <Calendar className="h-4 w-4" /> | ||
| <span>{formattedDate}</span> | ||
| </div> | ||
| {newsletter.readTime && ( | ||
| <div className="flex items-center gap-1.5"> | ||
| <Clock className="h-4 w-4" /> | ||
| <span>{newsletter.readTime}</span> | ||
| </div> | ||
| )} | ||
| {newsletter.author && <span>by {newsletter.author}</span>} | ||
| </div> | ||
|
|
||
| {newsletter.excerpt && ( | ||
| <p className="text-lg text-muted-foreground leading-relaxed"> | ||
| {newsletter.excerpt} | ||
| </p> | ||
| )} | ||
| </header> | ||
|
|
||
| {/* divider */} | ||
| <div className="border-t border-border mb-12" /> | ||
|
|
||
| {/* newsletter content */} | ||
| <div className="prose prose-lg max-w-none font-sans"> | ||
| <NewsletterContent content={newsletter.content as NewsletterContentItem[]} /> | ||
| </div> | ||
|
|
||
| {/* footer */} | ||
| <div className="mt-16 pt-8 border-t border-border"> | ||
| <Link href="/dashboard/newsletters"> | ||
| <Button variant="outline" className="w-full sm:w-auto"> | ||
| <ArrowLeft className="h-4 w-4 mr-2" /> | ||
| Back to all newsletters | ||
| </Button> | ||
| </Link> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
65 changes: 65 additions & 0 deletions
65
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <Link href={`/dashboard/newsletters/${newsletter.id}`}> | ||
| <Card className="overflow-hidden hover:shadow-lg transition-all duration-300 border-border hover:border-[#693dab] cursor-pointer"> | ||
| {newsletter.coverImage && ( | ||
| <div className="relative h-48 w-full overflow-hidden bg-muted"> | ||
| {typeof newsletter.coverImage === "string" ? ( | ||
| <img | ||
| src={newsletter.coverImage} | ||
| alt={newsletter.title} | ||
| className="w-full h-full object-cover transition-transform duration-300 hover:scale-105 " | ||
| /> | ||
| ) : ( | ||
| <Image | ||
| src={newsletter.coverImage} | ||
| alt={newsletter.title} | ||
| fill | ||
| className="object-cover transition-transform duration-300 hover:scale-105 hover:opacity-80" | ||
| /> | ||
| )} | ||
| </div> | ||
| )} | ||
| <div className="p-6 space-y-3"> | ||
| <h2 className={`text-2xl font-semibold text-foreground hover:text-primary transition-colors ${GeistSans.className}`}> | ||
| {newsletter.title} | ||
| </h2> | ||
|
|
||
| <div className="flex items-center gap-4 text-sm text-muted-foreground"> | ||
| <div className="flex items-center gap-1.5"> | ||
| <Calendar className="h-4 w-4" /> | ||
| <span>{formattedDate}</span> | ||
| </div> | ||
| {newsletter.readTime && ( | ||
| <div className="flex items-center gap-1.5"> | ||
| <Clock className="h-4 w-4" /> | ||
| <span>{newsletter.readTime}</span> | ||
| </div> | ||
| )} | ||
| </div> | ||
| <p className="text-foreground/80 line-clamp-2 leading-relaxed"> | ||
| {newsletter.excerpt} | ||
| </p> | ||
| {newsletter.author && ( | ||
| <p className="text-sm text-muted-foreground">by {newsletter.author}</p> | ||
| )} | ||
| </div> | ||
| </Card> | ||
| </Link> | ||
| ); | ||
| } | ||
|
|
115 changes: 115 additions & 0 deletions
115
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="space-y-6 font-sans"> | ||
| {content.map((item, index) => { | ||
| switch (item.type) { | ||
| case "paragraph": | ||
| return ( | ||
| <p key={index} className="text-foreground/90 leading-relaxed"> | ||
| {item.content} | ||
| </p> | ||
| ); | ||
|
|
||
| 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 ( | ||
| <HeadingTag | ||
| key={index} | ||
| className={headingClasses[item.level]} | ||
| > | ||
| {item.content} | ||
| </HeadingTag> | ||
| ); | ||
|
|
||
| case "bold": | ||
| return ( | ||
| <p key={index} className="font-semibold text-foreground mb-2"> | ||
| {item.content} | ||
| </p> | ||
| ); | ||
|
|
||
| case "link": | ||
| return ( | ||
| <div key={index} className="my-4"> | ||
| <Link | ||
| href={item.url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="text-primary hover:underline font-medium" | ||
| > | ||
| {item.text} | ||
| </Link> | ||
| </div> | ||
| ); | ||
|
|
||
| case "image": | ||
| return ( | ||
| <div key={index} className="my-8"> | ||
| <img | ||
| src={item.src} | ||
| alt={item.alt || ""} | ||
| className="w-full rounded-lg" | ||
| /> | ||
| </div> | ||
| ); | ||
|
|
||
| case "list": | ||
| const isRightAligned = item.align === "left"; | ||
| return ( | ||
| <div | ||
| key={index} | ||
| className={`my-4 ${ | ||
| isRightAligned ? "flex justify-end" : "" | ||
| }`} | ||
| > | ||
| <ul | ||
| className={`space-y-2 ${ | ||
| isRightAligned | ||
| ? "list-none text-right" | ||
| : "list-disc list-inside" | ||
| }`} | ||
| > | ||
| {item.items.map((listItem, itemIndex) => ( | ||
| <li | ||
| key={itemIndex} | ||
| className={`text-foreground/90 ${ | ||
| isRightAligned | ||
| ? "flex items-center justify-end gap-2" | ||
| : "" | ||
| }`} | ||
| > | ||
| <span>{listItem}</span> | ||
| {isRightAligned && ( | ||
| <span className="text-foreground/60 flex-shrink-0"> | ||
| • | ||
| </span> | ||
| )} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
| ); | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| })} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.