@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,154 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for ContentGrowthWidget
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface WidgetConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
layoutMode?: 'cards' | 'rows';
|
|
9
|
+
displayMode?: 'compact' | 'comfortable' | 'spacious';
|
|
10
|
+
theme?: 'light' | 'dark';
|
|
11
|
+
pageSize?: number;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
category?: string;
|
|
14
|
+
viewerMode?: 'inline' | 'modal' | 'external';
|
|
15
|
+
mode?: 'list' | 'article-only';
|
|
16
|
+
uuid?: string;
|
|
17
|
+
slug?: string;
|
|
18
|
+
articleId?: string; // Legacy support for uuid
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ContentGrowthWidget {
|
|
22
|
+
constructor(container: HTMLElement, config: WidgetConfig);
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Growth Widget
|
|
3
|
+
* Main widget class that combines list and viewer
|
|
4
|
+
*/
|
|
5
|
+
import { ContentGrowthAPI } from './utils/api-client.js';
|
|
6
|
+
import { ContentList } from './components/content-list.js';
|
|
7
|
+
import { ContentViewer } from './components/content-viewer.js';
|
|
8
|
+
|
|
9
|
+
export class ContentGrowthWidget {
|
|
10
|
+
constructor(container, config) {
|
|
11
|
+
console.log('[ContentGrowthWidget] Constructor called with config:', config);
|
|
12
|
+
|
|
13
|
+
this.container = typeof container === 'string'
|
|
14
|
+
? document.querySelector(container)
|
|
15
|
+
: container;
|
|
16
|
+
|
|
17
|
+
if (!this.container) {
|
|
18
|
+
throw new Error('Container not found');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.config = {
|
|
22
|
+
apiKey: config.apiKey || config['api-key'],
|
|
23
|
+
baseUrl: config.baseUrl || 'https://api.content-growth.com',
|
|
24
|
+
tags: this.parseTags(config.tags),
|
|
25
|
+
theme: config.theme || 'light',
|
|
26
|
+
layoutMode: config.layoutMode || config['layout-mode'] || 'cards', // 'cards' or 'rows'
|
|
27
|
+
displayMode: config.displayMode || config['display-mode'] || 'comfortable',
|
|
28
|
+
viewerMode: config.viewerMode || config['viewer-mode'] || 'inline', // 'inline' | 'modal' | 'external'
|
|
29
|
+
externalUrlPattern: config.externalUrlPattern || config['external-url-pattern'] || '/article/{id}',
|
|
30
|
+
externalTarget: config.externalTarget || config['external-target'] || 'article-{id}', // Tab name with {id}
|
|
31
|
+
pageSize: config.pageSize || config['page-size'] || 12,
|
|
32
|
+
mode: config.mode || 'list', // 'list' or 'article-only'
|
|
33
|
+
articleId: config.articleId || config['article-id'] // Article ID for article-only mode
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
console.log('[ContentGrowthWidget] Final config:', this.config);
|
|
37
|
+
|
|
38
|
+
if (!this.config.apiKey) {
|
|
39
|
+
throw new Error('API key is required');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('[ContentGrowthWidget] Creating API client with baseUrl:', this.config.baseUrl);
|
|
43
|
+
this.api = new ContentGrowthAPI({
|
|
44
|
+
apiKey: this.config.apiKey,
|
|
45
|
+
baseUrl: this.config.baseUrl
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.currentView = 'list'; // 'list' or 'viewer'
|
|
49
|
+
this.contentList = null;
|
|
50
|
+
this.contentViewer = null;
|
|
51
|
+
|
|
52
|
+
this.init();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialize the widget
|
|
57
|
+
*/
|
|
58
|
+
init() {
|
|
59
|
+
console.log('[ContentGrowthWidget] Initializing widget...');
|
|
60
|
+
// Apply theme
|
|
61
|
+
this.container.classList.add('cg-widget');
|
|
62
|
+
this.container.setAttribute('data-theme', this.config.theme);
|
|
63
|
+
|
|
64
|
+
// Check if article-only mode
|
|
65
|
+
if (this.config.mode === 'article-only' && this.config.articleId) {
|
|
66
|
+
console.log('[ContentGrowthWidget] Article-only mode, loading article:', this.config.articleId);
|
|
67
|
+
this.showPostInline(this.config.articleId);
|
|
68
|
+
} else {
|
|
69
|
+
// Create views
|
|
70
|
+
this.showList();
|
|
71
|
+
}
|
|
72
|
+
console.log('[ContentGrowthWidget] Widget initialized');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Show the list view
|
|
77
|
+
*/
|
|
78
|
+
showList() {
|
|
79
|
+
console.log('[ContentGrowthWidget] Showing list view...');
|
|
80
|
+
this.currentView = 'list';
|
|
81
|
+
this.container.innerHTML = '';
|
|
82
|
+
|
|
83
|
+
const listContainer = document.createElement('div');
|
|
84
|
+
listContainer.className = 'cg-list-view';
|
|
85
|
+
this.container.appendChild(listContainer);
|
|
86
|
+
|
|
87
|
+
console.log('[ContentGrowthWidget] Creating ContentList with options:', {
|
|
88
|
+
layoutMode: this.config.layoutMode,
|
|
89
|
+
displayMode: this.config.displayMode,
|
|
90
|
+
pageSize: this.config.pageSize,
|
|
91
|
+
tags: this.config.tags
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.contentList = new ContentList(listContainer, this.api, {
|
|
95
|
+
layoutMode: this.config.layoutMode,
|
|
96
|
+
displayMode: this.config.displayMode,
|
|
97
|
+
pageSize: this.config.pageSize,
|
|
98
|
+
tags: this.config.tags,
|
|
99
|
+
viewerMode: this.config.viewerMode,
|
|
100
|
+
externalUrlPattern: this.config.externalUrlPattern,
|
|
101
|
+
externalTarget: this.config.externalTarget,
|
|
102
|
+
onArticleClick: (article) => this.showPost(article.uuid)
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
this.contentList.init();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Show the content viewer
|
|
110
|
+
*/
|
|
111
|
+
showPost(uuid) {
|
|
112
|
+
this.currentView = 'viewer';
|
|
113
|
+
|
|
114
|
+
if (this.config.viewerMode === 'modal') {
|
|
115
|
+
this.showPostModal(uuid);
|
|
116
|
+
} else {
|
|
117
|
+
this.showPostInline(uuid);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Show content inline (replaces list)
|
|
123
|
+
*/
|
|
124
|
+
showPostInline(uuid) {
|
|
125
|
+
this.container.innerHTML = '';
|
|
126
|
+
|
|
127
|
+
const viewerContainer = document.createElement('div');
|
|
128
|
+
viewerContainer.className = 'cg-viewer-view';
|
|
129
|
+
this.container.appendChild(viewerContainer);
|
|
130
|
+
|
|
131
|
+
// In article-only mode, don't show back button (no list to go back to)
|
|
132
|
+
const showBackButton = this.config.mode !== 'article-only';
|
|
133
|
+
|
|
134
|
+
this.contentViewer = new ContentViewer(viewerContainer, this.api, {
|
|
135
|
+
displayMode: 'inline',
|
|
136
|
+
showBackButton: showBackButton,
|
|
137
|
+
onBack: showBackButton ? () => this.showList() : null
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.contentViewer.loadArticle(uuid);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Show content in modal
|
|
145
|
+
*/
|
|
146
|
+
showPostModal(uuid) {
|
|
147
|
+
// Create modal
|
|
148
|
+
const modal = document.createElement('div');
|
|
149
|
+
modal.className = 'cg-modal';
|
|
150
|
+
// Apply theme to modal
|
|
151
|
+
if (this.config.theme) {
|
|
152
|
+
modal.setAttribute('data-theme', this.config.theme);
|
|
153
|
+
}
|
|
154
|
+
modal.innerHTML = `
|
|
155
|
+
<div class="cg-modal-overlay"></div>
|
|
156
|
+
<div class="cg-modal-content">
|
|
157
|
+
<button class="cg-modal-close" aria-label="Close">
|
|
158
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
159
|
+
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
160
|
+
</svg>
|
|
161
|
+
</button>
|
|
162
|
+
<div class="cg-modal-body"></div>
|
|
163
|
+
</div>
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
document.body.appendChild(modal);
|
|
167
|
+
|
|
168
|
+
// Prevent body scroll
|
|
169
|
+
document.body.style.overflow = 'hidden';
|
|
170
|
+
|
|
171
|
+
// Load content
|
|
172
|
+
const modalBody = modal.querySelector('.cg-modal-body');
|
|
173
|
+
this.contentViewer = new ContentViewer(modalBody, this.api, {
|
|
174
|
+
displayMode: 'modal',
|
|
175
|
+
onBack: () => this.closeModal(modal)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.contentViewer.loadArticle(uuid);
|
|
179
|
+
|
|
180
|
+
// Close handlers
|
|
181
|
+
const closeBtn = modal.querySelector('.cg-modal-close');
|
|
182
|
+
const overlay = modal.querySelector('.cg-modal-overlay');
|
|
183
|
+
|
|
184
|
+
const closeModal = () => this.closeModal(modal);
|
|
185
|
+
closeBtn.addEventListener('click', closeModal);
|
|
186
|
+
overlay.addEventListener('click', closeModal);
|
|
187
|
+
|
|
188
|
+
// ESC key
|
|
189
|
+
const handleEsc = (e) => {
|
|
190
|
+
if (e.key === 'Escape') {
|
|
191
|
+
closeModal();
|
|
192
|
+
document.removeEventListener('keydown', handleEsc);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
document.addEventListener('keydown', handleEsc);
|
|
196
|
+
|
|
197
|
+
// Fade in
|
|
198
|
+
requestAnimationFrame(() => {
|
|
199
|
+
modal.classList.add('cg-modal--active');
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Close modal
|
|
205
|
+
*/
|
|
206
|
+
closeModal(modal) {
|
|
207
|
+
modal.classList.remove('cg-modal--active');
|
|
208
|
+
|
|
209
|
+
setTimeout(() => {
|
|
210
|
+
modal.remove();
|
|
211
|
+
document.body.style.overflow = '';
|
|
212
|
+
}, 300); // Match CSS transition duration
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Parse tags from string or array
|
|
217
|
+
*/
|
|
218
|
+
parseTags(tags) {
|
|
219
|
+
if (!tags) return [];
|
|
220
|
+
if (Array.isArray(tags)) return tags;
|
|
221
|
+
return tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Update configuration
|
|
226
|
+
*/
|
|
227
|
+
updateConfig(newConfig) {
|
|
228
|
+
Object.assign(this.config, newConfig);
|
|
229
|
+
this.init();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Destroy the widget
|
|
234
|
+
*/
|
|
235
|
+
destroy() {
|
|
236
|
+
this.container.innerHTML = '';
|
|
237
|
+
this.container.classList.remove('cg-widget');
|
|
238
|
+
this.container.removeAttribute('data-theme');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for ContentGrowthWidget
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface WidgetConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
layoutMode?: 'cards' | 'rows';
|
|
9
|
+
displayMode?: 'compact' | 'comfortable' | 'spacious';
|
|
10
|
+
theme?: 'light' | 'dark';
|
|
11
|
+
pageSize?: number;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
category?: string;
|
|
14
|
+
viewerMode?: 'inline' | 'modal' | 'external';
|
|
15
|
+
mode?: 'list' | 'article-only';
|
|
16
|
+
uuid?: string;
|
|
17
|
+
slug?: string;
|
|
18
|
+
articleId?: string; // Legacy support for uuid
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ContentGrowthWidget {
|
|
22
|
+
constructor(container: HTMLElement, config: WidgetConfig);
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|