From 7ca2076ca8de269bfe6aa7e36924737992cb4299 Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 12 Nov 2025 07:07:21 +0000 Subject: [PATCH] feat: add backend API endpoints for highlights and cross-references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/api/bible/cross-references/route.ts | 33 +++++++++ app/api/highlights/all/route.ts | 42 +++++++++++ app/api/highlights/bulk/route.ts | 87 +++++++++++++++-------- app/api/highlights/route.ts | 93 ++++++++----------------- 4 files changed, 162 insertions(+), 93 deletions(-) create mode 100644 app/api/bible/cross-references/route.ts create mode 100644 app/api/highlights/all/route.ts diff --git a/app/api/bible/cross-references/route.ts b/app/api/bible/cross-references/route.ts new file mode 100644 index 0000000..fe7e6d2 --- /dev/null +++ b/app/api/bible/cross-references/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/lib/db' + +export const runtime = 'nodejs' + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const verseId = searchParams.get('verseId') + + if (!verseId) { + return NextResponse.json( + { error: 'verseId parameter required' }, + { status: 400 } + ) + } + + // For now, return empty cross-references + // TODO: Implement actual cross-reference lookup in Phase 2.1B + // This would require a cross_references table mapping verses to related verses + + return NextResponse.json({ + verseId, + references: [] + }) + } catch (error) { + console.error('Error fetching cross-references:', error) + return NextResponse.json( + { error: 'Failed to fetch cross-references' }, + { status: 500 } + ) + } +} diff --git a/app/api/highlights/all/route.ts b/app/api/highlights/all/route.ts new file mode 100644 index 0000000..eaa3277 --- /dev/null +++ b/app/api/highlights/all/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/lib/db' +import { getAuth } from '@clerk/nextjs/server' + +export const runtime = 'nodejs' + +export async function GET(request: Request) { + try { + const { userId } = await getAuth(request) + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const highlights = await prisma.userHighlight.findMany({ + where: { userId }, + select: { + id: true, + verseId: true, + color: true, + createdAt: true, + updatedAt: true + } + }) + + return NextResponse.json({ + highlights: highlights.map(h => ({ + id: h.id, + verseId: h.verseId, + color: h.color, + createdAt: h.createdAt.getTime(), + updatedAt: h.updatedAt.getTime() + })), + serverTime: Date.now() + }) + } catch (error) { + console.error('Error fetching highlights:', error) + return NextResponse.json( + { error: 'Failed to fetch highlights' }, + { status: 500 } + ) + } +} diff --git a/app/api/highlights/bulk/route.ts b/app/api/highlights/bulk/route.ts index e67c0b9..4595495 100644 --- a/app/api/highlights/bulk/route.ts +++ b/app/api/highlights/bulk/route.ts @@ -1,44 +1,73 @@ -import { NextRequest, NextResponse } from 'next/server' +import { NextResponse } from 'next/server' import { prisma } from '@/lib/db' -import { verifyToken } from '@/lib/auth' +import { getAuth } from '@clerk/nextjs/server' -// POST /api/highlights/bulk?locale=en - Get highlights for multiple verses -export async function POST(req: NextRequest) { +export const runtime = 'nodejs' + +export async function POST(request: Request) { try { - const authHeader = req.headers.get('authorization') - if (!authHeader) { - return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + const { userId } = await getAuth(request) + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const token = authHeader.replace('Bearer ', '') - const decoded = await verifyToken(token) - if (!decoded) { - return NextResponse.json({ success: false, error: 'Invalid token' }, { status: 401 }) + const body = await request.json() + const { highlights } = body + + if (!Array.isArray(highlights)) { + return NextResponse.json({ error: 'Invalid input' }, { status: 400 }) } - const body = await req.json() - const { verseIds } = body + const synced = [] + const errors = [] - if (!Array.isArray(verseIds)) { - return NextResponse.json({ success: false, error: 'verseIds must be an array' }, { status: 400 }) - } + for (const item of highlights) { + try { + const existing = await prisma.userHighlight.findFirst({ + where: { + userId, + verseId: item.verseId + } + }) - const highlights = await prisma.highlight.findMany({ - where: { - userId: decoded.userId, - verseId: { in: verseIds } + if (existing) { + await prisma.userHighlight.update({ + where: { id: existing.id }, + data: { + color: item.color, + updatedAt: new Date() + } + }) + } else { + await prisma.userHighlight.create({ + data: { + userId, + verseId: item.verseId, + color: item.color, + createdAt: new Date(), + updatedAt: new Date() + } + }) + } + synced.push(item.verseId) + } catch (e) { + errors.push({ + verseId: item.verseId, + error: 'Failed to sync' + }) } - }) + } - // Convert array to object keyed by verseId for easier lookup - const highlightsMap: { [key: string]: any } = {} - highlights.forEach(highlight => { - highlightsMap[highlight.verseId] = highlight + return NextResponse.json({ + synced: synced.length, + errors, + serverTime: Date.now() }) - - return NextResponse.json({ success: true, highlights: highlightsMap }) } catch (error) { - console.error('Error fetching highlights:', error) - return NextResponse.json({ success: false, error: 'Failed to fetch highlights' }, { status: 500 }) + console.error('Error bulk syncing highlights:', error) + return NextResponse.json( + { error: 'Failed to sync highlights' }, + { status: 500 } + ) } } diff --git a/app/api/highlights/route.ts b/app/api/highlights/route.ts index d9b98b7..aa65d2d 100644 --- a/app/api/highlights/route.ts +++ b/app/api/highlights/route.ts @@ -1,81 +1,46 @@ -import { NextRequest, NextResponse } from 'next/server' +import { NextResponse } from 'next/server' import { prisma } from '@/lib/db' -import { verifyToken } from '@/lib/auth' +import { getAuth } from '@clerk/nextjs/server' -// GET /api/highlights?locale=en - Get all highlights for user -// POST /api/highlights?locale=en - Create new highlight -export async function GET(req: NextRequest) { +export const runtime = 'nodejs' + +export async function POST(request: Request) { try { - const authHeader = req.headers.get('authorization') - if (!authHeader) { - return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) + const { userId } = await getAuth(request) + if (!userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const token = authHeader.replace('Bearer ', '') - const decoded = await verifyToken(token) - if (!decoded) { - return NextResponse.json({ success: false, error: 'Invalid token' }, { status: 401 }) + const body = await request.json() + const { verseId, color } = body + + if (!verseId || !['yellow', 'orange', 'pink', 'blue'].includes(color)) { + return NextResponse.json({ error: 'Invalid input' }, { status: 400 }) } - const highlights = await prisma.highlight.findMany({ - where: { userId: decoded.userId }, - orderBy: { createdAt: 'desc' } - }) - - return NextResponse.json({ success: true, highlights }) - } catch (error) { - console.error('Error fetching highlights:', error) - return NextResponse.json({ success: false, error: 'Failed to fetch highlights' }, { status: 500 }) - } -} - -export async function POST(req: NextRequest) { - try { - const authHeader = req.headers.get('authorization') - if (!authHeader) { - return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) - } - - const token = authHeader.replace('Bearer ', '') - const decoded = await verifyToken(token) - if (!decoded) { - return NextResponse.json({ success: false, error: 'Invalid token' }, { status: 401 }) - } - - const body = await req.json() - const { verseId, color, note, tags } = body - - if (!verseId || !color) { - return NextResponse.json({ success: false, error: 'Missing required fields' }, { status: 400 }) - } - - // Check if highlight already exists - const existingHighlight = await prisma.highlight.findUnique({ - where: { - userId_verseId: { - userId: decoded.userId, - verseId - } - } - }) - - if (existingHighlight) { - return NextResponse.json({ success: false, error: 'Highlight already exists' }, { status: 400 }) - } - - const highlight = await prisma.highlight.create({ + const highlight = await prisma.userHighlight.create({ data: { - userId: decoded.userId, + userId, verseId, color, - note, - tags: tags || [] + createdAt: new Date(), + updatedAt: new Date() } }) - return NextResponse.json({ success: true, highlight }) + return NextResponse.json({ + id: highlight.id, + verseId: highlight.verseId, + color: highlight.color, + createdAt: highlight.createdAt.getTime(), + updatedAt: highlight.updatedAt.getTime(), + syncStatus: 'synced' + }) } catch (error) { console.error('Error creating highlight:', error) - return NextResponse.json({ success: false, error: 'Failed to create highlight' }, { status: 500 }) + return NextResponse.json( + { error: 'Failed to create highlight' }, + { status: 500 } + ) } }