@smartsuite-cms/emagazine-sdk 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/README.md +444 -0
  2. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,444 @@
1
+ # 📖 Hướng Dẫn Sử Dụng E-Magazine SDK
2
+
3
+ SDK giúp các project khác truy xuất dữ liệu bài đăng e-magazine từ hệ thống **Smart Suite CMS**.
4
+
5
+ ---
6
+
7
+ ## 📦 Cài Đặt
8
+
9
+ ### Cách 1: Cài từ npm (khuyến nghị)
10
+
11
+ ```bash
12
+ npm install @smartsuite-cms/emagazine-sdk
13
+ ```
14
+
15
+ ### Cách 2: Cài trực tiếp từ thư mục local
16
+
17
+ ```bash
18
+ # Trong project khác, trỏ đến thư mục sdk
19
+ npm install ../path/to/CMS/sdk
20
+ ```
21
+
22
+ ### Cách 3: Sao chép thư mục `dist/` vào project
23
+
24
+ Copy thư mục `sdk/dist/` và file `sdk/package.json` sang project đích, rồi import trực tiếp.
25
+
26
+ ### Build từ source (dành cho contributor)
27
+
28
+ ```bash
29
+ cd sdk
30
+ npm install
31
+ npm run build
32
+ ```
33
+
34
+ ### Publish lên npm registry
35
+
36
+ ```bash
37
+ cd sdk
38
+ npm publish --access public
39
+ # hoặc private:
40
+ # npm publish --access restricted
41
+ ```
42
+
43
+ ---
44
+
45
+ ## ⚡ Khởi Tạo
46
+
47
+ ```typescript
48
+ import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
49
+
50
+ const sdk = new EmagazineSDK({
51
+ supabaseUrl: 'YOUR_SUPABASE_URL',
52
+ supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
53
+ cmsBaseUrl: 'YOUR_CMS_BASE_URL', // URL CMS để tạo link preview
54
+ })
55
+ ```
56
+
57
+ | Thuộc tính | Bắt buộc | Mô tả |
58
+ |---|---|---|
59
+ | `supabaseUrl` | ✅ | URL Supabase project của CMS |
60
+ | `supabaseAnonKey` | ✅ | Anon key của Supabase project |
61
+ | `cmsBaseUrl` | ❌ | URL gốc CMS (dùng tạo link preview). Mặc định tự tính từ `supabaseUrl` |
62
+
63
+ ---
64
+
65
+ ## 📋 API Reference
66
+
67
+ ### 1. Lấy danh sách bài đăng
68
+
69
+ ```typescript
70
+ const result = await sdk.listEmagazines({
71
+ status: 'published', // 'draft' | 'pending' | 'published' | 'archived' | 'all'
72
+ page: 1,
73
+ limit: 10,
74
+ orderBy: 'published_at', // 'published_at' | 'created_at' | 'title' | 'views_count'
75
+ orderDirection: 'desc', // 'asc' | 'desc'
76
+ })
77
+
78
+ console.log(result.data) // Mảng bài viết
79
+ console.log(result.total) // Tổng số bài viết
80
+ console.log(result.totalPages) // Tổng số trang
81
+ ```
82
+
83
+ #### Các tùy chọn lọc
84
+
85
+ ```typescript
86
+ // Lọc theo danh mục
87
+ const result = await sdk.listEmagazines({
88
+ categoryId: 'uuid-of-category',
89
+ })
90
+
91
+ // Chỉ lấy bài nổi bật
92
+ const featured = await sdk.listEmagazines({
93
+ featured: true,
94
+ })
95
+
96
+ // Tìm kiếm theo tiêu đề
97
+ const searched = await sdk.listEmagazines({
98
+ search: 'từ khóa tìm kiếm',
99
+ })
100
+
101
+ // Kết hợp nhiều bộ lọc
102
+ const filtered = await sdk.listEmagazines({
103
+ status: 'published',
104
+ featured: true,
105
+ search: 'công nghệ',
106
+ page: 1,
107
+ limit: 5,
108
+ orderBy: 'views_count',
109
+ orderDirection: 'desc',
110
+ })
111
+ ```
112
+
113
+ #### Cấu trúc dữ liệu trả về (`EmagazineItem`)
114
+
115
+ ```typescript
116
+ interface EmagazineItem {
117
+ id: string
118
+ title: string
119
+ slug: string
120
+ summary: string | null
121
+ thumbnail_url: string | null
122
+ status: 'draft' | 'pending' | 'published' | 'archived' | 'trash'
123
+ editor_type: 'grapes' | 'custom'
124
+ seo_title: string | null
125
+ seo_description: string | null
126
+ views_count: number
127
+ is_featured: boolean
128
+ created_at: string
129
+ updated_at: string
130
+ published_at: string | null
131
+ author: { full_name: string; avatar_url: string | null } | null
132
+ category: { name: string } | null
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ### 2. Lấy chi tiết bài viết theo ID
139
+
140
+ ```typescript
141
+ const article = await sdk.getEmagazineById('article-uuid-here')
142
+
143
+ if (article) {
144
+ console.log(article.title) // Tiêu đề
145
+ console.log(article.preview_url) // Link preview trên CMS
146
+ console.log(article.html_content) // Nội dung HTML
147
+ console.log(article.css_content) // CSS đi kèm
148
+ }
149
+ ```
150
+
151
+ ### 3. Lấy chi tiết bài viết theo Slug
152
+
153
+ ```typescript
154
+ const article = await sdk.getEmagazineBySlug('bai-viet-dau-tien')
155
+ ```
156
+
157
+ #### Cấu trúc dữ liệu trả về (`EmagazineDetail`)
158
+
159
+ ```typescript
160
+ interface EmagazineDetail {
161
+ id: string
162
+ title: string
163
+ slug: string
164
+ summary: string | null
165
+ html_content: string | null // ⭐ Nội dung HTML
166
+ css_content: string | null // ⭐ CSS đi kèm
167
+ preview_url: string // ⭐ Link preview CMS
168
+ thumbnail_url: string | null
169
+ status: string
170
+ editor_type: 'grapes' | 'custom'
171
+ seo_title: string | null
172
+ seo_description: string | null
173
+ views_count: number
174
+ is_featured: boolean
175
+ created_at: string
176
+ updated_at: string
177
+ published_at: string | null
178
+ author: { full_name: string; avatar_url: string | null } | null
179
+ category: { name: string } | null
180
+ }
181
+ ```
182
+
183
+ ---
184
+
185
+ ### 4. Lấy danh sách danh mục
186
+
187
+ ```typescript
188
+ const categories = await sdk.listCategories()
189
+ // [{ id: '...', name: 'Technology', slug: 'technology', description: '...' }]
190
+ ```
191
+
192
+ ---
193
+
194
+ ### 5. Hiển thị bài viết lên DOM (Browser)
195
+
196
+ Sử dụng method `renderToElement()` để inject HTML + CSS vào một element:
197
+
198
+ ```typescript
199
+ const article = await sdk.getEmagazineById('article-uuid')
200
+ if (!article) return
201
+
202
+ const container = document.getElementById('article-container')!
203
+ const cleanup = sdk.renderToElement(article, container)
204
+
205
+ // Khi không cần nữa (ví dụ chuyển trang), gọi cleanup:
206
+ cleanup()
207
+ ```
208
+
209
+ > **Lưu ý:** Method này sẽ tự động inject `<style>` vào `<head>` và trả về hàm cleanup để xóa khi unmount.
210
+
211
+ #### Ví dụ trong React:
212
+
213
+ ```tsx
214
+ import { useEffect, useRef, useState } from 'react'
215
+ import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
216
+
217
+ const sdk = new EmagazineSDK({
218
+ supabaseUrl: 'YOUR_SUPABASE_URL',
219
+ supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
220
+ cmsBaseUrl: 'YOUR_CMS_BASE_URL',
221
+ })
222
+
223
+ function ArticleViewer({ articleId }: { articleId: string }) {
224
+ const containerRef = useRef<HTMLDivElement>(null)
225
+ const [loading, setLoading] = useState(true)
226
+
227
+ useEffect(() => {
228
+ let cleanup: (() => void) | undefined
229
+
230
+ const load = async () => {
231
+ setLoading(true)
232
+ const article = await sdk.getEmagazineById(articleId)
233
+ if (article && containerRef.current) {
234
+ cleanup = sdk.renderToElement(article, containerRef.current)
235
+ }
236
+ setLoading(false)
237
+ }
238
+
239
+ load()
240
+ return () => cleanup?.()
241
+ }, [articleId])
242
+
243
+ if (loading) return <div>Đang tải...</div>
244
+ return <div ref={containerRef} />
245
+ }
246
+ ```
247
+
248
+ ---
249
+
250
+ ### 6. Tạo HTML Page hoàn chỉnh
251
+
252
+ Tạo chuỗi HTML đầy đủ (bao gồm `<html>`, `<head>`, `<body>`) để dùng cho iframe hoặc server-side rendering:
253
+
254
+ ```typescript
255
+ const article = await sdk.getEmagazineById('article-uuid')
256
+ if (article) {
257
+ const fullHTML = sdk.generateFullPageHTML(article)
258
+
259
+ // Hiển thị trong iframe
260
+ const iframe = document.getElementById('preview-frame') as HTMLIFrameElement
261
+ iframe.srcdoc = fullHTML
262
+
263
+ // Hoặc ghi ra file (server-side)
264
+ // fs.writeFileSync('output.html', fullHTML)
265
+ }
266
+ ```
267
+
268
+ ---
269
+
270
+ ## 🔧 Ví Dụ Hoàn Chỉnh
271
+
272
+ ### Vanilla JavaScript
273
+
274
+ ```html
275
+ <!DOCTYPE html>
276
+ <html lang="vi">
277
+ <head>
278
+ <meta charset="UTF-8">
279
+ <title>E-Magazine Reader</title>
280
+ <style>
281
+ body { font-family: 'Segoe UI', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
282
+ .article-card { border: 1px solid #eee; border-radius: 12px; padding: 16px; margin: 12px 0; cursor: pointer; transition: box-shadow 0.3s; }
283
+ .article-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
284
+ .article-card img { width: 100%; height: 200px; object-fit: cover; border-radius: 8px; }
285
+ .article-card h2 { margin: 12px 0 8px; }
286
+ .article-card p { color: #666; }
287
+ </style>
288
+ </head>
289
+ <body>
290
+ <h1>📰 E-Magazine</h1>
291
+ <div id="articles-list"></div>
292
+ <div id="article-detail" style="display:none"></div>
293
+
294
+ <script type="module">
295
+ import { EmagazineSDK } from './path-to-sdk/dist/index.mjs'
296
+
297
+ const sdk = new EmagazineSDK({
298
+ supabaseUrl: 'YOUR_SUPABASE_URL',
299
+ supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
300
+ cmsBaseUrl: 'YOUR_CMS_BASE_URL',
301
+ })
302
+
303
+ // Hiển thị danh sách bài viết
304
+ const { data: articles, total } = await sdk.listEmagazines({
305
+ status: 'published',
306
+ limit: 10,
307
+ })
308
+
309
+ const list = document.getElementById('articles-list')
310
+ articles.forEach(article => {
311
+ const card = document.createElement('div')
312
+ card.className = 'article-card'
313
+ card.innerHTML = `
314
+ ${article.thumbnail_url ? `<img src="${article.thumbnail_url}" alt="${article.title}">` : ''}
315
+ <h2>${article.title}</h2>
316
+ <p>${article.summary || ''}</p>
317
+ <small>👤 ${article.author?.full_name || 'Ẩn danh'} · 📁 ${article.category?.name || ''}</small>
318
+ `
319
+ card.onclick = () => loadArticle(article.id)
320
+ list.appendChild(card)
321
+ })
322
+
323
+ // Xem chi tiết bài viết
324
+ async function loadArticle(id) {
325
+ const detail = await sdk.getEmagazineById(id)
326
+ if (!detail) return
327
+
328
+ const container = document.getElementById('article-detail')
329
+ container.style.display = 'block'
330
+ document.getElementById('articles-list').style.display = 'none'
331
+
332
+ sdk.renderToElement(detail, container)
333
+ }
334
+ </script>
335
+ </body>
336
+ </html>
337
+ ```
338
+
339
+ ### Next.js (Server Component)
340
+
341
+ ```tsx
342
+ // app/magazine/page.tsx
343
+ import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
344
+
345
+ const sdk = new EmagazineSDK({
346
+ supabaseUrl: process.env.SUPABASE_URL!,
347
+ supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
348
+ cmsBaseUrl: process.env.CMS_BASE_URL,
349
+ })
350
+
351
+ export default async function MagazinePage() {
352
+ const { data: articles } = await sdk.listEmagazines({
353
+ status: 'published',
354
+ limit: 12,
355
+ })
356
+
357
+ return (
358
+ <div>
359
+ <h1>E-Magazine</h1>
360
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
361
+ {articles.map(article => (
362
+ <a key={article.id} href={`/magazine/${article.slug}`}>
363
+ {article.thumbnail_url && <img src={article.thumbnail_url} alt={article.title} />}
364
+ <h2>{article.title}</h2>
365
+ <p>{article.summary}</p>
366
+ </a>
367
+ ))}
368
+ </div>
369
+ </div>
370
+ )
371
+ }
372
+ ```
373
+
374
+ ```tsx
375
+ // app/magazine/[slug]/page.tsx
376
+ import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
377
+
378
+ const sdk = new EmagazineSDK({
379
+ supabaseUrl: process.env.SUPABASE_URL!,
380
+ supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
381
+ cmsBaseUrl: process.env.CMS_BASE_URL,
382
+ })
383
+
384
+ export default async function ArticlePage({ params }: { params: { slug: string } }) {
385
+ const article = await sdk.getEmagazineBySlug(params.slug)
386
+ if (!article) return <div>Không tìm thấy bài viết</div>
387
+
388
+ return (
389
+ <div>
390
+ <style dangerouslySetInnerHTML={{ __html: article.css_content || '' }} />
391
+ <div dangerouslySetInnerHTML={{ __html: article.html_content || '' }} />
392
+ </div>
393
+ )
394
+ }
395
+ ```
396
+
397
+ ---
398
+
399
+ ## ⚠️ Lưu Ý Quan Trọng
400
+
401
+ 1. **RLS (Row Level Security):** SDK sử dụng `anon key` nên chỉ truy xuất được dữ liệu mà RLS policies cho phép. Nếu cần access tất cả, bạn cần tạo thêm RLS policy cho phép đọc public các bài `published`.
402
+
403
+ 2. **CORS:** Nếu gọi từ browser khác domain, đảm bảo Supabase project cho phép domain của bạn trong CORS settings.
404
+
405
+ 3. **Performance:** SDK trả về dữ liệu phân trang (pagination). Hãy sử dụng `limit` hợp lý (khuyến nghị 10-20 item/trang).
406
+
407
+ 4. **CSS Isolation:** Khi render `html_content` + `css_content`, CSS có thể ảnh hưởng đến layout hiện tại. Nên wrap trong một container có class riêng hoặc sử dụng `iframe` với `generateFullPageHTML()` để cách ly hoàn toàn.
408
+
409
+ 5. **preview_url:** Link preview trỏ đến trang CMS theo format `/corporate/e-magazine/preview/{id}`. Cần đảm bảo CMS đang chạy và route này accessible.
410
+
411
+ ---
412
+
413
+ ## 📂 Cấu Trúc SDK
414
+
415
+ ```
416
+ sdk/
417
+ ├── package.json
418
+ ├── tsconfig.json
419
+ └── src/
420
+ ├── index.ts # Entry point - export SDK class và types
421
+ ├── client.ts # EmagazineSDK class chính
422
+ └── types.ts # TypeScript interfaces
423
+ ```
424
+
425
+ ---
426
+
427
+ ## 🔄 Build & Phát Triển
428
+
429
+ ```bash
430
+ # Cài đặt dependencies
431
+ cd sdk
432
+ npm install
433
+
434
+ # Build production
435
+ npm run build
436
+
437
+ # Watch mode (dev)
438
+ npm run dev
439
+ ```
440
+
441
+ Sau khi build, output nằm trong `dist/`:
442
+ - `dist/index.js` — CommonJS
443
+ - `dist/index.mjs` — ES Module
444
+ - `dist/index.d.ts` — TypeScript declarations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartsuite-cms/emagazine-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "SDK to load and display e-magazine articles from Smart Suite CMS",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -26,4 +26,4 @@
26
26
  "tsup": "^8.0.0",
27
27
  "typescript": "^5.5.0"
28
28
  }
29
- }
29
+ }