Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track detail basic setup added #5

Merged
merged 7 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions app/components/TrackCard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import { Link } from 'react-router-dom';

const TrackCustomCard = styled(Card)`
&& {
Expand Down Expand Up @@ -70,6 +71,7 @@ const TrackPlayIcon = styled(PlayArrowIcon)`
export function TrackCard({
trackPlaying,
setTrackPlaying,
collectionId,
collectionName,
artistName,
shortDescription,
Expand Down Expand Up @@ -118,14 +120,16 @@ export function TrackCard({
</If>
</IconButton>
</If>
<Typography component="div" variant="h5">
<If
condition={!isEmpty(collectionName)}
otherwise={<T data-testid="collection_name_unavailable" id="collection_name_unavailable" />}
>
<T data-testid="collectionName" id="collection_name" values={{ collectionName: collectionName }} />
</If>
</Typography>
<Link to={`/tracks/${collectionId}`}>
<Typography component="div" variant="h5" color="text.secondary">
<If
condition={!isEmpty(collectionName)}
otherwise={<T data-testid="collection_name_unavailable" id="collection_name_unavailable" />}
>
<T data-testid="collectionName" id="collection_name" values={{ collectionName: collectionName }} />
</If>
</Typography>
</Link>
</TrackContentHeaderBox>
<Typography variant="subtitle1" color="text.secondary" component="div">
<If
Expand All @@ -152,6 +156,7 @@ export function TrackCard({
TrackCard.propTypes = {
trackPlaying: PropTypes.string,
setTrackPlaying: PropTypes.func,
collectionId: PropTypes.number,
artistName: PropTypes.string,
artworkUrl100: PropTypes.string,
collectionName: PropTypes.string,
Expand Down
20 changes: 12 additions & 8 deletions app/components/TrackCard/tests/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@ exports[`<TrackCard /> should render and match the snapshot 1`] = `
<div
class="e12nrugs2 MuiBox-root css-yhs9f2"
>
<div
class="MuiTypography-root MuiTypography-h5 css-ag7rrr-MuiTypography-root"
<a
href="/tracks/undefined"
>
<p
class="css-lh9t18 egtqi0h0"
data-testid="collection_name_unavailable"
<div
class="MuiTypography-root MuiTypography-h5 css-41an0y-MuiTypography-root"
>
Track name is unavailable
</p>
</div>
<p
class="css-lh9t18 egtqi0h0"
data-testid="collection_name_unavailable"
>
Track name is unavailable
</p>
</div>
</a>
</div>
<div
class="MuiTypography-root MuiTypography-subtitle1 css-ecpxie-MuiTypography-root"
Expand Down
25 changes: 21 additions & 4 deletions app/components/TrackCard/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,26 @@ import React from 'react';
import { renderWithIntl } from '@utils/testUtils';
import TrackCard from '../index';
import { translate } from '@app/utils/index';
import { BrowserRouter } from 'react-router-dom';

describe('<TrackCard />', () => {
it('should render and match the snapshot', () => {
const mockSetTrackPlaying = jest.fn();
const { baseElement } = renderWithIntl(<TrackCard setTrackPlaying={mockSetTrackPlaying} />);
const { baseElement } = renderWithIntl(
<BrowserRouter>
<TrackCard setTrackPlaying={mockSetTrackPlaying} />
</BrowserRouter>
);
expect(baseElement).toMatchSnapshot();
});

it('should contain 1 TrackCard component', () => {
const mockSetTrackPlaying = jest.fn();
const { getAllByTestId } = renderWithIntl(<TrackCard setTrackPlaying={mockSetTrackPlaying} />);
const { getAllByTestId } = renderWithIntl(
<BrowserRouter>
<TrackCard setTrackPlaying={mockSetTrackPlaying} />
</BrowserRouter>
);
expect(getAllByTestId('track-card').length).toBe(1);
});

Expand All @@ -31,7 +40,11 @@ describe('<TrackCard />', () => {
artworkUrl100: 'https://example.com/image.jpg',
previewUrl: 'https://example.com/preview.mp3'
};
const { getByTestId } = renderWithIntl(<TrackCard setTrackPlaying={mockSetTrackPlaying} {...trackData} />);
const { getByTestId } = renderWithIntl(
<BrowserRouter>
<TrackCard setTrackPlaying={mockSetTrackPlaying} {...trackData} />
</BrowserRouter>
);

expect(getByTestId('collectionName')).toHaveTextContent(trackData.collectionName);
expect(getByTestId('artistName')).toHaveTextContent(trackData.artistName);
Expand All @@ -44,7 +57,11 @@ describe('<TrackCard />', () => {
const artistNameUnavailable = translate('track_artist_name_unavailable');
const shortDescriptionUnavailable = translate('track_shortdesc_unavailable');

const { getByTestId } = renderWithIntl(<TrackCard setTrackPlaying={mockSetTrackPlaying} />);
const { getByTestId } = renderWithIntl(
<BrowserRouter>
<TrackCard setTrackPlaying={mockSetTrackPlaying} />
</BrowserRouter>
);
expect(getByTestId('collection_name_unavailable')).toHaveTextContent(collectionNameUnavailable);
expect(getByTestId('track_artist_name_unavailable')).toHaveTextContent(artistNameUnavailable);
expect(getByTestId('track_shortdesc_unavailable')).toHaveTextContent(shortDescriptionUnavailable);
Expand Down
9 changes: 9 additions & 0 deletions app/containers/TrackDetailContainer/Loadable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
*
* Asynchronously loads the component for TrackDetailContainer
*
*/

import loadable from 'utils/loadable';

export default loadable(() => import('./index'));
142 changes: 142 additions & 0 deletions app/containers/TrackDetailContainer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { memo, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { compose } from 'redux';
import { useParams } from 'react-router-dom';
import isEmpty from 'lodash/isEmpty';
import styled from '@emotion/styled';
import { trackDetailContainerCreators } from './reducer';
import { injectSaga } from 'redux-injectors';
import { Card, CardHeader, Container, Divider, Skeleton } from '@mui/material';
import TrackCard from '@components/TrackCard';
import If from '@components/If';
import T from '@components/T';
import get from 'lodash/get';
import trackDetailContainerSaga from './saga';
import { selectTrackDetailData, selectTrackError, selectTrackDetailLoading } from './selectors';
import { translate } from '@app/utils/index';

// Custom Styling
const CustomCard = styled(Card)`
&& {
margin: 1.25rem 0;
padding: 1rem;
max-width: ${(props) => props.maxwidth};
color: ${(props) => props.color};
${(props) => props.color && `color: ${props.color}`};
}
`;
const CustomCardHeader = styled(CardHeader)`
&& {
padding: 0;
}
`;

export const TrackDetailContainer = ({
trackDetailData,
trackError,
trackDetailLoading,
dispatchTrackDetail,
padding
}) => {
const { trackId } = useParams();

useEffect(() => {
dispatchTrackDetail(trackId);
}, []);

const renderSkeleton = () => {
return (
<>
<Skeleton data-testid="skeleton" animation="wave" variant="text" height={40} />
<Skeleton data-testid="skeleton" animation="wave" variant="text" height={40} />
<Skeleton data-testid="skeleton" animation="wave" variant="text" height={40} />
</>
);
};

const renderTrackDetail = () => {
const trackData = get(trackDetailData, 'results', [])[0];
const [trackPlaying, setTrackPlaying] = useState('');

return (
<>
<TrackCard trackPlaying={trackPlaying} setTrackPlaying={setTrackPlaying} {...trackData} />
<If condition={!isEmpty(trackPlaying)}>
<audio data-testid="audio" name="media" key={trackPlaying} autoPlay>
<source src={trackPlaying} />
</audio>
</If>
</>
);
};

const renderErrorState = () => {
let trackDetailError;
if (trackError) {
trackDetailError = trackError;
}
return (
!trackDetailLoading &&
trackDetailError && (
<CustomCard color={trackDetailError ? 'red' : 'grey'}>
<CustomCardHeader title={translate('track_list')} />
<Divider sx={{ mb: 1.25 }} light />
<If condition={trackDetailError} otherwise={<T data-testid="default-message" id={trackDetailError} />}>
<T data-testid="error-message" text={trackDetailError} />
</If>
</CustomCard>
)
);
};

return (
<Container padding={padding}>
<If condition={!trackDetailLoading} otherwise={renderSkeleton()}>
{renderTrackDetail()}
</If>
{renderErrorState()}
</Container>
);
};

TrackDetailContainer.propTypes = {
trackDetailData: PropTypes.shape({
resultCount: PropTypes.number,
results: PropTypes.array
}),
trackError: PropTypes.string,
trackDetailLoading: PropTypes.bool,
dispatchTrackDetail: PropTypes.func,
padding: PropTypes.number
};

TrackDetailContainer.defaultProps = {
trackDetailData: {},
trackError: null,
trackDetailLoading: false
};

const mapStateToProps = createStructuredSelector({
trackDetailData: selectTrackDetailData(),
trackError: selectTrackError(),
trackDetailLoading: selectTrackDetailLoading()
});

export function mapDispatchToProps(dispatch) {
const { requestGetTrackDetail } = trackDetailContainerCreators;
return {
dispatchTrackDetail: (trackId) => dispatch(requestGetTrackDetail(trackId))
};
}

const withConnect = connect(mapStateToProps, mapDispatchToProps);

export default compose(
withConnect,
memo,
injectSaga({ key: 'trackDetailContainer', saga: trackDetailContainerSaga })
)(TrackDetailContainer);

export const TrackDetailContainerTest = compose()(TrackDetailContainer);
36 changes: 36 additions & 0 deletions app/containers/TrackDetailContainer/reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
*
* TrackDetailContainer reducer
*
*/
import produce from 'immer';
import { createActions } from 'reduxsauce';
import get from 'lodash/get';

export const { Types: trackDetailContainerTypes, Creators: trackDetailContainerCreators } = createActions({
requestGetTrackDetail: ['trackId'],
successGetTrackDetail: ['data'],
failureGetTrackDetail: ['error']
});
export const initialState = { trackId: null, trackDetailData: {}, trackError: null, trackDetailLoading: false };

/* eslint-disable default-case, no-param-reassign */
export const trackDetailContainerReducer = (state = initialState, action) =>
produce(state, (draft) => {
switch (action.type) {
case trackDetailContainerTypes.REQUEST_GET_TRACK_DETAIL:
draft.trackId = action.trackId;
draft.trackDetailLoading = true;
break;
case trackDetailContainerTypes.SUCCESS_GET_TRACK_DETAIL:
draft.trackDetailData = action.data;
draft.trackDetailLoading = false;
break;
case trackDetailContainerTypes.FAILURE_GET_TRACK_DETAIL:
draft.trackError = get(action.error, 'message', 'something_went_wrong');
draft.trackDetailLoading = false;
break;
}
});

export default trackDetailContainerReducer;
19 changes: 19 additions & 0 deletions app/containers/TrackDetailContainer/saga.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { put, call, takeLatest } from 'redux-saga/effects';
import { getTrack } from '@services/trackApi';
import { trackDetailContainerTypes, trackDetailContainerCreators } from './reducer';

const { REQUEST_GET_TRACK_DETAIL } = trackDetailContainerTypes;
const { successGetTrackDetail, failureGetTrackDetail } = trackDetailContainerCreators;
export function* getTrackDetail(action) {
const response = yield call(getTrack, action.trackId);
const { data, ok } = response;
if (ok) {
yield put(successGetTrackDetail(data));
} else {
yield put(failureGetTrackDetail(data));
}
}
// Individual exports for testing
export default function* trackDetailContainerSaga() {
yield takeLatest(REQUEST_GET_TRACK_DETAIL, getTrackDetail);
}
26 changes: 26 additions & 0 deletions app/containers/TrackDetailContainer/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createSelector } from 'reselect';
import get from 'lodash/get';
import { initialState } from './reducer';

/**
* Direct selector to the trackDetailContainer state domain
*/

export const selectTrackDetailContainerDomain = (state) => state.trackDetailContainer || initialState;

/**
* Other specific selectors
*/

/**
* Default selector used by trackDetailContainer
*/

export const selectTrackDetailData = () =>
createSelector(selectTrackDetailContainerDomain, (substate) => get(substate, 'trackDetailData'));

export const selectTrackError = () =>
createSelector(selectTrackDetailContainerDomain, (substate) => get(substate, 'trackError'));

export const selectTrackDetailLoading = () =>
createSelector(selectTrackDetailContainerDomain, (substate) => get(substate, 'trackDetailLoading'));
Loading
Loading