Merge pull request #10 from Invest-Bot/dev

Merge Dev: Dynamic metatags, redis error handling, userpage animations
This commit is contained in:
zach 2023-01-26 19:00:22 -08:00 committed by GitHub
commit c7766aeda3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 475 additions and 359 deletions

View file

@ -11,11 +11,24 @@ import NavBar from "../components/dashboard/NavBar";
interface DashLayoutProps { interface DashLayoutProps {
children: React.ReactNode; children: React.ReactNode;
metaTags: {
title?: string;
ogTitle?: string;
description?: string;
ogDescription?: string;
content?: string;
imageUrl?: string;
themeColor?: string;
misc?: {
[key: string]: string;
};
};
} }
function DashLayout(props: DashLayoutProps) { function DashLayout(props: DashLayoutProps) {
// get the current route for animation purposes // get the current route for animation purposes
const router = useRouter(); const router = useRouter();
const title = props.metaTags.title ?? "Dashboard - toffee";
return ( return (
<m.div <m.div
className="bg-gradient-to-t from-zinc-900 to-[#3015457b]" className="bg-gradient-to-t from-zinc-900 to-[#3015457b]"
@ -26,18 +39,49 @@ function DashLayout(props: DashLayoutProps) {
> >
<Head> <Head>
<meta name="viewport" content="initial-scale=0.8" /> <meta name="viewport" content="initial-scale=0.8" />
<title>Dashboard - toffee</title> <title>{title}</title>
<meta name="description" content="Dashboard statistics for toffee" /> <meta
name="description"
content={
props.metaTags.description ?? "Dashboard statistics for toffee"
}
/>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#c084fc" /> <meta
<meta property="og:title" content="toffee" /> name="theme-color"
content={props.metaTags.themeColor ?? "#c084fc"}
/>
<meta
property="og:title"
content={props.metaTags.ogTitle ?? props.metaTags.title ?? "toffee"}
/>
<meta <meta
property="og:description" property="og:description"
content="Serving anny's community est. 2022" content={
props.metaTags.ogDescription ??
props.metaTags.description ??
"Dashboard statistics for toffee"
}
/>
<meta
property="og:image"
content={props.metaTags.imageUrl ?? "/img/logo.webp"}
/>
<meta
property="og:type"
content={props.metaTags.content ?? "website"}
/> />
<meta property="og:image" content="/img/logo.webp" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="toffee" /> <meta property="og:site_name" content="toffee" />
{props.metaTags.misc &&
Object.keys(props.metaTags.misc).map((key) => {
return (
<meta
key={key}
property={key}
content={props.metaTags.misc ? props.metaTags.misc[key] : ""}
/>
);
})}
</Head> </Head>
<div className="flex h-screen w-screen flex-col overflow-hidden lg:flex-row"> <div className="flex h-screen w-screen flex-col overflow-hidden lg:flex-row">

View file

@ -15,6 +15,18 @@ import { NavTemplate } from "./NavTemplates";
interface HomeLayoutProps { interface HomeLayoutProps {
navOptions: NavTemplate[]; navOptions: NavTemplate[];
children: React.ReactNode; children: React.ReactNode;
metaTags: {
title?: string;
ogTitle?: string;
description?: string;
ogDescription?: string;
content?: string;
imageUrl?: string;
themeColor?: string;
misc?: {
[key: string]: string;
};
};
} }
function HomeLayout(props: HomeLayoutProps) { function HomeLayout(props: HomeLayoutProps) {
@ -22,6 +34,7 @@ function HomeLayout(props: HomeLayoutProps) {
const navOptions = props.navOptions; const navOptions = props.navOptions;
// get the current route for animation purposes // get the current route for animation purposes
const router = useRouter(); const router = useRouter();
const title = props.metaTags.title ?? "Dashboard - toffee";
return ( return (
<m.div <m.div
initial="initial" initial="initial"
@ -30,18 +43,49 @@ function HomeLayout(props: HomeLayoutProps) {
variants={containerVariants} variants={containerVariants}
> >
<Head> <Head>
<title>toffee</title> <title>{title}</title>
<meta name="description" content="Serving anny's community est. 2022" /> <meta
name="description"
content={
props.metaTags.description ?? "Serving anny's community est. 2022"
}
/>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#c084fc" /> <meta
<meta property="og:title" content="toffee" /> name="theme-color"
content={props.metaTags.themeColor ?? "#c084fc"}
/>
<meta
property="og:title"
content={props.metaTags.ogTitle ?? props.metaTags.title ?? "toffee"}
/>
<meta <meta
property="og:description" property="og:description"
content="Serving anny's community est. 2022" content={
props.metaTags.ogDescription ??
props.metaTags.description ??
"Serving anny's community est. 2022"
}
/>
<meta
property="og:image"
content={props.metaTags.imageUrl ?? "/img/logo.webp"}
/>
<meta
property="og:type"
content={props.metaTags.content ?? "website"}
/> />
<meta property="og:image" content="/img/logo.webp" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="toffee" /> <meta property="og:site_name" content="toffee" />
{props.metaTags.misc &&
Object.keys(props.metaTags.misc).map((key) => {
return (
<meta
key={key}
property={key}
content={props.metaTags.misc ? props.metaTags.misc[key] : ""}
/>
);
})}
</Head> </Head>
<LazyMotion features={domAnimation}> <LazyMotion features={domAnimation}>

View file

@ -24,10 +24,11 @@ export function createRedisInstance(config = getRedisConfiguration()) {
lazyConnect: true, lazyConnect: true,
showFriendlyErrorStack: true, showFriendlyErrorStack: true,
enableAutoPipelining: true, enableAutoPipelining: true,
maxRetriesPerRequest: 0, maxRetriesPerRequest: 3,
retryStrategy: (times: number) => { retryStrategy: (times: number) => {
if (times > 3) { if (times > 3) {
throw new Error(`[Redis] Could not connect after ${times} attempts`); console.log(`[Redis] Could not connect after ${times} attempts`);
return undefined;
} }
return Math.min(times * 200, 1000); return Math.min(times * 200, 1000);
@ -45,11 +46,11 @@ export function createRedisInstance(config = getRedisConfiguration()) {
const redis = new Redis(options); const redis = new Redis(options);
redis.on("error", (error: unknown) => { redis.on("error", (error: unknown) => {
console.warn("[Redis] Error connecting", error); console.warn("[Redis] ", error);
}); });
return redis; return redis;
} catch (e) { } catch (e) {
throw new Error(`[Redis] Could not create a Redis instance`); console.log(`[Redis] Could not create a Redis instance`);
} }
} }

View file

@ -1,23 +0,0 @@
import Head from "next/head";
import { ReactElement } from "react";
import HomeLayout from "../layouts/HomeLayout";
import { homeMain } from "../layouts/NavTemplates";
function About() {
return (
<>
<Head>
<title>About - toffee</title>
</Head>
<div className="flex min-h-screen flex-col items-center justify-center py-2">
<p>about</p>
</div>
</>
);
}
About.getLayout = function getLayout(page: ReactElement) {
return <HomeLayout navOptions={homeMain}>{page}</HomeLayout>;
};
export default About;

View file

@ -1,28 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createRedisInstance } from "../../../misc/redis";
import { getChannelEmotes, getGlobalEmotes } from "../../../misc/7TVAPI";
type Data = {
[key: string]: any;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const redis = createRedisInstance();
try {
const channel = req.query.c
? await getChannelEmotes(redis, req.query.c as string)
: undefined;
const global = await getGlobalEmotes(redis);
redis.quit();
res.status(200).json({ channel, global });
} catch (e) {
console.log(e);
res
.status(500)
.json({ error: { message: "7TV or internal API is down", code: 10000 } });
}
}

View file

@ -1,30 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createRedisInstance } from "../../../misc/redis";
import { getUserByID, getGlobalEmotes } from "../../../misc/BTTVAPI";
type Data = {
[key: string]: any;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const redis = createRedisInstance();
try {
const channel = req.query.c
? (await getUserByID(redis, req.query.c as string)).channelEmotes
: undefined;
const global = await getGlobalEmotes(redis);
redis.quit();
res.status(200).json({ channel, global });
} catch (e) {
console.log(e);
res
.status(500)
.json({
error: { message: "BTTV or internal API is down", code: 10200 },
});
}
}

78
pages/api/emotes.ts Normal file
View file

@ -0,0 +1,78 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createRedisInstance } from "../../misc/redis";
import {
getGlobalEmotes as get7TVGlobalEmotes,
getChannelEmotes as get7TVChannelEmotes,
} from "../../misc/7TVAPI";
import {
getGlobalEmotes as getBTTVGlobalEmotes,
getUserByID as getBTTVUser,
} from "../../misc/BTTVAPI";
import {
getGlobalEmotes as getFFZGlobalEmotes,
getEmoteSet as getFFZEmoteSet,
} from "../../misc/FFZAPI";
import {
getGlobalEmotes as getTwitchGlobalEmotes,
getChannelEmotes as getTwitchChannelEmotes,
} from "../../misc/TwitchAPI";
type Data = {
[key: string]: any;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const redis = createRedisInstance();
if (!redis) {
res
.status(500)
.json({ error: { message: "Internal API is down", code: 50000 } });
return;
}
try {
const cachedJSON = await redis.get("ALL_EMOTES");
if (cachedJSON) {
const jsonRes = JSON.parse(cachedJSON);
redis.quit();
res.status(200).json(jsonRes);
return;
}
const ffzGlobal = await getFFZGlobalEmotes(redis);
const jsonRes = {
"7tv": {
global: (await get7TVGlobalEmotes(redis)).namedEmoteSet.emotes,
channel: (await get7TVChannelEmotes(redis, "61ad997effa9aba101bcfddf"))
.user.emote_sets[0].emotes,
},
bttv: {
global: await getBTTVGlobalEmotes(redis),
channel: (await getBTTVUser(redis, "56418014")).channelEmotes,
},
ffz: {
global: ffzGlobal.sets["3"].emoticons.concat(
ffzGlobal.sets["4330"].emoticons
),
channel: (await getFFZEmoteSet(redis, "341402")).set.emoticons,
},
twitch: {
global: (await getTwitchGlobalEmotes(redis)).data,
channel: (await getTwitchChannelEmotes(redis, "56418014")).data,
},
};
// cache emotelist for 20 minutes
await redis.set("ALL_EMOTES", JSON.stringify(jsonRes), "EX", 1200);
redis.quit();
res.status(200).json(jsonRes);
} catch (e) {
console.log(e);
res
.status(500)
.json({ error: { message: "Internal Emote API error", code: 10000 } });
}
}

View file

@ -16,6 +16,12 @@ export default async function handler(
const sortAsc = req.query.a ? (req.query.a as string) : undefined; const sortAsc = req.query.a ? (req.query.a as string) : undefined;
const redis = createRedisInstance(); const redis = createRedisInstance();
if (!redis) {
res.status(500).json({
error: { message: "Internal API is down", code: 50100 },
});
return;
}
let data = fakeData; let data = fakeData;
// calculate all net worths // calculate all net worths
@ -114,7 +120,7 @@ export default async function handler(
interface asset { interface asset {
name: string; name: string;
count: number; count: number;
provider: "7tv" | "bttv" | "ffz" | "ttv"; provider: "7tv" | "bttv" | "ffz" | "twitch";
} }
interface fakeDataEntry { interface fakeDataEntry {
id: number; id: number;
@ -185,7 +191,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfSigh", name: "annytfSigh",
count: 1, count: 1,
provider: "ttv", provider: "twitch",
}, },
{ {
name: "GabeN", name: "GabeN",
@ -230,7 +236,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfHeart", name: "annytfHeart",
count: 98, count: 98,
provider: "ttv", provider: "twitch",
}, },
{ {
name: "Catge", name: "Catge",
@ -275,7 +281,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfRave", name: "annytfRave",
count: 5, count: 5,
provider: "ttv", provider: "twitch",
}, },
], ],
badges: [adminBadge, botDevBadge], badges: [adminBadge, botDevBadge],
@ -310,7 +316,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfMelt", name: "annytfMelt",
count: 16, count: 16,
provider: "ttv", provider: "twitch",
}, },
], ],
badges: [CEOBadge, adminBadge], badges: [CEOBadge, adminBadge],
@ -350,7 +356,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfAngy", name: "annytfAngy",
count: 90, count: 90,
provider: "ttv", provider: "twitch",
}, },
], ],
badges: [adminBadge, botDevBadge], badges: [adminBadge, botDevBadge],
@ -414,7 +420,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfHug", name: "annytfHug",
count: 19, count: 19,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -433,7 +439,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfLUL", name: "annytfLUL",
count: 9, count: 9,
provider: "ttv", provider: "twitch",
}, },
{ {
name: "peepoSnow", name: "peepoSnow",
@ -487,7 +493,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfPain", name: "annytfPain",
count: 37, count: 37,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -526,7 +532,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfKnuckles", name: "annytfKnuckles",
count: 2, count: 2,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -565,7 +571,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfCheer", name: "annytfCheer",
count: 54, count: 54,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -604,7 +610,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfBonk", name: "annytfBonk",
count: 77, count: 77,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -648,7 +654,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfSit", name: "annytfSit",
count: 53, count: 53,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -711,7 +717,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfGamba", name: "annytfGamba",
count: 32, count: 32,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -745,7 +751,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfFlower", name: "annytfFlower",
count: 33, count: 33,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -779,7 +785,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfFlower", name: "annytfFlower",
count: 79, count: 79,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -813,7 +819,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfSad", name: "annytfSad",
count: 2, count: 2,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },
@ -847,7 +853,7 @@ const fakeData: fakeDataEntry[] = [
{ {
name: "annytfHeart", name: "annytfHeart",
count: 63, count: 63,
provider: "ttv", provider: "twitch",
}, },
], ],
}, },

View file

@ -1,30 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createRedisInstance } from "../../../misc/redis";
import { getEmoteSet, getGlobalEmotes } from "../../../misc/FFZAPI";
type Data = {
[key: string]: any;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const redis = createRedisInstance();
try {
const channel = req.query.s
? (await getEmoteSet(redis, req.query.s as string)).set.emoticons
: undefined;
let global = await getGlobalEmotes(redis);
// set global emotes to be the three sets within the global object ("3", "4330")
global = global.sets["3"].emoticons.concat(global.sets["4330"].emoticons);
redis.quit();
res.status(200).json({ channel, global });
} catch (e) {
console.log(e);
res
.status(500)
.json({ error: { message: "FFZ or internal API is down", code: 10300 } });
}
}

View file

@ -1,30 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createRedisInstance } from "../../../misc/redis";
import { getChannelEmotes, getGlobalEmotes } from "../../../misc/TwitchAPI";
type Data = {
[key: string]: any;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
const redis = createRedisInstance();
try {
const channel = req.query.c
? (await getChannelEmotes(redis, req.query.c as string)).data
: undefined;
const global = (await getGlobalEmotes(redis)).data;
redis.quit();
res.status(200).json({ channel, global });
} catch (e) {
console.log(e);
res
.status(500)
.json({
error: { message: "Twitch or internal API is down", code: 10100 },
});
}
}

View file

@ -1,23 +0,0 @@
import Head from "next/head";
import { ReactElement } from "react";
import HomeLayout from "../layouts/HomeLayout";
import { homeMain } from "../layouts/NavTemplates";
function About() {
return (
<>
<Head>
<title>Contact - toffee</title>
</Head>
<div className="flex min-h-screen flex-col items-center justify-center py-2">
<p>contact</p>
</div>
</>
);
}
About.getLayout = function getLayout(page: ReactElement) {
return <HomeLayout navOptions={homeMain}>{page}</HomeLayout>;
};
export default About;

View file

@ -1,5 +1,4 @@
import { m, Variants } from "framer-motion"; import { m, Variants } from "framer-motion";
import Head from "next/head";
import { ReactElement } from "react"; import { ReactElement } from "react";
import DashLayout from "../../layouts/DashLayout"; import DashLayout from "../../layouts/DashLayout";
@ -7,9 +6,6 @@ import DashLayout from "../../layouts/DashLayout";
function Dashboard() { function Dashboard() {
return ( return (
<> <>
<Head>
<title>Dashboard - toffee</title>
</Head>
<m.div <m.div
className="inline-grid w-full grid-cols-1 pt-2 pl-2 lg:h-full lg:grid-cols-5 lg:pl-0 lg:pr-2" className="inline-grid w-full grid-cols-1 pt-2 pl-2 lg:h-full lg:grid-cols-5 lg:pl-0 lg:pr-2"
variants={gridContainerVariants} variants={gridContainerVariants}
@ -83,7 +79,8 @@ const gridItemVariants: Variants = {
}; };
Dashboard.getLayout = function getLayout(page: ReactElement) { Dashboard.getLayout = function getLayout(page: ReactElement) {
return <DashLayout>{page}</DashLayout>; const metaTags = {};
return <DashLayout metaTags={metaTags}>{page}</DashLayout>;
}; };
export default Dashboard; export default Dashboard;

View file

@ -3,15 +3,13 @@ import { ReactElement, useEffect, useState } from "react";
import HomeLayout from "../layouts/HomeLayout"; import HomeLayout from "../layouts/HomeLayout";
import { homeMain } from "../layouts/NavTemplates"; import { homeMain } from "../layouts/NavTemplates";
import Image from "next/image"; import Image from "next/image";
import Head from "next/head";
function Home() { function Home() {
let api7tvEmotes = `/api/7tv/emotes?c=61ad997effa9aba101bcfddf`;
const [emotesUrls, setEmotes] = useState([]); const [emotesUrls, setEmotes] = useState([]);
const [currentEmote, setCurrentEmote] = useState(0); const [currentEmote, setCurrentEmote] = useState(0);
useEffect(() => { useEffect(() => {
fetch(api7tvEmotes) fetch("/api/emotes")
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
// if error, return // if error, return
@ -19,19 +17,16 @@ function Home() {
return; return;
} }
// get all emote URLs // get all emote URLs
let emoteUrls = data.channel.user.emote_sets[0].emotes.map( let emoteUrls = data["7tv"].channel.map((emote: any) => {
(emote: any) => { let base_url = emote.data.host.url;
let base_url = emote.data.host.url; // get the largest emote size, append it to the base url
// get the largest emote size, append it to the base url let largest = emote.data.host.files[emote.data.host.files.length - 1];
let largest = // if width != height, skip it
emote.data.host.files[emote.data.host.files.length - 1]; if (largest.width !== largest.height) {
// if width != height, skip it return null;
if (largest.width !== largest.height) {
return null;
}
return `https:${base_url}/${largest.name}`;
} }
); return `https:${base_url}/${largest.name}`;
});
// remove null values // remove null values
@ -78,9 +73,6 @@ function Home() {
return ( return (
<> <>
<Head>
<title>Home - toffee</title>
</Head>
<div className="flex h-full w-full flex-col items-center justify-center"> <div className="flex h-full w-full flex-col items-center justify-center">
<div className="inline-grid grid-cols-1 gap-20 text-white md:grid-cols-3"> <div className="inline-grid grid-cols-1 gap-20 text-white md:grid-cols-3">
<m.div <m.div
@ -198,7 +190,14 @@ const slideShowVariants = {
// set the layout for the page, this is used to wrap the page in a layout // set the layout for the page, this is used to wrap the page in a layout
Home.getLayout = function getLayout(page: ReactElement) { Home.getLayout = function getLayout(page: ReactElement) {
return <HomeLayout navOptions={homeMain}>{page}</HomeLayout>; const metaTags = {
title: "Home - toffee",
};
return (
<HomeLayout navOptions={homeMain} metaTags={metaTags}>
{page}
</HomeLayout>
);
}; };
export default Home; export default Home;

View file

@ -1,10 +1,8 @@
import { m, Variants } from "framer-motion"; import { m, Variants } from "framer-motion";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { ReactElement, useEffect, useState } from "react"; import { ReactElement, useEffect, useState } from "react";
import Loading from "../../components/common/Loading"; import Loading from "../../components/common/Loading";
import DashLayout from "../../layouts/DashLayout"; import DashLayout from "../../layouts/DashLayout";
import { fakeDataEntry } from "../api/fakeUsers";
function Ranking() { function Ranking() {
const [sortBy, setSortBy] = useState("netWorth"); const [sortBy, setSortBy] = useState("netWorth");
@ -75,9 +73,6 @@ function Ranking() {
return ( return (
<> <>
<Head>
<title>Ranking - toffee</title>
</Head>
<div className="flex w-full justify-center"> <div className="flex w-full justify-center">
<div className="ml-3 flex w-full max-w-7xl flex-col items-center justify-start font-plusJakarta font-semibold lg:ml-0"> <div className="ml-3 flex w-full max-w-7xl flex-col items-center justify-start font-plusJakarta font-semibold lg:ml-0">
{/* hidden if smaller than lg */} {/* hidden if smaller than lg */}
@ -293,7 +288,11 @@ const rankingDataLineVariants: Variants = {
}; };
Ranking.getLayout = function getLayout(page: ReactElement) { Ranking.getLayout = function getLayout(page: ReactElement) {
return <DashLayout>{page}</DashLayout>; const metaTags = {
title: "Ranking - toffee",
description: "Top investors on toffee",
};
return <DashLayout metaTags={metaTags}>{page}</DashLayout>;
}; };
export default Ranking; export default Ranking;

View file

@ -1,5 +1,4 @@
import { m, Variants } from "framer-motion"; import { m, Variants } from "framer-motion";
import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import { ReactElement } from "react"; import { ReactElement } from "react";
import HomeLayout from "../layouts/HomeLayout"; import HomeLayout from "../layouts/HomeLayout";
@ -8,9 +7,6 @@ import { homeMain } from "../layouts/NavTemplates";
function Team() { function Team() {
return ( return (
<> <>
<Head>
<title>Team - toffee</title>
</Head>
<div className="flex min-h-screen flex-col items-center justify-center py-2"> <div className="flex min-h-screen flex-col items-center justify-center py-2">
<m.div <m.div
className="grid w-[90vw] grid-cols-1 py-2 sm:grid-cols-2 md:grid-cols-4 lg:w-[75vw]" className="grid w-[90vw] grid-cols-1 py-2 sm:grid-cols-2 md:grid-cols-4 lg:w-[75vw]"
@ -136,7 +132,15 @@ const headerVariants: Variants = {
}; };
Team.getLayout = function getLayout(page: ReactElement) { Team.getLayout = function getLayout(page: ReactElement) {
return <HomeLayout navOptions={homeMain}>{page}</HomeLayout>; const metaTags = {
title: "Team - toffee",
description: "Meet the team behind toffee",
};
return (
<HomeLayout navOptions={homeMain} metaTags={metaTags}>
{page}
</HomeLayout>
);
}; };
export default Team; export default Team;

View file

@ -1,26 +1,37 @@
import { m } from "framer-motion"; import { m, Variants } from "framer-motion";
import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { ReactElement, useEffect, useState } from "react"; import { ReactElement, useEffect, useState } from "react";
import DashLayout from "../../../layouts/DashLayout"; import DashLayout from "../../../layouts/DashLayout";
import Image from "next/image"; import Image from "next/image";
import Loading from "../../../components/common/Loading"; import Loading from "../../../components/common/Loading";
import { GetServerSideProps } from "next";
// TODO: Animations interface EmoteURLs {
"7tv": { [key: string]: string };
bttv: { [key: string]: string };
ffz: { [key: string]: string };
twitch: { [key: string]: string };
[key: string]: { [key: string]: string };
}
function UserPage() { interface UserPageProps {
userData: { [key: string]: any };
}
function UserPage(props: UserPageProps) {
const [channelEmotes, setChannelEmotes] = useState<{ const [channelEmotes, setChannelEmotes] = useState<{
[key: string]: { [key: string]: string }; [key: string]: { [key: string]: string };
}>({}); }>({});
const [userData, setUserData] = useState<{ [key: string]: any }>({});
const [errorCode, setErrorCode] = useState<number | null>(null); const [errorCode, setErrorCode] = useState<number | null>(null);
const router = useRouter(); const router = useRouter();
const { username } = router.query; const { username } = router.query;
const title = username ? `${username} - toffee` : "toffee";
useEffect(() => { useEffect(() => {
if (!router.isReady) return; if (!router.isReady) return;
fetch("/api/7tv/emotes?c=61ad997effa9aba101bcfddf") if (props.userData.error) {
setErrorCode(props.userData.error.code);
}
fetch("/api/emotes")
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
// if error, return // if error, return
@ -28,100 +39,65 @@ function UserPage() {
setErrorCode(data.error.code); setErrorCode(data.error.code);
return; return;
} }
// construct js object with emote names as keys and emote urls as values // construct js object with emote names as keys and emote urls for each provider
let emotes: { [key: string]: string } = {}; // 7tv
data.channel.user.emote_sets[0].emotes.forEach((emote: any) => { let emotes: EmoteURLs = { "7tv": {}, bttv: {}, ffz: {}, twitch: {} };
data["7tv"].channel.forEach((emote: any) => {
let base_url = emote.data.host.url; let base_url = emote.data.host.url;
// get the largest emote size, append it to the base url // get the largest emote size, append it to the base url
let largest = emote.data.host.files[emote.data.host.files.length - 1]; let largest = emote.data.host.files[emote.data.host.files.length - 1];
emotes[emote.data.name] = `https:${base_url}/${largest.name}`; emotes["7tv"][emote.data.name] = `https:${base_url}/${largest.name}`;
}); });
// same for global emotes // same for global emotes
data.global.namedEmoteSet.emotes.forEach((emote: any) => { data["7tv"].global.forEach((emote: any) => {
let base_url = emote.data.host.url; let base_url = emote.data.host.url;
let largest = emote.data.host.files[emote.data.host.files.length - 1]; let largest = emote.data.host.files[emote.data.host.files.length - 1];
emotes[emote.data.name] = `https:${base_url}/${largest.name}`; emotes["7tv"][emote.data.name] = `https:${base_url}/${largest.name}`;
}); });
// set 7tv key to channelEmotes // bttv
setChannelEmotes((prev) => ({ ...prev, "7tv": emotes })); data["bttv"].channel.forEach((emote: any) => {
}); emotes["bttv"][
fetch("/api/bttv/emotes?c=56418014") emote.code
.then((res) => res.json()) ] = `https://cdn.betterttv.net/emote/${emote.id}/3x`;
.then((data) => {
if (data.error) {
setErrorCode(data.error.code);
return;
}
let emotes: { [key: string]: string } = {};
data.channel.forEach((emote: any) => {
emotes[emote.code] = `https://cdn.betterttv.net/emote/${emote.id}/3x`;
}); });
data.global.forEach((emote: any) => { data["bttv"].global.forEach((emote: any) => {
emotes[emote.code] = `https://cdn.betterttv.net/emote/${emote.id}/3x`; emotes["bttv"][
emote.code
] = `https://cdn.betterttv.net/emote/${emote.id}/3x`;
}); });
// add as bttv key to channelEmotes // ffz
setChannelEmotes((prev) => ({ ...prev, bttv: emotes })); data["ffz"].channel.forEach((emote: any) => {
});
fetch("/api/ffz/emotes?s=341402")
.then((res) => res.json())
.then((data) => {
if (data.error) {
setErrorCode(data.error.code);
return;
}
let emotes: { [key: string]: string } = {};
data.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 // 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[emote.name] = `https:${ emotes["ffz"][emote.name] = `https:${
emote.urls[ emote.urls[
Math.max(...Object.keys(emote.urls).map((k) => parseInt(k))) Math.max(...Object.keys(emote.urls).map((k) => parseInt(k)))
] ]
}`; }`;
}); });
data.global.forEach((emote: any) => { data["ffz"].global.forEach((emote: any) => {
emotes[emote.name] = `https:${ emotes["ffz"][emote.name] = `https:${
emote.urls[ emote.urls[
Math.max(...Object.keys(emote.urls).map((k) => parseInt(k))) Math.max(...Object.keys(emote.urls).map((k) => parseInt(k)))
] ]
}`; }`;
}); });
// add as ffz key to channelEmotes // twitch
setChannelEmotes((prev) => ({ ...prev, ffz: emotes })); data["twitch"].channel.forEach((emote: any) => {
}); emotes["twitch"][emote.name] = emote.images["url_4x"];
fetch("/api/twitch/emotes?c=56418014")
.then((res) => res.json())
.then((data) => {
if (data.error) {
setErrorCode(data.error.code);
return;
}
let emotes: { [key: string]: string } = {};
data.channel.forEach((emote: any) => {
emotes[emote.name] = emote.images["url_4x"];
}); });
data.global.forEach((emote: any) => { data["twitch"].global.forEach((emote: any) => {
emotes[emote.name] = emote.images["url_4x"]; emotes["twitch"][emote.name] = emote.images["url_4x"];
}); });
// add as twitch key to channelEmotes // set emotes to channelEmotes
setChannelEmotes((prev) => ({ ...prev, ttv: emotes })); setChannelEmotes(emotes);
});
fetch(`/api/fakeUsers?u=${username}`)
.then((res) => res.json())
.then((data) => {
if (data.error) {
setErrorCode(data.error.code);
}
setUserData(data.data);
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.isReady]); }, [router.isReady]);
if (errorCode !== null) { if (errorCode !== null) {
// 20000 = user not found // 20000 = user not found
// 10000 = 7tv api error // 10000 = emote api error
// 10100 = Twitch api error // 10100 = twitch api error
// 10200 = BTTV api error
// 10300 = FFZ api error
const errorMsg = errorCode === 20000 ? "User not found" : "API error"; const errorMsg = errorCode === 20000 ? "User not found" : "API error";
return ( return (
<m.div <m.div
@ -136,62 +112,57 @@ function UserPage() {
} }
// if json is empty, and if channelEmotes is incomplete, show loading screen // if json is empty, and if channelEmotes is incomplete, show loading screen
if ( if (Object.keys(channelEmotes).length < 4) {
Object.keys(channelEmotes).length < 4 ||
!userData ||
Object.keys(userData).length === 0
) {
return ( return (
<div className="flex h-screen w-full items-center justify-center text-3xl"> <div className="flex h-screen w-full items-center justify-center text-3xl">
<Loading /> <Loading />
</div> </div>
); );
} }
console.log(channelEmotes);
return ( return (
<> <>
<Head>
<title>{title}</title>
<meta
name="description"
content={`${username}'s portfolio on toffee`}
/>
</Head>
<div className="flex justify-center"> <div className="flex justify-center">
<div className="mt-7 inline-grid w-[calc(100%-40px)] max-w-5xl grid-cols-10 gap-3 pl-2 font-plusJakarta lg:mt-12 lg:pl-0 lg:pr-2"> <m.div
className="mt-7 inline-grid w-[calc(100%-40px)] max-w-5xl grid-cols-10 gap-3 pl-2 font-plusJakarta lg:mt-12 lg:pl-0 lg:pr-2"
variants={containerVariants}
>
{/* User "banner" */} {/* User "banner" */}
<div className="col-span-10 mb-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3"> <m.div
className="col-span-10 mb-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3"
variants={userBannerVariants}
>
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<div className="relative bottom-[70px] w-[169px]"> <div className="relative bottom-[54px] -left-7 w-[110px] md:bottom-[70px] md:left-0 md:w-[169px]">
<Image <Image
src={userData.avatar_url} src={props.userData.avatar_url}
alt="User avatar" alt="User avatar"
width={140} width={140}
height={140} height={140}
priority priority
className="absolute rounded-lg border-4" className="absolute rounded-lg border-4"
style={{ style={{
borderColor: userData.badges[0] borderColor: props.userData.badges[0]
? userData.badges[0].color ? props.userData.badges[0].color
: "grey", : "grey",
// "glow" effect // "glow" effect
boxShadow: `0px 0px 20px 1px ${ boxShadow: `0px 0px 20px 1px ${
userData.badges[0] props.userData.badges[0]
? userData.badges[0].color ? props.userData.badges[0].color
: "transparent" : "transparent"
}`, }`,
}} }}
/> />
</div> </div>
<div className="flex-col"> <div className="flex-col overflow-hidden overflow-ellipsis whitespace-nowrap">
<h1 className="text-4xl font-semibold text-white"> <h1 className="w-full overflow-hidden overflow-ellipsis whitespace-nowrap text-2xl font-semibold text-white lg:text-4xl">
{userData.name} {props.userData.name}
</h1> </h1>
{/* User's badges */} {/* User's badges */}
<div className="mt-1 flex flex-row text-sm"> <div className="mt-1 flex flex-row text-sm">
{userData.badges ? ( {props.userData.badges ? (
userData.badges.map( props.userData.badges.map(
(badge: { (badge: {
name: string; name: string;
color: string; color: string;
@ -220,14 +191,17 @@ function UserPage() {
$ $
</span> </span>
<span className="text-4xl text-white"> <span className="text-4xl text-white">
{userData.net_worth.toLocaleString("en-US")} {props.userData.net_worth.toLocaleString("en-US")}
</span> </span>
</h1> </h1>
</div> </div>
</div> </div>
</div> </m.div>
{/* Main Container */} {/* Main Container */}
<div className="col-span-10 inline-grid grid-cols-7 gap-3 rounded-2xl lg:col-span-7"> <m.div
className="col-span-10 inline-grid grid-cols-7 gap-3 rounded-2xl lg:col-span-7"
variants={mainContainerVariants}
>
{/* User's Rank/Graph */} {/* User's Rank/Graph */}
<div className="col-span-7 rounded-2xl bg-zinc-800 bg-opacity-70"> <div className="col-span-7 rounded-2xl bg-zinc-800 bg-opacity-70">
<div className="flex flex-row items-center justify-between p-5"> <div className="flex flex-row items-center justify-between p-5">
@ -238,7 +212,7 @@ function UserPage() {
<div className="flex items-center text-3xl font-bold"> <div className="flex items-center text-3xl font-bold">
<span className="text-zinc-400">#</span> <span className="text-zinc-400">#</span>
<span className="text-white"> <span className="text-white">
{userData.rank.toLocaleString("en-US")} {props.userData.rank.toLocaleString("en-US")}
</span> </span>
</div> </div>
</div> </div>
@ -256,7 +230,7 @@ function UserPage() {
$ $
</span> </span>
<span className="text-3xl text-white sm:text-4xl"> <span className="text-3xl text-white sm:text-4xl">
{userData.net_worth.toLocaleString("en-US")} {props.userData.net_worth.toLocaleString("en-US")}
</span> </span>
</h1> </h1>
</div> </div>
@ -275,7 +249,7 @@ function UserPage() {
{errorCode === 20000 ? ( {errorCode === 20000 ? (
<h1 className=" text-zinc-400">{`Could not load assets`}</h1> <h1 className=" text-zinc-400">{`Could not load assets`}</h1>
) : ( ) : (
userData.assets.map( props.userData.assets.map(
(asset: { (asset: {
name: string; name: string;
count: number; count: number;
@ -322,7 +296,7 @@ function UserPage() {
</div> </div>
<div className="flex w-full flex-row items-center justify-center"> <div className="flex w-full flex-row items-center justify-center">
{ {
// show provider logo (7tv, bttv, ffz, ttv) // show provider logo (7tv, bttv, ffz, twitch)
asset.provider === "7tv" ? ( asset.provider === "7tv" ? (
<div className="mr-1 pt-[1px] text-7tv "> <div className="mr-1 pt-[1px] text-7tv ">
<SevenTVLogo /> <SevenTVLogo />
@ -336,7 +310,7 @@ function UserPage() {
<FFZLogo /> <FFZLogo />
</div> </div>
) : ( ) : (
<div className="mr-1 w-4 pt-[1px] text-ttv"> <div className="mr-1 w-4 pt-[1px] text-twitch">
<TwitchLogo /> <TwitchLogo />
</div> </div>
) )
@ -352,29 +326,41 @@ function UserPage() {
)} )}
</div> </div>
</div> </div>
</div> </m.div>
{/* Sidebar */} {/* Sidebar */}
<div className="col-span-10 flex flex-col justify-start md:flex-row lg:col-span-3 lg:flex-col"> <m.div
<div className="center mb-3 mr-3 inline-grid grid-cols-2 gap-3 rounded-2xl bg-zinc-800 bg-opacity-70 p-5 text-xl font-medium lg:mr-0"> className="col-span-10 flex flex-col justify-start md:flex-row lg:col-span-3 lg:flex-col"
variants={sidebarVariants}
>
<m.div
className="center mb-3 mr-3 inline-grid grid-cols-2 gap-3 rounded-2xl bg-zinc-800 bg-opacity-70 p-5 text-xl font-medium lg:mr-0"
variants={sidebarItemVariants}
>
{/* User's Stats, left side is label, right side is value */} {/* User's Stats, left side is label, right side is value */}
<h1>Points</h1> <h1>Points</h1>
<h1>{userData.points.toLocaleString("en-US")}</h1> <h1>{props.userData.points.toLocaleString("en-US")}</h1>
<h1>Shares</h1> <h1>Shares</h1>
<h1>{userData.shares.toLocaleString("en-US")}</h1> <h1>{props.userData.shares.toLocaleString("en-US")}</h1>
<h1>Trades</h1> <h1>Trades</h1>
<h1>{(userData.trades ?? 0).toLocaleString("en-US")}</h1> <h1>{(props.userData.trades ?? 0).toLocaleString("en-US")}</h1>
<h1>Peak rank</h1> <h1>Peak rank</h1>
<h1>{(userData.peak_rank ?? 0).toLocaleString("en-US")}</h1> <h1>{(props.userData.peak_rank ?? 0).toLocaleString("en-US")}</h1>
<h1>Joined</h1> <h1>Joined</h1>
<h1> <h1>
{new Date(userData.joined ?? 0).toLocaleDateString("en-US", { {new Date(props.userData.joined ?? 0).toLocaleDateString(
year: "numeric", "en-US",
month: "short", {
})} year: "numeric",
month: "short",
}
)}
</h1> </h1>
</div> </m.div>
{/* User's Favorite Emote */} {/* User's Favorite Emote */}
<div className="flex flex-col rounded-2xl bg-zinc-800 bg-opacity-70"> <m.div
className="flex flex-col rounded-2xl bg-zinc-800 bg-opacity-70"
variants={sidebarItemVariants}
>
<div className="h-11 w-full rounded-t-2xl bg-pink-400"> <div className="h-11 w-full rounded-t-2xl bg-pink-400">
<h1 className="m-1 text-center text-2xl font-bold"> <h1 className="m-1 text-center text-2xl font-bold">
Favorite Emote Favorite Emote
@ -385,9 +371,9 @@ function UserPage() {
This user has not yet set a favorite emote. This user has not yet set a favorite emote.
</p> </p>
</div> </div>
</div> </m.div>
</div> </m.div>
</div> </m.div>
</div> </div>
</> </>
); );
@ -479,8 +465,130 @@ const TwitchLogo = () => {
); );
}; };
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<UserPageProps> = 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
);
const res = await fetch(url);
let user = await res.json();
// return error in user.data if user not found
if (user.error) {
user = { data: user };
}
return { props: { userData: user.data } };
};
UserPage.getLayout = function getLayout(page: ReactElement) { UserPage.getLayout = function getLayout(page: ReactElement) {
return <DashLayout>{page}</DashLayout>; const { userData } = page.props;
const metaTags = {
title: !userData.error
? `${userData.name ?? "User 404"} - toffee`
: "User 404 - toffee",
description: !userData.error
? `${userData.name}'s portfolio on toffee`
: "Couldn't find that user on toffee... :(",
imageUrl: !userData.error ? userData.avatar_url : undefined,
misc: {
"twitter:card": "summary",
},
};
return <DashLayout metaTags={metaTags}>{page}</DashLayout>;
}; };
export default UserPage; export default UserPage;

View file

@ -16,7 +16,7 @@ module.exports = {
colors: { colors: {
"7tv": "#4fc2bc", "7tv": "#4fc2bc",
bttv: "#d50014", bttv: "#d50014",
ttv: "#9146FF", twitch: "#9146FF",
}, },
}, },
}, },