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 spotify oauth flow

Luna 694c1cc5 f8cb8e7a

+305 -10
+86
app/login/spotify/page.tsx
··· 1 + "use client"; 2 + import Link from "next/link"; 3 + import { useRouter } from "next/navigation"; 4 + import { useEffect, useState } from "react"; 5 + import { BsDiscord } from "react-icons/bs"; 6 + import { HiArrowNarrowLeft } from "react-icons/hi"; 7 + 8 + import { User, userStore } from "@/common/user"; 9 + import ErrorBanner from "@/components/Error"; 10 + import { RouteErrorResponse } from "@/typings"; 11 + 12 + export default function Home() { 13 + const [error, setError] = useState<string>(); 14 + const router = useRouter(); 15 + 16 + useEffect(() => { 17 + 18 + userStore.setState(undefined); 19 + const params = new URLSearchParams(window.location.search); 20 + 21 + if (params.get("logout")) { 22 + 23 + fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/connections/spotify`, { 24 + headers: { 25 + authorization: localStorage.getItem("token") as string 26 + }, 27 + method: "DELETE" 28 + }) 29 + .then(() => { 30 + 31 + const redirect = localStorage.getItem("lastpage"); 32 + window.location.href = redirect || "/"; 33 + 34 + }) 35 + .catch(() => null); 36 + 37 + return; 38 + } 39 + 40 + if (!params.get("code")) window.location.href = `${process.env.NEXT_PUBLIC_API}/connections/spotify`; 41 + else 42 + fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/connections/spotify?${params.toString()}`, { 43 + headers: { 44 + authorization: localStorage.getItem("token") as string 45 + }, 46 + method: "PUT" 47 + }) 48 + .then(async (res) => { 49 + const response = await res.json().catch(() => null) as User; 50 + 51 + switch (res.status) { 52 + case 204: { 53 + const redirect = localStorage.getItem("lastpage"); 54 + router.push((redirect || "/") + "?spotify_login_success=true"); 55 + break; 56 + } 57 + default: { 58 + setError((response as unknown as RouteErrorResponse)?.message || "Error during authorization"); 59 + break; 60 + } 61 + } 62 + 63 + }) 64 + .catch(() => { 65 + setError("Error during authorization"); 66 + }); 67 + }, []); 68 + 69 + if (!error) return <></>; 70 + 71 + return ( 72 + <div className="w-full"> 73 + <ErrorBanner message={error} removeButton={true} /> 74 + <div className="ml-1 relative bottom-4 flex gap-2"> 75 + <Link href="/" className="text-blurple hover:opacity-80 dark:hover:bg-wamellow-alpha hover:bg-wamellow-100-alpha py-1 px-2 rounded-md duration-200 flex items-center"> 76 + <HiArrowNarrowLeft /> 77 + <span className="ml-1">Home</span> 78 + </Link> 79 + <Link href="/support" className="text-blurple hover:opacity-80 dark:hover:bg-wamellow-alpha hover:bg-wamellow-100-alpha py-1 px-2 rounded-md duration-200 flex items-center"> 80 + <BsDiscord /> 81 + <span className="ml-2">Support</span> 82 + </Link> 83 + </div> 84 + </div> 85 + ); 86 + }
+8 -5
app/profile/layout.tsx
··· 48 48 }); 49 49 }, [user]); 50 50 51 - if (user === undefined && !error) return <></>; 51 + if (error) return <ErrorBanner message={error} />; 52 + if (!user?.id) return <></>; 52 53 53 54 return ( 54 - <div className="flex flex-col w-full"> 55 + <div className="flex flex-col w-full h-full"> 55 56 <title>Your profile</title> 56 - 57 - {error && <ErrorBanner message={error} />} 58 57 59 58 <div className="text-lg sm:flex items-center"> 60 59 <div className="flex items-center"> ··· 97 96 { 98 97 name: "Text to Speech", 99 98 value: "/text-to-speech" 100 - } 99 + }// , 100 + // { 101 + // name: "Spotify", 102 + // value: "/spotify" 103 + // } 101 104 ]} 102 105 url={"/profile"} 103 106 disabled={!user}
+197
app/profile/spotify/page.tsx
··· 1 + "use client"; 2 + import Link from "next/link"; 3 + import { useEffect, useState } from "react"; 4 + import { BsSpotify } from "react-icons/bs"; 5 + 6 + import { userStore } from "@/common/user"; 7 + import Highlight from "@/components/discord/Markdown"; 8 + import DiscordMessage from "@/components/discord/Message"; 9 + import ErrorBanner from "@/components/Error"; 10 + import { ApiV1UsersMeConnectionsSpotifyGetResponse, RouteErrorResponse } from "@/typings"; 11 + 12 + export default function Home({ 13 + searchParams 14 + }: { 15 + searchParams: { spotify_login_success?: string } 16 + }) { 17 + const user = userStore((s) => s); 18 + 19 + const [spotify, setSpotify] = useState<ApiV1UsersMeConnectionsSpotifyGetResponse & { _fetched: boolean }>(); 20 + const [error, setError] = useState<string>(); 21 + 22 + useEffect(() => { 23 + fetch(`${process.env.NEXT_PUBLIC_API}/users/@me/connections/spotify`, { 24 + headers: { 25 + authorization: localStorage.getItem("token") as string 26 + } 27 + }) 28 + .then(async (res) => { 29 + const response = await res.json() as ApiV1UsersMeConnectionsSpotifyGetResponse; 30 + if (!response) return; 31 + 32 + switch (res.status) { 33 + case 200: { 34 + setError(undefined); 35 + setSpotify({ ...response, _fetched: true }); 36 + break; 37 + } 38 + case 404: { 39 + // @ts-expect-error Cuz 40 + setSpotify({ _fetched: true }); 41 + break; 42 + } 43 + default: { 44 + setError((response as unknown as RouteErrorResponse).message); 45 + break; 46 + } 47 + } 48 + 49 + }) 50 + .catch(() => { 51 + setError("Error while fetching user"); 52 + }); 53 + }, []); 54 + 55 + if (error) return <ErrorBanner message="Error while fetching spotify data" />; 56 + if (!spotify?._fetched) return <></>; 57 + 58 + return ( 59 + <div className="h-full"> 60 + 61 + {!spotify.displayName && 62 + <div className="w-full h-full flex flex-col items-center justify-center" style={{ marginTop: "20vh" }}> 63 + <div> 64 + 65 + <div className="mb-10 flex flex-col items-center"> 66 + <span className="text-4xl dark:text-neutral-100 text-neutral-900 font-semibold mb-1.5">Nothing to see here.. yet..</span> <br /> 67 + <span className="text-lg dark:text-neutral-400 text-neutral-600 font-semibold">Cool things will come soon</span> 68 + </div> 69 + 70 + <div> 71 + <Link href={`${process.env.NEXT_PUBLIC_API}/connections/spotify`} className="flex bg-[#1ed760] hover:bg-[#1ed760]/80 text-black py-2 px-4 rounded-md duration-200 justify-center"> 72 + <BsSpotify className="relative top-1" /> 73 + <span className="ml-2">Connect Spotify</span> 74 + </Link> 75 + </div> 76 + 77 + </div> 78 + </div> 79 + 80 + } 81 + 82 + {spotify.displayName && user?.id && 83 + <> 84 + 85 + <div className="flex items-center gap-2"> 86 + {/* eslint-disable-next-line @next/next/no-img-element */} 87 + <img src={spotify.avatar ? spotify.avatar : "/discord.png"} alt="your spotify avatar" className="rounded-lg mr-1 h-14 w-14" /> 88 + <div> 89 + <div className="text-2xl dark:text-neutral-200 text-neutral-800 font-medium flex gap-1 items-center"> 90 + {spotify.displayName} 91 + <BsSpotify className="h-4 relative top-0.5 text-[#1ed760]" /> 92 + </div> 93 + <div className="flex items-center"> 94 + <Link href="/login/spotify?logout=true" className="text-violet-500 opacity-60 hover:opacity-80 duration-200">Not you?</Link> 95 + {searchParams.spotify_login_success === "true" && spotify.displayName && <> 96 + <span className="mx-2 text-neutral-500">•</span> 97 + <div className="text-green-500 duration-200">Link was successfull!</div> 98 + </>} 99 + </div> 100 + </div> 101 + </div> 102 + 103 + <div className="w-full border-b dark:border-wamellow-light border-wamellow-100-light md:hidden mt-6" /> 104 + 105 + <div className="my-6 flex flex-col gap-6 md:dark:bg-wamellow md:bg-wamellow-100 rounded-xl md:p-6 overflow-hidden"> 106 + 107 + <DiscordMessage 108 + mode={"DARK"} 109 + user={{ 110 + username: user.global_name || user.username, 111 + avatar: user.avatar ? `https://cdn.discordapp.com/avatars/821472922140803112/${user.avatar}.webp?size=64` : "/discord.png", 112 + bot: false 113 + }} 114 + > 115 + 116 + <Highlight mode={"DARK"} text="wm play [https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT]()" /> 117 + 118 + </DiscordMessage> 119 + <DiscordMessage 120 + mode={"DARK"} 121 + user={{ 122 + username: "Wamellow", 123 + avatar: "/waya-v3.webp", 124 + bot: true 125 + }} 126 + > 127 + 128 + <div className="flex gap-1.5 h-3 mt-2 cursor-text"> 129 + <div className="dark:bg-neutral-600/90 bg-neutral-400/90 h-full w-12 rounded-full" /> 130 + <div className="dark:bg-neutral-600/90 bg-neutral-400/90 h-full w-24 rounded-full" /> 131 + <div className="dark:bg-blue-600/90 bg-blue-400/90 h-full w-20 rounded-full" /> 132 + <div className="dark:bg-neutral-600/90 bg-neutral-400/90 h-full w-8 rounded-full" /> 133 + <div className="dark:bg-neutral-500/90 h-full w-16 rounded-full" /> 134 + </div> 135 + <div className="flex flex-row gap-1.5 h-8 mt-3"> 136 + <div className="dark:border-neutral-600/90 border-neutral-400/90 border-2 h-full w-32 py-2.5 px-4 rounded-md flex items-center justify-center cursor-pointer"> 137 + <div className="dark:bg-neutral-600/90 bg-neutral-400/90 h-full w-full rounded-full" /> 138 + </div> 139 + <div className="dark:border-neutral-600/90 border-neutral-400/90 border-2 h-full w-16 py-2.5 px-4 rounded-md flex items-center justify-center cursor-pointer"> 140 + <div className="dark:bg-neutral-600/90 bg-neutral-400/90 h-full w-full rounded-full" /> 141 + </div> 142 + <div className="dark:border-neutral-600/90 border-neutral-400/90 border-2 h-full w-16 py-2.5 px-4 rounded-md flex items-center justify-center cursor-pointer"> 143 + <div className="dark:bg-neutral-600/90 bg-neutral-400/90 h-full w-full rounded-full" /> 144 + </div> 145 + </div> 146 + 147 + </DiscordMessage> 148 + 149 + <DiscordMessage 150 + mode={"DARK"} 151 + user={{ 152 + username: user.global_name || user.username, 153 + avatar: user.avatar ? `https://cdn.discordapp.com/avatars/821472922140803112/${user.avatar}.webp?size=64` : "/discord.png", 154 + bot: false 155 + }} 156 + > 157 + 158 + <Highlight mode={"DARK"} text="wm" /> 159 + 160 + </DiscordMessage> 161 + <DiscordMessage 162 + mode={"DARK"} 163 + user={{ 164 + username: "Wamellow", 165 + avatar: "/waya-v3.webp", 166 + bot: true 167 + }} 168 + > 169 + 170 + <div className="flex flex-row gap-1.5 h-3 mt-2 cursor-text"> 171 + <div className="dark:bg-neutral-700/90 bg-neutral-300/90 h-full w-12 rounded-full" /> 172 + <div className="dark:bg-neutral-700/90 bg-neutral-300/90 h-full w-20 rounded-full" /> 173 + <div className="dark:bg-blue-600/90 bg-blue-400/90 h-full w-20 rounded-full" /> 174 + </div> 175 + <div className="flex gap-1.5 h-8 mt-3"> 176 + <div className="dark:border-neutral-700/90 border-neutral-300/90 border-2 h-full w-32 py-2.5 px-4 rounded-md flex items-center justify-center cursor-pointer"> 177 + <div className="dark:bg-neutral-700/90 bg-neutral-300/90 h-full w-full rounded-full" /> 178 + </div> 179 + <div className="dark:border-neutral-700/90 border-neutral-300/90 border-2 h-full w-16 py-2.5 px-4 rounded-md flex items-center justify-center cursor-pointer"> 180 + <div className="dark:bg-neutral-700/90 bg-neutral-300/90 h-full w-full rounded-full" /> 181 + </div> 182 + <div className="dark:border-neutral-700/90 border-neutral-300/90 border-2 h-full w-16 py-2.5 px-4 rounded-md flex items-center justify-center cursor-pointer"> 183 + <div className="dark:bg-neutral-700/90 bg-neutral-300/90 h-full w-full rounded-full" /> 184 + </div> 185 + </div> 186 + 187 + </DiscordMessage> 188 + 189 + </div> 190 + 191 + </> 192 + } 193 + 194 + 195 + </div> 196 + ); 197 + }
+7 -4
components/Header.tsx
··· 21 21 const montserrat = Montserrat({ subsets: ["latin"] }); 22 22 23 23 interface Props { 24 - children: React.ReactNode 24 + children: React.ReactNode; 25 25 } 26 26 27 27 const Header: FunctionComponent<Props> = ({ children }) => { ··· 31 31 32 32 const path = usePathname() || "/"; 33 33 useEffect(() => { 34 - if (!["/login", "/logout"].includes(path)) localStorage.setItem("lastpage", path); 34 + if (!["/login", "/login/spotify", "/logout"].includes(path)) localStorage.setItem("lastpage", path); 35 + 36 + const params = new URLSearchParams(window.location.search); 37 + if (params.get("spotify_login_success") === "true" && path !== "/login/spotify") window.close(); 35 38 36 39 if (!path.startsWith("/dashboard/")) guildStore.setState(undefined); 37 40 }, [path]); ··· 157 160 ); 158 161 159 162 return ( 160 - <html lang="en" className="flex justify-center min-h-full max-w-full overflow-x-hidden"> 163 + <html lang="en" className="flex justify-center min-h-screen max-w-screen overflow-x-hidden"> 161 164 162 - <body className={`${inter.className} w-full max-w-7xl min-h-full`}> 165 + <body className={`${inter.className} w-full max-w-7xl`}> 163 166 164 167 <div className="absolute left-0 bg-gradient-to-r from-indigo-400 to-pink-400 h-8 w-full flex items-center justify-center text-white font-medium text-sm"> 165 168 <div className="hidden md:block">
+1
components/discord/Markdown.tsx
··· 40 40 41 41 return ( 42 42 <ReactMarkdown 43 + className="break-words" 43 44 rehypePlugins={[rehypeRaw]} 44 45 components={{ 45 46 h1: ({ ...props }) => <div className="text-3xl font-semibold" {...props} />,
+1 -1
components/discord/Message.tsx
··· 32 32 <span className="ml-1">BOT</span> 33 33 </div> 34 34 } 35 - <div className={`text-xs ${mode === "DARK" ? "text-neutral-400" : "text-neutral-600"}`}>{formatTime(new Date())}</div> 35 + <div className={`text-xs ml-1 ${mode === "DARK" ? "text-neutral-400" : "text-neutral-600"}`}>{formatTime(new Date())}</div> 36 36 </div> 37 37 {children} 38 38 </div>
+5
typings.ts
··· 247 247 voiceminutes: number; 248 248 invites: number; 249 249 }; 250 + } 251 + 252 + export interface ApiV1UsersMeConnectionsSpotifyGetResponse { 253 + displayName: string; 254 + avatar: string | null; 250 255 }