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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user