import { createSupabaseServiceRoleClient } from "@/lib/supabase/service-role"; import type { CollectionPhoto, PhotoCollection, PhotoCollectionInput, } from "@/types/admin/collection"; type CollectionRow = { id: string; title: string; slug: string; description: string | null; cover_photo_id: string | null; is_published: boolean; published_at: string | null; created_at: string; updated_at: string; }; type CollectionItemRow = { collection_id: string; photo_id: string; sort_order: number; }; const collectionSelect = "id,title,slug,description,cover_photo_id,is_published,published_at,created_at,updated_at"; const photoSelect = "id,unsplash_id,slug,description,alt_description,width,height,color,color_family,likes,urls,user_info,photo_created_at,picked_at,oss_url"; function slugify(value: string) { return value .trim() .toLowerCase() .replace(/['"]/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80); } function normalizeInput(input: PhotoCollectionInput) { const title = input.title.trim(); const slug = slugify(input.slug?.trim() || title); const photoIds = Array.from(new Set(input.photoIds.filter(Boolean))); const coverPhotoId = input.coverPhotoId && photoIds.includes(input.coverPhotoId) ? input.coverPhotoId : (photoIds[0] ?? null); if (!title) { throw new Error("Collection title is required."); } if (!slug) { throw new Error("Collection slug is required."); } return { title, slug, description: input.description?.trim() || null, coverPhotoId, isPublished: input.isPublished, photoIds, }; } async function assertPickedPhotos(photoIds: string[]) { if (photoIds.length === 0) { return; } const supabase = createSupabaseServiceRoleClient(); const { data, error } = await supabase .from("wg_gallery_photos") .select("id") .eq("is_picked", true) .in("id", photoIds); if (error) { throw error; } if ((data?.length ?? 0) !== photoIds.length) { throw new Error("Collections can only include picked photos."); } } async function getPhotosById(photoIds: string[]) { if (photoIds.length === 0) { return new Map(); } const supabase = createSupabaseServiceRoleClient(); const { data, error } = await supabase .from("wg_gallery_photos") .select(photoSelect) .in("id", photoIds); if (error) { throw error; } return new Map( ((data ?? []) as CollectionPhoto[]).map((photo) => [photo.id, photo]), ); } async function attachPhotos( collections: CollectionRow[], ): Promise { if (collections.length === 0) { return []; } const supabase = createSupabaseServiceRoleClient(); const collectionIds = collections.map((collection) => collection.id); const { data: items, error } = await supabase .from("wg_photo_collection_items") .select("collection_id,photo_id,sort_order") .in("collection_id", collectionIds) .order("sort_order", { ascending: true }) .order("created_at", { ascending: true }); if (error) { throw error; } const itemRows = (items ?? []) as CollectionItemRow[]; const photoIds = Array.from( new Set([ ...itemRows.map((item) => item.photo_id), ...collections .map((collection) => collection.cover_photo_id) .filter((id): id is string => Boolean(id)), ]), ); const photosById = await getPhotosById(photoIds); const itemsByCollection = new Map(); for (const item of itemRows) { const current = itemsByCollection.get(item.collection_id) ?? []; current.push(item); itemsByCollection.set(item.collection_id, current); } return collections.map((collection) => ({ ...collection, cover_photo: collection.cover_photo_id ? (photosById.get(collection.cover_photo_id) ?? null) : null, photos: (itemsByCollection.get(collection.id) ?? []) .map((item) => photosById.get(item.photo_id)) .filter((photo): photo is CollectionPhoto => Boolean(photo)), })); } export async function listAdminPhotoCollections() { const supabase = createSupabaseServiceRoleClient(); const { data, error } = await supabase .from("wg_photo_collections") .select(collectionSelect) .order("created_at", { ascending: false }) .limit(100); if (error) { throw error; } return attachPhotos((data ?? []) as CollectionRow[]); } export async function getAdminPhotoCollection(id: string) { const supabase = createSupabaseServiceRoleClient(); const { data, error } = await supabase .from("wg_photo_collections") .select(collectionSelect) .eq("id", id) .maybeSingle(); if (error) { throw error; } if (!data) { return null; } const [collection] = await attachPhotos([data]); return collection ?? null; } export async function listPickedPhotosForCollections() { const supabase = createSupabaseServiceRoleClient(); const { data, error } = await supabase .from("wg_gallery_photos") .select(photoSelect) .eq("is_picked", true) .order("picked_at", { ascending: false, nullsFirst: false }) .order("photo_created_at", { ascending: false }) .limit(120); if (error) { throw error; } return (data ?? []) as CollectionPhoto[]; } async function replaceCollectionItems( collectionId: string, photoIds: string[], ) { const supabase = createSupabaseServiceRoleClient(); const { error: deleteError } = await supabase .from("wg_photo_collection_items") .delete() .eq("collection_id", collectionId); if (deleteError) { throw deleteError; } if (photoIds.length === 0) { return; } const { error: insertError } = await supabase .from("wg_photo_collection_items") .insert( photoIds.map((photoId, index) => ({ collection_id: collectionId, photo_id: photoId, sort_order: index, })), ); if (insertError) { throw insertError; } } export async function createPhotoCollection(input: PhotoCollectionInput) { const normalized = normalizeInput(input); await assertPickedPhotos(normalized.photoIds); const supabase = createSupabaseServiceRoleClient(); const now = new Date().toISOString(); const { data, error } = await supabase .from("wg_photo_collections") .insert({ title: normalized.title, slug: normalized.slug, description: normalized.description, cover_photo_id: normalized.coverPhotoId, is_published: normalized.isPublished, published_at: normalized.isPublished ? now : null, }) .select(collectionSelect) .single(); if (error) { throw error; } await replaceCollectionItems(data.id, normalized.photoIds); return getAdminPhotoCollection(data.id); } export async function updatePhotoCollection( id: string, input: PhotoCollectionInput, ) { const normalized = normalizeInput(input); await assertPickedPhotos(normalized.photoIds); const current = await getAdminPhotoCollection(id); if (!current) { throw new Error("Collection was not found."); } const supabase = createSupabaseServiceRoleClient(); const now = new Date().toISOString(); const { error } = await supabase .from("wg_photo_collections") .update({ title: normalized.title, slug: normalized.slug, description: normalized.description, cover_photo_id: normalized.coverPhotoId, is_published: normalized.isPublished, published_at: normalized.isPublished && !current.published_at ? now : normalized.isPublished ? current.published_at : null, }) .eq("id", id); if (error) { throw error; } await replaceCollectionItems(id, normalized.photoIds); return getAdminPhotoCollection(id); } export async function deletePhotoCollection(id: string) { const supabase = createSupabaseServiceRoleClient(); const { error } = await supabase .from("wg_photo_collections") .delete() .eq("id", id); if (error) { throw error; } }