jedmund-svelte/src/routes/api/projects/+server.ts
Justin Edmund 09d417907b fix: save branding toggle fields in project API endpoints
POST/PUT/PATCH handlers were ignoring showFeaturedImageInHeader,
showBackgroundColorInHeader, and showLogoInHeader fields sent by
the form, so background colors weren't persisting.
2025-12-11 13:54:14 -08:00

223 lines
5.9 KiB
TypeScript

import type { RequestHandler } from './$types'
import type { Prisma } from '@prisma/client'
import { prisma } from '$lib/server/database'
import {
jsonResponse,
errorResponse,
getPaginationParams,
getPaginationMeta,
checkAdminAuth,
parseRequestBody
} from '$lib/server/api-utils'
import { logger } from '$lib/server/logger'
import { createSlug, ensureUniqueSlug } from '$lib/server/database'
import {
trackMediaUsage,
extractMediaIds,
type MediaUsageReference
} from '$lib/server/media-usage.js'
// Type for project creation request body
interface ProjectCreateBody {
title: string
subtitle?: string
description?: string
year: number
client?: string
role?: string
featuredImage?: string
logoUrl?: string
gallery?: Prisma.JsonValue
externalUrl?: string
caseStudyContent?: Prisma.JsonValue
backgroundColor?: string
highlightColor?: string
projectType?: string
displayOrder?: number
status?: string
password?: string | null
slug?: string
showFeaturedImageInHeader?: boolean
showBackgroundColorInHeader?: boolean
showLogoInHeader?: boolean
}
// GET /api/projects - List all projects
export const GET: RequestHandler = async (event) => {
try {
const { page, limit } = getPaginationParams(event.url)
const skip = (page - 1) * limit
// Check if admin is authenticated
const isAdmin = checkAdminAuth(event)
// Get filter parameters
const status = event.url.searchParams.get('status')
const projectType = event.url.searchParams.get('projectType')
const includeListOnly = event.url.searchParams.get('includeListOnly') === 'true'
const includePasswordProtected =
event.url.searchParams.get('includePasswordProtected') === 'true'
// Build where clause
const where: Prisma.ProjectWhereInput = {}
if (status) {
where.status = status
} else if (!isAdmin) {
// For non-admin users: only show published projects by default
const allowedStatuses = ['published']
if (includeListOnly) {
allowedStatuses.push('list-only')
}
if (includePasswordProtected) {
allowedStatuses.push('password-protected')
}
where.status = { in: allowedStatuses }
}
// For admin users: show all projects (no status filter applied)
if (projectType) {
where.projectType = projectType
}
// Get total count
const total = await prisma.project.count({ where })
// Get projects
const projects = await prisma.project.findMany({
where,
orderBy: [{ displayOrder: 'asc' }, { year: 'desc' }, { createdAt: 'desc' }],
skip,
take: limit
})
const pagination = getPaginationMeta(total, page, limit)
logger.info('Projects list retrieved', { total, page, limit })
return jsonResponse({
projects,
pagination
})
} catch (error) {
logger.error('Failed to retrieve projects', error as Error)
return errorResponse('Failed to retrieve projects', 500)
}
}
// POST /api/projects - Create a new project
export const POST: RequestHandler = async (event) => {
// Check authentication
if (!checkAdminAuth(event)) {
return errorResponse('Unauthorized', 401)
}
try {
const body = await parseRequestBody<ProjectCreateBody>(event.request)
if (!body) {
return errorResponse('Invalid request body', 400)
}
// Validate required fields
if (!body.title || !body.year) {
return errorResponse('Title and year are required', 400)
}
// Generate slug
const baseSlug = body.slug || createSlug(body.title)
const slug = await ensureUniqueSlug(baseSlug, 'project')
// Create project
const project = await prisma.project.create({
data: {
slug,
title: body.title,
subtitle: body.subtitle,
description: body.description,
year: body.year,
client: body.client,
role: body.role,
featuredImage: body.featuredImage,
logoUrl: body.logoUrl,
gallery: body.gallery || [],
externalUrl: body.externalUrl,
caseStudyContent: body.caseStudyContent,
backgroundColor: body.backgroundColor,
highlightColor: body.highlightColor,
projectType: body.projectType || 'work',
displayOrder: body.displayOrder || 0,
status: body.status || 'draft',
password: body.password || null,
publishedAt: body.status === 'published' ? new Date() : null,
showFeaturedImageInHeader: body.showFeaturedImageInHeader ?? true,
showBackgroundColorInHeader: body.showBackgroundColorInHeader ?? true,
showLogoInHeader: body.showLogoInHeader ?? true
}
})
// Track media usage
try {
const usageReferences: MediaUsageReference[] = []
// Track featured image
const featuredImageIds = extractMediaIds(body, 'featuredImage')
featuredImageIds.forEach((mediaId) => {
usageReferences.push({
mediaId,
contentType: 'project',
contentId: project.id,
fieldName: 'featuredImage'
})
})
// Track logo
const logoIds = extractMediaIds(body, 'logoUrl')
logoIds.forEach((mediaId) => {
usageReferences.push({
mediaId,
contentType: 'project',
contentId: project.id,
fieldName: 'logoUrl'
})
})
// Track gallery images
const galleryIds = extractMediaIds(body, 'gallery')
galleryIds.forEach((mediaId) => {
usageReferences.push({
mediaId,
contentType: 'project',
contentId: project.id,
fieldName: 'gallery'
})
})
// Track media in case study content
const contentIds = extractMediaIds(body, 'caseStudyContent')
contentIds.forEach((mediaId) => {
usageReferences.push({
mediaId,
contentType: 'project',
contentId: project.id,
fieldName: 'content'
})
})
if (usageReferences.length > 0) {
await trackMediaUsage(usageReferences)
}
} catch (error) {
logger.warn('Failed to track media usage for project', { projectId: project.id, error })
}
logger.info('Project created', { id: project.id, slug: project.slug })
return jsonResponse(project, 201)
} catch (error) {
logger.error('Failed to create project', error as Error)
return errorResponse('Failed to create project', 500)
}
}