@contentgrowth/content-widget 1.3.3 → 1.3.5

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 CHANGED
@@ -268,25 +268,81 @@ const widget = new ContentGrowthWidget(container, {
268
268
  />
269
269
  ```
270
270
 
271
+ **ContentCard:**
272
+
273
+ A standalone article card component. Use it to display individual articles outside of ContentList.
274
+
275
+ ```astro
276
+ <!-- Load by slug -->
277
+ <ContentCard
278
+ apiKey="pk_your_key_here"
279
+ slug="my-article-slug"
280
+ linkPattern="/articles/{slug}"
281
+ />
282
+
283
+ <!-- Load by UUID -->
284
+ <ContentCard
285
+ apiKey="pk_your_key_here"
286
+ uuid="article-uuid"
287
+ />
288
+
289
+ <!-- Use pre-loaded article data -->
290
+ <ContentCard
291
+ article={articleData}
292
+ linkPattern="/articles/{slug}"
293
+ showSummary={true}
294
+ showTags={true}
295
+ />
296
+ ```
297
+
298
+ | Prop | Type | Default | Description |
299
+ |------|------|---------|-------------|
300
+ | `apiKey` | string | - | Your API key (required unless `article` is provided) |
301
+ | `article` | Article | - | Pre-loaded article data (skips API fetch) |
302
+ | `slug` | string | - | Load specific article by slug |
303
+ | `uuid` | string | - | Load specific article by UUID |
304
+ | `linkPattern` | string | '/articles/{slug}' | URL pattern for link |
305
+ | `linkTarget` | string | - | Link target attribute |
306
+ | `showSummary` | boolean | true | Show article summary |
307
+ | `summaryMaxLength` | number | - | Truncate summary at length |
308
+ | `showTags` | boolean | false | Show article tags |
309
+ | `showCategory` | boolean | true | Show category badge |
310
+
271
311
  **FeaturedCard:**
272
312
 
273
313
  A compact card displaying the Featured Summary with customizable styling. Perfect for landing pages.
274
314
 
275
315
  ```astro
316
+ <!-- Find latest article in category -->
276
317
  <FeaturedCard
277
318
  apiKey="pk_your_key_here"
278
319
  category="announce"
279
320
  linkPattern="/articles/{slug}"
280
321
  layout="horizontal"
281
322
  borderStyle="dashed"
282
- itemsBackground="#f3f4f6"
283
323
  ctaText="Read full story"
284
324
  />
325
+
326
+ <!-- Load specific article by slug -->
327
+ <FeaturedCard
328
+ apiKey="pk_your_key_here"
329
+ slug="my-article-slug"
330
+ linkPattern="/articles/{slug}"
331
+ />
332
+
333
+ <!-- Use pre-loaded article data (for lists) -->
334
+ <FeaturedCard
335
+ article={articleData}
336
+ linkPattern="/articles/{slug}"
337
+ />
285
338
  ```
286
339
 
287
340
  | Prop | Type | Default | Description |
288
341
  |------|------|---------|-------------|
289
- | `apiKey` | string | - | Your API key |
342
+ | `apiKey` | string | - | Your API key (required unless `article` is provided) |
343
+ | `article` | Article | - | Pre-loaded article data (skips API fetch) |
344
+ | `slug` | string | - | Load specific article by slug |
345
+ | `uuid` | string | - | Load specific article by UUID |
290
346
  | `category` | string | - | Filter by category |
291
347
  | `tags` | string[] | [] | Filter by tags |
292
348
  | `layout` | 'vertical' \| 'horizontal' | auto | Card layout (auto uses article setting) |
@@ -299,6 +355,24 @@ A compact card displaying the Featured Summary with customizable styling. Perfec
299
355
  | `showReadingTime` | boolean | false | Show reading time |
300
356
  | `linkPattern` | string | '/articles/{slug}' | URL pattern for link |
301
357
 
358
+ **Featured Cards List:**
359
+
360
+ Display all articles as FeaturedCards in a grid using `displayAs`:
361
+
362
+ ```astro
363
+ <ContentList
364
+ apiKey="pk_your_key_here"
365
+ displayAs="featured-cards"
366
+ linkPattern="/articles/{slug}"
367
+ pageSize={12}
368
+ />
369
+ ```
370
+
371
+ | displayAs Value | Description |
372
+ |-----------------|-------------|
373
+ | `'default'` | Standard card/row layout |
374
+ | `'featured-cards'` | Renders each article as a FeaturedCard |
375
+
302
376
  ## API Client
303
377
 
304
378
  ### ContentGrowthClient
@@ -0,0 +1,170 @@
1
+ ---
2
+ import { marked } from 'marked';
3
+ import { ContentGrowthClient } from '../core/client';
4
+ import { formatDate, calculateReadingTime } from '../core/utils';
5
+ import type { Article, ArticleWithContent } from '../types';
6
+
7
+ interface ContentCardProps {
8
+ /**
9
+ * Pre-loaded article data (bypasses API fetch)
10
+ */
11
+ article?: Article | ArticleWithContent;
12
+
13
+ /**
14
+ * Load specific article by slug
15
+ */
16
+ slug?: string;
17
+
18
+ /**
19
+ * Load specific article by UUID
20
+ */
21
+ uuid?: string;
22
+
23
+ /**
24
+ * API key (required if fetching article)
25
+ */
26
+ apiKey?: string;
27
+
28
+ /**
29
+ * API base URL
30
+ */
31
+ baseUrl?: string;
32
+
33
+ /**
34
+ * URL pattern for the article link
35
+ * Supports placeholders: {uuid}, {slug}, {category}
36
+ * @default '/articles/{slug}'
37
+ */
38
+ linkPattern?: string;
39
+
40
+ /**
41
+ * Link target attribute
42
+ */
43
+ linkTarget?: string;
44
+
45
+ /**
46
+ * Show article summary
47
+ * @default true
48
+ */
49
+ showSummary?: boolean;
50
+
51
+ /**
52
+ * Maximum length of summary text
53
+ */
54
+ summaryMaxLength?: number;
55
+
56
+ /**
57
+ * Show article tags
58
+ * @default false
59
+ */
60
+ showTags?: boolean;
61
+
62
+ /**
63
+ * Show article category
64
+ * @default true
65
+ */
66
+ showCategory?: boolean;
67
+ }
68
+
69
+ type Props = ContentCardProps & { class?: string };
70
+
71
+ const {
72
+ apiKey,
73
+ baseUrl,
74
+ article: providedArticle,
75
+ slug,
76
+ uuid,
77
+ linkPattern = '/articles/{slug}',
78
+ linkTarget,
79
+ showSummary = true,
80
+ summaryMaxLength,
81
+ showTags = false,
82
+ showCategory = true,
83
+ class: className = ''
84
+ } = Astro.props;
85
+
86
+ // Determine article source
87
+ let article: Article | ArticleWithContent | null = null;
88
+ let error: string | null = null;
89
+
90
+ if (providedArticle) {
91
+ // Mode 1: Article provided directly
92
+ article = providedArticle;
93
+ } else if (apiKey) {
94
+ // Need to fetch article from API
95
+ const client = new ContentGrowthClient({ apiKey, baseUrl });
96
+
97
+ try {
98
+ if (uuid) {
99
+ // Mode 2: Load by UUID
100
+ article = await client.getArticle(uuid);
101
+ } else if (slug) {
102
+ // Mode 3: Load by slug
103
+ article = await client.getArticleBySlug(slug);
104
+ }
105
+ } catch (e) {
106
+ console.error('Failed to fetch article:', e);
107
+ error = e instanceof Error ? e.message : 'Failed to load article';
108
+ }
109
+ }
110
+
111
+ // Generate article URL
112
+ const getArticleUrl = (article: any) => {
113
+ if (!article) return '#';
114
+ return linkPattern
115
+ .replace('{uuid}', article.uuid || '')
116
+ .replace('{slug}', article.slug || article.uuid || '')
117
+ .replace('{category}', article.category || 'uncategorized');
118
+ };
119
+
120
+ // Truncate summary text
121
+ const truncateSummary = (text: string | null, maxLength?: number): string => {
122
+ if (!text) return '';
123
+ if (!maxLength || text.length <= maxLength) return text;
124
+ return text.substring(0, maxLength).trim() + '...';
125
+ };
126
+ ---
127
+
128
+ {article ? (
129
+ <article class={`cg-article-card ${className}`}>
130
+ <a href={getArticleUrl(article)} target={linkTarget} class="cg-card-link">
131
+ <div class="cg-card-content">
132
+ {showCategory && article.category && (
133
+ <div class="cg-card-category">
134
+ <span class="cg-category-badge">{article.category}</span>
135
+ </div>
136
+ )}
137
+
138
+ <h2 class="cg-card-title">{article.title}</h2>
139
+
140
+ {showSummary && article.summary && (
141
+ <p class="cg-card-summary">{truncateSummary(article.summary, summaryMaxLength)}</p>
142
+ )}
143
+
144
+ <div class="cg-card-meta">
145
+ <span class="cg-meta-author">{article.authorName}</span>
146
+ <span class="cg-meta-separator">•</span>
147
+ <time class="cg-meta-date" datetime={new Date(article.publishedAt * 1000).toISOString()}>
148
+ {formatDate(article.publishedAt)}
149
+ </time>
150
+ <span class="cg-meta-separator">•</span>
151
+ <span class="cg-meta-reading-time">{calculateReadingTime(article.wordCount)}</span>
152
+ </div>
153
+
154
+ {showTags && article.tags && article.tags.length > 0 && (
155
+ <div class="cg-card-tags">
156
+ {article.tags.map((tag) => (
157
+ <span class="cg-tag">{tag}</span>
158
+ ))}
159
+ </div>
160
+ )}
161
+ </div>
162
+ </a>
163
+ </article>
164
+ ) : (
165
+ error ? (
166
+ <div class={`cg-widget cg-error ${className}`}>
167
+ {error}
168
+ </div>
169
+ ) : null
170
+ )}
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import type { ContentListProps } from '../types/index.js';
7
7
  import { ContentGrowthClient } from '../core/client.js';
8
- import { formatDate, calculateReadingTime } from '../core/utils.js';
8
+ import FeaturedCard from './FeaturedCard.astro';
9
+ import ContentCard from './ContentCard.astro';
9
10
 
10
11
  interface Props extends ContentListProps {}
11
12
 
@@ -14,6 +15,7 @@ const {
14
15
  baseUrl,
15
16
  layout = 'cards',
16
17
  displayMode = 'comfortable',
18
+ displayAs = 'default',
17
19
  theme = 'light',
18
20
  pageSize = 12,
19
21
  tags = [],
@@ -67,10 +69,13 @@ function truncateSummary(text: string | null, maxLength?: number): string {
67
69
  if (!maxLength || text.length <= maxLength) return text;
68
70
  return text.substring(0, maxLength).trim() + '...';
69
71
  }
72
+
73
+ // Check if we're in featured-cards mode
74
+ const isFeaturedCardsMode = displayAs === 'featured-cards';
70
75
  ---
71
76
 
72
77
  <div
73
- class={`cg-content-list cg-layout-${layout} cg-display-${displayMode} cg-theme-${theme} ${className}`}
78
+ class={`cg-content-list cg-layout-${layout} cg-display-${displayMode} cg-theme-${theme} ${isFeaturedCardsMode ? 'cg-featured-cards-list' : ''} ${className}`}
74
79
  data-cg-widget="list"
75
80
  >
76
81
  {articles.length === 0 ? (
@@ -79,60 +84,38 @@ function truncateSummary(text: string | null, maxLength?: number): string {
79
84
  </div>
80
85
  ) : (
81
86
  <>
82
- <div class={`cg-articles-grid ${layout === 'cards' ? 'cg-grid' : 'cg-list'}`}>
87
+ <div class={`cg-articles-grid ${isFeaturedCardsMode ? 'cg-featured-cards-grid' : (layout === 'cards' ? 'cg-grid' : 'cg-list')}`}>
83
88
  {articles.map((article) => {
84
- // Build article URL from pattern
85
- const articleUrl = linkPattern
86
- .replace('{uuid}', article.uuid)
87
- .replace('{slug}', article.slug)
88
- .replace('{category}', article.category || '');
89
-
90
89
  // Build link target from pattern
91
90
  const articleTarget = linkTarget
92
91
  ? linkTarget
93
92
  .replace('{uuid}', article.uuid)
94
93
  .replace('{id}', article.uuid)
95
94
  : undefined;
96
-
97
- const readingTime = calculateReadingTime(article.wordCount);
98
- const publishedDate = formatDate(article.publishedAt);
99
95
 
96
+ // Featured Cards Mode - Use FeaturedCard component
97
+ if (isFeaturedCardsMode) {
98
+ return (
99
+ <FeaturedCard
100
+ article={article}
101
+ linkPattern={linkPattern}
102
+ linkTarget={articleTarget}
103
+ showCategory={true}
104
+ />
105
+ );
106
+ }
107
+
108
+ // Default Card Mode - Use ContentCard component
100
109
  return (
101
- <article class="cg-article-card">
102
- <a href={articleUrl} target={articleTarget} class="cg-card-link">
103
- <div class="cg-card-content">
104
- {article.category && (
105
- <div class="cg-card-category">
106
- <span class="cg-category-badge">{article.category}</span>
107
- </div>
108
- )}
109
-
110
- <h2 class="cg-card-title">{article.title}</h2>
111
-
112
- {showAiSummary && article.summary && (
113
- <p class="cg-card-summary">{truncateSummary(article.summary, summaryMaxLength)}</p>
114
- )}
115
-
116
- <div class="cg-card-meta">
117
- <span class="cg-meta-author">{article.authorName}</span>
118
- <span class="cg-meta-separator">•</span>
119
- <time class="cg-meta-date" datetime={new Date(article.publishedAt * 1000).toISOString()}>
120
- {publishedDate}
121
- </time>
122
- <span class="cg-meta-separator">•</span>
123
- <span class="cg-meta-reading-time">{readingTime}</span>
124
- </div>
125
-
126
- {showTags && article.tags && article.tags.length > 0 && (
127
- <div class="cg-card-tags">
128
- {article.tags.map((tag) => (
129
- <span class="cg-tag">{tag}</span>
130
- ))}
131
- </div>
132
- )}
133
- </div>
134
- </a>
135
- </article>
110
+ <ContentCard
111
+ article={article}
112
+ linkPattern={linkPattern}
113
+ linkTarget={articleTarget}
114
+ showSummary={showAiSummary}
115
+ summaryMaxLength={summaryMaxLength}
116
+ showTags={showTags}
117
+ showCategory={true}
118
+ />
136
119
  );
137
120
  })}
138
121
  </div>
@@ -1,9 +1,24 @@
1
1
  ---
2
2
  import { marked } from 'marked';
3
3
  import { ContentGrowthClient } from '../core/client';
4
- import type { FeaturedContentProps } from '../types';
4
+ import type { FeaturedContentProps, Article, ArticleWithContent } from '../types';
5
5
 
6
6
  interface FeaturedCardProps extends Omit<FeaturedContentProps, 'showBackButton' | 'backUrl'> {
7
+ /**
8
+ * Pre-loaded article data (bypasses API fetch - used by ContentList)
9
+ */
10
+ article?: Article | ArticleWithContent;
11
+
12
+ /**
13
+ * Load specific article by slug (bypasses category/tags search)
14
+ */
15
+ slug?: string;
16
+
17
+ /**
18
+ * Load specific article by UUID (bypasses category/tags search)
19
+ */
20
+ uuid?: string;
21
+
7
22
  /**
8
23
  * URL pattern for the article link
9
24
  * Supports placeholders: {uuid}, {slug}, {category}
@@ -57,6 +72,12 @@ interface FeaturedCardProps extends Omit<FeaturedContentProps, 'showBackButton'
57
72
  * @default '#f3f4f6'
58
73
  */
59
74
  itemsBackground?: string;
75
+
76
+ /**
77
+ * Link target attribute
78
+ * @default undefined (same tab)
79
+ */
80
+ linkTarget?: string;
60
81
  }
61
82
 
62
83
  type Props = FeaturedCardProps & { class?: string };
@@ -64,14 +85,18 @@ type Props = FeaturedCardProps & { class?: string };
64
85
  const {
65
86
  apiKey,
66
87
  baseUrl,
88
+ article: providedArticle,
89
+ slug,
90
+ uuid,
67
91
  tags = [],
68
92
  category,
69
93
  excludeTags = [],
70
94
  showCategory = true,
71
95
  showReadingTime = false,
72
96
  showAuthor = false,
73
- ctaText = 'Read full story',
97
+ ctaText: propCtaText,
74
98
  linkPattern = '/articles/{slug}',
99
+ linkTarget,
75
100
  layout: propLayout,
76
101
  borderStyle = 'none',
77
102
  borderColor = '#e5e7eb',
@@ -80,21 +105,36 @@ const {
80
105
  class: className = ''
81
106
  } = Astro.props;
82
107
 
83
- // Fetch featured article
84
- const client = new ContentGrowthClient({ apiKey, baseUrl });
85
- let article = null;
86
- let error = null;
87
-
88
- try {
89
- article = await client.getFeaturedArticle({
90
- tags,
91
- category,
92
- excludeTags
93
- });
94
- } catch (e) {
95
- // If 404, article remains null
96
- console.error('Failed to fetch featured article:', e);
97
- error = e instanceof Error ? e.message : 'Failed to load featured article';
108
+ // Determine article source
109
+ let article: Article | ArticleWithContent | null = null;
110
+ let error: string | null = null;
111
+
112
+ if (providedArticle) {
113
+ // Mode 1: Article provided directly (from ContentList)
114
+ article = providedArticle;
115
+ } else if (apiKey) {
116
+ // Need to fetch article from API
117
+ const client = new ContentGrowthClient({ apiKey, baseUrl });
118
+
119
+ try {
120
+ if (uuid) {
121
+ // Mode 2: Load by UUID
122
+ article = await client.getArticle(uuid, { excludeTags });
123
+ } else if (slug) {
124
+ // Mode 3: Load by slug
125
+ article = await client.getArticleBySlug(slug, { excludeTags });
126
+ } else {
127
+ // Mode 4: Find featured article by category/tags (original behavior)
128
+ article = await client.getFeaturedArticle({
129
+ tags,
130
+ category,
131
+ excludeTags
132
+ });
133
+ }
134
+ } catch (e) {
135
+ console.error('Failed to fetch article:', e);
136
+ error = e instanceof Error ? e.message : 'Failed to load article';
137
+ }
98
138
  }
99
139
 
100
140
  // Generate article URL
@@ -113,10 +153,13 @@ const getSummaryHtml = (article: any) => {
113
153
  return marked.parse(summaryText, { async: false }) as string;
114
154
  };
115
155
 
116
- const layout = propLayout || article?.featuredSummaryLayout || 'standard';
117
- const layoutClass = layout !== 'standard' ? `cg-layout-${layout}` : '';
156
+ const layout = propLayout || (article as any)?.featuredSummaryLayout || 'vertical';
157
+ const layoutClass = layout !== 'vertical' ? `cg-layout-${layout}` : '';
118
158
  const borderClass = borderStyle !== 'none' ? `cg-border-${borderStyle}` : '';
119
159
 
160
+ // Use ctaText from prop, or from article data, or fallback to default
161
+ const ctaText = propCtaText || (article as any)?.featuredCtaText || 'Read full story';
162
+
120
163
  // Custom CSS properties for styling
121
164
  const customStyles = [
122
165
  borderColor !== '#e5e7eb' ? `--cg-card-border-color: ${borderColor}` : '',
@@ -131,6 +174,8 @@ const customStyles = [
131
174
  class={`cg-widget cg-featured-card ${className} ${layoutClass} ${borderClass}`}
132
175
  style={customStyles || undefined}
133
176
  data-cg-widget="featured-card"
177
+ target={linkTarget}
178
+ rel={linkTarget === '_blank' ? 'noopener noreferrer' : undefined}
134
179
  >
135
180
  <article class="cg-featured-card-inner">
136
181
  {/* Header with category badge */}
@@ -184,5 +229,3 @@ const customStyles = [
184
229
  </div>
185
230
  ) : null
186
231
  )}
187
-
188
-
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import type { Article, ArticleWithContent } from '../types';
3
+ export interface ContentCardProps {
4
+ /**
5
+ * Pre-loaded article data (bypasses API fetch)
6
+ */
7
+ article?: Article | ArticleWithContent;
8
+ /**
9
+ * Load specific article by slug
10
+ */
11
+ slug?: string;
12
+ /**
13
+ * Load specific article by UUID
14
+ */
15
+ uuid?: string;
16
+ /**
17
+ * API key (required if fetching article)
18
+ */
19
+ apiKey?: string;
20
+ /**
21
+ * API base URL
22
+ */
23
+ baseUrl?: string;
24
+ /**
25
+ * URL pattern for the article link
26
+ * @default '/articles/{slug}'
27
+ */
28
+ linkPattern?: string;
29
+ /**
30
+ * Link target attribute
31
+ */
32
+ linkTarget?: string;
33
+ /**
34
+ * Show article summary
35
+ * @default true
36
+ */
37
+ showSummary?: boolean;
38
+ /**
39
+ * Maximum length of summary text
40
+ */
41
+ summaryMaxLength?: number;
42
+ /**
43
+ * Show article tags
44
+ * @default false
45
+ */
46
+ showTags?: boolean;
47
+ /**
48
+ * Show article category
49
+ * @default true
50
+ */
51
+ showCategory?: boolean;
52
+ /**
53
+ * Additional CSS class
54
+ */
55
+ className?: string;
56
+ }
57
+ export declare const ContentCard: React.FC<ContentCardProps>;
58
+ export default ContentCard;
59
+ //# sourceMappingURL=ContentCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContentCard.d.ts","sourceRoot":"","sources":["../../src/react/ContentCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC7B;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,kBAAkB,CAAC;IAEvC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAgIlD,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -0,0 +1,84 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ContentGrowthClient } from '../core/client';
3
+ import { formatDate, calculateReadingTime } from '../core/utils';
4
+ export const ContentCard = ({ apiKey, baseUrl, article: providedArticle, slug, uuid, linkPattern = '/articles/{slug}', linkTarget, showSummary = true, summaryMaxLength, showTags = false, showCategory = true, className = '' }) => {
5
+ const [article, setArticle] = useState(providedArticle || null);
6
+ const [loading, setLoading] = useState(!providedArticle && !!(apiKey && (slug || uuid)));
7
+ const [error, setError] = useState(null);
8
+ useEffect(() => {
9
+ // If article is provided, use it directly
10
+ if (providedArticle) {
11
+ setArticle(providedArticle);
12
+ setLoading(false);
13
+ return;
14
+ }
15
+ // Need API key and identifier to fetch
16
+ if (!apiKey || (!slug && !uuid)) {
17
+ setLoading(false);
18
+ return;
19
+ }
20
+ const fetchArticle = async () => {
21
+ setLoading(true);
22
+ try {
23
+ const client = new ContentGrowthClient({ apiKey, baseUrl });
24
+ if (uuid) {
25
+ const fetchedArticle = await client.getArticle(uuid);
26
+ setArticle(fetchedArticle);
27
+ }
28
+ else if (slug) {
29
+ const fetchedArticle = await client.getArticleBySlug(slug);
30
+ setArticle(fetchedArticle);
31
+ }
32
+ }
33
+ catch (err) {
34
+ setError(err instanceof Error ? err.message : 'Failed to load article');
35
+ }
36
+ finally {
37
+ setLoading(false);
38
+ }
39
+ };
40
+ fetchArticle();
41
+ }, [apiKey, baseUrl, providedArticle, uuid, slug]);
42
+ // Generate article URL
43
+ const getArticleUrl = (article) => {
44
+ return linkPattern
45
+ .replace('{uuid}', article.uuid || '')
46
+ .replace('{slug}', article.slug || article.uuid || '')
47
+ .replace('{category}', article.category || 'uncategorized');
48
+ };
49
+ // Truncate summary text
50
+ const truncateSummary = (text, maxLength) => {
51
+ if (!text)
52
+ return '';
53
+ if (!maxLength || text.length <= maxLength)
54
+ return text;
55
+ return text.substring(0, maxLength).trim() + '...';
56
+ };
57
+ if (loading) {
58
+ return (React.createElement("div", { className: `cg-widget cg-loading ${className}` },
59
+ React.createElement("div", { className: "cg-spinner" })));
60
+ }
61
+ if (error || !article) {
62
+ if (error) {
63
+ return (React.createElement("div", { className: `cg-widget cg-error ${className}` }, error));
64
+ }
65
+ return null;
66
+ }
67
+ const readingTime = calculateReadingTime(article.wordCount);
68
+ const publishedDate = formatDate(article.publishedAt);
69
+ return (React.createElement("article", { className: `cg-article-card ${className}` },
70
+ React.createElement("a", { href: getArticleUrl(article), target: linkTarget, className: "cg-card-link" },
71
+ React.createElement("div", { className: "cg-card-content" },
72
+ showCategory && article.category && (React.createElement("div", { className: "cg-card-category" },
73
+ React.createElement("span", { className: "cg-category-badge" }, article.category))),
74
+ React.createElement("h2", { className: "cg-card-title" }, article.title),
75
+ showSummary && article.summary && (React.createElement("p", { className: "cg-card-summary" }, truncateSummary(article.summary, summaryMaxLength))),
76
+ React.createElement("div", { className: "cg-card-meta" },
77
+ React.createElement("span", { className: "cg-meta-author" }, article.authorName),
78
+ React.createElement("span", { className: "cg-meta-separator" }, "\u2022"),
79
+ React.createElement("time", { className: "cg-meta-date", dateTime: new Date(article.publishedAt * 1000).toISOString() }, publishedDate),
80
+ React.createElement("span", { className: "cg-meta-separator" }, "\u2022"),
81
+ React.createElement("span", { className: "cg-meta-reading-time" }, readingTime)),
82
+ showTags && article.tags && article.tags.length > 0 && (React.createElement("div", { className: "cg-card-tags" }, article.tags.map((tag) => (React.createElement("span", { key: tag, className: "cg-tag" }, tag)))))))));
83
+ };
84
+ export default ContentCard;
@@ -1 +1 @@
1
- {"version":3,"file":"ContentList.d.ts","sourceRoot":"","sources":["../../src/react/ContentList.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,KAAK,EAAE,gBAAgB,EAAW,MAAM,mBAAmB,CAAC;AAEnE,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAiLvD,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ContentList.d.ts","sourceRoot":"","sources":["../../src/react/ContentList.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,KAAK,EAAE,gBAAgB,EAAW,MAAM,mBAAmB,CAAC;AAEnE,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAsKvD,CAAC;AAEF,eAAe,WAAW,CAAC"}