Spaces:
Running
Running
| import { useState } from "react"; | |
| import { useIntl } from "react-intl"; | |
| import { Input } from "@/components/input"; | |
| import { Label } from "@/components/label"; | |
| import { Collapse } from "@/components/collapse"; | |
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | |
| import { faGripVertical } from "@fortawesome/free-solid-svg-icons"; | |
| import { ChevronDownIcon } from "@heroicons/react/solid"; | |
| import { ChevronRightIcon } from "@heroicons/react/solid"; | |
| import classNames from "classnames"; | |
| import { TrashIcon } from "@heroicons/react/solid"; | |
| import { useUpdateEffect } from "react-use"; | |
| import { PlusIcon } from "@heroicons/react/solid"; | |
| import { ColorPicker } from "@/components/color-picker"; | |
| import { Switch } from "@/components/switch"; | |
| import { PaperAirplaneIcon } from "@heroicons/react/solid"; | |
| import { rgbaToHex } from "@/utils"; | |
| import { useMessage } from "@/components/message-editor/useMessage"; | |
| export default function EmbedsEditor() { | |
| const intl = useIntl(); | |
| const [message, setMessage] = useState({ | |
| content: "", | |
| embeds: [], | |
| buttons: [], | |
| }); | |
| const [webhookUrl, setWebhookUrl] = useState(""); | |
| const { post } = useMessage(); | |
| return ( | |
| <div className="bg-dark-600 w-full flex-col lg:flex-row flex min-h-screen"> | |
| <div className="w-full lg:w-1/2 mx-auto flex flex-col justify-center px-6 lg:px-4 py-10 max-w-2xl"> | |
| <div className="grid grid-cols-2 gap-x-6 gap-y-5 w-full"> | |
| <div className="bg-dark-400 rounded-lg p-4 w-full col-span-2 flex items-center justify-between gap-6"> | |
| <Input | |
| value={webhookUrl} | |
| placeholder="Webhook URL" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-100 focus:ring-[3px] ring-blue ring-opacity-80" | |
| onChange={(url) => setWebhookUrl(url as string)} | |
| /> | |
| <button | |
| className="bg-blue hover:bg-opacity-80 text-white font-semibold flex items-center gap-2 px-4 py-3 rounded-md text-sm whitespace-nowrap" | |
| onClick={() => post(webhookUrl, message)} | |
| > | |
| <PaperAirplaneIcon className="w-4 rotate-90" /> | |
| Send message | |
| </button> | |
| </div> | |
| <Form message={message} setMessage={setMessage} /> | |
| </div> | |
| </div> | |
| <div className="bg-dark-500 border-l-[2px] border-dark-400 p-10 top-0 sticky w-full lg:w-1/2 h-screen"> | |
| <p className="text-red bg-red bg-opacity-10 px-4 py-3 rounded-md text-center font-semibold"> | |
| preview will be here | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const Form = ({ | |
| message, | |
| setMessage, | |
| }: { | |
| message: any; | |
| setMessage: (e: any) => void; | |
| }) => { | |
| return ( | |
| <> | |
| <Collapse | |
| open={true} | |
| title="Author and Content" | |
| parentClassName="col-span-2" | |
| onOpenClassName="rounded-b-none" | |
| className="bg-blue rounded-lg p-4 text-white font-sans text-base uppercase tracking-wider font-semibold" | |
| > | |
| <div className="bg-dark-400 rounded-b-lg p-5 grid grid-cols-2 gap-4"> | |
| <div> | |
| <Label className="mb-2.5"> | |
| Username | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {message?.username?.length ?? 0}/80 | |
| </span> | |
| </Label> | |
| <Input | |
| value={message?.username} | |
| placeholder="Username" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-100 focus:ring-[3px] ring-blue ring-opacity-80" | |
| onChange={(username) => setMessage({ ...message, username })} | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5">Avatar URL</Label> | |
| <Input | |
| value={message?.avatar_url} | |
| placeholder="Avatar URL" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-100 focus:ring-[3px] ring-blue ring-opacity-80" | |
| onChange={(avatar_url) => setMessage({ ...message, avatar_url })} | |
| /> | |
| </div> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5"> | |
| CONTENT{" "} | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {message?.content?.length ?? 0}/2000 | |
| </span> | |
| </Label> | |
| <Input | |
| type="textarea" | |
| value={message.content} | |
| placeholder="Content" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-100 focus:ring-[3px] ring-blue ring-opacity-80" | |
| onChange={(content) => setMessage({ ...message, content })} | |
| /> | |
| </div> | |
| </div> | |
| </Collapse> | |
| <Collapse | |
| open={true} | |
| title={ | |
| <p> | |
| Embeds{" "} | |
| <span className="text-sm opacity-60 ml-2"> | |
| {message?.embeds?.length ?? 0} / 10 | |
| </span> | |
| </p> | |
| } | |
| parentClassName="col-span-2" | |
| onOpenClassName="rounded-b-none" | |
| className="bg-blue rounded-lg p-4 text-white font-sans text-base uppercase tracking-wider font-semibold" | |
| > | |
| <div className="bg-dark-400 rounded-b-lg p-5 grid grid-cols-1 gap-4"> | |
| <Embeds | |
| embeds={message?.embeds} | |
| setEmbeds={(embeds) => setMessage({ ...message, embeds })} | |
| /> | |
| </div> | |
| </Collapse> | |
| <Collapse | |
| open={true} | |
| title={ | |
| <p> | |
| Link Buttons | |
| <span className="text-sm opacity-60 ml-2"> | |
| {message?.buttons?.length ?? 0} / 25 | |
| </span> | |
| </p> | |
| } | |
| parentClassName="col-span-2" | |
| onOpenClassName="rounded-b-none" | |
| className="bg-blue rounded-lg p-4 text-white font-sans text-base uppercase tracking-wider font-semibold" | |
| > | |
| <div className="bg-dark-400 rounded-b-lg p-5 grid grid-cols-1 gap-4"> | |
| <Buttons | |
| buttons={message?.buttons} | |
| setButtons={(buttons) => setMessage({ ...message, buttons })} | |
| /> | |
| </div> | |
| </Collapse> | |
| </> | |
| ); | |
| }; | |
| const Embeds = ({ | |
| embeds, | |
| setEmbeds, | |
| }: { | |
| embeds: any[]; | |
| setEmbeds: (e: any) => void; | |
| }) => { | |
| const [open, setOpen] = useState(0); | |
| const onDelete = (i: number) => { | |
| const newEmbeds = embeds ?? []; | |
| newEmbeds.splice(i, 1); | |
| setEmbeds(newEmbeds); | |
| }; | |
| const setEmbed = (embed: any, index: number) => { | |
| const newEmbeds = embeds ?? []; | |
| newEmbeds[index] = embed; | |
| setEmbeds(newEmbeds); | |
| }; | |
| useUpdateEffect(() => setOpen(embeds?.length - 1), [embeds]); | |
| return ( | |
| <> | |
| {embeds?.map((embed: any, i: number) => ( | |
| <div key={i} className="flex"> | |
| <div | |
| className="h-full w-[3px] rounded-l-lg" | |
| style={{ backgroundColor: embed?.color ?? "#fff" }} | |
| /> | |
| <div className="bg-[#2f3136] rounded-r-lg w-full"> | |
| <div | |
| className={classNames( | |
| "px-4 pt-4 pb-2 flex justify-between items-center text-white font-semibold cursor-pointer", | |
| { | |
| "!pb-4": open !== i, | |
| } | |
| )} | |
| onClick={() => setOpen(open === i ? -1 : i)} | |
| > | |
| <div className="flex items-center justify-start gap-1 flex-1"> | |
| <ChevronRightIcon | |
| className={classNames("w-5", { | |
| "rotate-90": open === i, | |
| })} | |
| /> | |
| <p>Embed {i + 1}</p> | |
| </div> | |
| <div | |
| className="flex items-center justify-end gap-2" | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }} | |
| > | |
| <TrashIcon | |
| className="w-5 hover:text-danger" | |
| onClick={() => onDelete(i)} | |
| /> | |
| </div> | |
| </div> | |
| {open === i && ( | |
| <div className="px-4 pb-4 grid grid-cols-2 gap-2"> | |
| <Author | |
| author={embed?.author} | |
| setAuthor={(author) => setEmbed({ ...embed, author }, i)} | |
| /> | |
| <Body | |
| embed={embed} | |
| setEmbed={(embed) => setEmbed({ ...embed }, i)} | |
| /> | |
| <Fields | |
| fields={embed?.fields} | |
| setFields={(fields) => setEmbed({ ...embed, fields }, i)} | |
| /> | |
| <Images | |
| embed={embed} | |
| setEmbed={(newEmbed) => setEmbed({ ...newEmbed }, i)} | |
| /> | |
| <Footer | |
| embed={embed} | |
| setEmbed={(newEmbed) => setEmbed({ ...newEmbed }, i)} | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| {embeds?.length > 1 && ( | |
| <div className="flex items-center justify-center"> | |
| <FontAwesomeIcon | |
| icon={faGripVertical} | |
| className="w-8 text-dark-100" | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| <div className="w-full flex gap-3"> | |
| <button | |
| className="bg-transparent flex items-center gap-2 border-2 border-dashed border-darkGreen group rounded-md text-darkGreen hover:border-solid bg-opacity-10 hover:text-white transition-all duration-200 px-4 py-3 text-sm font-semibold relative z-1" | |
| onClick={() => { | |
| setEmbeds([ | |
| ...(embeds ?? []), | |
| { | |
| color: "#ff00ff", | |
| }, | |
| ]); | |
| }} | |
| > | |
| <PlusIcon className="w-4" /> | |
| Add Embed | |
| <div className="absolute left-0 h-full top-0 bg-darkGreen z-[-1] group-hover:w-full w-0 transition-all duration-200" /> | |
| </button> | |
| <button | |
| className="bg-transparent flex items-center gap-2 border-2 border-dashed border-danger group rounded-md text-danger hover:border-solid bg-opacity-10 hover:text-white transition-all duration-200 px-4 py-3 text-sm font-semibold relative z-1" | |
| onClick={() => setEmbeds([])} | |
| > | |
| <TrashIcon className="w-4" /> | |
| Clear embeds | |
| <div className="absolute left-0 h-full top-0 bg-danger z-[-1] group-hover:w-full w-0 transition-all duration-200" /> | |
| </button> | |
| </div> | |
| </> | |
| ); | |
| }; | |
| const Buttons = ({ | |
| buttons, | |
| setButtons, | |
| }: { | |
| buttons: any[]; | |
| setButtons: (e: any) => void; | |
| }) => { | |
| const [open, setOpen] = useState(0); | |
| const onDelete = (i: number) => { | |
| const newButtons = buttons ?? []; | |
| newButtons.splice(i, 1); | |
| setButtons(newButtons); | |
| }; | |
| const setButton = (button: any, index: number) => { | |
| const newButtons = buttons ?? []; | |
| newButtons[index] = button; | |
| setButtons(newButtons); | |
| }; | |
| useUpdateEffect(() => setOpen(buttons?.length - 1), [buttons]); | |
| return ( | |
| <> | |
| {buttons?.map((button: any, i: number) => ( | |
| <div key={i} className="flex"> | |
| <div className="bg-[#2f3136] rounded-r-lg w-full"> | |
| <div | |
| className={classNames( | |
| "px-4 pt-4 pb-2 flex justify-between items-center text-white font-semibold cursor-pointer", | |
| { | |
| "!pb-4": open !== i, | |
| } | |
| )} | |
| onClick={() => setOpen(open === i ? -1 : i)} | |
| > | |
| <div className="flex items-center justify-start gap-1 flex-1"> | |
| <ChevronRightIcon | |
| className={classNames("w-5", { | |
| "rotate-90": open === i, | |
| })} | |
| /> | |
| <p>Button {i + 1}</p> | |
| </div> | |
| <div | |
| className="flex items-center justify-end gap-2" | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }} | |
| > | |
| <TrashIcon | |
| className="w-5 hover:text-danger" | |
| onClick={() => onDelete(i)} | |
| /> | |
| </div> | |
| </div> | |
| {open === i && ( | |
| <div className="px-4 pb-4 grid grid-cols-2 gap-2"> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5 !text-xs"> | |
| Title | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {button?.label?.length ?? 0}/80 | |
| </span> | |
| </Label> | |
| <Input | |
| value={button?.label} | |
| placeholder="Label" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(label) => setButton({ ...button, label }, i)} | |
| /> | |
| </div> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5 !text-xs">URL</Label> | |
| <Input | |
| value={button?.url} | |
| placeholder="Label" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(url) => setButton({ ...button, url }, i)} | |
| /> | |
| </div> | |
| <div className="col-span-2 grid grid-cols-2 gap-4 mt-1"> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Native Emoji</Label> | |
| <Input | |
| value={button?.emoji?.name} | |
| placeholder="Native Emoji" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(name) => | |
| setButton( | |
| { | |
| ...button, | |
| emoji: { ...button?.emoji?.button, name }, | |
| }, | |
| i | |
| ) | |
| } | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Custom Emoji</Label> | |
| <Input | |
| value={button?.emoji?.id} | |
| placeholder="Custom Emoji" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(id) => | |
| setButton( | |
| { | |
| ...button, | |
| emoji: { ...button?.emoji?.button, id }, | |
| }, | |
| i | |
| ) | |
| } | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {buttons?.length > 1 && ( | |
| <div className="flex items-center justify-center"> | |
| <FontAwesomeIcon | |
| icon={faGripVertical} | |
| className="w-8 text-dark-100" | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| <div className="w-full flex gap-3"> | |
| <button | |
| className="bg-transparent flex items-center gap-2 border-2 border-dashed border-darkGreen group rounded-md text-darkGreen hover:border-solid bg-opacity-10 hover:text-white transition-all duration-200 px-4 py-3 text-sm font-semibold relative z-1" | |
| onClick={() => { | |
| setButtons([ | |
| ...(buttons ?? []), | |
| { | |
| label: "New Button", | |
| type: 2, | |
| style: 5, | |
| }, | |
| ]); | |
| }} | |
| > | |
| <PlusIcon className="w-4" /> | |
| Add button | |
| <div className="absolute left-0 h-full top-0 bg-darkGreen z-[-1] group-hover:w-full w-0 transition-all duration-200" /> | |
| </button> | |
| <button | |
| className="bg-transparent flex items-center gap-2 border-2 border-dashed border-danger group rounded-md text-danger hover:border-solid bg-opacity-10 hover:text-white transition-all duration-200 px-4 py-3 text-sm font-semibold relative z-1" | |
| onClick={() => setButtons([])} | |
| > | |
| <TrashIcon className="w-4" /> | |
| Clear buttons | |
| <div className="absolute left-0 h-full top-0 bg-danger z-[-1] group-hover:w-full w-0 transition-all duration-200" /> | |
| </button> | |
| </div> | |
| </> | |
| ); | |
| }; | |
| const Author = ({ | |
| author, | |
| setAuthor, | |
| }: { | |
| author: any; | |
| setAuthor: (a: any) => void; | |
| }) => { | |
| const [open, setOpen] = useState(true); | |
| return ( | |
| <div className="col-span-2"> | |
| <div | |
| className="text-white text-sm font-semibold tracking-wider flex items-center justify-start gap-1 cursor-pointer hover:bg-dark-400 px-2 py-1.5 rounded" | |
| onClick={() => setOpen(!open)} | |
| > | |
| <ChevronRightIcon | |
| className={classNames("w-5", { | |
| "rotate-90": open, | |
| })} | |
| /> | |
| Author | |
| </div> | |
| {open && ( | |
| <div className="grid grid-cols-2 gap-4 mt-2 pl-3"> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5 !text-xs"> | |
| Author | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {author?.name?.length ?? 0}/256 | |
| </span> | |
| </Label> | |
| <Input | |
| value={author?.name} | |
| placeholder="Author name" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(name) => setAuthor({ ...author, name })} | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Author URL</Label> | |
| <Input | |
| value={author?.url} | |
| placeholder="Author URL" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(url) => setAuthor({ ...author, url })} | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Author Icon URL</Label> | |
| <Input | |
| value={author?.icon_url} | |
| placeholder="Author Icon URL" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(icon_url) => setAuthor({ ...author, icon_url })} | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const Body = ({ | |
| embed, | |
| setEmbed, | |
| }: { | |
| embed: any; | |
| setEmbed: (a: any) => void; | |
| }) => { | |
| const [open, setOpen] = useState(true); | |
| return ( | |
| <div className="col-span-2"> | |
| <div | |
| className="text-white text-sm font-semibold tracking-wider flex items-center justify-start gap-1 cursor-pointer hover:bg-dark-400 px-2 py-1.5 rounded" | |
| onClick={() => setOpen(!open)} | |
| > | |
| <ChevronRightIcon | |
| className={classNames("w-5", { | |
| "rotate-90": open, | |
| })} | |
| /> | |
| Body | |
| </div> | |
| {open && ( | |
| <div className="grid grid-cols-2 gap-x-4 gap-y-3 mt-2 pl-3"> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5 !text-xs"> | |
| Title | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {embed?.title?.length ?? 0}/256 | |
| </span> | |
| </Label> | |
| <Input | |
| value={embed?.title} | |
| placeholder="Title" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(title) => setEmbed({ ...embed, title })} | |
| /> | |
| </div> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5 !text-xs"> | |
| Description{" "} | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {embed?.description?.length ?? 0}/4096 | |
| </span> | |
| </Label> | |
| <Input | |
| type="textarea" | |
| value={embed.description} | |
| placeholder="Content" | |
| className="bg-dark-600 bg-opacity-80 rounded-md p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(description) => setEmbed({ ...embed, description })} | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Title URL</Label> | |
| <Input | |
| value={embed?.url} | |
| placeholder="Title URL" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(url) => setEmbed({ ...embed, url })} | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Embed Color</Label> | |
| <div className="flex gap-3"> | |
| <Input | |
| value={embed?.color} | |
| placeholder="Embed color" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(color) => setEmbed({ ...embed, color })} | |
| /> | |
| <ColorPicker | |
| data={embed} | |
| full | |
| className="h-[42px] w-[44px] min-w-[44px] rounded" | |
| value={embed?.color} | |
| gradients={false} | |
| onChange={(c: any, datas) => { | |
| setEmbed({ ...embed, color: rgbaToHex(c) }); | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const Images = ({ | |
| embed, | |
| setEmbed, | |
| }: { | |
| embed: any; | |
| setEmbed: (a: any) => void; | |
| }) => { | |
| const [open, setOpen] = useState(true); | |
| return ( | |
| <div className="col-span-2"> | |
| <div | |
| className="text-white text-sm font-semibold tracking-wider flex items-center justify-start gap-1 cursor-pointer hover:bg-dark-400 px-2 py-1.5 rounded" | |
| onClick={() => setOpen(!open)} | |
| > | |
| <ChevronRightIcon | |
| className={classNames("w-5", { | |
| "rotate-90": open, | |
| })} | |
| /> | |
| Images | |
| </div> | |
| {open && ( | |
| <div className="grid grid-cols-2 gap-x-4 gap-y-3 mt-2 pl-3"> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Image URL</Label> | |
| <Input | |
| value={embed?.image?.url} | |
| placeholder="Image URL" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(url) => setEmbed({ ...embed, image: { url } })} | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Thumbnail URL</Label> | |
| <Input | |
| value={embed?.thumbnail?.url} | |
| placeholder="Thumbnail URL" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(url) => setEmbed({ ...embed, thumbnail: { url } })} | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const Fields = ({ | |
| fields, | |
| setFields, | |
| }: { | |
| fields: any; | |
| setFields: (a: any) => void; | |
| }) => { | |
| const [open, setOpen] = useState(true); | |
| return ( | |
| <div className="col-span-2"> | |
| <div | |
| className="text-white text-sm font-semibold tracking-wider flex items-end justify-start gap-1 cursor-pointer hover:bg-dark-400 px-2 py-1.5 rounded" | |
| onClick={() => setOpen(!open)} | |
| > | |
| <ChevronRightIcon | |
| className={classNames("w-5", { | |
| "rotate-90": open, | |
| })} | |
| /> | |
| Fields | |
| <span className="opacity-50 text-xs">{fields?.length ?? 0}/25</span> | |
| </div> | |
| {open && ( | |
| <div className="grid grid-cols-2 gap-4 mt-2 pl-3"> | |
| {fields?.map((field: any, i: number) => ( | |
| <Field | |
| key={i} | |
| index={i} | |
| field={field} | |
| setField={(newField) => { | |
| const newFields = fields ?? []; | |
| newFields[i] = newField; | |
| setFields(newFields); | |
| }} | |
| onDelete={() => { | |
| const newFields = fields ?? []; | |
| newFields.splice(i, 1); | |
| setFields(newFields); | |
| }} | |
| /> | |
| ))} | |
| <div className="w-full flex gap-3 col-span-2"> | |
| <button | |
| className="bg-transparent flex items-center gap-1 border-2 border-dashed border-darkGreen group rounded-md text-darkGreen hover:border-solid bg-opacity-10 hover:text-white transition-all duration-200 px-2.5 py-1.5 text-[13px] font-semibold relative z-1" | |
| onClick={() => { | |
| setFields([ | |
| ...(fields ?? []), | |
| { | |
| name: "Name", | |
| value: "Value", | |
| inline: true, | |
| }, | |
| ]); | |
| }} | |
| > | |
| <PlusIcon className="w-3" /> | |
| Add Field | |
| <div className="absolute left-0 h-full top-0 bg-darkGreen z-[-1] group-hover:w-full w-0 transition-all duration-200" /> | |
| </button> | |
| <button | |
| className="bg-transparent flex items-center gap-1 border-2 border-dashed border-danger group rounded-md text-danger hover:border-solid bg-opacity-10 hover:text-white transition-all duration-200 px-2.5 py-1.5 text-[13px] font-semibold relative z-1" | |
| onClick={() => setFields([])} | |
| > | |
| <TrashIcon className="w-3" /> | |
| Clear Fields | |
| <div className="absolute left-0 h-full top-0 bg-danger z-[-1] group-hover:w-full w-0 transition-all duration-200" /> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const Field = ({ | |
| field, | |
| index, | |
| setField, | |
| onDelete, | |
| }: { | |
| field: any; | |
| index: number; | |
| setField: (e: any) => void; | |
| onDelete: () => void; | |
| }) => { | |
| const [open, setOpen] = useState(true); | |
| return ( | |
| <div className="border-2 border-dashed border-dark-200 rounded-lg col-span-2 grid grid-cols-2 gap-4 p-3"> | |
| <div | |
| className="col-span-2 flex items-center justify-between gap-1 flex-1 text-sm text-white font-semibold cursor-pointer w-full" | |
| onClick={() => setOpen(!open)} | |
| > | |
| <div className="flex items-center justify-start gap-1"> | |
| <ChevronRightIcon | |
| className={classNames("w-4", { | |
| "rotate-90": open, | |
| })} | |
| /> | |
| <p>Field #{index + 1}</p> | |
| </div> | |
| <div | |
| className="flex items-center justify-end gap-2" | |
| onClick={(e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }} | |
| > | |
| <TrashIcon className="w-4 hover:text-danger" onClick={onDelete} /> | |
| </div> | |
| </div> | |
| {open && ( | |
| <> | |
| <div className="col-span-2 flex items-center justify-start gap-4"> | |
| <div className="w-full"> | |
| <Label className="mb-2.5 !text-xs"> | |
| Title | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {field?.name?.length ?? 0}/256 | |
| </span> | |
| </Label> | |
| <Input | |
| value={field?.name} | |
| placeholder="Field Name" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(name) => setField({ ...field, name })} | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Inline</Label> | |
| <Switch | |
| value={field?.inline} | |
| onChange={() => setField({ ...field, inline: !field?.inline })} | |
| /> | |
| </div> | |
| </div> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5 !text-xs"> | |
| Field value{" "} | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {field?.value?.length ?? 0}/1024 | |
| </span> | |
| </Label> | |
| <Input | |
| type="textarea" | |
| value={field.value} | |
| placeholder="Content" | |
| className="bg-dark-600 bg-opacity-80 rounded-md p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(value) => setField({ ...field, value })} | |
| /> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const Footer = ({ | |
| embed, | |
| setEmbed, | |
| }: { | |
| embed: any; | |
| setEmbed: (a: any) => void; | |
| }) => { | |
| const [open, setOpen] = useState(true); | |
| return ( | |
| <div className="col-span-2"> | |
| <div | |
| className="text-white text-sm font-semibold tracking-wider flex items-center justify-start gap-1 cursor-pointer hover:bg-dark-400 px-2 py-1.5 rounded" | |
| onClick={() => setOpen(!open)} | |
| > | |
| <ChevronRightIcon | |
| className={classNames("w-5", { | |
| "rotate-90": open, | |
| })} | |
| /> | |
| Body | |
| </div> | |
| {open && ( | |
| <div className="grid grid-cols-2 gap-x-4 gap-y-3 mt-2 pl-3"> | |
| <div className="col-span-2"> | |
| <Label className="mb-2.5 !text-xs"> | |
| Footer Text | |
| <span className="font-sans font-regular opacity-50 text-xs ml-1"> | |
| {embed?.footer?.name ?? 0}/256 | |
| </span> | |
| </Label> | |
| <Input | |
| value={embed?.footer?.text} | |
| placeholder="Footer Text" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(text) => | |
| setEmbed({ ...embed, footer: { ...embed?.footer, text } }) | |
| } | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Footer Icon Url</Label> | |
| <Input | |
| value={embed?.footer?.icon_url} | |
| placeholder="Footer Icon URL" | |
| className="bg-dark-600 bg-opacity-80 rounded-md p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(icon_url) => | |
| setEmbed({ ...embed, footer: { ...embed?.footer, icon_url } }) | |
| } | |
| /> | |
| </div> | |
| <div> | |
| <Label className="mb-2.5 !text-xs">Timestamp</Label> | |
| <Input | |
| value={embed?.timestamp} | |
| placeholder="Timestamp" | |
| className="bg-dark-600 bg-opacity-80 rounded p-3 text-sm text-white placeholder-dark-200 focus:ring-[3px] ring-white ring-opacity-50" | |
| onChange={(timestamp) => | |
| setEmbed({ ...embed, footer: { ...embed?.footer, timestamp } }) | |
| } | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |