@contentgrowth/content-widget 1.1.0 → 1.1.2

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.
@@ -1,154 +0,0 @@
1
- /**
2
- * Content Growth API Client
3
- * Handles fetching articles from the widget API (requires API key)
4
- */
5
- export class ContentGrowthAPI {
6
- constructor(config) {
7
- console.log('[ContentGrowthAPI] Constructor called with config:', config);
8
- this.apiKey = config.apiKey;
9
- this.baseUrl = config.baseUrl || 'https://api.content-growth.com';
10
- this.cache = new Map();
11
- this.cacheTTL = 5 * 60 * 1000; // 5 minutes
12
- console.log('[ContentGrowthAPI] Initialized with baseUrl:', this.baseUrl, 'apiKey:', this.apiKey);
13
- }
14
-
15
- /**
16
- * Fetch list of articles
17
- */
18
- async fetchArticles(options = {}) {
19
- const { page = 1, limit = 12, tags = [] } = options;
20
- console.log('[ContentGrowthAPI] fetchArticles called with options:', options);
21
-
22
- const params = new URLSearchParams({
23
- page: page.toString(),
24
- limit: limit.toString()
25
- });
26
-
27
- if (tags.length > 0) {
28
- params.set('tag', tags.join(','));
29
- }
30
-
31
- const url = `${this.baseUrl}/widget/articles?${params}`;
32
- const cacheKey = url;
33
- console.log('[ContentGrowthAPI] Request URL:', url);
34
-
35
- // Check cache
36
- const cached = this.getFromCache(cacheKey);
37
- if (cached) {
38
- console.log('[ContentGrowthAPI] Returning cached data');
39
- return cached;
40
- }
41
-
42
- try {
43
- console.log('[ContentGrowthAPI] Making fetch request with headers:', {
44
- 'X-API-Key': this.apiKey
45
- });
46
-
47
- const response = await fetch(url, {
48
- headers: {
49
- 'X-API-Key': this.apiKey
50
- }
51
- });
52
-
53
- console.log('[ContentGrowthAPI] Response status:', response.status, response.statusText);
54
-
55
- if (!response.ok) {
56
- const errorText = await response.text();
57
- console.error('[ContentGrowthAPI] Error response body:', errorText);
58
- throw new Error(`API Error: ${response.status} ${response.statusText}`);
59
- }
60
-
61
- const data = await response.json();
62
- console.log('[ContentGrowthAPI] Response data:', data);
63
-
64
- // Cache the result
65
- this.setCache(cacheKey, data);
66
-
67
- return data;
68
- } catch (error) {
69
- console.error('[ContentGrowthAPI] Failed to fetch articles:', error);
70
- throw error;
71
- }
72
- }
73
-
74
- /**
75
- * Fetch single article by UUID
76
- */
77
- async fetchArticle(uuid) {
78
- const url = `${this.baseUrl}/widget/articles/${uuid}`;
79
- const cacheKey = url;
80
- console.log('[ContentGrowthAPI] fetchArticle called for uuid:', uuid);
81
- console.log('[ContentGrowthAPI] Request URL:', url);
82
-
83
- // Check cache
84
- const cached = this.getFromCache(cacheKey);
85
- if (cached) {
86
- console.log('[ContentGrowthAPI] Returning cached article');
87
- return cached;
88
- }
89
-
90
- try {
91
- console.log('[ContentGrowthAPI] Making fetch request with headers:', {
92
- 'X-API-Key': this.apiKey
93
- });
94
-
95
- const response = await fetch(url, {
96
- headers: {
97
- 'X-API-Key': this.apiKey
98
- }
99
- });
100
-
101
- console.log('[ContentGrowthAPI] Response status:', response.status, response.statusText);
102
-
103
- if (!response.ok) {
104
- const errorText = await response.text();
105
- console.error('[ContentGrowthAPI] Error response body:', errorText);
106
- throw new Error(`API Error: ${response.status} ${response.statusText}`);
107
- }
108
-
109
- const data = await response.json();
110
- console.log('[ContentGrowthAPI] Response data:', data);
111
-
112
- // Cache the result
113
- this.setCache(cacheKey, data);
114
-
115
- return data;
116
- } catch (error) {
117
- console.error('[ContentGrowthAPI] Failed to fetch article:', error);
118
- throw error;
119
- }
120
- }
121
-
122
- /**
123
- * Get from cache if not expired
124
- */
125
- getFromCache(key) {
126
- const cached = this.cache.get(key);
127
- if (!cached) return null;
128
-
129
- const now = Date.now();
130
- if (now - cached.timestamp > this.cacheTTL) {
131
- this.cache.delete(key);
132
- return null;
133
- }
134
-
135
- return cached.data;
136
- }
137
-
138
- /**
139
- * Set cache with timestamp
140
- */
141
- setCache(key, data) {
142
- this.cache.set(key, {
143
- data,
144
- timestamp: Date.now()
145
- });
146
- }
147
-
148
- /**
149
- * Clear all cache
150
- */
151
- clearCache() {
152
- this.cache.clear();
153
- }
154
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Utility helper functions
3
- */
4
-
5
- /**
6
- * Format date to readable string
7
- */
8
- export function formatDate(timestamp) {
9
- const date = new Date(timestamp * 1000);
10
- return date.toLocaleDateString('en-US', {
11
- year: 'numeric',
12
- month: 'short',
13
- day: 'numeric'
14
- });
15
- }
16
-
17
- /**
18
- * Calculate reading time from word count or content
19
- * @param {number|string} wordCountOrContent - Word count (number) or content text (string)
20
- * @returns {string} Reading time string (e.g., "5 min read")
21
- */
22
- export function calculateReadingTime(wordCountOrContent) {
23
- if (!wordCountOrContent) return 'Unknown';
24
-
25
- const wordsPerMinute = 200;
26
- let words;
27
-
28
- // If it's a number, use it directly as word count
29
- if (typeof wordCountOrContent === 'number') {
30
- words = wordCountOrContent;
31
- if (words === 0) return 'Unknown'; // No word count available
32
- } else {
33
- // Otherwise, calculate from content text (fallback)
34
- words = wordCountOrContent.trim().split(/\s+/).filter(w => w.length > 0).length;
35
- }
36
-
37
- const minutes = Math.ceil(words / wordsPerMinute);
38
- return `${minutes} min read`;
39
- }
40
-
41
- /**
42
- * Truncate text to specified length
43
- */
44
- export function truncate(text, maxLength = 150) {
45
- if (!text || text.length <= maxLength) return text;
46
- return text.substring(0, maxLength).trim() + '...';
47
- }
48
-
49
- /**
50
- * Escape HTML to prevent XSS
51
- */
52
- export function escapeHtml(text) {
53
- const div = document.createElement('div');
54
- div.textContent = text;
55
- return div.innerHTML;
56
- }
57
-
58
- /**
59
- * Debounce function calls
60
- */
61
- export function debounce(func, wait) {
62
- let timeout;
63
- return function executedFunction(...args) {
64
- const later = () => {
65
- clearTimeout(timeout);
66
- func(...args);
67
- };
68
- clearTimeout(timeout);
69
- timeout = setTimeout(later, wait);
70
- };
71
- }
@@ -1,190 +0,0 @@
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
- }
@@ -1,289 +0,0 @@
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
- }