diff --git a/package.json b/package.json
index 475ce0c..dedf6fe 100644
--- a/package.json
+++ b/package.json
@@ -21,12 +21,14 @@
"@trpc/client": "^11.0.0-rc.446",
"@trpc/react-query": "^11.0.0-rc.446",
"@trpc/server": "^11.0.0-rc.446",
+ "chart.js": "^4.4.5",
"cloudinary": "^2.5.1",
"framer-motion": "^11.11.9",
"geist": "^1.3.0",
"ldrs": "^1.0.2",
"next": "^14.2.4",
"react": "^18.3.1",
+ "react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-parallax-tilt": "^1.7.246",
"server-only": "^0.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9290956..97d482c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
'@trpc/server':
specifier: ^11.0.0-rc.446
version: 11.0.0-rc.566
+ chart.js:
+ specifier: ^4.4.5
+ version: 4.4.5
cloudinary:
specifier: ^2.5.1
version: 2.5.1
@@ -44,6 +47,9 @@ importers:
react:
specifier: ^18.3.1
version: 18.3.1
+ react-chartjs-2:
+ specifier: ^5.2.0
+ version: 5.2.0(chart.js@4.4.5)(react@18.3.1)
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
@@ -171,6 +177,9 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+ '@kurkle/color@0.3.2':
+ resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
+
'@next/env@14.2.15':
resolution: {integrity: sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==}
@@ -555,6 +564,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ chart.js@4.4.5:
+ resolution: {integrity: sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==}
+ engines: {pnpm: '>=8'}
+
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -1539,6 +1552,12 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ react-chartjs-2@5.2.0:
+ resolution: {integrity: sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==}
+ peerDependencies:
+ chart.js: ^4.1.1
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@@ -1946,6 +1965,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
+ '@kurkle/color@0.3.2': {}
+
'@next/env@14.2.15': {}
'@next/eslint-plugin-next@14.2.15':
@@ -2336,6 +2357,10 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ chart.js@4.4.5:
+ dependencies:
+ '@kurkle/color': 0.3.2
+
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@@ -3409,6 +3434,11 @@ snapshots:
queue-microtask@1.2.3: {}
+ react-chartjs-2@5.2.0(chart.js@4.4.5)(react@18.3.1):
+ dependencies:
+ chart.js: 4.4.5
+ react: 18.3.1
+
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
diff --git a/src/app/_components/charts.tsx b/src/app/_components/charts.tsx
new file mode 100644
index 0000000..2c59d26
--- /dev/null
+++ b/src/app/_components/charts.tsx
@@ -0,0 +1,171 @@
+import Chart from "chart.js/auto";
+import { CategoryScale } from "chart.js";
+import { Bar } from "react-chartjs-2";
+import VerticalSelector from "./vertical-selector";
+import { useMemo, useState } from "react";
+
+Chart.register(CategoryScale);
+
+export const Charts = ({
+ longTermArtistData,
+ longTermTracksData,
+ longTermTracksByAlbum,
+}) => {
+ const options = ["Top albums", "Artist popularity"];
+ const [selectedOption, setSelectedOption] = useState(options[0]);
+ const a = "";
+ console.log({
+ longTermArtistData,
+ longTermTracksData,
+ longTermTracksByAlbum,
+ });
+ const topAlbums = Object.values(longTermTracksByAlbum).sort(
+ (a, b) => b.tracks.length - a.tracks.length,
+ );
+ const topAlbumsData = topAlbums.map((album, i) => {
+ const label = album.album.name;
+ return {
+ album: label,
+ position: album.tracks.length,
+ };
+ });
+ const topArtistsData = longTermArtistData.body.items.sort(
+ (a, b) => b.popularity - a.popularity,
+ );
+ // debugger;
+ const { ChartComponent } = useMemo(() => {
+ if (selectedOption === options[0]) {
+ const chartData = {
+ labels: topAlbumsData.map((data, i) => {
+ const label = `#${i + 1} ${data.album}`;
+ return label.length > 20 ? label.slice(0, 20) + "..." : label;
+ }),
+ datasets: [
+ {
+ label: "Top songs in this album",
+ data: topAlbumsData.map((data) => data.position),
+ backgroundColor: "rgba(75, 192, 192, 0.6)",
+ borderColor: "rgba(75, 192, 192, 1)",
+ borderWidth: 1,
+ barThickness: 27,
+ borderRadius: 2,
+ },
+ ],
+ };
+ return {
+ ChartComponent: () => (
+
+ ),
+ chartData,
+ };
+ }
+ if (selectedOption === options[1]) {
+ const chartData = {
+ labels: topArtistsData.map((data, i) => {
+ const label = `#${i + 1} ${data.name}`;
+ return label.length > 20 ? label.slice(0, 20) + "..." : label;
+ }),
+ datasets: [
+ {
+ label: "Top artists by popularity",
+ data: topArtistsData.map((data) => data.popularity),
+ backgroundColor: "rgba(75, 192, 192, 0.6)",
+ borderColor: "rgba(75, 192, 192, 1)",
+ borderWidth: 1,
+ barThickness: 27,
+ borderRadius: 2,
+ },
+ ],
+ };
+ return {
+ ChartComponent: () => (
+
+ ),
+ };
+ }
+ }, [options]);
+
+ return (
+
+ );
+};
+
+export default Charts;
diff --git a/src/app/_components/showcase.tsx b/src/app/_components/showcase.tsx
index bed4664..cc670ad 100644
--- a/src/app/_components/showcase.tsx
+++ b/src/app/_components/showcase.tsx
@@ -8,15 +8,22 @@ import UserShowcase from "./user-showcase";
import { TypographyH2 } from "./h2";
import { TypographyH1 } from "./h1";
import ScrollSlider from "./scroll-slider";
+import Charts from "./charts";
export function Showcase({
userData,
tracksByAlbum,
artists,
+ longTermArtistData,
+ longTermTracksData,
+ longTermTracksByAlbum,
}: {
userData: Record;
tracksByAlbum: Record;
artists: any[];
+ // longTermArtistData
+ // longTermTracksData
+ // longTermTracksByAlbum
}) {
const [spookify, setSpookify] = useState(true);
const [lastSpookyImageLoaded, setLastSpookyImageLoaded] = useState(0);
@@ -76,6 +83,23 @@ export function Showcase({
);
})}
+ {longTermArtistData && longTermTracksData && longTermTracksByAlbum && (
+ <>
+
+
+ Charts
+
+
+ Let's see what's trending in your world.
+
+
+
+ >
+ )}
>
);
}
diff --git a/src/app/_components/spotify-data.tsx b/src/app/_components/spotify-data.tsx
index 08d76fc..5b9b030 100644
--- a/src/app/_components/spotify-data.tsx
+++ b/src/app/_components/spotify-data.tsx
@@ -47,6 +47,9 @@ export default async function SpotifyData({
},
artists: userData?.artists,
tracksByAlbum: userData?.tracksByAlbum,
+ longTermArtistData: false,
+ longTermTracksData: false,
+ longTermTracksByAlbum: false,
}));
} else {
return getSpotifyData({
@@ -56,7 +59,14 @@ export default async function SpotifyData({
});
}
};
- const { artists, tracksByAlbum, userData } = await fetchData();
+ const {
+ artists,
+ tracksByAlbum,
+ userData,
+ longTermArtistData,
+ longTermTracksData,
+ longTermTracksByAlbum,
+ } = await fetchData();
if (!artists || !tracksByAlbum || !userData?.body) {
return Error fetching data
;
@@ -198,6 +208,9 @@ export default async function SpotifyData({
userData={userData?.body}
tracksByAlbum={tracksByAlbum}
artists={artists}
+ longTermArtistData={longTermArtistData}
+ longTermTracksData={longTermTracksData}
+ longTermTracksByAlbum={longTermTracksByAlbum}
/>
>
);
diff --git a/src/app/_components/vertical-selector.tsx b/src/app/_components/vertical-selector.tsx
new file mode 100644
index 0000000..28a8573
--- /dev/null
+++ b/src/app/_components/vertical-selector.tsx
@@ -0,0 +1,37 @@
+import { useState } from "react";
+
+export const VerticalSelector = ({
+ className,
+ options,
+ selectedOption,
+ setSelectedOption,
+}) => {
+ return (
+
+
+ {options.map((option) => (
+ -
+
+
+ ))}
+
+
+ );
+};
+
+export default VerticalSelector;
diff --git a/src/app/api/spotify-login/route.ts b/src/app/api/spotify-login/route.ts
index be3b492..380ebb6 100644
--- a/src/app/api/spotify-login/route.ts
+++ b/src/app/api/spotify-login/route.ts
@@ -11,7 +11,12 @@ const spotifyApi = new SpotifyWebApi({
const state = process.env.SPOTIFY_STATE as string;
export async function GET(req: NextApiRequest, res: NextApiResponse) {
- const scopes = ["user-library-read", "user-read-private", "user-top-read"];
+ const scopes = [
+ "user-library-read",
+ "user-read-private",
+ "user-top-read",
+ "user-read-recently-played",
+ ];
const authorizeURL = spotifyApi.createAuthorizeURL(scopes, state);
console.log(authorizeURL);
diff --git a/src/app/utils/getSpotifyData.ts b/src/app/utils/getSpotifyData.ts
index b40ec85..9f0fc75 100644
--- a/src/app/utils/getSpotifyData.ts
+++ b/src/app/utils/getSpotifyData.ts
@@ -2,6 +2,100 @@ import SpotifyWebApi from "spotify-web-api-node";
import { FETCH_ARTISTS_LIMIT, FETCH_TRACKS_LIMIT } from "./contants";
import { TrackByAlbum } from "../_components/spotify-data";
+// const getRecentlyPlayedSongs = async ({
+// spotifyApi,
+// accessToken,
+// }: {
+// spotifyApi: SpotifyWebApi;
+// accessToken: string;
+// }) => {
+// const now = Date.now();
+// const oneMonthAgo = now - 30 * 24 * 60 * 60 * 1000;
+
+// let allTracks: SpotifyApi.PlayHistoryObject[] = [];
+
+// const response = await spotifyApi.getMyRecentlyPlayedTracks({
+// after: oneMonthAgo,
+// limit: 50,
+// });
+// allTracks = response.body.items;
+
+// let newUrl = response.body.next;
+
+// try {
+// while (newUrl) {
+// const fetchResponse = await fetch(newUrl, {
+// headers: {
+// Authorization: `Bearer ${accessToken}`,
+// },
+// }).then((res) => res.json());
+
+// if (!fetchResponse?.data?.items) {
+// break;
+// }
+// allTracks = allTracks.concat(fetchResponse.data.items);
+
+// newUrl = response?.data?.next;
+// }
+// } catch (error) {}
+
+// return allTracks;
+// };
+
+const getRecentlyPlayedSongs = async ({
+ spotifyApi,
+ accessToken,
+}: {
+ spotifyApi: SpotifyWebApi;
+ accessToken: string;
+}) => {
+ const now = Date.now();
+ const oneMonthAgo = now - 30 * 24 * 60 * 60 * 1000;
+
+ let allTracks: SpotifyApi.PlayHistoryObject[] = [];
+
+ try {
+ // Initial API request using the SDK
+ const response = await spotifyApi.getMyRecentlyPlayedTracks({
+ after: oneMonthAgo,
+ limit: 50,
+ });
+
+ // Collect the first batch of tracks
+ allTracks = response.body.items;
+
+ // Check if there are more pages of results
+ let newUrl = response.body.next;
+
+ // Fetch additional pages of results if they exist
+ while (newUrl) {
+ const fetchResponse = await fetch(newUrl, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ // Parse the response JSON
+ const data = await fetchResponse.json();
+
+ // If no items are returned, exit the loop
+ if (!data.items) {
+ break;
+ }
+
+ // Concatenate the newly fetched tracks to the list
+ allTracks = allTracks.concat(data.items);
+
+ // Update the newUrl for the next request
+ newUrl = data.next;
+ }
+ } catch (error) {
+ console.error("Error fetching recently played tracks:", error);
+ }
+
+ return allTracks;
+};
+
export const getSpotifyData = async ({
accessToken,
tracksLimit,
@@ -18,7 +112,13 @@ export const getSpotifyData = async ({
});
spotifyApi.setAccessToken(accessToken);
- const [artistsData, tracksData, userData] = await Promise.all([
+ const [
+ artistsData,
+ tracksData,
+ userData,
+ longTermArtistData,
+ longTermTracksData,
+ ] = await Promise.all([
spotifyApi.getMyTopArtists({
limit: Math.min(
...[FETCH_ARTISTS_LIMIT, artistsLimit].filter(
@@ -36,6 +136,14 @@ export const getSpotifyData = async ({
time_range: "short_term",
}),
spotifyApi.getMe(),
+ spotifyApi.getMyTopArtists({
+ limit: 50,
+ time_range: "long_term",
+ }),
+ spotifyApi.getMyTopTracks({
+ limit: 50,
+ time_range: "long_term",
+ }),
]);
const artists = artistsData.body.items;
@@ -66,9 +174,39 @@ export const getSpotifyData = async ({
{},
);
+ const longTermTracks = longTermTracksData.body.items.map((track, i) => ({
+ ...track,
+ position: i + 1,
+ }));
+
+ const longTermTracksByAlbum = longTermTracksData.body.items.reduce(
+ (acc: Record, track) => {
+ if (!acc[track.album.id]) {
+ const tracksWithAlbum = longTermTracks.filter(
+ (t) => t.album.id === track.album.id,
+ );
+ acc[track.album.id] = {
+ album: track.album,
+ position:
+ tracksWithAlbum.reduce(
+ (acc, _track) => 50 / _track.position + acc,
+ 0,
+ ) / tracksWithAlbum.length,
+ tracks: [],
+ };
+ }
+ (acc[track.album.id] || ({ tracks: [] } as any)).tracks.push(track);
+ return acc;
+ },
+ {},
+ );
+
return {
userData,
tracksByAlbum,
artists,
+ longTermArtistData,
+ longTermTracksData,
+ longTermTracksByAlbum,
};
};