Cases are one queue for what someone has to decide, each with a thread (internal notes for those deciding), an assignee and a decision. Reports about a product go to the teams of the catalogs hosting it and to moderators, and only the reporter and moderators know who reported. A creator lowering a rating opens a rating review; a catalog's owner asking to delete it opens a deletion request; a catalog's team can ask for a product's removal everywhere; the person an action affected can appeal it once, decided by another moderator (an admin may decide their own, flagged). Resolving carries the case out: the rating is lowered, the product removed, the catalog deleted. Notifications reach people on the website (a bell with the unread count) and their proxies (realtime): moderators' edits to their products, removals and restores, decisions and replies on their cases, membership decisions. Also: moderators can restore a removed product, and a product's creator and moderators can see it when it's adult without an adult account (getting and wearing it still need one). Co-Authored-By: Claude Opus 5.5 <[email protected]>
99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
"use server";
|
|
// Cases (reports, reviews, requests, appeals) and notifications, from the website.
|
|
import { revalidatePath } from "next/cache";
|
|
import { redirect } from "next/navigation";
|
|
import { api, unwrap } from "@/lib/api";
|
|
import { attempt, field, type ActionState } from "@/lib/actions";
|
|
|
|
/** Reports a product, catalog, room or person. */
|
|
export async function report(_: ActionState, form: FormData): Promise<ActionState> {
|
|
const kind = field(form, "about");
|
|
const id = field(form, "id");
|
|
if (!["product", "catalog", "room", "person"].includes(kind)) return { error: "Nothing to report." };
|
|
return attempt(async () => {
|
|
unwrap(await api.cases.post({ kind: "report", about: { [kind]: id }, reason: field(form, "reason") }));
|
|
revalidatePath("/console/cases");
|
|
return "Reported. Moderators will look at it; you'll be told what they decide.";
|
|
});
|
|
}
|
|
|
|
/** Appeals an action from the audit log. */
|
|
export async function appeal(_: ActionState, form: FormData): Promise<ActionState> {
|
|
let id = 0;
|
|
const state = await attempt(async () => {
|
|
const { case: c } = unwrap(await api.cases.post({ kind: "appeal", audit: Number(field(form, "audit")), reason: field(form, "reason") }));
|
|
id = c.id;
|
|
});
|
|
if (state?.error) return state;
|
|
redirect(`/console/cases/${id}`);
|
|
}
|
|
|
|
/** A catalog's team asks moderators to remove a product everywhere. */
|
|
export async function requestRemoval(_: ActionState, form: FormData): Promise<ActionState> {
|
|
return attempt(async () => {
|
|
unwrap(
|
|
await api.cases.post({
|
|
kind: "removal-request",
|
|
about: { product: field(form, "product") },
|
|
catalog: field(form, "catalog"),
|
|
reason: field(form, "reason"),
|
|
}),
|
|
);
|
|
revalidatePath("/console/cases");
|
|
return "Asked. Moderators will decide.";
|
|
});
|
|
}
|
|
|
|
/** A catalog's owner asks for it to be deleted. */
|
|
export async function requestDeletion(_: ActionState, form: FormData): Promise<ActionState> {
|
|
const id = field(form, "id");
|
|
let kase = 0;
|
|
const state = await attempt(async () => {
|
|
kase = unwrap(await api.catalogs({ id }).delete.post({ reason: field(form, "reason") })).case;
|
|
});
|
|
if (state?.error) return state;
|
|
redirect(`/console/cases/${kase}`);
|
|
}
|
|
|
|
const onCase = (form: FormData) => {
|
|
const id = Number(field(form, "case"));
|
|
return { id, done: () => revalidatePath(`/console/cases/${id}`) };
|
|
};
|
|
|
|
export async function addNote(_: ActionState, form: FormData): Promise<ActionState> {
|
|
const { id, done } = onCase(form);
|
|
return attempt(async () => {
|
|
unwrap(await api.cases({ id }).notes.post({ body: field(form, "body"), internal: form.get("internal") === "on" }));
|
|
done();
|
|
});
|
|
}
|
|
|
|
export async function assign(_: ActionState, form: FormData): Promise<ActionState> {
|
|
const { id, done } = onCase(form);
|
|
return attempt(async () => {
|
|
const to = field(form, "to");
|
|
unwrap(await api.cases({ id }).assign.post({ to: to || null }));
|
|
done();
|
|
});
|
|
}
|
|
|
|
export async function decide(_: ActionState, form: FormData): Promise<ActionState> {
|
|
const { id, done } = onCase(form);
|
|
const decision = field(form, "decision");
|
|
if (decision !== "resolve" && decision !== "decline") return { error: "Choose to resolve or decline it." };
|
|
return attempt(async () => {
|
|
unwrap(await api.cases({ id }).decide.post({ decision, resolution: field(form, "resolution") }));
|
|
done();
|
|
revalidatePath("/console/cases");
|
|
revalidatePath("/console");
|
|
});
|
|
}
|
|
|
|
export async function markAllRead(_: ActionState): Promise<ActionState> {
|
|
return attempt(async () => {
|
|
unwrap(await api.notifications.read.post({}));
|
|
revalidatePath("/notifications");
|
|
revalidatePath("/", "layout");
|
|
});
|
|
}
|