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

Configure Feed

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

refactor: redesign homepage (#45)

authored by

Luna Seemann and committed by
GitHub
9faedcff 534a62d5

+314 -90
+51
app/(home)/cta.component.tsx
··· 1 + import { Button } from "@/components/ui/button"; 2 + import { cn } from "@/utils/cn"; 3 + import { Montserrat } from "next/font/google"; 4 + import Link from "next/link"; 5 + import { HiArrowRight, HiUserAdd } from "react-icons/hi"; 6 + 7 + const montserrat = Montserrat({ subsets: ["latin"] }); 8 + 9 + export function CallToAction() { 10 + return ( 11 + <div className="w-full my-8"> 12 + <div className="relative overflow-hidden rounded-2xl bg-linear-to-br from-violet-500/20 via-indigo-500/10 to-pink-500/20 p-8 md:p-12"> 13 + <div className="absolute -top-24 -right-24 size-64 bg-violet-400/20 rounded-full blur-3xl" /> 14 + <div className="absolute -bottom-24 -left-24 size-64 bg-pink-400/20 rounded-full blur-3xl" /> 15 + 16 + <div className="relative z-10 text-center max-w-2xl mx-auto"> 17 + <h2 className={cn(montserrat.className, "text-3xl md:text-4xl font-bold bg-linear-to-r from-white via-violet-200 to-white bg-clip-text text-transparent mb-4")}> 18 + Ready to get started? 19 + </h2> 20 + <p className="text-muted-foreground text-lg mb-8"> 21 + Accessibility, social notifications, and more — all free, all in one bot. 22 + </p> 23 + 24 + <div className="flex flex-col sm:flex-row gap-3 justify-center"> 25 + <Button 26 + size="lg" 27 + variant="secondary" 28 + className="text-base" 29 + asChild 30 + > 31 + <Link href="/login?invite=true" prefetch={false}> 32 + <HiUserAdd className="mr-1" /> 33 + Add to Discord 34 + </Link> 35 + </Button> 36 + <Button 37 + size="lg" 38 + className="text-base" 39 + asChild 40 + > 41 + <Link href="/dashboard"> 42 + Dashboard 43 + <HiArrowRight className="ml-1" /> 44 + </Link> 45 + </Button> 46 + </div> 47 + </div> 48 + </div> 49 + </div> 50 + ); 51 + }
+8 -7
app/(home)/faq.component.tsx
··· 163 163 )} 164 164 165 165 <Accordion 166 - className="w-full" 166 + className="w-full space-y-2" 167 167 type="single" 168 168 collapsible 169 169 defaultValue="0" ··· 172 172 <AccordionItem 173 173 value={index.toString()} 174 174 key={index} 175 + className="bg-wamellow/50 rounded-xl border-none px-4 data-[state=open]:bg-wamellow" 175 176 > 176 - <AccordionTrigger className="text-left"> 177 - <div className="flex items-start gap-3"> 178 - <div className="mt-1 text-lg"> 177 + <AccordionTrigger className="text-left hover:no-underline py-4"> 178 + <div className="flex items-start gap-4"> 179 + <div className="mt-0.5 p-2 rounded-lg bg-violet-500/10 text-violet-400"> 179 180 {item.startContent} 180 181 </div> 181 182 <div> 182 - <div itemProp="name"> 183 + <div itemProp="name" className="font-semibold text-foreground"> 183 184 {item.title} 184 185 </div> 185 186 {item.subtitle && ( 186 - <div className="text-sm text-muted-foreground font-normal"> 187 + <div className="text-sm text-muted-foreground font-normal mt-0.5"> 187 188 {item.subtitle} 188 189 </div> 189 190 )} 190 191 </div> 191 192 </div> 192 193 </AccordionTrigger> 193 - <AccordionContent className="mb-2 space-y-4"> 194 + <AccordionContent className="text-medium pb-4 pl-14 space-y-4 text-muted-foreground"> 194 195 {item.content} 195 196 </AccordionContent> 196 197 </AccordionItem>
+15 -70
app/(home)/page.tsx
··· 7 7 import DiscordMessage from "@/components/discord/message"; 8 8 import DiscordMessageEmbed from "@/components/discord/message-embed"; 9 9 import DiscordUser from "@/components/discord/user"; 10 - import ImageReduceMotion from "@/components/image-reduce-motion"; 11 10 import { AvatarGroup, UserAvatar } from "@/components/ui/avatar"; 12 11 import { Badge } from "@/components/ui/badge"; 13 12 import { Button } from "@/components/ui/button"; 14 13 import { Skeleton } from "@/components/ui/skeleton"; 15 14 import { Code } from "@/components/ui/typography"; 16 - import { defaultFetchOptions } from "@/lib/api"; 17 15 import CaptchaPic from "@/public/captcha.webp"; 18 16 import ArrowPic from "@/public/icons/arroww.webp"; 19 17 import LeaderboardPic from "@/public/leaderboard.webp"; 20 18 import NotificationsPic from "@/public/notifications-thumbnail.webp"; 19 + import SharkPic from "@/public/shark.webp"; 21 20 import SpacePic from "@/public/space.webp"; 22 - import WaifuPic from "@/public/waifu.webp"; 23 21 import WelcomePic from "@/public/welcome.webp"; 24 - import type { ApiV1TopguildsGetResponse } from "@/typings"; 25 22 import { cn } from "@/utils/cn"; 26 - import { toFixedArrayLength } from "@/utils/fixed-array-length"; 27 23 import { actor } from "@/utils/tts"; 28 24 import { getCanonicalUrl } from "@/utils/urls"; 29 25 import { Montserrat, Patrick_Hand } from "next/font/google"; ··· 34 30 import { BsDiscord, BsYoutube } from "react-icons/bs"; 35 31 import { HiArrowNarrowRight, HiArrowRight, HiCash, HiCheck, HiFire, HiLockOpen, HiUserAdd } from "react-icons/hi"; 36 32 37 - import { Commands } from "./commands.component"; 33 + import { CallToAction } from "./cta.component"; 38 34 import { Faq } from "./faq.component"; 39 35 import { Ratings } from "./ratings.component"; 36 + import { TTSDemo } from "./tts-demo.component"; 40 37 41 38 const montserrat = Montserrat({ subsets: ["latin"] }); 42 39 const handwritten = Patrick_Hand({ subsets: ["latin"], weight: "400" }); ··· 66 63 }); 67 64 68 65 export default async function Home() { 69 - const topGuildsPromise = fetch(`${process.env.NEXT_PUBLIC_API}/top-guilds`, defaultFetchOptions) 70 - .then((res) => res.json()) 71 - .catch(() => null) as Promise<ApiV1TopguildsGetResponse[] | null>; 72 - 73 66 const heads = await headers(); 74 67 const isEmbedded = heads.get("sec-fetch-dest") === "iframe"; 75 68 76 - const topGuilds = await topGuildsPromise; 77 - 78 69 return ( 79 70 <div className="flex items-center flex-col w-full"> 80 71 81 - <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)]"> 72 + <div className="flex flex-col md:flex-row w-full items-center justify-center md:justify-start gap-8 py-8 md:py-0 mb-16 md:mb-12 min-h-[calc(100svh-14rem)] md:min-h-[500px] md:h-[calc(100dvh-16rem)]"> 82 73 <div className="md:min-w-96 w-full md:w-2/3 xl:w-1/2 flex flex-col space-y-6"> 83 74 84 - <Suspense fallback={<Skeleton className="w-60 h-6! m-0!" isLoading={true} />}> 75 + <Suspense fallback={<Skeleton className="w-52 h-6! m-0! mb-6!" isLoading={true} />}> 85 76 <Ratings /> 86 77 </Suspense> 87 78 88 - <h1 className={cn(montserrat.className, "lg:text-7xl md:text-6xl text-5xl font-semibold dark:text-neutral-100 text-neutral-900 break-words")}> 79 + <h1 className={cn(montserrat.className, "lg:text-7xl md:text-6xl text-5xl max-w-md md:max-w-xl font-semibold dark:text-neutral-100 text-neutral-900 break-words")}> 89 80 <span className="bg-linear-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent h-20 break-keep"> 90 81 Accessibility 91 82 </span> ··· 96 87 </span> 97 88 </h1> 98 89 99 - <span className="text-lg font-medium max-w-152 mb-4"> 90 + <span className="text-lg font-medium max-w-152 mb-4 hidden md:block"> 100 91 Accessibility where it&apos;s needed the most: Discord Voice Chats. 101 - Social notifications to stay connected and up to date with anyone, anywhere. 102 - Simple, customizable, free, and built in public. 92 + Social notifications to stay connected and up to date, anywhere. 103 93 </span> 104 - 105 - <AvatarGroup className="mr-auto md:hidden"> 106 - {toFixedArrayLength(topGuilds || [], 8) 107 - ?.map((guild) => ( 108 - <UserAvatar 109 - key={"mobileGuildGrid-" + guild.id} 110 - alt={guild.name} 111 - className="-mr-2" 112 - src={guild.icon ? guild.icon + "?size=128" : "/discord.webp"} 113 - /> 114 - )) 115 - } 116 - </AvatarGroup> 117 94 118 95 <div className="space-y-4"> 119 96 <Link ··· 161 138 </div> 162 139 </div> 163 140 164 - <div className="ml-auto w-fit xl:w-1/2 hidden md:block"> 165 - <div className="flex gap-4 rotate-6 relative left-14 w-fit"> 166 - {[0, 1, 2, 3].map((i) => ( 167 - <div 168 - key={"guildGridThing-" + i} 169 - className={cn( 170 - "flex flex-col gap-4", 171 - i % 2 === 1 ? "mt-4 animate-guilds" : "animate-guilds-2", 172 - (i === 0 || i === 3) && "hidden xl:flex") 173 - } 174 - > 175 - {toFixedArrayLength(topGuilds || [], 12) 176 - .slice(i * 3, (i * 3) + 3) 177 - .map((guild, i2) => ( 178 - <Link 179 - key={"guildGrid-" + guild.id + i + i2} 180 - className="relative md:h-32 h-24 md:w-32 w-24 hover:scale-110 duration-200" 181 - href={getCanonicalUrl("leaderboard", guild.id)} 182 - prefetch={false} 183 - > 184 - <ImageReduceMotion 185 - alt="server" 186 - className="rounded-xl bg-wamellow" 187 - url={(guild.icon || "/discord.webp")?.split(".").slice(0, -1).join(".")} 188 - size={128} 189 - /> 190 - </Link> 191 - )) 192 - } 193 - </div> 194 - ))} 195 - </div> 141 + <div className="mt-8 w-full max-w-sm mx-auto md:mt-0 md:w-auto md:max-w-none md:ml-auto xl:w-1/3 md:flex md:items-center md:justify-center md:rotate-3 md:scale-110 md:relative md:left-16"> 142 + <TTSDemo /> 196 143 </div> 197 144 </div> 198 145 199 - <div className="animate-scroll rounded-medium rotate-180 md:rounded-3xl md:rotate-0"> 146 + <div className="animate-scroll rounded-medium rotate-180 rounded-md md:rounded-3xl md:rotate-0"> 200 147 <div className="animate-scroll-wheel" /> 201 148 </div> 202 149 ··· 354 301 355 302 <div className="bg-discord-gray w-full md:w-1/2 px-8 py-4 rounded-lg"> 356 303 <DiscordMessage {...messageProps()}> 357 - <DiscordMarkdown mode={"DARK"} text="Hey **@everyone**, Linus Tech Tips just posted a new video!\n[youtube.com/watch?v=tN-arR2UoRk](https://youtube.com/watch?v=tN-arR2UoRk)" /> 304 + <DiscordMarkdown mode={"DARK"} text="Hey **@everyone**, I just posted a new video!!\n[youtube.com/watch?v=tN-arR2UoRk](https://youtube.com/watch?v=tN-arR2UoRk)" /> 358 305 <DiscordMessageEmbed 359 306 mode="DARK" 360 - title="My wife insisted I do this for her" 307 + title="Lundale server goes brrrrr" 361 308 color={0x8A_57_FF} 362 309 > 363 310 <Image ··· 422 369 height={905 / 3} 423 370 itemProp="image" 424 371 loading="lazy" 425 - src={WaifuPic} 372 + src={SharkPic} 426 373 width={640 / 3} 427 374 /> 428 375 </DiscordMessage> ··· 724 671 content="FUCK EVERYTHING! EXCEPT LUNA, LUNA MUST BE PROTECTED AT ALL COSTS" 725 672 /> 726 673 727 - <Suspense> 728 - <Commands /> 729 - </Suspense> 674 + <CallToAction /> 730 675 731 676 </div> 732 677 );
+140
app/(home)/tts-demo.component.tsx
··· 1 + "use client"; 2 + 3 + import DiscordChannel from "@/components/discord/channel"; 4 + import DiscordChannelCategory from "@/components/discord/channel-category"; 5 + import DiscordUser from "@/components/discord/user"; 6 + import { actor } from "@/utils/tts"; 7 + import Image from "next/image"; 8 + import { useEffect, useRef, useState } from "react"; 9 + 10 + const DEMO_MESSAGES = [ 11 + "Hello everyone, welcome to the voice channel!", 12 + "Accessibility where it's needed most.", 13 + "Type in the chat, I'll speak for you.", 14 + "97 voices across 10 languages!" 15 + ]; 16 + 17 + const SUPPORTED_LANGS = ["us", "fr", "de", "es", "br", "pt", "id", "it", "jp", "kr"] as const; 18 + 19 + const LANG_TO_NAME_MAP = Object 20 + .fromEntries( 21 + Object 22 + .values(actor) 23 + .map(([name, langCode]) => [langCode, name]) 24 + ); 25 + 26 + enum AnimationPhase { 27 + Typing = 0, 28 + Speaking = 1, 29 + Pausing = 2 30 + } 31 + 32 + export function TTSDemo() { 33 + const [messageIndex, setMessageIndex] = useState(0); 34 + const [charIndex, setCharIndex] = useState(0); 35 + const [phase, setPhase] = useState<AnimationPhase>(AnimationPhase.Typing); 36 + 37 + const currentMessage = DEMO_MESSAGES[messageIndex]; 38 + const displayedText = currentMessage.slice(0, charIndex); 39 + 40 + const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); 41 + 42 + useEffect(() => { 43 + if (phase !== AnimationPhase.Typing) return; 44 + 45 + if (charIndex < currentMessage.length) { 46 + timeoutRef.current = setTimeout(() => { 47 + setCharIndex((c) => c + 1); 48 + }, 40 + Math.random() * 30); 49 + } else { 50 + timeoutRef.current = setTimeout(() => { 51 + setPhase(AnimationPhase.Speaking); 52 + }, 100); 53 + } 54 + 55 + return () => { 56 + if (timeoutRef.current) clearTimeout(timeoutRef.current); 57 + }; 58 + }, [phase, charIndex, currentMessage.length]); 59 + 60 + useEffect(() => { 61 + if (phase !== AnimationPhase.Speaking) return; 62 + 63 + const speakDuration = 2_000 + currentMessage.length * 30; 64 + timeoutRef.current = setTimeout(() => { 65 + setPhase(AnimationPhase.Pausing); 66 + }, speakDuration); 67 + 68 + return () => { 69 + if (timeoutRef.current) clearTimeout(timeoutRef.current); 70 + }; 71 + }, [phase, currentMessage.length]); 72 + 73 + useEffect(() => { 74 + if (phase !== AnimationPhase.Pausing) return; 75 + 76 + timeoutRef.current = setTimeout(() => { 77 + setMessageIndex((i) => (i + 1) % DEMO_MESSAGES.length); 78 + setCharIndex(0); 79 + setPhase(AnimationPhase.Typing); 80 + }, 800); 81 + 82 + return () => { 83 + if (timeoutRef.current) clearTimeout(timeoutRef.current); 84 + }; 85 + }, [phase]); 86 + 87 + const isSpeaking = phase === AnimationPhase.Speaking; 88 + const isTyping = phase === AnimationPhase.Typing && charIndex < currentMessage.length; 89 + 90 + return ( 91 + <div className="relative w-full"> 92 + <div className="absolute inset-0 -z-10"> 93 + <div className="absolute -inset-4 bg-gradient-to-br from-violet-600/30 via-indigo-500/20 to-pink-500/30 rounded-3xl blur-2xl animate-pulse-slow" /> 94 + <div className="absolute -top-8 -left-8 size-32 bg-violet-500/25 rounded-full blur-3xl animate-float" /> 95 + <div className="absolute -bottom-6 -right-6 size-24 bg-pink-500/20 rounded-full blur-2xl animate-float-delayed" /> 96 + </div> 97 + 98 + <div className="flex flex-col gap-3 relative"> 99 + <div className="bg-discord-gray rounded-lg py-4 px-8 border border-white/5 backdrop-blur-sm"> 100 + <DiscordChannelCategory name="#/voice/dev/null"> 101 + <DiscordChannel 102 + type="voice" 103 + name="• Public" 104 + > 105 + <DiscordUser username="Luna" avatar="/luna.webp" isMuted /> 106 + <DiscordUser username="Duck" avatar="/space.webp" isMuted /> 107 + <DiscordUser username="Wamellow" avatar="/waya-v3.webp" isTalking={isSpeaking} isBot /> 108 + </DiscordChannel> 109 + </DiscordChannelCategory> 110 + </div> 111 + 112 + {/* Chat input */} 113 + <div className="bg-[#383a40] rounded-lg p-3 w-full border border-white/5 backdrop-blur-sm"> 114 + <div className="flex items-center gap-2"> 115 + <Image src="/luna.webp" alt="" width={64} height={64} className="size-6 rounded-full" /> 116 + <div className="flex-1 text-sm text-white min-h-5"> 117 + {displayedText} 118 + {isTyping && ( 119 + <span className="inline-block w-0.5 h-4 bg-white ml-0.5 animate-blink" /> 120 + )} 121 + </div> 122 + </div> 123 + </div> 124 + 125 + <div className="flex gap-1"> 126 + {SUPPORTED_LANGS.map((lang) => ( 127 + <Image 128 + key={lang} 129 + src={`/icons/${lang}.webp`} 130 + alt={LANG_TO_NAME_MAP[lang] || lang} 131 + width={64} 132 + height={64} 133 + className="size-5 rounded-sm opacity-60 hover:opacity-100 transition-opacity" 134 + /> 135 + ))} 136 + </div> 137 + </div> 138 + </div> 139 + ); 140 + }
+80
app/globals.css
··· 299 299 } 300 300 } 301 301 302 + .animate-blink { 303 + animation: blink 1s step-end infinite; 304 + } 305 + 306 + @keyframes blink { 307 + 308 + 0%, 309 + 100% { 310 + opacity: 1; 311 + } 312 + 313 + 50% { 314 + opacity: 0; 315 + } 316 + } 317 + 318 + .animate-sound-wave { 319 + animation: sound-wave 0.6s ease-in-out infinite alternate; 320 + } 321 + 322 + @keyframes sound-wave { 323 + 0% { 324 + transform: scaleY(0.4); 325 + } 326 + 327 + 100% { 328 + transform: scaleY(1); 329 + } 330 + } 331 + 332 + .animate-pulse-slow { 333 + animation: pulse-slow 4s ease-in-out infinite; 334 + } 335 + 336 + @keyframes pulse-slow { 337 + 338 + 0%, 339 + 100% { 340 + opacity: 0.6; 341 + transform: scale(1); 342 + } 343 + 344 + 50% { 345 + opacity: 1; 346 + transform: scale(1.05); 347 + } 348 + } 349 + 350 + .animate-float { 351 + animation: float 6s ease-in-out infinite; 352 + } 353 + 354 + @keyframes float { 355 + 356 + 0%, 357 + 100% { 358 + transform: translate(0, 0); 359 + } 360 + 361 + 50% { 362 + transform: translate(10px, -10px); 363 + } 364 + } 365 + 366 + .animate-float-delayed { 367 + animation: float-delayed 7s ease-in-out infinite; 368 + } 369 + 370 + @keyframes float-delayed { 371 + 372 + 0%, 373 + 100% { 374 + transform: translate(-8px, 8px); 375 + } 376 + 377 + 50% { 378 + transform: translate(0, 0); 379 + } 380 + } 381 + 302 382 .shake { 303 383 position: relative; 304 384 animation: shake 60ms infinite alternate;
+2 -2
components/discord/channel.tsx
··· 31 31 {name} 32 32 </span> 33 33 </span> 34 - {children && 34 + {children && ( 35 35 <div className="ml-6 mt-2 flex flex-col gap-3"> 36 36 {children} 37 37 </div> 38 - } 38 + )} 39 39 </div> 40 40 ); 41 41 }
+7 -6
components/discord/message-embed.tsx
··· 42 42 <div 43 43 className={cn( 44 44 mode === "DARK" ? "text-neutral-200" : "text-neutral-800", 45 - "w-full font-light p-3 rounded-sm border-l-4", 45 + "w-full font-light p-3 rounded-sm border-l-4 mt-2", 46 46 className 47 47 )} 48 48 style={{ ··· 53 53 54 54 <div className="flex w-full max-w-full"> 55 55 <div className={thumbnail ? "w-9/12" : "w-full"}> 56 - {author && 56 + {author && ( 57 57 <div 58 58 className={cn( 59 59 mode === "DARK" ? "text-neutral-100" : "text-neutral-900", ··· 67 67 embed={true} 68 68 /> 69 69 </div> 70 - } 71 - {title && 70 + )} 71 + {title && ( 72 72 <div 73 73 className={cn( 74 74 mode === "DARK" ? "text-neutral-100" : "text-neutral-900", 75 - "font-semibold text-lg mb-2" 75 + "font-semibold text-lg mb-2", 76 + !author && "-mt-1" 76 77 )} 77 78 > 78 79 <DiscordMarkdown ··· 81 82 embed={true} 82 83 /> 83 84 </div> 84 - } 85 + )} 85 86 <div className="text-sm"> 86 87 {children} 87 88 </div>
+11 -5
components/discord/user.tsx
··· 1 1 import { cn } from "@/utils/cn"; 2 + import { BsMicMuteFill } from "react-icons/bs"; 2 3 3 4 import DiscordAppBadge from "./app-badge"; 4 5 import { UserAvatar } from "../ui/avatar"; ··· 8 9 avatar: string; 9 10 isBot?: boolean; 10 11 isTalking?: boolean; 12 + isMuted?: boolean; 11 13 } 12 14 13 15 export default function DiscordUser({ 14 16 username, 15 17 avatar, 16 18 isBot, 17 - isTalking 19 + isTalking, 20 + isMuted 18 21 }: Props) { 19 22 return ( 20 - <div className="flex items-center space-x-2"> 23 + <div className={cn("flex items-center space-x-2", isTalking && "text-primary-foreground")}> 21 24 <UserAvatar 22 25 alt={`${username}'s avatar`} 23 - className={cn("size-6 shrink-0", isTalking && "outline-1.5 outline-green-500")} 26 + className={cn("size-6 shrink-0", isTalking && "outline-2 outline-green-500")} 24 27 src={avatar} 25 28 username={username} 26 29 /> 27 30 <div className="font-medium whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer" > 28 31 {username} 29 32 </div> 30 - {isBot && 33 + {isBot && ( 31 34 <DiscordAppBadge /> 32 - } 35 + )} 36 + {isMuted && !isTalking && ( 37 + <BsMicMuteFill className="ml-auto" /> 38 + )} 33 39 </div> 34 40 ); 35 41 }
public/notifications-thumbnail.webp

This is a binary file and will not be displayed.

public/shark.webp

This is a binary file and will not be displayed.

public/waifu.webp

This is a binary file and will not be displayed.