├── .eslintrc.cjs ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── README.md ├── index.html ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public ├── drc.svg └── vite.svg ├── src ├── App.jsx ├── assets │ ├── css │ │ ├── App.css │ │ ├── components │ │ │ ├── Loader.css │ │ │ ├── PageLoading.css │ │ │ ├── Skeleton.css │ │ │ ├── SkeletonData.css │ │ │ ├── card.css │ │ │ └── min-skeleton.css │ │ ├── custom.css │ │ └── index.css │ ├── img │ │ ├── artist.jpeg │ │ ├── bg-default.svg │ │ ├── drc.svg │ │ ├── images-1.png │ │ ├── login.jpg │ │ └── spotify.jpg │ └── react.svg ├── components │ ├── AlbumsData.jsx │ ├── ArtistsData.jsx │ ├── CardArtist.jsx │ ├── CardDetails.jsx │ ├── CardPlaylist.jsx │ ├── CardSkeleton.jsx │ ├── CardSong.jsx │ ├── CardTrack.jsx │ ├── CardTracksContainer.jsx │ ├── FormLogin.jsx │ ├── GoogleAuthButton.jsx │ ├── GreetUser.jsx │ ├── Hero.jsx │ ├── Loader.jsx │ ├── LoadingPage.jsx │ ├── ModalPlayer.jsx │ ├── MusicApp.jsx │ ├── Musics.jsx │ ├── NavbarHome.jsx │ ├── PlaylistsData.jsx │ ├── Search.jsx │ ├── Sidebar.jsx │ ├── SidebarItem.jsx │ ├── SkeletonData.jsx │ ├── SongsData.jsx │ ├── TrackData.jsx │ ├── TrackDataTable.jsx │ ├── TrackView.jsx │ └── UserData.jsx ├── data │ ├── AppContext.jsx │ ├── getData.js │ ├── hookFunc.js │ ├── secureData.js │ └── utilsFunc.js ├── main.jsx ├── pages │ ├── Albums.jsx │ ├── Artists.jsx │ ├── Dashboard.jsx │ ├── Home.jsx │ ├── Login.jsx │ ├── PlayList.jsx │ └── Tracks.jsx └── routes │ └── routes.jsx ├── tailwind.config.cjs └── vite.config.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['plugin:react/recommended', 'airbnb', 'prettier'], 7 | overrides: [], 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module', 11 | }, 12 | plugins: ['react'], 13 | rules: { 14 | 'react/prop-types': 'off', 15 | 'jsx-a11y/no-static-element-interactions': [ 16 | 'error', 17 | { 18 | handlers: [ 19 | 'onClick', 20 | 'onMouseDown', 21 | 'onMouseUp', 22 | 'onKeyPress', 23 | 'onKeyDown', 24 | 'onKeyUp', 25 | ], 26 | allowExpressionValues: true, 27 | }, 28 | ], 29 | 'no-console': 2, 30 | 31 | ignorePropertyModificationsFor: true, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "prettier" 10 | ], 11 | "overrides": [], 12 | "parserOptions": { 13 | "ecmaVersion": "latest", 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "react", 18 | "prettier" 19 | ], 20 | "rules": { 21 | "indent": [ 22 | "error", 23 | "tab" 24 | ], 25 | "linebreak-style": [ 26 | "error", 27 | "unix" 28 | ], 29 | "quotes": [ 30 | "error", 31 | "double" 32 | ], 33 | "semi": [ 34 | "error", 35 | "always" 36 | ], 37 | "prettier/prettier": "error", 38 | "rules": { 39 | "react/prop-types": "off", 40 | "jsx-a11y/no-static-element-interactions": [ 41 | "error", 42 | { 43 | "handlers": [ 44 | "onClick", 45 | "onMouseDown", 46 | "onMouseUp", 47 | "onKeyPress", 48 | "onKeyDown", 49 | "onKeyUp" 50 | ], 51 | "allowExpressionValues": true 52 | } 53 | ] 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.env 26 | .env 27 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify App 2 | 3 | A music application using the Spotify API to achieve serving as project of the month in the GDA academy 4 | 5 | ## Context du projet 6 | 7 | L'objectif du projet est de créer une Plateforme web de streaming musical. Grâce à la plateforme, on va pouvoir rechercher des chansons, des albums, des artistes, des playlists et les écoutes en ligne. 8 | 9 | La plateforme doit : 10 | 11 | - Être basée sur l'[API de spotify](https://developer.spotify.com/documentation/web-api/quick-start/). 12 | - Donner aux utilisateurs la possibilité de se connecter avec leurs comptes Google 13 | - Proposer un champ de recherche qui permet de taper le titre d’une chanson et de faire une recherche. 14 | - Donner aux utilisateurs la possibilité d'écouter la musique en ligne 15 | 16 | ## Critères d'évaluation 17 | 18 | Le projet sera évalué sur base de : 19 | 20 | - L’UI/UX. 21 | - La qualité du code 22 | 23 | Points obligatoires : 24 | 25 | - Le site sera réalisé en React.js, mais devra respecter les conventions de HTML5, CSS3 et JS ES6+ 26 | - Le site doit être responsive, respecter les règles d’accessibilité, 27 | - Vous pouvez utiliser un framework CSS de votre choix. 28 | - Vous devez utiliser ESLint pour le style du code. (notamment ES6) 29 | 30 | Il s’agit d’un produit minimum viable. Vous pouvez ensuite ajouter toutes les fonctionnalités qui vous semblent pertinentes. 31 | 32 | Aucune maquette graphique n'a été définie pour le site, vous avez carte blanche. 33 | 34 | Quelques exemples : 35 | 36 | - Créer des playlists 37 | - Donner des informations sur les artistes 38 | - Proposer aux utilisateurs des playlists sur base de leurs écoutes 39 | - ... 40 | 41 | ## Modalités pédagogiques 42 | 43 | Le projet individuel et doit être réalisé en 10 jours 44 | 45 | ## Livrables 46 | 47 | - Un dépôt Github contenant votre code source ; 48 | - Le schéma de votre maquette graphique. 49 | - Le lien du site déployé 50 | 51 | ## Ressources 52 | 53 | - [API de spotify](https://developer.spotify.com/documentation/web-api/quick-start/) 54 | - [SDK de spotify](https://developer.spotify.com/documentation/web-playback-sdk/) 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Spotify project, React month project 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "/dist" 4 | base ="/" 5 | [[redirects]] 6 | from = "/*" 7 | to = "/index.html" 8 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "dotenv": "^16.0.2", 13 | "jwt-decode": "^3.1.2", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.3.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.17", 20 | "@types/react-dom": "^18.0.6", 21 | "@vitejs/plugin-react": "^2.0.1", 22 | "autoprefixer": "^10.4.8", 23 | "eslint": "^8.23.0", 24 | "eslint-config-airbnb": "^19.0.4", 25 | "eslint-config-prettier": "^8.5.0", 26 | "eslint-plugin-prettier": "^4.2.1", 27 | "eslint-plugin-react": "^7.31.8", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "postcss": "^8.4.16", 30 | "tailwindcss": "^3.1.8", 31 | "vite": "^3.0.7" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/drc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 | import './assets/css/App.css'; 4 | import './assets/css/custom.css'; 5 | import routes from './routes/routes'; 6 | import MusicContext from './data/AppContext'; 7 | import { CLIENT_ID, CLIENT_SECRET } from './data/secureData'; 8 | import { verifyUserConnect } from './data/utilsFunc'; 9 | 10 | function App() { 11 | const { setSetting, setUserIsConnect } = MusicContext(); 12 | useEffect(() => { 13 | setUserIsConnect(verifyUserConnect()); 14 | (async () => { 15 | const response = await fetch('https://accounts.spotify.com/api/token', { 16 | method: 'POST', 17 | headers: { 18 | // Considerer comme un formulaire HTML 19 | 'Content-Type': 'application/x-www-form-urlencoded', 20 | }, 21 | body: `grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`, 22 | }); 23 | const responseData = await response.json(); 24 | const { access_token, token_type } = responseData; 25 | setSetting((setting) => ({ 26 | ...setting, 27 | token: access_token, 28 | authorize_token: `${token_type} ${access_token}`, 29 | })); 30 | })(); 31 | }, [setSetting]); 32 | 33 | return ( 34 | 35 | 36 | {routes.map(({ path, element }, index) => ( 37 | 38 | ))} 39 | 40 | 41 | ); 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/assets/css/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdekoCode/spotify-app/06222a25c97fbc5f90b55875e19ce5006b48e224/src/assets/css/App.css -------------------------------------------------------------------------------- /src/assets/css/components/Loader.css: -------------------------------------------------------------------------------- 1 | .loader-container { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | bottom: 0; 7 | } 8 | 9 | .wrapper { 10 | position: absolute; 11 | top: 50%; 12 | left: 55%; 13 | transform: translate(-50%, -50%); 14 | width: 142px; 15 | height: 40px; 16 | margin: -20px 0 0 -71px; 17 | filter: contrast(20); 18 | } 19 | 20 | .dot { 21 | position: absolute; 22 | width: 16px; 23 | height: 16px; 24 | top: 12px; 25 | left: 5px; 26 | backdrop-filter: blur(4px); 27 | background: #000; 28 | border-radius: 50%; 29 | transform: translateX(0); 30 | animation: dot 2.8s infinite; 31 | } 32 | 33 | .dots { 34 | transform: translateX(0); 35 | margin-top: 12px; 36 | margin-left: 31px; 37 | animation: dots 2.8s infinite; 38 | } 39 | 40 | .wrapper span { 41 | display: block; 42 | float: left; 43 | width: 16px; 44 | height: 16px; 45 | margin-left: 16px; 46 | background: rgba(0, 0, 0, 0.5); 47 | border-radius: 50%; 48 | backdrop-filter: blur(3px); 49 | } 50 | 51 | @keyframes dot { 52 | 50% { 53 | transform: translateX(96px); 54 | } 55 | } 56 | 57 | @keyframes dots { 58 | 50% { 59 | transform: translateX(-31px); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/css/components/PageLoading.css: -------------------------------------------------------------------------------- 1 | .page-loader { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | background: #eee; 11 | /* background: linear-gradient(110deg, #beb9b9 15%, #f5f5f5 30%, #a09d9d 45%); */ 12 | background: linear-gradient(110deg, #d3d0d0 10%, #f5f5f5 20%, #d3d0d0 33%); 13 | background-size: 200% 100%; 14 | animation: Fadeskeleton 2.5s ease-in-out infinite alternate; 15 | } 16 | @keyframes Fadeskeleton { 17 | /* L'element va passer de son opacité de 1 jusqu'à 0.6 */ 18 | to { 19 | opacity: 0.6; 20 | background-position-x: -200%; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/css/components/Skeleton.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --time-sm: 0.55s; 3 | --fadeUp: fadeUp var(--time-sm) forwards; 4 | } 5 | .cards-container { 6 | width: 100%; 7 | margin: 0 auto; 8 | 9 | min-height: 100vh; 10 | height: auto; 11 | padding: 50px; 12 | font-family: Arial, "Segoe UI", Helvetica, sans-serif; 13 | display: flex; 14 | justify-content: space-around; 15 | flex-wrap: wrap; 16 | gap: 1.5rem; 17 | align-items: center; 18 | background: linear-gradient(to right, #434343, #212121); 19 | } 20 | .cards-grid { 21 | display: grid; 22 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 23 | width: 100%; 24 | gap: 1rem; 25 | padding: 0 20px; 26 | margin: 4rem auto 0; 27 | } 28 | .cards-grid .card { 29 | background-color: #f1f1f1; 30 | } 31 | .card-title { 32 | margin: 10px 0; 33 | } 34 | .card-text { 35 | color: #4c4c4c; 36 | } 37 | .cards-grid .card-img { 38 | display: block; 39 | height: 200px; 40 | width: 100%; 41 | background-color: #4c4c4c; 42 | object-fit: cover; 43 | } 44 | .card.is-loading { 45 | border: 1px solid #4c4c4c; 46 | } 47 | .card.is-loading :is(.card-img, *[class*="skeleton-"]) { 48 | background-color: #4c4c4c; 49 | } 50 | .card-content { 51 | padding: 15px 25px; 52 | } 53 | *.is-loading .skeleton-title { 54 | min-height: 30px; 55 | } 56 | 57 | /* Tous les element qui ont dans leurs classe "skeleton-QuelQueChose" */ 58 | *.is-loading *[class*="skeleton-"] { 59 | margin-bottom: 10px; 60 | background: linear-gradient(110deg, #414141 8%, #838383 18%, #414141 33%); 61 | background-size: 200% 100%; 62 | animation: Fadeskeleton 1.75s ease-in-out infinite alternate; 63 | } 64 | *.is-loading .skeleton-text { 65 | max-width: 65%; 66 | min-height: 15px; 67 | } 68 | *.is-loading .skeleton-text:first-of-type, 69 | .skeleton-text:last-of-type { 70 | max-width: 90%; 71 | } 72 | *.is-loading .skeleton-text:last-of-type { 73 | min-height: 20px; 74 | } 75 | *.is-loading img.skeleton-anim { 76 | animation: Fadeskeleton 05s ease-in-out infinite alternate; 77 | } 78 | .fade-result { 79 | background-color: #f1f1f1; 80 | position: fixed; 81 | top: 0; 82 | left: 0; 83 | right: 0; 84 | bottom: 0; 85 | display: flex; 86 | flex-direction: column; 87 | align-items: center; 88 | justify-content: center; 89 | } 90 | .fade-result button { 91 | background-color: #434343; 92 | padding: 8px 12px; 93 | border-radius: 5px; 94 | color: #f1f1f1; 95 | } 96 | .refresh { 97 | width: 100px; 98 | height: 100px; 99 | } 100 | .card.loaded { 101 | animation: fadeUp 0.35s forwards; 102 | } 103 | /* 104 | background: #eee; 105 | background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%); 106 | background-size: 200% 100%; 107 | */ 108 | @keyframes Fadeskeleton { 109 | /* L'element va passer de son opacité de 1 jusqu'à 0.6 */ 110 | to { 111 | opacity: 0.6; 112 | background-position-x: -200%; 113 | } 114 | } 115 | 116 | @keyframes fadeUp { 117 | from { 118 | opacity: 0; 119 | transform: translateY(15px); 120 | } 121 | to { 122 | opacity: 1; 123 | transform: translateY(0); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/assets/css/components/SkeletonData.css: -------------------------------------------------------------------------------- 1 | .card-title { 2 | margin: 10px 0; 3 | } 4 | .card-text { 5 | color: #4c4c4c; 6 | } 7 | .cards-grid .card-img { 8 | display: block; 9 | height: 200px; 10 | width: 100%; 11 | background-color: #4c4c4c; 12 | object-fit: cover; 13 | } 14 | 15 | .card.is-loading :is(.card-img, *[class*="skeleton-"]) { 16 | background-color: #4c4c4c; 17 | } 18 | .card-content { 19 | padding: 15px 25px; 20 | } 21 | *.is-loading .skeleton-title { 22 | min-height: 30px; 23 | } 24 | 25 | /* Tous les element qui ont dans leurs classe "skeleton-QuelQueChose" */ 26 | *.is-loading *[class*="skeleton-"] { 27 | margin-bottom: 10px; 28 | background: linear-gradient(110deg, #414141 8%, #838383 18%, #414141 33%); 29 | background-size: 200% 100%; 30 | animation: Fadeskeleton 1.75s ease-in-out infinite alternate; 31 | } 32 | *.is-loading .skeleton-text { 33 | max-width: 65%; 34 | min-height: 15px; 35 | } 36 | *.is-loading .skeleton-text:first-of-type, 37 | .skeleton-text:last-of-type { 38 | max-width: 90%; 39 | } 40 | *.is-loading .skeleton-text:last-of-type { 41 | min-height: 20px; 42 | } 43 | *.is-loading img.skeleton-anim { 44 | animation: Fadeskeleton 05s ease-in-out infinite alternate; 45 | } 46 | .fade-result { 47 | background-color: #f1f1f1; 48 | position: fixed; 49 | top: 0; 50 | left: 0; 51 | right: 0; 52 | bottom: 0; 53 | display: flex; 54 | flex-direction: column; 55 | align-items: center; 56 | justify-content: center; 57 | } 58 | .fade-result button { 59 | background-color: #434343; 60 | padding: 8px 12px; 61 | border-radius: 5px; 62 | color: #f1f1f1; 63 | } 64 | .refresh { 65 | width: 100px; 66 | height: 100px; 67 | } 68 | .card.loaded { 69 | animation: fadeUp 0.35s forwards; 70 | } 71 | /* 72 | background: #eee; 73 | background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%); 74 | background-size: 200% 100%; 75 | */ 76 | @keyframes Fadeskeleton { 77 | /* L'element va passer de son opacité de 1 jusqu'à 0.6 */ 78 | to { 79 | opacity: 0.6; 80 | background-position-x: -200%; 81 | } 82 | } 83 | 84 | @keyframes fadeUp { 85 | from { 86 | opacity: 0; 87 | transform: translateY(15px); 88 | } 89 | to { 90 | opacity: 1; 91 | transform: translateY(0); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/assets/css/components/card.css: -------------------------------------------------------------------------------- 1 | .card-track { 2 | padding: 16px; 3 | isolation: isolate; 4 | position: relative; 5 | min-height: 300px; 6 | min-width: 220px; 7 | } 8 | .card-track:hover { 9 | color: #fff; 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/css/components/min-skeleton.css: -------------------------------------------------------------------------------- 1 | .box-ajax { 2 | width: 100%; 3 | min-height: 100vh; 4 | height: auto; 5 | background-color: #ddd; 6 | padding: 50px; 7 | font-family: Arial, "Segoe UI", Helvetica, sans-serif; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | .title { 13 | margin-bottom: 0.9rem; 14 | } 15 | .box-ajax .card { 16 | margin: 10px; 17 | min-width: 300px; 18 | max-width: 300px; 19 | width: auto; 20 | background-color: #fff; 21 | border-radius: 5px; 22 | overflow: hidden; 23 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 24 | } 25 | .card img { 26 | width: 100%; 27 | height: auto; 28 | } 29 | .card-content { 30 | padding: 20px 25px; 31 | } 32 | .card p { 33 | min-height: 90px; 34 | } 35 | .card-content { 36 | min-height: 200px; 37 | } 38 | .card * { 39 | transition: opacity 0.35s; 40 | } 41 | .card.is-loading :is(.card-img, h2, .title, p) { 42 | background: #eee; 43 | background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%); 44 | background-size: 200% 100%; 45 | animation: gradientLoading 1.15s infinite linear forwards; 46 | border-radius: 5px; 47 | } 48 | .card.is-loading .card-img { 49 | min-height: 200px; 50 | border-radius: 5px 5px 0 0; 51 | } 52 | 53 | .card.is-loading .title { 54 | min-height: 25px; 55 | } 56 | .card.is-loading { 57 | min-height: 380px; 58 | } 59 | .card.is-loading p, 60 | .card.is-loading .text { 61 | min-height: 90px; 62 | } 63 | @keyframes gradientLoading { 64 | to { 65 | /* On veux déplacer l'acces des absicess à moins 200% */ 66 | background-position-x: -200%; 67 | opacity: 0.6; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | .dropdown:focus-within .dropdown-menu { 2 | opacity: 1; 3 | transform: translate(0) scale(1); 4 | visibility: visible; 5 | } 6 | 7 | .btn-m { 8 | background-color: #6617cb; 9 | background-image: linear-gradient(315deg, #6617cb 0%, #cb218e 74%); 10 | box-shadow: 0 0 0 0 #ec008c, 0.2rem 0.2rem 30px #6617cb; 11 | } 12 | .btn-m:hover { 13 | box-shadow: 0 0 0 0 #ec008c, 0.2rem 0.2rem 60px #6617cb; 14 | } 15 | .main-navigation { 16 | display: flex; 17 | } 18 | .grid-auto { 19 | display: grid; 20 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 21 | width: 100%; 22 | margin: auto; 23 | } 24 | .modal { 25 | position: fixed; 26 | display: flex; 27 | align-items: center; 28 | justify-content: flex-end; 29 | height: 352px; 30 | width: 245px; 31 | bottom: 0; 32 | left: 0; 33 | right: 0; 34 | z-index: 150; 35 | display: flex; 36 | align-items: flex-end; 37 | } 38 | .modal iframe { 39 | height: 100%; 40 | display: flex; 41 | } 42 | 43 | .modal .hide { 44 | position: absolute; 45 | top: -20px; 46 | right: -40px; 47 | z-index: 150; 48 | width: 40px; 49 | height: 40px; 50 | cursor: pointer; 51 | border-radius: 50%; 52 | } 53 | 54 | /* custom rounded classes */ 55 | .rounded-xxl { 56 | border-radius: 30px; 57 | } 58 | 59 | .rounded-t-xxl { 60 | border-top-left-radius: 30px; 61 | border-top-right-radius: 30px; 62 | } 63 | 64 | .rounded-b-xxl { 65 | border-bottom-left-radius: 30px; 66 | border-bottom-right-radius: 30px; 67 | } 68 | 69 | /* Range Input */ 70 | 71 | .x-slider * { 72 | width: 350px !important; 73 | } 74 | 75 | .x-slider input::-webkit-slider-thumb { 76 | width: 20px; 77 | height: 20px; 78 | border-radius: 50%; 79 | background-color: #636363; 80 | border: 5px solid #24c55d; 81 | box-shadow: 0px 0px 40px -4px rgba(0, 0, 0, 0.76); 82 | transition: all 0.1s ease-in-out; 83 | outline: none; 84 | appearance: none; 85 | } 86 | 87 | .x-slider input::-webkit-slider-thumb:hover { 88 | border-color: #1bb350; 89 | } 90 | 91 | /* Player Controls */ 92 | .x-player-controls .play-pause { 93 | transform: scale(3); 94 | } 95 | 96 | .x-player-controls .play-pause:hover { 97 | transform: scale(3.1); 98 | } 99 | 100 | .x-player-controls .center-controls { 101 | transform: scale(2.5); 102 | } 103 | 104 | .x-player-controls .center-controls:hover { 105 | transform: scale(2.7); 106 | } 107 | 108 | .x-player-controls .side-controls { 109 | transform: scale(1.8); 110 | } 111 | 112 | .x-player-controls .side-controls:hover { 113 | transform: scale(2); 114 | } 115 | .bg-spotify { 116 | --tw-bg-opacity: 1; 117 | background-color: rgba(29, 185, 84, var(--tw-bg-opacity)); 118 | transition: 0.35s; 119 | } 120 | .bg-spotify:hover { 121 | color: rgba(255, 255, 255, 0.95) !important; 122 | } 123 | .text-spotify:hover { 124 | --tw-text-opacity: 1; 125 | color: rgba(29, 185, 84, var(--tw-text-opacity)); 126 | transition: 0.35s; 127 | } 128 | .spotify-card { 129 | transition: 0.35s; 130 | } 131 | 132 | .spotify-card:hover { 133 | background-color: rgba(9, 9, 9, 0.9); 134 | } 135 | .bg-app { 136 | background-image: url('../img/bg-default.svg'); 137 | background-position: center center; 138 | background-size: cover; 139 | background-repeat: no-repeat; 140 | } 141 | .bg-app-linear { 142 | background: linear-gradient( 143 | 110deg, 144 | hsl(0, 0.5%, 99%, 0.933) 8%, 145 | hsl(0, 4%, 95%) 18% 15%, 146 | hsla(0, 0.1%, 93%, 0.933) 33% 147 | ); 148 | } 149 | 150 | .card { 151 | position: relative; 152 | } 153 | .card-img { 154 | opacity: 0; 155 | transition: 0.3s; 156 | } 157 | .card:hover .card-img { 158 | opacity: 1; 159 | } 160 | .card-bg { 161 | background-size: cover; 162 | background-position: center; 163 | position: absolute; 164 | top: 0; 165 | bottom: 0; 166 | left: 0; 167 | right: 0; 168 | border-radius: 24px; 169 | overflow: hidden; 170 | filter: brightness(0.75) saturate(1.2) contrast(0.85); 171 | transition: 0.35s; 172 | } 173 | .card-content { 174 | position: absolute; 175 | padding: 1.5rem; 176 | color: #fff; 177 | } 178 | .card-content p { 179 | color: rgba(255, 255, 255, 0.6); 180 | } 181 | .card-content .title { 182 | font-size: 1.9rem; 183 | color: rgba(255, 255, 255, 0.9); 184 | text-shadow: 2px 2px 20px rgba(0, 0, 0, 0.2); 185 | } 186 | .card-tag { 187 | text-transform: uppercase; 188 | font-size: 0.9rem; 189 | } 190 | .card:hover .card-bg { 191 | transform: scale(1.05); 192 | } 193 | .cards:hover > .card:not(:hover) .card-bg { 194 | filter: brightness(0.5) saturate(0) contrast(1.2); 195 | } 196 | .modal.is-track { 197 | bottom: 35px; 198 | border-bottom-left-radius: 0; 199 | border-bottom-right-radius: 0; 200 | } 201 | .is-track { 202 | min-height: 80px; 203 | max-height: 90px; 204 | width: 100%; 205 | } 206 | .is-track .hide { 207 | top: -36px; 208 | right: 0px; 209 | } 210 | 211 | @media only screen and (min-width: 576px) { 212 | .cards { 213 | grid-template-columns: repeat(2, 1fr); 214 | } 215 | 216 | .cards:hover > .card:not(:hover) .card-bg { 217 | filter: brightness(0.5) saturate(0) contrast(1.2) blur(5px); 218 | } 219 | } 220 | 221 | @media only screen and (min-width: 992px) { 222 | .cards { 223 | grid-template-columns: repeat(4, 1fr); 224 | } 225 | } 226 | 227 | @media only screen and (min-width: 768px) { 228 | .is-track { 229 | height: 352px; 230 | width: 280px; 231 | min-height: max-content; 232 | max-width: inherit; 233 | max-height: inherit; 234 | } 235 | 236 | .modal.is-track { 237 | bottom: 0; 238 | } 239 | .is-track .hide { 240 | top: -20px; 241 | right: -40px; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 12px; 3 | border-radius: 10px; 4 | } 5 | 6 | ::-webkit-scrollbar-track { 7 | background-color: hsl(0, 7%, 92%); 8 | border-radius: 10px; 9 | } 10 | 11 | ::-webkit-scrollbar-thumb { 12 | --tw-bg-opacity: 1; 13 | background-color: rgba(29, 185, 84, var(--tw-bg-opacity)); 14 | transition: 0.35s; 15 | } 16 | @tailwind base; 17 | @tailwind components; 18 | @tailwind utilities; 19 | -------------------------------------------------------------------------------- /src/assets/img/artist.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdekoCode/spotify-app/06222a25c97fbc5f90b55875e19ce5006b48e224/src/assets/img/artist.jpeg -------------------------------------------------------------------------------- /src/assets/img/bg-default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/drc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/img/images-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdekoCode/spotify-app/06222a25c97fbc5f90b55875e19ce5006b48e224/src/assets/img/images-1.png -------------------------------------------------------------------------------- /src/assets/img/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdekoCode/spotify-app/06222a25c97fbc5f90b55875e19ce5006b48e224/src/assets/img/login.jpg -------------------------------------------------------------------------------- /src/assets/img/spotify.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NdekoCode/spotify-app/06222a25c97fbc5f90b55875e19ce5006b48e224/src/assets/img/spotify.jpg -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AlbumsData.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import CardDetails from './CardDetails'; 3 | import MusicContext from '../data/AppContext'; 4 | import useFetch from '../data/hookFunc'; 5 | import SkeletonData from './SkeletonData'; 6 | 7 | function AlbumsData() { 8 | const urlAlbums = `https://api.spotify.com/v1/browse/new-releases`; 9 | const [albums, setAlbums] = useState({}); 10 | const { 11 | dataSongs, 12 | setDataSong, 13 | searchUser, 14 | newAlbums, 15 | isLoading, 16 | setting, 17 | setIsLoading, 18 | } = MusicContext(); 19 | const [albumsData, albumsLoading] = useFetch( 20 | urlAlbums, 21 | newAlbums, 22 | setting.authorize_token, 23 | isLoading, 24 | ); 25 | useEffect(() => { 26 | setIsLoading(albumsLoading); 27 | 28 | if (searchUser.length > 1) { 29 | // eslint-disable-next-line no-shadow 30 | const { albums } = dataSongs; 31 | if (albums !== undefined) { 32 | setAlbums(albums.items); 33 | setDataSong((d) => ({ ...d, albums })); 34 | } 35 | } else { 36 | setAlbums(albumsData.albums); 37 | setDataSong((d) => ({ ...d, albums: albumsData.albums })); 38 | } 39 | }, [searchUser, dataSongs.albums, albumsData.albums]); 40 | 41 | if (!isLoading && albums !== undefined && Object.keys(albums).length > 0) { 42 | const { items } = albums; 43 | if (items !== undefined) { 44 | return ( 45 |
46 |

47 | Suggest Albums of all time 48 |

49 |
50 | {items.map((album, key) => ( 51 | // eslint-disable-next-line react/no-array-index-key 52 | 53 | ))} 54 |
55 |
56 | ); 57 | } 58 | 59 | return ( 60 |
61 |

62 | Suggest{' '} 63 | 64 | {searchUser} 65 | {' '} 66 | Albums of all time 67 |

68 |
69 | {albums.map((album, index) => ( 70 | // eslint-disable-next-line react/no-array-index-key 71 | 72 | ))} 73 |
74 |
75 | ); 76 | } 77 | return ; 78 | } 79 | 80 | export default AlbumsData; 81 | -------------------------------------------------------------------------------- /src/components/ArtistsData.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import useFetch from '../data/hookFunc'; 4 | import { idArtist } from '../data/getData'; 5 | import CardArtist from './CardArtist'; 6 | import SkeletonData from './SkeletonData'; 7 | 8 | function ArtistsData() { 9 | const urlArtists = `https://api.spotify.com/v1/artists?ids=${idArtist.join( 10 | ',', 11 | )}`; 12 | const [artists, setArtists] = useState({}); 13 | const { 14 | dataSongs, 15 | setDataSong, 16 | searchUser, 17 | newArtists, 18 | isLoading, 19 | setting, 20 | setIsLoading, 21 | } = MusicContext(); 22 | const [artistsData, artistsLoading] = useFetch( 23 | urlArtists, 24 | newArtists, 25 | setting.authorize_token, 26 | isLoading, 27 | ); 28 | useEffect(() => { 29 | setIsLoading(artistsLoading); 30 | 31 | if (searchUser.length > 1) { 32 | // eslint-disable-next-line no-shadow 33 | const { artists } = dataSongs; 34 | if (artists !== undefined) { 35 | setArtists(artists.items); 36 | setDataSong((d) => ({ ...d, artists })); 37 | } 38 | } else { 39 | // eslint-disable-next-line no-shadow 40 | const { artists } = artistsData; 41 | setArtists(artists); 42 | setDataSong((d) => ({ ...d, artists })); 43 | } 44 | }, [searchUser, dataSongs.artists, artistsData.artists]); 45 | 46 | if (!isLoading && artists !== undefined && Object.keys(artists).length > 0) { 47 | const { items } = artists; 48 | if (items === undefined) { 49 | return ( 50 |
51 |

52 | Suggest{' '} 53 | 54 | {searchUser} 55 | {' '} 56 | Artists 57 |

58 |
59 | {artists.map((artist, index) => ( 60 | // eslint-disable-next-line react/no-array-index-key 61 | 62 | ))} 63 |
64 |
65 | ); 66 | } 67 | return ( 68 |
69 |

70 | Suggest{' '} 71 | 72 | {searchUser} 73 | {' '} 74 | of all time Artists 75 |

76 |
77 | {items.map((artist, index) => ( 78 | // eslint-disable-next-line react/no-array-index-key 79 | 80 | ))} 81 |
82 |
83 | ); 84 | } 85 | 86 | return ; 87 | } 88 | 89 | export default ArtistsData; 90 | -------------------------------------------------------------------------------- /src/components/CardArtist.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import artistImg from '../assets/img/artist.jpeg'; 4 | import { convertFollowersNumber } from '../data/utilsFunc'; 5 | 6 | const CardArtist = ({ artist }) => { 7 | const { followers, id, images, name, popularity, type } = artist; 8 | const { handleFrame, setTypePlay, setShowFrame, setIdSong } = MusicContext(); 9 | const showPlayer = useCallback(() => { 10 | setIdSong(id); 11 | setTypePlay(type); 12 | handleFrame(); 13 | setShowFrame(true); 14 | }); 15 | const image = images[0]; 16 | return ( 17 |
18 |
19 | Profile picture 24 |
28 |
29 |

30 | {name} 31 |

32 |
33 | 50 |
51 |
52 |
53 |
54 |

55 | {convertFollowersNumber(followers.total)} 56 |

57 |

Followers

58 |
59 |
60 |

{popularity}

61 |

Popularity

62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default CardArtist; 69 | -------------------------------------------------------------------------------- /src/components/CardDetails.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import React, { useCallback } from 'react'; 3 | import MusicContext from '../data/AppContext'; 4 | import { catString } from '../data/utilsFunc'; 5 | 6 | function CardDetails({ album }) { 7 | // eslint-disable-next-line camelcase 8 | const { artists, images, name, total_tracks, release_date, id, type } = album; 9 | const { handleFrame, setShowFrame, setTypePlay, setIdSong } = MusicContext(); 10 | const showPlayer = useCallback(() => { 11 | setIdSong(id); 12 | setTypePlay(type); 13 | handleFrame(); 14 | setShowFrame(true); 15 | }); 16 | return ( 17 | //
18 |
23 | {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} 24 | 30 |
31 | 36 |
37 |
38 | 54 |
55 |
56 |
57 |

58 | {catString(name)} 59 |

60 |
61 | {total_tracks} 62 |
63 |
64 |
65 |
Album
66 |
67 | {release_date.split('-')[0]} 68 |
69 |
70 | 71 |
72 | {catString(artists[0].name)} 73 |
74 |
75 |
76 |
77 | //
78 | ); 79 | } 80 | 81 | export default CardDetails; 82 | -------------------------------------------------------------------------------- /src/components/CardPlaylist.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import artistImg from '../assets/img/artist.jpeg'; 4 | import { catString } from '../data/utilsFunc'; 5 | 6 | function CardPlaylist({ playlist }) { 7 | const { description, href, id, images, name, owner, type } = playlist; 8 | const { handleFrame, setTypePlay, setShowFrame, setIdSong } = MusicContext(); 9 | const showPlayer = useCallback((evt) => { 10 | evt.preventDefault(); 11 | setIdSong(id); 12 | setTypePlay(type); 13 | handleFrame(); 14 | setShowFrame(true); 15 | }); 16 | const [image] = images; 17 | return ( 18 | 23 | {/* Image Cover */} 24 | 29 | {/* Title */} 30 |

{name}

31 | {/* Description */} 32 |

33 | {catString(description, 90)} 34 |

35 |

36 | Créer par : 37 | {owner.display_name} 38 |

39 |
40 | ); 41 | } 42 | 43 | export default CardPlaylist; 44 | -------------------------------------------------------------------------------- /src/components/CardSkeleton.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/heading-has-content */ 2 | import React from 'react'; 3 | 4 | function CardSkeleton() { 5 | return ( 6 |
7 |
8 |
9 |

10 |

11 |

12 |

13 |

14 |
15 | ); 16 | } 17 | 18 | export default CardSkeleton; 19 | -------------------------------------------------------------------------------- /src/components/CardSong.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React, { useCallback } from 'react'; 3 | import MusicContext from '../data/AppContext'; 4 | import artistImg from '../assets/img/artist.jpeg'; 5 | function CardSong({ song }) { 6 | const { id, album, type, artists, name } = song; 7 | const { handleFrame, setTypePlay, setShowFrame, setIdSong } = MusicContext(); 8 | 9 | const showPlayer = useCallback((evt) => { 10 | evt.preventDefault(); 11 | setIdSong(id); 12 | setTypePlay(type); 13 | handleFrame(); 14 | setShowFrame(true); 15 | }); 16 | return ( 17 | 18 |
19 |
20 |
21 | {name} 26 | 27 |
28 | 43 | 59 | 76 |
77 |
78 |
79 |

{name}

80 |
81 |

{artists[0].name}

82 |
83 |
84 |
85 |
86 |
87 | ); 88 | } 89 | 90 | export default CardSong; 91 | -------------------------------------------------------------------------------- /src/components/CardTrack.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | 4 | import artistImg from '../assets/img/artist.jpeg'; 5 | 6 | function CardTrack({ track }) { 7 | const { album, artists, name, id, type } = track; 8 | 9 | const { handleFrame, setTypePlay, setShowFrame, setIdSong } = MusicContext(); 10 | const showPlayer = useCallback(() => { 11 | setIdSong(id); 12 | setTypePlay(type); 13 | handleFrame(); 14 | setShowFrame(true); 15 | }); 16 | return ( 17 |
18 |
22 |
23 | {name} 28 |
29 | 45 |
46 |
47 |
48 |

49 | {artists.map( 50 | (artist, index) => 51 | artist.name + (index + 1 < artists.length ? ', ' : ''), 52 | )} 53 |

54 |

{name}

55 |
56 |
57 |
58 | ); 59 | } 60 | 61 | export default CardTrack; 62 | -------------------------------------------------------------------------------- /src/components/CardTracksContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | function CardTracksContainer({ children }) { 5 | return ( 6 |
7 | {children} 8 | 12 |

See More

13 |
14 |
15 | ); 16 | } 17 | 18 | export default CardTracksContainer; 19 | -------------------------------------------------------------------------------- /src/components/FormLogin.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import MusicContext from '../data/AppContext'; 4 | import { setDataStorage, verifyUserConnect } from '../data/utilsFunc'; 5 | import GoogleAuthButton from './GoogleAuthButton'; 6 | 7 | function FormLogin() { 8 | const navigate = useNavigate(); 9 | const [userData, setUserData] = useState({ 10 | email: '', 11 | password: '', 12 | username: '', 13 | }); 14 | const { userIsConnect, setUserIsConnect } = MusicContext(); 15 | const [stateForm, setStateForm] = useState({ 16 | valid: false, 17 | connected: false, 18 | messageAlert: '', 19 | }); 20 | const validDataLength = (value, el, dataLength) => { 21 | if (value.length < dataLength) { 22 | el.style.borderColor = 'red'; 23 | setStateForm((data) => ({ ...data, valid: false })); 24 | } else { 25 | el.style.borderColor = 'transparent'; 26 | setStateForm((data) => ({ ...data, valid: true })); 27 | } 28 | }; 29 | const validEmail = (target) => { 30 | if (/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(target.value)) { 31 | return true; 32 | } 33 | target.style.borderColor = 'red'; 34 | return false; 35 | }; 36 | const handleChange = useCallback(({ target }) => { 37 | const { name } = target; 38 | const { type } = target; 39 | const value = target.value.trim(); 40 | setUserData((data) => ({ ...data, [name]: value })); 41 | validDataLength(value, target, 3); 42 | if (type === 'password') { 43 | validDataLength(value, target, 8); 44 | } 45 | 46 | if (type === 'email') { 47 | setStateForm((data) => ({ ...data, valid: validEmail(target) })); 48 | } 49 | }); 50 | const handleSubmit = useCallback((evt) => { 51 | evt.preventDefault(); 52 | // "Les données entrer sont invalides" 53 | const formData = {}; 54 | const validForm = Object.values(userData).every((item) => item.length > 2); 55 | setStateForm((data) => ({ 56 | ...data, 57 | valid: validForm, 58 | })); 59 | if (validForm) { 60 | formData.email = userData.email; 61 | formData.password = userData.password; 62 | formData.username = userData.username; 63 | setDataStorage('userData', userData); 64 | verifyUserConnect(); 65 | setUserIsConnect(true); 66 | setStateForm((data) => ({ 67 | ...data, 68 | connected: true, 69 | })); 70 | } else { 71 | setStateForm((data) => ({ 72 | ...data, 73 | messageAlert: 'Les données entrer sont invalides', 74 | })); 75 | } 76 | }); 77 | useEffect(() => { 78 | if (verifyUserConnect()) { 79 | navigate('/dashboard'); 80 | } 81 | }, [userIsConnect]); 82 | return ( 83 |
84 |
85 |
92 |
96 |

97 | Brand 98 |

99 |

106 | {stateForm.messageAlert.length < 2 107 | ? 'Welcome back!' 108 | : stateForm.messageAlert} 109 |

110 | 111 |
112 | 113 | 114 | or login with email 115 | 116 | 117 |
118 |
119 | 125 | 135 |
136 |
137 | 143 | 153 |
154 |
155 | 161 | 172 |
173 |
174 | 180 |
181 | 182 |
183 |
184 | ); 185 | } 186 | 187 | export default FormLogin; 188 | -------------------------------------------------------------------------------- /src/components/GoogleAuthButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | // On a installer ce paquer pour pouvoir traduire le token que nous renvois google quand on clique sur ce boutton 3 | // eslint-disable-next-line camelcase 4 | import jwt_decode from 'jwt-decode'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import { setDataStorage } from '../data/utilsFunc'; 7 | import MusicContext from '../data/AppContext'; 8 | 9 | function GoogleAuthButton() { 10 | const { setUserIsConnect, setUserData } = MusicContext(); 11 | const navigate = useNavigate(); 12 | function handleCallbackResponse(response) { 13 | /* La reponse que va nous renvoyer google quand on va cliquer sur le boutton "Se connecter avec google" */ 14 | const userObject = jwt_decode(response.credential); 15 | const userData = { 16 | email: userObject?.email, 17 | username: userObject?.name, 18 | image: userObject?.picture, 19 | }; 20 | setDataStorage('userData', userData); 21 | setUserData(userData); 22 | setUserIsConnect(true); 23 | navigate('/dashboard'); 24 | } 25 | useEffect(() => { 26 | /* global google */ 27 | google.accounts.id.initialize({ 28 | client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID, 29 | callback: handleCallbackResponse, 30 | }); 31 | google.accounts.id.renderButton(document.getElementById('googleButton'), { 32 | theme: 'outline', 33 | size: 'large', 34 | }); 35 | }, []); 36 | return ( 37 |
38 | {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} 39 | 44 |
45 | 46 | 50 | 54 | 58 | 62 | 63 |
64 |

65 | Sign in with Google 66 |

67 |
68 |
69 | ); 70 | } 71 | 72 | export default GoogleAuthButton; 73 | -------------------------------------------------------------------------------- /src/components/GreetUser.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const GreetUser = ({ user }) => { 4 | return ( 5 |

6 | Hi, 7 | 8 | {user ? user : 'Arick Bulakali.'} 9 | 10 | 👋 11 |

12 | ); 13 | }; 14 | 15 | export default GreetUser; 16 | -------------------------------------------------------------------------------- /src/components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import heroImg from '../assets/img/images-1.png'; 4 | 5 | const Hero = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |

12 | Find and listen your favorite 13 | Artist 14 | Music Here 15 |

16 |

17 | The best Music website communicate a feel and make easy for 18 | visitors to discover your podcast. 19 |

20 | 21 |
22 | 26 | Begin Listen 27 | 28 | 32 | Get Started 33 | 34 |
35 |
36 |
37 |
38 | 43 |
44 |
45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default Hero; 52 | -------------------------------------------------------------------------------- /src/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | const Loader = () => { 2 | return ( 3 |
4 | 5 |
6 | 7 | 8 | 9 |
10 |
11 | ); 12 | }; 13 | export default Loader; 14 | -------------------------------------------------------------------------------- /src/components/LoadingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loader from './Loader'; 3 | import '../assets/css/components/PageLoading.css'; 4 | import '../assets/css/components/Loader.css'; 5 | const LoadingPage = () => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default LoadingPage; 16 | -------------------------------------------------------------------------------- /src/components/ModalPlayer.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | 4 | const ModalPlayer = memo(({ id, visibility, type }) => { 5 | const url = `https://open.spotify.com/embed/${type}/${id}?utm_source=generator`; 6 | const { setShowFrame } = MusicContext(); 7 | const hidePlayer = useCallback(() => { 8 | setShowFrame(false); 9 | }); 10 | if (visibility) { 11 | const className = 'modal ' + (type === 'track' ? 'is-track' : 'is-list'); 12 | 13 | // is-track 14 | return ( 15 |
16 |
20 | 25 | Close Circle 26 | 33 | 41 | 42 |
43 | 52 |
53 | ); 54 | } 55 | return null; 56 | }); 57 | 58 | export default ModalPlayer; 59 | -------------------------------------------------------------------------------- /src/components/MusicApp.jsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect } from 'react'; 2 | import Sidebar from './Sidebar'; 3 | import MusicContext from '../data/AppContext'; 4 | import ModalPlayer from './ModalPlayer'; 5 | import UserData from './UserData'; 6 | import GreetUser from './GreetUser'; 7 | import Search from './Search'; 8 | import { useNavigate } from 'react-router-dom'; 9 | import { getDataStorage } from '../data/utilsFunc'; 10 | 11 | const MusicApp = memo(({ children }) => { 12 | const { idSong, userIsConnect, showFrame, typePlay } = MusicContext(); 13 | const navigate = useNavigate(); 14 | const user = getDataStorage('userData'); 15 | useEffect(() => { 16 | if (!userIsConnect) { 17 | navigate('/login'); 18 | } 19 | }, [userIsConnect]); 20 | return ( 21 | <> 22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 |
34 | 35 |
36 |
{children}
37 |
38 |
39 |
40 |
41 | 42 | ); 43 | }); 44 | 45 | export default MusicApp; 46 | -------------------------------------------------------------------------------- /src/components/Musics.jsx: -------------------------------------------------------------------------------- 1 | import MusicContext from '../data/AppContext'; 2 | import AlbumsData from './AlbumsData'; 3 | import CardTracksContainer from './CardTracksContainer'; 4 | import TrackData from './TrackData'; 5 | // import TrackData from "./TrackDataTable"; 6 | 7 | const Musics = () => { 8 | const { searchUser } = MusicContext(); 9 | 10 | return ( 11 |
12 | {searchUser.length < 1 && ( 13 |
14 |

15 | Artists you must love the most 16 |

17 | 25 |
26 | )} 27 |
28 |

29 | Your most favourite tracks of all time 30 |

31 | 32 | 33 | 34 |
35 | 36 | 37 |
38 | ); 39 | }; 40 | 41 | export default Musics; 42 | -------------------------------------------------------------------------------- /src/components/NavbarHome.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import logo from '../assets/img/drc.svg'; 5 | import MusicContext from '../data/AppContext'; 6 | import { homeRoute } from '../routes/routes'; 7 | const NavbarHome = () => { 8 | const { menu, toggleMenu } = MusicContext(); 9 | return ( 10 | 91 | ); 92 | }; 93 | 94 | export default NavbarHome; 95 | -------------------------------------------------------------------------------- /src/components/PlaylistsData.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import useFetch from '../data/hookFunc'; 4 | import CardPlaylist from './CardPlaylist'; 5 | import SkeletonData from './SkeletonData'; 6 | 7 | const PlaylistsData = () => { 8 | const urlPlaylist = 'https://api.spotify.com/v1/browse/featured-playlists'; 9 | const [playlists, setPlaylists] = useState({}); 10 | const { 11 | dataSongs, 12 | setDataSong, 13 | searchUser, 14 | newPlaylists, 15 | isLoading, 16 | setting, 17 | setIsLoading, 18 | } = MusicContext(); 19 | const [playlistsData, playlistsLoading] = useFetch( 20 | urlPlaylist, 21 | newPlaylists, 22 | setting.authorize_token, 23 | isLoading, 24 | ); 25 | useEffect(() => { 26 | setIsLoading(playlistsLoading); 27 | 28 | if (searchUser.length > 1) { 29 | let { playlists } = dataSongs; 30 | if (playlists !== undefined) { 31 | setPlaylists(playlists.items); 32 | setDataSong((d) => ({ ...d, playlists })); 33 | } 34 | } else { 35 | setPlaylists(playlistsData.playlists); 36 | setDataSong((d) => ({ ...d, playlists: playlistsData.playlists })); 37 | } 38 | }, [searchUser, dataSongs.playlists, playlistsData.playlists]); 39 | 40 | if ( 41 | !isLoading && 42 | playlists !== undefined && 43 | Object.keys(playlists).length > 0 44 | ) { 45 | const { items } = playlists; 46 | if (items !== undefined) { 47 | return ( 48 |
49 |

50 | Suggest Playlists of all time 51 |

52 |
53 | {items.map((playlist, index) => ( 54 | 55 | ))} 56 |
57 |
58 | ); 59 | } 60 | 61 | return ( 62 |
63 |

64 | Suggest{' '} 65 | 66 | {searchUser} 67 | {' '} 68 | Playlists of all time 69 |

70 |
71 | {playlists.map((playlist, index) => ( 72 | 73 | ))} 74 |
75 |
76 | ); 77 | } 78 | 79 | return ; 80 | }; 81 | 82 | export default PlaylistsData; 83 | -------------------------------------------------------------------------------- /src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import { findAndSetData } from '../data/getData'; 4 | 5 | const Search = () => { 6 | const { 7 | setting, 8 | searchUser, 9 | setDataSong, 10 | setIsLoading, 11 | isLoading, 12 | setSearchUser, 13 | } = MusicContext(); 14 | const [input, setInput] = useState(searchUser); 15 | const params = { 16 | method: 'GET', 17 | headers: { 18 | Accept: 'application/json', 19 | 'Content-Type': 'application/json', 20 | Authorization: setting.authorize_token, 21 | }, 22 | }; 23 | const url = `https://api.spotify.com/v1/search?q=${searchUser}&type=album,track,artist,playlist,show,episode&include_external=audio?limit=15`; 24 | const searchData = () => { 25 | (async () => { 26 | const response = await fetch(url, params); 27 | const responseData = await response.json(); 28 | if (response.ok) { 29 | setIsLoading(false); 30 | setDataSong(responseData); 31 | } else { 32 | setIsLoading(false); 33 | } 34 | })(); 35 | }; 36 | const handleSubmit = (evt) => { 37 | evt.preventDefault(); 38 | setSearchUser(input.trim()); 39 | if (input.length > 1 && searchUser.length > 1) { 40 | searchData(); 41 | } 42 | }; 43 | const handleChange = useCallback(({ target }) => { 44 | const value = target.value.trim(); 45 | setInput(value); 46 | setSearchUser(value); 47 | 48 | if (input.length > 0 && searchUser.length > 0) { 49 | searchData(); 50 | } 51 | }); 52 | useEffect(() => { 53 | if (input.length > 0 && searchUser.length > 0) { 54 | searchData(); 55 | } 56 | }, [input, searchUser]); 57 | return ( 58 |
62 | 65 |
66 | 75 | 76 | 96 |
97 |
98 | ); 99 | }; 100 | 101 | export default Search; 102 | -------------------------------------------------------------------------------- /src/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import logo from '../assets/img/drc.svg'; 2 | import { asideLinks } from '../routes/routes'; 3 | import SidebarItem from './SidebarItem'; 4 | const Sidebar = () => { 5 | return ( 6 | 22 | ); 23 | }; 24 | 25 | export default Sidebar; 26 | -------------------------------------------------------------------------------- /src/components/SidebarItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | const SidebarItem = ({ path, name, icon }) => { 5 | return ( 6 |
  • 7 | 11 | (nav.isActive 12 | ? ' bg-spotify ' 13 | : 'text-spotify hover:bg-card hover:text-spotify ') + 14 | 'lg:rounded-r-full flex justify-center lg:justify-start items-center sm:space-x-2 py-3 px-4 w-full lg:px-6 lg:py-2 lg:w-full text-spotify lg:bg-spotify lg:text-white' 15 | } 16 | > 17 | {icon} 18 | {name} 19 | 20 |
  • 21 | ); 22 | }; 23 | 24 | export default SidebarItem; 25 | -------------------------------------------------------------------------------- /src/components/SkeletonData.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CardSkeleton from './CardSkeleton'; 3 | import '../assets/css/components/SkeletonData.css'; 4 | 5 | const SkeletonData = ({ parent = true }) => { 6 | if (parent) { 7 | return ( 8 |
    9 | 10 | 11 | 12 | 13 |
    14 | ); 15 | } 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default SkeletonData; 27 | -------------------------------------------------------------------------------- /src/components/SongsData.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import { idArtist } from '../data/getData'; 4 | import useFetch from '../data/hookFunc'; 5 | import CardTrack from './CardTrack'; 6 | import SkeletonData from './SkeletonData'; 7 | 8 | const SongsData = () => { 9 | let searchArtist = idArtist.slice(0, 1).join(','); 10 | 11 | const urlTracks = `https://api.spotify.com/v1/recommendations?seed_artists=${searchArtist}&seed_genres=classic,country&seed_tracks=6CO4WFWJGcaU5IByGLUYUj,13BVU634EX7PqtRoKj0ZWZ&limit=24`; 12 | const [tracks, setTracks] = useState({}); 13 | const { 14 | dataSongs, 15 | setDataSong, 16 | searchUser, 17 | newTracks, 18 | isLoading, 19 | setting, 20 | setIsLoading, 21 | } = MusicContext(); 22 | const [tracksData, tracksLoading] = useFetch( 23 | urlTracks, 24 | newTracks, 25 | setting.authorize_token, 26 | isLoading, 27 | ); 28 | useEffect(() => { 29 | setIsLoading(tracksLoading); 30 | if (searchUser.length > 1) { 31 | let { tracks } = dataSongs; 32 | if (tracks !== undefined) { 33 | setTracks(tracks.items); 34 | setDataSong((d) => ({ ...d, tracks })); 35 | } 36 | } else { 37 | setTracks(tracksData.tracks); 38 | setDataSong((d) => ({ ...d, tracks: tracksData.tracks })); 39 | } 40 | }, [searchUser, dataSongs.tracks, tracksData.tracks]); 41 | 42 | if (!isLoading && tracks !== undefined && Object.keys(tracks).length > 0) { 43 | return ( 44 | <> 45 |

    46 | Your most favourite tracks of all time 47 |

    48 |
    49 | {tracks.map((track, index) => ( 50 | 51 | ))} 52 |
    53 | 54 | ); 55 | } 56 | return ; 57 | }; 58 | 59 | export default SongsData; 60 | -------------------------------------------------------------------------------- /src/components/TrackData.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import { idArtist } from '../data/getData'; 4 | import useFetch from '../data/hookFunc'; 5 | import CardSong from './CardSong'; 6 | import SkeletonData from './SkeletonData'; 7 | 8 | const TrackData = () => { 9 | let searchArtist = idArtist.slice(0, 1).join(','); 10 | 11 | const urlTracks = `https://api.spotify.com/v1/recommendations?seed_artists=${searchArtist}&seed_genres=classic,country&seed_tracks=6CO4WFWJGcaU5IByGLUYUj,13BVU634EX7PqtRoKj0ZWZ&limit=15`; 12 | const [tracks, setTracks] = useState({}); 13 | const { 14 | dataSongs, 15 | setDataSong, 16 | searchUser, 17 | newTracks, 18 | isLoading, 19 | setting, 20 | setIsLoading, 21 | } = MusicContext(); 22 | const [tracksData, tracksLoading] = useFetch( 23 | urlTracks, 24 | newTracks, 25 | setting.authorize_token, 26 | isLoading, 27 | ); 28 | useEffect(() => { 29 | setIsLoading(tracksLoading); 30 | if (searchUser.length > 1) { 31 | let { tracks } = dataSongs; 32 | if (tracks !== undefined) { 33 | setTracks(tracks.items); 34 | setDataSong((d) => ({ ...d, tracks })); 35 | } 36 | } else { 37 | setTracks(tracksData.tracks); 38 | setDataSong((d) => ({ ...d, tracks: tracksData.tracks })); 39 | } 40 | }, [searchUser, dataSongs.tracks, tracksData.tracks]); 41 | 42 | if (!isLoading && tracks !== undefined && Object.keys(tracks).length > 0) { 43 | return ( 44 | <> 45 | {tracks.map((song, index) => ( 46 | 47 | ))} 48 | 49 | ); 50 | } 51 | return ; 52 | }; 53 | 54 | export default TrackData; 55 | -------------------------------------------------------------------------------- /src/components/TrackDataTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TrackDataTable = () => { 4 | return ( 5 |
    6 |
    7 |
    8 |
    TRACK
    9 |
    DURATION
    10 |
    11 | 12 | 36 | 37 |
    38 |
    39 | ); 40 | }; 41 | 42 | export default TrackDataTable; 43 | -------------------------------------------------------------------------------- /src/components/TrackView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TrackView = () => { 4 | return ( 5 |
    6 |
    7 |
    8 |
    9 |
    10 | album 15 |
    16 |
    17 |

    18 | Whatever You Like 19 |

    20 |

    21 | T.I. ⋄ Paper Trail 22 |

    23 | 29 | Play on Spotify 30 | 31 |
    32 |
    33 |
    34 |

    Track Features

    35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 | 50 |
    51 |
    52 |

    Features Description

    53 |
    54 |

    Danceability

    55 |

    56 | Danceability describes how suitable a track is for dancing.{' '} 57 |

    58 |
    59 |
    60 |

    Acousticness

    61 |

    62 | High value represents high confidence that the track is 63 | acoustic. 64 |

    65 |
    66 |
    67 |

    Energy

    68 |

    69 | Energy represents a perceptual measure of intensity and 70 | activity. Typically, energetic tracks feel fast, loud, and 71 | noisy. 72 |

    73 |
    74 |
    75 |

    Instrumentalness

    76 |

    77 | Predicts whether a track contains no vocals. “Ooh” and “aah” 78 | sounds are treated as instrumental in this context. Rap or 79 | spoken word tracks are clearly “vocal”. High value represents 80 | the greater likelihood the track contains no vocal content. 81 |

    82 |
    83 |
    84 |

    Liveness

    85 |

    86 | Detects the presence of an audience in the recording. Higher 87 | liveness values represent an increased probability that the 88 | track was performed live. 89 |

    90 |
    91 |
    92 |

    Valence

    93 |

    94 | A measure of the musical positiveness conveyed by a track. 95 | Tracks with high valence sound more positive (e.g. happy, 96 | cheerful, euphoric), while tracks with low valence sound more 97 | negative (e.g. sad, depressed, angry). 98 |

    99 |
    100 |
    101 |

    Speechiness

    102 |

    103 | Speechiness detects the presence of spoken words in a track. 104 | The more exclusively speech-like the recording (e.g. talk 105 | show, audio book, poetry), the higher the attribute value.{' '} 106 |

    107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 | ); 114 | }; 115 | 116 | export default TrackView; 117 | -------------------------------------------------------------------------------- /src/components/UserData.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import MusicContext from '../data/AppContext'; 3 | import { disconnectedUser, verifyUserConnect } from '../data/utilsFunc'; 4 | 5 | const UserData = () => { 6 | const { userIsConnect, setUserIsConnect } = MusicContext(); 7 | const disconnected = useCallback((evt) => { 8 | evt.preventDefault(); 9 | setUserIsConnect(!userIsConnect); 10 | disconnectedUser(); 11 | }); 12 | return ( 13 |
    14 |
    15 |
    16 | 23 | Home 24 | 25 | 26 | 27 | 28 | Home 29 | 30 |
    31 | {userIsConnect && ( 32 | 37 |
    38 | 44 | 45 | 46 | 47 |

    Logout

    48 |
    49 |
    50 | )} 51 |
    52 |
    53 | ); 54 | }; 55 | 56 | export default UserData; 57 | -------------------------------------------------------------------------------- /src/data/AppContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, memo, useContext, useMemo, useState } from 'react'; 2 | 3 | const AppContext = createContext(); 4 | export const ContextProvider = memo(({ children }) => { 5 | const [setting, setSetting] = useState({ 6 | main_url: 'https://api.spotify.com/v1', 7 | token: '', 8 | authorize_token: '', 9 | }); 10 | const [userData, setUserData] = useState({}); 11 | const [userIsConnect, setUserIsConnect] = useState(false); 12 | const [typePlay, setTypePlay] = useState({}); 13 | const [menu, toggleMenu] = useState(false); 14 | const [isLoading, setIsLoading] = useState(true); 15 | const [idSong, setIdSong] = useState(''); 16 | const [showFrame, setShowFrame] = useState(false); 17 | const handleFrame = () => { 18 | setShowFrame(!showFrame); 19 | }; 20 | const [searchUser, setSearchUser] = useState(''); 21 | const [newAlbums, setNewAlbums] = useState({}); 22 | const [newTracks, setNewTracks] = useState({}); 23 | const [newArtists, setNewArtists] = useState({}); 24 | const [newPlaylists, setNewPlaylists] = useState({}); 25 | const [dataSongs, setDataSong] = useState({ 26 | artists: {}, 27 | albums: {}, 28 | tracks: {}, 29 | playlists: {}, 30 | }); 31 | const value = { 32 | setting, 33 | setSetting, 34 | userData, 35 | setUserData, 36 | isLoading, 37 | setIsLoading, 38 | showFrame, 39 | setShowFrame, 40 | handleFrame, 41 | idSong, 42 | setIdSong, 43 | dataSongs, 44 | setDataSong, 45 | searchUser, 46 | setSearchUser, 47 | menu, 48 | toggleMenu, 49 | typePlay, 50 | setTypePlay, 51 | newAlbums, 52 | setNewAlbums, 53 | newTracks, 54 | setNewTracks, 55 | newArtists, 56 | setNewArtists, 57 | newPlaylists, 58 | setNewPlaylists, 59 | userIsConnect, 60 | setUserIsConnect, 61 | }; 62 | 63 | return {children}; 64 | }); 65 | const MusicContext = () => useContext(AppContext); 66 | export default MusicContext; 67 | -------------------------------------------------------------------------------- /src/data/getData.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const idArtist = [ 4 | "4OBJLual30L7gRl5UkeRcT", 5 | "2dIgFjalVxs4ThymZ67YCE", 6 | "5VO5GFUAOEURtWW9oWvbcV", 7 | "0is7KJiz3t87LiJWUO1tNI", 8 | "0GOx72r5AAEKRGQFn3xqXK", 9 | "6IflU2YrY5Cyw7YoBICosV", 10 | "4bSBGQWUDeonXg7P4ys0CM", 11 | "4sbXXFzEWJY2zsZjelerjX", 12 | "20M8IJbzy7Y5EBRfwDIUmb", 13 | "3qfrrrSO7utFdJkM2tvMRb", 14 | "7x3eTVPlBiPjXHn3qotY86", 15 | "7xNYY1Zkb1vks5m9ATlJok", 16 | ]; 17 | export default function fetchData(url, token) { 18 | let data, 19 | loading = true; 20 | const params = { 21 | method: "GET", 22 | headers: { 23 | Accept: "application/json", 24 | "Content-Type": "application/json", 25 | Authorization: token, 26 | }, 27 | }; 28 | // On met une fonction asynchrone que l'on va appeler plus tard pour executer les requetes 29 | (async () => { 30 | const response = await fetch(url, params); 31 | const responseData = await response.json(); 32 | if (response.ok) { 33 | data = responseData; 34 | loading = false; 35 | } else { 36 | loading = false; 37 | } 38 | })(); 39 | return [data, loading]; 40 | } 41 | export function findAndSetData(url, token, setData) { 42 | const [data, loading] = fetchData(url, token); 43 | if (data !== undefined && data !== null) { 44 | setData(data); 45 | return [data, loading]; 46 | } 47 | 48 | return [data, loading]; 49 | } 50 | -------------------------------------------------------------------------------- /src/data/hookFunc.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export default function useFetch(url, data, token) { 4 | // On initialise l'Etat du des données à charger en AJAX 5 | const [state, setState] = useState({ 6 | items: [], 7 | loading: true, 8 | }); 9 | let params = { 10 | method: 'GET', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | Authorization: token, 14 | }, 15 | }; 16 | 17 | useEffect(() => { 18 | // On met une fonction asynchrone que l'on va appeler plus tard pour executer les requetes 19 | (async () => { 20 | const response = await fetch(url, params); 21 | data = await response.json(); 22 | if (response.ok) { 23 | setState((state) => ({ 24 | items: Object.assign(state.items, data), 25 | loading: false, 26 | })); 27 | } else { 28 | setState((state) => ({ ...state, loading: false })); 29 | } 30 | })(); 31 | }, [state.items, url, state.loading, token]); 32 | return [state.items, state.loading]; 33 | } 34 | -------------------------------------------------------------------------------- /src/data/secureData.js: -------------------------------------------------------------------------------- 1 | export const CLIENT_ID = import.meta.env.VITE_CLIENT_ID; 2 | export const CLIENT_SECRET = import.meta.env.VITE_CLIENT_SECRET; 3 | export const urlParams = 'offset=20&limit=20'; 4 | -------------------------------------------------------------------------------- /src/data/utilsFunc.js: -------------------------------------------------------------------------------- 1 | export function catString(str, length = 20) { 2 | if (str.length > length) { 3 | return str.substring(0, length) + '...'; 4 | } 5 | return str; 6 | } 7 | export function getDataStorage(key) { 8 | return JSON.parse(localStorage.getItem(key)); 9 | } 10 | export function setDataStorage(key, data) { 11 | localStorage.setItem(key, JSON.stringify(data)); 12 | } 13 | 14 | export function verifyUserConnect() { 15 | if ( 16 | getDataStorage('userData') !== null && 17 | getDataStorage('userData') !== undefined 18 | ) { 19 | return Object.keys(getDataStorage('userData')).length > 1; 20 | } 21 | return false; 22 | } 23 | 24 | export function connectedUser(newUser) { 25 | setDataStorage('userData', newUser); 26 | } 27 | export function disconnectedUser() { 28 | localStorage.removeItem('userData'); 29 | } 30 | function floatFormat(number) { 31 | return Number.parseFloat(number).toFixed(1); 32 | } 33 | export function convertFollowersNumber(value) { 34 | if (value > 1_000_000) { 35 | return parseFloat(floatFormat(value / 1_000_000)) + 'M'; 36 | } else if (value > 1_000) { 37 | return parseFloat(floatFormat(value / 1_000)) + 'K'; 38 | } 39 | return value; 40 | } 41 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { ContextProvider } from './data/AppContext'; 4 | import App from './App'; 5 | import './assets/css/index.css'; 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | 11 | , 12 | ); 13 | -------------------------------------------------------------------------------- /src/pages/Albums.jsx: -------------------------------------------------------------------------------- 1 | import AlbumsData from "../components/AlbumsData"; 2 | import MusicApp from "../components/MusicApp"; 3 | 4 | const Albums = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default Albums; 13 | -------------------------------------------------------------------------------- /src/pages/Artists.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ArtistsData from "../components/ArtistsData"; 3 | import MusicApp from "../components/MusicApp"; 4 | 5 | const Artists = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Artists; 14 | -------------------------------------------------------------------------------- /src/pages/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import "../assets/css/App.css"; 2 | import MusicApp from "../components/MusicApp"; 3 | import Musics from "../components/Musics"; 4 | 5 | export default function Dashboard() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import '../assets/css/App.css'; 2 | import Hero from '../components/Hero'; 3 | import NavbarHome from '../components/NavbarHome'; 4 | 5 | export default function Home() { 6 | return ( 7 |
    8 |
    9 |
    10 | 11 | 12 |
    13 |
    14 |
    15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormLogin from '../components/FormLogin'; 3 | import NavbarHome from '../components/NavbarHome'; 4 | 5 | const Login = () => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Login; 15 | -------------------------------------------------------------------------------- /src/pages/PlayList.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MusicApp from "../components/MusicApp"; 3 | import PlaylistsData from "../components/PlaylistsData"; 4 | 5 | const PlayList = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default PlayList; 14 | -------------------------------------------------------------------------------- /src/pages/Tracks.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "../assets/css/components/card.css"; 3 | import MusicApp from "../components/MusicApp"; 4 | import SongsData from "../components/SongsData"; 5 | 6 | const Tracks = () => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Tracks; 15 | -------------------------------------------------------------------------------- /src/routes/routes.jsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import LoadingPage from '../components/LoadingPage'; 3 | const Albums = lazy(() => import('../pages/Albums')); 4 | const PlayList = lazy(() => import('../pages/PlayList')); 5 | const Dashboard = lazy(() => import('../pages/Dashboard')); 6 | const Login = lazy(() => import('../pages/Login')); 7 | const Tracks = lazy(() => import('../pages/Tracks')); 8 | const Home = lazy(() => import('../pages/Home')); 9 | const Artists = lazy(() => import('../pages/Artists')); 10 | 11 | const routes = [ 12 | { 13 | path: '/', 14 | element: ( 15 | }> 16 | 17 | 18 | ), 19 | }, 20 | { 21 | path: '/dashboard', 22 | element: ( 23 | }> 24 | 25 | 26 | ), 27 | }, 28 | { 29 | path: '/login', 30 | element: ( 31 | }> 32 | 33 | 34 | ), 35 | }, 36 | { 37 | path: '/albums', 38 | element: ( 39 | }> 40 | 41 | 42 | ), 43 | }, 44 | 45 | { 46 | path: '/tracks', 47 | element: ( 48 | }> 49 | 50 | 51 | ), 52 | }, 53 | 54 | { 55 | path: '/artists', 56 | element: ( 57 | }> 58 | 59 | 60 | ), 61 | }, 62 | 63 | { 64 | path: '/playlist', 65 | element: ( 66 | }> 67 | 68 | 69 | ), 70 | }, 71 | ]; 72 | 73 | export const asideLinks = [ 74 | // { 75 | // path: "/profile", 76 | // name: "Profile", 77 | // icon: ( 78 | // 84 | // 85 | // 86 | // 87 | // ), 88 | // }, 89 | { 90 | path: '/dashboard', 91 | name: 'Dashboard', 92 | icon: ( 93 | 100 | Home 101 | 102 | 103 | 104 | ), 105 | }, 106 | { 107 | path: '/albums', 108 | name: 'Albums', 109 | icon: ( 110 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ), 123 | }, 124 | 125 | { 126 | path: '/tracks', 127 | name: 'Tracks', 128 | icon: ( 129 | 135 | 136 | 137 | 138 | ), 139 | }, 140 | { 141 | path: '/artists', 142 | name: 'Artists', 143 | icon: ( 144 | 150 | 151 | 152 | 153 | ), 154 | }, 155 | { 156 | path: '/playlist', 157 | name: 'Playlist', 158 | icon: ( 159 | 165 | 166 | 167 | 168 | ), 169 | }, 170 | ]; 171 | 172 | export const homeRoute = [ 173 | { 174 | path: '/', 175 | name: 'Home', 176 | icon: '', 177 | }, 178 | { 179 | path: '/dashboard', 180 | name: 'Premium', 181 | icon: '', 182 | }, 183 | 184 | { 185 | path: '/login', 186 | name: 'Login', 187 | icon: '', 188 | }, 189 | ]; 190 | 191 | export default routes; 192 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------