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.

new leaderboard pagination

Luna a887ba47 d8f911bb

+119 -110
-49
app/leaderboard/[guildId]/login.component.tsx
··· 1 - "use client"; 2 - import Link from "next/link"; 3 - import { FunctionComponent } from "react"; 4 - import { HiArrowLeft, HiArrowRight } from "react-icons/hi"; 5 - 6 - import { userStore } from "@/common/user"; 7 - import LoginButton from "@/components/login-button"; 8 - 9 - const PageComponent: FunctionComponent<{ searchParams: { page: string, type: string }, membersLength: number }> = ({ searchParams, membersLength }) => { 10 - const user = userStore((s) => s); 11 - 12 - if (!user?.__fetched) return <></>; 13 - 14 - if (!user?.id) return ( 15 - <LoginButton 16 - className="w-full text-center" 17 - addClassName="justify-center" 18 - message="Login to view more" 19 - /> 20 - ); 21 - 22 - return ( 23 - <> 24 - <Link 25 - href={(searchParams.page && (parseInt(searchParams.page) || 0) !== 0) ? `?page=${(parseInt(searchParams.page) || 0) - 1}${searchParams.type ? `&type=${searchParams.type}` : ""}` : ""} 26 - className={`dark:bg-wamellow bg-wamellow-160 hover:dark:bg-wamellow-light hover:bg-wamellow-160-light h-full w-14 rounded-l-md duration-100 flex items-center ${(!searchParams.page || (parseInt(searchParams.page) || 0) === 0) ? "cursor-not-allowed opacity-50" : "opacity-80 cursor-pointer"}`} 27 - > 28 - <HiArrowLeft className="m-auto text-2xl font-thin dark:text-neutral-300 text-neutral-700 p-1" /> 29 - </Link> 30 - 31 - <input 32 - className="outline-none text-center w-14 min-h-full dark:bg-wamellow bg-wamellow-160 font-semibold text-lg flex items-center text-neutral-500 rounded-none opacity-80" 33 - value={searchParams.page ?? 0} 34 - inputMode="numeric" 35 - disabled={true} 36 - /> 37 - 38 - <Link 39 - href={membersLength >= 10 ? `?page=${(parseInt(searchParams.page) || 0) + 1}${searchParams.type ? `&type=${searchParams.type}` : ""}` : ""} 40 - className={`dark:bg-wamellow bg-wamellow-160 hover:dark:bg-wamellow-light hover:bg-wamellow-160-light h-full w-14 rounded-r-md duration-100 flex items-center ${membersLength >= 10 ? "opacity-80 cursor-pointer" : "cursor-not-allowed opacity-50"}`} 41 - > 42 - <HiArrowRight className="m-auto text-2xl font-thin dark:text-neutral-300 text-neutral-700 p-1" /> 43 - </Link> 44 - </> 45 - ); 46 - 47 - }; 48 - 49 - export default PageComponent;
+66 -58
app/leaderboard/[guildId]/page.tsx
··· 11 11 import decimalToRgb from "@/utils/decimalToRgb"; 12 12 import { getCanonicalUrl } from "@/utils/urls"; 13 13 14 - import PageComponent from "./login.component"; 14 + import Pagination from "./pagination.component"; 15 15 import SideComponent from "./side.component"; 16 16 17 17 interface LeaderboardProps { params: { guildId: string }, searchParams: { page: string, type: "messages" | "voiceminutes" | "invites" } } ··· 41 41 async function getTopMembers(guildId: string, options: { page: number, type: string }): Promise<ApiV1GuildsTopmembersGetResponse[]> { 42 42 if (options.type && options.type !== "voiceminutes" && options.type !== "invites") return []; 43 43 44 - const res = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/top-members?type=${options.type || "messages"}&page=${options.page}`, { 44 + const res = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/top-members?type=${options.type || "messages"}&page=${options.page - 1}`, { 45 45 headers: { Authorization: process.env.API_SECRET as string }, 46 46 next: { revalidate: 60 } 47 47 }); ··· 50 50 return members; 51 51 } 52 52 53 + async function getPagination(guildId: string, options: { type: string }): Promise<{ pages: number; members: number }> { 54 + const res = await fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/top-members/pagination?type=${options.type || "messages"}`, { 55 + headers: { Authorization: process.env.API_SECRET as string }, 56 + next: { revalidate: 60 * 60 } 57 + }); 58 + 59 + const pagination = await res.json(); 60 + return pagination; 61 + } 62 + 53 63 export const generateMetadata = async ({ 54 64 params 55 65 }: LeaderboardProps): Promise<Metadata> => { ··· 83 93 84 94 export default async function Home({ params, searchParams }: LeaderboardProps) { 85 95 const guildPromise = getGuild(params.guildId); 86 - const membersPromise = getTopMembers(params.guildId, { page: parseInt(searchParams.page || "0"), type: searchParams.type }); 96 + const membersPromise = getTopMembers(params.guildId, { page: parseInt(searchParams.page || "1"), type: searchParams.type }); 87 97 const designPromise = getDesign(params.guildId); 98 + const paginationPromise = getPagination(params.guildId, { type: searchParams.type }); 88 99 89 - const [guild, members, design] = await Promise.all([guildPromise, membersPromise, designPromise]).catch(() => []); 100 + const [guild, members, design, pagination] = await Promise.all([guildPromise, membersPromise, designPromise, paginationPromise]).catch(() => []); 90 101 91 102 const backgroundRgb = decimalToRgb(design?.backgroundColor || 0); 92 103 const intl = new Intl.NumberFormat("en", { notation: "standard" }); 104 + 105 + const candisplay = guild?.id && Array.isArray(members) && (!searchParams.type || searchParams.type === "voiceminutes" || searchParams.type === "invites"); 93 106 94 107 return ( 95 108 <div className="w-full"> ··· 147 160 <h2 itemProp="name" className="display-hidden sr-only">Top 10 users in {guild?.name}</h2> 148 161 <link itemProp="itemListOrder" href="https://schema.org/ItemListOrderDescending" /> 149 162 150 - { 151 - (guild?.id && (!searchParams.type || searchParams.type === "voiceminutes" || searchParams.type === "invites")) ? 152 - (members || []).sort((a, b) => (b?.activity?.[searchParams.type] ?? 0) - (a?.activity?.[searchParams.type] ?? 0)).map((member, i) => 153 - <div 154 - key={"leaderboard-" + searchParams.type + member.id} 155 - className={cn("mb-4 rounded-md p-3 flex items-center", design?.backgroundColor ? "dark:bg-wamellow/60 bg-wamellow-100/60" : "dark:bg-wamellow bg-wamellow-100")} 156 - > 157 - 158 - <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" /> 159 - <div> 160 - <div className="text-xl font-medium dark:text-neutral-200 text-neutral-800">{member.globalName || member.username || "Unknown user"}</div> 161 - <div className="text-sm dark:text-neutral-300 text-neutral-700">@{member.username}</div> 162 - </div> 163 + {candisplay ? 164 + members.sort((a, b) => (b?.activity?.[searchParams.type] ?? 0) - (a?.activity?.[searchParams.type] ?? 0)).map((member, i) => 165 + <div 166 + key={"leaderboard-" + searchParams.type + member.id + i} 167 + className={cn("mb-4 rounded-md p-3 flex items-center", design?.backgroundColor ? "dark:bg-wamellow/60 bg-wamellow-100/60" : "dark:bg-wamellow bg-wamellow-100")} 168 + > 169 + <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" /> 170 + <div> 171 + <div className="text-xl font-medium dark:text-neutral-200 text-neutral-800">{member.globalName || member.username || "Unknown user"}</div> 172 + <div className="text-sm dark:text-neutral-300 text-neutral-700">@{member.username}</div> 173 + </div> 163 174 164 - <div className="ml-auto flex text-xl font-medium dark:text-neutral-200 text-neutral-800"> 165 - <span>{searchParams.type === "voiceminutes" ? member.activity?.formattedVoicetime : intl.format(member.activity?.[searchParams.type || "messages"])}</span> 175 + <div className="ml-auto flex text-xl font-medium dark:text-neutral-200 text-neutral-800"> 176 + <span>{searchParams.type === "voiceminutes" ? member.activity?.formattedVoicetime : intl.format(member.activity?.[searchParams.type || "messages"])}</span> 166 177 167 - <svg 168 - xmlns="http://www.w3.org/2000/svg" 169 - height="0.9em" 170 - viewBox={searchParams.type === "invites" ? "0 0 640 512" : "0 0 448 512"} 171 - className={cn("ml-1 relative", searchParams.type === "voiceminutes" && "ml-2")} 172 - style={{ top: searchParams.type === "messages" ? 0 : 4 }} 173 - fill="#d4d4d4" 174 - > 175 - {(searchParams.type === "messages" || !searchParams.type) && <path d="M448 296c0 66.3-53.7 120-120 120h-8c-17.7 0-32-14.3-32-32s14.3-32 32-32h8c30.9 0 56-25.1 56-56v-8H320c-35.3 0-64-28.7-64-64V160c0-35.3 28.7-64 64-64h64c35.3 0 64 28.7 64 64v32 32 72zm-256 0c0 66.3-53.7 120-120 120H64c-17.7 0-32-14.3-32-32s14.3-32 32-32h8c30.9 0 56-25.1 56-56v-8H64c-35.3 0-64-28.7-64-64V160c0-35.3 28.7-64 64-64h64c35.3 0 64 28.7 64 64v32 32 72z" />} 176 - {searchParams.type === "voiceminutes" && <path d="M301.1 34.8C312.6 40 320 51.4 320 64V448c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h67.8L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3zM412.6 181.5C434.1 199.1 448 225.9 448 256s-13.9 56.9-35.4 74.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C393.1 284.4 400 271 400 256s-6.9-28.4-17.7-37.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5z" />} 177 - {searchParams.type === "invites" && <path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z" />} 178 - </svg> 178 + <svg 179 + xmlns="http://www.w3.org/2000/svg" 180 + height="0.9em" 181 + viewBox={searchParams.type === "invites" ? "0 0 640 512" : "0 0 448 512"} 182 + className={cn("ml-1 relative", searchParams.type === "voiceminutes" && "ml-2")} 183 + style={{ top: searchParams.type === "messages" ? 0 : 4 }} 184 + fill="#d4d4d4" 185 + > 186 + {(searchParams.type === "messages" || !searchParams.type) && <path d="M448 296c0 66.3-53.7 120-120 120h-8c-17.7 0-32-14.3-32-32s14.3-32 32-32h8c30.9 0 56-25.1 56-56v-8H320c-35.3 0-64-28.7-64-64V160c0-35.3 28.7-64 64-64h64c35.3 0 64 28.7 64 64v32 32 72zm-256 0c0 66.3-53.7 120-120 120H64c-17.7 0-32-14.3-32-32s14.3-32 32-32h8c30.9 0 56-25.1 56-56v-8H64c-35.3 0-64-28.7-64-64V160c0-35.3 28.7-64 64-64h64c35.3 0 64 28.7 64 64v32 32 72z" />} 187 + {searchParams.type === "voiceminutes" && <path d="M301.1 34.8C312.6 40 320 51.4 320 64V448c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h67.8L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3zM412.6 181.5C434.1 199.1 448 225.9 448 256s-13.9 56.9-35.4 74.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C393.1 284.4 400 271 400 256s-6.9-28.4-17.7-37.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5z" />} 188 + {searchParams.type === "invites" && <path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z" />} 189 + </svg> 179 190 180 - </div> 191 + </div> 181 192 182 - <CircularProgress 183 - className="ml-4" 184 - aria-label="progress" 185 - size="lg" 186 - color="secondary" 187 - classNames={{ 188 - svg: "drop-shadow-md" 189 - }} 190 - value={(member.activity[searchParams.type || "messages"] * 100) / members[i - 1]?.activity[searchParams.type || "messages"] || 100} 191 - showValueLabel={true} 192 - /> 193 + <CircularProgress 194 + className="ml-4" 195 + aria-label="progress" 196 + size="lg" 197 + color="secondary" 198 + classNames={{ 199 + svg: "drop-shadow-md" 200 + }} 201 + value={(member.activity[searchParams.type || "messages"] * 100) / members[i - 1]?.activity[searchParams.type || "messages"] || 100} 202 + showValueLabel={true} 203 + /> 193 204 194 - </div> 195 - ) 196 - : 197 - <ScreenMessage 198 - title="Nothing to see here.." 199 - description="Seems like you got a little lost, huh?" 200 - href="/" 201 - button="Go back home" 202 - top="0rem" 203 - icon={<HiHome />} 204 - /> 205 + </div> 206 + ) 207 + : 208 + <ScreenMessage 209 + title="Nothing to see here.." 210 + description="Seems like you got a little lost, huh?" 211 + href="/" 212 + button="Go back home" 213 + top="0rem" 214 + icon={<HiHome />} 215 + /> 205 216 } 206 217 207 - <div className="flex h-10 mt-5"> 208 - {guild?.id && <PageComponent searchParams={searchParams} membersLength={members?.length || 0} />} 209 - </div> 210 - 218 + {guild?.id && <Pagination key={searchParams.type} guildId={params.guildId} searchParams={searchParams} data={pagination} />} 211 219 </div> 212 220 213 221 <div className="md:w-1/4 mt-8 md:mt-0">
+50
app/leaderboard/[guildId]/pagination.component.tsx
··· 1 + "use client"; 2 + 3 + import { Pagination as UiPagination } from "@nextui-org/react"; 4 + import { useRouter } from "next/navigation"; 5 + 6 + import { userStore } from "@/common/user"; 7 + import LoginButton from "@/components/login-button"; 8 + import { getCanonicalUrl } from "@/utils/urls"; 9 + 10 + function Pagination( 11 + { 12 + guildId, 13 + searchParams, 14 + data 15 + }: { 16 + guildId: string; 17 + searchParams: { page: string, type: string }; 18 + data: { pages: number; members: number }; 19 + } 20 + ) { 21 + const user = userStore((s) => s); 22 + const router = useRouter(); 23 + 24 + if (!user?.__fetched) return <></>; 25 + 26 + if (!user?.id) return ( 27 + <LoginButton 28 + addClassName="justify-center" 29 + message="Login to view more" 30 + /> 31 + ); 32 + 33 + return ( 34 + <UiPagination 35 + className="w-full" 36 + classNames={{ prev: "bg-wamellow", item: "bg-wamellow", next: "bg-wamellow" }} 37 + color="secondary" 38 + showControls 39 + total={data.pages} 40 + size="lg" 41 + page={parseInt(searchParams.page || "0")} 42 + onChange={(now) => { 43 + router.push(getCanonicalUrl("leaderboard", guildId, `?page=${now}${searchParams.type ? `&type=${searchParams.type}` : ""}`)); 44 + }} 45 + /> 46 + ); 47 + 48 + } 49 + 50 + export default Pagination;
+2 -1
app/leaderboard/[guildId]/side.component.tsx
··· 65 65 <span className="text-xl font-medium dark:text-neutral-100 text-neutral-900 px-1">How this works</span> 66 66 <hr className="my-2 dark:border-wamellow-light border-wamellow-100-light" /> 67 67 68 - <div className="text-sm px-1">Users are sorted from most to least active for each category, updates once per minute.</div> 68 + <div className="px-1">Users are sorted from most to least active for each category, updates once per minute.</div> 69 69 </div> 70 70 71 71 <Modal ··· 88 88 }} 89 89 > 90 90 <ErrorBanner message="Takes a few seconds to apply" type="info" removeButton /> 91 + Are you sure you want to delete the leaderboard? It will be gone forever, probably, who knows. 91 92 </Modal> 92 93 93 94 </div>
+1 -2
components/Header.tsx
··· 38 38 setLoggedin(!!localStorage.getItem("token")); 39 39 40 40 authorizeUser({ stateHook: setLoginstate, page: path }).then((_user) => { 41 - (_user || { __fetched: false }).__fetched = true; 42 - if (_user) userStore.setState(_user); 41 + userStore.setState(Object.assign(_user || {}, { __fetched: true })); 43 42 }); 44 43 45 44 const devToolsEnabled = localStorage.getItem("devToolsEnabled");