From 6b44c1b7d09077f8fd6d2d87b1bb2aadb66e7867 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Fri, 13 Jun 2025 06:55:21 -0400 Subject: [PATCH] Simplify photos architecture --- .../migration.sql | 2 + prisma/migrations/migrate-photos-to-media.sql | 105 +++++++++++ prisma/schema.prisma | 34 +++- scripts/migrate-alttext-to-description.sql | 11 ++ scripts/migrate-photos-to-media.ts | 133 +++++++++++++ src/lib/components/PhotoItem.svelte | 10 +- src/lib/components/SmartImage.svelte | 2 +- .../components/admin/MediaDetailsModal.svelte | 170 ++++++++--------- src/lib/components/admin/Modal.svelte | 56 +++++- src/routes/admin/media/+page.svelte | 176 +++--------------- src/routes/api/albums/[id]/photos/+server.ts | 174 ++++++++++------- src/routes/api/media/+server.ts | 4 +- src/routes/api/media/[id]/+server.ts | 25 ++- src/routes/api/media/bulk-upload/+server.ts | 144 +++++++++++++- src/routes/api/photos/+server.ts | 129 +++++++------ .../photos/[albumSlug]/[photoId]/+server.ts | 82 +++++--- src/routes/api/photos/[id]/+server.ts | 135 ++++++++------ .../photos/[albumSlug]/[photoId]/+page.svelte | 52 +----- src/routes/photos/p/[id]/+page.svelte | 7 +- src/routes/photos/p/[id]/+page.ts | 2 +- 20 files changed, 930 insertions(+), 523 deletions(-) create mode 100644 prisma/migrations/20250613060835_simplify_photo_architecture/migration.sql create mode 100644 prisma/migrations/migrate-photos-to-media.sql create mode 100644 scripts/migrate-alttext-to-description.sql create mode 100644 scripts/migrate-photos-to-media.ts diff --git a/prisma/migrations/20250613060835_simplify_photo_architecture/migration.sql b/prisma/migrations/20250613060835_simplify_photo_architecture/migration.sql new file mode 100644 index 0000000..5990d5a --- /dev/null +++ b/prisma/migrations/20250613060835_simplify_photo_architecture/migration.sql @@ -0,0 +1,2 @@ +-- This migration was already applied manually or is empty +-- Placeholder file to satisfy Prisma migration requirements \ No newline at end of file diff --git a/prisma/migrations/migrate-photos-to-media.sql b/prisma/migrations/migrate-photos-to-media.sql new file mode 100644 index 0000000..d2e44bf --- /dev/null +++ b/prisma/migrations/migrate-photos-to-media.sql @@ -0,0 +1,105 @@ +-- Step 1: Add new columns to Media table +ALTER TABLE "Media" +ADD COLUMN IF NOT EXISTS "photoCaption" TEXT, +ADD COLUMN IF NOT EXISTS "photoTitle" VARCHAR(255), +ADD COLUMN IF NOT EXISTS "photoDescription" TEXT, +ADD COLUMN IF NOT EXISTS "photoSlug" VARCHAR(255), +ADD COLUMN IF NOT EXISTS "photoPublishedAt" TIMESTAMP(3); + +-- Step 2: Create AlbumMedia table +CREATE TABLE IF NOT EXISTS "AlbumMedia" ( + "id" SERIAL NOT NULL, + "albumId" INTEGER NOT NULL, + "mediaId" INTEGER NOT NULL, + "displayOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AlbumMedia_pkey" PRIMARY KEY ("id") +); + +-- Step 3: Create indexes for AlbumMedia +CREATE UNIQUE INDEX IF NOT EXISTS "AlbumMedia_albumId_mediaId_key" ON "AlbumMedia"("albumId", "mediaId"); +CREATE INDEX IF NOT EXISTS "AlbumMedia_albumId_idx" ON "AlbumMedia"("albumId"); +CREATE INDEX IF NOT EXISTS "AlbumMedia_mediaId_idx" ON "AlbumMedia"("mediaId"); + +-- Step 4: Add foreign key constraints +ALTER TABLE "AlbumMedia" +ADD CONSTRAINT "AlbumMedia_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE, +ADD CONSTRAINT "AlbumMedia_mediaId_fkey" FOREIGN KEY ("mediaId") REFERENCES "Media"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Step 5: Migrate data from Photo to Media (for photos without mediaId) +UPDATE "Media" m +SET + "photoCaption" = p."caption", + "photoTitle" = p."title", + "photoDescription" = p."description", + "photoSlug" = p."slug", + "photoPublishedAt" = p."publishedAt", + "isPhotography" = CASE WHEN p."showInPhotos" = true THEN true ELSE m."isPhotography" END +FROM "Photo" p +WHERE p."mediaId" = m."id"; + +-- Step 6: For photos without mediaId, create new Media records +INSERT INTO "Media" ( + "filename", + "mimeType", + "size", + "url", + "thumbnailUrl", + "width", + "height", + "exifData", + "isPhotography", + "photoCaption", + "photoTitle", + "photoDescription", + "photoSlug", + "photoPublishedAt", + "createdAt", + "updatedAt" +) +SELECT + p."filename", + 'image/jpeg', -- Default, adjust as needed + 0, -- Default size + p."url", + p."thumbnailUrl", + p."width", + p."height", + p."exifData", + p."showInPhotos", + p."caption", + p."title", + p."description", + p."slug", + p."publishedAt", + p."createdAt", + NOW() +FROM "Photo" p +WHERE p."mediaId" IS NULL; + +-- Step 7: Create AlbumMedia records from existing Photo-Album relationships +INSERT INTO "AlbumMedia" ("albumId", "mediaId", "displayOrder", "createdAt") +SELECT + p."albumId", + COALESCE(p."mediaId", ( + SELECT m."id" + FROM "Media" m + WHERE m."url" = p."url" + AND m."photoSlug" = p."slug" + LIMIT 1 + )), + p."displayOrder", + p."createdAt" +FROM "Photo" p +WHERE p."albumId" IS NOT NULL +AND (p."mediaId" IS NOT NULL OR EXISTS ( + SELECT 1 FROM "Media" m + WHERE m."url" = p."url" + AND m."photoSlug" = p."slug" +)); + +-- Step 8: Add unique constraint on photoSlug +CREATE UNIQUE INDEX IF NOT EXISTS "Media_photoSlug_key" ON "Media"("photoSlug"); + +-- Note: Do NOT drop the Photo table yet - we'll do that after verifying the migration \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c36028e..42ef646 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -76,7 +76,8 @@ model Album { updatedAt DateTime @updatedAt // Relations - photos Photo[] + photos Photo[] // Will be removed after migration + media AlbumMedia[] @@index([slug]) @@index([status]) @@ -127,16 +128,24 @@ model Media { width Int? height Int? exifData Json? // EXIF data for photos - altText String? @db.Text // Alt text for accessibility - description String? @db.Text // Optional description + description String? @db.Text // Description (used for alt text and captions) isPhotography Boolean @default(false) // Star for photos experience + + // Photo-specific fields (migrated from Photo model) + photoCaption String? @db.Text // Caption when used as standalone photo + photoTitle String? @db.VarChar(255) // Title when used as standalone photo + photoDescription String? @db.Text // Description when used as standalone photo + photoSlug String? @unique @db.VarChar(255) // Slug for standalone photo + photoPublishedAt DateTime? // Published date for standalone photo + usedIn Json @default("[]") // Track where media is used (legacy) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations usage MediaUsage[] - photos Photo[] + photos Photo[] // Will be removed after migration + albums AlbumMedia[] } // Media usage tracking table @@ -155,4 +164,21 @@ model MediaUsage { @@unique([mediaId, contentType, contentId, fieldName]) @@index([mediaId]) @@index([contentType, contentId]) +} + +// Album-Media relationship table (many-to-many) +model AlbumMedia { + id Int @id @default(autoincrement()) + albumId Int + mediaId Int + displayOrder Int @default(0) + createdAt DateTime @default(now()) + + // Relations + album Album @relation(fields: [albumId], references: [id], onDelete: Cascade) + media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade) + + @@unique([albumId, mediaId]) + @@index([albumId]) + @@index([mediaId]) } \ No newline at end of file diff --git a/scripts/migrate-alttext-to-description.sql b/scripts/migrate-alttext-to-description.sql new file mode 100644 index 0000000..85611fc --- /dev/null +++ b/scripts/migrate-alttext-to-description.sql @@ -0,0 +1,11 @@ +-- Consolidate altText into description +-- If description is null or empty, copy altText value +-- If both exist, keep description (assuming it's more comprehensive) +UPDATE "Media" +SET description = COALESCE(NULLIF(description, ''), "altText") +WHERE "altText" IS NOT NULL AND "altText" != ''; + +-- Show how many records were affected +SELECT COUNT(*) as updated_records +FROM "Media" +WHERE "altText" IS NOT NULL AND "altText" != ''; \ No newline at end of file diff --git a/scripts/migrate-photos-to-media.ts b/scripts/migrate-photos-to-media.ts new file mode 100644 index 0000000..f5cc73b --- /dev/null +++ b/scripts/migrate-photos-to-media.ts @@ -0,0 +1,133 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +async function main() { + console.log('Starting photo to media migration...') + + try { + // Step 1: Get all photos + const photos = await prisma.photo.findMany({ + include: { + album: true, + media: true + } + }) + + console.log(`Found ${photos.length} photos to migrate`) + + // Step 2: Process each photo + let migratedCount = 0 + let createdMediaCount = 0 + let albumMediaCount = 0 + + for (const photo of photos) { + if (photo.mediaId && photo.media) { + // Photo has associated media - update the media record + await prisma.media.update({ + where: { id: photo.mediaId }, + data: { + photoCaption: photo.caption, + photoTitle: photo.title, + photoDescription: photo.description, + photoSlug: photo.slug, + photoPublishedAt: photo.publishedAt, + isPhotography: photo.showInPhotos + } + }) + migratedCount++ + } else { + // Photo has no media - create new media record + const newMedia = await prisma.media.create({ + data: { + filename: photo.filename, + originalName: photo.filename, + mimeType: 'image/jpeg', // Default, could be improved + size: 0, // Unknown + url: photo.url, + thumbnailUrl: photo.thumbnailUrl, + width: photo.width, + height: photo.height, + exifData: photo.exifData, + isPhotography: photo.showInPhotos, + photoCaption: photo.caption, + photoTitle: photo.title, + photoDescription: photo.description, + photoSlug: photo.slug, + photoPublishedAt: photo.publishedAt, + createdAt: photo.createdAt + } + }) + createdMediaCount++ + + // Update the photo to reference the new media + await prisma.photo.update({ + where: { id: photo.id }, + data: { mediaId: newMedia.id } + }) + } + + // Create AlbumMedia record if photo belongs to an album + if (photo.albumId) { + const mediaId = photo.mediaId || (await prisma.photo.findUnique({ + where: { id: photo.id }, + select: { mediaId: true } + }))?.mediaId + + if (mediaId) { + // Check if AlbumMedia already exists + const existing = await prisma.albumMedia.findUnique({ + where: { + albumId_mediaId: { + albumId: photo.albumId, + mediaId: mediaId + } + } + }) + + if (!existing) { + await prisma.albumMedia.create({ + data: { + albumId: photo.albumId, + mediaId: mediaId, + displayOrder: photo.displayOrder, + createdAt: photo.createdAt + } + }) + albumMediaCount++ + } + } + } + } + + console.log(`Migration completed:`) + console.log(`- Updated ${migratedCount} existing media records`) + console.log(`- Created ${createdMediaCount} new media records`) + console.log(`- Created ${albumMediaCount} album-media relationships`) + + // Step 3: Verify migration + const mediaWithPhotoData = await prisma.media.count({ + where: { + OR: [ + { photoCaption: { not: null } }, + { photoTitle: { not: null } }, + { photoSlug: { not: null } } + ] + } + }) + + const albumMediaRelations = await prisma.albumMedia.count() + + console.log(`\nVerification:`) + console.log(`- Media records with photo data: ${mediaWithPhotoData}`) + console.log(`- Album-media relationships: ${albumMediaRelations}`) + + } catch (error) { + console.error('Migration failed:', error) + process.exit(1) + } finally { + await prisma.$disconnect() + } +} + +main() \ No newline at end of file diff --git a/src/lib/components/PhotoItem.svelte b/src/lib/components/PhotoItem.svelte index f714439..8671d88 100644 --- a/src/lib/components/PhotoItem.svelte +++ b/src/lib/components/PhotoItem.svelte @@ -21,12 +21,12 @@ // For individual photos, check if we have album context if (albumSlug) { // Navigate to photo within album - const photoId = item.id.replace('photo-', '') // Remove 'photo-' prefix - goto(`/photos/${albumSlug}/${photoId}`) + const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes + goto(`/photos/${albumSlug}/${mediaId}`) } else { - // Navigate to individual photo page using the photo ID - const photoId = item.id.replace('photo-', '') // Remove 'photo-' prefix - goto(`/photos/p/${photoId}`) + // Navigate to individual photo page using the media ID + const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes + goto(`/photos/p/${mediaId}`) } } } diff --git a/src/lib/components/SmartImage.svelte b/src/lib/components/SmartImage.svelte index 31a56fe..d67489b 100644 --- a/src/lib/components/SmartImage.svelte +++ b/src/lib/components/SmartImage.svelte @@ -14,7 +14,7 @@ let { media, - alt = media.altText || media.filename || '', + alt = media.description || media.filename || '', class: className = '', containerWidth, loading = 'lazy', diff --git a/src/lib/components/admin/MediaDetailsModal.svelte b/src/lib/components/admin/MediaDetailsModal.svelte index c48e617..da650d4 100644 --- a/src/lib/components/admin/MediaDetailsModal.svelte +++ b/src/lib/components/admin/MediaDetailsModal.svelte @@ -17,7 +17,6 @@ let { isOpen = $bindable(), media, onClose, onUpdate }: Props = $props() // Form state - let altText = $state('') let description = $state('') let isPhotography = $state(false) let isSaving = $state(false) @@ -43,8 +42,8 @@ // Initialize form when media changes $effect(() => { if (media) { - altText = media.altText || '' - description = media.description || '' + // Use description if available, otherwise fall back to altText for backwards compatibility + description = media.description || media.altText || '' isPhotography = media.isPhotography || false error = '' successMessage = '' @@ -77,7 +76,6 @@ } function handleClose() { - altText = '' description = '' isPhotography = false error = '' @@ -99,7 +97,8 @@ 'Content-Type': 'application/json' }, body: JSON.stringify({ - altText: altText.trim() || null, + // Use description for both altText and description fields + altText: description.trim() || null, description: description.trim() || null, isPhotography: isPhotography }) @@ -198,7 +197,7 @@ size="jumbo" closeOnBackdrop={!isSaving} closeOnEscape={!isSaving} - on:close={handleClose} + onClose={handleClose} showCloseButton={false} >
@@ -206,7 +205,7 @@
{#if media.mimeType.startsWith('image/')}
- +
{:else}
@@ -289,10 +288,7 @@ {/if}
- -
-
@@ -387,84 +383,77 @@ {/if}
- -
- -
- - -
-