From 72db810066b1a092aaa7ee8acdc0b27876af9ce5 Mon Sep 17 00:00:00 2001 From: Lea Date: Thu, 10 Aug 2023 23:52:12 +0200 Subject: [PATCH] feat: allow creating and editing vanity invites --- components/cards/InviteCard.tsx | 39 +++-- components/pages/inspector/InviteList.tsx | 174 +++++++++++++++++----- lib/actions.ts | 20 +++ 3 files changed, 186 insertions(+), 47 deletions(-) diff --git a/components/cards/InviteCard.tsx b/components/cards/InviteCard.tsx index ee2a7f4..af25ea7 100644 --- a/components/cards/InviteCard.tsx +++ b/components/cards/InviteCard.tsx @@ -8,21 +8,31 @@ import { Button } from "../ui/button"; import { AlertDialogFooter, AlertDialogHeader, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, AlertDialogTrigger } from "../ui/alert-dialog"; import { toast } from "../ui/use-toast"; import { Input } from "../ui/input"; -import { useState } from "react"; -import { deleteInvite, editInvite } from "@/lib/actions"; +import { useMemo, useState } from "react"; +import { deleteInvite, editInvite, editInviteChannel } from "@/lib/actions"; +import { ChannelDropdown } from "../pages/inspector/InviteList"; export function InviteCard({ invite, - channel, + channel: channelInput, + channelList, user, }: { invite: ChannelInvite; channel?: Channel; + channelList?: Channel[]; user?: User; }) { - const [editDraft, setEditDraft] = useState(""); + const [editDraft, setEditDraft] = useState(invite._id); const [deleted, setDeleted] = useState(false); const [code, setCode] = useState(invite._id); + const [channelId, setChannelId] = useState(channelInput?._id ?? ""); + const [channelDraft, setChannelDraft] = useState(channelInput?._id ?? ""); + + const channel = useMemo( + () => channelList?.find(channel => channel._id == channelId) || channelInput, + [channelId, channelList, channelInput] +); if (deleted) return <>; @@ -73,20 +83,29 @@ export function InviteCard({

Invites are case sensitive.

- setEditDraft(e.currentTarget.value)} - placeholder={code} - /> +
+ setEditDraft(e.currentTarget.value)} + placeholder={code} + /> + +
{ try { - await editInvite(code, editDraft); + if (code != editDraft) await editInvite(code, editDraft); + if (channel?._id != channelDraft) await editInviteChannel(editDraft, channelDraft); setCode(editDraft); setEditDraft(""); + setChannelId(channelDraft); toast({ title: "Invite edited" }); } catch(e) { toast({ diff --git a/components/pages/inspector/InviteList.tsx b/components/pages/inspector/InviteList.tsx index 85b7511..f9dc2ff 100644 --- a/components/pages/inspector/InviteList.tsx +++ b/components/pages/inspector/InviteList.tsx @@ -7,34 +7,35 @@ import { Command, CommandItem } from "@/components/ui/command"; import { Input } from "@/components/ui/input"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; import { toast } from "@/components/ui/use-toast"; -import { bulkDeleteInvites } from "@/lib/actions"; +import { bulkDeleteInvites, createInvite } from "@/lib/actions"; import { ChannelInvite } from "@/lib/db"; import { ChevronsUpDown } from "lucide-react"; import { useMemo, useState } from "react"; import { Channel, Server, User } from "revolt-api"; -export default function ServerInviteList({ server, invites, channels, users }: { +export default function ServerInviteList({ server, invites: invitesInput, channels, users }: { server: Server, invites: ChannelInvite[], channels?: Channel[], users?: User[], }) { + const [invites, setInvites] = useState(invitesInput); const [selectVanityOnly, setSelectVanityOnly] = useState(false); const [selectChannel, setSelectChannel] = useState(false); const [selectUser, setSelectUser] = useState(false); const [vanityFilter, setVanityFilter] = useState(null); const [channelFilter, setChannelFilter] = useState(""); const [userFilter, setUserFilter] = useState(""); - const [deletedInvites, setDeletedInvites] = useState([]); + const [inviteDraft, setInviteDraft] = useState(""); + const [inviteChannelDraft, setInviteChannelDraft] = useState(""); const filteredInvites = useMemo(() => { return invites - ?.filter(invite => !deletedInvites.includes(invite._id)) ?.filter(invite => vanityFilter === true ? invite.vanity : vanityFilter === false ? !invite.vanity : true) ?.filter(invite => channelFilter ? invite.channel == channelFilter : true) ?.filter(invite => userFilter ? invite.creator == userFilter : true) ?.reverse(); - }, [vanityFilter, channelFilter, userFilter, invites, deletedInvites]); + }, [vanityFilter, channelFilter, userFilter, invites]); return (
@@ -139,38 +140,97 @@ export default function ServerInviteList({ server, invites, channels, users }: { - - - - - - - Bulk delete invites - - - - This will delete all invites that match your filter options. -
- {filteredInvites.length} invite{filteredInvites.length == 1 ? '' : 's'} will be deleted. -
- - { - try { - await bulkDeleteInvites(filteredInvites.map(i => i._id)); - setDeletedInvites([...deletedInvites, ...filteredInvites.map(i => i._id)]); - toast({ title: "Selected invites have been deleted" }); - } catch(e) { - toast({ - title: "Failed to delete invite", - description: String(e), - variant: "destructive", - }); - } - }} - >Bulk delete - Cancel - + + + + + + + Bulk delete invites + + + + This will delete all invites that match your filter options. +
+ {filteredInvites.length} invite{filteredInvites.length == 1 ? '' : 's'} will be deleted. +
+ + { + try { + await bulkDeleteInvites(filteredInvites.map(i => i._id)); + setInvites(invites.filter(invite => !filteredInvites.find(i => i._id == invite._id))); + toast({ title: "Selected invites have been deleted" }); + } catch(e) { + toast({ + title: "Failed to delete invites", + description: String(e), + variant: "destructive", + }); + } + }} + >Bulk delete + Cancel + +
+
+ + + + + + + + + Create vanity invite + + + +

Invites are case sensitive.

+
+ setInviteDraft(e.currentTarget.value)} + placeholder="fortnite" + /> + +
+
+ + { + try { + const newInvite: ChannelInvite = { + _id: inviteDraft, + channel: inviteChannelDraft, + creator: server.owner, + server: server._id, + type: "Server", + vanity: true, + }; + await createInvite(newInvite); + setInvites([...invites, newInvite]); + setInviteDraft(""); + toast({ + title: "Vanity invite created", + description: rvlt.gg/{inviteDraft} + }); + } catch(e) { + toast({ + title: "Failed to create invite", + description: String(e), + variant: "destructive", + }); + } + }} + >Create + Cancel +
@@ -178,9 +238,49 @@ export default function ServerInviteList({ server, invites, channels, users }: { {filteredInvites.map(invite => ( c._id == invite.channel)} + channelList={channels} user={users?.find(c => c._id == invite.creator)} key={invite._id} />))} ) } + +export function ChannelDropdown({ channels, value, setValue }: { + channels: Channel[], + value: string, + setValue: (value: string) => any, +}) { + const [expanded, setExpanded] = useState(false); + + return ( + + + + + + + {channels?.map((channel) => ( + { + setExpanded(false); + setValue(channel._id); + }} + >{'#' + (channel as any).name} + ))} + + + + ); +} diff --git a/lib/actions.ts b/lib/actions.ts index 8478eed..a8db87f 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -618,6 +618,26 @@ export async function editInvite(invite: string, newInvite: string) { .insertOne({ ...value, _id: newInvite }); } +export async function editInviteChannel(invite: string, newChannel: string) { + await checkPermission("channels/update/invites", { invite, newChannel }); + + if (!invite) throw new Error("invite is empty"); + + await mongo() + .db("revolt") + .collection("channel_invites") + .updateOne({ _id: invite }, { $set: { channel: newChannel } }); +} + +export async function createInvite(invite: ChannelInvite) { + await checkPermission("channels/update/invites", invite); + + await mongo() + .db("revolt") + .collection("channel_invites") + .insertOne(invite); +} + export async function bulkDeleteInvites(invites: string[]) { await checkPermission("channels/update/invites", invites);