Some checks failed
CI / Tests / 🧪 Test (push) Has been cancelled
Mesh detail page at /dashboard/meshes/[id]: - Header: flex-col → flex-row at sm breakpoint. Live/Invite buttons stretch full-width stacked on mobile (flex-1), auto-width side-by- side from sm up. - "Generate invite link" truncates to "Invite" on mobile (viewport constrained) so the button fits next to Live. - Members + active-invites rows: stack metadata vertically on mobile (flex-col → sm:flex-row), wrap badges inside with flex-wrap so the member display-name + role + revoked badges don't horizontal-scroll. Invites list at /dashboard/invites: - Wrap the table in overflow-x-auto with min-w-[560px] on the table itself. 5-column data-table that genuinely needs horizontal space — don't fake it with card stacking, let the user scroll naturally. Quick-send composer deferred to a follow-up — writes a message to the mesh, which requires a client-side encryption decision (ed25519 keypair in the browser? key derivation from session? plaintext-to- broker and break E2E?). Parked as its own spec. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
import Link from "next/link";
|
|
|
|
import { getMyInvitesResponseSchema } from "@turbostarter/api/schema";
|
|
import { handle } from "@turbostarter/api/utils";
|
|
import { Badge } from "@turbostarter/ui-web/badge";
|
|
|
|
import { pathsConfig } from "~/config/paths";
|
|
import { api } from "~/lib/api/server";
|
|
import { getMetadata } from "~/lib/metadata";
|
|
import {
|
|
DashboardHeader,
|
|
DashboardHeaderDescription,
|
|
DashboardHeaderTitle,
|
|
} from "~/modules/common/layout/dashboard/header";
|
|
|
|
export const generateMetadata = getMetadata({
|
|
title: "Invites",
|
|
description: "Invites you've issued.",
|
|
});
|
|
|
|
export default async function InvitesPage() {
|
|
const { sent } = await handle(api.my.invites.$get, {
|
|
schema: getMyInvitesResponseSchema,
|
|
})();
|
|
|
|
return (
|
|
<>
|
|
<DashboardHeader>
|
|
<div>
|
|
<DashboardHeaderTitle>Invites</DashboardHeaderTitle>
|
|
<DashboardHeaderDescription>
|
|
Invite links you've issued across all your meshes.
|
|
</DashboardHeaderDescription>
|
|
</div>
|
|
</DashboardHeader>
|
|
{sent.length === 0 ? (
|
|
<div className="rounded-lg border border-dashed p-10 text-center">
|
|
<p className="text-muted-foreground">
|
|
You haven't issued any invites yet. Open a mesh and generate
|
|
one.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto rounded-lg border">
|
|
<table className="w-full min-w-[560px] text-sm">
|
|
<thead className="text-muted-foreground border-b text-left text-xs uppercase">
|
|
<tr>
|
|
<th className="px-4 py-3 font-medium">Mesh</th>
|
|
<th className="px-4 py-3 font-medium">Role</th>
|
|
<th className="px-4 py-3 font-medium">Uses</th>
|
|
<th className="px-4 py-3 font-medium">Expires</th>
|
|
<th className="px-4 py-3 font-medium">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y">
|
|
{sent.map((inv) => (
|
|
<tr key={inv.id}>
|
|
<td className="px-4 py-3">
|
|
{inv.meshId ? (
|
|
<Link
|
|
href={pathsConfig.dashboard.user.meshes.mesh(inv.meshId)}
|
|
className="group flex flex-col gap-0.5"
|
|
>
|
|
<span className="group-hover:text-primary font-medium underline underline-offset-4">
|
|
{inv.meshName ?? "—"}
|
|
</span>
|
|
<span className="text-muted-foreground font-mono text-xs">
|
|
{inv.meshSlug ?? "—"}
|
|
</span>
|
|
</Link>
|
|
) : (
|
|
<span className="text-muted-foreground">—</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<Badge variant="outline">{inv.role}</Badge>
|
|
</td>
|
|
<td className="px-4 py-3 font-mono text-xs">
|
|
{inv.usedCount} / {inv.maxUses}
|
|
</td>
|
|
<td className="text-muted-foreground px-4 py-3 text-xs">
|
|
{new Date(inv.expiresAt).toLocaleDateString()}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{inv.revokedAt ? (
|
|
<Badge className="bg-destructive/15 text-destructive text-xs">
|
|
revoked
|
|
</Badge>
|
|
) : new Date(inv.expiresAt) < new Date() ? (
|
|
<Badge variant="outline" className="text-xs">
|
|
expired
|
|
</Badge>
|
|
) : inv.usedCount >= inv.maxUses ? (
|
|
<Badge variant="outline" className="text-xs">
|
|
exhausted
|
|
</Badge>
|
|
) : (
|
|
<Badge className="bg-success/15 text-success text-xs">
|
|
active
|
|
</Badge>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|