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
|
createdAt DateTime @default(now()) // Timestamp for when the entry was created
|
||||||
updatedAt DateTime @updatedAt // Auto-updating timestamp for when the entry was last updated
|
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 { TypographyH1 } from "./h1";
|
||||||
import { TypographyH4 } from "./h4";
|
import { TypographyH4 } from "./h4";
|
||||||
import SpotifyLogin from "./spotify-login";
|
import SpotifyLogin from "./spotify-login";
|
||||||
|
import { PlaceholderDataLink } from "./placeholder-data-link";
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-y-8 pt-4">
|
<div className="flex flex-col items-center pt-4">
|
||||||
<div className="text-center">
|
<div className="mb-8 text-center">
|
||||||
<TypographyH1>Spooky Spotify Showcase</TypographyH1>
|
<TypographyH1>Spooky Spotify Showcase</TypographyH1>
|
||||||
<TypographyH4 className="font-normal opacity-90">
|
<TypographyH4 className="font-normal opacity-90">
|
||||||
<a
|
<a
|
||||||
@@ -20,6 +21,10 @@ export function LoginPage() {
|
|||||||
</TypographyH4>
|
</TypographyH4>
|
||||||
</div>
|
</div>
|
||||||
<SpotifyLogin className="grow-0" />
|
<SpotifyLogin className="grow-0" />
|
||||||
|
<PlaceholderDataLink
|
||||||
|
className="mt-2"
|
||||||
|
text="Don't have an Spotify account? Use mine!"
|
||||||
|
/>
|
||||||
</div>
|
</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 SpotifyWebApi from "spotify-web-api-node";
|
||||||
import Showcase from "./showcase";
|
import Showcase from "./showcase";
|
||||||
import { FETCH_ARTISTS_LIMIT, FETCH_TRACKS_LIMIT } from "../utils/contants";
|
import { FETCH_ARTISTS_LIMIT, FETCH_TRACKS_LIMIT } from "../utils/contants";
|
||||||
|
import { getSpotifyData } from "../utils/getSpotifyData";
|
||||||
|
import { api } from "@/trpc/server";
|
||||||
|
|
||||||
export type TrackByAlbum = {
|
export type TrackByAlbum = {
|
||||||
album: {
|
album: {
|
||||||
@@ -18,56 +20,59 @@ export type TrackByAlbum = {
|
|||||||
export default async function SpotifyData({
|
export default async function SpotifyData({
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
placeholderData,
|
||||||
}: {
|
}: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
|
placeholderData: boolean;
|
||||||
}) {
|
}) {
|
||||||
const spotifyApi = new SpotifyWebApi({
|
const fetchData = async () => {
|
||||||
clientId: process.env.SPOTIFY_CLIENT_ID,
|
if (placeholderData) {
|
||||||
clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
|
//I'm so sorry for what I did here
|
||||||
redirectUri: process.env.SPOTIFY_REDIRECT_URI,
|
return api.userData
|
||||||
});
|
.get({
|
||||||
spotifyApi.setAccessToken(accessToken);
|
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([
|
if (!artists || !tracksByAlbum || !userData?.body) {
|
||||||
spotifyApi.getMyTopArtists({
|
return <div>Error fetching data</div>;
|
||||||
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;
|
if (userData.body.id === process.env.SPOTIFY_OWNER_USER_ID) {
|
||||||
const tracks = tracksData.body.items.map((track, i) => ({
|
await api.userData.create({
|
||||||
...track,
|
spotifyUserId: userData.body.id,
|
||||||
position: i + 1,
|
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 = {
|
// type Track = {
|
||||||
// id: string;
|
// id: string;
|
||||||
// name: string;
|
// name: string;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { api, HydrateClient } from "@/trpc/server";
|
import { api, HydrateClient } from "@/trpc/server";
|
||||||
import SpotifyLogin from "./_components/spotify-login";
|
|
||||||
import SpotifyData from "./_components/spotify-data";
|
import SpotifyData from "./_components/spotify-data";
|
||||||
import SpotifyWebApi from "spotify-web-api-node";
|
import SpotifyWebApi from "spotify-web-api-node";
|
||||||
import LoginPage from "./_components/login-page";
|
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 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 access_token = searchParams?.access_token;
|
||||||
const refresh_token = searchParams?.refresh_token;
|
const refresh_token = searchParams?.refresh_token;
|
||||||
let userIsLogged = !!(
|
let userIsLogged = !!(
|
||||||
@@ -41,10 +42,11 @@ export default async function Home({
|
|||||||
<HydrateClient>
|
<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">
|
<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">
|
<div className="container flex flex-col items-center justify-center gap-8 px-4 pb-16 pt-8">
|
||||||
{userIsLogged ? (
|
{userIsLogged || placeholderDataSelected ? (
|
||||||
<SpotifyData
|
<SpotifyData
|
||||||
accessToken={access_token as string}
|
accessToken={access_token as string}
|
||||||
refreshToken={refresh_token as string}
|
refreshToken={refresh_token as string}
|
||||||
|
placeholderData={placeholderDataSelected}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LoginPage />
|
<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 { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||||
import * as cloudinary from "cloudinary";
|
import * as cloudinary from "cloudinary";
|
||||||
import { entryRouter } from "./routers/entry";
|
import { entryRouter } from "./routers/entry";
|
||||||
|
import { userDataRouter } from "./routers/user-data";
|
||||||
|
|
||||||
cloudinary.v2.config({
|
cloudinary.v2.config({
|
||||||
secure: true,
|
secure: true,
|
||||||
@@ -15,6 +16,7 @@ cloudinary.v2.config({
|
|||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
post: postRouter,
|
post: postRouter,
|
||||||
entry: entryRouter,
|
entry: entryRouter,
|
||||||
|
userData: userDataRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// 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