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')],
 }