Hosted rooms phase 2: moderators, bans, privacy, invites, images, favorites, ratings, recent rooms, occupancy watch
CI / test (push) Successful in 1m10s
CI / test (push) Successful in 1m10s
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]>
This commit is contained in:
1 parent
43183ec7ee
commit
76c8e0342e
16 files changed
+3851
-73
No files matched your search
@@ -4,6 +4,7 @@ import { PageHeader, Tag } from "@/components/status";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { api, getMe, unwrap, type HostedRoom } from "@/lib/api";
|
||||
import { RoomActions } from "./room-actions";
|
||||
|
||||
export const metadata: Metadata = { title: "Rooms" };
|
||||
|
||||
@@ -39,7 +40,7 @@ export default async function Rooms() {
|
||||
<ul className="grid gap-4 sm:grid-cols-2">
|
||||
{hosted.map((r) => (
|
||||
<li key={r.n}>
|
||||
<HostedCard room={r} mine={own.has(r.n)} />
|
||||
<HostedCard room={r} mine={own.has(r.n)} signedIn={!!me} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -93,13 +94,18 @@ export default async function Rooms() {
|
||||
);
|
||||
}
|
||||
|
||||
function HostedCard({ room: r, mine }: { room: HostedRoom; mine: boolean }) {
|
||||
function HostedCard({ room: r, mine, signedIn }: { room: HostedRoom; mine: boolean; signedIn: boolean }) {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardContent className="space-y-3">
|
||||
<Card className="h-full overflow-hidden py-0">
|
||||
{r.image && (
|
||||
// The room's image, from the hub.
|
||||
<img src={r.image} alt="" className="aspect-[332/281] w-full bg-muted object-cover" loading="lazy" />
|
||||
)}
|
||||
<CardContent className="space-y-3 py-6">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">{r.name}</span>
|
||||
{r.rating === "adult" && <Tag tone="adult">Adult</Tag>}
|
||||
{r.privacy === "private" && <Tag>Private</Tag>}
|
||||
{r.occupancy > 0 && (
|
||||
<Tag tone="online" dot>
|
||||
{r.occupancy} here
|
||||
@@ -113,8 +119,10 @@ function HostedCard({ room: r, mine }: { room: HostedRoom; mine: boolean }) {
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{r.ownerName} · {r.occupancy}/{r.maxUsers}
|
||||
{r.ratings > 0 && ` · ★ ${r.score.toFixed(1)} (${r.ratings})`}
|
||||
</p>
|
||||
{r.description && <p className="line-clamp-3 text-sm whitespace-pre-line">{r.description}</p>}
|
||||
{signedIn && <RoomActions n={r.n} favorite={r.favorite} canRate={!mine} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user