@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 +76 -2
- package/dist/astro/ContentCard.astro +170 -0
- package/dist/astro/ContentList.astro +30 -47
- package/dist/astro/FeaturedCard.astro +64 -21
- package/dist/react/ContentCard.d.ts +59 -0
- package/dist/react/ContentCard.d.ts.map +1 -0
- package/dist/react/ContentCard.js +84 -0
- package/dist/react/ContentList.d.ts.map +1 -1
- package/dist/react/ContentList.js +12 -21
- package/dist/react/FeaturedCard.d.ts +14 -2
- package/dist/react/FeaturedCard.d.ts.map +1 -1
- package/dist/react/FeaturedCard.js +38 -13
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/styles.css +17 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/vue/ContentCard.vue +148 -0
- package/dist/vue/ContentList.vue +35 -33
- package/dist/vue/FeaturedCard.vue +90 -38
- package/dist/widget/widget.css +1 -1
- package/dist/widget/widget.dev.css +17 -0
- package/dist/widget/widget.dev.js +3 -3
- package/dist/widget/widget.js +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
//
|
|
84
|
-
|
|
85
|
-
let
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
article =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 || '
|
|
117
|
-
const layoutClass = 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;
|
|
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"}
|