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.

refactor leaderboard pages

Luna 18eca834 c08eba66

+179 -150
-6
app/leaderboard/[guildId]/layout.tsx
··· 1 1 import { Image } from "@nextui-org/react"; 2 2 import { Metadata } from "next"; 3 - import { cookies } from "next/headers"; 4 3 import { HiAnnotation, HiLink, HiUsers, HiVolumeUp } from "react-icons/hi"; 5 4 6 5 import ImageReduceMotion from "@/components/image-reduce-motion"; ··· 69 68 params, 70 69 children 71 70 }: Props) { 72 - 73 71 const guildPromise = getGuild(params.guildId); 74 72 const designPromise = getDesign(params.guildId); 75 73 const paginationPromise = getPagination(params.guildId); ··· 81 79 const backgroundRgb = design && "backgroundColor" in design && design.backgroundColor 82 80 ? decimalToRgb(design.backgroundColor || 0) 83 81 : undefined; 84 - 85 - const cookieStore = cookies(); 86 - const currentCircular = cookieStore.get("lbc")?.value; 87 82 88 83 return ( 89 84 <div className="w-full"> ··· 177 172 guild={guild} 178 173 design={design} 179 174 pagination={pagination} 180 - currentCircular={currentCircular as undefined} 181 175 /> 182 176 </div> 183 177
+154
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 + ); 154 + }
+19 -140
app/leaderboard/[guildId]/page.tsx
··· 1 - import { Badge, Chip, CircularProgress } from "@nextui-org/react"; 2 1 import { cookies } from "next/headers"; 3 2 import Image from "next/image"; 4 - import Link from "next/link"; 5 - import { HiBadgeCheck } from "react-icons/hi"; 6 3 7 - import DiscordAppBadge from "@/components/discord/app-badge"; 8 - import ImageReduceMotion from "@/components/image-reduce-motion"; 9 4 import { AddButton, HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 10 5 import { getGuild } from "@/lib/api"; 11 6 import SadWumpusPic from "@/public/sad-wumpus.gif"; 12 - import cn from "@/utils/cn"; 13 - import { intl } from "@/utils/numbers"; 14 7 15 8 import { getDesign, getPagination, getTopMembers } from "./api"; 16 - import Icon from "./icon.component"; 17 9 import Pagination from "./pagination.component"; 18 - 19 - interface Props { 20 - params: { guildId: string }; 21 - searchParams: { page: string, type: "messages" | "voiceminutes" | "invites" }; 22 - } 10 + import { redirect } from "next/navigation"; 11 + import Member from "./member.component"; 23 12 24 13 export const revalidate = 60 * 60; 25 14 26 15 export default async function Home({ 27 16 params, 28 17 searchParams 29 - }: Props) { 30 - if (searchParams) searchParams.type ||= "messages"; 18 + }: { 19 + params: { guildId: string }; 20 + searchParams: { page: string, type: "messages" | "voiceminutes" | "invites" }; 21 + }) { 22 + const cookieStore = cookies(); 31 23 24 + if (searchParams) searchParams.type ||= "messages"; 32 25 const page = parseInt(searchParams.page || "1"); 26 + 27 + if (page !== 1 && !cookieStore.get("hasSession")) redirect("/login?callback=/leaderboard/%5BguildId%5D/messages%3Fpage=" + page) 33 28 34 29 const guildPromise = getGuild(params.guildId); 35 30 const membersPromise = getTopMembers(params.guildId, { page, type: searchParams.type }); ··· 56 51 <SupportButton /> 57 52 </>} 58 53 > 59 - <Image src={SadWumpusPic} alt="" height={141} width={124} /> 54 + <Image src={SadWumpusPic} alt="" height={141 * 1.5} width={124 * 1.5} /> 60 55 </ScreenMessage> 61 56 ); 62 57 } ··· 99 94 ); 100 95 } 101 96 102 - const key = "lbc"; 103 - 104 - async function publish() { 105 - "use server"; 106 - 107 - const cookieStore = cookies(); 108 - const currentCircular = cookieStore.get(key)?.value; 109 - 110 - cookies().set(key, currentCircular !== "server" ? "server" : "next"); 111 - } 112 - 113 - const cookieStore = cookies(); 114 - const currentCircular = cookieStore.get(key)?.value; 115 - 116 97 return ( 117 98 <> 118 99 {members 119 100 .sort((a, b) => (b.activity[searchParams.type] ?? 0) - (a.activity[searchParams.type] ?? 0)) 120 101 .map((member, i) => 121 - <div 122 - key={"leaderboard-" + searchParams.type + member.id + i} 123 - className="mb-4 rounded-xl p-3 flex items-center dark:bg-wamellow bg-wamellow-100 w-full overflow-hidden" 124 - > 125 - <Badge 126 - className={cn( 127 - "size-6 font-bold", 128 - (() => { 129 - const rank = i + ((page - 1) * 20) + 1; 130 - 131 - if (rank === 1) return "bg-[#ffe671] text-[#ff9e03] border-2 border-[#1c1b1f]"; 132 - if (rank === 2) return "bg-[#c1e5fb] text-[#6093ba] border-2 border-[#1c1b1f]"; 133 - if (rank === 3) return "bg-[#f8c396] text-[#c66e04] border-2 border-[#1c1b1f]"; 134 - return "bg-[#1c1b1f]"; 135 - })() 136 - )} 137 - showOutline={false} 138 - content={ 139 - <span className="px-[3px]"> 140 - {intl.format(i + ((page - 1) * 20) + 1)} 141 - </span> 142 - } 143 - size="sm" 144 - placement="bottom-left" 145 - > 146 - <ImageReduceMotion 147 - alt={`${member.username}'s profile picture`} 148 - className="rounded-full h-12 w-12 mr-3" 149 - url={`https://cdn.discordapp.com/avatars/${member.id}/${member.avatar}`} 150 - size={128} 151 - /> 152 - </Badge> 153 - 154 - <div className="w-full max-w-[calc(100%-17rem)]"> 155 - <div className="flex items-center gap-2"> 156 - <span className="text-xl font-medium dark:text-neutral-200 text-neutral-800 truncate"> 157 - {member.globalName || member.username || "Unknown user"} 158 - </span> 159 - {member.bot && 160 - <DiscordAppBadge /> 161 - } 162 - {member.id === "821472922140803112" && 163 - <UserBadge>Developer</UserBadge> 164 - } 165 - {member.id === "845287163712372756" && 166 - <UserBadge>WOMEN</UserBadge> 167 - } 168 - </div> 169 - <div className="text-sm dark:text-neutral-300 text-neutral-700 truncate"> 170 - @{member.username} 171 - </div> 172 - </div> 173 - 174 - <div className="ml-auto flex text-xl font-medium dark:text-neutral-200 text-neutral-800"> 175 - <span className="mr-1 break-keep"> 176 - {searchParams.type === "voiceminutes" 177 - ? member.activity?.formattedVoicetime 178 - : intl.format(member.activity?.[searchParams.type || "messages"]) 179 - } 180 - </span> 181 - 182 - <Icon type={searchParams.type} /> 183 - </div> 184 - 185 - <form action={publish}> 186 - <CircularProgress 187 - as="button" 188 - type="submit" 189 - className="ml-4" 190 - aria-label="progress" 191 - size="lg" 192 - color={ 193 - currentCircular === "next" 194 - ? "default" 195 - : "secondary" 196 - } 197 - classNames={{ 198 - svg: "drop-shadow-md" 199 - }} 200 - value={ 201 - currentCircular === "next" 202 - ? (member.activity[searchParams.type || "messages"] * 100) / (members[i - 1]?.activity[searchParams.type || "messages"] || 1) 203 - : (member.activity[searchParams.type || "messages"] * 100) / parseInt(pagination[searchParams.type || "messages"].total.toString()) 204 - || 100 205 - } 206 - showValueLabel={true} 207 - /> 208 - </form> 209 - 210 - </div> 102 + <Member 103 + key={member.id} 104 + member={member} 105 + index={i + (page * 20) - 19} 106 + type={searchParams.type} 107 + pagination={pagination} 108 + members={members} 109 + /> 211 110 )} 212 111 213 112 <Pagination ··· 216 115 pages={pagination[searchParams.type].pages} 217 116 /> 218 117 </> 219 - ); 220 - } 221 - 222 - function UserBadge({ 223 - children 224 - }: { 225 - children: React.ReactNode 226 - }) { 227 - return ( 228 - <Chip 229 - as={Link} 230 - color="secondary" 231 - href="/team?utm_source=wamellow.com&utm_medium=leaderboard" 232 - size="sm" 233 - startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />} 234 - target="_blank" 235 - variant="flat" 236 - > 237 - <span className="font-bold">{children}</span> 238 - </Chip> 239 118 ); 240 119 }
+6 -4
app/leaderboard/[guildId]/side.component.tsx
··· 20 20 export default function Side({ 21 21 guild, 22 22 design, 23 - pagination, 24 - currentCircular 23 + pagination 25 24 }: { 26 25 guild: ApiV1GuildsGetResponse | ApiError | undefined; 27 26 design: ApiV1GuildsModulesLeaderboardGetResponse | ApiError | undefined; 28 27 pagination: ApiV1GuildsTopmembersPaginationGetResponse | ApiError | undefined; 29 - currentCircular: "next" | "server" | undefined; 30 28 }) { 31 29 const cookies = useCookies(); 32 30 const router = useRouter(); ··· 154 152 Users are sorted from most to least active for each category, updates once per minute. 155 153 <br /> 156 154 <br /> 157 - The percentage {currentCircular !== "server" ? "indicates the gap in messages needed to surpass the next user" : "reflects the contribution of server activity from that user"}. 155 + The percentage { 156 + cookies.get("lbc") !== "server" 157 + ? "indicates the gap in messages needed to surpass the next user" 158 + : "reflects the contribution of server activity from that user" 159 + }. 158 160 </AccordionItem> 159 161 160 162 {guild && "id" in guild && guild?.inviteUrl ?