diff --git a/server/index.js b/server/index.js
index 839b695..dc0e24f 100644
--- a/server/index.js
+++ b/server/index.js
@@ -46,6 +46,18 @@ function slugify(value) {
.replace(/(^-|-$)+/g, '') || crypto.randomUUID()
}
+function normalizeCategoryForSearch(category) {
+ if (!category) return null
+ // Convert slug format to display format for backward compatibility
+ const categoryMap = {
+ 'books-magazine': 'Books & Magazine',
+ 'clothing-shoes': 'Clothing & Shoes',
+ 'collectibles-art': 'Collectibles & Art',
+ 'toys-hobbies': 'Toys & Hobbies'
+ }
+ return categoryMap[category.toLowerCase()] || category
+}
+
async function generateUniqueSlug(title, excludeId) {
const base = slugify(title)
let slug = base
@@ -138,6 +150,7 @@ function mapPostRow(row) {
sections: row.sections || [],
footer: row.footer,
isEditorsPick: row.is_editors_pick,
+ category: row.category,
createdAt: row.created_at,
updatedAt: row.updated_at
}
@@ -192,11 +205,21 @@ function getUploadFields() {
return fields
}
-app.get('/posts', async (_req, res) => {
+app.get('/posts', async (req, res) => {
try {
- const result = await query(
- 'SELECT * FROM blog_posts ORDER BY created_at DESC'
- )
+ const { category } = req.query
+ let queryText = 'SELECT * FROM blog_posts'
+ const queryParams = []
+
+ if (category) {
+ const normalizedCategory = normalizeCategoryForSearch(category)
+ queryText += ' WHERE category = $1'
+ queryParams.push(normalizedCategory)
+ }
+
+ queryText += ' ORDER BY created_at DESC'
+
+ const result = await query(queryText, queryParams)
const posts = result.rows.map(mapPostRow).map(post => ({
...post,
excerpt: createExcerpt(post.sections)
@@ -251,8 +274,8 @@ app.post('/posts', upload.fields(getUploadFields()), async (req, res) => {
await ensureEditorsPickLimit(null, isEditorsPick)
const result = await query(
- `INSERT INTO blog_posts (title, slug, preview_image, link_url, sections, footer, is_editors_pick)
- VALUES ($1, $2, $3, $4, $5, $6, $7)
+ `INSERT INTO blog_posts (title, slug, preview_image, link_url, sections, footer, is_editors_pick, category)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`,
[
payload.title.trim(),
@@ -261,7 +284,8 @@ app.post('/posts', upload.fields(getUploadFields()), async (req, res) => {
payload.linkUrl || null,
JSON.stringify(sections),
payload.footer || null,
- isEditorsPick
+ isEditorsPick,
+ payload.category || null
]
)
@@ -317,8 +341,9 @@ app.put('/posts/:id', upload.fields(getUploadFields()), async (req, res) => {
link_url = $4,
sections = $5,
footer = $6,
- is_editors_pick = $7
- WHERE id = $8
+ is_editors_pick = $7,
+ category = $8
+ WHERE id = $9
RETURNING *`,
[
payload.title.trim(),
@@ -328,6 +353,7 @@ app.put('/posts/:id', upload.fields(getUploadFields()), async (req, res) => {
JSON.stringify(sections),
payload.footer || null,
isEditorsPick,
+ payload.category || null,
id
]
)
diff --git a/server/migrations.js b/server/migrations.js
index 56538eb..1317ded 100644
--- a/server/migrations.js
+++ b/server/migrations.js
@@ -11,11 +11,26 @@ async function runMigrations() {
sections JSONB NOT NULL DEFAULT '[]'::jsonb,
footer TEXT,
is_editors_pick BOOLEAN NOT NULL DEFAULT FALSE,
+ category TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
`)
+ await query(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM information_schema.columns
+ WHERE table_name = 'blog_posts'
+ AND column_name = 'category'
+ ) THEN
+ ALTER TABLE blog_posts ADD COLUMN category TEXT;
+ END IF;
+ END $$;
+ `)
+
await query(`
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index f2cfc40..b834e27 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -1,9 +1,21 @@
'use client'
import { FormEvent, useEffect, useMemo, useState } from 'react'
-import { BlogPost, BlogPostSection } from '@/types/blog'
+import { BlogPost, BlogPostSection, CATEGORY_LABELS, BlogCategory } from '@/types/blog'
import { resolveMediaUrl } from '@/lib/media'
+function getCategoryLabel(category: string | null | undefined): string {
+ if (!category) return 'None'
+
+ // Try as slug first
+ if (category in CATEGORY_LABELS) {
+ return CATEGORY_LABELS[category as BlogCategory]
+ }
+
+ // Return as-is if it's already a display name
+ return category
+}
+
const MAX_SECTIONS = 5
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:4005'
@@ -63,6 +75,7 @@ export default function AdminPage() {
const [linkUrl, setLinkUrl] = useState('')
const [footer, setFooter] = useState('')
const [isEditorsPick, setIsEditorsPick] = useState(false)
+ const [category, setCategory] = useState
+ Handpicked Treasures from the eBay Universe - Est. 2024 +