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,156 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { GroupRole } 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";
|
||||
|
||||
export function GroupPeople({
|
||||
groupId,
|
||||
canManage,
|
||||
}: {
|
||||
groupId: string;
|
||||
canManage: boolean;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const members = api.group.members.useQuery({ groupId });
|
||||
const [email, setEmail] = useState("");
|
||||
const [role, setRole] = useState<GroupRole>("member");
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
const setMember = api.group.setMember.useMutation({
|
||||
onSuccess: async () => {
|
||||
toast.success("Member updated");
|
||||
setEmail("");
|
||||
await utils.group.members.invalidate({ groupId });
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
const invite = api.group.inviteEmail.useMutation({
|
||||
onSuccess: () => toast.success("Invite emailed"),
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
const createCode = api.group.createCode.useMutation({
|
||||
onSuccess: (result) => {
|
||||
setCode(result.code);
|
||||
toast.success("Invite code created");
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
const remove = api.group.removeMember.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.group.members.invalidate({ groupId });
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Group members</CardTitle>
|
||||
<CardDescription>
|
||||
People who can be added to events in this group.
|
||||
</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({ groupId, userId: member.userId })
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{canManage ? (
|
||||
<>
|
||||
<form
|
||||
className="flex flex-wrap items-end gap-2"
|
||||
onSubmit={(formEvent) => {
|
||||
formEvent.preventDefault();
|
||||
setMember.mutate({ groupId, 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 GroupRole)}
|
||||
>
|
||||
<option value="member">member</option>
|
||||
<option value="owner">owner</option>
|
||||
</select>
|
||||
<Button type="submit" disabled={setMember.isPending}>
|
||||
Add existing user
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={invite.isPending}
|
||||
onClick={() =>
|
||||
invite.mutate({
|
||||
email,
|
||||
groupId,
|
||||
groupRole: role,
|
||||
})
|
||||
}
|
||||
>
|
||||
Email invite
|
||||
</Button>
|
||||
</form>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
createCode.mutate({
|
||||
groupId,
|
||||
reusable: true,
|
||||
maxUses: 25,
|
||||
groupRole: "member",
|
||||
})
|
||||
}
|
||||
>
|
||||
Create reusable code
|
||||
</Button>
|
||||
{code ? (
|
||||
<p className="text-sm">
|
||||
Share this code once: <code>{code}</code>
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user