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,144 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { EventRole } from "@album/contracts";
|
||||
import { api } from "@/trpc/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
const roles: EventRole[] = ["owner", "manager", "moderator", "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 [email, setEmail] = useState("");
|
||||
const [role, setRole] = useState<EventRole>("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: () => toast.success("Invite emailed"),
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>People</CardTitle>
|
||||
<CardDescription>
|
||||
The couple can both be owners. Managers run the day. Moderators review photos.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ul className="flex flex-col gap-2">
|
||||
{(members.data ?? []).map((member) => (
|
||||
<li key={member.id} className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="font-medium">{member.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{member.email}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{member.role}</Badge>
|
||||
{canManage ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
remove.mutate({ eventId, userId: member.userId })
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{canManage ? (
|
||||
<form
|
||||
className="flex flex-wrap items-end gap-2"
|
||||
onSubmit={(formEvent) => {
|
||||
formEvent.preventDefault();
|
||||
if (role === "owner" && !canGrantOwner) {
|
||||
toast.error("Only an owner can add another owner");
|
||||
return;
|
||||
}
|
||||
setMember.mutate({ eventId, email, role });
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
required
|
||||
placeholder="email@example.com"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<select
|
||||
className="rounded-md border bg-background px-2 py-1.5 text-sm"
|
||||
value={role}
|
||||
onChange={(event) => setRole(event.target.value as EventRole)}
|
||||
>
|
||||
{roles
|
||||
.filter((value) => value !== "owner" || canGrantOwner)
|
||||
.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button type="submit" disabled={setMember.isPending}>
|
||||
Add existing user
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={invite.isPending || !event.data?.groupId}
|
||||
onClick={() => {
|
||||
if (!event.data?.groupId) return;
|
||||
invite.mutate({
|
||||
email,
|
||||
groupId: event.data.groupId,
|
||||
eventId,
|
||||
eventRole: role,
|
||||
groupRole: "member",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Email invite
|
||||
</Button>
|
||||
</form>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user