diff --git a/.gitignore b/.gitignore index 63c739d..bbe04ac 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ next-env.d.ts # Sentry Config File .sentryclirc + +# Webpack bundle analyzer +analyze diff --git a/next.config.js b/next.config.js index ff32472..d59a8a8 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,7 @@ const nextBuildId = require("next-build-id"); +const withBundleAnalyzer = require("@next/bundle-analyzer")({ + enabled: false, +}); /** @type {import('next').NextConfig} */ const nextConfig = { @@ -26,10 +29,9 @@ const nextConfig = { }, }; -module.exports = nextConfig; +module.exports = withBundleAnalyzer(nextConfig); - -// Injected content via Sentry wizard below +// // Injected content via Sentry wizard below const { withSentryConfig } = require("@sentry/nextjs"); @@ -43,17 +45,17 @@ module.exports = withSentryConfig( silent: true, org: "sentry", project: "scoresaber-reloaded", - url: "https://sentry.fascinated.cc/" + url: "https://sentry.fascinated.cc/", }, { // For all available options, see: // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, + widenClientFileUpload: false, // Transpiles SDK to be compatible with IE11 (increases bundle size) - transpileClientSDK: true, + transpileClientSDK: false, // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) tunnelRoute: "/monitoring", @@ -63,5 +65,5 @@ module.exports = withSentryConfig( // Automatically tree-shake Sentry logger statements to reduce bundle size disableLogger: true, - } + }, ); diff --git a/package-lock.json b/package-lock.json index 92b6dfd..25b1298 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "bluebird": "^3.7.2", "chart.js": "^4.4.0", "clsx": "^2.0.0", + "date-fns": "^2.30.0", "encoding": "^0.1.13", - "moment": "^2.29.4", "next": "13.5.6", "next-build-id": "^3.0.0", "node-fetch-cache": "^3.1.3", @@ -27,11 +27,13 @@ "zustand": "^4.4.3" }, "devDependencies": { + "@next/bundle-analyzer": "^13.5.6", "@types/node": "^20", "@types/node-fetch-cache": "^3.0.3", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.4.16", + "cross-env": "^7.0.3", "eslint": "^8", "eslint-config-next": "13.5.6", "postcss": "^8.4.31", @@ -66,7 +68,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -228,6 +229,15 @@ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" }, + "node_modules/@next/bundle-analyzer": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.5.6.tgz", + "integrity": "sha512-4P5YVpR3N/B5+p0TQ/rPAr+9fsjkdfCVTGzJhKwE7XHqS+QME4gYxAYeGKkfkHEkP2A3GKXs8QSp0LjIvWLI3g==", + "dev": true, + "dependencies": { + "webpack-bundle-analyzer": "4.7.0" + } + }, "node_modules/@next/env": { "version": "13.5.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", @@ -434,6 +444,12 @@ "node": ">=10" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.23", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", + "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", + "dev": true + }, "node_modules/@rollup/plugin-commonjs": { "version": "24.0.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz", @@ -960,6 +976,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1657,6 +1682,24 @@ "node": ">= 0.6" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1695,6 +1738,21 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1832,6 +1890,12 @@ "node": ">=6.0.0" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.557", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.557.tgz", @@ -2824,6 +2888,21 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -3555,6 +3634,12 @@ "resolved": "https://registry.npmjs.org/locko/-/locko-1.0.0.tgz", "integrity": "sha512-eK3TW5bJs6BrjmtzEb+RSyKt48etEitQviC6A6qMMSYl0dDFl0tIOD23QV/i0/rTuuXUgSYWX1yssbtop97Gog==" }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3744,12 +3829,13 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, "engines": { - "node": "*" + "node": ">=10" } }, "node_modules/ms": { @@ -4072,6 +4158,15 @@ "wrappy": "1" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4720,8 +4815,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", @@ -5047,6 +5141,20 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5432,6 +5540,15 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -5681,6 +5798,38 @@ "node": ">=10.13.0" } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", + "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==", + "dev": true, + "dependencies": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", @@ -5784,6 +5933,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 70fa296..5666679 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "bluebird": "^3.7.2", "chart.js": "^4.4.0", "clsx": "^2.0.0", + "date-fns": "^2.30.0", "encoding": "^0.1.13", - "moment": "^2.29.4", "next": "13.5.6", "next-build-id": "^3.0.0", "node-fetch-cache": "^3.1.3", @@ -28,11 +28,13 @@ "zustand": "^4.4.3" }, "devDependencies": { + "@next/bundle-analyzer": "^13.5.6", "@types/node": "^20", "@types/node-fetch-cache": "^3.0.3", "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.4.16", + "cross-env": "^7.0.3", "eslint": "^8", "eslint-config-next": "13.5.6", "postcss": "^8.4.31", diff --git a/src/components/player/PlayerInfo.tsx b/src/components/player/PlayerInfo.tsx index 4c2337c..88bc9c1 100644 --- a/src/components/player/PlayerInfo.tsx +++ b/src/components/player/PlayerInfo.tsx @@ -19,8 +19,8 @@ import { useStore } from "zustand"; import Avatar from "../Avatar"; import Card from "../Card"; import Label from "../Label"; -import PlayerChart from "./PlayerChart"; +const PlayerChart = dynamic(() => import("./PlayerChart")); const ReactCountryFlag = dynamic(() => import("react-country-flag")); type PlayerInfoProps = { diff --git a/src/components/player/Score.tsx b/src/components/player/Score.tsx index 532e825..ab66560 100644 --- a/src/components/player/Score.tsx +++ b/src/components/player/Score.tsx @@ -2,13 +2,13 @@ import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberScore } from "@/schemas/scoresaber/score"; import { formatNumber } from "@/utils/number"; +import { formatDate, formatTimeAgo } from "@/utils/timeUtils"; import { CheckIcon, GlobeAsiaAustraliaIcon, XMarkIcon, } from "@heroicons/react/20/solid"; import clsx from "clsx"; -import moment from "moment"; import Image from "next/image"; import ScoreStatLabel from "./ScoreStatLabel"; @@ -28,8 +28,11 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {

#{score.rank}

-

- {moment(score.timeSet).fromNow()} +

+ {formatTimeAgo(score.timeSet)}

{/* Song Image */} @@ -60,9 +63,11 @@ export default function Score({ score, player, leaderboard }: ScoreProps) { {/* Time Set (Mobile) */}
- {" "} -

- {moment(score.timeSet).fromNow()} +

+ {formatTimeAgo(score.timeSet)}

diff --git a/src/store/scoresaberScoresStore.ts b/src/store/scoresaberScoresStore.ts index 48ec9f0..6a7c9fa 100644 --- a/src/store/scoresaberScoresStore.ts +++ b/src/store/scoresaberScoresStore.ts @@ -3,7 +3,6 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberSmallerPlayerScore } from "@/schemas/scoresaber/smaller/smallerPlayerScore"; import { ScoreSaberAPI } from "@/utils/scoresaber/api"; -import moment from "moment"; import { toast } from "react-toastify"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -215,8 +214,8 @@ export const useScoresaberScoresStore = create()( if (timeUntilRefreshMs > 0) { console.log( "Waiting", - moment.duration(timeUntilRefreshMs).humanize(), - "to refresh scores for players", + timeUntilRefreshMs / 1000, + "seconds to refresh scores for players", ); setTimeout( () => useScoresaberScoresStore.getState().updatePlayerScores(), diff --git a/src/store/settingsStore.ts b/src/store/settingsStore.ts index ec39356..f01fa1e 100644 --- a/src/store/settingsStore.ts +++ b/src/store/settingsStore.ts @@ -3,7 +3,6 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { SortType, SortTypes } from "@/types/SortTypes"; import { ScoreSaberAPI } from "@/utils/scoresaber/api"; -import moment from "moment"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -81,7 +80,7 @@ export const useSettingsStore = create()( if (timeUntilRefreshMs > 0) { console.log( "Waiting", - moment.duration(timeUntilRefreshMs).humanize(), + timeUntilRefreshMs / 1000, "to refresh profiles", ); setTimeout(() => this.refreshProfiles(), timeUntilRefreshMs); diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts new file mode 100644 index 0000000..40f6d2e --- /dev/null +++ b/src/utils/timeUtils.ts @@ -0,0 +1,40 @@ +import { formatDistanceToNow, parseISO } from "date-fns"; + +/** + * Formats a timestamp to a human readable format + * eg: 1 minute ago, 2 hours ago, 3 days ago + * + * @param timestamp the timestamp to format + * @returns the formatted timestamp + */ +export function formatTimeAgo(timestamp: string) { + const date = parseISO(timestamp); + const now = new Date(); + if (date > now) { + return "just now"; + } + + const timeDifference = formatDistanceToNow(date); + if (timeDifference === "less than a minute") { + return "just now"; + } else { + return `${timeDifference.replace("about", "").replace("almost", "")} ago`; + } +} + +/** + * Formats a timestamp to a human readable format + * + * @param timestamp the timestamp to format + * @returns the formatted timestamp + */ +export function formatDate(timestamp: string) { + const date = parseISO(timestamp); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); +}