@contentgrowth/content-widget 1.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.
- package/LICENSE +21 -0
- package/README.md +442 -0
- package/dist/astro/ContentList.astro +177 -0
- package/dist/astro/ContentViewer.astro +252 -0
- package/dist/astro/index.d.ts +9 -0
- package/dist/astro/index.d.ts.map +1 -0
- package/dist/astro/index.js +8 -0
- package/dist/core/client.d.ts +67 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +217 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/utils.d.ts +32 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +70 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/react/ContentList.d.ts +12 -0
- package/dist/react/ContentList.d.ts.map +1 -0
- package/dist/react/ContentList.js +106 -0
- package/dist/react/ContentViewer.d.ts +12 -0
- package/dist/react/ContentViewer.d.ts.map +1 -0
- package/dist/react/ContentViewer.js +97 -0
- package/dist/react/hooks.d.ts +63 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +140 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/styles.css +970 -0
- package/dist/types/index.d.ts +271 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +16 -0
- package/dist/vue/ContentList.vue +166 -0
- package/dist/vue/ContentViewer.vue +137 -0
- package/dist/vue/composables.d.ts +64 -0
- package/dist/vue/composables.d.ts.map +1 -0
- package/dist/vue/composables.js +165 -0
- package/dist/vue/index.d.ts +10 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +8 -0
- package/dist/widget/content-card.js +190 -0
- package/dist/widget/content-list.js +289 -0
- package/dist/widget/content-viewer.js +230 -0
- package/dist/widget/index.js +40 -0
- package/dist/widget/utils/api-client.js +154 -0
- package/dist/widget/utils/helpers.js +71 -0
- package/dist/widget/widget-js/content-card.js +190 -0
- package/dist/widget/widget-js/content-list.js +289 -0
- package/dist/widget/widget-js/content-viewer.js +230 -0
- package/dist/widget/widget-js/index.js +40 -0
- package/dist/widget/widget-js/utils/api-client.js +154 -0
- package/dist/widget/widget-js/utils/helpers.js +71 -0
- package/dist/widget/widget-js/widget.d.ts +24 -0
- package/dist/widget/widget-js/widget.js +240 -0
- package/dist/widget/widget.d.ts +24 -0
- package/dist/widget/widget.js +240 -0
- package/package.json +99 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue Composables for Content Growth API
|
|
3
|
+
*/
|
|
4
|
+
import { ref, watch } from 'vue';
|
|
5
|
+
import { ContentGrowthClient } from '../core/client.js';
|
|
6
|
+
/**
|
|
7
|
+
* Composable to fetch articles list
|
|
8
|
+
*/
|
|
9
|
+
export function useArticles(options) {
|
|
10
|
+
const articles = ref([]);
|
|
11
|
+
const pagination = ref(null);
|
|
12
|
+
const loading = ref(true);
|
|
13
|
+
const error = ref(null);
|
|
14
|
+
const fetchArticles = async () => {
|
|
15
|
+
loading.value = true;
|
|
16
|
+
error.value = null;
|
|
17
|
+
try {
|
|
18
|
+
const client = new ContentGrowthClient({
|
|
19
|
+
apiKey: options.apiKey,
|
|
20
|
+
baseUrl: options.baseUrl
|
|
21
|
+
});
|
|
22
|
+
const result = await client.listArticles({
|
|
23
|
+
page: options.page,
|
|
24
|
+
limit: options.limit,
|
|
25
|
+
tags: options.tags,
|
|
26
|
+
category: options.category
|
|
27
|
+
});
|
|
28
|
+
articles.value = result.articles;
|
|
29
|
+
pagination.value = result.pagination;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
error.value = err;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
loading.value = false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
// Fetch on mount
|
|
39
|
+
fetchArticles();
|
|
40
|
+
// Watch for options changes
|
|
41
|
+
watch(() => [options.apiKey, options.baseUrl, options.page, options.limit, options.tags, options.category], () => {
|
|
42
|
+
fetchArticles();
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
articles,
|
|
46
|
+
pagination,
|
|
47
|
+
loading,
|
|
48
|
+
error,
|
|
49
|
+
refetch: fetchArticles
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Composable to fetch a single article
|
|
54
|
+
*/
|
|
55
|
+
export function useArticle(options) {
|
|
56
|
+
const article = ref(null);
|
|
57
|
+
const loading = ref(true);
|
|
58
|
+
const error = ref(null);
|
|
59
|
+
const fetchArticle = async () => {
|
|
60
|
+
loading.value = true;
|
|
61
|
+
error.value = null;
|
|
62
|
+
try {
|
|
63
|
+
const client = new ContentGrowthClient({
|
|
64
|
+
apiKey: options.apiKey,
|
|
65
|
+
baseUrl: options.baseUrl
|
|
66
|
+
});
|
|
67
|
+
const result = await client.getArticle(options.uuid);
|
|
68
|
+
article.value = result;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
error.value = err;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
loading.value = false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
// Fetch on mount
|
|
78
|
+
fetchArticle();
|
|
79
|
+
// Watch for options changes
|
|
80
|
+
watch(() => [options.apiKey, options.uuid, options.baseUrl], () => {
|
|
81
|
+
fetchArticle();
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
article,
|
|
85
|
+
loading,
|
|
86
|
+
error,
|
|
87
|
+
refetch: fetchArticle
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Composable to fetch categories
|
|
92
|
+
*/
|
|
93
|
+
export function useCategories(options) {
|
|
94
|
+
const categories = ref([]);
|
|
95
|
+
const loading = ref(true);
|
|
96
|
+
const error = ref(null);
|
|
97
|
+
const fetchCategories = async () => {
|
|
98
|
+
loading.value = true;
|
|
99
|
+
error.value = null;
|
|
100
|
+
try {
|
|
101
|
+
const client = new ContentGrowthClient({
|
|
102
|
+
apiKey: options.apiKey,
|
|
103
|
+
baseUrl: options.baseUrl
|
|
104
|
+
});
|
|
105
|
+
const result = await client.getCategories();
|
|
106
|
+
categories.value = result.categories;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
error.value = err;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
loading.value = false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// Fetch on mount
|
|
116
|
+
fetchCategories();
|
|
117
|
+
// Watch for options changes
|
|
118
|
+
watch(() => [options.apiKey, options.baseUrl], () => {
|
|
119
|
+
fetchCategories();
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
categories,
|
|
123
|
+
loading,
|
|
124
|
+
error,
|
|
125
|
+
refetch: fetchCategories
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Composable to fetch tags
|
|
130
|
+
*/
|
|
131
|
+
export function useTags(options) {
|
|
132
|
+
const tags = ref([]);
|
|
133
|
+
const loading = ref(true);
|
|
134
|
+
const error = ref(null);
|
|
135
|
+
const fetchTags = async () => {
|
|
136
|
+
loading.value = true;
|
|
137
|
+
error.value = null;
|
|
138
|
+
try {
|
|
139
|
+
const client = new ContentGrowthClient({
|
|
140
|
+
apiKey: options.apiKey,
|
|
141
|
+
baseUrl: options.baseUrl
|
|
142
|
+
});
|
|
143
|
+
const result = await client.getTags();
|
|
144
|
+
tags.value = result.tags;
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
error.value = err;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
loading.value = false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
// Fetch on mount
|
|
154
|
+
fetchTags();
|
|
155
|
+
// Watch for options changes
|
|
156
|
+
watch(() => [options.apiKey, options.baseUrl], () => {
|
|
157
|
+
fetchTags();
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
tags,
|
|
161
|
+
loading,
|
|
162
|
+
error,
|
|
163
|
+
refetch: fetchTags
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth Content Widget - Vue Components
|
|
3
|
+
*
|
|
4
|
+
* Note: Vue components are not compiled by TypeScript.
|
|
5
|
+
* They should be processed by your Vue build tool (Vite, etc.).
|
|
6
|
+
* Import them directly from the .vue files in your Vue project.
|
|
7
|
+
*/
|
|
8
|
+
export * from './composables.js';
|
|
9
|
+
export type { ContentListProps, ContentViewerProps } from '../types/index.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vue/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,kBAAkB,CAAC;AAGjC,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth Content Widget - Vue Components
|
|
3
|
+
*
|
|
4
|
+
* Note: Vue components are not compiled by TypeScript.
|
|
5
|
+
* They should be processed by your Vue build tool (Vite, etc.).
|
|
6
|
+
* Import them directly from the .vue files in your Vue project.
|
|
7
|
+
*/
|
|
8
|
+
export * from './composables.js';
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Card Component
|
|
3
|
+
* Displays a single content item in compact or expanded mode
|
|
4
|
+
*/
|
|
5
|
+
import { formatDate, calculateReadingTime, escapeHtml } from '../utils/helpers.js';
|
|
6
|
+
|
|
7
|
+
export class ContentCard {
|
|
8
|
+
constructor(article, options = {}) {
|
|
9
|
+
this.article = article;
|
|
10
|
+
this.displayMode = options.displayMode || 'compact';
|
|
11
|
+
this.viewerMode = options.viewerMode || 'inline';
|
|
12
|
+
this.externalUrlPattern = options.externalUrlPattern || '/article/{id}';
|
|
13
|
+
this.externalTarget = options.externalTarget || 'article-{id}';
|
|
14
|
+
this.onExpand = options.onExpand || null;
|
|
15
|
+
this.onClick = options.onClick || null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render the content card
|
|
20
|
+
*/
|
|
21
|
+
render() {
|
|
22
|
+
// Map display modes: compact/comfortable/spacious all use compact layout
|
|
23
|
+
// expanded shows summary
|
|
24
|
+
const layoutMode = this.displayMode === 'expanded' ? 'expanded' : 'compact';
|
|
25
|
+
|
|
26
|
+
// For external mode, wrap in <a> tag
|
|
27
|
+
if (this.viewerMode === 'external') {
|
|
28
|
+
const link = document.createElement('a');
|
|
29
|
+
link.className = `cg-card cg-card--${this.displayMode}`;
|
|
30
|
+
link.dataset.contentId = this.article.uuid;
|
|
31
|
+
|
|
32
|
+
// Generate URL and target
|
|
33
|
+
const url = this.externalUrlPattern.replace('{id}', this.article.uuid);
|
|
34
|
+
const target = this.externalTarget.replace('{id}', this.article.uuid);
|
|
35
|
+
|
|
36
|
+
link.href = url;
|
|
37
|
+
link.target = target;
|
|
38
|
+
link.rel = 'noopener'; // Security best practice
|
|
39
|
+
|
|
40
|
+
if (layoutMode === 'compact') {
|
|
41
|
+
link.innerHTML = this.renderCompact();
|
|
42
|
+
} else {
|
|
43
|
+
link.innerHTML = this.renderExpanded();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add expand button handler
|
|
47
|
+
const expandBtn = link.querySelector('.cg-expand-btn');
|
|
48
|
+
if (expandBtn && this.onExpand) {
|
|
49
|
+
expandBtn.addEventListener('click', (e) => {
|
|
50
|
+
e.preventDefault(); // Prevent link navigation
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
this.onExpand(this.article, link);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return link;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// For inline/modal mode, use regular article element
|
|
60
|
+
const card = document.createElement('article');
|
|
61
|
+
card.className = `cg-card cg-card--${this.displayMode}`;
|
|
62
|
+
card.dataset.contentId = this.article.uuid;
|
|
63
|
+
|
|
64
|
+
if (layoutMode === 'compact') {
|
|
65
|
+
card.innerHTML = this.renderCompact();
|
|
66
|
+
} else {
|
|
67
|
+
card.innerHTML = this.renderExpanded();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add click handler for the whole card
|
|
71
|
+
card.addEventListener('click', (e) => {
|
|
72
|
+
// Don't trigger if clicking expand button
|
|
73
|
+
if (e.target.closest('.cg-expand-btn')) return;
|
|
74
|
+
|
|
75
|
+
if (this.onClick) {
|
|
76
|
+
this.onClick(this.article);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Add expand button handler if in compact mode
|
|
81
|
+
const expandBtn = card.querySelector('.cg-expand-btn');
|
|
82
|
+
if (expandBtn && this.onExpand) {
|
|
83
|
+
expandBtn.addEventListener('click', (e) => {
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
this.onExpand(this.article, card);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return card;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render compact mode (title, meta only)
|
|
94
|
+
*/
|
|
95
|
+
renderCompact() {
|
|
96
|
+
const readingTime = calculateReadingTime(this.article.wordCount);
|
|
97
|
+
|
|
98
|
+
return `
|
|
99
|
+
<div class="cg-card-header">
|
|
100
|
+
<h3 class="cg-card-title">${escapeHtml(this.article.title)}</h3>
|
|
101
|
+
<button class="cg-expand-btn" aria-label="Show more" title="Show summary">
|
|
102
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
103
|
+
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
104
|
+
</svg>
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="cg-card-meta">
|
|
108
|
+
<span class="cg-meta-item cg-author">
|
|
109
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
110
|
+
<path d="M7 7C8.65685 7 10 5.65685 10 4C10 2.34315 8.65685 1 7 1C5.34315 1 4 2.34315 4 4C4 5.65685 5.34315 7 7 7Z" stroke="currentColor" stroke-width="1.5"/>
|
|
111
|
+
<path d="M13 13C13 10.7909 10.3137 9 7 9C3.68629 9 1 10.7909 1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
112
|
+
</svg>
|
|
113
|
+
${escapeHtml(this.article.authorName)}
|
|
114
|
+
</span>
|
|
115
|
+
<span class="cg-meta-item cg-date">
|
|
116
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
117
|
+
<rect x="1" y="2" width="12" height="11" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
|
118
|
+
<path d="M4 1V3M10 1V3M1 5H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
119
|
+
</svg>
|
|
120
|
+
${formatDate(this.article.publishedAt)}
|
|
121
|
+
</span>
|
|
122
|
+
<span class="cg-meta-item cg-reading-time">
|
|
123
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
124
|
+
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5"/>
|
|
125
|
+
<path d="M7 3.5V7L9.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
126
|
+
</svg>
|
|
127
|
+
${readingTime}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render expanded mode (with summary and tags)
|
|
135
|
+
*/
|
|
136
|
+
renderExpanded() {
|
|
137
|
+
const readingTime = calculateReadingTime(this.article.wordCount);
|
|
138
|
+
const summary = this.article.summary || '';
|
|
139
|
+
const tags = this.article.tags || [];
|
|
140
|
+
|
|
141
|
+
return `
|
|
142
|
+
<div class="cg-card-header">
|
|
143
|
+
<h3 class="cg-card-title">${escapeHtml(this.article.title)}</h3>
|
|
144
|
+
<button class="cg-expand-btn cg-expand-btn--collapse" aria-label="Show less" title="Hide summary">
|
|
145
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
146
|
+
<path d="M12 10L8 6L4 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
147
|
+
</svg>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
${summary ? `
|
|
152
|
+
<div class="cg-card-summary">
|
|
153
|
+
<p>${escapeHtml(summary)}</p>
|
|
154
|
+
</div>
|
|
155
|
+
` : ''}
|
|
156
|
+
|
|
157
|
+
${tags.length > 0 ? `
|
|
158
|
+
<div class="cg-card-tags">
|
|
159
|
+
${tags.map(tag => `
|
|
160
|
+
<span class="cg-tag">${escapeHtml(tag)}</span>
|
|
161
|
+
`).join('')}
|
|
162
|
+
</div>
|
|
163
|
+
` : ''}
|
|
164
|
+
|
|
165
|
+
<div class="cg-card-meta">
|
|
166
|
+
<span class="cg-meta-item cg-author">
|
|
167
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
168
|
+
<path d="M7 7C8.65685 7 10 5.65685 10 4C10 2.34315 8.65685 1 7 1C5.34315 1 4 2.34315 4 4C4 5.65685 5.34315 7 7 7Z" stroke="currentColor" stroke-width="1.5"/>
|
|
169
|
+
<path d="M13 13C13 10.7909 10.3137 9 7 9C3.68629 9 1 10.7909 1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
170
|
+
</svg>
|
|
171
|
+
${escapeHtml(this.article.authorName)}
|
|
172
|
+
</span>
|
|
173
|
+
<span class="cg-meta-item cg-date">
|
|
174
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
175
|
+
<rect x="1" y="2" width="12" height="11" rx="2" stroke="currentColor" stroke-width="1.5"/>
|
|
176
|
+
<path d="M4 1V3M10 1V3M1 5H13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
177
|
+
</svg>
|
|
178
|
+
${formatDate(this.article.publishedAt)}
|
|
179
|
+
</span>
|
|
180
|
+
<span class="cg-meta-item cg-reading-time">
|
|
181
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
182
|
+
<circle cx="7" cy="7" r="6" stroke="currentColor" stroke-width="1.5"/>
|
|
183
|
+
<path d="M7 3.5V7L9.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
184
|
+
</svg>
|
|
185
|
+
${readingTime}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content List Component
|
|
3
|
+
* Displays a list of content items with pagination
|
|
4
|
+
*/
|
|
5
|
+
import { ContentCard } from './content-card.js';
|
|
6
|
+
|
|
7
|
+
export class ContentList {
|
|
8
|
+
constructor(container, api, options = {}) {
|
|
9
|
+
console.log('[ContentList] Constructor called with options:', options);
|
|
10
|
+
this.container = container;
|
|
11
|
+
this.api = api;
|
|
12
|
+
this.options = {
|
|
13
|
+
layoutMode: options.layoutMode || 'cards', // 'cards' or 'rows'
|
|
14
|
+
displayMode: options.displayMode || 'comfortable',
|
|
15
|
+
pageSize: parseInt(options.pageSize) || 12,
|
|
16
|
+
tags: options.tags || [],
|
|
17
|
+
viewerMode: options.viewerMode || 'inline', // 'inline' | 'modal' | 'external'
|
|
18
|
+
externalUrlPattern: options.externalUrlPattern || '/article/{id}',
|
|
19
|
+
externalTarget: options.externalTarget || 'article-{id}',
|
|
20
|
+
onArticleClick: options.onArticleClick || null
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
console.log('[ContentList] Final options:', this.options);
|
|
24
|
+
|
|
25
|
+
this.currentPage = 1;
|
|
26
|
+
this.totalPages = 1;
|
|
27
|
+
this.articles = [];
|
|
28
|
+
this.loading = false;
|
|
29
|
+
this.expandedCards = new Set();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize and render the list
|
|
34
|
+
*/
|
|
35
|
+
async init() {
|
|
36
|
+
console.log('[ContentList] Initializing...');
|
|
37
|
+
this.render();
|
|
38
|
+
await this.loadArticles();
|
|
39
|
+
console.log('[ContentList] Initialization complete');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render the list container
|
|
44
|
+
*/
|
|
45
|
+
render() {
|
|
46
|
+
const layoutClass = this.options.layoutMode === 'rows' ? 'cg-content-rows' : 'cg-content-grid';
|
|
47
|
+
|
|
48
|
+
this.container.innerHTML = `
|
|
49
|
+
<div class="cg-content-list">
|
|
50
|
+
<div class="cg-list-header">
|
|
51
|
+
<button class="cg-display-toggle" title="Toggle display mode">
|
|
52
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
53
|
+
<rect x="2" y="2" width="16" height="4" rx="1" stroke="currentColor" stroke-width="1.5"/>
|
|
54
|
+
<rect x="2" y="8" width="16" height="4" rx="1" stroke="currentColor" stroke-width="1.5"/>
|
|
55
|
+
<rect x="2" y="14" width="16" height="4" rx="1" stroke="currentColor" stroke-width="1.5"/>
|
|
56
|
+
</svg>
|
|
57
|
+
<span>${this.options.displayMode === 'compact' ? 'Show summaries' : 'Hide summaries'}</span>
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="${layoutClass}"></div>
|
|
61
|
+
<div class="cg-pagination"></div>
|
|
62
|
+
</div>
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
// Add toggle handler
|
|
66
|
+
const toggle = this.container.querySelector('.cg-display-toggle');
|
|
67
|
+
toggle.addEventListener('click', () => this.toggleDisplayMode());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load articles from API
|
|
72
|
+
*/
|
|
73
|
+
async loadArticles(page = 1) {
|
|
74
|
+
if (this.loading) {
|
|
75
|
+
console.log('[ContentList] Already loading, skipping...');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('[ContentList] Loading articles for page:', page);
|
|
80
|
+
this.loading = true;
|
|
81
|
+
this.showLoading();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
console.log('[ContentList] Calling api.fetchArticles with:', {
|
|
85
|
+
page,
|
|
86
|
+
limit: this.options.pageSize,
|
|
87
|
+
tags: this.options.tags
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const data = await this.api.fetchArticles({
|
|
91
|
+
page,
|
|
92
|
+
limit: this.options.pageSize,
|
|
93
|
+
tags: this.options.tags
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log('[ContentList] Received data:', data);
|
|
97
|
+
|
|
98
|
+
this.articles = data.articles || [];
|
|
99
|
+
this.currentPage = data.pagination?.page || 1;
|
|
100
|
+
this.totalPages = data.pagination?.totalPages || 1;
|
|
101
|
+
|
|
102
|
+
console.log('[ContentList] Loaded', this.articles.length, 'articles');
|
|
103
|
+
|
|
104
|
+
this.renderArticles();
|
|
105
|
+
this.renderPagination();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('[ContentList] Error loading articles:', error);
|
|
108
|
+
this.showError('Failed to load articles. Please try again.');
|
|
109
|
+
} finally {
|
|
110
|
+
this.loading = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Render content grid
|
|
116
|
+
*/
|
|
117
|
+
renderArticles() {
|
|
118
|
+
const grid = this.container.querySelector('.cg-content-grid, .cg-content-rows');
|
|
119
|
+
|
|
120
|
+
if (this.articles.length === 0) {
|
|
121
|
+
grid.innerHTML = `
|
|
122
|
+
<div class="cg-empty-state">
|
|
123
|
+
<svg width="64" height="64" viewBox="0 0 64 64" fill="none">
|
|
124
|
+
<rect x="8" y="12" width="48" height="40" rx="4" stroke="currentColor" stroke-width="2"/>
|
|
125
|
+
<path d="M16 24H48M16 32H48M16 40H32" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
126
|
+
</svg>
|
|
127
|
+
<p>No articles found</p>
|
|
128
|
+
</div>
|
|
129
|
+
`;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
grid.innerHTML = '';
|
|
134
|
+
|
|
135
|
+
this.articles.forEach(article => {
|
|
136
|
+
const isExpanded = this.expandedCards.has(article.uuid);
|
|
137
|
+
const card = new ContentCard(article, {
|
|
138
|
+
displayMode: isExpanded ? 'expanded' : this.options.displayMode,
|
|
139
|
+
viewerMode: this.options.viewerMode,
|
|
140
|
+
externalUrlPattern: this.options.externalUrlPattern,
|
|
141
|
+
externalTarget: this.options.externalTarget,
|
|
142
|
+
onExpand: (article, cardElement) => this.handleExpand(article, cardElement),
|
|
143
|
+
onClick: (article) => this.handleArticleClick(article)
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
grid.appendChild(card.render());
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Handle expand/collapse of a card
|
|
152
|
+
*/
|
|
153
|
+
handleExpand(article, cardElement) {
|
|
154
|
+
const isExpanded = this.expandedCards.has(article.uuid);
|
|
155
|
+
|
|
156
|
+
if (isExpanded) {
|
|
157
|
+
this.expandedCards.delete(article.uuid);
|
|
158
|
+
} else {
|
|
159
|
+
this.expandedCards.add(article.uuid);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Re-render just this card
|
|
163
|
+
const newCard = new ContentCard(article, {
|
|
164
|
+
displayMode: isExpanded ? this.options.displayMode : 'expanded',
|
|
165
|
+
viewerMode: this.options.viewerMode,
|
|
166
|
+
externalUrlPattern: this.options.externalUrlPattern,
|
|
167
|
+
externalTarget: this.options.externalTarget,
|
|
168
|
+
onExpand: (article, cardElement) => this.handleExpand(article, cardElement),
|
|
169
|
+
onClick: (article) => this.handleArticleClick(article)
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
cardElement.replaceWith(newCard.render());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Handle article click
|
|
177
|
+
*/
|
|
178
|
+
handleArticleClick(article) {
|
|
179
|
+
if (this.options.onArticleClick) {
|
|
180
|
+
this.options.onArticleClick(article);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Toggle display mode for all cards
|
|
186
|
+
*/
|
|
187
|
+
toggleDisplayMode() {
|
|
188
|
+
this.options.displayMode = this.options.displayMode === 'compact' ? 'expanded' : 'compact';
|
|
189
|
+
this.expandedCards.clear(); // Reset individual expansions
|
|
190
|
+
this.renderArticles();
|
|
191
|
+
|
|
192
|
+
// Update button text
|
|
193
|
+
const toggle = this.container.querySelector('.cg-display-toggle span');
|
|
194
|
+
toggle.textContent = this.options.displayMode === 'compact' ? 'Show summaries' : 'Hide summaries';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Render pagination controls
|
|
199
|
+
*/
|
|
200
|
+
renderPagination() {
|
|
201
|
+
const pagination = this.container.querySelector('.cg-pagination');
|
|
202
|
+
|
|
203
|
+
if (this.totalPages <= 1) {
|
|
204
|
+
pagination.innerHTML = '';
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const prevDisabled = this.currentPage === 1;
|
|
209
|
+
const nextDisabled = this.currentPage === this.totalPages;
|
|
210
|
+
|
|
211
|
+
pagination.innerHTML = `
|
|
212
|
+
<button class="cg-btn-prev" ${prevDisabled ? 'disabled' : ''}>
|
|
213
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
214
|
+
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
215
|
+
</svg>
|
|
216
|
+
Previous
|
|
217
|
+
</button>
|
|
218
|
+
<span class="cg-page-info">Page ${this.currentPage} of ${this.totalPages}</span>
|
|
219
|
+
<button class="cg-btn-next" ${nextDisabled ? 'disabled' : ''}>
|
|
220
|
+
Next
|
|
221
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
222
|
+
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
223
|
+
</svg>
|
|
224
|
+
</button>
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
// Add event listeners
|
|
228
|
+
const prevBtn = pagination.querySelector('.cg-btn-prev');
|
|
229
|
+
const nextBtn = pagination.querySelector('.cg-btn-next');
|
|
230
|
+
|
|
231
|
+
prevBtn.addEventListener('click', () => {
|
|
232
|
+
if (this.currentPage > 1) {
|
|
233
|
+
this.loadArticles(this.currentPage - 1);
|
|
234
|
+
this.scrollToTop();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
nextBtn.addEventListener('click', () => {
|
|
239
|
+
if (this.currentPage < this.totalPages) {
|
|
240
|
+
this.loadArticles(this.currentPage + 1);
|
|
241
|
+
this.scrollToTop();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Show loading state
|
|
248
|
+
*/
|
|
249
|
+
showLoading() {
|
|
250
|
+
const grid = this.container.querySelector('.cg-content-grid, .cg-content-rows');
|
|
251
|
+
if (grid) {
|
|
252
|
+
grid.innerHTML = `
|
|
253
|
+
<div class="cg-loading">
|
|
254
|
+
<div class="cg-spinner"></div>
|
|
255
|
+
<p>Loading articles...</p>
|
|
256
|
+
</div>
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Show error message
|
|
263
|
+
*/
|
|
264
|
+
showError(message) {
|
|
265
|
+
const grid = this.container.querySelector('.cg-content-grid, .cg-content-rows');
|
|
266
|
+
if (grid) {
|
|
267
|
+
grid.innerHTML = `
|
|
268
|
+
<div class="cg-error">
|
|
269
|
+
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
270
|
+
<circle cx="24" cy="24" r="20" stroke="currentColor" stroke-width="2"/>
|
|
271
|
+
<path d="M24 16V26M24 32V32.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
272
|
+
</svg>
|
|
273
|
+
<p>${message}</p>
|
|
274
|
+
<button class="cg-retry-btn">Try Again</button>
|
|
275
|
+
</div>
|
|
276
|
+
`;
|
|
277
|
+
|
|
278
|
+
const retryBtn = grid.querySelector('.cg-retry-btn');
|
|
279
|
+
retryBtn.addEventListener('click', () => this.loadArticles(this.currentPage));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Scroll to top of widget
|
|
285
|
+
*/
|
|
286
|
+
scrollToTop() {
|
|
287
|
+
this.container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
288
|
+
}
|
|
289
|
+
}
|