"use client"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import QRCode from "qrcode"; import { createMyInviteInputSchema, type CreateMyInviteInput, } from "@turbostarter/api/schema"; import { handle } from "@turbostarter/api/utils"; import { Badge } from "@turbostarter/ui-web/badge"; import { Button } from "@turbostarter/ui-web/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@turbostarter/ui-web/form"; import { Input } from "@turbostarter/ui-web/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@turbostarter/ui-web/select"; import { api } from "~/lib/api/client"; interface GeneratedInvite { id: string; token: string; inviteLink: string; joinUrl: string; /** Short human-friendly URL, preferred for sharing. Null if the backend didn't mint one. */ shortUrl: string | null; expiresAt: Date; qrDataUrl: string; } export const InviteGenerator = ({ meshId }: { meshId: string }) => { const [result, setResult] = useState(null); const [copied, setCopied] = useState<"url" | "cli" | null>(null); const [showAdvanced, setShowAdvanced] = useState(false); const form = useForm({ resolver: zodResolver(createMyInviteInputSchema), defaultValues: { role: "member", maxUses: 1, expiresInDays: 7 }, }); const onSubmit = async (values: CreateMyInviteInput) => { try { const res = (await handle(api.my.meshes[":id"].invites.$post)({ param: { id: meshId }, json: values, })) as { id: string; token: string; inviteLink: string; joinUrl: string; shortUrl: string | null; expiresAt: string; }; // QR encodes the SHORT URL when available — scannable at camera distance // and short enough for the QR to stay low-density. Falls back to the // long token URL for legacy invites minted before the shortener shipped. const qrTarget = res.shortUrl ?? res.joinUrl; const qrDataUrl = await QRCode.toDataURL(qrTarget, { width: 256, margin: 1, color: { dark: "#141413", light: "#ffffff" }, }); setResult({ id: res.id, token: res.token, inviteLink: res.inviteLink, joinUrl: res.joinUrl, shortUrl: res.shortUrl, expiresAt: new Date(res.expiresAt), qrDataUrl, }); } catch (e) { form.setError("root", { message: e instanceof Error ? e.message : "Failed to generate invite.", }); } }; const copy = async (text: string, key: "url" | "cli") => { await navigator.clipboard.writeText(text); setCopied(key); setTimeout(() => setCopied(null), 2000); }; if (result) { // Prefer the short URL everywhere it exists. CLI command still uses the // long token because the broker resolves by token — swapping CLI to short // codes is part of the v2 protocol, not this URL-shortener change. const primaryUrl = result.shortUrl ?? result.joinUrl; const cliCmd = `claudemesh join ${result.token}`; return (
Invite QR code
Share this link
{primaryUrl}
expires {result.expiresAt.toLocaleDateString()}

How your teammate joins:

Paste the link in Slack / Telegram / email. They land on a page with step-by-step install, or run the CLI directly if they already have it:

{cliCmd}
); } return (

One-time invite for a new member. Valid for 7 days.

{/* Advanced options — hidden by default. Defaults ship 90% of users. */}
{showAdvanced && (
( Role )} /> ( Max uses field.onChange(Number(e.target.value))} /> )} /> ( Expires in (days) field.onChange(Number(e.target.value))} /> )} />
)}
{form.formState.errors.root && (

{form.formState.errors.root.message}

)}
); };