Skip to content

Commit 0e7d06c

Browse files
committed
feat: implement feed management system with new API routes, sync utilities, and configuration UI
1 parent c1e5ead commit 0e7d06c

File tree

24 files changed

+1508
-16
lines changed

24 files changed

+1508
-16
lines changed

app/api/db-sync/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ async function checkSchema(): Promise<SchemaCheckResult> {
355355
const warnings = differences.filter(d => d.severity === 'warning').length
356356

357357
return {
358-
isValid: criticalIssues === 0 && missingTables.length === 0,
358+
isValid: criticalIssues === 0 && missingTables.length === 0 && warnings === 0,
359359
schemaVersion: SCHEMA_VERSION,
360360
differences,
361361
missingTables,

app/api/feeds/articles/route.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
export const dynamic = 'force-dynamic'
2+
import { NextRequest, NextResponse } from "next/server"
3+
import { validateRequest } from "@/lib/auth"
4+
import { executeQuery } from "@/lib/mysql"
5+
import { triggerBackgroundSyncIfNeeded } from "@/lib/feed-sync"
6+
7+
export async function GET(request: NextRequest) {
8+
const user = await validateRequest(request)
9+
if (!user) {
10+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
11+
}
12+
13+
try {
14+
// 1. SWR: Perform a non-blocking check to see if feeds need a background sync
15+
// This function will spawn a background promise and return immediately
16+
triggerBackgroundSyncIfNeeded()
17+
18+
const { searchParams } = new URL(request.url)
19+
const category_slug = searchParams.get('category_slug')
20+
const source_id = searchParams.get('source_id')
21+
const search = searchParams.get('q')
22+
const startDate = searchParams.get('startDate')
23+
const endDate = searchParams.get('endDate')
24+
const page = parseInt(searchParams.get('page') || '1')
25+
const limit = parseInt(searchParams.get('limit') || '20')
26+
const offset = (page - 1) * limit
27+
28+
let query = `
29+
SELECT a.*, s.name as source_name, c.name as category_name
30+
FROM feed_articles a
31+
JOIN feed_sources s ON a.source_id = s.id
32+
JOIN feed_categories c ON s.category_id = c.id
33+
WHERE 1=1
34+
`
35+
const params: any[] = []
36+
37+
if (category_slug && category_slug !== 'all') {
38+
query += ` AND c.slug = ?`
39+
params.push(category_slug)
40+
}
41+
42+
if (source_id) {
43+
query += ` AND s.id = ?`
44+
params.push(source_id)
45+
}
46+
47+
if (search) {
48+
query += ` AND (a.title LIKE ? OR a.description LIKE ?)`
49+
params.push(`%${search}%`, `%${search}%`)
50+
}
51+
52+
if (startDate) {
53+
query += ` AND DATE(COALESCE(a.pub_date, a.created_at)) >= ?`
54+
params.push(startDate)
55+
}
56+
57+
if (endDate) {
58+
query += ` AND DATE(COALESCE(a.pub_date, a.created_at)) <= ?`
59+
params.push(endDate)
60+
}
61+
62+
// Get total count
63+
const countQuery = query.replace('SELECT a.*, s.name as source_name, c.name as category_name', 'SELECT COUNT(*) as total')
64+
const [countResult]: any = await executeQuery(countQuery, params)
65+
const total = countResult?.total || 0
66+
67+
// Get paginated data
68+
query += ` ORDER BY COALESCE(a.pub_date, a.created_at) DESC LIMIT ${Number(limit)} OFFSET ${Number(offset)}`
69+
70+
const articles = await executeQuery(query, params)
71+
72+
return NextResponse.json({
73+
success: true,
74+
articles,
75+
pagination: {
76+
page,
77+
limit,
78+
total,
79+
totalPages: Math.ceil(total / limit)
80+
}
81+
})
82+
} catch (error) {
83+
console.error("[NewsFeed] Error getting articles:", error)
84+
return NextResponse.json({ success: false, error: "Failed to fetch articles" }, { status: 500 })
85+
}
86+
}

app/api/feeds/categories/route.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
import { validateRequest, requireAdminRole } from "@/lib/auth"
3+
import { executeQuery } from "@/lib/mysql"
4+
5+
export async function GET(request: NextRequest) {
6+
const user = await validateRequest(request)
7+
if (!user) {
8+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
9+
}
10+
11+
try {
12+
const categories = await executeQuery(`SELECT * FROM feed_categories ORDER BY display_order ASC, name ASC`)
13+
return NextResponse.json({ success: true, categories })
14+
} catch (error) {
15+
console.error("[NewsFeed] Error getting categories:", error)
16+
return NextResponse.json({ success: false, error: "Failed to get categories" }, { status: 500 })
17+
}
18+
}
19+
20+
export async function POST(request: NextRequest) {
21+
const user = await validateRequest(request)
22+
if (!user) {
23+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
24+
}
25+
26+
const roleError = requireAdminRole(user)
27+
if (roleError) return roleError
28+
29+
try {
30+
const body = await request.json()
31+
const { name, slug, display_order = 0 } = body
32+
33+
if (!name || !slug) {
34+
return NextResponse.json({ success: false, error: "Name and slug are required" }, { status: 400 })
35+
}
36+
37+
const result = await executeQuery(
38+
`INSERT INTO feed_categories (name, slug, display_order) VALUES (?, ?, ?)`,
39+
[name, slug, display_order]
40+
)
41+
42+
return NextResponse.json({
43+
success: true,
44+
message: "Category created",
45+
id: (result as any).insertId
46+
})
47+
} catch (error: any) {
48+
console.error("[NewsFeed] Error creating category:", error)
49+
if (error.code === 'ER_DUP_ENTRY') {
50+
return NextResponse.json({ success: false, error: "A category with this slug already exists" }, { status: 400 })
51+
}
52+
return NextResponse.json({ success: false, error: "Failed to create category" }, { status: 500 })
53+
}
54+
}
55+
56+
export async function PUT(request: NextRequest) {
57+
const user = await validateRequest(request)
58+
if (!user) {
59+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
60+
}
61+
62+
const roleError = requireAdminRole(user)
63+
if (roleError) return roleError
64+
65+
try {
66+
const body = await request.json()
67+
const { id, name, slug, display_order } = body
68+
69+
if (!id || (!name && !slug && display_order === undefined)) {
70+
return NextResponse.json({ success: false, error: "ID and at least one field to update are required" }, { status: 400 })
71+
}
72+
73+
// Build dynamic update
74+
const updates: string[] = []
75+
const values: any[] = []
76+
77+
if (name) { updates.push('name = ?'); values.push(name) }
78+
if (slug) { updates.push('slug = ?'); values.push(slug) }
79+
if (display_order !== undefined) { updates.push('display_order = ?'); values.push(display_order) }
80+
81+
values.push(id)
82+
83+
await executeQuery(
84+
`UPDATE feed_categories SET ${updates.join(', ')} WHERE id = ?`,
85+
values
86+
)
87+
88+
return NextResponse.json({ success: true, message: "Category updated" })
89+
} catch (error: any) {
90+
console.error("[NewsFeed] Error updating category:", error)
91+
if (error.code === 'ER_DUP_ENTRY') {
92+
return NextResponse.json({ success: false, error: "A category with this slug already exists" }, { status: 400 })
93+
}
94+
return NextResponse.json({ success: false, error: "Failed to update category" }, { status: 500 })
95+
}
96+
}
97+
98+
export async function DELETE(request: NextRequest) {
99+
const user = await validateRequest(request)
100+
if (!user) {
101+
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
102+
}
103+
104+
const roleError = requireAdminRole(user)
105+
if (roleError) return roleError
106+
107+
try {
108+
const { searchParams } = new URL(request.url)
109+
const id = searchParams.get('id')
110+
111+
if (!id) {
112+
return NextResponse.json({ success: false, error: "ID is required" }, { status: 400 })
113+
}
114+
115+
await executeQuery(`DELETE FROM feed_categories WHERE id = ?`, [id])
116+
117+
return NextResponse.json({ success: true, message: "Category deleted" })
118+
} catch (error) {
119+
console.error("[NewsFeed] Error deleting category:", error)
120+
return NextResponse.json({ success: false, error: "Failed to delete category" }, { status: 500 })
121+
}
122+
}

app/api/feeds/debug-fetch/route.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const dynamic = 'force-dynamic'
2+
import { NextResponse } from "next/server"
3+
import { executeQuery } from "@/lib/mysql"
4+
5+
export async function GET() {
6+
try {
7+
const rawJoin = await executeQuery(`
8+
SELECT a.*, s.name as source_name, c.name as category_name
9+
FROM feed_articles a
10+
JOIN feed_sources s ON a.source_id = s.id
11+
JOIN feed_categories c ON s.category_id = c.id
12+
LIMIT 5
13+
`)
14+
15+
// Explicitly test JSON serialization
16+
const testJson = JSON.stringify(rawJoin)
17+
18+
return NextResponse.json({
19+
success: true,
20+
data: JSON.parse(testJson), // If it serialized properly, this works
21+
count: (rawJoin as any[]).length
22+
})
23+
} catch (error: any) {
24+
return NextResponse.json({
25+
success: false,
26+
error_message: error.message,
27+
error_name: error.name,
28+
stack: error.stack
29+
}, { status: 500 })
30+
}
31+
}

app/api/feeds/debug/route.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const dynamic = 'force-dynamic'
2+
import { NextResponse } from "next/server"
3+
import { executeQuery } from "@/lib/mysql"
4+
5+
export async function GET() {
6+
try {
7+
const cats = await executeQuery("SELECT * FROM feed_categories")
8+
const sources = await executeQuery("SELECT * FROM feed_sources")
9+
const articlesQuery = "SELECT COUNT(*) as c FROM feed_articles"
10+
const [articles]: any = await executeQuery(articlesQuery)
11+
const joinRaw = await executeQuery(`
12+
SELECT a.id, a.title, c.slug
13+
FROM feed_articles a
14+
JOIN feed_sources s ON a.source_id = s.id
15+
JOIN feed_categories c ON s.category_id = c.id
16+
LIMIT 5
17+
`)
18+
return NextResponse.json({
19+
cats,
20+
sources,
21+
article_count: articles?.c || 0,
22+
joinRaw,
23+
})
24+
} catch (error: any) {
25+
return NextResponse.json({ error: error.message }, { status: 500 })
26+
}
27+
}

0 commit comments

Comments
 (0)