@fullstackdatasolutions/articles 0.1.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.
Files changed (53) hide show
  1. package/README.md +324 -0
  2. package/dist/index.cjs +1027 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +232 -0
  5. package/dist/index.d.ts +232 -0
  6. package/dist/index.js +974 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/server.cjs +732 -0
  9. package/dist/server.cjs.map +1 -0
  10. package/dist/server.d.cts +127 -0
  11. package/dist/server.d.ts +127 -0
  12. package/dist/server.js +684 -0
  13. package/dist/server.js.map +1 -0
  14. package/package.json +82 -0
  15. package/src/ArticleCard.tsx +63 -0
  16. package/src/ArticleCategoryGrid.tsx +73 -0
  17. package/src/ArticleSchemas.tsx +82 -0
  18. package/src/ArticleSearchBar.tsx +50 -0
  19. package/src/ArticlesHero.tsx +33 -0
  20. package/src/ArticlesPage.tsx +167 -0
  21. package/src/CategoryArticlesPage.tsx +84 -0
  22. package/src/CommentForm.tsx +98 -0
  23. package/src/CommentItem.tsx +123 -0
  24. package/src/CommentThread.tsx +40 -0
  25. package/src/CommentsSection.tsx +84 -0
  26. package/src/FeaturedArticle.tsx +63 -0
  27. package/src/LatestArticles.tsx +42 -0
  28. package/src/LatestArticlesSection.tsx +68 -0
  29. package/src/__tests__/ArticleCard.test.tsx +78 -0
  30. package/src/__tests__/ArticleCategoryGrid.test.tsx +98 -0
  31. package/src/__tests__/ArticleSchemas.test.tsx +130 -0
  32. package/src/__tests__/ArticleSearchBar.test.tsx +79 -0
  33. package/src/__tests__/ArticlesHero.test.tsx +38 -0
  34. package/src/__tests__/ArticlesPage.test.tsx +155 -0
  35. package/src/__tests__/CategoryArticlesPage.test.tsx +156 -0
  36. package/src/__tests__/CommentForm.test.tsx +80 -0
  37. package/src/__tests__/CommentItem.test.tsx +149 -0
  38. package/src/__tests__/CommentsSection.test.tsx +118 -0
  39. package/src/__tests__/FeaturedArticle.test.tsx +85 -0
  40. package/src/__tests__/LatestArticles.test.tsx +157 -0
  41. package/src/__tests__/LatestArticlesSection.test.tsx +105 -0
  42. package/src/__tests__/jest-dom.d.ts +1 -0
  43. package/src/__tests__/seoUtils.test.ts +217 -0
  44. package/src/__tests__/useArticles.test.ts +207 -0
  45. package/src/articleTypes.ts +21 -0
  46. package/src/articlesConfig.ts +98 -0
  47. package/src/commentTypes.ts +14 -0
  48. package/src/index.ts +35 -0
  49. package/src/markdown.ts +314 -0
  50. package/src/seoUtils.ts +155 -0
  51. package/src/server-articles.ts +265 -0
  52. package/src/server.ts +25 -0
  53. package/src/useArticles.ts +93 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1027 @@
1
+ "use client"
2
+ "use strict";
3
+ "use client";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __defProps = Object.defineProperties;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
11
+ var __getProtoOf = Object.getPrototypeOf;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
15
+ var __spreadValues = (a, b) => {
16
+ for (var prop in b || (b = {}))
17
+ if (__hasOwnProp.call(b, prop))
18
+ __defNormalProp(a, prop, b[prop]);
19
+ if (__getOwnPropSymbols)
20
+ for (var prop of __getOwnPropSymbols(b)) {
21
+ if (__propIsEnum.call(b, prop))
22
+ __defNormalProp(a, prop, b[prop]);
23
+ }
24
+ return a;
25
+ };
26
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
27
+ var __export = (target, all) => {
28
+ for (var name in all)
29
+ __defProp(target, name, { get: all[name], enumerable: true });
30
+ };
31
+ var __copyProps = (to, from, except, desc) => {
32
+ if (from && typeof from === "object" || typeof from === "function") {
33
+ for (let key of __getOwnPropNames(from))
34
+ if (!__hasOwnProp.call(to, key) && key !== except)
35
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
36
+ }
37
+ return to;
38
+ };
39
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
40
+ // If the importer is in node compatibility mode or this is not an ESM
41
+ // file that has been converted to a CommonJS file using a Babel-
42
+ // compatible transform (i.e. "__esModule" has not been set), then set
43
+ // "default" to the CommonJS "module.exports" for node compatibility.
44
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
45
+ mod
46
+ ));
47
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
48
+ var __async = (__this, __arguments, generator) => {
49
+ return new Promise((resolve, reject) => {
50
+ var fulfilled = (value) => {
51
+ try {
52
+ step(generator.next(value));
53
+ } catch (e) {
54
+ reject(e);
55
+ }
56
+ };
57
+ var rejected = (value) => {
58
+ try {
59
+ step(generator.throw(value));
60
+ } catch (e) {
61
+ reject(e);
62
+ }
63
+ };
64
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
65
+ step((generator = generator.apply(__this, __arguments)).next());
66
+ });
67
+ };
68
+
69
+ // src/index.ts
70
+ var src_exports = {};
71
+ __export(src_exports, {
72
+ ArticleCard: () => ArticleCard,
73
+ ArticleCategoryGrid: () => ArticleCategoryGrid,
74
+ ArticleSchema: () => ArticleSchema,
75
+ ArticleSearchBar: () => ArticleSearchBar,
76
+ ArticlesHero: () => ArticlesHero,
77
+ ArticlesPage: () => ArticlesPage,
78
+ BreadcrumbSchema: () => BreadcrumbSchema,
79
+ CategoryArticlesPage: () => CategoryArticlesPage,
80
+ CollectionPageSchema: () => CollectionPageSchema,
81
+ CommentForm: () => CommentForm,
82
+ CommentItem: () => CommentItem,
83
+ CommentThread: () => CommentThread,
84
+ CommentsSection: () => CommentsSection,
85
+ DEFAULT_CATEGORIES_PAGE_SIZE: () => DEFAULT_CATEGORIES_PAGE_SIZE,
86
+ DEFAULT_LAYOUT: () => DEFAULT_LAYOUT,
87
+ DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
88
+ FeaturedArticle: () => FeaturedArticle,
89
+ LatestArticles: () => LatestArticles,
90
+ LatestArticlesSection: () => LatestArticlesSection,
91
+ useArticles: () => useArticles
92
+ });
93
+ module.exports = __toCommonJS(src_exports);
94
+
95
+ // src/ArticleCategoryGrid.tsx
96
+ var import_image = __toESM(require("next/image"), 1);
97
+ var import_link = __toESM(require("next/link"), 1);
98
+ var import_react = require("react");
99
+ var import_jsx_runtime = require("react/jsx-runtime");
100
+ function ArticleCategoryGrid({ categories, pageSize = 8 }) {
101
+ const [visibleCount, setVisibleCount] = (0, import_react.useState)(pageSize);
102
+ (0, import_react.useEffect)(() => {
103
+ setVisibleCount(pageSize);
104
+ }, [categories, pageSize]);
105
+ if (categories.length === 0)
106
+ return null;
107
+ const visible = categories.slice(0, visibleCount);
108
+ const hasMore = visibleCount < categories.length;
109
+ 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: [
110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-center mb-12", children: [
111
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-3xl font-bold text-foreground mb-4", children: "Browse by Category" }),
112
+ /* @__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
+ ] }),
114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-6", 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: [
115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative h-52 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
116
+ import_image.default,
117
+ {
118
+ src: cat.featuredImage,
119
+ alt: cat.name,
120
+ fill: true,
121
+ sizes: "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw",
122
+ className: "object-cover group-hover:scale-105 transition-transform duration-300"
123
+ }
124
+ ) }),
125
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-4 text-center", children: [
126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "font-semibold text-foreground text-base", children: cat.name }),
127
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-sm text-muted-foreground mt-1", children: [
128
+ cat.count,
129
+ " ",
130
+ cat.count === 1 ? "article" : "articles"
131
+ ] })
132
+ ] })
133
+ ] }) }, cat.slug)) }),
134
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-10 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
135
+ "button",
136
+ {
137
+ type: "button",
138
+ onClick: () => setVisibleCount((c) => c + pageSize),
139
+ 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",
140
+ children: "Load more categories"
141
+ }
142
+ ) })
143
+ ] }) });
144
+ }
145
+
146
+ // src/ArticleSchemas.tsx
147
+ var import_jsx_runtime2 = require("react/jsx-runtime");
148
+ function ArticleSchema({ article, articleUrl, siteName }) {
149
+ const schema = __spreadProps(__spreadValues({
150
+ "@context": "https://schema.org",
151
+ "@type": "Article",
152
+ headline: article.title,
153
+ description: article.excerpt,
154
+ image: article.featuredImage
155
+ }, article.date && { datePublished: new Date(article.date).toISOString() }), {
156
+ author: { "@type": "Person", name: article.author },
157
+ publisher: { "@type": "Organization", name: siteName },
158
+ mainEntityOfPage: { "@type": "WebPage", "@id": articleUrl }
159
+ });
160
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
161
+ "script",
162
+ {
163
+ type: "application/ld+json",
164
+ dangerouslySetInnerHTML: { __html: JSON.stringify(schema) }
165
+ }
166
+ );
167
+ }
168
+ function BreadcrumbSchema({ items }) {
169
+ const schema = {
170
+ "@context": "https://schema.org",
171
+ "@type": "BreadcrumbList",
172
+ itemListElement: items.map((item, index) => ({
173
+ "@type": "ListItem",
174
+ position: index + 1,
175
+ name: item.name,
176
+ item: item.url
177
+ }))
178
+ };
179
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
180
+ "script",
181
+ {
182
+ type: "application/ld+json",
183
+ dangerouslySetInnerHTML: { __html: JSON.stringify(schema) }
184
+ }
185
+ );
186
+ }
187
+ function CollectionPageSchema({
188
+ title,
189
+ description,
190
+ url,
191
+ articleCount
192
+ }) {
193
+ const schema = {
194
+ "@context": "https://schema.org",
195
+ "@type": "CollectionPage",
196
+ name: title,
197
+ description,
198
+ url,
199
+ numberOfItems: articleCount
200
+ };
201
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
202
+ "script",
203
+ {
204
+ type: "application/ld+json",
205
+ dangerouslySetInnerHTML: { __html: JSON.stringify(schema) }
206
+ }
207
+ );
208
+ }
209
+
210
+ // src/ArticleSearchBar.tsx
211
+ var import_lucide_react = require("lucide-react");
212
+ var import_jsx_runtime3 = require("react/jsx-runtime");
213
+ function ArticleSearchBar({
214
+ value,
215
+ onChange,
216
+ disabled,
217
+ resultCount
218
+ }) {
219
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("section", { className: "py-8 bg-background border-b border-border", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "max-w-4xl mx-auto px-4 sm:px-6 lg:px-8", children: [
220
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", children: [
221
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react.Search, { className: "absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5" }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
223
+ "input",
224
+ {
225
+ type: "text",
226
+ placeholder: "Search articles (3+ characters required)...",
227
+ value,
228
+ onChange: (e) => onChange(e.target.value),
229
+ disabled,
230
+ 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"
231
+ }
232
+ ),
233
+ value && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
234
+ "button",
235
+ {
236
+ type: "button",
237
+ onClick: () => onChange(""),
238
+ "aria-label": "Clear search",
239
+ className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground",
240
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react.X, { className: "h-5 w-5" })
241
+ }
242
+ )
243
+ ] }),
244
+ value && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-2 text-sm text-muted-foreground", children: resultCount === 0 ? "No articles found matching your search." : `Found ${resultCount} article${resultCount === 1 ? "" : "s"} matching "${value}"` })
245
+ ] }) });
246
+ }
247
+
248
+ // src/ArticlesHero.tsx
249
+ var import_link2 = __toESM(require("next/link"), 1);
250
+ var import_jsx_runtime4 = require("react/jsx-runtime");
251
+ function ArticlesHero() {
252
+ 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: "Vox Populus Insights" }),
254
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xl text-muted-foreground mb-8", children: "Expert analysis, campaign strategies, and voter engagement insights from the frontlines of democracy." }),
255
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col sm:flex-row gap-4 justify-center", children: [
256
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
257
+ import_link2.default,
258
+ {
259
+ href: "#latest",
260
+ 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",
261
+ children: "Browse Latest Articles"
262
+ }
263
+ ),
264
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
265
+ import_link2.default,
266
+ {
267
+ href: "/contact",
268
+ 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",
269
+ children: "Contact Our Team"
270
+ }
271
+ )
272
+ ] })
273
+ ] }) }) });
274
+ }
275
+
276
+ // src/FeaturedArticle.tsx
277
+ var import_link3 = __toESM(require("next/link"), 1);
278
+ var import_image2 = __toESM(require("next/image"), 1);
279
+ var import_lucide_react2 = require("lucide-react");
280
+ var import_jsx_runtime5 = require("react/jsx-runtime");
281
+ function FeaturedArticle({ article }) {
282
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("section", { className: "py-16 bg-background", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "grid lg:grid-cols-2 gap-12 items-center", children: [
283
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
284
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "inline-block bg-primary/10 text-primary text-sm font-medium px-3 py-1 rounded-full mb-4", children: "FEATURED ARTICLE" }),
285
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { className: "text-3xl font-bold text-foreground mb-4", children: article.title }),
286
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-lg text-muted-foreground mb-6", children: article.excerpt }),
287
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-6 text-sm text-muted-foreground mb-6", children: [
288
+ article.date && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
289
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.Calendar, { className: "h-4 w-4" }),
290
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: article.date })
291
+ ] }),
292
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
293
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.Clock, { className: "h-4 w-4" }),
294
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: article.readTime })
295
+ ] }),
296
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2", children: [
297
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.User, { className: "h-4 w-4" }),
298
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: article.author })
299
+ ] })
300
+ ] }),
301
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
302
+ import_link3.default,
303
+ {
304
+ href: `/articles/${article.slug}`,
305
+ 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",
306
+ children: [
307
+ "Read Full Article",
308
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.ArrowRight, { className: "ml-2 h-4 w-4" })
309
+ ]
310
+ }
311
+ )
312
+ ] }),
313
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "bg-card rounded-lg p-8", children: [
314
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
315
+ import_image2.default,
316
+ {
317
+ src: article.featuredImage,
318
+ alt: article.title,
319
+ width: 800,
320
+ height: 450,
321
+ className: "w-full h-64 object-cover rounded-lg mb-4"
322
+ }
323
+ ),
324
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "bg-primary/10 text-primary text-xs font-medium px-2 py-1 rounded", children: article.category }) })
325
+ ] })
326
+ ] }) }) });
327
+ }
328
+
329
+ // src/LatestArticlesSection.tsx
330
+ var import_lucide_react4 = require("lucide-react");
331
+
332
+ // src/LatestArticles.tsx
333
+ var import_react2 = require("react");
334
+
335
+ // src/ArticleCard.tsx
336
+ var import_image3 = __toESM(require("next/image"), 1);
337
+ var import_link4 = __toESM(require("next/link"), 1);
338
+ var import_lucide_react3 = require("lucide-react");
339
+ var import_jsx_runtime6 = require("react/jsx-runtime");
340
+ function ArticleCard({ article }) {
341
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "rounded-lg border border-border bg-card text-card-foreground shadow-sm hover:shadow-lg transition-shadow duration-300", children: [
342
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "p-0", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
343
+ import_image3.default,
344
+ {
345
+ src: article.featuredImage,
346
+ alt: article.title,
347
+ className: "w-full h-48 object-cover rounded-t-lg",
348
+ width: 400,
349
+ height: 200
350
+ }
351
+ ) }),
352
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "p-6", children: [
353
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex gap-2 mb-3", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "bg-primary/10 text-primary text-xs font-medium px-2 py-1 rounded", children: article.category }) }),
354
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "text-lg font-semibold leading-none tracking-tight mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
355
+ import_link4.default,
356
+ {
357
+ href: `/articles/${article.slug}`,
358
+ className: "hover:text-indigo-600 transition-colors",
359
+ children: article.title
360
+ }
361
+ ) }),
362
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-muted-foreground mb-4 line-clamp-3", children: article.excerpt }),
363
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center justify-between text-sm text-muted-foreground border-t pt-4", children: [
364
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-3", children: [
365
+ article.date && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-2", children: [
366
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Calendar, { className: "h-3 w-3" }),
367
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: article.date })
368
+ ] }),
369
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-2", children: [
370
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Clock, { className: "h-3 w-3" }),
371
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: article.readTime })
372
+ ] })
373
+ ] }),
374
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
375
+ import_link4.default,
376
+ {
377
+ href: `/articles/${article.slug}`,
378
+ className: "inline-flex items-center justify-center h-8 w-8 p-0 rounded-md hover:bg-accent transition-colors",
379
+ "aria-label": `Read ${article.title}`,
380
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.ArrowRight, { className: "h-4 w-4" })
381
+ }
382
+ )
383
+ ] })
384
+ ] })
385
+ ] });
386
+ }
387
+
388
+ // src/LatestArticles.tsx
389
+ var import_jsx_runtime7 = require("react/jsx-runtime");
390
+ function LatestArticles({ articles, pageSize }) {
391
+ const [visibleCount, setVisibleCount] = (0, import_react2.useState)(pageSize);
392
+ (0, import_react2.useEffect)(() => {
393
+ setVisibleCount(pageSize);
394
+ }, [articles, pageSize]);
395
+ const visible = articles.slice(0, visibleCount);
396
+ const hasMore = visibleCount < articles.length;
397
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
398
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "grid md:grid-cols-2 lg:grid-cols-3 gap-8", children: visible.map((article) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ArticleCard, { article }, article.slug)) }),
399
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-10 text-center", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
400
+ "button",
401
+ {
402
+ type: "button",
403
+ onClick: () => setVisibleCount((c) => c + pageSize),
404
+ 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",
405
+ children: "Load more articles"
406
+ }
407
+ ) })
408
+ ] });
409
+ }
410
+
411
+ // src/LatestArticlesSection.tsx
412
+ var import_jsx_runtime8 = require("react/jsx-runtime");
413
+ function DescriptionText({
414
+ searchQuery,
415
+ count
416
+ }) {
417
+ const pluralSuffix = count === 1 ? "" : "s";
418
+ const text = searchQuery ? `Found ${count} article${pluralSuffix} matching your search.` : "Stay informed with our latest insights on voter engagement, campaign strategies, and civic technology.";
419
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-lg text-muted-foreground", children: text });
420
+ }
421
+ function LatestArticlesSection({
422
+ articles,
423
+ searchQuery,
424
+ onClearSearch,
425
+ pageSize = 6
426
+ }) {
427
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("section", { id: "latest", className: "py-16 bg-muted/50", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: [
428
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "text-center mb-12", children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h2", { className: "text-3xl font-bold text-foreground mb-4", children: searchQuery ? "Search Results" : "Latest Articles" }),
430
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(DescriptionText, { searchQuery, count: articles.length })
431
+ ] }),
432
+ articles.length === 0 && searchQuery ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "text-center py-12", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "max-w-md mx-auto", children: [
433
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.Search, { className: "h-12 w-12 text-muted-foreground mx-auto mb-4" }),
434
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { className: "text-lg font-semibold text-foreground mb-2", children: "No articles found" }),
435
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("p", { className: "text-muted-foreground mb-4", children: [
436
+ `We couldn't find any articles matching "`,
437
+ searchQuery,
438
+ '". Try adjusting your search terms.'
439
+ ] }),
440
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
441
+ "button",
442
+ {
443
+ type: "button",
444
+ onClick: onClearSearch,
445
+ 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",
446
+ children: "Clear search"
447
+ }
448
+ )
449
+ ] }) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LatestArticles, { articles, pageSize }, searchQuery)
450
+ ] }) });
451
+ }
452
+
453
+ // src/articlesConfig.ts
454
+ var DEFAULT_PAGE_SIZE = 6;
455
+ var DEFAULT_CATEGORIES_PAGE_SIZE = 8;
456
+ var DEFAULT_LAYOUT = [
457
+ "hero",
458
+ "search",
459
+ "featured",
460
+ "latest",
461
+ "categories"
462
+ ];
463
+
464
+ // src/useArticles.ts
465
+ var import_react3 = require("react");
466
+ function useArticles() {
467
+ const [searchQuery, setSearchQuery] = (0, import_react3.useState)("");
468
+ const debounceRef = (0, import_react3.useRef)(null);
469
+ const [articles, setArticles] = (0, import_react3.useState)([]);
470
+ const [categories, setCategories] = (0, import_react3.useState)([]);
471
+ const [loading, setLoading] = (0, import_react3.useState)(true);
472
+ const [error, setError] = (0, import_react3.useState)(null);
473
+ const fetchArticles = (0, import_react3.useCallback)((query) => __async(this, null, function* () {
474
+ try {
475
+ setLoading(true);
476
+ const url = query ? `/api/articles?q=${encodeURIComponent(query)}` : "/api/articles";
477
+ const response = yield fetch(url);
478
+ if (!response.ok)
479
+ throw new Error("Failed to fetch articles");
480
+ const data = yield response.json();
481
+ setArticles(data.articles);
482
+ if (!(query == null ? void 0 : query.trim())) {
483
+ const catMap = /* @__PURE__ */ new Map();
484
+ for (const article of data.articles) {
485
+ for (const cat of article.categories) {
486
+ if (!catMap.has(cat)) {
487
+ catMap.set(cat, { count: 0, featuredImage: article.featuredImage });
488
+ }
489
+ catMap.get(cat).count++;
490
+ }
491
+ }
492
+ setCategories(
493
+ Array.from(catMap.entries()).map(([name, { count, featuredImage }]) => ({
494
+ name,
495
+ slug: name.toLowerCase().replaceAll(/\s+/g, "-").replaceAll(/[^a-z0-9-]/g, ""),
496
+ count,
497
+ featuredImage
498
+ })).sort((a, b) => b.count - a.count)
499
+ );
500
+ }
501
+ setError(null);
502
+ } catch (err) {
503
+ setError(err instanceof Error ? err.message : "Failed to load articles");
504
+ setArticles([]);
505
+ } finally {
506
+ setLoading(false);
507
+ }
508
+ }), []);
509
+ (0, import_react3.useEffect)(() => {
510
+ fetchArticles("");
511
+ }, [fetchArticles]);
512
+ const handleSearch = (0, import_react3.useCallback)(
513
+ (query) => {
514
+ setSearchQuery(query);
515
+ if (debounceRef.current)
516
+ clearTimeout(debounceRef.current);
517
+ if (query === "") {
518
+ fetchArticles("");
519
+ } else if (query.length >= 3) {
520
+ debounceRef.current = setTimeout(() => fetchArticles(query), 1e3);
521
+ }
522
+ },
523
+ [fetchArticles]
524
+ );
525
+ (0, import_react3.useEffect)(
526
+ () => () => {
527
+ if (debounceRef.current)
528
+ clearTimeout(debounceRef.current);
529
+ },
530
+ []
531
+ );
532
+ return { articles, categories, searchQuery, loading, error, handleSearch };
533
+ }
534
+
535
+ // src/ArticlesPage.tsx
536
+ var import_jsx_runtime9 = require("react/jsx-runtime");
537
+ function buildThemeVars(theme) {
538
+ if (!theme)
539
+ return {};
540
+ const vars = {};
541
+ if (theme.fontFamily)
542
+ vars["--articles-font-family"] = theme.fontFamily;
543
+ if (theme.headerColor)
544
+ vars["--articles-header-color"] = theme.headerColor;
545
+ if (theme.textColor)
546
+ vars["--articles-text-color"] = theme.textColor;
547
+ if (theme.backgroundColor)
548
+ vars["--articles-bg-color"] = theme.backgroundColor;
549
+ if (theme.linkColor)
550
+ vars["--articles-link-color"] = theme.linkColor;
551
+ if (theme.linkHoverColor)
552
+ vars["--articles-link-hover-color"] = theme.linkHoverColor;
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;
565
+ return vars;
566
+ }
567
+ function renderSection(section, ctx) {
568
+ const {
569
+ articles,
570
+ displayedArticles,
571
+ categories,
572
+ searchQuery,
573
+ loading,
574
+ error,
575
+ handleSearch,
576
+ pageSize,
577
+ categoriesPageSize
578
+ } = ctx;
579
+ switch (section) {
580
+ case "hero":
581
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ArticlesHero, {}, "hero");
582
+ case "search":
583
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
584
+ ArticleSearchBar,
585
+ {
586
+ value: searchQuery,
587
+ onChange: handleSearch,
588
+ disabled: loading,
589
+ resultCount: articles.length
590
+ },
591
+ "search"
592
+ );
593
+ case "featured":
594
+ return articles.length > 0 && !searchQuery ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeaturedArticle, { article: articles[0] }, "featured") : null;
595
+ case "latest":
596
+ if (loading && articles.length === 0) {
597
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("section", { id: "latest", className: "py-16 bg-muted/50 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center py-24", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "text-center", children: [
598
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4" }),
599
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-muted-foreground", children: "Loading articles..." })
600
+ ] }) }) }, "latest");
601
+ }
602
+ if (error) {
603
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("section", { id: "latest", className: "py-16 bg-muted/50 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "bg-red-50 border border-red-200 rounded-lg p-6 max-w-md text-center", children: [
604
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h3", { className: "text-lg font-semibold text-red-800 mb-2", children: "Error Loading Articles" }),
605
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-red-600 mb-4", children: error }),
606
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
607
+ "button",
608
+ {
609
+ type: "button",
610
+ onClick: () => handleSearch(""),
611
+ 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",
612
+ children: "Try again"
613
+ }
614
+ )
615
+ ] }) }) }, "latest");
616
+ }
617
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
618
+ LatestArticlesSection,
619
+ {
620
+ articles: displayedArticles,
621
+ searchQuery,
622
+ onClearSearch: () => handleSearch(""),
623
+ pageSize
624
+ },
625
+ "latest"
626
+ );
627
+ case "categories":
628
+ return searchQuery ? null : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
629
+ ArticleCategoryGrid,
630
+ {
631
+ categories,
632
+ pageSize: categoriesPageSize
633
+ },
634
+ "categories"
635
+ );
636
+ default:
637
+ return null;
638
+ }
639
+ }
640
+ function ArticlesPage({ config }) {
641
+ var _a, _b, _c;
642
+ const state = useArticles();
643
+ const layout = (_a = config.layout) != null ? _a : DEFAULT_LAYOUT;
644
+ const pageSize = (_b = config.pageSize) != null ? _b : DEFAULT_PAGE_SIZE;
645
+ const categoriesPageSize = (_c = config.categoriesPageSize) != null ? _c : DEFAULT_CATEGORIES_PAGE_SIZE;
646
+ const themeVars = buildThemeVars(config.theme);
647
+ const hasFeatured = layout.includes("featured");
648
+ const articlesWithoutFeatured = hasFeatured ? state.articles.slice(1) : state.articles;
649
+ const displayedArticles = state.searchQuery ? state.articles : articlesWithoutFeatured;
650
+ const ctx = __spreadProps(__spreadValues({}, state), { displayedArticles, pageSize, categoriesPageSize });
651
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: themeVars, children: [
652
+ layout.map((section) => renderSection(section, ctx)),
653
+ state.articles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
654
+ CollectionPageSchema,
655
+ {
656
+ title: `Articles | ${config.siteName}`,
657
+ description: `Browse all ${state.articles.length} articles on ${config.siteName}.`,
658
+ url: `${config.siteUrl}/articles`,
659
+ articleCount: state.articles.length
660
+ }
661
+ )
662
+ ] });
663
+ }
664
+
665
+ // src/CategoryArticlesPage.tsx
666
+ var import_image4 = __toESM(require("next/image"), 1);
667
+ var import_link5 = __toESM(require("next/link"), 1);
668
+ var import_jsx_runtime10 = require("react/jsx-runtime");
669
+ function getCategoryDescription(config, slug, categoryName) {
670
+ var _a;
671
+ const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[slug];
672
+ if (!raw)
673
+ return { short: `Browse all articles in ${categoryName}.` };
674
+ if (typeof raw === "string")
675
+ return { short: raw };
676
+ return raw;
677
+ }
678
+ function CategoryArticlesPage({ category, articles, config }) {
679
+ var _a;
680
+ if (articles.length === 0)
681
+ return null;
682
+ const categoryName = articles[0].category;
683
+ const heroImage = articles[0].featuredImage;
684
+ const description = getCategoryDescription(config, category, categoryName);
685
+ const pageSize = (_a = config.pageSize) != null ? _a : DEFAULT_PAGE_SIZE;
686
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
687
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
688
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
689
+ BreadcrumbSchema,
690
+ {
691
+ items: [
692
+ { name: "Home", url: siteUrl },
693
+ { name: "Articles", url: `${siteUrl}/articles` },
694
+ { name: categoryName, url: `${siteUrl}/articles/category/${category}` }
695
+ ]
696
+ }
697
+ ),
698
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("section", { className: "relative h-72 md:h-96 flex items-center justify-center overflow-hidden", children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
700
+ import_image4.default,
701
+ {
702
+ src: heroImage,
703
+ alt: categoryName,
704
+ fill: true,
705
+ sizes: "100vw",
706
+ className: "object-cover",
707
+ priority: true
708
+ }
709
+ ),
710
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "absolute inset-0 bg-black/60" }),
711
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "relative z-10 text-center text-white px-4", children: [
712
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-sm font-semibold uppercase tracking-widest mb-3 opacity-80", children: "Category" }),
713
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("h1", { className: "text-4xl md:text-5xl font-bold mb-3", children: categoryName }),
714
+ description.long && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-lg opacity-90 max-w-2xl mx-auto mt-2", children: description.long }),
715
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("p", { className: "text-lg opacity-70 mt-2", children: [
716
+ articles.length,
717
+ " article",
718
+ articles.length === 1 ? "" : "s"
719
+ ] })
720
+ ] })
721
+ ] }),
722
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("section", { className: "py-16 bg-muted/50 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", children: [
723
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "mb-8 flex items-center justify-between", children: [
724
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_link5.default, { href: "/articles", className: "text-sm text-primary hover:underline", children: "\u2190 All Articles" }),
725
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-sm text-muted-foreground", children: description.short })
726
+ ] }),
727
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(LatestArticles, { articles, pageSize })
728
+ ] }) })
729
+ ] });
730
+ }
731
+
732
+ // src/CommentsSection.tsx
733
+ var import_react6 = require("next-auth/react");
734
+ var import_react7 = require("react");
735
+
736
+ // src/CommentForm.tsx
737
+ var import_react4 = require("react");
738
+ var import_jsx_runtime11 = require("react/jsx-runtime");
739
+ var MAX_LENGTH = 2e3;
740
+ var WARN_THRESHOLD = 1800;
741
+ function submitLabel(submitting, parentId) {
742
+ if (submitting)
743
+ return "Posting\u2026";
744
+ return parentId ? "Reply" : "Post comment";
745
+ }
746
+ function CommentForm({ articleSlug, parentId, onSuccess, onCancel }) {
747
+ const [body, setBody] = (0, import_react4.useState)("");
748
+ const [submitting, setSubmitting] = (0, import_react4.useState)(false);
749
+ const [error, setError] = (0, import_react4.useState)(null);
750
+ const remaining = MAX_LENGTH - body.length;
751
+ function handleSubmit(e) {
752
+ return __async(this, null, function* () {
753
+ var _a;
754
+ e.preventDefault();
755
+ if (!body.trim() || submitting)
756
+ return;
757
+ setSubmitting(true);
758
+ setError(null);
759
+ try {
760
+ const res = yield fetch(`/api/articles/${articleSlug}/comments`, {
761
+ method: "POST",
762
+ headers: { "Content-Type": "application/json" },
763
+ body: JSON.stringify({ body: body.trim(), parentId: parentId != null ? parentId : null })
764
+ });
765
+ if (!res.ok) {
766
+ const data = yield res.json();
767
+ setError((_a = data.error) != null ? _a : "Failed to post comment");
768
+ return;
769
+ }
770
+ setBody("");
771
+ onSuccess();
772
+ } catch (e2) {
773
+ setError("Network error \u2014 please try again");
774
+ } finally {
775
+ setSubmitting(false);
776
+ }
777
+ });
778
+ }
779
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-2", children: [
780
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
781
+ "textarea",
782
+ {
783
+ value: body,
784
+ onChange: (e) => setBody(e.target.value),
785
+ maxLength: MAX_LENGTH,
786
+ rows: parentId ? 3 : 4,
787
+ placeholder: parentId ? "Write a reply\u2026" : "Join the discussion\u2026",
788
+ disabled: submitting,
789
+ 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",
790
+ "aria-label": parentId ? "Reply text" : "Comment text"
791
+ }
792
+ ),
793
+ remaining <= MAX_LENGTH - WARN_THRESHOLD && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: `text-xs ${remaining < 0 ? "text-destructive" : "text-muted-foreground"}`, children: [
794
+ remaining,
795
+ " characters remaining"
796
+ ] }),
797
+ error && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-destructive", children: error }),
798
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
799
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
800
+ "button",
801
+ {
802
+ type: "submit",
803
+ disabled: submitting || body.trim().length === 0 || body.length > MAX_LENGTH,
804
+ 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",
805
+ children: submitLabel(submitting, parentId)
806
+ }
807
+ ),
808
+ onCancel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
809
+ "button",
810
+ {
811
+ type: "button",
812
+ onClick: onCancel,
813
+ className: "rounded-md px-4 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground",
814
+ children: "Cancel"
815
+ }
816
+ )
817
+ ] })
818
+ ] });
819
+ }
820
+
821
+ // src/CommentItem.tsx
822
+ var import_react5 = require("react");
823
+ var import_jsx_runtime12 = require("react/jsx-runtime");
824
+ function relativeTime(iso) {
825
+ const diff = Date.now() - new Date(iso).getTime();
826
+ const minutes = Math.floor(diff / 6e4);
827
+ if (minutes < 1)
828
+ return "just now";
829
+ if (minutes < 60)
830
+ return `${minutes}m ago`;
831
+ const hours = Math.floor(minutes / 60);
832
+ if (hours < 24)
833
+ return `${hours}h ago`;
834
+ const days = Math.floor(hours / 24);
835
+ if (days < 30)
836
+ return `${days}d ago`;
837
+ const months = Math.floor(days / 30);
838
+ if (months < 12)
839
+ return `${months}mo ago`;
840
+ return `${Math.floor(months / 12)}y ago`;
841
+ }
842
+ function CommentItem({
843
+ comment,
844
+ articleSlug,
845
+ currentUserId,
846
+ isReply = false,
847
+ onRefresh
848
+ }) {
849
+ const [showReplyForm, setShowReplyForm] = (0, import_react5.useState)(false);
850
+ const [deleting, setDeleting] = (0, import_react5.useState)(false);
851
+ const replies = "replies" in comment ? comment.replies : [];
852
+ const canReply = !isReply && !!currentUserId && !comment.isDeleted;
853
+ const canDelete = !!currentUserId && currentUserId === comment.authorId && !comment.isDeleted;
854
+ function handleDelete() {
855
+ return __async(this, null, function* () {
856
+ if (deleting)
857
+ return;
858
+ setDeleting(true);
859
+ try {
860
+ yield fetch(`/api/articles/${articleSlug}/comments/${comment.id}`, { method: "DELETE" });
861
+ onRefresh();
862
+ } finally {
863
+ setDeleting(false);
864
+ }
865
+ });
866
+ }
867
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: `${isReply ? "ml-8 border-l-2 border-border pl-4" : ""}`, children: [
868
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "rounded-lg bg-muted/40 p-3 space-y-1", children: comment.isDeleted ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm italic text-muted-foreground", children: "Comment removed" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
869
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
870
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-sm font-medium text-foreground", children: comment.authorName }),
871
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-xs text-muted-foreground", children: relativeTime(comment.createdAt) })
872
+ ] }),
873
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-sm text-foreground whitespace-pre-wrap break-words", children: comment.body }),
874
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex gap-3 pt-1", children: [
875
+ canReply && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
876
+ "button",
877
+ {
878
+ onClick: () => setShowReplyForm((v) => !v),
879
+ className: "text-xs text-muted-foreground hover:text-foreground",
880
+ children: showReplyForm ? "Cancel" : "Reply"
881
+ }
882
+ ),
883
+ canDelete && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
884
+ "button",
885
+ {
886
+ onClick: handleDelete,
887
+ disabled: deleting,
888
+ className: "text-xs text-muted-foreground hover:text-destructive disabled:opacity-50",
889
+ children: deleting ? "Deleting\u2026" : "Delete"
890
+ }
891
+ )
892
+ ] })
893
+ ] }) }),
894
+ showReplyForm && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-2 ml-2", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
895
+ CommentForm,
896
+ {
897
+ articleSlug,
898
+ parentId: comment.id,
899
+ onSuccess: () => {
900
+ setShowReplyForm(false);
901
+ onRefresh();
902
+ },
903
+ onCancel: () => setShowReplyForm(false)
904
+ }
905
+ ) }),
906
+ replies.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "mt-2 space-y-2", children: replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
907
+ CommentItem,
908
+ {
909
+ comment: __spreadProps(__spreadValues({}, reply), { replies: [] }),
910
+ articleSlug,
911
+ currentUserId,
912
+ isReply: true,
913
+ onRefresh
914
+ },
915
+ reply.id
916
+ )) })
917
+ ] });
918
+ }
919
+
920
+ // src/CommentThread.tsx
921
+ var import_jsx_runtime13 = require("react/jsx-runtime");
922
+ function CommentThread({
923
+ comments,
924
+ articleSlug,
925
+ currentUserId,
926
+ onRefresh
927
+ }) {
928
+ if (comments.length === 0) {
929
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-muted-foreground text-center py-6", children: "No comments yet. Be the first to share your thoughts." });
930
+ }
931
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "space-y-4", children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
932
+ CommentItem,
933
+ {
934
+ comment,
935
+ articleSlug,
936
+ currentUserId,
937
+ onRefresh
938
+ },
939
+ comment.id
940
+ )) });
941
+ }
942
+
943
+ // src/CommentsSection.tsx
944
+ var import_jsx_runtime14 = require("react/jsx-runtime");
945
+ function CommentsSection({ articleSlug, config }) {
946
+ var _a, _b, _c, _d, _e, _f;
947
+ const { data: session, status } = (0, import_react6.useSession)();
948
+ const [comments, setComments] = (0, import_react7.useState)([]);
949
+ const [loading, setLoading] = (0, import_react7.useState)(true);
950
+ const [fetchError, setFetchError] = (0, import_react7.useState)(null);
951
+ const perArticleEnabled = (_b = (_a = config.perArticleOverride) == null ? void 0 : _a[articleSlug]) != null ? _b : true;
952
+ const enabled = config.enabled && perArticleEnabled;
953
+ const currentUserId = (_f = (_e = (_c = session == null ? void 0 : session.user) == null ? void 0 : _c.email) != null ? _e : (_d = session == null ? void 0 : session.user) == null ? void 0 : _d.name) != null ? _f : void 0;
954
+ const fetchComments = (0, import_react7.useCallback)(() => __async(this, null, function* () {
955
+ setLoading(true);
956
+ setFetchError(null);
957
+ try {
958
+ const res = yield fetch(`/api/articles/${articleSlug}/comments`);
959
+ if (!res.ok)
960
+ throw new Error("Failed to load comments");
961
+ const data = yield res.json();
962
+ setComments(data.comments);
963
+ } catch (e) {
964
+ setFetchError("Could not load comments. Please try again.");
965
+ } finally {
966
+ setLoading(false);
967
+ }
968
+ }), [articleSlug]);
969
+ (0, import_react7.useEffect)(() => {
970
+ if (enabled) {
971
+ fetchComments().catch(() => void 0);
972
+ }
973
+ }, [fetchComments, enabled]);
974
+ if (!enabled)
975
+ return null;
976
+ const count = comments.length;
977
+ const label = count === 1 ? "1 comment" : `${count} comments`;
978
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("section", { "aria-label": "Comments", className: "mt-12 pt-8 border-t border-border space-y-6", children: [
979
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h2", { className: "text-xl font-semibold text-foreground", children: loading ? "Comments" : label }),
980
+ status === "authenticated" ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CommentForm, { articleSlug, onSuccess: fetchComments }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
981
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
982
+ "button",
983
+ {
984
+ onClick: () => (0, import_react6.signIn)(),
985
+ className: "text-primary underline hover:no-underline focus:outline-none",
986
+ children: "Sign in"
987
+ }
988
+ ),
989
+ " ",
990
+ "to join the discussion."
991
+ ] }),
992
+ fetchError && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-destructive", children: fetchError }),
993
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: "Loading comments\u2026" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
994
+ CommentThread,
995
+ {
996
+ comments,
997
+ articleSlug,
998
+ currentUserId,
999
+ onRefresh: fetchComments
1000
+ }
1001
+ )
1002
+ ] });
1003
+ }
1004
+ // Annotate the CommonJS export names for ESM import in node:
1005
+ 0 && (module.exports = {
1006
+ ArticleCard,
1007
+ ArticleCategoryGrid,
1008
+ ArticleSchema,
1009
+ ArticleSearchBar,
1010
+ ArticlesHero,
1011
+ ArticlesPage,
1012
+ BreadcrumbSchema,
1013
+ CategoryArticlesPage,
1014
+ CollectionPageSchema,
1015
+ CommentForm,
1016
+ CommentItem,
1017
+ CommentThread,
1018
+ CommentsSection,
1019
+ DEFAULT_CATEGORIES_PAGE_SIZE,
1020
+ DEFAULT_LAYOUT,
1021
+ DEFAULT_PAGE_SIZE,
1022
+ FeaturedArticle,
1023
+ LatestArticles,
1024
+ LatestArticlesSection,
1025
+ useArticles
1026
+ });
1027
+ //# sourceMappingURL=index.cjs.map