This commit is contained in:
2024-10-18 03:48:11 -03:00
parent 36626e3d4c
commit 4a5dae35d1
8 changed files with 423 additions and 3 deletions

View File

@@ -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: () => (
<Bar
className="w-full"
data={chartData}
options={{
indexAxis: "y",
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
beginAtZero: true,
grid: {
display: false,
},
ticks: {
color: "#94a3b8",
},
},
x: {
grid: {
display: false,
},
ticks: {
color: "#94a3b8",
stepSize: 1,
},
},
},
}}
/>
),
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: () => (
<Bar
className="w-full"
data={chartData}
options={{
indexAxis: "y",
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
beginAtZero: true,
grid: {
display: false,
},
ticks: {
color: "#94a3b8",
},
},
x: {
grid: {
display: false,
},
ticks: {
stepSize: 1,
color: "#94a3b8",
},
},
},
}}
/>
),
};
}
}, [options]);
return (
<div className="flex items-start gap-2">
<VerticalSelector
className="w-fit justify-start"
options={options}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
/>
<div
className="chart-container"
style={{ width: "100%", height: "1500px" }}
>
<ChartComponent />
</div>
</div>
);
};
export default Charts;

View File

@@ -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<string, any>;
tracksByAlbum: Record<string, any>;
artists: any[];
// longTermArtistData
// longTermTracksData
// longTermTracksByAlbum
}) {
const [spookify, setSpookify] = useState(true);
const [lastSpookyImageLoaded, setLastSpookyImageLoaded] = useState(0);
@@ -76,6 +83,23 @@ export function Showcase({
);
})}
</div>
{longTermArtistData && longTermTracksData && longTermTracksByAlbum && (
<>
<ScrollSlider className="w-full">
<TypographyH1 className="mb-2 mt-8 self-center text-center text-3xl lg:text-4xl">
Charts
</TypographyH1>
<p className="text-center text-lg text-white text-opacity-40 backdrop-blur-lg backdrop-filter">
Let's see what's trending in your world.
</p>
<Charts
longTermArtistData={longTermArtistData}
longTermTracksData={longTermTracksData}
longTermTracksByAlbum={longTermTracksByAlbum}
/>
</ScrollSlider>
</>
)}
</>
);
}

View File

@@ -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 <div>Error fetching data</div>;
@@ -198,6 +208,9 @@ export default async function SpotifyData({
userData={userData?.body}
tracksByAlbum={tracksByAlbum}
artists={artists}
longTermArtistData={longTermArtistData}
longTermTracksData={longTermTracksData}
longTermTracksByAlbum={longTermTracksByAlbum}
/>
</>
);

View File

@@ -0,0 +1,37 @@
import { useState } from "react";
export const VerticalSelector = ({
className,
options,
selectedOption,
setSelectedOption,
}) => {
return (
<div className={`rounded-lg bg-white p-2 shadow ${className}`}>
<ul className="space-y-2">
{options.map((option) => (
<li key={option}>
<button
className={`relative w-full overflow-hidden rounded-md p-3 text-left transition-colors duration-200 ${
selectedOption === option
? "bg-black text-white"
: "bg-transparent text-gray-800 hover:bg-gray-100"
}`}
onClick={() => setSelectedOption(option)}
aria-pressed={selectedOption === option}
style={{
position: "relative",
zIndex: selectedOption === option ? 1 : 0,
transition: "background-color 0.3s ease, color 0.3s ease",
}}
>
<span className="relative">{option}</span>
</button>
</li>
))}
</ul>
</div>
);
};
export default VerticalSelector;

View File

@@ -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);

View File

@@ -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<string, TrackByAlbum>, 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,
};
};