Files
t5de-hub/apps/web/app/products/actions.ts
T
dhkatzandClaude Opus 5.5 97ce2be569
CI / test (push) Successful in 1m4s
Deleting products: by their creators, and removal by moderation
A creator deletes a product: it's no longer listed and nobody new can get it, while people who have it
keep it (its page, files and library entries stay, and rooms keep showing it), as old revisions stay
available. Moderation removes one everywhere, with a reason: its page and files answer 410, and it
leaves libraries and rooms. Both are on the product's page and in the audit log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 08:10:24 -07:00

97 lines
3.1 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { api, ApiError, unwrap } from "@/lib/api";
import { attempt, field, type ActionState } from "@/lib/actions";
const done = (id: string) => {
revalidatePath(`/products/${id}`);
revalidatePath("/library");
};
/** The person's library and a product: get it, remove it, stay on a revision or follow the latest. */
export async function libraryAction(_: ActionState, form: FormData): Promise<ActionState> {
const id = field(form, "id");
return attempt(async () => {
const lib = api.library({ id });
switch (field(form, "intent")) {
case "get":
unwrap(await lib.put());
break;
case "remove":
unwrap(await lib.delete());
break;
case "stay":
unwrap(await lib.patch({ stayOn: Number(field(form, "revision")) }));
break;
case "follow":
unwrap(await lib.patch({ stayOn: null }));
break;
}
done(id);
});
}
/** Its creator deletes the product. */
export async function deleteProduct(_: ActionState, form: FormData): Promise<ActionState> {
const id = field(form, "id");
const state = await attempt(async () => {
unwrap(await api.products({ id }).delete());
done(id);
revalidatePath("/products");
});
if (state?.error) return state;
redirect(`/products/${id}`);
}
/** Moderation removes the product. */
export async function removeProduct(_: ActionState, form: FormData): Promise<ActionState> {
const id = field(form, "id");
const state = await attempt(async () => {
unwrap(await api.products({ id }).remove.post({ reason: field(form, "reason") }));
done(id);
revalidatePath("/products");
});
if (state?.error) return state;
redirect("/products");
}
export async function retract(_: ActionState, form: FormData): Promise<ActionState> {
const id = field(form, "id");
return attempt(async () => {
unwrap(await api.products({ id }).revisions({ n: Number(field(form, "revision")) }).retract.post());
done(id);
});
}
export async function saveProduct(_: ActionState, form: FormData): Promise<ActionState> {
const id = field(form, "id");
const rating = field(form, "rating");
const state = await attempt(async () => {
unwrap(
await api.products({ id }).patch({
name: field(form, "name"),
description: typeof form.get("description") === "string" ? String(form.get("description")) : "",
...(rating === "adult" ? { rating: "adult" as const } : {}),
}),
);
done(id);
revalidatePath("/products");
});
if (state?.error) return state;
redirect(`/products/${id}`);
}
/** An upload token and the catalog's upload address, for the browser to send the file there itself. */
export async function startUpload(
catalog: string,
product?: string,
): Promise<{ token: string; url: string } | { error: string }> {
try {
return unwrap(await api.products.upload.post({ catalog, ...(product ? { product } : {}) }));
} catch (e) {
if (e instanceof ApiError) return { error: e.message };
throw e;
}
}