├── .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 |
24 |
28 |
29 |
30 | {name}
31 |
32 |
33 |
37 |
42 |
48 |
49 |
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 | //
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 |
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 |
26 |
27 |
28 |
32 |
40 |
41 |
42 |
43 |
48 |
56 |
57 |
58 |
59 |
63 |
70 |
74 |
75 |
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 |
28 |
29 |
34 |
42 |
43 |
44 |
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 |
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 |
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 |
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 |
11 |
12 |
18 |
19 | NdekoMusic
20 |
21 |
22 |
toggleMenu(true)}
24 | className="sm:block md:hidden text-gray-500 hover:text-gray-700 focus:text-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500"
25 | >
26 |
39 |
40 |
41 |
42 |
43 |
44 |
88 |
89 |
90 |
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 |
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 |
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 |
15 |
16 |
32 |
33 |
34 |
Track Features
35 |
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 |
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 |
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 |
--------------------------------------------------------------------------------