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

Configure Feed

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

add AI image pages

Luna ac67a9df a1e35c8e

+633 -179
+82 -128
app/(home)/ai/page.tsx
··· 1 1 import { Chip } from "@nextui-org/react"; 2 2 import { Metadata } from "next"; 3 - import { Montserrat, Patrick_Hand } from "next/font/google"; 3 + import { Montserrat } from "next/font/google"; 4 4 import Image from "next/image"; 5 5 import Link from "next/link"; 6 6 import { BsDiscord } from "react-icons/bs"; 7 - import { HiChevronRight, HiLightningBolt, HiUserAdd } from "react-icons/hi"; 7 + import { HiUserAdd } from "react-icons/hi"; 8 8 9 - import Badge from "@/components/badge"; 10 - import ImageGrid from "@/components/image-grid"; 11 - import ImageReduceMotion from "@/components/image-reduce-motion"; 12 - import TextInput from "@/components/inputs/TextInput"; 9 + import { getUploads } from "@/app/ai-gallary/api"; 10 + import Notice from "@/components/notice"; 13 11 import { ServerButton } from "@/components/server-button"; 14 - import ArrowPic from "@/public/arroww.webp"; 15 - import { ApiV1AiResponse } from "@/typings"; 12 + import CommandPic from "@/public/image-command.webp"; 16 13 import cn from "@/utils/cn"; 17 14 import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; 18 15 19 16 const montserrat = Montserrat({ subsets: ["latin"] }); 20 - const handwritten = Patrick_Hand({ subsets: ["latin"], weight: "400" }); 21 17 22 18 export const revalidate = 3600; 23 19 24 - const fetchOptions = { headers: { Authorization: process.env.API_SECRET as string }, next: { revalidate: 60 * 60 } }; 25 - 26 20 export const generateMetadata = async (): Promise<Metadata> => { 27 21 28 - const title = "Supercharged image Ai for Discord"; 29 - const description = "Unlock complimentary access to a variety of free /image generation models directly within your Discord server."; 22 + const title = "Free /image Ai for Discord"; 23 + const description = "Summon the enchantment of AI generated images to your Discord server with our versatile /image command, featuring over 40+ distinct SD and 10+ SDXL models."; 30 24 const url = getCanonicalUrl("ai"); 31 25 32 26 return { ··· 53 47 }; 54 48 55 49 export default async function Home() { 56 - const models = await fetch(`${process.env.NEXT_PUBLIC_API}/ai`, fetchOptions).then((res) => res.json()) as ApiV1AiResponse[]; 50 + const uploads = await getUploads(); 57 51 58 52 const styles = { 59 53 h2: cn(montserrat.className, "lg:text-5xl text-4xl bg-gradient-to-b bg-clip-text text-transparent from-neutral-200 from-40% to-neutral-400 font-bold underline decoration-violet-400"), ··· 63 57 return ( 64 58 <div className="flex items-center flex-col w-full"> 65 59 66 - <div className="md:text-5xl text-4xl font-semibold md:mb-6 mb-4 dark:text-neutral-100 text-neutral-900 flex items-center gap-2 w-full"> 67 - <h1 className={montserrat.className}> 68 - <span className="bg-gradient-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent break-keep">Supercharged Ai</span> 69 - </h1> 70 - <HiLightningBolt className="text-pink-400 rotate-6" /> 60 + <div className="flex flex-col gap-4 w-full items-center mb-20"> 71 61 <Chip 72 - className="ml-auto" 73 62 color="secondary" 74 63 variant="flat" 75 64 size="lg" 76 65 > 77 66 <span className="font-semibold text-lg"> 78 - Free 67 + Free, duh 79 68 </span> 80 69 </Chip> 81 - </div> 82 70 83 - {models && 84 - <ImageGrid 85 - images={ 86 - models 87 - .slice(0, 24) 88 - .map((model) => ({ 89 - id: model.title, 90 - url: model.url || "/discord.webp" 91 - })) 92 - } 93 - /> 94 - } 71 + <h1 className={cn(montserrat.className, "lg:text-7xl md:text-6xl text-5xl font-semibold dark:text-neutral-100 text-neutral-900 break-words flex flex-col items-center gap-4")}> 72 + <div> 73 + <span className="underline decoration-blurple break-keep">Create Artwork</span> 74 + {" with "} 75 + </div> 76 + <div className="bg-gradient-to-r from-indigo-400 to-pink-400 bg-clip-text text-transparent h-20 break-keep">Wamellow AI</div> 77 + </h1> 95 78 96 - <div className="md:text-xl text-lg lg:flex w-full mt-4 gap-4"> 97 - <span className="font-medium"> 98 - Unlock complimentary access to a variety of free image generation models directly within your Discord server. Powered by LuxuryLabs. 79 + <span className="text-lg text-center font-medium max-w-xl mb-4"> 80 + Create amazing artwork of animies and more or view {"length" in uploads ? uploads.length - 1 : "..."}+ uploads of our free /image Ai models directly within your Discord server. 99 81 </span> 100 82 101 - <div className="flex flex-col min-w-full lg:min-w-[420px]"> 102 - 103 - <div className="lg:ml-auto flex gap-2 mt-4 lg:mt-0"> 104 - <ServerButton 105 - as={Link} 106 - startContent={<HiUserAdd />} 107 - className="w-1/2 lg:w-fit !text-xl !font-medium" 108 - color="secondary" 109 - href="/login?invite=true" 110 - size="lg" 111 - > 112 - <span className="block sm:hidden">Invite</span> 113 - <span className="hidden sm:block">Invite Wamellow</span> 114 - </ServerButton> 115 - <ServerButton 116 - as={Link} 117 - startContent={<BsDiscord />} 118 - className="w-1/2 lg:w-fit !text-xl !font-medium" 119 - href="/support" 120 - size="lg" 121 - > 122 - <span className="block sm:hidden">Support</span> 123 - <span className="hidden sm:block">Join support</span> 124 - </ServerButton> 125 - </div> 126 - 127 - 128 - <span className={`lg:ml-auto flex gap-2 text-neutral-500 font-mediumr ${handwritten.className} mt-3 opacity-80 pl-20 lg:pr-20 rotate-2`}> 129 - <Image src={ArrowPic} width={24} height={24} alt="arrow up" className="h-5 w-5 relative top-px" draggable={false} /> 130 - Get started here in seconds 131 - </span> 132 - 133 - </div> 134 - 135 - </div> 136 - 137 - <div className="lg:mt-14 mt-10" /> 138 - 139 - <div className="w-full"> 140 - <h2 className={styles.h2}>/image command</h2> 141 - <div className="my-8 font-medium space-y-4"> 142 - <div className="max-w-xl"> 143 - We recommend that you play around with each model yourself as this page uses the same query for everything, even non-anime models. 144 - </div> 145 - 146 - <TextInput 147 - name="Query" 148 - defaultState="anime girl, close up, intricate details, dramatic lighting, bloom, highly detailed, micro details" 149 - disabled 150 - /> 151 - <TextInput 152 - name="Negative query" 153 - max={300} 154 - defaultState="(poorly drawn face)++, (dark skin, tan skin)++, (character out of frame)1.3, bad quality, low-res, (monochrome)1.1, (grayscale)1.3, acne, skin blemishes, bad anatomy, text, error, missing fingers, extra limbs, missing limbs, cut off<" 155 - disabled 156 - /> 83 + <div className="flex gap-2 lg:mt-0"> 84 + <ServerButton 85 + as={Link} 86 + startContent={<HiUserAdd />} 87 + className="w-1/2 lg:w-fit !text-xl !font-medium" 88 + color="secondary" 89 + href="/login?invite=true" 90 + size="lg" 91 + > 92 + <span className="block sm:hidden">Invite</span> 93 + <span className="hidden sm:block">Invite Wamellow</span> 94 + </ServerButton> 95 + <ServerButton 96 + as={Link} 97 + startContent={<BsDiscord />} 98 + className="w-1/2 lg:w-fit !text-xl !font-medium" 99 + href="/support" 100 + size="lg" 101 + > 102 + <span className="block sm:hidden">Support</span> 103 + <span className="hidden sm:block">Join support</span> 104 + </ServerButton> 157 105 </div> 158 106 </div> 159 107 160 108 <article itemScope itemType="http://schema.org/Article" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-10 w-full"> 161 - 162 - {models.map((item, i) => ( 163 - <div key={item.title + i} className="gap-10 items-center w-full relative max-w-lg hover:scale-105 duration-200"> 164 - <Image 165 - alt="" 166 - className="rounded-xl shadow-md w-full mt-2" 167 - height={512} 168 - itemProp="image" 169 - loading="lazy" 170 - src={item.url} 171 - width={512} 172 - /> 173 - <div className="absolute bottom-0 left-0 w-full"> 174 - <div className="bg-wamellow backdrop-blur-md backdrop-brightness-50 m-2 p-2 rounded-xl"> 175 - <h3 className={styles.h3}>{item.title}</h3> 176 - </div> 177 - </div> 178 - </div> 179 - ))} 109 + {Array.isArray(uploads) ? 110 + uploads 111 + .map((item, i) => ( 112 + <Link 113 + key={item.id + i} 114 + className="gap-10 items-center w-full relative max-w-lg hover:scale-105 transition-transform duration-300 ease-in-out" 115 + href={`/ai-gallary/${item.id}`} 116 + > 117 + <Image 118 + alt="" 119 + className="rounded-xl shadow-md w-full mt-2" 120 + height={512} 121 + itemProp="image" 122 + loading="lazy" 123 + src={`https://r2.wamellow.com/ai-image/${item.id}.webp`} 124 + width={512} 125 + /> 126 + <div className="absolute bottom-0 left-0 w-full z-20"> 127 + <div className="bg-wamellow backdrop-blur-xl backdrop-brightness-[.25] m-2 p-2 rounded-xl space-y-2"> 128 + <Chip 129 + color="secondary" 130 + variant="flat" 131 + > 132 + /image 133 + </Chip> 134 + <h3 className={styles.h3}>{item.model}</h3> 135 + </div> 136 + </div> 137 + </Link> 138 + )) 139 + : 140 + <Notice message={uploads.message || "Something went wrong..."} /> 141 + } 180 142 </article> 181 143 182 - <div className="w-full mt-6 md:flex gap-4 items-center"> 183 - <div className="flex gap-4 items-center"> 184 - <span className="flex items-center gap-2"> 185 - <ImageReduceMotion url="/luna" size={64} alt="mwlica's profile picture" className="w-12 h-12 rounded-full" /> 186 - <div> 187 - <div className="flex items-center gap-2"> 188 - <span className="text-xl font-medium dark:text-neutral-200 text-neutral-800">@mwlica</span> <Badge text="Developer" /> 189 - </div> 190 - <span className="dark:text-neutral-300 text-neutral-700">Cute femboy</span> 191 - </div> 192 - </span> 193 - <HiChevronRight className="w-8 h-8" /> 194 - </div> 195 - <span className={`${handwritten.className} text-2xl break-words block mt-2`}>„WHY IS THERE A TRAIN LMFAO“</span> 196 - </div> 144 + <Image 145 + alt="/image command usage" 146 + className="w-full rounded-md shadow-md" 147 + height={438} 148 + src={CommandPic} 149 + width={1723} 150 + /> 197 151 198 - </div> 152 + </div > 199 153 ); 200 154 }
+144
app/ai-gallary/[uploadId]/layout.tsx
··· 1 + import { Chip } from "@nextui-org/react"; 2 + import { Metadata } from "next"; 3 + import NextImage from "next/image"; 4 + import Link from "next/link"; 5 + import { HiHome, HiPlus } from "react-icons/hi"; 6 + 7 + import Footer from "@/components/footer"; 8 + import Notice from "@/components/notice"; 9 + import { ScreenMessage, SupportButton } from "@/components/screen-message"; 10 + import { ServerButton } from "@/components/server-button"; 11 + import { getGuild } from "@/lib/api"; 12 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 13 + import { getBaseUrl, getCanonicalUrl } from "@/utils/urls"; 14 + 15 + import { getUpload, getUploads } from "../api"; 16 + import Side from "./side.component"; 17 + 18 + export interface Props { 19 + params: { uploadId: string }; 20 + children: React.ReactNode; 21 + } 22 + 23 + export const revalidate = 60 * 60; 24 + 25 + export const generateMetadata = async ({ 26 + params 27 + }: Props): Promise<Metadata> => { 28 + const upload = await getUpload(params.uploadId); 29 + 30 + const title = "Free /image Ai for Discord"; 31 + const description = `View an amazing AI generated image ${"model" in upload ? `using the ${upload.model}` : ""} created with our versatile /image command. ${"prompt" in upload ? `Using the prompt: ${upload.prompt}` : ""}`.replace(" ", " "); 32 + const images = "id" in upload ? `https://r2.wamellow.com/ai-image/${upload.id}.webp` : `${getBaseUrl()}/waya-v3.jpg`; 33 + const url = getCanonicalUrl("ai-gallary", params.uploadId); 34 + 35 + return { 36 + title, 37 + description, 38 + alternates: { 39 + canonical: url 40 + }, 41 + openGraph: { 42 + title, 43 + description, 44 + type: "website", 45 + url, 46 + images 47 + }, 48 + twitter: { 49 + card: "summary", 50 + site: "wamellow.com", 51 + title, 52 + description, 53 + images 54 + } 55 + }; 56 + }; 57 + 58 + export default async function RootLayout({ 59 + params, 60 + children 61 + }: Props) { 62 + const upload = await getUpload(params.uploadId); 63 + const guild = "guildId" in upload ? await getGuild(upload.guildId) : undefined; 64 + const uploads = "model" in upload ? await getUploads({ model: upload.model }) : undefined; 65 + 66 + return ( 67 + <div className="w-full space-y-12"> 68 + 69 + <div className="md:flex"> 70 + 71 + <div className="md:w-3/4 md:mr-6"> 72 + {"id" in upload ? 73 + children 74 + : 75 + <ScreenMessage 76 + top="4rem" 77 + title="Something went wrong!" 78 + description={upload.message} 79 + buttons={<> 80 + <ServerButton 81 + as={Link} 82 + href="/" 83 + startContent={<HiHome />} 84 + > 85 + Go back to Home 86 + </ServerButton> 87 + <SupportButton /> 88 + </>} 89 + > 90 + <NextImage src={SadWumpusPic} alt="" height={141} width={124} /> 91 + </ScreenMessage> 92 + } 93 + </div> 94 + 95 + <div className="md:w-1/4 mt-8 md:mt-0"> 96 + <Side 97 + upload={upload} 98 + guild={guild} 99 + /> 100 + </div> 101 + 102 + </div> 103 + 104 + <div> 105 + <h2 className="text-3xl font-bold mb-4 text-neutral-200">More like this /image</h2> 106 + <span className="relative bottom-4 mb-4"> 107 + Images that were also generated with the <Chip>{"model" in upload ? upload.model : "..."}</Chip> model. 108 + </span> 109 + {Array.isArray(uploads) ? 110 + <div className="flex flex-wrap gap-4"> 111 + {uploads 112 + .map((upload) => ( 113 + <Link 114 + key={upload.id} 115 + className="h-24 w-24" 116 + href={`/ai-gallary/${upload.id}`} 117 + > 118 + <NextImage 119 + alt={upload.prompt} 120 + className="rounded-lg" 121 + height={128} 122 + src={`https://r2.wamellow.com/ai-image/${upload.id}.webp`} 123 + width={128} 124 + /> 125 + </Link> 126 + ))} 127 + <Link 128 + href="/login?invite=true" 129 + target="_blank" 130 + className="h-24 w-24 border-2 dark:border-wamellow border-wamellow-100 p-4 flex justify-center items-center rounded-lg drop-shadow-md overflow-hidden relative duration-100 outline-violet-500 hover:outline" 131 + > 132 + <HiPlus /> 133 + </Link> 134 + </div> 135 + : 136 + <Notice message={uploads?.message || "Something went wrong..."} /> 137 + } 138 + </div> 139 + 140 + <Footer className="mt-10" /> 141 + 142 + </div> 143 + ); 144 + }
+48
app/ai-gallary/[uploadId]/page.tsx
··· 1 + import { Chip, Image } from "@nextui-org/react"; 2 + import NextImage from "next/image"; 3 + import { notFound } from "next/navigation"; 4 + 5 + import { getUpload } from "../api"; 6 + 7 + export interface Props { 8 + params: { uploadId: string }; 9 + } 10 + 11 + export const revalidate = 60 * 60; 12 + 13 + export default async function Home({ 14 + params 15 + }: Props) { 16 + const upload = await getUpload(params.uploadId); 17 + if (!upload || "statusCode" in upload) return notFound(); 18 + 19 + return ( 20 + <div className="relative"> 21 + <Image 22 + alt={upload.prompt} 23 + as={NextImage} 24 + className="rounded-lg" 25 + height={1024} 26 + isBlurred 27 + isZoomed 28 + src={`https://r2.wamellow.com/ai-image/${upload.id}.webp`} 29 + width={1024} 30 + /> 31 + 32 + <div className="relative md:absolute px-2 md:p-4 bottom-8 md:bottom-0 left-0 z-10 -mb-466md:mb-0"> 33 + <div className="bg-wamellow backdrop-blur-xl backdrop-brightness-50 rounded-lg overflow-hidden shadow-lg p-4"> 34 + <Chip 35 + color="secondary" 36 + className="mb-2" 37 + variant="flat" 38 + size="lg" 39 + > 40 + {upload.model} 41 + </Chip> 42 + <div className="text-xl font-medium text-neutral-200">{upload.prompt}</div> 43 + <div className="text-medium">{upload.negativePrompt}</div> 44 + </div> 45 + </div> 46 + </div> 47 + ); 48 + }
+181
app/ai-gallary/[uploadId]/side.component.tsx
··· 1 + "use client"; 2 + 3 + import { Accordion, AccordionItem, Button, Chip, Tooltip } from "@nextui-org/react"; 4 + import Link from "next/link"; 5 + import { FaReddit, FaTwitter } from "react-icons/fa"; 6 + import { HiHand, HiShare, HiUserGroup } from "react-icons/hi"; 7 + 8 + import { webStore } from "@/common/webstore"; 9 + import Ad from "@/components/ad"; 10 + import { CopyToClipboardButton } from "@/components/copy-to-clipboard"; 11 + import ImageReduceMotion from "@/components/image-reduce-motion"; 12 + import { formatDate } from "@/components/time"; 13 + import { ApiError, ApiV1GuildsGetResponse } from "@/typings"; 14 + import { getCanonicalUrl } from "@/utils/urls"; 15 + 16 + import { ExtendedUpload } from "../api"; 17 + 18 + export default function Side({ 19 + upload, 20 + guild 21 + }: { 22 + upload: ExtendedUpload | ApiError; 23 + guild: ApiV1GuildsGetResponse | ApiError | undefined; 24 + }) { 25 + const web = webStore((w) => w); 26 + 27 + return ( 28 + <div className="flex flex-col gap-3"> 29 + 30 + {"id" in upload && 31 + <div className="flex gap-2 w-full"> 32 + <CopyToClipboardButton 33 + className="w-full !justify-start" 34 + title="Share this page" 35 + text={getCanonicalUrl("ai-gallary", upload.id as string)} 36 + icon={<HiShare />} 37 + /> 38 + <Tooltip content="Share on Reddit" delay={0} closeDelay={0} showArrow> 39 + <Button 40 + as={Link} 41 + href={`https://reddit.com/submit?title=${encodeURIComponent(`Created an /image with a ${upload.model} AI model on wamellow.com`)}&text=${`Hey! I created this AI /image with the ${upload.model} model and the prompt: "${encodeURIComponent(upload.prompt)}" ${encodeURIComponent("\n\n")}${getCanonicalUrl("ai-gallary", upload.id as string)}`}`} 42 + target="_blank" 43 + isIconOnly 44 + > 45 + <FaReddit /> 46 + </Button> 47 + </Tooltip> 48 + <Tooltip content="Share on Twitter/X" delay={0} closeDelay={0} showArrow> 49 + <Button 50 + as={Link} 51 + href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(`Created an #ai /image with a ${upload.model} AI model on on wamellow.com! The query is "${upload.prompt}"`)}&url=${encodeURIComponent(getCanonicalUrl("ai-gallary", upload.id as string))}&hashtags=${encodeURIComponent("wamellow,discord")}`} 52 + target="_blank" 53 + isIconOnly 54 + > 55 + <FaTwitter /> 56 + </Button> 57 + </Tooltip> 58 + </div> 59 + } 60 + 61 + <Ad 62 + title="/image AI for free" 63 + description="Create your own amazing AI /image-s with Wamellow for free in your Discord Server using the best SDXL models and 40+ more!" 64 + /> 65 + 66 + <Accordion 67 + variant="shadow" 68 + className="bg-wamellow" 69 + selectionMode="multiple" 70 + defaultExpandedKeys={["1"]} 71 + disableAnimation={web.reduceMotions} 72 + > 73 + <AccordionItem 74 + key="1" 75 + aria-label="about" 76 + title="About" 77 + classNames={{ content: "mb-2 space-y-4" }} 78 + > 79 + 80 + <div className="flex items-center justify-between"> 81 + <span>Created</span> 82 + <Chip 83 + className="select-none" 84 + radius="sm" 85 + > 86 + {"createdAt" in upload ? 87 + formatDate(upload.createdAt as string, "en-US") 88 + : 89 + "unknown" 90 + } 91 + </Chip> 92 + </div> 93 + 94 + {"author" in upload && 95 + <div className="flex items-center justify-between"> 96 + <span>Author</span> 97 + <Chip 98 + className="flex select-none" 99 + startContent={ 100 + <ImageReduceMotion 101 + className="rounded-full" 102 + alt="uploader's avatar" 103 + url={"https://cdn.discordapp.com/avatars/" + upload.authorId + "/" + upload.author.avatar} 104 + size={16} 105 + /> 106 + } 107 + > 108 + @{upload.author.username} 109 + </Chip> 110 + </div> 111 + } 112 + 113 + <div className="flex items-center justify-between"> 114 + <span>Rating</span> 115 + <Chip 116 + className="font-bold select-none" 117 + color={"nsfw" in upload && upload.nsfw ? "danger" : "default"} 118 + radius="sm" 119 + startContent={ 120 + <span className="mx-1"> 121 + {"nsfw" in upload && upload.nsfw ? <HiHand /> : <HiUserGroup />} 122 + </span> 123 + } 124 + > 125 + {"nsfw" in upload && upload.nsfw ? "Mature" : "Everyone"} 126 + </Chip> 127 + </div> 128 + 129 + </AccordionItem> 130 + </Accordion> 131 + 132 + <Accordion 133 + variant="shadow" 134 + className="bg-wamellow" 135 + selectionMode="multiple" 136 + disableAnimation={web.reduceMotions} 137 + > 138 + <AccordionItem 139 + key="1" 140 + aria-label="command" 141 + title="Command" 142 + classNames={{ content: "mb-2 space-y-4" }} 143 + subtitle="Copy it and run it in Discord" 144 + > 145 + {"prompt" in upload && 146 + <span className="select-all"> 147 + /image query: {upload?.prompt} {upload?.negativePrompt && `negative-query: ${upload?.negativePrompt}`} model: {upload?.model} 148 + </span> 149 + } 150 + </AccordionItem> 151 + </Accordion> 152 + 153 + {guild && "id" in guild && 154 + <Link 155 + href={getCanonicalUrl("leaderboard", guild.id, "?utm_source=wamellow.com&utm_medium=ai-gallary")} 156 + target="_blank" 157 + className="flex gap-3 items-center border-2 border-wamellow p-3 rounded-lg shadow-md" 158 + > 159 + <ImageReduceMotion 160 + className="rounded-full h-12 w-12" 161 + alt="Server icon" 162 + url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`} 163 + size={56} 164 + /> 165 + 166 + <div> 167 + <div className="text-lg text-neutral-200 font-semibold">{guild.name}</div> 168 + <div className="text-sm font-medium">View leaderboard</div> 169 + </div> 170 + </Link> 171 + } 172 + 173 + <div> 174 + 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. 175 + </div> 176 + 177 + 178 + </div > 179 + ); 180 + 181 + }
+42
app/ai-gallary/api.ts
··· 1 + import { ApiError } from "@/typings"; 2 + 3 + export interface Upload { 4 + id: string; 5 + guildId?: string | null; 6 + authorId: string; 7 + 8 + prompt: string; 9 + negativePrompt?: string | null; 10 + model: string; 11 + 12 + verified: boolean; 13 + nsfw: boolean; 14 + 15 + createdAt: string; 16 + } 17 + 18 + export async function getUploads(options?: { model?: string; page?: number }): Promise<Upload[] | ApiError> { 19 + const res = await fetch(`${process.env.NEXT_PUBLIC_API}/ai${options?.model ? `?model=${options.model}` : ""}${options?.page ? `?page=${options.page - 1}` : ""}`, { 20 + headers: { Authorization: process.env.API_SECRET as string }, 21 + next: { revalidate: 60 * 60 } 22 + }); 23 + 24 + return res.json(); 25 + } 26 + 27 + export interface ExtendedUpload extends Upload { 28 + author: { 29 + username: string; 30 + globalName: string; 31 + avatar: string | null; 32 + }; 33 + } 34 + 35 + export async function getUpload(uploadId: string): Promise<ExtendedUpload | ApiError> { 36 + const res = await fetch(`${process.env.NEXT_PUBLIC_API}/ai/${uploadId}`, { 37 + headers: { Authorization: process.env.API_SECRET as string }, 38 + next: { revalidate: 60 * 60 } 39 + }); 40 + 41 + return res.json(); 42 + }
+104 -46
app/profile/analytics/page.tsx
··· 1 1 "use client"; 2 2 3 - import { JSX, SVGProps, useEffect, useState } from "react"; 3 + import { useEffect, useState } from "react"; 4 4 import { HiIdentification } from "react-icons/hi"; 5 - import { Area, AreaChart, Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; 5 + import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; 6 6 7 - import Badge from "@/components/badge"; 8 7 import Box from "@/components/box"; 9 8 import { StatsBar } from "@/components/counter"; 10 9 import { ScreenMessage } from "@/components/screen-message"; ··· 90 89 number: data.map((enty) => enty.uses).reduce((prev, curr) => prev + curr), 91 90 gained: data.filter((entry) => entry.snapshot === `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, "0")}-${yesterday.getDate().toString().padStart(2, "0")}`).map((enty) => enty.uses).reduce((prev, curr) => prev + curr, 0), 92 91 append: "yesterday" 93 - }, 94 - { 95 - name: "--", 96 - number: 1, 97 - gained: 0, 98 - append: "yesterday" 99 92 } 100 93 ]} 101 94 /> ··· 117 110 118 111 <div className="flex mx-10 mt-5 "> 119 112 <span className="text-sm">{options.name}</span> 120 - <Badge text="Lorem ipsum" /> 121 113 </div> 122 114 123 115 <ResponsiveContainer ··· 132 124 bottom: 5 133 125 }} 134 126 > 135 - <XAxis dataKey="snapshot" tickFormatter={(str: string) => `${str.split("-")[1]}/${str.split("-")[2]}`} style={{ fontSize: 12, fontWeight: 600 }} axisLine={false} tickLine={false} /> 136 - <YAxis style={{ fontSize: 12, fontWeight: 600 }} axisLine={false} tickLine={false} /> 137 - <Tooltip contentStyle={{ borderRadius: "8px", fontSize: 14, paddingInline: "8px", paddingBlock: "6px" }} labelStyle={{ marginBottom: ~4, color: "#000000", fontWeight: 400 }} itemStyle={{ marginBottom: ~4 }} labelFormatter={(label: string) => `${convertMonthToName(new Date(label).getMonth())} ${label.split("-")[2]}`} /> 138 - <CartesianGrid strokeDasharray="3 3" opacity={0.1} vertical={false} /> 139 - <Area type="monotone" dataKey={options.dataKey} strokeWidth={2} stroke="rgb(124 58 237)" fill="rgb(139 92 246)" style={{ opacity: 0.9 }} /> 127 + <XAxis 128 + axisLine={false} 129 + dataKey="snapshot" 130 + tick={<CustomXAxisTick />} 131 + tickFormatter={(str: string) => `${str.split("-")[1]}/${str.split("-")[2]}`} 132 + /> 133 + <YAxis 134 + style={{ fontSize: 12, fontWeight: 600 }} 135 + axisLine={false} 136 + tickLine={false} 137 + /> 138 + <Tooltip 139 + contentStyle={{ borderRadius: "8px", fontSize: 14, paddingInline: "8px", paddingBlock: "6px" }} 140 + labelStyle={{ marginBottom: ~4, color: "#000000", fontWeight: 400 }} 141 + itemStyle={{ marginBottom: ~4 }} 142 + labelFormatter={(label: string) => `${convertMonthToName(new Date(label).getMonth())} ${label.split("-")[2]}`} 143 + /> 144 + <CartesianGrid 145 + strokeDasharray="3 3" 146 + opacity={0.1} 147 + vertical={false} 148 + /> 149 + <Area 150 + type="monotone" 151 + dataKey={options.dataKey} 152 + strokeWidth={2} 153 + stroke="rgb(124 58 237)" 154 + fill="rgb(139 92 246)" 155 + style={{ opacity: 0.9 }} 156 + /> 140 157 </AreaChart> 141 158 </ResponsiveContainer> 142 159 ··· 145 162 } 146 163 147 164 function ChartBar(options: { name: string; data: NekosticResponse[]; dataKey: keyof CalcNames }) { 165 + const data = calcNameOccurrences(options.data) 166 + .filter((entry) => 167 + entry.name !== "eval" && 168 + entry.name !== "shell" && 169 + entry.name !== "deploycommands" && 170 + entry.name !== "emotes" && 171 + entry.name !== "giveaway" 172 + ); 173 + 148 174 return ( 149 175 <Box none className="dark:bg-wamellow bg-wamellow-100 w-full rounded-md"> 150 176 151 177 <div className="flex mx-10 mt-5 "> 152 178 <span className="text-sm">{options.name}</span> 153 - <Badge text="Lorem ipsum" /> 154 179 </div> 155 180 156 181 <ResponsiveContainer ··· 158 183 height={300} 159 184 > 160 185 <BarChart 161 - data={calcNameOccurrences(options.data)} 186 + data={data} 162 187 margin={{ 163 188 top: 25, 164 189 right: 40, 165 190 bottom: 5 166 191 }} 167 192 > 168 - <XAxis dataKey="name" style={{ fontSize: 12, fontWeight: 600 }} axisLine={false} tickLine={false} /> 169 - <YAxis style={{ fontSize: 12, fontWeight: 600 }} axisLine={false} tickLine={false} /> 170 - <Tooltip cursor={false} contentStyle={{ borderRadius: "8px", fontSize: 14, paddingInline: "8px", paddingBlock: "6px" }} labelStyle={{ marginBottom: ~4, color: "#000000", fontWeight: 400 }} itemStyle={{ marginBottom: ~4 }} /> 171 - <CartesianGrid strokeDasharray="3 3" opacity={0.1} vertical={false} /> 172 - <Bar dataKey={options.dataKey} shape={<RoundedBar />} fill="rgb(86, 61, 146)" style={{ borderRadius: 10, overflow: "hidden" }} /> 193 + <XAxis 194 + axisLine={false} 195 + dataKey="name" 196 + tick={<CustomXAxisTick />} 197 + /> 198 + <YAxis 199 + style={{ fontSize: 12, fontWeight: 600 }} 200 + axisLine={false} 201 + tickLine={false} 202 + /> 203 + <Tooltip 204 + contentStyle={{ borderRadius: "8px", fontSize: 14, paddingInline: "8px", paddingBlock: "6px" }} 205 + labelStyle={{ marginBottom: ~4, color: "#000000", fontWeight: 400 }} 206 + itemStyle={{ marginBottom: ~4 }} 207 + cursor={false} 208 + /> 209 + <CartesianGrid 210 + strokeDasharray="3 3" 211 + opacity={0.1} 212 + vertical={false} 213 + /> 214 + <Bar 215 + dataKey={options.dataKey} 216 + fill="rgb(86, 61, 146)" 217 + radius={[5, 5, 0, 0]} 218 + > 219 + {data.map((_, index) => 220 + <Cell 221 + key={"cell-" + index} 222 + /> 223 + )} 224 + </Bar> 173 225 </BarChart> 174 226 </ResponsiveContainer> 175 227 ··· 177 229 ); 178 230 } 179 231 180 - function getPath(x: number, y: number, width: number, height: number) { 181 - const x2 = x + width; 182 - const y2 = y + height; 232 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 233 + function CustomXAxisTick(props: any) { 234 + const { x, y, payload } = props; 183 235 184 - const cornerRadius = 8; 185 - 186 - return ` 187 - M${x},${y + cornerRadius} 188 - A${cornerRadius},${cornerRadius} 0 0 1 ${x + cornerRadius},${y} 189 - L${x2 - cornerRadius},${y} 190 - A${cornerRadius},${cornerRadius} 0 0 1 ${x2},${y + cornerRadius} 191 - L${x2},${y2} 192 - L${x},${y2} 193 - L${x},${y + cornerRadius} 194 - Z 195 - `; 196 - } 197 - 198 - function RoundedBar(props: JSX.IntrinsicAttributes & SVGProps<SVGPathElement>) { 199 - return <path d={getPath(props.x as number, props.y as number, props.width as number, props.height as number)} {...props} />; 236 + return ( 237 + <g transform={`translate(${x},${y})`}> 238 + <text 239 + x={0} 240 + y={0} 241 + dy={16} 242 + textAnchor="end" 243 + fill="#666" 244 + transform="rotate(-35)" 245 + className="text-[11px] font-semibold" 246 + > 247 + {payload.value} 248 + </text> 249 + </g> 250 + ); 200 251 } 201 252 202 253 interface SnapshotData { ··· 218 269 snapshotData[snapshot].users += users; 219 270 } 220 271 221 - return Object.entries(snapshotData).map(([snapshot, data]) => ({ 222 - snapshot, 223 - ...data 224 - })); 272 + return Object 273 + .entries(snapshotData) 274 + .map(([snapshot, data]) => ({ 275 + snapshot, 276 + ...data 277 + })); 225 278 } 226 279 227 280 function calcNameOccurrences(data: NekosticResponse[]): CalcNames[] { ··· 232 285 else nameOccurrences[item.name] = item.uses; 233 286 } 234 287 235 - return Object.entries(nameOccurrences).map(([name, count]) => ({ name, count })); 288 + return Object 289 + .entries(nameOccurrences) 290 + .map(([name, count]) => ({ 291 + name, 292 + count 293 + })); 236 294 }
+21
components/time.tsx
··· 1 + export function formatDate(dateString: string, userLanguage: string): string { 2 + const date = new Date(dateString); 3 + const today = new Date(); 4 + const yesterday = new Date(today); 5 + yesterday.setDate(today.getDate() - 1); 6 + 7 + const options: Intl.DateTimeFormatOptions = { 8 + hour: "numeric", 9 + minute: "numeric" 10 + }; 11 + 12 + const dateFormatter = new Intl.DateTimeFormat(userLanguage, options); 13 + 14 + if (date.toDateString() === today.toDateString()) { 15 + return `Today at ${dateFormatter.format(date)}`; 16 + } else if (date.toDateString() === yesterday.toDateString()) { 17 + return `Yesterday at ${dateFormatter.format(date)}`; 18 + } else { 19 + return date.toLocaleDateString(userLanguage); 20 + } 21 + }
+6
next.config.js
··· 29 29 hostname: "the-net.loves-genshin.lol", 30 30 port: "", 31 31 pathname: "/images/ai/**" 32 + }, 33 + { 34 + protocol: "https", 35 + hostname: "r2.wamellow.com", 36 + port: "", 37 + pathname: "/ai-image/**" 32 38 } 33 39 ] 34 40 }
public/image-command.webp

This is a binary file and will not be displayed.

+5 -5
typings.ts
··· 1 1 import { actor } from "./utils/tts"; 2 2 3 + export interface ApiError { 4 + statusCode: number; 5 + message: string; 6 + } 7 + 3 8 export interface ApiV1TopguildsGetResponse { 4 9 id: string; 5 10 name: string; ··· 335 340 336 341 whitelistChannelIds: string[]; 337 342 whitelistRoleIds: string[]; 338 - } 339 - 340 - export interface ApiV1AiResponse { 341 - title: string; 342 - url: string; 343 343 } 344 344 345 345 export interface PronounsResponse {