mirror of
https://github.com/FranP-code/spooky-spotify-showcase.git
synced 2025-10-13 00:02:36 +00:00
Placeholder data option
This commit is contained in:
@@ -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])
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
20
src/app/_components/placeholder-data-link.tsx
Normal file
20
src/app/_components/placeholder-data-link.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
62
src/app/utils/getSpotifyData.ts
Normal file
62
src/app/utils/getSpotifyData.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
|
||||
160
src/server/api/routers/user-data.ts
Normal file
160
src/server/api/routers/user-data.ts
Normal 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),
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user