Skip to content

Commit

Permalink
feat: develop card for songs and store api data in redux
Browse files Browse the repository at this point in the history
  • Loading branch information
Wednesday Solutions authored and Wednesday Solutions committed Oct 4, 2024
1 parent c02a957 commit 778aa5e
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 17 deletions.
71 changes: 71 additions & 0 deletions app/components/MediaItemCard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import styled from '@emotion/styled';
import { string } from 'prop-types';
import { If } from '../If/index';

const Card = styled.div`
border-style: solid;
border-width: 1px;
padding: 5px;
border-radius: 5px;
&:hover {
cursor: pointer;
background: #eaeaea;
}
&:active {
background: #cacaca;
}
`;

/**
* MediaItemCard component that displays information about a Media item (like a song).
* It shows the repository's name, full name, and star count.
*
* @date 01/03/2024 - 14:47:28
*
* @param {Object} props - The component props.
* @param {string} props.trackName - The name of the track.
* @param {string} props.collectionName - The name of the collection.
* @param {string} props.artistName - The name of the artist.
* @param {string} props.country - The name of the country the media is from
* @param {string} props.primaryGenreName - Genre name
* @param {string} props.thumbnailSrc - thumbnail of the media
* @returns {JSX.Element} The RepoCard component displaying the repository information.
*/
export function MediaItemCard({ trackName, collectionName, artistName, country, primaryGenreName, thumbnailSrc }) {
return (
<Card>
<If condition={thumbnailSrc}>
<img src={thumbnailSrc} alt="thumbnail" />
</If>
<If condition={trackName}>
<div>{trackName}</div>
</If>
<If condition={collectionName}>
<div>{collectionName}</div>
</If>
<If condition={artistName}>
<div>{artistName}</div>
</If>
<If condition={country}>
<div>{country}</div>
</If>
<If condition={primaryGenreName}>
<div>{primaryGenreName}</div>
</If>
</Card>
);
}

MediaItemCard.propTypes = {
trackName: string,
collectionName: string,
artistName: string,
country: string,
primaryGenreName: string,
thumbnailSrc: string
};

export default MediaItemCard;
123 changes: 107 additions & 16 deletions app/containers/SongsContainer/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import React from 'react';
import React, { memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import { injectSaga } from 'redux-injectors';
import styled from '@emotion/styled';
import { IconButton, InputAdornment, OutlinedInput } from '@mui/material';
import { Search as SearchIcon } from '@mui/icons-material';
import isEmpty from 'lodash/isEmpty';
import debounce from 'lodash/debounce';
import { translate } from '@app/utils';
import { getSongs } from '@app/services/songApi';
import { selectLoading, selectSongName, selectSongsData, selectSongsError } from './selectors';
import { songsContainerCreators } from './reducer';
import songsContainerSaga from './saga';
import { If } from '@app/components/If/index';
import { For } from '@app/components/For/index';
import { MediaItemCard } from '@app/components/MediaItemCard/index';

const StyledOutlinedInput = styled(OutlinedInput)`
legend {
Expand All @@ -16,37 +26,56 @@ const StyledOutlinedInput = styled(OutlinedInput)`
}
`;

const Grid = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
`;

/**
* SongsContainer component is responsible for fetching and displaying song data fetched from itunes
* @returns {JSX.Element} The SongsContainer component
*/
export default function SongsContainer() {
const handleOnChange = async (songName) => {
if (!isEmpty(songName)) {
// searchRepos(songName);
// console.log('You are looking for ', songName);
try {
await getSongs(songName);
// console.log('itunes res = ', res);
} catch (error) {
// console.error('Itunes api error = ', error);
}
export function SongsContainer({
dispatchRequestGetItuneSongs,
dispatchClearItuneSongs,
intl,
songsData,
songsError,
songName,
history,
loading
}) {
useEffect(() => {
if (songName && songsData?.results?.length === 0) {
dispatchRequestGetItuneSongs(songName);
}
}, []);

const searchSongName = (sName) => {
dispatchRequestGetItuneSongs(sName);
};

const handleOnChange = async (sName) => {
if (!isEmpty(sName)) {
searchSongName(sName);
} else {
// dispatchClearGithubRepos();
dispatchClearItuneSongs();
}
};

const debouncedHandleOnChange = debounce(handleOnChange, 200);

const handleOnSearchIconClick = () => {
// console.log('Search icon clicked');
searchSongName(songName);
};
return (
<>
<StyledOutlinedInput
inputProps={{ 'data-testid': 'search-bar' }}
onChange={(event) => debouncedHandleOnChange(event.target.value)}
fullWidth
defaultValue={''}
defaultValue={songName}
placeholder={translate('songs_search_input_placeholder')}
endAdornment={
<InputAdornment position="end">
Expand All @@ -61,6 +90,68 @@ export default function SongsContainer() {
</InputAdornment>
}
/>
<If condition={songsData?.results?.length > 0}>
<For
of={songsData.results}
ParentComponent={Grid}
renderItem={(item, index) => {
return (
<MediaItemCard
key={index}
trackName={item?.trackName}
collectionName={item?.collectionName}
artistName={item?.artistName}
country={item?.country}
primaryGenreName={item?.primaryGenreName}
thumbnailSrc={item?.artworkUrl100}
/>
);
}}
/>
</If>
</>
);
}

SongsContainer.propTypes = {
dispatchRequestGetItuneSongs: PropTypes.func,
dispatchClearItuneSongs: PropTypes.func,
intl: PropTypes.object,
songsData: PropTypes.shape({
resultCount: PropTypes.number,
results: PropTypes.array
}),
songsError: PropTypes.string,
songName: PropTypes.string,
history: PropTypes.object,
loading: PropTypes.bool
};

SongsContainer.defaultProps = {
songsData: {},
songsError: null
};

const mapStateToProps = createStructuredSelector({
loading: selectLoading(),
songsData: selectSongsData(),
songsError: selectSongsError(),
songName: selectSongName()
});

// eslint-disable-next-line require-jsdoc
export function mapDispatchToProps(dispatch) {
const { requestGetItuneSongs, clearItuneSongs } = songsContainerCreators;
return {
dispatchRequestGetItuneSongs: (songName) => dispatch(requestGetItuneSongs(songName)),
dispatchClearItuneSongs: () => dispatch(clearItuneSongs())
};
}

const withConnect = connect(mapStateToProps, mapDispatchToProps);

export default compose(
withConnect,
memo,
injectSaga({ key: 'songsContainer', saga: songsContainerSaga })
)(SongsContainer);
44 changes: 44 additions & 0 deletions app/containers/SongsContainer/reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
*
* HomeContainer reducer
*
*/
import { produce } from 'immer';
import { createActions } from 'reduxsauce';
import get from 'lodash/get';

export const { Types: songsContainerTypes, Creators: songsContainerCreators } = createActions({
requestGetItuneSongs: ['songName'],
successGetItuneSongs: ['data'],
failureGetItuneSongs: ['error'],
clearItuneSongs: {}
});
export const initialState = { songName: null, songsData: {}, songsError: null, loading: null };

export const songsContainerReducer = (state = initialState, action) =>
produce(state, (draft) => {
switch (action.type) {
case songsContainerTypes.REQUEST_GET_ITUNE_SONGS:
draft.songName = action.songName;
draft.loading = true;
break;
case songsContainerTypes.CLEAR_ITUNE_SONGS:
draft.songName = null;
draft.songsError = null;
draft.songsData = {};
draft.loading = null;
break;
case songsContainerTypes.SUCCESS_GET_ITUNE_SONGS:
draft.songsData = action.data;
draft.songsError = null;
draft.loading = false;
break;
case songsContainerTypes.FAILURE_GET_ITUNE_SONGS:
draft.songsError = get(action.error, 'message', 'something_went_wrong');
draft.songsData = null;
draft.loading = false;
break;
}
});

export default songsContainerReducer;
37 changes: 37 additions & 0 deletions app/containers/SongsContainer/saga.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { put, call, takeLatest } from 'redux-saga/effects';
import { songsContainerCreators, songsContainerTypes } from './reducer';
import { getSongs } from '@services/songApi';

const { REQUEST_GET_ITUNE_SONGS } = songsContainerTypes;
const { successGetItuneSongs, failureGetItuneSongs } = songsContainerCreators;

/**
* A saga that handles fetching GitHub repositories based on a given repository name.
* On success, it dispatches a success action with the fetched data.
* On failure, it dispatches a failure action with the error data.
*
* @date 01/03/2024 - 14:47:28
*
* @param {Object} action - The action object containing the repository name.
* @yields {Effect} The effect of calling the API, and then either the success or failure action.
*/
export function* getItuneSongs(action) {
const response = yield call(getSongs, action.songName);
const { data, ok } = response;
if (ok) {
yield put(successGetItuneSongs(data));
} else {
yield put(failureGetItuneSongs(data));
}
}

/**
* registering events
* @date 04/03/2024 - 12:57:49
*
* @export
* @returns {{}}
*/
export default function* songsContainerSaga() {
yield takeLatest(REQUEST_GET_ITUNE_SONGS, getItuneSongs);
}
27 changes: 27 additions & 0 deletions app/containers/SongsContainer/selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createSelector } from 'reselect';
import get from 'lodash/get';
import { initialState } from './reducer';

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

export const selectSongsContainerDomain = (state) => state.songsContainer || initialState;

/**
* Other specific selectors
*/

/**
* Default selector used by HomeContainer
*/

export const selectSongsData = () =>
createSelector(selectSongsContainerDomain, (substate) => get(substate, 'songsData'));

export const selectLoading = () => createSelector(selectSongsContainerDomain, (substate) => get(substate, 'loading'));

export const selectSongsError = () =>
createSelector(selectSongsContainerDomain, (substate) => get(substate, 'songsError'));

export const selectSongName = () => createSelector(selectSongsContainerDomain, (substate) => get(substate, 'songName'));
4 changes: 3 additions & 1 deletion app/create-root-reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { combineReducers } from 'redux';
import LanguageProviderReducer from '@containers/LanguageProvider/reducer';
import HomeContainerReducer from '@containers/HomeContainer/reducer';
import SongsContainerReducer from './containers/SongsContainer/reducer';

/**
* Merges the main reducer with the router state and dynamically injected reducers
Expand All @@ -13,6 +14,7 @@ export default function createRootReducer(injectedReducer = {}) {
return combineReducers({
...injectedReducer,
language: LanguageProviderReducer,
homeContainer: HomeContainerReducer
homeContainer: HomeContainerReducer,
songsContainer: SongsContainerReducer
});
}

0 comments on commit 778aa5e

Please sign in to comment.