Files
t5de-hub/apps/web/app/rooms/[n]/edit/page.tsx
dhkatzandClaude Opus 5.5 76c8e0342e
CI / test (push) Successful in 1m10s
Hosted rooms phase 2: moderators, bans, privacy, invites, images, favorites, ratings, recent rooms, occupancy watch
Moderators (up to 20 IMVU accounts, set by ID or name) and the owner account boot
and ban over realtime (room.boot); bans are listed and lifted over REST. Private
rooms are unlisted and joined by the owner, moderators and holders of an accepted
invite (room.invite / room.invited / room.invite_reply / room.invite_answer).
Room images (PNG/JPEG by magic bytes, up to 512 KB) are stored in the database
and served with cache headers. Per-person favorites, ratings and recent rooms.
room.watch pushes room.occupancy; participants carry their IMVU name, and
GET .../participants lists them. Contract in docs/hosted-rooms.md, "Phase 2".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 06:07:41 -07:00

100 lines
3.7 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { ActionForm, SubmitButton } from "@/components/forms";
import { PageHeader } from "@/components/status";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { FieldGroup } from "@/components/ui/field";
import { api, getMe, unwrap } from "@/lib/api";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { closeRoom, removeImage, saveImage, saveRoom } from "../../actions";
import { RoomFields } from "../../room-fields";
export const metadata: Metadata = { title: "Edit room" };
export default async function EditRoom({ params }: { params: Promise<{ n: string }> }) {
const { n } = await params;
if (!/^[0-9]{1,9}$/.test(n)) notFound();
const me = await getMe();
if (!me) redirect(`/signin?next=/rooms/${n}/edit`);
const [{ rooms }, { links }] = await Promise.all([
api["hosted-rooms"].get({ query: { mine: "1" } }).then(unwrap),
api.account.get().then(unwrap),
]);
const room = rooms.find((r) => String(r.n) === n);
if (!room) notFound();
return (
<div className="space-y-6">
<PageHeader title={`Edit ${room.name}`}>
<Link href="/rooms" className="underline-offset-4 hover:underline">
Back to rooms
</Link>
</PageHeader>
<Card>
<CardHeader>
<CardDescription>
Owned by {room.ownerName} · {room.occupancy} here now
</CardDescription>
</CardHeader>
<CardContent>
<ActionForm action={saveRoom} fields={{ n: room.n }}>
<FieldGroup>
<RoomFields room={room} canAdult={links.some((l) => l.adult)} moderators />
<div>
<SubmitButton pendingLabel="Saving…">Save</SubmitButton>
</div>
</FieldGroup>
</ActionForm>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Image</CardTitle>
<CardDescription>Shown in room lists.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{room.image && (
// The room's image, from the hub.
<img src={room.image} alt="" className="aspect-[332/281] w-full max-w-xs rounded-md bg-muted object-cover" />
)}
<ActionForm action={saveImage} fields={{ n: room.n }}>
<Field>
<FieldLabel htmlFor="image">{room.image ? "Replace" : "Upload"}</FieldLabel>
<Input id="image" name="image" type="file" accept="image/png,image/jpeg" required className="max-w-md" />
<FieldDescription>PNG or JPEG, up to 512 KB.</FieldDescription>
</Field>
<div>
<SubmitButton pendingLabel="Uploading…">Save image</SubmitButton>
</div>
</ActionForm>
{room.image && (
<ActionForm action={removeImage} fields={{ n: room.n }}>
<SubmitButton variant="outline" pendingLabel="Removing…">
Remove image
</SubmitButton>
</ActionForm>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Close room</CardTitle>
<CardDescription>Everyone in it is sent out. It can&apos;t be opened again.</CardDescription>
</CardHeader>
<CardContent>
<ActionForm action={closeRoom} fields={{ n: room.n }}>
<SubmitButton variant="destructive" pendingLabel="Closing…" confirm={`Close ${room.name}?`}>
Close room
</SubmitButton>
</ActionForm>
</CardContent>
</Card>
</div>
);
}