@fullstackdatasolutions/articles 0.1.0 → 0.2.0

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/dist/index.js CHANGED
@@ -50,8 +50,7 @@ function ArticleCategoryGrid({ categories, pageSize = 8 }) {
50
50
  useEffect(() => {
51
51
  setVisibleCount(pageSize);
52
52
  }, [categories, pageSize]);
53
- if (categories.length === 0)
54
- return null;
53
+ if (categories.length === 0) return null;
55
54
  const visible = categories.slice(0, visibleCount);
56
55
  const hasMore = visibleCount < categories.length;
57
56
  return /* @__PURE__ */ jsx("section", { className: "py-16 bg-background", children: /* @__PURE__ */ jsxs("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: [
@@ -59,26 +58,37 @@ function ArticleCategoryGrid({ categories, pageSize = 8 }) {
59
58
  /* @__PURE__ */ jsx("h2", { className: "text-3xl font-bold text-foreground mb-4", children: "Browse by Category" }),
60
59
  /* @__PURE__ */ jsx("p", { className: "text-lg text-muted-foreground", children: "Explore our articles by topic to find the insights most relevant to your interests." })
61
60
  ] }),
62
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-6", children: visible.map((cat) => /* @__PURE__ */ jsx(Link, { href: `/articles/category/${cat.slug}`, children: /* @__PURE__ */ jsxs("div", { className: "group overflow-hidden rounded-xl border border-border shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer bg-card", children: [
63
- /* @__PURE__ */ jsx("div", { className: "relative h-52 overflow-hidden", children: /* @__PURE__ */ jsx(
64
- Image,
65
- {
66
- src: cat.featuredImage,
67
- alt: cat.name,
68
- fill: true,
69
- sizes: "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw",
70
- className: "object-cover group-hover:scale-105 transition-transform duration-300"
71
- }
72
- ) }),
73
- /* @__PURE__ */ jsxs("div", { className: "p-4 text-center", children: [
74
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-foreground text-base", children: cat.name }),
75
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [
76
- cat.count,
77
- " ",
78
- cat.count === 1 ? "article" : "articles"
79
- ] })
80
- ] })
81
- ] }) }, cat.slug)) }),
61
+ /* @__PURE__ */ jsx(
62
+ "div",
63
+ {
64
+ className: "sm:grid-cols-4",
65
+ style: {
66
+ display: "grid",
67
+ gridTemplateColumns: "repeat(1, minmax(0, 1fr))",
68
+ gap: "1.5rem"
69
+ },
70
+ children: visible.map((cat) => /* @__PURE__ */ jsx(Link, { href: `/articles/category/${cat.slug}`, children: /* @__PURE__ */ jsxs("div", { className: "group overflow-hidden rounded-xl border border-border shadow-sm hover:shadow-lg transition-all duration-300 cursor-pointer bg-card", children: [
71
+ /* @__PURE__ */ jsx("div", { className: "overflow-hidden", style: { position: "relative", height: "13rem" }, children: /* @__PURE__ */ jsx(
72
+ Image,
73
+ {
74
+ src: cat.featuredImage,
75
+ alt: cat.name,
76
+ fill: true,
77
+ sizes: "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw",
78
+ className: "object-cover group-hover:scale-105 transition-transform duration-300"
79
+ }
80
+ ) }),
81
+ /* @__PURE__ */ jsxs("div", { className: "p-4 text-center", children: [
82
+ /* @__PURE__ */ jsx("h3", { className: "font-semibold text-foreground text-base", children: cat.name }),
83
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [
84
+ cat.count,
85
+ " ",
86
+ cat.count === 1 ? "article" : "articles"
87
+ ] })
88
+ ] })
89
+ ] }) }, cat.slug))
90
+ }
91
+ ),
82
92
  hasMore && /* @__PURE__ */ jsx("div", { className: "mt-10 text-center", children: /* @__PURE__ */ jsx(
83
93
  "button",
84
94
  {
@@ -196,10 +206,15 @@ function ArticleSearchBar({
196
206
  // src/ArticlesHero.tsx
197
207
  import Link2 from "next/link";
198
208
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
199
- function ArticlesHero() {
209
+ var DEFAULT_TITLE = "Vox Populus Insights";
210
+ var DEFAULT_DESCRIPTION = "Expert analysis, campaign strategies, and voter engagement insights from the frontlines of democracy.";
211
+ function ArticlesHero({
212
+ title = DEFAULT_TITLE,
213
+ description = DEFAULT_DESCRIPTION
214
+ }) {
200
215
  return /* @__PURE__ */ jsx4("section", { className: "bg-linear-to-r from-gray-50 to-gray-100 py-16 md:py-24", children: /* @__PURE__ */ jsx4("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: /* @__PURE__ */ jsxs3("div", { className: "max-w-3xl mx-auto text-center", children: [
201
- /* @__PURE__ */ jsx4("h1", { className: "text-4xl md:text-5xl font-bold text-foreground mb-6", children: "Vox Populus Insights" }),
202
- /* @__PURE__ */ jsx4("p", { className: "text-xl text-muted-foreground mb-8", children: "Expert analysis, campaign strategies, and voter engagement insights from the frontlines of democracy." }),
216
+ /* @__PURE__ */ jsx4("h1", { className: "text-4xl md:text-5xl font-bold text-foreground mb-6", children: title }),
217
+ /* @__PURE__ */ jsx4("p", { className: "text-xl text-muted-foreground mb-8", children: description }),
203
218
  /* @__PURE__ */ jsxs3("div", { className: "flex flex-col sm:flex-row gap-4 justify-center", children: [
204
219
  /* @__PURE__ */ jsx4(
205
220
  Link2,
@@ -423,8 +438,7 @@ function useArticles() {
423
438
  setLoading(true);
424
439
  const url = query ? `/api/articles?q=${encodeURIComponent(query)}` : "/api/articles";
425
440
  const response = yield fetch(url);
426
- if (!response.ok)
427
- throw new Error("Failed to fetch articles");
441
+ if (!response.ok) throw new Error("Failed to fetch articles");
428
442
  const data = yield response.json();
429
443
  setArticles(data.articles);
430
444
  if (!(query == null ? void 0 : query.trim())) {
@@ -460,8 +474,7 @@ function useArticles() {
460
474
  const handleSearch = useCallback(
461
475
  (query) => {
462
476
  setSearchQuery(query);
463
- if (debounceRef.current)
464
- clearTimeout(debounceRef.current);
477
+ if (debounceRef.current) clearTimeout(debounceRef.current);
465
478
  if (query === "") {
466
479
  fetchArticles("");
467
480
  } else if (query.length >= 3) {
@@ -472,8 +485,7 @@ function useArticles() {
472
485
  );
473
486
  useEffect3(
474
487
  () => () => {
475
- if (debounceRef.current)
476
- clearTimeout(debounceRef.current);
488
+ if (debounceRef.current) clearTimeout(debounceRef.current);
477
489
  },
478
490
  []
479
491
  );
@@ -483,36 +495,24 @@ function useArticles() {
483
495
  // src/ArticlesPage.tsx
484
496
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
485
497
  function buildThemeVars(theme) {
486
- if (!theme)
487
- return {};
498
+ if (!theme) return {};
488
499
  const vars = {};
489
- if (theme.fontFamily)
490
- vars["--articles-font-family"] = theme.fontFamily;
491
- if (theme.headerColor)
492
- vars["--articles-header-color"] = theme.headerColor;
493
- if (theme.textColor)
494
- vars["--articles-text-color"] = theme.textColor;
495
- if (theme.backgroundColor)
496
- vars["--articles-bg-color"] = theme.backgroundColor;
497
- if (theme.linkColor)
498
- vars["--articles-link-color"] = theme.linkColor;
499
- if (theme.linkHoverColor)
500
- vars["--articles-link-hover-color"] = theme.linkHoverColor;
501
- if (theme.linkTextDecoration)
502
- vars["--articles-link-decoration"] = theme.linkTextDecoration;
503
- if (theme.headerFontWeight)
504
- vars["--articles-header-font-weight"] = String(theme.headerFontWeight);
505
- if (theme.headerFontSize)
506
- vars["--articles-header-font-size"] = theme.headerFontSize;
507
- if (theme.bodyFontSize)
508
- vars["--articles-body-font-size"] = theme.bodyFontSize;
509
- if (theme.lineHeight)
510
- vars["--articles-line-height"] = theme.lineHeight;
511
- if (theme.borderRadius)
512
- vars["--articles-border-radius"] = theme.borderRadius;
500
+ if (theme.fontFamily) vars["--articles-font-family"] = theme.fontFamily;
501
+ if (theme.headerColor) vars["--articles-header-color"] = theme.headerColor;
502
+ if (theme.textColor) vars["--articles-text-color"] = theme.textColor;
503
+ if (theme.backgroundColor) vars["--articles-bg-color"] = theme.backgroundColor;
504
+ if (theme.linkColor) vars["--articles-link-color"] = theme.linkColor;
505
+ if (theme.linkHoverColor) vars["--articles-link-hover-color"] = theme.linkHoverColor;
506
+ if (theme.linkTextDecoration) vars["--articles-link-decoration"] = theme.linkTextDecoration;
507
+ if (theme.headerFontWeight) vars["--articles-header-font-weight"] = String(theme.headerFontWeight);
508
+ if (theme.headerFontSize) vars["--articles-header-font-size"] = theme.headerFontSize;
509
+ if (theme.bodyFontSize) vars["--articles-body-font-size"] = theme.bodyFontSize;
510
+ if (theme.lineHeight) vars["--articles-line-height"] = theme.lineHeight;
511
+ if (theme.borderRadius) vars["--articles-border-radius"] = theme.borderRadius;
513
512
  return vars;
514
513
  }
515
- function renderSection(section, ctx) {
514
+ function renderSection(section, ctx, config) {
515
+ var _a, _b;
516
516
  const {
517
517
  articles,
518
518
  displayedArticles,
@@ -526,7 +526,14 @@ function renderSection(section, ctx) {
526
526
  } = ctx;
527
527
  switch (section) {
528
528
  case "hero":
529
- return /* @__PURE__ */ jsx9(ArticlesHero, {}, "hero");
529
+ return /* @__PURE__ */ jsx9(
530
+ ArticlesHero,
531
+ {
532
+ title: (_a = config.hero) == null ? void 0 : _a.title,
533
+ description: (_b = config.hero) == null ? void 0 : _b.description
534
+ },
535
+ "hero"
536
+ );
530
537
  case "search":
531
538
  return /* @__PURE__ */ jsx9(
532
539
  ArticleSearchBar,
@@ -581,6 +588,7 @@ function renderSection(section, ctx) {
581
588
  },
582
589
  "categories"
583
590
  );
591
+ // 'newsletter' is intentionally omitted — inject it via the layout prop in the consuming app
584
592
  default:
585
593
  return null;
586
594
  }
@@ -597,7 +605,7 @@ function ArticlesPage({ config }) {
597
605
  const displayedArticles = state.searchQuery ? state.articles : articlesWithoutFeatured;
598
606
  const ctx = __spreadProps(__spreadValues({}, state), { displayedArticles, pageSize, categoriesPageSize });
599
607
  return /* @__PURE__ */ jsxs8("div", { style: themeVars, children: [
600
- layout.map((section) => renderSection(section, ctx)),
608
+ layout.map((section) => renderSection(section, ctx, config)),
601
609
  state.articles.length > 0 && /* @__PURE__ */ jsx9(
602
610
  CollectionPageSchema,
603
611
  {
@@ -617,16 +625,13 @@ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
617
625
  function getCategoryDescription(config, slug, categoryName) {
618
626
  var _a;
619
627
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[slug];
620
- if (!raw)
621
- return { short: `Browse all articles in ${categoryName}.` };
622
- if (typeof raw === "string")
623
- return { short: raw };
628
+ if (!raw) return { short: `Browse all articles in ${categoryName}.` };
629
+ if (typeof raw === "string") return { short: raw };
624
630
  return raw;
625
631
  }
626
632
  function CategoryArticlesPage({ category, articles, config }) {
627
633
  var _a;
628
- if (articles.length === 0)
629
- return null;
634
+ if (articles.length === 0) return null;
630
635
  const categoryName = articles[0].category;
631
636
  const heroImage = articles[0].featuredImage;
632
637
  const description = getCategoryDescription(config, category, categoryName);
@@ -687,8 +692,7 @@ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
687
692
  var MAX_LENGTH = 2e3;
688
693
  var WARN_THRESHOLD = 1800;
689
694
  function submitLabel(submitting, parentId) {
690
- if (submitting)
691
- return "Posting\u2026";
695
+ if (submitting) return "Posting\u2026";
692
696
  return parentId ? "Reply" : "Post comment";
693
697
  }
694
698
  function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
@@ -700,8 +704,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
700
704
  return __async(this, null, function* () {
701
705
  var _a;
702
706
  e.preventDefault();
703
- if (!body.trim() || submitting)
704
- return;
707
+ if (!body.trim() || submitting) return;
705
708
  setSubmitting(true);
706
709
  setError(null);
707
710
  try {
@@ -772,19 +775,14 @@ import { Fragment, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
772
775
  function relativeTime(iso) {
773
776
  const diff = Date.now() - new Date(iso).getTime();
774
777
  const minutes = Math.floor(diff / 6e4);
775
- if (minutes < 1)
776
- return "just now";
777
- if (minutes < 60)
778
- return `${minutes}m ago`;
778
+ if (minutes < 1) return "just now";
779
+ if (minutes < 60) return `${minutes}m ago`;
779
780
  const hours = Math.floor(minutes / 60);
780
- if (hours < 24)
781
- return `${hours}h ago`;
781
+ if (hours < 24) return `${hours}h ago`;
782
782
  const days = Math.floor(hours / 24);
783
- if (days < 30)
784
- return `${days}d ago`;
783
+ if (days < 30) return `${days}d ago`;
785
784
  const months = Math.floor(days / 30);
786
- if (months < 12)
787
- return `${months}mo ago`;
785
+ if (months < 12) return `${months}mo ago`;
788
786
  return `${Math.floor(months / 12)}y ago`;
789
787
  }
790
788
  function CommentItem({
@@ -801,8 +799,7 @@ function CommentItem({
801
799
  const canDelete = !!currentUserId && currentUserId === comment.authorId && !comment.isDeleted;
802
800
  function handleDelete() {
803
801
  return __async(this, null, function* () {
804
- if (deleting)
805
- return;
802
+ if (deleting) return;
806
803
  setDeleting(true);
807
804
  try {
808
805
  yield fetch(`/api/articles/${articleSlug}/comments/${comment.id}`, { method: "DELETE" });
@@ -904,8 +901,7 @@ function CommentsSection({ articleSlug, config }) {
904
901
  setFetchError(null);
905
902
  try {
906
903
  const res = yield fetch(`/api/articles/${articleSlug}/comments`);
907
- if (!res.ok)
908
- throw new Error("Failed to load comments");
904
+ if (!res.ok) throw new Error("Failed to load comments");
909
905
  const data = yield res.json();
910
906
  setComments(data.comments);
911
907
  } catch (e) {
@@ -919,8 +915,7 @@ function CommentsSection({ articleSlug, config }) {
919
915
  fetchComments().catch(() => void 0);
920
916
  }
921
917
  }, [fetchComments, enabled]);
922
- if (!enabled)
923
- return null;
918
+ if (!enabled) return null;
924
919
  const count = comments.length;
925
920
  const label = count === 1 ? "1 comment" : `${count} comments`;
926
921
  return /* @__PURE__ */ jsxs12("section", { "aria-label": "Comments", className: "mt-12 pt-8 border-t border-border space-y-6", children: [