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.

Merge Luna-devv/rm/nextui (#23)

Partially remove nextui in favour of shadcn

authored by

Luna and committed by
GitHub
c3004431 6e97efdc

+1355 -1735
+86 -63
app/(home)/bot/pronouns/layout.tsx
··· 2 2 import { Montserrat, Patrick_Hand } from "next/font/google"; 3 3 import Image from "next/image"; 4 4 import Link from "next/link"; 5 - import { BsDiscord } from "react-icons/bs"; 6 5 import { HiHome, HiUserAdd } from "react-icons/hi"; 7 6 8 - import { ClientButton } from "@/components/client"; 9 7 import Comment from "@/components/comment"; 10 8 import ImageGrid from "@/components/image-grid"; 9 + import { Button } from "@/components/ui/button"; 11 10 import { defaultFetchOptions } from "@/lib/api"; 12 11 import ArrowPic from "@/public/icons/arroww.webp"; 13 12 import type { ApiV1TopguildsGetResponse } from "@/typings"; ··· 57 56 .then((res) => res.json()) 58 57 .catch(() => null) as ApiV1TopguildsGetResponse[] | null; 59 58 60 - return ( 61 - <div> 59 + return (<> 60 + <div className="lg:text-7xl text-5xl flex font-semibold md:mb-6 mb-4 dark:text-neutral-100 text-neutral-900 break-words w-full"> 61 + <h1 className={montserrat.className}> 62 + <span className="bg-gradient-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent h-20 break-keep">Pronouns</span> 63 + {" in "} 64 + <span className="underline decoration-blurple break-keep">discord</span> 65 + </h1> 66 + </div> 62 67 63 - <div className="lg:text-7xl text-5xl flex font-semibold md:mb-6 mb-4 dark:text-neutral-100 text-neutral-900 break-words w-full"> 64 - <h1 className={montserrat.className}> 65 - <span className="bg-gradient-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent h-20 break-keep">Pronouns</span> 66 - {" in "} 67 - <span className="underline decoration-blurple break-keep">discord</span> 68 - </h1> 69 - </div> 70 - 71 - { 72 - topGuilds && 68 + { 69 + topGuilds && 73 70 <ImageGrid images={topGuilds 74 71 .sort((a, b) => b.memberCount - a.memberCount) 75 72 .map((guild) => ({ ··· 78 75 link: getCanonicalUrl("leaderboard", guild.id) 79 76 }))} 80 77 /> 81 - } 78 + } 82 79 83 - <div className="md:text-xl text-lg lg:flex w-full mt-4"> 84 - <div className="font-medium w-full grid grid-cols-2 md:flex flex-wrap h-min gap-2"> 85 - <ClientButton 86 - as={Link} 80 + <div className="md:text-xl text-lg lg:flex w-full mt-4"> 81 + <div className="font-medium w-full grid grid-cols-2 md:flex flex-wrap h-min gap-2"> 82 + <Button 83 + asChild 84 + className="w-1/2 lg:w-fit text-lg" 85 + > 86 + <Link 87 + prefetch={false} 87 88 href="/bot/pronouns" 88 89 > 89 90 <HiHome /> 90 - </ClientButton> 91 - <ClientButton 92 - as={Link} 91 + </Link> 92 + </Button> 93 + <Button 94 + asChild 95 + className="w-1/2 lg:w-fit text-lg" 96 + > 97 + <Link 98 + prefetch={false} 93 99 href="/bot/pronouns/pronouns" 94 100 > 95 101 Pronouns 96 - </ClientButton> 97 - <ClientButton 98 - as={Link} 102 + </Link> 103 + </Button> 104 + <Button 105 + asChild 106 + className="w-1/2 lg:w-fit text-lg" 107 + > 108 + <Link 109 + prefetch={false} 99 110 href="/bot/pronouns/genders" 100 111 > 101 112 Genders 102 - </ClientButton> 103 - <ClientButton 104 - as={Link} 113 + </Link> 114 + </Button> 115 + <Button 116 + asChild 117 + className="w-1/2 lg:w-fit text-lg" 118 + > 119 + <Link 120 + prefetch={false} 105 121 href="/bot/pronouns/sexualities" 106 122 > 107 123 Sexualities 108 - </ClientButton> 109 - </div> 124 + </Link> 125 + </Button> 126 + </div> 110 127 111 - <div className="flex flex-col min-w-full lg:min-w-[420px]"> 128 + <div className="flex flex-col min-w-full lg:min-w-[420px]"> 112 129 113 - <div className="lg:ml-auto flex gap-2 text-xl font-medium mt-4 lg:mt-0"> 114 - <ClientButton 115 - as={Link} 116 - startContent={<HiUserAdd />} 117 - className="button-primary w-1/2 lg:w-fit !text-xl !font-medium" 130 + <div className="lg:ml-auto flex gap-2 text-xl font-medium mt-4 lg:mt-0"> 131 + <Button 132 + asChild 133 + className="w-1/2 lg:w-fit text-lg font-medium" 134 + variant="secondary" 135 + > 136 + <Link 137 + prefetch={false} 118 138 href="https://discord.com/oauth2/authorize?client_id=912003493777268767&permissions=8&scope=bot%20applications.commands" 119 - size="lg" 120 139 > 140 + <HiUserAdd /> 121 141 <span className="block sm:hidden">Invite</span> 122 142 <span className="hidden sm:block">Invite Pronouns</span> 123 - </ClientButton> 124 - <ClientButton 125 - as={Link} 126 - startContent={<BsDiscord />} 127 - className="w-1/2 lg:w-fit !text-xl !font-medium" 143 + </Link> 144 + </Button> 145 + <Button 146 + asChild 147 + className="w-1/2 lg:w-fit text-lg" 148 + > 149 + <Link 150 + prefetch={false} 128 151 href="/support" 129 - size="lg" 130 152 > 153 + <HiUserAdd /> 131 154 <span className="block sm:hidden">Support</span> 132 - <span className="hidden sm:block">Join support</span> 133 - </ClientButton> 134 - </div> 155 + <span className="hidden sm:block">Join Support</span> 156 + </Link> 157 + </Button> 158 + </div> 135 159 136 160 137 - <span className={cn(handwritten.className, "lg:ml-auto flex gap-2 text-neutral-500 font-mediumr mt-3 opacity-80 pl-20 lg:pr-20 rotate-2")}> 138 - <Image src={ArrowPic} width={24} height={24} alt="arrow up" className="h-5 w-5 relative top-px" draggable={false} /> 139 - Get started here in seconds 140 - </span> 161 + <span className={cn(handwritten.className, "lg:ml-auto flex gap-2 text-neutral-500 font-mediumr mt-3 opacity-80 pl-20 lg:pr-20 rotate-2")}> 162 + <Image src={ArrowPic} width={24} height={24} alt="arrow up" className="h-5 w-5 relative top-px" draggable={false} /> 163 + Get started here in seconds 164 + </span> 141 165 142 - </div> 166 + </div> 143 167 144 - </div> 168 + </div> 145 169 146 170 147 - <div className="lg:my-10 my-8 w-full"> 148 - {children} 149 - </div> 171 + <div className="lg:my-10 my-8 w-full"> 172 + {children} 173 + </div> 150 174 151 - <Comment 152 - username="@deleted user" 153 - bio="Pronouns user" 154 - avatar="/discord.webp" 155 - content="I have a lot of friends who have different preferred pronouns and identities and I think it's super sweet y'all have the feature that they can change their pronouns anytime so I put your bot in my servers and a friend may put it in theirs too 🥰" 156 - /> 175 + <Comment 176 + username="@deleted user" 177 + bio="Pronouns user" 178 + avatar="/discord.webp" 179 + content="I have a lot of friends who have different preferred pronouns and identities and I think it's super sweet y'all have the feature that they can change their pronouns anytime so I put your bot in my servers and a friend may put it in theirs too 🥰" 180 + /> 157 181 158 - </div > 159 - ); 182 + </>); 160 183 }
+19 -16
app/(home)/bot/pronouns/page.tsx
··· 1 - import { Chip } from "@nextui-org/react"; 2 1 import { Montserrat } from "next/font/google"; 3 2 import Image from "next/image"; 4 3 import Link from "next/link"; 5 - import { HiCash, HiPlus } from "react-icons/hi"; 4 + import { BsPlus } from "react-icons/bs"; 5 + import { HiCash } from "react-icons/hi"; 6 6 7 7 import Box from "@/components/box"; 8 - import { ClientButton } from "@/components/client"; 9 8 import DiscordMessage from "@/components/discord/message"; 9 + import { Badge } from "@/components/ui/badge"; 10 + import { Button } from "@/components/ui/button"; 10 11 import { cn } from "@/utils/cn"; 11 12 12 13 const montserrat = Montserrat({ subsets: ["latin"] }); ··· 45 46 46 47 <Box className="flex flex-col md:flex-row gap-10 items-center"> 47 48 <div className="md:w-1/2"> 48 - <Chip 49 + <Badge 49 50 className="mb-2" 50 - color="secondary" 51 51 variant="flat" 52 - startContent={<HiCash className="mx-1" />} 52 + radius="rounded" 53 53 > 54 - <span className="font-semibold">Everything for free</span> 55 - </Chip> 54 + <HiCash /> 55 + Everything for free 56 + </Badge> 56 57 57 58 <h3 className={styles.h3}>Sexualities, Genders & Pronouns</h3> 58 59 ··· 62 63 </div> 63 64 64 65 <div className="flex gap-2 mt-6"> 65 - <ClientButton 66 - as={Link} 67 - className="bg-wamellow-alpha" 68 - startContent={<HiPlus />} 69 - href="/support" 70 - > 71 - Request additional 72 - </ClientButton> 66 + <Button asChild> 67 + <Link 68 + prefetch={false} 69 + href="/support" 70 + target="_blank" 71 + > 72 + <BsPlus /> 73 + Request Additional 74 + </Link> 75 + </Button> 73 76 </div> 74 77 </div> 75 78
+9 -9
app/(home)/commands.component.tsx
··· 1 1 import { HiFire, HiInformationCircle } from "react-icons/hi"; 2 2 3 3 import Box from "@/components/box"; 4 - import { ClientChip } from "@/components/client"; 4 + import { Badge } from "@/components/ui/badge"; 5 5 import { defaultFetchOptions } from "@/lib/api"; 6 6 import { intl } from "@/utils/numbers"; 7 7 ··· 26 26 className="p-5 pb-3 dark:bg-wamellow bg-wamellow-100 rounded-lg my-4 w-full" 27 27 > 28 28 <div className="flex"> 29 - <ClientChip 30 - color="secondary" 29 + <Badge 31 30 variant="flat" 32 - size="sm" 33 - startContent={<HiFire className="ml-1" />} 31 + radius="rounded" 34 32 > 35 - <span className="font-semibold">Popular Slash Commands</span> 36 - </ClientChip> 33 + <HiFire /> 34 + Popular Slash Commands 35 + </Badge> 36 + 37 37 <div className="ml-auto flex items-center gap-1 opacity-80"> 38 - <span className="text-xs">Since 7th December</span> 38 + <span className="text-xs">Since September 2024</span> 39 39 <HiInformationCircle /> 40 40 </div> 41 41 </div> 42 42 43 - {commands && Array.isArray(commands) ? 43 + {commands && Array.isArray(commands) && commands.length ? 44 44 <div className="divide-y divide-wamellow"> 45 45 {commands 46 46 .sort((a, b) => b.uses - a.uses)
+12 -20
app/(home)/debug/page.tsx
··· 1 1 import type { Metadata } from "next"; 2 2 import { cookies, headers } from "next/headers"; 3 - import Link from "next/link"; 4 3 import { HiTrash } from "react-icons/hi"; 5 4 6 5 import Box from "@/components/box"; 7 - import { ClientButton } from "@/components/client"; 8 6 import { Shiggy } from "@/components/shiggy"; 7 + import { Button } from "@/components/ui/button"; 9 8 import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; 10 9 11 10 import Panel from "./panel.component"; 12 11 13 12 export const generateMetadata = (): Metadata => { 14 13 const title = "Shiggy"; 15 - const description = ""; 14 + const description = "Uhhhh debug page???"; 16 15 const url = getCanonicalUrl("debug"); 17 16 18 17 return { 19 18 title, 20 19 description, 20 + 21 21 alternates: { 22 22 canonical: url 23 23 }, 24 + 24 25 openGraph: { 25 26 title, 26 27 description, ··· 31 32 type: "image/gif" 32 33 } 33 34 }, 35 + 34 36 twitter: { 35 37 card: "summary", 36 38 title, ··· 39 41 url: `${getBaseUrl()}/shiggy.gif`, 40 42 alt: title 41 43 } 42 - } 44 + }, 45 + 46 + robots: "noindex, follow" 43 47 }; 44 48 }; 45 49 ··· 101 105 items={(await cookies()).getAll()} 102 106 action={(cookie) => ( 103 107 <form action={deleteCookie}> 104 - <ClientButton 105 - type="submit" 106 - isIconOnly 107 - > 108 + <Button type="submit"> 108 109 <HiTrash className="text-red-400" /> 109 - </ClientButton> 110 + </Button> 110 111 <input className="hidden" type="text" name="name" defaultValue={cookie.name} readOnly /> 111 112 </form> 112 113 )} 113 114 > 114 115 <div className="mt-4 flex gap-2 items-center"> 115 116 <form action={deleteCookie}> 116 - <ClientButton 117 - type="submit" 118 - > 117 + <Button type="submit"> 119 118 Delete all cookies 120 - </ClientButton> 119 + </Button> 121 120 </form> 122 - <ClientButton 123 - as={Link} 124 - href="/logout" 125 - prefetch={false} 126 - > 127 - Logout 128 - </ClientButton> 129 121 </div> 130 122 </Panel> 131 123
+225 -213
app/(home)/page.tsx
··· 3 3 import Image from "next/image"; 4 4 import Link from "next/link"; 5 5 import { Suspense } from "react"; 6 - import { BsDiscord, BsYoutube } from "react-icons/bs"; 6 + import { BsYoutube } from "react-icons/bs"; 7 7 import { HiArrowNarrowRight, HiArrowRight, HiCash, HiCheck, HiFire, HiLockOpen, HiUserAdd } from "react-icons/hi"; 8 8 9 - import { Avatar } from "@/components/avatar"; 10 9 import Box from "@/components/box"; 11 - import { ClientAvatarGroup, ClientButton, ClientChip } from "@/components/client"; 12 10 import Comment from "@/components/comment"; 13 11 import DiscordAppBadge from "@/components/discord/app-badge"; 14 12 import DiscordChannel from "@/components/discord/channel"; ··· 18 16 import DiscordMessageEmbed from "@/components/discord/message-embed"; 19 17 import DiscordUser from "@/components/discord/user"; 20 18 import ImageReduceMotion from "@/components/image-reduce-motion"; 19 + import { AvatarGroup, UserAvatar } from "@/components/ui/avatar"; 20 + import { Badge } from "@/components/ui/badge"; 21 + import { Button } from "@/components/ui/button"; 21 22 import { Skeleton } from "@/components/ui/skeleton"; 22 23 import { defaultFetchOptions } from "@/lib/api"; 23 24 import AiPic from "@/public/ai.webp"; ··· 79 80 return ( 80 81 <div className="flex items-center flex-col w-full"> 81 82 82 - <div className="flex w-full items-center gap-8 mb-16 md:mb-12 min-h-[500px] h-[calc(100svh-14rem)] md:h-[calc(100svh-17rem)]"> 83 + <div className="flex w-full items-center gap-8 mb-16 md:mb-12 min-h-[500px] h-[calc(100svh-14rem)] md:h-[calc(100dvh-16rem)]"> 83 84 <div className="md:min-w-96 w-full md:w-2/3 xl:w-1/2 flex flex-col space-y-6"> 84 85 85 86 <Suspense fallback={<Skeleton className="w-[15rem] !h-6 !m-0" isLoading={true} />}> ··· 103 104 Stay updated with dailyposts and receive social notifications! 104 105 </span> 105 106 106 - <ClientAvatarGroup 107 - className="mr-auto md:hidden" 108 - max={8} 109 - renderCount={renderCount} 110 - > 107 + <AvatarGroup className="mr-auto md:hidden"> 111 108 {toFixedArrayLength(topGuilds || [], 8) 112 109 ?.map((guild) => ( 113 - <Avatar 110 + <UserAvatar 114 111 key={"mobileGuildGrid-" + guild.id} 112 + alt={guild.name} 113 + className="-mr-2" 115 114 src={guild.icon ? guild.icon + "?size=128" : "/discord.webp"} 116 - alt={guild.name} 117 - title={guild.name} 118 115 /> 119 116 )) 120 117 } 121 - </ClientAvatarGroup> 118 + </AvatarGroup> 122 119 123 120 <div className="space-y-4"> 124 121 <Link 125 122 className="flex gap-1 items-center text-violet-400 hover:underline w-fit" 126 - href={getCanonicalUrl("dashboard", "?utm_source=wamellow.com&utm_medium=home")} 123 + href={getCanonicalUrl("dashboard")} 127 124 > 128 125 Go to Dashboard <HiArrowNarrowRight /> 129 126 </Link> 130 127 131 - <div className="flex gap-2 lg:mt-0"> 132 - <ClientButton 133 - as={Link} 134 - className="w-1/2 lg:w-fit !text-xl !font-medium" 135 - color="secondary" 136 - prefetch={false} 137 - href="/login?invite=true" 138 - size="lg" 139 - startContent={<HiUserAdd />} 128 + <div className="flex gap-2"> 129 + <Button 130 + asChild 131 + className="w-1/2 lg:w-fit text-lg font-medium" 132 + variant="secondary" 140 133 > 141 - <span className="block sm:hidden">Invite</span> 142 - <span className="hidden sm:block">Invite Wamellow</span> 143 - </ClientButton> 144 - <ClientButton 145 - as={Link} 146 - startContent={<BsDiscord />} 147 - className="w-1/2 lg:w-fit !text-xl !font-medium" 148 - href="/support" 149 - size="lg" 134 + <Link 135 + prefetch={false} 136 + href="/login?invite=true" 137 + > 138 + <HiUserAdd /> 139 + <span className="block sm:hidden">Invite</span> 140 + <span className="hidden sm:block">Invite Wamellow</span> 141 + </Link> 142 + </Button> 143 + <Button 144 + asChild 145 + className="w-1/2 lg:w-fit text-lg" 150 146 > 151 - <span className="block sm:hidden">Support</span> 152 - <span className="hidden sm:block">Join support</span> 153 - </ClientButton> 147 + <Link 148 + prefetch={false} 149 + href="/support" 150 + > 151 + <HiUserAdd /> 152 + <span className="block sm:hidden">Support</span> 153 + <span className="hidden sm:block">Join Support</span> 154 + </Link> 155 + </Button> 154 156 </div> 155 157 156 158 <span className={cn("lg:ml-auto flex gap-2 text-neutral-500 font-medium mt-3 opacity-80 pl-20 lg:pr-20 rotate-2", handwritten.className)}> ··· 178 180 <Link 179 181 key={"guildGrid-" + guild.id + i + i2} 180 182 className="relative md:h-32 h-24 md:w-32 w-24 hover:scale-110 duration-200" 181 - href={getCanonicalUrl("leaderboard", guild.id, "?utm_source=wamellow.com&utm_medium=home")} 183 + href={getCanonicalUrl("leaderboard", guild.id)} 182 184 prefetch={false} 183 185 > 184 186 <ImageReduceMotion ··· 196 198 </div> 197 199 </div> 198 200 199 - <div className="flex flex-col items-center space-x-2"> 200 - <div className="animate-scroll rounded-lg rotate-180 md:rounded-3xl md:rotate-0"> 201 - <div className="animate-scroll-wheel" /> 202 - </div> 203 - <span className="hidden md:block text-lg font-medium mt-2 text-neutral-500/50">Scroll down...</span> 201 + <div className="animate-scroll rounded-medium rotate-180 md:rounded-3xl md:rotate-0"> 202 + <div className="animate-scroll-wheel" /> 204 203 </div> 205 204 206 205 <article ··· 214 213 215 214 <Box className="flex flex-col md:flex-row gap-10 items-center"> 216 215 <div className="md:w-1/2 flex flex-col items-start"> 217 - <ClientChip 216 + <Badge 218 217 className="mb-2" 219 - color="secondary" 220 218 variant="flat" 221 - size="sm" 222 - startContent={<HiCash className="mx-1" />} 219 + radius="rounded" 223 220 > 224 - <span className="font-semibold">100% free forever</span> 225 - </ClientChip> 221 + <HiCash /> 222 + 100% free forever 223 + </Badge> 226 224 227 225 <h3 className={styles.h3}>40 Voices in 8 Languages</h3> 228 226 ··· 231 229 Great for people with aphonia, dysphonia, or other speech impairments. 232 230 </div> 233 231 234 - <ClientAvatarGroup 235 - className="mt-4" 236 - max={8} 237 - > 232 + <AvatarGroup className="mt-4"> 238 233 {["us", "de", "es", "fr", "jp", "kr", "br", "id"].map((lang) => { 239 234 const name = Object 240 235 .entries(actor) ··· 242 237 ?.[1][0] || lang; 243 238 244 239 return ( 245 - <Avatar 246 - size="sm" 240 + <UserAvatar 247 241 key={"ttsLang-" + lang} 242 + alt={name} 243 + className="-mr-2 size-8" 248 244 src={`/icons/${lang}.webp`} 249 - alt={name} 250 - title={name} 251 245 /> 252 246 ); 253 247 })} 254 - <span className="sr-only">Change Text-to-Speech language and voice</span> 255 - </ClientAvatarGroup> 248 + </AvatarGroup> 256 249 257 250 <div className="flex gap-2 mt-5"> 258 251 <Invite /> 259 - <ClientButton 260 - as={Link} 261 - className="bg-wamellow" 262 - startContent={<BsYoutube />} 263 - href="https://youtu.be/NS5fZ1ltovE?si=I3nViYb4sx3n3Uvo" 264 - target="_blank" 265 - > 266 - Watch YouTube Video 267 - </ClientButton> 252 + <Button asChild> 253 + <Link 254 + prefetch={false} 255 + href="https://youtu.be/NS5fZ1ltovE?si=I3nViYb4sx3n3Uvo" 256 + target="_blank" 257 + > 258 + <BsYoutube /> 259 + Watch YouTube Video 260 + </Link> 261 + </Button> 268 262 </div> 269 263 </div> 270 264 ··· 305 299 306 300 <Box className="flex flex-col md:flex-row-reverse gap-10 items-center"> 307 301 <div className="md:w-1/2"> 308 - <ClientChip 302 + <Badge 309 303 className="mb-2" 310 - color="secondary" 311 304 variant="flat" 312 - size="sm" 313 - startContent={<HiCash className="mx-1" />} 305 + radius="rounded" 314 306 > 315 - <span className="font-semibold">Free styling, 30 Channels</span> 316 - </ClientChip> 307 + <HiCash /> 308 + Free styling — 30 channels 309 + </Badge> 310 + 317 311 <h3 className={styles.h3}>YouTube, Twitch, Bluesky & Reddit</h3> 312 + 318 313 <div className="pt-6"> 319 314 Set up notifications with free custom messages and embeds for up to 30 channels and get notified in less than a minute. 320 315 ··· 334 329 </div> 335 330 <div className="flex gap-2 mt-6"> 336 331 <Invite /> 337 - <ClientButton 338 - as={Link} 339 - className="bg-wamellow" 340 - startContent={<BsYoutube />} 341 - href="https://youtu.be/xizs-hrwK4I?si=6pIYALygtNhUwpph" 342 - target="_blank" 343 - > 344 - Watch Tutorial 345 - </ClientButton> 346 - <ClientButton 347 - as={Link} 348 - className="bg-wamellow" 349 - startContent={<HiArrowRight />} 350 - href="/dashboard?to=notifications&utm_source=wamellow.com&utm_medium=home" 351 - > 352 - Setup 353 - </ClientButton> 332 + <Button asChild> 333 + <Link 334 + prefetch={false} 335 + href="https://youtu.be/xizs-hrwK4I?si=6pIYALygtNhUwpph" 336 + target="_blank" 337 + > 338 + <BsYoutube /> 339 + Watch Tutorial 340 + </Link> 341 + </Button> 342 + <Button asChild> 343 + <Link 344 + prefetch={false} 345 + href="/dashboard?to=notifications" 346 + target="_blank" 347 + > 348 + <HiArrowRight /> 349 + Setup 350 + </Link> 351 + </Button> 354 352 </div> 355 353 </div> 356 354 ··· 380 378 381 379 <Box className="flex flex-col md:flex-row gap-10 items-center"> 382 380 <div className="md:w-1/2"> 383 - <ClientChip 381 + <Badge 384 382 className="mb-2" 385 - color="secondary" 386 383 variant="flat" 387 - size="sm" 388 - startContent={<HiCash className="mx-1" />} 384 + radius="rounded" 389 385 > 390 - <span className="font-semibold">100% no money loss</span> 391 - </ClientChip> 386 + <HiCash /> 387 + No premium required 388 + </Badge> 389 + 392 390 <h3 className={styles.h3}>Free /image command</h3> 391 + 393 392 <div className="pt-6"> 394 393 Summon the enchantment of AI-generated images to your Discord server with our versatile /image command, featuring over 40 distinct custom models. 395 394 Customize the rating, quality, aesthetics, image width and height, upscaled, generation steps and the CFG scale all for free. 396 395 </div> 397 - <div className="p-4 pb-3 border dark:border-wamellow-alpha border-wamellow-100 rounded-lg my-8"> 398 - <ClientChip 396 + <div className="p-4 pb-3 border border-divider rounded-lg my-8"> 397 + <Badge 399 398 className="mb-2" 400 - color="secondary" 401 399 variant="flat" 402 - size="sm" 403 - startContent={<HiFire className="mx-1" />} 400 + radius="rounded" 404 401 > 405 - <span className="font-semibold">NSFW Supported</span> 406 - </ClientChip> 402 + <HiFire /> 403 + Supports NSFW 404 + </Badge> 407 405 <div className="text-base"> 408 406 Generate spicy images and more in nsfw marked channels. 409 407 </div> 410 408 </div> 411 409 <div className="flex gap-2 mt-6"> 412 410 <Invite /> 413 - <ClientButton 414 - as={Link} 415 - className="bg-wamellow" 416 - startContent={<HiArrowRight />} 417 - href="/ai-gallery?utm_source=wamellow.com&utm_medium=home" 418 - > 419 - View Images 420 - </ClientButton> 411 + <Button asChild> 412 + <Link 413 + href="/ai-gallery" 414 + target="_blank" 415 + > 416 + <HiArrowRight /> 417 + View Images 418 + </Link> 419 + </Button> 421 420 </div> 422 421 </div> 423 422 ··· 443 442 444 443 <Box className="flex flex-col md:flex-row-reverse gap-10 items-center"> 445 444 <div className="md:w-1/2"> 446 - <ClientChip 445 + <Badge 447 446 className="mb-2" 448 - color="secondary" 449 447 variant="flat" 450 - size="sm" 451 - startContent={<HiCash className="mx-1" />} 448 + radius="rounded" 452 449 > 453 - <span className="font-semibold">100% sexy forever</span> 454 - </ClientChip> 450 + <HiCash /> 451 + 100% sexy for free 452 + </Badge> 453 + 455 454 <h3 className={styles.h3}>/anime command</h3> 455 + 456 456 <div className="pt-6"> 457 457 Unleash the magic of anime right within your Discord server with Wamellow{"'"}s 25+ categories. 458 458 Dive into a world of adorable nekos, charming waifus, and much more, all at your fingertips. 459 459 Whether it{"'"}s sharing the cutest characters or discovering stunning artwork, bring the joy of anime directly to your community, making your server a hub for all things anime-related. 460 460 </div> 461 - <div className="p-4 pb-3 border dark:border-wamellow-alpha border-wamellow-100 rounded-lg my-8"> 462 - <ClientChip 461 + <div className="p-4 pb-3 border border-divider rounded-lg my-8"> 462 + <Badge 463 463 className="mb-2" 464 - color="secondary" 465 464 variant="flat" 466 - size="sm" 467 - startContent={<HiFire className="mx-1" />} 465 + radius="rounded" 468 466 > 469 - <span className="font-semibold">NSFW Supported</span> 470 - </ClientChip> 467 + <HiFire /> 468 + Supports NSFW 469 + </Badge> 471 470 <div className="text-base"> 472 471 Find spicy nekos, waifus, and more in nsfw marked channels. 473 472 </div> ··· 498 497 499 498 <Box className="flex flex-col md:flex-row gap-10 items-center"> 500 499 <div className="md:w-1/2"> 501 - <ClientChip 500 + <Badge 502 501 className="mb-2" 503 - color="secondary" 504 502 variant="flat" 505 - size="sm" 506 - startContent={<HiCash className="mx-1" />} 503 + radius="rounded" 507 504 > 508 - <span className="font-semibold">100% free forever</span> 509 - </ClientChip> 505 + <HiCash /> 506 + Free custom backgrounds 507 + </Badge> 508 + 510 509 <h3 className={styles.h3}>/leaderboard & /rank</h3> 510 + 511 511 <div className="pt-6"> 512 - Enhance your server{"’"}s engagement with our text-, voice- and invite based leaderboards, tailored to track and reward your most active members. 512 + Enhance your server{"’"}s engagement with text-, voice- and invite based leaderboards, tailored to track and reward your most active members. 513 513 By motivating your members to communicate, you{"’"}ll cultivate a more active server community. 514 514 </div> 515 515 <div className="flex gap-2 mt-6"> 516 516 <Invite /> 517 - <ClientButton 518 - as={Link} 519 - className="bg-wamellow" 520 - startContent={<HiArrowRight />} 521 - href="/leaderboard/828676951023550495?utm_source=wamellow.com&utm_medium=home" 522 - > 523 - View Leaderboard 524 - </ClientButton> 517 + <Button asChild> 518 + <Link 519 + href="/leaderboard/828676951023550495" 520 + target="_blank" 521 + > 522 + <HiArrowRight /> 523 + View Leaderboard 524 + </Link> 525 + </Button> 525 526 </div> 526 527 </div> 527 528 ··· 545 546 546 547 <Box className="flex flex-col md:flex-row gap-10 items-center"> 547 548 <div className="md:w-1/2"> 548 - <ClientChip 549 + <Badge 549 550 className="mb-2" 550 - color="secondary" 551 551 variant="flat" 552 - size="sm" 553 - startContent={<HiCash className="mx-1" />} 552 + radius="rounded" 554 553 > 555 - <span className="font-semibold">My lawyer said that title below</span> 556 - </ClientChip> 554 + <HiCash /> 555 + My lawyer said that title below 556 + </Badge> 557 + 557 558 <h3 className={styles.h3}>POGBOARD DEEZ NUTS</h3> 559 + 558 560 <div className="pt-6"> 559 561 With Starboards, you have the power to highlight remarkable messages in your server. 560 562 When you come across a message that is either funny or hilarious, simply react with a star, and if enough people star the message, it will be posted to the set starboard channel. 561 563 </div> 562 564 <div className="flex gap-2 mt-6"> 563 565 <Invite /> 564 - <ClientButton 565 - as={Link} 566 - className="bg-wamellow" 567 - startContent={<HiArrowRight />} 568 - href="/dashboard?to=starboard&utm_source=wamellow.com&utm_medium=home" 569 - > 570 - Setup 571 - </ClientButton> 566 + <Button asChild> 567 + <Link 568 + href="/dashboard?to=starboard" 569 + target="_blank" 570 + > 571 + <HiArrowRight /> 572 + Setup 573 + </Link> 574 + </Button> 572 575 </div> 573 576 </div> 574 577 ··· 603 606 604 607 <Box className="flex flex-col md:flex-row-reverse gap-10 items-center"> 605 608 <div className="md:w-1/2"> 606 - <ClientChip 609 + <Badge 607 610 className="mb-2" 608 - color="secondary" 609 611 variant="flat" 610 - size="sm" 611 - startContent={<HiCash className="mx-1" />} 612 + radius="rounded" 612 613 > 613 - <span className="font-semibold">w/ free image card backgrounds</span> 614 - </ClientChip> 614 + <HiCash /> 615 + w/ free image card backgrounds 616 + </Badge> 617 + 615 618 <h3 className={styles.h3}>Greetings</h3> 619 + 616 620 <div className="pt-6"> 617 - Give a warm welcome to new members, introducing them to rules, topics, and ongoing events. 618 - Whether gaming, joining a guild, or casual chat, every member should sense a strong community bond. 621 + Give a warm welcome to new members, introducing them to rules, and topics with a fully customized message. 619 622 </div> 620 623 <div className="flex gap-2 mt-6"> 621 624 <Invite /> 622 - <ClientButton 623 - as={Link} 624 - className="bg-wamellow" 625 - startContent={<HiArrowRight />} 626 - href="/dashboard?to=greeting&utm_source=wamellow.com&utm_medium=home" 627 - > 628 - Setup 629 - </ClientButton> 625 + <Button asChild> 626 + <Link 627 + href="/dashboard?to=greeting" 628 + target="_blank" 629 + > 630 + <HiArrowRight /> 631 + Setup 632 + </Link> 633 + </Button> 630 634 </div> 631 635 </div> 632 636 ··· 651 655 652 656 <Box className="flex flex-col md:flex-row gap-10 items-center"> 653 657 <div className="md:w-1/2"> 654 - <ClientChip 658 + <Badge 655 659 className="mb-2" 656 - color="secondary" 657 660 variant="flat" 658 - size="sm" 659 - startContent={<HiCash className="mx-1" />} 661 + radius="rounded" 660 662 > 661 - <span className="font-semibold">Of course it{"'"}s free</span> 662 - </ClientChip> 663 + <HiCash /> 664 + Free and Secure 665 + </Badge> 666 + 663 667 <h3 className={styles.h3}>Captcha verification</h3> 668 + 664 669 <div className="pt-6"> 665 670 Protect your server from unwanted attacks, such as bot-raids, with our captcha verification system. 666 671 Ensure that only human members can access your channels, safeguarding your server from raider attacks and ensuring a safe and secure environment for all your members. 667 672 </div> 668 673 <div className="flex gap-2 mt-6"> 669 674 <Invite /> 670 - <ClientButton 671 - as={Link} 672 - className="bg-wamellow" 673 - startContent={<HiLockOpen />} 674 - href="/passport/1125063180801036329?utm_source=wamellow.com&utm_medium=home" 675 - > 676 - Try it out 677 - </ClientButton> 678 - <ClientButton 679 - as={Link} 680 - className="bg-wamellow" 681 - startContent={<HiArrowRight />} 682 - href="/dashboard?to=greeting&utm_source=wamellow.com&utm_medium=home" 683 - > 684 - Setup 685 - </ClientButton> 675 + <Button asChild> 676 + <Link 677 + href="/passport/1125063180801036329" 678 + target="_blank" 679 + > 680 + <HiLockOpen /> 681 + Try it out 682 + </Link> 683 + </Button> 684 + <Button asChild> 685 + <Link 686 + href="/dashboard?to=greeting/passport" 687 + target="_blank" 688 + > 689 + <HiArrowRight /> 690 + Setup 691 + </Link> 692 + </Button> 686 693 </div> 687 694 </div> 688 695 ··· 705 712 706 713 <Box className="flex flex-col md:flex-row-reverse gap-10 items-center"> 707 714 <div className="md:w-1/2"> 708 - <ClientChip 715 + <Badge 709 716 className="mb-2" 710 - color="secondary" 711 717 variant="flat" 712 - size="sm" 713 - startContent={<HiCash className="mx-1" />} 718 + radius="rounded" 714 719 > 715 - <span className="font-semibold">30 tags free</span> 716 - </ClientChip> 720 + <HiCash /> 721 + 30 tags free 722 + </Badge> 723 + 717 724 <h3 className={styles.h3}>Wamellow tags</h3> 725 + 718 726 <div className="pt-6"> 719 727 Easily handle frequently asked questions, common queries, and repetitive tasks in a snap. 720 728 Empower your server with quick access to custom commands. ··· 722 730 </div> 723 731 <div className="flex gap-2 mt-6"> 724 732 <Invite /> 725 - <ClientButton 726 - as={Link} 727 - className="bg-wamellow" 728 - startContent={<HiArrowRight />} 729 - href="/dashboard?to=custom-commands&utm_source=wamellow.com&utm_medium=home" 730 - > 731 - Setup 732 - </ClientButton> 733 + <Button asChild> 734 + <Link 735 + href="/dashboard?to=custom-commands" 736 + target="_blank" 737 + > 738 + <HiArrowRight /> 739 + Setup 740 + </Link> 741 + </Button> 733 742 </div> 734 743 </div> 735 744 ··· 785 794 786 795 function Invite() { 787 796 return ( 788 - <ClientButton 789 - as={Link} 790 - color="secondary" 791 - href="/login?invite=true" 792 - prefetch={false} 793 - startContent={<HiUserAdd />} 797 + <Button 798 + variant="secondary" 799 + asChild 794 800 > 795 - <span className="block sm:hidden">Invite</span> 796 - <span className="hidden sm:block">Invite Wamellow</span> 797 - </ClientButton> 801 + <Link 802 + href="/login?invite=true" 803 + prefetch={false} 804 + > 805 + <HiUserAdd /> 806 + <span className="block sm:hidden">Invite</span> 807 + <span className="hidden sm:block">Invite Wamellow</span> 808 + </Link> 809 + </Button> 798 810 ); 799 811 }
+77 -66
app/(home)/pro/page.tsx
··· 1 - import { Button, Chip } from "@nextui-org/react"; 2 1 import type { Metadata } from "next"; 3 2 import { Montserrat } from "next/font/google"; 4 3 import Link from "next/link"; ··· 8 7 9 8 import Comment from "@/components/comment"; 10 9 import ImageGrid from "@/components/image-grid"; 10 + import { Badge } from "@/components/ui/badge"; 11 + import { Button } from "@/components/ui/button"; 12 + import { defaultFetchOptions } from "@/lib/api"; 11 13 import type { ApiV1TopguildsGetResponse } from "@/typings"; 12 14 import { cn } from "@/utils/cn"; 13 15 import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; 14 16 17 + const montserrat = Montserrat({ subsets: ["latin"] }); 15 18 const maybe = null; 16 - const montserrat = Montserrat({ subsets: ["latin"] }); 17 19 18 - export const revalidate = 3600; 20 + const items = [ 21 + { title: "Price", free: "$0 /month", pro: "$3.99 /month" }, 22 + { title: "Custom commands", free: 30, pro: Infinity }, 23 + { title: "Social notifications", free: 30, pro: Infinity }, 24 + { title: "Dailyposts", free: 30, pro: Infinity }, 25 + // { title: "Stickymessages", free: 10, pro: 50 }, 26 + // { title: "Custom footers", free: false, pro: true }, 27 + { title: "Welcome roles", free: 5, pro: 10 }, 28 + { title: "Welcome pings", free: 5, pro: 15 }, 29 + // { title: "Level roles", free: 15, pro: 25 }, 30 + { title: "Spotify control", free: maybe, pro: true, url: "/profile/spotify" }, 31 + { title: "Custom /rank sub-text", free: false, pro: true }, 32 + // { title: "Display user as webhook", free: false, pro: true }, 33 + { title: "Passport bypass", free: false, pro: true }, 34 + // { title: "Custom page color", free: false, pro: true }, 35 + // { title: "Statistics & Analytics", free: false, pro: true }, 36 + { title: "Crosspost social notifications", free: false, pro: true } 37 + ]; 19 38 20 - const fetchOptions = { headers: { Authorization: process.env.API_SECRET as string }, next: { revalidate: 60 * 60 } }; 39 + export const revalidate = 3600; 21 40 22 41 export const generateMetadata = (): Metadata => { 23 42 ··· 49 68 }; 50 69 51 70 export default async function Home() { 52 - const topGuilds = await fetch(`${process.env.NEXT_PUBLIC_API}/top-guilds`, fetchOptions).then((res) => res.json()) as ApiV1TopguildsGetResponse[]; 53 - 54 - const buttons = (<> 55 - <Button 56 - as={Link} 57 - className="w-1/2 font-medium" 58 - href="/login?invite=true" 59 - prefetch={false} 60 - startContent={<HiUserAdd />} 61 - > 62 - Get started 63 - </Button> 64 - <Button 65 - as={Link} 66 - color="secondary" 67 - className="w-1/2 font-medium" 68 - href="https://lunish.nl/kofi" 69 - startContent={<HiLightningBolt />} 70 - > 71 - Subscribe 72 - </Button> 73 - </>); 74 - 75 - const displayState = (is: string | number | boolean | null) => { 76 - if (typeof is === "boolean" || is === null) { 77 - if (is === true) return <HiOutlineCheck className="dark:text-violet-400 text-violet-600 w-6 h-6" />; 78 - if (is === false) return <HiX className="dark:text-red-400 text-red-600 w-6 h-6" />; 79 - if (is === null) return <BsQuestionLg className="text-orange-400 dark:text-orange-600 w-6 h-6" title="To be discussed" />; 80 - } 81 - 82 - if (is === Infinity) return <IoMdInfinite className="w-7 h-7" title="Infinite" />; 83 - return is; 84 - }; 71 + const topGuilds = await fetch(`${process.env.NEXT_PUBLIC_API}/top-guilds`, defaultFetchOptions).then((res) => res.json()) as ApiV1TopguildsGetResponse[]; 85 72 86 73 return ( 87 74 <div className="flex items-center flex-col w-full"> ··· 93 80 <span className="bg-gradient-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent break-keep hidden md:block">Professional</span> 94 81 </h1> 95 82 <HiLightningBolt className="text-pink-400 rotate-6" /> 96 - <Chip 97 - className="ml-auto" 98 - color="secondary" 83 + <Badge 84 + className="ml-auto mt-1.5" 85 + size="lg" 99 86 variant="flat" 100 - size="lg" 87 + radius="rounded" 101 88 > 102 - <span className="font-semibold">Not available</span> 103 - </Chip> 89 + Not available 90 + </Badge> 104 91 </div> 105 92 106 93 {topGuilds && ··· 126 113 <span className="bg-gradient-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent w-1/4 hidden md:block">Pro+ ULTRA HD</span> 127 114 </div> 128 115 129 - {[ 130 - { title: "Price", free: "$0 /month", pro: "$3.99 /month" }, 131 - { title: "Custom commands", free: 30, pro: Infinity }, 132 - { title: "Social notifications", free: 30, pro: Infinity }, 133 - { title: "Dailyposts", free: 30, pro: Infinity }, 134 - // { title: "Stickymessages", free: 10, pro: 50 }, 135 - // { title: "Custom footers", free: false, pro: true }, 136 - { title: "Welcome roles", free: 5, pro: 10 }, 137 - { title: "Welcome pings", free: 5, pro: 15 }, 138 - // { title: "Level roles", free: 15, pro: 25 }, 139 - { title: "Spotify control", free: maybe, pro: true, url: "/profile/spotify" }, 140 - { title: "Custom /rank sub-text", free: false, pro: true }, 141 - // { title: "Display user as webhook", free: false, pro: true }, 142 - { title: "Passport bypass", free: false, pro: true }, 143 - // { title: "Custom page color", free: false, pro: true }, 144 - // { title: "Statistics & Analytics", free: false, pro: true }, 145 - { title: "Crosspost social notifications", free: false, pro: true } 146 - ].map((item) => ( 116 + {items.map((item) => ( 147 117 <div key={item.title} className="flex items-center py-4"> 148 118 <span className="md:text-base text-sm font-medium w-2/4 md:pr-0 pr-4">{item.title}</span> 149 119 <span className="dark:text-neutral-200 text-neutral-700 font-medium w-1/4"> ··· 159 129 <div className="flex items-center pt-4"> 160 130 <div className="w-1/2" /> 161 131 <div className="hidden md:flex w-1/2 gap-4"> 162 - {buttons} 132 + <Subscribe /> 163 133 </div> 164 134 </div> 165 135 ··· 187 157 188 158 <div className="flex gap-2 items-center"> 189 159 <span className="dark:text-neutral-200 text-neutral-800 font-medium text-sm">Upgrade your guilds further!</span> 190 - <Chip 191 - color="secondary" 160 + <Badge 192 161 variant="flat" 162 + radius="rounded" 193 163 > 194 - <span className="font-semibold">Not available</span> 195 - </Chip> 164 + Not available 165 + </Badge> 196 166 </div> 197 167 198 168 <button className="flex dark:text-violet-400 text-violet-600 bg-violet-600/30 hover:bg-violet-600/10 py-2 px-4 rounded-md duration-200 justify-center gap-2 w-full opacity-30 cursor-not-allowed" disabled> ··· 205 175 206 176 </div> 207 177 ); 178 + } 179 + 180 + function displayState(is: string | number | boolean | null) { 181 + if (typeof is === "boolean" || is === null) { 182 + if (is === true) return <HiOutlineCheck className="dark:text-violet-400 text-violet-600 w-6 h-6" />; 183 + if (is === false) return <HiX className="dark:text-red-400 text-red-600 w-6 h-6" />; 184 + if (is === null) return <BsQuestionLg className="text-orange-400 dark:text-orange-600 w-6 h-6" title="To be discussed" />; 185 + } 186 + 187 + if (is === Infinity) return <IoMdInfinite className="w-7 h-7" title="Infinite" />; 188 + return is; 189 + } 190 + 191 + function Subscribe() { 192 + return (<> 193 + <Button asChild> 194 + <Link 195 + className="w-1/2" 196 + prefetch={false} 197 + href="/invite" 198 + target="_blank" 199 + > 200 + <HiUserAdd /> 201 + Invite Wamellow 202 + </Link> 203 + </Button> 204 + <Button 205 + asChild 206 + variant="secondary" 207 + > 208 + <Link 209 + className="w-1/2" 210 + prefetch={false} 211 + href="https://ko-fi.com/mwlica" 212 + target="_blank" 213 + > 214 + <HiLightningBolt /> 215 + Subscribe 216 + </Link> 217 + </Button> 218 + </>); 208 219 }
+9 -20
app/(home)/status/cluster.component.tsx
··· 1 - import { Chip } from "@nextui-org/react"; 2 1 import Image from "next/image"; 3 - import { FaCrown } from "react-icons/fa6"; 4 2 import { HiLightningBolt } from "react-icons/hi"; 5 3 4 + import { Badge } from "@/components/ui/badge"; 6 5 import { cn } from "@/utils/cn"; 7 6 import { intl } from "@/utils/numbers"; 8 7 ··· 46 45 </div> 47 46 </div> 48 47 49 - {cluster.id === 0 50 - ? <Chip 51 - className="-mt-2 w-1/6" 52 - startContent={<FaCrown className="ml-1" />} 53 - color="warning" 54 - variant="flat" 55 - > 56 - {cluster.ping}ms 57 - </Chip> 58 - : <Chip 59 - className={cn(cluster.ping > 0 && "text-neutral-400 bg-wamellow w-1/6")} 60 - startContent={<HiLightningBolt className="ml-1" />} 61 - variant="flat" 62 - color={cluster.ping < 0 ? "danger" : "default"} 63 - > 64 - {cluster.ping}ms 65 - </Chip> 66 - } 48 + <Badge 49 + className={cn(cluster.ping > 0 && "text-neutral-400 bg-wamellow max-w-1/6")} 50 + variant={cluster.ping < 0 ? "destructive" : "default"} 51 + radius="rounded" 52 + > 53 + <HiLightningBolt /> 54 + {cluster.ping}ms 55 + </Badge> 67 56 </div> 68 57 ); 69 58 }
-13
app/(home)/status/node.component.tsx
··· 1 - import { Chip } from "@nextui-org/react"; 2 1 import Image from "next/image"; 3 2 import type { ReactNode } from "react"; 4 - import { FaCrown } from "react-icons/fa"; 5 3 6 4 import type { ApiNode } from "./api"; 7 5 ··· 20 18 <span className="text-neutral-300"> 21 19 #{index} 22 20 </span> 23 - 24 - {node.id.endsWith("-lun-1") && 25 - <Chip 26 - className="ml-auto hidden sm:flex" 27 - startContent={<FaCrown className="ml-1" />} 28 - color="warning" 29 - variant="flat" 30 - > 31 - master 32 - </Chip> 33 - } 34 21 </div> 35 22 36 23 <div>
+10 -10
app/(home)/team/repository.component.tsx
··· 1 1 import Link from "next/link"; 2 2 import { HiBeaker, HiExternalLink, HiStar } from "react-icons/hi"; 3 3 4 - import { ClientChip } from "@/components/client"; 4 + import { Badge } from "@/components/ui/badge"; 5 5 import { getRepository } from "@/lib/github"; 6 6 import { cn } from "@/utils/cn"; 7 7 ··· 28 28 <span className="text-lg text-neutral-200 font-medium -mb-0.5"> 29 29 {repo.full_name} 30 30 </span> 31 - <ClientChip 32 - startContent={<HiStar className="mx-0.5 size-4" />} 31 + <Badge 33 32 variant="flat" 34 - color="secondary" 35 - size="sm" 33 + radius="rounded" 36 34 > 35 + <HiStar /> 37 36 {repo.stargazers_count} 38 - </ClientChip> 39 - <ClientChip 40 - startContent={<HiBeaker className="mx-1 size-3" />} 41 - size="sm" 37 + </Badge> 38 + <Badge 39 + variant="flat" 40 + radius="rounded" 42 41 > 42 + <HiBeaker /> 43 43 {repo.language} 44 - </ClientChip> 44 + </Badge> 45 45 </div> 46 46 <span className="opacity-75">{repo.description}</span> 47 47 </div>
+24 -50
app/ai-gallery/(home)/filter.component.tsx
··· 1 1 "use client"; 2 2 3 - import { Badge, Button, Popover, PopoverContent, PopoverTrigger } from "@nextui-org/react"; 4 3 import { useRouter } from "next/navigation"; 5 - import { useCookies } from "next-client-cookies"; 6 4 import { useEffect, useState } from "react"; 7 5 8 6 import Switch from "@/components/inputs/switch"; 7 + import { Button } from "@/components/ui/button"; 8 + import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 9 9 10 - export default function SearchFilter( 10 + export function SearchFilter( 11 11 { 12 12 searchParams 13 13 }: { ··· 19 19 } 20 20 ) { 21 21 const router = useRouter(); 22 - const cookies = useCookies(); 23 22 24 23 const [isEmbedded, setEmbedded] = useState(false); 25 24 ··· 31 30 32 31 return ( 33 32 <Popover 34 - offset={6} 35 - placement="bottom" 36 33 > 37 - 38 - <Badge 39 - color="secondary" 40 - content="enable 18+" 41 - placement="top-left" 42 - isInvisible={cookies.get("dismissed-ai-nsfw") === "true"} 43 - > 44 - <PopoverTrigger> 45 - <Button 46 - onClick={() => 47 - cookies.set( 48 - "dismissed-ai-nsfw", 49 - "true", 50 - { 51 - expires: 28, 52 - sameSite: "Strict" 53 - } 54 - ) 55 - } 56 - > 57 - Search filter 58 - </Button> 59 - </PopoverTrigger> 60 - </Badge> 34 + <PopoverTrigger asChild> 35 + <Button> 36 + Search Filter 37 + </Button> 38 + </PopoverTrigger> 61 39 62 40 <PopoverContent 63 - className="w-[240px] backdrop-blur-xl backdrop-brightness-50 bg-black/80" 41 + className="space-y-2" 42 + align="end" 64 43 > 65 - {(titleProps) => ( 66 - <div className="px-1 pt-2 w-full"> 67 - <p className="text-small font-bold text-foreground" {...titleProps}> 68 - Search options 69 - </p> 70 - <div className="mt-2 flex flex-col gap-2 w-full"> 71 - <Switch 72 - name="Show 18+" 73 - defaultState={searchParams.nsfw === "true"} 74 - onSave={(checked) => { 75 - const params = new URLSearchParams(searchParams); 76 - params.delete("nsfw"); 44 + <h4 className="font-medium leading-none">Search Options</h4> 45 + <div className="flex flex-col gap-2 w-full"> 46 + <Switch 47 + className="-mb-5" 48 + name="Show 18+" 49 + defaultState={searchParams.nsfw === "true"} 50 + onSave={(checked) => { 51 + const params = new URLSearchParams(searchParams); 52 + params.delete("nsfw"); 77 53 78 - if (!checked) params.append("nsfw", "true"); 54 + if (!checked) params.append("nsfw", "true"); 79 55 80 - router.replace(`?${params.toString()}`); 81 - }} 82 - /> 83 - </div> 84 - </div> 85 - )} 56 + router.replace(`?${params.toString()}`); 57 + }} 58 + /> 59 + </div> 86 60 </PopoverContent> 87 61 88 62 </Popover>
+29 -28
app/ai-gallery/(home)/layout.tsx
··· 2 2 import { Montserrat } from "next/font/google"; 3 3 import Image from "next/image"; 4 4 import Link from "next/link"; 5 - import { BsDiscord } from "react-icons/bs"; 6 5 import { HiUserAdd } from "react-icons/hi"; 7 6 8 - import { ClientButton } from "@/components/client"; 9 7 import { Footer } from "@/components/footer"; 8 + import { Button } from "@/components/ui/button"; 10 9 import CommandPic from "@/public/image-command.webp"; 11 10 import { cn } from "@/utils/cn"; 12 11 import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; ··· 55 54 56 55 return (<> 57 56 58 - <h1 59 - className={cn(montserrat.className, "lg:text-5xl text-4xl font-bold dark:text-neutral-100 text-neutral-900 break-words mb-2 w-full")} 60 - > 57 + <h1 className={cn(montserrat.className, "lg:text-5xl text-4xl font-bold dark:text-neutral-100 text-neutral-900 break-words mb-2 w-full")}> 61 58 <span className="bg-gradient-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent h-20 break-keep">/image Ai</span> 62 59 {" generated in "} 63 60 <span className="underline decoration-blurple break-keep">Discord</span> ··· 70 67 prefetch={false} 71 68 target="_blank" 72 69 > 73 - <span className="sr-only">/image command usage</span> 74 70 <Image 75 - alt="" 71 + alt="/image command usage" 76 72 className="w-full rounded-md shadow-md mt-12" 77 73 height={580} 78 74 src={CommandPic} ··· 80 76 /> 81 77 </Link> 82 78 83 - <div className="flex gap-2 mt-4"> 84 - <ClientButton 85 - as={Link} 86 - className="w-1/2 lg:w-fit !text-xl !font-medium" 87 - color="secondary" 88 - href="/login?invite=true" 89 - prefetch={false} 90 - size="lg" 91 - startContent={<HiUserAdd />} 79 + <div className="flex gap-2 m-4 w-full"> 80 + <Button 81 + asChild 82 + className="w-1/2 lg:w-fit text-lg font-medium" 83 + variant="secondary" 92 84 > 93 - <span className="block sm:hidden">Invite</span> 94 - <span className="hidden sm:block">Invite Wamellow</span> 95 - </ClientButton> 96 - <ClientButton 97 - as={Link} 98 - startContent={<BsDiscord />} 99 - className="w-1/2 lg:w-fit !text-xl !font-medium" 100 - href="/support" 101 - size="lg" 85 + <Link 86 + prefetch={false} 87 + href="/login?invite=true" 88 + > 89 + <HiUserAdd /> 90 + <span className="block sm:hidden">Invite</span> 91 + <span className="hidden sm:block">Invite Wamellow</span> 92 + </Link> 93 + </Button> 94 + <Button 95 + asChild 96 + className="w-1/2 lg:w-fit text-lg" 102 97 > 103 - <span className="block sm:hidden">Support</span> 104 - <span className="hidden sm:block">Join support</span> 105 - </ClientButton> 98 + <Link 99 + prefetch={false} 100 + href="/support" 101 + > 102 + <HiUserAdd /> 103 + <span className="block sm:hidden">Support</span> 104 + <span className="hidden sm:block">Join Support</span> 105 + </Link> 106 + </Button> 106 107 </div> 107 108 108 109 <Footer />
+6 -11
app/ai-gallery/(home)/page.tsx
··· 1 - import { Chip } from "@nextui-org/react"; 2 1 import Image from "next/image"; 3 2 import Link from "next/link"; 4 3 5 4 import Notice from "@/components/notice"; 6 5 import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 6 + import { Badge } from "@/components/ui/badge"; 7 7 import SadWumpusPic from "@/public/sad-wumpus.gif"; 8 8 9 9 import { getUploads } from "../api"; 10 - import SearchFilter from "./filter.component"; 10 + import { SearchFilter } from "./filter.component"; 11 11 import Pagination from "./pagination.component"; 12 12 13 13 interface Props { ··· 46 46 } 47 47 48 48 if (!uploads.results.length) { 49 - return ( 50 - <Notice 51 - message="No uploads were found for the specified model." 52 - /> 53 - ); 49 + return <Notice message="No uploads were found for the specified model." />; 54 50 } 55 51 56 52 return (<> ··· 82 78 width={300} 83 79 /> 84 80 85 - <Chip 81 + <Badge 86 82 className="absolute top-2 left-2 z-10 backdrop-blur-xl backdrop-brightness-50" 87 - color="secondary" 83 + variant="secondary" 88 84 size="sm" 89 - variant="dot" 90 85 > 91 86 {upload.model} 92 - </Chip> 87 + </Badge> 93 88 94 89 <div className="p-3 flex gap-2"> 95 90 <p className="truncate">
+11 -21
app/ai-gallery/[uploadId]/layout.tsx
··· 1 - import { Button } from "@nextui-org/react"; 2 1 import type { Metadata } from "next"; 3 - import NextImage from "next/image"; 2 + import Image from "next/image"; 4 3 import Link from "next/link"; 5 - import { HiArrowLeft, HiHome, HiPlus } from "react-icons/hi"; 4 + import { HiArrowLeft, HiPlus } from "react-icons/hi"; 6 5 7 - import { ClientButton } from "@/components/client"; 8 6 import { Footer } from "@/components/footer"; 9 7 import Notice from "@/components/notice"; 10 - import { ScreenMessage, SupportButton } from "@/components/screen-message"; 8 + import { ScreenMessage } from "@/components/screen-message"; 9 + import { Button } from "@/components/ui/button"; 11 10 import { getPageAnalytics } from "@/lib/analytics"; 12 11 import { getGuild } from "@/lib/api"; 13 12 import SadWumpusPic from "@/public/sad-wumpus.gif"; ··· 84 83 top="4rem" 85 84 title="Something went wrong!" 86 85 description={upload.message} 87 - buttons={<> 88 - <ClientButton 89 - as={Link} 90 - href="/" 91 - startContent={<HiHome />} 92 - > 93 - Go back to Home 94 - </ClientButton> 95 - <SupportButton /> 96 - </>} 97 86 > 98 - <NextImage src={SadWumpusPic} alt="" height={141} width={124} /> 87 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 99 88 </ScreenMessage> 100 89 } 101 90 </div> ··· 125 114 className="h-24 w-24" 126 115 href={`/ai-gallery/${upload.id}`} 127 116 > 128 - <NextImage 117 + <Image 129 118 alt={upload.prompt} 130 119 className="rounded-lg" 131 120 height={128} ··· 150 139 </div> 151 140 152 141 <Button 142 + asChild 153 143 className="!mt-5" 154 - as={Link} 155 - href="/ai-gallery" 156 - startContent={<HiArrowLeft />} 157 144 > 158 - View all Uploads 145 + <Link href="/ai-gallery"> 146 + <HiArrowLeft /> 147 + View all Uploads 148 + </Link> 159 149 </Button> 160 150 161 151 <Footer className="mt-10" />
+33 -39
app/ai-gallery/[uploadId]/page.tsx
··· 1 - import { Chip, Image } from "@nextui-org/react"; 2 - import NextImage from "next/image"; 1 + import Image from "next/image"; 3 2 import Link from "next/link"; 4 3 import { HiExternalLink } from "react-icons/hi"; 4 + 5 + import { Badge } from "@/components/ui/badge"; 5 6 6 7 import { getUpload } from "../api"; 7 8 ··· 19 20 20 21 const src = `https://r2.wamellow.com/ai-image/${upload.id}.webp`; 21 22 22 - return ( 23 - <div> 24 - 25 - <div className="relative"> 26 - <Image 27 - alt={upload.prompt} 28 - as={NextImage} 29 - className="rounded-lg" 30 - height={1024} 31 - isBlurred 32 - isZoomed 33 - src={src} 34 - width={1024} 35 - /> 23 + return (<> 24 + <div className="relative"> 25 + <Image 26 + alt={upload.prompt} 27 + className="rounded-lg" 28 + height={1024} 29 + src={src} 30 + width={1024} 31 + /> 36 32 37 - <div className="relative md:absolute px-2 md:p-4 bottom-8 md:bottom-0 left-0 z-10 -mb-466md:mb-0"> 38 - <div className="bg-wamellow backdrop-blur-xl backdrop-brightness-50 rounded-lg overflow-hidden shadow-lg p-4"> 39 - <Chip 40 - color="secondary" 41 - className="mb-2" 42 - variant="dot" 43 - size="lg" 44 - > 45 - {upload.model} 46 - </Chip> 47 - <div className="text-xl font-medium text-neutral-200">{upload.prompt}</div> 48 - <div className="text-medium">{upload.negativePrompt}</div> 49 - </div> 33 + <div className="relative md:absolute px-2 md:p-4 bottom-8 md:bottom-0 left-0 z-10 -mb-466md:mb-0"> 34 + <div className="bg-wamellow backdrop-blur-xl backdrop-brightness-50 rounded-lg overflow-hidden shadow-lg p-4"> 35 + <Badge 36 + className="mb-2" 37 + variant="secondary" 38 + radius="rounded" 39 + > 40 + {upload.model} 41 + </Badge> 42 + <div className="text-xl font-medium text-neutral-200">{upload.prompt}</div> 43 + <div className="text-medium">{upload.negativePrompt}</div> 50 44 </div> 51 45 </div> 46 + </div> 52 47 53 48 54 - <Link 55 - className="my-1 z-20 flex items-center gap-1" 56 - target="_blank" 57 - href={src} 58 - > 59 - <span>Open original file</span> 60 - <HiExternalLink /> 61 - </Link> 49 + <Link 50 + className="my-1 z-20 flex items-center gap-1" 51 + target="_blank" 52 + href={src} 53 + > 54 + Open original file 55 + <HiExternalLink /> 56 + </Link> 62 57 63 - </div> 64 - ); 58 + </>); 65 59 }
+40 -85
app/ai-gallery/[uploadId]/side.component.tsx
··· 1 1 "use client"; 2 2 3 - import { Accordion, AccordionItem, Button, Chip, Tooltip } from "@nextui-org/react"; 3 + import { Accordion, AccordionItem } from "@nextui-org/react"; 4 4 import Link from "next/link"; 5 5 import { useCookies } from "next-client-cookies"; 6 - import { FaReddit, FaTwitter } from "react-icons/fa"; 7 - import { HiCheck, HiHand, HiShare, HiUserGroup } from "react-icons/hi"; 6 + import { HiHand, HiUserGroup } from "react-icons/hi"; 8 7 9 8 import Ad from "@/components/ad"; 10 - import { CopyToClipboardButton } from "@/components/copy-to-clipboard"; 11 9 import ImageReduceMotion from "@/components/image-reduce-motion"; 10 + import { Share } from "@/components/share"; 12 11 import { formatDate } from "@/components/time"; 12 + import { Badge } from "@/components/ui/badge"; 13 13 import type { AnalyticsError, AnalyticsResponse } from "@/lib/analytics"; 14 14 import type { ApiError, ApiV1GuildsGetResponse, ApiV1UploadGetResponse } from "@/typings"; 15 - import { truncate } from "@/utils/truncate"; 16 15 import { getCanonicalUrl } from "@/utils/urls"; 17 16 18 17 export default function Side({ ··· 26 25 }) { 27 26 const cookies = useCookies(); 28 27 29 - const prompt = "prompt" in upload 30 - ? truncate(upload.prompt.split(" ").map((str) => str.replace(/^\w/, (char) => char.toUpperCase())).join(" "), 32) 31 - : null; 32 - 33 28 return ( 34 29 <div className="flex flex-col gap-3"> 35 - 36 - {"id" in upload && 37 - <div className="flex gap-2 w-full"> 38 - <CopyToClipboardButton 39 - className="w-full !justify-start" 40 - title="Share image" 41 - text={getCanonicalUrl("ai-gallery", upload.id as string)} 42 - icon={<HiShare />} 43 - /> 44 - <Tooltip content="Share on Reddit" delay={0} closeDelay={0} showArrow> 45 - <Button 46 - as={Link} 47 - href={`https://reddit.com/submit?title=${encodeURIComponent(`${prompt} - /image using ${upload.model} with wamellow.com`)}&text=${`Hey! I created this AI /image with the ${upload.model} model and the prompt: "${encodeURIComponent(upload.prompt)}"${encodeURIComponent("\n")}what are your thoughts on it? ${encodeURIComponent("\n\n")}${getCanonicalUrl("ai-gallery", upload.id as string)}`}`} 48 - target="_blank" 49 - isIconOnly 50 - > 51 - <FaReddit /> 52 - </Button> 53 - </Tooltip> 54 - <Tooltip content="Share on Twitter/X" delay={0} closeDelay={0} showArrow> 55 - <Button 56 - as={Link} 57 - href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(`Created an #ai /image with the ${upload.model} AI model on on wamellow.com with the prompt: ${prompt}`)}&url=${encodeURIComponent(getCanonicalUrl("ai-gallery", upload.id as string))}&hashtags=${encodeURIComponent("wamellow,discord")}`} 58 - target="_blank" 59 - isIconOnly 60 - > 61 - <FaTwitter /> 62 - </Button> 63 - </Tooltip> 64 - </div> 30 + {"prompt" in upload && 31 + <Share 32 + title="Share image" 33 + url={getCanonicalUrl("ai-gallery", upload.id)} 34 + text={`${upload.author.username} created an #ai image with the #wamellow bot for #discord using ${upload.model}!`} 35 + /> 65 36 } 66 37 67 38 <Ad ··· 85 56 86 57 <div className="flex items-center justify-between"> 87 58 <span>Created</span> 88 - <Chip 89 - className="select-none" 90 - radius="sm" 91 - > 59 + <Badge> 92 60 {"createdAt" in upload ? 93 61 formatDate(upload.createdAt as string, "en-US") 94 62 : 95 63 "unknown" 96 64 } 97 - </Chip> 65 + </Badge> 98 66 </div> 99 67 100 68 {analytics && "results" in analytics && 101 69 <div className="flex items-center justify-between"> 102 - <span>Views</span> 103 - <Chip 104 - className="select-none" 105 - radius="sm" 106 - > 70 + Views 71 + <Badge> 107 72 {Array.isArray(analytics.results) && analytics.results.length ? 108 73 (analytics.results[0].pageviews) 109 74 : ··· 111 76 } 112 77 {" "} 113 78 views 114 - </Chip> 79 + </Badge> 115 80 </div> 116 81 } 117 82 118 83 {"author" in upload && 119 84 <div className="flex items-center justify-between"> 120 - <span>Author</span> 121 - <Chip 122 - className="flex select-none" 123 - startContent={ 124 - <ImageReduceMotion 125 - className="rounded-full" 126 - alt="uploader's avatar" 127 - url={"https://cdn.discordapp.com/avatars/" + upload.authorId + "/" + upload.author.avatar} 128 - size={16} 129 - /> 130 - } 131 - > 85 + Author 86 + <Badge className="flex"> 87 + <ImageReduceMotion 88 + className="rounded-full relative right-1" 89 + alt="uploader's avatar" 90 + url={"https://cdn.discordapp.com/avatars/" + upload.authorId + "/" + upload.author.avatar} 91 + size={16} 92 + /> 93 + 132 94 {upload.author.username} 133 95 134 96 {upload.author.bot && 135 - <Chip 136 - className="ml-2 h-4.5" 137 - startContent={<HiCheck />} 97 + <Badge 98 + className="relative left-1" 138 99 color="secondary" 139 100 variant="flat" 140 - size="sm" 101 + size="xs" 141 102 > 142 - <span className="font-semibold">APP</span> 143 - </Chip> 103 + BOT 104 + </Badge> 144 105 } 145 - </Chip> 106 + </Badge> 146 107 </div> 147 108 } 148 109 149 110 <div className="flex items-center justify-between"> 150 - <span>Rating</span> 151 - <Chip 111 + Rating 112 + <Badge 152 113 className="font-bold select-none" 153 - color={"nsfw" in upload && upload.nsfw ? "danger" : "default"} 154 - radius="sm" 155 - startContent={ 156 - <span className="mx-1"> 157 - {"nsfw" in upload && upload.nsfw 158 - ? <HiHand /> 159 - : <HiUserGroup /> 160 - } 161 - </span> 114 + variant={"nsfw" in upload && upload.nsfw ? "destructive" : "default"} 115 + > 116 + {"nsfw" in upload && upload.nsfw 117 + ? <HiHand /> 118 + : <HiUserGroup /> 162 119 } 163 - > 164 120 {"nsfw" in upload && upload.nsfw ? "Mature" : "Everyone"} 165 - </Chip> 121 + </Badge> 166 122 </div> 167 123 168 124 </AccordionItem> ··· 203 159 /> 204 160 205 161 <div> 206 - <div className="text-lg text-neutral-200 font-semibold">{guild.name}</div> 162 + <div className="text-lg text-neutral-200 font-semibold truncate">{guild.name}</div> 207 163 <div className="text-sm font-medium">View leaderboard</div> 208 164 </div> 209 165 </Link> ··· 213 169 The image has been generated by artificial intelligence (AI) and not by a human creator. Wamellow and its developers disclaim any responsibility for the content of the images and reserve all rights to the media. 214 170 </div> 215 171 216 - 217 - </div > 172 + </div> 218 173 ); 219 174 220 175 }
-69
app/ai-gallery/generate/layout.tsx
··· 1 - import { Button } from "@nextui-org/react"; 2 - import type { Metadata } from "next"; 3 - import Link from "next/link"; 4 - import { HiArrowLeft } from "react-icons/hi"; 5 - 6 - import { Footer } from "@/components/footer"; 7 - import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; 8 - 9 - export interface Props { 10 - params: Promise<{ uploadId: string; }>; 11 - children: React.ReactNode; 12 - } 13 - 14 - export const revalidate = 3600; 15 - 16 - export const generateMetadata = async ({ params }: Props): Promise<Metadata> => { 17 - const { uploadId } = await params; 18 - 19 - const title = "Generate images with your Intel® Arc™ A-Series GPU"; 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."; 21 - const images = `${getBaseUrl()}/waya-v3.webp?v=2`; 22 - const url = getCanonicalUrl("ai-gallery", uploadId); 23 - 24 - return { 25 - title, 26 - description, 27 - alternates: { 28 - canonical: url 29 - }, 30 - openGraph: { 31 - title, 32 - description, 33 - type: "website", 34 - url, 35 - images 36 - }, 37 - twitter: { 38 - card: "summary", 39 - site: "wamellow.com", 40 - title, 41 - description, 42 - images 43 - } 44 - }; 45 - }; 46 - 47 - export default function RootLayout({ 48 - children 49 - }: Props) { 50 - 51 - return ( 52 - <div className="w-full space-y-12"> 53 - 54 - {children} 55 - 56 - <Button 57 - className="!mt-5" 58 - as={Link} 59 - href="/ai-gallery" 60 - startContent={<HiArrowLeft />} 61 - > 62 - View all Uploads 63 - </Button> 64 - 65 - <Footer className="mt-10" /> 66 - 67 - </div> 68 - ); 69 - }
-164
app/ai-gallery/generate/page.tsx
··· 1 - "use client"; 2 - 3 - import { Button, Image, Spinner } from "@nextui-org/react"; 4 - import NextImage from "next/image"; 5 - import { useRouter, useSearchParams } from "next/navigation"; 6 - import { useCookies } from "next-client-cookies"; 7 - import { useEffect, useState } from "react"; 8 - import { HiPrinter } from "react-icons/hi"; 9 - 10 - import DumbTextInput from "@/components/inputs/dumb-text-input"; 11 - import LinkTag from "@/components/link-tag"; 12 - import Notice from "@/components/notice"; 13 - import { cn } from "@/utils/cn"; 14 - 15 - import Time from "./time.component"; 16 - import UploadButton from "./upload.component"; 17 - 18 - enum State { 19 - Idle = 0, 20 - Loading = 1 21 - } 22 - 23 - export default function Home() { 24 - const cookies = useCookies(); 25 - const search = useSearchParams(); 26 - const router = useRouter(); 27 - 28 - const imageUrl = search.get("image_url"); 29 - 30 - const [state, setState] = useState<State>(State.Idle); 31 - const [error, setError] = useState<string | null>(null); 32 - 33 - const [gpu, setGpu] = useState<string | null>(null); 34 - 35 - const [baseUrl, setBaseUrl] = useState("https://ai.local.wamellow.com"); 36 - const [model, setModel] = useState("animagine-xl-v3"); 37 - const [prompt, setPrompt] = useState(""); 38 - 39 - useEffect(() => { 40 - fetch(baseUrl) 41 - .then((res) => res.json() as Promise<{ gpu: string; }>) 42 - .then((res) => { 43 - setError(null); 44 - setGpu(res.gpu || null); 45 - }) 46 - .catch((err) => { 47 - setError(`Could not fetch local GPU instance (${err.toString()})`); 48 - }); 49 - }, [baseUrl]); 50 - 51 - async function generate() { 52 - setState(State.Loading); 53 - 54 - let params = new URLSearchParams(); 55 - params.delete("image_url"); 56 - router.push(`?${params.toString()}`, { scroll: false }); 57 - 58 - const reqparams = new URLSearchParams({ 59 - prompt: prompt 60 - }); 61 - 62 - const res = await fetch(`${baseUrl}/generate/image/${model}?${reqparams.toString()}`) 63 - .then((res) => res.json()) as { url: string; duration: number; }; 64 - 65 - params = new URLSearchParams(); 66 - params.delete("image_url"); 67 - params.append("image_url", res.url); 68 - 69 - router.push(`?${params.toString()}`, { scroll: false }); 70 - 71 - setState(State.Idle); 72 - } 73 - 74 - return ( 75 - <div className="w-full"> 76 - 77 - {error && 78 - <Notice message={error} /> 79 - } 80 - 81 - <div className="flex flex-col-reverse md:flex-row"> 82 - 83 - <div className="md:w-2/3 w-full md:mr-6"> 84 - {imageUrl && state !== State.Loading ? 85 - <Image 86 - as={NextImage} 87 - className="rounded-lg" 88 - height={1024} 89 - isBlurred 90 - isZoomed 91 - src={imageUrl || ""} 92 - width={1024} 93 - onError={() => { 94 - const params = new URLSearchParams(); 95 - params.delete("image_url"); 96 - router.push(`?${params.toString()}`, { scroll: false }); 97 - }} 98 - /> 99 - : 100 - <div className="w-full aspect-square border border-violet-400 bg-wamellow flex items-center justify-center rounded-lg shadow-xl"> 101 - {state === State.Loading 102 - ? 103 - <div> 104 - <Spinner color="secondary" /> 105 - </div> 106 - : 107 - <span className="text-2xl font-medium text-neutral-200"> 108 - No image generated yet 109 - </span> 110 - } 111 - </div> 112 - } 113 - </div> 114 - 115 - <div className="md:w-1/3 mb-8 md:mt-0 flex flex-col"> 116 - <DumbTextInput name="Base URL" value={baseUrl} setValue={setBaseUrl} thin /> 117 - <DumbTextInput name="Model" value={model} setValue={setModel} thin /> 118 - <DumbTextInput name="Prompt" value={prompt} setValue={setPrompt} placeholder="bocchi the rock" thin /> 119 - 120 - <Button 121 - className="mt-4" 122 - color="secondary" 123 - onClick={generate} 124 - startContent={ 125 - state === State.Loading 126 - ? <></> 127 - : <HiPrinter /> 128 - } 129 - isLoading={state === State.Loading} 130 - isDisabled={!gpu || !prompt || !model} 131 - > 132 - Generate 133 - </Button> 134 - 135 - <div className="flex justify-between"> 136 - <span className="font-medium"> 137 - {gpu || "unknown gpu"} 138 - </span> 139 - 140 - <span className="italic"> 141 - (<Time loading={state === State.Loading} />) 142 - </span> 143 - </div> 144 - 145 - <UploadButton 146 - model={model} 147 - prompt={prompt} 148 - /> 149 - 150 - <div 151 - className={cn( 152 - "mt-6", 153 - cookies.get("devTools") === "true" && "hidden md:block" 154 - )} 155 - > 156 - Generate AI images using your own <LinkTag href="https://www.intel.com/content/www/us/en/products/details/discrete-gpus/arc.html">Intel® Arc™ A-Series GPU</LinkTag> (16gb vram minimum). 157 - Make sure you have <LinkTag href="https://github.com/Luna-devv/intel-arc-ai">Luna-devv/intel-arc-ai</LinkTag> installed and running locally on your machine or local network. 158 - </div> 159 - 160 - </div> 161 - </div> 162 - </div> 163 - ); 164 - }
-34
app/ai-gallery/generate/time.component.tsx
··· 1 - import { useEffect, useRef, useState } from "react"; 2 - 3 - export default function Time({ 4 - loading 5 - }: { 6 - loading: boolean; 7 - }) { 8 - const intervalRef = useRef<NodeJS.Timeout | null>(null); 9 - 10 - const [previousLoading, setPreviousLoading] = useState(false); 11 - const [estimatedTime, setEstimatedTime] = useState(0); 12 - 13 - useEffect(() => { 14 - if (loading === previousLoading) return; 15 - 16 - if (!previousLoading && loading) setEstimatedTime(0); 17 - setPreviousLoading(loading); 18 - 19 - if (!loading) { 20 - if (intervalRef.current) clearInterval(intervalRef.current); 21 - return; 22 - } 23 - 24 - intervalRef.current = setInterval(() => { 25 - setEstimatedTime((prev) => prev + 100); 26 - }, 100); 27 - }, [loading]); 28 - 29 - return ( 30 - <span className="inline"> 31 - {(estimatedTime / 1000).toFixed(1)}s 32 - </span> 33 - ); 34 - }
-117
app/ai-gallery/generate/upload.component.tsx
··· 1 - "use client"; 2 - 3 - import { Button, Checkbox } from "@nextui-org/react"; 4 - import { useSearchParams } from "next/navigation"; 5 - import { useCookies } from "next-client-cookies"; 6 - import { useEffect, useState } from "react"; 7 - import { HiCloudUpload } from "react-icons/hi"; 8 - 9 - import type { ApiV1UploadGetResponse } from "@/typings"; 10 - import { cn } from "@/utils/cn"; 11 - 12 - enum State { 13 - Idle = 0, 14 - Loading = 1, 15 - Success = 2 16 - } 17 - 18 - export default function UploadButton({ 19 - model, 20 - prompt 21 - }: { 22 - model: string; 23 - prompt: string; 24 - }) { 25 - const search = useSearchParams(); 26 - const cookies = useCookies(); 27 - 28 - const imageUrl = search.get("image_url"); 29 - 30 - const [state, setState] = useState<State>(State.Idle); 31 - const [error, setError] = useState<string | null>(null); 32 - 33 - const [nsfw, setNsfw] = useState(false); 34 - 35 - const isDisabled = !cookies.get("session") || !imageUrl; 36 - 37 - useEffect(() => { 38 - setState(State.Idle); 39 - }, [imageUrl]); 40 - 41 - async function upload() { 42 - setState(State.Loading); 43 - 44 - const res = await fetch(`${process.env.NEXT_PUBLIC_API}/ai`, { 45 - method: "POST", 46 - credentials: "include", 47 - headers: { 48 - "Content-Type": "application/json" 49 - }, 50 - body: JSON.stringify({ 51 - url: imageUrl, 52 - model: model, 53 - prompt: prompt, 54 - nsfw: nsfw 55 - }) 56 - }) 57 - .then((res) => res.json()) as ApiV1UploadGetResponse; 58 - 59 - if ("message" in res) { 60 - setError(res.message as string); 61 - return; 62 - } 63 - 64 - setState(State.Success); 65 - } 66 - 67 - return ( 68 - <div 69 - className={cn( 70 - "flex flex-col", 71 - isDisabled && "cursor-not-allowed select-none blur-sm opacity-75" 72 - )} 73 - > 74 - 75 - <div className="mt-6 flex items-center"> 76 - <Checkbox 77 - isSelected={nsfw} 78 - onValueChange={(now) => setNsfw(now)} 79 - color="secondary" 80 - isDisabled={isDisabled} 81 - /> 82 - <span className="font-medium">Is NSFW content?</span> 83 - </div> 84 - 85 - <Button 86 - className="mt-4" 87 - color={ 88 - state === State.Success 89 - ? "success" 90 - : "secondary" 91 - } 92 - onClick={upload} 93 - startContent={ 94 - state === State.Loading 95 - ? <></> 96 - : <HiCloudUpload /> 97 - } 98 - isLoading={state === State.Loading} 99 - isDisabled={state === State.Success || isDisabled} 100 - > 101 - { 102 - state === State.Success 103 - ? "Upload Successful" 104 - : "Upload" 105 - } 106 - </Button> 107 - 108 - {error && 109 - <span className="dark:text-red-500 text-red-400 text-sm mt-1"> 110 - {error} 111 - </span> 112 - } 113 - 114 - </div> 115 - ); 116 - 117 - }
+11 -27
app/dashboard/[guildId]/layout.tsx
··· 3 3 import { Button, Skeleton } from "@nextui-org/react"; 4 4 import Image from "next/image"; 5 5 import Link from "next/link"; 6 - import { redirect, useParams, usePathname } from "next/navigation"; 6 + import { redirect, useParams } from "next/navigation"; 7 7 import { useCookies } from "next-client-cookies"; 8 8 import { Suspense, useEffect, useMemo, useState } from "react"; 9 - import { HiArrowNarrowLeft, HiBell, HiChartBar, HiCode, HiCursorClick, HiEye, HiHome, HiPaperAirplane, HiShare, HiStar, HiUserAdd, HiUsers, HiViewGridAdd } from "react-icons/hi"; 9 + import { HiArrowNarrowLeft, HiBell, HiChartBar, HiCode, HiEye, HiHome, HiPaperAirplane, HiStar, HiUserAdd, HiUsers, HiViewGridAdd } from "react-icons/hi"; 10 10 import { useQuery } from "react-query"; 11 11 12 12 import { guildStore } from "@/common/guilds"; 13 13 import { ClientButton } from "@/components/client"; 14 - import { CopyToClipboardButton } from "@/components/copy-to-clipboard"; 15 14 import ImageReduceMotion from "@/components/image-reduce-motion"; 16 15 import { ListTab } from "@/components/list"; 17 16 import { ScreenMessage, SupportButton } from "@/components/screen-message"; ··· 19 18 import SadWumpusPic from "@/public/sad-wumpus.gif"; 20 19 import type { ApiV1GuildsChannelsGetResponse, ApiV1GuildsEmojisGetResponse, ApiV1GuildsGetResponse, ApiV1GuildsRolesGetResponse } from "@/typings"; 21 20 import { intl } from "@/utils/numbers"; 22 - import { getCanonicalUrl } from "@/utils/urls"; 23 21 24 22 function useGuildData<T extends unknown[]>( 25 23 url: string, ··· 46 44 }) { 47 45 const cookies = useCookies(); 48 46 const params = useParams(); 49 - const path = usePathname(); 50 47 51 48 const [error, setError] = useState<string>(); 52 49 const [loaded, setLoaded] = useState<string[]>([]); ··· 54 51 const guild = guildStore((g) => g); 55 52 56 53 const session = useMemo(() => cookies.get("session"), [cookies]); 57 - const isDevMode = useMemo(() => cookies.get("devTools") === "true", [cookies]); 58 54 59 55 if (!session) redirect(`/login?callback=/dashboard/${params.guildId}`); 60 56 ··· 108 104 <title>{`${guild?.name}'s Dashboard`}</title> 109 105 110 106 <div className="flex flex-col gap-5 mb-3"> 111 - <div className="flex gap-2"> 112 - <Button 113 - as={Link} 114 - className="w-fit" 115 - href="/profile" 116 - startContent={<HiArrowNarrowLeft />} 117 - > 118 - Serverlist 119 - </Button> 120 - {isDevMode && 121 - <CopyToClipboardButton 122 - text={getCanonicalUrl("leaderboard", params.guildId?.toString() as string)} 123 - items={[ 124 - { icon: <HiShare />, name: "Copy page url", description: "Creates a link to this specific page", text: getCanonicalUrl(...path.split("/").slice(1)) }, 125 - { 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] || "/"}`) } 126 - ]} 127 - icon={<HiShare />} 128 - /> 129 - } 130 - </div> 107 + <Button 108 + as={Link} 109 + className="w-fit" 110 + href="/profile" 111 + startContent={<HiArrowNarrowLeft />} 112 + > 113 + Serverlist 114 + </Button> 131 115 132 116 <div className="text-lg flex gap-5"> 133 117 <Skeleton isLoaded={!isLoading} className="rounded-full h-14 w-14 ring-offset-[var(--background-rgb)] ring-2 ring-offset-2 ring-violet-400/40 shrink-0"> ··· 228 212 (guild && loaded.length === 3) ? children : <></> 229 213 } 230 214 231 - </div > 215 + </div> 232 216 ); 233 217 }
+33 -12
app/globals.css
··· 4 4 5 5 @layer base { 6 6 .dark { 7 + --background-rgb: rgb(3, 2, 6); 8 + 9 + --wamellow: #ffffff0d; 10 + --wamellow-100: #ffffff1a; 11 + --wamellow-200: #ffffff33; 7 12 --font-outfit: 'Outfit', sans-serif; 8 13 --font-noto-sans-jp: 'Noto Sans JP', sans-serif; 9 14 ··· 11 16 --wamellow-rgb: rgba(255, 255, 255, 0.16); 12 17 13 18 --foreground: 210 20% 98%; 19 + 14 20 --card: 224 71.4% 4.1%; 15 21 --card-foreground: 210 20% 98%; 22 + 16 23 --popover: 260 3% 4.1%; 17 24 --popover-foreground: 210 20% 98%; 25 + 18 26 --primary: 0 0% 100%; 19 27 --primary-foreground: 220.9 39.3% 89%; 28 + 20 29 --secondary: 258 89% 66%; 21 30 --secondary-foreground: 210 20% 98%; 31 + 32 + --flat: var(--secondary); 33 + --flat-foreground: 270 59% 85%; 34 + 22 35 --muted: 260 3% 16%; 23 - --muted-foreground: 217.9 10.6% 64.9%; 36 + --muted-foreground: 217 10% 65%; 37 + 24 38 --accent: 258 89% 66%; 25 39 --accent-foreground: 210 20% 98%; 26 - --destructive: 0 62.8% 30.6%; 40 + 41 + --destructive: 0 62.8% 40.6; 27 42 --destructive-foreground: 210 20% 98%; 43 + 28 44 --border: 260 3% 16%; 29 45 --input: 258 89% 66%; 30 46 --ring: 258 89% 66%; ··· 38 54 } 39 55 40 56 @layer base { 41 - * { 42 - @apply border-border; 57 + * { 58 + @apply border-border; 59 + } 60 + svg { 61 + @apply shrink-0; 62 + } 63 + button { 64 + @apply cursor-pointer 65 + } 66 + html { 67 + font-family: var(--font-outfit), var(--font-noto-sans-jp), sans-serif; 68 + scroll-behavior: smooth; 69 + min-height: 100svh; 70 + background: var(--background-rgb); 43 71 } 44 - } 45 - 46 - html { 47 - font-family: var(--font-outfit), var(--font-noto-sans-jp), sans-serif; 48 - 49 - min-height: 100svh!important; 50 - scroll-behavior: smooth; 51 72 } 52 73 53 74 div[id="bg"] { ··· 202 223 } 203 224 204 225 .button { 205 - @apply flex dark:text-neutral-300 text-neutral-700 dark:bg-wamellow bg-wamellow-100 dark:hover:bg-wamellow-light hover:bg-wamellow-100-light py-2 px-4 duration-200 justify-center gap-2 items-center text-medium 226 + @apply flex text-white bg-wamellow-100 hover:bg-wamellow-200 py-2 px-4 duration-200 justify-center gap-2 items-center text-medium 206 227 } 207 228 208 229 .button-primary {
+10 -91
app/layout.tsx
··· 11 11 import { HiBookOpen } from "react-icons/hi"; 12 12 import { SiKofi } from "react-icons/si"; 13 13 14 - import Header from "@/components/header"; 15 - import LoginButton from "@/components/login-button"; 16 - import Notice, { NoticeType } from "@/components/notice"; 14 + import { Header } from "@/components/header"; 15 + import { LoginButton } from "@/components/login-button"; 17 16 import { cn } from "@/utils/cn"; 18 17 import { getBaseUrl } from "@/utils/urls"; 19 18 ··· 64 63 }, 65 64 66 65 description, 67 - keywords: [ 68 - "discord", 69 - "bot", 70 - "app", 71 - "intefration", 72 - "discord bot", 73 - "discord app", 74 - "discord application", 75 - "app list", 76 - "waya", 77 - "waya bot", 78 - "waya.one", 79 - "mwya", 80 - "mellow", 81 - "wamellow", 82 - "mwlica", 83 - "lunish", 84 - "Luna-devv", 85 - "mee6 alternative", 86 - "arcane alternative", 87 - "dyno alternative", 88 - "starboard", 89 - "ranks", 90 - "leaderboard", 91 - "lb", 92 - "leaderboards", 93 - "text to speech", 94 - "captcha", 95 - "passport", 96 - "verification", 97 - "verify", 98 - "captcha.bot", 99 - "security", 100 - "tts", 101 - "text to speech", 102 - "free", 103 - "customizable", 104 - "next-gen", 105 - "next generation", 106 - "ai", 107 - "ai images", 108 - "nsfw detection", 109 - "moderation", 110 - "anime", 111 - "nekos", 112 - "waifus", 113 - "chat to speech", 114 - "accessibility", 115 - "aphonia", 116 - "dysphonia", 117 - "mute", 118 - "liapew", 119 - "wumpus", 120 - "wumpus store", 121 - "wumpus bots", 122 - "youtube notifications", 123 - "youtube notifis", 124 - "youtube to discord", 125 - "twitch notifications", 126 - "twitch notifis", 127 - "twitch to discord", 128 - "bluesky notifications", 129 - "bluesky notifis", 130 - "bluesky to discord", 131 - "bluesky", 132 - "bsky" 133 - ], 134 66 135 67 alternates: { 136 68 canonical: getBaseUrl() ··· 195 127 <div id="bg" className="absolute top-0 right-0 w-screen h-screen -z-50" /> 196 128 <Noise /> 197 129 198 - <NoScript /> 199 130 <NavBar /> 200 131 201 132 <Provider> ··· 239 170 ); 240 171 } 241 172 242 - function NoScript() { 243 - return ( 244 - <noscript className="p-4 pb-0 flex"> 245 - <Notice 246 - className="mb-0" 247 - message="This site needs JavaScript to work - Please either enable JavaScript or update to a supported Browser." 248 - type={NoticeType.Info} 249 - /> 250 - </noscript> 251 - ); 252 - } 253 - 254 173 async function NavBar() { 255 174 const jar = await cookies(); 256 175 257 176 return ( 258 - <nav className="p-4 flex items-center gap-2 text-base font-medium dark:text-neutral-300 text-neutral-700 select-none h-20"> 177 + <nav className="p-4 flex items-center gap-2 text-base font-medium text-neutral-300 select-none h-20 relative"> 259 178 <Link 260 179 aria-label="Go to Wamellow's homepage" 261 - className={cn("font-semibold flex items-center gap-2 mr-2", lexend.className)} 180 + className={cn("font-semibold flex items-center mr-2 shrink-0", lexend.className)} 262 181 href="/" 263 182 > 264 - <Image src="/waya-v3.webp" width={64} height={64} alt="" className="rounded-full w-8 h-8 shrink-0" /> 183 + <Image src="/waya-v3.webp" width={64} height={64} alt="" className="rounded-full size-8 shrink-0 mr-2" /> 265 184 <span className="text-xl dark:text-neutral-100 text-neutral-900 hidden sm:block">Wamellow</span> 266 185 </Link> 267 186 ··· 270 189 orientation="vertical" 271 190 /> 272 191 273 - <div className="flex"> 192 + <div className="flex shrink-0"> 274 193 <Link 275 194 href="https://ko-fi.com/mwlica" 276 - className="dark:hover:bg-wamellow-alpha hover:bg-wamellow-100-alpha py-1 px-3 rounded-md duration-200 hidden sm:flex items-center gap-2 group" 195 + className="hover:bg-wamellow py-1 px-3 rounded-md duration-200 hidden sm:flex items-center gap-2 group" 277 196 > 278 197 <SiKofi className="group-hover:text-[#ff6c6b] duration-200 mt-0.5" /> 279 198 Donate 280 199 </Link> 281 200 <Link 282 201 href="/docs/index" 283 - className="dark:hover:bg-wamellow-alpha hover:bg-wamellow-100-alpha py-1 px-3 rounded-md duration-200 flex items-center gap-2 group" 202 + className="hover:bg-wamellow py-1 px-3 rounded-md duration-200 flex items-center gap-2 group" 284 203 > 285 204 <HiBookOpen className="group-hover:text-neutral-300 duration-200 h-5 w-5 mt-0.5" /> 286 205 Docs ··· 288 207 </div> 289 208 290 209 {jar.get("session")?.value 291 - ? <Header className="ml-auto" /> 292 - : <LoginButton /> 210 + ? <Header /> 211 + : <LoginButton className="ml-auto" /> 293 212 } 294 213 </nav> 295 214 );
+12 -9
app/leaderboard/[guildId]/member.component.tsx
··· 3 3 import Link from "next/link"; 4 4 import { HiBadgeCheck } from "react-icons/hi"; 5 5 6 - import { ClientBadge, ClientChip, ClientCircularProgress } from "@/components/client"; 6 + import { ClientBadge, ClientCircularProgress } from "@/components/client"; 7 7 import DiscordAppBadge from "@/components/discord/app-badge"; 8 8 import ImageReduceMotion from "@/components/image-reduce-motion"; 9 + import { Badge } from "@/components/ui/badge"; 9 10 import type { ApiV1GuildsTopmembersGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 10 11 import getAverageColor from "@/utils/average-color"; 11 12 import { cn } from "@/utils/cn"; ··· 165 166 children: React.ReactNode; 166 167 }) { 167 168 return ( 168 - <ClientChip 169 - as={Link} 170 - color="secondary" 169 + <Link 170 + prefetch={false} 171 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 172 target="_blank" 175 - variant="flat" 176 173 > 177 - <span className="font-bold">{children}</span> 178 - </ClientChip> 174 + <Badge 175 + variant="flat" 176 + radius="rounded" 177 + > 178 + <HiBadgeCheck /> 179 + {children} 180 + </Badge> 181 + </Link> 179 182 ); 180 183 } 181 184
+2 -5
app/leaderboard/[guildId]/pagination.component.tsx
··· 4 4 import { useRouter } from "next/navigation"; 5 5 import { useCookies } from "next-client-cookies"; 6 6 7 - import LoginButton from "@/components/login-button"; 7 + import { LoginButton } from "@/components/login-button"; 8 8 9 9 interface Props { 10 10 searchParams: { ··· 19 19 const router = useRouter(); 20 20 21 21 if (!cookies.get("session")) return ( 22 - <LoginButton 23 - addClassName="justify-center" 24 - message="Login to view more" 25 - /> 22 + <LoginButton message="Login to view more"/> 26 23 ); 27 24 28 25 return (
+8 -32
app/leaderboard/[guildId]/side.component.tsx
··· 1 1 "use client"; 2 2 3 - import { Accordion, AccordionItem, Button, Code, Tooltip } from "@nextui-org/react"; 3 + import { Accordion, AccordionItem, Button, Code } from "@nextui-org/react"; 4 4 import Link from "next/link"; 5 5 import { useRouter } from "next/navigation"; 6 6 import { useCookies } from "next-client-cookies"; 7 7 import { useState } from "react"; 8 8 import { BsDiscord } from "react-icons/bs"; 9 - import { FaReddit, FaTwitter } from "react-icons/fa"; 10 - import { HiAnnotation, HiLink, HiShare, HiTrash, HiViewGridAdd, HiVolumeUp } from "react-icons/hi"; 9 + import { HiAnnotation, HiLink, HiTrash, HiViewGridAdd, HiVolumeUp } from "react-icons/hi"; 11 10 12 11 import Ad from "@/components/ad"; 13 - import { CopyToClipboardButton } from "@/components/copy-to-clipboard"; 14 12 import Modal from "@/components/modal"; 15 13 import Notice, { NoticeType } from "@/components/notice"; 14 + import { Share } from "@/components/share"; 16 15 import type { ApiError, ApiV1GuildsGetResponse, ApiV1GuildsTopmembersPaginationGetResponse } from "@/typings"; 17 16 import { intl } from "@/utils/numbers"; 18 17 import { getCanonicalUrl } from "@/utils/urls"; ··· 33 32 <div className="flex flex-col gap-3"> 34 33 35 34 {guild && "id" in guild && 36 - <div className="flex gap-2 w-full"> 37 - <CopyToClipboardButton 38 - className="w-full !justify-start" 39 - title="Share link" 40 - text={getCanonicalUrl("leaderboard", guild.id as string)} 41 - icon={<HiShare />} 42 - /> 43 - <Tooltip content="Share on Reddit" delay={0} closeDelay={0} showArrow> 44 - <Button 45 - as={Link} 46 - href={`https://reddit.com/submit?title=${encodeURIComponent(`${guild.name} discord leaderboard | wamellow.com`)}&text=${`Check out the leaderboard for ${guild.name} on wamellow.com! Join ${guild.inviteUrl || "the server"} and be the top member :)${encodeURIComponent("\n\n")}${getCanonicalUrl("leaderboard", guild.id as string)}`}`} 47 - target="_blank" 48 - isIconOnly 49 - > 50 - <FaReddit /> 51 - </Button> 52 - </Tooltip> 53 - <Tooltip content="Share on Twitter/X" delay={0} closeDelay={0} showArrow> 54 - <Button 55 - as={Link} 56 - href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(`Check out the leaderboard for ${guild.name} on wamellow.com! Join ${guild.inviteUrl ? (guild.inviteUrl?.split("//")[1] + " ") : ""}and be the top member :)\n`)}&url=${encodeURIComponent(getCanonicalUrl("leaderboard", guild.id))}&hashtags=${encodeURIComponent("wamellow,discord")}`} 57 - target="_blank" 58 - isIconOnly 59 - > 60 - <FaTwitter /> 61 - </Button> 62 - </Tooltip> 63 - </div> 35 + <Share 36 + title="Share leaderboard" 37 + url={getCanonicalUrl("leaderboard", guild.id)} 38 + text={`Check out the leaderboard for ${guild.name} on #wamellow! ${guild.inviteUrl ? `Join ${guild.inviteUrl.split("://")[1]} and get to be the top #discord member :p` : ""}`} 39 + /> 64 40 } 65 41 66 42 {guild && "inviteUrl" in guild && guild.inviteUrl &&
+10 -7
app/provider.tsx
··· 7 7 import { QueryClient, QueryClientProvider } from "react-query"; 8 8 9 9 import { guildStore } from "@/common/guilds"; 10 + import { TooltipProvider } from "@/components/ui/tooltip"; 10 11 11 12 const queryClient = new QueryClient(); 12 13 ··· 37 38 }, [path]); 38 39 39 40 return ( 40 - <NextUIProvider> 41 - <QueryClientProvider client={queryClient}> 42 - <main className="dark:text-neutral-400 text-neutral-700 flex flex-col items-center justify-between md:p-5 p-3 w-6xl max-w-full mt-2 md:mt-10"> 43 - {children} 44 - </main> 45 - </QueryClientProvider> 46 - </NextUIProvider> 41 + <TooltipProvider> 42 + <NextUIProvider> 43 + <QueryClientProvider client={queryClient}> 44 + <main className="dark:text-neutral-400 text-neutral-700 flex flex-col items-center justify-between md:p-5 p-3 w-6xl max-w-full mt-2 md:mt-10"> 45 + {children} 46 + </main> 47 + </QueryClientProvider> 48 + </NextUIProvider> 49 + </TooltipProvider> 47 50 ); 48 51 }
-62
components/avatar.tsx
··· 1 - "use client"; 2 - 3 - import { AvatarIcon, type AvatarProps as BaseAvatarProps, useAvatar } from "@nextui-org/react"; 4 - import Image from "next/image"; 5 - import { forwardRef, useMemo } from "react"; 6 - 7 - export type AvatarProps = BaseAvatarProps; 8 - 9 - export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(({ src: source, ...props }, ref) => { 10 - const { 11 - src, 12 - icon = <AvatarIcon />, 13 - alt, 14 - classNames, 15 - slots, 16 - name, 17 - showFallback, 18 - fallback: fallbackComponent, 19 - getInitials, 20 - getAvatarProps, 21 - getImageProps 22 - } = useAvatar({ 23 - ref, 24 - src: source?.replace(/size=\d{1,4}/, "size=16"), 25 - ...props 26 - }); 27 - 28 - const fallback = useMemo(() => { 29 - if (!showFallback && src) return null; 30 - 31 - const ariaLabel = alt || name || "avatar"; 32 - 33 - if (fallbackComponent) { 34 - return ( 35 - <div 36 - aria-label={ariaLabel} 37 - className={slots.fallback({ class: classNames?.fallback })} 38 - role="img" 39 - > 40 - {fallbackComponent} 41 - </div> 42 - ); 43 - } 44 - 45 - return name 46 - ? <span aria-label={ariaLabel} className={slots.name({ class: classNames?.name })} role="img"> 47 - {getInitials(name)} 48 - </span> 49 - : <span aria-label={ariaLabel} className={slots.icon({ class: classNames?.icon })} role="img"> 50 - {icon} 51 - </span>; 52 - }, [showFallback, src, fallbackComponent, name, classNames]); 53 - 54 - return ( 55 - <div {...getAvatarProps()}> 56 - {src && <Image {...getImageProps()} src={source as string} alt={alt} width={64} height={64} />} 57 - {fallback} 58 - </div> 59 - ); 60 - }); 61 - 62 - Avatar.displayName = "MyAvatar";
+1 -9
components/client.tsx
··· 1 1 "use client"; 2 2 3 - import { AvatarGroup, Badge, Button, Chip, CircularProgress, Image, Skeleton } from "@nextui-org/react"; 3 + import { AvatarGroup, Badge, Button, CircularProgress, Image, Skeleton } from "@nextui-org/react"; 4 4 5 5 export function ClientButton(props: React.ComponentProps<typeof Button>) { 6 6 return ( 7 7 <Button {...props}> 8 8 {props.children} 9 9 </Button> 10 - ); 11 - } 12 - 13 - export function ClientChip(props: React.ComponentProps<typeof Chip>) { 14 - return ( 15 - <Chip {...props}> 16 - {props.children} 17 - </Chip> 18 10 ); 19 11 } 20 12
+15 -62
components/copy-to-clipboard.tsx
··· 1 1 "use client"; 2 2 3 - import { Button, ButtonGroup, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "@nextui-org/react"; 4 3 import { useRef, useState } from "react"; 5 - import { HiChevronDown } from "react-icons/hi"; 4 + 5 + import { Button } from "./ui/button"; 6 6 7 7 interface Props { 8 8 icon?: React.ReactNode; 9 9 text: string; 10 10 title?: string; 11 - className?: string; 12 - items?: { icon?: React.ReactNode; name: string; description?: string; text: string; }[]; 13 11 needsWait?: boolean; 14 12 } 15 13 16 14 export function CopyToClipboardButton({ 17 15 icon, 18 16 text, 19 - title, 20 - className, 21 - items 17 + title 22 18 }: Props) { 23 19 const timeoutRef = useRef<NodeJS.Timeout | null>(null); 24 20 ··· 33 29 } 34 30 35 31 return ( 36 - <ButtonGroup 37 - className={className} 32 + <Button 33 + className="w-full !justify-start truncate" 34 + variant={saved 35 + ? "secondary" 36 + : undefined 37 + } 38 + onClick={() => handleCopy(text)} 38 39 > 39 - 40 - <Button 41 - className="w-full !justify-start" 42 - color={ 43 - saved 44 - ? "secondary" 45 - : undefined 46 - } 47 - onClick={() => handleCopy(text)} 48 - startContent={icon} 49 - > 50 - {saved 51 - ? "Copied to clipboard" 52 - : (title || "Copy to clipboard") 53 - } 54 - </Button> 55 - 56 - {items && 57 - <Dropdown 58 - placement="bottom-end" 59 - className="backdrop-blur-xl backdrop-brightness-50" 60 - > 61 - <DropdownTrigger> 62 - <Button 63 - className={className} 64 - color={ 65 - saved 66 - ? "secondary" 67 - : undefined 68 - } 69 - isIconOnly 70 - > 71 - <HiChevronDown /> 72 - </Button> 73 - </DropdownTrigger> 74 - <DropdownMenu variant="faded"> 75 - {items.map((item, i) => ( 76 - <DropdownItem 77 - key={i} 78 - className="backdrop-blur-md backdrop-brightness-75" 79 - closeOnSelect 80 - onClick={() => handleCopy(item.text)} 81 - description={item.description} 82 - showDivider={i !== (items?.length || 0) - 1} 83 - startContent={item.icon} 84 - > 85 - {item.name} 86 - </DropdownItem> 87 - ))} 88 - </DropdownMenu> 89 - </Dropdown> 40 + {icon} 41 + {saved 42 + ? "Copied to clipboard" 43 + : (title || "Copy to clipboard") 90 44 } 91 - 92 - </ButtonGroup> 45 + </Button> 93 46 ); 94 47 }
+12 -11
components/dashboard/lists/navigation.tsx
··· 1 1 "use client"; 2 2 3 - import { Button } from "@nextui-org/react"; 4 3 import Link from "next/link"; 5 4 import { HiArrowLeft, HiExternalLink } from "react-icons/hi"; 6 5 7 6 import { guildStore } from "@/common/guilds"; 7 + import { Button } from "@/components/ui/button"; 8 8 9 9 interface Props { 10 10 href: `/${string}`; ··· 27 27 28 28 return ( 29 29 <div className="flex items-start justify-between gap-2 relative bottom-2 mb-5 md:mb-3"> 30 - <div className="flex flex-col md:flex-row gap-2"> 30 + <div className="flex flex-col md:flex-row gap-3"> 31 31 <Button 32 - as={Link} 33 - href={`/dashboard/${guildId}${href}`} 34 - startContent={<HiArrowLeft />} 32 + asChild 35 33 size="sm" 36 34 > 37 - Back to channels 35 + <Link href={`/dashboard/${guildId}${href}`}> 36 + <HiArrowLeft /> 37 + Back to channels 38 + </Link> 38 39 </Button> 39 40 40 41 <div className="flex items-center gap-1.5"> ··· 55 56 </div> 56 57 57 58 <Button 58 - as={Link} 59 - href={`/docs${docs}`} 60 - target="_blank" 61 - endContent={<HiExternalLink />} 59 + asChild 62 60 size="sm" 63 61 > 64 - Read docs & view placeholders 62 + <Link href={`/docs${docs}`}> 63 + Read the docs & view placeholders 64 + <HiExternalLink /> 65 + </Link> 65 66 </Button> 66 67 </div> 67 68 );
+19 -18
components/discord/message.tsx
··· 4 4 5 5 import { cn } from "@/utils/cn"; 6 6 7 - import { Avatar } from "../avatar"; 7 + import { UserAvatar } from "../ui/avatar"; 8 8 import DiscordAppBadge from "./app-badge"; 9 9 10 10 interface Props { ··· 31 31 user, 32 32 mode 33 33 }: Props) { 34 - 35 - function formatTime(date: Date) { 36 - const timeString = date.toLocaleString("en-US", { 37 - hour: "numeric", 38 - minute: "numeric", 39 - timeZone: "Europe/Vienna" 40 - }); 41 - 42 - return `Today at ${timeString}`; 43 - } 44 - 45 34 return ( 46 35 <div className={cn("group relative rounded-lg px-1 w-full", mode === "DARK" ? "text-neutral-100" : "text-neutral-900")}> 47 36 ··· 54 43 } 55 44 /> 56 45 <div className="mx-0.5 flex items-center gap-1 font-semibold whitespace-nowrap overflow-hidden text-ellipsis shrink-0"> 57 - <Avatar 58 - className="h-4 w-4" 59 - radius="full" 46 + <UserAvatar 47 + alt={`${commandUsed.username}'s avatar`} 48 + className="size-4" 60 49 src={commandUsed.avatar} 50 + username={commandUsed.username} 61 51 /> 62 52 <span className={mode === "DARK" ? "text-violet-400" : "text-violet-600"}> 63 53 {commandUsed.username} ··· 74 64 75 65 <div className="flex flex-row items-start pointer-events-none [&>*]:pointer-events-auto"> 76 66 <div className="flex justify-start items-center w-[52px] shrink-0"> 77 - <Avatar 78 - className="h-10 w-10 hover:cursor-pointer mt-1" 79 - radius="full" 67 + <UserAvatar 68 + alt={`${user.username}'s avatar`} 69 + className="size-10 hover:cursor-pointer mt-1" 80 70 src={user.avatar} 71 + username={user.username} 81 72 /> 82 73 </div> 83 74 ··· 106 97 107 98 </div> 108 99 ); 100 + } 101 + 102 + function formatTime(date: Date) { 103 + const timeString = date.toLocaleString("en-US", { 104 + hour: "numeric", 105 + minute: "numeric", 106 + timeZone: "Europe/Vienna" 107 + }); 108 + 109 + return `Today at ${timeString}`; 109 110 }
+5 -6
components/discord/user.tsx
··· 1 - import React from "react"; 2 - 3 1 import { cn } from "@/utils/cn"; 4 2 5 - import { Avatar } from "../avatar"; 3 + import { UserAvatar } from "../ui/avatar"; 6 4 import DiscordAppBadge from "./app-badge"; 7 5 8 6 interface Props { ··· 20 18 }: Props) { 21 19 return ( 22 20 <div className="flex items-center space-x-2"> 23 - <Avatar 24 - className={cn("h-6 w-6 shrink-0", isTalking && "outline-1.5 outline-green-500")} 25 - radius="full" 21 + <UserAvatar 22 + alt={`${username}'s avatar`} 23 + className={cn("size-6 shrink-0", isTalking && "outline-1.5 outline-green-500")} 26 24 src={avatar} 25 + username={username} 27 26 /> 28 27 <div className="font-medium whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer" > 29 28 {username}
+49 -41
components/footer.tsx
··· 1 1 import Image from "next/image"; 2 2 import Link from "next/link"; 3 3 import type { HTMLProps } from "react"; 4 - import { BiCopyright, BiLogoGithub, BiLogoGmail, BiLogoTiktok, BiLogoYoutube } from "react-icons/bi"; 4 + import { BiCopyright, BiLogoGithub, BiLogoGmail, BiLogoReddit, BiLogoTiktok, BiLogoYoutube } from "react-icons/bi"; 5 5 import { BsDiscord } from "react-icons/bs"; 6 6 import { FaBluesky } from "react-icons/fa6"; 7 7 import { HiBookOpen, HiCloud, HiCube, HiHand, HiLibrary, HiUserAdd } from "react-icons/hi"; ··· 12 12 import BlahajPic from "@/public/blahaj.webp"; 13 13 import { cn } from "@/utils/cn"; 14 14 15 - import { ClientChip } from "./client"; 15 + import { Badge } from "./ui/badge"; 16 16 17 17 export async function Footer(props: HTMLProps<HTMLDivElement>) { 18 18 ··· 21 21 22 22 return ( 23 23 <div 24 - className={cn("text-neutral-500 w-full mt-10 text-left", props.className)} 24 + className={cn("text-primary/75 w-full mt-10 text-left", props.className)} 25 25 {...props} 26 26 > 27 27 28 - <div className="flex items-center dark:text-neutral-100 text-neutral-900 gap-1 font-semibold"> 28 + <div className="flex items-center gap-1 font-semibold"> 29 29 <BsDiscord className="relative top-[1px] text-[#f8746e]" /> 30 30 <span className="text-xl bg-gradient-to-r from-red-400 to-yellow-400 bg-clip-text text-transparent">Wamellow</span> 31 31 <span className="text-xl bg-gradient-to-r from-yellow-400 to-blue-400 bg-clip-text text-transparent">for</span> ··· 46 46 <HiCube /> 47 47 <span className="flex items-center"> 48 48 Made by 49 - <ClientChip 50 - className="relative top-0.5 ml-0.5" 51 - as={Link} 52 - href="/team" 53 - startContent={<Image src={dev?.avatarUrl as string} alt="Luna" width={18} height={18} className="rounded-full" />} 54 - > 55 - {dev?.username} 56 - </ClientChip> 49 + <Link href="/team"> 50 + <Badge 51 + className="relative top-[3px] ml-0.5" 52 + radius="rounded" 53 + > 54 + <Image 55 + src={dev?.avatarUrl as string} 56 + alt="avatar" 57 + width={18} 58 + height={18} 59 + className="rounded-full relative right-1.5 px-[1px]" 60 + /> 61 + {dev?.username} 62 + </Badge> 63 + </Link> 57 64 </span> 58 65 </span> 59 66 </div> ··· 65 72 </div> 66 73 67 74 <div className="w-full flex justify-center mt-20 mb-4 hover:rotate-2 duration-500"> 68 - <Image src={BlahajPic} alt="Blahaj" width={1500 / 2} height={Math.round(775 / 2)} className="h-42" /> 75 + <Image 76 + alt="Blahaj" 77 + src={BlahajPic} 78 + height={Math.round(775 / 2)} 79 + width={1500 / 2} 80 + /> 69 81 </div> 70 82 </div> 71 83 ); ··· 74 86 function Socials() { 75 87 return ( 76 88 <div className="ml-auto svg-max flex flex-wrap items-center gap-2 mt-2 md:mt-0"> 77 - <Link href="https://tiktok.com/@wamellow.com" className="text-neutral-400 hover:dark:text-neutral-300 hover:text-neutral-700 duration-200 h-6 w-6" aria-label="Wamellow on TikTok"> 89 + <Link href="https://tiktok.com/@wamellow.com" className="text-white/75 hover:text-white duration-200 size-6" aria-label="Wamellow on TikTok"> 78 90 <BiLogoTiktok /> 79 - <span className="sr-only">Wamellow on TikTok</span> 80 91 </Link> 81 - <Link href="https://youtube.com/@wamellow" className="text-neutral-400 hover:dark:text-neutral-300 hover:text-neutral-700 duration-200 h-6 w-6" aria-label="Wamellow on YouTube"> 92 + <Link href="https://youtube.com/@wamellow" className="text-white/75 hover:text-white duration-200 size-6" aria-label="Wamellow on YouTube"> 82 93 <BiLogoYoutube /> 83 - <span className="sr-only">Wamellow on YouTube</span> 84 94 </Link> 85 - <Link href="https://bsky.app/profile/lunish.nl" className="text-neutral-400 hover:dark:text-neutral-300 hover:text-neutral-700 duration-200 h-6 w-6" aria-label="Wamellow on Twitter (X.com)"> 95 + <Link href="https://bsky.app/profile/lunish.nl" className="text-white/75 hover:text-white duration-200 size-6" aria-label="Wamellow on Twitter (X.com)"> 86 96 <FaBluesky className="p-0.5" /> 87 - <span className="sr-only">Wamellow&apos;s developer on Bluesky</span> 88 97 </Link> 89 - <Link href="https://github.com/Luna-devv" className="text-neutral-400 hover:dark:text-neutral-300 hover:text-neutral-700 duration-200 h-6 w-6" aria-label="Wamellow's developers on GitHub"> 98 + <Link href="https://github.com/Luna-devv" className="text-white/75 hover:text-white duration-200 size-6" aria-label="Wamellow's developers on GitHub"> 90 99 <BiLogoGithub /> 91 - <span className="sr-only">Wamellow{"'"}s developers on GitHub</span> 92 100 </Link> 93 - <Link href="mailto:support@wamellow.com" className="text-neutral-400 hover:dark:text-neutral-300 hover:text-neutral-700 duration-200 h-6 w-6" aria-label="Contact Wamellow via email"> 101 + <Link href="https://reddit.com/r/wamellow" className="text-white/75 hover:text-white duration-200 size-6" aria-label="Wamellow on Reddit"> 102 + <BiLogoReddit /> 103 + </Link> 104 + <Link href="mailto:support@wamellow.com" className="text-white/75 hover:text-white duration-200 size-6" aria-label="Contact Wamellow via email"> 94 105 <BiLogoGmail /> 95 - <span className="sr-only">Contact Wamellow via email</span> 96 106 </Link> 97 - <Link href="https://lunish.nl/kofi" className="text-neutral-400 hover:dark:text-neutral-300 hover:text-neutral-700 duration-200 h-[22px] w-[22px]" aria-label="Support Wamellow's developers monetarily on Kofi"> 107 + <Link href="https://ko-fi.com/mwlica" className="text-white/75 hover:text-white duration-200 h-[22px] w-[22px]" aria-label="Support Wamellow's developers monetarily on Kofi"> 98 108 <SiKofi /> 99 - <span className="sr-only">Support Wamellow{"'"}s developers monetarily on Kofi</span> 100 109 </Link> 101 - <Link href="/vote" className="text-[#ff3366] duration-200 h-6 w-6" aria-label="Wamellow on top.gg"> 110 + <Link href="/vote" className="text-[#ff3366] duration-200 size-6" aria-label="Vote for Wamellow on top.gg"> 102 111 <TopggIcon /> 103 - <span className="sr-only">Wamellow on top.gg</span> 104 112 </Link> 105 113 </div> 106 114 ); ··· 110 118 return ( 111 119 <div className="flex gap-12 dark:text-neutral-400 text-neutral-600 select-none"> 112 120 <div> 113 - <div className="font-medium dark:text-neutral-300 text-neutral-800 mb-1">Legal blah blah</div> 121 + <div className="font-medium dark:text-neutral-200 text-neutral-800 mb-1">Legal blah blah</div> 114 122 <Link 115 - className="flex items-center gap-1.5" 116 - href="/terms?utm_source=wamellow.com&utm_medium=footer" 123 + className="text-primary/75 hover:text-primary/65 duration-200 flex items-center gap-2" 124 + href="/terms" 117 125 > 118 126 <HiLibrary /> 119 - <span>Terms of Service</span> 127 + Terms of Service 120 128 </Link> 121 129 <Link 122 - className="flex items-center gap-2" 123 - href="/privacy?utm_source=wamellow.com&utm_medium=footer" 130 + className="text-primary/75 hover:text-primary/65 duration-200 flex items-center gap-2" 131 + href="/privacy" 124 132 > 125 133 <HiHand /> 126 - <span>Privacy Policy</span> 134 + Privacy Policy 127 135 </Link> 128 136 </div> 129 137 <div> 130 - <div className="font-medium dark:text-neutral-300 text-neutral-800 mb-1">Links</div> 138 + <div className="font-medium dark:text-neutral-200 text-neutral-800 mb-1">Links</div> 131 139 <Link 132 - className="flex items-center gap-2" 140 + className="text-primary/75 hover:text-primary/65 duration-200 flex items-center gap-2" 133 141 href="/support" 134 142 > 135 143 <BsDiscord /> 136 144 Support 137 145 </Link> 138 146 <Link 139 - className="flex items-center gap-2" 140 - href="/docs/index?utm_source=wamellow.com&utm_medium=footer" 147 + className="text-primary/75 hover:text-primary/65 duration-200 flex items-center gap-2" 148 + href="/docs/index" 141 149 > 142 150 <HiBookOpen /> 143 151 Documentation 144 152 </Link> 145 153 <Link 146 - className="flex items-center gap-2" 147 - href="/status?utm_source=wamellow.com&utm_medium=footer" 154 + className="text-primary/75 hover:text-primary/65 duration-200 flex items-center gap-2" 155 + href="/status" 148 156 > 149 157 <HiCloud /> 150 158 Status 151 159 </Link> 152 160 <Link 153 - className="flex items-center gap-2" 161 + className="text-primary/75 hover:text-primary/65 duration-200 flex items-center gap-2" 154 162 href="/login?invite=true" 155 163 prefetch={false} 156 164 >
+74 -98
components/header.tsx
··· 1 1 "use client"; 2 2 3 - import { Button, Chip, Skeleton, Switch, Tooltip } from "@nextui-org/react"; 3 + import { Switch } from "@nextui-org/react"; 4 4 import { AnimatePresence, motion, MotionConfig } from "framer-motion"; 5 5 import Link from "next/link"; 6 6 import { useRouter } from "next/navigation"; 7 7 import { useCookies } from "next-client-cookies"; 8 - import React, { useEffect, useState } from "react"; 9 - import { HiAdjustments, HiBadgeCheck, HiBeaker, HiChartPie, HiChevronDown, HiEyeOff, HiFire, HiIdentification, HiLogout, HiTrendingUp, HiViewGridAdd } from "react-icons/hi"; 8 + import { useCallback, useEffect, useMemo, useState } from "react"; 9 + import { HiAdjustments, HiBeaker, HiChartPie, HiChevronDown, HiEyeOff, HiFire, HiIdentification, HiLogout, HiTrendingUp, HiViewGridAdd } from "react-icons/hi"; 10 10 11 11 import { userStore } from "@/common/user"; 12 12 import { webStore } from "@/common/webstore"; 13 - import LoginButton from "@/components/login-button"; 13 + import { LoginButton } from "@/components/login-button"; 14 14 import { authorize } from "@/utils/authorize-user"; 15 15 import { cn } from "@/utils/cn"; 16 16 17 17 import ImageReduceMotion from "./image-reduce-motion"; 18 + import { Button } from "./ui/button"; 19 + import { Skeleton } from "./ui/skeleton"; 18 20 19 21 enum State { 20 22 Idle = 0, ··· 22 24 Failure = 2 23 25 } 24 26 25 - export default function Header(props: React.ComponentProps<"div">) { 27 + const split = { type: "split" } as const; 28 + 29 + export function Header() { 26 30 const cookies = useCookies(); 27 31 const devTools = cookies.get("devTools") === "true"; 28 32 const reduceMotions = cookies.get("reduceMotions") === "true"; ··· 47 51 }); 48 52 }, []); 49 53 50 - 51 - const UserButton = () => ( 52 - <button 53 - className={cn( 54 - "ml-auto flex dark:hover:bg-wamellow hover:bg-wamellow-100 py-2 px-4 rounded-md duration-200 items-center", 55 - menu && "dark:bg-wamellow bg-wamellow-100" 56 - )} 57 - onClick={() => setMenu(!menu)} 58 - > 59 - 60 - <Skeleton isLoaded={!!user?.id} className="rounded-full mr-2 h-[30px] w-[30px]"> 61 - <ImageReduceMotion 62 - alt="your avatar" 63 - className="rounded-full" 64 - url={`https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}`} 65 - size={96} 66 - /> 67 - </Skeleton> 68 - 69 - {!user?.id ? 70 - <Skeleton className="rounded-xl w-20 h-4" /> 71 - : 72 - <> 73 - <div className="mr-1 relative bottom-[1px]">{user?.globalName || user?.username}</div> 74 - <HiChevronDown /> 75 - </> 76 - } 77 - 78 - </button> 79 - ); 80 - 81 - const split = { type: "split" }; 82 - 83 - const buttons = [ 54 + const buttons = useMemo(() => [ 84 55 split, 85 56 { 86 57 name: "Dashboard", ··· 139 110 : 140 111 [] 141 112 ) 142 - ]; 113 + ], [user, reduceMotions, devTools]); 114 + 115 + const UserButton = useCallback(() => ( 116 + <button 117 + className={cn( 118 + "ml-auto truncate flex hover:bg-wamellow py-2 px-4 rounded-lg duration-200 items-center", 119 + menu && "bg-wamellow" 120 + )} 121 + onClick={() => setMenu(!menu)} 122 + > 123 + {!user?.id ? 124 + <> 125 + <Skeleton className="rounded-full mr-2 size-[30p]" /> 126 + <Skeleton className="rounded-xl w-20 h-4" /> 127 + </> 128 + : 129 + <> 130 + <ImageReduceMotion 131 + alt="your avatar" 132 + className="rounded-full mr-2 size-[30px] shrink-0" 133 + url={`https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}`} 134 + size={96} 135 + /> 143 136 144 - const UserDropdown = () => ( 137 + <p className="mr-1 relative bottom-[1px] truncate block">{user.globalName || user.username}</p> 138 + <HiChevronDown /> 139 + </> 140 + } 141 + </button> 142 + ), [user, menu]); 143 + 144 + const UserDropdown = useCallback(() => ( 145 145 <motion.div 146 146 initial="closed" 147 147 animate={menu ? "open" : "closed"} ··· 159 159 } 160 160 }} 161 161 className=" 162 - relative top-2 sm:right-[268px] w-full sm:w-72 dark:bg-black/40 bg-white/40 rounded-xl backdrop-blur-3xl backdrop-brightness-75 overflow-hidden shadow-xl 163 - flex flex-col py-2 sm:py-1 p-2 sm:p-0 text-base 162 + w-full sm:w-72 bg-black/40 rounded-xl backdrop-blur-2xl backdrop-brightness-75 shadow-xl 163 + flex flex-col py-2 sm:py-1 p-2 sm:p-0 164 164 [--y-closed:-16px] [--opacity-closed:0%] sm:[--scale-closed:90%] 165 165 [--y-open:0px] [--opacity-open:100%] sm:[--scale-open:100%] 166 166 " 167 167 > 168 168 <div className="flex items-center space-x-3 px-4 py-2"> 169 - <Skeleton isLoaded={!!user?.id} className="rounded-full h-14 w-14 sm:h-10 sm:w-10 shrink-0"> 170 - <ImageReduceMotion 171 - alt="your avatar" 172 - className="rounded-full" 173 - url={`https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}`} 174 - size={128} 175 - /> 176 - </Skeleton> 169 + <ImageReduceMotion 170 + alt="your avatar" 171 + className="rounded-full size-14 sm:size-10 shrink-0" 172 + url={`https://cdn.discordapp.com/avatars/${user?.id}/${user?.avatar}`} 173 + size={128} 174 + /> 177 175 <div className="w-full"> 178 - <div className="dark:text-neutral-200 text-neutral-800 truncate max-w-44 flex items-center gap-2"> 179 - <span className="font-medium text-xl sm:text-base"> 180 - {user?.globalName || `@${user?.username}`} 181 - </span> 182 - {user?.id === "821472922140803112" && 183 - <Chip color="secondary" size="sm" variant="flat" startContent={<HiBadgeCheck className="h-3.5 w-3.5 mr-1" />}> 184 - <span className="font-bold">Developer</span> 185 - </Chip> 186 - } 176 + <div className="text-neutral-200 max-w-40 truncate font-medium text-xl sm:text-base"> 177 + {user?.globalName || user?.username} 187 178 </div> 188 - <div className="text-neutral-500 dark:text-neutral-400 max-w-40 truncate -mt-1"> 189 - <span className="text-medium sm:text-sm"> 190 - @{user?.username} 191 - </span> 179 + <div className="text-neutral-400 max-w-40 truncate -mt-1 text-medium sm:text-sm"> 180 + @{user?.username} 192 181 </div> 193 182 </div> 194 - <Tooltip 195 - content="Logout" 196 - closeDelay={0} 197 - showArrow 183 + <button 184 + className="ml-auto text-red-500 m-4" 185 + onClick={() => { 186 + window.location.href = "/login?logout=true"; 187 + userStore.setState({ __fetched: true }); 188 + setMenu(false); 189 + }} 198 190 > 199 - <button 200 - className="ml-auto text-red-500 m-4" 201 - onClick={() => { 202 - window.location.href = "/login?logout=true"; 203 - userStore.setState({ __fetched: true }); 204 - setMenu(false); 205 - }} 206 - > 207 - <HiLogout className="h-6 w-6 sm:h-5 sm:w-5" /> 208 - </button> 209 - </Tooltip> 191 + <HiLogout className="size-5" /> 192 + </button> 210 193 </div> 211 194 212 195 {buttons.map((button, i) => { ··· 217 200 if ("url" in button) return ( 218 201 <Button 219 202 key={"headerButton-" + button.name + button.url} 220 - as={Link} 221 - href={button.url} 222 - className="w-full font-medium !justify-start !text-xl !my-1 sm:!text-medium sm:!my-0 bg-transparent" 203 + asChild 204 + className="w-full font-medium justify-start text-xl my-1 sm:my-0 sm:text-medium bg-transparent hover:bg-wamellow rounded-sm" 223 205 onClick={() => setMenu(false)} 224 - startContent={button.icon} 225 206 > 226 - {button.name} 207 + <Link href={button.url!}> 208 + {button.icon} 209 + {button.name} 210 + </Link> 227 211 </Button> 228 212 ); 229 213 ··· 244 228 size="sm" 245 229 /> 246 230 </div> 247 - 248 231 ); 249 - 250 232 })} 251 233 </motion.div> 252 - ); 234 + ), [user, menu, reduceMotions, devTools]); 253 235 254 - return (<div {...props}> 255 - 236 + return (<> 256 237 {state === State.Failure 257 - ? <LoginButton state={state} /> 238 + ? <LoginButton state={state} className="ml-auto" /> 258 239 : <UserButton /> 259 240 } 260 241 ··· 267 248 > 268 249 <AnimatePresence initial={false}> 269 250 {user?.id && menu && 270 - <div className="pr-4 flex text-base font-medium dark:text-neutral-300 text-neutral-700 overflow-x-hidden"> 271 - <div className="ml-auto"> 272 - <div className="absolute left-0 sm:left-auto px-4 sm:px-0 z-40 w-full sm:w-0"> 273 - <UserDropdown /> 274 - </div> 275 - </div> 251 + <div className="absolute top-[72px] right-3.5 z-50"> 252 + <UserDropdown /> 276 253 </div> 277 254 } 278 255 </AnimatePresence> 279 256 </MotionConfig> 280 - 281 - </div>); 257 + </>); 282 258 }
+37 -41
components/inputs/multi-select-menu.tsx
··· 127 127 128 128 <button 129 129 className={cn( 130 - "mt-1 min-h-12 w-full dark:bg-wamellow bg-wamellow-100 rounded-xl flex items-center px-3 duration-100 wamellow-modal", 130 + "mt-1 min-h-12 w-full bg-wamellow rounded-lg flex items-center px-3 duration-100 wamellow-modal", 131 131 open && "outline outline-violet-400 outline-2", 132 132 (values.find((v) => !!v.error) || error) && !open && "outline outline-red-500 outline-1", 133 133 state === State.Success && !open && "outline outline-green-500 outline-1", ··· 150 150 <button 151 151 key={"multiselected-" + v.value} 152 152 className={cn( 153 - "relative px-2 dark:bg-wamellow-alpha bg-wamellow-100-alpha rounded-md flex items-center gap-1 wamellow-modal", 153 + "relative px-2 bg-wamellow rounded-md flex items-center gap-1 wamellow-modal", 154 154 open && "hover:!bg-red-500/50 text-neutral-100 duration-200" 155 155 )} 156 - // style={v.color ? { color: `#${v.color.toString(16)}` } : {}} 157 156 onClick={(e) => { 158 157 if (!open) return; 159 158 e.stopPropagation(); ··· 184 183 </button> 185 184 186 185 {open && 187 - <div className="absolute mt-2 w-full dark:bg-wamellow bg-wamellow-100 backdrop-blur-xl backdrop-brightness-75 rounded-lg max-h-40 overflow-y-scroll shadow-xl z-20 wamellow-modal"> 186 + <div className="absolute mt-2 w-full bg-wamellow backdrop-blur-lg backdrop-brightness-50 rounded-lg max-h-40 overflow-y-scroll shadow-lg z-20 wamellow-modal"> 188 187 <ClickOutside onClose={(() => setOpen(false))} /> 189 - 190 - <div className="dark:bg-wamellow-alpha bg-wamellow-100-alpha"> 191 - {items.map((item) => ( 192 - <button 193 - className={cn( 194 - "p-4 py-2 w-full text-left duration-200 flex items-center dark:hover:bg-wamellow-alpha hover:bg-wamellow-100-alpha", 195 - item.error && "dark:bg-red-500/10 hover:dark:bg-red-500/25 bg-red-500/30 hover:bg-red-500/40" 196 - )} 197 - style={item.color ? { color: `#${item.color.toString(16)}` } : {}} 198 - key={"multiselect-" + item.value} 199 - onClick={() => { 200 - setState(State.Idle); 201 - setValues((v) => { 202 - if (v.length >= max || v.find((i) => i.value === item.value)) return v.filter((i) => i.value !== item.value); 203 - return [...v, item]; 204 - }); 205 - }} 206 - > 207 - {item?.icon && 208 - <span className="mr-2"> 209 - {item?.icon} 210 - </span> 211 - } 212 - 213 - <span className="max-w-[calc(100%-1rem)] truncate"> 214 - {item.name} 188 + {items.map((item) => ( 189 + <button 190 + className={cn( 191 + "p-4 py-2 w-full text-left duration-200 flex items-center hover:bg-wamellow", 192 + item.error && "dark:bg-red-500/10 hover:dark:bg-red-500/25 bg-red-500/30 hover:bg-red-500/40" 193 + )} 194 + style={item.color ? { color: `#${item.color.toString(16)}` } : {}} 195 + key={"multiselect-" + item.value} 196 + onClick={() => { 197 + setState(State.Idle); 198 + setValues((v) => { 199 + if (v.length >= max || v.find((i) => i.value === item.value)) return v.filter((i) => i.value !== item.value); 200 + return [...v, item]; 201 + }); 202 + }} 203 + > 204 + {item?.icon && 205 + <span className="mr-2"> 206 + {item?.icon} 215 207 </span> 208 + } 216 209 217 - {values.find((v) => v.value === item.value) && 218 - <HiCheck className="ml-1" /> 219 - } 210 + <span className="max-w-[calc(100%-1rem)] truncate"> 211 + {item.name} 212 + </span> 213 + 214 + {values.find((v) => v.value === item.value) && 215 + <HiCheck className="relative left-1 top-[1px]" /> 216 + } 220 217 221 - {item.error && 222 - <div className="ml-auto text-sm flex items-center gap-1 text-red-500"> 223 - <HiExclamationCircle /> {item.error} 224 - </div> 225 - } 226 - </button> 227 - ))} 228 - </div> 218 + {item.error && 219 + <div className="ml-auto text-sm flex items-center gap-1 text-red-500"> 220 + <HiExclamationCircle /> {item.error} 221 + </div> 222 + } 223 + </button> 224 + ))} 229 225 </div> 230 226 } 231 227
+1 -1
components/inputs/number-input.tsx
··· 88 88 }, [defaultState]); 89 89 90 90 function handleSave() { 91 - if (def === value || !value) return; 91 + if (def === value || value === undefined) return; 92 92 setError(undefined); 93 93 setState(State.Loading); 94 94
+36 -39
components/inputs/select-menu.tsx
··· 126 126 127 127 <button 128 128 className={cn( 129 - "mt-1 h-12 w-full dark:bg-wamellow bg-wamellow-100 rounded-xl flex items-center px-3 duration-100 wamellow-modal", 130 - open && "outline outline-violet-400 outline-2", 129 + "mt-1 h-12 w-full bg-wamellow rounded-lg flex items-center px-3 wamellow-modal", 130 + open && "outline outline-violet-400 outline-offset-2 outline-2", 131 131 (value?.error || error) && !open && "outline outline-red-500 outline-1", 132 132 state === State.Success && !open && "outline outline-green-500 outline-1", 133 133 (state === State.Loading || disabled) && "cursor-not-allowed opacity-50" ··· 166 166 </button> 167 167 168 168 {open && 169 - <div className="absolute mt-2 w-full dark:bg-wamellow bg-wamellow-100 backdrop-blur-xl backdrop-brightness-75 rounded-lg max-h-40 overflow-y-scroll overflow-x-hidden shadow-xl z-20 wamellow-modal"> 169 + <div className="absolute mt-2 w-full bg-wamellow backdrop-blur-lg backdrop-brightness-50 rounded-lg max-h-40 overflow-y-scroll shadow-lg z-20 wamellow-modal"> 170 170 <ClickOutside onClose={(() => setOpen(false))} /> 171 + {items.map((item) => ( 172 + <button 173 + key={"select-" + item.value} 174 + className={cn( 175 + "p-4 py-2 w-full text-left duration-200 flex items-center hover:bg-wamellow", 176 + item.error && "dark:bg-red-500/10 hover:dark:bg-red-500/25 bg-red-500/30 hover:bg-red-500/40" 177 + )} 178 + style={item.color ? { color: `#${item.color.toString(16)}` } : {}} 179 + onClick={() => { 180 + setOpen(false); 181 + setState(State.Idle); 182 + if (value?.value) setDefaultalue(value.value); 183 + setValue(item); 184 + }} 185 + > 186 + {item?.icon && 187 + <span className="mr-2"> 188 + {item?.icon} 189 + </span> 190 + } 171 191 172 - <div className="dark:bg-wamellow-alpha bg-wamellow-100-alpha"> 173 - {items.map((item) => ( 174 - <button 175 - key={"select-" + item.value} 176 - className={cn( 177 - "p-4 py-2 w-full text-left duration-200 flex items-center dark:hover:bg-wamellow-alpha hover:bg-wamellow-100-alpha", 178 - item.error && "dark:bg-red-500/10 hover:dark:bg-red-500/25 bg-red-500/30 hover:bg-red-500/40" 179 - )} 180 - style={item.color ? { color: `#${item.color.toString(16)}` } : {}} 181 - onClick={() => { 182 - setOpen(false); 183 - setState(State.Idle); 184 - if (value?.value) setDefaultalue(value.value); 185 - setValue(item); 186 - }} 187 - > 188 - {item?.icon && 189 - <span className="mr-2"> 190 - {item?.icon} 191 - </span> 192 - } 192 + <span className={cn("truncate", item.error && "max-w-[calc(100%-13rem)]")}> 193 + {item.name} 194 + </span> 193 195 194 - <span className={cn("truncate", item.error && "max-w-[calc(100%-13rem)]")}> 195 - {item.name} 196 - </span> 196 + {value?.value === item.value && 197 + <HiCheck className="ml-1" /> 198 + } 197 199 198 - {value?.value === item.value && 199 - <HiCheck className="ml-1" /> 200 - } 201 - 202 - {item.error && 203 - <div className="ml-auto text-sm flex items-center gap-1 text-red-500"> 204 - <HiExclamationCircle /> {item.error} 205 - </div> 206 - } 207 - </button> 208 - ))} 209 - </div> 200 + {item.error && 201 + <div className="ml-auto text-sm flex items-center gap-1 text-red-500"> 202 + <HiExclamationCircle /> {item.error} 203 + </div> 204 + } 205 + </button> 206 + ))} 210 207 </div> 211 208 } 212 209
+23 -25
components/login-button.tsx
··· 1 1 "use client"; 2 2 3 - import { Button } from "@nextui-org/react"; 4 3 import { Montserrat } from "next/font/google"; 5 4 import Link from "next/link"; 6 5 import { BsDiscord } from "react-icons/bs"; 7 6 import { HiExclamation } from "react-icons/hi"; 8 7 9 8 import { cn } from "@/utils/cn"; 9 + 10 + import { Button } from "./ui/button"; 10 11 11 12 const montserrat = Montserrat({ subsets: ["latin"] }); 12 13 ··· 20 21 state?: State; 21 22 message?: string; 22 23 className?: string; 23 - addClassName?: string; // idk why that name 24 24 } 25 25 26 - export default function LoginButton({ 26 + export function LoginButton({ 27 27 state, 28 28 message, 29 - className, 30 - addClassName 29 + className 31 30 }: Props) { 32 31 if (state === State.Loading) return <></>; 33 - 34 - function Icon() { 35 - if (!state) return <BsDiscord />; 36 - if (state === State.Failure) return <HiExclamation className="h-5 w-5" />; 37 - } 32 + console.log(className); 38 33 39 34 return ( 40 - <div className={className || "ml-auto"}> 41 - <Button 42 - as={Link} 43 - className={cn( 44 - "hover:bg-blurple hover:text-white dark:bg-wamellow bg-wamellow-100", 45 - state === State.Failure && "dark:bg-danger/80 bg-danger/80 text-white", 46 - addClassName 47 - )} 35 + <Button 36 + asChild 37 + className={className} 38 + variant={state === State.Failure ? "destructive" : "default"} 39 + > 40 + <Link 48 41 href="/login" 49 42 prefetch={false} 50 - startContent={<Icon />} 51 43 > 44 + <Icon state={state} /> 52 45 {!state ? 53 46 <span className={cn(montserrat.className, "font-semibold")}> 54 47 {message || 55 - <> 56 - <span className="hidden md:block">Login with Discord</span> 57 - <span className="md:hidden">Discord login</span> 58 - </> 48 + <> 49 + <span className="hidden md:block">Login with Discord</span> 50 + <span className="md:hidden">Discord login</span> 51 + </> 59 52 } 60 53 </span> 61 54 : 62 55 <span>Authorization failed</span> 63 56 } 64 - </Button> 65 - </div> 57 + </Link> 58 + </Button> 66 59 ); 60 + } 61 + 62 + function Icon({ state }: { state?: State; }) { 63 + if (state === State.Failure) return <HiExclamation className="mt-0.5 size-5" />; 64 + return <BsDiscord />; 67 65 }
+1 -1
components/modal.tsx
··· 120 120 aria-label="Loading..." 121 121 className="mt-3 mb-4 h-0.5" 122 122 classNames={{ 123 - track: "dark:bg-wamellow-light bg-wamellow-100-light", 123 + track: "bg-divider", 124 124 indicator: "bg-violet-500" 125 125 }} 126 126 value={0}
+86
components/share.tsx
··· 1 + import Link from "next/link"; 2 + import { BsTwitter } from "react-icons/bs"; 3 + import { HiShare } from "react-icons/hi"; 4 + import { SiBluesky } from "react-icons/si"; 5 + 6 + import { CopyToClipboardButton } from "./copy-to-clipboard"; 7 + import { Button } from "./ui/button"; 8 + import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; 9 + 10 + const PLATFORMS = ["twitter", "bluesky"] as const; 11 + 12 + export function Share({ 13 + title, 14 + url, 15 + text 16 + }: { 17 + title: string; 18 + url: string; 19 + text: string; 20 + }) { 21 + return ( 22 + <div className="flex gap-2"> 23 + <CopyToClipboardButton 24 + title={title} 25 + text={url} 26 + icon={<HiShare />} 27 + /> 28 + 29 + {PLATFORMS.map((platform) => ( 30 + <ShareButton 31 + key={platform} 32 + platform={platform} 33 + url={url} 34 + text={text} 35 + /> 36 + ))} 37 + </div> 38 + ); 39 + } 40 + 41 + function ShareButton({ 42 + platform, 43 + url, 44 + text 45 + }: { 46 + platform: string; 47 + url: string; 48 + text: string; 49 + }) { 50 + const intentUrl = getIntentLink(platform, url, text + "\n"); 51 + 52 + return ( 53 + <Tooltip> 54 + <TooltipTrigger asChild> 55 + <Button asChild> 56 + <Link 57 + href={intentUrl} 58 + target="_blank" 59 + > 60 + <Icon platform={platform} /> 61 + </Link> 62 + </Button> 63 + </TooltipTrigger> 64 + 65 + <TooltipContent> 66 + Share on {platform.replace(/^\w/, (char) => char.toUpperCase())} 67 + </TooltipContent> 68 + </Tooltip> 69 + ); 70 + } 71 + 72 + function getIntentLink(platform: string, url: string, text: string) { 73 + switch (platform) { 74 + case "twitter": return `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`; 75 + case "bluesky": return `https://bsky.app/intent/compose?text=${encodeURIComponent(text + url)}`; 76 + } 77 + 78 + return ""; 79 + } 80 + 81 + function Icon({ platform }: { platform: string; }) { 82 + switch (platform) { 83 + case "twitter": return <BsTwitter />; 84 + case "bluesky": return <SiBluesky />; 85 + } 86 + }
+89
components/ui/avatar.tsx
··· 1 + "use client"; 2 + 3 + import * as AvatarPrimitive from "@radix-ui/react-avatar"; 4 + import * as React from "react"; 5 + 6 + import { cn } from "@/utils/cn"; 7 + 8 + const Avatar = React.forwardRef< 9 + React.ElementRef<typeof AvatarPrimitive.Root>, 10 + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> 11 + >(({ className, ...props }, ref) => ( 12 + <AvatarPrimitive.Root 13 + ref={ref} 14 + className={cn( 15 + "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", 16 + className 17 + )} 18 + {...props} 19 + /> 20 + )); 21 + Avatar.displayName = AvatarPrimitive.Root.displayName; 22 + 23 + const AvatarImage = React.forwardRef< 24 + React.ElementRef<typeof AvatarPrimitive.Image>, 25 + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> 26 + >(({ className, ...props }, ref) => ( 27 + <AvatarPrimitive.Image 28 + ref={ref} 29 + className={cn("aspect-square h-full w-full", className)} 30 + {...props} 31 + /> 32 + )); 33 + AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 + 35 + const AvatarFallback = React.forwardRef< 36 + React.ElementRef<typeof AvatarPrimitive.Fallback>, 37 + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> 38 + >(({ className, ...props }, ref) => ( 39 + <AvatarPrimitive.Fallback 40 + ref={ref} 41 + className={cn( 42 + "flex h-full w-full items-center justify-center rounded-full bg-muted", 43 + className 44 + )} 45 + {...props} 46 + /> 47 + )); 48 + AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 + 50 + function UserAvatar({ 51 + src, 52 + username, 53 + className, 54 + alt, 55 + ...props 56 + }: { 57 + src: string; 58 + username?: string | null; 59 + alt: string; 60 + } & React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>) { 61 + return ( 62 + <Avatar 63 + className={cn("rounded-full", className)} 64 + {...props} 65 + > 66 + <AvatarImage alt={alt} src={src} /> 67 + <AvatarFallback>{(username || "Wamellow").toLowerCase().slice(0, 2)}</AvatarFallback> 68 + </Avatar> 69 + ); 70 + } 71 + 72 + function AvatarGroup({ 73 + children, 74 + className, 75 + ...props 76 + }: { 77 + children: ReturnType<typeof UserAvatar>[]; 78 + } & React.ComponentPropsWithoutRef<"div">) { 79 + return ( 80 + <div 81 + className={cn("flex", className)} 82 + {...props} 83 + > 84 + {children} 85 + </div> 86 + ); 87 + } 88 + 89 + export { Avatar, AvatarFallback, AvatarGroup, AvatarImage, UserAvatar };
+19 -5
components/ui/badge.tsx
··· 4 4 import { cn } from "@/utils/cn"; 5 5 6 6 const badgeVariants = cva( 7 - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 7 + "w-fit inline-flex items-center rounded-full border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 [&>svg]:relative [&>svg]:right-1", 8 8 { 9 9 variants: { 10 10 variant: { 11 11 default: "border-transparent bg-primary/10 text-primary-foreground", 12 12 secondary: "border-transparent bg-secondary text-secondary-foreground", 13 + flat: "border-transparent bg-flat/40 text-flat-foreground", 13 14 destructive: "border-transparent bg-destructive text-destructive-foreground", 14 15 outline: "text-foreground" 16 + }, 17 + size: { 18 + default: "px-2.5 py-0.5 text-xs", 19 + xs: "px-1.5 h-3.5 text-xxs", 20 + sm: "px-2 py-0.5 text-xs", 21 + lg: "px-6 py-2 text-base" 22 + }, 23 + radius: { 24 + default: "rounded-md", 25 + rounded: "rounded-full", 26 + square: "rounded-none" 15 27 } 16 28 }, 17 29 defaultVariants: { 18 - variant: "default" 30 + variant: "default", 31 + size: "default", 32 + radius: "default" 19 33 } 20 34 } 21 35 ); 22 36 23 37 export interface BadgeProps 24 38 extends React.HTMLAttributes<HTMLDivElement>, 25 - VariantProps<typeof badgeVariants> { } 39 + VariantProps<typeof badgeVariants> {} 26 40 27 - function Badge({ className, variant, ...props }: BadgeProps) { 41 + function Badge({ className, variant, size, radius, ...props }: BadgeProps) { 28 42 return ( 29 - <div className={cn(badgeVariants({ variant }), className)} {...props} /> 43 + <div className={cn(badgeVariants({ variant, size, radius }), className)} {...props} /> 30 44 ); 31 45 } 32 46
+3 -3
components/ui/button.tsx
··· 6 6 import { cn } from "@/utils/cn"; 7 7 8 8 const buttonVariants = cva( 9 - "inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 + "inline-flex justify-center items-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&>svg]:size-5", 10 10 { 11 11 variants: { 12 12 variant: { 13 - default: "bg-primary/10 text-primary-foreground hover:bg-primary/5", 13 + default: "bg-wamellow text-primary-foreground hover:bg-wamellow-200", 14 14 destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 15 success: "bg-success text-success-foreground hover:bg-success/90", 16 16 outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", ··· 21 21 }, 22 22 size: { 23 23 default: "h-10 px-4 py-2", 24 - sm: "h-9 rounded-md px-3", 24 + sm: "h-9 rounded-md px-4", 25 25 lg: "h-11 rounded-md px-8", 26 26 icon: "h-7 w-7 p-1.5" 27 27 }
+30
components/ui/tooltip.tsx
··· 1 + "use client"; 2 + 3 + import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 4 + import * as React from "react"; 5 + 6 + import { cn } from "@/utils/cn"; 7 + 8 + const TooltipProvider = TooltipPrimitive.Provider; 9 + 10 + const Tooltip = TooltipPrimitive.Root; 11 + 12 + const TooltipTrigger = TooltipPrimitive.Trigger; 13 + 14 + const TooltipContent = React.forwardRef< 15 + React.ElementRef<typeof TooltipPrimitive.Content>, 16 + React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> 17 + >(({ className, sideOffset = 4, ...props }, ref) => ( 18 + <TooltipPrimitive.Content 19 + ref={ref} 20 + sideOffset={sideOffset} 21 + className={cn( 22 + "z-50 overflow-hidden rounded-md bg-popover/30 px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", 23 + className 24 + )} 25 + {...props} 26 + /> 27 + )); 28 + TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 + 30 + export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
+2 -1
package.json
··· 13 13 "@discordjs/rest": "^2.4.3", 14 14 "@nextui-org/react": "^2.6.11", 15 15 "@odiffey/discord-markdown": "^3.3.0", 16 - "@radix-ui/react-dialog": "^1.1.6", 16 + "@radix-ui/react-avatar": "^1.1.3", 17 17 "@radix-ui/react-popover": "^1.1.6", 18 18 "@radix-ui/react-separator": "^1.1.2", 19 19 "@radix-ui/react-slot": "^1.1.2", 20 + "@radix-ui/react-tooltip": "^1.1.8", 20 21 "autoprefixer": "^10.4.20", 21 22 "class-variance-authority": "^0.7.1", 22 23 "clsx": "^2.1.1",
+164 -74
pnpm-lock.yaml
··· 20 20 '@odiffey/discord-markdown': 21 21 specifier: ^3.3.0 22 22 version: 3.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 23 - '@radix-ui/react-dialog': 24 - specifier: ^1.1.6 25 - version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 23 + '@radix-ui/react-avatar': 24 + specifier: ^1.1.3 25 + version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 26 26 '@radix-ui/react-popover': 27 27 specifier: ^1.1.6 28 28 version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 32 32 '@radix-ui/react-slot': 33 33 specifier: ^1.1.2 34 34 version: 1.1.2(@types/react@19.0.10)(react@19.0.0) 35 + '@radix-ui/react-tooltip': 36 + specifier: ^1.1.8 37 + version: 1.1.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 35 38 autoprefixer: 36 39 specifier: ^10.4.20 37 40 version: 10.4.20(postcss@8.5.3) ··· 116 119 version: 13.8.0 117 120 '@stylistic/eslint-plugin': 118 121 specifier: ^4.1.0 119 - version: 4.1.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 122 + version: 4.1.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 120 123 '@types/node': 121 124 specifier: ^22.13.8 122 125 version: 22.13.8 ··· 128 131 version: 19.0.4(@types/react@19.0.10) 129 132 eslint: 130 133 specifier: ^9.21.0 131 - version: 9.21.0(jiti@1.21.7) 134 + version: 9.21.0(jiti@2.4.2) 132 135 eslint-config-next: 133 136 specifier: ^15.2.0 134 - version: 15.2.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 137 + version: 15.2.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 135 138 eslint-plugin-path-alias: 136 139 specifier: ^2.1.0 137 - version: 2.1.0(eslint@9.21.0(jiti@1.21.7)) 140 + version: 2.1.0(eslint@9.21.0(jiti@2.4.2)) 138 141 eslint-plugin-react: 139 142 specifier: ^7.37.4 140 - version: 7.37.4(eslint@9.21.0(jiti@1.21.7)) 143 + version: 7.37.4(eslint@9.21.0(jiti@2.4.2)) 141 144 eslint-plugin-react-compiler: 142 145 specifier: 19.0.0-beta-e1e972c-20250221 143 - version: 19.0.0-beta-e1e972c-20250221(eslint@9.21.0(jiti@1.21.7)) 146 + version: 19.0.0-beta-e1e972c-20250221(eslint@9.21.0(jiti@2.4.2)) 144 147 eslint-plugin-react-hooks: 145 148 specifier: ^5.2.0 146 - version: 5.2.0(eslint@9.21.0(jiti@1.21.7)) 149 + version: 5.2.0(eslint@9.21.0(jiti@2.4.2)) 147 150 eslint-plugin-simple-import-sort: 148 151 specifier: ^12.1.1 149 - version: 12.1.1(eslint@9.21.0(jiti@1.21.7)) 152 + version: 12.1.1(eslint@9.21.0(jiti@2.4.2)) 150 153 eslint-plugin-unused-imports: 151 154 specifier: ^4.1.4 152 - version: 4.1.4(@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7)) 155 + version: 4.1.4(@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)) 153 156 typescript-eslint: 154 157 specifier: ^8.25.0 155 - version: 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 158 + version: 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 156 159 157 160 packages: 158 161 ··· 1190 1193 '@types/react-dom': 1191 1194 optional: true 1192 1195 1196 + '@radix-ui/react-avatar@1.1.3': 1197 + resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==} 1198 + peerDependencies: 1199 + '@types/react': '*' 1200 + '@types/react-dom': '*' 1201 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 1202 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 1203 + peerDependenciesMeta: 1204 + '@types/react': 1205 + optional: true 1206 + '@types/react-dom': 1207 + optional: true 1208 + 1193 1209 '@radix-ui/react-compose-refs@1.1.1': 1194 1210 resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} 1195 1211 peerDependencies: ··· 1352 1368 '@types/react': 1353 1369 optional: true 1354 1370 1371 + '@radix-ui/react-tooltip@1.1.8': 1372 + resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==} 1373 + peerDependencies: 1374 + '@types/react': '*' 1375 + '@types/react-dom': '*' 1376 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 1377 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 1378 + peerDependenciesMeta: 1379 + '@types/react': 1380 + optional: true 1381 + '@types/react-dom': 1382 + optional: true 1383 + 1355 1384 '@radix-ui/react-use-callback-ref@1.1.0': 1356 1385 resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} 1357 1386 peerDependencies: ··· 1406 1435 '@types/react': 1407 1436 optional: true 1408 1437 1438 + '@radix-ui/react-visually-hidden@1.1.2': 1439 + resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} 1440 + peerDependencies: 1441 + '@types/react': '*' 1442 + '@types/react-dom': '*' 1443 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 1444 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 1445 + peerDependenciesMeta: 1446 + '@types/react': 1447 + optional: true 1448 + '@types/react-dom': 1449 + optional: true 1450 + 1409 1451 '@radix-ui/rect@1.1.0': 1410 1452 resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} 1411 1453 ··· 3325 3367 resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} 3326 3368 hasBin: true 3327 3369 3370 + jiti@2.4.2: 3371 + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} 3372 + hasBin: true 3373 + 3328 3374 js-cookie@3.0.5: 3329 3375 resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} 3330 3376 engines: {node: '>=14'} ··· 4721 4767 '@emotion/memoize@0.7.4': 4722 4768 optional: true 4723 4769 4724 - '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0(jiti@1.21.7))': 4770 + '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0(jiti@2.4.2))': 4725 4771 dependencies: 4726 - eslint: 9.21.0(jiti@1.21.7) 4772 + eslint: 9.21.0(jiti@2.4.2) 4727 4773 eslint-visitor-keys: 3.4.3 4728 4774 4729 4775 '@eslint-community/regexpp@4.12.1': {} ··· 6043 6089 '@types/react': 19.0.10 6044 6090 '@types/react-dom': 19.0.4(@types/react@19.0.10) 6045 6091 6092 + '@radix-ui/react-avatar@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 6093 + dependencies: 6094 + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) 6095 + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6096 + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.10)(react@19.0.0) 6097 + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.10)(react@19.0.0) 6098 + react: 19.0.0 6099 + react-dom: 19.0.0(react@19.0.0) 6100 + optionalDependencies: 6101 + '@types/react': 19.0.10 6102 + '@types/react-dom': 19.0.4(@types/react@19.0.10) 6103 + 6046 6104 '@radix-ui/react-compose-refs@1.1.1(@types/react@19.0.10)(react@19.0.0)': 6047 6105 dependencies: 6048 6106 react: 19.0.0 ··· 6200 6258 optionalDependencies: 6201 6259 '@types/react': 19.0.10 6202 6260 6261 + '@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 6262 + dependencies: 6263 + '@radix-ui/primitive': 1.1.1 6264 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0) 6265 + '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0) 6266 + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6267 + '@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0) 6268 + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6269 + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6270 + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6271 + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6272 + '@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0) 6273 + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0) 6274 + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6275 + react: 19.0.0 6276 + react-dom: 19.0.0(react@19.0.0) 6277 + optionalDependencies: 6278 + '@types/react': 19.0.10 6279 + '@types/react-dom': 19.0.4(@types/react@19.0.10) 6280 + 6203 6281 '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.10)(react@19.0.0)': 6204 6282 dependencies: 6205 6283 react: 19.0.0 ··· 6239 6317 react: 19.0.0 6240 6318 optionalDependencies: 6241 6319 '@types/react': 19.0.10 6320 + 6321 + '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 6322 + dependencies: 6323 + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 6324 + react: 19.0.0 6325 + react-dom: 19.0.0(react@19.0.0) 6326 + optionalDependencies: 6327 + '@types/react': 19.0.10 6328 + '@types/react-dom': 19.0.4(@types/react@19.0.10) 6242 6329 6243 6330 '@radix-ui/rect@1.1.0': {} 6244 6331 ··· 7246 7333 7247 7334 '@sapphire/snowflake@3.5.5': {} 7248 7335 7249 - '@stylistic/eslint-plugin@4.1.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': 7336 + '@stylistic/eslint-plugin@4.1.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': 7250 7337 dependencies: 7251 - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 7252 - eslint: 9.21.0(jiti@1.21.7) 7338 + '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 7339 + eslint: 9.21.0(jiti@2.4.2) 7253 7340 eslint-visitor-keys: 4.2.0 7254 7341 espree: 10.3.0 7255 7342 estraverse: 5.3.0 ··· 7342 7429 7343 7430 '@types/unist@3.0.3': {} 7344 7431 7345 - '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': 7432 + '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': 7346 7433 dependencies: 7347 7434 '@eslint-community/regexpp': 4.12.1 7348 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 7435 + '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 7349 7436 '@typescript-eslint/scope-manager': 8.25.0 7350 - '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 7351 - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 7437 + '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 7438 + '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 7352 7439 '@typescript-eslint/visitor-keys': 8.25.0 7353 - eslint: 9.21.0(jiti@1.21.7) 7440 + eslint: 9.21.0(jiti@2.4.2) 7354 7441 graphemer: 1.4.0 7355 7442 ignore: 5.3.2 7356 7443 natural-compare: 1.4.0 ··· 7359 7446 transitivePeerDependencies: 7360 7447 - supports-color 7361 7448 7362 - '@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': 7449 + '@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': 7363 7450 dependencies: 7364 7451 '@typescript-eslint/scope-manager': 8.25.0 7365 7452 '@typescript-eslint/types': 8.25.0 7366 7453 '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) 7367 7454 '@typescript-eslint/visitor-keys': 8.25.0 7368 7455 debug: 4.4.0 7369 - eslint: 9.21.0(jiti@1.21.7) 7456 + eslint: 9.21.0(jiti@2.4.2) 7370 7457 typescript: 5.8.2 7371 7458 transitivePeerDependencies: 7372 7459 - supports-color ··· 7376 7463 '@typescript-eslint/types': 8.25.0 7377 7464 '@typescript-eslint/visitor-keys': 8.25.0 7378 7465 7379 - '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': 7466 + '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': 7380 7467 dependencies: 7381 7468 '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) 7382 - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 7469 + '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 7383 7470 debug: 4.4.0 7384 - eslint: 9.21.0(jiti@1.21.7) 7471 + eslint: 9.21.0(jiti@2.4.2) 7385 7472 ts-api-utils: 2.0.1(typescript@5.8.2) 7386 7473 typescript: 5.8.2 7387 7474 transitivePeerDependencies: ··· 7403 7490 transitivePeerDependencies: 7404 7491 - supports-color 7405 7492 7406 - '@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2)': 7493 + '@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': 7407 7494 dependencies: 7408 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@1.21.7)) 7495 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) 7409 7496 '@typescript-eslint/scope-manager': 8.25.0 7410 7497 '@typescript-eslint/types': 8.25.0 7411 7498 '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) 7412 - eslint: 9.21.0(jiti@1.21.7) 7499 + eslint: 9.21.0(jiti@2.4.2) 7413 7500 typescript: 5.8.2 7414 7501 transitivePeerDependencies: 7415 7502 - supports-color ··· 7999 8086 8000 8087 escape-string-regexp@4.0.0: {} 8001 8088 8002 - eslint-config-next@15.2.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2): 8089 + eslint-config-next@15.2.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2): 8003 8090 dependencies: 8004 8091 '@next/eslint-plugin-next': 15.2.0 8005 8092 '@rushstack/eslint-patch': 1.10.5 8006 - '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 8007 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 8008 - eslint: 9.21.0(jiti@1.21.7) 8093 + '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 8094 + '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 8095 + eslint: 9.21.0(jiti@2.4.2) 8009 8096 eslint-import-resolver-node: 0.3.9 8010 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7)) 8011 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)) 8012 - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@1.21.7)) 8013 - eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@1.21.7)) 8014 - eslint-plugin-react-hooks: 5.2.0(eslint@9.21.0(jiti@1.21.7)) 8097 + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) 8098 + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)) 8099 + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@2.4.2)) 8100 + eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@2.4.2)) 8101 + eslint-plugin-react-hooks: 5.2.0(eslint@9.21.0(jiti@2.4.2)) 8015 8102 optionalDependencies: 8016 8103 typescript: 5.8.2 8017 8104 transitivePeerDependencies: ··· 8027 8114 transitivePeerDependencies: 8028 8115 - supports-color 8029 8116 8030 - eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7)): 8117 + eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)): 8031 8118 dependencies: 8032 8119 '@nolyfill/is-core-module': 1.0.39 8033 8120 debug: 4.4.0 8034 8121 enhanced-resolve: 5.18.1 8035 - eslint: 9.21.0(jiti@1.21.7) 8122 + eslint: 9.21.0(jiti@2.4.2) 8036 8123 get-tsconfig: 4.10.0 8037 8124 is-bun-module: 1.3.0 8038 8125 stable-hash: 0.0.4 8039 8126 tinyglobby: 0.2.12 8040 8127 optionalDependencies: 8041 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)) 8128 + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)) 8042 8129 transitivePeerDependencies: 8043 8130 - supports-color 8044 8131 8045 - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)): 8132 + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)): 8046 8133 dependencies: 8047 8134 debug: 3.2.7 8048 8135 optionalDependencies: 8049 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 8050 - eslint: 9.21.0(jiti@1.21.7) 8136 + '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 8137 + eslint: 9.21.0(jiti@2.4.2) 8051 8138 eslint-import-resolver-node: 0.3.9 8052 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@1.21.7)) 8139 + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) 8053 8140 transitivePeerDependencies: 8054 8141 - supports-color 8055 8142 8056 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)): 8143 + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)): 8057 8144 dependencies: 8058 8145 '@rtsao/scc': 1.1.0 8059 8146 array-includes: 3.1.8 ··· 8062 8149 array.prototype.flatmap: 1.3.3 8063 8150 debug: 3.2.7 8064 8151 doctrine: 2.1.0 8065 - eslint: 9.21.0(jiti@1.21.7) 8152 + eslint: 9.21.0(jiti@2.4.2) 8066 8153 eslint-import-resolver-node: 0.3.9 8067 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@1.21.7)) 8154 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)) 8068 8155 hasown: 2.0.2 8069 8156 is-core-module: 2.16.1 8070 8157 is-glob: 4.0.3 ··· 8076 8163 string.prototype.trimend: 1.0.9 8077 8164 tsconfig-paths: 3.15.0 8078 8165 optionalDependencies: 8079 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 8166 + '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 8080 8167 transitivePeerDependencies: 8081 8168 - eslint-import-resolver-typescript 8082 8169 - eslint-import-resolver-webpack 8083 8170 - supports-color 8084 8171 8085 - eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0(jiti@1.21.7)): 8172 + eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0(jiti@2.4.2)): 8086 8173 dependencies: 8087 8174 aria-query: 5.3.2 8088 8175 array-includes: 3.1.8 ··· 8092 8179 axobject-query: 4.1.0 8093 8180 damerau-levenshtein: 1.0.8 8094 8181 emoji-regex: 9.2.2 8095 - eslint: 9.21.0(jiti@1.21.7) 8182 + eslint: 9.21.0(jiti@2.4.2) 8096 8183 hasown: 2.0.2 8097 8184 jsx-ast-utils: 3.3.5 8098 8185 language-tags: 1.0.9 ··· 8101 8188 safe-regex-test: 1.1.0 8102 8189 string.prototype.includes: 2.0.1 8103 8190 8104 - eslint-plugin-path-alias@2.1.0(eslint@9.21.0(jiti@1.21.7)): 8191 + eslint-plugin-path-alias@2.1.0(eslint@9.21.0(jiti@2.4.2)): 8105 8192 dependencies: 8106 - eslint: 9.21.0(jiti@1.21.7) 8193 + eslint: 9.21.0(jiti@2.4.2) 8107 8194 find-pkg: 2.0.0 8108 8195 get-tsconfig: 4.10.0 8109 8196 nanomatch: 1.2.13 8110 8197 transitivePeerDependencies: 8111 8198 - supports-color 8112 8199 8113 - eslint-plugin-react-compiler@19.0.0-beta-e1e972c-20250221(eslint@9.21.0(jiti@1.21.7)): 8200 + eslint-plugin-react-compiler@19.0.0-beta-e1e972c-20250221(eslint@9.21.0(jiti@2.4.2)): 8114 8201 dependencies: 8115 8202 '@babel/core': 7.26.9 8116 8203 '@babel/parser': 7.26.9 8117 8204 '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.9) 8118 - eslint: 9.21.0(jiti@1.21.7) 8205 + eslint: 9.21.0(jiti@2.4.2) 8119 8206 hermes-parser: 0.25.1 8120 8207 zod: 3.24.2 8121 8208 zod-validation-error: 3.4.0(zod@3.24.2) 8122 8209 transitivePeerDependencies: 8123 8210 - supports-color 8124 8211 8125 - eslint-plugin-react-hooks@5.2.0(eslint@9.21.0(jiti@1.21.7)): 8212 + eslint-plugin-react-hooks@5.2.0(eslint@9.21.0(jiti@2.4.2)): 8126 8213 dependencies: 8127 - eslint: 9.21.0(jiti@1.21.7) 8214 + eslint: 9.21.0(jiti@2.4.2) 8128 8215 8129 - eslint-plugin-react@7.37.4(eslint@9.21.0(jiti@1.21.7)): 8216 + eslint-plugin-react@7.37.4(eslint@9.21.0(jiti@2.4.2)): 8130 8217 dependencies: 8131 8218 array-includes: 3.1.8 8132 8219 array.prototype.findlast: 1.2.5 ··· 8134 8221 array.prototype.tosorted: 1.1.4 8135 8222 doctrine: 2.1.0 8136 8223 es-iterator-helpers: 1.2.1 8137 - eslint: 9.21.0(jiti@1.21.7) 8224 + eslint: 9.21.0(jiti@2.4.2) 8138 8225 estraverse: 5.3.0 8139 8226 hasown: 2.0.2 8140 8227 jsx-ast-utils: 3.3.5 ··· 8148 8235 string.prototype.matchall: 4.0.12 8149 8236 string.prototype.repeat: 1.0.0 8150 8237 8151 - eslint-plugin-simple-import-sort@12.1.1(eslint@9.21.0(jiti@1.21.7)): 8238 + eslint-plugin-simple-import-sort@12.1.1(eslint@9.21.0(jiti@2.4.2)): 8152 8239 dependencies: 8153 - eslint: 9.21.0(jiti@1.21.7) 8240 + eslint: 9.21.0(jiti@2.4.2) 8154 8241 8155 - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7)): 8242 + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)): 8156 8243 dependencies: 8157 - eslint: 9.21.0(jiti@1.21.7) 8244 + eslint: 9.21.0(jiti@2.4.2) 8158 8245 optionalDependencies: 8159 - '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 8246 + '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 8160 8247 8161 8248 eslint-scope@8.2.0: 8162 8249 dependencies: ··· 8167 8254 8168 8255 eslint-visitor-keys@4.2.0: {} 8169 8256 8170 - eslint@9.21.0(jiti@1.21.7): 8257 + eslint@9.21.0(jiti@2.4.2): 8171 8258 dependencies: 8172 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@1.21.7)) 8259 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) 8173 8260 '@eslint-community/regexpp': 4.12.1 8174 8261 '@eslint/config-array': 0.19.2 8175 8262 '@eslint/core': 0.12.0 ··· 8204 8291 natural-compare: 1.4.0 8205 8292 optionator: 0.9.4 8206 8293 optionalDependencies: 8207 - jiti: 1.21.7 8294 + jiti: 2.4.2 8208 8295 transitivePeerDependencies: 8209 8296 - supports-color 8210 8297 ··· 8805 8892 '@pkgjs/parseargs': 0.11.0 8806 8893 8807 8894 jiti@1.21.7: {} 8895 + 8896 + jiti@2.4.2: 8897 + optional: true 8808 8898 8809 8899 js-cookie@3.0.5: {} 8810 8900 ··· 10044 10134 possible-typed-array-names: 1.1.0 10045 10135 reflect.getprototypeof: 1.0.10 10046 10136 10047 - typescript-eslint@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2): 10137 + typescript-eslint@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2): 10048 10138 dependencies: 10049 - '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2))(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 10050 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 10051 - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@1.21.7))(typescript@5.8.2) 10052 - eslint: 9.21.0(jiti@1.21.7) 10139 + '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 10140 + '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 10141 + '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) 10142 + eslint: 9.21.0(jiti@2.4.2) 10053 10143 typescript: 5.8.2 10054 10144 transitivePeerDependencies: 10055 10145 - supports-color
+13 -7
tailwind.config.js
··· 14 14 theme: { 15 15 extend: { 16 16 colors: { 17 - 'wamellow': wamellow, 18 - 'wamellow-light': 'rgb(30, 32, 34)', 19 - 'wamellow-alpha': 'rgba(255, 255, 255, 0.06)', 20 - 'wamellow-100': '#e2e8f0', 21 - 'wamellow-100-light': '#ced3da', 22 - 'wamellow-100-alpha': 'rgba(0, 0, 0, 0.06)', 23 - 'wamellow-900-alpha': 'rgba(0, 0, 0, 0.54)', 24 17 'blurple': '#5865f2', 25 18 'blurple-dark': '#454fbf', 26 19 'discord-gray': '#1c1d23', 27 20 foreground: 'hsl(var(--foreground))', 21 + wamellow: { 22 + DEFAULT: 'var(--wamellow)' 23 + }, 24 + "wamellow-100": { 25 + DEFAULT: 'var(--wamellow-100)' 26 + }, 27 + "wamellow-200": { 28 + DEFAULT: 'var(--wamellow-200)' 29 + }, 28 30 card: { 29 31 DEFAULT: 'hsl(var(--card))', 30 32 foreground: 'hsl(var(--card-foreground))' ··· 41 43 DEFAULT: 'hsl(var(--secondary))', 42 44 foreground: 'hsl(var(--secondary-foreground))' 43 45 }, 46 + flat: { 47 + DEFAULT: 'hsl(var(--flat))', 48 + foreground: 'hsl(var(--flat-foreground))' 49 + }, 44 50 muted: { 45 51 DEFAULT: 'hsl(var(--muted))', 46 52 foreground: 'hsl(var(--muted-foreground))'