feat: T5DE's website: landing page, download, changelog and FAQ
CI / test (push) Successful in 13s
CI / image (push) Successful in 12s
Release / release (push) Successful in 3s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-11-03 01:55:42 -08:00
commit 8401f30f78
35 files changed
+2216

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
import type { Metadata } from "next";
import Markdown from "react-markdown";
import { allReleases, formatDate, releasesPage } from "@/lib/releases";
export const metadata: Metadata = { title: "Changelog" };
// Release notes are Markdown, written by the client's CI: what's in each release.
const markdown = {
p: (props: React.ComponentProps<"p">) => <p className="text-muted-foreground" {...props} />,
ul: (props: React.ComponentProps<"ul">) => <ul className="list-disc space-y-1 pl-5 text-muted-foreground" {...props} />,
ol: (props: React.ComponentProps<"ol">) => <ol className="list-decimal space-y-1 pl-5 text-muted-foreground" {...props} />,
code: (props: React.ComponentProps<"code">) => <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs break-all" {...props} />,
a: (props: React.ComponentProps<"a">) => <a className="text-foreground underline underline-offset-4" {...props} />,
strong: (props: React.ComponentProps<"strong">) => <strong className="font-medium text-foreground" {...props} />,
h1: (props: React.ComponentProps<"h3">) => <h3 className="font-medium" {...props} />,
h2: (props: React.ComponentProps<"h3">) => <h3 className="font-medium" {...props} />,
h3: (props: React.ComponentProps<"h3">) => <h3 className="font-medium" {...props} />,
};
export default async function ChangelogPage() {
const releases = await allReleases();
return (
<div className="max-w-2xl space-y-10">
<h1 className="text-3xl font-semibold tracking-tight">Changelog</h1>
{releases === null ? (
<p className="text-muted-foreground">
Couldn&apos;t load the releases. They&apos;re on{" "}
<a href={releasesPage} className="text-foreground underline underline-offset-4">
git.t5de.dev
</a>
.
</p>
) : (
releases.map((r) => (
<article key={r.tag} id={r.tag} className="space-y-4 border-t pt-8 first:border-t-0 first:pt-0">
<header className="flex flex-wrap items-baseline gap-x-3">
<h2 className="text-xl font-semibold">
<a href={`#${r.tag}`}>{r.version}</a>
</h2>
<time dateTime={r.date} className="text-sm text-muted-foreground">
{formatDate(r.date)}
</time>
</header>
<div className="space-y-3 text-sm">
<Markdown components={markdown}>{r.body}</Markdown>
</div>
</article>
))
)}
</div>
);
}
+81
View File
@@ -0,0 +1,81 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Checksum, Download } from "@/components/download";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { formatDate, latestRelease, nightlyRelease, releasesPage } from "@/lib/releases";
export const metadata: Metadata = { title: "Download" };
export default async function DownloadPage() {
const [release, nightly] = await Promise.all([latestRelease(), nightlyRelease()]);
return (
<div className="space-y-12">
<section className="max-w-2xl space-y-5">
<h1 className="text-3xl font-semibold tracking-tight">Download T5DE</h1>
<p className="text-muted-foreground">
For Windows. Install IMVU Classic{release?.imvu && <> {release.imvu}</>} from imvu.com first, then run
T5DE-Setup.exe.
</p>
<Download release={release} />
</section>
<section className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Windows Protected Your PC?</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
T5DE&apos;s setup isn&apos;t code-signed yet, so Windows may warn about it. Click{" "}
<strong className="text-foreground">More info</strong>, then{" "}
<strong className="text-foreground">Run anyway</strong>.
</p>
<p>To check the file first, compare its checksum with the one above in PowerShell:</p>
<code className="block rounded bg-muted px-2 py-1.5 font-mono text-xs text-foreground">
Get-FileHash .\T5DE-Setup.exe
</code>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardDescription>For Testers</CardDescription>
<CardTitle>Nightly Build</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
<p>
The latest changes, built every time they land. It may break, and it uses the staging hub
(hub.staging.t5de.dev) unless you choose another in Settings.
</p>
{nightly?.setup ? (
<>
<p>
<a href={nightly.setup.url} className="text-foreground underline underline-offset-4">
Download the nightly
</a>{" "}
· {nightly.version} · {formatDate(nightly.date)}
</p>
{nightly.sha256 && <Checksum sha256={nightly.sha256} />}
</>
) : (
<a href={releasesPage} className="text-foreground underline underline-offset-4">
Releases on git.t5de.dev
</a>
)}
</CardContent>
</Card>
</section>
<p className="text-sm text-muted-foreground">
Older versions are on{" "}
<a href={releasesPage} className="text-foreground underline underline-offset-4">
git.t5de.dev
</a>
. What changed:{" "}
<Link href="/changelog" className="text-foreground underline underline-offset-4">
changelog
</Link>
.
</p>
</div>
);
}
+128
View File
@@ -0,0 +1,128 @@
import type { Metadata } from "next";
import Link from "next/link";
import { hubURL } from "@/lib/links";
export const metadata: Metadata = { title: "FAQ" };
const issues = "https://git.t5de.dev/t5de/t5de/issues";
function Path({ children }: { children: React.ReactNode }) {
return <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs break-all text-foreground">{children}</code>;
}
const questions: { q: string; a: React.ReactNode }[] = [
{
q: "What is T5DE?",
a: (
<p>
An unofficial patch for the IMVU Classic client for Windows. It adds a modern renderer, community products and
rooms hosted by T5DE&apos;s hub, removes ads and tracking, and fixes small things. It isn&apos;t made by or
affiliated with IMVU. Use it at your own risk.
</p>
),
},
{
q: "What do I need?",
a: (
<p>
Windows, and IMVU Classic installed from imvu.com. T5DE&apos;s setup patches a copy of it, so your IMVU
Classic stays as it is and both can be installed side by side.
</p>
),
},
{
q: "Windows says it protected my PC. Is the setup safe?",
a: (
<p>
The setup isn&apos;t code-signed yet, so Windows warns about it. Click More info, then Run anyway. You can
compare the file&apos;s checksum with the one on the{" "}
<Link href="/download" className="text-foreground underline underline-offset-4">
download page
</Link>{" "}
first. T5DE&apos;s source is public on git.t5de.dev.
</p>
),
},
{
q: "Does T5DE see my IMVU password?",
a: (
<p>
No. You sign in to IMVU as usual, and your IMVU sign-in never leaves your computer. The{" "}
<a href={hubURL} className="text-foreground underline underline-offset-4">
hub
</a>{" "}
has its own account; you link avatars to it by putting a code in their tagline.
</p>
),
},
{
q: "How do I update?",
a: (
<p>
T5DE checks for updates when it starts and offers them in the client, keeping your patches and settings. A
newer setup run by hand offers to update too.
</p>
),
},
{
q: "How do I change patches, repair or uninstall?",
a: (
<p>
Run the setup again, or open T5DE in Windows&apos; Apps &amp; features (Installed apps), to change patches,
repair or uninstall.
</p>
),
},
{
q: "What does T5DE send, and to whom?",
a: (
<p>
Nothing goes to IMVU&apos;s analytics or crash reporting. With your consent, T5DE sends anonymous usage
statistics and crash reports to T5DE; you can turn both off in the client&apos;s Settings, under T5DE.
</p>
),
},
{
q: "What are nightlies and the staging hub?",
a: (
<p>
Nightly builds have the latest changes for testers and use the staging hub (hub.staging.t5de.dev), with its
own accounts. Releases use hub.t5de.dev. Either can switch in Settings, under T5DE, T5DE Hub, Server.
</p>
),
},
{
q: "Something went wrong. Where are the logs?",
a: (
<p>
The client&apos;s log is <Path>%APPDATA%\T5DE\IMVULog.log</Path>, and T5DE&apos;s proxy&apos;s is{" "}
<Path>%TEMP%\t5de-proxy.log</Path>. Please report bugs on{" "}
<a href={issues} className="text-foreground underline underline-offset-4">
git.t5de.dev
</a>
, with what happened and the logs if you can (they may contain your avatar name).
</p>
),
},
];
export default function FAQPage() {
return (
<div className="max-w-2xl space-y-8">
<h1 className="text-3xl font-semibold tracking-tight">FAQ</h1>
<div className="divide-y border-y">
{questions.map(({ q, a }) => (
<details key={q} className="group py-4">
<summary className="cursor-pointer list-none font-medium marker:hidden">
<span className="flex items-center justify-between gap-4">
{q}
<span className="text-muted-foreground transition-transform group-open:rotate-45">+</span>
</span>
</summary>
<div className="pt-3 text-sm text-muted-foreground">{a}</div>
</details>
))}
</div>
</div>
);
}
+164
View File
@@ -0,0 +1,164 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (@media (prefers-color-scheme: dark));
@theme inline {
--font-heading: var(--font-sans);
--font-sans: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
@media (prefers-color-scheme: dark) {
:root {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
/* Product descriptions: creators' HTML (cleaned by the API), laid out plainly. */
@layer components {
.product-description {
@apply space-y-3 text-sm leading-relaxed break-words;
}
.product-description :where(h1, h2, h3, h4, h5, h6) {
@apply font-semibold;
}
.product-description :where(h1) {
@apply text-xl;
}
.product-description :where(h2) {
@apply text-lg;
}
.product-description :where(a) {
@apply underline underline-offset-4;
}
.product-description :where(ul) {
@apply list-disc pl-5;
}
.product-description :where(ol) {
@apply list-decimal pl-5;
}
.product-description :where(img) {
@apply inline-block h-auto max-w-full;
}
.product-description :where(td, th) {
@apply border px-2 py-1;
}
.product-description :where(hr) {
@apply my-4;
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+54
View File
@@ -0,0 +1,54 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { Geist, Geist_Mono } from "next/font/google";
import { NavLink } from "@/components/nav-link";
import { hubURL } from "@/lib/links";
import { cn } from "@/lib/utils";
import logo from "@/components/t5de.png";
import "./globals.css";
const sans = Geist({ subsets: ["latin"], variable: "--font-sans" });
const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono" });
export const metadata: Metadata = {
metadataBase: new URL("https://t5de.dev"),
title: { template: "%s · T5DE", default: "T5DE" },
description: "An unofficial patch for the IMVU Classic client: a modern renderer, community products, and no tracking.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={cn("font-sans antialiased", sans.variable, mono.variable)}>
<body className="flex min-h-dvh flex-col">
<header className="border-b">
<div className="mx-auto flex max-w-5xl flex-wrap items-center gap-x-6 gap-y-2 px-4 py-3 sm:px-6">
<Link href="/" aria-label="T5DE home" className="shrink-0">
<Image src={logo} alt="T5DE" priority className="size-9" />
</Link>
<nav className="flex flex-wrap items-center gap-1 text-sm">
<NavLink href="/download">Download</NavLink>
<NavLink href="/changelog">Changelog</NavLink>
<NavLink href="/faq">FAQ</NavLink>
</nav>
<a
href={hubURL}
className="ml-auto rounded-md px-2.5 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
Hub
</a>
</div>
</header>
<main className="mx-auto w-full max-w-5xl flex-1 px-4 py-10 sm:px-6 sm:py-14">{children}</main>
<footer className="border-t">
<div className="mx-auto flex max-w-5xl flex-wrap gap-x-6 gap-y-2 px-4 py-6 text-sm text-muted-foreground sm:px-6">
<span>T5DE is unofficial: not made by or affiliated with IMVU.</span>
<a href="https://git.t5de.dev/t5de" className="hover:text-foreground sm:ml-auto">
Source
</a>
</div>
</footer>
</body>
</html>
);
}
+104
View File
@@ -0,0 +1,104 @@
import Link from "next/link";
import { BoxIcon, DoorOpenIcon, GaugeIcon, ShieldIcon, SparklesIcon, UsersIcon } from "lucide-react";
import { Download } from "@/components/download";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { hubURL } from "@/lib/links";
import { latestRelease } from "@/lib/releases";
const features = [
{
icon: GaugeIcon,
title: "A Modern Renderer",
text: "Smooth frame rates up to 360 FPS or uncapped, with animations and the camera drawn between frames.",
},
{
icon: BoxIcon,
title: "Community Products",
text: "Clothing and furniture made by the community, from catalogs on the hub, in the shop and your inventory.",
},
{
icon: DoorOpenIcon,
title: "T5DE Rooms",
text: "Rooms hosted by T5DE's hub: public or private, with moderators, invites, favorites and ratings.",
},
{
icon: UsersIcon,
title: "One Account",
text: "Link all your avatars to one hub account, see who's online, and show the T5 badge if you like.",
},
{
icon: ShieldIcon,
title: "No Tracking",
text: "IMVU's analytics, crash reports, log uploads and device fingerprinting are blocked.",
},
{
icon: SparklesIcon,
title: "Small Fixes",
text: "No ads, Creator Mode, more room filters, product IDs in the shop, and faster downloads.",
},
];
const steps = [
{ title: "Install IMVU Classic", text: "From imvu.com, as usual. T5DE leaves it untouched." },
{ title: "Run T5DE's Setup", text: "It finds IMVU Classic and installs a patched copy. Pick the patches you want." },
{ title: "Sign In", text: "With your IMVU account. T5DE keeps itself up to date." },
];
export default async function Home() {
const release = await latestRelease();
return (
<div className="space-y-20">
<section className="max-w-2xl space-y-5 pt-4">
<p className="text-sm font-medium text-muted-foreground">T5DE</p>
<h1 className="text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
IMVU Classic, kept alive and made better.
</h1>
<p className="text-lg text-pretty text-muted-foreground">
An unofficial patch for the IMVU Classic client: a modern renderer, community products, hosted rooms, and no
tracking.
</p>
<div className="pt-2">
<Download release={release}>
<a href={hubURL} className={buttonVariants({ size: "lg", variant: "outline", className: "px-4" })}>
Open the Hub
</a>
</Download>
</div>
</section>
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{features.map(({ icon: Icon, title, text }) => (
<Card key={title}>
<CardHeader>
<Icon className="size-5 text-muted-foreground" />
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">{text}</CardContent>
</Card>
))}
</section>
<section className="space-y-6">
<h2 className="text-2xl font-semibold tracking-tight">Getting Started</h2>
<ol className="grid gap-6 sm:grid-cols-3">
{steps.map((step, i) => (
<li key={step.title} className="space-y-1.5">
<span className="flex size-7 items-center justify-center rounded-full bg-muted text-sm font-medium tabular-nums">
{i + 1}
</span>
<p className="font-medium">{step.title}</p>
<p className="text-sm text-muted-foreground">{step.text}</p>
</li>
))}
</ol>
<p className="text-sm text-muted-foreground">
Windows only.{" "}
<Link href="/faq" className="text-foreground underline underline-offset-4">
Questions?
</Link>
</p>
</section>
</div>
);
}