322 lines
8.8 KiB
TypeScript
322 lines
8.8 KiB
TypeScript
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<string, CollectionPhoto>();
|
|
}
|
|
|
|
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<PhotoCollection[]> {
|
|
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<string, CollectionItemRow[]>();
|
|
|
|
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<CollectionRow>();
|
|
|
|
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<CollectionRow>();
|
|
|
|
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;
|
|
}
|
|
}
|