From 6d51d0dd211d1562639734a18b6e56f26f72a970 Mon Sep 17 00:00:00 2001 From: thunder30 <ngoctinh3004@gmail.com> Date: Mon, 10 Jan 2022 17:13:46 +0700 Subject: [PATCH] Build Center & Songs Components --- atoms/playlistAtom.js | 11 ++++++ components/Center.jsx | 80 ++++++++++++++++++++++++++++++++++++++++++ components/Sidebar.jsx | 47 ++++++++++++++++++------- components/Song.jsx | 30 ++++++++++++++++ components/Songs.jsx | 17 +++++++++ hooks/useSpotify.js | 27 ++++++++++++++ lib/time.js | 10 ++++++ package-lock.json | 62 +++++++++++++++++++++++++++++++- package.json | 5 ++- pages/_app.jsx | 6 +++- pages/_middleware.jsx | 2 +- pages/index.jsx | 20 +++++------ tailwind.config.js | 16 ++++----- 13 files changed, 299 insertions(+), 34 deletions(-) create mode 100644 atoms/playlistAtom.js create mode 100644 components/Center.jsx create mode 100644 components/Song.jsx create mode 100644 components/Songs.jsx create mode 100644 hooks/useSpotify.js create mode 100644 lib/time.js diff --git a/atoms/playlistAtom.js b/atoms/playlistAtom.js new file mode 100644 index 0000000..13386c8 --- /dev/null +++ b/atoms/playlistAtom.js @@ -0,0 +1,11 @@ +import { atom } from 'recoil' + +export const playlistState = atom({ + key: 'playlistState', + default: null, +}) + +export const playlistIdState = atom({ + key: 'playlistIdState', + default: '37i9dQZF1EUMDoJuT8yJsl', +}) diff --git a/components/Center.jsx b/components/Center.jsx new file mode 100644 index 0000000..79c301c --- /dev/null +++ b/components/Center.jsx @@ -0,0 +1,80 @@ +import { useSession } from 'next-auth/react' +import { ChevronDownIcon } from '@heroicons/react/outline' +import { useEffect, useState } from 'react' +import { shuffle } from 'lodash' +import { useRecoilState, useRecoilValue } from 'recoil' +import { playlistIdState, playlistState } from '../atoms/playlistAtom' +import useSpotify from '../hooks/useSpotify' +import Songs from './Songs' + +const colors = [ + 'from-indigo-500', + 'from-blue-500', + 'from-green-500', + 'from-red-500', + 'from-yellow-500', + 'from-pink-500', + 'from-purple-500', +] + +function Center() { + const spotifyApi = useSpotify() + const { data: session } = useSession() + const [color, setColor] = useState(null) + const playlistId = useRecoilValue(playlistIdState) + const [playlist, setPlaylist] = useRecoilState(playlistState) + + console.log('Playlist - ', playlist) + + useEffect(() => { + setColor(shuffle(colors).pop()) + }, [playlistId]) + + useEffect(() => { + spotifyApi + .getPlaylist(playlistId) + .then((data) => setPlaylist(data.body)) + .catch((error) => console.log(error)) + }, [spotifyApi, playlistId]) + + return ( + <div className="flex-grow h-screen overflow-y-scroll scrollbar-hide"> + <header className="absolute top-5 right-8"> + <div + className="flex items-center bg-black space-x-3 + opacity-90 hover:opacity-80 cursor-pointer rounded-full p-1 pr-2 text-white text-sm" + > + <img + className="rounded-full w-10 h-10" + src={session?.user.image} + alt="avatar" + /> + <h2>{session?.user.name}</h2> + <ChevronDownIcon className="h-5 w-5" /> + </div> + </header> + <section + className={`flex items-end space-x-7 + bg-gradient-to-b to-black ${color} h-80 text-white p-8`} + > + <img + className="w-44 h-44 shadow-2xl" + src={playlist?.images[0]?.url} + alt="playlist image" + /> + <div> + <p>PLAYLIST</p> + <h1 className="text-2xl md:text-3xl xl:text-5xl font-bold"> + {playlist?.name} + </h1> + </div> + </section> + + <div> + <Songs /> + </div> + </div> + ) +} + +export default Center diff --git a/components/Sidebar.jsx b/components/Sidebar.jsx index ebd602e..1d13134 100644 --- a/components/Sidebar.jsx +++ b/components/Sidebar.jsx @@ -7,12 +7,36 @@ import { HeartIcon, } from '@heroicons/react/outline' import { signOut, useSession } from 'next-auth/react' +import { useEffect, useState } from 'react' +import { useRecoilState } from 'recoil' +import useSpotify from '../hooks/useSpotify' +import { playlistIdState } from '../atoms/playlistAtom' function Sidebar() { + const spotifyApi = useSpotify() const { data: session, status } = useSession() - console.log(session) + const [playlist, setPlaylist] = useState([]) + const [playlistId, setPlaylistId] = useRecoilState(playlistIdState) + //console.log('session sidebar - ', session) + // console.log(` YOu picked playlistId: `, playlistId) + + useEffect(() => { + if (spotifyApi.getAccessToken()) { + spotifyApi + .getUserPlaylists() + .then((data) => { + setPlaylist(data.body.items) + }) + .catch((err) => console.error(err)) + } + }, [session, spotifyApi]) + return ( - <div className="text-gray-500 p-5 text-sm border-r border-gray-900"> + <div + className="text-gray-500 p-5 text-xs lg:text-sm border-r border-gray-900 + overflow-y-scroll scrollbar-hide h-screen sm:max-w-[12rem] lg:max-w-[15rem] + hidden md:inline-flex" + > <div className="space-y-4"> <button className="flex items-center space-x-4 hover:text-white" @@ -49,16 +73,15 @@ function Sidebar() { <hr className="border-t-[0.1px] border-gray-900" /> {/* Playlist */} - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> - <p className="cursor-point hover:text-white">Playlist name</p> + {playlist.map((playlist) => ( + <p + key={playlist.id} + className="cursor-pointer hover:text-white" + onClick={() => setPlaylistId(playlist.id)} + > + {playlist.name} + </p> + ))} </div> </div> ) diff --git a/components/Song.jsx b/components/Song.jsx new file mode 100644 index 0000000..856b973 --- /dev/null +++ b/components/Song.jsx @@ -0,0 +1,30 @@ +import { millisToMinutesAndSeconds } from '../lib/time' + +function Song({ order, track }) { + // console.log(track) + return ( + <div className="grid grid-cols-2"> + <div className="flex items-center space-x-4"> + <p>{order + 1}</p> + <img + className="w-10 h-10" + src={track.track.album.images[0].url} + alt="track image" + /> + <div> + <p>{track.track.name}</p> + <p>{track.track.artists[0].name}</p> + </div> + </div> + <div + className="flex items-center justify-between + ml-auto md:ml-0" + > + <p className="hidden md:inline">{track.track.album.name}</p> + <p>{millisToMinutesAndSeconds(track.track.duration_ms)}</p> + </div> + </div> + ) +} + +export default Song diff --git a/components/Songs.jsx b/components/Songs.jsx new file mode 100644 index 0000000..6084f30 --- /dev/null +++ b/components/Songs.jsx @@ -0,0 +1,17 @@ +import { useRecoilValue } from 'recoil' +import { playlistState } from '../atoms/playlistAtom' +import Song from './Song' + +function Songs() { + const playlist = useRecoilValue(playlistState) + + return ( + <div className="flex flex-col px-8 space-y-1 pb-20 text-white"> + {playlist?.tracks.items.map((track, index) => ( + <Song key={track.track.id} order={index} track={track} /> + ))} + </div> + ) +} + +export default Songs diff --git a/hooks/useSpotify.js b/hooks/useSpotify.js new file mode 100644 index 0000000..5654645 --- /dev/null +++ b/hooks/useSpotify.js @@ -0,0 +1,27 @@ +import { signIn, useSession } from 'next-auth/react' +import { useEffect } from 'react' +import SpotifyWebApi from 'spotify-web-api-node' +// import spotifyApi from '../lib/spotify' + +const spotifyApi = new SpotifyWebApi({ + clientId: process.env.NEXT_PUBLIC_CLIENT_ID, + clientSecret: process.env.NEXT_PUBLIC_CLIENT_SECRET, +}) + +function useSpotify() { + const { data: session, status } = useSession() + + useEffect(() => { + if (session) { + // If refresh token attempt fails, direct user to login + if (session.error === 'RefreshAccessTokenError') { + signIn() + } + + spotifyApi.setAccessToken(session.user.accessToken) + } + }, [session]) + return spotifyApi +} + +export default useSpotify diff --git a/lib/time.js b/lib/time.js new file mode 100644 index 0000000..31c33c9 --- /dev/null +++ b/lib/time.js @@ -0,0 +1,10 @@ +import { constSelector } from 'recoil' + +export const millisToMinutesAndSeconds = (millis) => { + const minutes = Math.floor(millis / 60000) + const seconds = ((millis % 60000) / 1000).toFixed(0) + + return seconds == 60 + ? minutes + 1 + ':00' + : minutes + ':' + (seconds < 10 ? '0' : '') + seconds +} diff --git a/package-lock.json b/package-lock.json index b16bb91..73363b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,11 +6,14 @@ "": { "dependencies": { "@heroicons/react": "^1.0.5", + "lodash": "^4.17.21", "next": "latest", "next-auth": "^4.0.6", "react": "^17.0.2", "react-dom": "^17.0.2", - "spotify-web-api-node": "^5.0.2" + "recoil": "^0.5.2", + "spotify-web-api-node": "^5.0.2", + "tailwind-scrollbar-hide": "^1.1.7" }, "devDependencies": { "@types/node": "17.0.4", @@ -2071,6 +2074,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=" + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2648,6 +2656,11 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -3637,6 +3650,25 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.5.2.tgz", + "integrity": "sha512-Edibzpu3dbUMLy6QRg73WL8dvMl9Xqhp+kU+f2sJtXxsaXvAlxU/GcnDE8HXPkprXrhHF2e6SZozptNvjNF5fw==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/regenerator-runtime": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", @@ -4040,6 +4072,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-scrollbar-hide": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz", + "integrity": "sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA==" + }, "node_modules/tailwindcss": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.12.tgz", @@ -5990,6 +6027,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, + "hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE=" + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -6374,6 +6416,11 @@ "p-locate": "^4.1.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -7066,6 +7113,14 @@ "picomatch": "^2.2.1" } }, + "recoil": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.5.2.tgz", + "integrity": "sha512-Edibzpu3dbUMLy6QRg73WL8dvMl9Xqhp+kU+f2sJtXxsaXvAlxU/GcnDE8HXPkprXrhHF2e6SZozptNvjNF5fw==", + "requires": { + "hamt_plus": "1.0.2" + } + }, "regenerator-runtime": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", @@ -7359,6 +7414,11 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "tailwind-scrollbar-hide": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-1.1.7.tgz", + "integrity": "sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA==" + }, "tailwindcss": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.12.tgz", diff --git a/package.json b/package.json index ff027a3..0bbc6e0 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,14 @@ }, "dependencies": { "@heroicons/react": "^1.0.5", + "lodash": "^4.17.21", "next": "latest", "next-auth": "^4.0.6", "react": "^17.0.2", "react-dom": "^17.0.2", - "spotify-web-api-node": "^5.0.2" + "recoil": "^0.5.2", + "spotify-web-api-node": "^5.0.2", + "tailwind-scrollbar-hide": "^1.1.7" }, "devDependencies": { "@types/node": "17.0.4", diff --git a/pages/_app.jsx b/pages/_app.jsx index 7a61480..0861324 100644 --- a/pages/_app.jsx +++ b/pages/_app.jsx @@ -1,10 +1,14 @@ import '../styles/globals.css' import { SessionProvider } from 'next-auth/react' +import { RecoilBridge, RecoilRoot } from 'recoil' function MyApp({ Component, pageProps: { session, ...pageProps } }) { + // console.log(`session _app - `, session) return ( <SessionProvider session={session}> - <Component {...pageProps} /> + <RecoilRoot> + <Component {...pageProps} /> + </RecoilRoot> </SessionProvider> ) } diff --git a/pages/_middleware.jsx b/pages/_middleware.jsx index a6022d9..9f35a9a 100644 --- a/pages/_middleware.jsx +++ b/pages/_middleware.jsx @@ -8,7 +8,7 @@ export async function middleware(req) { const token = await getToken({ req, secret }) const { pathname } = req.nextUrl - console.log(pathname) + //console.log(pathname) if (token && pathname === '/login') { return NextResponse.redirect('/') diff --git a/pages/index.jsx b/pages/index.jsx index 294cfef..86e84c5 100644 --- a/pages/index.jsx +++ b/pages/index.jsx @@ -1,14 +1,14 @@ import { getSession } from 'next-auth/react' import Head from 'next/head' import Sidebar from '../components/Sidebar' +import Center from '../components/Center' export default function Home() { return ( <div className="bg-black h-screen overflow-hidden"> - <main> - {/* Side bar */} + <main className="flex"> <Sidebar /> - {/* Center */} + <Center /> </main> <div>{/* Player */}</div> @@ -16,10 +16,10 @@ export default function Home() { ) } -// export async function getServerSideProps(context) { -// return { -// props: { -// session: await getSession(context), -// }, -// } -// } +export async function getServerSideProps(context) { + return { + props: { + session: await getSession(context), + }, + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 4cd6138..880e5e7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,10 +1,10 @@ module.exports = { - content: [ - './pages/**/*.{js,ts,jsx,tsx}', - './components/**/*.{js,ts,jsx,tsx}', - ], - theme: { - extend: {}, - }, - plugins: [], + content: [ + './pages/**/*.{js,ts,jsx,tsx}', + './components/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: {}, + }, + plugins: [require('tailwind-scrollbar-hide')], }