"use client"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useState } from "react"; import { toast } from "sonner"; import type { EventRole } from "@album/contracts"; import { api } from "@/trpc/react"; import { PendingInvites } from "@/components/pending-invites"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { MailIcon } from "lucide-react"; import { effectiveEvent } from "@/lib/event-lifecycle"; import { galleryIsPublic } from "@/lib/publishing"; import { EmailHistory } from "./email-history"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; const roles: EventRole[] = ["owner", "manager", "moderator", "viewer"]; const roleLabels: Record = { owner: "Owner", manager: "Manager", moderator: "Moderator", viewer: "Viewer" }; export function EventPeople({ eventId, canManage, canGrantOwner, }: { eventId: string; canManage: boolean; canGrantOwner: boolean; }) { const utils = api.useUtils(); const members = api.manager.members.useQuery({ eventId }); const guests = api.manager.guests.useQuery({ eventId }, { refetchInterval: 5000 }); const [guestSearch, setGuestSearch] = useState(""); const [preview, setPreview] = useState(false); const emailPreview = api.manager.emailPreview.useQuery({ eventId }, { enabled: preview }); const notify = api.manager.notifyGuests.useMutation({ onSuccess: async (result) => { toast.success(`${result.queued} emails queued`); setPreview(false); await utils.manager.guests.invalidate({ eventId }); }, onError: (error) => toast.error(error.message), }); const previouslyContacted = new Set((guests.data ?? []).filter((guest) => guest.email && (guest.notifiedAt || guest.notificationClaimedAt)).map((guest) => guest.email!.toLowerCase())); const eligible = new Set((guests.data ?? []).filter((guest) => guest.email && guest.notifyWhenReady && !previouslyContacted.has(guest.email.toLowerCase())).map((guest) => guest.email!.toLowerCase())).size; const [email, setEmail] = useState(""); const [role, setRole] = useState("manager"); const setMember = api.manager.setMember.useMutation({ onSuccess: async () => { toast.success("Member updated"); setEmail(""); await utils.manager.members.invalidate({ eventId }); }, onError: (error) => toast.error(error.message), }); const invite = api.group.inviteEmail.useMutation({ onSuccess: async () => { toast.success("Invite emailed"); setEmail(""); await utils.group.pendingInvites.invalidate(); }, onError: (error) => toast.error(error.message), }); const event = api.manager.event.useQuery({ eventId }); const remove = api.manager.removeMember.useMutation({ onSuccess: async () => { await utils.manager.members.invalidate({ eventId }); }, onError: (error) => toast.error(error.message), }); return ( People Guests attend or receive gallery notifications. Accounts have permission to manage this event; attending does not grant account access.

Guests

setGuestSearch(e.target.value)} /> {guests.isLoading ?

Loading guests…

: guests.isError ?

Could not load guests.

: !guests.data?.length ?

No guests yet. Guests appear when they register their details or contribute.

: null}
    {(guests.data ?? []).filter((guest) => `${guest.displayName ?? ""} ${guest.email ?? ""}`.toLowerCase().includes(guestSearch.toLowerCase())).map((guest) =>
  • {guest.displayName ?? "Anonymous guest"}

    {guest.email ?? "No email provided"}

    {guest.notifiedAt ? `Emailed ${new Date(guest.notifiedAt).toLocaleDateString()}` : guest.notificationClaimedAt ? "Delivery pending / needs review" : guest.email && guest.notifyWhenReady ? "Opted into gallery email" : "Not subscribed"}
  • )}
{event.data?.permissions.includes("gallery.release") ?

Complete the event and make the gallery public first. Only opted-in guests who have not already been emailed are eligible; repeated addresses are deduplicated.

{preview ?

The gallery for {event.data.title} is ready

{emailPreview.data ?