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.

update to nextjs 15

Luna a11b6f31 088dad1d

+558 -564
+9 -11
app/(home)/debug/page.tsx
··· 43 43 }; 44 44 }; 45 45 46 - export default function Home() { 46 + export default async function Home() { 47 47 const headerList: { name: string, value: string }[] = []; 48 - for (const [key, value] of headers().entries()) { 48 + for (const [key, value] of (await headers()).entries()) { 49 49 headerList.push({ name: key, value }); 50 50 } 51 51 52 - if (cookies().get("devTools")?.value !== "true") { 52 + if ((await cookies()).get("devTools")?.value !== "true") { 53 53 return ( 54 54 <Box 55 55 className="relative mb-64 mt-12" ··· 67 67 async function deleteCookie(formData: FormData) { 68 68 "use server"; 69 69 70 - const cookieStore = cookies(); 70 + const jar = await cookies(); 71 71 72 72 function del(name: string) { 73 - cookieStore.set( 73 + jar.set( 74 74 name, 75 75 "", 76 76 { ··· 87 87 return; 88 88 } 89 89 90 - const cookieNames = cookieStore.getAll(); 90 + const cookieNames = jar.getAll(); 91 91 for (const cookie of cookieNames) { 92 92 if (cookie.name !== "devTools") del(cookie.name); 93 93 } 94 94 } 95 95 96 96 return ( 97 - <div className="md:flex gap-8"> 98 - 97 + (<div className="md:flex gap-8"> 99 98 <div className="space-y-10"> 100 99 <Panel 101 100 name="Cookies 🍪" 102 - items={cookies().getAll()} 101 + items={(await cookies()).getAll()} 103 102 action={(cookie) => ( 104 103 <form action={deleteCookie}> 105 104 <ServerButton ··· 135 134 items={headerList} 136 135 /> 137 136 </div> 138 - 139 137 <Shiggy className="mt-auto h-52" /> 140 - </div> 138 + </div>) 141 139 ); 142 140 }
+3 -2
app/[pathname]/page.tsx
··· 1 1 import { notFound, redirect } from "next/navigation"; 2 2 3 3 interface Props { 4 - params: { pathname: string } 4 + params: Promise<{ pathname: string }> 5 5 } 6 6 7 7 const fetchOptions = { next: { revalidate: 60 * 60 } }; 8 8 const utm = "?utm_source=wamellow.com&utm_medium=redirect"; 9 9 10 10 export default async function Home({ params }: Props) { 11 + const { pathname } = await params; 11 12 12 - switch (params.pathname) { 13 + switch (pathname) { 13 14 case "support": return redirect("https://discord.com/invite/DNyyA2HFM9"); 14 15 case "vote": return redirect("https://top.gg/bot/1125449347451068437/vote" + utm); 15 16 case "wumpus": return redirect("https://wumpus.store/bot/1125449347451068437" + utm);
+11 -13
app/ai-gallery/(home)/page.tsx
··· 11 11 import Pagination from "./pagination.component"; 12 12 13 13 interface Props { 14 - searchParams: { 14 + searchParams: Promise<{ 15 15 page: string; 16 16 model: string; 17 17 nsfw: string 18 - }; 18 + }>; 19 19 } 20 20 21 21 export const revalidate = 3600; 22 22 23 - export default async function Home({ 24 - searchParams 25 - }: Props) { 23 + export default async function Home({ searchParams }: Props) { 24 + const { page, model, nsfw } = await searchParams; 25 + 26 26 const uploads = await getUploads({ 27 - page: parseInt(searchParams.page || "1"), 28 - model: searchParams.model, 29 - nsfw: searchParams.nsfw === "true" 27 + page: parseInt(page || "1"), 28 + model: model, 29 + nsfw: nsfw === "true" 30 30 }); 31 31 32 32 if ("message" in uploads) { ··· 61 61 > 62 62 Images that were generated using the /image Ai in discord with Wamellow. 63 63 </div> 64 - <SearchFilter 65 - searchParams={searchParams} 66 - /> 64 + <SearchFilter searchParams={{ page, model, nsfw }} /> 67 65 </div> 68 66 69 67 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-4"> ··· 104 102 </div> 105 103 106 104 <Pagination 107 - key={searchParams.model} 108 - searchParams={searchParams} 105 + key={model} 106 + searchParams={{ page, model }} 109 107 pages={uploads.pagination.pages} 110 108 /> 111 109
+11 -12
app/ai-gallery/[uploadId]/layout.tsx
··· 18 18 import Side from "./side.component"; 19 19 20 20 export interface Props { 21 - params: { uploadId: string }; 21 + params: Promise<{ uploadId: string }>; 22 22 children: React.ReactNode; 23 23 } 24 24 25 25 export const revalidate = 3600; 26 26 27 - export const generateMetadata = async ({ 28 - params 29 - }: Props): Promise<Metadata> => { 30 - const upload = await getUpload(params.uploadId); 27 + export const generateMetadata = async ({ params }: Props): Promise<Metadata> => { 28 + const { uploadId } = await params; 29 + 30 + const upload = await getUpload(uploadId); 31 31 const prompt = "prompt" in upload 32 32 ? truncate(upload.prompt.split(" ").map((str) => str.replace(/^\w/, (char) => char.toUpperCase())).join(" "), 24) 33 33 : null; ··· 35 35 const title = prompt ? `${prompt} - /image Ai` : "Free /image Ai for Discord"; 36 36 const description = `Amazing AI generated images ${"model" in upload ? `using the ${upload.model}` : ""}, created using Wamellow's versatile /image command for Discord.`.replace(/ +/g, " "); 37 37 const images = "id" in upload ? `https://r2.wamellow.com/ai-image/${upload.id}.webp` : `${getBaseUrl()}/waya-v3.webp`; 38 - const url = getCanonicalUrl("ai-gallery", params.uploadId); 38 + const url = getCanonicalUrl("ai-gallery", uploadId); 39 39 40 40 return { 41 41 title, ··· 60 60 }; 61 61 }; 62 62 63 - export default async function RootLayout({ 64 - params, 65 - children 66 - }: Props) { 67 - const upload = await getUpload(params.uploadId); 63 + export default async function RootLayout({ params, children }: Props) { 64 + const { uploadId } = await params; 65 + 66 + const upload = await getUpload(uploadId); 68 67 69 68 const [guild, uploads, analytics] = await Promise.all([ 70 69 "guildId" in upload ? getGuild(upload.guildId) : undefined, 71 70 "model" in upload ? getUploads({ query: upload.prompt, nsfw: upload.nsfw }) : undefined, 72 - getPageAnalytics("/ai-gallery/" + params.uploadId) 71 + getPageAnalytics("/ai-gallery/" + uploadId) 73 72 ]); 74 73 75 74 return (
+6 -6
app/ai-gallery/[uploadId]/page.tsx
··· 6 6 import { getUpload } from "../api"; 7 7 8 8 export interface Props { 9 - params: { uploadId: string }; 9 + params: Promise<{ uploadId: string }>; 10 10 } 11 11 12 - export const revalidate = 60 * 60; 12 + export const revalidate = 3600; 13 13 14 - export default async function Home({ 15 - params 16 - }: Props) { 17 - const upload = await getUpload(params.uploadId); 14 + export default async function Home({ params }: Props) { 15 + const { uploadId } = await params; 16 + 17 + const upload = await getUpload(uploadId); 18 18 if (!upload || "statusCode" in upload) return; 19 19 20 20 const src = `https://r2.wamellow.com/ai-image/${upload.id}.webp`;
+4 -5
app/ai-gallery/generate/layout.tsx
··· 7 7 import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; 8 8 9 9 export interface Props { 10 - params: { uploadId: string }; 10 + params: Promise<{ uploadId: string }>; 11 11 children: React.ReactNode; 12 12 } 13 13 14 14 export const revalidate = 3600; 15 15 16 - export const generateMetadata = async ({ 17 - params 18 - }: Props): Promise<Metadata> => { 16 + export const generateMetadata = async ({ params }: Props): Promise<Metadata> => { 17 + const { uploadId } = await params; 19 18 20 19 const title = "Generate images with your Intel® Arc™ A-Series GPU"; 21 20 const description = "Generate AI images using your Intel® Arc™ A-Series GPU with Luna-devv/intel-arc-ai installed and running locally on your machine or network."; 22 21 const images = `${getBaseUrl()}/waya-v3.webp?v=2`; 23 - const url = getCanonicalUrl("ai-gallery", params.uploadId); 22 + const url = getCanonicalUrl("ai-gallery", uploadId); 24 23 25 24 return { 26 25 title,
+4 -4
app/dashboard/[guildId]/layout.tsx
··· 58 58 { 59 59 enabled: !!guild?.id, 60 60 onSettled: (d) => { 61 - if (!d||"message" in d) { 61 + if (!d || "message" in d) { 62 62 setError(d?.message || "Failed to fetch channels."); 63 63 return; 64 64 } ··· 80 80 { 81 81 enabled: !!guild?.id, 82 82 onSettled: (d) => { 83 - if (!d||"message" in d) { 83 + if (!d || "message" in d) { 84 84 setError(d?.message || "Failed to fetch roles."); 85 85 return; 86 86 } ··· 102 102 { 103 103 enabled: !!guild?.id, 104 104 onSettled: (d) => { 105 - if (!d||"message" in d) { 105 + if (!d || "message" in d) { 106 106 setError(d?.message || "Failed to fetch emojis."); 107 107 return; 108 108 } ··· 143 143 </Button> 144 144 {isDevMode && 145 145 <CopyToClipboardButton 146 - text={getCanonicalUrl("leaderboard", params.guildId.toString())} 146 + text={getCanonicalUrl("leaderboard", params.guildId?.toString() as string)} 147 147 items={[ 148 148 { icon: <HiShare />, name: "Copy page url", description: "Creates a link to this specific page", text: getCanonicalUrl(...path.split("/").slice(1)) }, 149 149 { icon: <HiCursorClick />, name: "Copy dash-to url", description: "Creates a dash-to link to the current tab", text: getCanonicalUrl(`dashboard?to=${path.split("/dashboard/")[1].split("/")[1] || "/"}`) }
+6 -6
app/dashboard/page.tsx
··· 1 1 import { redirect } from "next/navigation"; 2 2 3 - export default function Home({ 4 - searchParams 5 - }: { 6 - searchParams: Record<string, string>; 7 - }) { 8 - redirect(`/profile?${objectToSearchParams(searchParams)}`); 3 + interface Props { 4 + searchParams: Promise<Record<string, string>>; 5 + } 6 + 7 + export default async function Home({ searchParams }: Props) { 8 + redirect(`/profile?${objectToSearchParams(await searchParams)}`); 9 9 } 10 10 11 11 function objectToSearchParams(obj: Record<string, string>): string {
+8 -11
app/docs/[...pathname]/layout.tsx
··· 9 9 import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; 10 10 11 11 interface Props { 12 - params: { pathname: string[] }; 12 + params: Promise<{ pathname: string[] }>; 13 13 children: React.ReactNode; 14 14 } 15 15 16 - export const generateMetadata = async ({ 17 - params 18 - }: Props): Promise<Metadata> => { 19 - const meta = metadata.pages.find((page) => page.file === `${params.pathname.join("/").toLowerCase()}.md`); 16 + export const generateMetadata = async ({ params }: Props): Promise<Metadata> => { 17 + const { pathname } = await params; 18 + const meta = metadata.pages.find((page) => page.file === `${pathname.join("/").toLowerCase()}.md`); 20 19 21 20 const title = meta?.file === "index.md" 22 21 ? "Documentation" 23 22 : `${meta?.name} docs`; 24 23 25 - const url = getCanonicalUrl("docs", ...params.pathname); 24 + const url = getCanonicalUrl("docs", ...pathname); 26 25 const images = { 27 26 url: meta?.image || `${getBaseUrl()}/waya-v3.webp?v=2`, 28 27 alt: meta?.description, ··· 52 51 }; 53 52 }; 54 53 55 - export default async function RootLayout({ 56 - params, 57 - children 58 - }: Props) { 59 - const meta = metadata.pages.find((page) => page.file === `${params.pathname.join("/").toLowerCase()}.md`); 54 + export default async function RootLayout({ params, children }: Props) { 55 + const { pathname } = await params; 56 + const meta = metadata.pages.find((page) => page.file === `${pathname.join("/").toLowerCase()}.md`); 60 57 61 58 const title = meta?.file === "index.md" 62 59 ? "Wamellow"
+4 -3
app/docs/[...pathname]/page.tsx
··· 10 10 import SadWumpusPic from "@/public/sad-wumpus.gif"; 11 11 12 12 interface Props { 13 - params: { pathname: string[] }; 13 + params: Promise<{ pathname: string[] }>; 14 14 } 15 15 16 16 const PATH = `${process.cwd()}/public/docs` as const; 17 17 18 18 export default async function Home({ params }: Props) { 19 - const markdown = await readFile(`${PATH}/${params.pathname.join("/").toLowerCase()}.md`, "utf-8").catch(() => null); 20 - const meta = metadata.pages.find((page) => page.file === `${params.pathname.join("/").toLowerCase()}.md`); 19 + const { pathname } = await params; 20 + const markdown = await readFile(`${PATH}/${pathname.join("/").toLowerCase()}.md`, "utf-8").catch(() => null); 21 + const meta = metadata.pages.find((page) => page.file === `${pathname.join("/").toLowerCase()}.md`); 21 22 22 23 if (!markdown || !meta) { 23 24 return (
+3 -3
app/layout.tsx
··· 134 134 ); 135 135 } 136 136 137 - function NavBar() { 138 - const cookieStore = cookies(); 137 + async function NavBar() { 138 + const jar = await cookies(); 139 139 140 140 return ( 141 141 <nav className="p-4 flex items-center gap-2 text-base font-medium dark:text-neutral-300 text-neutral-700 select-none h-20"> ··· 170 170 </Link> 171 171 </div> 172 172 173 - {cookieStore.get("hasSession")?.value === "true" 173 + {jar.get("hasSession")?.value === "true" 174 174 ? <Header className="ml-auto" /> 175 175 : <LoginButton /> 176 176 }
+15 -16
app/leaderboard/[guildId]/layout.tsx
··· 14 14 import Side from "./side.component"; 15 15 16 16 export interface Props { 17 - params: { guildId: string }; 17 + params: Promise<{ guildId: string }>; 18 18 children: React.ReactNode; 19 19 } 20 20 21 21 export const revalidate = 3600; 22 22 23 - export const generateMetadata = async ({ 24 - params 25 - }: Props): Promise<Metadata> => { 26 - const guild = await getGuild(params.guildId); 23 + export const generateMetadata = async ({ params }: Props): Promise<Metadata> => { 24 + const { guildId } = await params; 25 + 26 + const guild = await getGuild(guildId); 27 27 const name = guild && "name" in guild ? guild.name : "Unknown"; 28 28 29 29 const title = `${name}'s Leaderboard`; 30 30 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.` : ""}`; 31 - const url = getCanonicalUrl("leaderboard", params.guildId); 31 + const url = getCanonicalUrl("leaderboard", guildId); 32 32 33 33 const date = new Date(); 34 34 const cacheQuery = `${date.getDate()}${date.getHours()}`; ··· 45 45 url, 46 46 type: "website", 47 47 images: { 48 - url: getCanonicalUrl("leaderboard", params.guildId, `open-graph.png?ca=${cacheQuery}`), 48 + url: getCanonicalUrl("leaderboard", guildId, `open-graph.png?ca=${cacheQuery}`), 49 49 width: 1200, 50 50 height: 630, 51 51 type: "image/png" ··· 56 56 title, 57 57 description, 58 58 images: { 59 - url: getCanonicalUrl("leaderboard", params.guildId, `open-graph.png?ca=${cacheQuery}`), 59 + url: getCanonicalUrl("leaderboard", guildId, `open-graph.png?ca=${cacheQuery}`), 60 60 alt: title 61 61 } 62 62 }, ··· 64 64 }; 65 65 }; 66 66 67 - export default async function RootLayout({ 68 - params, 69 - children 70 - }: Props) { 71 - const guildPromise = getGuild(params.guildId); 72 - const designPromise = getDesign(params.guildId); 73 - const paginationPromise = getPagination(params.guildId); 67 + export default async function RootLayout({ params, children }: Props) { 68 + const { guildId } = await params; 69 + 70 + const guildPromise = getGuild(guildId); 71 + const designPromise = getDesign(guildId); 72 + const paginationPromise = getPagination(guildId); 74 73 75 74 const [guild, design, pagination] = await Promise.all([guildPromise, designPromise, paginationPromise]).catch(() => []); 76 75 ··· 146 145 icon: <HiLink /> 147 146 } 148 147 ]} 149 - url={`/leaderboard/${params.guildId}`} 148 + url={`/leaderboard/${guildId}`} 150 149 searchParamName="type" 151 150 disabled={!guild} 152 151 >
+203 -203
app/leaderboard/[guildId]/member.component.tsx
··· 1 - import { Badge, Chip, CircularProgress } from "@nextui-org/react"; 2 - import { cookies } from "next/headers"; 3 - import Image from "next/image"; 4 - import Link from "next/link"; 5 - import { HiBadgeCheck } from "react-icons/hi"; 6 - 7 - import DiscordAppBadge from "@/components/discord/app-badge"; 8 - import ImageReduceMotion from "@/components/image-reduce-motion"; 9 - import { ApiV1GuildsTopmembersGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 10 - import getAverageColor from "@/utils/average-color"; 11 - import cn from "@/utils/cn"; 12 - import { intl } from "@/utils/numbers"; 13 - 14 - import Icon from "./icon.component"; 15 - 16 - export default async function Member( 17 - { 18 - index, 19 - type, 20 - member, 21 - members, 22 - pagination 23 - }: { 24 - index: number, 25 - type: "messages" | "voiceminutes" | "invites", 26 - member: ApiV1GuildsTopmembersGetResponse, 27 - members: ApiV1GuildsTopmembersGetResponse[], 28 - pagination: ApiV1GuildsTopmembersPaginationGetResponse, 29 - } 30 - ) { 31 - const emojiUrl = `https://r2.wamellow.com/emoji/${member.emoji}`; 32 - const averageColor = member.emoji 33 - ? await getAverageColor(emojiUrl + "?size=16") 34 - : null; 35 - 36 - async function publish() { 37 - "use server"; 38 - 39 - const cookieStore = cookies(); 40 - const currentCircular = cookieStore.get("lbc")?.value; 41 - 42 - cookieStore.set( 43 - "lbc", 44 - currentCircular !== "server" 45 - ? "server" 46 - : "next" 47 - ); 48 - } 49 - 50 - const cookieStore = cookies(); 51 - const currentCircular = cookieStore.get("lbc")?.value; 52 - 53 - return ( 54 - <div 55 - className={cn( 56 - "mb-4 rounded-xl p-3 flex items-center dark:bg-wamellow bg-wamellow-100 w-full overflow-hidden" 57 - )} 58 - style={averageColor ? { backgroundColor: averageColor + "50" } : {}} 59 - > 60 - <Badge 61 - className={cn( 62 - "size-6 font-bold", 63 - (() => { 64 - if (index === 1) return "bg-[#ffe671] text-[#ff9e03] border-2 border-[#1c1b1f]"; 65 - if (index === 2) return "bg-[#c1e5fb] text-[#6093ba] border-2 border-[#1c1b1f]"; 66 - if (index === 3) return "bg-[#f8c396] text-[#c66e04] border-2 border-[#1c1b1f]"; 67 - return "bg-[#1c1b1f]"; 68 - })() 69 - )} 70 - showOutline={false} 71 - content={ 72 - <span className="px-[3px]"> 73 - {intl.format(index)} 74 - </span> 75 - } 76 - size="sm" 77 - placement="bottom-left" 78 - > 79 - <ImageReduceMotion 80 - alt={`${member.username}'s profile picture`} 81 - className="rounded-full h-12 w-12 mr-3" 82 - url={`https://cdn.discordapp.com/avatars/${member.id}/${member.avatar}`} 83 - size={128} 84 - /> 85 - </Badge> 86 - 87 - <div className="w-full md:max-w-fit"> 88 - <div className="flex items-center gap-2"> 89 - <span className="text-xl font-medium dark:text-neutral-200 text-neutral-800 truncate"> 90 - {member.globalName || member.username || "Unknown user"} 91 - </span> 92 - {member.bot && 93 - <DiscordAppBadge /> 94 - } 95 - {member.id === "821472922140803112" && 96 - <UserBadge>Developer</UserBadge> 97 - } 98 - {member.id === "845287163712372756" && 99 - <UserBadge>WOMEN</UserBadge> 100 - } 101 - </div> 102 - <div className="text-sm dark:text-neutral-300 text-neutral-700 truncate"> 103 - @{member.username} 104 - </div> 105 - </div> 106 - 107 - {member.emoji && 108 - <div className="w-full hidden sm:block relative mr-6 -ml-48 md:-ml-6 lg:ml-6"> 109 - <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"> 110 - {new Array(12).fill(0).map((_, i) => 111 - <Emoji 112 - key={"emoji-" + member.id + i} 113 - index={i} 114 - emojiUrl={emojiUrl} 115 - /> 116 - )} 117 - </div> 118 - </div> 119 - } 120 - 121 - <div className="ml-auto flex text-xl font-medium dark:text-neutral-200 text-neutral-800"> 122 - <span className="mr-1 break-keep text-nowrap"> 123 - {type === "voiceminutes" 124 - ? member.activity?.formattedVoicetime 125 - : intl.format(member.activity?.[type || "messages"]) 126 - } 127 - </span> 128 - 129 - <Icon type={type} /> 130 - </div> 131 - 132 - <form action={publish}> 133 - <CircularProgress 134 - as="button" 135 - type="submit" 136 - className="ml-4" 137 - aria-label="progress" 138 - size="lg" 139 - color={ 140 - currentCircular === "next" 141 - ? "default" 142 - : "secondary" 143 - } 144 - classNames={{ 145 - svg: "drop-shadow-md" 146 - }} 147 - value={ 148 - currentCircular === "next" 149 - ? (member.activity[type] * 100) / (members[index - 1]?.activity[type] || 1) 150 - : (member.activity[type] * 100) / parseInt(pagination[type].total.toString()) 151 - || 100 152 - } 153 - showValueLabel={true} 154 - /> 155 - </form> 156 - 157 - </div > 158 - ); 159 - 160 - } 161 - 162 - function UserBadge({ 163 - children 164 - }: { 165 - children: React.ReactNode 166 - }) { 167 - return ( 168 - <Chip 169 - as={Link} 170 - color="secondary" 171 - href="/team?utm_source=wamellow.com&utm_medium=leaderboard" 172 - size="sm" 173 - startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />} 174 - target="_blank" 175 - variant="flat" 176 - > 177 - <span className="font-bold">{children}</span> 178 - </Chip> 179 - ); 180 - } 181 - 182 - function Emoji({ 183 - index, 184 - emojiUrl 185 - }: { 186 - index: number; 187 - emojiUrl: string; 188 - }) { 189 - return ( 190 - <Image 191 - alt="" 192 - className="rounded-xl relative size-8 aspect-square" 193 - draggable={false} 194 - height={64} 195 - src={emojiUrl} 196 - style={{ 197 - transform: `rotate(${(index / 2.3) * 360 + index}deg)`, 198 - top: `${index * 2 % 4}px`, 199 - left: `${index * 8 / 2}px` 200 - }} 201 - width={64} 202 - /> 203 - ); 1 + import { Badge, Chip, CircularProgress } from "@nextui-org/react"; 2 + import { cookies } from "next/headers"; 3 + import Image from "next/image"; 4 + import Link from "next/link"; 5 + import { HiBadgeCheck } from "react-icons/hi"; 6 + 7 + import DiscordAppBadge from "@/components/discord/app-badge"; 8 + import ImageReduceMotion from "@/components/image-reduce-motion"; 9 + import { ApiV1GuildsTopmembersGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 10 + import getAverageColor from "@/utils/average-color"; 11 + import cn from "@/utils/cn"; 12 + import { intl } from "@/utils/numbers"; 13 + 14 + import Icon from "./icon.component"; 15 + 16 + export default async function Member( 17 + { 18 + index, 19 + type, 20 + member, 21 + members, 22 + pagination 23 + }: { 24 + index: number, 25 + type: "messages" | "voiceminutes" | "invites", 26 + member: ApiV1GuildsTopmembersGetResponse, 27 + members: ApiV1GuildsTopmembersGetResponse[], 28 + pagination: ApiV1GuildsTopmembersPaginationGetResponse, 29 + } 30 + ) { 31 + const emojiUrl = `https://r2.wamellow.com/emoji/${member.emoji}`; 32 + const averageColor = member.emoji 33 + ? await getAverageColor(emojiUrl + "?size=16") 34 + : null; 35 + 36 + async function publish() { 37 + "use server"; 38 + 39 + const jar = await cookies(); 40 + const currentCircular = jar.get("lbc")?.value; 41 + 42 + jar.set( 43 + "lbc", 44 + currentCircular !== "server" 45 + ? "server" 46 + : "next" 47 + ); 48 + } 49 + 50 + const jar = await cookies(); 51 + const currentCircular = jar.get("lbc")?.value; 52 + 53 + return ( 54 + <div 55 + className={cn( 56 + "mb-4 rounded-xl p-3 flex items-center dark:bg-wamellow bg-wamellow-100 w-full overflow-hidden" 57 + )} 58 + style={averageColor ? { backgroundColor: averageColor + "50" } : {}} 59 + > 60 + <Badge 61 + className={cn( 62 + "size-6 font-bold", 63 + (() => { 64 + if (index === 1) return "bg-[#ffe671] text-[#ff9e03] border-2 border-[#1c1b1f]"; 65 + if (index === 2) return "bg-[#c1e5fb] text-[#6093ba] border-2 border-[#1c1b1f]"; 66 + if (index === 3) return "bg-[#f8c396] text-[#c66e04] border-2 border-[#1c1b1f]"; 67 + return "bg-[#1c1b1f]"; 68 + })() 69 + )} 70 + showOutline={false} 71 + content={ 72 + <span className="px-[3px]"> 73 + {intl.format(index)} 74 + </span> 75 + } 76 + size="sm" 77 + placement="bottom-left" 78 + > 79 + <ImageReduceMotion 80 + alt={`${member.username}'s profile picture`} 81 + className="rounded-full h-12 w-12 mr-3" 82 + url={`https://cdn.discordapp.com/avatars/${member.id}/${member.avatar}`} 83 + size={128} 84 + /> 85 + </Badge> 86 + 87 + <div className="w-full md:max-w-fit"> 88 + <div className="flex items-center gap-2"> 89 + <span className="text-xl font-medium dark:text-neutral-200 text-neutral-800 truncate"> 90 + {member.globalName || member.username || "Unknown user"} 91 + </span> 92 + {member.bot && 93 + <DiscordAppBadge /> 94 + } 95 + {member.id === "821472922140803112" && 96 + <UserBadge>Developer</UserBadge> 97 + } 98 + {member.id === "845287163712372756" && 99 + <UserBadge>WOMEN</UserBadge> 100 + } 101 + </div> 102 + <div className="text-sm dark:text-neutral-300 text-neutral-700 truncate"> 103 + @{member.username} 104 + </div> 105 + </div> 106 + 107 + {member.emoji && 108 + <div className="w-full hidden sm:block relative mr-6 -ml-48 md:-ml-6 lg:ml-6"> 109 + <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"> 110 + {new Array(12).fill(0).map((_, i) => 111 + <Emoji 112 + key={"emoji-" + member.id + i} 113 + index={i} 114 + emojiUrl={emojiUrl} 115 + /> 116 + )} 117 + </div> 118 + </div> 119 + } 120 + 121 + <div className="ml-auto flex text-xl font-medium dark:text-neutral-200 text-neutral-800"> 122 + <span className="mr-1 break-keep text-nowrap"> 123 + {type === "voiceminutes" 124 + ? member.activity?.formattedVoicetime 125 + : intl.format(member.activity?.[type || "messages"]) 126 + } 127 + </span> 128 + 129 + <Icon type={type} /> 130 + </div> 131 + 132 + <form action={publish}> 133 + <CircularProgress 134 + as="button" 135 + type="submit" 136 + className="ml-4" 137 + aria-label="progress" 138 + size="lg" 139 + color={ 140 + currentCircular === "next" 141 + ? "default" 142 + : "secondary" 143 + } 144 + classNames={{ 145 + svg: "drop-shadow-md" 146 + }} 147 + value={ 148 + currentCircular === "next" 149 + ? (member.activity[type] * 100) / (members[index - 1]?.activity[type] || 1) 150 + : (member.activity[type] * 100) / parseInt(pagination[type].total.toString()) 151 + || 100 152 + } 153 + showValueLabel={true} 154 + /> 155 + </form> 156 + 157 + </div > 158 + ); 159 + 160 + } 161 + 162 + function UserBadge({ 163 + children 164 + }: { 165 + children: React.ReactNode 166 + }) { 167 + return ( 168 + <Chip 169 + as={Link} 170 + color="secondary" 171 + href="/team?utm_source=wamellow.com&utm_medium=leaderboard" 172 + size="sm" 173 + startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />} 174 + target="_blank" 175 + variant="flat" 176 + > 177 + <span className="font-bold">{children}</span> 178 + </Chip> 179 + ); 180 + } 181 + 182 + function Emoji({ 183 + index, 184 + emojiUrl 185 + }: { 186 + index: number; 187 + emojiUrl: string; 188 + }) { 189 + return ( 190 + <Image 191 + alt="" 192 + className="rounded-xl relative size-8 aspect-square" 193 + draggable={false} 194 + height={64} 195 + src={emojiUrl} 196 + style={{ 197 + transform: `rotate(${(index / 2.3) * 360 + index}deg)`, 198 + top: `${index * 2 % 4}px`, 199 + left: `${index * 8 / 2}px` 200 + }} 201 + width={64} 202 + /> 203 + ); 204 204 }
+5 -6
app/leaderboard/[guildId]/open-graph.png/route.tsx
··· 16 16 17 17 export const revalidate = 3600; // 1 hour 18 18 19 - export async function GET( 20 - request: NextRequest, 21 - { params }: Props 22 - ) { 19 + export async function GET(request: NextRequest, { params }: Props) { 20 + const { guildId } = await params; 21 + 23 22 let type = request.nextUrl.searchParams.get("type"); 24 23 if (type !== "messages" && type !== "voiceminutes" && type !== "invites") type = "messages"; 25 24 26 - const guild = await getGuild(params.guildId); 27 - const members = await getTopMembers(params.guildId, { page: 1, type: "messages" }, { force: true }); 25 + const guild = await getGuild(guildId); 26 + const members = await getTopMembers(guildId, { page: 1, type: "messages" }, { force: true }); 28 27 29 28 const guildExists = guild && "id" in guild; 30 29
+29 -24
app/leaderboard/[guildId]/page.tsx
··· 12 12 13 13 export const revalidate = 3600; 14 14 15 - export default async function Home({ 16 - params, 17 - searchParams 18 - }: { 19 - params: { guildId: string }; 20 - searchParams: { page: string, type: "messages" | "voiceminutes" | "invites" }; 21 - }) { 22 - const cookieStore = cookies(); 15 + interface Props { 16 + params: Promise<{ guildId: string }>; 17 + searchParams: Promise<{ 18 + page: string; 19 + type: "messages" | "voiceminutes" | "invites"; 20 + }>; 21 + } 22 + 23 + export default async function Home({ searchParams, params }: Props) { 24 + const search = await searchParams; 25 + const { guildId } = await params; 26 + 27 + const jar = await cookies(); 23 28 24 - if (searchParams) searchParams.type ||= "messages"; 25 - const page = parseInt(searchParams.page || "1"); 29 + const type = search.type || "messages"; 30 + const page = parseInt(search.page || "1"); 26 31 27 - if (page !== 1 && !cookieStore.get("hasSession")) redirect("/login?callback=/leaderboard/%5BguildId%5D/messages%3Fpage=" + page); 32 + if (page !== 1 && !jar.get("hasSession")) redirect("/login?callback=/leaderboard/%5BguildId%5D/messages%3Fpage=" + page); 28 33 29 - const guildPromise = getGuild(params.guildId); 30 - const membersPromise = getTopMembers(params.guildId, { page, type: searchParams.type }); 31 - const designPromise = getDesign(params.guildId); 32 - const paginationPromise = getPagination(params.guildId); 34 + const guildPromise = getGuild(guildId); 35 + const membersPromise = getTopMembers(guildId, { page, type: type }); 36 + const designPromise = getDesign(guildId); 37 + const paginationPromise = getPagination(guildId); 33 38 34 39 const [guild, members, design, pagination] = await Promise.all([guildPromise, membersPromise, designPromise, paginationPromise]).catch(() => []); 35 40 ··· 58 63 59 64 const candisplay = guild.name && 60 65 ( 61 - searchParams.type === "messages" || 62 - searchParams.type === "voiceminutes" || 63 - searchParams.type === "invites" 66 + type === "messages" || 67 + type === "voiceminutes" || 68 + type === "invites" 64 69 ) && 65 - pagination[searchParams.type].pages >= parseInt(searchParams.page || "0"); 70 + pagination[type].pages >= page; 66 71 67 72 if (!candisplay) { 68 73 return ( ··· 96 101 97 102 return (<> 98 103 {members 99 - .sort((a, b) => (b.activity[searchParams.type] ?? 0) - (a.activity[searchParams.type] ?? 0)) 104 + .sort((a, b) => (b.activity[type] ?? 0) - (a.activity[type] ?? 0)) 100 105 .map((member, i) => 101 106 <Member 102 107 key={member.id} 103 108 member={member} 104 109 index={i + (page * 20) - 19} 105 - type={searchParams.type} 110 + type={type} 106 111 pagination={pagination} 107 112 members={members} 108 113 /> 109 114 )} 110 115 111 116 <Pagination 112 - key={searchParams.type} 113 - searchParams={searchParams} 114 - pages={pagination[searchParams.type].pages} 117 + key={type} 118 + searchParams={search} 119 + pages={pagination[type].pages} 115 120 /> 116 121 </>); 117 122 }
+9 -13
app/leaderboard/[guildId]/pagination.component.tsx
··· 6 6 7 7 import LoginButton from "@/components/login-button"; 8 8 9 - export default function Pagination( 10 - { 11 - searchParams, 12 - pages 13 - }: { 14 - searchParams: { 15 - page: string; 16 - type: string 17 - }; 18 - pages: number; 19 - } 20 - ) { 9 + interface Props { 10 + searchParams: { 11 + page: string; 12 + type: string; 13 + }; 14 + pages: number; 15 + } 16 + 17 + export default function Pagination({ searchParams, pages }: Props) { 21 18 const cookies = useCookies(); 22 19 const router = useRouter(); 23 - 24 20 25 21 if (!cookies.get("hasSession")) return ( 26 22 <LoginButton
+7 -7
app/login/route.ts
··· 13 13 14 14 export async function GET(request: Request) { 15 15 const { searchParams } = new URL(request.url); 16 - const cookieStore = cookies(); 16 + const jar = await cookies(); 17 17 18 18 const logout = searchParams.get("logout"); 19 - const session = cookieStore.get("session"); 19 + const session = jar.get("session"); 20 20 21 21 if (logout) { 22 22 ··· 35 35 return Response.json(data); 36 36 } 37 37 38 - cookieStore.set( 38 + jar.set( 39 39 "session", 40 40 "", 41 41 { ··· 44 44 } 45 45 ); 46 46 47 - cookieStore.set( 47 + jar.set( 48 48 "hasSession", 49 49 "", 50 50 { ··· 61 61 62 62 if (!code) { 63 63 const callback = searchParams.get("callback"); 64 - const lastpage = cookieStore.get("lastpage"); 64 + const lastpage = jar.get("lastpage"); 65 65 66 66 redirect(`${process.env.NEXT_PUBLIC_LOGIN}${invite ? "+bot" : ""}&state=${encodeURIComponent(callback || lastpage?.value || "/")}`); 67 67 } ··· 77 77 redirect(redirectUrl); 78 78 } 79 79 80 - cookieStore.set( 80 + jar.set( 81 81 "session", 82 82 res.session, 83 83 defaultCookieOptions 84 84 ); 85 85 86 - cookieStore.set( 86 + jar.set( 87 87 "hasSession", 88 88 "true", 89 89 {
+2 -2
app/login/spotify/route.ts
··· 5 5 6 6 export async function GET(request: Request) { 7 7 const { searchParams } = new URL(request.url); 8 - const cookieStore = cookies(); 8 + const jar = await cookies(); 9 9 10 10 const logout = searchParams.get("logout"); 11 - const session = cookieStore.get("session"); 11 + const session = jar.get("session"); 12 12 13 13 if (!session?.value) { 14 14 redirect("/login?callback=" + encodeURIComponent(`/login/spotify${logout === "true" ? "?logout=true" : ""}`));
+200 -198
app/passport/[guildId]/page.tsx
··· 1 - import { Metadata } from "next"; 2 - import Image from "next/image"; 3 - import Link from "next/link"; 4 - import { BsDiscord } from "react-icons/bs"; 5 - import { HiChartBar, HiCheck, HiLightningBolt, HiLockClosed, HiStar, HiUsers, HiX } from "react-icons/hi"; 6 - 7 - import ImageReduceMotion from "@/components/image-reduce-motion"; 8 - import { ListFeature } from "@/components/list"; 9 - import Notice, { NoticeType } from "@/components/notice"; 10 - import { OverviewLink } from "@/components/overview-link"; 11 - import { ServerButton } from "@/components/server-button"; 12 - import { getGuild } from "@/lib/api"; 13 - import paintPic from "@/public/paint.webp"; 14 - import decimalToRgb from "@/utils/decimalToRgb"; 15 - import { intl } from "@/utils/numbers"; 16 - import { getCanonicalUrl } from "@/utils/urls"; 17 - 18 - import { getPassport } from "./api"; 19 - import Verify from "./verify.component"; 20 - 21 - interface Props { 22 - params: { guildId: string }; 23 - searchParams: { page: string, type: string }; 24 - } 25 - 26 - export const revalidate = 60; 27 - 28 - export const generateMetadata = async ({ 29 - params 30 - }: Props): Promise<Metadata> => { 31 - const guild = await getGuild(params.guildId); 32 - const name = guild && "name" in guild ? guild.name : "Unknown"; 33 - 34 - const title = `Verify in ${name}`; 35 - const description = `Easily verify yourself in ${name} with a simple and safe captcha in the web to gain access all channels.`; 36 - const url = getCanonicalUrl("passport", params.guildId); 37 - 38 - return { 39 - title, 40 - description, 41 - alternates: { 42 - canonical: url 43 - }, 44 - openGraph: { 45 - title, 46 - description, 47 - url, 48 - type: "website", 49 - images: guild && "icon" in guild && guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=256` : "/discord.png" 50 - }, 51 - twitter: { 52 - card: "summary", 53 - title, 54 - description 55 - } 56 - }; 57 - }; 58 - 59 - export default async function Home({ params }: Props) { 60 - const guildPromise = getGuild(params.guildId); 61 - const passportPromise = getPassport(params.guildId); 62 - 63 - const [guild, passport] = await Promise.all([guildPromise, passportPromise]); 64 - 65 - const guildExists = guild && "id" in guild; 66 - 67 - const backgroundRgb = typeof passport === "object" && "backgroundColor" in passport && passport.backgroundColor 68 - ? decimalToRgb(passport.backgroundColor || 0) 69 - : undefined; 70 - 71 - return ( 72 - <div className="w-full"> 73 - 74 - {backgroundRgb && 75 - <style> 76 - {` 77 - :root { 78 - --background-rgb: rgb(${backgroundRgb.r}, ${backgroundRgb.g}, ${backgroundRgb.b}); 79 - } 80 - `} 81 - </style> 82 - } 83 - 84 - {typeof passport === "object" && "message" in passport && 85 - <Notice type={NoticeType.Error} message={passport.message} /> 86 - } 87 - 88 - {guild && "id" in guild && guild?.id === "1125063180801036329" && 89 - <Notice type={NoticeType.Info} message="This is a demo server to test out passport verification." > 90 - <ServerButton 91 - as={Link} 92 - color="secondary" 93 - href="https://discord.gg/2nrK8DfjPt" 94 - target="_blank" 95 - startContent={<BsDiscord />} 96 - > 97 - Join Server 98 - </ServerButton> 99 - </Notice> 100 - } 101 - 102 - <div className="grid md:flex gap-6"> 103 - 104 - <div className="w-full md:max-w-[384px] overflow-hidden rounded-xl dark:bg-wamellow bg-wamellow-100 relative"> 105 - 106 - <Image 107 - alt="" 108 - className="w-full object-cover h-[216px]" 109 - src={guild && "banner" in guild && guild.banner ? `https://cdn.discordapp.com/banners/${guild?.id}/${guild?.banner}?size=512` : paintPic.src} 110 - width={960} 111 - height={540} 112 - /> 113 - <div className="absolute top-0 w-full h-[216px]" style={{ background: "linear-gradient(180deg, rgba(0,0,0,0) 50%, rgb(22, 19, 31) 100%)" }} /> 114 - 115 - <div 116 - className="text-lg flex gap-5 items-center absolute top-[146px] rounded-3xl z-20 left-[4px] md:left-1.5 py-4 px-5 backdrop-blur-3xl backdrop-brightness-90 shadow-md" 117 - > 118 - <ImageReduceMotion 119 - alt="Server icon" 120 - className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40" 121 - url={guildExists ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}` : "/discord"} 122 - size={128} 123 - /> 124 - 125 - <div className="flex flex-col gap-1"> 126 - <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium max-w-64 truncate"> 127 - {guildExists ? guild.name : "Unknown Server"} 128 - </div> 129 - <div className="text-sm font-semibold flex items-center gap-1"> 130 - <HiUsers /> {guildExists ? intl.format(guild?.memberCount) : 0} 131 - 132 - <Image src="https://cdn.discordapp.com/emojis/875797879401361408.webp?size=32" width={18} height={18} alt="boost icon" className="ml-2" /> 133 - <span className="ml-2">Level {guildExists ? guild?.premiumTier : 0}</span> 134 - </div> 135 - </div> 136 - </div> 137 - 138 - <div className="mx-4 mb-4 mt-10 font-medium"> 139 - 140 - <span className="text-sm font-bold dark:text-neutral-400 text-neutral-600">GET ACCESS TO</span> 141 - <ul> 142 - {[ 143 - "Secure server", 144 - `${guild && "memberCount" in guild ? intl.format(guild?.memberCount) : 0} members` 145 - ].map((name) => ( 146 - <li key={name} className="flex gap-1 items-center"> 147 - <HiCheck className="text-violet-400" /> 148 - {name} 149 - </li> 150 - ))} 151 - <li className="flex gap-1 items-center" title="The cake is a lie"> 152 - <HiX className="text-red-400" /> 153 - Cakes 154 - </li> 155 - </ul> 156 - 157 - { 158 - guild && "id" in guild && 159 - passport === true && 160 - <Verify 161 - guild={guild} 162 - /> 163 - } 164 - 165 - </div> 166 - 167 - </div> 168 - 169 - <div> 170 - 171 - <div className="w-full h-min overflow-hidden rounded-xl dark:bg-wamellow bg-wamellow-100 py-4 px-5"> 172 - 173 - <div className="mb-4 text-neutral-100 font-semibold text-xl">Modern, Simple, Wamellow 👋</div> 174 - <ListFeature 175 - items={[ 176 - { icon: <HiLockClosed />, title: "Secure", description: "Unrivaled user privacy with our top-notch verification systems.", color: 0xa84b56 }, 177 - { icon: <BsDiscord />, title: "Integration", description: "Unparalleled Discord integration, setting us apart from the rest.", color: 0x4752c4 }, 178 - { icon: <HiStar />, title: "Easy", description: "The most user-friendly and visually appealing verification process.", color: 0x7f43d8 }, 179 - { icon: <HiLightningBolt />, title: "Fast", description: "Welcome new members swiftly with the fastest verification method available.", color: 0xff9156 } 180 - ]} 181 - /> 182 - <div className="mt-4 text-sm text-neutral-500">*We actually have no idea what to put here</div> 183 - </div> 184 - 185 - <OverviewLink 186 - className="mt-6" 187 - title="View Leaderboard" 188 - message="Easily access and view the top chatters, voice timers, and inviters from this server." 189 - url={`/leaderboard/${params.guildId}`} 190 - icon={<HiChartBar />} 191 - /> 192 - 193 - </div> 194 - 195 - </div> 196 - 197 - </div> 198 - ); 1 + import { Metadata } from "next"; 2 + import Image from "next/image"; 3 + import Link from "next/link"; 4 + import { BsDiscord } from "react-icons/bs"; 5 + import { HiChartBar, HiCheck, HiLightningBolt, HiLockClosed, HiStar, HiUsers, HiX } from "react-icons/hi"; 6 + 7 + import ImageReduceMotion from "@/components/image-reduce-motion"; 8 + import { ListFeature } from "@/components/list"; 9 + import Notice, { NoticeType } from "@/components/notice"; 10 + import { OverviewLink } from "@/components/overview-link"; 11 + import { ServerButton } from "@/components/server-button"; 12 + import { getGuild } from "@/lib/api"; 13 + import paintPic from "@/public/paint.webp"; 14 + import decimalToRgb from "@/utils/decimalToRgb"; 15 + import { intl } from "@/utils/numbers"; 16 + import { getCanonicalUrl } from "@/utils/urls"; 17 + 18 + import { getPassport } from "./api"; 19 + import Verify from "./verify.component"; 20 + 21 + interface Props { 22 + params: Promise<{ guildId: string }>; 23 + searchParams: Promise<{ page: string, type: string }>; 24 + } 25 + 26 + export const revalidate = 60; 27 + 28 + export const generateMetadata = async ({ params }: Props): Promise<Metadata> => { 29 + const { guildId } = await params; 30 + 31 + const guild = await getGuild(guildId); 32 + const name = guild && "name" in guild ? guild.name : "Unknown"; 33 + 34 + const title = `Verify in ${name}`; 35 + const description = `Easily verify yourself in ${name} with a simple and safe captcha in the web to gain access all channels.`; 36 + const url = getCanonicalUrl("passport", guildId); 37 + 38 + return { 39 + title, 40 + description, 41 + alternates: { 42 + canonical: url 43 + }, 44 + openGraph: { 45 + title, 46 + description, 47 + url, 48 + type: "website", 49 + images: guild && "icon" in guild && guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=256` : "/discord.png" 50 + }, 51 + twitter: { 52 + card: "summary", 53 + title, 54 + description 55 + } 56 + }; 57 + }; 58 + 59 + export default async function Home({ params }: Props) { 60 + const { guildId } = await params; 61 + 62 + const guildPromise = getGuild(guildId); 63 + const passportPromise = getPassport(guildId); 64 + 65 + const [guild, passport] = await Promise.all([guildPromise, passportPromise]); 66 + 67 + const guildExists = guild && "id" in guild; 68 + 69 + const backgroundRgb = typeof passport === "object" && "backgroundColor" in passport && passport.backgroundColor 70 + ? decimalToRgb(passport.backgroundColor || 0) 71 + : undefined; 72 + 73 + return ( 74 + <div className="w-full"> 75 + 76 + {backgroundRgb && 77 + <style> 78 + {` 79 + :root { 80 + --background-rgb: rgb(${backgroundRgb.r}, ${backgroundRgb.g}, ${backgroundRgb.b}); 81 + } 82 + `} 83 + </style> 84 + } 85 + 86 + {typeof passport === "object" && "message" in passport && 87 + <Notice type={NoticeType.Error} message={passport.message} /> 88 + } 89 + 90 + {guild && "id" in guild && guild?.id === "1125063180801036329" && 91 + <Notice type={NoticeType.Info} message="This is a demo server to test out passport verification." > 92 + <ServerButton 93 + as={Link} 94 + color="secondary" 95 + href="https://discord.gg/2nrK8DfjPt" 96 + target="_blank" 97 + startContent={<BsDiscord />} 98 + > 99 + Join Server 100 + </ServerButton> 101 + </Notice> 102 + } 103 + 104 + <div className="grid md:flex gap-6"> 105 + 106 + <div className="w-full md:max-w-[384px] overflow-hidden rounded-xl dark:bg-wamellow bg-wamellow-100 relative"> 107 + 108 + <Image 109 + alt="" 110 + className="w-full object-cover h-[216px]" 111 + src={guild && "banner" in guild && guild.banner ? `https://cdn.discordapp.com/banners/${guild?.id}/${guild?.banner}?size=512` : paintPic.src} 112 + width={960} 113 + height={540} 114 + /> 115 + <div className="absolute top-0 w-full h-[216px]" style={{ background: "linear-gradient(180deg, rgba(0,0,0,0) 50%, rgb(22, 19, 31) 100%)" }} /> 116 + 117 + <div 118 + className="text-lg flex gap-5 items-center absolute top-[146px] rounded-3xl z-20 left-[4px] md:left-1.5 py-4 px-5 backdrop-blur-3xl backdrop-brightness-90 shadow-md" 119 + > 120 + <ImageReduceMotion 121 + alt="Server icon" 122 + className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40" 123 + url={guildExists ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}` : "/discord"} 124 + size={128} 125 + /> 126 + 127 + <div className="flex flex-col gap-1"> 128 + <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium max-w-64 truncate"> 129 + {guildExists ? guild.name : "Unknown Server"} 130 + </div> 131 + <div className="text-sm font-semibold flex items-center gap-1"> 132 + <HiUsers /> {guildExists ? intl.format(guild?.memberCount) : 0} 133 + 134 + <Image src="https://cdn.discordapp.com/emojis/875797879401361408.webp?size=32" width={18} height={18} alt="boost icon" className="ml-2" /> 135 + <span className="ml-2">Level {guildExists ? guild?.premiumTier : 0}</span> 136 + </div> 137 + </div> 138 + </div> 139 + 140 + <div className="mx-4 mb-4 mt-10 font-medium"> 141 + 142 + <span className="text-sm font-bold dark:text-neutral-400 text-neutral-600">GET ACCESS TO</span> 143 + <ul> 144 + {[ 145 + "Secure server", 146 + `${guild && "memberCount" in guild ? intl.format(guild?.memberCount) : 0} members` 147 + ].map((name) => ( 148 + <li key={name} className="flex gap-1 items-center"> 149 + <HiCheck className="text-violet-400" /> 150 + {name} 151 + </li> 152 + ))} 153 + <li className="flex gap-1 items-center" title="The cake is a lie"> 154 + <HiX className="text-red-400" /> 155 + Cakes 156 + </li> 157 + </ul> 158 + 159 + { 160 + guild && "id" in guild && 161 + passport === true && 162 + <Verify 163 + guild={guild} 164 + /> 165 + } 166 + 167 + </div> 168 + 169 + </div> 170 + 171 + <div> 172 + 173 + <div className="w-full h-min overflow-hidden rounded-xl dark:bg-wamellow bg-wamellow-100 py-4 px-5"> 174 + 175 + <div className="mb-4 text-neutral-100 font-semibold text-xl">Modern, Simple, Wamellow 👋</div> 176 + <ListFeature 177 + items={[ 178 + { icon: <HiLockClosed />, title: "Secure", description: "Unrivaled user privacy with our top-notch verification systems.", color: 0xa84b56 }, 179 + { icon: <BsDiscord />, title: "Integration", description: "Unparalleled Discord integration, setting us apart from the rest.", color: 0x4752c4 }, 180 + { icon: <HiStar />, title: "Easy", description: "The most user-friendly and visually appealing verification process.", color: 0x7f43d8 }, 181 + { icon: <HiLightningBolt />, title: "Fast", description: "Welcome new members swiftly with the fastest verification method available.", color: 0xff9156 } 182 + ]} 183 + /> 184 + <div className="mt-4 text-sm text-neutral-500">*We actually have no idea what to put here</div> 185 + </div> 186 + 187 + <OverviewLink 188 + className="mt-6" 189 + title="View Leaderboard" 190 + message="Easily access and view the top chatters, voice timers, and inviters from this server." 191 + url={`/leaderboard/${guildId}`} 192 + icon={<HiChartBar />} 193 + /> 194 + 195 + </div> 196 + 197 + </div> 198 + 199 + </div> 200 + ); 199 201 }
+9 -6
app/profile/spotify/page.tsx
··· 1 1 "use client"; 2 + 2 3 import Image from "next/image"; 3 4 import Link from "next/link"; 5 + import { use } from "react"; 4 6 import { BsSpotify } from "react-icons/bs"; 5 7 import { useQuery } from "react-query"; 6 8 ··· 13 15 import SadWumpusPic from "@/public/sad-wumpus.gif"; 14 16 import { ApiV1UsersMeConnectionsSpotifyGetResponse } from "@/typings"; 15 17 16 - export default function Home({ 17 - searchParams 18 - }: { 19 - searchParams: { spotify_login_success?: string } 20 - }) { 18 + interface Props { 19 + searchParams: Promise<{ spotify_login_success?: string }> 20 + } 21 + 22 + export default function Home({ searchParams }: Props) { 23 + const search = use(searchParams); 21 24 const user = userStore((s) => s); 22 25 23 26 const url = "/users/@me/connections/spotify" as const; ··· 80 83 > 81 84 Not you? 82 85 </Link> 83 - {searchParams.spotify_login_success === "true" && data.displayName && <> 86 + {search.spotify_login_success === "true" && data.displayName && <> 84 87 <span className="mx-2 text-neutral-500">•</span> 85 88 <div className="text-green-500 duration-200">Link was successfull!</div> 86 89 </>}
+4 -6
app/user/[userId]/layout.tsx
··· 7 7 import Side from "./side.component"; 8 8 9 9 interface Props { 10 - params: { userId: string }; 10 + params: Promise<{ userId: string }>; 11 11 children: React.ReactNode; 12 12 } 13 13 14 - export default async function RootLayout({ 15 - params, 16 - children 17 - }: Props) { 18 - const user = await getUser(params.userId); 14 + export default async function RootLayout({ params, children }: Props) { 15 + const { userId } = await params; 19 16 17 + const user = await getUser(userId); 20 18 const userExists = user && "id" in user; 21 19 22 20 return (
+1 -1
components/discord/markdown.tsx
··· 1 1 import "./markdown.css"; 2 2 3 - import md from "@odiffey/discord-markdown"; 3 + import * as md from "@odiffey/discord-markdown"; 4 4 5 5 import cn from "@/utils/cn"; 6 6
+5 -6
components/markdown/index.tsx
··· 79 79 80 80 return ( 81 81 <ReactMarkdown 82 - // @ts-expect-error they broke types 83 82 rehypePlugins={[rehypeRaw]} 84 83 components={{ 85 84 h1: (props) => ( ··· 154 153 ), 155 154 156 155 table: (props) => <table className="mt-4 table-auto w-full divide-y-1 divide-wamellow overflow-scroll" {...props} />, 157 - th: ({ isHeader, ...props }) => <th className=" px-2 pb-2 font-medium text-neutral-800 dark:text-neutral-200 text-left" {...props} />, 158 - tr: ({ isHeader, ...props }) => <tr className="divide-x-1 divide-wamellow" {...props} />, 159 - td: ({ isHeader, ...props }) => <td className="px-2 py-1 divide-x-8 divide-wamellow break-all" {...props} />, 156 + th: (props) => <th className=" px-2 pb-2 font-medium text-neutral-800 dark:text-neutral-200 text-left" {...props} />, 157 + tr: (props) => <tr className="divide-x-1 divide-wamellow" {...props} />, 158 + td: (props) => <td className="px-2 py-1 divide-x-8 divide-wamellow break-all" {...props} />, 160 159 161 160 iframe: ({ className, ...props }) => { 162 161 if (ALLOWED_IFRAMES.some((url) => props.src?.startsWith(url))) { ··· 182 181 ); 183 182 }, 184 183 185 - ol: ({ ordered, ...props }) => <ol className="list-decimal list-inside space-y-1 marker:text-neutral-300/40 my-1" {...props} />, 186 - ul: ({ ordered, ...props }) => <ul className="list-disc list-inside space-y-1 marker:text-neutral-300/40 my-1" {...props} />, 184 + ol: (props) => <ol className="list-decimal list-inside space-y-1 marker:text-neutral-300/40 my-1" {...props} />, 185 + ul: (props) => <ul className="list-disc list-inside space-y-1 marker:text-neutral-300/40 my-1" {...props} />, 187 186 p: (props) => <span {...props} /> 188 187 189 188 }}