Placeholder data option

This commit is contained in:
2024-10-18 00:29:48 -03:00
parent 4ff27e46b7
commit d50e553278
8 changed files with 377 additions and 47 deletions

View File

@@ -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])
}

View File

@@ -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 (
<div className="flex flex-col items-center gap-y-8 pt-4">
<div className="text-center">
<div className="flex flex-col items-center pt-4">
<div className="mb-8 text-center">
<TypographyH1>Spooky Spotify Showcase</TypographyH1>
<TypographyH4 className="font-normal opacity-90">
<a
@@ -20,6 +21,10 @@ export function LoginPage() {
</TypographyH4>
</div>
<SpotifyLogin className="grow-0" />
<PlaceholderDataLink
className="mt-2"
text="Don't have an Spotify account? Use mine!"
/>
</div>
);
}

View File

@@ -0,0 +1,20 @@
export const PlaceholderDataLink = ({
className,
text,
}: {
className?: string;
text: string;
}) => {
return (
<div className={`flex flex-col ${className}`}>
<a
href="/?placeholder-data=true"
className="text-center text-lg text-white text-opacity-40 backdrop-blur-lg backdrop-filter transition-all duration-300 ease-in-out hover:text-opacity-60 hover:underline"
>
{text}
</a>
</div>
);
};
export default PlaceholderDataLink;

View File

@@ -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 <div>Error fetching data</div>;
}
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<string, TrackByAlbum>, 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;

View File

@@ -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({
<HydrateClient>
<main className="justify-centerbg-gradient-to-r flex min-h-screen flex-col items-center bg-gradient-to-r from-slate-900 to-slate-700 text-slate-200">
<div className="container flex flex-col items-center justify-center gap-8 px-4 pb-16 pt-8">
{userIsLogged ? (
{userIsLogged || placeholderDataSelected ? (
<SpotifyData
accessToken={access_token as string}
refreshToken={refresh_token as string}
placeholderData={placeholderDataSelected}
/>
) : (
<LoginPage />

View File

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

View File

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

View File

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