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

Configure Feed

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

add basic user pages

Luna 2550fcd1 32614dcc

+227 -7
+11
app/user/[userId]/api.ts
··· 1 + import { defaultFetchOptions } from "@/lib/api"; 2 + import { ApiV1UsersGetResponse, RouteErrorResponse } from "@/typings"; 3 + 4 + export async function getUser(userId: string): Promise<ApiV1UsersGetResponse | RouteErrorResponse | undefined> { 5 + const res = await fetch( 6 + `${process.env.NEXT_PUBLIC_API}/users/${userId}`, 7 + defaultFetchOptions 8 + ); 9 + 10 + return res.json(); 11 + }
+71
app/user/[userId]/layout.tsx
··· 1 + import ImageReduceMotion from "@/components/image-reduce-motion"; 2 + import { Image } from "@nextui-org/react"; 3 + import { getUser } from "./api"; 4 + import paintPic from "@/public/paint.webp"; 5 + import Side from "./side.component"; 6 + 7 + interface Props { 8 + params: { userId: string }; 9 + children: React.ReactNode; 10 + } 11 + 12 + export default async function RootLayout({ 13 + params, 14 + children 15 + }: Props) { 16 + const user = await getUser(params.userId); 17 + 18 + const userExists = user && "id" in user; 19 + 20 + return ( 21 + <div className="w-full"> 22 + 23 + <div className="relative mb-16 w-full"> 24 + <Image 25 + alt="" 26 + className="w-full object-cover" 27 + classNames={{ img: "h-36 md:h-64", blurredImg: "h-40 md:h-72 -top-5" }} 28 + isBlurred 29 + src={userExists && user.bannerUrl ? user.bannerUrl : paintPic.src} 30 + width={3840 / 2} 31 + height={2160 / 2} 32 + /> 33 + 34 + <div 35 + className="text-lg flex gap-5 items-center absolute top-[100px] md:top-[203px] left-[12px] md:left-8 py-4 px-5 rounded-3xl z-20 backdrop-blur-3xl backdrop-brightness-75 shadow-md" 36 + > 37 + <ImageReduceMotion 38 + alt="Server icon" 39 + className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40" 40 + url={userExists ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}` : "/discord"} 41 + size={128} 42 + /> 43 + 44 + <div className="flex flex-col gap-1"> 45 + <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium max-w-md truncate"> 46 + {userExists ? (user.globalName || user.username) : "Unknown User"} 47 + </div> 48 + <div className="text-sm font-semibold flex items-center gap-1"> 49 + @{userExists ? user.username : "Unknown User"} 50 + </div> 51 + </div> 52 + </div> 53 + </div> 54 + 55 + <div className="md:flex"> 56 + 57 + <div className="lg:w-3/4 md:w-2/3 w-full md:mr-6" > 58 + {children} 59 + </div> 60 + 61 + <div className="lg:w-1/4 md:w-1/3 mt-8 md:mt-0"> 62 + <Side 63 + user={user} 64 + /> 65 + </div> 66 + 67 + </div> 68 + 69 + </div > 70 + ); 71 + }
+19
app/user/[userId]/page.tsx
··· 1 + import { getUser } from "./api"; 2 + 3 + interface Props { 4 + params: { userId: string }; 5 + } 6 + 7 + export const revalidate = 60 * 60; 8 + 9 + export default async function Home({ 10 + params, 11 + }: Props) { 12 + const user = await getUser(params.userId); 13 + 14 + return ( 15 + <> 16 + <i>User has no bio yet</i> 17 + </> 18 + ) 19 + }
+90
app/user/[userId]/side.component.tsx
··· 1 + "use client"; 2 + 3 + import { ClientCountUp } from "@/components/counter"; 4 + import { ApiV1UsersGetResponse, RouteErrorResponse } from "@/typings"; 5 + import { Accordion, AccordionItem, Skeleton } from "@nextui-org/react"; 6 + import { useCookies } from "next-client-cookies"; 7 + import { HiAnnotation, HiLink, HiVolumeUp } from "react-icons/hi"; 8 + 9 + export default function Side({ 10 + user 11 + }: { 12 + user: ApiV1UsersGetResponse | RouteErrorResponse | undefined; 13 + }) { 14 + const cookies = useCookies(); 15 + const userExists = user && "id" in user; 16 + 17 + function Counters() { 18 + return ( 19 + <div className="md:ml-auto grid items-center gap-5 mt-6 md:mt-0"> 20 + <div> 21 + <div className="flex items-center gap-1 text-sm font-medium"> 22 + <HiVolumeUp /> 23 + Voice 24 + </div> 25 + 26 + {!userExists || !user?.activity 27 + ? <Skeleton className="rounded-md mt-1.5 w-20 h-6 mb-1" /> 28 + : 29 + <span className="text-2xl dark:text-neutral-100 text-neutral-900 font-medium"> 30 + {user.activity?.formattedVoicetime} 31 + </span> 32 + } 33 + </div> 34 + <div> 35 + <div className="flex items-center gap-1 text-sm font-medium"> 36 + <HiAnnotation /> 37 + Messages 38 + </div> 39 + 40 + {!userExists || !user?.activity 41 + ? <Skeleton className="rounded-md mt-1.5 w-12 h-6 mb-1" /> 42 + : 43 + <ClientCountUp 44 + className="text-2xl dark:text-neutral-100 text-neutral-900 font-medium" 45 + end={user.activity.messages || 0} 46 + /> 47 + } 48 + </div> 49 + <div> 50 + <div className="flex items-center gap-1 text-sm font-medium"> 51 + <HiLink /> 52 + Invites 53 + </div> 54 + 55 + {!userExists || !user?.activity 56 + ? <Skeleton className="rounded-md mt-1.5 w-8 h-6 mb-1" /> 57 + : 58 + <ClientCountUp 59 + className="text-2xl dark:text-neutral-100 text-neutral-900 font-medium" 60 + end={user.activity.invites || 0} 61 + /> 62 + } 63 + </div> 64 + </div> 65 + ) 66 + } 67 + 68 + return ( 69 + <div> 70 + 71 + <Accordion 72 + variant="shadow" 73 + className="bg-wamellow" 74 + selectionMode="multiple" 75 + defaultExpandedKeys={["1"]} 76 + disableAnimation={cookies.get("reduceMotions") === "true"} 77 + > 78 + <AccordionItem 79 + key="1" 80 + aria-label="user's activity" 81 + title={`${userExists ? user.username : "Unknown"}'s Activity`} 82 + classNames={{ content: "mb-2 space-y-4" }} 83 + subtitle="Activity from all servers" 84 + > 85 + <Counters /> 86 + </AccordionItem> 87 + </Accordion> 88 + </div> 89 + ) 90 + }
+18 -7
components/section.tsx
··· 1 + import cn from "@/utils/cn"; 1 2 import { Divider } from "@nextui-org/react"; 2 3 3 4 export function Section({ 4 5 title, 6 + showDivider = true, 5 7 children, 8 + 9 + className, 6 10 ...props 7 11 }: { 8 12 title: string; 13 + showDivider?: boolean; 9 14 children?: React.ReactNode; 10 15 } & React.HTMLAttributes<HTMLDivElement>) { 11 16 return ( 12 17 <> 13 - <Divider className="mt-12 mb-4" /> 18 + {showDivider && <Divider className="mt-12 mb-4" />} 14 19 15 - <div {...props}> 20 + <div 21 + className={cn("mb-3", className)} 22 + {...props} 23 + > 16 24 <h3 className="text-xl text-neutral-200">{title}</h3> 17 25 {children && 18 - <p className="dark:text-neutral-500 text-neutral-400 mb-3"> 26 + <p className="dark:text-neutral-500 text-neutral-400"> 19 27 {children} 20 28 </p> 21 29 } ··· 31 39 ...props 32 40 }: { 33 41 title: string; 34 - description: string; 42 + description?: string; 35 43 children: React.ReactNode; 36 44 } & React.HTMLAttributes<HTMLDivElement>) { 37 45 return ( 38 46 <div {...props}> 39 47 <h3 className="text-medium font-medium text-neutral-300 mt-5">{title}</h3> 40 48 <div className="dark:text-neutral-500 text-neutral-400 mb-3"> 41 - <div className="mb-3"> 42 - {description} 43 - </div> 49 + {description && 50 + <div className="mb-3"> 51 + {description} 52 + </div> 53 + } 54 + 44 55 {children} 45 56 </div> 46 57 </div>
+18
typings.ts
··· 367 367 bot?: boolean; 368 368 }; 369 369 } 370 + 371 + export interface ApiV1UsersGetResponse { 372 + id: string; 373 + username: string; 374 + globalName: string | null; 375 + avatar: string | null; 376 + 377 + bannerUrl: string | null; 378 + voteCount: number; 379 + likeCount: number; 380 + 381 + activity: Required<ApiV1MeGetResponse>["activity"]; 382 + guilds: { 383 + guildId: string; 384 + activity: Required<ApiV1MeGetResponse>["activity"]; 385 + }[]; 386 + } 387 + 370 388 export interface PronounsResponse { 371 389 status: number; 372 390 content: string[];