The weeb for the next gen discord boat - Wamellow wamellow.com
bot discord
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

add bot style customization

shi.gg 9b3a89d7 eeeab334

verified
+432 -13
+1 -1
app/(dynamic-assets)/favicon.ico/route.ts
··· 5 5 export const revalidate = 691200; // 8 days 6 6 7 7 export async function GET() { 8 - const user = await getUser(process.env.CLIENT_ID as string); 8 + const user = await getUser(process.env.NEXT_PUBLIC_CLIENT_ID as string); 9 9 10 10 const avatar = await fetch(user?.avatar 11 11 ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp?size=64`
+1 -1
app/(dynamic-assets)/waya-v3.webp/route.ts
··· 3 3 export const revalidate = 691200; // 8 days 4 4 5 5 export async function GET() { 6 - const user = await getUser(process.env.CLIENT_ID as string); 6 + const user = await getUser(process.env.NEXT_PUBLIC_CLIENT_ID as string); 7 7 8 8 const avatar = await fetch(user?.avatar 9 9 ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp?size=256`
+1 -1
app/(home)/page.tsx
··· 279 279 <div className="bg-[#313338] h-0.5 w-full sm:w-0.5 sm:h-32 md:h-0.5 md:w-full lg:w-0.5 lg:h-32 rounded-full ml-2" /> 280 280 281 281 <DiscordMessage {...messageProps("tts voice")}> 282 - <DiscordMarkdown mode={"DARK"} text="Now talking..." /> 282 + Now talking... 283 283 </DiscordMessage> 284 284 </div> 285 285
+1 -1
app/(home)/ratings.component.tsx
··· 11 11 return ( 12 12 <Link 13 13 className="flex gap-2 items-center w-fit !h-6" 14 - href={`https://top.gg/bot/${process.env.CLIENT_ID}`} 14 + href={`https://top.gg/bot/${process.env.NEXT_PUBLIC_CLIENT_ID}`} 15 15 target="_blank" 16 16 title={`Average review score of ${reviews.averageScore}/5 based on ${reviews.count} reviews`} 17 17 >
+17 -4
app/dashboard/[guildId]/notifications/style.component.tsx
··· 1 1 "use client"; 2 2 import Link from "next/link"; 3 - import { useCallback, useMemo, useRef, useState } from "react"; 3 + import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 4 4 import { HiOutlineUpload, HiPencil, HiSparkles, HiX } from "react-icons/hi"; 5 5 6 6 import { guildStore } from "@/common/guilds"; 7 + import { DiscordMarkdown } from "@/components/discord/markdown"; 7 8 import DiscordMessage from "@/components/discord/message"; 8 9 import DumbTextInput from "@/components/inputs/dumb-text-input"; 9 10 import Modal from "@/components/modal"; ··· 31 32 const [open, setOpen] = useState(false); 32 33 33 34 return (<> 34 - <div className="w-full relative overflow-hidden rounded-lg border border-border group p-px mt-5"> 35 + <div className="w-full relative overflow-hidden rounded-xl border border-border group p-px mb-5"> 35 36 <span className="absolute inset-[-1000%] animate-[spin_5s_linear_infinite_reverse] bg-[conic-gradient(from_90deg_at_0%_50%,#8b5cf6_50%,var(--wamellow-rgb)_100%)]" /> 36 37 37 - <div className="backdrop-blur-3xl backdrop-brightness-[25%] rounded-[6px] p-5 md:py-8 md:pl-10 flex flex-col md:flex-row gap-5 md:gap-0"> 38 + <div className="backdrop-blur-3xl backdrop-brightness-[25%] rounded-[10px] p-5 md:py-8 md:pl-10 flex flex-col md:flex-row gap-5 md:gap-0"> 38 39 <div className="flex gap-6 items-center"> 39 40 <UserAvatar 40 41 alt={premium && item.username ? item.username : "Wamellow"} ··· 111 112 bot: true 112 113 }} 113 114 > 114 - Woooooooo! 115 + {username === "DarkViperAU" 116 + ? <DiscordMarkdown mode={"DARK"} text="[youtube.com/shorts/3zTsuUCGnN8](#)" /> 117 + : "Woooooooo!" 118 + } 115 119 </DiscordMessage> 116 120 </div> 117 121 ); ··· 154 158 const [avatar, setAvatar] = useState<ArrayBuffer | string | null>(avatarUrl); 155 159 const [error, setError] = useState<string | null>(null); 156 160 161 + useEffect( 162 + () => { 163 + if (!isOpen) return; 164 + setAvatar(avatarUrl); 165 + }, 166 + [isOpen, avatarUrl] 167 + ); 168 + 157 169 const renderable = useMemo( 158 170 () => !avatar || typeof avatar === "string" 159 171 ? avatar || "/waya-v3.webp" ··· 194 206 placeholder="DarkViperAU" 195 207 value={name} 196 208 setValue={setName} 209 + max={32} 197 210 /> 198 211 199 212 <input
+3
app/dashboard/[guildId]/page.tsx
··· 8 8 import { Section } from "@/components/section"; 9 9 10 10 import { OverviewLink } from "../../../components/overview-link"; 11 + import { BotStyle } from "./style.component"; 11 12 import { TTSSettings } from "./tts.component"; 12 13 import FollowUpdates from "./updates.component"; 13 14 ··· 22 23 url={`/leaderboard/${params.guildId}`} 23 24 icon={<HiChartBar />} 24 25 /> 26 + 27 + <BotStyle /> 25 28 26 29 <FollowUpdates /> 27 30
+383
app/dashboard/[guildId]/style.component.tsx
··· 1 + "use client"; 2 + import Image from "next/image"; 3 + import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 4 + import { HiOutlineUpload, HiPencil, HiX } from "react-icons/hi"; 5 + 6 + import { guildStore } from "@/common/guilds"; 7 + import DiscordMessage from "@/components/discord/message"; 8 + import DumbTextInput from "@/components/inputs/dumb-text-input"; 9 + import Modal from "@/components/modal"; 10 + import { Section } from "@/components/section"; 11 + import { UserAvatar } from "@/components/ui/avatar"; 12 + import { Button } from "@/components/ui/button"; 13 + import { type ApiError, type ApiV1GuildsGetResponse, type ApiV1GuildsStylePatchResponse, GuildFlags } from "@/typings"; 14 + import { State } from "@/utils/captcha"; 15 + import { cn } from "@/utils/cn"; 16 + 17 + const ALLOWED_FILE_TYPES = ["image/jpg", "image/jpeg", "image/png", "image/webp", "image/gif", "image/apng"]; 18 + const MAX_FILE_SIZE = 8 * 1024 * 1024; 19 + 20 + export function BotStyle() { 21 + const guild = guildStore((g) => g!); 22 + const premium = ((guild?.flags || 0) & GuildFlags.Premium) === GuildFlags.Premium; 23 + 24 + const [open, setOpen] = useState(false); 25 + 26 + const avatarUrl = premium && guild.style.avatar ? `https://cdn.discordapp.com/guilds/${guild.id}/users/${process.env.NEXT_PUBLIC_CLIENT_ID}/avatars/${guild.style.avatar}?size=256` : "/waya-v3.webp"; 27 + const bannerUrl = premium && guild.style.banner ? `https://cdn.discordapp.com/guilds/${guild.id}/users/${process.env.NEXT_PUBLIC_CLIENT_ID}/banners/${guild.style.banner}?size=1024` : null; 28 + 29 + return (<> 30 + <div className="w-full relative overflow-hidden rounded-xl border border-border group p-px mb-5"> 31 + <span className="absolute inset-[-1000%] animate-[spin_5s_linear_infinite_reverse] bg-[conic-gradient(from_90deg_at_0%_50%,#8b5cf6_50%,var(--wamellow-rgb)_100%)]" /> 32 + 33 + <div className="backdrop-blur-3xl backdrop-brightness-[25%] rounded-[10px] p-5 md:py-8 md:pl-10 flex flex-col md:flex-row gap-5 md:gap-0"> 34 + <div className="flex gap-6 items-center"> 35 + <UserAvatar 36 + alt={premium && guild.style.username ? guild.style.username : "Wamellow"} 37 + className="size-24" 38 + src={avatarUrl} 39 + /> 40 + 41 + <div className="space-y-2"> 42 + <span className="text-3xl font-medium text-primary-foreground"> 43 + {guild.style.username ? guild.style.username : "Wamellow"} 44 + </span> 45 + <div className="flex"> 46 + <Button onClick={() => setOpen(true)}> 47 + <HiPencil /> 48 + Change Style 49 + </Button> 50 + {(guild.style.username || guild.style.avatar || guild.style.banner) && ( 51 + <DeleteStyleButton 52 + guildId={guild.id} 53 + onDelete={() => { 54 + guildStore.setState((g) => { 55 + if (!g) return g; 56 + return { ...g, style: { username: null, avatar: null, banner: null } }; 57 + }); 58 + }} 59 + /> 60 + )} 61 + </div> 62 + </div> 63 + </div> 64 + 65 + <ExampleMessages /> 66 + </div> 67 + </div> 68 + 69 + <ChangeStyleModal 70 + guildId={guild.id} 71 + 72 + username={guild.style.username} 73 + avatarUrl={avatarUrl} 74 + bannerUrl={bannerUrl} 75 + 76 + isOpen={open} 77 + onClose={() => setOpen(false)} 78 + onEdit={(style) => { 79 + guildStore.setState((g) => { 80 + if (!g) return g; 81 + return { ...g, style: { ...style } }; 82 + }); 83 + }} 84 + /> 85 + </>); 86 + } 87 + 88 + function ExampleMessages() { 89 + return ( 90 + <div className="w-full relative"> 91 + <ExampleMessage className="top-1 md:-top-5 md:right-1 rotate-1 md:rotate-2 z-10" username="Kurzgesagt" avatarUrl="https://yt3.googleusercontent.com/ytc/AIdro_n1Ribd7LwdP_qKtqWL3ZDfIgv9M1d6g78VwpHGXVR2Ir4=s176-c-k-c0x00ffffff-no-rj-mo" /> 92 + <ExampleMessage className="md:-bottom-5 right-0 md:-rotate-1" username="DarkViperAU" avatarUrl="https://yt3.googleusercontent.com/ytc/AIdro_lpNK9jpdw9D63LuUYt3SLbFpIQ5yD4DV0D5mwPrCp7cEw=s176-c-k-c0x00ffffff-no-rj-mo" /> 93 + </div> 94 + ); 95 + } 96 + 97 + function ExampleMessage({ className, username, avatarUrl }: { className?: string; username?: string; avatarUrl?: string; }) { 98 + return ( 99 + <div className={cn("bg-discord-gray px-3 py-2 rounded-lg w-full md:max-w-sm relative md:absolute border border-wamellow shadow-lg", className)}> 100 + <DiscordMessage 101 + mode="DARK" 102 + commandUsed={{ 103 + name: "tts voice", 104 + username: "@mwlica", 105 + avatar: "/luna.webp", 106 + bot: false 107 + }} 108 + user={{ 109 + username: username || "Wamellow", 110 + avatar: avatarUrl || "/waya-v3.webp", 111 + bot: true 112 + }} 113 + > 114 + Woooooooo! 115 + </DiscordMessage> 116 + </div> 117 + ); 118 + } 119 + 120 + function isValidUsername(value: string | null): boolean { 121 + if (!value) return false; 122 + if (value.length < 2 || value.length > 32) return false; 123 + if (["everyone", "here"].includes(value.toLowerCase())) return false; 124 + if (/@|#|:|```|discord|clyde/i.test(value)) return false; 125 + return true; 126 + } 127 + 128 + interface Props { 129 + guildId: string; 130 + 131 + username: string | null; 132 + avatarUrl: string | null; 133 + bannerUrl: string | null; 134 + 135 + isOpen: boolean; 136 + onClose: () => void; 137 + onEdit: (opts: ApiV1GuildsGetResponse["style"]) => void; 138 + } 139 + 140 + export function ChangeStyleModal({ 141 + guildId, 142 + 143 + username, 144 + avatarUrl, 145 + bannerUrl, 146 + 147 + isOpen, 148 + onClose, 149 + onEdit 150 + }: Props) { 151 + const [name, setName] = useState(username); 152 + const [bio, setBio] = useState<string | null>(null); 153 + const [avatar, setAvatar] = useState<ArrayBuffer | string | null>(avatarUrl); 154 + const [banner, setBanner] = useState<ArrayBuffer | string | null>(bannerUrl); 155 + const [error, setError] = useState<string | null>(null); 156 + 157 + useEffect( 158 + () => { 159 + if (!isOpen) return; 160 + setAvatar(avatarUrl); 161 + setBanner(bannerUrl); 162 + }, 163 + [isOpen, avatarUrl, bannerUrl] 164 + ); 165 + 166 + const render = useCallback( 167 + (buf: ArrayBuffer | string | null) => !buf || typeof buf === "string" 168 + ? buf 169 + : URL.createObjectURL(new Blob([buf])), 170 + [] 171 + ); 172 + 173 + const renderableAvatar = useMemo(() => render(avatar), [avatar]); 174 + const renderableBanner = useMemo(() => render(banner), [banner]); 175 + 176 + return (<> 177 + <Modal<ApiV1GuildsStylePatchResponse> 178 + title="Edit Style" 179 + isOpen={isOpen} 180 + onClose={() => { 181 + onClose(); 182 + setError(null); 183 + }} 184 + onSubmit={() => { 185 + const valid = isValidUsername(name); 186 + if (!name || !valid) return new Error("Invalid name"); 187 + 188 + const formData = new FormData(); 189 + formData.append("json_payload", JSON.stringify({ username: name, bio: bio })); 190 + if (avatar && typeof avatar !== "string") formData.append("file[0]", new Blob([avatar]), "avatar"); 191 + if (banner && typeof banner !== "string") formData.append("file[1]", new Blob([banner]), "banner"); 192 + 193 + return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/style`, { 194 + method: "PATCH", 195 + credentials: "include", 196 + body: formData 197 + }); 198 + }} 199 + onSuccess={(style) => { 200 + onEdit(style); 201 + setError(null); 202 + }} 203 + isDisabled={!name || Boolean(error)} 204 + > 205 + <DumbTextInput 206 + name="Username" 207 + placeholder="DarkViperAU" 208 + value={name} 209 + setValue={setName} 210 + max={32} 211 + /> 212 + 213 + <DumbTextInput 214 + name="Bio" 215 + placeholder="gaming" 216 + value={bio} 217 + setValue={setBio} 218 + max={190} 219 + multiline 220 + /> 221 + 222 + <div className="space-y-2"> 223 + <FileUpload 224 + name="Avatar" 225 + onUpload={setAvatar} 226 + /> 227 + 228 + <FileUpload 229 + name="Banner" 230 + onUpload={setBanner} 231 + /> 232 + </div> 233 + 234 + <Section tight title="Preview"> 235 + <div className="bg-discord-gray p-3 mt-1 rounded-lg w-full"> 236 + <DiscordMessage 237 + mode="DARK" 238 + commandUsed={{ 239 + name: "tts voice", 240 + username: "@mwlica", 241 + avatar: "/luna.webp", 242 + bot: false 243 + }} 244 + user={{ 245 + username: name || "Wamellow", 246 + avatar: renderableAvatar || "/waya-v3.webp", 247 + bot: true 248 + }} 249 + > 250 + Now talking... 251 + </DiscordMessage> 252 + </div> 253 + 254 + {renderableBanner && ( 255 + <Image 256 + alt="Guild Banner" 257 + src={renderableBanner} 258 + className="w-full h-auto aspect-[599/251] object-cover rounded-lg mt-4" 259 + width={599} 260 + height={251} 261 + /> 262 + )} 263 + </Section> 264 + 265 + </Modal> 266 + </>); 267 + } 268 + 269 + function FileUpload({ 270 + name, 271 + onUpload 272 + }: { 273 + name?: string; 274 + onUpload: (buffer: ArrayBuffer | null) => void; 275 + }) { 276 + const ref = useRef<HTMLInputElement | null>(null); 277 + const [error, setError] = useState<string | null>(null); 278 + const [buf, setBuf] = useState<ArrayBuffer | null>(null); 279 + 280 + return (<> 281 + <input 282 + accept={ALLOWED_FILE_TYPES.join()} 283 + className="hidden" 284 + onChange={(e) => { 285 + setBuf(null); 286 + 287 + const file = e.target.files?.[0]; 288 + if (!file) return; 289 + 290 + if (!ALLOWED_FILE_TYPES.includes(file.type)) { 291 + setError(`File type must be one of ${ALLOWED_FILE_TYPES.join(", ")}`); 292 + return; 293 + } 294 + 295 + if (file.size > MAX_FILE_SIZE) { 296 + setError(`File size must be less than ${MAX_FILE_SIZE / 1024 / 1024}MiB`); 297 + return; 298 + } 299 + 300 + 301 + file.arrayBuffer().then((buffer) => { 302 + setBuf(buffer); 303 + onUpload(buffer); 304 + }); 305 + }} 306 + ref={ref} 307 + type="file" 308 + /> 309 + 310 + <div className="flex"> 311 + <Button onClick={() => ref.current?.click()}> 312 + <HiOutlineUpload /> 313 + Upload {name} 314 + </Button> 315 + {buf && ( 316 + <Button 317 + className="text-red-400" 318 + variant="link" 319 + onClick={() => { 320 + setBuf(null); 321 + onUpload(null); 322 + }} 323 + > 324 + <HiX /> 325 + Remove {name} 326 + </Button> 327 + )} 328 + </div> 329 + {error && <p className="text-red-500 text-sm mt-1">{error}</p>} 330 + </>); 331 + } 332 + 333 + function DeleteStyleButton({ 334 + guildId, 335 + onDelete 336 + }: { 337 + guildId: string; 338 + onDelete: () => void; 339 + }) { 340 + const [state, setState] = useState<State>(State.Idle); 341 + const [error, setError] = useState<string | null>(null); 342 + 343 + const del = useCallback( 344 + async () => { 345 + setState(State.Loading); 346 + 347 + const response = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/style`, { 348 + method: "DELETE", 349 + credentials: "include" 350 + }); 351 + 352 + if (response.ok) { 353 + onDelete(); 354 + setState(State.Success); 355 + return; 356 + } 357 + 358 + setState(State.Idle); 359 + 360 + const res = await response.json() as ApiError | null; 361 + if (res && "message" in res) { 362 + setError(res.message); 363 + return; 364 + } 365 + 366 + setError("An unknown error occurred"); 367 + }, 368 + [guildId, onDelete] 369 + ); 370 + 371 + return (<> 372 + <Button 373 + className="text-red-400" 374 + variant="link" 375 + onClick={del} 376 + icon={<HiX />} 377 + loading={state === State.Loading} 378 + > 379 + Delete 380 + </Button> 381 + {error && <p className="text-red-400">{error}</p>} 382 + </>); 383 + }
+2 -2
app/login/open-graph/page.tsx
··· 18 18 openGraph: { 19 19 title, 20 20 description, 21 - url: `https://discord.com/api/v9/applications/${process.env.CLIENT_ID}/og.png`, 21 + url: `https://discord.com/api/v9/applications/${process.env.NEXT_PUBLIC_CLIENT_ID}/og.png`, 22 22 type: "website", 23 23 images: { 24 24 url, ··· 32 32 title, 33 33 description, 34 34 images: { 35 - url: `https://discord.com/api/v9/applications/${process.env.CLIENT_ID}/og.png`, 35 + url: `https://discord.com/api/v9/applications/${process.env.NEXT_PUBLIC_CLIENT_ID}/og.png`, 36 36 alt: title 37 37 } 38 38 }
+1 -1
app/login/route.ts
··· 86 86 function generateOauthUrl(invite: boolean, redirectUrl: string | undefined, guildId: string | null) { 87 87 const params = new URLSearchParams(); 88 88 89 - params.append("client_id", process.env.CLIENT_ID as string); 89 + params.append("client_id", process.env.NEXT_PUBLIC_CLIENT_ID as string); 90 90 params.append("redirect_uri", getCanonicalUrl("login")); 91 91 params.append("permissions", permissions.reduce((acc, cur) => acc + Number(cur), 0).toString()); 92 92 params.append("prompt", "none");
+3 -1
components/inputs/dumb-text-input.tsx
··· 14 14 max?: number; 15 15 thin?: boolean; 16 16 type?: string; 17 + multiline?: boolean; 17 18 18 19 dataName?: string; 19 20 } ··· 28 29 max = 256, 29 30 thin, 30 31 type, 32 + multiline, 31 33 dataName 32 34 }: Props) { 33 35 const className = cn( ··· 81 83 /> 82 84 </div> 83 85 : 84 - max > 300 ? 86 + (max > 300 || multiline) ? 85 87 <textarea 86 88 className={className} 87 89 placeholder={placeholder}
+1 -1
lib/topgg.ts
··· 6 6 } 7 7 8 8 export async function getReviews() { 9 - const res = await fetch(`https://top.gg/api/bots/${process.env.CLIENT_ID}`, { 9 + const res = await fetch(`https://top.gg/api/bots/${process.env.NEXT_PUBLIC_CLIENT_ID}`, { 10 10 headers: { 11 11 authorization: process.env.TOPGG_TOKEN! 12 12 },
+6
next.config.js
··· 30 30 port: "", 31 31 pathname: "/banners/**" 32 32 }, 33 + { 34 + protocol: "https", 35 + hostname: "cdn.discordapp.com", 36 + port: "", 37 + pathname: "/guilds/**" 38 + }, 33 39 34 40 { 35 41 protocol: "https",
+12
typings.ts
··· 51 51 }; 52 52 embedLinks: boolean; 53 53 flags: number; 54 + style: { 55 + username: string | null; 56 + avatar: string | null; 57 + banner: string | null; 58 + } 59 + } 60 + 61 + export interface ApiV1GuildsStylePatchResponse { 62 + username: string | null; 63 + avatar: string | null; 64 + banner: string | null; 65 + bio: string | null; 54 66 } 55 67 56 68 export interface ApiV1GuildsTopmembersGetResponse {