- Update album CRUD endpoints to handle content field - Add /api/albums/[id]/media endpoint for managing album media - Add /api/media/[id]/albums endpoint for media-album associations - Create album routes for public album viewing - Update album queries to use new MediaAlbum join table - Support filtering and sorting in album listings Enables rich content albums with flexible media associations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
164 lines
4.3 KiB
TypeScript
164 lines
4.3 KiB
TypeScript
import type { RequestHandler } from './$types'
|
|
import { prisma } from '$lib/server/database'
|
|
import { jsonResponse, errorResponse, checkAdminAuth } from '$lib/server/api-utils'
|
|
import { logger } from '$lib/server/logger'
|
|
|
|
// GET /api/albums - Get published photography albums (or all albums if admin)
|
|
export const GET: RequestHandler = async (event) => {
|
|
try {
|
|
const url = new URL(event.request.url)
|
|
const limit = parseInt(url.searchParams.get('limit') || '50')
|
|
const offset = parseInt(url.searchParams.get('offset') || '0')
|
|
|
|
// Check if this is an admin request
|
|
const isAdmin = checkAdminAuth(event)
|
|
|
|
// Fetch albums - all for admin, only published for public
|
|
const albums = await prisma.album.findMany({
|
|
where: isAdmin
|
|
? {}
|
|
: {
|
|
status: 'published'
|
|
},
|
|
include: {
|
|
media: {
|
|
orderBy: { displayOrder: 'asc' },
|
|
take: 1, // Only need the first photo for cover
|
|
include: {
|
|
media: {
|
|
select: {
|
|
id: true,
|
|
url: true,
|
|
thumbnailUrl: true,
|
|
width: true,
|
|
height: true,
|
|
dominantColor: true,
|
|
colors: true,
|
|
aspectRatio: true,
|
|
photoCaption: true
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_count: {
|
|
select: {
|
|
media: true
|
|
}
|
|
}
|
|
},
|
|
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }],
|
|
skip: offset,
|
|
take: limit
|
|
})
|
|
|
|
// Get total count for pagination
|
|
const totalCount = await prisma.album.count({
|
|
where: isAdmin
|
|
? {}
|
|
: {
|
|
status: 'published'
|
|
}
|
|
})
|
|
|
|
// Transform albums for response
|
|
const transformedAlbums = albums.map((album) => ({
|
|
id: album.id,
|
|
slug: album.slug,
|
|
title: album.title,
|
|
description: album.description,
|
|
date: album.date,
|
|
location: album.location,
|
|
photoCount: album._count.media,
|
|
coverPhoto: album.media[0]?.media
|
|
? {
|
|
id: album.media[0].media.id,
|
|
url: album.media[0].media.url,
|
|
thumbnailUrl: album.media[0].media.thumbnailUrl,
|
|
width: album.media[0].media.width,
|
|
height: album.media[0].media.height,
|
|
dominantColor: album.media[0].media.dominantColor,
|
|
colors: album.media[0].media.colors,
|
|
aspectRatio: album.media[0].media.aspectRatio,
|
|
caption: album.media[0].media.photoCaption
|
|
}
|
|
: null,
|
|
hasContent: !!album.content, // Indicates if album has composed content
|
|
// Include additional fields for admin
|
|
...(isAdmin
|
|
? {
|
|
status: album.status,
|
|
showInUniverse: album.showInUniverse,
|
|
publishedAt: album.publishedAt,
|
|
createdAt: album.createdAt,
|
|
updatedAt: album.updatedAt,
|
|
coverPhotoId: album.coverPhotoId,
|
|
photos: album.media.map((m) => ({
|
|
id: m.media.id,
|
|
url: m.media.url,
|
|
thumbnailUrl: m.media.thumbnailUrl,
|
|
caption: m.media.photoCaption
|
|
})),
|
|
_count: album._count
|
|
}
|
|
: {})
|
|
}))
|
|
|
|
const response = {
|
|
albums: transformedAlbums,
|
|
pagination: {
|
|
total: totalCount,
|
|
limit,
|
|
offset,
|
|
hasMore: offset + limit < totalCount
|
|
}
|
|
}
|
|
|
|
return jsonResponse(response)
|
|
} catch (error) {
|
|
logger.error('Failed to fetch albums', error as Error)
|
|
return errorResponse('Failed to fetch albums', 500)
|
|
}
|
|
}
|
|
|
|
// POST /api/albums - Create a new album (admin only)
|
|
export const POST: RequestHandler = async (event) => {
|
|
// Check admin auth
|
|
if (!checkAdminAuth(event)) {
|
|
return errorResponse('Unauthorized', 401)
|
|
}
|
|
|
|
try {
|
|
const body = await event.request.json()
|
|
|
|
// Validate required fields
|
|
if (!body.title || !body.slug) {
|
|
return errorResponse('Title and slug are required', 400)
|
|
}
|
|
|
|
// Create the album
|
|
const album = await prisma.album.create({
|
|
data: {
|
|
title: body.title,
|
|
slug: body.slug,
|
|
description: body.description || null,
|
|
date: body.date ? new Date(body.date) : null,
|
|
location: body.location || null,
|
|
showInUniverse: body.showInUniverse ?? false,
|
|
status: body.status || 'draft',
|
|
content: body.content || null,
|
|
publishedAt: body.status === 'published' ? new Date() : null
|
|
}
|
|
})
|
|
|
|
return jsonResponse(album, 201)
|
|
} catch (error) {
|
|
logger.error('Failed to create album', error as Error)
|
|
|
|
// Check for unique constraint violation
|
|
if (error instanceof Error && error.message.includes('Unique constraint')) {
|
|
return errorResponse('An album with this slug already exists', 409)
|
|
}
|
|
|
|
return errorResponse('Failed to create album', 500)
|
|
}
|
|
}
|