The weeb for the next gen discord boat - Wamellow wamellow.com
bot discord
at master 12 kB view raw
1import type { GuildEmbed } from "@/typings"; 2import { cn } from "@/utils/cn"; 3import React, { useState } from "react"; 4import { BiMoon, BiSun } from "react-icons/bi"; 5import { FaFloppyDisk } from "react-icons/fa6"; 6import { HiChevronDown, HiChevronUp } from "react-icons/hi"; 7 8import { DiscordMarkdown } from "./discord/markdown"; 9import DiscordMessage from "./discord/message"; 10import DiscordMessageEmbed from "./discord/message-embed"; 11import DumbColorInput from "./inputs/dumb-color-input"; 12import DumbTextInput from "./inputs/dumb-text-input"; 13import { Button } from "./ui/button"; 14 15enum State { 16 Idle = 0, 17 Loading = 1, 18 Success = 2 19} 20 21interface Props { 22 children?: React.ReactNode; 23 24 name: string; 25 url: string; 26 dataName: string; 27 28 defaultMessage?: { content?: string | null; embed?: GuildEmbed; }; 29 isCollapseable?: boolean; 30 31 messageAttachmentComponent?: React.ReactNode; 32 showMessageAttachmentComponentInEmbed?: boolean; 33 34 user?: { 35 username: string; 36 avatar: string; 37 bot: boolean; 38 }; 39 40 disabled?: boolean; 41 onSave?: (state: { content: string; embed: GuildEmbed; }) => void; 42} 43 44export default function MessageCreatorEmbed({ 45 children, 46 47 name, 48 url, 49 dataName, 50 51 defaultMessage, 52 isCollapseable, 53 54 messageAttachmentComponent, 55 showMessageAttachmentComponentInEmbed, 56 57 user, 58 59 disabled, 60 onSave 61}: Props) { 62 const [state, setState] = useState<State>(State.Idle); 63 const [error, setError] = useState<string | null>(null); 64 65 const [content, setContent] = useState<string>(defaultMessage?.content || ""); 66 const [embed, setEmbed] = useState<string>(JSON.stringify(defaultMessage?.embed || {})); 67 const [embedfooter, setEmbedfooter] = useState<string>(JSON.stringify(defaultMessage?.embed?.footer || {})); 68 69 const [open, setOpen] = useState<boolean>(!isCollapseable); 70 const [mode, setMode] = useState<"DARK" | "LIGHT">("DARK"); 71 72 const modeToggle = ( 73 <div 74 className={cn( 75 mode === "DARK" ? "bg-wamellow-light" : "bg-wamellow-100-light", 76 "flex gap-1 text-neutral-400 rounded-md overflow-hidden" 77 )} 78 > 79 <button 80 onClick={() => setMode("DARK")} 81 className={cn( 82 "py-2 px-3 rounded-md", 83 mode === "DARK" ? "bg-wamellow" : "hover:bg-wamellow-100-alpha" 84 )} 85 > 86 <BiMoon className="h-5 w-5" /> 87 </button> 88 <button 89 onClick={() => setMode("LIGHT")} 90 className={cn( 91 "py-2 px-3 rounded-md", 92 mode === "LIGHT" ? "bg-wamellow-100" : "hover:bg-wamellow-alpha" 93 )} 94 > 95 <BiSun className="h-5 w-5" /> 96 </button> 97 </div> 98 ); 99 100 async function save() { 101 setError(null); 102 setState(State.Loading); 103 104 const body = { 105 content, 106 embed: Object.assign( 107 JSON.parse(embed), 108 embedfooter.length 109 ? { footer: JSON.parse(embedfooter) } 110 : undefined 111 ) 112 }; 113 114 if (!body.embed.footer.text) body.embed.footer = { text: null }; 115 116 const res = await fetch(`${process.env.NEXT_PUBLIC_API}${url}`, { 117 method: "PATCH", 118 credentials: "include", 119 headers: { 120 "Content-Type": "application/json" 121 }, 122 body: JSON.stringify(dataName.includes(".") 123 ? { [dataName.split(".")[0]]: { [dataName.split(".")[1]]: body } } 124 : { [dataName]: body } 125 ) 126 }) 127 .then((r) => r.json()) 128 .catch(() => null); 129 130 if (!res || "status" in res) { 131 setState(State.Idle); 132 setError( 133 "message" in res 134 ? res.message 135 : "Something went wrong while saving.." 136 ); 137 138 return; 139 } 140 141 if (onSave) onSave(body); 142 setState(State.Success); 143 setTimeout(() => setState(State.Idle), 1_000 * 8); 144 } 145 146 return ( 147 <div> 148 <div 149 className={cn( 150 "mt-8 mb-4 border-2 dark:border-wamellow border-wamellow-100 rounded-xl md:px-4 md:pb-4 px-2 py-2", 151 error && "outline-solid outline-red-500 outline-1" 152 )} 153 > 154 <div className="text-lg py-2 dark:text-neutral-700 text-neutral-300 font-medium px-2">{name}</div> 155 156 {isCollapseable && 157 <div className={cn("md:mx-2 mx-1", open ? "lg:mb-0 mb-2" : "mb-2")}> 158 <button 159 className="dark:bg-wamellow hover:dark:bg-wamellow-light bg-wamellow-100 hover:bg-wamellow-100-light duration-200 cursor-pointer rounded-md dark:text-neutral-400 text-neutral-600 flex items-center h-12 px-3 w-full" 160 onClick={() => setOpen(!open)} 161 > 162 {open ? 163 <> 164 <span>Collaps</span> 165 <HiChevronUp className="ml-auto h-4 w-4" /> 166 </> 167 : 168 <> 169 <span>Expand</span> 170 <HiChevronDown className="ml-auto h-4 w-4" /> 171 </> 172 } 173 </button> 174 </div> 175 } 176 177 {open && 178 <div className="md:m-1 relative"> 179 180 {children && 181 <div className={cn("mx-1", isCollapseable && "mt-6")}> 182 {children} 183 </div> 184 } 185 186 <div className="lg:flex gap-1"> 187 188 <div className="lg:w-3/6 m-1"> 189 190 <DumbTextInput placeholder="Content" value={content} setValue={setContent} max={2_000} disabled={disabled} /> 191 <DumbTextInput placeholder="Embed Title" value={embed} setValue={setEmbed} max={256} dataName="title" disabled={disabled} /> 192 <DumbTextInput placeholder="Embed Description" value={embed} setValue={setEmbed} max={4_096} dataName="description" disabled={disabled} /> 193 <div className="flex gap-2"> 194 <DumbColorInput placeholder="Embed Color" value={embed} setValue={setEmbed} dataName="color" disabled={disabled} /> 195 <DumbTextInput placeholder="Embed Thumbnail" value={embed} setValue={setEmbed} max={256} dataName="thumbnail" disabled={disabled} /> 196 </div> 197 <DumbTextInput placeholder="Embed Image" value={embed} setValue={setEmbed} max={256} dataName="image" disabled={disabled} /> 198 <div className="flex gap-2"> 199 <DumbTextInput placeholder="Embed Footer Icon" value={embedfooter} setValue={setEmbedfooter} max={256} dataName="icon_url" disabled={disabled} /> 200 <DumbTextInput placeholder="Embed Footer" value={embedfooter} setValue={setEmbedfooter} max={256} dataName="text" disabled={disabled} /> 201 </div> 202 203 <Button 204 className="mt-1 w-full" 205 onClick={() => save()} 206 icon={<FaFloppyDisk />} 207 disabled={disabled} 208 loading={state === State.Loading} 209 variant="secondary" 210 > 211 Save Changes 212 </Button> 213 214 </div> 215 216 <div className="md:hidden flex m-2 mt-4"> 217 218 <div className="flex items-center w-full"> 219 <span className="text-lg dark:text-neutral-300 text-neutral-700 font-medium">Color Theme</span> 220 221 <div className="ml-auto flex items-center"> 222 {modeToggle} 223 </div> 224 </div> 225 226 </div> 227 228 <div 229 className={cn( 230 "relative lg:w-3/6 lg:mt-2 m-1 md:mt-8 mt-4 min-h-full rounded-md p-4 break-all overflow-hidden max-w-full text-neutral-200", 231 mode === "DARK" ? "bg-discord-gray" : "bg-white" 232 )} 233 > 234 235 <div className="absolute z-10 top-2 right-2 hidden md:block"> 236 {modeToggle} 237 </div> 238 239 <DiscordMessage 240 mode={mode} 241 user={user || { 242 username: "Wamellow", 243 avatar: "/waya-v3.webp", 244 bot: true 245 }} 246 > 247 <DiscordMarkdown 248 mode={mode} 249 text={content || ""} 250 /> 251 252 <DiscordMessageEmbed 253 mode={mode} 254 title={JSON.parse(embed).title} 255 color={JSON.parse(embed).color} 256 thumbnail={JSON.parse(embed).thumbnail} 257 image={JSON.parse(embed).image} 258 footer={JSON.parse(embedfooter)} 259 > 260 {JSON.parse(embed).description && <DiscordMarkdown mode={mode} text={JSON.parse(embed).description} />} 261 {showMessageAttachmentComponentInEmbed && messageAttachmentComponent} 262 </DiscordMessageEmbed> 263 264 {!showMessageAttachmentComponentInEmbed && messageAttachmentComponent} 265 </DiscordMessage> 266 </div> 267 268 </div> 269 <div className="text-sm m-1 text-neutral-500"> 270 The preview might display things wrong* 271 </div> 272 273 </div>} 274 275 </div> 276 277 <div className="flex relative bottom-3"> 278 <div className="ml-auto mb-2"> 279 {error && <div className="ml-auto text-red-500 text-sm">{error}</div>} 280 {state === State.Success && <div className="ml-auto text-green-500 text-sm">Saved</div>} 281 </div> 282 </div> 283 284 </div> 285 ); 286}