manage and cleanup some types

This commit is contained in:
3zachm 2023-01-28 01:46:11 -08:00
parent c7766aeda3
commit 1393df6fd0
9 changed files with 175 additions and 141 deletions

6
interfaces/APIError.ts Normal file
View file

@ -0,0 +1,6 @@
export default interface APIError {
error: {
message: string;
code: number;
};
}

5
interfaces/UserAsset.ts Normal file
View file

@ -0,0 +1,5 @@
export default interface UserAsset {
name: string;
count: number;
provider: "7tv" | "bttv" | "ffz" | "twitch";
}

5
interfaces/UserBadge.ts Normal file
View file

@ -0,0 +1,5 @@
export default interface UserBadge {
name: string;
color: string;
priority: number;
}

View file

@ -0,0 +1,12 @@
import UserAsset from "./UserAsset";
import UserBadge from "./UserBadge";
export default interface UserFakeDataEntry {
id: number;
name: string;
points: number;
daily_change: number;
daily_change_percent: number;
assets: UserAsset[];
badges: UserBadge[];
}

View file

@ -0,0 +1,5 @@
import UserJSONEntry from "./UserJSONEntry";
export default interface UserFakeDataJSON {
data: UserJSONEntry[];
}

View file

@ -0,0 +1,8 @@
import UserFakeDataEntry from "./UserFakeDataEntry";
export default interface UserJSONEntry extends UserFakeDataEntry {
net_worth: number;
shares: number;
avatar_url: string;
rank: number;
}

View file

@ -1,4 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import UserBadge from "../../interfaces/UserBadge";
import UserFakeDataEntry from "../../interfaces/UserFakeDataEntry";
import UserJSONEntry from "../../interfaces/UserJSONEntry";
import { createRedisInstance } from "../../misc/redis"; import { createRedisInstance } from "../../misc/redis";
import { getUserByName } from "../../misc/TwitchAPI"; import { getUserByName } from "../../misc/TwitchAPI";
import { fakePrices } from "./fakePrices"; import { fakePrices } from "./fakePrices";
@ -23,43 +26,14 @@ export default async function handler(
return; return;
} }
let data = fakeData; let userJSON: UserJSONEntry[];
// calculate all net worths let userList: UserFakeDataEntry[] = fakeData;
data = data.map((user) => { let avatarURL = "/img/logo.webp"; // default avatar
return {
...user,
net_worth:
user.points +
user.assets.reduce(
(a, b) => a + b.count * (fakePrices[b.name] ?? 0),
0
),
};
});
// calculate ranking based on net worth
data = data.sort((a, b) => (b.net_worth ?? 0) - (a.net_worth ?? 0));
data = data.map((user, i) => {
return {
...user,
rank: i + 1,
// calculate total assets held (shares)
shares: user.assets.reduce((a, b) => a + b.count, 0),
// sort users badges by priority
badges: (user.badges ?? []).sort((a, b) => b.priority - a.priority ?? 0),
// sort users assets by total value
assets: user.assets.sort(
(a, b) =>
(fakePrices[b.name] ?? 0) * b.count -
(fakePrices[a.name] ?? 0) * a.count
),
};
});
// if username is specified, only return that user // if username is specified, only return that user
if (username) { if (username) {
// if user does not exist, return error // filter for user, add required types
data = data.filter((u) => u.name === username); userList = userList.filter((u) => u.name === username);
if (data.length === 0) { if (userList.length === 0) {
res res
.status(404) .status(404)
.json({ error: { message: "User not found", code: 20000 } }); .json({ error: { message: "User not found", code: 20000 } });
@ -80,92 +54,95 @@ export default async function handler(
// temp who cares // temp who cares
twitchData.data[0] = {}; twitchData.data[0] = {};
twitchData.data[0].profile_image_url = "/img/logo.webp"; twitchData.data[0].profile_image_url = "/img/logo.webp";
} else {
avatarURL = twitchData.data[0].profile_image_url;
} }
// add users profile picture url
data = data.map((u) => {
return {
...u,
avatar_url: twitchData.data[0].profile_image_url ?? "",
};
});
res.status(200).json({ data: data[0] });
return;
} }
userJSON = userList.map((user) => {
return {
...user,
// calculate total assets held (shares)
shares: user.assets.reduce((a, b) => a + b.count, 0),
// sort users badges by priority
badges: (user.badges ?? []).sort((a, b) => b.priority - a.priority ?? 0),
avatar_url: avatarURL,
rank: 0,
// sort users assets by total value
assets: user.assets.sort(
(a, b) =>
(fakePrices[b.name] ?? 0) * b.count -
(fakePrices[a.name] ?? 0) * a.count
),
// calculate net worth
net_worth:
user.points +
user.assets.reduce(
(a, b) => a + b.count * (fakePrices[b.name] ?? 0),
0
),
};
});
// calculate ranking based on net worth
userJSON = userJSON.sort((a, b) => (b.net_worth ?? 0) - (a.net_worth ?? 0));
userJSON = userJSON.map((u, i) => {
return {
...u,
rank: i + 1,
};
});
if (sortBy) { if (sortBy) {
if (sortBy === "daily_change") { if (sortBy === "daily_change") {
data = data.sort((a, b) => b.daily_change - a.daily_change); userJSON = userJSON.sort((a, b) => b.daily_change - a.daily_change);
} else if (sortBy === "daily_change_percent") { } else if (sortBy === "daily_change_percent") {
data = data.sort( userJSON = userJSON.sort(
(a, b) => b.daily_change_percent - a.daily_change_percent (a, b) => b.daily_change_percent - a.daily_change_percent
); );
} else if (sortBy === "shares") { } else if (sortBy === "shares") {
data = data.sort((a, b) => (b.shares ?? 0) - (a.shares ?? 0)); userJSON = userJSON.sort((a, b) => (b.shares ?? 0) - (a.shares ?? 0));
} else if (sortBy === "points") { } else if (sortBy === "points") {
data = data.sort((a, b) => b.points - a.points); userJSON = userJSON.sort((a, b) => b.points - a.points);
} else if (sortBy === "name") { } else if (sortBy === "name") {
data = data.sort((a, b) => a.name.localeCompare(b.name)); userJSON = userJSON.sort((a, b) => a.name.localeCompare(b.name));
} }
if (sortAsc === "true") { if (sortAsc === "true") {
// slow but only needed for temporary fake data anyway // slow but only needed for temporary fake data anyway
data = data.reverse(); userJSON = userJSON.reverse();
} }
} }
// fake loading time // fake loading time
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, 250 + Math.random() * 1000) setTimeout(resolve, 250 + Math.random() * 1000)
); );
res.status(200).json({ data: data }); res.status(200).json({ data: userJSON });
} }
interface asset { const adminBadge: UserBadge = {
name: string;
count: number;
provider: "7tv" | "bttv" | "ffz" | "twitch";
}
interface fakeDataEntry {
id: number;
name: string;
points: number;
daily_change: number;
daily_change_percent: number;
assets: asset[];
net_worth?: number;
shares?: number;
avatar_url?: string;
badges?: badge[];
}
interface badge {
name: string;
color: string;
priority: number;
}
const adminBadge: badge = {
name: "Admin", name: "Admin",
color: "#CC3333", color: "#CC3333",
priority: 99999, priority: 99999,
}; };
const CEOBadge: badge = { const CEOBadge: UserBadge = {
name: "CEO", name: "CEO",
color: "#F97316", color: "#F97316",
priority: 100000, priority: 100000,
}; };
const webDevBadge: badge = { const webDevBadge: UserBadge = {
name: "Web Dev", name: "Web Dev",
color: "#a855f7", color: "#a855f7",
priority: 50000, priority: 50000,
}; };
const botDevBadge: badge = { const botDevBadge: UserBadge = {
name: "Bot Dev", name: "Bot Dev",
color: "#48b2f1", color: "#48b2f1",
priority: 50001, priority: 50001,
}; };
const fakeData: fakeDataEntry[] = [ const fakeData: UserFakeDataEntry[] = [
{ {
id: 4, id: 4,
name: "3zachm", name: "3zachm",
@ -384,6 +361,7 @@ const fakeData: fakeDataEntry[] = [
provider: "7tv", provider: "7tv",
}, },
], ],
badges: [],
}, },
{ {
id: 6, id: 6,
@ -423,6 +401,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 7, id: 7,
@ -462,6 +441,7 @@ const fakeData: fakeDataEntry[] = [
provider: "ffz", provider: "ffz",
}, },
], ],
badges: [],
}, },
{ {
id: 8, id: 8,
@ -496,6 +476,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 9, id: 9,
@ -535,6 +516,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 10, id: 10,
@ -574,6 +556,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 11, id: 11,
@ -613,6 +596,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 12, id: 12,
@ -657,6 +641,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 13, id: 13,
@ -686,6 +671,7 @@ const fakeData: fakeDataEntry[] = [
provider: "7tv", provider: "7tv",
}, },
], ],
badges: [],
}, },
{ {
id: 14, id: 14,
@ -720,6 +706,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 15, id: 15,
@ -754,6 +741,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 16, id: 16,
@ -788,6 +776,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 17, id: 17,
@ -822,6 +811,7 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
{ {
id: 18, id: 18,
@ -856,7 +846,6 @@ const fakeData: fakeDataEntry[] = [
provider: "twitch", provider: "twitch",
}, },
], ],
badges: [],
}, },
]; ];
export type { fakeDataEntry };

View file

@ -2,6 +2,7 @@ import { m, Variants } from "framer-motion";
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 UserJSONEntry from "../../interfaces/UserJSONEntry";
import DashLayout from "../../layouts/DashLayout"; import DashLayout from "../../layouts/DashLayout";
function Ranking() { function Ranking() {
@ -158,47 +159,44 @@ function Ranking() {
> >
{ {
// generate table rows // generate table rows
fakeData.map( fakeData.map((entry: UserJSONEntry, index: number) => {
(entry: { [key: string]: any }, index: number) => { // if daily change is negative, make it red
// if daily change is negative, make it red let changeClass = " text-lime-500";
let changeClass = " text-lime-500"; if (entry.daily_change_percent < 0) {
if (entry.daily_change_percent < 0) { changeClass = " text-red-500";
changeClass = " text-red-500";
}
return (
<m.div
className="inline-grid w-full grid-flow-col grid-cols-[1fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-700 px-5 py-2 text-right md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"
key={entry.id}
variants={rankingDataLineVariants}
>
<h1 className="text-left md:text-center">
{index + 1}
</h1>
<Link
href={`/user/${entry.name}`}
className="overflow-hidden"
>
<h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left">
{entry.name}
</h1>
</Link>
<h1>{entry.net_worth.toLocaleString("en-US")}</h1>
<h1 className="hidden md:block">
{entry.points.toLocaleString("en-US")}
</h1>
<h1 className="hidden md:block">
{entry.shares.toLocaleString("en-US")}
</h1>
<h1 className={changeClass}>
{(
Math.round(entry.daily_change_percent * 1000) /
10
).toFixed(1) + "%"}
</h1>
</m.div>
);
} }
) return (
<m.div
className="inline-grid w-full grid-flow-col grid-cols-[1fr_4fr_3fr_2fr] gap-2 border-b-2 border-zinc-700 px-5 py-2 text-right md:grid-cols-[0.5fr_4fr_repeat(3,_2fr)_1.5fr]"
key={entry.id}
variants={rankingDataLineVariants}
>
<h1 className="text-left md:text-center">
{index + 1}
</h1>
<Link
href={`/user/${entry.name}`}
className="overflow-hidden"
>
<h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left">
{entry.name}
</h1>
</Link>
<h1>{entry.net_worth.toLocaleString("en-US")}</h1>
<h1 className="hidden md:block">
{entry.points.toLocaleString("en-US")}
</h1>
<h1 className="hidden md:block">
{entry.shares.toLocaleString("en-US")}
</h1>
<h1 className={changeClass}>
{(
Math.round(entry.daily_change_percent * 1000) / 10
).toFixed(1) + "%"}
</h1>
</m.div>
);
})
} }
</m.div> </m.div>
) )

View file

@ -5,6 +5,8 @@ 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"; import { GetServerSideProps } from "next";
import UserJSONEntry from "../../../interfaces/UserJSONEntry";
import APIError from "../../../interfaces/APIError";
interface EmoteURLs { interface EmoteURLs {
"7tv": { [key: string]: string }; "7tv": { [key: string]: string };
@ -15,7 +17,8 @@ interface EmoteURLs {
} }
interface UserPageProps { interface UserPageProps {
userData: { [key: string]: any }; userData: UserJSONEntry;
serverError: APIError | null;
} }
function UserPage(props: UserPageProps) { function UserPage(props: UserPageProps) {
@ -28,8 +31,9 @@ function UserPage(props: UserPageProps) {
useEffect(() => { useEffect(() => {
if (!router.isReady) return; if (!router.isReady) return;
if (props.userData.error) { // if it is of
setErrorCode(props.userData.error.code); if (props.serverError) {
setErrorCode(props.serverError.error.code);
} }
fetch("/api/emotes") fetch("/api/emotes")
.then((res) => res.json()) .then((res) => res.json())
@ -342,18 +346,15 @@ function UserPage(props: UserPageProps) {
<h1>Shares</h1> <h1>Shares</h1>
<h1>{props.userData.shares.toLocaleString("en-US")}</h1> <h1>{props.userData.shares.toLocaleString("en-US")}</h1>
<h1>Trades</h1> <h1>Trades</h1>
<h1>{(props.userData.trades ?? 0).toLocaleString("en-US")}</h1> <h1>{(0).toLocaleString("en-US")}</h1>
<h1>Peak rank</h1> <h1>Peak rank</h1>
<h1>{(props.userData.peak_rank ?? 0).toLocaleString("en-US")}</h1> <h1>{(0).toLocaleString("en-US")}</h1>
<h1>Joined</h1> <h1>Joined</h1>
<h1> <h1>
{new Date(props.userData.joined ?? 0).toLocaleDateString( {new Date(0).toLocaleDateString("en-US", {
"en-US", year: "numeric",
{ month: "short",
year: "numeric", })}
month: "short",
}
)}
</h1> </h1>
</m.div> </m.div>
{/* User's Favorite Emote */} {/* User's Favorite Emote */}
@ -565,25 +566,30 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (
`/api/fakeUsers?u=${context.query.username}`, `/api/fakeUsers?u=${context.query.username}`,
process.env.NEXT_PUBLIC_URL process.env.NEXT_PUBLIC_URL
); );
// TODO: add error handling
const res = await fetch(url); const res = await fetch(url);
let user = await res.json(); let user = await res.json();
// return error in user.data if user not found
if (user.error) { if (user.error) {
user = { data: user }; return {
props: {
userData: user,
serverError: user,
},
};
} }
return { props: { userData: user.data } }; return { props: { userData: user.data[0], serverError: null } };
}; };
UserPage.getLayout = function getLayout(page: ReactElement) { UserPage.getLayout = function getLayout(page: ReactElement) {
const { userData } = page.props; const { userData, serverError } = page.props;
const metaTags = { const metaTags = {
title: !userData.error title: !serverError
? `${userData.name ?? "User 404"} - toffee` ? `${userData.name ?? "User 404"} - toffee`
: "User 404 - toffee", : "User 404 - toffee",
description: !userData.error description: !serverError
? `${userData.name}'s portfolio on toffee` ? `${userData.name}'s portfolio on toffee`
: "Couldn't find that user on toffee... :(", : "Couldn't find that user on toffee... :(",
imageUrl: !userData.error ? userData.avatar_url : undefined, imageUrl: !serverError ? userData.avatar_url : undefined,
misc: { misc: {
"twitter:card": "summary", "twitter:card": "summary",
}, },