408 lines
10 KiB
TypeScript
408 lines
10 KiB
TypeScript
import { PERMS } from "@/lib/permission-slugs";
|
|
import type { HousekeepingPageInput } from "../../../route-handlers";
|
|
import { contentQuery } from "../queries/content-queries";
|
|
import {
|
|
type ContentCommandField,
|
|
ContentCommandForm,
|
|
} from "./content-command-form";
|
|
import {
|
|
ContentPageFrame,
|
|
type ContentPageProps,
|
|
parseContentListInput,
|
|
} from "./content-page-frame";
|
|
|
|
export function ContentEngagementPage({
|
|
context,
|
|
result,
|
|
routeId,
|
|
}: ContentPageProps) {
|
|
const canEvents = context.has(PERMS.EVENTS_EDIT);
|
|
const canPolls = context.has(PERMS.POLLS_EDIT);
|
|
const canPrefixes = context.has(PERMS.PREFIXES_EDIT);
|
|
const creatingEvent = routeId === "content.engagement.event-create";
|
|
const creatingPoll = routeId === "content.engagement.poll-create";
|
|
const eventFields: readonly ContentCommandField[] = [
|
|
{
|
|
name: "id",
|
|
label: "Event ID",
|
|
type: "identifier",
|
|
required: !creatingEvent,
|
|
},
|
|
{
|
|
name: "title",
|
|
label: "Title",
|
|
type: "text",
|
|
required: creatingEvent,
|
|
maxLength: 255,
|
|
},
|
|
{
|
|
name: "description",
|
|
label: "Description",
|
|
type: "textarea",
|
|
required: creatingEvent,
|
|
maxLength: 20_000,
|
|
},
|
|
{
|
|
name: "typeId",
|
|
label: "Event type ID",
|
|
type: "identifier",
|
|
required: creatingEvent,
|
|
},
|
|
{ name: "roomId", label: "Room ID", type: "identifier" },
|
|
{
|
|
name: "startsAt",
|
|
label: "Starts at",
|
|
type: "text",
|
|
required: creatingEvent,
|
|
maxLength: 50,
|
|
},
|
|
{ name: "endsAt", label: "Ends at", type: "text", maxLength: 50 },
|
|
{ name: "maxPlayers", label: "Maximum players", type: "number", min: 1 },
|
|
{ name: "isRecurring", label: "Recurring", type: "checkbox" },
|
|
{
|
|
name: "recurrenceRule",
|
|
label: "Recurrence rule",
|
|
type: "text",
|
|
maxLength: 255,
|
|
},
|
|
{
|
|
name: "status",
|
|
label: "Status",
|
|
type: "select",
|
|
options: [
|
|
{ value: "draft", label: "Draft" },
|
|
{ value: "published", label: "Published" },
|
|
{ value: "cancelled", label: "Cancelled" },
|
|
{ value: "completed", label: "Completed" },
|
|
],
|
|
},
|
|
{ name: "image", label: "Image URL", type: "text", maxLength: 500 },
|
|
];
|
|
const pollFields: readonly ContentCommandField[] = [
|
|
{
|
|
name: "id",
|
|
label: "Poll ID",
|
|
type: "identifier",
|
|
required: !creatingPoll,
|
|
},
|
|
{
|
|
name: "title",
|
|
label: "Title",
|
|
type: "text",
|
|
required: creatingPoll,
|
|
maxLength: 255,
|
|
},
|
|
{
|
|
name: "description",
|
|
label: "Description",
|
|
type: "textarea",
|
|
maxLength: 2_000,
|
|
},
|
|
{
|
|
name: "status",
|
|
label: "Status",
|
|
type: "select",
|
|
options: [
|
|
{ value: "draft", label: "Draft" },
|
|
{ value: "active", label: "Active" },
|
|
{ value: "closed", label: "Closed" },
|
|
],
|
|
},
|
|
{ name: "showResults", label: "Show results", type: "checkbox" },
|
|
{
|
|
name: "multipleChoice",
|
|
label: "Allow multiple choices",
|
|
type: "checkbox",
|
|
},
|
|
{ name: "startsAt", label: "Starts at", type: "text", maxLength: 50 },
|
|
{ name: "endsAt", label: "Ends at", type: "text", maxLength: 50 },
|
|
];
|
|
return (
|
|
<ContentPageFrame
|
|
title="Engagement"
|
|
description="Manage events, polls, and community prefixes."
|
|
result={result}
|
|
forms={
|
|
<div className="grid gap-3 lg:grid-cols-2">
|
|
{canEvents && routeId === "content.engagement.event-types" ? (
|
|
<ContentCommandForm
|
|
commandId="content.engagement.event-type.change"
|
|
buttonLabel="Save event type"
|
|
input={{ action: "update" }}
|
|
fields={[
|
|
{
|
|
name: "id",
|
|
label: "Type ID",
|
|
type: "identifier",
|
|
required: true,
|
|
},
|
|
{ name: "name", label: "Name", type: "text", maxLength: 100 },
|
|
{ name: "slug", label: "Slug", type: "text", maxLength: 100 },
|
|
{
|
|
name: "description",
|
|
label: "Description",
|
|
type: "textarea",
|
|
maxLength: 500,
|
|
},
|
|
{ name: "color", label: "Color", type: "text", maxLength: 20 },
|
|
{ name: "icon", label: "Icon", type: "text", maxLength: 50 },
|
|
{ name: "isActive", label: "Active", type: "checkbox" },
|
|
{
|
|
name: "minRank",
|
|
label: "Minimum rank",
|
|
type: "number",
|
|
min: 0,
|
|
max: 7,
|
|
},
|
|
]}
|
|
/>
|
|
) : null}
|
|
{canEvents &&
|
|
routeId?.includes("event") &&
|
|
routeId !== "content.engagement.event-types" ? (
|
|
<>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.event.change"
|
|
buttonLabel="Save event"
|
|
input={{ action: creatingEvent ? "create" : "update" }}
|
|
fields={eventFields}
|
|
/>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.event-prize.change"
|
|
buttonLabel="Save prize"
|
|
input={{ action: "create" }}
|
|
fields={[
|
|
{
|
|
name: "eventId",
|
|
label: "Event ID",
|
|
type: "identifier",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "position",
|
|
label: "Position",
|
|
type: "number",
|
|
min: 1,
|
|
defaultValue: 1,
|
|
},
|
|
{
|
|
name: "prizeType",
|
|
label: "Prize type",
|
|
type: "select",
|
|
options: [
|
|
{ value: "badge", label: "Badge" },
|
|
{ value: "credits", label: "Credits" },
|
|
{ value: "pixels", label: "Pixels" },
|
|
{ value: "points", label: "Points" },
|
|
{ value: "item", label: "Item" },
|
|
],
|
|
},
|
|
{
|
|
name: "badgeCode",
|
|
label: "Badge code",
|
|
type: "text",
|
|
maxLength: 50,
|
|
},
|
|
{
|
|
name: "credits",
|
|
label: "Credits",
|
|
type: "number",
|
|
min: 0,
|
|
defaultValue: 0,
|
|
},
|
|
{
|
|
name: "pixels",
|
|
label: "Pixels",
|
|
type: "number",
|
|
min: 0,
|
|
defaultValue: 0,
|
|
},
|
|
{
|
|
name: "points",
|
|
label: "Points",
|
|
type: "number",
|
|
min: 0,
|
|
defaultValue: 0,
|
|
},
|
|
{ name: "itemId", label: "Item ID", type: "identifier" },
|
|
{
|
|
name: "description",
|
|
label: "Description",
|
|
type: "text",
|
|
maxLength: 255,
|
|
},
|
|
]}
|
|
/>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.event-winner.add"
|
|
buttonLabel="Add winner"
|
|
input={{}}
|
|
fields={[
|
|
{
|
|
name: "eventId",
|
|
label: "Event ID",
|
|
type: "identifier",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "userId",
|
|
label: "User ID",
|
|
type: "identifier",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "position",
|
|
label: "Position",
|
|
type: "number",
|
|
min: 1,
|
|
defaultValue: 1,
|
|
},
|
|
]}
|
|
/>
|
|
</>
|
|
) : null}
|
|
{canPolls && routeId?.includes("poll") ? (
|
|
<>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.poll.change"
|
|
buttonLabel="Save poll"
|
|
input={{ action: creatingPoll ? "create" : "update" }}
|
|
fields={pollFields}
|
|
/>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.poll-question.change"
|
|
buttonLabel="Save question"
|
|
input={{ action: "create" }}
|
|
fields={[
|
|
{
|
|
name: "pollId",
|
|
label: "Poll ID",
|
|
type: "identifier",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "question",
|
|
label: "Question",
|
|
type: "text",
|
|
required: true,
|
|
maxLength: 500,
|
|
},
|
|
{
|
|
name: "type",
|
|
label: "Question type",
|
|
type: "select",
|
|
options: [
|
|
{ value: "single", label: "Single choice" },
|
|
{ value: "multiple", label: "Multiple choice" },
|
|
{ value: "text", label: "Free text" },
|
|
],
|
|
},
|
|
{
|
|
name: "sortOrder",
|
|
label: "Sort order",
|
|
type: "number",
|
|
min: 0,
|
|
defaultValue: 0,
|
|
},
|
|
{
|
|
name: "options",
|
|
label: "Options",
|
|
type: "textarea",
|
|
required: true,
|
|
maxLength: 20_000,
|
|
},
|
|
]}
|
|
/>
|
|
</>
|
|
) : null}
|
|
{canPrefixes && routeId === "content.engagement.prefixes" ? (
|
|
<>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.prefix.change"
|
|
buttonLabel="Save prefix"
|
|
input={{ action: "update" }}
|
|
fields={[
|
|
{
|
|
name: "id",
|
|
label: "Prefix ID",
|
|
type: "identifier",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "text",
|
|
label: "Text",
|
|
type: "text",
|
|
required: true,
|
|
maxLength: 255,
|
|
},
|
|
{
|
|
name: "color",
|
|
label: "Color",
|
|
type: "text",
|
|
required: true,
|
|
maxLength: 32,
|
|
},
|
|
{ name: "icon", label: "Icon", type: "text", maxLength: 255 },
|
|
{
|
|
name: "effect",
|
|
label: "Effect",
|
|
type: "text",
|
|
maxLength: 255,
|
|
},
|
|
{ name: "active", label: "Active", type: "checkbox" },
|
|
]}
|
|
/>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.prefix-blacklist.change"
|
|
buttonLabel="Update blacklist"
|
|
input={{ action: "add" }}
|
|
fields={[
|
|
{
|
|
name: "word",
|
|
label: "Word",
|
|
type: "text",
|
|
required: true,
|
|
maxLength: 255,
|
|
},
|
|
]}
|
|
/>
|
|
<ContentCommandForm
|
|
commandId="content.engagement.prefix-settings.update"
|
|
buttonLabel="Save prefix settings"
|
|
input={{}}
|
|
fields={[
|
|
{
|
|
name: "settings",
|
|
label: "Prefix settings JSON",
|
|
type: "json",
|
|
required: true,
|
|
maxLength: 20_000,
|
|
},
|
|
]}
|
|
/>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export async function renderContentEngagementPage(
|
|
input: HousekeepingPageInput,
|
|
) {
|
|
const routeId = input.match.routeId as ContentPageProps["routeId"];
|
|
const result = await contentQuery.run(input.context, {
|
|
routeId: routeId ?? "content.engagement.events",
|
|
params: input.match.params,
|
|
list: parseContentListInput(input.searchParams ?? {}),
|
|
});
|
|
return (
|
|
<ContentEngagementPage
|
|
context={input.context}
|
|
result={result}
|
|
routeId={routeId}
|
|
/>
|
|
);
|
|
}
|