154 lines
6.1 KiB
TypeScript
154 lines
6.1 KiB
TypeScript
"use server";
|
|
import { revalidatePath } from "next/cache";
|
|
import { rawDb, uniqueSlug } from "@/lib/db/client";
|
|
import { redirect } from "next/navigation";
|
|
|
|
export async function createCollection(name: string, description?: string): Promise<{ id: number; slug: string } | null> {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return null;
|
|
const slug = uniqueSlug(rawDb, "collections", trimmed);
|
|
// New collections land at the end of the manual order. Wrap the read +
|
|
// insert in a tx so concurrent creates don't both pick the same
|
|
// position.
|
|
const tx = rawDb.transaction(() => {
|
|
const max = rawDb.prepare(`SELECT COALESCE(MAX(position), -1) AS m FROM collections`).get() as { m: number };
|
|
const r = rawDb.prepare(`
|
|
INSERT INTO collections (name, slug, description, position) VALUES (?, ?, ?, ?) RETURNING id
|
|
`).get(trimmed, slug, description?.trim() || null, max.m + 1) as { id: number };
|
|
return r.id;
|
|
});
|
|
const id = tx();
|
|
revalidatePath("/collection");
|
|
return { id, slug };
|
|
}
|
|
|
|
export async function createCollectionAction(formData: FormData) {
|
|
const name = String(formData.get("name") ?? "");
|
|
const description = String(formData.get("description") ?? "");
|
|
const created = await createCollection(name, description);
|
|
if (created) redirect(`/collection/${created.slug}`);
|
|
}
|
|
|
|
export async function addImageToCollection(collectionId: number, imageId: number) {
|
|
// Wrap the read-then-insert in a transaction so concurrent calls
|
|
// can't both compute the same MAX(position) and produce duplicate
|
|
// ordering values.
|
|
const tx = rawDb.transaction(() => {
|
|
const max = rawDb.prepare(`SELECT COALESCE(MAX(position), -1) AS m FROM collection_images WHERE collection_id = ?`).get(collectionId) as { m: number };
|
|
rawDb.prepare(`
|
|
INSERT OR IGNORE INTO collection_images (collection_id, image_id, position) VALUES (?, ?, ?)
|
|
`).run(collectionId, imageId, max.m + 1);
|
|
rawDb.prepare(`UPDATE collections SET last_used_at = (unixepoch() * 1000) WHERE id = ?`).run(collectionId);
|
|
});
|
|
tx();
|
|
revalidatePath(`/collection`);
|
|
revalidatePath(`/image/${imageId}`);
|
|
}
|
|
|
|
export async function removeImageFromCollection(collectionId: number, imageId: number) {
|
|
rawDb.prepare(`DELETE FROM collection_images WHERE collection_id = ? AND image_id = ?`).run(collectionId, imageId);
|
|
revalidatePath(`/collection`);
|
|
revalidatePath(`/image/${imageId}`);
|
|
}
|
|
|
|
export async function deleteCollection(collectionId: number) {
|
|
rawDb.prepare(`DELETE FROM collections WHERE id = ?`).run(collectionId);
|
|
revalidatePath("/collection");
|
|
redirect("/collection");
|
|
}
|
|
|
|
/**
|
|
* Reorder a single image within a collection. The drag-and-drop UI passes
|
|
* the image being moved and the image it should now sit before (or null
|
|
* to drop at the end). We pull the current ordered list, splice the
|
|
* moved image into its new index, then rewrite every position so the
|
|
* sequence is dense (0..N-1) regardless of any gaps the previous
|
|
* ordering may have had.
|
|
*/
|
|
export async function reorderCollectionImage(
|
|
collectionId: number,
|
|
movedImageId: number,
|
|
beforeImageId: number | null,
|
|
): Promise<void> {
|
|
const rows = rawDb.prepare(`
|
|
SELECT image_id FROM collection_images
|
|
WHERE collection_id = ?
|
|
ORDER BY position ASC, image_id ASC
|
|
`).all(collectionId) as Array<{ image_id: number }>;
|
|
const ids = rows.map((r) => r.image_id);
|
|
const fromIdx = ids.indexOf(movedImageId);
|
|
if (fromIdx === -1) return;
|
|
ids.splice(fromIdx, 1);
|
|
let toIdx = beforeImageId == null ? ids.length : ids.indexOf(beforeImageId);
|
|
if (toIdx === -1) toIdx = ids.length;
|
|
ids.splice(toIdx, 0, movedImageId);
|
|
|
|
const tx = rawDb.transaction(() => {
|
|
const update = rawDb.prepare(`
|
|
UPDATE collection_images SET position = ? WHERE collection_id = ? AND image_id = ?
|
|
`);
|
|
for (let i = 0; i < ids.length; i++) {
|
|
update.run(i, collectionId, ids[i]);
|
|
}
|
|
});
|
|
tx();
|
|
|
|
const slugRow = rawDb.prepare(`SELECT slug FROM collections WHERE id = ?`).get(collectionId) as { slug: string } | undefined;
|
|
revalidatePath("/collection");
|
|
if (slugRow) revalidatePath(`/collection/${slugRow.slug}`);
|
|
}
|
|
|
|
/**
|
|
* Reorder a collection in the manual list on /collection. Pulls the
|
|
* current ordered ids, splices the moved one to its new index, then
|
|
* rewrites every position so the sequence is dense (0..N-1).
|
|
*/
|
|
export async function reorderCollection(
|
|
movedId: number,
|
|
beforeId: number | null,
|
|
): Promise<void> {
|
|
const rows = rawDb.prepare(`
|
|
SELECT id FROM collections ORDER BY position ASC, id ASC
|
|
`).all() as Array<{ id: number }>;
|
|
const ids = rows.map((r) => r.id);
|
|
const fromIdx = ids.indexOf(movedId);
|
|
if (fromIdx === -1) return;
|
|
ids.splice(fromIdx, 1);
|
|
let toIdx = beforeId == null ? ids.length : ids.indexOf(beforeId);
|
|
if (toIdx === -1) toIdx = ids.length;
|
|
ids.splice(toIdx, 0, movedId);
|
|
|
|
const tx = rawDb.transaction(() => {
|
|
const update = rawDb.prepare(`UPDATE collections SET position = ? WHERE id = ?`);
|
|
for (let i = 0; i < ids.length; i++) update.run(i, ids[i]);
|
|
});
|
|
tx();
|
|
revalidatePath("/collection");
|
|
}
|
|
|
|
/** Rename a collection. Returns the new slug for client-side redirect.
|
|
* Wraps the slug-uniqueness check + UPDATE in a transaction so two
|
|
* concurrent renames can't both compute the same slug and crash on
|
|
* the UNIQUE constraint (or worse, race past it). */
|
|
export async function renameCollection(id: number, name: string): Promise<{ slug: string; name: string } | null> {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return null;
|
|
const tx = rawDb.transaction(() => {
|
|
const current = rawDb.prepare(`SELECT name, slug FROM collections WHERE id = ?`).get(id) as
|
|
| { name: string; slug: string }
|
|
| undefined;
|
|
if (!current) return null;
|
|
if (current.name === trimmed) {
|
|
return { slug: current.slug, name: trimmed };
|
|
}
|
|
const slug = uniqueSlug(rawDb, "collections", trimmed, id);
|
|
rawDb.prepare(`UPDATE collections SET name = ?, slug = ? WHERE id = ?`).run(trimmed, slug, id);
|
|
return { slug, name: trimmed };
|
|
});
|
|
const result = tx() as { slug: string; name: string } | null;
|
|
if (!result) return null;
|
|
revalidatePath("/collection");
|
|
revalidatePath(`/collection/${result.slug}`);
|
|
return result;
|
|
}
|