Skip to content

Commit 4d99405

Browse files
authored
Merge pull request #3 from MigHerCas/step3/wrap-up
Step3: Wrap up.
2 parents b19fab0 + ad023a1 commit 4d99405

20 files changed

+206
-509
lines changed

Diff for: package.json

+1-10
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,15 @@
1111
"lint": "tsc --noEmit && eslint --fix src/**/*.ts{,x}"
1212
},
1313
"dependencies": {
14-
"@testing-library/jest-dom": "^5.11.4",
15-
"@testing-library/react": "^11.1.0",
16-
"@testing-library/user-event": "^12.1.10",
17-
"@types/enzyme": "^3.10.8",
18-
"@types/enzyme-adapter-react-16": "^1.0.6",
19-
"@types/jest": "^26.0.15",
2014
"@types/node": "^12.0.0",
2115
"@types/react": "^16.9.53",
2216
"@types/react-dom": "^16.9.8",
23-
"enzyme": "^3.11.0",
24-
"enzyme-adapter-react-16": "^1.15.5",
2517
"node-sass": "4.14.1",
2618
"react": "^17.0.1",
2719
"react-dom": "^17.0.1",
2820
"react-scripts": "4.0.0",
2921
"react-test-renderer": "^17.0.1",
30-
"typescript": "^4.0.3",
31-
"web-vitals": "^0.2.4"
22+
"typescript": "^4.0.3"
3223
},
3324
"devDependencies": {
3425
"@typescript-eslint/eslint-plugin": "^4.7.0",

Diff for: src/App.tsx

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React, { useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { TrackItem } from './api/track';
33
import Grid from './components/layout/Grid';
44
import Header from './components/layout/Header';
55
import PlaylistsPanel from './components/PlaylistsPanel';
6+
import ErrorModal from './components/shared/ErrorModal';
67
import TracksPanel from './components/TracksPanel';
78
import useToken from './hooks/useToken';
89

@@ -12,12 +13,24 @@ const {
1213
} = process.env;
1314

1415
export default function App(): JSX.Element {
15-
const { accessToken } = useToken(
16+
const { accessToken, accessError } = useToken(
1617
REACT_APP_SPOTIFY_ACCOUNT_TOKEN_API_URL,
1718
REACT_APP_SPOTIFY_AUTH_HEADER
1819
);
1920
const [selectedPlaylist, setSelectedPlaylist] = useState<string>('');
2021
const [selectedTracks, setSelectedTracks] = useState<TrackItem[]>([]);
22+
const firstTrackRef = React.createRef<HTMLButtonElement>();
23+
24+
useEffect(() => {
25+
if (firstTrackRef.current) {
26+
// Added timeout to fit playlist item animation (300ms)
27+
setTimeout(() => firstTrackRef?.current?.focus(), 300);
28+
}
29+
}, [firstTrackRef]);
30+
31+
if (accessError) {
32+
return <ErrorModal error={accessError} />;
33+
}
2134

2235
return (
2336
<div className="App">
@@ -29,7 +42,7 @@ export default function App(): JSX.Element {
2942
setSelectedPlaylistId={setSelectedPlaylist}
3043
setSelectedTracks={setSelectedTracks}
3144
/>
32-
<TracksPanel tracks={selectedTracks} />
45+
<TracksPanel tracks={selectedTracks} firstTrackRef={firstTrackRef} />
3346
</Grid>
3447
</div>
3548
);

Diff for: src/api/error.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type ApiError = 'Access error' | 'Fetch error' | '';

Diff for: src/components/PlaylistItem.tsx renamed to src/components/PlaylistComponent.tsx

+24-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { Dispatch, SetStateAction } from 'react';
22
import { TrackItem } from '../api/track';
33
import { AccessToken } from '../api/user';
44
import usePlaylist from '../hooks/usePlaylist';
5+
import ErrorModal from './shared/ErrorModal';
56

67
interface Props {
78
playlistId: string;
@@ -18,10 +19,23 @@ export default function PlaylistComponent({
1819
setSelectedPlaylistId,
1920
setSelectedTracks,
2021
}: Props): JSX.Element {
21-
const { playlist } = usePlaylist(accessToken, playlistId);
22+
const { playlist, playlistLoading, playlistError } = usePlaylist(
23+
accessToken,
24+
playlistId
25+
);
26+
2227
const { id, name, description, images, collaborative, tracks } = playlist;
2328

29+
const handleClick = () => {
30+
setSelectedPlaylistId(id);
31+
setSelectedTracks(tracks.items);
32+
};
33+
2434
// If playlist data has been fetched
35+
if (playlistError) {
36+
return <ErrorModal error={playlistError} />;
37+
}
38+
2539
if (id !== '') {
2640
return (
2741
<li className="playlist__item">
@@ -31,24 +45,26 @@ export default function PlaylistComponent({
3145
className={`playlist__button padding-default radius--big ${
3246
selectedPlaylistId === id ? 'selected' : ''
3347
}`}
34-
onClick={() => {
35-
setSelectedPlaylistId(id);
36-
setSelectedTracks(tracks.items);
37-
}}
48+
onClick={handleClick}
3849
>
3950
<div className="playlist__image-wrapper">
4051
<img src={images[0].url} alt="Alt text" />
4152
</div>
4253
<div className="playlist__details">
43-
<h2 className="playlist__title">{name}</h2>
54+
<h2 className="playlist__title">
55+
{playlistLoading ? 'Loading...' : name}
56+
</h2>
4457
<h3 className="playlist__subtitle">
4558
{collaborative ? 'Collaborative' : 'Non collaborative'}
4659
</h3>
47-
<p className="playlist__description">{description}</p>
60+
<p className="playlist__description">
61+
{playlistLoading ? 'Loading...' : description}
62+
</p>
4863
</div>
4964
</button>
5065
</li>
5166
);
5267
}
53-
return <p>Loading</p>;
68+
69+
return <></>;
5470
}

Diff for: src/components/PlaylistsPanel.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { Dispatch, SetStateAction } from 'react';
22
import { TrackItem } from '../api/track';
33
import { AccessToken } from '../api/user';
44
import DEFAULT_PLAYLISTS_IDS from '../constants/constants';
5-
import PlaylistComponent from './PlaylistItem';
5+
import PlaylistComponent from './PlaylistComponent';
66

77
interface Props {
88
accessToken: AccessToken;

Diff for: src/components/TrackComponent.tsx

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
1-
import React from 'react';
1+
import React, { RefObject } from 'react';
22
import { Artist } from '../api/track';
33

44
interface Props {
55
trackName: string;
66
artists: Artist[];
7+
firstTrack: boolean;
8+
firstTrackRef: RefObject<HTMLButtonElement>;
79
}
810

11+
interface TrackButtonProps {
12+
children: React.ReactNode;
13+
}
14+
15+
type Ref = HTMLButtonElement;
16+
917
export default function TrackComponent({
1018
trackName,
1119
artists,
20+
firstTrack = false,
21+
firstTrackRef,
1222
}: Props): JSX.Element {
13-
return (
14-
<li className="track__item">
23+
const TrackButton = React.forwardRef<Ref, TrackButtonProps>(
24+
({ children }: TrackButtonProps, ref) => (
1525
<button
1626
type="button"
27+
ref={firstTrack ? ref : null}
1728
className="track__button padding-default radius--big"
1829
>
30+
{children}
31+
</button>
32+
)
33+
);
34+
35+
return (
36+
<li className="track__item">
37+
<TrackButton ref={firstTrackRef}>
1938
<h2 className="track__title">{trackName}</h2>
2039
{artists &&
2140
artists.map(({ id, name }) => (
2241
<h3 key={id} className="track__subtitle">
2342
{name}
2443
</h3>
2544
))}
26-
</button>
45+
</TrackButton>
2746
</li>
2847
);
2948
}

Diff for: src/components/TracksPanel.tsx

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1-
import React from 'react';
1+
import React, { RefObject } from 'react';
22
import { TrackItem } from '../api/track';
33
import TrackComponent from './TrackComponent';
44

55
interface Props {
66
tracks: TrackItem[];
7+
firstTrackRef: RefObject<HTMLButtonElement>;
78
}
89

9-
export default function TracksPanel({ tracks }: Props): JSX.Element {
10+
export default function TracksPanel({
11+
tracks,
12+
firstTrackRef,
13+
}: Props): JSX.Element {
1014
return (
1115
<section className="panel tracks-panel">
1216
<ol className="scrolling-wrapper padding-default shadow--dark radius--big">
1317
{tracks &&
14-
tracks.map((trackItem) => {
18+
tracks.map((trackItem, index) => {
1519
const { track } = trackItem;
1620

1721
return (
1822
<TrackComponent
1923
key={track.id}
2024
trackName={track.name}
2125
artists={track.artists}
26+
firstTrack={index === 0}
27+
firstTrackRef={firstTrackRef}
2228
/>
2329
);
2430
})}

Diff for: src/components/shared/ErrorModal.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { useEffect } from 'react';
2+
import { ApiError } from '../../api/error';
3+
4+
interface Props {
5+
error: ApiError;
6+
}
7+
export default function ErrorModal({ error }: Props): JSX.Element {
8+
let errorTitle;
9+
let errorDescription;
10+
11+
switch (error) {
12+
case 'Access error':
13+
errorTitle = 'Something failed while trying to authenticate';
14+
errorDescription =
15+
'This might be caused by a network error or a login issue, check it out!';
16+
break;
17+
case 'Fetch error':
18+
errorTitle = 'Something failed while trying to fetch a playlist.';
19+
errorDescription = 'You better checkout your requested playlists ids!';
20+
break;
21+
default:
22+
}
23+
24+
useEffect(() => {
25+
document.querySelector('body')?.classList.add('locked-by-modal');
26+
}, []);
27+
28+
return (
29+
<div className="error-modal">
30+
<h1 className="error-title">{errorTitle}</h1>
31+
<h2 className="error-description">{errorDescription}</h2>
32+
</div>
33+
);
34+
}

Diff for: src/hooks/usePlaylist.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useEffect, useState } from 'react';
2+
import { ApiError } from '../api/error';
23
import { PlaylistItem } from '../api/playlist';
34
import { AccessToken } from '../api/user';
45
import NullyPlaylist from '../utils';
56

67
type PlaylistHookReturn = {
78
playlist: PlaylistItem;
89
playlistLoading: boolean;
9-
playlistError: boolean;
10+
playlistError: ApiError;
1011
};
1112

1213
const { REACT_APP_SPOTIFY_PLAYLIST_URL } = process.env;
@@ -17,13 +18,13 @@ const usePlaylist = (
1718
): PlaylistHookReturn => {
1819
const [playlist, setPlaylist] = useState<PlaylistItem>(NullyPlaylist);
1920
const [playlistLoading, setIsLoading] = useState(false);
20-
const [playlistError, setIsError] = useState(false);
21+
const [playlistError, setIsError] = useState<ApiError>('');
2122

2223
useEffect(() => {
2324
const url = `${REACT_APP_SPOTIFY_PLAYLIST_URL}/${playlistId}`;
2425

2526
const fetchPlaylist = async (): Promise<void> => {
26-
setIsError(false);
27+
setIsError('');
2728
setIsLoading(true);
2829

2930
try {
@@ -35,6 +36,11 @@ const usePlaylist = (
3536
});
3637

3738
const apiResponse = await response;
39+
40+
if (!apiResponse.ok) {
41+
setIsError('Fetch error');
42+
}
43+
3844
const playlistItem: PlaylistItem = await apiResponse.json();
3945
setPlaylist(playlistItem);
4046
setIsLoading(false);

Diff for: src/hooks/useToken.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useEffect, useState } from 'react';
2+
import { ApiError } from '../api/error';
23
import { AccessToken, AccessTokenResponse } from '../api/user';
34

45
type UseTokenHookReturn = {
56
accessToken: AccessToken | undefined;
67
accessIsLoading: boolean;
7-
accessError: boolean;
8+
accessError: ApiError;
89
};
910

1011
const useToken = (
@@ -13,12 +14,12 @@ const useToken = (
1314
): UseTokenHookReturn => {
1415
const [accessToken, setAccessToken] = useState<AccessToken>();
1516
const [accessIsLoading, setAccessIsLoading] = useState(false);
16-
const [accessError, setAccessError] = useState(false);
17+
const [accessError, setAccessError] = useState<ApiError>('');
1718

1819
useEffect(() => {
1920
const url = `${REACT_APP_SPOTIFY_ACCOUNT_TOKEN_API_URL}`;
2021
const getAccessToken = async (): Promise<void> => {
21-
setAccessError(false);
22+
setAccessError('');
2223
setAccessIsLoading(true);
2324

2425
try {
@@ -36,7 +37,7 @@ const useToken = (
3637
setAccessToken(accessTokenResponse.access_token);
3738
setAccessIsLoading(false);
3839
} catch (error) {
39-
setAccessError(error);
40+
setAccessError('Access error');
4041
}
4142
};
4243

Diff for: src/scss/_base.scss

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ body {
2121
scroll-behavior: smooth;
2222
}
2323

24+
body.locked-by-modal {
25+
overflow: hidden;
26+
}
27+
2428
/*
2529
* Reset top margins on titles that are first-child.
2630
*/

0 commit comments

Comments
 (0)