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

@@ -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",

30
pnpm-lock.yaml generated
View File

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

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,
};
};