@fullstackdatasolutions/articles 0.5.1 → 0.7.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/README.md +51 -6
- package/dist/index.cjs +64 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +62 -37
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +87 -24
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +86 -23
- package/dist/server.js.map +1 -1
- package/package.json +11 -11
- package/src/ArticleBackLink.tsx +32 -0
- package/src/ArticleContent.tsx +4 -7
- package/src/__tests__/ArticleBackLink.test.tsx +89 -0
- package/src/__tests__/ArticleContent.test.tsx +72 -28
- package/src/__tests__/articlesConfig.test.ts +57 -0
- package/src/__tests__/markdown.test.ts +278 -0
- package/src/__tests__/renderMdx.test.tsx +280 -0
- package/src/__tests__/server-articles.test.ts +36 -1
- package/src/articlesConfig.ts +2 -0
- package/src/index.ts +1 -0
- package/src/markdown.ts +8 -6
- package/src/renderMdx.tsx +47 -0
- package/src/server-articles.ts +20 -11
package/README.md
CHANGED
|
@@ -23,12 +23,21 @@ In Tailwind CSS v4, you add a `@source` directive to your global CSS file — **
|
|
|
23
23
|
```css
|
|
24
24
|
/* app/globals.css */
|
|
25
25
|
@import "tailwindcss";
|
|
26
|
+
@plugin "@tailwindcss/typography";
|
|
26
27
|
|
|
27
28
|
@source "./node_modules/@fullstackdatasolutions/articles/src";
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
The package components use Tailwind semantic tokens (`bg-card`, `text-muted-foreground`, `border-border`, etc.) that shadcn/ui and v0-generated apps already define — so the styling integrates automatically with your existing theme.
|
|
31
32
|
|
|
33
|
+
Install `@tailwindcss/typography` if not already present:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @tailwindcss/typography
|
|
37
|
+
# or
|
|
38
|
+
pnpm add @tailwindcss/typography
|
|
39
|
+
```
|
|
40
|
+
|
|
32
41
|
### 3. Add TypeScript path aliases
|
|
33
42
|
|
|
34
43
|
If you're using TypeScript and want IDE autocomplete without a pre-built `dist/`, add these paths to `tsconfig.json`. This is **required in a monorepo** (where the package is a workspace dep) and optional when installed from npm (the published `dist/` files work without it).
|
|
@@ -160,7 +169,7 @@ The route implementations import from `@fullstackdatasolutions/articles/server`.
|
|
|
160
169
|
|
|
161
170
|
### 7. Add your content
|
|
162
171
|
|
|
163
|
-
Create `public/articles/[slug]/article.md` for each article. Required frontmatter:
|
|
172
|
+
Create `public/articles/[slug]/article.md` or `public/articles/[slug]/article.mdx` for each article. Required frontmatter:
|
|
164
173
|
|
|
165
174
|
```markdown
|
|
166
175
|
---
|
|
@@ -173,6 +182,8 @@ tags: [campaigns, strategy]
|
|
|
173
182
|
Article body in Markdown...
|
|
174
183
|
```
|
|
175
184
|
|
|
185
|
+
Article directories can also be nested below grouping folders. For example, `public/articles/game-system/article-name/article.mdx` is discovered as the slug `game-system/article-name`. If your app exposes nested article URLs with slash-separated slugs, use a catch-all Next.js route such as `app/articles/[...slug]/page.tsx` and join the slug segments before calling the server helpers.
|
|
186
|
+
|
|
176
187
|
---
|
|
177
188
|
|
|
178
189
|
## `ArticlesConfig` reference
|
|
@@ -189,6 +200,7 @@ Article body in Markdown...
|
|
|
189
200
|
| `hero` | `HeroConfig` | — | Hero section title and description. Omit to use built-in defaults. |
|
|
190
201
|
| `comments` | `CommentsConfig` | — | Comments feature config. Omit to disable entirely. |
|
|
191
202
|
| `showToc` | `boolean` | `true` | Show the table of contents on article detail pages. Set to `false` to hide on all articles. |
|
|
203
|
+
| `showBackToArticles` | `boolean` | `true` | Show the back-navigation link on article detail pages. Set to `false` to hide `ArticleBackLink`. |
|
|
192
204
|
| `description` | `string` | — | Short description used as the RSS feed channel description. Falls back to `siteName` if omitted. |
|
|
193
205
|
|
|
194
206
|
**Default layout order:** `['hero', 'search', 'featured', 'latest', 'categories']`
|
|
@@ -325,6 +337,36 @@ import { ArticleTOC } from '@fullstackdatasolutions/articles/server'
|
|
|
325
337
|
|
|
326
338
|
---
|
|
327
339
|
|
|
340
|
+
## ArticleBackLink
|
|
341
|
+
|
|
342
|
+
`ArticleBackLink` renders a back-navigation link on article detail pages. Returns `null` when `config.showBackToArticles === false`, so it is safe to render unconditionally.
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
import { ArticleBackLink } from '@fullstackdatasolutions/articles'
|
|
346
|
+
|
|
347
|
+
<ArticleBackLink config={siteConfig} />
|
|
348
|
+
|
|
349
|
+
// With custom props:
|
|
350
|
+
<ArticleBackLink
|
|
351
|
+
config={siteConfig}
|
|
352
|
+
href="/blog"
|
|
353
|
+
label="Back to Blog"
|
|
354
|
+
className="custom-class"
|
|
355
|
+
/>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
| Prop | Type | Required | Description |
|
|
359
|
+
|---|---|---|---|
|
|
360
|
+
| `config` | `ArticlesConfig` | yes | Site config. Must include `showBackToArticles?: boolean` from ArticlesConfig. |
|
|
361
|
+
| `href` | `string` | no | Link destination. Defaults to `/articles`. |
|
|
362
|
+
| `label` | `string` | no | Link text. Defaults to `Back to Articles`. |
|
|
363
|
+
| `className` | `string` | no | Optional CSS class to override default styling. |
|
|
364
|
+
|
|
365
|
+
- Returns `null` when `config.showBackToArticles === false`
|
|
366
|
+
- Gate used in article detail pages: `config.showBackToArticles !== false`
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
328
370
|
## ScrollToTop
|
|
329
371
|
|
|
330
372
|
`ScrollToTop` is a fixed-position floating button in the bottom-right corner that appears when the user scrolls past 300px, and smooth-scrolls back to the top on click. Client component (`'use client'`). No props required - fully self-contained, safe to place anywhere in your layout.
|
|
@@ -518,7 +560,7 @@ JSON-LD structured data (`ArticleSchema`, `BreadcrumbSchema`, `CollectionPageSch
|
|
|
518
560
|
| `articleType` | `string` | no | Optional article type value included in article metadata/structured data. |
|
|
519
561
|
| `series` | `string` | no | Optional series label for grouping related articles. |
|
|
520
562
|
|
|
521
|
-
Articles can be authored as either `article.md` or `article.mdx` inside `public/articles/{slug}/`. Markdown files are converted to HTML; MDX files are returned as `mdxSource` for the consuming app to render. Reading time and the table of contents are generated from the article body automatically.
|
|
563
|
+
Articles can be authored as either `article.md` or `article.mdx` inside `public/articles/{slug}/`. The `{slug}` may be a path-like slug such as `game-system/article-name`. Markdown files are converted to HTML; MDX files are returned as `mdxSource` for the consuming app to render. Reading time and the table of contents are generated from the article body automatically.
|
|
522
564
|
|
|
523
565
|
---
|
|
524
566
|
|
|
@@ -572,6 +614,7 @@ And point the Tailwind `@source` at the workspace path:
|
|
|
572
614
|
|
|
573
615
|
```css
|
|
574
616
|
/* app/globals.css */
|
|
617
|
+
@plugin "@tailwindcss/typography";
|
|
575
618
|
@source "../../packages/articles/src";
|
|
576
619
|
```
|
|
577
620
|
|
|
@@ -593,13 +636,15 @@ This app uses `@fullstackdatasolutions/articles` for the articles section.
|
|
|
593
636
|
- `app/api/articles/` — thin API route wrappers; do not add business logic here
|
|
594
637
|
- `app/api/articles/comments-store.ts` — reference file-backed comment persistence helper used by the comment API routes
|
|
595
638
|
- `app/articles/feed.xml/route.ts` — RSS 2.0 feed (copy from reference implementation)
|
|
596
|
-
- `public/articles/[slug]/article.md` or `article.mdx` — one directory per article
|
|
639
|
+
- `public/articles/[slug]/article.md` or `article.mdx` — one directory per article; nested slugs such as `game-system/article-name` are supported
|
|
597
640
|
|
|
598
641
|
### CSS (Tailwind v4)
|
|
599
642
|
The package ships its source TypeScript files so Tailwind can scan them.
|
|
600
|
-
`app/globals.css`
|
|
643
|
+
`app/globals.css` must contain:
|
|
644
|
+
@plugin "@tailwindcss/typography";
|
|
601
645
|
@source "./node_modules/@fullstackdatasolutions/articles/src";
|
|
602
|
-
If
|
|
646
|
+
If the `@source` line is missing, most component classes will not render correctly. Note: `ArticleCategoryGrid` images, `CategoryArticlesPage` hero, and `ArticleDetailHero` image use inline styles for critical layout so they render correctly without this line. All other styling still requires it.
|
|
647
|
+
The `@plugin "@tailwindcss/typography"` line is required for article body prose styling. Install with `pnpm add @tailwindcss/typography`.
|
|
603
648
|
|
|
604
649
|
### Components
|
|
605
650
|
- `ArticleTOC` — server component imported from `@fullstackdatasolutions/articles/server`; renders `article.toc[]` as a nav block. Controlled by `siteConfig.showToc` (defaults `true`). Returns `null` when empty.
|
|
@@ -610,7 +655,7 @@ For sitemaps and metadata: `import { getArticleSitemapEntries, generateArticlesI
|
|
|
610
655
|
|
|
611
656
|
### Rules
|
|
612
657
|
- Change `config/articles.ts` first; only edit library source if config cannot express the need.
|
|
613
|
-
- When adding a new article: create `public/articles/[slug]/article.md` with title, excerpt, author, tags.
|
|
658
|
+
- When adding a new article: create `public/articles/[slug]/article.md` or `article.mdx` with title, excerpt, author, tags. Nested slugs such as `public/articles/game-system/article-name/article.mdx` are supported.
|
|
614
659
|
- API routes are copied from the library — update the package version and re-copy if the library API changes.
|
|
615
660
|
- `hero.title` and `hero.description` in `ArticlesConfig` control the hero heading — pass them to customize per app.
|
|
616
661
|
- `showToc?: boolean` (default `true`) controls TOC visibility on all article detail pages.
|
package/dist/index.cjs
CHANGED
|
@@ -69,6 +69,7 @@ var __async = (__this, __arguments, generator) => {
|
|
|
69
69
|
// src/index.ts
|
|
70
70
|
var src_exports = {};
|
|
71
71
|
__export(src_exports, {
|
|
72
|
+
ArticleBackLink: () => ArticleBackLink,
|
|
72
73
|
ArticleCard: () => ArticleCard,
|
|
73
74
|
ArticleCategoryGrid: () => ArticleCategoryGrid,
|
|
74
75
|
ArticleDetailHero: () => ArticleDetailHero,
|
|
@@ -1088,18 +1089,42 @@ function ArticleNavigation({ previous, next, basePath }) {
|
|
|
1088
1089
|
] }) });
|
|
1089
1090
|
}
|
|
1090
1091
|
|
|
1091
|
-
// src/
|
|
1092
|
+
// src/ArticleBackLink.tsx
|
|
1093
|
+
var import_link8 = __toESM(require("next/link"), 1);
|
|
1094
|
+
var import_lucide_react8 = require("lucide-react");
|
|
1092
1095
|
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
1096
|
+
function ArticleBackLink({
|
|
1097
|
+
config,
|
|
1098
|
+
href = "/articles",
|
|
1099
|
+
label = "Back to Articles",
|
|
1100
|
+
className
|
|
1101
|
+
}) {
|
|
1102
|
+
if (config.showBackToArticles === false) return null;
|
|
1103
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1104
|
+
import_link8.default,
|
|
1105
|
+
{
|
|
1106
|
+
href,
|
|
1107
|
+
className: className != null ? className : "inline-flex items-center gap-2 mb-6 -ml-3 px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors",
|
|
1108
|
+
children: [
|
|
1109
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react8.ArrowLeft, { className: "h-4 w-4" }),
|
|
1110
|
+
label
|
|
1111
|
+
]
|
|
1112
|
+
}
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/ArticleTOC.tsx
|
|
1117
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1093
1118
|
function ArticleTOC({ toc, className }) {
|
|
1094
1119
|
if (!toc.length) return null;
|
|
1095
|
-
return /* @__PURE__ */ (0,
|
|
1120
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
1096
1121
|
"nav",
|
|
1097
1122
|
{
|
|
1098
1123
|
"aria-label": "Table of contents",
|
|
1099
1124
|
className: `mb-8 rounded-lg border border-border bg-muted/40 px-6 py-4 ${className != null ? className : ""}`,
|
|
1100
1125
|
children: [
|
|
1101
|
-
/* @__PURE__ */ (0,
|
|
1102
|
-
/* @__PURE__ */ (0,
|
|
1126
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "On this page" }),
|
|
1127
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "space-y-1 text-sm", children: toc.map((item) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { style: { paddingLeft: `${Math.max(0, item.depth - 2) * 1}rem` }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1103
1128
|
"a",
|
|
1104
1129
|
{
|
|
1105
1130
|
href: `#${item.id}`,
|
|
@@ -1114,8 +1139,8 @@ function ArticleTOC({ toc, className }) {
|
|
|
1114
1139
|
|
|
1115
1140
|
// src/ScrollToTop.tsx
|
|
1116
1141
|
var import_react5 = require("react");
|
|
1117
|
-
var
|
|
1118
|
-
var
|
|
1142
|
+
var import_lucide_react9 = require("lucide-react");
|
|
1143
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
1119
1144
|
function ScrollToTop() {
|
|
1120
1145
|
const [visible, setVisible] = (0, import_react5.useState)(false);
|
|
1121
1146
|
(0, import_react5.useEffect)(() => {
|
|
@@ -1124,13 +1149,13 @@ function ScrollToTop() {
|
|
|
1124
1149
|
return () => globalThis.removeEventListener("scroll", onScroll);
|
|
1125
1150
|
}, []);
|
|
1126
1151
|
if (!visible) return null;
|
|
1127
|
-
return /* @__PURE__ */ (0,
|
|
1152
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
1128
1153
|
"button",
|
|
1129
1154
|
{
|
|
1130
1155
|
"aria-label": "Scroll to top",
|
|
1131
1156
|
onClick: () => globalThis.scrollTo({ top: 0, behavior: "smooth" }),
|
|
1132
1157
|
className: "fixed bottom-8 right-8 z-50 rounded-full bg-primary p-2 shadow-md text-primary-foreground hover:bg-primary/90 transition-colors",
|
|
1133
|
-
children: /* @__PURE__ */ (0,
|
|
1158
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react9.ArrowUp, { className: "h-5 w-5" })
|
|
1134
1159
|
}
|
|
1135
1160
|
);
|
|
1136
1161
|
}
|
|
@@ -1141,7 +1166,7 @@ var import_react9 = require("react");
|
|
|
1141
1166
|
|
|
1142
1167
|
// src/CommentForm.tsx
|
|
1143
1168
|
var import_react6 = require("react");
|
|
1144
|
-
var
|
|
1169
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
1145
1170
|
var MAX_LENGTH = 2e3;
|
|
1146
1171
|
var WARN_THRESHOLD = 1800;
|
|
1147
1172
|
function submitLabel(submitting, parentId) {
|
|
@@ -1180,8 +1205,8 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
|
|
|
1180
1205
|
}
|
|
1181
1206
|
});
|
|
1182
1207
|
}
|
|
1183
|
-
return /* @__PURE__ */ (0,
|
|
1184
|
-
/* @__PURE__ */ (0,
|
|
1208
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-2", children: [
|
|
1209
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
1185
1210
|
"textarea",
|
|
1186
1211
|
{
|
|
1187
1212
|
value: body,
|
|
@@ -1194,13 +1219,13 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
|
|
|
1194
1219
|
"aria-label": parentId ? "Reply text" : "Comment text"
|
|
1195
1220
|
}
|
|
1196
1221
|
),
|
|
1197
|
-
remaining <= MAX_LENGTH - WARN_THRESHOLD && /* @__PURE__ */ (0,
|
|
1222
|
+
remaining <= MAX_LENGTH - WARN_THRESHOLD && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: `text-xs ${remaining < 0 ? "text-destructive" : "text-muted-foreground"}`, children: [
|
|
1198
1223
|
remaining,
|
|
1199
1224
|
" characters remaining"
|
|
1200
1225
|
] }),
|
|
1201
|
-
error && /* @__PURE__ */ (0,
|
|
1202
|
-
/* @__PURE__ */ (0,
|
|
1203
|
-
/* @__PURE__ */ (0,
|
|
1226
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-xs text-destructive", children: error }),
|
|
1227
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex gap-2", children: [
|
|
1228
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
1204
1229
|
"button",
|
|
1205
1230
|
{
|
|
1206
1231
|
type: "submit",
|
|
@@ -1209,7 +1234,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
|
|
|
1209
1234
|
children: submitLabel(submitting, parentId)
|
|
1210
1235
|
}
|
|
1211
1236
|
),
|
|
1212
|
-
onCancel && /* @__PURE__ */ (0,
|
|
1237
|
+
onCancel && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
1213
1238
|
"button",
|
|
1214
1239
|
{
|
|
1215
1240
|
type: "button",
|
|
@@ -1224,7 +1249,7 @@ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
|
|
|
1224
1249
|
|
|
1225
1250
|
// src/CommentItem.tsx
|
|
1226
1251
|
var import_react7 = require("react");
|
|
1227
|
-
var
|
|
1252
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
1228
1253
|
function relativeTime(iso) {
|
|
1229
1254
|
const diff = Date.now() - new Date(iso).getTime();
|
|
1230
1255
|
const minutes = Math.floor(diff / 6e4);
|
|
@@ -1262,15 +1287,15 @@ function CommentItem({
|
|
|
1262
1287
|
}
|
|
1263
1288
|
});
|
|
1264
1289
|
}
|
|
1265
|
-
return /* @__PURE__ */ (0,
|
|
1266
|
-
/* @__PURE__ */ (0,
|
|
1267
|
-
/* @__PURE__ */ (0,
|
|
1268
|
-
/* @__PURE__ */ (0,
|
|
1269
|
-
/* @__PURE__ */ (0,
|
|
1290
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: `${isReply ? "ml-8 border-l-2 border-border pl-4" : ""}`, children: [
|
|
1291
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "rounded-lg bg-muted/40 p-3 space-y-1", children: comment.isDeleted ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm italic text-muted-foreground", children: "Comment removed" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
|
|
1292
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
|
|
1293
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sm font-medium text-foreground", children: comment.authorName }),
|
|
1294
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-xs text-muted-foreground", children: relativeTime(comment.createdAt) })
|
|
1270
1295
|
] }),
|
|
1271
|
-
/* @__PURE__ */ (0,
|
|
1272
|
-
/* @__PURE__ */ (0,
|
|
1273
|
-
canReply && /* @__PURE__ */ (0,
|
|
1296
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm text-foreground whitespace-pre-wrap break-words", children: comment.body }),
|
|
1297
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex gap-3 pt-1", children: [
|
|
1298
|
+
canReply && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
1274
1299
|
"button",
|
|
1275
1300
|
{
|
|
1276
1301
|
onClick: () => setShowReplyForm((v) => !v),
|
|
@@ -1278,7 +1303,7 @@ function CommentItem({
|
|
|
1278
1303
|
children: showReplyForm ? "Cancel" : "Reply"
|
|
1279
1304
|
}
|
|
1280
1305
|
),
|
|
1281
|
-
canDelete && /* @__PURE__ */ (0,
|
|
1306
|
+
canDelete && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
1282
1307
|
"button",
|
|
1283
1308
|
{
|
|
1284
1309
|
onClick: handleDelete,
|
|
@@ -1289,7 +1314,7 @@ function CommentItem({
|
|
|
1289
1314
|
)
|
|
1290
1315
|
] })
|
|
1291
1316
|
] }) }),
|
|
1292
|
-
showReplyForm && /* @__PURE__ */ (0,
|
|
1317
|
+
showReplyForm && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "mt-2 ml-2", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
1293
1318
|
CommentForm,
|
|
1294
1319
|
{
|
|
1295
1320
|
articleSlug,
|
|
@@ -1301,7 +1326,7 @@ function CommentItem({
|
|
|
1301
1326
|
onCancel: () => setShowReplyForm(false)
|
|
1302
1327
|
}
|
|
1303
1328
|
) }),
|
|
1304
|
-
replies.length > 0 && /* @__PURE__ */ (0,
|
|
1329
|
+
replies.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "mt-2 space-y-2", children: replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
1305
1330
|
CommentItem,
|
|
1306
1331
|
{
|
|
1307
1332
|
comment: __spreadProps(__spreadValues({}, reply), { replies: [] }),
|
|
@@ -1316,7 +1341,7 @@ function CommentItem({
|
|
|
1316
1341
|
}
|
|
1317
1342
|
|
|
1318
1343
|
// src/CommentThread.tsx
|
|
1319
|
-
var
|
|
1344
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
1320
1345
|
function CommentThread({
|
|
1321
1346
|
comments,
|
|
1322
1347
|
articleSlug,
|
|
@@ -1324,9 +1349,9 @@ function CommentThread({
|
|
|
1324
1349
|
onRefresh
|
|
1325
1350
|
}) {
|
|
1326
1351
|
if (comments.length === 0) {
|
|
1327
|
-
return /* @__PURE__ */ (0,
|
|
1352
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-sm text-muted-foreground text-center py-6", children: "No comments yet. Be the first to share your thoughts." });
|
|
1328
1353
|
}
|
|
1329
|
-
return /* @__PURE__ */ (0,
|
|
1354
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
1330
1355
|
CommentItem,
|
|
1331
1356
|
{
|
|
1332
1357
|
comment,
|
|
@@ -1339,7 +1364,7 @@ function CommentThread({
|
|
|
1339
1364
|
}
|
|
1340
1365
|
|
|
1341
1366
|
// src/CommentsSection.tsx
|
|
1342
|
-
var
|
|
1367
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
1343
1368
|
function CommentsSection({ articleSlug, config }) {
|
|
1344
1369
|
var _a, _b, _c, _d, _e, _f;
|
|
1345
1370
|
const { data: session, status } = (0, import_react8.useSession)();
|
|
@@ -1371,10 +1396,10 @@ function CommentsSection({ articleSlug, config }) {
|
|
|
1371
1396
|
if (!enabled) return null;
|
|
1372
1397
|
const count = comments.length;
|
|
1373
1398
|
const label = count === 1 ? "1 comment" : `${count} comments`;
|
|
1374
|
-
return /* @__PURE__ */ (0,
|
|
1375
|
-
/* @__PURE__ */ (0,
|
|
1376
|
-
status === "authenticated" ? /* @__PURE__ */ (0,
|
|
1377
|
-
/* @__PURE__ */ (0,
|
|
1399
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("section", { "aria-label": "Comments", className: "mt-12 pt-8 border-t border-border space-y-6", children: [
|
|
1400
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h2", { className: "text-xl font-semibold text-foreground", children: loading ? "Comments" : label }),
|
|
1401
|
+
status === "authenticated" ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CommentForm, { articleSlug, onSuccess: fetchComments }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
|
|
1402
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
1378
1403
|
"button",
|
|
1379
1404
|
{
|
|
1380
1405
|
onClick: () => (0, import_react8.signIn)(),
|
|
@@ -1385,8 +1410,8 @@ function CommentsSection({ articleSlug, config }) {
|
|
|
1385
1410
|
" ",
|
|
1386
1411
|
"to join the discussion."
|
|
1387
1412
|
] }),
|
|
1388
|
-
fetchError && /* @__PURE__ */ (0,
|
|
1389
|
-
loading ? /* @__PURE__ */ (0,
|
|
1413
|
+
fetchError && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "text-sm text-destructive", children: fetchError }),
|
|
1414
|
+
loading ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "text-sm text-muted-foreground", children: "Loading comments\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
1390
1415
|
CommentThread,
|
|
1391
1416
|
{
|
|
1392
1417
|
comments,
|
|
@@ -1399,6 +1424,7 @@ function CommentsSection({ articleSlug, config }) {
|
|
|
1399
1424
|
}
|
|
1400
1425
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1401
1426
|
0 && (module.exports = {
|
|
1427
|
+
ArticleBackLink,
|
|
1402
1428
|
ArticleCard,
|
|
1403
1429
|
ArticleCategoryGrid,
|
|
1404
1430
|
ArticleDetailHero,
|