# Blogs Dashboard — Full API Integration Design

**Date:** 2026-06-01  
**Branch:** feature/auth-hardening  
**Scope:** Wire the news list, create/edit form, and table actions to the real dashboard API. Categories, tags, and featured news are already wired; this spec covers only the gaps.

---

## Background

The `/dashboard/blogs` section has four sub-sections. Three are already wired to `authApi`:

| Sub-section | Hook | Status |
|---|---|---|
| Categories CRUD | `use-taxonomy-data.ts` → `useCategoriesList` / `useTaxonomyMutations("category")` | ✅ Done |
| Tags CRUD | `use-taxonomy-data.ts` → `useTagsList` / `useTaxonomyMutations("tag")` | ✅ Done |
| Featured news list + toggle | `use-featured-news-data.ts` | ✅ Done |
| **News list + create/edit** | `use-dashboard-blogs-data.ts` (mock) | ❌ This spec |

---

## API Endpoints (from Postman collection `40597903-c6f480a0-10a1-4b4e-9d84-5531919100a6`)

All routes are under `authApi` base URL (`/api/auth-proxy` → backend `/api/dashboard/`).

| Method | Path | Purpose |
|---|---|---|
| GET | `/news?page=&per_page=` | Paginated list |
| POST | `/news` | Create (multipart/form-data) |
| GET | `/news/{id}` | Fetch single for edit hydration |
| PUT | `/news/{id}` | Update (POST + `_method: put`) |
| DELETE | `/news/{id}` | Delete |
| POST | `/toggle-featured/{id}` | Toggle featured flag |
| GET | `/category?per_page=100` | Category options for the form |
| GET | `/tag?per_page=100` | Tag options for the form |

---

## Section 1 — `hooks/use-news-data.ts` (new file)

Mirrors `use-featured-news-data.ts` in structure. Uses `authApi` + SWR with a mock-fallback on 404/403/network errors so the UI stays usable during backend downtime.

### `RawNewsItem` (internal type)
Normalises the API response shape into typed fields before mapping.

### `NewsItem` (exported type)
Normalised view shape used by containers and the edit form:
```ts
interface NewsItem {
  id: string;
  titleAr: string;
  titleEn: string;
  subTitleAr: string;
  subTitleEn: string;
  descriptionAr: string;
  descriptionEn: string;
  metaTitleAr: string;
  metaTitleEn: string;
  metaDescriptionAr: string;
  metaDescriptionEn: string;
  source: string;
  slug: string;
  direction: "rtl" | "ltr";
  isFeatured: boolean;
  categoryId: string;
  subCategoryId: string;
  tags: string[];           // array of tag IDs
  mainImageUrl: string | null;
  metaImageUrl: string | null;
  status: FilterStatus;
  scheduleDate: string | null;
  scheduleTime: string | null;
  // display fields for the table
  titleDisplay: { ar: string; en: string };
  categoryDisplay: { ar: string; en: string };
  image: string;
  imageAlt: { ar: string; en: string };
  publishDate: string | null;
  publishTime: string | null;
  featured: boolean;
  views: number;
}
```

### Exports

**`useNewsList({ page, perPage, search?, status?, categoryId? })`**
- SWR key: `["/news", queryParams]`
- Fetcher: `authApi.get("/news", { params, validateStatus: () => true })`
- On 404/403/network: returns mock fallback, sets `isFallback: true`
- Returns: `{ data: NewsItem[], totalPages, totalItems, currentPage, isLoading, isError, isFallback, refetch }`

**`useNewsDetail(id: string | undefined)`**
- Enabled only when `id` is truthy
- SWR key: `["/news", id]`
- Fetcher: `authApi.get("/news/{id}", { validateStatus: () => true })`
- Returns: `{ data: NewsItem | null, isLoading, isError, isFallback, refetch }`

**`useNewsMutations()`**
- `create(fd: FormData): Promise<void>` → `authApi.post("/news", fd)`
- `update(id: string, fd: FormData): Promise<void>` → `authApi.post("/news/{id}", fd)` with `_method: put` in FormData
- `remove(id: string): Promise<void>` → `authApi.delete("/news/{id}")`
- After each success: invalidates all SWR keys where `key[0] === "/news"`
- Returns `isMutating: boolean`

---

## Section 2 — `BlogsPageContainer` changes

**Replace mock hook:**
```ts
// Before
const { data, totalPages, totalItems, categories } = useDashboardBlogsData(params);

// After
const { data, totalPages, totalItems, isLoading, isError } = useNewsList(params);
```

**Wire delete action:**
- Add `const { remove } = useNewsMutations()` and `const [deleting, setDeleting] = useState<string | null>(null)`
- Add an inline `AlertDialog` directly in `BlogsPageContainer` (no new file — same pattern as `FeaturedUnfeatureDialog` in `FeaturedPageContainer`)
- Pass `onDelete={(id) => setDeleting(id)}` down to `BlogsTable`
- On confirm: call `remove(deleting)`, close dialog

**Wire toggle featured:**
- Import `useToggleFeatured` from `use-featured-news-data.ts` (already exists)
- Pass `onToggleFeatured={(id) => toggle(id)}` to `BlogsTable`

**Remove "change status" action:**
- No `/news/change-status` endpoint exists in the API. Remove this row action from `BlogsTable`.

---

## Section 3 — `BlogUpsertForm` changes

**Switch API client:**
- Replace `import { api } from "@/swr/config"` with `import { authApi } from "@/swr/auth-config"`
- Replace `api.post(url, formData)` with `authApi.post(url, formData)`

**Fix category/tag fetching:**
- Replace `useQuery<...>("/category")` and `useQuery<...>("/tag", { per_page: 100 })` with direct `authApi` calls via `useSWR`
- SWR keys: `["/category", { per_page: 100 }]` and `["/tag", { per_page: 100 }]`
- Normalisation helpers `normaliseCategories` and `normaliseTags` stay unchanged

**Add edit hydration:**
- Call `useNewsDetail(effectiveBlogId)` when `mode === "edit"`
- On load, call `form.reset(mapNewsItemToFormValues(detail))` once when `detail` first becomes non-null
- `mapNewsItemToFormValues` maps all `NewsItem` fields to `BlogUpsertFormValues` including `categoryId`, `subCategoryId`, `tags`, `direction`, `metaTitleAr/En`, `metaDescriptionAr/En`
- Image fields: `BlogUpsertFormValues.mainImage` and `metaImage` are widened from `File | null` to `File | string | null`. On edit load, the URL strings from `mainImageUrl`/`metaImageUrl` are set as the initial value — `FileUploadDropzone` already accepts `File | string | null` (confirmed). The user must re-upload to replace them; if they don't, the URL string is not appended to the FormData (only `File` instances are sent).

**Remove legacy props:**
- Remove `initialData?: DashboardBlogItem` prop
- Remove `mapInitialDataToValues` adapter
- Remove the TODO comment

---

## Section 4 — `blog-upsert-form-view.tsx` editor swap

**Replace Lexical with TipTap:**
```tsx
// Before
import { LexicalRichTextEditor } from "@/components/lexical/lexical-rich-text-editor";
<LexicalRichTextEditor
  value={field.value}
  onChange={field.onChange}
  dir="rtl"
  placeholder={t("descriptionPlaceholderAr")}
  namespace="BlogEditorAr"
/>

// After
import { RichTextEditor } from "@/components/tiptap/rich-text-editor";
<RichTextEditor
  value={field.value}
  onChange={field.onChange}
/>
```

- Two instances replaced: Arabic (`dir="rtl"`) and English (`dir="ltr"`)
- `namespace` prop dropped (Lexical-specific)
- `isHtmlEmpty` validation helper unchanged (both editors output HTML strings)
- The TipTap CSS import (`import "./tiptap.css"`) is already inside `rich-text-editor.tsx` — no changes needed at the view level

---

## Files Changed

| File | Change |
|---|---|
| `hooks/use-news-data.ts` | **New** — `useNewsList`, `useNewsDetail`, `useNewsMutations` |
| `components/dashboard/blogs/blogs-page-container.tsx` | Replace mock hook, wire delete + toggle-featured |
| `components/dashboard/blogs/blogs-table.tsx` | Add `onDelete` prop, remove "change status" action |
| `components/dashboard/blogs/blog-upsert-form.tsx` | Switch to `authApi`, fix category/tag fetch, add edit hydration, remove legacy props |
| `components/dashboard/blogs/blog-upsert-form-view.tsx` | Swap `LexicalRichTextEditor` → TipTap `RichTextEditor` |

**No changes needed:** `categories-page-container.tsx`, `tags-page-container.tsx`, `featured-page-container.tsx`, `use-taxonomy-data.ts`, `use-featured-news-data.ts`, all view/table components for those sub-sections.

---

## Out of Scope

- Change-status endpoint (not in the API collection)
- News detail/preview page hydration (separate concern)
- Re-ordering featured news (backend endpoint not finalised, silently absorbed in existing hook)
