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

Configure Feed

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

add leaderboard emoji cards

Luna c827030c 18eca834

+1200 -817
+24
app/globals.css
··· 135 135 } 136 136 } 137 137 138 + .shake { 139 + position: relative; 140 + animation: shake 60ms infinite alternate; 141 + } 142 + 143 + @keyframes shake { 144 + 0% { 145 + margin-right: 0px 146 + } 147 + 148 + 50% { 149 + transform: rotate(1deg); 150 + } 151 + 152 + 80% { 153 + transform: rotate(-1deg); 154 + } 155 + 156 + 100% { 157 + margin-right: 10px 158 + } 159 + 160 + } 161 + 138 162 button.subpixel-antialiased:not(.w-unit-10):not(.default):not(.bg-secondary):not(.button-primary):not(.button-blurple):not(.button-underline), 139 163 a[role="button"]:not(.w-unit-10):not(.default):not(.bg-secondary):not(.button-primary):not(.button-blurple):not(.button-underline) { 140 164 @apply py-2 px-4 duration-200 justify-center gap-2 items-center text-medium
+202 -153
app/leaderboard/[guildId]/member.component.tsx
··· 1 - import { Badge, Chip, CircularProgress } from "@nextui-org/react"; 2 - 3 - import Link from "next/link"; 4 - import { HiBadgeCheck } from "react-icons/hi"; 5 - import cn from "@/utils/cn"; 6 - import { cookies } from "next/headers"; 7 - import { intl } from "@/utils/numbers"; 8 - import ImageReduceMotion from "@/components/image-reduce-motion"; 9 - import DiscordAppBadge from "@/components/discord/app-badge"; 10 - import { ApiV1GuildsTopmembersGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 11 - import Icon from "./icon.component"; 12 - 13 - export default async function Member( 14 - { 15 - index, 16 - type, 17 - member, 18 - members, 19 - pagination 20 - }: { 21 - index: number, 22 - type: "messages" | "voiceminutes" | "invites", 23 - member: ApiV1GuildsTopmembersGetResponse, 24 - members: ApiV1GuildsTopmembersGetResponse[], 25 - pagination: ApiV1GuildsTopmembersPaginationGetResponse, 26 - } 27 - ) { 28 - 29 - async function publish() { 30 - "use server"; 31 - 32 - const cookieStore = cookies(); 33 - const currentCircular = cookieStore.get("lbc")?.value; 34 - 35 - cookieStore.set( 36 - "lbc", 37 - currentCircular !== "server" 38 - ? "server" 39 - : "next" 40 - ); 41 - } 42 - 43 - const cookieStore = cookies(); 44 - const currentCircular = cookieStore.get("lbc")?.value; 45 - 46 - return ( 47 - <div className="mb-4 rounded-xl p-3 flex items-center dark:bg-wamellow bg-wamellow-100 w-full overflow-hidden"> 48 - <Badge 49 - className={cn( 50 - "size-6 font-bold", 51 - (() => { 52 - if (index === 1) return "bg-[#ffe671] text-[#ff9e03] border-2 border-[#1c1b1f]"; 53 - if (index === 2) return "bg-[#c1e5fb] text-[#6093ba] border-2 border-[#1c1b1f]"; 54 - if (index === 3) return "bg-[#f8c396] text-[#c66e04] border-2 border-[#1c1b1f]"; 55 - return "bg-[#1c1b1f]"; 56 - })() 57 - )} 58 - showOutline={false} 59 - content={ 60 - <span className="px-[3px]"> 61 - {intl.format(index)} 62 - </span> 63 - } 64 - size="sm" 65 - placement="bottom-left" 66 - > 67 - <ImageReduceMotion 68 - alt={`${member.username}'s profile picture`} 69 - className="rounded-full h-12 w-12 mr-3" 70 - url={`https://cdn.discordapp.com/avatars/${member.id}/${member.avatar}`} 71 - size={128} 72 - /> 73 - </Badge> 74 - 75 - <div className="w-full max-w-[calc(100%-17rem)]"> 76 - <div className="flex items-center gap-2"> 77 - <span className="text-xl font-medium dark:text-neutral-200 text-neutral-800 truncate"> 78 - {member.globalName || member.username || "Unknown user"} 79 - </span> 80 - {member.bot && 81 - <DiscordAppBadge /> 82 - } 83 - {member.id === "821472922140803112" && 84 - <UserBadge>Developer</UserBadge> 85 - } 86 - {member.id === "845287163712372756" && 87 - <UserBadge>WOMEN</UserBadge> 88 - } 89 - </div> 90 - <div className="text-sm dark:text-neutral-300 text-neutral-700 truncate"> 91 - @{member.username} 92 - </div> 93 - </div> 94 - 95 - <div className="ml-auto flex text-xl font-medium dark:text-neutral-200 text-neutral-800"> 96 - <span className="mr-1 break-keep"> 97 - {type === "voiceminutes" 98 - ? member.activity?.formattedVoicetime 99 - : intl.format(member.activity?.[type || "messages"]) 100 - } 101 - </span> 102 - 103 - <Icon type={type} /> 104 - </div> 105 - 106 - <form action={publish}> 107 - <CircularProgress 108 - as="button" 109 - type="submit" 110 - className="ml-4" 111 - aria-label="progress" 112 - size="lg" 113 - color={ 114 - currentCircular === "next" 115 - ? "default" 116 - : "secondary" 117 - } 118 - classNames={{ 119 - svg: "drop-shadow-md" 120 - }} 121 - value={ 122 - currentCircular === "next" 123 - ? (member.activity[type] * 100) / (members[index - 1]?.activity[type] || 1) 124 - : (member.activity[type] * 100) / parseInt(pagination[type].total.toString()) 125 - || 100 126 - } 127 - showValueLabel={true} 128 - /> 129 - </form> 130 - 131 - </div> 132 - ); 133 - 134 - } 135 - 136 - function UserBadge({ 137 - children 138 - }: { 139 - children: React.ReactNode 140 - }) { 141 - return ( 142 - <Chip 143 - as={Link} 144 - color="secondary" 145 - href="/team?utm_source=wamellow.com&utm_medium=leaderboard" 146 - size="sm" 147 - startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />} 148 - target="_blank" 149 - variant="flat" 150 - > 151 - <span className="font-bold">{children}</span> 152 - </Chip> 153 - ); 1 + import { Badge, Chip, CircularProgress } from "@nextui-org/react"; 2 + 3 + import Link from "next/link"; 4 + import { HiBadgeCheck } from "react-icons/hi"; 5 + import cn from "@/utils/cn"; 6 + import { cookies } from "next/headers"; 7 + import { intl } from "@/utils/numbers"; 8 + import ImageReduceMotion from "@/components/image-reduce-motion"; 9 + import DiscordAppBadge from "@/components/discord/app-badge"; 10 + import { ApiV1GuildsTopmembersGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 11 + import Icon from "./icon.component"; 12 + import getAverageColor from "@/utils/average-color"; 13 + import Image from "next/image"; 14 + 15 + export default async function Member( 16 + { 17 + index, 18 + type, 19 + member, 20 + members, 21 + pagination 22 + }: { 23 + index: number, 24 + type: "messages" | "voiceminutes" | "invites", 25 + member: ApiV1GuildsTopmembersGetResponse, 26 + members: ApiV1GuildsTopmembersGetResponse[], 27 + pagination: ApiV1GuildsTopmembersPaginationGetResponse, 28 + } 29 + ) { 30 + const emojiUrl = `https://r2.wamellow.com/emoji/${member.emoji}.webp`; 31 + const averageColor = member.emoji 32 + ? await getAverageColor(emojiUrl + "?size=16") 33 + : null; 34 + 35 + async function publish() { 36 + "use server"; 37 + 38 + const cookieStore = cookies(); 39 + const currentCircular = cookieStore.get("lbc")?.value; 40 + 41 + cookieStore.set( 42 + "lbc", 43 + currentCircular !== "server" 44 + ? "server" 45 + : "next" 46 + ); 47 + } 48 + 49 + const cookieStore = cookies(); 50 + const currentCircular = cookieStore.get("lbc")?.value; 51 + 52 + return ( 53 + <div 54 + className={cn( 55 + "mb-4 rounded-xl p-3 flex items-center dark:bg-wamellow bg-wamellow-100 w-full overflow-hidden" 56 + )} 57 + style={averageColor ? { backgroundColor: averageColor + "50" } : {}} 58 + > 59 + <Badge 60 + className={cn( 61 + "size-6 font-bold", 62 + (() => { 63 + if (index === 1) return "bg-[#ffe671] text-[#ff9e03] border-2 border-[#1c1b1f]"; 64 + if (index === 2) return "bg-[#c1e5fb] text-[#6093ba] border-2 border-[#1c1b1f]"; 65 + if (index === 3) return "bg-[#f8c396] text-[#c66e04] border-2 border-[#1c1b1f]"; 66 + return "bg-[#1c1b1f]"; 67 + })() 68 + )} 69 + showOutline={false} 70 + content={ 71 + <span className="px-[3px]"> 72 + {intl.format(index)} 73 + </span> 74 + } 75 + size="sm" 76 + placement="bottom-left" 77 + > 78 + <ImageReduceMotion 79 + alt={`${member.username}'s profile picture`} 80 + className="rounded-full h-12 w-12 mr-3" 81 + url={`https://cdn.discordapp.com/avatars/${member.id}/${member.avatar}`} 82 + size={128} 83 + /> 84 + </Badge> 85 + 86 + <div className="w-full md:max-w-fit"> 87 + <div className="flex items-center gap-2"> 88 + <span className="text-xl font-medium dark:text-neutral-200 text-neutral-800 truncate"> 89 + {member.globalName || member.username || "Unknown user"} 90 + </span> 91 + {member.bot && 92 + <DiscordAppBadge /> 93 + } 94 + {member.id === "821472922140803112" && 95 + <UserBadge>Developer</UserBadge> 96 + } 97 + {member.id === "845287163712372756" && 98 + <UserBadge>WOMEN</UserBadge> 99 + } 100 + </div> 101 + <div className="text-sm dark:text-neutral-300 text-neutral-700 truncate"> 102 + @{member.username} 103 + </div> 104 + </div> 105 + 106 + {member.emoji && 107 + <div className="w-full hidden sm:block relative mr-6 -ml-48 md:-ml-6 lg:ml-6"> 108 + <div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-6 w-full gap-2 absolute -bottom-9 rotate-1"> 109 + {new Array(12).fill(0).map((_, i) => 110 + <Emoji 111 + key={"emoji-" + member.id + i} 112 + index={i} 113 + emojiUrl={emojiUrl} 114 + /> 115 + )} 116 + </div> 117 + </div> 118 + } 119 + 120 + <div className="ml-auto flex text-xl font-medium dark:text-neutral-200 text-neutral-800"> 121 + <span className="mr-1 break-keep text-nowrap"> 122 + {type === "voiceminutes" 123 + ? member.activity?.formattedVoicetime 124 + : intl.format(member.activity?.[type || "messages"]) 125 + } 126 + </span> 127 + 128 + <Icon type={type} /> 129 + </div> 130 + 131 + <form action={publish}> 132 + <CircularProgress 133 + as="button" 134 + type="submit" 135 + className="ml-4" 136 + aria-label="progress" 137 + size="lg" 138 + color={ 139 + currentCircular === "next" 140 + ? "default" 141 + : "secondary" 142 + } 143 + classNames={{ 144 + svg: "drop-shadow-md" 145 + }} 146 + value={ 147 + currentCircular === "next" 148 + ? (member.activity[type] * 100) / (members[index - 1]?.activity[type] || 1) 149 + : (member.activity[type] * 100) / parseInt(pagination[type].total.toString()) 150 + || 100 151 + } 152 + showValueLabel={true} 153 + /> 154 + </form> 155 + 156 + </div > 157 + ); 158 + 159 + } 160 + 161 + function UserBadge({ 162 + children 163 + }: { 164 + children: React.ReactNode 165 + }) { 166 + return ( 167 + <Chip 168 + as={Link} 169 + color="secondary" 170 + href="/team?utm_source=wamellow.com&utm_medium=leaderboard" 171 + size="sm" 172 + startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />} 173 + target="_blank" 174 + variant="flat" 175 + > 176 + <span className="font-bold">{children}</span> 177 + </Chip> 178 + ); 179 + } 180 + 181 + function Emoji({ 182 + index, 183 + emojiUrl 184 + }: { 185 + index: number; 186 + emojiUrl: string; 187 + }) { 188 + return ( 189 + <Image 190 + alt="" 191 + className="rounded-xl relative size-8 aspect-square" 192 + draggable={false} 193 + height={48} 194 + src={emojiUrl} 195 + style={{ 196 + transform: `rotate(${(index / 2.3) * 360 + index}deg)`, 197 + top: `${index * 2 % 4}px`, 198 + left: `${index * 8 / 2}px` 199 + }} 200 + width={48} 201 + /> 202 + ) 154 203 }
+2 -4
app/profile/layout.tsx
··· 16 16 import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 17 17 import { cacheOptions, getData } from "@/lib/api"; 18 18 import SadWumpusPic from "@/public/sad-wumpus.gif"; 19 - import { ApiV1MeGetResponse } from "@/typings"; 19 + import { ApiV1UsersMeGetResponse } from "@/typings"; 20 20 import decimalToRgb from "@/utils/decimalToRgb"; 21 21 22 22 export default function RootLayout({ ··· 30 30 if (!hasSession) redirect("/login?callback=/profile"); 31 31 32 32 const user = userStore((u) => u); 33 - const accent = decimalToRgb(user?.accentColor as number); 34 33 35 34 const url = "/users/@me" as const; 36 35 37 36 const { data, error } = useQuery( 38 37 url, 39 - () => getData<ApiV1MeGetResponse>(url), 38 + () => getData<ApiV1UsersMeGetResponse>(url), 40 39 { 41 40 enabled: !!user?.id, 42 41 onSuccess: (d) => userStore.setState({ ··· 75 74 <Skeleton 76 75 isLoaded={!!user?.id} 77 76 className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40 shrink-0 relative top-1" 78 - style={user?.accentColor ? { "--tw-ring-color": `rgb(${accent.r}, ${accent.g}, ${accent.b})` } as React.CSSProperties : {}} 79 77 > 80 78 <ImageReduceMotion url={`https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}`} size={128} alt="you" /> 81 79 </Skeleton>
+191
app/profile/rank/card-style.component.tsx
··· 1 + import { userStore } from "@/common/user"; 2 + import { User } from "@/common/user"; 3 + import Box from "@/components/box"; 4 + import { ApiV1UsersMeRankEmojiDeleteResponse, ApiV1UsersMeRankEmojiPutResponse } from "@/typings"; 5 + import cn from "@/utils/cn"; 6 + import { deepMerge } from "@/utils/deepMerge"; 7 + import sleep from "@/utils/sleep"; 8 + import { Button } from "@nextui-org/react"; 9 + import { ApiError } from "next/dist/server/api-utils"; 10 + import Image from "next/image"; 11 + import Link from "next/link"; 12 + import { ChangeEvent, useRef, useState } from "react"; 13 + import { HiUpload } from "react-icons/hi"; 14 + 15 + enum State { 16 + Idle = 0, 17 + Loading = 1, 18 + Success = 3 19 + } 20 + 21 + export default function CardSyle() { 22 + const user = userStore((s) => s); 23 + const ref = useRef<HTMLInputElement | null>(null); 24 + 25 + const [state, setState] = useState<State>(State.Idle); 26 + const [error, setError] = useState<string | null>(null); 27 + 28 + if (user?.id && !user.extended) return <></>; 29 + 30 + // TODO: Confetti & rainbow animation 31 + // TODO: Add skeletons 32 + // TODO: Image replace animation(?) 33 + // TODO: Better error message 34 + 35 + async function upload(e: ChangeEvent<HTMLInputElement>) { 36 + setError(null); 37 + 38 + const file = e.target.files?.[0]; 39 + if (!file) return; 40 + 41 + setState(State.Loading); 42 + 43 + const formData = new FormData(); 44 + formData.append("file", file); 45 + 46 + const res = await fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/rank/emoji`, { 47 + method: "PUT", 48 + credentials: "include", 49 + body: formData 50 + }) 51 + .then((r) => r.json()) 52 + .catch(() => null) as ApiV1UsersMeRankEmojiPutResponse | ApiError | null; 53 + 54 + if (!res || "message" in res) { 55 + setState(State.Idle) 56 + setError( 57 + res && "message" in res 58 + ? res.message 59 + : "Failed to update" 60 + ); 61 + return; 62 + } 63 + 64 + await sleep(1000 * 3); 65 + setState(State.Success); 66 + 67 + userStore.setState({ 68 + ...deepMerge<User>(user, { 69 + extended: { rank: { emoji: res.id } } 70 + }) 71 + }); 72 + } 73 + 74 + async function remove() { 75 + setError(null); 76 + setState(State.Loading); 77 + 78 + const res = await fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/rank/emoji`, { 79 + method: "DELETE", 80 + credentials: "include" 81 + }) 82 + .then((r) => r.json()) 83 + .catch(() => null) as ApiV1UsersMeRankEmojiDeleteResponse | ApiError | null; 84 + 85 + if (!res || "message" in res) { 86 + setState(State.Idle) 87 + setError( 88 + res && "message" in res 89 + ? res.message 90 + : "Failed to remove" 91 + ); 92 + return; 93 + } 94 + 95 + setState(State.Idle); 96 + 97 + userStore.setState({ 98 + ...deepMerge<User>(user, { 99 + extended: { rank: { emoji: null } } 100 + }) 101 + }); 102 + } 103 + 104 + return ( 105 + <div> 106 + <input 107 + accept={["image/jpg", "image/jpeg", "image/png", "image/webp"].join()} 108 + className="hidden" 109 + onChange={upload} 110 + ref={ref} 111 + type="file" 112 + /> 113 + 114 + <Box className="flex h-56"> 115 + <div className="w-1/2 flex items-center justify-center"> 116 + <div className="flex flex-col"> 117 + <Button 118 + className={cn(state === State.Loading && "shake")} 119 + color="secondary" 120 + startContent={state !== State.Loading && <HiUpload />} 121 + onClick={() => ref.current?.click()} 122 + isLoading={state === State.Loading} 123 + > 124 + {state === State.Success 125 + ? "Looking good!" 126 + : "Upload Emoji" 127 + } 128 + </Button> 129 + {user?.extended?.rank?.emoji && 130 + <button 131 + onClick={() => remove()} 132 + className="text-red-400 hover:underline md:text-sm w-fit mt-1" 133 + > 134 + Remove 135 + </button> 136 + } 137 + </div> 138 + </div> 139 + <div className="w-1/2 grid grid-cols-6 lg:grid-cols-6 gap-4 rotate-1 relative bottom-4"> 140 + {new Array(18).fill(0).map((_, i) => 141 + <Emoji 142 + key={"emoji-" + i} 143 + index={i} 144 + emojiId={user?.extended?.rank?.emoji || null} 145 + /> 146 + )} 147 + </div> 148 + </Box> 149 + 150 + <div className="flex"> 151 + {error && 152 + <div className="ml-auto text-red-500 text-sm"> 153 + {error} 154 + </div> 155 + } 156 + </div> 157 + </div> 158 + ); 159 + } 160 + 161 + function Emoji({ 162 + index, 163 + emojiId 164 + }: { 165 + index: number, 166 + emojiId: string | null 167 + }) { 168 + const classNames = "rounded-xl relative size-12 aspect-square"; 169 + 170 + if (!emojiId) return ( 171 + <div 172 + className={cn(classNames, "bg-wamellow shadow-xl hover:scale-105 duration-100")} 173 + /> 174 + ) 175 + 176 + return ( 177 + <Image 178 + alt="" 179 + className={classNames} 180 + draggable={false} 181 + height={64} 182 + src={`https://r2.wamellow.com/emoji/${emojiId}.webp`} 183 + style={{ 184 + transform: `rotate(${(index / 2.3) * 360}deg)`, 185 + top: `${index * 2 % 4}px`, 186 + left: `${index * 8 / 2}px` 187 + }} 188 + width={64} 189 + /> 190 + ); 191 + }
+131
app/profile/rank/leaderboard-style.component.tsx
··· 1 + import { userStore } from "@/common/user"; 2 + import { User } from "@/common/user"; 3 + import { ApiError, ApiV1UsersMeGetResponse, ApiV1UsersGetResponse } from "@/typings"; 4 + import cn from "@/utils/cn"; 5 + import { deepMerge } from "@/utils/deepMerge"; 6 + import { useState } from "react"; 7 + 8 + export default function LeaderboardStyle() { 9 + const user = userStore((s) => s); 10 + 11 + const [error, setError] = useState<string | null>(null); 12 + 13 + if (user?.id && !user.extended) return <></>; 14 + 15 + async function update(useLeaderboardList: boolean) { 16 + if (user?.extended?.rank?.useLeaderboardList === useLeaderboardList) return; 17 + setError(null); 18 + 19 + const res = await fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/rank`, { 20 + method: "PATCH", 21 + credentials: "include", 22 + headers: { 23 + "Content-Type": "application/json" 24 + }, 25 + body: JSON.stringify({ useLeaderboardList }) 26 + }) 27 + .then((r) => r.json()) 28 + .catch(() => null) as ApiV1UsersMeGetResponse["rank"] | ApiError | null; 29 + 30 + if (!res || "message" in res) { 31 + setError( 32 + res && "message" in res 33 + ? res.message 34 + : "Failed to update" 35 + ); 36 + return; 37 + } 38 + 39 + userStore.setState({ 40 + ...deepMerge<User>(user, { 41 + extended: { rank: res } 42 + }) 43 + }); 44 + } 45 + 46 + return (<> 47 + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 48 + 49 + <button 50 + className="w-full" 51 + onClick={() => update(false)} 52 + > 53 + <div 54 + className={cn( 55 + "border-2 duration-200 rounded-md group p-6 mt-1 grid grid-rows-5 grid-cols-2 gap-3", 56 + user?.extended?.rank?.useLeaderboardList 57 + ? "dark:border-neutral-700 hover:border-neutral-500 border-neutral-300 " 58 + : "dark:border-violet-400/60 dark:hover:border-violet-400 border-violet-600/60 hover:border-violet-600" 59 + )} 60 + > 61 + {new Array(10).fill("").map((_, i) => 62 + <div key={i} className="flex gap-2"> 63 + <div 64 + className={cn( 65 + "duration-200 h-6 w-6 aspect-square rounded-full", 66 + user?.extended?.rank?.useLeaderboardList 67 + ? "dark:bg-neutral-700/90 dark:group-hover:bg-neutral-400/60 bg-neutral-300/90 group-hover:bg-neutral-600/60" 68 + : "dark:bg-violet-400/50 dark:group-hover:bg-violet-400/70 bg-violet-600/50 group-hover:bg-violet-600/70" 69 + )} 70 + /> 71 + <div 72 + className={cn( 73 + "duration-200 h-6 rounded-full", 74 + user?.extended?.rank?.useLeaderboardList 75 + ? "dark:bg-neutral-700/80 dark:group-hover:bg-neutral-400/50 bg-neutral-300/80 group-hover:bg-neutral-600/50" 76 + : "dark:bg-violet-400/40 dark:group-hover:bg-violet-400/60 bg-violet-600/40 group-hover:bg-violet-600/60" 77 + )} 78 + style={{ width: `${30 + ((i % 1.7) + (i % 3) + (i % 2)) * 10}%` }} 79 + /> 80 + </div> 81 + )} 82 + </div> 83 + </button> 84 + 85 + <button 86 + className="w-full" 87 + onClick={() => update(true)} 88 + > 89 + <div 90 + className={cn( 91 + "border-2 duration-200 rounded-md p-4 mt-1 flex flex-col gap-2 group", 92 + !user?.extended?.rank?.useLeaderboardList 93 + ? "dark:border-neutral-700 hover:border-neutral-500 border-neutral-300 " 94 + : "dark:border-violet-400/60 dark:hover:border-violet-400 border-violet-600/60 hover:border-violet-600" 95 + )} 96 + > 97 + {new Array(8).fill("").map((_, i) => 98 + <div key={i} className="flex gap-2"> 99 + <div 100 + className={cn( 101 + `duration-200 h-4 w-4 aspect-square rounded-full`, 102 + !user?.extended?.rank?.useLeaderboardList 103 + ? "dark:bg-neutral-700/90 dark:group-hover:bg-neutral-400/60 bg-neutral-300/90 group-hover:bg-neutral-600/60" 104 + : "dark:bg-violet-400/50 dark:group-hover:bg-violet-400/70 bg-violet-600/50 group-hover:bg-violet-600/70" 105 + )} 106 + /> 107 + <div 108 + className={cn( 109 + "duration-200 h-4 rounded-full", 110 + !user?.extended?.rank?.useLeaderboardList 111 + ? "dark:bg-neutral-700/80 dark:group-hover:bg-neutral-400/50 bg-neutral-300/80 group-hover:bg-neutral-600/50" 112 + : "dark:bg-violet-400/40 dark:group-hover:bg-violet-400/60 bg-violet-600/40 group-hover:bg-violet-600/60" 113 + )} 114 + style={{ width: `${30 + ((i % 1.7) + (i % 3) + (i % 2)) * 10}%` }} 115 + /> 116 + </div> 117 + )} 118 + </div> 119 + </button> 120 + 121 + </div> 122 + 123 + <div className="flex"> 124 + {error && 125 + <div className="ml-auto text-red-500 text-sm"> 126 + {error} 127 + </div> 128 + } 129 + </div> 130 + </>); 131 + }
+117 -182
app/profile/rank/page.tsx
··· 1 - "use client"; 2 - 3 - import { User, userStore } from "@/common/user"; 4 - import ImageUrlInput from "@/components/inputs/image-url-input"; 5 - import SelectInput from "@/components/inputs/select-menu"; 6 - import TextInput from "@/components/inputs/text-input"; 7 - import { deepMerge } from "@/utils/deepMerge"; 8 - 9 - export default function Home() { 10 - const user = userStore((s) => s); 11 - 12 - if (user?.id && !user.extended) return <></>; 13 - 14 - return ( 15 - <div> 16 - 17 - <div className="lg:flex gap-3"> 18 - <div className="lg:w-1/2"> 19 - <SelectInput 20 - name="Secondary text" 21 - url="/users/@me/rank" 22 - dataName="subText.type" 23 - description="This text will be displayed bellow the /rank progressbar." 24 - items={[ 25 - { 26 - name: "None", 27 - value: 0 28 - }, 29 - { 30 - name: "ETA to next milestone reach date", 31 - value: 1 32 - }, 33 - { 34 - name: "ETA to next milestone reach relative date", 35 - value: 2 36 - }, 37 - { 38 - name: "Custom text", 39 - value: 3, 40 - error: "Not done yet" 41 - } 42 - ]} 43 - defaultState={user?.extended?.rank?.subText?.type} 44 - onSave={(options) => { 45 - if (!user) return; 46 - userStore.setState(deepMerge<User>(user, { extended: { rank: { subText: { type: Number(options.value) as 0 | 1 | 2 | 3 } } } })); 47 - }} 48 - /> 49 - </div> 50 - 51 - <div className="lg:w-1/2 flex gap-2 w-full"> 52 - 53 - <div className="w-1/2"> 54 - <TextInput 55 - key="textColor" 56 - name="Text color" 57 - url="/users/@me/rank" 58 - dataName="textColor" 59 - description="Color used for your username." 60 - type="color" 61 - defaultState={user?.extended?.rank?.textColor ?? 0} 62 - onSave={(value) => { 63 - if (!user) return; 64 - userStore.setState(deepMerge<User>(user, { extended: { rank: { textColor: Number(value) } } })); 65 - }} 66 - /> 67 - </div> 68 - 69 - <div className="w-1/2"> 70 - <TextInput 71 - key="barColor" 72 - name="Bar color" 73 - url="/users/@me/rank" 74 - dataName="barColor" 75 - description="Color used for the progress bar." 76 - type="color" 77 - defaultState={user?.extended?.rank?.barColor ?? 0} 78 - onSave={(value) => { 79 - if (!user) return; 80 - userStore.setState(deepMerge<User>(user, { extended: { rank: { barColor: Number(value) } } })); 81 - }} 82 - /> 83 - </div> 84 - 85 - </div> 86 - </div> 87 - 88 - <ImageUrlInput 89 - name="Background" 90 - url="/users/@me/rank" 91 - ratio="aspect-[4/1]" 92 - dataName="background" 93 - description="Enter a url which should be the background of your /rank card. The recomended image ration is 4:1 and recommended resolution 1024x256px." 94 - defaultState={user?.extended?.rank?.background || ""} 95 - onSave={(value) => { 96 - if (!user) return; 97 - userStore.setState(deepMerge<User>(user, { extended: { rank: { background: value } } })); 98 - }} 99 - /> 100 - 101 - <div className="text-lg dark:text-neutral-300 text-neutral-700 font-medium mb-1 mt-3">Leaderboard syle</div> 102 - 103 - <div className="grid grid-cols-1 md:grid-cols-2 gap-4 "> 104 - 105 - <button 106 - className="w-full" 107 - onClick={() => { 108 - if (user?.extended?.rank?.useLeaderboardList === false) return; 109 - 110 - fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/rank`, { 111 - method: "PATCH", 112 - credentials: "include", 113 - headers: { 114 - "Content-Type": "application/json" 115 - }, 116 - body: JSON.stringify({ useLeaderboardList: false }) 117 - }) 118 - .then(async (res) => { 119 - const response = await res.json(); 120 - if (!response) return; 121 - 122 - switch (res.status) { 123 - case 200: { 124 - userStore.setState({ ...deepMerge<User>(user, { extended: { rank: { useLeaderboardList: false } } }) }); 125 - break; 126 - } 127 - } 128 - 129 - }); 130 - }} 131 - > 132 - <div className={`border-2 ${user?.extended?.rank?.useLeaderboardList ? "dark:border-neutral-700 hover:border-neutral-500 border-neutral-300 " : "dark:border-violet-400/60 dark:hover:border-violet-400 border-violet-600/60 hover:border-violet-600"} duration-200 rounded-md group p-6 mt-1 grid grid-rows-5 grid-cols-2 gap-3`}> 133 - {new Array(10).fill("").map((_, i) => 134 - <div key={i} className="flex gap-2"> 135 - <div className={`${user?.extended?.rank?.useLeaderboardList ? "dark:bg-neutral-700/90 dark:group-hover:bg-neutral-400/60 bg-neutral-300/90 group-hover:bg-neutral-600/60" : "dark:bg-violet-400/50 dark:group-hover:bg-violet-400/70 bg-violet-600/50 group-hover:bg-violet-600/70"} duration-200 h-6 w-6 aspect-square rounded-full`} /> 136 - <div className={`${user?.extended?.rank?.useLeaderboardList ? "dark:bg-neutral-700/80 dark:group-hover:bg-neutral-400/50 bg-neutral-300/80 group-hover:bg-neutral-600/50" : "dark:bg-violet-400/40 dark:group-hover:bg-violet-400/60 bg-violet-600/40 group-hover:bg-violet-600/60"} duration-200 h-6 rounded-full`} style={{ width: `${30 + ((i % 1.7) + (i % 3) + (i % 2)) * 10}%` }} /> 137 - </div> 138 - )} 139 - </div> 140 - </button> 141 - 142 - <button 143 - className="w-full" 144 - onClick={() => { 145 - if (user?.extended?.rank?.useLeaderboardList === true) return; 146 - 147 - fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/rank`, { 148 - method: "PATCH", 149 - credentials: "include", 150 - headers: { 151 - "Content-Type": "application/json" 152 - }, 153 - body: JSON.stringify({ useLeaderboardList: true }) 154 - }) 155 - .then(async (res) => { 156 - const response = await res.json(); 157 - if (!response) return; 158 - 159 - switch (res.status) { 160 - case 200: { 161 - userStore.setState({ ...deepMerge<User>(user, { extended: { rank: { useLeaderboardList: true } } }) }); 162 - break; 163 - } 164 - } 165 - 166 - }); 167 - }} 168 - > 169 - <div className={`border-2 ${!user?.extended?.rank?.useLeaderboardList ? "dark:border-neutral-700 hover:border-neutral-500 border-neutral-300 " : "dark:border-violet-400/60 dark:hover:border-violet-400 border-violet-600/60 hover:border-violet-600"} duration-200 rounded-md p-4 mt-1 flex flex-col gap-2 group`}> 170 - {new Array(8).fill("").map((_, i) => 171 - <div key={i} className="flex gap-2"> 172 - <div className={`${!user?.extended?.rank?.useLeaderboardList ? "dark:bg-neutral-700/90 dark:group-hover:bg-neutral-400/60 bg-neutral-300/90 group-hover:bg-neutral-600/60" : "dark:bg-violet-400/50 dark:group-hover:bg-violet-400/70 bg-violet-600/50 group-hover:bg-violet-600/70"} duration-200 h-4 w-4 aspect-square rounded-full`} /> 173 - <div className={`${!user?.extended?.rank?.useLeaderboardList ? "dark:bg-neutral-700/80 dark:group-hover:bg-neutral-400/50 bg-neutral-300/80 group-hover:bg-neutral-600/50" : "dark:bg-violet-400/40 dark:group-hover:bg-violet-400/60 bg-violet-600/40 group-hover:bg-violet-600/60"} duration-200 h-4 rounded-full`} style={{ width: `${30 + ((i % 1.7) + (i % 3) + (i % 2)) * 10}%` }} /> 174 - </div> 175 - )} 176 - </div> 177 - </button> 178 - 179 - </div> 180 - 181 - </div > 182 - ); 1 + "use client"; 2 + 3 + import { User, userStore } from "@/common/user"; 4 + import ImageUrlInput from "@/components/inputs/image-url-input"; 5 + import SelectInput from "@/components/inputs/select-menu"; 6 + import TextInput from "@/components/inputs/text-input"; 7 + import { deepMerge } from "@/utils/deepMerge"; 8 + import LeaderboardStyle from "./leaderboard-style.component"; 9 + import { Section } from "@/components/section"; 10 + import CardSyle from "./card-style.component"; 11 + 12 + export default function Home() { 13 + const user = userStore((s) => s); 14 + 15 + if (user?.id && !user.extended) return <></>; 16 + 17 + return ( 18 + <div> 19 + 20 + <div className="lg:flex gap-3"> 21 + <div className="lg:w-1/2"> 22 + <SelectInput 23 + name="Secondary text" 24 + url="/users/@me/rank" 25 + dataName="subText.type" 26 + description="This text will be displayed bellow the /rank progressbar." 27 + items={[ 28 + { 29 + name: "None", 30 + value: 0 31 + }, 32 + { 33 + name: "ETA to next milestone reach date", 34 + value: 1 35 + }, 36 + { 37 + name: "ETA to next milestone reach relative date", 38 + value: 2 39 + }, 40 + { 41 + name: "Custom text", 42 + value: 3, 43 + error: "Not done yet" 44 + } 45 + ]} 46 + defaultState={user?.extended?.rank?.subText?.type} 47 + onSave={(options) => { 48 + userStore.setState(deepMerge<User>(user, { extended: { rank: { subText: { type: Number(options.value) as 0 | 1 | 2 | 3 } } } })); 49 + }} 50 + /> 51 + </div> 52 + 53 + <div className="lg:w-1/2 flex gap-2 w-full"> 54 + 55 + <div className="w-1/2"> 56 + <TextInput 57 + key="textColor" 58 + name="Text color" 59 + url="/users/@me/rank" 60 + dataName="textColor" 61 + description="Color used for your username." 62 + type="color" 63 + defaultState={user?.extended?.rank?.textColor ?? 0} 64 + onSave={(value) => { 65 + userStore.setState(deepMerge<User>(user, { extended: { rank: { textColor: Number(value) } } })); 66 + }} 67 + /> 68 + </div> 69 + 70 + <div className="w-1/2"> 71 + <TextInput 72 + key="barColor" 73 + name="Bar color" 74 + url="/users/@me/rank" 75 + dataName="barColor" 76 + description="Color used for the progress bar." 77 + type="color" 78 + defaultState={user?.extended?.rank?.barColor ?? 0} 79 + onSave={(value) => { 80 + userStore.setState(deepMerge<User>(user, { extended: { rank: { barColor: Number(value) } } })); 81 + }} 82 + /> 83 + </div> 84 + 85 + </div> 86 + </div> 87 + 88 + <ImageUrlInput 89 + name="Background" 90 + url="/users/@me/rank" 91 + ratio="aspect-[4/1]" 92 + dataName="background" 93 + description="Enter a url which should be the background of your /rank card. The recomended image ration is 4:1 and recommended resolution 1024x256px." 94 + defaultState={user?.extended?.rank?.background || ""} 95 + onSave={(value) => { 96 + userStore.setState(deepMerge<User>(user, { extended: { rank: { background: value } } })); 97 + }} 98 + /> 99 + 100 + <Section 101 + title="/leaderboard style" 102 + > 103 + Choose how your personal /leaderboard should look like. 104 + </Section> 105 + 106 + <LeaderboardStyle /> 107 + 108 + <Section 109 + title="Web leaderboard card style" 110 + > 111 + Customize your card for web leaderboards. 112 + </Section> 113 + 114 + <CardSyle /> 115 + 116 + </div> 117 + ); 183 118 }
+2 -2
common/user.ts
··· 1 1 import { create } from "zustand"; 2 2 3 - import { ApiV1MeGetResponse } from "@/typings"; 3 + import { ApiV1UsersMeGetResponse } from "@/typings"; 4 4 5 5 export interface User { 6 6 session: string; ··· 15 15 16 16 __fetched: boolean; 17 17 18 - extended: ApiV1MeGetResponse | undefined; 18 + extended: ApiV1UsersMeGetResponse | undefined; 19 19 } 20 20 21 21 export const userStore = create<User | undefined>(() => ({
+42 -42
next.config.js
··· 1 - /** @type {import('next').NextConfig} */ 2 - const nextConfig = { 3 - reactStrictMode: false, 4 - eslint: { 5 - ignoreDuringBuilds: true 6 - }, 7 - images: { 8 - remotePatterns: [ 9 - { 10 - protocol: "https", 11 - hostname: "cdn.discordapp.com", 12 - port: "", 13 - pathname: "/**" 14 - }, 15 - { 16 - protocol: "https", 17 - hostname: "cdn.waya.one", 18 - port: "", 19 - pathname: "/r/**" 20 - }, 21 - { 22 - protocol: "https", 23 - hostname: "image-api.wamellow.com", 24 - port: "", 25 - pathname: "/" 26 - }, 27 - { 28 - protocol: "https", 29 - hostname: "r2.wamellow.com", 30 - port: "", 31 - pathname: "/ai-image/**" 32 - }, 33 - { 34 - protocol: "https", 35 - hostname: "ai.local.wamellow.com", 36 - port: "", 37 - pathname: "/static/**" 38 - } 39 - ] 40 - } 41 - }; 42 - 1 + /** @type {import('next').NextConfig} */ 2 + const nextConfig = { 3 + reactStrictMode: false, 4 + eslint: { 5 + ignoreDuringBuilds: true 6 + }, 7 + images: { 8 + remotePatterns: [ 9 + { 10 + protocol: "https", 11 + hostname: "cdn.discordapp.com", 12 + port: "", 13 + pathname: "/**" 14 + }, 15 + { 16 + protocol: "https", 17 + hostname: "cdn.waya.one", 18 + port: "", 19 + pathname: "/r/**" 20 + }, 21 + { 22 + protocol: "https", 23 + hostname: "image-api.wamellow.com", 24 + port: "", 25 + pathname: "/" 26 + }, 27 + { 28 + protocol: "https", 29 + hostname: "r2.wamellow.com", 30 + port: "", 31 + pathname: "/**" 32 + }, 33 + { 34 + protocol: "https", 35 + hostname: "ai.local.wamellow.com", 36 + port: "", 37 + pathname: "/static/**" 38 + } 39 + ] 40 + } 41 + }; 42 + 43 43 module.exports = nextConfig;
+445 -434
typings.ts
··· 1 - import { actor } from "./utils/tts"; 2 - 3 - export interface ApiError { 4 - statusCode: number; 5 - message: string; 6 - } 7 - 8 - export interface ApiV1TopguildsGetResponse { 9 - id: string; 10 - name: string; 11 - icon: string | null; 12 - memberCount: number; 13 - 14 - verified: boolean; 15 - partnered: boolean; 16 - } 17 - 18 - export interface UserGuild { 19 - id: string; 20 - name: string; 21 - icon: string | null; 22 - } 23 - 24 - export interface RouteErrorResponse { 25 - statusCode: number; 26 - message: string; 27 - } 28 - 29 - export interface ApiV1GuildsGetResponse { 30 - id: string; 31 - name: string; 32 - icon: string | null; 33 - banner: string | null; 34 - memberCount: number; 35 - premiumTier: number; 36 - inviteUrl: string | undefined; 37 - description: string | null; 38 - follownewsChannel?: { 39 - id?: string; 40 - name?: string; 41 - }; 42 - tts: { 43 - channelId: string | null; 44 - announceUser: boolean; 45 - logChannelId: string | null; 46 - priorityRoleId: string | null; 47 - maxLength?: number | null; 48 - } 49 - } 50 - 51 - export interface ApiV1GuildsTopmembersGetResponse { 52 - id: string; 53 - globalName: string | null; 54 - username: string | null; 55 - avatar: string | null; 56 - bot?: true; 57 - activity: ApiV1MeGetResponse["activity"] & { formattedVoicetime: string }; 58 - } 59 - 60 - export interface ApiV1GuildsTopmembersPaginationGetResponse { 61 - messages: { 62 - pages: number; 63 - total: number; 64 - }; 65 - voiceminutes: { 66 - pages: number; 67 - total: string; 68 - formattedTotal: string; 69 - }; 70 - invites: { 71 - pages: number; 72 - total: number; 73 - }; 74 - } 75 - 76 - export interface ApiV1GuildsChannelsGetResponse { 77 - name: string; 78 - id: string; 79 - missingPermissions: string[] 80 - } 81 - 82 - export interface ApiV1GuildsRolesGetResponse { 83 - name: string; 84 - id: string; 85 - missingPermissions: string[]; 86 - position: number; 87 - color: number; 88 - } 89 - 90 - export interface ApiV1GuildsEmojisGetResponse { 91 - name: string; 92 - id: string; 93 - animated: boolean; 94 - } 95 - 96 - export interface GuildEmbed { 97 - title: string | null; 98 - description: string | null; 99 - thumbnail: string | null; 100 - image: string | null; 101 - color: number; 102 - footer: { 103 - text: string | null; 104 - icon_url: string | null; 105 - } 106 - } 107 - 108 - export interface ApiV1GuildsModulesWelcomeGetResponse { 109 - enabled: boolean; 110 - channelId?: string; 111 - 112 - message: { 113 - content?: string; 114 - embed?: GuildEmbed 115 - }; 116 - 117 - roleIds: string[]; 118 - pingIds: string[]; 119 - deleteAfter?: number; 120 - deleteAfterLeave?: boolean; 121 - restore: boolean; 122 - 123 - dm: { 124 - enabled: boolean; 125 - message: { 126 - content?: string; 127 - embed?: GuildEmbed; 128 - }; 129 - }; 130 - 131 - reactions: { 132 - welcomeMessageEmojis: string[], 133 - firstMessageEmojis: string[], 134 - }; 135 - 136 - card: { 137 - enabled: boolean; 138 - inEmbed: boolean; 139 - background?: string; 140 - textColor?: number; 141 - }; 142 - 143 - button: { 144 - enabled: boolean; 145 - style: 1 | 2 | 3 | 4; 146 - emoji?: string | null; 147 - label?: string | null; 148 - ping?: boolean; 149 - type: 0; 150 - }; 151 - } 152 - 153 - export interface ApiV1GuildsModulesByeGetResponse { 154 - enabled: boolean; 155 - channelId?: string; 156 - webhookURL?: string; 157 - 158 - message: { 159 - content?: string; 160 - embed?: GuildEmbed 161 - }; 162 - 163 - deleteAfter?: number; 164 - 165 - card: { 166 - enabled: boolean; 167 - inEmbed: boolean; 168 - background?: string; 169 - textColor?: number; 170 - }; 171 - } 172 - 173 - export interface ApiV1GuildsModulesStarboardGetResponse { 174 - enabled: boolean; 175 - channelId?: string; 176 - emoji: string; 177 - requiredEmojis: number; 178 - embedColor: number; 179 - style: number; 180 - 181 - allowNSFW: boolean; 182 - allowBots: boolean; 183 - allowEdits: boolean; 184 - allowSelfReact: boolean; 185 - displayReference: boolean; 186 - 187 - blacklistRoleIds: string[]; 188 - blacklistChannelIds: string[]; 189 - delete: boolean; 190 - } 191 - 192 - export interface ApiV1GuildsModulesLeaderboardUpdatingPostResponse { 193 - leaderboardId: string; 194 - guildId: string; 195 - 196 - channelId: string; 197 - messageId: string; 198 - 199 - type: "messages" | "voiceminutes" | "invites"; 200 - 201 - /** 202 - * 0 - text based 203 - * 1 - image grid 204 - * 2 - image list 205 - */ 206 - structure: number; 207 - styles: { 208 - useQuotes: boolean; 209 - rank: "**" | "__" | "*" | "`" | null; 210 - number: "**" | "__" | "*" | "`" | null; 211 - user: "**" | "__" | "*" | "`" | null; 212 - } 213 - 214 - range: "daily" | "weekly" | "monthly" | "alltime"; 215 - display: "tag" | "username" | "nickname" | "id"; 216 - 217 - background: string | null; 218 - emoji: string | null; 219 - 220 - updatedAt: string; 221 - createdAt: string; 222 - } 223 - 224 - export interface ApiV1GuildsModulesLeaderboardGetResponse { 225 - bannerUrl: string | null; 226 - 227 - backgroundColor: number | null; 228 - textColor: number | null; 229 - accentColor: number | null; 230 - 231 - blacklistChannelIds: string[]; 232 - 233 - roles: { 234 - messages: string[]; 235 - voiceminutes: string[]; 236 - // invites: string[]; // again'st tos 237 - } | undefined; 238 - 239 - updating: ApiV1GuildsModulesLeaderboardUpdatingPostResponse[]; 240 - } 241 - 242 - export interface ApiV1GuildsModulesPassportGetResponse { 243 - enabled: boolean; 244 - channelId?: string; 245 - /** 246 - * We're currently on free tier 247 - */ 248 - captchaType: "slide" | "word" | "icon" | "match" | "winlinze" | "nine" | "random"; 249 - /** 250 - * 0 - Ban 251 - * 1 - Kick 252 - * 2 - Assign role 253 - */ 254 - punishment: 0 | 1 | 2; 255 - punishmentRoleId?: string; 256 - 257 - successRoleId?: string; 258 - unverifiedRoleId?: string; 259 - 260 - sendFailedDm: boolean; 261 - alsoFailIf: ("disposableEmailAddress")[] 262 - 263 - backgroundColor?: number; 264 - textColor?: number; 265 - accentColor?: number; 266 - } 267 - 268 - export interface ApiV1MeGetResponse { 269 - voteCount?: number; 270 - 271 - rank?: { 272 - background?: string | null; 273 - textColor?: number; 274 - barColor?: number; 275 - useLeaderboardList?: boolean; 276 - subText?: { 277 - type: 0 | 1 | 2 | 3 // 0: off, 1: date, 2: relative, 3: custom 278 - content?: string; 279 - }; 280 - }; 281 - tts?: { 282 - defaultVoice?: keyof typeof actor; 283 - defaultFiletype?: "ogg" | "wav" | "mp3"; 284 - commandUses?: number; 285 - }; 286 - activity?: { 287 - messages: number; 288 - voiceminutes: number; 289 - invites: number; 290 - formattedVoicetime: string; 291 - }; 292 - } 293 - 294 - export interface ApiV1UsersMeConnectionsSpotifyGetResponse { 295 - displayName: string; 296 - avatar: string | null; 297 - playing: { 298 - name: string; 299 - id: string; 300 - artists: string; 301 - duration: string; 302 - } | undefined; 303 - } 304 - 305 - export interface ApiV1GuildsModulesTagsGetResponse { 306 - id: string; 307 - guildId: string; 308 - applicationCommandId?: string; 309 - 310 - name: string; 311 - permission: string | null; 312 - aliases: string[]; 313 - 314 - message: { 315 - content: string | null; 316 - embed?: GuildEmbed; 317 - }; 318 - 319 - authorId: string; 320 - 321 - createdAt: Date; 322 - } 323 - 324 - export interface ApiV1GuildsModulesEmbedmessagelinksGetResponse { 325 - enabled: boolean; 326 - color?: number | null; 327 - display: 0 | 1 | 2; 328 - } 329 - 330 - export interface ApiV1GuildsModulesNsfwModerationGetResponse { 331 - enabled: boolean; 332 - logChannelId: string | null; 333 - /** 334 - * @example 335 - * 0 - Nothing 336 - * 1 - Ban 337 - * 2 - Kick 338 - * 3 - Delete message 339 - */ 340 - punishment: 0 | 1 | 2 | 3; 341 - timeout: number; 342 - 343 - whitelistChannelIds: string[]; 344 - whitelistRoleIds: string[]; 345 - } 346 - 347 - export interface Upload { 348 - id: string; 349 - guildId?: string | null; 350 - authorId: string; 351 - 352 - prompt: string; 353 - negativePrompt?: string | null; 354 - model: string; 355 - 356 - verified: boolean; 357 - nsfw: boolean; 358 - 359 - createdAt: string; 360 - } 361 - 362 - export interface ApiV1UploadsGetResponse { 363 - results: Upload[]; 364 - pagination: { 365 - total: number; 366 - pages: number; 367 - } 368 - } 369 - 370 - export interface ApiV1UploadGetResponse extends Upload { 371 - author: { 372 - username: string; 373 - globalName: string; 374 - avatar: string | null; 375 - bot?: boolean; 376 - }; 377 - } 378 - 379 - export interface ApiV1UsersGetResponse { 380 - id: string; 381 - username: string; 382 - globalName: string | null; 383 - avatar: string | null; 384 - 385 - bannerUrl: string | null; 386 - voteCount: number; 387 - likeCount: number; 388 - 389 - activity: Required<ApiV1MeGetResponse>["activity"]; 390 - guilds: { 391 - guildId: string; 392 - activity: Required<ApiV1MeGetResponse>["activity"]; 393 - }[]; 394 - } 395 - 396 - export interface ApiV1GuildsModulesNotificationsGetResponse { 397 - id: string; 398 - guildId: string; 399 - channelId: string; 400 - roleId: string | null; 401 - 402 - type: 0; 403 - creatorId: string; 404 - 405 - message: { 406 - content: string | null; 407 - embed?: GuildEmbed; 408 - }; 409 - 410 - createdAt: Date; 411 - 412 - creator: { 413 - id: string; 414 - username: string; 415 - customUrl: string; 416 - avatarUrl: string; 417 - subs: string; 418 - videos: string; 419 - views: string; 420 - } 421 - 422 - } 423 - 424 - export interface PronounsResponse { 425 - status: number; 426 - content: string[]; 427 - } 428 - 429 - export interface NekosticResponse { 430 - event: string; 431 - name: string; 432 - uses: number 433 - users: number; 434 - snapshot: string; 1 + import { actor } from "./utils/tts"; 2 + 3 + export interface ApiError { 4 + statusCode: number; 5 + message: string; 6 + } 7 + 8 + export interface ApiV1TopguildsGetResponse { 9 + id: string; 10 + name: string; 11 + icon: string | null; 12 + memberCount: number; 13 + 14 + verified: boolean; 15 + partnered: boolean; 16 + } 17 + 18 + export interface UserGuild { 19 + id: string; 20 + name: string; 21 + icon: string | null; 22 + } 23 + 24 + export interface RouteErrorResponse { 25 + statusCode: number; 26 + message: string; 27 + } 28 + 29 + export interface ApiV1GuildsGetResponse { 30 + id: string; 31 + name: string; 32 + icon: string | null; 33 + banner: string | null; 34 + memberCount: number; 35 + premiumTier: number; 36 + inviteUrl: string | undefined; 37 + description: string | null; 38 + follownewsChannel?: { 39 + id?: string; 40 + name?: string; 41 + }; 42 + tts: { 43 + channelId: string | null; 44 + announceUser: boolean; 45 + logChannelId: string | null; 46 + priorityRoleId: string | null; 47 + maxLength?: number | null; 48 + } 49 + } 50 + 51 + export interface ApiV1GuildsTopmembersGetResponse { 52 + id: string; 53 + username: string | null; 54 + globalName: string | null; 55 + avatar: string | null; 56 + bot: true; 57 + emoji: string | null; 58 + activity: ApiV1UsersMeGetResponse["activity"] & { formattedVoicetime: string }; 59 + } 60 + 61 + export interface ApiV1GuildsTopmembersPaginationGetResponse { 62 + messages: { 63 + pages: number; 64 + total: number; 65 + }; 66 + voiceminutes: { 67 + pages: number; 68 + total: string; 69 + formattedTotal: string; 70 + }; 71 + invites: { 72 + pages: number; 73 + total: number; 74 + }; 75 + } 76 + 77 + export interface ApiV1GuildsChannelsGetResponse { 78 + name: string; 79 + id: string; 80 + missingPermissions: string[]; 81 + } 82 + 83 + export interface ApiV1GuildsRolesGetResponse { 84 + name: string; 85 + id: string; 86 + missingPermissions: string[]; 87 + position: number; 88 + color: number; 89 + } 90 + 91 + export interface ApiV1GuildsEmojisGetResponse { 92 + name: string; 93 + id: string; 94 + animated: boolean; 95 + } 96 + 97 + export interface GuildEmbed { 98 + title: string | null; 99 + description: string | null; 100 + thumbnail: string | null; 101 + image: string | null; 102 + color: number; 103 + footer: { 104 + text: string | null; 105 + icon_url: string | null; 106 + } 107 + } 108 + 109 + export interface ApiV1GuildsModulesWelcomeGetResponse { 110 + enabled: boolean; 111 + channelId?: string; 112 + 113 + message: { 114 + content?: string; 115 + embed?: GuildEmbed 116 + }; 117 + 118 + roleIds: string[]; 119 + pingIds: string[]; 120 + deleteAfter?: number; 121 + deleteAfterLeave?: boolean; 122 + restore: boolean; 123 + 124 + dm: { 125 + enabled: boolean; 126 + message: { 127 + content?: string; 128 + embed?: GuildEmbed; 129 + }; 130 + }; 131 + 132 + reactions: { 133 + welcomeMessageEmojis: string[], 134 + firstMessageEmojis: string[], 135 + }; 136 + 137 + card: { 138 + enabled: boolean; 139 + inEmbed: boolean; 140 + background?: string; 141 + textColor?: number; 142 + }; 143 + 144 + button: { 145 + enabled: boolean; 146 + style: 1 | 2 | 3 | 4; 147 + emoji?: string | null; 148 + label?: string | null; 149 + ping?: boolean; 150 + type: 0; 151 + }; 152 + } 153 + 154 + export interface ApiV1GuildsModulesByeGetResponse { 155 + enabled: boolean; 156 + channelId?: string; 157 + webhookURL?: string; 158 + 159 + message: { 160 + content?: string; 161 + embed?: GuildEmbed 162 + }; 163 + 164 + deleteAfter?: number; 165 + 166 + card: { 167 + enabled: boolean; 168 + inEmbed: boolean; 169 + background?: string; 170 + textColor?: number; 171 + }; 172 + } 173 + 174 + export interface ApiV1GuildsModulesStarboardGetResponse { 175 + enabled: boolean; 176 + channelId?: string; 177 + emoji: string; 178 + requiredEmojis: number; 179 + embedColor: number; 180 + style: number; 181 + 182 + allowNSFW: boolean; 183 + allowBots: boolean; 184 + allowEdits: boolean; 185 + allowSelfReact: boolean; 186 + displayReference: boolean; 187 + 188 + blacklistRoleIds: string[]; 189 + blacklistChannelIds: string[]; 190 + delete: boolean; 191 + } 192 + 193 + export interface ApiV1GuildsModulesLeaderboardUpdatingPostResponse { 194 + leaderboardId: string; 195 + guildId: string; 196 + 197 + channelId: string; 198 + messageId: string; 199 + 200 + type: "messages" | "voiceminutes" | "invites"; 201 + 202 + /** 203 + * 0 - text based 204 + * 1 - image grid 205 + * 2 - image list 206 + */ 207 + structure: number; 208 + styles: { 209 + useQuotes: boolean; 210 + rank: "**" | "__" | "*" | "`" | null; 211 + number: "**" | "__" | "*" | "`" | null; 212 + user: "**" | "__" | "*" | "`" | null; 213 + } 214 + 215 + range: "daily" | "weekly" | "monthly" | "alltime"; 216 + display: "tag" | "username" | "nickname" | "id"; 217 + 218 + background: string | null; 219 + emoji: string | null; 220 + 221 + updatedAt: string; 222 + createdAt: string; 223 + } 224 + 225 + export interface ApiV1GuildsModulesLeaderboardGetResponse { 226 + bannerUrl: string | null; 227 + 228 + backgroundColor: number | null; 229 + textColor: number | null; 230 + accentColor: number | null; 231 + 232 + blacklistChannelIds: string[]; 233 + 234 + roles: { 235 + messages: string[]; 236 + voiceminutes: string[]; 237 + // invites: string[]; // again'st tos 238 + } | undefined; 239 + 240 + updating: ApiV1GuildsModulesLeaderboardUpdatingPostResponse[]; 241 + } 242 + 243 + export interface ApiV1GuildsModulesPassportGetResponse { 244 + enabled: boolean; 245 + channelId?: string; 246 + /** 247 + * We're currently on free tier 248 + */ 249 + captchaType: "slide" | "word" | "icon" | "match" | "winlinze" | "nine" | "random"; 250 + /** 251 + * 0 - Ban 252 + * 1 - Kick 253 + * 2 - Assign role 254 + */ 255 + punishment: 0 | 1 | 2; 256 + punishmentRoleId?: string; 257 + 258 + successRoleId?: string; 259 + unverifiedRoleId?: string; 260 + 261 + sendFailedDm: boolean; 262 + alsoFailIf: ("disposableEmailAddress")[] 263 + 264 + backgroundColor?: number; 265 + textColor?: number; 266 + accentColor?: number; 267 + } 268 + 269 + export interface ApiV1UsersMeGetResponse { 270 + voteCount?: number; 271 + 272 + rank?: { 273 + background?: string | null; 274 + emoji?: string | null; 275 + textColor?: number; 276 + barColor?: number; 277 + useLeaderboardList?: boolean; 278 + subText?: { 279 + type: 0 | 1 | 2 | 3 // 0: off, 1: date, 2: relative, 3: custom 280 + content?: string; 281 + }; 282 + }; 283 + tts?: { 284 + defaultVoice?: keyof typeof actor; 285 + defaultFiletype?: "ogg" | "wav" | "mp3"; 286 + commandUses?: number; 287 + }; 288 + activity?: { 289 + messages: number; 290 + voiceminutes: number; 291 + invites: number; 292 + formattedVoicetime: string; 293 + }; 294 + } 295 + 296 + export interface ApiV1UsersMeConnectionsSpotifyGetResponse { 297 + displayName: string; 298 + avatar: string | null; 299 + playing: { 300 + name: string; 301 + id: string; 302 + artists: string; 303 + duration: string; 304 + } | undefined; 305 + } 306 + 307 + export interface ApiV1GuildsModulesTagsGetResponse { 308 + id: string; 309 + guildId: string; 310 + applicationCommandId?: string; 311 + 312 + name: string; 313 + permission: string | null; 314 + aliases: string[]; 315 + 316 + message: { 317 + content: string | null; 318 + embed?: GuildEmbed; 319 + }; 320 + 321 + authorId: string; 322 + 323 + createdAt: Date; 324 + } 325 + 326 + export interface ApiV1GuildsModulesEmbedmessagelinksGetResponse { 327 + enabled: boolean; 328 + color?: number | null; 329 + display: 0 | 1 | 2; 330 + } 331 + 332 + export interface ApiV1GuildsModulesNsfwModerationGetResponse { 333 + enabled: boolean; 334 + logChannelId: string | null; 335 + /** 336 + * @example 337 + * 0 - Nothing 338 + * 1 - Ban 339 + * 2 - Kick 340 + * 3 - Delete message 341 + */ 342 + punishment: 0 | 1 | 2 | 3; 343 + timeout: number; 344 + 345 + whitelistChannelIds: string[]; 346 + whitelistRoleIds: string[]; 347 + } 348 + 349 + export interface Upload { 350 + id: string; 351 + guildId?: string | null; 352 + authorId: string; 353 + 354 + prompt: string; 355 + negativePrompt?: string | null; 356 + model: string; 357 + 358 + verified: boolean; 359 + nsfw: boolean; 360 + 361 + createdAt: string; 362 + } 363 + 364 + export interface ApiV1UploadsGetResponse { 365 + results: Upload[]; 366 + pagination: { 367 + total: number; 368 + pages: number; 369 + } 370 + } 371 + 372 + export interface ApiV1UploadGetResponse extends Upload { 373 + author: { 374 + username: string; 375 + globalName: string; 376 + avatar: string | null; 377 + bot?: boolean; 378 + }; 379 + } 380 + 381 + export interface ApiV1UsersGetResponse { 382 + id: string; 383 + username: string; 384 + globalName: string | null; 385 + avatar: string | null; 386 + 387 + bannerUrl: string | null; 388 + voteCount: number; 389 + likeCount: number; 390 + 391 + activity: Required<ApiV1UsersMeGetResponse>["activity"]; 392 + guilds: { 393 + guildId: string; 394 + activity: Required<ApiV1UsersMeGetResponse>["activity"]; 395 + }[]; 396 + } 397 + 398 + export interface ApiV1GuildsModulesNotificationsGetResponse { 399 + id: string; 400 + guildId: string; 401 + channelId: string; 402 + roleId: string | null; 403 + 404 + type: 0; 405 + creatorId: string; 406 + 407 + message: { 408 + content: string | null; 409 + embed?: GuildEmbed; 410 + }; 411 + 412 + createdAt: Date; 413 + 414 + creator: { 415 + id: string; 416 + username: string; 417 + customUrl: string; 418 + avatarUrl: string; 419 + subs: string; 420 + videos: string; 421 + views: string; 422 + } 423 + 424 + } 425 + export interface ApiV1UsersMeRankEmojiPutResponse { 426 + id: string; 427 + url: string; 428 + } 429 + 430 + export interface ApiV1UsersMeRankEmojiDeleteResponse { 431 + id: null; 432 + url: null; 433 + } 434 + 435 + export interface PronounsResponse { 436 + status: number; 437 + content: string[]; 438 + } 439 + 440 + export interface NekosticResponse { 441 + event: string; 442 + name: string; 443 + uses: number 444 + users: number; 445 + snapshot: string; 435 446 }
+39
utils/average-color.ts
··· 1 + import sharp from "sharp"; 2 + 3 + const cache = new Map<string, string>(); 4 + 5 + export default async function getAverageColor(url: string) { 6 + 7 + const cached = cache.get(url); 8 + if (cached) return cached; 9 + 10 + const { data, info } = await sharp( 11 + await (await fetch(url)).arrayBuffer() 12 + ) 13 + .raw() 14 + .toBuffer({ resolveWithObject: true }); 15 + 16 + const { width, height } = info; 17 + const pixelCount = width * height; 18 + 19 + let redSum = 0; 20 + let greenSum = 0; 21 + let blueSum = 0; 22 + 23 + for (let i = 0; i < data.length; i += 4) { 24 + redSum += data[i]; 25 + greenSum += data[i + 1]; 26 + blueSum += data[i + 2]; 27 + } 28 + 29 + const rgb = { 30 + r: Math.round(redSum / pixelCount), 31 + g: Math.round(greenSum / pixelCount), 32 + b: Math.round(blueSum / pixelCount) 33 + }; 34 + 35 + const hex = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}` 36 + cache.set(url, hex); 37 + 38 + return hex; 39 + }
+5
utils/sleep.ts
··· 1 + export default function sleep(ms: number) { 2 + return new Promise((resolve) => { 3 + setTimeout(resolve, ms); 4 + }); 5 + }