The weeb for the next gen discord boat - Wamellow
wamellow.com
bot
discord
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}