diff --git a/components/dashboard/NavBar.tsx b/components/dashboard/NavBar.tsx index af25a5f..6d3277f 100644 --- a/components/dashboard/NavBar.tsx +++ b/components/dashboard/NavBar.tsx @@ -24,7 +24,7 @@ function NavBar() { variants={navIconVariants} className="pr-5 lg:pr-0 lg:pt-3 lg:pb-3" > - <ActiveLink href="/dashboard/ranking"> + <ActiveLink href="/ranking"> <RankingIcon /> </ActiveLink> </m.div> diff --git a/layouts/DashLayout.tsx b/layouts/DashLayout.tsx index 9235338..612004c 100644 --- a/layouts/DashLayout.tsx +++ b/layouts/DashLayout.tsx @@ -8,7 +8,6 @@ import { import Head from "next/head"; import { useRouter } from "next/router"; import NavBar from "../components/dashboard/NavBar"; -import { NavTemplate } from "./NavTemplates"; interface DashLayoutProps { children: React.ReactNode; @@ -26,6 +25,7 @@ function DashLayout(props: DashLayoutProps) { variants={containerVariants} > <Head> + <meta name="viewport" content="initial-scale=0.8" /> <title>Dashboard - toffee</title> <meta name="description" content="Dashboard statistics for toffee" /> <link rel="icon" href="/favicon.ico" /> diff --git a/misc/7TVAPI.tsx b/misc/7TVAPI.tsx index b7e6e78..5ffebf3 100644 --- a/misc/7TVAPI.tsx +++ b/misc/7TVAPI.tsx @@ -1,8 +1,7 @@ -import Redis from "ioredis"; - -let redis = new Redis(process.env.REDIS_URL); +import type RedisInstance from "ioredis"; async function applyCache( + redis: RedisInstance, key: string, query: string, gql: boolean, @@ -11,7 +10,7 @@ async function applyCache( if (await redis.get(key)) { return JSON.parse((await redis.get(key)) as string); } else { - const response = await fetchEndpoint(query, gql); + const response = await fetchEndpoint(redis, query, gql); if (response != null) { await redis.set(key, JSON.stringify(response), "EX", cacheTime); } @@ -19,7 +18,11 @@ async function applyCache( } } -async function fetchEndpoint(query: string, gql: boolean = false) { +async function fetchEndpoint( + redis: RedisInstance, + query: string, + gql: boolean = false +) { if (await redis.get("7TV.RATE_LIMIT")) { await new Promise((resolve) => setTimeout(resolve, 1000)); } else { @@ -50,7 +53,7 @@ async function fetchGQL(query: string) { return json; } -async function getGlobalEmotes() { +async function getGlobalEmotes(redis: RedisInstance) { const gqlQuery = `query { namedEmoteSet(name: GLOBAL) { emote_count @@ -78,10 +81,10 @@ async function getGlobalEmotes() { } } }`; - return await applyCache("7TV.GLOBAL_EMOTES", gqlQuery, true, 3600); + return await applyCache(redis, "7TV.GLOBAL_EMOTES", gqlQuery, true, 3600); } -async function getChannelEmotes(channelID: string) { +async function getChannelEmotes(redis: RedisInstance, channelID: string) { const gqlQuery = `query { user(id: "${channelID}") { emote_sets { @@ -112,6 +115,7 @@ async function getChannelEmotes(channelID: string) { } }`; return await applyCache( + redis, "7TV.CHANNEL_EMOTES_" + channelID, gqlQuery, true, diff --git a/misc/BTTVAPI.tsx b/misc/BTTVAPI.tsx new file mode 100644 index 0000000..675c58c --- /dev/null +++ b/misc/BTTVAPI.tsx @@ -0,0 +1,58 @@ +import type RedisInstance from "ioredis"; + +async function applyCache( + redis: RedisInstance, + key: string, + query: string, + + cacheTime: number +) { + if (await redis.get(key)) { + return JSON.parse((await redis.get(key)) as string); + } else { + const response = await fetchEndpoint(redis, query); + if (response != null) { + await redis.set(key, JSON.stringify(response), "EX", cacheTime); + } + return response; + } +} + +async function fetchEndpoint(redis: RedisInstance, query: string) { + if (await redis.get("BTTV.RATE_LIMIT")) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + await redis.set("BTTV.RATE_LIMIT", "1", "EX", 1); + } + + const requestHeaders = new Headers(); + requestHeaders.append("Content-Type", "application/json"); + requestHeaders.append("User-Agent", "toffee-web/indev"); + + const response = await fetch(query, { + headers: requestHeaders, + }); + + const json = await response.json(); + return json; +} + +async function getGlobalEmotes(redis: RedisInstance) { + return await applyCache( + redis, + "BTTV.GLOBAL_EMOTES", + "https://api.betterttv.net/3/cached/emotes/global", + 3600 + ); +} + +async function getUserByID(redis: RedisInstance, channelID: string) { + return await applyCache( + redis, + `BTTV.CHANNEL_EMOTES.${channelID}`, + `https://api.betterttv.net/3/cached/users/twitch/${channelID}`, + 3600 + ); +} + +export { getGlobalEmotes, getUserByID }; diff --git a/misc/FFZAPI.tsx b/misc/FFZAPI.tsx new file mode 100644 index 0000000..06cb411 --- /dev/null +++ b/misc/FFZAPI.tsx @@ -0,0 +1,58 @@ +import type RedisInstance from "ioredis"; + +async function applyCache( + redis: RedisInstance, + key: string, + query: string, + + cacheTime: number +) { + if (await redis.get(key)) { + return JSON.parse((await redis.get(key)) as string); + } else { + const response = await fetchEndpoint(redis, query); + if (response != null) { + await redis.set(key, JSON.stringify(response), "EX", cacheTime); + } + return response; + } +} + +async function fetchEndpoint(redis: RedisInstance, query: string) { + if (await redis.get("FFZ.RATE_LIMIT")) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + await redis.set("FFZ.RATE_LIMIT", "1", "EX", 1); + } + + const requestHeaders = new Headers(); + requestHeaders.append("Content-Type", "application/json"); + requestHeaders.append("User-Agent", "toffee-web/indev"); + + const response = await fetch(query, { + headers: requestHeaders, + }); + + const json = await response.json(); + return json; +} + +async function getGlobalEmotes(redis: RedisInstance) { + return await applyCache( + redis, + "FFZ.GLOBAL_EMOTES", + "https://api.frankerfacez.com/v1/set/global", + 3600 + ); +} + +async function getEmoteSet(redis: RedisInstance, setID: string) { + return await applyCache( + redis, + `FFZ.EMOTE_SET.${setID}`, + `https://api.frankerfacez.com/v1/set/${setID}`, + 3600 + ); +} + +export { getGlobalEmotes, getEmoteSet }; diff --git a/misc/TwitchAPI.tsx b/misc/TwitchAPI.tsx new file mode 100644 index 0000000..ab16d86 --- /dev/null +++ b/misc/TwitchAPI.tsx @@ -0,0 +1,94 @@ +import RedisInstance from "ioredis"; + +async function applyCache( + redis: RedisInstance, + key: string, + query: string, + cacheTime: number +) { + if (await redis.get(key)) { + return JSON.parse((await redis.get(key)) as string); + } else { + const response = await fetchEndpoint(redis, query); + if (response != null) { + await redis.set(key, JSON.stringify(response), "EX", cacheTime); + } + return response; + } +} + +async function authTwitch(redis: RedisInstance) { + let auth = await redis.get("TWITCH.AUTH"); + if (auth) { + return auth; + } else { + const response = await fetch( + `https://id.twitch.tv/oauth2/token?client_id=${process.env.TWITCH_CLIENT_ID}&client_secret=${process.env.TWITCH_SECRET}&grant_type=client_credentials`, + { + method: "POST", + } + ); + const json = await response.json(); + await redis.set("TWITCH.OAUTH", json.access_token, "EX", json.expires_in); + + return json.access_token; + } +} + +async function fetchEndpoint(redis: RedisInstance, query: string) { + if (await redis.get("TWITCH.RATE_LIMIT")) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + await redis.set("TWITCH.RATE_LIMIT", "1", "EX", 1); + } + + const auth = await authTwitch(redis); + + const requestHeaders = new Headers(); + requestHeaders.append("Client-ID", process.env.TWITCH_CLIENT_ID ?? ""); + requestHeaders.append("Authorization", `Bearer ${auth}`); + + const response = await fetch(query, { + headers: requestHeaders, + }); + const json = await response.json(); + return json; +} + +async function getUserByName(redis: RedisInstance, username: string) { + return await applyCache( + redis, + "TWITCH.USER_" + username, + `https://api.twitch.tv/helix/users?login=${username}`, + 600 + ); +} + +async function getUserByID(redis: RedisInstance, userID: string) { + return await applyCache( + redis, + "TWITCH.USER_" + userID, + `https://api.twitch.tv/helix/users?id=${userID}`, + 600 + ); +} + +async function getGlobalEmotes(redis: RedisInstance) { + return await applyCache( + redis, + "TWITCH.GLOBAL_EMOTES", + `https://api.twitch.tv/helix/chat/emotes/global`, + 3600 + ); +} + +async function getChannelEmotes(redis: RedisInstance, channelID: string) { + return await applyCache( + redis, + "TWITCH.CHANNEL_EMOTES_" + channelID, + `https://api.twitch.tv/helix/chat/emotes?broadcaster_id=${channelID}`, + 600 + ); +} + +export { getUserByName, getUserByID, getGlobalEmotes, getChannelEmotes }; diff --git a/misc/redis.ts b/misc/redis.ts new file mode 100644 index 0000000..d118502 --- /dev/null +++ b/misc/redis.ts @@ -0,0 +1,55 @@ +// https://makerkit.dev/blog/tutorials/nextjs-redis (I got tired of having redis issues) +import Redis, { RedisOptions } from "ioredis"; + +const configuration = { + redis: { + host: process.env.REDIS_HOST ?? "localhost", + port: parseInt(process.env.REDIS_PORT ?? "6379"), + password: process.env.REDIS_PASSWORD ?? null, + }, +}; + +function getRedisConfiguration(): { + port: number; + host: string; + password: string | null; +} { + return configuration.redis; +} + +export function createRedisInstance(config = getRedisConfiguration()) { + try { + const options: RedisOptions = { + host: config.host, + lazyConnect: true, + showFriendlyErrorStack: true, + enableAutoPipelining: true, + maxRetriesPerRequest: 0, + retryStrategy: (times: number) => { + if (times > 3) { + throw new Error(`[Redis] Could not connect after ${times} attempts`); + } + + return Math.min(times * 200, 1000); + }, + }; + + if (config.port) { + options.port = config.port; + } + + if (config.password) { + options.password = config.password; + } + + const redis = new Redis(options); + + redis.on("error", (error: unknown) => { + console.warn("[Redis] Error connecting", error); + }); + + return redis; + } catch (e) { + throw new Error(`[Redis] Could not create a Redis instance`); + } +} diff --git a/next.config.js b/next.config.js index bd38de3..6f65b5a 100644 --- a/next.config.js +++ b/next.config.js @@ -3,7 +3,13 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, images: { - domains: ["cdn.discordapp.com", "static-cdn.jtvnw.net", "cdn.7tv.app"], + domains: [ + "cdn.discordapp.com", + "static-cdn.jtvnw.net", + "cdn.7tv.app", + "cdn.betterttv.net", + "cdn.frankerfacez.com", + ], }, }; diff --git a/package-lock.json b/package-lock.json index 841dc3b..0468ee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "eslint": "8.28.0", "eslint-config-next": "13.0.4", "framer-motion": "^7.6.19", - "ioredis": "^4.28.5", + "ioredis": "^5.2.5", "next": "13.0.4", "react": "18.2.0", "react-dom": "18.2.0", @@ -122,6 +122,11 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@motionone/animation": { "version": "10.15.1", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz", @@ -1342,9 +1347,9 @@ } }, "node_modules/denque": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", - "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", "engines": { "node": ">=0.10" } @@ -2515,38 +2520,28 @@ } }, "node_modules/ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.5.tgz", + "integrity": "sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==", "dependencies": { + "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.0.1", "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" }, "engines": { - "node": ">=6" + "node": ">=12.22.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/ioredis" } }, - "node_modules/ioredis/node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "engines": { - "node": ">=6" - } - }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -2841,9 +2836,9 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dependencies": { "minimist": "^1.2.0" }, @@ -3050,11 +3045,6 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -4025,11 +4015,6 @@ "node": ">=8.10.0" } }, - "node_modules/redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" - }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -5075,6 +5060,11 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, + "@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "@motionone/animation": { "version": "10.15.1", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz", @@ -5868,9 +5858,9 @@ "dev": true }, "denque": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", - "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" }, "detect-libc": { "version": "2.0.1", @@ -6728,28 +6718,19 @@ } }, "ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.5.tgz", + "integrity": "sha512-7HKo/ClM2DGLRXdFq8ruS3Uuadensz4A76wPOU0adqlOqd1qkhoLPDaBhmVhUhNGpB+J65/bhLmNB8DDY99HJQ==", "requires": { + "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.0.1", "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" - }, - "dependencies": { - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" - } } }, "is-arrayish": { @@ -6940,9 +6921,9 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "requires": { "minimist": "^1.2.0" } @@ -7094,11 +7075,6 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -7736,11 +7712,6 @@ "picomatch": "^2.2.1" } }, - "redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" - }, "redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", diff --git a/package.json b/package.json index 51afc68..dab8fe0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "eslint": "8.28.0", "eslint-config-next": "13.0.4", "framer-motion": "^7.6.19", - "ioredis": "^4.28.5", + "ioredis": "^5.2.5", "next": "13.0.4", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/pages/api/7tv/emotes.ts b/pages/api/7tv/emotes.ts index ab4694e..4718558 100644 --- a/pages/api/7tv/emotes.ts +++ b/pages/api/7tv/emotes.ts @@ -1,4 +1,5 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { createRedisInstance } from "../../../misc/redis"; import { getChannelEmotes, getGlobalEmotes } from "../../../misc/7TVAPI"; type Data = { @@ -9,10 +10,19 @@ export default async function handler( req: NextApiRequest, res: NextApiResponse<Data> ) { - const channel = req.query.c - ? await getChannelEmotes(req.query.c as string) - : undefined; - const global = await getGlobalEmotes(); + const redis = createRedisInstance(); - res.status(200).json({ channel, global }); + 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 } }); + } } diff --git a/pages/api/bttv/emotes.ts b/pages/api/bttv/emotes.ts new file mode 100644 index 0000000..fbdf506 --- /dev/null +++ b/pages/api/bttv/emotes.ts @@ -0,0 +1,30 @@ +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 }, + }); + } +} diff --git a/pages/api/fakePrices.ts b/pages/api/fakePrices.ts new file mode 100644 index 0000000..85fe5e9 --- /dev/null +++ b/pages/api/fakePrices.ts @@ -0,0 +1,804 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +type Data = { + [key: string]: any; +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<Data> +) { + const emote = req.query.emote ? (req.query.emote as string) : undefined; + if (!emote) { + res.status(200).json({ data: fakePrices }); + return; + } + res.status(200).json({ data: fakePrices[emote] }); +} + +const fakePrices: { [key: string]: number } = { + "3Head": 521, + "4Health": 801, + ":3": 104, + ":(": 799, + AAAA: 717, + AAAAUUUUUUGHHHHHHH: 342, + AINTNAURWAY: 195, + AREYOUAFEMBOY: 135, + AREYOUAGIRL: 494, + AYAYAjam: 907, + Adge: 646, + AlienPls3: 460, + Amonge: 608, + AngelThump: 195, + AnnyLebronJam: 967, + Aware: 571, + BAND: 472, + BASED: 794, + BEGGING: 365, + BLUBBERS: 211, + BLUBBERSWTF: 713, + BOOBEST: 107, + BOTHA: 472, + BRUHgers: 536, + Baby: 927, + Binoculars: 441, + Blindge: 426, + BocchiPossessed: 545, + Bruhgi: 779, + CEASE: 708, + COCKA: 656, + CapybaraStare: 295, + CatChest: 267, + ChadMeeting: 454, + Chatting: 330, + ChugU: 661, + Clueless: 888, + Coldge: 314, + Comfi: 187, + Copege: 841, + Corpa: 536, + CumTime: 199, + DIESOFAYAYA: 657, + DIESOFCRINGE: 320, + DONUT: 57, + Danki: 864, + Deadge: 322, + Despairge: 437, + Deutschge: 626, + EDM: 572, + FLASHBANG: 308, + FeelsWeakMan: 88, + Fishinge: 956, + Flushed: 290, + FluteTime: 557, + GOODONE: 804, + GachiPls: 415, + Gambage: 876, + Gayge: 434, + GetOutOfMyHead: 338, + GoodGirl: 761, + GroupWankge: 540, + GuysRefreshChatterinoIUploadedAnotherEmote: 677, + HACKERMANS: 85, + HUH: 582, + HYPERS: 142, + Homi: 745, + HopOnBlueArchive: 386, + IDC: 277, + ILOST: 894, + JIGACHAD: 865, + Jamgie: 156, + Jammies: 308, + Joel: 586, + KEKW: 356, + KKool: 821, + KoroneOkite: 90, + LETHIMCOOK: 940, + LICKA: 866, + LUBBERS: 808, + LULE: 244, + Lagging: 314, + LickAnnyThighs: 66, + Life: 894, + LoveForever: 21, + MODS: 180, + MYHEART: 122, + MaN: 181, + MadgeJuice: 926, + MadgeTime: 481, + Madgeclap: 534, + Malding: 940, + Maxwell: 656, + MyHonestReaction: 314, + NAUR: 93, + NGOthis: 454, + NOBOOBS: 855, + NODDERS: 836, + NOTED: 293, + Naruga: 572, + Naruge: 872, + Nerdge: 46, + NessieTwerk: 666, + OFFLINECHAT: 780, + OMEGA: 934, + OMG: 631, + "OhTheMiseryEverybodyWantsToBeMyEnemySpareTheSympathyEverybodyWantsToBeMyEnemy-y-y-y-y": 798, + Okayeg: 485, + OkaygeBusiness: 493, + Oldge: 763, + OrangexddRun: 96, + PEPELEPSY: 122, + PLEASE: 607, + PagMan: 775, + PantsGrab: 503, + Party: 181, + PeepoKittyHug: 360, + PensiveWobble: 843, + PepeClown: 384, + PepeNPC: 131, + Pepepains: 105, + PepoG: 987, + PogOanny: 14, + PogTasty: 211, + PogU: 245, + Pogpega: 902, + PokiShare: 274, + Poorge: 789, + PoroRoast: 199, + Programming: 161, + RAGEY: 322, + RAVE: 67, + RESETTING: 749, + RIDING: 364, + RIPBOZO: 638, + Ratge: 981, + SCHIZO: 457, + SHITUP: 643, + SNIFFA: 220, + SPEED: 956, + SadCat: 747, + Sadeg: 313, + SadgeCry: 869, + SaguiPls: 363, + Siti: 396, + Smadge: 222, + SmugCat: 447, + SnowTime: 545, + SoCute: 317, + Spoopy: 281, + Suske: 642, + THESE: 401, + THIS: 311, + Tasty: 530, + ThisStream: 497, + TouchGrass: 608, + TurtleRush: 608, + UOHHH: 426, + VIBE: 939, + VIBEOFF: 36, + VaN: 479, + VeryBased: 456, + ViolinTime: 406, + WHAT: 941, + WHOMEGALUL: 804, + WakuWaku: 221, + WeebRun: 582, + WhoAsked: 459, + Wickedgi: 870, + Wigglecat: 649, + Wokege: 377, + YourMom: 939, + amongnnE: 142, + annE: 552, + annie: 292, + annyBlob: 471, + annyBop: 749, + annyCD: 673, + annyCucumber: 689, + annyDespair: 736, + annyExcitedHug: 926, + annyGAMBA: 885, + annyGasm: 49, + annyGlare: 462, + annyHappy: 960, + annyHug: 979, + annyJam: 544, + annyLava: 288, + annyNODDERS: 730, + annyNOW: 68, + annyOffline: 241, + annyPadoru: 453, + annyPag: 377, + annyPoggies: 729, + annyPooPoo: 295, + annySCHIZO: 766, + annySmile: 467, + annySussy: 695, + annySwipe: 992, + annyTalk: 951, + annyThighs: 458, + annyVibe: 914, + annykiss: 308, + annysilly: 996, + annystare: 34, + annytfCum: 27, + anyaPls: 322, + anyatf: 58, + baseg: 551, + borpaSpin: 793, + burh: 716, + catKISS: 88, + catRAVE: 912, + catbaby: 673, + chatlookwhatannytaughtme: 515, + danse: 213, + deadass: 815, + degen: 551, + dejj: 843, + donowall: 660, + dvaAss: 871, + elmoFire: 117, + ewLeague: 240, + ewOverwatch: 397, + forsen: 482, + forsenGa: 327, + forsenGaPick: 149, + forsenLaughingAtYou: 292, + frenn: 575, + golive: 103, + guraFukkireta: 767, + guraWiggle: 132, + guraWink: 891, + happ: 525, + heCrazy: 437, + hiThere: 54, + hmmMeeting: 749, + iAsked: 203, + jupijej: 207, + kiss0: 738, + kok: 313, + koklick: 990, + koroneWink: 824, + lebronJAM: 890, + liccanny: 205, + lobaPls: 436, + majj: 813, + meow: 757, + monkaE: 376, + monkaLaugh: 335, + nekoNya: 948, + nessieWalk: 672, + nise: 658, + nyanPls: 330, + pL: 38, + paapoHappy: 126, + peepoCat: 673, + peepoClap: 772, + peepoFAT: 408, + peepoFeet: 909, + peepoFinger: 185, + peepoFlower: 569, + peepoFlute: 361, + peepoHeadbang: 615, + peepoHigh: 125, + peepoLeaveToAnny: 711, + peepoPag: 101, + peepoPopcorn: 234, + peepoRant: 628, + peepoRiot: 874, + peepoSnow: 90, + peepoStuck: 188, + peepoTalk: 452, + pepeKneel: 87, + pepePoint: 641, + pepegaJAMMER: 603, + plink: 892, + poggcrazy: 319, + pogs: 829, + poroPls: 87, + ppHop: 589, + ppHopper: 843, + ppPoof: 188, + sadWankge: 300, + sajj: 44, + sitt: 450, + soncic: 483, + sonic: 464, + squirrelJAM: 431, + toffee1: 591, + toffee2: 51, + toffee3: 3, + toffeeBlankies: 249, + toffeeBop: 801, + toffeeChat: 405, + toffeeClap: 552, + toffeeComfy: 383, + toffeeConfused: 548, + toffeeDinkDonk: 782, + toffeePat: 235, + toffeeTap: 815, + vp: 113, + wideVIBE: 972, + wideannyBop: 866, + widepeepoHappy: 969, + widepeepoMASTURBATION77769420GANGSHITNOMOREFORTNITE19DOLLERFORTNITECARD: 846, + xQchatting: 196, + xdd666: 700, + xddfdhjsd0f76ds5r26FDSFHD88hjdbsa67vr7xlLLhdsgfcxz632nkDFSGATVMVLN8CXFJJMVMMMM111111111111111111111: 866, + xdding: 735, + xqcCoomer: 397, + xqcGoofy: 360, + xqcRecord: 773, + xqcTwerk: 531, + yayAnny: 437, + yoshiJAM: 137, + zSpooky: 350, + zyzzBass: 680, + // bttv + ":tf:": 325, + CiGrip: 697, + DatSauce: 903, + ForeverAlone: 663, + GabeN: 497, + HailHelix: 447, + ShoopDaWhoop: 502, + "M&Mjc": 196, + bttvNice: 558, + TwaT: 176, + WatChuSay: 947, + tehPoleCat: 611, + TaxiBro: 527, + BroBalt: 384, + CandianRage: 431, + "D:": 192, + VisLaud: 966, + KaRappa: 247, + FishMoley: 276, + Hhhehehe: 945, + KKona: 596, + PoleDoge: 485, + sosGame: 473, + CruW: 591, + RarePepe: 866, + haHAA: 979, + FeelsBirthdayMan: 536, + RonSmug: 776, + KappaCool: 777, + FeelsBadMan: 483, + bUrself: 934, + ConcernDoge: 520, + FeelsGoodMan: 202, + FireSpeed: 82, + NaM: 412, + SourPls: 231, + FeelsSnowMan: 639, + FeelsSnowyMan: 929, + LuL: 603, + SoSnowy: 647, + SaltyCorn: 486, + monkaS: 189, + VapeNation: 570, + ariW: 926, + notsquishY: 438, + FeelsAmazingMan: 92, + DuckerZ: 417, + IceCold: 21, + SqShy: 84, + Wowee: 533, + WubTF: 494, + cvR: 913, + cvL: 337, + cvHazmat: 599, + cvMask: 423, + DogChamp: 947, + annytfBanana: 730, + annySaur: 236, + annytfBlink: 108, + AnnySilly: 469, + annyBlankies: 747, + annyHop: 861, + annyHopper: 941, + annyTeaTime: 398, + annyD: 530, + annyDVibe: 907, + annyDHyper: 451, + annyDFast: 117, + KissaVei: 158, + Annie: 597, + annyPls: 91, + // ffz + monkaW: 969, + "5Head": 750, + Prayge: 434, + PauseChamp: 903, + Pepega: 817, + TWINGO: 256, + PepeHands: 190, + monkaTOS: 146, + monkaHmm: 549, + Dolan: 536, + SmileW: 166, + ABABABABA: 95, + peepoBlanket: 798, + pikachuS: 715, + AYAYAHyper: 164, + YEP: 371, + widepeepoBlanket: 100, + HandsUp: 684, + peepoSad: 167, + HyperKorone: 456, + AYAYA: 503, + forsenCD: 344, + Hahaa: 291, + LULW: 463, + WICKED: 133, + EZY: 494, + OkayChamp: 950, + PepegaPig: 217, + POGGIES: 487, + peepoWTF: 734, + ConfusedCat: 508, + PainPeko: 433, + KKrikey: 235, + COPIUM: 582, + Madge: 595, + Catge: 811, + stopbeingMean: 522, + NOPE: 262, + OMEGALUL: 648, + AYAYAY: 725, + PogO: 548, + Sadge: 41, + PepegaPhone: 800, + Widega: 543, + ZrehplaR: 23, + YooHoo: 617, + ManChicken: 588, + BeanieHipster: 480, + CatBag: 253, + ZreknarF: 143, + LilZ: 952, + ZliL: 662, + LaterSooner: 375, + BORT: 738, + OBOY: 942, + OiMinna: 354, + AndKnuckles: 828, + // twitch + annytfRaid: 493, + annytfNote: 893, + annytfPout: 971, + annytfAyaya: 180, + annytfPika: 328, + annytfPrime: 830, + annytfLewd: 314, + annytfRave: 615, + annytfLurk: 898, + annytfPog: 958, + annytfD: 759, + annytfCry: 399, + annytfHeart: 472, + annytfSad: 390, + annytfMelt: 157, + annytfWICKED: 94, + annytfCheer: 780, + annytfREEE: 272, + annytfLUL: 122, + annytfScuffed: 318, + annytfAngy: 677, + annytfHug: 826, + annytfCool: 121, + annytfPain: 16, + annytfBonk: 949, + annytfKnuckles: 182, + annytfSigh: 220, + annytfWoah: 582, + annytfBite: 580, + annytfSilly: 837, + annytfWow: 381, + annytfPray: 438, + annytfPats: 410, + annytfGasm: 822, + annytfSit: 39, + annytfFlower: 137, + annytfLeave: 819, + annytfGamba: 847, + annytfLuv: 483, + annytfHarucchiHug: 142, + annytfAnnE: 324, + annytfDinkDonk: 380, + annytfAhriluv: 232, + annytfW: 101, + annytfWoke: 145, + annytfBedge: 409, + annytfBusiness: 142, + annytfPeek: 98, + annytfDonowall: 581, + NewRecord: 836, + Awwdible: 794, + Lechonk: 567, + Getcamped: 378, + SUBprise: 580, + FallHalp: 95, + FallCry: 450, + FallWinning: 261, + MechaRobot: 984, + ImTyping: 993, + Shush: 506, + MyAvatar: 41, + PizzaTime: 330, + LaundryBasket: 852, + ModLove: 683, + PotFriend: 605, + Jebasted: 72, + PogBones: 761, + PoroSad: 357, + KEKHeim: 290, + CaitlynS: 874, + HarleyWink: 244, + WhySoSerious: 705, + DarkKnight: 101, + FamilyMan: 278, + RyuChamp: 814, + HungryPaimon: 920, + TransgenderPride: 409, + PansexualPride: 707, + NonbinaryPride: 866, + LesbianPride: 763, + IntersexPride: 136, + GenderFluidPride: 73, + GayPride: 471, + BisexualPride: 163, + AsexualPride: 533, + PogChamp: 898, + GlitchNRG: 877, + GlitchLit: 45, + StinkyGlitch: 697, + GlitchCat: 208, + FootGoal: 542, + FootYellow: 297, + FootBall: 679, + BlackLivesMatter: 637, + ExtraLife: 394, + VirtualHug: 452, + "R-)": 779, + "R)": 242, + ";-p": 911, + ";p": 168, + ";-P": 584, + ";P": 571, + ":-p": 758, + ":p": 709, + ":-P": 137, + ":P": 602, + ";-)": 349, + ";)": 745, + ":-\\": 30, + ":\\": 725, + ":-/": 403, + ":/": 187, + "<3": 51, + ":-o": 622, + ":o": 384, + ":-O": 994, + ":O": 629, + "8-)": 881, + "B-)": 390, + "B)": 489, + "o.o": 570, + o_o: 738, + "o.O": 287, + o_O: 168, + "O.O": 452, + O_O: 33, + "O.o": 187, + O_o: 513, + ":-Z": 729, + ":Z": 762, + ":-z": 658, + ":z": 526, + ":-|": 978, + ":|": 224, + ">(": 141, + ":-D": 699, + ":D": 926, + ":-(": 864, + ":-)": 141, + BOP: 826, + SingsNote: 46, + SingsMic: 347, + TwitchSings: 419, + SoonerLater: 187, + HolidayTree: 822, + HolidaySanta: 340, + HolidayPresent: 830, + HolidayLog: 251, + HolidayCookie: 105, + GunRun: 899, + PixelBob: 829, + FBPenalty: 123, + FBChallenge: 645, + FBCatch: 769, + FBBlock: 284, + FBSpiral: 633, + FBPass: 365, + FBRun: 688, + MaxLOL: 17, + TwitchRPG: 258, + PinkMercy: 369, + MercyWing2: 2, + MercyWing1: 546, + PartyHat: 581, + EarthDay: 649, + TombRaid: 904, + PopCorn: 485, + FBtouchdown: 621, + TPFufun: 321, + TwitchVotes: 858, + DarkMode: 90, + HSWP: 729, + HSCheers: 802, + PowerUpL: 88, + PowerUpR: 616, + LUL: 958, + EntropyWins: 639, + TPcrunchyroll: 286, + TwitchUnity: 349, + Squid4: 548, + Squid3: 113, + Squid2: 768, + Squid1: 649, + CrreamAwk: 186, + CarlSmile: 822, + TwitchLit: 125, + TehePelo: 124, + TearGlove: 354, + SabaPing: 94, + PunOko: 145, + KonCha: 656, + Kappu: 597, + InuyoFace: 434, + BigPhish: 169, + BegWan: 621, + ThankEgg: 391, + MorphinTime: 106, + TheIlluminati: 531, + TBAngel: 925, + MVGame: 873, + NinjaGrumpy: 345, + PartyTime: 773, + RlyTho: 830, + UWot: 265, + YouDontSay: 744, + KAPOW: 757, + ItsBoshyTime: 605, + CoolStoryBob: 193, + TriHard: 121, + SuperVinlin: 500, + FreakinStinkin: 860, + Poooound: 411, + CurseLit: 318, + BatChest: 642, + BrainSlug: 48, + PrimeMe: 619, + StrawBeary: 813, + RaccAttack: 172, + UncleNox: 583, + WTRuck: 118, + TooSpicy: 761, + Jebaited: 363, + DogFace: 911, + BlargNaut: 148, + TakeNRG: 27, + GivePLZ: 581, + imGlitch: 514, + pastaThat: 48, + copyThis: 426, + UnSane: 97, + DatSheffy: 289, + TheTarFu: 818, + PicoMause: 570, + TinyFace: 31, + DxCat: 538, + RuleFive: 903, + VoteNay: 113, + VoteYea: 223, + PJSugar: 11, + DoritosChip: 187, + OpieOP: 977, + FutureMan: 893, + ChefFrank: 481, + StinkyCheese: 419, + NomNom: 162, + SmoocherZ: 863, + cmonBruh: 93, + KappaWealth: 776, + MikeHogu: 497, + VoHiYo: 646, + KomodoHype: 295, + SeriousSloth: 379, + OSFrog: 807, + OhMyDog: 124, + KappaClaus: 209, + KappaRoss: 298, + MingLee: 338, + SeemsGood: 89, + twitchRaid: 258, + bleedPurple: 949, + duDudu: 442, + riPepperonis: 192, + NotLikeThis: 838, + DendiFace: 534, + CoolCat: 995, + KappaPride: 915, + ShadyLulu: 372, + ArgieB8: 267, + CorgiDerp: 511, + PraiseIt: 557, + TTours: 122, + mcaT: 154, + NotATK: 388, + HeyGuys: 453, + Mau5: 421, + PRChase: 443, + WutFace: 20, + BuddhaBar: 622, + PermaSmug: 769, + panicBasket: 285, + BabyRage: 315, + HassaanChop: 246, + TheThing: 890, + EleGiggle: 284, + RitzMitz: 671, + YouWHY: 796, + PipeHype: 343, + BrokeBack: 440, + ANELE: 156, + PanicVis: 865, + GrammarKing: 77, + PeoplesChamp: 634, + SoBayed: 700, + BigBrother: 657, + Keepo: 800, + Kippa: 835, + RalpherZ: 322, + TF2John: 862, + ThunBeast: 408, + WholeWheat: 193, + DAESuppy: 787, + FailFish: 395, + HotPokket: 399, + ResidentSleeper: 460, + FUNgineer: 747, + PMSTwin: 830, + ShazBotstix: 315, + BibleThump: 278, + AsianGlow: 461, + DBstyle: 968, + BloodTrail: 687, + OneHand: 801, + FrankerZ: 893, + SMOrc: 727, + ArsonNoSexy: 99, + PunchTrees: 762, + SSSsss: 800, + Kreygasm: 413, + KevinTurtle: 111, + PJSalt: 115, + SwiftRage: 251, + DansGame: 46, + GingerPower: 762, + BCWarrior: 409, + MrDestructoid: 811, + JonCarnage: 359, + Kappa: 40, + RedCoat: 789, + TheRinger: 669, + StoneLightning: 867, + OptimizePrime: 654, + JKanStyle: 655, + ":)": 594, +}; + +export { fakePrices }; diff --git a/pages/api/fakeRanking.ts b/pages/api/fakeRanking.ts deleted file mode 100644 index d7eb206..0000000 --- a/pages/api/fakeRanking.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -type Data = { - [key: string]: any; -}; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse<Data> -) { - const sortBy = req.query.s ? (req.query.s as string) : undefined; - const sortAsc = req.query.a ? (req.query.a as string) : undefined; - - let data = fakeData; - if (sortBy) { - if (sortBy === "netWorth") { - data = data.sort((a, b) => b.netWorth - a.netWorth); - } else if (sortBy === "dailyChange") { - data = data.sort((a, b) => b.dailyChange - a.dailyChange); - } else if (sortBy === "dailyChangePercent") { - data = data.sort((a, b) => b.dailyChangePercent - a.dailyChangePercent); - } else if (sortBy === "shares") { - data = data.sort((a, b) => b.shares - a.shares); - } else if (sortBy === "points") { - data = data.sort((a, b) => b.points - a.points); - } else if (sortBy === "name") { - data = data.sort((a, b) => a.name.localeCompare(b.name)); - } - if (sortAsc === "true") { - // slow but only needed for temporary fake data anyway - data = data.reverse(); - } - } - // fake loading time - await new Promise((resolve) => - setTimeout(resolve, 250 + Math.random() * 1000) - ); - res.status(200).json({ data }); -} - -interface fakeDataEntry { - id: number; - name: string; - netWorth: number; - points: number; - shares: number; - dailyChange: number; - dailyChangePercent: number; -} - -const fakeData: fakeDataEntry[] = [ - { - id: 4, - name: "3zachm", - netWorth: 10030, // stocks + points - points: 70, /// uninvested points - shares: 20, - dailyChange: -500, - dailyChangePercent: -0.0498504486540378863409770687936, - }, - { - id: 1, - name: "ModulatingForce", - netWorth: 142910, - points: 10020, - shares: 200, - dailyChange: 5420, - dailyChangePercent: 0.0379259673920649359736897347981, - }, - { - id: 2, - name: "notohh", - netWorth: 153495392, - points: 10020, - shares: 2432, - dailyChange: 0, - dailyChangePercent: 0, - }, - { - id: 3, - name: "SecondSockSan", - netWorth: 153495, - points: 15020, - shares: 20, - dailyChange: -10432, - dailyChangePercent: -0.06796312583471774324896576435715, - }, - { - id: 0, - name: "e__n__t__e", - netWorth: 429481824, - points: 1002022, - shares: 94214, - dailyChange: 329444422, - dailyChangePercent: 4.2932124926634939999741296760186, - }, - { - id: 5, - name: "luckytohavefoundyou14252", - netWorth: 8024, - points: 423, - shares: 4, - dailyChange: 9, - dailyChangePercent: 0.00112163509471585244267198404786, - }, - { - id: 6, - name: "ZeroxZerich", - netWorth: 842190, - points: 88542, - shares: 532, - dailyChange: -10219, - dailyChangePercent: -0.01213384153219582279533121979601, - }, - { - id: 7, - name: "joeeyo", - netWorth: 10000000, - points: 9999979, - shares: 1, - dailyChange: 1, - dailyChangePercent: 0.0000001, - }, - { - id: 8, - name: "dd_maru", - netWorth: 10328421, - points: 328421, - shares: 252, - dailyChange: 85192, - dailyChangePercent: 0.00824830823607984221402284047097, - }, - { - id: 9, - name: "Goldeneye128", - netWorth: 58292, - points: 6521, - shares: 63, - dailyChange: -1942, - dailyChangePercent: -0.03331503465312564331297605160228, - }, - { - id: 10, - name: "lilpastatv", - netWorth: 7328919, - points: 40, - shares: 93, - dailyChange: 921821, - dailyChangePercent: 0.12577857662228222197571019682439, - }, - { - id: 11, - name: "domiswitch", - netWorth: 43290, - points: 5002, - shares: 15, - dailyChange: 2429, - dailyChangePercent: 0.05610995610995610995610995610996, - }, - { - id: 12, - name: "minosura", - netWorth: 904328, - points: 32901, - shares: 83, - dailyChange: 94821, - dailyChangePercent: 0.10485244291894091524314186887943, - }, - { - id: 13, - name: "scienceteam_member", - netWorth: 34894, - points: 958, - shares: 5, - dailyChange: -7964, - dailyChangePercent: -0.22823408035765461110792686421734, - }, - { - id: 14, - name: "witchdev", - netWorth: 94382912, - points: 8532, - shares: 329, - dailyChange: -421, - dailyChangePercent: -0.0000044605531984433792422085896, - }, - { - id: 15, - name: "justone123879", - netWorth: 8889123, - points: 86333, - shares: 153, - dailyChange: 53289, - dailyChangePercent: 0.00599485461051669551653183334284, - }, - { - id: 16, - name: "marcelr_", - netWorth: 400329, - points: 39291, - shares: 52, - dailyChange: 1329, - dailyChangePercent: 0.00331976948959480827019776234047, - }, - { - id: 17, - name: "fossabot", - netWorth: 20005, - points: 0, - shares: 1, - dailyChange: -31042, - dailyChangePercent: -1.5517120719820044988752811797051, - }, -]; - -export type { fakeDataEntry }; diff --git a/pages/api/fakeUsers.ts b/pages/api/fakeUsers.ts new file mode 100644 index 0000000..052e9dc --- /dev/null +++ b/pages/api/fakeUsers.ts @@ -0,0 +1,856 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { createRedisInstance } from "../../misc/redis"; +import { getUserByName } from "../../misc/TwitchAPI"; +import { fakePrices } from "./fakePrices"; + +type Data = { + [key: string]: any; +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<Data> +) { + const username = req.query.u ? (req.query.u as string) : undefined; + const sortBy = req.query.s ? (req.query.s as string) : undefined; + const sortAsc = req.query.a ? (req.query.a as string) : undefined; + + const redis = createRedisInstance(); + + let data = fakeData; + // calculate all net worths + data = data.map((user) => { + 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) { + // if user does not exist, return error + data = data.filter((u) => u.name === username); + if (data.length === 0) { + res + .status(404) + .json({ error: { message: "User not found", code: 20000 } }); + return; + } + // get twitch data for user + let twitchData: { data: { [key: string]: any }[] }; + try { + twitchData = await getUserByName(redis, username); + } catch (e) { + res.status(500).json({ + error: { message: "Twitch or internal API is down", code: 10100 }, + }); + return; + } + // if data is empty, user does not exist + if (twitchData.data.length === 0) { + // temp who cares + twitchData.data[0] = {}; + twitchData.data[0].profile_image_url = "/img/logo.webp"; + } + // 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; + } + if (sortBy) { + if (sortBy === "daily_change") { + data = data.sort((a, b) => b.daily_change - a.daily_change); + } else if (sortBy === "daily_change_percent") { + data = data.sort( + (a, b) => b.daily_change_percent - a.daily_change_percent + ); + } else if (sortBy === "shares") { + data = data.sort((a, b) => (b.shares ?? 0) - (a.shares ?? 0)); + } else if (sortBy === "points") { + data = data.sort((a, b) => b.points - a.points); + } else if (sortBy === "name") { + data = data.sort((a, b) => a.name.localeCompare(b.name)); + } + if (sortAsc === "true") { + // slow but only needed for temporary fake data anyway + data = data.reverse(); + } + } + // fake loading time + await new Promise((resolve) => + setTimeout(resolve, 250 + Math.random() * 1000) + ); + res.status(200).json({ data: data }); +} + +interface asset { + name: string; + count: number; + provider: "7tv" | "bttv" | "ffz" | "ttv"; +} +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", + color: "#CC3333", + priority: 99999, +}; + +const CEOBadge: badge = { + name: "CEO", + color: "#F97316", + priority: 100000, +}; + +const webDevBadge: badge = { + name: "Web Dev", + color: "#a855f7", + priority: 50000, +}; + +const botDevBadge: badge = { + name: "Bot Dev", + color: "#48b2f1", + priority: 50001, +}; + +const fakeData: fakeDataEntry[] = [ + { + id: 4, + name: "3zachm", + points: 7420, // uninvested points + daily_change: -500, + daily_change_percent: -0.0498504486540378863409770687936, + assets: [ + { + name: "JIGACHAD", + count: 420, + provider: "7tv", + }, + { + name: "annykiss", + count: 8, + provider: "7tv", + }, + { + name: "HUH", + count: 1, + provider: "7tv", + }, + { + name: "annytfSigh", + count: 1, + provider: "ttv", + }, + { + name: "GabeN", + count: 3, + provider: "bttv", + }, + { + name: "widepeepoBlanket", + count: 1, + provider: "ffz", + }, + { + name: "plink", + count: 1, + provider: "7tv", + }, + ], + badges: [adminBadge, webDevBadge], + }, + { + id: 1, + name: "ModulatingForce", + points: 10020, + daily_change: 5420, + daily_change_percent: 0.0379259673920649359736897347981, + assets: [ + { + name: "OhTheMiseryEverybodyWantsToBeMyEnemySpareTheSympathyEverybodyWantsToBeMyEnemy-y-y-y-y", + count: 39, + provider: "7tv", + }, + { + name: "Clueless", + count: 727, + provider: "7tv", + }, + { + name: "AnnySilly", + count: 4, + provider: "bttv", + }, + { + name: "annytfHeart", + count: 98, + provider: "ttv", + }, + { + name: "Catge", + count: 4, + provider: "ffz", + }, + ], + badges: [adminBadge, botDevBadge], + }, + { + id: 2, + name: "notohh", + points: 10020, + daily_change: 0, + daily_change_percent: 0, + assets: [ + { + name: "YourMom", + count: 81, + provider: "7tv", + }, + { + name: "CumTime", + count: 92, + provider: "7tv", + }, + { + name: "KissaVei", + count: 1, + provider: "bttv", + }, + { + name: "SNIFFA", + count: 1219, + provider: "7tv", + }, + { + name: "FeelsBirthdayMan", + count: 1, + provider: "ffz", + }, + { + name: "annytfRave", + count: 5, + provider: "ttv", + }, + ], + badges: [adminBadge, botDevBadge], + }, + { + id: 3, + name: "SecondSockSan", + points: 15020, + daily_change: -10432, + daily_change_percent: -0.06796312583471774324896576435715, + assets: [ + { + name: "AYAYAjam", + count: 46, + provider: "7tv", + }, + { + name: "GabeN", + count: 3, + provider: "bttv", + }, + { + name: "ThisStream", + count: 210, + provider: "7tv", + }, + { + name: "BAND", + count: 91, + provider: "7tv", + }, + { + name: "annytfMelt", + count: 16, + provider: "ttv", + }, + ], + badges: [CEOBadge, adminBadge], + }, + { + id: 0, + name: "mzntori", + points: 922022, + daily_change: 329444422, + daily_change_percent: 4.2932124926634939999741296760186, + assets: [ + { + name: "peepoSnow", + count: 72, + provider: "7tv", + }, + { + name: "annyHop", + count: 61, + provider: "bttv", + }, + { + name: "annyExcitedHug", + count: 26, + provider: "7tv", + }, + { + name: "AAAA", + count: 65, + provider: "7tv", + }, + { + name: "peepoWTF", + count: 60, + provider: "ffz", + }, + { + name: "annytfAngy", + count: 90, + provider: "ttv", + }, + ], + badges: [adminBadge, botDevBadge], + }, + { + id: 5, + name: "luckytohavefoundyou14252", + points: 423, + daily_change: 9, + daily_change_percent: 0.00112163509471585244267198404786, + assets: [ + { + name: "HACKERMANS", + count: 59, + provider: "7tv", + }, + { + name: "THIS", + count: 70, + provider: "7tv", + }, + { + name: "lebronJAM", + count: 66, + provider: "7tv", + }, + ], + }, + { + id: 6, + name: "ZeroxZerich", + points: 88542, + daily_change: -10219, + daily_change_percent: -0.01213384153219582279533121979601, + assets: [ + { + name: "WeebRun", + count: 10, + provider: "7tv", + }, + { + name: "annySaur", + count: 7, + provider: "bttv", + }, + { + name: "BAND", + count: 49, + provider: "7tv", + }, + { + name: "SNIFFA", + count: 78, + provider: "7tv", + }, + { + name: "PepegaPhone", + count: 142, + provider: "ffz", + }, + { + name: "annytfHug", + count: 19, + provider: "ttv", + }, + ], + }, + { + id: 7, + name: "joeeyo", + points: 99979, + daily_change: 1, + daily_change_percent: 0.0000001, + assets: [ + { + name: "Siti", + count: 32, + provider: "7tv", + }, + { + name: "annytfLUL", + count: 9, + provider: "ttv", + }, + { + name: "peepoSnow", + count: 37, + provider: "7tv", + }, + { + name: "MadgeJuice", + count: 70, + provider: "7tv", + }, + { + name: "annyBlankies", + count: 88, + provider: "bttv", + }, + { + name: "TWINGO", + count: 98, + provider: "ffz", + }, + ], + }, + { + id: 8, + name: "dd_maru", + points: 208421, + daily_change: 85192, + daily_change_percent: 0.00824830823607984221402284047097, + assets: [ + { + name: "BocchiPossessed", + count: 56, + provider: "7tv", + }, + { + name: "toffeeConfused", + count: 64, + provider: "7tv", + }, + { + name: "annytfBanana", + count: 15, + provider: "bttv", + }, + { + name: "ewLeague", + count: 64, + provider: "7tv", + }, + { + name: "annytfPain", + count: 37, + provider: "ttv", + }, + ], + }, + { + id: 9, + name: "Goldeneye128", + points: 6521, + daily_change: -1942, + daily_change_percent: -0.03331503465312564331297605160228, + assets: [ + { + name: "PagMan", + count: 52, + provider: "7tv", + }, + { + name: "CapybaraStare", + count: 47, + provider: "7tv", + }, + { + name: "GabeN", + count: 52, + provider: "bttv", + }, + { + name: "GroupWankge", + count: 38, + provider: "7tv", + }, + { + name: "annyCucumber", + count: 90, + provider: "7tv", + }, + { + name: "annytfKnuckles", + count: 2, + provider: "ttv", + }, + ], + }, + { + id: 10, + name: "lilpastatv", + points: 40, + daily_change: 921821, + daily_change_percent: 0.12577857662228222197571019682439, + assets: [ + { + name: "Wigglecat", + count: 205, + provider: "7tv", + }, + { + name: "guraWink", + count: 5, + provider: "7tv", + }, + { + name: "annyPls", + count: 5, + provider: "bttv", + }, + { + name: "golive", + count: 46, + provider: "7tv", + }, + { + name: "COPIUM", + count: 82, + provider: "ffz", + }, + { + name: "annytfCheer", + count: 54, + provider: "ttv", + }, + ], + }, + { + id: 11, + name: "domiswitch", + points: 5002, + daily_change: 2429, + daily_change_percent: 0.05610995610995610995610995610996, + assets: [ + { + name: "peepoFlute", + count: 81, + provider: "7tv", + }, + { + name: "WhoAsked", + count: 44, + provider: "7tv", + }, + { + name: "pL", + count: 24, + provider: "7tv", + }, + { + name: "peepoSnow", + count: 13, + provider: "7tv", + }, + { + name: "annytfBlink", + count: 10, + provider: "bttv", + }, + { + name: "annytfBonk", + count: 77, + provider: "ttv", + }, + ], + }, + { + id: 12, + name: "minosura", + points: 32901, + daily_change: 94821, + daily_change_percent: 0.10485244291894091524314186887943, + assets: [ + { + name: "Okayeg", + count: 100, + provider: "7tv", + }, + { + name: "burh", + count: 100, + provider: "7tv", + }, + { + name: "annyHop", + count: 16, + provider: "bttv", + }, + { + name: "AndKnuckles", + count: 17, + provider: "ffz", + }, + { + name: "yoshiJAM", + count: 67, + provider: "7tv", + }, + { + name: "WhoAsked", + count: 59, + provider: "7tv", + }, + { + name: "annytfSit", + count: 53, + provider: "ttv", + }, + ], + }, + { + id: 13, + name: "scienceteam_member", + points: 958, + daily_change: -7964, + daily_change_percent: -0.22823408035765461110792686421734, + assets: [ + { + name: "LULE", + count: 43, + provider: "7tv", + }, + { + name: "Madgeclap", + count: 82, + provider: "7tv", + }, + { + name: "annyDFast", + count: 22, + provider: "bttv", + }, + { + name: "PeepoKittyHug", + count: 7, + provider: "7tv", + }, + ], + }, + { + id: 14, + name: "witchdev", + points: 8532, + daily_change: -421, + daily_change_percent: -0.0000044605531984433792422085896, + assets: [ + { + name: "SNIFFA", + count: 76, + provider: "7tv", + }, + { + name: "annyCD", + count: 62, + provider: "7tv", + }, + { + name: "annyBlankies", + count: 74, + provider: "bttv", + }, + { + name: "anyatf", + count: 24, + provider: "7tv", + }, + { + name: "annytfGamba", + count: 32, + provider: "ttv", + }, + ], + }, + { + id: 15, + name: "justone123879", + points: 86333, + daily_change: 53289, + daily_change_percent: 0.00599485461051669551653183334284, + assets: [ + { + name: "Homi", + count: 9, + provider: "7tv", + }, + { + name: "wideVIBE", + count: 61, + provider: "7tv", + }, + { + name: "Annie", + count: 24, + provider: "bttv", + }, + { + name: "Lagging", + count: 92, + provider: "7tv", + }, + { + name: "annytfFlower", + count: 33, + provider: "ttv", + }, + ], + }, + { + id: 16, + name: "marcelr_", + points: 39291, + daily_change: 1329, + daily_change_percent: 0.00331976948959480827019776234047, + assets: [ + { + name: "peepoStuck", + count: 91, + provider: "7tv", + }, + { + name: "PokiShare", + count: 13, + provider: "7tv", + }, + { + name: "VeryBased", + count: 7, + provider: "7tv", + }, + { + name: "annyHopper", + count: 24, + provider: "bttv", + }, + { + name: "annytfFlower", + count: 79, + provider: "ttv", + }, + ], + }, + { + id: 17, + name: "fossabot", + points: 0, + daily_change: -31042, + daily_change_percent: -1.5517120719820044988752811797051, + assets: [ + { + name: "catbaby", + count: 41, + provider: "7tv", + }, + { + name: "peepoCat", + count: 41, + provider: "7tv", + }, + { + name: "plink", + count: 32, + provider: "7tv", + }, + { + name: "AngelThump", + count: 41, + provider: "bttv", + }, + { + name: "annytfSad", + count: 2, + provider: "ttv", + }, + ], + }, + { + id: 18, + name: "Headdesking1", + points: 429, + daily_change: 0, + daily_change_percent: 0, + assets: [ + { + name: "anyaPls", + count: 92, + provider: "7tv", + }, + { + name: "toffeeDinkDonk", + count: 6, + provider: "7tv", + }, + { + name: "SoCute", + count: 99, + provider: "7tv", + }, + { + name: "annyBlankies", + count: 42, + provider: "bttv", + }, + { + name: "annytfHeart", + count: 63, + provider: "ttv", + }, + ], + }, +]; + +export type { fakeDataEntry }; diff --git a/pages/api/ffz/emotes.ts b/pages/api/ffz/emotes.ts new file mode 100644 index 0000000..dbc58c9 --- /dev/null +++ b/pages/api/ffz/emotes.ts @@ -0,0 +1,30 @@ +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 } }); + } +} diff --git a/pages/api/twitch/emotes.ts b/pages/api/twitch/emotes.ts new file mode 100644 index 0000000..9452544 --- /dev/null +++ b/pages/api/twitch/emotes.ts @@ -0,0 +1,30 @@ +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 }, + }); + } +} diff --git a/pages/index.tsx b/pages/index.tsx index 197ffd0..25bd22e 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -2,11 +2,10 @@ import { m } from "framer-motion"; import { ReactElement, useEffect, useState } from "react"; import HomeLayout from "../layouts/HomeLayout"; import { homeMain } from "../layouts/NavTemplates"; -import type { NextPageWithLayout } from "./_app"; import Image from "next/image"; import Head from "next/head"; -const Home: NextPageWithLayout = () => { +function Home() { let api7tvEmotes = `/api/7tv/emotes?c=61ad997effa9aba101bcfddf`; const [emotesUrls, setEmotes] = useState([]); const [currentEmote, setCurrentEmote] = useState(0); @@ -15,6 +14,10 @@ const Home: NextPageWithLayout = () => { fetch(api7tvEmotes) .then((res) => res.json()) .then((data) => { + // if error, return + if (data.error) { + return; + } // get all emote URLs let emoteUrls = data.channel.user.emote_sets[0].emotes.map( (emote: any) => { @@ -79,49 +82,25 @@ const Home: NextPageWithLayout = () => { <title>Home - toffee</title> </Head> <div className="flex h-full w-full flex-col items-center justify-center"> - <div className="inline-grid grid-cols-1 gap-10 text-white md:grid-cols-3"> + <div className="inline-grid grid-cols-1 gap-20 text-white md:grid-cols-3"> <m.div - className="flex flex-col from-purple-400 to-pink-600 font-plusJakarta text-7xl font-semibold sm:text-8xl md:col-span-2" + className="flex flex-col from-purple-400 to-pink-600 font-plusJakarta md:col-span-2" variants={sloganContainerVariants} initial="initial" animate="animate" > - <m.div - className="flex flex-row items-center" - variants={sloganHeaderVariants} - > - <h1 className="bg-gradient-to-b bg-clip-text text-transparent"> - t - </h1> - <h1>ax-free</h1> - </m.div> - <m.div - className="flex flex-row items-center" - variants={sloganHeaderVariants} - > - <h1 className="bg-gradient-to-tl bg-clip-text text-transparent"> + <div className="flex flex-row text-8xl font-bold italic"> + <m.h1 variants={sloganHeaderVariants}>t</m.h1> + <m.h1 className="text-orange-400" variants={sloganHeaderVariants}> off - </h1> - <h1>line</h1> - </m.div> - <m.div - className="flex flex-row items-center" - variants={sloganHeaderVariants} - > - <h1 className="bg-gradient-to-l bg-clip-text text-transparent"> - e - </h1> - <h1>mote</h1> - </m.div> - <m.div - className="flex flex-row items-center" - variants={sloganHeaderVariants} - > - <h1 className="bg-gradient-to-bl bg-clip-text text-transparent"> - e - </h1> - <h1>xchange</h1> - </m.div> + </m.h1> + <m.h1 variants={sloganHeaderVariants}>ee</m.h1> + </div> + <div className="text-xl italic"> + <m.h2 variants={sloganHeaderVariants}> + a tax-free offline emote exchange utility + </m.h2> + </div> </m.div> <m.div className="flex items-center justify-center" @@ -146,7 +125,7 @@ const Home: NextPageWithLayout = () => { </div> </> ); -}; +} const sloganContainerVariants = { initial: { @@ -163,7 +142,7 @@ const sloganContainerVariants = { bounce: 0.5, stiffness: 150, delayChildren: 1.0, - staggerChildren: 0.45, + staggerChildren: 0.1, }, }, }; @@ -171,9 +150,11 @@ const sloganContainerVariants = { const sloganHeaderVariants = { initial: { opacity: 0, + y: -15, }, animate: { opacity: 1, + y: 0, }, }; diff --git a/pages/dashboard/ranking.tsx b/pages/ranking/index.tsx similarity index 74% rename from pages/dashboard/ranking.tsx rename to pages/ranking/index.tsx index 832718d..5c6bb14 100644 --- a/pages/dashboard/ranking.tsx +++ b/pages/ranking/index.tsx @@ -1,9 +1,10 @@ import { m, Variants } from "framer-motion"; import Head from "next/head"; +import Link from "next/link"; import { ReactElement, useEffect, useState } from "react"; import Loading from "../../components/common/Loading"; import DashLayout from "../../layouts/DashLayout"; -import { fakeDataEntry } from "../api/fakeRanking"; +import { fakeDataEntry } from "../api/fakeUsers"; function Ranking() { const [sortBy, setSortBy] = useState("netWorth"); @@ -14,7 +15,7 @@ function Ranking() { useEffect(() => { setDataLoaded(false); // fetch data from api on change to sort method - fetch(`/api/fakeRanking?s=${sortBy}&a=${sortAsc}`) + fetch(`/api/fakeUsers?s=${sortBy}&a=${sortAsc}`) .then((res) => res.json()) .then((data) => { setFakeData(data.data); @@ -78,13 +79,13 @@ function Ranking() { <title>Ranking - toffee</title> </Head> <div className="flex w-full justify-center"> - <div className="ml-3 flex w-full flex-col items-center justify-start font-robotoMono 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 */} <m.h1 - className="hidden bg-gradient-to-tr from-purple-500 to-purple-100 bg-clip-text py-10 text-center font-plusJakarta text-5xl font-bold text-white text-transparent lg:block lg:text-6xl" + className="hidden py-10 text-center text-5xl font-normal text-pink-300 lg:block lg:text-6xl" variants={headerVariants} > - Top Investors + top investors </m.h1> {/* TODO: responsive for extremely skinny displays (i.e. galaxy fold), or really for mobile entirely so info is not lost */} <m.div @@ -162,39 +163,47 @@ function Ranking() { > { // generate table rows - fakeData.map((entry: fakeDataEntry, index) => { - // if daily change is negative, make it red - let changeClass = " text-lime-500"; - if (entry.dailyChangePercent < 0) { - changeClass = " text-red-500"; + fakeData.map( + (entry: { [key: string]: any }, index: number) => { + // if daily change is negative, make it red + let changeClass = " text-lime-500"; + if (entry.daily_change_percent < 0) { + 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> - <h1 className="overflow-hidden overflow-ellipsis whitespace-nowrap text-left"> - {entry.name} - </h1> - <h1>{entry.netWorth.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.dailyChangePercent * 1000) / 10 - ).toFixed(1) + "%"} - </h1> - </m.div> - ); - }) + ) } </m.div> ) diff --git a/pages/user/[username]/index.tsx b/pages/user/[username]/index.tsx new file mode 100644 index 0000000..c0057c8 --- /dev/null +++ b/pages/user/[username]/index.tsx @@ -0,0 +1,486 @@ +import { m } from "framer-motion"; +import Head from "next/head"; +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"; + +// TODO: Animations + +function UserPage() { + const [channelEmotes, setChannelEmotes] = useState<{ + [key: string]: { [key: string]: string }; + }>({}); + const [userData, setUserData] = useState<{ [key: string]: any }>({}); + const [errorCode, setErrorCode] = useState<number | null>(null); + const router = useRouter(); + const { username } = router.query; + const title = username ? `${username} - toffee` : "toffee"; + + useEffect(() => { + if (!router.isReady) return; + fetch("/api/7tv/emotes?c=61ad997effa9aba101bcfddf") + .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 as values + let emotes: { [key: string]: string } = {}; + data.channel.user.emote_sets[0].emotes.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]; + emotes[emote.data.name] = `https:${base_url}/${largest.name}`; + }); + // same for global emotes + data.global.namedEmoteSet.emotes.forEach((emote: any) => { + let base_url = emote.data.host.url; + let largest = emote.data.host.files[emote.data.host.files.length - 1]; + emotes[emote.data.name] = `https:${base_url}/${largest.name}`; + }); + // set 7tv key to channelEmotes + setChannelEmotes((prev) => ({ ...prev, "7tv": emotes })); + }); + fetch("/api/bttv/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.code] = `https://cdn.betterttv.net/emote/${emote.id}/3x`; + }); + data.global.forEach((emote: any) => { + emotes[emote.code] = `https://cdn.betterttv.net/emote/${emote.id}/3x`; + }); + // add as bttv key to channelEmotes + setChannelEmotes((prev) => ({ ...prev, bttv: emotes })); + }); + 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 + emotes[emote.name] = `https:${ + emote.urls[ + Math.max(...Object.keys(emote.urls).map((k) => parseInt(k))) + ] + }`; + }); + data.global.forEach((emote: any) => { + emotes[emote.name] = `https:${ + emote.urls[ + Math.max(...Object.keys(emote.urls).map((k) => parseInt(k))) + ] + }`; + }); + // add as ffz key to channelEmotes + setChannelEmotes((prev) => ({ ...prev, ffz: emotes })); + }); + 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) => { + emotes[emote.name] = emote.images["url_4x"]; + }); + // add as twitch key to channelEmotes + setChannelEmotes((prev) => ({ ...prev, ttv: 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 + }, [router.isReady]); + + if (errorCode !== null) { + // 20000 = user not found + // 10000 = 7tv api error + // 10100 = Twitch api error + // 10200 = BTTV api error + // 10300 = FFZ api error + const errorMsg = errorCode === 20000 ? "User not found" : "API error"; + return ( + <m.div + className="flex h-screen w-full items-center justify-center text-3xl" + initial={{ opacity: 0, y: -50 }} + animate={{ opacity: 1, y: 0, transition: { duration: 1.0 } }} + exit={{ opacity: 0, y: -25 }} + > + <p>{errorMsg}</p> + </m.div> + ); + } + + // if json is empty, and if channelEmotes is incomplete, show loading screen + if ( + Object.keys(channelEmotes).length < 4 || + !userData || + Object.keys(userData).length === 0 + ) { + return ( + <div className="flex h-screen w-full items-center justify-center text-3xl"> + <Loading /> + </div> + ); + } + console.log(channelEmotes); + return ( + <> + <Head> + <title>{title}</title> + <meta + name="description" + content={`${username}'s portfolio on toffee`} + /> + </Head> + <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"> + {/* User "banner" */} + <div className="col-span-10 mb-2 rounded-2xl bg-zinc-800 bg-opacity-70 p-3"> + <div className="flex items-center justify-between p-4"> + <div className="flex flex-row items-center"> + <div className="relative bottom-[70px] w-[169px]"> + <Image + src={userData.avatar_url} + alt="User avatar" + width={140} + height={140} + priority + className="absolute rounded-lg border-4" + style={{ + borderColor: userData.badges[0] + ? userData.badges[0].color + : "grey", + // "glow" effect + boxShadow: `0px 0px 20px 1px ${ + userData.badges[0] + ? userData.badges[0].color + : "transparent" + }`, + }} + /> + </div> + <div className="flex-col"> + <h1 className="text-4xl font-semibold text-white"> + {userData.name} + </h1> + {/* User's badges */} + <div className="mt-1 flex flex-row text-sm"> + {userData.badges ? ( + userData.badges.map( + (badge: { + name: string; + color: string; + priority: number; + }) => { + return ( + <div + style={{ backgroundColor: badge.color }} + className="mr-1 rounded-md bg-purple-500 px-2" + key={badge.name} + > + <span className="text-white">{badge.name}</span> + </div> + ); + } + ) + ) : ( + <></> + )} + </div> + </div> + </div> + <div className="hidden md:block"> + <h1> + <span className="text-4xl font-semibold text-zinc-400"> + $ + </span> + <span className="text-4xl text-white"> + {userData.net_worth.toLocaleString("en-US")} + </span> + </h1> + </div> + </div> + </div> + {/* Main Container */} + <div className="col-span-10 inline-grid grid-cols-7 gap-3 rounded-2xl lg:col-span-7"> + {/* User's Rank/Graph */} + <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-col px-2"> + <h1 className="mb-1 whitespace-nowrap text-center text-xl font-medium text-white underline"> + Global Rank + </h1> + <div className="flex items-center text-3xl font-bold"> + <span className="text-zinc-400">#</span> + <span className="text-white"> + {userData.rank.toLocaleString("en-US")} + </span> + </div> + </div> + <div className="hidden md:block"> + <Image + src="/img/well_drawn_rank_chart.webp" + alt="Rank chart" + width={497} + height={100} + /> + </div> + <div className="md:hidden"> + <h1> + <span className="text-3xl font-semibold text-zinc-400 sm:text-4xl"> + $ + </span> + <span className="text-3xl text-white sm:text-4xl"> + {userData.net_worth.toLocaleString("en-US")} + </span> + </h1> + </div> + </div> + </div> + {/* User's Assets */} + <div className="col-span-7 flex flex-col rounded-2xl bg-zinc-800 bg-opacity-70"> + {/* User's Assets Header */} + <div className="h-11 w-full rounded-t-2xl bg-pink-400"> + <h1 className="m-1 text-center text-2xl font-bold"> + Top Assets + </h1> + </div> + {/* User's Assets Body */} + <div className="inline-grid grid-cols-2 items-center justify-start gap-2 p-5 sm:grid-cols-3 xl:grid-cols-4"> + {errorCode === 20000 ? ( + <h1 className=" text-zinc-400">{`Could not load assets`}</h1> + ) : ( + userData.assets.map( + (asset: { + name: string; + count: number; + provider: string; + }) => ( + <div + className="flex items-center justify-center" + key={asset.name} + > + <div className="flex h-44 w-full max-w-[256px] flex-col items-center rounded-xl bg-zinc-900 bg-opacity-80 p-2"> + <div className="mt-2 mb-2 h-24 w-24"> + <div className="flex h-full w-full items-center justify-start p-2"> + { + // 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 ? ( + <h1 className="text-center text-zinc-400">{`404 :(`}</h1> + ) : ( + <Image + src={ + channelEmotes[asset.provider][ + asset.name + ] ?? "" + } + alt={asset.name} + width={100} + height={100} + className="max-h-[100px]" + /> + ) + } + {/* Fix asset count to bottom right of image */} + <div className="relative rounded-full bg-zinc-900 bg-opacity-80 p-1"> + <p + className="absolute -bottom-10 -right-2 -rotate-12 text-lg font-bold text-white" + style={{ textShadow: "0px 0px 4px black" }} + > + x{asset.count} + </p> + </div> + </div> + </div> + <div className="flex w-full flex-row items-center justify-center"> + { + // show provider logo (7tv, bttv, ffz, ttv) + asset.provider === "7tv" ? ( + <div className="mr-1 pt-[1px] text-7tv "> + <SevenTVLogo /> + </div> + ) : asset.provider === "bttv" ? ( + <div className="mr-1 pt-[1px] text-bttv"> + <BTTVLogo /> + </div> + ) : asset.provider === "ffz" ? ( + <div className="h-5 w-6 text-white"> + <FFZLogo /> + </div> + ) : ( + <div className="mr-1 w-4 pt-[1px] text-ttv"> + <TwitchLogo /> + </div> + ) + } + <p className="text-md max-w-[80%] overflow-hidden overflow-ellipsis whitespace-nowrap font-bold text-white"> + {asset.name} + </p> + </div> + </div> + </div> + ) + ) + )} + </div> + </div> + </div> + {/* Sidebar */} + <div className="col-span-10 flex flex-col justify-start md:flex-row lg:col-span-3 lg:flex-col"> + <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"> + {/* User's Stats, left side is label, right side is value */} + <h1>Points</h1> + <h1>{userData.points.toLocaleString("en-US")}</h1> + <h1>Shares</h1> + <h1>{userData.shares.toLocaleString("en-US")}</h1> + <h1>Trades</h1> + <h1>{(userData.trades ?? 0).toLocaleString("en-US")}</h1> + <h1>Peak rank</h1> + <h1>{(userData.peak_rank ?? 0).toLocaleString("en-US")}</h1> + <h1>Joined</h1> + <h1> + {new Date(userData.joined ?? 0).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + })} + </h1> + </div> + {/* User's Favorite Emote */} + <div className="flex flex-col rounded-2xl bg-zinc-800 bg-opacity-70"> + <div className="h-11 w-full rounded-t-2xl bg-pink-400"> + <h1 className="m-1 text-center text-2xl font-bold"> + Favorite Emote + </h1> + </div> + <div> + <p className="m-5 text-lg text-zinc-400"> + This user has not yet set a favorite emote. + </p> + </div> + </div> + </div> + </div> + </div> + </> + ); +} + +const SevenTVLogo = () => { + return ( + <svg viewBox="0 0 109.6 80.9" width="1em"> + <g> + <path + d="M84.1,22.2l5-8.7,2.7-4.6L86.8.2V0H60.1l5,8.7,5,8.7,2.8,4.8H84.1" + fill="currentColor" + ></path> + <path + d="M29,80.6l5-8.7,5-8.7,5-8.7,5-8.7,5-8.7,5-8.7L62.7,22l-5-8.7-5-8.7L49.9.1H7.7l-5,8.7L0,13.4l5,8.7v.2h32l-5,8.7-5,8.7-5,8.7-5,8.7-5,8.7L8.5,72l5,8.7v.2H29" + fill="currentColor" + ></path> + <path + d="M70.8,80.6H86.1l5-8.7,5-8.7,5-8.7,5-8.7,3.5-6-5-8.7v-.2H89.2l-5,8.7-5,8.7-.7,1.3-5-8.7-5-8.7-.7-1.3-5,8.7-5,8.7L55,53.1l5,8.7,5,8.7,5,8.7.8,1.4" + fill="currentColor" + ></path> + </g> + </svg> + ); +}; + +const FFZLogo = () => { + return ( + <svg viewBox="0 0 396 396"> + <path + d="m150.06,151.58c-.77-6.33.31-12.45,1.99-18.5,2.37-8.51,9.11-14.22,15.62-18.44,8.71-5.65,18.98-8.6,30.02-8.89,7.26-.19,12.89,3.13,18.17,6.2,3.98,2.32,7.8,6.66,10.25,11.43,2.99,5.83,5.92,11.51,7.05,18.18,1.17,6.9,4.69,13.3,9.05,18.55,4.61,5.55,5.63,11.86,5.53,18.1-.15,8.77,3.32,16.07,6.61,23.64.93,2.14,2.15,3.87,4.76,3.08,2.58-.77,4.04-2.69,3.78-5.44-.32-3.41-.64-6.86-1.43-10.18-1.01-4.18-1.54-8.31-1.45-12.65.19-9.37,6.06-15.83,12.06-21.43,5.81-5.44,9.29-4.23,15.15,1.48,7.52,7.32,14.31,15.22,18.49,24.91,3.86,8.94,7.62,17.97,10.54,27.24,1.79,5.7,2.26,11.9,2.71,17.92.35,4.7-2.69,8.43-6.01,11.17-12.05,9.97-24.04,20.06-38.64,26.39-3.06,1.32-5.93,3.65-8.27,5.9-4.3,4.13-8.7,4.8-14.22,3.81-6.67-1.2-12.21,2.2-17.76,5.37-10.83,6.19-21.98,11.44-34.99,10.56-2.65-.18-5.35-.19-7.99.05-9.67.89-18.35-1.86-26.44-6.91-5.06-3.16-10.74-5.27-15.39-9.12-.62-.51-1.58-.75-1.96-1.37-3.61-5.84-8.03-5.41-13.72-2.82-7.66,3.48-8.07,3.02-13.2-3.77-3.48-4.6-8.91-6.59-13.27-10.01-11.43-8.97-22.52-18.11-29.39-31.42-2.17-4.21-2.9-8.38-2.59-12.82.76-10.87,1-21.85,7.82-31.3,6.01-8.32,10.79-17.68,19.29-23.9,5.9-4.32,10.15-2.9,14.27,3.26,6.93,10.36,7.99,21.4,4.42,33.03-1.46,4.76-.62,9.52-.8,14.28-.09,2.23,2.26,4.61,3.36,4.24,2.47-.83,5.83.99,7.52-2.37,5.96-11.87,14.26-22.67,17.36-35.82,1.65-7.02,2.21-14.34,1.72-21.63Z" + fill="currentColor" + /> + </svg> + ); +}; + +const BTTVLogo = () => { + return ( + <svg viewBox="0 0 300 300" height="1em"> + <path + fill="transparent" + d="M249.771 150A99.771 99.922 0 0 1 150 249.922 99.771 99.922 0 0 1 50.229 150 99.771 99.922 0 0 1 150 50.078 99.771 99.922 0 0 1 249.771 150Z" + ></path> + <path + fill="currentColor" + d="M150 1.74C68.409 1.74 1.74 68.41 1.74 150S68.41 298.26 150 298.26h148.26V150.17h-.004c0-.057.004-.113.004-.17C298.26 68.409 231.59 1.74 150 1.74zm0 49c55.11 0 99.26 44.15 99.26 99.26 0 55.11-44.15 99.26-99.26 99.26-55.11 0-99.26-44.15-99.26-99.26 0-55.11 44.15-99.26 99.26-99.26z" + ></path> + <path + fill="currentColor" + d="M161.388 70.076c-10.662 0-19.42 7.866-19.42 17.67 0 9.803 8.758 17.67 19.42 17.67 10.662 0 19.42-7.867 19.42-17.67 0-9.804-8.758-17.67-19.42-17.67zm45.346 24.554-.02.022-.004.002c-5.402 2.771-11.53 6.895-18.224 11.978l-.002.002-.004.002c-25.943 19.766-60.027 54.218-80.344 80.33h-.072l-1.352 1.768c-5.114 6.69-9.267 12.762-12.098 18.006l-.082.082.022.021v.002l.004.002.174.176.052-.053.102.053-.07.072c30.826 30.537 81.213 30.431 111.918-.273 30.783-30.784 30.8-81.352.04-112.152l-.005-.004zM87.837 142.216c-9.803 0-17.67 8.758-17.67 19.42 0 10.662 7.867 19.42 17.67 19.42 9.804 0 17.67-8.758 17.67-19.42 0-10.662-7.866-19.42-17.67-19.42z" + ></path> + </svg> + ); +}; + +const TwitchLogo = () => { + return ( + <svg x="0px" y="0px" viewBox="0 0 2400 2800"> + <g> + <polygon + className="fill-white" + points="2200,1300 1800,1700 1400,1700 1050,2050 1050,1700 600,1700 600,200 2200,200" + /> + <g> + <g id="Layer_1-2"> + <path + fill="currentColor" + d="M500,0L0,500v1800h600v500l500-500h400l900-900V0H500z M2200,1300l-400,400h-400l-350,350v-350H600V200h1600 V1300z" + /> + <rect + x="1700" + y="550" + fill="currentColor" + width="200" + height="600" + /> + <rect + x="1150" + y="550" + fill="currentColor" + width="200" + height="600" + /> + </g> + </g> + </g> + </svg> + ); +}; + +UserPage.getLayout = function getLayout(page: ReactElement) { + return <DashLayout>{page}</DashLayout>; +}; + +export default UserPage; diff --git a/public/img/well_drawn_rank_chart.webp b/public/img/well_drawn_rank_chart.webp new file mode 100644 index 0000000..39d254f Binary files /dev/null and b/public/img/well_drawn_rank_chart.webp differ diff --git a/tailwind.config.js b/tailwind.config.js index 325ade7..0685455 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -13,6 +13,11 @@ module.exports = { robotoMono: ["Roboto Mono", "monospace"], minecraft: ["Minecraft", "Roboto", "sans-serif"], }, + colors: { + "7tv": "#4fc2bc", + bttv: "#d50014", + ttv: "#9146FF", + }, }, }, plugins: [],