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]>
This commit is contained in:
1 parent
07bbc11565
commit
97ce2be569
12 files changed
+2435
-14
No files matched your search
@@ -6,8 +6,9 @@ import { PageHeader, Tag } from "@/components/status";
|
||||
import { DateTime } from "@/components/time";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getMe, type Revision } from "@/lib/api";
|
||||
import { libraryAction, retract } from "../actions";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getMe, isModerator, type Revision } from "@/lib/api";
|
||||
import { deleteProduct, libraryAction, removeProduct, retract } from "../actions";
|
||||
import { loadProduct } from "../load";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||
@@ -27,9 +28,16 @@ export default async function ProductPage({ params }: { params: Promise<{ id: st
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
{product.creator && <span>by {product.creator}</span>}
|
||||
<RatingTag rating={product.rating} />
|
||||
{product.deleted && <Tag tone="danger">deleted</Tag>}
|
||||
</span>
|
||||
</PageHeader>
|
||||
|
||||
{product.deleted && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{mine ? "You deleted this product." : "Its creator deleted this product."} People who have it keep it.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(product.image || product.categories.length > 0) && (
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{product.image && (
|
||||
@@ -44,7 +52,7 @@ export default async function ProductPage({ params }: { params: Promise<{ id: st
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{me ? (
|
||||
{product.deleted && !inLibrary ? null : me ? (
|
||||
<ActionForm action={libraryAction} fields={{ id }} className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{inLibrary ? (
|
||||
@@ -66,10 +74,21 @@ export default async function ProductPage({ params }: { params: Promise<{ id: st
|
||||
Sign in to get it
|
||||
</Link>
|
||||
)}
|
||||
{mine && (
|
||||
<Link href={`/products/${id}/edit`} className={buttonVariants({ variant: "outline" })}>
|
||||
Edit
|
||||
</Link>
|
||||
{mine && !product.deleted && (
|
||||
<>
|
||||
<Link href={`/products/${id}/edit`} className={buttonVariants({ variant: "outline" })}>
|
||||
Edit
|
||||
</Link>
|
||||
<ActionForm action={deleteProduct} fields={{ id }}>
|
||||
<SubmitButton
|
||||
variant="outline"
|
||||
pendingLabel="Deleting…"
|
||||
confirm="Delete this product? Nobody new can get it. People who have it keep it."
|
||||
>
|
||||
Delete
|
||||
</SubmitButton>
|
||||
</ActionForm>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -82,6 +101,26 @@ export default async function ProductPage({ params }: { params: Promise<{ id: st
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isModerator(me) && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Moderation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ActionForm action={removeProduct} fields={{ id }} className="flex flex-wrap items-center gap-2">
|
||||
<Input name="reason" required maxLength={500} placeholder="Reason" aria-label="Reason" className="max-w-sm" />
|
||||
<SubmitButton
|
||||
variant="destructive"
|
||||
pendingLabel="Removing…"
|
||||
confirm="Remove this product everywhere? Its page, files and library entries go."
|
||||
>
|
||||
Remove
|
||||
</SubmitButton>
|
||||
</ActionForm>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Revisions</CardTitle>
|
||||
|
||||
@@ -32,6 +32,30 @@ export async function libraryAction(_: ActionState, form: FormData): Promise<Act
|
||||
});
|
||||
}
|
||||
|
||||
/** 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 () => {
|
||||
|
||||
@@ -2,13 +2,13 @@ import "server-only";
|
||||
import { notFound } from "next/navigation";
|
||||
import { api, ApiError, unwrap, type ProductPage } from "@/lib/api";
|
||||
|
||||
/** The product, or not found (also when it's adult and the visitor can't see adult products). */
|
||||
/** The product, or not found (also when it's adult and the visitor can't see adult products, or it was removed). */
|
||||
export async function loadProduct(id: string): Promise<ProductPage> {
|
||||
if (!/^[a-z2-7]{10}$/.test(id)) notFound();
|
||||
try {
|
||||
return unwrap(await api.products({ id }).get());
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 404) notFound();
|
||||
if (e instanceof ApiError && (e.status === 404 || e.status === 410)) notFound();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user