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

migrate /dashboard -> /profile & update queries

+420 -481
+31 -23
app/dashboard/[guildId]/custom-commands/page.tsx
··· 1 1 "use client"; 2 2 3 3 import { Button, Chip, Tooltip } from "@nextui-org/react"; 4 + import Image from "next/image"; 4 5 import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation"; 5 6 import { useCallback, useEffect } from "react"; 6 7 import { HiViewGridAdd } from "react-icons/hi"; ··· 14 15 import { ScreenMessage } from "@/components/screen-message"; 15 16 import { cacheOptions, getData } from "@/lib/api"; 16 17 import { Permissions } from "@/lib/discord"; 18 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 17 19 import { ApiV1GuildsModulesTagsGetResponse } from "@/typings"; 18 20 19 21 import CreateTag, { Style } from "./create.component"; ··· 28 30 const queryClient = useQueryClient(); 29 31 30 32 const url = `/guilds/${params.guildId}/modules/tags` as const; 31 - const key = ["guilds", params.guildId, "modules", "custom-commands"] as const; 32 33 33 34 const { data, isLoading, error } = useQuery( 34 - key, 35 + url, 35 36 () => getData<ApiV1GuildsModulesTagsGetResponse[]>(url), 36 37 { 37 38 enabled: !!params.guildId, ··· 40 41 ); 41 42 42 43 const tagId = search.get("id"); 43 - const tag = data?.find((t) => t.tagId === tagId); 44 + const tag = (Array.isArray(data) ? data : []).find((t) => t.tagId === tagId); 44 45 45 46 const createQueryString = useCallback((name: string, value: string) => { 46 47 const params = new URLSearchParams(search); ··· 49 50 return params.toString(); 50 51 }, [search]); 51 52 53 + useEffect(() => { 54 + if (!Array.isArray(data)) return; 55 + if (data && !tag && data[0]) setTagId(data[0].tagId); 56 + }, [data]); 57 + 58 + if (error || (data && "message" in data)) { 59 + return ( 60 + <ScreenMessage 61 + top="0rem" 62 + title="Something went wrong on this page.." 63 + description={ 64 + (data && "message" in data ? data.message : `${error}`) 65 + || "An unknown error occurred."} 66 + href={`/dashboard/${guild?.id}`} 67 + button="Go back to overview" 68 + icon={<HiViewGridAdd />} 69 + > 70 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 71 + </ScreenMessage> 72 + ); 73 + } 74 + 75 + if (isLoading || !data) return <></>; 76 + 52 77 const setTagId = (id: string) => { 53 78 router.push(pathname + "?" + createQueryString("id", id)); 54 79 }; ··· 56 81 const editTag = <T extends keyof ApiV1GuildsModulesTagsGetResponse>(k: keyof ApiV1GuildsModulesTagsGetResponse, value: ApiV1GuildsModulesTagsGetResponse[T]) => { 57 82 if (!tag) return; 58 83 59 - queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(key, () => [ 84 + queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () => [ 60 85 ...(data?.filter((t) => t.tagId !== tag.tagId) || []), 61 86 { ...tag, [k]: value } 62 87 ]); 63 88 }; 64 89 65 90 const addTag = (tag: ApiV1GuildsModulesTagsGetResponse) => { 66 - queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(key, () => [ 91 + queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () => [ 67 92 ...(data || []), 68 93 tag 69 94 ]); 70 95 }; 71 96 72 97 const removeTag = (id: string) => { 73 - queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(key, () => 98 + queryClient.setQueryData<ApiV1GuildsModulesTagsGetResponse[]>(url, () => 74 99 data?.filter((t) => t.tagId !== id) || [] 75 100 ); 76 101 }; 77 - 78 - useEffect(() => { 79 - if (data && !tag && data[0]) setTagId(data[0].tagId); 80 - }, [data?.length]); 81 - 82 - if (!data || isLoading) return <></>; 83 - if (error) { 84 - return ( 85 - <ScreenMessage 86 - title="Something went wrong.." 87 - description={error.toString() || "We couldn't load the data for this page."} 88 - href={`/dashboard/${guild?.id}`} 89 - button="Go back to overview" 90 - icon={<HiViewGridAdd />} 91 - /> 92 - ); 93 - } 94 102 95 103 return ( 96 104 <>
+2 -2
app/dashboard/[guildId]/layout.tsx
··· 167 167 <Button 168 168 as={Link} 169 169 className="w-fit" 170 - href="/dashboard" 170 + href="/profile" 171 171 startContent={<HiArrowNarrowLeft />} 172 172 > 173 173 Serverlist ··· 259 259 buttons={<> 260 260 <ServerButton 261 261 as={Link} 262 - href="/dashboard" 262 + href="/profile" 263 263 startContent={<HiViewGridAdd />} 264 264 > 265 265 Go back to Dashboard
+21 -13
app/dashboard/[guildId]/leaderboards/page.tsx
··· 1 1 "use client"; 2 + import Image from "next/image"; 2 3 import { useParams } from "next/navigation"; 3 - import { useState } from "react"; 4 + import { useCookies } from "next-client-cookies"; 4 5 import { HiChartBar, HiViewGridAdd } from "react-icons/hi"; 5 6 import { useQuery } from "react-query"; 6 7 7 8 import { Guild, guildStore } from "@/common/guilds"; 8 - import { webStore } from "@/common/webstore"; 9 9 import Betweener from "@/components/Betweener"; 10 10 import ImageUrlInput from "@/components/inputs/ImageUrlInput"; 11 11 import MultiSelectMenu from "@/components/inputs/MultiSelectMenu"; 12 12 import TextInput from "@/components/inputs/TextInput"; 13 13 import { ScreenMessage } from "@/components/screen-message"; 14 - import { getData } from "@/lib/api"; 14 + import { cacheOptions, getData } from "@/lib/api"; 15 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 15 16 import { ApiV1GuildsModulesLeaderboardGetResponse } from "@/typings"; 16 17 17 18 import OverviewLinkComponent from "../../../../components/OverviewLinkComponent"; 18 19 import UpdatingLeaderboardCard from "./updating.component"; 19 20 20 21 export default function Home() { 22 + const cookies = useCookies(); 23 + 21 24 const guild = guildStore((g) => g); 22 25 const web = webStore((w) => w); 23 26 const params = useParams(); 24 27 25 28 const url = `/guilds/${params.guildId}/modules/leaderboard` as const; 26 29 27 - const [data, setData] = useState<ApiV1GuildsModulesLeaderboardGetResponse | null>(null); 28 - 29 - const { isLoading, error } = useQuery( 30 - ["guilds", params.guildId, "modules", "leaderboard"], 30 + const { data, isLoading, error } = useQuery( 31 + url, 31 32 () => getData<ApiV1GuildsModulesLeaderboardGetResponse>(url), 32 33 { 33 34 enabled: !!params.guildId, 34 - onSuccess: (d) => setData(d) 35 + ...cacheOptions, 36 + refetchOnMount: true 35 37 } 36 38 ); 37 39 38 - if (!data || isLoading) return <></>; 39 - if (error) { 40 + if (error || (data && "message" in data)) { 40 41 return ( 41 42 <ScreenMessage 42 - title="Something went wrong.." 43 - description={error.toString() || "We couldn't load the data for this page."} 43 + top="0rem" 44 + title="Something went wrong on this page.." 45 + description={ 46 + (data && "message" in data ? data.message : `${error}`) 47 + || "An unknown error occurred."} 44 48 href={`/dashboard/${guild?.id}`} 45 49 button="Go back to overview" 46 50 icon={<HiViewGridAdd />} 47 - /> 51 + > 52 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 53 + </ScreenMessage> 48 54 ); 49 55 } 56 + 57 + if (isLoading || !data) return <></>; 50 58 51 59 return ( 52 60 <div>
+20 -10
app/dashboard/[guildId]/nsfw-image-scanning/page.tsx
··· 1 1 "use client"; 2 2 3 3 import { Code } from "@nextui-org/react"; 4 + import Image from "next/image"; 4 5 import { useParams } from "next/navigation"; 5 6 import { useState } from "react"; 6 7 import { HiViewGridAdd } from "react-icons/hi"; ··· 12 13 import Switch from "@/components/inputs/Switch"; 13 14 import Notice, { NoticeType } from "@/components/notice"; 14 15 import { ScreenMessage } from "@/components/screen-message"; 15 - import { getData } from "@/lib/api"; 16 - import { ApiV1GuildsModulesNsfwModerationGetResponse } from "@/typings"; 16 + import { cacheOptions, getData } from "@/lib/api"; 17 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 18 + import { ApiV1GuildsModulesNsfwModerationGetResponse, RouteErrorResponse } from "@/typings"; 17 19 18 20 export default function Home() { 19 21 const guild = guildStore((g) => g); ··· 21 23 22 24 const url = `/guilds/${params.guildId}/modules/nsfw-image-scanning` as const; 23 25 24 - const [data, setData] = useState<ApiV1GuildsModulesNsfwModerationGetResponse | null>(null); 26 + const [data, setData] = useState<ApiV1GuildsModulesNsfwModerationGetResponse | RouteErrorResponse>(); 25 27 26 28 const { isLoading, error } = useQuery( 27 - ["guilds", params.guildId, "modules", "nsfw-image-scanning"], 29 + url, 28 30 () => getData<ApiV1GuildsModulesNsfwModerationGetResponse>(url), 29 31 { 30 32 enabled: !!params.guildId, 31 - onSuccess: (d) => setData(d) 33 + onSuccess: (d) => setData(d), 34 + ...cacheOptions, 35 + refetchOnMount: true 32 36 } 33 37 ); 34 38 ··· 38 42 setData(updatedLocalData); 39 43 }; 40 44 41 - if (!data || isLoading) return <></>; 42 - if (error) { 45 + if (error || (data && "message" in data)) { 43 46 return ( 44 47 <ScreenMessage 45 - title="Something went wrong.." 46 - description={error.toString() || "We couldn't load the data for this page."} 48 + top="0rem" 49 + title="Something went wrong on this page.." 50 + description={ 51 + (data && "message" in data ? data.message : `${error}`) 52 + || "An unknown error occurred."} 47 53 href={`/dashboard/${guild?.id}`} 48 54 button="Go back to overview" 49 55 icon={<HiViewGridAdd />} 50 - /> 56 + > 57 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 58 + </ScreenMessage> 51 59 ); 52 60 } 61 + 62 + if (isLoading || !data) return <></>; 53 63 54 64 return ( 55 65 <>
+15 -8
app/dashboard/[guildId]/starboard/page.tsx
··· 15 15 import Switch from "@/components/inputs/Switch"; 16 16 import { ScreenMessage } from "@/components/screen-message"; 17 17 import { getData } from "@/lib/api"; 18 - import { ApiV1GuildsModulesStarboardGetResponse } from "@/typings"; 18 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 19 + import { ApiV1GuildsModulesStarboardGetResponse, RouteErrorResponse } from "@/typings"; 19 20 20 21 export default function Home() { 21 22 const guild = guildStore((g) => g); ··· 23 24 24 25 const url = `/guilds/${params.guildId}/modules/starboard` as const; 25 26 26 - const [data, setData] = useState<ApiV1GuildsModulesStarboardGetResponse | null>(null); 27 + const [data, setData] = useState<ApiV1GuildsModulesStarboardGetResponse | RouteErrorResponse>(); 27 28 28 29 const { isLoading, error } = useQuery( 29 - ["guilds", params.guildId, "modules", "starboard"], 30 + url, 30 31 () => getData<ApiV1GuildsModulesStarboardGetResponse>(url), 31 32 { 32 33 enabled: !!params.guildId, ··· 88 89 } 89 90 }; 90 91 91 - if (!data || isLoading) return <></>; 92 - if (error) { 92 + if (error || (data && "message" in data)) { 93 93 return ( 94 94 <ScreenMessage 95 - title="Something went wrong.." 96 - description={error.toString() || "We couldn't load the data for this page."} 95 + top="0rem" 96 + title="Something went wrong on this page.." 97 + description={ 98 + (data && "message" in data ? data.message : `${error}`) 99 + || "An unknown error occurred."} 97 100 href={`/dashboard/${guild?.id}`} 98 101 button="Go back to overview" 99 102 icon={<HiViewGridAdd />} 100 - /> 103 + > 104 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 105 + </ScreenMessage> 101 106 ); 102 107 } 108 + 109 + if (isLoading || !data) return <></>; 103 110 104 111 return ( 105 112 <>
+18 -283
app/dashboard/page.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@nextui-org/react"; 4 - import { motion } from "framer-motion"; 5 - import Image from "next/image"; 6 - import Link from "next/link"; 7 - import { useSearchParams } from "next/navigation"; 8 - import { useCookies } from "next-client-cookies"; 9 - import { useEffect, useState } from "react"; 10 - import { HiRefresh, HiUserAdd, HiViewGrid, HiViewList } from "react-icons/hi"; 11 - 12 - import { userStore } from "@/common/user"; 13 - import { webStore } from "@/common/webstore"; 14 - import ImageReduceMotion from "@/components/image-reduce-motion"; 15 - import DumbTextInput from "@/components/inputs/Dumb_TextInput"; 16 - import Notice, { NoticeType } from "@/components/notice"; 17 - import { ScreenMessage } from "@/components/screen-message"; 18 - import SadWumpusPic from "@/public/sad-wumpus.gif"; 19 - import { RouteErrorResponse, UserGuild } from "@/typings"; 20 - import cn from "@/utils/cn"; 21 - 22 - const MAX_GUILDS = 24; 23 - 24 - export default function Home() { 25 - const cookies = useCookies(); 26 - if (cookies.get("hasSession") !== "true") window.location.href = "/login"; 27 - 28 - const web = webStore((w) => w); 29 - const user = userStore((s) => s); 30 - 31 - const [error, setError] = useState<string>(); 32 - const [guilds, setGuilds] = useState<UserGuild[] | undefined>(); 33 - const [search, setSearch] = useState<string>(""); 34 - const [display, setDisplay] = useState<"LIST" | "GRID">("GRID"); 35 - 36 - function filter(guild: UserGuild) { 37 - if (!search) return true; 38 - 39 - if (guild.name.toLowerCase().includes(search.toLowerCase())) return true; 40 - if (search.toLowerCase().includes(guild.name.toLowerCase())) return true; 1 + import { redirect } from "next/navigation"; 41 2 42 - if (guild.id.includes(search)) return true; 43 - if (search.includes(guild.id)) return true; 3 + export default function Home({ 4 + searchParams 5 + }: { 6 + searchParams: Record<string, string>; 7 + }) { 8 + redirect(`/profile?${objectToSearchParams(searchParams)}`); 9 + } 44 10 45 - return false; 46 - } 47 - 48 - const { length } = (guilds || []).filter(filter); 49 - 50 - useEffect(() => { 11 + function objectToSearchParams(obj: Record<string, string>): string { 12 + if (!Object.keys(obj).length) return ""; 51 13 52 - setDisplay((localStorage.getItem("dashboardServerSelectStyle") || "GRID") as "LIST" | "GRID"); 14 + const params = new URLSearchParams(); 53 15 54 - fetch(`${process.env.NEXT_PUBLIC_API}/guilds/@me`, { 55 - credentials: "include" 56 - }) 57 - .then(async (res) => { 58 - const response = await res.json() as UserGuild[]; 59 - if (!response) return; 16 + Object.keys(obj).forEach((key) => { 17 + const value = obj[key]; 18 + if (value !== null && value !== undefined) { 19 + params.append(key, value.toString()); 20 + } 21 + }); 60 22 61 - switch (res.status) { 62 - case 200: { 63 - setGuilds(response); 64 - break; 65 - } 66 - default: { 67 - setError((response as unknown as RouteErrorResponse).message || "Error while fetching guilds"); 68 - break; 69 - } 70 - } 71 - 72 - }) 73 - .catch(() => { 74 - setError("Error while fetching guilds"); 75 - }); 76 - 77 - }, []); 78 - 79 - useEffect(() => { 80 - localStorage.setItem("dashboardServerSelectStyle", display); 81 - }, [display]); 82 - 83 - return ( 84 - <div className="flex flex-col w-full"> 85 - <title>Dashboard</title> 86 - 87 - {error && 88 - <Notice 89 - type={NoticeType.Error} 90 - message={error} 91 - /> 92 - } 93 - 94 - <div className="md:flex md:items-center"> 95 - <div> 96 - <div className="text-2xl dark:text-neutral-100 text-neutral-900 font-semibold mb-2">👋 Heyia, {user?.globalName || `@${user?.username}`}</div> 97 - <div className="text-lg font-medium">Select a server you want to manage.</div> 98 - </div> 99 - 100 - <div className="md:hidden mt-3"> 101 - <DumbTextInput 102 - value={search} 103 - setValue={setSearch} 104 - placeholder="Search" 105 - thin 106 - /> 107 - </div> 108 - 109 - <div className="md:ml-auto flex gap-3 mt-4 md:mt-0"> 110 - <div className="hidden md:block"> 111 - <DumbTextInput 112 - value={search} 113 - setValue={setSearch} 114 - placeholder="Search" 115 - thin 116 - /> 117 - </div> 118 - <Button 119 - as={Link} 120 - className="w-1/2 md:w-min" 121 - href="/login?invite=true" 122 - prefetch={false} 123 - startContent={<HiUserAdd />} 124 - > 125 - Add to Server 126 - </Button> 127 - <Button 128 - as={Link} 129 - className="button-primary w-1/2 md:w-min" 130 - href="/login" 131 - prefetch={false} 132 - startContent={<HiRefresh />} 133 - > 134 - Reload 135 - </Button> 136 - </div> 137 - </div> 138 - 139 - <div className="flex gap-3"> 140 - <hr className="mx-0 p-1 my-4 dark:border-wamellow-light border-wamellow-100-light w-full" /> 141 - 142 - <div className="dark:bg-wamellow bg-wamellow-100 md:flex gap-1 dark:text-neutral-400 text-neutral-600 rounded-md overflow-hidden w-[72px] mb-5 hidden"> 143 - <button 144 - onClick={() => setDisplay("GRID")} 145 - className={cn("h-7 w-8 flex items-center justify-center p-[4px] rounded-md", display === "GRID" && "dark:bg-wamellow bg-wamellow-100")} 146 - > 147 - <HiViewGrid /> 148 - </button> 149 - <button 150 - onClick={() => setDisplay("LIST")} 151 - className={cn("h-7 w-8 flex items-center justify-center p-[4px] rounded-md", display === "LIST" && "dark:bg-wamellow bg-wamellow-100")} 152 - > 153 - <HiViewList /> 154 - </button> 155 - </div> 156 - </div> 157 - 158 - {Array.isArray(guilds) ? 159 - <motion.ul 160 - variants={{ 161 - hidden: { opacity: 1, scale: 0 }, 162 - visible: { 163 - opacity: 1, 164 - scale: 1, 165 - transition: { 166 - delayChildren: guilds.length > 20 ? 0.2 : 0.3, 167 - staggerChildren: guilds.length > 20 ? 0.1 : 0.2 168 - } 169 - } 170 - }} 171 - initial={web.reduceMotions ? "visible" : "hidden"} 172 - animate="visible" 173 - className={cn("grid grid-cols-1 gap-4 w-full", display === "GRID" && "lg:grid-cols-3 md:grid-cols-2")} 174 - > 175 - 176 - {guilds 177 - .filter(filter) 178 - .slice(0, MAX_GUILDS) 179 - .map((guild) => ( 180 - <motion.li 181 - key={"guildGrid" + guild.id} 182 - variants={{ 183 - hidden: { y: 20, opacity: 0 }, 184 - visible: { 185 - y: 0, 186 - opacity: 1, 187 - transition: { 188 - type: "spring", 189 - bounce: guilds.length > 20 ? 0.2 : 0.4, 190 - duration: guilds.length > 20 ? 0.35 : 0.7 191 - } 192 - } 193 - }} 194 - className="dark:bg-wamellow bg-wamellow-100 p-4 flex items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline group/card" 195 - > 196 - <ImageReduceMotion 197 - alt="" 198 - className="absolute top-[-48px] left-0 w-full z-0 blur-xl opacity-30 pointer-events-none" 199 - size={24} 200 - url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`} 201 - forceStatic={true} 202 - /> 203 - 204 - <ImageReduceMotion 205 - alt={`Server icon of @${guild.name}`} 206 - className="rounded-lg h-14 w-14 z-1 relative drop-shadow-md" 207 - size={56} 208 - url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`} 209 - /> 210 - 211 - <div className="ml-3 text-sm relative bottom-1"> 212 - <span className="text-lg dark:text-neutral-200 font-medium text-neutral-800 mb-1 sm:max-w-64 lg:max-w-56 truncate"> 213 - {guild.name} 214 - </span> 215 - <div className="flex gap-2"> 216 - <ManageButton guildId={guild.id} /> 217 - <LeaderboardButton guildId={guild.id} /> 218 - </div> 219 - </div> 220 - 221 - </motion.li> 222 - ))} 223 - 224 - <motion.a 225 - href="/login?invite=true" 226 - target="_blank" 227 - key={"guildGrid" + guilds.length} 228 - variants={{ 229 - hidden: { y: 20, opacity: 0 }, 230 - visible: { 231 - y: 0, 232 - opacity: 1, 233 - transition: { 234 - type: "spring", 235 - bounce: 0.4, 236 - duration: 0.7 237 - } 238 - } 239 - }} 240 - className="border-2 dark:border-wamellow border-wamellow-100 p-4 flex justify-center items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline" 241 - > 242 - Click to add a new server 243 - </motion.a> 244 - 245 - </motion.ul> 246 - : 247 - <div className={cn("border-2 dark:border-wamellow border-wamellow-100 p-4 flex justify-center items-center rounded-lg drop-shadow-md overflow-hidden relative h-24", display === "GRID" && "md:w-1/2 lg:w-1/3")}> 248 - Loading your servers... 249 - </div> 250 - } 251 - 252 - {length > MAX_GUILDS && 253 - <ScreenMessage 254 - title="There are too many servers.." 255 - description={`To save some performance, use the search to find a guild. Showing ${MAX_GUILDS} out of ~${length < 100 ? length : Math.round(length / 100) * 100}.`} 256 - > 257 - <Image src={SadWumpusPic} alt="" height={141} width={124} /> 258 - </ScreenMessage> 259 - } 260 - 261 - </div> 262 - ); 263 - } 264 - 265 - function ManageButton({ guildId }: { guildId: string }) { 266 - const searchParams = useSearchParams(); 267 - 268 - return ( 269 - <Button 270 - as={Link} 271 - href={`/dashboard/${guildId}${searchParams.get("to") ? `/${searchParams.get("to")}` : ""}`} 272 - className="default dark:bg-neutral-500/40 hover:dark:bg-neutral-500/20 bg-neutral-400/40 hover:bg-neutral-400/20 text-sm h-9" 273 - > 274 - Manage 275 - </Button> 276 - ); 277 - } 278 - 279 - function LeaderboardButton({ guildId }: { guildId: string }) { 280 - return ( 281 - <Button 282 - as={Link} 283 - href={`/leaderboard/${guildId}`} 284 - className="default dark:bg-neutral-500/40 hover:dark:bg-neutral-500/20 bg-neutral-400/40 hover:bg-neutral-400/20 text-sm h-9 md:opacity-0 group-hover/card:opacity-100" 285 - > 286 - Leaderboard 287 - </Button> 288 - ); 23 + return params.toString(); 289 24 }
+32 -41
app/profile/analytics/page.tsx
··· 1 1 "use client"; 2 2 3 - import { useEffect, useState } from "react"; 4 - import { HiIdentification } from "react-icons/hi"; 3 + import Image from "next/image"; 4 + import { useQuery } from "react-query"; 5 5 import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; 6 6 7 7 import Box from "@/components/box"; 8 8 import { StatsBar } from "@/components/counter"; 9 - import { ScreenMessage } from "@/components/screen-message"; 10 - import { NekosticResponse, RouteErrorResponse } from "@/typings"; 9 + import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 10 + import { cacheOptions, getData } from "@/lib/api"; 11 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 12 + import { NekosticResponse } from "@/typings"; 11 13 import { convertMonthToName } from "@/utils/time"; 12 14 13 15 interface CalcUses { ··· 22 24 } 23 25 24 26 export default function Home() { 25 - const [error, setError] = useState<string>(); 26 - const [data, setData] = useState<NekosticResponse[]>(); 27 + const url = "" as const; 27 28 28 - useEffect(() => { 29 + const { isLoading, data, error } = useQuery( 30 + ["nekostic", "statistics"], 31 + () => getData<NekosticResponse[]>(url, process.env.NEXT_PUBLIC_NEKOSTIC as string), 32 + cacheOptions 33 + ); 29 34 30 - fetch(process.env.NEXT_PUBLIC_NEKOSTIC as string) 31 - .then(async (res) => { 32 - const response = await res.json() as NekosticResponse[]; 33 - if (!response) return; 34 - 35 - switch (res.status) { 36 - case 200: { 37 - setData(response.sort((a, b) => new Date(a.snapshot).getTime() - new Date(b.snapshot).getTime())); 38 - break; 39 - } 40 - default: { 41 - setData([]); 42 - setError((response as unknown as RouteErrorResponse).message); 43 - break; 44 - } 45 - } 46 - 47 - }) 48 - .catch(() => { 49 - setError("Error while fetching analytics data"); 50 - }); 51 - 52 - }, []); 53 - 54 - if (error) { 55 - return <> 35 + if (error || (data && "message" in data)) { 36 + return ( 56 37 <ScreenMessage 57 - title="Something went wrong.." 58 - description={error} 59 - href="/profile" 60 - button="Go back to overview" 61 - icon={<HiIdentification />} 62 - /> 63 - </>; 38 + top="0rem" 39 + title="Something went wrong on this page.." 40 + description={ 41 + (data && "message" in data ? data.message : `${error}`) 42 + || "An unknown error occurred."} 43 + buttons={<> 44 + <HomeButton /> 45 + <SupportButton /> 46 + </>} 47 + > 48 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 49 + </ScreenMessage> 50 + ); 64 51 } 65 52 66 - if (!data?.length) return <></>; 53 + if (isLoading || !data) return <></>; 54 + 55 + data.sort((a, b) => 56 + new Date(a.snapshot).getTime() - new Date(b.snapshot).getTime() 57 + ); 67 58 68 59 const now = new Date(); 69 60 const yesterday = new Date();
+13 -7
app/profile/layout.tsx
··· 13 13 import ImageReduceMotion from "@/components/image-reduce-motion"; 14 14 import { ListTab } from "@/components/list"; 15 15 import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 16 - import { getData } from "@/lib/api"; 16 + import { cacheOptions, getData } from "@/lib/api"; 17 17 import SadWumpusPic from "@/public/sad-wumpus.gif"; 18 18 import { ApiV1MeGetResponse } from "@/typings"; 19 19 import decimalToRgb from "@/utils/decimalToRgb"; ··· 26 26 const cookies = useCookies(); 27 27 if (cookies.get("hasSession") !== "true") window.location.href = "/login"; 28 28 29 - const user = userStore((g) => g); 29 + const user = userStore((u) => u); 30 30 const accent = decimalToRgb(user?.accentColor as number); 31 31 32 32 const url = "/users/@me" as const; 33 33 34 - const { status } = useQuery( 35 - ["users", "@me"], 34 + const { data, error } = useQuery( 35 + url, 36 36 () => getData<ApiV1MeGetResponse>(url), 37 37 { 38 38 enabled: !!user?.id, 39 - onSuccess: (d) => userStore.setState({ ...user, extended: d }) 39 + onSuccess: (d) => userStore.setState({ 40 + ...user, 41 + extended: "statusCode" in d ? {} : d 42 + }), 43 + ...cacheOptions 40 44 } 41 45 ); 42 46 43 - if (status === "error") { 47 + if (error || (data && "message" in data)) { 44 48 return ( 45 49 <ScreenMessage 46 50 top="0rem" 47 51 title="Something went wrong on this page.." 48 - description="An unknown error occurred." 52 + description={ 53 + (data && "message" in data ? data.message : `${error}`) 54 + || "An unknown error occurred."} 49 55 buttons={<> 50 56 <HomeButton /> 51 57 <SupportButton />
+223 -30
app/profile/page.tsx
··· 1 1 "use client"; 2 2 3 3 import { Button } from "@nextui-org/react"; 4 + import { motion } from "framer-motion"; 5 + import Image from "next/image"; 4 6 import Link from "next/link"; 5 - import { BsDiscord } from "react-icons/bs"; 6 - import { HiLightningBolt, HiViewGridAdd } from "react-icons/hi"; 7 + import { useSearchParams } from "next/navigation"; 8 + import { useCookies } from "next-client-cookies"; 9 + import { useState } from "react"; 10 + import { HiRefresh, HiUserAdd } from "react-icons/hi"; 11 + import { useQuery } from "react-query"; 7 12 8 13 import { userStore } from "@/common/user"; 9 - import OverviewLinkComponent from "@/components/OverviewLinkComponent"; 14 + import ImageReduceMotion from "@/components/image-reduce-motion"; 15 + import DumbTextInput from "@/components/inputs/Dumb_TextInput"; 16 + import Notice, { NoticeType } from "@/components/notice"; 17 + import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 18 + import { cacheOptions, getData } from "@/lib/api"; 19 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 20 + import { UserGuild } from "@/typings"; 21 + import cn from "@/utils/cn"; 22 + 23 + const MAX_GUILDS = 24; 10 24 11 25 export default function Home() { 12 - const user = userStore((s) => s); 26 + const cookies = useCookies(); 27 + const user = userStore((u) => u); 28 + 29 + const display = "GRID"; 30 + const [search, setSearch] = useState<string>(""); 31 + 32 + function filter(guild: UserGuild) { 33 + if (!search) return true; 34 + 35 + if (guild.name.toLowerCase().includes(search.toLowerCase())) return true; 36 + if (search.toLowerCase().includes(guild.name.toLowerCase())) return true; 37 + 38 + if (guild.id.includes(search)) return true; 39 + if (search.includes(guild.id)) return true; 40 + 41 + return false; 42 + } 43 + 44 + const url = "/guilds/@me" as const; 45 + 46 + const { isLoading, data, error } = useQuery( 47 + url, 48 + () => getData<UserGuild[]>(url), 49 + { 50 + enabled: !!user?.id, 51 + ...cacheOptions 52 + } 53 + ); 54 + 55 + if (error || (data && "message" in data)) { 56 + return ( 57 + <ScreenMessage 58 + top="0rem" 59 + title="Something went wrong on this page.." 60 + description={ 61 + (data && "message" in data ? data.message : `${error}`) 62 + || "An unknown error occurred."} 63 + buttons={<> 64 + <HomeButton /> 65 + <SupportButton /> 66 + </>} 67 + > 68 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 69 + </ScreenMessage> 70 + ); 71 + } 72 + 73 + if (isLoading || !data) return <></>; 74 + 75 + const { length } = (data || []).filter(filter); 76 + 77 + const springAnimation = { 78 + hidden: { y: 20, opacity: 0 }, 79 + visible: { 80 + y: 0, 81 + opacity: 1, 82 + transition: { 83 + type: "spring", 84 + bounce: data.length > 20 ? 0.2 : 0.4, 85 + duration: data.length > 20 ? 0.35 : 0.7 86 + } 87 + } 88 + } as const; 13 89 14 90 return ( 15 - <div> 91 + <div className="flex flex-col w-full"> 16 92 17 - <div className="w-full md:flex gap-3"> 18 - <OverviewLinkComponent 19 - className="md:w-2/3" 20 - title="Add Wamellow to your server" 21 - message="If you haven't already, now is the ideal moment to introduce Wamellow to your server." 22 - url="/login?invite=true" 23 - icon={<HiLightningBolt />} 93 + {error ? 94 + <Notice 95 + type={NoticeType.Error} 96 + message={`${error}`} 24 97 /> 25 - <OverviewLinkComponent 26 - className="md:w-1/3" 27 - title="Dashboard" 28 - message="Effortlessly handle all your guilds." 29 - url="/dashboard" 30 - icon={<HiViewGridAdd />} 31 - /> 98 + : 99 + <></> 100 + } 101 + 102 + <div className="flex flex-col md:flex-row md:items-center gap-2"> 103 + 104 + <div className="relative top-2 md:max-w-sm w-full"> 105 + <DumbTextInput 106 + value={search} 107 + setValue={setSearch} 108 + placeholder="Search" 109 + thin 110 + /> 111 + </div> 112 + 113 + <div className="md:ml-auto flex gap-3 md:mt-0"> 114 + <Button 115 + as={Link} 116 + className="w-1/2 md:w-min" 117 + href="/login?invite=true" 118 + prefetch={false} 119 + startContent={<HiUserAdd />} 120 + > 121 + Add to Server 122 + </Button> 123 + <Button 124 + as={Link} 125 + className="button-primary w-1/2 md:w-min" 126 + href="/login" 127 + prefetch={false} 128 + startContent={<HiRefresh />} 129 + > 130 + Reload 131 + </Button> 132 + </div> 133 + 32 134 </div> 33 135 34 - <hr className="mb-3 dark:border-wamellow-light border-wamellow-100-light" /> 136 + {!isLoading && 137 + <motion.ul 138 + variants={{ 139 + hidden: { opacity: 1, scale: 0 }, 140 + visible: { 141 + opacity: 1, 142 + scale: 1, 143 + transition: { 144 + delayChildren: data.length > 20 ? 0.2 : 0.3, 145 + staggerChildren: data.length > 20 ? 0.1 : 0.2 146 + } 147 + } 148 + }} 149 + initial={cookies.get("reduceMotions") === "true" ? "visible" : "hidden"} 150 + animate="visible" 151 + className={cn( 152 + "grid grid-cols-1 gap-4 w-full mt-4", 153 + display === "GRID" && "lg:grid-cols-3 md:grid-cols-2" 154 + )} 155 + > 35 156 36 - <div>Hey {user?.username}, thanks for testing out the early version of this bot :)</div> 37 - <div>There will be more exciting stuff coming soon&trade;</div> 157 + {data 158 + .filter(filter) 159 + .slice(0, MAX_GUILDS) 160 + .map((guild) => ( 161 + <motion.li 162 + key={"guildGrid" + guild.id} 163 + className="dark:bg-wamellow bg-wamellow-100 p-4 flex items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline group/card" 164 + variants={springAnimation} 165 + > 166 + <ImageReduceMotion 167 + alt="" 168 + className="absolute top-[-48px] left-0 w-full z-0 blur-xl opacity-30 pointer-events-none" 169 + size={24} 170 + url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`} 171 + forceStatic={true} 172 + /> 38 173 39 - <div className="flex mt-2"> 40 - <Button 41 - as={Link} 42 - href="/support" 43 - startContent={<BsDiscord />} 174 + <ImageReduceMotion 175 + alt={`Server icon of @${guild.name}`} 176 + className="rounded-lg h-14 w-14 z-1 relative drop-shadow-md" 177 + size={56} 178 + url={`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}`} 179 + /> 180 + 181 + <div className="ml-3 text-sm relative bottom-1"> 182 + <span className="text-lg dark:text-neutral-200 font-medium text-neutral-800 mb-1 sm:max-w-64 lg:max-w-56 truncate"> 183 + {guild.name} 184 + </span> 185 + <div className="flex gap-2"> 186 + <ManageButton guildId={guild.id} /> 187 + <LeaderboardButton guildId={guild.id} /> 188 + </div> 189 + </div> 190 + 191 + </motion.li> 192 + ))} 193 + 194 + <motion.a 195 + href="/login?invite=true" 196 + target="_blank" 197 + key={"guildGrid" + data.length} 198 + className="border-2 dark:border-wamellow border-wamellow-100 p-4 flex justify-center items-center rounded-lg drop-shadow-md overflow-hidden relative h-24 duration-100 outline-violet-500 hover:outline" 199 + variants={springAnimation} 200 + > 201 + Click to add a new server 202 + </motion.a> 203 + 204 + </motion.ul> 205 + } 206 + 207 + {length > MAX_GUILDS && 208 + <ScreenMessage 209 + title="There are too many servers.." 210 + description={`To save some performance, use the search to find a guild. Showing ${MAX_GUILDS} out of ~${length < 100 ? length : Math.round(length / 100) * 100}.`} 44 211 > 45 - Join our server for updates 46 - </Button> 47 - </div> 212 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 213 + </ScreenMessage> 214 + } 48 215 49 216 </div> 217 + ); 218 + } 219 + 220 + function ManageButton({ guildId }: { guildId: string }) { 221 + const searchParams = useSearchParams(); 222 + 223 + return ( 224 + <Button 225 + as={Link} 226 + href={`/dashboard/${guildId}${searchParams.get("to") ? `/${searchParams.get("to")}` : ""}`} 227 + className="default dark:bg-neutral-500/40 hover:dark:bg-neutral-500/20 bg-neutral-400/40 hover:bg-neutral-400/20 text-sm h-9" 228 + > 229 + Manage 230 + </Button> 231 + ); 232 + } 233 + 234 + function LeaderboardButton({ guildId }: { guildId: string }) { 235 + return ( 236 + <Button 237 + as={Link} 238 + href={`/leaderboard/${guildId}`} 239 + className="default dark:bg-neutral-500/40 hover:dark:bg-neutral-500/20 bg-neutral-400/40 hover:bg-neutral-400/20 text-sm h-9 md:opacity-0 group-hover/card:opacity-100" 240 + > 241 + Leaderboard 242 + </Button> 50 243 ); 51 244 }
+35 -56
app/profile/spotify/page.tsx
··· 1 1 "use client"; 2 2 import Image from "next/image"; 3 3 import Link from "next/link"; 4 - import { useEffect, useState } from "react"; 5 4 import { BsSpotify } from "react-icons/bs"; 6 - import { HiIdentification } from "react-icons/hi"; 5 + import { useQuery } from "react-query"; 7 6 8 7 import { userStore } from "@/common/user"; 9 8 import Box from "@/components/box"; 10 9 import Highlight from "@/components/discord/markdown"; 11 10 import DiscordMessage from "@/components/discord/message"; 12 - import { ScreenMessage } from "@/components/screen-message"; 13 - import { ApiV1UsersMeConnectionsSpotifyGetResponse, RouteErrorResponse } from "@/typings"; 11 + import { HomeButton, ScreenMessage, SupportButton } from "@/components/screen-message"; 12 + import { cacheOptions, getData } from "@/lib/api"; 13 + import SadWumpusPic from "@/public/sad-wumpus.gif"; 14 + import { ApiV1UsersMeConnectionsSpotifyGetResponse } from "@/typings"; 14 15 15 16 export default function Home({ 16 17 searchParams ··· 19 20 }) { 20 21 const user = userStore((s) => s); 21 22 22 - const [spotify, setSpotify] = useState<ApiV1UsersMeConnectionsSpotifyGetResponse & { _fetched: boolean }>(); 23 - const [error, setError] = useState<string>(); 23 + const url = "/users/@me/connections/spotify" as const; 24 24 25 - useEffect(() => { 26 - fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/connections/spotify`, { 27 - credentials: "include" 28 - }) 29 - .then(async (res) => { 30 - const response = await res.json() as ApiV1UsersMeConnectionsSpotifyGetResponse; 31 - if (!response) return; 32 - 33 - switch (res.status) { 34 - case 200: { 35 - setError(undefined); 36 - setSpotify({ ...response, _fetched: true }); 37 - break; 38 - } 39 - case 404: { 40 - // @ts-expect-error Cuz 41 - setSpotify({ _fetched: true }); 42 - break; 43 - } 44 - default: { 45 - // @ts-expect-error Cuz 46 - setSpotify({ _fetched: true }); 47 - setError((response as unknown as RouteErrorResponse).message); 48 - break; 49 - } 50 - } 25 + const { isLoading, data, error } = useQuery( 26 + url, 27 + () => getData<ApiV1UsersMeConnectionsSpotifyGetResponse>(url), 28 + cacheOptions 29 + ); 51 30 52 - }) 53 - .catch(() => { 54 - setError("Error while fetching user"); 55 - }); 56 - }, []); 57 - 58 - if (error) { 59 - return <> 31 + if (error || (data && "message" in data)) { 32 + return ( 60 33 <ScreenMessage 61 - title="Something went wrong.." 62 - description={error} 63 - href="/profile" 64 - button="Go back to overview" 65 - icon={<HiIdentification />} 66 - /> 67 - </>; 34 + top="0rem" 35 + title="Something went wrong on this page.." 36 + description={ 37 + (data && "message" in data ? data.message : `${error}`) 38 + || "An unknown error occurred."} 39 + buttons={<> 40 + <HomeButton /> 41 + <SupportButton /> 42 + </>} 43 + > 44 + <Image src={SadWumpusPic} alt="" height={141} width={124} /> 45 + </ScreenMessage> 46 + ); 68 47 } 69 48 70 - if (!spotify?._fetched) return <></>; 49 + if (isLoading || !data) return <></>; 71 50 72 51 return ( 73 52 <div className="h-full"> 74 53 75 - {!spotify.displayName && 54 + {!data.displayName && 76 55 <ScreenMessage 77 56 title="Nothing to see here.. yet.." 78 57 description="Cool things will come soon" ··· 83 62 /> 84 63 } 85 64 86 - {spotify.displayName && user?.id && 65 + {data.displayName && user?.id && 87 66 <> 88 67 89 68 <div className="flex items-center gap-2"> 90 69 {/* eslint-disable-next-line @next/next/no-img-element */} 91 - <img src={spotify.avatar ? spotify.avatar : "/discord.webp"} alt="your spotify avatar" className="rounded-lg mr-1 h-14 w-14" /> 70 + <img src={data.avatar ? data.avatar : "/discord.webp"} alt="your spotify avatar" className="rounded-lg mr-1 h-14 w-14" /> 92 71 <div> 93 72 <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium flex gap-1 items-center"> 94 - {spotify.displayName} 73 + {data.displayName} 95 74 <BsSpotify className="h-4 relative top-0.5 text-[#1ed760]" /> 96 75 </div> 97 76 <div className="flex items-center"> ··· 102 81 > 103 82 Not you? 104 83 </Link> 105 - {searchParams.spotify_login_success === "true" && spotify.displayName && <> 84 + {searchParams.spotify_login_success === "true" && data.displayName && <> 106 85 <span className="mx-2 text-neutral-500">•</span> 107 86 <div className="text-green-500 duration-200">Link was successfull!</div> 108 87 </>} ··· 123 102 }} 124 103 > 125 104 126 - <Highlight mode={"DARK"} text={`wm play [https://open.spotify.com/track/${spotify.playing?.id || "4cOdK2wGLETKBW3PvgPWqT"}](#)`} /> 105 + <Highlight mode={"DARK"} text={`wm play [https://open.data.com/track/${data.playing?.id || "4cOdK2wGLETKBW3PvgPWqT"}](#)`} /> 127 106 128 107 </DiscordMessage> 129 108 <DiscordMessage ··· 137 116 138 117 <div className="flex items-center gap-1"> 139 118 <Image src="https://cdn.discordapp.com/emojis/845043307351900183.gif?size=44&quality=lossless" height={18} width={18} alt="" /> 140 - <Highlight mode={"DARK"} text={`@${user.username} now playing [${spotify.playing?.name || "Never Gonna Give You Up"}](#) for **${spotify.playing?.duration || "3 minutes 33 seconds"}**`} /> 119 + <Highlight mode={"DARK"} text={`@${user.username} now playing [${data.playing?.name || "Never Gonna Give You Up"}](#) for **${data.playing?.duration || "3 minutes 33 seconds"}**`} /> 141 120 </div> 142 121 143 122 <div className="flex flex-row gap-1.5 h-8 mt-3"> ··· 177 156 178 157 <div className="flex items-center gap-1"> 179 158 <Image src="https://cdn.discordapp.com/emojis/845043307351900183.gif?size=44&quality=lossless" height={18} width={18} alt="" /> 180 - <Highlight mode={"DARK"} text={`@${user.username} is playing [${spotify.playing?.name || "Never Gonna Give You Up"}](#) by ${spotify.playing?.artists || "[Rick Astley]()"}`} /> 159 + <Highlight mode={"DARK"} text={`@${user.username} is playing [${data.playing?.name || "Never Gonna Give You Up"}](#) by ${data.playing?.artists || "[Rick Astley]()"}`} /> 181 160 </div> 182 161 183 162 <div className="flex gap-1.5 h-8 mt-3">
+5 -4
components/list.tsx
··· 28 28 if (!key && typeof key !== "string") return; 29 29 30 30 if (!searchParamName) { 31 - router.push(`${url}${key}`); 31 + router.push(`${url}${key}?${params.toString()}`); 32 32 return; 33 33 } 34 34 ··· 48 48 tabList: "w-full relative rounded-none p-0 border-b border-divider", 49 49 tab: "w-fit px-4 h-12 relative right-2.5" 50 50 }} 51 - defaultSelectedKey={searchParamName 52 - ? (params.get(searchParamName) || "") 53 - : path.split(url)[1].split("/").slice(0, 2).join("/") 51 + defaultSelectedKey={ 52 + searchParamName 53 + ? (params.get(searchParamName) || "") 54 + : path.split(url)[1].split("/").slice(0, 2).join("/") 54 55 } 55 56 onSelectionChange={handleChange} 56 57 variant="underlined"
+5 -4
lib/api.ts
··· 1 - import { ApiError, ApiV1GuildsGetResponse } from "@/typings"; 1 + import { ApiError, ApiV1GuildsGetResponse, RouteErrorResponse } from "@/typings"; 2 2 3 3 export const cacheOptions = { 4 4 cacheTime: 1000 * 60 * 5, ··· 8 8 9 9 export const defaultFetchOptions = { headers: { Authorization: process.env.API_SECRET as string }, next: { revalidate: 60 * 60 } }; 10 10 11 - export async function getData<T>(path: string) { 12 - const response = await fetch(`${process.env.NEXT_PUBLIC_API}${path}`, { 11 + export async function getData<T>(path: string, domain?: string) { 12 + console.log(`${domain || process.env.NEXT_PUBLIC_API}${path}`); 13 + const response = await fetch(`${domain || process.env.NEXT_PUBLIC_API}${path}`, { 13 14 credentials: "include" 14 15 }); 15 16 16 - return response.json() as Promise<T>; 17 + return response.json() as Promise<T | RouteErrorResponse>; 17 18 } 18 19 19 20 export async function getGuild(guildId?: string | null): Promise<ApiV1GuildsGetResponse | ApiError | undefined> {