From d50e5532788318f64ebb35ba6289506a3feb2c11 Mon Sep 17 00:00:00 2001 From: Francisco Pessano Date: Fri, 18 Oct 2024 00:29:48 -0300 Subject: [PATCH] Placeholder data option --- prisma/schema.prisma | 74 ++++++++ src/app/_components/login-page.tsx | 9 +- src/app/_components/placeholder-data-link.tsx | 20 +++ src/app/_components/spotify-data.tsx | 91 +++++----- src/app/page.tsx | 6 +- src/app/utils/getSpotifyData.ts | 62 +++++++ src/server/api/root.ts | 2 + src/server/api/routers/user-data.ts | 160 ++++++++++++++++++ 8 files changed, 377 insertions(+), 47 deletions(-) create mode 100644 src/app/_components/placeholder-data-link.tsx create mode 100644 src/app/utils/getSpotifyData.ts create mode 100644 src/server/api/routers/user-data.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 33ded7c..40b6fe0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,3 +34,77 @@ model GeneratedImage { createdAt DateTime @default(now()) // Timestamp for when the entry was created updatedAt DateTime @updatedAt // Auto-updating timestamp for when the entry was last updated } + +model ArtistImage { + id String @id @default(uuid()) + url String + artistId String + artist Artist @relation(fields: [artistId], references: [id]) +} + +model Artist { + id String @id @default(uuid()) + name String + images ArtistImage[] + + UserData UserData[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Album { + id String @id @default(uuid()) + name String + images AlbumImage[] + tracks Track[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // UserData UserData[] +} + +model AlbumImage { + id String @id @default(uuid()) + url String + albumId String + album Album @relation(fields: [albumId], references: [id]) +} + +model SpotifyUserImage { + id String @id @default(uuid()) + url String + spotifyUserId String + spotifyUser SpotifyUser @relation(fields: [spotifyUserId], references: [id]) +} + +model Track { + id String @id @default(uuid()) + name String + albumId String + album Album @relation(fields: [albumId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model SpotifyUser { + id String @id @default(uuid()) + spotifyUserId String @unique + displayName String? + images SpotifyUserImage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + UserData UserData[] +} + +model UserData { + id Int @id @default(autoincrement()) // Auto-incrementing ID + artists Artist[] // Relation to the Artist model + // albums Album[] // Relation to the Album model + tracksByAlbum String + spotifyUserId String + spotifyUser SpotifyUser @relation(fields: [spotifyUserId], references: [id]) +} \ No newline at end of file diff --git a/src/app/_components/login-page.tsx b/src/app/_components/login-page.tsx index 2e02e77..586332b 100644 --- a/src/app/_components/login-page.tsx +++ b/src/app/_components/login-page.tsx @@ -2,11 +2,12 @@ import Link from "next/link"; import { TypographyH1 } from "./h1"; import { TypographyH4 } from "./h4"; import SpotifyLogin from "./spotify-login"; +import { PlaceholderDataLink } from "./placeholder-data-link"; export function LoginPage() { return ( -
-
+
+
Spooky Spotify Showcase
+
); } diff --git a/src/app/_components/placeholder-data-link.tsx b/src/app/_components/placeholder-data-link.tsx new file mode 100644 index 0000000..8a00c0b --- /dev/null +++ b/src/app/_components/placeholder-data-link.tsx @@ -0,0 +1,20 @@ +export const PlaceholderDataLink = ({ + className, + text, +}: { + className?: string; + text: string; +}) => { + return ( +
+ + {text} + +
+ ); +}; + +export default PlaceholderDataLink; diff --git a/src/app/_components/spotify-data.tsx b/src/app/_components/spotify-data.tsx index 6ba3ce0..08d930e 100644 --- a/src/app/_components/spotify-data.tsx +++ b/src/app/_components/spotify-data.tsx @@ -1,6 +1,8 @@ import SpotifyWebApi from "spotify-web-api-node"; import Showcase from "./showcase"; import { FETCH_ARTISTS_LIMIT, FETCH_TRACKS_LIMIT } from "../utils/contants"; +import { getSpotifyData } from "../utils/getSpotifyData"; +import { api } from "@/trpc/server"; export type TrackByAlbum = { album: { @@ -18,56 +20,59 @@ export type TrackByAlbum = { export default async function SpotifyData({ accessToken, refreshToken, + placeholderData, }: { accessToken: string; refreshToken: string; + placeholderData: boolean; }) { - const spotifyApi = new SpotifyWebApi({ - clientId: process.env.SPOTIFY_CLIENT_ID, - clientSecret: process.env.SPOTIFY_CLIENT_SECRET, - redirectUri: process.env.SPOTIFY_REDIRECT_URI, - }); - spotifyApi.setAccessToken(accessToken); + const fetchData = async () => { + if (placeholderData) { + //I'm so sorry for what I did here + return api.userData + .get({ + spotifyUserId: process.env.SPOTIFY_OWNER_USER_ID as string, + }) + .then((userData) => ({ + userData: { + body: userData?.spotifyUser + ? { + ...userData.spotifyUser, + display_name: userData.spotifyUser.displayName, + } + : null, + }, + artists: userData?.artists, + tracksByAlbum: userData?.tracksByAlbum, + })); + } else { + return getSpotifyData({ + accessToken, + }); + } + }; + const { artists, tracksByAlbum, userData } = await fetchData(); - const [artistsData, tracksData, userData] = await Promise.all([ - spotifyApi.getMyTopArtists({ - limit: FETCH_ARTISTS_LIMIT, - time_range: "short_term", - }), - spotifyApi.getMyTopTracks({ - limit: FETCH_TRACKS_LIMIT, - time_range: "short_term", - }), - spotifyApi.getMe(), - ]); + if (!artists || !tracksByAlbum || !userData?.body) { + return
Error fetching data
; + } - const artists = artistsData.body.items; - const tracks = tracksData.body.items.map((track, i) => ({ - ...track, - position: i + 1, - })); + if (userData.body.id === process.env.SPOTIFY_OWNER_USER_ID) { + await api.userData.create({ + spotifyUserId: userData.body.id, + artists: artists.map((artist) => ({ + name: artist.name, + images: artist.images, + })), + tracksByAlbum: tracksByAlbum, + user: { + id: userData.body.id, + displayName: userData.body.display_name || "", + images: userData.body.images || [], + }, + }); + } - const tracksByAlbum = tracksData.body.items.reduce( - (acc: Record, track) => { - if (!acc[track.album.id]) { - const tracksWithAlbum = tracks.filter( - (t) => t.album.id === track.album.id, - ); - acc[track.album.id] = { - album: track.album, - position: - tracksWithAlbum.reduce( - (acc, _track) => FETCH_TRACKS_LIMIT / _track.position + acc, - 0, - ) / tracksWithAlbum.length, - tracks: [], - }; - } - (acc[track.album.id] || ({ tracks: [] } as any)).tracks.push(track); - return acc; - }, - {}, - ); // type Track = { // id: string; // name: string; diff --git a/src/app/page.tsx b/src/app/page.tsx index 97dcf8d..a4260ea 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,5 @@ import Link from "next/link"; import { api, HydrateClient } from "@/trpc/server"; -import SpotifyLogin from "./_components/spotify-login"; import SpotifyData from "./_components/spotify-data"; import SpotifyWebApi from "spotify-web-api-node"; import LoginPage from "./_components/login-page"; @@ -12,6 +11,8 @@ export default async function Home({ }) { const hello = await api.post.hello({ text: "from tRPC" }); + const placeholderData = searchParams?.["placeholder-data"]; + const placeholderDataSelected = placeholderData?.toString() === "true"; const access_token = searchParams?.access_token; const refresh_token = searchParams?.refresh_token; let userIsLogged = !!( @@ -41,10 +42,11 @@ export default async function Home({
- {userIsLogged ? ( + {userIsLogged || placeholderDataSelected ? ( ) : ( diff --git a/src/app/utils/getSpotifyData.ts b/src/app/utils/getSpotifyData.ts new file mode 100644 index 0000000..2511e86 --- /dev/null +++ b/src/app/utils/getSpotifyData.ts @@ -0,0 +1,62 @@ +import SpotifyWebApi from "spotify-web-api-node"; +import { FETCH_ARTISTS_LIMIT, FETCH_TRACKS_LIMIT } from "./contants"; +import { TrackByAlbum } from "../_components/spotify-data"; + +export const getSpotifyData = async ({ + accessToken, +}: { + accessToken: string; +}) => { + const spotifyApi = new SpotifyWebApi({ + clientId: process.env.SPOTIFY_CLIENT_ID, + clientSecret: process.env.SPOTIFY_CLIENT_SECRET, + redirectUri: process.env.SPOTIFY_REDIRECT_URI, + }); + spotifyApi.setAccessToken(accessToken); + + const [artistsData, tracksData, userData] = await Promise.all([ + spotifyApi.getMyTopArtists({ + limit: FETCH_ARTISTS_LIMIT, + time_range: "short_term", + }), + spotifyApi.getMyTopTracks({ + limit: FETCH_TRACKS_LIMIT, + time_range: "short_term", + }), + spotifyApi.getMe(), + ]); + + const artists = artistsData.body.items; + const tracks = tracksData.body.items.map((track, i) => ({ + ...track, + position: i + 1, + })); + + const tracksByAlbum = tracksData.body.items.reduce( + (acc: Record, track) => { + if (!acc[track.album.id]) { + const tracksWithAlbum = tracks.filter( + (t) => t.album.id === track.album.id, + ); + acc[track.album.id] = { + album: track.album, + position: + tracksWithAlbum.reduce( + (acc, _track) => FETCH_TRACKS_LIMIT / _track.position + acc, + 0, + ) / tracksWithAlbum.length, + tracks: [], + }; + } + (acc[track.album.id] || ({ tracks: [] } as any)).tracks.push(track); + return acc; + }, + {}, + ); + + return { + userData, + tracksByAlbum, + artists, + }; +}; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 64cf98f..ba66e95 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -2,6 +2,7 @@ import { postRouter } from "@/server/api/routers/post"; import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc"; import * as cloudinary from "cloudinary"; import { entryRouter } from "./routers/entry"; +import { userDataRouter } from "./routers/user-data"; cloudinary.v2.config({ secure: true, @@ -15,6 +16,7 @@ cloudinary.v2.config({ export const appRouter = createTRPCRouter({ post: postRouter, entry: entryRouter, + userData: userDataRouter, }); // export type definition of API diff --git a/src/server/api/routers/user-data.ts b/src/server/api/routers/user-data.ts new file mode 100644 index 0000000..5cb0143 --- /dev/null +++ b/src/server/api/routers/user-data.ts @@ -0,0 +1,160 @@ +import { z } from "zod"; +import { createTRPCRouter, publicProcedure } from "../trpc"; + +export const userDataRouter = createTRPCRouter({ + get: publicProcedure + .input(z.object({ spotifyUserId: z.string() })) + .query(async ({ ctx, input }) => { + const spotifyUser = await ctx.db.spotifyUser.findUnique({ + where: { + spotifyUserId: input.spotifyUserId, + }, + }); + if (!spotifyUser) { + return null; + } + const userData = await ctx.db.userData.findFirst({ + include: { + spotifyUser: { + select: { + spotifyUserId: true, + displayName: true, + images: true, + }, + }, + artists: { + select: { + name: true, + images: true, + }, + }, + // albums: { + // select: { + // name: true, + // images: true, + // tracks: { + // select: { + // name: true, + // }, + // }, + // }, + // }, + }, + where: { + spotifyUser: { + spotifyUserId: input.spotifyUserId, + }, + }, + }); + + if (!userData) { + return null; + } + + return { + ...userData, + spotifyUser: { + ...spotifyUser, + images: userData.spotifyUser.images, + id: spotifyUser.spotifyUserId, + }, + tracksByAlbum: JSON.parse(userData.tracksByAlbum), + }; + }), + create: publicProcedure + .input( + z.object({ + spotifyUserId: z.string(), + tracksByAlbum: z.record( + z.object({ + album: z.object({ + id: z.string(), + name: z.string(), + images: z.array( + z.object({ + url: z.string(), + }), + ), + }), + position: z.number(), + tracks: z.array( + z.object({ + id: z.string(), + name: z.string(), + }), + ), + }), + ), + artists: z.array( + z.object({ + name: z.string(), + images: z.array( + z.object({ + url: z.string(), + }), + ), + }), + ), + user: z.object({ + id: z.string(), + displayName: z.string(), + images: z.array( + z.object({ + url: z.string(), + }), + ), + }), + }), + ) + .mutation(async ({ ctx, input }) => { + const spotifyUser = await ctx.db.spotifyUser.findUnique({ + where: { + spotifyUserId: input.spotifyUserId, + }, + }); + if (spotifyUser) { + return null; + } + await ctx.db.userData.create({ + data: { + spotifyUser: { + create: { + displayName: input.user.displayName, + spotifyUserId: input.user.id, + images: { + create: input.user.images.map((image) => ({ + url: image.url, + })), + }, + }, + }, + artists: { + create: input.artists.map((artist) => ({ + name: artist.name, + images: { + create: artist.images.map((image) => ({ + url: image.url, + })), + }, + })), + }, + // albums: { + // create: input.albums.map((album) => ({ + // name: album.name, + // images: { + // create: album.images.map((image) => ({ + // url: image.url, + // })), + // }, + // tracks: { + // create: album.tracks.map((track) => ({ + // name: track.name, + // })), + // }, + // })), + // }, + tracksByAlbum: JSON.stringify(input.tracksByAlbum), + }, + }); + }), +});