npm package: @smartsuite-cms/emagazine-sdk
Description: SDK to load and display e-magazine articles from Smart Suite CMS
Latest version: 1.0.2

# 📖 Hướng Dẫn Sử Dụng E-Magazine SDK

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**.

---

## 📦 Cài Đặt

### Cách 1: Cài từ npm (khuyến nghị)

```bash
npm install @smartsuite-cms/emagazine-sdk
```

### Cách 2: Cài trực tiếp từ thư mục local

```bash
# Trong project khác, trỏ đến thư mục sdk
npm install ../path/to/CMS/sdk
```

### Cách 3: Sao chép thư mục `dist/` vào project

Copy thư mục `sdk/dist/` và file `sdk/package.json` sang project đích, rồi import trực tiếp.

### Build từ source (dành cho contributor)

```bash
cd sdk
npm install
npm run build
```

### Publish lên npm registry

```bash
cd sdk
npm publish --access public
# hoặc private:
# npm publish --access restricted
```

---

## ⚡ Khởi Tạo

```typescript
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'

const sdk = new EmagazineSDK({
  supabaseUrl: 'YOUR_SUPABASE_URL',
  supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
  cmsBaseUrl: 'YOUR_CMS_BASE_URL', // URL CMS để tạo link preview
})
```

| Thuộc tính | Bắt buộc | Mô tả |
|---|---|---|
| `supabaseUrl` | ✅ | URL Supabase project của CMS |
| `supabaseAnonKey` | ✅ | Anon key của Supabase project |
| `cmsBaseUrl` | ❌ | URL gốc CMS (dùng tạo link preview). Mặc định tự tính từ `supabaseUrl` |

---

## 📋 API Reference

### 1. Lấy danh sách bài đăng

```typescript
const result = await sdk.listEmagazines({
  status: 'published',  // 'draft' | 'pending' | 'published' | 'archived' | 'all'
  page: 1,
  limit: 10,
  orderBy: 'published_at', // 'published_at' | 'created_at' | 'title' | 'views_count'
  orderDirection: 'desc',  // 'asc' | 'desc'
})

console.log(result.data)       // Mảng bài viết
console.log(result.total)      // Tổng số bài viết
console.log(result.totalPages) // Tổng số trang
```

#### Các tùy chọn lọc

```typescript
// Lọc theo danh mục
const result = await sdk.listEmagazines({
  categoryId: 'uuid-of-category',
})

// Chỉ lấy bài nổi bật
const featured = await sdk.listEmagazines({
  featured: true,
})

// Tìm kiếm theo tiêu đề
const searched = await sdk.listEmagazines({
  search: 'từ khóa tìm kiếm',
})

// Kết hợp nhiều bộ lọc
const filtered = await sdk.listEmagazines({
  status: 'published',
  featured: true,
  search: 'công nghệ',
  page: 1,
  limit: 5,
  orderBy: 'views_count',
  orderDirection: 'desc',
})
```

#### Cấu trúc dữ liệu trả về (`EmagazineItem`)

```typescript
interface EmagazineItem {
  id: string
  title: string
  slug: string
  summary: string | null
  thumbnail_url: string | null
  status: 'draft' | 'pending' | 'published' | 'archived' | 'trash'
  editor_type: 'grapes' | 'custom'
  seo_title: string | null
  seo_description: string | null
  views_count: number
  is_featured: boolean
  created_at: string
  updated_at: string
  published_at: string | null
  author: { full_name: string; avatar_url: string | null } | null
  category: { name: string } | null
}
```

---

### 2. Lấy chi tiết bài viết theo ID

```typescript
const article = await sdk.getEmagazineById('article-uuid-here')

if (article) {
  console.log(article.title)        // Tiêu đề
  console.log(article.preview_url)  // Link preview trên CMS
  console.log(article.html_content) // Nội dung HTML
  console.log(article.css_content)  // CSS đi kèm
}
```

### 3. Lấy chi tiết bài viết theo Slug

```typescript
const article = await sdk.getEmagazineBySlug('bai-viet-dau-tien')
```

#### Cấu trúc dữ liệu trả về (`EmagazineDetail`)

```typescript
interface EmagazineDetail {
  id: string
  title: string
  slug: string
  summary: string | null
  html_content: string | null   // ⭐ Nội dung HTML
  css_content: string | null    // ⭐ CSS đi kèm
  preview_url: string           // ⭐ Link preview CMS
  thumbnail_url: string | null
  status: string
  editor_type: 'grapes' | 'custom'
  seo_title: string | null
  seo_description: string | null
  views_count: number
  is_featured: boolean
  created_at: string
  updated_at: string
  published_at: string | null
  author: { full_name: string; avatar_url: string | null } | null
  category: { name: string } | null
}
```

---

### 4. Lấy danh sách danh mục

```typescript
const categories = await sdk.listCategories()
// [{ id: '...', name: 'Technology', slug: 'technology', description: '...' }]
```

---

### 5. Hiển thị bài viết lên DOM (Browser)

Sử dụng method `renderToElement()` để inject HTML + CSS vào một element:

```typescript
const article = await sdk.getEmagazineById('article-uuid')
if (!article) return

const container = document.getElementById('article-container')!
const cleanup = sdk.renderToElement(article, container)

// Khi không cần nữa (ví dụ chuyển trang), gọi cleanup:
cleanup()
```

> **Lưu ý:** Method này sẽ tự động inject `<style>` vào `<head>` và trả về hàm cleanup để xóa khi unmount.

#### Ví dụ trong React:

```tsx
import { useEffect, useRef, useState } from 'react'
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'

const sdk = new EmagazineSDK({
  supabaseUrl: 'YOUR_SUPABASE_URL',
  supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
  cmsBaseUrl: 'YOUR_CMS_BASE_URL',
})

function ArticleViewer({ articleId }: { articleId: string }) {
  const containerRef = useRef<HTMLDivElement>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    let cleanup: (() => void) | undefined

    const load = async () => {
      setLoading(true)
      const article = await sdk.getEmagazineById(articleId)
      if (article && containerRef.current) {
        cleanup = sdk.renderToElement(article, containerRef.current)
      }
      setLoading(false)
    }

    load()
    return () => cleanup?.()
  }, [articleId])

  if (loading) return <div>Đang tải...</div>
  return <div ref={containerRef} />
}
```

---

### 6. Tạo HTML Page hoàn chỉnh

Tạo chuỗi HTML đầy đủ (bao gồm `<html>`, `<head>`, `<body>`) để dùng cho iframe hoặc server-side rendering:

```typescript
const article = await sdk.getEmagazineById('article-uuid')
if (article) {
  const fullHTML = sdk.generateFullPageHTML(article)

  // Hiển thị trong iframe
  const iframe = document.getElementById('preview-frame') as HTMLIFrameElement
  iframe.srcdoc = fullHTML

  // Hoặc ghi ra file (server-side)
  // fs.writeFileSync('output.html', fullHTML)
}
```

---

## 🔧 Ví Dụ Hoàn Chỉnh

### Vanilla JavaScript

```html
<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8">
  <title>E-Magazine Reader</title>
  <style>
    body { font-family: 'Segoe UI', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
    .article-card { border: 1px solid #eee; border-radius: 12px; padding: 16px; margin: 12px 0; cursor: pointer; transition: box-shadow 0.3s; }
    .article-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
    .article-card img { width: 100%; height: 200px; object-fit: cover; border-radius: 8px; }
    .article-card h2 { margin: 12px 0 8px; }
    .article-card p { color: #666; }
  </style>
</head>
<body>
  <h1>📰 E-Magazine</h1>
  <div id="articles-list"></div>
  <div id="article-detail" style="display:none"></div>

  <script type="module">
    import { EmagazineSDK } from './path-to-sdk/dist/index.mjs'

    const sdk = new EmagazineSDK({
      supabaseUrl: 'YOUR_SUPABASE_URL',
      supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
      cmsBaseUrl: 'YOUR_CMS_BASE_URL',
    })

    // Hiển thị danh sách bài viết
    const { data: articles, total } = await sdk.listEmagazines({
      status: 'published',
      limit: 10,
    })

    const list = document.getElementById('articles-list')
    articles.forEach(article => {
      const card = document.createElement('div')
      card.className = 'article-card'
      card.innerHTML = `
        ${article.thumbnail_url ? `<img src="${article.thumbnail_url}" alt="${article.title}">` : ''}
        <h2>${article.title}</h2>
        <p>${article.summary || ''}</p>
        <small>👤 ${article.author?.full_name || 'Ẩn danh'} · 📁 ${article.category?.name || ''}</small>
      `
      card.onclick = () => loadArticle(article.id)
      list.appendChild(card)
    })

    // Xem chi tiết bài viết
    async function loadArticle(id) {
      const detail = await sdk.getEmagazineById(id)
      if (!detail) return

      const container = document.getElementById('article-detail')
      container.style.display = 'block'
      document.getElementById('articles-list').style.display = 'none'

      sdk.renderToElement(detail, container)
    }
  </script>
</body>
</html>
```

### Next.js (Server Component)

```tsx
// app/magazine/page.tsx
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'

const sdk = new EmagazineSDK({
  supabaseUrl: process.env.SUPABASE_URL!,
  supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
  cmsBaseUrl: process.env.CMS_BASE_URL,
})

export default async function MagazinePage() {
  const { data: articles } = await sdk.listEmagazines({
    status: 'published',
    limit: 12,
  })

  return (
    <div>
      <h1>E-Magazine</h1>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
        {articles.map(article => (
          <a key={article.id} href={`/magazine/${article.slug}`}>
            {article.thumbnail_url && <img src={article.thumbnail_url} alt={article.title} />}
            <h2>{article.title}</h2>
            <p>{article.summary}</p>
          </a>
        ))}
      </div>
    </div>
  )
}
```

```tsx
// app/magazine/[slug]/page.tsx
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'

const sdk = new EmagazineSDK({
  supabaseUrl: process.env.SUPABASE_URL!,
  supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
  cmsBaseUrl: process.env.CMS_BASE_URL,
})

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const article = await sdk.getEmagazineBySlug(params.slug)
  if (!article) return <div>Không tìm thấy bài viết</div>

  return (
    <div>
      <style dangerouslySetInnerHTML={{ __html: article.css_content || '' }} />
      <div dangerouslySetInnerHTML={{ __html: article.html_content || '' }} />
    </div>
  )
}
```

---

## ⚠️ Lưu Ý Quan Trọng

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`.

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.

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).

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.

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.

---

## 📂 Cấu Trúc SDK

```
sdk/
├── package.json
├── tsconfig.json
└── src/
    ├── index.ts       # Entry point - export SDK class và types
    ├── client.ts      # EmagazineSDK class chính
    └── types.ts       # TypeScript interfaces
```

---

## 🔄 Build & Phát Triển

```bash
# Cài đặt dependencies
cd sdk
npm install

# Build production
npm run build

# Watch mode (dev)
npm run dev
```

Sau khi build, output nằm trong `dist/`:
- `dist/index.js` — CommonJS
- `dist/index.mjs` — ES Module
- `dist/index.d.ts` — TypeScript declarations