180 | )}
181 | >
182 | );
183 | }
184 |
185 | export default Playlist;
186 |
--------------------------------------------------------------------------------
/my-app/src/pages/Playlist/styles.css:
--------------------------------------------------------------------------------
1 | #album {
2 | display: flex;
3 | background-repeat: no-repeat;
4 | background-size: cover;
5 | background-position: center;
6 | }
7 |
8 | #album .album-info {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | width: 23%;
13 | }
14 |
15 | #album .album-info .album-title {
16 | text-align: center;
17 | }
18 |
19 | #album .album-info .album-image {
20 | background-position: 50%;
21 | margin-bottom: 10px;
22 | }
23 |
24 | #album .album-info span {
25 | color: #c4c4c4;
26 | font-size: 12px;
27 | }
28 |
29 | #album .album-info .album-artists {
30 | margin-top: 5px;
31 | text-align: center;
32 | }
33 |
34 | #album .album-info .album-artists span:hover {
35 | color: #fff;
36 | text-decoration: underline;
37 | text-underline-offset: 5px;
38 | cursor: pointer;
39 | }
40 |
41 | #album .album-info .album-artists a + a::before {
42 | content: ", ";
43 | }
44 |
45 | #album .album-info .album-year {
46 | display: flex;
47 | justify-content: center;
48 | }
49 |
50 | #album .album-info .album-year a + a::before {
51 | content: "-";
52 | margin: 0 5px;
53 | }
54 |
55 | #album .album-info .album-options {
56 | margin-bottom: 15px;
57 | }
58 |
59 | #album .album-info .album-options svg {
60 | margin: 0 5px;
61 | }
62 |
63 | #album .album-tracks {
64 | margin-left: 30px;
65 | width: 100%;
66 | }
--------------------------------------------------------------------------------
/my-app/src/pages/Profile/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import './styles.css';
6 |
7 | import { FaPlay } from 'react-icons/fa';
8 |
9 | import { useSelector, useDispatch } from 'react-redux';
10 |
11 | import api from '../../services/api';
12 |
13 | import defaultImage from '../../assets/default-image.jpg';
14 |
15 | import millisToMinutesAndSeconds from '../../utils/millisToMinutesAndSeconds';
16 |
17 | import * as Player from '../../store/modules/player/actions';
18 |
19 | function Profile() {
20 | const [user, setUser] = useState([]);
21 | const [artists, setArtists] = useState([]);
22 | const [tracks, setTracks] = useState([]);
23 |
24 | const [recently, setRecently] = useState([]);
25 |
26 | const [tracksTerm, setTracksTerm] = useState('long_term');
27 | const [artistsTerm, setArtistsTerm] = useState('long_term');
28 |
29 | const [load, setLoad] = useState(true);
30 |
31 | const dispatch = useDispatch();
32 |
33 | const trackData = useSelector((state) => state.player.data);
34 |
35 | async function loadUser() {
36 | const response = await api.get(`/me`);
37 |
38 | setUser(response.data);
39 |
40 | setLoad(false);
41 | }
42 |
43 | async function loadRecently() {
44 | const response = await api.get('/me/player/recently-played?limit=10');
45 |
46 | setRecently(response.data.items);
47 |
48 | setLoad(false);
49 | }
50 |
51 | useEffect(() => {
52 | loadUser();
53 | loadRecently();
54 | }, []);
55 |
56 | useEffect(() => {
57 | async function loadTracks() {
58 | const response = await api.get(
59 | `/me/top/tracks?time_range=${tracksTerm}&limit=10`
60 | );
61 |
62 | setTracks(response.data.items);
63 |
64 | setLoad(false);
65 | }
66 |
67 | loadTracks();
68 | }, [tracksTerm]);
69 |
70 | useEffect(() => {
71 | async function loadArtists() {
72 | const response = await api.get(
73 | `/me/top/artists?time_range=${artistsTerm}&limit=10`
74 | );
75 |
76 | setArtists(response.data.items);
77 |
78 | setLoad(false);
79 | }
80 |
81 | loadArtists();
82 | }, [artistsTerm]);
83 |
84 | return (
85 | <>
86 | {load ? (
87 |
301 | )}
302 | >
303 | );
304 | }
305 |
306 | export default Profile;
307 |
--------------------------------------------------------------------------------
/my-app/src/pages/Profile/styles.css:
--------------------------------------------------------------------------------
1 | #profile .header {
2 | display: flex;
3 | align-items: center;
4 | flex-direction: column;
5 | margin-bottom: 50px;
6 | }
7 |
8 | #profile h2 {
9 | margin-bottom: 0 !important;
10 | }
11 |
12 | #profile .header .user-picture {
13 | border-radius: 50%;
14 | margin-bottom: 10px;
15 | }
16 |
17 | #profile .header .username {
18 | font-weight: bold;
19 | text-transform: capitalize;
20 | font-size: 30px;
21 | }
22 |
23 | #profile .options-header {
24 | display: flex;
25 | flex-direction: row;
26 | justify-content: space-between;
27 | align-items: center;
28 | margin-bottom: 20px;
29 | }
30 |
31 | #profile .tracks .tracks-list .tracks-table span {
32 | cursor: pointer;
33 | }
34 |
35 | #profile .options-header .options span {
36 | margin-left: 10px;
37 | color: #ccc;
38 | font-weight: bold;
39 | }
40 |
41 | #profile .options-header .options span:hover {
42 | color: #fff;
43 | cursor: pointer;
44 | }
45 |
46 | #profile .tracks {
47 | margin-bottom: 30px;
48 | }
49 |
50 | #profile .artists-and-recently {
51 | display: flex;
52 | }
53 |
54 | #profile .artists-and-recently .artists {
55 | display: flex;
56 | flex-direction: column;
57 | }
58 |
59 | #profile .artists-and-recently .artists .artists-list .artist {
60 | display: flex;
61 | align-items: center;
62 | padding: 15px 0;
63 | border-bottom: 1px solid #282828;
64 | }
65 |
66 | #profile .artists-and-recently .artists .artists-list .artist:last-child {
67 | border: 0;
68 | }
69 |
70 | #profile .artists-and-recently .artists .artists-list .artist .artist-cover {
71 | height: 3rem;
72 | width: 3em;
73 | border-radius: 50%;
74 | margin-right: 10px;
75 | }
76 |
77 | #profile .artists-and-recently .artists .artists-list .artist span {
78 | font-weight: bold;
79 | color: #ccc;
80 | }
81 |
82 | #profile .artists-and-recently .artists .artists-list .artist span:hover {
83 | color: #fff;
84 | }
85 |
86 | #profile .artists-and-recently .recently-played {
87 | margin-left: 30px;
88 | width: 30%;
89 | }
90 |
91 | #profile .artists-and-recently .recently-played .recently-list .track {
92 | display: flex;
93 | align-items: center;
94 | margin-bottom: 15px;
95 | }
96 |
97 | #profile .artists-and-recently .recently-played .recently-list .track span {
98 | text-overflow: ellipsis;
99 | white-space: nowrap;
100 | overflow: hidden;
101 | max-width: 50%;
102 | }
103 |
104 | #profile .artists-and-recently .recently-played .recently-list .track .track-cover {
105 | width: 4rem;
106 | height: 4rem;
107 | margin-right: 10px;
108 | }
109 |
110 | @media (max-width: 810px) {
111 | #profile .tracks .tracks-list {
112 | overflow-x: auto;
113 | }
114 |
115 | #profile .options-header {
116 | flex-direction: column !important;
117 | align-items: flex-start !important;
118 | }
119 |
120 | #profile .options-header .options {
121 | margin-top: 20px;
122 | }
123 |
124 | #profile .options-header .options span {
125 | margin-left: 0 !important;
126 | }
127 |
128 | #profile .options-header .options span + span {
129 | margin-left: 10px !important;
130 | }
131 |
132 | #profile .artists-and-recently {
133 | flex-direction: column;
134 | }
135 |
136 | #profile .artists-and-recently .artists {
137 | margin-bottom: 30px;
138 | }
139 |
140 | #profile .artists-and-recently .recently-played {
141 | margin-left: 0px;
142 | width: 100%;
143 | }
144 |
145 | #profile .artists-and-recently .artists .artists-list .artist .artist-cover {
146 | height: 4rem !important;
147 | width: 4rem !important;
148 | }
149 |
150 | #profile .artists-and-recently .recently-played .recently-list .track .track-cover {
151 | width: 4rem !important;
152 | height: 4rem !important;
153 | }
154 | }
--------------------------------------------------------------------------------
/my-app/src/pages/Recently/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import './styles.css';
6 |
7 | import { useDispatch } from 'react-redux';
8 |
9 | import api from '../../services/api';
10 |
11 | import * as Player from '../../store/modules/player/actions';
12 |
13 | export default function Recently() {
14 | const dispatch = useDispatch();
15 | const [recently, setRecently] = useState([]);
16 |
17 | const [load, setLoad] = useState(true);
18 |
19 | async function loadRecently() {
20 | const response = await api.get('/me/player/recently-played?limit=50');
21 |
22 | setRecently(response.data.items);
23 |
24 | setLoad(false);
25 | }
26 |
27 | useEffect(() => {
28 | loadRecently();
29 | }, []);
30 |
31 | return (
32 | <>
33 | {load ? (
34 |
37 |
Tocadas recentemente
38 |
39 | {recently.map((data) => (
40 |
41 |
47 | dispatch(Player.playTrack(data.track))
48 | }
49 | />
50 |
51 | {data.track.name}
52 |
53 |
54 |
55 |
58 | {data.track.artists[0].name}
59 |
60 |
61 |
62 |
65 | {data.track.album.name}
66 |
67 |
68 |
69 |
70 | ))}
71 |
72 |
73 | )}
74 | >
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/my-app/src/pages/Recently/styles.css:
--------------------------------------------------------------------------------
1 | #recently {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | #recently h2 {
7 | margin-bottom: 20px;
8 | }
9 |
10 | #recently .track {
11 | text-align: center;
12 | }
13 |
14 | #recently .track .cover {
15 | margin-bottom: 10px;
16 | transition: .2s;
17 | cursor: pointer;
18 | }
19 |
20 | #recently .track .cover:hover {
21 | filter: brightness(.3);
22 | }
23 |
24 | #recently .track .track-name {
25 | font-weight: bold;
26 | font-size: 14px;
27 | text-overflow: ellipsis;
28 | overflow: hidden;
29 | -webkit-box-orient: vertical;
30 | display: -webkit-box;
31 | -webkit-line-clamp: 2;
32 | }
33 |
34 |
35 | #recently .track .artist-and-album {
36 | margin-top: 5px;
37 | display: flex;
38 | flex-direction: column;
39 | }
40 |
41 | #recently .track .artist-and-album span {
42 | font-size: 13px;
43 | margin-bottom: 5px;
44 | color: #ccc;
45 | text-overflow: ellipsis;
46 | overflow: hidden;
47 | -webkit-box-orient: vertical;
48 | display: -webkit-box;
49 | -webkit-line-clamp: 2;
50 | }
51 |
52 | #recently .track .artist-and-album span:hover {
53 | text-decoration: underline;
54 | text-underline-offset: 2px;
55 | }
56 |
57 | @media (max-width: 810px) {
58 | #recently .track .cover {
59 | width: 9rem;
60 | height: 9rem;
61 | }
62 | }
63 |
64 | @media (max-height: 450px) {
65 | #recently .track .cover {
66 | width: 100%;
67 | height: 13rem;
68 | }
69 | }
--------------------------------------------------------------------------------
/my-app/src/pages/Search/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { debounce } from 'lodash';
4 |
5 | import { Link } from 'react-router-dom';
6 |
7 | import { FiSearch } from 'react-icons/fi';
8 |
9 | import './styles.css';
10 |
11 | import api from '../../services/api';
12 |
13 | import defaultImage from '../../assets/default-image.jpg';
14 |
15 | export default function Search() {
16 | const [artists, setArtists] = useState([]);
17 | const [albums, setAlbums] = useState([]);
18 | const [playlists, setPlaylists] = useState([]);
19 |
20 | async function load(value) {
21 | const response = await api.get(
22 | `/search?q=${value}&type=album%2Cartist%2Cplaylist%2Ctrack&market=br`
23 | );
24 |
25 | setArtists(response.data.artists.items);
26 | setAlbums(response.data.albums.items);
27 | setPlaylists(response.data.playlists.items);
28 | }
29 | const delayedSearch = debounce((value) => {
30 | load(value);
31 | }, 1000);
32 |
33 | return (
34 |
35 |
36 |
delayedSearch(e.target.value)}
40 | placeholder="Busque artistas, músicas ou podcasts"
41 | />
42 |
43 | {artists.length !== 0 && (
44 |
45 |
Artistas
46 |
47 | {artists.map((artist) => (
48 |
49 |
50 |
60 |
61 | {artist.name}
62 |
63 |
64 |
65 | ))}
66 |
67 |
68 | )}
69 | {artists.length !== 0 && (
70 |
71 |
Albums
72 |
73 | {albums.map((data) => (
74 |
75 |
76 |
86 |
87 | {data.name}
88 |
89 |
90 |
93 |
94 | {data.artists[0].name}
95 |
96 |
97 |
98 | ))}
99 |
100 |
101 | )}
102 | {playlists.length !== 0 && (
103 |
104 |
Playlists
105 |
106 | {playlists.map((data) => (
107 |
108 |
109 |
119 |
120 | {data.name}
121 |
122 |
123 |
124 | ))}
125 |
126 |
127 | )}
128 |
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/my-app/src/pages/Search/styles.css:
--------------------------------------------------------------------------------
1 | #search .icon {
2 | position: absolute;
3 | min-width: 50px;
4 | margin-top: 6px;
5 | color: #000;
6 | }
7 |
8 | #search .search-input {
9 | text-overflow: ellipsis;
10 | border-radius: 500px;
11 | width: 32%;
12 | padding: 10px 50px;
13 | }
14 |
15 | #search .album-info, .artist-info {
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 | }
20 |
21 | #search .album-info a, .artist-info a {
22 | text-align: center;
23 | }
24 |
25 | #search .album-info .album-name, .artist-info .artist-name {
26 | font-weight: bold;
27 | margin-top: 10px;
28 | margin-bottom: 5px;
29 | text-overflow: ellipsis;
30 | overflow: hidden;
31 | -webkit-box-orient: vertical;
32 | display: -webkit-box;
33 | -webkit-line-clamp: 2;
34 | }
35 |
36 | #search .album-info .album-artist {
37 | color: #ccc;
38 | font-size: 14px;
39 | }
40 |
41 | #search .album-info .album-artist:hover {
42 | color: #fff;
43 | text-decoration: underline;
44 | }
45 |
46 | #search .search-results h2 {
47 | margin: 15px 0;
48 | }
49 |
50 | #search .search-results .artists .artist-info .artist-cover {
51 | border-radius: 50%;
52 | }
53 |
54 | #search .search-results .artists .artist-info .artist-cover, .album-info .album-cover {
55 | margin-bottom: 10px;
56 | }
57 |
58 | #search .search-results > div {
59 | margin-bottom: 50px;
60 | }
61 |
62 | @media (max-width: 810px) {
63 | #search .search-input {
64 | width: 100%;
65 | }
66 |
67 | #search .search-results .artists .artist-info .artist-cover, .album-info .album-cover {
68 | width: 9rem;
69 | height: 9rem;
70 | }
71 | }
--------------------------------------------------------------------------------
/my-app/src/pages/User/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { Link, useParams } from 'react-router-dom';
4 |
5 | import './styles.css';
6 |
7 | import api from '../../services/api';
8 |
9 | import defaultImage from '../../assets/default-image.jpg';
10 |
11 | export default function User() {
12 | const [user, setUser] = useState([]);
13 | const [userPicture, setUserPicture] = useState([]);
14 | const [playlists, setPlaylists] = useState([]);
15 |
16 | const [next, setNext] = useState('');
17 |
18 | const [load, setLoad] = useState(true);
19 |
20 | const id = useParams().userId;
21 |
22 | useEffect(() => {
23 | async function loadUser() {
24 | const response = await api.get(`/users/${id}`);
25 |
26 | setUser(response.data);
27 |
28 | if (response.data.images.length === 0) {
29 | setUserPicture(defaultImage);
30 | } else {
31 | setUserPicture(response.data.images[0].url);
32 | }
33 |
34 | setLoad(false);
35 | }
36 |
37 | async function loadPlaylists() {
38 | const response = await api.get(`/users/${id}/playlists?limit=50`);
39 |
40 | setPlaylists(response.data.items);
41 | setNext(response.data.next);
42 |
43 | setLoad(false);
44 | }
45 |
46 | loadUser();
47 | loadPlaylists();
48 | }, [id]);
49 |
50 | async function loadMore() {
51 | const endpointURL = next.replace('https://api.spotify.com/v1', '');
52 |
53 | const response = await api.get(endpointURL);
54 |
55 | setPlaylists((oldValue) => [...oldValue, ...response.data.items]);
56 | setNext(response.data.next);
57 | }
58 |
59 | return (
60 | <>
61 | {load &&
Carregando... }
62 | {!load && (
63 |
64 |
65 |
75 |
{user.display_name}
76 |
77 |
78 | {playlists.map((playlist) => (
79 |
80 |
81 |
91 |
92 |
93 | {playlist.name}
94 |
95 |
96 |
97 |
98 | ))}
99 |
100 | {next && (
101 |
102 |
103 | Carregar mais
104 |
105 |
106 | )}
107 |
108 | )}
109 | >
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/my-app/src/pages/User/styles.css:
--------------------------------------------------------------------------------
1 | #profile .header {
2 | display: flex;
3 | align-items: center;
4 | flex-direction: column;
5 | margin-bottom: 50px;
6 | }
7 |
8 | #profile .header .user-picture {
9 | border-radius: 50%;
10 | margin-bottom: 10px;
11 | }
12 |
13 | #profile .header .username {
14 | font-weight: bold;
15 | text-transform: capitalize;
16 | font-size: 30px;
17 | text-align: center;
18 | }
19 |
20 | #profile .options-header {
21 | display: flex;
22 | flex-direction: row;
23 | justify-content: space-between;
24 | align-items: center;
25 | margin-bottom: 20px;
26 | }
27 |
28 | #profile .options-header .options span {
29 | margin-left: 10px;
30 | color: #ccc;
31 | font-weight: bold;
32 | }
33 |
34 | #profile .options-header .options span:hover {
35 | color: #fff;
36 | cursor: pointer;
37 | }
38 |
39 | #profile h2 {
40 | margin-bottom: 20px;
41 | }
42 |
43 | #profile .item {
44 | text-align: center;
45 | }
46 |
47 | #profile .item .name-and-description {
48 | display: flex;
49 | flex-direction: column;
50 | margin-top: 10px;
51 | }
52 |
53 | #profile .item .name-and-description .name {
54 | font-weight: bold;
55 | margin-bottom: 10px;
56 | }
57 |
58 | #profile .item .name-and-description .description {
59 | color: #ccc;
60 | }
61 |
62 | .load-more {
63 | text-align: center;
64 | margin-top: 50px;
65 | }
--------------------------------------------------------------------------------
/my-app/src/pages/UserAlbums/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import './styles.css';
6 |
7 | import api from '../../services/api';
8 |
9 | import defaultImage from '../../assets/default-image.jpg';
10 |
11 | export default function UserAlbums() {
12 | const [albums, setAlbums] = useState([]);
13 | const [next, setNext] = useState('');
14 |
15 | const [load, setLoad] = useState(true);
16 |
17 | async function loadAlbums() {
18 | const response = await api.get('/me/albums?limit=50');
19 |
20 | setAlbums(response.data.items);
21 | setNext(response.data.next);
22 |
23 | setLoad(false);
24 | }
25 |
26 | useEffect(() => {
27 | loadAlbums();
28 | }, []);
29 |
30 | async function loadMore() {
31 | const endpointURL = next.replace('https://api.spotify.com/v1', '');
32 |
33 | const response = await api.get(endpointURL);
34 |
35 | setAlbums(albums.concat(response.data.items));
36 | setNext(response.data.next);
37 | }
38 |
39 | return (
40 | <>
41 | {load ? (
42 |
Carregando...
43 | ) : (
44 |
45 |
Álbuns que você salvou
46 |
47 | {albums.map((data) => (
48 |
49 |
50 |
60 |
61 | {data.album.name}
62 |
63 |
64 |
67 |
68 | {data.album.artists[0].name}
69 |
70 |
71 |
72 | ))}
73 |
74 | {next && (
75 |
76 |
77 | Carregar mais
78 |
79 |
80 | )}
81 |
82 | )}
83 | >
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/my-app/src/pages/UserAlbums/styles.css:
--------------------------------------------------------------------------------
1 | #albums h2 {
2 | margin-bottom: 20px;
3 | }
4 |
5 | #albums .item {
6 | text-align: center;
7 | }
8 |
9 | #albums .item {
10 | display: flex;
11 | flex-direction: column;
12 | margin-top: 10px;
13 | }
14 |
15 | #albums .item .cover {
16 | margin-bottom: 8px;
17 | }
18 |
19 | #albums .item .name {
20 | font-weight: bold;
21 | }
22 |
23 | #albums .item .artist {
24 | color: #ccc;
25 | font-size: 14px;
26 | margin-top: 8px;
27 | }
28 |
29 | #albums .item .artist:hover {
30 | text-decoration: underline;
31 | }
32 |
33 | .load-more {
34 | text-align: center;
35 | margin-top: 50px;
36 | }
--------------------------------------------------------------------------------
/my-app/src/pages/UserArtists/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import './styles.css';
6 |
7 | import api from '../../services/api';
8 |
9 | import defaultImage from '../../assets/default-image.jpg';
10 |
11 | export default function UserArtists() {
12 | const [artists, setArtists] = useState([]);
13 | const [next, setNext] = useState('');
14 |
15 | const [load, setLoad] = useState(true);
16 |
17 | async function loadArtists() {
18 | const response = await api.get('/me/following?type=artist&limit=50');
19 |
20 | setArtists(response.data.artists.items);
21 | setNext(response.data.artists.next);
22 |
23 | setLoad(false);
24 | }
25 |
26 | useEffect(() => {
27 | loadArtists();
28 | }, []);
29 |
30 | async function loadMore() {
31 | const endpointURL = next.replace('https://api.spotify.com/v1', '');
32 |
33 | const response = await api.get(endpointURL);
34 |
35 | setArtists([...artists, ...response.data.artists.items]);
36 | setNext(response.data.artists.next);
37 | }
38 |
39 | return (
40 | <>
41 | {load ? (
42 |
Carregando...
43 | ) : (
44 |
45 |
Artistas que você segue
46 |
47 | {artists.map((artist) => (
48 |
49 |
50 |
60 |
61 |
62 | {artist.name}
63 |
64 |
65 |
66 |
67 | ))}
68 |
69 | {next && (
70 |
71 |
72 | Carregar mais
73 |
74 |
75 | )}
76 |
77 | )}
78 | >
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/my-app/src/pages/UserArtists/styles.css:
--------------------------------------------------------------------------------
1 | #artists .item .artist-cover {
2 | border-radius: 50%;
3 | }
4 |
5 | #artists h2 {
6 | margin-bottom: 20px;
7 | }
8 |
9 | #artists .item {
10 | text-align: center;
11 | }
12 |
13 | #artists .item .name-and-description {
14 | display: flex;
15 | flex-direction: column;
16 | margin-top: 10px;
17 | }
18 |
19 | #artists .item .name-and-description .name {
20 | font-weight: bold;
21 | margin-bottom: 10px;
22 | }
23 |
24 | #artists .item .name-and-description .description {
25 | color: #ccc;
26 | }
27 |
28 | .load-more {
29 | text-align: center;
30 | margin-top: 50px;
31 | }
--------------------------------------------------------------------------------
/my-app/src/pages/UserPlaylists/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | import './styles.css';
6 |
7 | import api from '../../services/api';
8 |
9 | import defaultImage from '../../assets/default-image.jpg';
10 |
11 | export default function UserPlaylists() {
12 | const [playlists, setPlaylists] = useState([]);
13 | const [next, setNext] = useState('');
14 |
15 | const [load, setLoad] = useState(true);
16 |
17 | useEffect(() => {
18 | async function loadPlaylists() {
19 | const response = await api.get('/me/playlists?limit=50');
20 |
21 | setPlaylists((oldValue) => [...oldValue, ...response.data.items]);
22 | setNext(response.data.next);
23 |
24 | setLoad(false);
25 | }
26 |
27 | loadPlaylists();
28 | }, []);
29 |
30 | async function loadMore() {
31 | const endpointURL = next.replace('https://api.spotify.com/v1', '');
32 |
33 | const response = await api.get(endpointURL);
34 |
35 | setPlaylists([...playlists, ...response.data.items]);
36 | setNext(response.data.next);
37 | }
38 |
39 | return (
40 | <>
41 | {load ? (
42 |
Carregando...
43 | ) : (
44 |
45 |
Suas playlists
46 |
47 | {playlists.map((playlist) => (
48 |
49 |
50 |
60 |
61 |
62 | {playlist.name}
63 |
64 |
65 |
66 |
67 | ))}
68 |
69 | {next && (
70 |
71 |
72 | Carregar mais
73 |
74 |
75 | )}
76 |
77 | )}
78 | >
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/my-app/src/pages/UserPlaylists/styles.css:
--------------------------------------------------------------------------------
1 | #playlists h2 {
2 | margin-bottom: 20px;
3 | }
4 |
5 | #playlists .item {
6 | text-align: center;
7 | }
8 |
9 | #playlists .item .name-and-description {
10 | display: flex;
11 | flex-direction: column;
12 | margin-top: 10px;
13 | }
14 |
15 | #playlists .item .name-and-description .name {
16 | font-weight: bold;
17 | margin-bottom: 10px;
18 | }
19 |
20 | #playlists .item .name-and-description .description {
21 | color: #ccc;
22 | }
23 |
24 | .load-more {
25 | text-align: center;
26 | margin-top: 50px;
27 | }
28 |
--------------------------------------------------------------------------------
/my-app/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
3 |
4 | import Body from './components/Body';
5 |
6 | import Login from './pages/Login';
7 | import Home from './pages/Home';
8 | import Search from './pages/Search';
9 | import Album from './pages/Album';
10 | import Playlist from './pages/Playlist';
11 | import Categories from './pages/Categories';
12 | import Artist from './pages/Artist';
13 | import Profile from './pages/Profile';
14 | import Recently from './pages/Recently';
15 | import Liked from './pages/Liked';
16 |
17 | import UserPlaylists from './pages/UserPlaylists';
18 | import UserArtists from './pages/UserArtists';
19 | import UserAlbums from './pages/UserAlbums';
20 |
21 | import User from './pages/User';
22 |
23 | import getHashParams from './utils/getHashParams';
24 |
25 | const token = getHashParams().access_token;
26 |
27 | function PrivateRoute({ component: Component, ...rest }) {
28 | return (
29 |
32 | token !== undefined ? (
33 |
34 | ) : (
35 |
41 | )
42 | }
43 | />
44 | );
45 | }
46 |
47 | const Routes = () => (
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
61 |
65 |
69 |
70 |
74 |
78 |
79 |
80 |
81 |
82 |
83 | );
84 |
85 | export default Routes;
86 |
--------------------------------------------------------------------------------
/my-app/src/services/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import getHashParams from '../utils/getHashParams';
4 |
5 | const token = getHashParams().access_token;
6 |
7 | const api = axios.create({
8 | baseURL: 'https://api.spotify.com/v1',
9 | headers: {
10 | Authorization: `Bearer ${token}`,
11 | },
12 | });
13 |
14 | export default api;
15 |
--------------------------------------------------------------------------------
/my-app/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 |
3 | import rootReducer from './modules/rootReducer';
4 |
5 | const store = createStore(rootReducer);
6 |
7 | export default store;
8 |
--------------------------------------------------------------------------------
/my-app/src/store/modules/player/actions.js:
--------------------------------------------------------------------------------
1 | export function playTrack(trackData) {
2 | return {
3 | type: 'PLAY_TRACK',
4 | trackData,
5 | };
6 | }
7 |
--------------------------------------------------------------------------------
/my-app/src/store/modules/player/reducer.js:
--------------------------------------------------------------------------------
1 | const INITIAL_STATE = {
2 | data: [],
3 | };
4 |
5 | export default function player(state = INITIAL_STATE, action) {
6 | switch (action.type) {
7 | case 'PLAY_TRACK':
8 | return {
9 | data: action.trackData,
10 | };
11 | default:
12 | return state;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/my-app/src/store/modules/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import player from './player/reducer';
4 |
5 | export default combineReducers({ player });
6 |
--------------------------------------------------------------------------------
/my-app/src/utils/getHashParams.js:
--------------------------------------------------------------------------------
1 | function getHashParams() {
2 | const hashParams = {};
3 | let e;
4 | const r = /([^&;=]+)=?([^&;]*)/g;
5 | const q = window.location.hash.substring(1);
6 | while ((e = r.exec(q))) {
7 | hashParams[e[1]] = decodeURIComponent(e[2]);
8 | }
9 | return hashParams;
10 | }
11 |
12 | export default getHashParams;
13 |
--------------------------------------------------------------------------------
/my-app/src/utils/millisToMinutesAndSeconds.js:
--------------------------------------------------------------------------------
1 | function millisToMinutesAndSeconds(millis) {
2 | const minutes = Math.floor(millis / 60000);
3 | const seconds = ((millis % 60000) / 1000).toFixed(0);
4 | return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
5 | }
6 |
7 | export default millisToMinutesAndSeconds;
8 |
--------------------------------------------------------------------------------
/spotify-clone-app-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/spotify-clone-app-logo.png
--------------------------------------------------------------------------------
/spotify-clone-app-screenshots.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/spotify-clone-app-screenshots.jpg
--------------------------------------------------------------------------------