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.

fixing leaderboard types

Luna 9c7545df 639f7d2b

+118 -66
+5 -3
app/leaderboard/[guildId]/api.ts
··· 1 + import { ApiError } from "next/dist/server/api-utils"; 2 + 1 3 import { ApiV1GuildsModulesLeaderboardGetResponse, ApiV1GuildsTopmembersGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 2 4 3 - export async function getDesign(guildId: string): Promise<ApiV1GuildsModulesLeaderboardGetResponse> { 5 + export async function getDesign(guildId: string): Promise<ApiV1GuildsModulesLeaderboardGetResponse | ApiError | undefined> { 4 6 const res = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/leaderboard`, { 5 7 headers: { Authorization: process.env.API_SECRET as string }, 6 8 next: { revalidate: 60 * 60 } ··· 9 11 return res.json(); 10 12 } 11 13 12 - export async function getTopMembers(guildId: string, options: { page: number, type: string }): Promise<ApiV1GuildsTopmembersGetResponse[]> { 14 + export async function getTopMembers(guildId: string, options: { page: number, type: string }): Promise<ApiV1GuildsTopmembersGetResponse[] | ApiError | undefined> { 13 15 if (options.type !== "messages" && options.type !== "voiceminutes" && options.type !== "invites") return []; 14 16 15 17 const res = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/top-members?type=${options.type}&page=${options.page - 1}`, { ··· 20 22 return res.json(); 21 23 } 22 24 23 - export async function getPagination(guildId: string): Promise<ApiV1GuildsTopmembersPaginationGetResponse> { 25 + export async function getPagination(guildId: string): Promise<ApiV1GuildsTopmembersPaginationGetResponse | ApiError | undefined> { 24 26 const res = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/top-members/pagination`, { 25 27 headers: { Authorization: process.env.API_SECRET as string }, 26 28 next: { revalidate: 60 * 60 }
+37 -15
app/leaderboard/[guildId]/layout.tsx
··· 8 8 import { getGuild } from "@/lib/api"; 9 9 import paintPic from "@/public/paint.webp"; 10 10 import decimalToRgb from "@/utils/decimalToRgb"; 11 + import { intl } from "@/utils/numbers"; 11 12 import { getCanonicalUrl } from "@/utils/urls"; 12 13 13 14 import { getDesign, getPagination } from "./api"; ··· 24 25 params 25 26 }: Props): Promise<Metadata> => { 26 27 const guild = await getGuild(params.guildId); 28 + const name = guild && "name" in guild ? guild.name : "Unknown"; 27 29 28 - const title = `${guild?.name || "Unknown"}'s Leaderboard`; 29 - const description = `Discover the most active chatters, voice timers, and top inviters. ${guild?.name ? `Explore the community of the ${guild.name} discord server right from your web browser.` : ""}`; 30 + const title = `${name}'s Leaderboard`; 31 + const description = `Discover the most active chatters, voice timers, and top inviters. ${name ? `Explore the community of the ${name} discord server right from your web browser.` : ""}`; 30 32 const url = getCanonicalUrl("leaderboard", params.guildId); 31 33 32 34 const date = new Date(); ··· 53 55 twitter: { 54 56 card: "summary_large_image", 55 57 title, 56 - description 58 + description, 59 + images: { 60 + url: getCanonicalUrl("leaderboard", params.guildId, `open-graph.png?ca=${cacheQuery}`), 61 + alt: title 62 + } 57 63 }, 58 - robots: guild?.name ? "index, follow" : "noindex" 64 + robots: name ? "index, follow" : "noindex" 59 65 }; 60 66 }; 61 67 ··· 70 76 71 77 const [guild, design, pagination] = await Promise.all([guildPromise, designPromise, paginationPromise]).catch(() => []); 72 78 73 - const backgroundRgb = decimalToRgb(design?.backgroundColor || 0); 74 - const intl = new Intl.NumberFormat("en", { notation: "standard" }); 79 + const guildExists = guild && "id" in guild; 80 + 81 + const backgroundRgb = design && "backgroundColor" in design && design?.backgroundColor 82 + ? decimalToRgb(design?.backgroundColor || 0) 83 + : undefined; 75 84 76 85 const cookieStore = cookies(); 77 86 const currentCircular = cookieStore.get("lbc")?.value; ··· 79 88 return ( 80 89 <div className="w-full"> 81 90 82 - {design?.backgroundColor && 91 + {backgroundRgb && 83 92 <style> 84 93 {` 85 94 :root { ··· 95 104 className="w-full object-cover" 96 105 classNames={{ img: "h-36 md:h-64", blurredImg: "h-40 md:h-72 -top-5" }} 97 106 isBlurred 98 - src={design?.banner || paintPic.src} 107 + src={design && "banner" in design && design.banner ? design.banner : paintPic.src} 99 108 width={3840 / 2} 100 109 height={2160 / 2} 101 110 /> ··· 103 112 <div 104 113 className="text-lg flex gap-5 items-center absolute bottom-[-44px] md:bottom-[-34px] left-[12px] md:left-10 py-4 px-5 rounded-3xl z-20 backdrop-blur-3xl backdrop-brightness-75 shadow-md" 105 114 > 106 - <ImageReduceMotion url={`https://cdn.discordapp.com/icons/${guild?.id}/${guild?.icon}`} size={128} alt="Server icon" className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40" /> 115 + <ImageReduceMotion 116 + alt="Server icon" 117 + className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40" 118 + url={guildExists ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}` : "/discord"} 119 + size={128} 120 + /> 121 + 107 122 <div className="flex flex-col gap-1"> 108 - <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium">{guild?.name || "Unknown Server"}</div> 123 + <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium">{guildExists ? guild.name : "Unknown Server"}</div> 109 124 <div className="text-sm font-semibold flex items-center gap-1"> 110 - <HiUsers /> {intl.format(guild?.memberCount || 0)} 111 - <Image src="https://cdn.discordapp.com/emojis/875797879401361408.webp" width={18} height={18} alt="boost icon" className="ml-2" /> <span className="ml-2">Level {guild?.premiumTier || 0}</span> 125 + <HiUsers /> {guildExists ? intl.format(guild?.memberCount) : 0} 126 + 127 + <Image src="https://cdn.discordapp.com/emojis/875797879401361408.webp" width={18} height={18} alt="boost icon" className="ml-2" /> 128 + <span className="ml-2">Level {guildExists ? guild?.premiumTier : 0}</span> 112 129 </div> 113 130 </div> 114 131 </div> ··· 142 159 <div className="md:flex"> 143 160 144 161 <div itemScope itemType="https://schema.org/ItemList" className="md:w-3/4 md:mr-6"> 145 - <h2 itemProp="name" className="display-hidden sr-only">Top 10 users in {guild?.name}</h2> 162 + {guildExists && <h2 itemProp="name" className="display-hidden sr-only">Top 10 users in {guild?.name}</h2>} 146 163 <link itemProp="itemListOrder" href="https://schema.org/ItemListOrderDescending" /> 147 164 148 165 {children} ··· 150 167 </div> 151 168 152 169 <div className="md:w-1/4 mt-8 md:mt-0"> 153 - <Side guild={guild} design={design} pagination={pagination} currentCircular={currentCircular as undefined} /> 170 + <Side 171 + guild={guild} 172 + design={design} 173 + pagination={pagination} 174 + currentCircular={currentCircular as undefined} 175 + /> 154 176 </div> 155 177 156 178 </div> 157 179 158 - </div> 180 + </div > 159 181 ); 160 182 }
+36 -14
app/leaderboard/[guildId]/page.tsx
··· 7 7 import { AddButton, HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 8 8 import { getGuild } from "@/lib/api"; 9 9 import SadWumpusPic from "@/public/sad-wumpus.gif"; 10 + import { intl } from "@/utils/numbers"; 10 11 11 12 import { getDesign, getPagination, getTopMembers } from "./api"; 12 13 import Icon from "./icon.component"; ··· 30 31 const [guild, members, design, pagination] = await Promise.all([guildPromise, membersPromise, designPromise, paginationPromise]).catch(() => []); 31 32 32 33 let error = ""; 33 - if (guild && "message" in guild) error = guild.message as string; 34 - if ("message" in members) error = members.message as string; 35 - if ("message" in design) error = design.message as string; 36 - if ("message" in pagination) error = pagination.message as string; 34 + if (guild && "message" in guild) error = guild.message; 35 + if (members && "message" in members) error = members.message; 36 + if (design && "message" in design) error = design.message; 37 + if (pagination && "message" in pagination) error = pagination.message; 37 38 38 - if (error) { 39 + // wtf is this 40 + if (error || !guild || !members || !design || !pagination || "message" in guild || "message" in members || "message" in design || "message" in pagination) { 39 41 return ( 40 42 <ScreenMessage 41 43 top="0rem" ··· 51 53 ); 52 54 } 53 55 54 - const candisplay = guild?.name && (searchParams.type === "messages" || searchParams.type === "voiceminutes" || searchParams.type === "invites") && pagination[searchParams.type].pages >= parseInt(searchParams.page || "0"); 56 + const candisplay = guild.name && 57 + ( 58 + searchParams.type === "messages" || 59 + searchParams.type === "voiceminutes" || 60 + searchParams.type === "invites" 61 + ) && 62 + pagination[searchParams.type].pages >= parseInt(searchParams.page || "0"); 55 63 56 64 if (!candisplay) { 57 65 return ( ··· 69 77 ); 70 78 } 71 79 72 - if (!members.length) { 80 + if (!Array.isArray(members) || !members.length) { 73 81 return ( 74 82 <ScreenMessage 75 83 top="0rem" ··· 83 91 ); 84 92 } 85 93 86 - const intl = new Intl.NumberFormat("en", { notation: "standard" }); 87 94 const key = "lbc"; 88 95 89 96 async function publish() { ··· 105 112 key={"leaderboard-" + searchParams.type + member.id + i} 106 113 className="mb-4 rounded-xl p-3 flex items-center dark:bg-wamellow bg-wamellow-100" 107 114 > 108 - <ImageReduceMotion url={`https://cdn.discordapp.com/avatars/${member.id}/${member.avatar}`} size={128} alt={`${member.username}'s profile picture`} className="rounded-full h-12 w-12 mr-3" /> 115 + <ImageReduceMotion 116 + alt={`${member.username}'s profile picture`} 117 + className="rounded-full h-12 w-12 mr-3" 118 + url={`https://cdn.discordapp.com/avatars/${member.id}/${member.avatar}`} 119 + size={128} 120 + /> 121 + 109 122 <div> 110 123 <div className="flex items-center gap-2"> 111 124 <span className="text-xl font-medium dark:text-neutral-200 text-neutral-800">{member.globalName || member.username || "Unknown user"}</span> 112 125 {member.id === "821472922140803112" && 113 126 <Badge>Developer</Badge> 114 127 } 115 - {/* {member.id === "797012765352001557" && 128 + {member.id === "845287163712372756" && 116 129 <Badge> 117 - <Image alt="" className="h-6 w-32 rounded-md" height={24} src={RickPic} width={128} /> 130 + WOMEN 118 131 </Badge> 119 - } */} 132 + } 120 133 </div> 121 134 <span className="text-sm dark:text-neutral-300 text-neutral-700">@{member.username}</span> 122 135 </div> ··· 153 166 </div> 154 167 )} 155 168 156 - <Pagination key={searchParams.type} guildId={params.guildId} searchParams={searchParams} pages={pagination[searchParams.type].pages} /> 169 + <Pagination 170 + key={searchParams.type} 171 + guildId={params.guildId} 172 + searchParams={searchParams} 173 + pages={pagination[searchParams.type].pages} 174 + /> 157 175 </> 158 176 ); 159 177 } ··· 164 182 children: React.ReactNode 165 183 }) { 166 184 return ( 167 - <Chip color="secondary" size="sm" variant="flat" startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />}> 185 + <Chip 186 + color="secondary" 187 + size="sm" variant="flat" 188 + startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />} 189 + > 168 190 <span className="font-bold">{children}</span> 169 191 </Chip> 170 192 );
+34 -31
app/leaderboard/[guildId]/side.component.tsx
··· 13 13 import { CopyToClipboardButton } from "@/components/copy-to-clipboard"; 14 14 import Modal from "@/components/modal"; 15 15 import Notice, { NoticeType } from "@/components/notice"; 16 - import { ApiV1GuildsGetResponse, ApiV1GuildsModulesLeaderboardGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 16 + import { ApiError, ApiV1GuildsGetResponse, ApiV1GuildsModulesLeaderboardGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 17 + import { intl } from "@/utils/numbers"; 17 18 import { getCanonicalUrl } from "@/utils/urls"; 18 19 19 20 export default function Side({ ··· 22 23 pagination, 23 24 currentCircular 24 25 }: { 25 - guild: ApiV1GuildsGetResponse | undefined; 26 - design: ApiV1GuildsModulesLeaderboardGetResponse; 27 - pagination: ApiV1GuildsTopmembersPaginationGetResponse; 26 + guild: ApiV1GuildsGetResponse | ApiError | undefined; 27 + design: ApiV1GuildsModulesLeaderboardGetResponse | ApiError | undefined; 28 + pagination: ApiV1GuildsTopmembersPaginationGetResponse | ApiError | undefined; 28 29 currentCircular: "next" | "server" | undefined; 29 30 }) { 30 31 const web = webStore((w) => w); 31 32 const router = useRouter(); 32 33 33 34 const [modal, setModal] = useState(false); 34 - const intl = new Intl.NumberFormat("en", { notation: "standard" }); 35 35 36 36 return ( 37 37 <div className="flex flex-col gap-3"> 38 38 {!!design} 39 39 40 - {guild && 40 + {guild && "id" in guild && 41 41 <div className="flex gap-2 w-full"> 42 42 <CopyToClipboardButton 43 43 className="w-full !justify-start" 44 44 title="Share this page" 45 - text={getCanonicalUrl("leaderboard", guild?.id as string)} 45 + text={getCanonicalUrl("leaderboard", guild.id as string)} 46 46 icon={<HiShare />} 47 47 /> 48 48 <Tooltip content="Share on Reddit" delay={0} closeDelay={0} showArrow> ··· 68 68 </div> 69 69 } 70 70 71 - {guild?.inviteUrl && 71 + {guild && "inviteUrl" in guild && guild.inviteUrl && 72 72 <Button 73 73 as={Link} 74 74 className="w-full !justify-start" ··· 89 89 disableAnimation={web.reduceMotions} 90 90 > 91 91 92 - {web.devToolsEnabled ? 92 + {guild && "id" in guild && web.devToolsEnabled ? 93 93 <AccordionItem 94 94 key="1" 95 95 aria-label="admin tools" ··· 106 106 <Button 107 107 as={Link} 108 108 className="w-full !justify-start mt-2" 109 - href={getCanonicalUrl("dashboard", guild?.id as string)} 109 + href={getCanonicalUrl("dashboard", guild.id as string)} 110 110 startContent={<HiViewGridAdd />} 111 111 > 112 112 Dashboard ··· 128 128 The percentage {currentCircular !== "server" ? "indicates the gap in messages needed to surpass the next user" : "reflects the contribution of server activity from that user"}. 129 129 </AccordionItem> 130 130 131 - <AccordionItem 132 - key="3" 133 - aria-label="server activity" 134 - title="Server activity" 135 - classNames={{ content: "mb-2" }} 136 - > 137 - <div className="flex items-center gap-1"> 138 - <HiAnnotation className="mr-1" /> 139 - <span className="font-semibold">{intl.format(pagination.messages.total)}</span> messages 140 - </div> 141 - <div className="flex items-center gap-1"> 142 - <HiVolumeUp className="mr-1" /> 143 - <span className="font-semibold">{pagination.voiceminutes.total}</span> in voice 144 - </div> 145 - <div className="flex items-center gap-1"> 146 - <HiLink className="mr-1" /> 147 - <span className="font-semibold"> {intl.format(pagination.invites.total)}</span> invites 148 - </div> 149 - </AccordionItem> 131 + {pagination && "messages" in pagination ? 132 + <AccordionItem 133 + key="3" 134 + aria-label="server activity" 135 + title="Server activity" 136 + classNames={{ content: "mb-2" }} 137 + > 138 + <div className="flex items-center gap-1"> 139 + <HiAnnotation className="mr-1" /> 140 + <span className="font-semibold">{intl.format(pagination.messages.total)}</span> messages 141 + </div> 142 + <div className="flex items-center gap-1"> 143 + <HiVolumeUp className="mr-1" /> 144 + <span className="font-semibold">{pagination.voiceminutes.total}</span> in voice 145 + </div> 146 + <div className="flex items-center gap-1"> 147 + <HiLink className="mr-1" /> 148 + <span className="font-semibold"> {intl.format(pagination.invites.total)}</span> invites 149 + </div> 150 + </AccordionItem> 151 + : undefined as unknown as JSX.Element 152 + } 150 153 151 - {guild?.inviteUrl ? 154 + {guild && "id" in guild && guild?.inviteUrl ? 152 155 <AccordionItem 153 156 key="4" 154 157 aria-label="invite privacy" ··· 172 175 show={modal} 173 176 onClose={() => setModal(false)} 174 177 onSubmit={() => { 175 - return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guild?.id}/top-members`, { 178 + return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guild && "id" in guild ? guild?.id : ""}/top-members`, { 176 179 method: "DELETE", 177 180 headers: { 178 181 "Content-Type": "application/json",
+1 -1
components/screen-message.tsx
··· 53 53 54 54 <div className="mb-8 flex flex-col items-center text-center"> 55 55 <span className="text-4xl dark:text-neutral-100 text-neutral-900 font-semibold">{title}</span> <br /> 56 - <span className="text-lg dark:text-neutral-400 text-neutral-600 font-semibold -mt-2">{description}</span> 56 + <span className="text-lg dark:text-neutral-400 text-neutral-600 font-semibold max-w-xl">{description}</span> 57 57 </div> 58 58 59 59 {(button && props.href) &&
+4 -2
lib/api.ts
··· 1 - import { ApiV1GuildsGetResponse } from "@/typings"; 1 + import { ApiError, ApiV1GuildsGetResponse } from "@/typings"; 2 2 3 3 export const cacheOptions = { 4 4 cacheTime: 1000 * 60 * 5, 5 5 refetchOnWindowFocus: false, 6 6 refetchOnMount: false 7 7 }; 8 + 9 + export const defaultFetchOptions = { headers: { Authorization: process.env.API_SECRET as string }, next: { revalidate: 60 * 60 } }; 8 10 9 11 export async function getData<T>(path: string) { 10 12 const response = await fetch(`${process.env.NEXT_PUBLIC_API}${path}`, { ··· 16 18 return response.json() as Promise<T>; 17 19 } 18 20 19 - export async function getGuild(guildId?: string | null): Promise<ApiV1GuildsGetResponse | undefined> { 21 + export async function getGuild(guildId?: string | null): Promise<ApiV1GuildsGetResponse | ApiError | undefined> { 20 22 if (!guildId) return; 21 23 22 24 const res = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}`, {
+1
utils/numbers.ts
··· 1 + export const intl = new Intl.NumberFormat("en", { notation: "standard" });