Initial commit of Vellum, an event photo product for guest uploads, host moderation, and original-quality galleries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 19:36:14 -04:00
co-authored by Cursor
commit 27e2f196eb
149 changed files with 13847 additions and 0 deletions
@@ -0,0 +1,66 @@
import { redirect } from "next/navigation";
import { headers } from "next/headers";
import Link from "next/link";
import { auth } from "@/server/auth";
import { createServerCaller } from "@/trpc/server";
import { RedeemInviteButton } from "./redeem-button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export default async function InvitationPage({
params,
}: {
params: Promise<{ token: string }>;
}) {
const { token } = await params;
const session = await auth.api.getSession({ headers: await headers() });
const caller = await createServerCaller();
let preview;
try {
preview = await caller.invites.preview({ token });
} catch {
return (
<main className="page-pad mx-auto max-w-md py-16">
<Card>
<CardHeader>
<CardTitle>Invite not found</CardTitle>
<CardDescription>This link may have expired.</CardDescription>
</CardHeader>
</Card>
</main>
);
}
if (!session) {
redirect(`/sign-up?invite=${encodeURIComponent(token)}`);
}
return (
<main className="page-pad mx-auto max-w-md py-16">
<Card>
<CardHeader>
<CardTitle className="text-2xl font-semibold tracking-tight">Join this event</CardTitle>
<CardDescription>
{preview.eventRole
? `You'll join as event ${preview.eventRole}.`
: preview.groupRole
? `You'll join the group as ${preview.groupRole}.`
: "This invite grants access."}
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<RedeemInviteButton token={token} />
<Button asChild variant="ghost">
<Link href="/dashboard">Skip</Link>
</Button>
</CardContent>
</Card>
</main>
);
}
@@ -0,0 +1,33 @@
"use client";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { api } from "@/trpc/react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export function RedeemInviteButton({ token }: { token: string }) {
const router = useRouter();
const redeem = api.invites.redeem.useMutation({
onSuccess: (result) => {
toast.success("Invite accepted");
router.push(
result.eventId
? `/dashboard/events/${result.eventId}`
: "/dashboard",
);
router.refresh();
},
onError: (error) => toast.error(error.message),
});
return (
<Button
onClick={() => redeem.mutate({ token })}
disabled={redeem.isPending}
>
{redeem.isPending ? <Spinner data-icon="inline-start" /> : null}
Accept invite
</Button>
);
}