New uploads are drafts: only their creator and moderators see them, nobody can get or wear them, and they're deleted if not published within 30 days (their catalogs get delete changes). The creator reviews the info and publishes it. Editing a product's info is a permission check now (product:edit): moderators can edit anyone's with a reason the creator sees, lock fields (product:lock; locked fields need product:edit-locked) and lower ratings (product:rate-lower, not below its files'). Every change is kept in the product's info history, with who and why, and can be undone by the same rules. Products can be unlisted (out of search and lists, the link still works) and tagged; lists filter by tag and catalog. The console has a Products section (everyone's for moderators, by status) and a drafts to-do. A product's own category is left for later: the hub has no list of IMVU's categories to pick from. Co-Authored-By: Claude Opus 5.5 <[email protected]>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { notFound, redirect } from "next/navigation";
|
|
import { cache } from "react";
|
|
import { NavLink } from "@/components/header-client";
|
|
import { api, getMe, isModerator, unwrap, type ConsoleData, type Person } from "@/lib/api";
|
|
|
|
/** The signed-in moderator, or off to sign in; others get not found (these pages aren't for them). */
|
|
export async function requireModerator(path: string): Promise<Person> {
|
|
const me = await getMe();
|
|
if (!me) redirect(`/signin?next=${encodeURIComponent(path)}`);
|
|
if (!isModerator(me)) notFound();
|
|
return me;
|
|
}
|
|
|
|
/** The console as this person sees it (once per request); undefined if it isn't for them. */
|
|
const loadConsole = cache(async (): Promise<ConsoleData | undefined> => {
|
|
const answer = await api.console.get();
|
|
if (answer.error) return undefined;
|
|
return unwrap(answer);
|
|
});
|
|
|
|
/**
|
|
* The signed-in person and their console, or off to sign in. People who manage nothing get not found: the console
|
|
* is for those controlling a product, catalog or room.
|
|
*/
|
|
export async function requireConsole(path: string): Promise<{ me: Person; console: ConsoleData }> {
|
|
const me = await getMe();
|
|
if (!me) redirect(`/signin?next=${encodeURIComponent(path)}`);
|
|
const data = await loadConsole();
|
|
if (!data) notFound();
|
|
return { me, console: data };
|
|
}
|
|
|
|
type Section = ConsoleData["sections"][number];
|
|
|
|
// Settings are on the overview.
|
|
const tabs: { section: Section; href: string; label: string }[] = [
|
|
{ section: "overview", href: "/console", label: "Overview" },
|
|
{ section: "products", href: "/console/products", label: "Products" },
|
|
{ section: "catalogs", href: "/console/catalogs", label: "Catalogs" },
|
|
{ section: "people", href: "/console/people", label: "People" },
|
|
{ section: "audit", href: "/console/audit", label: "Audit" },
|
|
];
|
|
|
|
/** The console's sections this person may see. */
|
|
export async function ConsoleNav() {
|
|
const sections = new Set<Section>((await loadConsole())?.sections ?? ["overview"]);
|
|
return (
|
|
<nav className="mb-8 flex gap-1 border-b pb-3 text-sm">
|
|
{tabs
|
|
.filter((t) => sections.has(t.section))
|
|
.map((t) => (
|
|
<NavLink key={t.section} href={t.href} exact={t.section === "overview"}>
|
|
{t.label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
);
|
|
}
|