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
+103
View File
@@ -0,0 +1,103 @@
"use client";
import { useState } from "react";
import { api } from "@/trpc/react";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export function GuestGallery({ slug }: { slug: string }) {
const gallery = api.event.gallery.useQuery(slug);
const [active, setActive] = useState<string | null>(null);
if (gallery.isLoading) {
return (
<div className="columns-2 gap-3 sm:columns-3">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} className="mb-3 h-40 w-full break-inside-avoid" />
))}
</div>
);
}
const photos = gallery.data ?? [];
const selected = photos.find((photo) => photo.id === active);
if (photos.length === 0) {
return (
<Empty className="border">
<EmptyHeader>
<EmptyTitle>No photos in the gallery yet</EmptyTitle>
<EmptyDescription>
Photos will appear here after the event people release the gallery.
</EmptyDescription>
</EmptyHeader>
</Empty>
);
}
return (
<>
<div className="columns-2 gap-2 sm:columns-3 sm:gap-3">
{photos.map((photo, index) =>
photo.thumbUrl || photo.displayUrl ? (
<button
key={photo.id}
type="button"
className="photo-rise mb-2 block w-full break-inside-avoid overflow-hidden rounded-xl focus-visible:ring-3 focus-visible:ring-ring/50 sm:mb-3"
style={{ animationDelay: `${Math.min(index, 12) * 40}ms` }}
onClick={() => setActive(photo.id)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={photo.thumbUrl ?? photo.displayUrl ?? ""}
alt={
photo.contributorName
? `Photo from ${photo.contributorName}`
: "Event photo"
}
width={photo.width ?? 800}
height={photo.height ?? 1000}
loading="lazy"
className="w-full transition-transform duration-300 ease-out hover:scale-[1.03] motion-reduce:transition-none motion-reduce:hover:scale-100"
/>
</button>
) : null,
)}
</div>
<Dialog open={Boolean(selected)} onOpenChange={(open) => !open && setActive(null)}>
<DialogContent className="overflow-hidden border-none bg-background p-3 sm:max-w-3xl sm:p-4">
<DialogHeader className="px-1">
<DialogTitle>
{selected?.contributorName ?? "Shared by a guest"}
</DialogTitle>
<DialogDescription className="sr-only">
Full-size event photo
</DialogDescription>
</DialogHeader>
{selected?.displayUrl || selected?.thumbUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={selected.displayUrl ?? selected.thumbUrl ?? ""}
alt=""
width={selected.width ?? 1600}
height={selected.height ?? 1200}
className="max-h-[75dvh] w-full rounded-lg object-contain"
/>
) : null}
</DialogContent>
</Dialog>
</>
);
}