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> | |
); | |
}; | |