import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { CatalogStatus } from "@/components/catalogs"; import { ActionForm, SubmitButton } from "@/components/forms"; import { formatSize, RatingTag } from "@/components/products"; import { PageHeader, Tag } from "@/components/status"; import { DateTime } from "@/components/time"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Field, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { api, unwrap, type CatalogMember, type CatalogPage } from "@/lib/api"; import { AuditTable } from "../../audit-table"; import { ConsoleNav, requireConsole } from "../../console-nav"; import { addOperator, createInvite, decideMember, delist, removeFromTeam, revokeInvite, rotateKey, saveCatalog, setSuspended, } from "../actions"; export const metadata: Metadata = { title: "Catalog" }; type Perm = CatalogPage["permissions"][number]; export default async function ManageCatalog({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const { me } = await requireConsole(`/console/catalogs/${id}`); const answer = await api.catalogs({ id }).get(); if (answer.error) notFound(); const data = unwrap(answer); const has = (p: Perm) => data.permissions.includes(p); if (!has("audit:read")) notFound(); const c = data.catalog; const [team, members, invites, hosted, audit] = await Promise.all([ api.catalogs({ id }).team.get().then(unwrap), has("member:approve") ? api.catalogs({ id }).members.get().then(unwrap) : undefined, has("member:invite") ? api.catalogs({ id }).invites.get().then(unwrap) : undefined, api.catalogs({ id }).products.get().then(unwrap), api.console.audit.get({ query: { catalog: id, limit: 20 } }).then(unwrap), ]); const byStatus = (s: CatalogMember["status"]) => members?.members.filter((m) => m.status === s) ?? []; const onTeam = team.roles.some((r) => r.personId === me.id); return ( <> {c.url} {c.status === "approved" && ( Public page )}
{(c.status === "suspended" || c.status === "rejected") && ( {c.status === "suspended" ? "Suspended by moderation" : "Rejected"} {c.statusReason || "No reason given."}{" "} {c.status === "suspended" && "Its products aren't available from it, and it can't take uploads."} )}
{data.members && ( <> )}
Settings {!has("catalog:edit") && Only the owner and operators change these.}
Name Address Takedown contact
{has("catalog:edit") && Save}
{has("catalog:rotate-key") && ( Key The catalog signs in to the hub with its ID ({id}) and key. A new key stops the old one at once. New key )} Team The owner, and operators: they manage everything but the team itself.
    {team.owner && (
  • {team.owner.username} owner
  • )} {team.roles.map((r) => (
  • {r.username} operator {(has("team:manage") || r.personId === me.id) && ( {r.personId === me.id ? "Leave" : "Remove"} )}
  • ))}
{has("team:manage") && (
Add operator
)} {!onTeam && team.owner?.personId !== me.id && (

You see this as a moderator.

)}
{members && ( Members {c.membership === "open" ? "Open: anyone can get its products. Bans still apply." : c.membership === "approval" ? "By approval: people ask, the team decides." : "Invite only: people join with an invitation."}
Add member
)} {invites && ( Invitations Links that let people join (also an approval or invite-only catalog) at once. {invites.invites.length > 0 && ( Link Used Expires {invites.invites.map((i) => ( {i.url} {i.uses} {i.maxUses !== null && ` of ${i.maxUses}`} {i.expiresAt ? : "never"} Revoke ))}
)}
Uses Days New invitation
)} Products What it hosts. Delisting stops hosting a product here; other catalogs may still host it. {hosted.products.length === 0 ? (

Nothing yet.

) : ( Product Creator Revisions Since {has("hosting:delist") && } {hosted.products.map((p) => ( {p.name} {" "} {" "} {p.removed && removed} {p.deleted && deleted} {p.creator} {p.revisions.join(", ")} {has("hosting:delist") && (
Delist
)}
))}
)}
{has("catalog:suspend") && (c.status === "approved" || c.status === "suspended") && ( Moderation A suspended catalog's products aren't available from it, it can't take uploads, and its team sees why. {c.status === "suspended" ? ( Reinstate ) : (
Suspend
)}
)} Recent Changes All changes to this catalog
); } function Stat({ label, value, note }: { label: string; value: string; note: string }) { return ( {label} {value} {note} ); } function Choice({ name, label, value, options }: { name: string; label: string; value: string; options: [string, string][] }) { return ( {label} {options.map(([v, l]) => ( {l} ))} ); } const memberActions = { approve: { label: "Approve", variant: "default" as const }, decline: { label: "Decline", variant: "outline" as const }, ban: { label: "Ban", variant: "outline" as const }, remove: { label: "Remove", variant: "outline" as const }, unban: { label: "Unban", variant: "outline" as const }, }; function MemberList({ id, title, members, actions, }: { id: string; title: string; members: CatalogMember[]; actions: (keyof typeof memberActions)[]; }) { if (members.length === 0) return null; return (

{title} {members.length}

); }