@fullstackdatasolutions/articles 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -3
- package/dist/index.cjs +81 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +81 -86
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +18 -36
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +9 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +20 -38
- package/dist/server.js.map +1 -1
- package/package.json +10 -2
- package/src/ArticleCategoryGrid.tsx +9 -2
- package/src/ArticlesHero.tsx +15 -8
- package/src/ArticlesPage.tsx +13 -3
- package/src/__tests__/ArticlesHero.test.tsx +16 -0
- package/src/articlesConfig.ts +10 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ArticleCategoryGrid.tsx","../src/ArticleSchemas.tsx","../src/ArticleSearchBar.tsx","../src/ArticlesHero.tsx","../src/FeaturedArticle.tsx","../src/LatestArticlesSection.tsx","../src/LatestArticles.tsx","../src/ArticleCard.tsx","../src/articlesConfig.ts","../src/useArticles.ts","../src/ArticlesPage.tsx","../src/CategoryArticlesPage.tsx","../src/CommentsSection.tsx","../src/CommentForm.tsx","../src/CommentItem.tsx","../src/CommentThread.tsx"],"sourcesContent":["'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useState, useEffect } from 'react'\nimport type { CategoryInfo } from './articleTypes'\n\ntype ArticleCategoryGridProps = Readonly<{\n categories: CategoryInfo[]\n pageSize?: number\n}>\n\nexport function ArticleCategoryGrid({ categories, pageSize = 8 }: ArticleCategoryGridProps) {\n const [visibleCount, setVisibleCount] = useState(pageSize)\n\n useEffect(() => {\n setVisibleCount(pageSize)\n }, [categories, pageSize])\n\n if (categories.length === 0) return null\n\n const visible = categories.slice(0, visibleCount)\n const hasMore = visibleCount < categories.length\n\n return (\n <section className=\"py-16 bg-background\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"text-center mb-12\">\n <h2 className=\"text-3xl font-bold text-foreground mb-4\">Browse by Category</h2>\n <p className=\"text-lg text-muted-foreground\">\n Explore our articles by topic to find the insights most relevant to your interests.\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-6\">\n {visible.map((cat) => (\n <Link key={cat.slug} href={`/articles/category/${cat.slug}`}>\n <div className=\"group overflow-hidden rounded-xl border border-border shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer bg-card\">\n <div className=\"relative h-52 overflow-hidden\">\n <Image\n src={cat.featuredImage}\n alt={cat.name}\n fill\n sizes=\"(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-300\"\n />\n </div>\n <div className=\"p-4 text-center\">\n <h3 className=\"font-semibold text-foreground text-base\">{cat.name}</h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {cat.count} {cat.count === 1 ? 'article' : 'articles'}\n </p>\n </div>\n </div>\n </Link>\n ))}\n </div>\n\n {hasMore && (\n <div className=\"mt-10 text-center\">\n <button\n type=\"button\"\n onClick={() => setVisibleCount((c) => c + pageSize)}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Load more categories\n </button>\n </div>\n )}\n </div>\n </section>\n )\n}\n","import { Article } from './articleTypes'\n\ntype ArticleSchemaProps = Readonly<{\n article: Article\n articleUrl: string\n siteName: string\n}>\n\nexport function ArticleSchema({ article, articleUrl, siteName }: ArticleSchemaProps) {\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline: article.title,\n description: article.excerpt,\n image: article.featuredImage,\n ...(article.date && { datePublished: new Date(article.date).toISOString() }),\n author: { '@type': 'Person', name: article.author },\n publisher: { '@type': 'Organization', name: siteName },\n mainEntityOfPage: { '@type': 'WebPage', '@id': articleUrl },\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}\n />\n )\n}\n\ntype BreadcrumbSchemaProps = Readonly<{\n items: ReadonlyArray<{ name: string; url: string }>\n}>\n\nexport function BreadcrumbSchema({ items }: BreadcrumbSchemaProps) {\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n name: item.name,\n item: item.url,\n })),\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}\n />\n )\n}\n\ntype CollectionPageSchemaProps = Readonly<{\n title: string\n description: string\n url: string\n articleCount: number\n}>\n\nexport function CollectionPageSchema({\n title,\n description,\n url,\n articleCount,\n}: CollectionPageSchemaProps) {\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'CollectionPage',\n name: title,\n description,\n url,\n numberOfItems: articleCount,\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}\n />\n )\n}\n","import { Search, X } from 'lucide-react'\n\ntype ArticleSearchBarProps = Readonly<{\n value: string\n onChange: (query: string) => void\n disabled?: boolean\n resultCount?: number\n}>\n\nexport function ArticleSearchBar({\n value,\n onChange,\n disabled,\n resultCount,\n}: ArticleSearchBarProps) {\n return (\n <section className=\"py-8 bg-background border-b border-border\">\n <div className=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\n <input\n type=\"text\"\n placeholder=\"Search articles (3+ characters required)...\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n disabled={disabled}\n className=\"flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 pl-10 pr-10 py-3 text-lg\"\n />\n {value && (\n <button\n type=\"button\"\n onClick={() => onChange('')}\n aria-label=\"Clear search\"\n className=\"absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n )}\n </div>\n {value && (\n <div className=\"mt-2 text-sm text-muted-foreground\">\n {resultCount === 0\n ? 'No articles found matching your search.'\n : `Found ${resultCount} article${resultCount === 1 ? '' : 's'} matching \"${value}\"`}\n </div>\n )}\n </div>\n </section>\n )\n}\n","import Link from 'next/link'\n\nexport function ArticlesHero() {\n return (\n <section className=\"bg-linear-to-r from-gray-50 to-gray-100 py-16 md:py-24\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"max-w-3xl mx-auto text-center\">\n <h1 className=\"text-4xl md:text-5xl font-bold text-foreground mb-6\">\n Vox Populus Insights\n </h1>\n <p className=\"text-xl text-muted-foreground mb-8\">\n Expert analysis, campaign strategies, and voter engagement insights from the frontlines\n of democracy.\n </p>\n <div className=\"flex flex-col sm:flex-row gap-4 justify-center\">\n <Link\n href=\"#latest\"\n className=\"inline-flex items-center justify-center px-4 py-2 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md font-medium text-sm transition-colors\"\n >\n Browse Latest Articles\n </Link>\n <Link\n href=\"/contact\"\n className=\"inline-flex items-center justify-center px-4 py-2 border border-primary text-primary hover:bg-primary/10 rounded-md font-medium text-sm transition-colors\"\n >\n Contact Our Team\n </Link>\n </div>\n </div>\n </div>\n </section>\n )\n}\n","import Link from 'next/link'\nimport Image from 'next/image'\nimport { ArrowRight, Calendar, Clock, User } from 'lucide-react'\nimport type { Article } from './articleTypes'\n\ntype FeaturedArticleProps = Readonly<{\n article: Article\n}>\n\nexport function FeaturedArticle({ article }: FeaturedArticleProps) {\n return (\n <section className=\"py-16 bg-background\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"grid lg:grid-cols-2 gap-12 items-center\">\n <div>\n <span className=\"inline-block bg-primary/10 text-primary text-sm font-medium px-3 py-1 rounded-full mb-4\">\n FEATURED ARTICLE\n </span>\n <h2 className=\"text-3xl font-bold text-foreground mb-4\">{article.title}</h2>\n <p className=\"text-lg text-muted-foreground mb-6\">{article.excerpt}</p>\n <div className=\"flex items-center gap-6 text-sm text-muted-foreground mb-6\">\n {article.date && (\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"h-4 w-4\" />\n <span>{article.date}</span>\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" />\n <span>{article.readTime}</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <User className=\"h-4 w-4\" />\n <span>{article.author}</span>\n </div>\n </div>\n <Link\n href={`/articles/${article.slug}`}\n className=\"inline-flex items-center justify-center px-4 py-2 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md font-medium text-sm transition-colors\"\n >\n Read Full Article\n <ArrowRight className=\"ml-2 h-4 w-4\" />\n </Link>\n </div>\n <div className=\"bg-card rounded-lg p-8\">\n <Image\n src={article.featuredImage}\n alt={article.title}\n width={800}\n height={450}\n className=\"w-full h-64 object-cover rounded-lg mb-4\"\n />\n <div className=\"flex gap-2\">\n <span className=\"bg-primary/10 text-primary text-xs font-medium px-2 py-1 rounded\">\n {article.category}\n </span>\n </div>\n </div>\n </div>\n </div>\n </section>\n )\n}\n","'use client'\n\nimport { Search } from 'lucide-react'\nimport type { Article } from './articleTypes'\nimport { LatestArticles } from './LatestArticles'\n\ninterface LatestArticlesSectionProps {\n readonly articles: Article[]\n readonly searchQuery: string\n readonly onClearSearch: () => void\n readonly pageSize?: number\n}\n\nfunction DescriptionText({\n searchQuery,\n count,\n}: {\n readonly searchQuery: string\n readonly count: number\n}) {\n const pluralSuffix = count === 1 ? '' : 's'\n const text = searchQuery\n ? `Found ${count} article${pluralSuffix} matching your search.`\n : 'Stay informed with our latest insights on voter engagement, campaign strategies, and civic technology.'\n return <p className=\"text-lg text-muted-foreground\">{text}</p>\n}\n\nexport function LatestArticlesSection({\n articles,\n searchQuery,\n onClearSearch,\n pageSize = 6,\n}: Readonly<LatestArticlesSectionProps>) {\n return (\n <section id=\"latest\" className=\"py-16 bg-muted/50\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"text-center mb-12\">\n <h2 className=\"text-3xl font-bold text-foreground mb-4\">\n {searchQuery ? 'Search Results' : 'Latest Articles'}\n </h2>\n <DescriptionText searchQuery={searchQuery} count={articles.length} />\n </div>\n\n {articles.length === 0 && searchQuery ? (\n <div className=\"text-center py-12\">\n <div className=\"max-w-md mx-auto\">\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-4\" />\n <h3 className=\"text-lg font-semibold text-foreground mb-2\">No articles found</h3>\n <p className=\"text-muted-foreground mb-4\">\n We couldn't find any articles matching "{searchQuery}". Try adjusting\n your search terms.\n </p>\n <button\n type=\"button\"\n onClick={onClearSearch}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Clear search\n </button>\n </div>\n </div>\n ) : (\n <LatestArticles articles={articles} pageSize={pageSize} key={searchQuery} />\n )}\n </div>\n </section>\n )\n}\n","'use client'\n\nimport { useState, useEffect } from 'react'\nimport type { Article } from './articleTypes'\nimport { ArticleCard } from './ArticleCard'\n\ninterface LatestArticlesProps {\n readonly articles: Article[]\n readonly pageSize: number\n}\n\nexport function LatestArticles({ articles, pageSize }: Readonly<LatestArticlesProps>) {\n const [visibleCount, setVisibleCount] = useState(pageSize)\n\n useEffect(() => {\n setVisibleCount(pageSize)\n }, [articles, pageSize])\n\n const visible = articles.slice(0, visibleCount)\n const hasMore = visibleCount < articles.length\n\n return (\n <div>\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-8\">\n {visible.map((article) => (\n <ArticleCard key={article.slug} article={article} />\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-10 text-center\">\n <button\n type=\"button\"\n onClick={() => setVisibleCount((c) => c + pageSize)}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Load more articles\n </button>\n </div>\n )}\n </div>\n )\n}\n","'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { ArrowRight, Calendar, Clock } from 'lucide-react'\nimport type { Article } from './articleTypes'\n\ntype ArticleCardProps = Readonly<{\n article: Article\n}>\n\nexport function ArticleCard({ article }: ArticleCardProps) {\n return (\n <div className=\"rounded-lg border border-border bg-card text-card-foreground shadow-sm hover:shadow-lg transition-shadow duration-300\">\n <div className=\"p-0\">\n <Image\n src={article.featuredImage}\n alt={article.title}\n className=\"w-full h-48 object-cover rounded-t-lg\"\n width={400}\n height={200}\n />\n </div>\n <div className=\"p-6\">\n <div className=\"flex gap-2 mb-3\">\n <span className=\"bg-primary/10 text-primary text-xs font-medium px-2 py-1 rounded\">\n {article.category}\n </span>\n </div>\n <h3 className=\"text-lg font-semibold leading-none tracking-tight mb-2\">\n <Link\n href={`/articles/${article.slug}`}\n className=\"hover:text-indigo-600 transition-colors\"\n >\n {article.title}\n </Link>\n </h3>\n <p className=\"text-muted-foreground mb-4 line-clamp-3\">{article.excerpt}</p>\n <div className=\"flex items-center justify-between text-sm text-muted-foreground border-t pt-4\">\n <div className=\"flex items-center gap-3\">\n {article.date && (\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"h-3 w-3\" />\n <span>{article.date}</span>\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n <Clock className=\"h-3 w-3\" />\n <span>{article.readTime}</span>\n </div>\n </div>\n <Link\n href={`/articles/${article.slug}`}\n className=\"inline-flex items-center justify-center h-8 w-8 p-0 rounded-md hover:bg-accent transition-colors\"\n aria-label={`Read ${article.title}`}\n >\n <ArrowRight className=\"h-4 w-4\" />\n </Link>\n </div>\n </div>\n </div>\n )\n}\n","/** Keys for each renderable section of the articles listing page. */\nexport type ArticlesSection =\n | 'hero'\n | 'search'\n | 'featured'\n | 'latest'\n | 'categories'\n | 'newsletter'\n\n/**\n * CSS custom-property overrides applied to the library's wrapper div.\n * All fields are optional; omitted fields fall back to the consuming app's Tailwind theme.\n */\nexport interface ArticlesTheme {\n /** Font family for the articles section. Example: `\"'Inter', sans-serif\"` */\n fontFamily?: string\n /** Color for article card titles and section headings. Example: `'#111827'` */\n headerColor?: string\n /** Color for body and excerpt text. Example: `'#6b7280'` */\n textColor?: string\n /** Background color for sections and cards. Example: `'#ffffff'` */\n backgroundColor?: string\n /** Color for \"Read More\" links and inline links. Example: `'#4f46e5'` */\n linkColor?: string\n /** Hover color for links. Example: `'#4338ca'` */\n linkHoverColor?: string\n /** Text decoration for links. Values: `'underline'` | `'none'` */\n linkTextDecoration?: string\n /** Font weight for headings. Example: `700` */\n headerFontWeight?: string | number\n /** Font size for article card titles. Example: `'1.25rem'` */\n headerFontSize?: string\n /** Font size for body text. Example: `'1rem'` */\n bodyFontSize?: string\n /** Line height for body text. Example: `'1.75'` */\n lineHeight?: string\n /** Border radius for cards. Example: `'0.5rem'` */\n borderRadius?: string\n}\n\n/** Per-category text used in meta descriptions and on the category hero page. */\nexport interface CategoryDescription {\n /** Short description — used in meta description and category grid card. */\n short: string\n /** Long description — rendered as a hero paragraph on the category page. */\n long?: string\n}\n\n/** Controls the comments feature. Attach to `ArticlesConfig.comments`. */\nexport interface CommentsConfig {\n /** Set to `true` to enable comments globally across all articles. */\n enabled: boolean\n /** Maximum reply nesting depth. Default: `1` (replies to top-level only). */\n maxDepth?: number\n /**\n * Per-article overrides keyed by slug.\n * Example: `{ 'sensitive-article': false }` disables comments on that article only.\n */\n perArticleOverride?: Record<string, boolean>\n}\n\n/** Top-level configuration object. Pass one instance to every library component. */\nexport interface ArticlesConfig {\n /** Canonical base URL of the site, used in metadata and JSON-LD. Example: `'https://yoursite.com'` */\n siteUrl: string\n /** Site name shown in metadata titles and JSON-LD publisher fields. Example: `'Vox Populus'` */\n siteName: string\n /** Number of articles shown per page in the grid and loaded on each \"Load more\" click. Default: `6` */\n pageSize?: number\n /** Number of category cards shown before a \"Load more categories\" button appears. Default: `8` */\n categoriesPageSize?: number\n /**\n * Ordered list of sections to render on the articles listing page.\n * Omit a key to hide that section entirely. Default: `['hero','search','featured','latest','categories']`\n */\n layout?: ArticlesSection[]\n /** CSS custom-property overrides for colors, fonts, and spacing. All fields optional. */\n theme?: ArticlesTheme\n /**\n * Descriptive text for each category, keyed by category slug.\n * A plain string is treated as the short description only.\n * Example: `{ 'campaigns': { short: 'Campaign tips', long: 'Full paragraph...' } }`\n */\n categoryDescriptions?: Record<string, string | CategoryDescription>\n /** Comments configuration. Omit or set `enabled: false` to hide comments entirely. */\n comments?: CommentsConfig\n}\n\nexport const DEFAULT_PAGE_SIZE = 6\nexport const DEFAULT_CATEGORIES_PAGE_SIZE = 8\n\nexport const DEFAULT_LAYOUT: ArticlesSection[] = [\n 'hero',\n 'search',\n 'featured',\n 'latest',\n 'categories',\n]\n","'use client'\n\nimport { useState, useCallback, useEffect, useRef } from 'react'\nimport { Article, CategoryInfo } from './articleTypes'\n\nexport interface UseArticlesReturn {\n articles: Article[]\n categories: CategoryInfo[]\n searchQuery: string\n loading: boolean\n error: string | null\n handleSearch: (query: string) => void\n}\n\nexport function useArticles(): UseArticlesReturn {\n const [searchQuery, setSearchQuery] = useState('')\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const [articles, setArticles] = useState<Article[]>([])\n const [categories, setCategories] = useState<CategoryInfo[]>([])\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n const fetchArticles = useCallback(async (query: string) => {\n try {\n setLoading(true)\n const url = query ? `/api/articles?q=${encodeURIComponent(query)}` : '/api/articles'\n const response = await fetch(url)\n\n if (!response.ok) throw new Error('Failed to fetch articles')\n\n const data = await response.json()\n setArticles(data.articles)\n\n if (!query?.trim()) {\n const catMap = new Map<string, { count: number; featuredImage: string }>()\n for (const article of data.articles as Article[]) {\n for (const cat of article.categories) {\n if (!catMap.has(cat)) {\n catMap.set(cat, { count: 0, featuredImage: article.featuredImage })\n }\n catMap.get(cat)!.count++\n }\n }\n setCategories(\n Array.from(catMap.entries())\n .map(([name, { count, featuredImage }]) => ({\n name,\n slug: name\n .toLowerCase()\n .replaceAll(/\\s+/g, '-')\n .replaceAll(/[^a-z0-9-]/g, ''),\n count,\n featuredImage,\n }))\n .sort((a, b) => b.count - a.count)\n )\n }\n\n setError(null)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load articles')\n setArticles([])\n } finally {\n setLoading(false)\n }\n }, [])\n\n useEffect(() => {\n fetchArticles('')\n }, [fetchArticles])\n\n const handleSearch = useCallback(\n (query: string) => {\n setSearchQuery(query)\n if (debounceRef.current) clearTimeout(debounceRef.current)\n if (query === '') {\n fetchArticles('')\n } else if (query.length >= 3) {\n debounceRef.current = setTimeout(() => fetchArticles(query), 1000)\n }\n },\n [fetchArticles]\n )\n\n useEffect(\n () => () => {\n if (debounceRef.current) clearTimeout(debounceRef.current)\n },\n []\n )\n\n return { articles, categories, searchQuery, loading, error, handleSearch }\n}\n","'use client'\n\nimport React from 'react'\nimport type { Article, CategoryInfo } from './articleTypes'\nimport { ArticleCategoryGrid } from './ArticleCategoryGrid'\nimport { CollectionPageSchema } from './ArticleSchemas'\nimport { ArticleSearchBar } from './ArticleSearchBar'\nimport { ArticlesHero } from './ArticlesHero'\nimport { FeaturedArticle } from './FeaturedArticle'\nimport { LatestArticlesSection } from './LatestArticlesSection'\nimport {\n ArticlesConfig,\n ArticlesSection,\n ArticlesTheme,\n DEFAULT_CATEGORIES_PAGE_SIZE,\n DEFAULT_LAYOUT,\n DEFAULT_PAGE_SIZE,\n} from './articlesConfig'\nimport { useArticles } from './useArticles'\n\ninterface SectionContext {\n articles: Article[]\n displayedArticles: Article[]\n categories: CategoryInfo[]\n searchQuery: string\n loading: boolean\n error: string | null\n handleSearch: (query: string) => void\n pageSize: number\n categoriesPageSize: number\n}\n\nfunction buildThemeVars(theme?: ArticlesTheme): React.CSSProperties {\n if (!theme) return {}\n const vars: Record<string, string> = {}\n if (theme.fontFamily) vars['--articles-font-family'] = theme.fontFamily\n if (theme.headerColor) vars['--articles-header-color'] = theme.headerColor\n if (theme.textColor) vars['--articles-text-color'] = theme.textColor\n if (theme.backgroundColor) vars['--articles-bg-color'] = theme.backgroundColor\n if (theme.linkColor) vars['--articles-link-color'] = theme.linkColor\n if (theme.linkHoverColor) vars['--articles-link-hover-color'] = theme.linkHoverColor\n if (theme.linkTextDecoration) vars['--articles-link-decoration'] = theme.linkTextDecoration\n if (theme.headerFontWeight) vars['--articles-header-font-weight'] = String(theme.headerFontWeight)\n if (theme.headerFontSize) vars['--articles-header-font-size'] = theme.headerFontSize\n if (theme.bodyFontSize) vars['--articles-body-font-size'] = theme.bodyFontSize\n if (theme.lineHeight) vars['--articles-line-height'] = theme.lineHeight\n if (theme.borderRadius) vars['--articles-border-radius'] = theme.borderRadius\n return vars as React.CSSProperties\n}\n\nfunction renderSection(section: ArticlesSection, ctx: SectionContext): React.ReactNode {\n const {\n articles,\n displayedArticles,\n categories,\n searchQuery,\n loading,\n error,\n handleSearch,\n pageSize,\n categoriesPageSize,\n } = ctx\n\n switch (section) {\n case 'hero':\n return <ArticlesHero key=\"hero\" />\n\n case 'search':\n return (\n <ArticleSearchBar\n key=\"search\"\n value={searchQuery}\n onChange={handleSearch}\n disabled={loading}\n resultCount={articles.length}\n />\n )\n\n case 'featured':\n return articles.length > 0 && !searchQuery ? (\n <FeaturedArticle key=\"featured\" article={articles[0]} />\n ) : null\n\n case 'latest':\n if (loading && articles.length === 0) {\n return (\n <section key=\"latest\" id=\"latest\" className=\"py-16 bg-muted/50 flex-1\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center py-24\">\n <div className=\"text-center\">\n <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4\" />\n <p className=\"text-muted-foreground\">Loading articles...</p>\n </div>\n </div>\n </section>\n )\n }\n if (error) {\n return (\n <section key=\"latest\" id=\"latest\" className=\"py-16 bg-muted/50 flex-1\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center\">\n <div className=\"bg-red-50 border border-red-200 rounded-lg p-6 max-w-md text-center\">\n <h3 className=\"text-lg font-semibold text-red-800 mb-2\">Error Loading Articles</h3>\n <p className=\"text-red-600 mb-4\">{error}</p>\n <button\n type=\"button\"\n onClick={() => handleSearch('')}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Try again\n </button>\n </div>\n </div>\n </section>\n )\n }\n return (\n <LatestArticlesSection\n key=\"latest\"\n articles={displayedArticles}\n searchQuery={searchQuery}\n onClearSearch={() => handleSearch('')}\n pageSize={pageSize}\n />\n )\n\n case 'categories':\n return searchQuery ? null : (\n <ArticleCategoryGrid\n key=\"categories\"\n categories={categories}\n pageSize={categoriesPageSize}\n />\n )\n\n // 'newsletter' is intentionally omitted — inject it via the layout prop in the consuming app\n default:\n return null\n }\n}\n\nexport function ArticlesPage({ config }: Readonly<{ config: ArticlesConfig }>) {\n const state = useArticles()\n const layout = config.layout ?? DEFAULT_LAYOUT\n const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE\n const categoriesPageSize = config.categoriesPageSize ?? DEFAULT_CATEGORIES_PAGE_SIZE\n const themeVars = buildThemeVars(config.theme)\n\n const hasFeatured = layout.includes('featured')\n const articlesWithoutFeatured = hasFeatured ? state.articles.slice(1) : state.articles\n const displayedArticles = state.searchQuery ? state.articles : articlesWithoutFeatured\n\n const ctx: SectionContext = { ...state, displayedArticles, pageSize, categoriesPageSize }\n\n return (\n <div style={themeVars}>\n {layout.map((section) => renderSection(section, ctx))}\n {state.articles.length > 0 && (\n <CollectionPageSchema\n title={`Articles | ${config.siteName}`}\n description={`Browse all ${state.articles.length} articles on ${config.siteName}.`}\n url={`${config.siteUrl}/articles`}\n articleCount={state.articles.length}\n />\n )}\n </div>\n )\n}\n","'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { BreadcrumbSchema } from './ArticleSchemas'\nimport { LatestArticles } from './LatestArticles'\nimport { ArticlesConfig, DEFAULT_PAGE_SIZE } from './articlesConfig'\nimport type { Article } from './articleTypes'\n\nfunction getCategoryDescription(\n config: ArticlesConfig,\n slug: string,\n categoryName: string\n): { short: string; long?: string } {\n const raw = config.categoryDescriptions?.[slug]\n if (!raw) return { short: `Browse all articles in ${categoryName}.` }\n if (typeof raw === 'string') return { short: raw }\n return raw\n}\n\ntype CategoryArticlesPageProps = Readonly<{\n category: string\n articles: Article[]\n config: ArticlesConfig\n}>\n\nexport function CategoryArticlesPage({ category, articles, config }: CategoryArticlesPageProps) {\n if (articles.length === 0) return null\n\n const categoryName = articles[0].category\n const heroImage = articles[0].featuredImage\n const description = getCategoryDescription(config, category, categoryName)\n const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE\n const siteUrl = config.siteUrl.replace(/\\/$/, '')\n\n return (\n <div>\n <BreadcrumbSchema\n items={[\n { name: 'Home', url: siteUrl },\n { name: 'Articles', url: `${siteUrl}/articles` },\n { name: categoryName, url: `${siteUrl}/articles/category/${category}` },\n ]}\n />\n {/* Hero */}\n <section className=\"relative h-72 md:h-96 flex items-center justify-center overflow-hidden\">\n <Image\n src={heroImage}\n alt={categoryName}\n fill\n sizes=\"100vw\"\n className=\"object-cover\"\n priority\n />\n <div className=\"absolute inset-0 bg-black/60\" />\n <div className=\"relative z-10 text-center text-white px-4\">\n <p className=\"text-sm font-semibold uppercase tracking-widest mb-3 opacity-80\">\n Category\n </p>\n <h1 className=\"text-4xl md:text-5xl font-bold mb-3\">{categoryName}</h1>\n {description.long && (\n <p className=\"text-lg opacity-90 max-w-2xl mx-auto mt-2\">{description.long}</p>\n )}\n <p className=\"text-lg opacity-70 mt-2\">\n {articles.length} article{articles.length === 1 ? '' : 's'}\n </p>\n </div>\n </section>\n\n {/* Articles */}\n <section className=\"py-16 bg-muted/50 flex-1\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"mb-8 flex items-center justify-between\">\n <Link href=\"/articles\" className=\"text-sm text-primary hover:underline\">\n ← All Articles\n </Link>\n <p className=\"text-sm text-muted-foreground\">{description.short}</p>\n </div>\n <LatestArticles articles={articles} pageSize={pageSize} />\n </div>\n </section>\n </div>\n )\n}\n","'use client'\n\nimport { useSession, signIn } from 'next-auth/react'\nimport { useCallback, useEffect, useState } from 'react'\nimport { CommentForm } from './CommentForm'\nimport { CommentThread } from './CommentThread'\nimport type { CommentsConfig } from './articlesConfig'\nimport type { ArticleCommentWithReplies } from './commentTypes'\n\ninterface CommentsSectionProps {\n readonly articleSlug: string\n readonly config: CommentsConfig\n}\n\nexport function CommentsSection({ articleSlug, config }: CommentsSectionProps) {\n const { data: session, status } = useSession()\n const [comments, setComments] = useState<ArticleCommentWithReplies[]>([])\n const [loading, setLoading] = useState(true)\n const [fetchError, setFetchError] = useState<string | null>(null)\n\n const perArticleEnabled = config.perArticleOverride?.[articleSlug] ?? true\n const enabled = config.enabled && perArticleEnabled\n\n const currentUserId = session?.user?.email ?? session?.user?.name ?? undefined\n\n const fetchComments = useCallback(async () => {\n setLoading(true)\n setFetchError(null)\n try {\n const res = await fetch(`/api/articles/${articleSlug}/comments`)\n if (!res.ok) throw new Error('Failed to load comments')\n const data = (await res.json()) as { comments: ArticleCommentWithReplies[] }\n setComments(data.comments)\n } catch {\n setFetchError('Could not load comments. Please try again.')\n } finally {\n setLoading(false)\n }\n }, [articleSlug])\n\n useEffect(() => {\n if (enabled) {\n fetchComments().catch(() => undefined)\n }\n }, [fetchComments, enabled])\n\n if (!enabled) return null\n\n const count = comments.length\n const label = count === 1 ? '1 comment' : `${count} comments`\n\n return (\n <section aria-label=\"Comments\" className=\"mt-12 pt-8 border-t border-border space-y-6\">\n <h2 className=\"text-xl font-semibold text-foreground\">{loading ? 'Comments' : label}</h2>\n\n {status === 'authenticated' ? (\n <CommentForm articleSlug={articleSlug} onSuccess={fetchComments} />\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n <button\n onClick={() => signIn()}\n className=\"text-primary underline hover:no-underline focus:outline-none\"\n >\n Sign in\n </button>{' '}\n to join the discussion.\n </p>\n )}\n\n {fetchError && <p className=\"text-sm text-destructive\">{fetchError}</p>}\n\n {loading ? (\n <p className=\"text-sm text-muted-foreground\">Loading comments…</p>\n ) : (\n <CommentThread\n comments={comments}\n articleSlug={articleSlug}\n currentUserId={currentUserId}\n onRefresh={fetchComments}\n />\n )}\n </section>\n )\n}\n","'use client'\n\nimport { useState } from 'react'\n\nconst MAX_LENGTH = 2000\nconst WARN_THRESHOLD = 1800\n\nfunction submitLabel(submitting: boolean, parentId?: string): string {\n if (submitting) return 'Posting…'\n return parentId ? 'Reply' : 'Post comment'\n}\n\ninterface CommentFormProps {\n readonly articleSlug: string\n readonly parentId?: string\n readonly onSuccess: () => void\n readonly onCancel?: () => void\n}\n\nexport function CommentForm({ articleSlug, parentId, onSuccess, onCancel }: CommentFormProps) {\n const [body, setBody] = useState('')\n const [submitting, setSubmitting] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const remaining = MAX_LENGTH - body.length\n\n async function handleSubmit(e: React.FormEvent) {\n e.preventDefault()\n if (!body.trim() || submitting) return\n\n setSubmitting(true)\n setError(null)\n\n try {\n const res = await fetch(`/api/articles/${articleSlug}/comments`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ body: body.trim(), parentId: parentId ?? null }),\n })\n\n if (!res.ok) {\n const data = (await res.json()) as { error?: string }\n setError(data.error ?? 'Failed to post comment')\n return\n }\n\n setBody('')\n onSuccess()\n } catch {\n setError('Network error — please try again')\n } finally {\n setSubmitting(false)\n }\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-2\">\n <textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n maxLength={MAX_LENGTH}\n rows={parentId ? 3 : 4}\n placeholder={parentId ? 'Write a reply…' : 'Join the discussion…'}\n disabled={submitting}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 resize-none\"\n aria-label={parentId ? 'Reply text' : 'Comment text'}\n />\n\n {remaining <= MAX_LENGTH - WARN_THRESHOLD && (\n <p className={`text-xs ${remaining < 0 ? 'text-destructive' : 'text-muted-foreground'}`}>\n {remaining} characters remaining\n </p>\n )}\n\n {error && <p className=\"text-xs text-destructive\">{error}</p>}\n\n <div className=\"flex gap-2\">\n <button\n type=\"submit\"\n disabled={submitting || body.trim().length === 0 || body.length > MAX_LENGTH}\n className=\"rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {submitLabel(submitting, parentId)}\n </button>\n\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"rounded-md px-4 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground\"\n >\n Cancel\n </button>\n )}\n </div>\n </form>\n )\n}\n","'use client'\n\nimport { useState } from 'react'\nimport { CommentForm } from './CommentForm'\nimport type { ArticleComment, ArticleCommentWithReplies } from './commentTypes'\n\nfunction relativeTime(iso: string): string {\n const diff = Date.now() - new Date(iso).getTime()\n const minutes = Math.floor(diff / 60_000)\n if (minutes < 1) return 'just now'\n if (minutes < 60) return `${minutes}m ago`\n const hours = Math.floor(minutes / 60)\n if (hours < 24) return `${hours}h ago`\n const days = Math.floor(hours / 24)\n if (days < 30) return `${days}d ago`\n const months = Math.floor(days / 30)\n if (months < 12) return `${months}mo ago`\n return `${Math.floor(months / 12)}y ago`\n}\n\ninterface CommentItemProps {\n readonly comment: ArticleCommentWithReplies | ArticleComment\n readonly articleSlug: string\n readonly currentUserId?: string\n readonly isReply?: boolean\n readonly onRefresh: () => void\n}\n\nexport function CommentItem({\n comment,\n articleSlug,\n currentUserId,\n isReply = false,\n onRefresh,\n}: CommentItemProps) {\n const [showReplyForm, setShowReplyForm] = useState(false)\n const [deleting, setDeleting] = useState(false)\n\n const replies = 'replies' in comment ? comment.replies : []\n const canReply = !isReply && !!currentUserId && !comment.isDeleted\n const canDelete = !!currentUserId && currentUserId === comment.authorId && !comment.isDeleted\n\n async function handleDelete() {\n if (deleting) return\n setDeleting(true)\n try {\n await fetch(`/api/articles/${articleSlug}/comments/${comment.id}`, { method: 'DELETE' })\n onRefresh()\n } finally {\n setDeleting(false)\n }\n }\n\n return (\n <div className={`${isReply ? 'ml-8 border-l-2 border-border pl-4' : ''}`}>\n <div className=\"rounded-lg bg-muted/40 p-3 space-y-1\">\n {comment.isDeleted ? (\n <p className=\"text-sm italic text-muted-foreground\">Comment removed</p>\n ) : (\n <>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-foreground\">{comment.authorName}</span>\n <span className=\"text-xs text-muted-foreground\">\n {relativeTime(comment.createdAt)}\n </span>\n </div>\n <p className=\"text-sm text-foreground whitespace-pre-wrap break-words\">\n {comment.body}\n </p>\n <div className=\"flex gap-3 pt-1\">\n {canReply && (\n <button\n onClick={() => setShowReplyForm((v) => !v)}\n className=\"text-xs text-muted-foreground hover:text-foreground\"\n >\n {showReplyForm ? 'Cancel' : 'Reply'}\n </button>\n )}\n {canDelete && (\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"text-xs text-muted-foreground hover:text-destructive disabled:opacity-50\"\n >\n {deleting ? 'Deleting…' : 'Delete'}\n </button>\n )}\n </div>\n </>\n )}\n </div>\n\n {showReplyForm && (\n <div className=\"mt-2 ml-2\">\n <CommentForm\n articleSlug={articleSlug}\n parentId={comment.id}\n onSuccess={() => {\n setShowReplyForm(false)\n onRefresh()\n }}\n onCancel={() => setShowReplyForm(false)}\n />\n </div>\n )}\n\n {replies.length > 0 && (\n <div className=\"mt-2 space-y-2\">\n {replies.map((reply) => (\n <CommentItem\n key={reply.id}\n comment={{ ...reply, replies: [] }}\n articleSlug={articleSlug}\n currentUserId={currentUserId}\n isReply\n onRefresh={onRefresh}\n />\n ))}\n </div>\n )}\n </div>\n )\n}\n","'use client'\n\nimport { CommentItem } from './CommentItem'\nimport type { ArticleCommentWithReplies } from './commentTypes'\n\ninterface CommentThreadProps {\n readonly comments: ArticleCommentWithReplies[]\n readonly articleSlug: string\n readonly currentUserId?: string\n readonly onRefresh: () => void\n}\n\nexport function CommentThread({\n comments,\n articleSlug,\n currentUserId,\n onRefresh,\n}: CommentThreadProps) {\n if (comments.length === 0) {\n return (\n <p className=\"text-sm text-muted-foreground text-center py-6\">\n No comments yet. Be the first to share your thoughts.\n </p>\n )\n }\n\n return (\n <div className=\"space-y-4\">\n {comments.map((comment) => (\n <CommentItem\n key={comment.id}\n comment={comment}\n articleSlug={articleSlug}\n currentUserId={currentUserId}\n onRefresh={onRefresh}\n />\n ))}\n </div>\n )\n}\n"],"mappings":"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","names":["jsx","jsx","jsxs","Link","jsx","jsxs","Link","Image","jsx","jsxs","Search","useState","useEffect","Image","Link","ArrowRight","Calendar","Clock","jsx","jsxs","jsx","jsxs","useState","useEffect","jsx","jsxs","Search","useState","useEffect","useState","useEffect","jsx","jsxs","Image","Link","jsx","jsxs","Image","Link","useCallback","useEffect","useState","useState","jsx","jsxs","useState","e","useState","jsx","jsxs","useState","jsx","jsx","jsxs","useState","useCallback","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/ArticleCategoryGrid.tsx","../src/ArticleSchemas.tsx","../src/ArticleSearchBar.tsx","../src/ArticlesHero.tsx","../src/FeaturedArticle.tsx","../src/LatestArticlesSection.tsx","../src/LatestArticles.tsx","../src/ArticleCard.tsx","../src/articlesConfig.ts","../src/useArticles.ts","../src/ArticlesPage.tsx","../src/CategoryArticlesPage.tsx","../src/CommentsSection.tsx","../src/CommentForm.tsx","../src/CommentItem.tsx","../src/CommentThread.tsx"],"sourcesContent":["'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { useState, useEffect } from 'react'\nimport type { CategoryInfo } from './articleTypes'\n\ntype ArticleCategoryGridProps = Readonly<{\n categories: CategoryInfo[]\n pageSize?: number\n}>\n\nexport function ArticleCategoryGrid({ categories, pageSize = 8 }: ArticleCategoryGridProps) {\n const [visibleCount, setVisibleCount] = useState(pageSize)\n\n useEffect(() => {\n setVisibleCount(pageSize)\n }, [categories, pageSize])\n\n if (categories.length === 0) return null\n\n const visible = categories.slice(0, visibleCount)\n const hasMore = visibleCount < categories.length\n\n return (\n <section className=\"py-16 bg-background\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"text-center mb-12\">\n <h2 className=\"text-3xl font-bold text-foreground mb-4\">Browse by Category</h2>\n <p className=\"text-lg text-muted-foreground\">\n Explore our articles by topic to find the insights most relevant to your interests.\n </p>\n </div>\n\n <div\n className=\"sm:grid-cols-4\"\n style={{\n display: 'grid',\n gridTemplateColumns: 'repeat(1, minmax(0, 1fr))',\n gap: '1.5rem',\n }}\n >\n {visible.map((cat) => (\n <Link key={cat.slug} href={`/articles/category/${cat.slug}`}>\n <div className=\"group overflow-hidden rounded-xl border border-border shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer bg-card\">\n <div className=\"overflow-hidden\" style={{ position: 'relative', height: '13rem' }}>\n <Image\n src={cat.featuredImage}\n alt={cat.name}\n fill\n sizes=\"(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-300\"\n />\n </div>\n <div className=\"p-4 text-center\">\n <h3 className=\"font-semibold text-foreground text-base\">{cat.name}</h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {cat.count} {cat.count === 1 ? 'article' : 'articles'}\n </p>\n </div>\n </div>\n </Link>\n ))}\n </div>\n\n {hasMore && (\n <div className=\"mt-10 text-center\">\n <button\n type=\"button\"\n onClick={() => setVisibleCount((c) => c + pageSize)}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Load more categories\n </button>\n </div>\n )}\n </div>\n </section>\n )\n}\n","import { Article } from './articleTypes'\n\ntype ArticleSchemaProps = Readonly<{\n article: Article\n articleUrl: string\n siteName: string\n}>\n\nexport function ArticleSchema({ article, articleUrl, siteName }: ArticleSchemaProps) {\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline: article.title,\n description: article.excerpt,\n image: article.featuredImage,\n ...(article.date && { datePublished: new Date(article.date).toISOString() }),\n author: { '@type': 'Person', name: article.author },\n publisher: { '@type': 'Organization', name: siteName },\n mainEntityOfPage: { '@type': 'WebPage', '@id': articleUrl },\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}\n />\n )\n}\n\ntype BreadcrumbSchemaProps = Readonly<{\n items: ReadonlyArray<{ name: string; url: string }>\n}>\n\nexport function BreadcrumbSchema({ items }: BreadcrumbSchemaProps) {\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n name: item.name,\n item: item.url,\n })),\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}\n />\n )\n}\n\ntype CollectionPageSchemaProps = Readonly<{\n title: string\n description: string\n url: string\n articleCount: number\n}>\n\nexport function CollectionPageSchema({\n title,\n description,\n url,\n articleCount,\n}: CollectionPageSchemaProps) {\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'CollectionPage',\n name: title,\n description,\n url,\n numberOfItems: articleCount,\n }\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}\n />\n )\n}\n","import { Search, X } from 'lucide-react'\n\ntype ArticleSearchBarProps = Readonly<{\n value: string\n onChange: (query: string) => void\n disabled?: boolean\n resultCount?: number\n}>\n\nexport function ArticleSearchBar({\n value,\n onChange,\n disabled,\n resultCount,\n}: ArticleSearchBarProps) {\n return (\n <section className=\"py-8 bg-background border-b border-border\">\n <div className=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\n <input\n type=\"text\"\n placeholder=\"Search articles (3+ characters required)...\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n disabled={disabled}\n className=\"flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 pl-10 pr-10 py-3 text-lg\"\n />\n {value && (\n <button\n type=\"button\"\n onClick={() => onChange('')}\n aria-label=\"Clear search\"\n className=\"absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n )}\n </div>\n {value && (\n <div className=\"mt-2 text-sm text-muted-foreground\">\n {resultCount === 0\n ? 'No articles found matching your search.'\n : `Found ${resultCount} article${resultCount === 1 ? '' : 's'} matching \"${value}\"`}\n </div>\n )}\n </div>\n </section>\n )\n}\n","import Link from 'next/link'\n\nconst DEFAULT_TITLE = 'Vox Populus Insights'\nconst DEFAULT_DESCRIPTION =\n 'Expert analysis, campaign strategies, and voter engagement insights from the frontlines of democracy.'\n\ntype ArticlesHeroProps = Readonly<{\n title?: string\n description?: string\n}>\n\nexport function ArticlesHero({\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n}: ArticlesHeroProps) {\n return (\n <section className=\"bg-linear-to-r from-gray-50 to-gray-100 py-16 md:py-24\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"max-w-3xl mx-auto text-center\">\n <h1 className=\"text-4xl md:text-5xl font-bold text-foreground mb-6\">{title}</h1>\n <p className=\"text-xl text-muted-foreground mb-8\">{description}</p>\n <div className=\"flex flex-col sm:flex-row gap-4 justify-center\">\n <Link\n href=\"#latest\"\n className=\"inline-flex items-center justify-center px-4 py-2 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md font-medium text-sm transition-colors\"\n >\n Browse Latest Articles\n </Link>\n <Link\n href=\"/contact\"\n className=\"inline-flex items-center justify-center px-4 py-2 border border-primary text-primary hover:bg-primary/10 rounded-md font-medium text-sm transition-colors\"\n >\n Contact Our Team\n </Link>\n </div>\n </div>\n </div>\n </section>\n )\n}\n","import Link from 'next/link'\nimport Image from 'next/image'\nimport { ArrowRight, Calendar, Clock, User } from 'lucide-react'\nimport type { Article } from './articleTypes'\n\ntype FeaturedArticleProps = Readonly<{\n article: Article\n}>\n\nexport function FeaturedArticle({ article }: FeaturedArticleProps) {\n return (\n <section className=\"py-16 bg-background\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"grid lg:grid-cols-2 gap-12 items-center\">\n <div>\n <span className=\"inline-block bg-primary/10 text-primary text-sm font-medium px-3 py-1 rounded-full mb-4\">\n FEATURED ARTICLE\n </span>\n <h2 className=\"text-3xl font-bold text-foreground mb-4\">{article.title}</h2>\n <p className=\"text-lg text-muted-foreground mb-6\">{article.excerpt}</p>\n <div className=\"flex items-center gap-6 text-sm text-muted-foreground mb-6\">\n {article.date && (\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"h-4 w-4\" />\n <span>{article.date}</span>\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" />\n <span>{article.readTime}</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <User className=\"h-4 w-4\" />\n <span>{article.author}</span>\n </div>\n </div>\n <Link\n href={`/articles/${article.slug}`}\n className=\"inline-flex items-center justify-center px-4 py-2 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md font-medium text-sm transition-colors\"\n >\n Read Full Article\n <ArrowRight className=\"ml-2 h-4 w-4\" />\n </Link>\n </div>\n <div className=\"bg-card rounded-lg p-8\">\n <Image\n src={article.featuredImage}\n alt={article.title}\n width={800}\n height={450}\n className=\"w-full h-64 object-cover rounded-lg mb-4\"\n />\n <div className=\"flex gap-2\">\n <span className=\"bg-primary/10 text-primary text-xs font-medium px-2 py-1 rounded\">\n {article.category}\n </span>\n </div>\n </div>\n </div>\n </div>\n </section>\n )\n}\n","'use client'\n\nimport { Search } from 'lucide-react'\nimport type { Article } from './articleTypes'\nimport { LatestArticles } from './LatestArticles'\n\ninterface LatestArticlesSectionProps {\n readonly articles: Article[]\n readonly searchQuery: string\n readonly onClearSearch: () => void\n readonly pageSize?: number\n}\n\nfunction DescriptionText({\n searchQuery,\n count,\n}: {\n readonly searchQuery: string\n readonly count: number\n}) {\n const pluralSuffix = count === 1 ? '' : 's'\n const text = searchQuery\n ? `Found ${count} article${pluralSuffix} matching your search.`\n : 'Stay informed with our latest insights on voter engagement, campaign strategies, and civic technology.'\n return <p className=\"text-lg text-muted-foreground\">{text}</p>\n}\n\nexport function LatestArticlesSection({\n articles,\n searchQuery,\n onClearSearch,\n pageSize = 6,\n}: Readonly<LatestArticlesSectionProps>) {\n return (\n <section id=\"latest\" className=\"py-16 bg-muted/50\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"text-center mb-12\">\n <h2 className=\"text-3xl font-bold text-foreground mb-4\">\n {searchQuery ? 'Search Results' : 'Latest Articles'}\n </h2>\n <DescriptionText searchQuery={searchQuery} count={articles.length} />\n </div>\n\n {articles.length === 0 && searchQuery ? (\n <div className=\"text-center py-12\">\n <div className=\"max-w-md mx-auto\">\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-4\" />\n <h3 className=\"text-lg font-semibold text-foreground mb-2\">No articles found</h3>\n <p className=\"text-muted-foreground mb-4\">\n We couldn't find any articles matching "{searchQuery}". Try adjusting\n your search terms.\n </p>\n <button\n type=\"button\"\n onClick={onClearSearch}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Clear search\n </button>\n </div>\n </div>\n ) : (\n <LatestArticles articles={articles} pageSize={pageSize} key={searchQuery} />\n )}\n </div>\n </section>\n )\n}\n","'use client'\n\nimport { useState, useEffect } from 'react'\nimport type { Article } from './articleTypes'\nimport { ArticleCard } from './ArticleCard'\n\ninterface LatestArticlesProps {\n readonly articles: Article[]\n readonly pageSize: number\n}\n\nexport function LatestArticles({ articles, pageSize }: Readonly<LatestArticlesProps>) {\n const [visibleCount, setVisibleCount] = useState(pageSize)\n\n useEffect(() => {\n setVisibleCount(pageSize)\n }, [articles, pageSize])\n\n const visible = articles.slice(0, visibleCount)\n const hasMore = visibleCount < articles.length\n\n return (\n <div>\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-8\">\n {visible.map((article) => (\n <ArticleCard key={article.slug} article={article} />\n ))}\n </div>\n {hasMore && (\n <div className=\"mt-10 text-center\">\n <button\n type=\"button\"\n onClick={() => setVisibleCount((c) => c + pageSize)}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Load more articles\n </button>\n </div>\n )}\n </div>\n )\n}\n","'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { ArrowRight, Calendar, Clock } from 'lucide-react'\nimport type { Article } from './articleTypes'\n\ntype ArticleCardProps = Readonly<{\n article: Article\n}>\n\nexport function ArticleCard({ article }: ArticleCardProps) {\n return (\n <div className=\"rounded-lg border border-border bg-card text-card-foreground shadow-sm hover:shadow-lg transition-shadow duration-300\">\n <div className=\"p-0\">\n <Image\n src={article.featuredImage}\n alt={article.title}\n className=\"w-full h-48 object-cover rounded-t-lg\"\n width={400}\n height={200}\n />\n </div>\n <div className=\"p-6\">\n <div className=\"flex gap-2 mb-3\">\n <span className=\"bg-primary/10 text-primary text-xs font-medium px-2 py-1 rounded\">\n {article.category}\n </span>\n </div>\n <h3 className=\"text-lg font-semibold leading-none tracking-tight mb-2\">\n <Link\n href={`/articles/${article.slug}`}\n className=\"hover:text-indigo-600 transition-colors\"\n >\n {article.title}\n </Link>\n </h3>\n <p className=\"text-muted-foreground mb-4 line-clamp-3\">{article.excerpt}</p>\n <div className=\"flex items-center justify-between text-sm text-muted-foreground border-t pt-4\">\n <div className=\"flex items-center gap-3\">\n {article.date && (\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"h-3 w-3\" />\n <span>{article.date}</span>\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n <Clock className=\"h-3 w-3\" />\n <span>{article.readTime}</span>\n </div>\n </div>\n <Link\n href={`/articles/${article.slug}`}\n className=\"inline-flex items-center justify-center h-8 w-8 p-0 rounded-md hover:bg-accent transition-colors\"\n aria-label={`Read ${article.title}`}\n >\n <ArrowRight className=\"h-4 w-4\" />\n </Link>\n </div>\n </div>\n </div>\n )\n}\n","/** Keys for each renderable section of the articles listing page. */\nexport type ArticlesSection =\n | 'hero'\n | 'search'\n | 'featured'\n | 'latest'\n | 'categories'\n | 'newsletter'\n\n/**\n * CSS custom-property overrides applied to the library's wrapper div.\n * All fields are optional; omitted fields fall back to the consuming app's Tailwind theme.\n */\nexport interface ArticlesTheme {\n /** Font family for the articles section. Example: `\"'Inter', sans-serif\"` */\n fontFamily?: string\n /** Color for article card titles and section headings. Example: `'#111827'` */\n headerColor?: string\n /** Color for body and excerpt text. Example: `'#6b7280'` */\n textColor?: string\n /** Background color for sections and cards. Example: `'#ffffff'` */\n backgroundColor?: string\n /** Color for \"Read More\" links and inline links. Example: `'#4f46e5'` */\n linkColor?: string\n /** Hover color for links. Example: `'#4338ca'` */\n linkHoverColor?: string\n /** Text decoration for links. Values: `'underline'` | `'none'` */\n linkTextDecoration?: string\n /** Font weight for headings. Example: `700` */\n headerFontWeight?: string | number\n /** Font size for article card titles. Example: `'1.25rem'` */\n headerFontSize?: string\n /** Font size for body text. Example: `'1rem'` */\n bodyFontSize?: string\n /** Line height for body text. Example: `'1.75'` */\n lineHeight?: string\n /** Border radius for cards. Example: `'0.5rem'` */\n borderRadius?: string\n}\n\n/** Per-category text used in meta descriptions and on the category hero page. */\nexport interface CategoryDescription {\n /** Short description — used in meta description and category grid card. */\n short: string\n /** Long description — rendered as a hero paragraph on the category page. */\n long?: string\n}\n\n/** Controls the comments feature. Attach to `ArticlesConfig.comments`. */\nexport interface CommentsConfig {\n /** Set to `true` to enable comments globally across all articles. */\n enabled: boolean\n /** Maximum reply nesting depth. Default: `1` (replies to top-level only). */\n maxDepth?: number\n /**\n * Per-article overrides keyed by slug.\n * Example: `{ 'sensitive-article': false }` disables comments on that article only.\n */\n perArticleOverride?: Record<string, boolean>\n}\n\n/** Text content for the hero section at the top of the articles listing page. */\nexport interface HeroConfig {\n /** Main heading displayed in the hero. Default: `'Vox Populus Insights'` */\n title?: string\n /** Subheading paragraph displayed beneath the title. Default: the built-in description. */\n description?: string\n}\n\n/** Top-level configuration object. Pass one instance to every library component. */\nexport interface ArticlesConfig {\n /** Canonical base URL of the site, used in metadata and JSON-LD. Example: `'https://yoursite.com'` */\n siteUrl: string\n /** Site name shown in metadata titles and JSON-LD publisher fields. Example: `'Vox Populus'` */\n siteName: string\n /** Number of articles shown per page in the grid and loaded on each \"Load more\" click. Default: `6` */\n pageSize?: number\n /** Number of category cards shown before a \"Load more categories\" button appears. Default: `8` */\n categoriesPageSize?: number\n /**\n * Ordered list of sections to render on the articles listing page.\n * Omit a key to hide that section entirely. Default: `['hero','search','featured','latest','categories']`\n */\n layout?: ArticlesSection[]\n /** CSS custom-property overrides for colors, fonts, and spacing. All fields optional. */\n theme?: ArticlesTheme\n /**\n * Descriptive text for each category, keyed by category slug.\n * A plain string is treated as the short description only.\n * Example: `{ 'campaigns': { short: 'Campaign tips', long: 'Full paragraph...' } }`\n */\n categoryDescriptions?: Record<string, string | CategoryDescription>\n /** Comments configuration. Omit or set `enabled: false` to hide comments entirely. */\n comments?: CommentsConfig\n /** Hero section title and description. Omit to use the built-in defaults. */\n hero?: HeroConfig\n}\n\nexport const DEFAULT_PAGE_SIZE = 6\nexport const DEFAULT_CATEGORIES_PAGE_SIZE = 8\n\nexport const DEFAULT_LAYOUT: ArticlesSection[] = [\n 'hero',\n 'search',\n 'featured',\n 'latest',\n 'categories',\n]\n","'use client'\n\nimport { useState, useCallback, useEffect, useRef } from 'react'\nimport { Article, CategoryInfo } from './articleTypes'\n\nexport interface UseArticlesReturn {\n articles: Article[]\n categories: CategoryInfo[]\n searchQuery: string\n loading: boolean\n error: string | null\n handleSearch: (query: string) => void\n}\n\nexport function useArticles(): UseArticlesReturn {\n const [searchQuery, setSearchQuery] = useState('')\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const [articles, setArticles] = useState<Article[]>([])\n const [categories, setCategories] = useState<CategoryInfo[]>([])\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n const fetchArticles = useCallback(async (query: string) => {\n try {\n setLoading(true)\n const url = query ? `/api/articles?q=${encodeURIComponent(query)}` : '/api/articles'\n const response = await fetch(url)\n\n if (!response.ok) throw new Error('Failed to fetch articles')\n\n const data = await response.json()\n setArticles(data.articles)\n\n if (!query?.trim()) {\n const catMap = new Map<string, { count: number; featuredImage: string }>()\n for (const article of data.articles as Article[]) {\n for (const cat of article.categories) {\n if (!catMap.has(cat)) {\n catMap.set(cat, { count: 0, featuredImage: article.featuredImage })\n }\n catMap.get(cat)!.count++\n }\n }\n setCategories(\n Array.from(catMap.entries())\n .map(([name, { count, featuredImage }]) => ({\n name,\n slug: name\n .toLowerCase()\n .replaceAll(/\\s+/g, '-')\n .replaceAll(/[^a-z0-9-]/g, ''),\n count,\n featuredImage,\n }))\n .sort((a, b) => b.count - a.count)\n )\n }\n\n setError(null)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load articles')\n setArticles([])\n } finally {\n setLoading(false)\n }\n }, [])\n\n useEffect(() => {\n fetchArticles('')\n }, [fetchArticles])\n\n const handleSearch = useCallback(\n (query: string) => {\n setSearchQuery(query)\n if (debounceRef.current) clearTimeout(debounceRef.current)\n if (query === '') {\n fetchArticles('')\n } else if (query.length >= 3) {\n debounceRef.current = setTimeout(() => fetchArticles(query), 1000)\n }\n },\n [fetchArticles]\n )\n\n useEffect(\n () => () => {\n if (debounceRef.current) clearTimeout(debounceRef.current)\n },\n []\n )\n\n return { articles, categories, searchQuery, loading, error, handleSearch }\n}\n","'use client'\n\nimport React from 'react'\nimport type { Article, CategoryInfo } from './articleTypes'\nimport { ArticleCategoryGrid } from './ArticleCategoryGrid'\nimport { CollectionPageSchema } from './ArticleSchemas'\nimport { ArticleSearchBar } from './ArticleSearchBar'\nimport { ArticlesHero } from './ArticlesHero'\nimport { FeaturedArticle } from './FeaturedArticle'\nimport { LatestArticlesSection } from './LatestArticlesSection'\nimport {\n ArticlesConfig,\n ArticlesSection,\n ArticlesTheme,\n DEFAULT_CATEGORIES_PAGE_SIZE,\n DEFAULT_LAYOUT,\n DEFAULT_PAGE_SIZE,\n} from './articlesConfig'\nimport { useArticles } from './useArticles'\n\ninterface SectionContext {\n articles: Article[]\n displayedArticles: Article[]\n categories: CategoryInfo[]\n searchQuery: string\n loading: boolean\n error: string | null\n handleSearch: (query: string) => void\n pageSize: number\n categoriesPageSize: number\n}\n\nfunction buildThemeVars(theme?: ArticlesTheme): React.CSSProperties {\n if (!theme) return {}\n const vars: Record<string, string> = {}\n if (theme.fontFamily) vars['--articles-font-family'] = theme.fontFamily\n if (theme.headerColor) vars['--articles-header-color'] = theme.headerColor\n if (theme.textColor) vars['--articles-text-color'] = theme.textColor\n if (theme.backgroundColor) vars['--articles-bg-color'] = theme.backgroundColor\n if (theme.linkColor) vars['--articles-link-color'] = theme.linkColor\n if (theme.linkHoverColor) vars['--articles-link-hover-color'] = theme.linkHoverColor\n if (theme.linkTextDecoration) vars['--articles-link-decoration'] = theme.linkTextDecoration\n if (theme.headerFontWeight) vars['--articles-header-font-weight'] = String(theme.headerFontWeight)\n if (theme.headerFontSize) vars['--articles-header-font-size'] = theme.headerFontSize\n if (theme.bodyFontSize) vars['--articles-body-font-size'] = theme.bodyFontSize\n if (theme.lineHeight) vars['--articles-line-height'] = theme.lineHeight\n if (theme.borderRadius) vars['--articles-border-radius'] = theme.borderRadius\n return vars as React.CSSProperties\n}\n\nfunction renderSection(\n section: ArticlesSection,\n ctx: SectionContext,\n config: ArticlesConfig\n): React.ReactNode {\n const {\n articles,\n displayedArticles,\n categories,\n searchQuery,\n loading,\n error,\n handleSearch,\n pageSize,\n categoriesPageSize,\n } = ctx\n\n switch (section) {\n case 'hero':\n return (\n <ArticlesHero\n key=\"hero\"\n title={config.hero?.title}\n description={config.hero?.description}\n />\n )\n\n case 'search':\n return (\n <ArticleSearchBar\n key=\"search\"\n value={searchQuery}\n onChange={handleSearch}\n disabled={loading}\n resultCount={articles.length}\n />\n )\n\n case 'featured':\n return articles.length > 0 && !searchQuery ? (\n <FeaturedArticle key=\"featured\" article={articles[0]} />\n ) : null\n\n case 'latest':\n if (loading && articles.length === 0) {\n return (\n <section key=\"latest\" id=\"latest\" className=\"py-16 bg-muted/50 flex-1\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center py-24\">\n <div className=\"text-center\">\n <div className=\"animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4\" />\n <p className=\"text-muted-foreground\">Loading articles...</p>\n </div>\n </div>\n </section>\n )\n }\n if (error) {\n return (\n <section key=\"latest\" id=\"latest\" className=\"py-16 bg-muted/50 flex-1\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center\">\n <div className=\"bg-red-50 border border-red-200 rounded-lg p-6 max-w-md text-center\">\n <h3 className=\"text-lg font-semibold text-red-800 mb-2\">Error Loading Articles</h3>\n <p className=\"text-red-600 mb-4\">{error}</p>\n <button\n type=\"button\"\n onClick={() => handleSearch('')}\n className=\"inline-flex items-center justify-center px-4 py-2 border border-input bg-background hover:bg-accent rounded-md font-medium text-sm transition-colors\"\n >\n Try again\n </button>\n </div>\n </div>\n </section>\n )\n }\n return (\n <LatestArticlesSection\n key=\"latest\"\n articles={displayedArticles}\n searchQuery={searchQuery}\n onClearSearch={() => handleSearch('')}\n pageSize={pageSize}\n />\n )\n\n case 'categories':\n return searchQuery ? null : (\n <ArticleCategoryGrid\n key=\"categories\"\n categories={categories}\n pageSize={categoriesPageSize}\n />\n )\n\n // 'newsletter' is intentionally omitted — inject it via the layout prop in the consuming app\n default:\n return null\n }\n}\n\nexport function ArticlesPage({ config }: Readonly<{ config: ArticlesConfig }>) {\n const state = useArticles()\n const layout = config.layout ?? DEFAULT_LAYOUT\n const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE\n const categoriesPageSize = config.categoriesPageSize ?? DEFAULT_CATEGORIES_PAGE_SIZE\n const themeVars = buildThemeVars(config.theme)\n\n const hasFeatured = layout.includes('featured')\n const articlesWithoutFeatured = hasFeatured ? state.articles.slice(1) : state.articles\n const displayedArticles = state.searchQuery ? state.articles : articlesWithoutFeatured\n\n const ctx: SectionContext = { ...state, displayedArticles, pageSize, categoriesPageSize }\n\n return (\n <div style={themeVars}>\n {layout.map((section) => renderSection(section, ctx, config))}\n {state.articles.length > 0 && (\n <CollectionPageSchema\n title={`Articles | ${config.siteName}`}\n description={`Browse all ${state.articles.length} articles on ${config.siteName}.`}\n url={`${config.siteUrl}/articles`}\n articleCount={state.articles.length}\n />\n )}\n </div>\n )\n}\n","'use client'\n\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { BreadcrumbSchema } from './ArticleSchemas'\nimport { LatestArticles } from './LatestArticles'\nimport { ArticlesConfig, DEFAULT_PAGE_SIZE } from './articlesConfig'\nimport type { Article } from './articleTypes'\n\nfunction getCategoryDescription(\n config: ArticlesConfig,\n slug: string,\n categoryName: string\n): { short: string; long?: string } {\n const raw = config.categoryDescriptions?.[slug]\n if (!raw) return { short: `Browse all articles in ${categoryName}.` }\n if (typeof raw === 'string') return { short: raw }\n return raw\n}\n\ntype CategoryArticlesPageProps = Readonly<{\n category: string\n articles: Article[]\n config: ArticlesConfig\n}>\n\nexport function CategoryArticlesPage({ category, articles, config }: CategoryArticlesPageProps) {\n if (articles.length === 0) return null\n\n const categoryName = articles[0].category\n const heroImage = articles[0].featuredImage\n const description = getCategoryDescription(config, category, categoryName)\n const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE\n const siteUrl = config.siteUrl.replace(/\\/$/, '')\n\n return (\n <div>\n <BreadcrumbSchema\n items={[\n { name: 'Home', url: siteUrl },\n { name: 'Articles', url: `${siteUrl}/articles` },\n { name: categoryName, url: `${siteUrl}/articles/category/${category}` },\n ]}\n />\n {/* Hero */}\n <section className=\"relative h-72 md:h-96 flex items-center justify-center overflow-hidden\">\n <Image\n src={heroImage}\n alt={categoryName}\n fill\n sizes=\"100vw\"\n className=\"object-cover\"\n priority\n />\n <div className=\"absolute inset-0 bg-black/60\" />\n <div className=\"relative z-10 text-center text-white px-4\">\n <p className=\"text-sm font-semibold uppercase tracking-widest mb-3 opacity-80\">\n Category\n </p>\n <h1 className=\"text-4xl md:text-5xl font-bold mb-3\">{categoryName}</h1>\n {description.long && (\n <p className=\"text-lg opacity-90 max-w-2xl mx-auto mt-2\">{description.long}</p>\n )}\n <p className=\"text-lg opacity-70 mt-2\">\n {articles.length} article{articles.length === 1 ? '' : 's'}\n </p>\n </div>\n </section>\n\n {/* Articles */}\n <section className=\"py-16 bg-muted/50 flex-1\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\">\n <div className=\"mb-8 flex items-center justify-between\">\n <Link href=\"/articles\" className=\"text-sm text-primary hover:underline\">\n ← All Articles\n </Link>\n <p className=\"text-sm text-muted-foreground\">{description.short}</p>\n </div>\n <LatestArticles articles={articles} pageSize={pageSize} />\n </div>\n </section>\n </div>\n )\n}\n","'use client'\n\nimport { useSession, signIn } from 'next-auth/react'\nimport { useCallback, useEffect, useState } from 'react'\nimport { CommentForm } from './CommentForm'\nimport { CommentThread } from './CommentThread'\nimport type { CommentsConfig } from './articlesConfig'\nimport type { ArticleCommentWithReplies } from './commentTypes'\n\ninterface CommentsSectionProps {\n readonly articleSlug: string\n readonly config: CommentsConfig\n}\n\nexport function CommentsSection({ articleSlug, config }: CommentsSectionProps) {\n const { data: session, status } = useSession()\n const [comments, setComments] = useState<ArticleCommentWithReplies[]>([])\n const [loading, setLoading] = useState(true)\n const [fetchError, setFetchError] = useState<string | null>(null)\n\n const perArticleEnabled = config.perArticleOverride?.[articleSlug] ?? true\n const enabled = config.enabled && perArticleEnabled\n\n const currentUserId = session?.user?.email ?? session?.user?.name ?? undefined\n\n const fetchComments = useCallback(async () => {\n setLoading(true)\n setFetchError(null)\n try {\n const res = await fetch(`/api/articles/${articleSlug}/comments`)\n if (!res.ok) throw new Error('Failed to load comments')\n const data = (await res.json()) as { comments: ArticleCommentWithReplies[] }\n setComments(data.comments)\n } catch {\n setFetchError('Could not load comments. Please try again.')\n } finally {\n setLoading(false)\n }\n }, [articleSlug])\n\n useEffect(() => {\n if (enabled) {\n fetchComments().catch(() => undefined)\n }\n }, [fetchComments, enabled])\n\n if (!enabled) return null\n\n const count = comments.length\n const label = count === 1 ? '1 comment' : `${count} comments`\n\n return (\n <section aria-label=\"Comments\" className=\"mt-12 pt-8 border-t border-border space-y-6\">\n <h2 className=\"text-xl font-semibold text-foreground\">{loading ? 'Comments' : label}</h2>\n\n {status === 'authenticated' ? (\n <CommentForm articleSlug={articleSlug} onSuccess={fetchComments} />\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n <button\n onClick={() => signIn()}\n className=\"text-primary underline hover:no-underline focus:outline-none\"\n >\n Sign in\n </button>{' '}\n to join the discussion.\n </p>\n )}\n\n {fetchError && <p className=\"text-sm text-destructive\">{fetchError}</p>}\n\n {loading ? (\n <p className=\"text-sm text-muted-foreground\">Loading comments…</p>\n ) : (\n <CommentThread\n comments={comments}\n articleSlug={articleSlug}\n currentUserId={currentUserId}\n onRefresh={fetchComments}\n />\n )}\n </section>\n )\n}\n","'use client'\n\nimport { useState } from 'react'\n\nconst MAX_LENGTH = 2000\nconst WARN_THRESHOLD = 1800\n\nfunction submitLabel(submitting: boolean, parentId?: string): string {\n if (submitting) return 'Posting…'\n return parentId ? 'Reply' : 'Post comment'\n}\n\ninterface CommentFormProps {\n readonly articleSlug: string\n readonly parentId?: string\n readonly onSuccess: () => void\n readonly onCancel?: () => void\n}\n\nexport function CommentForm({ articleSlug, parentId, onSuccess, onCancel }: CommentFormProps) {\n const [body, setBody] = useState('')\n const [submitting, setSubmitting] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const remaining = MAX_LENGTH - body.length\n\n async function handleSubmit(e: React.FormEvent) {\n e.preventDefault()\n if (!body.trim() || submitting) return\n\n setSubmitting(true)\n setError(null)\n\n try {\n const res = await fetch(`/api/articles/${articleSlug}/comments`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ body: body.trim(), parentId: parentId ?? null }),\n })\n\n if (!res.ok) {\n const data = (await res.json()) as { error?: string }\n setError(data.error ?? 'Failed to post comment')\n return\n }\n\n setBody('')\n onSuccess()\n } catch {\n setError('Network error — please try again')\n } finally {\n setSubmitting(false)\n }\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-2\">\n <textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n maxLength={MAX_LENGTH}\n rows={parentId ? 3 : 4}\n placeholder={parentId ? 'Write a reply…' : 'Join the discussion…'}\n disabled={submitting}\n className=\"w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 disabled:opacity-50 resize-none\"\n aria-label={parentId ? 'Reply text' : 'Comment text'}\n />\n\n {remaining <= MAX_LENGTH - WARN_THRESHOLD && (\n <p className={`text-xs ${remaining < 0 ? 'text-destructive' : 'text-muted-foreground'}`}>\n {remaining} characters remaining\n </p>\n )}\n\n {error && <p className=\"text-xs text-destructive\">{error}</p>}\n\n <div className=\"flex gap-2\">\n <button\n type=\"submit\"\n disabled={submitting || body.trim().length === 0 || body.length > MAX_LENGTH}\n className=\"rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {submitLabel(submitting, parentId)}\n </button>\n\n {onCancel && (\n <button\n type=\"button\"\n onClick={onCancel}\n className=\"rounded-md px-4 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground\"\n >\n Cancel\n </button>\n )}\n </div>\n </form>\n )\n}\n","'use client'\n\nimport { useState } from 'react'\nimport { CommentForm } from './CommentForm'\nimport type { ArticleComment, ArticleCommentWithReplies } from './commentTypes'\n\nfunction relativeTime(iso: string): string {\n const diff = Date.now() - new Date(iso).getTime()\n const minutes = Math.floor(diff / 60_000)\n if (minutes < 1) return 'just now'\n if (minutes < 60) return `${minutes}m ago`\n const hours = Math.floor(minutes / 60)\n if (hours < 24) return `${hours}h ago`\n const days = Math.floor(hours / 24)\n if (days < 30) return `${days}d ago`\n const months = Math.floor(days / 30)\n if (months < 12) return `${months}mo ago`\n return `${Math.floor(months / 12)}y ago`\n}\n\ninterface CommentItemProps {\n readonly comment: ArticleCommentWithReplies | ArticleComment\n readonly articleSlug: string\n readonly currentUserId?: string\n readonly isReply?: boolean\n readonly onRefresh: () => void\n}\n\nexport function CommentItem({\n comment,\n articleSlug,\n currentUserId,\n isReply = false,\n onRefresh,\n}: CommentItemProps) {\n const [showReplyForm, setShowReplyForm] = useState(false)\n const [deleting, setDeleting] = useState(false)\n\n const replies = 'replies' in comment ? comment.replies : []\n const canReply = !isReply && !!currentUserId && !comment.isDeleted\n const canDelete = !!currentUserId && currentUserId === comment.authorId && !comment.isDeleted\n\n async function handleDelete() {\n if (deleting) return\n setDeleting(true)\n try {\n await fetch(`/api/articles/${articleSlug}/comments/${comment.id}`, { method: 'DELETE' })\n onRefresh()\n } finally {\n setDeleting(false)\n }\n }\n\n return (\n <div className={`${isReply ? 'ml-8 border-l-2 border-border pl-4' : ''}`}>\n <div className=\"rounded-lg bg-muted/40 p-3 space-y-1\">\n {comment.isDeleted ? (\n <p className=\"text-sm italic text-muted-foreground\">Comment removed</p>\n ) : (\n <>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-foreground\">{comment.authorName}</span>\n <span className=\"text-xs text-muted-foreground\">\n {relativeTime(comment.createdAt)}\n </span>\n </div>\n <p className=\"text-sm text-foreground whitespace-pre-wrap break-words\">\n {comment.body}\n </p>\n <div className=\"flex gap-3 pt-1\">\n {canReply && (\n <button\n onClick={() => setShowReplyForm((v) => !v)}\n className=\"text-xs text-muted-foreground hover:text-foreground\"\n >\n {showReplyForm ? 'Cancel' : 'Reply'}\n </button>\n )}\n {canDelete && (\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"text-xs text-muted-foreground hover:text-destructive disabled:opacity-50\"\n >\n {deleting ? 'Deleting…' : 'Delete'}\n </button>\n )}\n </div>\n </>\n )}\n </div>\n\n {showReplyForm && (\n <div className=\"mt-2 ml-2\">\n <CommentForm\n articleSlug={articleSlug}\n parentId={comment.id}\n onSuccess={() => {\n setShowReplyForm(false)\n onRefresh()\n }}\n onCancel={() => setShowReplyForm(false)}\n />\n </div>\n )}\n\n {replies.length > 0 && (\n <div className=\"mt-2 space-y-2\">\n {replies.map((reply) => (\n <CommentItem\n key={reply.id}\n comment={{ ...reply, replies: [] }}\n articleSlug={articleSlug}\n currentUserId={currentUserId}\n isReply\n onRefresh={onRefresh}\n />\n ))}\n </div>\n )}\n </div>\n )\n}\n","'use client'\n\nimport { CommentItem } from './CommentItem'\nimport type { ArticleCommentWithReplies } from './commentTypes'\n\ninterface CommentThreadProps {\n readonly comments: ArticleCommentWithReplies[]\n readonly articleSlug: string\n readonly currentUserId?: string\n readonly onRefresh: () => void\n}\n\nexport function CommentThread({\n comments,\n articleSlug,\n currentUserId,\n onRefresh,\n}: CommentThreadProps) {\n if (comments.length === 0) {\n return (\n <p className=\"text-sm text-muted-foreground text-center py-6\">\n No comments yet. Be the first to share your thoughts.\n </p>\n )\n }\n\n return (\n <div className=\"space-y-4\">\n {comments.map((comment) => (\n <CommentItem\n key={comment.id}\n comment={comment}\n articleSlug={articleSlug}\n currentUserId={currentUserId}\n onRefresh={onRefresh}\n />\n ))}\n </div>\n )\n}\n"],"mappings":"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","names":["jsx","jsx","jsxs","Link","jsx","jsxs","Link","Image","jsx","jsxs","Search","useState","useEffect","Image","Link","ArrowRight","Calendar","Clock","jsx","jsxs","jsx","jsxs","useState","useEffect","jsx","jsxs","Search","useState","useEffect","useState","useEffect","jsx","jsxs","Image","Link","jsx","jsxs","Image","Link","useCallback","useEffect","useState","useState","jsx","jsxs","useState","e","useState","jsx","jsxs","useState","jsx","jsx","jsxs","useState","useCallback","useEffect"]}
|
package/dist/server.cjs
CHANGED
|
@@ -243,8 +243,7 @@ var customRenderer = () => {
|
|
|
243
243
|
const styleLinks = (nodes) => {
|
|
244
244
|
nodes.forEach((n) => {
|
|
245
245
|
var _a2, _b2;
|
|
246
|
-
if (n.type !== "element")
|
|
247
|
-
return;
|
|
246
|
+
if (n.type !== "element") return;
|
|
248
247
|
const el = n;
|
|
249
248
|
if (el.tagName === "a") {
|
|
250
249
|
const isBackRef = ((_a2 = el.properties) == null ? void 0 : _a2["dataFootnoteBackref"]) !== void 0 || ((_b2 = el.children[0]) == null ? void 0 : _b2.type) === "text" && el.children[0].value === "\u21A9";
|
|
@@ -260,8 +259,7 @@ var customRenderer = () => {
|
|
|
260
259
|
el.properties.className = "text-primary hover:underline break-all";
|
|
261
260
|
}
|
|
262
261
|
}
|
|
263
|
-
if (el.children)
|
|
264
|
-
styleLinks(el.children);
|
|
262
|
+
if (el.children) styleLinks(el.children);
|
|
265
263
|
});
|
|
266
264
|
};
|
|
267
265
|
styleLinks(liEl.children);
|
|
@@ -338,10 +336,8 @@ function findArticleImage(slug) {
|
|
|
338
336
|
const articleDir = import_node_path.default.join(articlesDirectory, slug);
|
|
339
337
|
const entries = import_node_fs.default.readdirSync(articleDir, { withFileTypes: true });
|
|
340
338
|
const names = entries.map((entry) => {
|
|
341
|
-
if (typeof entry === "string")
|
|
342
|
-
|
|
343
|
-
if (entry && typeof entry.name === "string")
|
|
344
|
-
return entry.name;
|
|
339
|
+
if (typeof entry === "string") return entry;
|
|
340
|
+
if (entry && typeof entry.name === "string") return entry.name;
|
|
345
341
|
return null;
|
|
346
342
|
}).filter((name) => Boolean(name));
|
|
347
343
|
const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
@@ -354,11 +350,9 @@ function findArticleImage(slug) {
|
|
|
354
350
|
}
|
|
355
351
|
}
|
|
356
352
|
function sanitizeImagePath2(rawPath, articleSlug) {
|
|
357
|
-
if (!rawPath || typeof rawPath !== "string")
|
|
358
|
-
return null;
|
|
353
|
+
if (!rawPath || typeof rawPath !== "string") return null;
|
|
359
354
|
const cleanPath = rawPath.replaceAll(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
360
|
-
if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://"))
|
|
361
|
-
return cleanPath;
|
|
355
|
+
if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) return cleanPath;
|
|
362
356
|
if (cleanPath.includes("..") || cleanPath.includes("\\") || cleanPath.startsWith("/")) {
|
|
363
357
|
console.warn(`Potentially unsafe image path detected: ${cleanPath}`);
|
|
364
358
|
return null;
|
|
@@ -379,8 +373,7 @@ function sanitizeImagePath2(rawPath, articleSlug) {
|
|
|
379
373
|
}
|
|
380
374
|
function getAvailableArticleSlugs() {
|
|
381
375
|
try {
|
|
382
|
-
if (!import_node_fs.default.existsSync(articlesDirectory))
|
|
383
|
-
return [];
|
|
376
|
+
if (!import_node_fs.default.existsSync(articlesDirectory)) return [];
|
|
384
377
|
const items = import_node_fs.default.readdirSync(articlesDirectory, { withFileTypes: true });
|
|
385
378
|
return items.filter((item) => item.isDirectory()).filter((dir) => {
|
|
386
379
|
const articleDir = import_node_path.default.join(articlesDirectory, dir.name);
|
|
@@ -390,12 +383,9 @@ function getAvailableArticleSlugs() {
|
|
|
390
383
|
});
|
|
391
384
|
return entries.some((entry) => {
|
|
392
385
|
const name = typeof entry === "string" ? entry : entry == null ? void 0 : entry.name;
|
|
393
|
-
if (name !== "article.md")
|
|
394
|
-
|
|
395
|
-
if (typeof entry === "
|
|
396
|
-
return true;
|
|
397
|
-
if (typeof (entry == null ? void 0 : entry.isFile) === "function")
|
|
398
|
-
return entry.isFile();
|
|
386
|
+
if (name !== "article.md") return false;
|
|
387
|
+
if (typeof entry === "string") return true;
|
|
388
|
+
if (typeof (entry == null ? void 0 : entry.isFile) === "function") return entry.isFile();
|
|
399
389
|
return true;
|
|
400
390
|
});
|
|
401
391
|
} catch (e) {
|
|
@@ -411,8 +401,7 @@ function getArticleSummary(slug) {
|
|
|
411
401
|
return __async(this, null, function* () {
|
|
412
402
|
try {
|
|
413
403
|
const articlePath = import_node_path.default.join(articlesDirectory, slug, "article.md");
|
|
414
|
-
if (!import_node_fs.default.existsSync(articlePath))
|
|
415
|
-
return null;
|
|
404
|
+
if (!import_node_fs.default.existsSync(articlePath)) return null;
|
|
416
405
|
const fileContent = import_node_fs.default.readFileSync(articlePath, "utf8");
|
|
417
406
|
const { data, content: markdownContent } = (0, import_gray_matter.default)(fileContent);
|
|
418
407
|
const readTime = getReadingTime(markdownContent);
|
|
@@ -457,8 +446,7 @@ function getArticleSummary(slug) {
|
|
|
457
446
|
var getArticleMetadata = (0, import_react.cache)((slug) => __async(void 0, null, function* () {
|
|
458
447
|
try {
|
|
459
448
|
const articlePath = import_node_path.default.join(articlesDirectory, slug, "article.md");
|
|
460
|
-
if (!import_node_fs.default.existsSync(articlePath))
|
|
461
|
-
return null;
|
|
449
|
+
if (!import_node_fs.default.existsSync(articlePath)) return null;
|
|
462
450
|
const fileContent = import_node_fs.default.readFileSync(articlePath, "utf8");
|
|
463
451
|
const { data, content: markdownContent } = (0, import_gray_matter.default)(fileContent);
|
|
464
452
|
const htmlContent = yield markdownToHtml(markdownContent, slug);
|
|
@@ -507,12 +495,9 @@ var getAllArticles = (0, import_react.cache)(() => __async(void 0, null, functio
|
|
|
507
495
|
const articles = yield Promise.all(slugs.map((slug) => getArticleSummary(slug)));
|
|
508
496
|
const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
509
497
|
return articles.filter((article) => article !== null).filter((article) => !article.date || article.date <= currentDate).sort((a, b) => {
|
|
510
|
-
if (!a.date && !b.date)
|
|
511
|
-
|
|
512
|
-
if (!
|
|
513
|
-
return 1;
|
|
514
|
-
if (!b.date)
|
|
515
|
-
return -1;
|
|
498
|
+
if (!a.date && !b.date) return 0;
|
|
499
|
+
if (!a.date) return 1;
|
|
500
|
+
if (!b.date) return -1;
|
|
516
501
|
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
|
517
502
|
});
|
|
518
503
|
}));
|
|
@@ -520,8 +505,7 @@ function getAdjacentArticles(currentSlug) {
|
|
|
520
505
|
return __async(this, null, function* () {
|
|
521
506
|
const allArticles = yield getAllArticles();
|
|
522
507
|
const currentIndex = allArticles.findIndex((article) => article.slug === currentSlug);
|
|
523
|
-
if (currentIndex === -1)
|
|
524
|
-
return { previous: null, next: null };
|
|
508
|
+
if (currentIndex === -1) return { previous: null, next: null };
|
|
525
509
|
const previous = currentIndex < allArticles.length - 1 ? allArticles[currentIndex + 1] : null;
|
|
526
510
|
const next = currentIndex > 0 ? allArticles[currentIndex - 1] : null;
|
|
527
511
|
return { previous, next };
|
|
@@ -529,8 +513,7 @@ function getAdjacentArticles(currentSlug) {
|
|
|
529
513
|
}
|
|
530
514
|
function searchArticles(query) {
|
|
531
515
|
return __async(this, null, function* () {
|
|
532
|
-
if (!(query == null ? void 0 : query.trim()))
|
|
533
|
-
return getAllArticles();
|
|
516
|
+
if (!(query == null ? void 0 : query.trim())) return getAllArticles();
|
|
534
517
|
const articles = yield getAllArticles();
|
|
535
518
|
const searchTerm = query.toLowerCase().trim();
|
|
536
519
|
return articles.filter((article) => {
|
|
@@ -665,8 +648,7 @@ function generateCategoryMetadata(categorySlug, config) {
|
|
|
665
648
|
return __async(this, null, function* () {
|
|
666
649
|
var _a, _b;
|
|
667
650
|
const articles = yield getArticlesByCategory(categorySlug);
|
|
668
|
-
if (articles.length === 0)
|
|
669
|
-
return { title: "Category Not Found" };
|
|
651
|
+
if (articles.length === 0) return { title: "Category Not Found" };
|
|
670
652
|
const categoryName = articles[0].category;
|
|
671
653
|
const siteUrl = config.siteUrl.replace(/\/$/, "");
|
|
672
654
|
const categoryUrl = `${siteUrl}/articles/category/${categorySlug}`;
|