Website: Next.js app for accounts, linking, who's online, device sign-in and the console
apps/web: Next.js 16 (App Router), Tailwind 4 and shadcn/ui, light and dark from the system. Pages render on the server and call the API with Eden Treaty (typed by @t5de/api), forwarding the visitor's cookie; forms use server actions; sign-up, sign-in and sign-out use Better Auth's React client. /api is rewritten to the API so its cookies are first-party. Who's online follows the API's event stream (Next's gzip is off: it held the stream back). Standalone output with a Dockerfile; deploys to Vercel as is. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7afada1c63
commit
64c552330b
57 files changed
+3834
No files matched your search
@@ -0,0 +1,82 @@
|
||||
// Small labels. Color only means something: green is online, blue is age verified or adult, red is banned.
|
||||
import type { Link } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const tones = {
|
||||
neutral: "border-border bg-muted/60 text-muted-foreground",
|
||||
strong: "border-foreground/15 bg-foreground/5 text-foreground",
|
||||
online: "border-emerald-600/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
|
||||
adult: "border-sky-600/20 bg-sky-500/10 text-sky-700 dark:text-sky-400",
|
||||
danger: "border-red-600/20 bg-red-500/10 text-red-700 dark:text-red-400",
|
||||
};
|
||||
|
||||
export function Tag({
|
||||
tone = "neutral",
|
||||
dot = false,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
tone?: keyof typeof tones;
|
||||
dot?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-6 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium whitespace-nowrap",
|
||||
tones[tone],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{dot && <span className="size-1.5 rounded-full bg-current" aria-hidden />}
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** A round initial standing in for an avatar picture. */
|
||||
export function Initial({ name }: { name: string }) {
|
||||
return (
|
||||
<span
|
||||
className="grid size-10 shrink-0 place-items-center rounded-full border bg-muted text-sm font-semibold text-muted-foreground uppercase"
|
||||
aria-hidden
|
||||
>
|
||||
{name.slice(0, 1)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** The community products an account wears. */
|
||||
export function Outfit({ items }: { items: { id: string; name: string }[] }) {
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs text-muted-foreground">Wearing</div>
|
||||
<ul className="flex flex-wrap gap-1.5">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className="rounded-md border bg-background px-2 py-0.5 text-xs">
|
||||
{item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** A page's title and the line under it. */
|
||||
export function PageHeader({ title, children }: { title: React.ReactNode; children?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="mb-8 space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">{title}</h1>
|
||||
{children && <div className="text-muted-foreground">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** An IMVU account's age status: blue when it can see adult community products. */
|
||||
export function AgeTag({ link }: { link: Pick<Link, "ageVerified" | "accessPass"> }) {
|
||||
if (link.ageVerified) return <Tag tone="adult">Age verified</Tag>;
|
||||
if (link.accessPass) return <Tag tone="adult">Access Pass</Tag>;
|
||||
return <Tag>Not age verified</Tag>;
|
||||
}
|
||||
Reference in new issue
Block a user