Goal
Admin can create and edit blog authors (with multilingual profile fields and Cloudinary profile photo) and manage blog categories (with 3 locale translations and display order).
Scope
Author routes
app/[locale]/admin/blog/authors/page.tsx — author list table (server component, direct DB query)
app/[locale]/admin/blog/authors/new/page.tsx — create author
app/[locale]/admin/blog/authors/[id]/page.tsx — edit author (notFound if missing)
Author components
-
BlogAuthorListTable.tsx — desktop table + mobile cards: photo thumbnail (32x32), name (EN), jobTitle (EN), post count, Edit link, Delete button (disabled with tooltip if postCount > 0)
-
BlogAuthorForm.tsx — create/edit mode:
- Profile photo upload (reuses
BlogImageUpload, existing /api/admin/blog/images route)
- Locale tabs (EN/UK/PL) for: name (required), jobTitle, company, city, bio (textarea)
- Slug: auto-generated from EN name, positioned below translations
- Social media editor: dynamic rows with platform dropdown (lowercase values: github, linkedin, x, website, youtube, instagram, facebook, behance, dribbble) + URL input
- Dirty tracking in edit mode (JSON snapshot comparison), context-aware submit button tooltips
- Submit: POST
/api/admin/blog/authors (create) or PUT /api/admin/blog/authors/[id] (edit)
Author API routes
app/api/admin/blog/authors/route.ts — POST create (existing, extended with optional fields)
app/api/admin/blog/authors/[id]/route.ts — PUT update, DELETE (409 if author has posts)
No GET endpoints — list/detail data fetched by server components via direct DB queries.
Image upload: separate Cloudinary upload via existing /api/admin/blog/images route (not bundled in POST/PUT).
Delete guard: explicit postCount check (DB FK uses onDelete: 'set null', business rule blocks deletion).
Category routes
app/[locale]/admin/blog/categories/page.tsx — inline CRUD (no separate create/edit pages)
Category component
BlogCategoryManager.tsx — single-page inline management:
- List all categories ordered by displayOrder, showing title (EN), description (EN), post count
- Move up/down buttons to reorder (swap displayOrder values between adjacent items)
- Inline create: slug (auto-gen from EN title) + 3 locale titles + descriptions → POST
- Inline edit: single-open expand pattern, slug + locale tabs (title + description) → PUT
- Delete: disabled if postCount > 0 (tooltip shows count), confirm dialog
Category API routes
app/api/admin/blog/categories/route.ts — POST create (existing, extended with optional description)
app/api/admin/blog/categories/[id]/route.ts — PUT update, DELETE (409 if category has posts)
app/api/admin/blog/categories/reorder/route.ts — POST swap (body: { id1, id2 })
Query layer — db/queries/blog/admin-blog.ts
9 new functions added:
getAdminBlogAuthorsFull() — list with EN name, imageUrl, jobTitle, postCount (via subquery)
getAdminBlogAuthorById(id) — full author with all locale translations + socialMedia cast
updateBlogAuthor(id, input) — update base fields + upsert translations (onConflictDoUpdate)
deleteBlogAuthor(id) — throws AUTHOR_HAS_POSTS if posts reference this author
getAdminBlogCategoriesFull() — list with all translations (for inline edit) + postCount
getAdminBlogCategoryById(id) — single with all locale translations
updateBlogCategory(id, input) — update slug + upsert translations
deleteBlogCategory(id) — throws CATEGORY_HAS_POSTS if posts use this category
swapBlogCategoryOrder(id1, id2) — fetch both displayOrder values, swap with two UPDATEs
Extended existing functions:
CreateBlogAuthorInput — added optional imageUrl, imagePublicId, socialMedia, bio, jobTitle, company, city
CreateBlogCategoryInput — added optional description per locale
- Both backward compatible with existing inline forms (optional fields)
Validation — lib/validation/admin-blog.ts
New schemas: updateBlogAuthorSchema, updateBlogCategorySchema, swapCategoryOrderSchema
Extended: createBlogAuthorSchema (optional image/social/bio fields), createBlogCategorySchema (optional description)
New: socialMediaEntrySchema — { platform: string.min(1), url: string.url() }
Full file list
New files (8):
app/[locale]/admin/blog/authors/new/page.tsx — create author page
app/[locale]/admin/blog/authors/[id]/page.tsx — edit author page
app/api/admin/blog/authors/[id]/route.ts — PUT + DELETE
app/api/admin/blog/categories/[id]/route.ts — PUT + DELETE
app/api/admin/blog/categories/reorder/route.ts — POST swap
components/admin/blog/BlogAuthorListTable.tsx
components/admin/blog/BlogAuthorForm.tsx
components/admin/blog/BlogCategoryManager.tsx
Modified files (4):
app/[locale]/admin/blog/authors/page.tsx — replaced stub with real page
app/[locale]/admin/blog/categories/page.tsx — replaced stub with real page
db/queries/blog/admin-blog.ts — 9 new functions, extended 2 create inputs
lib/validation/admin-blog.ts — 3 new schemas, extended 2 existing
Verification (all passed):
- Create author with photo + 3 locales + social links → appears in list and post dropdown
- Edit author: change name/photo/social → changes reflected
- Delete author with 0 posts → succeeds; with posts → button disabled, tooltip shown
- Create category with 3 locale titles + descriptions → appears in list and post checkboxes
- Edit category inline → changes reflected
- Reorder categories (move up/down) → order changes
- Delete category with 0 posts → succeeds; with posts → button disabled
- Inline creation from post form still works (backward compatible)
- TypeScript compilation: zero new errors
Depends on: Issue #4
Expected impact
Full blog content management from admin panel. No Sanity required for any blog operation.
Out of scope
Goal
Admin can create and edit blog authors (with multilingual profile fields and Cloudinary profile photo) and manage blog categories (with 3 locale translations and display order).
Scope
Author routes
app/[locale]/admin/blog/authors/page.tsx— author list table (server component, direct DB query)app/[locale]/admin/blog/authors/new/page.tsx— create authorapp/[locale]/admin/blog/authors/[id]/page.tsx— edit author (notFound if missing)Author components
BlogAuthorListTable.tsx— desktop table + mobile cards: photo thumbnail (32x32), name (EN), jobTitle (EN), post count, Edit link, Delete button (disabled with tooltip if postCount > 0)BlogAuthorForm.tsx— create/edit mode:BlogImageUpload, existing/api/admin/blog/imagesroute)/api/admin/blog/authors(create) or PUT/api/admin/blog/authors/[id](edit)Author API routes
app/api/admin/blog/authors/route.ts— POST create (existing, extended with optional fields)app/api/admin/blog/authors/[id]/route.ts— PUT update, DELETE (409 if author has posts)No GET endpoints — list/detail data fetched by server components via direct DB queries.
Image upload: separate Cloudinary upload via existing
/api/admin/blog/imagesroute (not bundled in POST/PUT).Delete guard: explicit postCount check (DB FK uses
onDelete: 'set null', business rule blocks deletion).Category routes
app/[locale]/admin/blog/categories/page.tsx— inline CRUD (no separate create/edit pages)Category component
BlogCategoryManager.tsx— single-page inline management:Category API routes
app/api/admin/blog/categories/route.ts— POST create (existing, extended with optional description)app/api/admin/blog/categories/[id]/route.ts— PUT update, DELETE (409 if category has posts)app/api/admin/blog/categories/reorder/route.ts— POST swap (body:{ id1, id2 })Query layer —
db/queries/blog/admin-blog.ts9 new functions added:
getAdminBlogAuthorsFull()— list with EN name, imageUrl, jobTitle, postCount (via subquery)getAdminBlogAuthorById(id)— full author with all locale translations + socialMedia castupdateBlogAuthor(id, input)— update base fields + upsert translations (onConflictDoUpdate)deleteBlogAuthor(id)— throwsAUTHOR_HAS_POSTSif posts reference this authorgetAdminBlogCategoriesFull()— list with all translations (for inline edit) + postCountgetAdminBlogCategoryById(id)— single with all locale translationsupdateBlogCategory(id, input)— update slug + upsert translationsdeleteBlogCategory(id)— throwsCATEGORY_HAS_POSTSif posts use this categoryswapBlogCategoryOrder(id1, id2)— fetch both displayOrder values, swap with two UPDATEsExtended existing functions:
CreateBlogAuthorInput— added optional imageUrl, imagePublicId, socialMedia, bio, jobTitle, company, cityCreateBlogCategoryInput— added optional description per localeValidation —
lib/validation/admin-blog.tsNew schemas:
updateBlogAuthorSchema,updateBlogCategorySchema,swapCategoryOrderSchemaExtended:
createBlogAuthorSchema(optional image/social/bio fields),createBlogCategorySchema(optional description)New:
socialMediaEntrySchema—{ platform: string.min(1), url: string.url() }Full file list
New files (8):
app/[locale]/admin/blog/authors/new/page.tsx— create author pageapp/[locale]/admin/blog/authors/[id]/page.tsx— edit author pageapp/api/admin/blog/authors/[id]/route.ts— PUT + DELETEapp/api/admin/blog/categories/[id]/route.ts— PUT + DELETEapp/api/admin/blog/categories/reorder/route.ts— POST swapcomponents/admin/blog/BlogAuthorListTable.tsxcomponents/admin/blog/BlogAuthorForm.tsxcomponents/admin/blog/BlogCategoryManager.tsxModified files (4):
app/[locale]/admin/blog/authors/page.tsx— replaced stub with real pageapp/[locale]/admin/blog/categories/page.tsx— replaced stub with real pagedb/queries/blog/admin-blog.ts— 9 new functions, extended 2 create inputslib/validation/admin-blog.ts— 3 new schemas, extended 2 existingVerification (all passed):
Depends on: Issue #4
Expected impact
Full blog content management from admin panel. No Sanity required for any blog operation.
Out of scope