@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/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A self-contained Next.js articles library providing components, server utilities, SEO helpers, and a comments system. Drop it into any Next.js + Tailwind CSS v4 app.
|
|
4
4
|
|
|
5
|
+
Built by [Full Stack Data Solutions](https://fullstackdatasolutions.com).
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Quick start
|
|
@@ -127,6 +129,7 @@ Article body in Markdown...
|
|
|
127
129
|
| `layout` | `ArticlesSection[]` | see below | Ordered sections to render. Omit a key to hide it. |
|
|
128
130
|
| `theme` | `ArticlesTheme` | — | CSS custom-property overrides for colors and fonts. |
|
|
129
131
|
| `categoryDescriptions` | `Record<string, string \| CategoryDescription>` | — | Short/long text per category slug. |
|
|
132
|
+
| `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
|
|
130
133
|
| `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
|
|
131
134
|
|
|
132
135
|
**Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
|
|
@@ -155,6 +158,19 @@ All fields are optional. Omitted fields fall back to your Tailwind theme tokens.
|
|
|
155
158
|
|
|
156
159
|
---
|
|
157
160
|
|
|
161
|
+
## Hero section
|
|
162
|
+
|
|
163
|
+
Customize the hero heading by passing `title` and `description` to `HeroConfig`. Omit the `hero` field entirely to use built-in defaults.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
hero: {
|
|
167
|
+
title: 'My App Articles',
|
|
168
|
+
description: 'Insights and updates from our team.',
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
158
174
|
## Comments setup
|
|
159
175
|
|
|
160
176
|
### 1. Enable in config
|
|
@@ -291,12 +307,13 @@ This app uses `@fullstackdatasolutions/articles` for the articles section.
|
|
|
291
307
|
The package ships its source TypeScript files so Tailwind can scan them.
|
|
292
308
|
`app/globals.css` contains:
|
|
293
309
|
@source "./node_modules/@fullstackdatasolutions/articles/src";
|
|
294
|
-
If this line is missing, component classes
|
|
310
|
+
If this line is missing, most component classes will not render correctly. Note: `ArticleCategoryGrid` image containers use inline styles for critical layout so images remain visible, but all other styling still requires this line.
|
|
295
311
|
|
|
296
312
|
### Rules
|
|
297
313
|
- Change `config/articles.ts` first; only edit library source if config cannot express the need.
|
|
298
314
|
- When adding a new article: create `public/articles/[slug]/article.md` with title, excerpt, author, tags.
|
|
299
315
|
- API routes are copied from the library — update the package version and re-copy if the library API changes.
|
|
316
|
+
- `hero.title` and `hero.description` in `ArticlesConfig` control the hero heading — pass them to customize per app.
|
|
300
317
|
- Full library docs: `node_modules/@fullstackdatasolutions/articles/README.md`
|
|
301
318
|
```
|
|
302
319
|
|
|
@@ -307,8 +324,8 @@ If this line is missing, component classes (layout, images, line-clamp) will not
|
|
|
307
324
|
The package publishes to npm automatically when you push a tag:
|
|
308
325
|
|
|
309
326
|
```bash
|
|
310
|
-
git tag articles-v0.2.
|
|
311
|
-
git push origin articles-v0.2.
|
|
327
|
+
git tag articles-v0.2.1
|
|
328
|
+
git push origin articles-v0.2.1
|
|
312
329
|
```
|
|
313
330
|
|
|
314
331
|
GitHub Actions runs typecheck → test → build → `npm publish --access public`. Requires an `NPM_TOKEN` secret in the repo settings.
|
package/dist/index.cjs
CHANGED
|
@@ -102,8 +102,7 @@ function ArticleCategoryGrid({ categories, pageSize = 8 }) {
|
|
|
102
102
|
(0, import_react.useEffect)(() => {
|
|
103
103
|
setVisibleCount(pageSize);
|
|
104
104
|
}, [categories, pageSize]);
|
|
105
|
-
if (categories.length === 0)
|
|
106
|
-
return null;
|
|
105
|
+
if (categories.length === 0) return null;
|
|
107
106
|
const visible = categories.slice(0, visibleCount);
|
|
108
107
|
const hasMore = visibleCount < categories.length;
|
|
109
108
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", { className: "py-16 bg-background", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: [
|
|
@@ -111,26 +110,37 @@ function ArticleCategoryGrid({ categories, pageSize = 8 }) {
|
|
|
111
110
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-3xl font-bold text-foreground mb-4", children: "Browse by Category" }),
|
|
112
111
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-lg text-muted-foreground", children: "Explore our articles by topic to find the insights most relevant to your interests." })
|
|
113
112
|
] }),
|
|
114
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
113
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
114
|
+
"div",
|
|
115
|
+
{
|
|
116
|
+
className: "sm:grid-cols-4",
|
|
117
|
+
style: {
|
|
118
|
+
display: "grid",
|
|
119
|
+
gridTemplateColumns: "repeat(1, minmax(0, 1fr))",
|
|
120
|
+
gap: "1.5rem"
|
|
121
|
+
},
|
|
122
|
+
children: visible.map((cat) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_link.default, { href: `/articles/category/${cat.slug}`, children: /* @__PURE__ */ (0, import_jsx_runtime.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: [
|
|
123
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "overflow-hidden", style: { position: "relative", height: "13rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
124
|
+
import_image.default,
|
|
125
|
+
{
|
|
126
|
+
src: cat.featuredImage,
|
|
127
|
+
alt: cat.name,
|
|
128
|
+
fill: true,
|
|
129
|
+
sizes: "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw",
|
|
130
|
+
className: "object-cover group-hover:scale-105 transition-transform duration-300"
|
|
131
|
+
}
|
|
132
|
+
) }),
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-4 text-center", children: [
|
|
134
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "font-semibold text-foreground text-base", children: cat.name }),
|
|
135
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-sm text-muted-foreground mt-1", children: [
|
|
136
|
+
cat.count,
|
|
137
|
+
" ",
|
|
138
|
+
cat.count === 1 ? "article" : "articles"
|
|
139
|
+
] })
|
|
140
|
+
] })
|
|
141
|
+
] }) }, cat.slug))
|
|
142
|
+
}
|
|
143
|
+
),
|
|
134
144
|
hasMore && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-10 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
135
145
|
"button",
|
|
136
146
|
{
|
|
@@ -248,10 +258,15 @@ function ArticleSearchBar({
|
|
|
248
258
|
// src/ArticlesHero.tsx
|
|
249
259
|
var import_link2 = __toESM(require("next/link"), 1);
|
|
250
260
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
251
|
-
|
|
261
|
+
var DEFAULT_TITLE = "Vox Populus Insights";
|
|
262
|
+
var DEFAULT_DESCRIPTION = "Expert analysis, campaign strategies, and voter engagement insights from the frontlines of democracy.";
|
|
263
|
+
function ArticlesHero({
|
|
264
|
+
title = DEFAULT_TITLE,
|
|
265
|
+
description = DEFAULT_DESCRIPTION
|
|
266
|
+
}) {
|
|
252
267
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("section", { className: "bg-linear-to-r from-gray-50 to-gray-100 py-16 md:py-24", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "max-w-3xl mx-auto text-center", children: [
|
|
253
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { className: "text-4xl md:text-5xl font-bold text-foreground mb-6", children:
|
|
254
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xl text-muted-foreground mb-8", children:
|
|
268
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { className: "text-4xl md:text-5xl font-bold text-foreground mb-6", children: title }),
|
|
269
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xl text-muted-foreground mb-8", children: description }),
|
|
255
270
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col sm:flex-row gap-4 justify-center", children: [
|
|
256
271
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
257
272
|
import_link2.default,
|
|
@@ -475,8 +490,7 @@ function useArticles() {
|
|
|
475
490
|
setLoading(true);
|
|
476
491
|
const url = query ? `/api/articles?q=${encodeURIComponent(query)}` : "/api/articles";
|
|
477
492
|
const response = yield fetch(url);
|
|
478
|
-
if (!response.ok)
|
|
479
|
-
throw new Error("Failed to fetch articles");
|
|
493
|
+
if (!response.ok) throw new Error("Failed to fetch articles");
|
|
480
494
|
const data = yield response.json();
|
|
481
495
|
setArticles(data.articles);
|
|
482
496
|
if (!(query == null ? void 0 : query.trim())) {
|
|
@@ -512,8 +526,7 @@ function useArticles() {
|
|
|
512
526
|
const handleSearch = (0, import_react3.useCallback)(
|
|
513
527
|
(query) => {
|
|
514
528
|
setSearchQuery(query);
|
|
515
|
-
if (debounceRef.current)
|
|
516
|
-
clearTimeout(debounceRef.current);
|
|
529
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
517
530
|
if (query === "") {
|
|
518
531
|
fetchArticles("");
|
|
519
532
|
} else if (query.length >= 3) {
|
|
@@ -524,8 +537,7 @@ function useArticles() {
|
|
|
524
537
|
);
|
|
525
538
|
(0, import_react3.useEffect)(
|
|
526
539
|
() => () => {
|
|
527
|
-
if (debounceRef.current)
|
|
528
|
-
clearTimeout(debounceRef.current);
|
|
540
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
529
541
|
},
|
|
530
542
|
[]
|
|
531
543
|
);
|
|
@@ -535,36 +547,24 @@ function useArticles() {
|
|
|
535
547
|
// src/ArticlesPage.tsx
|
|
536
548
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
537
549
|
function buildThemeVars(theme) {
|
|
538
|
-
if (!theme)
|
|
539
|
-
return {};
|
|
550
|
+
if (!theme) return {};
|
|
540
551
|
const vars = {};
|
|
541
|
-
if (theme.fontFamily)
|
|
542
|
-
|
|
543
|
-
if (theme.
|
|
544
|
-
|
|
545
|
-
if (theme.
|
|
546
|
-
|
|
547
|
-
if (theme.
|
|
548
|
-
|
|
549
|
-
if (theme.
|
|
550
|
-
|
|
551
|
-
if (theme.
|
|
552
|
-
|
|
553
|
-
if (theme.linkTextDecoration)
|
|
554
|
-
vars["--articles-link-decoration"] = theme.linkTextDecoration;
|
|
555
|
-
if (theme.headerFontWeight)
|
|
556
|
-
vars["--articles-header-font-weight"] = String(theme.headerFontWeight);
|
|
557
|
-
if (theme.headerFontSize)
|
|
558
|
-
vars["--articles-header-font-size"] = theme.headerFontSize;
|
|
559
|
-
if (theme.bodyFontSize)
|
|
560
|
-
vars["--articles-body-font-size"] = theme.bodyFontSize;
|
|
561
|
-
if (theme.lineHeight)
|
|
562
|
-
vars["--articles-line-height"] = theme.lineHeight;
|
|
563
|
-
if (theme.borderRadius)
|
|
564
|
-
vars["--articles-border-radius"] = theme.borderRadius;
|
|
552
|
+
if (theme.fontFamily) vars["--articles-font-family"] = theme.fontFamily;
|
|
553
|
+
if (theme.headerColor) vars["--articles-header-color"] = theme.headerColor;
|
|
554
|
+
if (theme.textColor) vars["--articles-text-color"] = theme.textColor;
|
|
555
|
+
if (theme.backgroundColor) vars["--articles-bg-color"] = theme.backgroundColor;
|
|
556
|
+
if (theme.linkColor) vars["--articles-link-color"] = theme.linkColor;
|
|
557
|
+
if (theme.linkHoverColor) vars["--articles-link-hover-color"] = theme.linkHoverColor;
|
|
558
|
+
if (theme.linkTextDecoration) vars["--articles-link-decoration"] = theme.linkTextDecoration;
|
|
559
|
+
if (theme.headerFontWeight) vars["--articles-header-font-weight"] = String(theme.headerFontWeight);
|
|
560
|
+
if (theme.headerFontSize) vars["--articles-header-font-size"] = theme.headerFontSize;
|
|
561
|
+
if (theme.bodyFontSize) vars["--articles-body-font-size"] = theme.bodyFontSize;
|
|
562
|
+
if (theme.lineHeight) vars["--articles-line-height"] = theme.lineHeight;
|
|
563
|
+
if (theme.borderRadius) vars["--articles-border-radius"] = theme.borderRadius;
|
|
565
564
|
return vars;
|
|
566
565
|
}
|
|
567
|
-
function renderSection(section, ctx) {
|
|
566
|
+
function renderSection(section, ctx, config) {
|
|
567
|
+
var _a, _b;
|
|
568
568
|
const {
|
|
569
569
|
articles,
|
|
570
570
|
displayedArticles,
|
|
@@ -578,7 +578,14 @@ function renderSection(section, ctx) {
|
|
|
578
578
|
} = ctx;
|
|
579
579
|
switch (section) {
|
|
580
580
|
case "hero":
|
|
581
|
-
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
581
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
582
|
+
ArticlesHero,
|
|
583
|
+
{
|
|
584
|
+
title: (_a = config.hero) == null ? void 0 : _a.title,
|
|
585
|
+
description: (_b = config.hero) == null ? void 0 : _b.description
|
|
586
|
+
},
|
|
587
|
+
"hero"
|
|
588
|
+
);
|
|
582
589
|
case "search":
|
|
583
590
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
584
591
|
ArticleSearchBar,
|
|
@@ -633,6 +640,7 @@ function renderSection(section, ctx) {
|
|
|
633
640
|
},
|
|
634
641
|
"categories"
|
|
635
642
|
);
|
|
643
|
+
// 'newsletter' is intentionally omitted — inject it via the layout prop in the consuming app
|
|
636
644
|
default:
|
|
637
645
|
return null;
|
|
638
646
|
}
|
|
@@ -649,7 +657,7 @@ function ArticlesPage({ config }) {
|
|
|
649
657
|
const displayedArticles = state.searchQuery ? state.articles : articlesWithoutFeatured;
|
|
650
658
|
const ctx = __spreadProps(__spreadValues({}, state), { displayedArticles, pageSize, categoriesPageSize });
|
|
651
659
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: themeVars, children: [
|
|
652
|
-
layout.map((section) => renderSection(section, ctx)),
|
|
660
|
+
layout.map((section) => renderSection(section, ctx, config)),
|
|
653
661
|
state.articles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
654
662
|
CollectionPageSchema,
|
|
655
663
|
{
|
|
@@ -669,16 +677,13 @@ var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
|
669
677
|
function getCategoryDescription(config, slug, categoryName) {
|
|
670
678
|
var _a;
|
|
671
679
|
const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[slug];
|
|
672
|
-
if (!raw)
|
|
673
|
-
|
|
674
|
-
if (typeof raw === "string")
|
|
675
|
-
return { short: raw };
|
|
680
|
+
if (!raw) return { short: `Browse all articles in ${categoryName}.` };
|
|
681
|
+
if (typeof raw === "string") return { short: raw };
|
|
676
682
|
return raw;
|
|
677
683
|
}
|
|
678
684
|
function CategoryArticlesPage({ category, articles, config }) {
|
|
679
685
|
var _a;
|
|
680
|
-
if (articles.length === 0)
|
|
681
|
-
return null;
|
|
686
|
+
if (articles.length === 0) return null;
|
|
682
687
|
const categoryName = articles[0].category;
|
|
683
688
|
const heroImage = articles[0].featuredImage;
|
|
684
689
|
const description = getCategoryDescription(config, category, categoryName);
|
|
@@ -739,8 +744,7 @@ var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
|
739
744
|
var MAX_LENGTH = 2e3;
|
|
740
745
|
var WARN_THRESHOLD = 1800;
|
|
741
746
|
function submitLabel(submitting, parentId) {
|
|
742
|
-
if (submitting)
|
|
743
|
-
return "Posting\u2026";
|
|
747
|
+
if (submitting) return "Posting\u2026";
|
|
744
748
|
return parentId ? "Reply" : "Post comment";
|
|
745
749
|
}
|
|
746
750
|
function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
|
|
@@ -752,8 +756,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
|
|
|
752
756
|
return __async(this, null, function* () {
|
|
753
757
|
var _a;
|
|
754
758
|
e.preventDefault();
|
|
755
|
-
if (!body.trim() || submitting)
|
|
756
|
-
return;
|
|
759
|
+
if (!body.trim() || submitting) return;
|
|
757
760
|
setSubmitting(true);
|
|
758
761
|
setError(null);
|
|
759
762
|
try {
|
|
@@ -824,19 +827,14 @@ var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
|
824
827
|
function relativeTime(iso) {
|
|
825
828
|
const diff = Date.now() - new Date(iso).getTime();
|
|
826
829
|
const minutes = Math.floor(diff / 6e4);
|
|
827
|
-
if (minutes < 1)
|
|
828
|
-
|
|
829
|
-
if (minutes < 60)
|
|
830
|
-
return `${minutes}m ago`;
|
|
830
|
+
if (minutes < 1) return "just now";
|
|
831
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
831
832
|
const hours = Math.floor(minutes / 60);
|
|
832
|
-
if (hours < 24)
|
|
833
|
-
return `${hours}h ago`;
|
|
833
|
+
if (hours < 24) return `${hours}h ago`;
|
|
834
834
|
const days = Math.floor(hours / 24);
|
|
835
|
-
if (days < 30)
|
|
836
|
-
return `${days}d ago`;
|
|
835
|
+
if (days < 30) return `${days}d ago`;
|
|
837
836
|
const months = Math.floor(days / 30);
|
|
838
|
-
if (months < 12)
|
|
839
|
-
return `${months}mo ago`;
|
|
837
|
+
if (months < 12) return `${months}mo ago`;
|
|
840
838
|
return `${Math.floor(months / 12)}y ago`;
|
|
841
839
|
}
|
|
842
840
|
function CommentItem({
|
|
@@ -853,8 +851,7 @@ function CommentItem({
|
|
|
853
851
|
const canDelete = !!currentUserId && currentUserId === comment.authorId && !comment.isDeleted;
|
|
854
852
|
function handleDelete() {
|
|
855
853
|
return __async(this, null, function* () {
|
|
856
|
-
if (deleting)
|
|
857
|
-
return;
|
|
854
|
+
if (deleting) return;
|
|
858
855
|
setDeleting(true);
|
|
859
856
|
try {
|
|
860
857
|
yield fetch(`/api/articles/${articleSlug}/comments/${comment.id}`, { method: "DELETE" });
|
|
@@ -956,8 +953,7 @@ function CommentsSection({ articleSlug, config }) {
|
|
|
956
953
|
setFetchError(null);
|
|
957
954
|
try {
|
|
958
955
|
const res = yield fetch(`/api/articles/${articleSlug}/comments`);
|
|
959
|
-
if (!res.ok)
|
|
960
|
-
throw new Error("Failed to load comments");
|
|
956
|
+
if (!res.ok) throw new Error("Failed to load comments");
|
|
961
957
|
const data = yield res.json();
|
|
962
958
|
setComments(data.comments);
|
|
963
959
|
} catch (e) {
|
|
@@ -971,8 +967,7 @@ function CommentsSection({ articleSlug, config }) {
|
|
|
971
967
|
fetchComments().catch(() => void 0);
|
|
972
968
|
}
|
|
973
969
|
}, [fetchComments, enabled]);
|
|
974
|
-
if (!enabled)
|
|
975
|
-
return null;
|
|
970
|
+
if (!enabled) return null;
|
|
976
971
|
const count = comments.length;
|
|
977
972
|
const label = count === 1 ? "1 comment" : `${count} comments`;
|
|
978
973
|
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("section", { "aria-label": "Comments", className: "mt-12 pt-8 border-t border-border space-y-6", children: [
|