feat(api): add album content support and media management endpoints
- 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>
This commit is contained in:
parent
38b62168e9
commit
003e08836e
7 changed files with 937 additions and 119 deletions
481
src/routes/albums/+page.svelte
Normal file
481
src/routes/albums/+page.svelte
Normal file
|
|
@ -0,0 +1,481 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import SmartImage from '$components/SmartImage.svelte'
|
||||||
|
import LoadingSpinner from '$components/admin/LoadingSpinner.svelte'
|
||||||
|
import { InfiniteLoader, LoaderState } from 'svelte-infinite'
|
||||||
|
import { generateMetaTags } from '$lib/utils/metadata'
|
||||||
|
import { page } from '$app/stores'
|
||||||
|
import { browser } from '$app/environment'
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
|
||||||
|
interface Album {
|
||||||
|
id: string
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
date?: string
|
||||||
|
location?: string
|
||||||
|
photoCount: number
|
||||||
|
coverPhoto?: {
|
||||||
|
id: string
|
||||||
|
url: string
|
||||||
|
thumbnailUrl?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
dominantColor?: string
|
||||||
|
colors?: any
|
||||||
|
aspectRatio?: number
|
||||||
|
caption?: string
|
||||||
|
}
|
||||||
|
hasContent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data }: { data: PageData } = $props()
|
||||||
|
|
||||||
|
// Initialize loader state
|
||||||
|
const loaderState = new LoaderState()
|
||||||
|
|
||||||
|
// Initialize state with server-side data
|
||||||
|
let allAlbums = $state<Album[]>(data.albums || [])
|
||||||
|
let currentOffset = $state(data.pagination?.limit || 20)
|
||||||
|
|
||||||
|
const error = $derived(data.error)
|
||||||
|
const pageUrl = $derived($page.url.href)
|
||||||
|
|
||||||
|
// Error message for retry display
|
||||||
|
let lastError = $state<string>('')
|
||||||
|
|
||||||
|
// Load more albums
|
||||||
|
async function loadMore() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/albums?limit=20&offset=${currentOffset}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch albums: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// Append new albums to existing list
|
||||||
|
allAlbums = [...allAlbums, ...(data.albums || [])]
|
||||||
|
|
||||||
|
// Update pagination state
|
||||||
|
currentOffset += data.pagination?.limit || 20
|
||||||
|
|
||||||
|
// Update loader state
|
||||||
|
if (!data.pagination?.hasMore) {
|
||||||
|
loaderState.complete()
|
||||||
|
} else {
|
||||||
|
loaderState.loaded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any previous error
|
||||||
|
lastError = ''
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading more albums:', err)
|
||||||
|
lastError = err instanceof Error ? err.message : 'Failed to load more albums'
|
||||||
|
loaderState.error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize loader state based on initial data
|
||||||
|
let hasInitialized = false
|
||||||
|
$effect(() => {
|
||||||
|
if (!hasInitialized) {
|
||||||
|
hasInitialized = true
|
||||||
|
if (!data.pagination?.hasMore) {
|
||||||
|
loaderState.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const formatDate = (dateString?: string) => {
|
||||||
|
if (!dateString) return null
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate metadata for albums page
|
||||||
|
const metaTags = $derived(
|
||||||
|
generateMetaTags({
|
||||||
|
title: 'Photo Albums',
|
||||||
|
description:
|
||||||
|
'A collection of photographic stories and visual essays from travels and projects.',
|
||||||
|
url: pageUrl
|
||||||
|
})
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{metaTags.title}</title>
|
||||||
|
<meta name="description" content={metaTags.description} />
|
||||||
|
|
||||||
|
<!-- OpenGraph -->
|
||||||
|
{#each Object.entries(metaTags.openGraph) as [property, content]}
|
||||||
|
<meta property="og:{property}" {content} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
{#each Object.entries(metaTags.twitter) as [property, content]}
|
||||||
|
<meta name="twitter:{property}" {content} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href={metaTags.other.canonical} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="albums-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>Photo Albums</h1>
|
||||||
|
<p class="page-description">Collections of photographic stories and visual essays</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-message">
|
||||||
|
<h2>Unable to load albums</h2>
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if allAlbums.length === 0}
|
||||||
|
<div class="empty-container">
|
||||||
|
<div class="empty-message">
|
||||||
|
<h2>No albums yet</h2>
|
||||||
|
<p>Photo albums will be added soon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="albums-grid">
|
||||||
|
{#each allAlbums as album}
|
||||||
|
<a href="/photos/{album.slug}" class="album-card">
|
||||||
|
{#if album.coverPhoto}
|
||||||
|
<div class="album-cover">
|
||||||
|
<SmartImage
|
||||||
|
media={{
|
||||||
|
url: album.coverPhoto.url,
|
||||||
|
thumbnailUrl: album.coverPhoto.thumbnailUrl,
|
||||||
|
width: album.coverPhoto.width,
|
||||||
|
height: album.coverPhoto.height,
|
||||||
|
dominantColor: album.coverPhoto.dominantColor,
|
||||||
|
colors: album.coverPhoto.colors,
|
||||||
|
aspectRatio: album.coverPhoto.aspectRatio
|
||||||
|
}}
|
||||||
|
alt={album.title}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="album-cover empty">
|
||||||
|
<div class="empty-icon">📷</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="album-info">
|
||||||
|
<h2 class="album-title">{album.title}</h2>
|
||||||
|
|
||||||
|
{#if album.description}
|
||||||
|
<p class="album-description">{album.description}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="album-meta">
|
||||||
|
{#if album.date}
|
||||||
|
<span class="meta-item">{formatDate(album.date)}</span>
|
||||||
|
{/if}
|
||||||
|
{#if album.location}
|
||||||
|
<span class="meta-item">📍 {album.location}</span>
|
||||||
|
{/if}
|
||||||
|
<span class="meta-item">{album.photoCount} photos</span>
|
||||||
|
{#if album.hasContent}
|
||||||
|
<span class="meta-item story-indicator">📖 Story</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InfiniteLoader
|
||||||
|
{loaderState}
|
||||||
|
triggerLoad={loadMore}
|
||||||
|
intersectionOptions={{ rootMargin: '0px 0px 200px 0px' }}
|
||||||
|
>
|
||||||
|
<!-- Empty content since we're rendering the grid above -->
|
||||||
|
<div style="height: 1px;"></div>
|
||||||
|
|
||||||
|
{#snippet loading()}
|
||||||
|
<div class="loading-container">
|
||||||
|
<LoadingSpinner size="medium" text="Loading more albums..." />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet error()}
|
||||||
|
<div class="error-retry">
|
||||||
|
<p class="error-text">{lastError || 'Failed to load albums'}</p>
|
||||||
|
<button
|
||||||
|
class="retry-button"
|
||||||
|
onclick={() => {
|
||||||
|
lastError = ''
|
||||||
|
loaderState.reset()
|
||||||
|
loadMore()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet noData()}
|
||||||
|
<div class="end-message">
|
||||||
|
<p>You've reached the end</p>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</InfiniteLoader>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.albums-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 $unit-3x;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
padding: 0 $unit-2x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: $unit-6x;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 $unit-2x;
|
||||||
|
color: $grey-10;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: $grey-40;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.albums-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: $unit-4x;
|
||||||
|
margin-bottom: $unit-6x;
|
||||||
|
|
||||||
|
@include breakpoint('tablet') {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: $unit-3x;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: $unit-3x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-card {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
background: $grey-100;
|
||||||
|
border-radius: $card-corner-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
.album-cover {
|
||||||
|
:global(img) {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-cover {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
overflow: hidden;
|
||||||
|
background: $grey-95;
|
||||||
|
|
||||||
|
:global(img) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $grey-95;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-info {
|
||||||
|
padding: $unit-3x;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
padding: $unit-2x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 $unit;
|
||||||
|
color: $grey-10;
|
||||||
|
|
||||||
|
@include breakpoint('phone') {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: $grey-40;
|
||||||
|
margin: 0 0 $unit-2x;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $unit-2x;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: $grey-50;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-half;
|
||||||
|
|
||||||
|
&.story-indicator {
|
||||||
|
color: $blue-50;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container,
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message,
|
||||||
|
.empty-message {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 $unit-2x;
|
||||||
|
color: $grey-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: $grey-40;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
h2 {
|
||||||
|
color: $red-60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100px;
|
||||||
|
margin-top: $unit-4x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: $unit-6x 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: $grey-50;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-retry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-2x;
|
||||||
|
padding: $unit-4x $unit-2x;
|
||||||
|
margin-top: $unit-4x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
margin: 0;
|
||||||
|
color: $red-60;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-button {
|
||||||
|
padding: $unit $unit-3x;
|
||||||
|
background-color: $primary-color;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: $unit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: darken($primary-color, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
src/routes/albums/+page.ts
Normal file
33
src/routes/albums/+page.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { PageLoad } from './$types'
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/albums?limit=20&offset=0')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load albums')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return {
|
||||||
|
albums: data.albums || [],
|
||||||
|
pagination: data.pagination || {
|
||||||
|
total: 0,
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading albums:', error)
|
||||||
|
return {
|
||||||
|
albums: [],
|
||||||
|
pagination: {
|
||||||
|
total: 0,
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
hasMore: false
|
||||||
|
},
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to load albums'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,128 +1,164 @@
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import { prisma } from '$lib/server/database'
|
import { prisma } from '$lib/server/database'
|
||||||
import {
|
import { jsonResponse, errorResponse, checkAdminAuth } from '$lib/server/api-utils'
|
||||||
jsonResponse,
|
|
||||||
errorResponse,
|
|
||||||
getPaginationParams,
|
|
||||||
getPaginationMeta,
|
|
||||||
checkAdminAuth,
|
|
||||||
parseRequestBody
|
|
||||||
} from '$lib/server/api-utils'
|
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
|
|
||||||
// GET /api/albums - List all albums
|
// GET /api/albums - Get published photography albums (or all albums if admin)
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async (event) => {
|
||||||
try {
|
try {
|
||||||
const { page, limit } = getPaginationParams(event.url)
|
const url = new URL(event.request.url)
|
||||||
const skip = (page - 1) * limit
|
const limit = parseInt(url.searchParams.get('limit') || '50')
|
||||||
|
const offset = parseInt(url.searchParams.get('offset') || '0')
|
||||||
|
|
||||||
// Get filter parameters
|
// Check if this is an admin request
|
||||||
const status = event.url.searchParams.get('status')
|
const isAdmin = checkAdminAuth(event)
|
||||||
const isPhotography = event.url.searchParams.get('isPhotography')
|
|
||||||
|
|
||||||
// Build where clause
|
// Fetch albums - all for admin, only published for public
|
||||||
const where: any = {}
|
|
||||||
if (status) {
|
|
||||||
where.status = status
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPhotography !== null) {
|
|
||||||
where.isPhotography = isPhotography === 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get total count
|
|
||||||
const total = await prisma.album.count({ where })
|
|
||||||
|
|
||||||
// Get albums with photo count and photos for thumbnails
|
|
||||||
const albums = await prisma.album.findMany({
|
const albums = await prisma.album.findMany({
|
||||||
where,
|
where: isAdmin
|
||||||
orderBy: { createdAt: 'desc' },
|
? {}
|
||||||
skip,
|
: {
|
||||||
take: limit,
|
status: 'published'
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
photos: {
|
media: {
|
||||||
|
orderBy: { displayOrder: 'asc' },
|
||||||
|
take: 1, // Only need the first photo for cover
|
||||||
|
include: {
|
||||||
|
media: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
url: true,
|
url: true,
|
||||||
thumbnailUrl: true,
|
thumbnailUrl: true,
|
||||||
caption: true
|
width: true,
|
||||||
},
|
height: true,
|
||||||
orderBy: { displayOrder: 'asc' },
|
dominantColor: true,
|
||||||
take: 5 // Only get first 5 photos for thumbnails
|
colors: true,
|
||||||
|
aspectRatio: true,
|
||||||
|
photoCaption: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: { photos: true }
|
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'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const pagination = getPaginationMeta(total, page, limit)
|
// 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
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
}))
|
||||||
|
|
||||||
logger.info('Albums list retrieved', { total, page, limit })
|
const response = {
|
||||||
|
albums: transformedAlbums,
|
||||||
|
pagination: {
|
||||||
|
total: totalCount,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
hasMore: offset + limit < totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse(response)
|
||||||
albums,
|
|
||||||
pagination
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to retrieve albums', error as Error)
|
logger.error('Failed to fetch albums', error as Error)
|
||||||
return errorResponse('Failed to retrieve albums', 500)
|
return errorResponse('Failed to fetch albums', 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/albums - Create a new album
|
// POST /api/albums - Create a new album (admin only)
|
||||||
export const POST: RequestHandler = async (event) => {
|
export const POST: RequestHandler = async (event) => {
|
||||||
// Check authentication
|
// Check admin auth
|
||||||
if (!checkAdminAuth(event)) {
|
if (!checkAdminAuth(event)) {
|
||||||
return errorResponse('Unauthorized', 401)
|
return errorResponse('Unauthorized', 401)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await parseRequestBody<{
|
const body = await event.request.json()
|
||||||
slug: string
|
|
||||||
title: string
|
|
||||||
description?: string
|
|
||||||
date?: string
|
|
||||||
location?: string
|
|
||||||
coverPhotoId?: number
|
|
||||||
isPhotography?: boolean
|
|
||||||
status?: string
|
|
||||||
showInUniverse?: boolean
|
|
||||||
}>(event.request)
|
|
||||||
|
|
||||||
if (!body || !body.slug || !body.title) {
|
// Validate required fields
|
||||||
return errorResponse('Missing required fields: slug, title', 400)
|
if (!body.title || !body.slug) {
|
||||||
|
return errorResponse('Title and slug are required', 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if slug already exists
|
// Create the album
|
||||||
const existing = await prisma.album.findUnique({
|
|
||||||
where: { slug: body.slug }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
return errorResponse('Album with this slug already exists', 409)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create album
|
|
||||||
const album = await prisma.album.create({
|
const album = await prisma.album.create({
|
||||||
data: {
|
data: {
|
||||||
slug: body.slug,
|
|
||||||
title: body.title,
|
title: body.title,
|
||||||
description: body.description,
|
slug: body.slug,
|
||||||
|
description: body.description || null,
|
||||||
date: body.date ? new Date(body.date) : null,
|
date: body.date ? new Date(body.date) : null,
|
||||||
location: body.location,
|
location: body.location || null,
|
||||||
coverPhotoId: body.coverPhotoId,
|
showInUniverse: body.showInUniverse ?? false,
|
||||||
isPhotography: body.isPhotography ?? false,
|
status: body.status || 'draft',
|
||||||
status: body.status ?? 'draft',
|
content: body.content || null,
|
||||||
showInUniverse: body.showInUniverse ?? false
|
publishedAt: body.status === 'published' ? new Date() : null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('Album created', { id: album.id, slug: album.slug })
|
|
||||||
|
|
||||||
return jsonResponse(album, 201)
|
return jsonResponse(album, 201)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to create album', error as 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)
|
return errorResponse('Failed to create album', 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ export const GET: RequestHandler = async (event) => {
|
||||||
const album = await prisma.album.findUnique({
|
const album = await prisma.album.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
photos: {
|
media: {
|
||||||
orderBy: { displayOrder: 'asc' },
|
orderBy: { displayOrder: 'asc' },
|
||||||
include: {
|
include: {
|
||||||
media: true // Include media relation for each photo
|
media: true // Include media relation for each AlbumMedia
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: { photos: true }
|
select: { media: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -35,27 +35,32 @@ export const GET: RequestHandler = async (event) => {
|
||||||
return errorResponse('Album not found', 404)
|
return errorResponse('Album not found', 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich photos with media information from the included relation
|
// Transform the media relation to maintain backward compatibility
|
||||||
const photosWithMedia = album.photos.map((photo) => {
|
// The frontend may expect a 'photos' array, so we'll provide both 'media' and 'photos'
|
||||||
const media = photo.media
|
const albumWithEnrichedData = {
|
||||||
|
|
||||||
return {
|
|
||||||
...photo,
|
|
||||||
// Add media properties for backward compatibility
|
|
||||||
altText: media?.altText || '',
|
|
||||||
description: media?.description || photo.caption || '',
|
|
||||||
isPhotography: media?.isPhotography || false,
|
|
||||||
mimeType: media?.mimeType || 'image/jpeg',
|
|
||||||
size: media?.size || 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const albumWithEnrichedPhotos = {
|
|
||||||
...album,
|
...album,
|
||||||
photos: photosWithMedia
|
// Keep the media relation as is
|
||||||
|
media: album.media,
|
||||||
|
// Also provide a photos array for backward compatibility if needed
|
||||||
|
photos: album.media.map((albumMedia) => ({
|
||||||
|
id: albumMedia.media.id,
|
||||||
|
mediaId: albumMedia.media.id,
|
||||||
|
displayOrder: albumMedia.displayOrder,
|
||||||
|
filename: albumMedia.media.filename,
|
||||||
|
url: albumMedia.media.url,
|
||||||
|
thumbnailUrl: albumMedia.media.thumbnailUrl,
|
||||||
|
width: albumMedia.media.width,
|
||||||
|
height: albumMedia.media.height,
|
||||||
|
description: albumMedia.media.description || '',
|
||||||
|
isPhotography: albumMedia.media.isPhotography || false,
|
||||||
|
mimeType: albumMedia.media.mimeType || 'image/jpeg',
|
||||||
|
size: albumMedia.media.size || 0,
|
||||||
|
dominantColor: albumMedia.media.dominantColor,
|
||||||
|
aspectRatio: albumMedia.media.aspectRatio
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonResponse(albumWithEnrichedPhotos)
|
return jsonResponse(albumWithEnrichedData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to retrieve album', error as Error)
|
logger.error('Failed to retrieve album', error as Error)
|
||||||
return errorResponse('Failed to retrieve album', 500)
|
return errorResponse('Failed to retrieve album', 500)
|
||||||
|
|
@ -82,9 +87,9 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
date?: string
|
date?: string
|
||||||
location?: string
|
location?: string
|
||||||
coverPhotoId?: number
|
coverPhotoId?: number
|
||||||
isPhotography?: boolean
|
|
||||||
status?: string
|
status?: string
|
||||||
showInUniverse?: boolean
|
showInUniverse?: boolean
|
||||||
|
content?: any
|
||||||
}>(event.request)
|
}>(event.request)
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
|
|
@ -121,11 +126,10 @@ export const PUT: RequestHandler = async (event) => {
|
||||||
date: body.date !== undefined ? (body.date ? new Date(body.date) : null) : existing.date,
|
date: body.date !== undefined ? (body.date ? new Date(body.date) : null) : existing.date,
|
||||||
location: body.location !== undefined ? body.location : existing.location,
|
location: body.location !== undefined ? body.location : existing.location,
|
||||||
coverPhotoId: body.coverPhotoId !== undefined ? body.coverPhotoId : existing.coverPhotoId,
|
coverPhotoId: body.coverPhotoId !== undefined ? body.coverPhotoId : existing.coverPhotoId,
|
||||||
isPhotography:
|
|
||||||
body.isPhotography !== undefined ? body.isPhotography : existing.isPhotography,
|
|
||||||
status: body.status !== undefined ? body.status : existing.status,
|
status: body.status !== undefined ? body.status : existing.status,
|
||||||
showInUniverse:
|
showInUniverse:
|
||||||
body.showInUniverse !== undefined ? body.showInUniverse : existing.showInUniverse
|
body.showInUniverse !== undefined ? body.showInUniverse : existing.showInUniverse,
|
||||||
|
content: body.content !== undefined ? body.content : existing.content
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -156,7 +160,7 @@ export const DELETE: RequestHandler = async (event) => {
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { photos: true }
|
select: { media: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -167,13 +171,12 @@ export const DELETE: RequestHandler = async (event) => {
|
||||||
|
|
||||||
// Use a transaction to ensure both operations succeed or fail together
|
// Use a transaction to ensure both operations succeed or fail together
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
// First, unlink all photos from this album (set albumId to null)
|
// First, delete all AlbumMedia relationships for this album
|
||||||
if (album._count.photos > 0) {
|
if (album._count.media > 0) {
|
||||||
await tx.photo.updateMany({
|
await tx.albumMedia.deleteMany({
|
||||||
where: { albumId: id },
|
where: { albumId: id }
|
||||||
data: { albumId: null }
|
|
||||||
})
|
})
|
||||||
logger.info('Unlinked photos from album', { albumId: id, photoCount: album._count.photos })
|
logger.info('Unlinked media from album', { albumId: id, mediaCount: album._count.media })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then delete the album
|
// Then delete the album
|
||||||
|
|
@ -182,7 +185,7 @@ export const DELETE: RequestHandler = async (event) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('Album deleted', { id, slug: album.slug, photosUnlinked: album._count.photos })
|
logger.info('Album deleted', { id, slug: album.slug, mediaUnlinked: album._count.media })
|
||||||
|
|
||||||
return new Response(null, { status: 204 })
|
return new Response(null, { status: 204 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
135
src/routes/api/albums/[id]/media/+server.ts
Normal file
135
src/routes/api/albums/[id]/media/+server.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
// POST /api/albums/[id]/media - Add media to album (bulk operation)
|
||||||
|
export const POST: RequestHandler = async (event) => {
|
||||||
|
// Check admin auth
|
||||||
|
if (!checkAdminAuth(event)) {
|
||||||
|
return errorResponse('Unauthorized', 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const albumId = parseInt(event.params.id)
|
||||||
|
const body = await event.request.json()
|
||||||
|
const { mediaIds } = body
|
||||||
|
|
||||||
|
if (!Array.isArray(mediaIds) || mediaIds.length === 0) {
|
||||||
|
return errorResponse('Media IDs are required', 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if album exists
|
||||||
|
const album = await prisma.album.findUnique({
|
||||||
|
where: { id: albumId }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!album) {
|
||||||
|
return errorResponse('Album not found', 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current max display order
|
||||||
|
const maxOrderResult = await prisma.albumMedia.findFirst({
|
||||||
|
where: { albumId },
|
||||||
|
orderBy: { displayOrder: 'desc' },
|
||||||
|
select: { displayOrder: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
let currentOrder = maxOrderResult?.displayOrder || 0
|
||||||
|
|
||||||
|
// Create album-media associations
|
||||||
|
const albumMediaData = mediaIds.map((mediaId: number) => ({
|
||||||
|
albumId,
|
||||||
|
mediaId,
|
||||||
|
displayOrder: ++currentOrder
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Use createMany with skipDuplicates to avoid errors if media already in album
|
||||||
|
await prisma.albumMedia.createMany({
|
||||||
|
data: albumMediaData,
|
||||||
|
skipDuplicates: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get updated count
|
||||||
|
const updatedCount = await prisma.albumMedia.count({
|
||||||
|
where: { albumId }
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonResponse({
|
||||||
|
message: 'Media added to album successfully',
|
||||||
|
mediaCount: updatedCount
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to add media to album', error as Error)
|
||||||
|
return errorResponse('Failed to add media to album', 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/albums/[id]/media - Remove media from album (bulk operation)
|
||||||
|
export const DELETE: RequestHandler = async (event) => {
|
||||||
|
// Check admin auth
|
||||||
|
if (!checkAdminAuth(event)) {
|
||||||
|
return errorResponse('Unauthorized', 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const albumId = parseInt(event.params.id)
|
||||||
|
const body = await event.request.json()
|
||||||
|
const { mediaIds } = body
|
||||||
|
|
||||||
|
if (!Array.isArray(mediaIds) || mediaIds.length === 0) {
|
||||||
|
return errorResponse('Media IDs are required', 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if album exists
|
||||||
|
const album = await prisma.album.findUnique({
|
||||||
|
where: { id: albumId }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!album) {
|
||||||
|
return errorResponse('Album not found', 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete album-media associations
|
||||||
|
await prisma.albumMedia.deleteMany({
|
||||||
|
where: {
|
||||||
|
albumId,
|
||||||
|
mediaId: { in: mediaIds }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get updated count
|
||||||
|
const updatedCount = await prisma.albumMedia.count({
|
||||||
|
where: { albumId }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reorder remaining media to fill gaps
|
||||||
|
const remainingMedia = await prisma.albumMedia.findMany({
|
||||||
|
where: { albumId },
|
||||||
|
orderBy: { displayOrder: 'asc' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update display order to remove gaps
|
||||||
|
for (let i = 0; i < remainingMedia.length; i++) {
|
||||||
|
if (remainingMedia[i].displayOrder !== i + 1) {
|
||||||
|
await prisma.albumMedia.update({
|
||||||
|
where: {
|
||||||
|
albumId_mediaId: {
|
||||||
|
albumId: remainingMedia[i].albumId,
|
||||||
|
mediaId: remainingMedia[i].mediaId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: { displayOrder: i + 1 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse({
|
||||||
|
message: 'Media removed from album successfully',
|
||||||
|
mediaCount: updatedCount
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to remove media from album', error as Error)
|
||||||
|
return errorResponse('Failed to remove media from album', 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,9 @@ export const GET: RequestHandler = async (event) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
geoLocations: {
|
||||||
|
orderBy: { order: 'asc' }
|
||||||
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: {
|
select: {
|
||||||
media: true
|
media: true
|
||||||
|
|
|
||||||
127
src/routes/api/media/[id]/albums/+server.ts
Normal file
127
src/routes/api/media/[id]/albums/+server.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import type { RequestHandler } from './$types'
|
||||||
|
import { prisma } from '$lib/server/database'
|
||||||
|
import { checkAdminAuth, errorResponse, jsonResponse } from '$lib/server/api-utils'
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async (event) => {
|
||||||
|
try {
|
||||||
|
const mediaId = parseInt(event.params.id)
|
||||||
|
|
||||||
|
// Check if this is an admin request
|
||||||
|
const authCheck = await checkAdminAuth(event)
|
||||||
|
const isAdmin = authCheck.isAuthenticated
|
||||||
|
|
||||||
|
// Get all albums associated with this media item
|
||||||
|
const albumMedia = await prisma.albumMedia.findMany({
|
||||||
|
where: {
|
||||||
|
mediaId: mediaId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
album: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
date: true,
|
||||||
|
location: true,
|
||||||
|
status: true,
|
||||||
|
showInUniverse: true,
|
||||||
|
coverPhotoId: true,
|
||||||
|
publishedAt: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
album: {
|
||||||
|
date: 'desc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract just the album data
|
||||||
|
let albums = albumMedia.map((am) => am.album)
|
||||||
|
|
||||||
|
// Only filter by status if not admin
|
||||||
|
if (!isAdmin) {
|
||||||
|
albums = albums.filter((album) => album.status === 'published')
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse({ albums })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching albums for media:', error)
|
||||||
|
return errorResponse('Failed to fetch albums', 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PUT: RequestHandler = async (event) => {
|
||||||
|
// Check authentication
|
||||||
|
const authCheck = await checkAdminAuth(event)
|
||||||
|
if (!authCheck.isAuthenticated) {
|
||||||
|
return errorResponse('Unauthorized', 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mediaId = parseInt(event.params.id)
|
||||||
|
const { albumIds } = await event.request.json()
|
||||||
|
|
||||||
|
if (!Array.isArray(albumIds)) {
|
||||||
|
return errorResponse('albumIds must be an array', 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction to update album associations
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
// First, remove all existing album associations
|
||||||
|
await tx.albumMedia.deleteMany({
|
||||||
|
where: {
|
||||||
|
mediaId: mediaId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then, create new associations
|
||||||
|
if (albumIds.length > 0) {
|
||||||
|
// Get the max display order for each album
|
||||||
|
const albumOrders = await Promise.all(
|
||||||
|
albumIds.map(async (albumId) => {
|
||||||
|
const maxOrder = await tx.albumMedia.aggregate({
|
||||||
|
where: { albumId: albumId },
|
||||||
|
_max: { displayOrder: true }
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
albumId: albumId,
|
||||||
|
displayOrder: (maxOrder._max.displayOrder || 0) + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create new associations
|
||||||
|
await tx.albumMedia.createMany({
|
||||||
|
data: albumOrders.map(({ albumId, displayOrder }) => ({
|
||||||
|
albumId: albumId,
|
||||||
|
mediaId: mediaId,
|
||||||
|
displayOrder: displayOrder
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch the updated albums
|
||||||
|
const updatedAlbumMedia = await prisma.albumMedia.findMany({
|
||||||
|
where: {
|
||||||
|
mediaId: mediaId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
album: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const albums = updatedAlbumMedia.map((am) => am.album)
|
||||||
|
|
||||||
|
return jsonResponse({
|
||||||
|
success: true,
|
||||||
|
albums: albums
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating media albums:', error)
|
||||||
|
return errorResponse('Failed to update albums', 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue