mirror of
https://github.com/FranP-code/spooky-spotify-showcase.git
synced 2025-10-13 00:02:36 +00:00
Charts
This commit is contained in:
@@ -21,12 +21,14 @@
|
|||||||
"@trpc/client": "^11.0.0-rc.446",
|
"@trpc/client": "^11.0.0-rc.446",
|
||||||
"@trpc/react-query": "^11.0.0-rc.446",
|
"@trpc/react-query": "^11.0.0-rc.446",
|
||||||
"@trpc/server": "^11.0.0-rc.446",
|
"@trpc/server": "^11.0.0-rc.446",
|
||||||
|
"chart.js": "^4.4.5",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.5.1",
|
||||||
"framer-motion": "^11.11.9",
|
"framer-motion": "^11.11.9",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"ldrs": "^1.0.2",
|
"ldrs": "^1.0.2",
|
||||||
"next": "^14.2.4",
|
"next": "^14.2.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-parallax-tilt": "^1.7.246",
|
"react-parallax-tilt": "^1.7.246",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
|
|||||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^11.0.0-rc.446
|
specifier: ^11.0.0-rc.446
|
||||||
version: 11.0.0-rc.566
|
version: 11.0.0-rc.566
|
||||||
|
chart.js:
|
||||||
|
specifier: ^4.4.5
|
||||||
|
version: 4.4.5
|
||||||
cloudinary:
|
cloudinary:
|
||||||
specifier: ^2.5.1
|
specifier: ^2.5.1
|
||||||
version: 2.5.1
|
version: 2.5.1
|
||||||
@@ -44,6 +47,9 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 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:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
@@ -171,6 +177,9 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.2':
|
||||||
|
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
|
||||||
|
|
||||||
'@next/env@14.2.15':
|
'@next/env@14.2.15':
|
||||||
resolution: {integrity: sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==}
|
resolution: {integrity: sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==}
|
||||||
|
|
||||||
@@ -555,6 +564,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
chart.js@4.4.5:
|
||||||
|
resolution: {integrity: sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==}
|
||||||
|
engines: {pnpm: '>=8'}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
@@ -1539,6 +1552,12 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
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:
|
react-dom@18.3.1:
|
||||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1946,6 +1965,8 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.2': {}
|
||||||
|
|
||||||
'@next/env@14.2.15': {}
|
'@next/env@14.2.15': {}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@14.2.15':
|
'@next/eslint-plugin-next@14.2.15':
|
||||||
@@ -2336,6 +2357,10 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
chart.js@4.4.5:
|
||||||
|
dependencies:
|
||||||
|
'@kurkle/color': 0.3.2
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
@@ -3409,6 +3434,11 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
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):
|
react-dom@18.3.1(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|||||||
171
src/app/_components/charts.tsx
Normal file
171
src/app/_components/charts.tsx
Normal 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;
|
||||||
@@ -8,15 +8,22 @@ import UserShowcase from "./user-showcase";
|
|||||||
import { TypographyH2 } from "./h2";
|
import { TypographyH2 } from "./h2";
|
||||||
import { TypographyH1 } from "./h1";
|
import { TypographyH1 } from "./h1";
|
||||||
import ScrollSlider from "./scroll-slider";
|
import ScrollSlider from "./scroll-slider";
|
||||||
|
import Charts from "./charts";
|
||||||
|
|
||||||
export function Showcase({
|
export function Showcase({
|
||||||
userData,
|
userData,
|
||||||
tracksByAlbum,
|
tracksByAlbum,
|
||||||
artists,
|
artists,
|
||||||
|
longTermArtistData,
|
||||||
|
longTermTracksData,
|
||||||
|
longTermTracksByAlbum,
|
||||||
}: {
|
}: {
|
||||||
userData: Record<string, any>;
|
userData: Record<string, any>;
|
||||||
tracksByAlbum: Record<string, any>;
|
tracksByAlbum: Record<string, any>;
|
||||||
artists: any[];
|
artists: any[];
|
||||||
|
// longTermArtistData
|
||||||
|
// longTermTracksData
|
||||||
|
// longTermTracksByAlbum
|
||||||
}) {
|
}) {
|
||||||
const [spookify, setSpookify] = useState(true);
|
const [spookify, setSpookify] = useState(true);
|
||||||
const [lastSpookyImageLoaded, setLastSpookyImageLoaded] = useState(0);
|
const [lastSpookyImageLoaded, setLastSpookyImageLoaded] = useState(0);
|
||||||
@@ -76,6 +83,23 @@ export function Showcase({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ export default async function SpotifyData({
|
|||||||
},
|
},
|
||||||
artists: userData?.artists,
|
artists: userData?.artists,
|
||||||
tracksByAlbum: userData?.tracksByAlbum,
|
tracksByAlbum: userData?.tracksByAlbum,
|
||||||
|
longTermArtistData: false,
|
||||||
|
longTermTracksData: false,
|
||||||
|
longTermTracksByAlbum: false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return getSpotifyData({
|
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) {
|
if (!artists || !tracksByAlbum || !userData?.body) {
|
||||||
return <div>Error fetching data</div>;
|
return <div>Error fetching data</div>;
|
||||||
@@ -198,6 +208,9 @@ export default async function SpotifyData({
|
|||||||
userData={userData?.body}
|
userData={userData?.body}
|
||||||
tracksByAlbum={tracksByAlbum}
|
tracksByAlbum={tracksByAlbum}
|
||||||
artists={artists}
|
artists={artists}
|
||||||
|
longTermArtistData={longTermArtistData}
|
||||||
|
longTermTracksData={longTermTracksData}
|
||||||
|
longTermTracksByAlbum={longTermTracksByAlbum}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
37
src/app/_components/vertical-selector.tsx
Normal file
37
src/app/_components/vertical-selector.tsx
Normal 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;
|
||||||
@@ -11,7 +11,12 @@ const spotifyApi = new SpotifyWebApi({
|
|||||||
const state = process.env.SPOTIFY_STATE as string;
|
const state = process.env.SPOTIFY_STATE as string;
|
||||||
|
|
||||||
export async function GET(req: NextApiRequest, res: NextApiResponse) {
|
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);
|
const authorizeURL = spotifyApi.createAuthorizeURL(scopes, state);
|
||||||
|
|
||||||
console.log(authorizeURL);
|
console.log(authorizeURL);
|
||||||
|
|||||||
@@ -2,6 +2,100 @@ import SpotifyWebApi from "spotify-web-api-node";
|
|||||||
import { FETCH_ARTISTS_LIMIT, FETCH_TRACKS_LIMIT } from "./contants";
|
import { FETCH_ARTISTS_LIMIT, FETCH_TRACKS_LIMIT } from "./contants";
|
||||||
import { TrackByAlbum } from "../_components/spotify-data";
|
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 ({
|
export const getSpotifyData = async ({
|
||||||
accessToken,
|
accessToken,
|
||||||
tracksLimit,
|
tracksLimit,
|
||||||
@@ -18,7 +112,13 @@ export const getSpotifyData = async ({
|
|||||||
});
|
});
|
||||||
spotifyApi.setAccessToken(accessToken);
|
spotifyApi.setAccessToken(accessToken);
|
||||||
|
|
||||||
const [artistsData, tracksData, userData] = await Promise.all([
|
const [
|
||||||
|
artistsData,
|
||||||
|
tracksData,
|
||||||
|
userData,
|
||||||
|
longTermArtistData,
|
||||||
|
longTermTracksData,
|
||||||
|
] = await Promise.all([
|
||||||
spotifyApi.getMyTopArtists({
|
spotifyApi.getMyTopArtists({
|
||||||
limit: Math.min(
|
limit: Math.min(
|
||||||
...[FETCH_ARTISTS_LIMIT, artistsLimit].filter(
|
...[FETCH_ARTISTS_LIMIT, artistsLimit].filter(
|
||||||
@@ -36,6 +136,14 @@ export const getSpotifyData = async ({
|
|||||||
time_range: "short_term",
|
time_range: "short_term",
|
||||||
}),
|
}),
|
||||||
spotifyApi.getMe(),
|
spotifyApi.getMe(),
|
||||||
|
spotifyApi.getMyTopArtists({
|
||||||
|
limit: 50,
|
||||||
|
time_range: "long_term",
|
||||||
|
}),
|
||||||
|
spotifyApi.getMyTopTracks({
|
||||||
|
limit: 50,
|
||||||
|
time_range: "long_term",
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const artists = artistsData.body.items;
|
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 {
|
return {
|
||||||
userData,
|
userData,
|
||||||
tracksByAlbum,
|
tracksByAlbum,
|
||||||
artists,
|
artists,
|
||||||
|
longTermArtistData,
|
||||||
|
longTermTracksData,
|
||||||
|
longTermTracksByAlbum,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user