import { m, Variants } from "framer-motion"; import { useRouter } from "next/router"; import { ReactElement, useEffect, useState } from "react"; import DashLayout from "../../../layouts/DashLayout"; import Image from "next/image"; import Loading from "../../../components/common/Loading"; import { GetServerSideProps } from "next"; import UserJSONEntry from "../../../interfaces/UserJSONEntry"; import APIError from "../../../interfaces/APIError"; import RankChart from "../../../components/userpage/RankChart"; import RankHistoryJSON from "../../../interfaces/ChartRankHistoryJSON"; interface EmoteURLs { "7tv": { [key: string]: string }; bttv: { [key: string]: string }; ffz: { [key: string]: string }; twitch: { [key: string]: string }; [key: string]: { [key: string]: string }; } interface UserPageProps { userData: UserJSONEntry; serverError: APIError | null; } function UserPage(props: UserPageProps) { const [channelEmotes, setChannelEmotes] = useState<{ [key: string]: { [key: string]: string }; }>({}); const [errorCode, setErrorCode] = useState(null); const router = useRouter(); const { username } = router.query; const [rankHistory, setRankHistory] = useState( randomRankHistory(props.userData.rank) ); useEffect(() => { if (!router.isReady) return; // if it is of if (props.serverError) { setErrorCode(props.serverError.error.code); } fetch("/api/emotes") .then((res) => res.json()) .then((data) => { // if error, return if (data.error) { setErrorCode(data.error.code); return; } // construct js object with emote names as keys and emote urls for each provider // 7tv let emotes: EmoteURLs = { "7tv": {}, bttv: {}, ffz: {}, twitch: {} }; data["7tv"].channel.forEach((emote: any) => { let base_url = emote.data.host.url; // get the largest emote size, append it to the base url let largest = emote.data.host.files[emote.data.host.files.length - 1]; // if null return if (!largest) { return false; } emotes["7tv"][emote.data.name] = `https:${base_url}/${largest.name}`; }); // same for global emotes data["7tv"].global.forEach((emote: any) => { let base_url = emote.data.host.url; let largest = emote.data.host.files[emote.data.host.files.length - 1]; emotes["7tv"][emote.data.name] = `https:${base_url}/${largest.name}`; }); // bttv data["bttv"].channel.forEach((emote: any) => { emotes["bttv"][ emote.code ] = `https://cdn.betterttv.net/emote/${emote.id}/3x`; }); data["bttv"].global.forEach((emote: any) => { emotes["bttv"][ emote.code ] = `https://cdn.betterttv.net/emote/${emote.id}/3x`; }); // ffz data["ffz"].channel.forEach((emote: any) => { // ffz emotes don't have all sizes available, so we need to get the largest one by taking the largest key in the urls object emotes["ffz"][emote.name] = `${ emote.urls[ Math.max(...Object.keys(emote.urls).map((k) => parseInt(k))) ] }`; }); data["ffz"].global.forEach((emote: any) => { emotes["ffz"][emote.name] = `${ emote.urls[ Math.max(...Object.keys(emote.urls).map((k) => parseInt(k))) ] }`; }); // twitch data["twitch"].channel.forEach((emote: any) => { emotes["twitch"][emote.name] = emote.images["url_4x"]; }); data["twitch"].global.forEach((emote: any) => { emotes["twitch"][emote.name] = emote.images["url_4x"]; }); // set emotes to channelEmotes setChannelEmotes(emotes); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.isReady]); if (errorCode !== null) { // 20000 = user not found // 10000 = emote api error // 10100 = twitch api error const errorMsg = errorCode === 20000 ? "User not found" : "API error"; return (

{errorMsg}

); } // if json is empty, and if channelEmotes is incomplete, show loading screen if (Object.keys(channelEmotes).length < 4) { return (
); } return ( <>
{/* User "banner" */}
User avatar

{props.userData.name}

{/* User's badges */}
{props.userData.badges ? ( props.userData.badges.map( (badge: { name: string; color: string; priority: number; }) => { return (
{badge.name}
); } ) ) : ( <> )}
{/* User's net worth (Desktop) */}

$ {props.userData.net_worth.toLocaleString("en-US")}

{/* Main Container */} {/* User's Rank/Graph */}

Global Rank

# {props.userData.rank.toLocaleString("en-US")}
{/* User's Rank Graph (Desktop) */}
setRankHistory(randomRankHistory(props.userData.rank)) } initial={{ color: "rgb(244, 114, 182)", }} whileHover={{ scale: 1.05, backgroundColor: "rgb(244, 114, 182)", color: "white", }} >

randomize

{/* User's net worth (Mobile) */}

$ {props.userData.net_worth.toLocaleString("en-US")}

{/* User's Graph (Mobile) */}
setRankHistory(randomRankHistory(props.userData.rank)) } initial={{ color: "rgb(244, 114, 182)", }} whileHover={{ scale: 1.05, backgroundColor: "rgb(244, 114, 182)", color: "white", }} >

randomize

{/* User's Assets */}
{/* User's Assets Header */}

Top Assets

{/* User's Assets Body */}
{errorCode === 20000 ? (

{`Could not load assets`}

) : ( props.userData.assets.map( (asset: { name: string; count: number; provider: string; }) => (
{ // if error code is 10000 or emote does not exist, show placeholder image errorCode === 10000 || channelEmotes[asset.provider] === undefined || channelEmotes[asset.provider][asset.name] === undefined ? (

{`404 :(`}

) : ( {asset.name} ) } {/* Fix asset count to bottom right of image */}

x{asset.count}

{ // show provider logo (7tv, bttv, ffz, twitch) asset.provider === "7tv" ? (
) : asset.provider === "bttv" ? (
) : asset.provider === "ffz" ? (
) : (
) }

{asset.name}

) ) )}
{/* Sidebar */} {/* User's Stats, left side is label, right side is value */}

Points

{props.userData.points.toLocaleString("en-US")}

Shares

{props.userData.shares.toLocaleString("en-US")}

Trades

{(0).toLocaleString("en-US")}

Peak rank

{(0).toLocaleString("en-US")}

Joined

{new Date(0).toLocaleDateString("en-US", { year: "numeric", month: "short", })}

{/* User's Favorite Emote */}

Favorite Emote

This user has not yet set a favorite emote.

); } const SevenTVLogo = () => { return ( ); }; const FFZLogo = () => { return ( ); }; const BTTVLogo = () => { return ( ); }; const TwitchLogo = () => { return ( ); }; const randomRankHistory = (currentRank: number): RankHistoryJSON => { // make a random rank array of size 31 ranging 1 - 18, with a 50% chance to remain the previous index's rank, end with current rank let prevRank = Math.floor(Math.random() * 18) + 1; const history: number[] = Array.from({ length: 31 }, (_, i) => { if (i === 30) return currentRank; let chance = i === 0 ? 0 : Math.random(); prevRank = chance <= 0.5 ? prevRank : Math.floor(Math.random() * 18) + 1; return prevRank; }); return { rank: history, }; }; const containerVariants: Variants = { initial: { opacity: 0, y: 20, }, animate: { opacity: 1, y: 0, transition: { duration: 0.75, ease: "easeOut", delayChildren: 0.3, staggerChildren: 0.25, }, }, exit: { opacity: 0, y: 20, transition: { duration: 0.5, ease: "easeOut", }, }, }; const userBannerVariants: Variants = { initial: { opacity: 0, x: 20, }, animate: { opacity: 1, x: 0, transition: { duration: 0.75, type: "spring", }, }, }; const mainContainerVariants: Variants = { initial: { opacity: 0, y: 20, }, animate: { opacity: 1, y: 0, transition: { duration: 0.75, ease: "easeOut", }, }, }; const sidebarVariants: Variants = { initial: { opacity: 0, x: 20, }, animate: { opacity: 1, x: 0, transition: { duration: 0.75, ease: "easeOut", delayChildren: 0.3, staggerChildren: 0.25, }, }, }; const sidebarItemVariants: Variants = { initial: { opacity: 0, x: 20, }, animate: { opacity: 1, x: 0, transition: { duration: 0.75, ease: "easeOut", }, }, }; export const getServerSideProps: GetServerSideProps = async ( context ) => { // cache, currently 30s till stale context.res.setHeader( "Cache-Control", "public, s-maxage=45, stale-while-revalidate=30" ); // data fetch const url = new URL( `/api/fakeUsers?u=${context.query.username}`, process.env.NEXT_PUBLIC_URL ); // TODO: add error handling const res = await fetch(url); let user = await res.json(); if (user.error) { return { props: { userData: user, serverError: user, }, }; } return { props: { userData: user.data[0], serverError: null } }; }; UserPage.getLayout = function getLayout(page: ReactElement) { const { userData, serverError } = page.props; const metaTags = { title: !serverError ? `${userData.name ?? "User 404"} - toffee` : "User 404 - toffee", description: !serverError ? `${userData.name}'s portfolio on toffee` : "Couldn't find that user on toffee... :(", imageUrl: !serverError ? userData.avatar_url : undefined, misc: { "twitter:card": "summary", }, }; return {page}; }; export default UserPage;