@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
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(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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:
|
|
202
|
-
/* @__PURE__ */ jsx4("p", { className: "text-xl text-muted-foreground mb-8", children:
|
|
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
|
-
|
|
491
|
-
if (theme.
|
|
492
|
-
|
|
493
|
-
if (theme.
|
|
494
|
-
|
|
495
|
-
if (theme.
|
|
496
|
-
|
|
497
|
-
if (theme.
|
|
498
|
-
|
|
499
|
-
if (theme.
|
|
500
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|