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", 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..cccf9bc --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx @@ -0,0 +1,119 @@ +"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..ffba8fd --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx @@ -0,0 +1,67 @@ +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..999d1c3 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx @@ -0,0 +1,199 @@ +"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 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..5f648cf --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts @@ -0,0 +1,224 @@ + +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. 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!" + } + ] + }, + { + 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! 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!" + } + ] + }, + { + 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: "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!" + } + ] + } +]; 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..bc6cce7 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +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..5badbe3 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts @@ -0,0 +1,79 @@ +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 new file mode 100644 index 0000000..5376bd0 --- /dev/null +++ b/apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts @@ -0,0 +1,86 @@ +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/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx index eabb046..5302581 100644 --- a/apps/web/src/components/dashboard/Sidebar.tsx +++ b/apps/web/src/components/dashboard/Sidebar.tsx @@ -17,12 +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 { useFilterStore } from "@/store/useFilterStore"; const SIDEBAR_ROUTES = [ { @@ -35,6 +36,11 @@ const SIDEBAR_ROUTES = [ label: "Projects", icon: , }, + { + path: "/dashboard/newsletters", + label: "Newsletters", + 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..812b369 --- /dev/null +++ b/apps/web/src/types/newsletter.ts @@ -0,0 +1,53 @@ +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; + title: string; + date: string; + excerpt: string; + coverImage?: StaticImageData | string; + author?: string; + readTime?: string; + content: NewsletterContentItem[]; +} +