diff --git a/.env b/.env
index c46b54b4..286a22a0 100644
--- a/.env
+++ b/.env
@@ -1 +1,2 @@
-GITHUB_URL=https://api.github.com/
\ No newline at end of file
+GITHUB_URL=https://api.github.com/
+ITUNES_URL=https://itunes.apple.com/
\ No newline at end of file
diff --git a/.env.development b/.env.development
index c46b54b4..286a22a0 100644
--- a/.env.development
+++ b/.env.development
@@ -1 +1,2 @@
-GITHUB_URL=https://api.github.com/
\ No newline at end of file
+GITHUB_URL=https://api.github.com/
+ITUNES_URL=https://itunes.apple.com/
\ No newline at end of file
diff --git a/.env.local b/.env.local
index a0152a72..36f757bf 100644
--- a/.env.local
+++ b/.env.local
@@ -1,2 +1,3 @@
GITHUB_URL=https://api.github.com/
-IS_LOCAL=true
\ No newline at end of file
+IS_LOCAL=true
+ITUNES_URL=https://itunes.apple.com/
\ No newline at end of file
diff --git a/app/components/MediaItemCard/index.js b/app/components/MediaItemCard/index.js
new file mode 100644
index 00000000..5b767844
--- /dev/null
+++ b/app/components/MediaItemCard/index.js
@@ -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 (
+
+
+
+
+
+ {trackName}
+
+
+ {collectionName}
+
+
+ {artistName}
+
+
+ {country}
+
+
+ {primaryGenreName}
+
+
+ );
+}
+
+MediaItemCard.propTypes = {
+ trackName: string,
+ collectionName: string,
+ artistName: string,
+ country: string,
+ primaryGenreName: string,
+ thumbnailSrc: string
+};
+
+export default MediaItemCard;
diff --git a/app/containers/SongsContainer/index.js b/app/containers/SongsContainer/index.js
new file mode 100644
index 00000000..691f337f
--- /dev/null
+++ b/app/containers/SongsContainer/index.js
@@ -0,0 +1,157 @@
+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 { 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 {
+ display: none;
+ }
+ > fieldset {
+ top: 0;
+ }
+`;
+
+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 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 {
+ dispatchClearItuneSongs();
+ }
+ };
+
+ const debouncedHandleOnChange = debounce(handleOnChange, 200);
+
+ const handleOnSearchIconClick = () => {
+ searchSongName(songName);
+ };
+ return (
+ <>
+ debouncedHandleOnChange(event.target.value)}
+ fullWidth
+ defaultValue={songName}
+ placeholder={translate('songs_search_input_placeholder')}
+ endAdornment={
+
+
+
+
+
+ }
+ />
+ 0}>
+ {
+ return (
+
+ );
+ }}
+ />
+
+ >
+ );
+}
+
+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);
diff --git a/app/containers/SongsContainer/loadable.js b/app/containers/SongsContainer/loadable.js
new file mode 100644
index 00000000..c0cba514
--- /dev/null
+++ b/app/containers/SongsContainer/loadable.js
@@ -0,0 +1,7 @@
+/**
+ * Asynchronously loads the component for NotFoundPage
+ */
+
+import loadable from '@utils/loadable';
+
+export default loadable(() => import('./index'));
diff --git a/app/containers/SongsContainer/reducer.js b/app/containers/SongsContainer/reducer.js
new file mode 100644
index 00000000..ceb5a478
--- /dev/null
+++ b/app/containers/SongsContainer/reducer.js
@@ -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;
diff --git a/app/containers/SongsContainer/saga.js b/app/containers/SongsContainer/saga.js
new file mode 100644
index 00000000..bc47bdb7
--- /dev/null
+++ b/app/containers/SongsContainer/saga.js
@@ -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);
+}
diff --git a/app/containers/SongsContainer/selectors.js b/app/containers/SongsContainer/selectors.js
new file mode 100644
index 00000000..5fd4be1b
--- /dev/null
+++ b/app/containers/SongsContainer/selectors.js
@@ -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'));
diff --git a/app/create-root-reducer.js b/app/create-root-reducer.js
index 6d8d1ec6..51b676c3 100644
--- a/app/create-root-reducer.js
+++ b/app/create-root-reducer.js
@@ -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
@@ -13,6 +14,7 @@ export default function createRootReducer(injectedReducer = {}) {
return combineReducers({
...injectedReducer,
language: LanguageProviderReducer,
- homeContainer: HomeContainerReducer
+ homeContainer: HomeContainerReducer,
+ songsContainer: SongsContainerReducer
});
}
diff --git a/app/routeConfig.js b/app/routeConfig.js
index 77ae319f..4a83833f 100644
--- a/app/routeConfig.js
+++ b/app/routeConfig.js
@@ -1,12 +1,18 @@
import routeConstants from '@utils/routeConstants';
import NotFound from '@app/containers/NotFoundPage/loadable';
import HomeContainer from '@app/containers/HomeContainer/loadable';
+import SongsContainer from './containers/SongsContainer/loadable';
export const routeConfig = {
repos: {
component: HomeContainer,
...routeConstants.repos
},
+ songs: {
+ component: SongsContainer,
+ route: '/songs',
+ exact: true
+ },
notFoundPage: {
component: NotFound,
route: '/'
diff --git a/app/services/songApi.js b/app/services/songApi.js
new file mode 100644
index 00000000..3697aa64
--- /dev/null
+++ b/app/services/songApi.js
@@ -0,0 +1,7 @@
+import { generateApiClient } from '@utils/apiUtils';
+const songApi = generateApiClient('itunes');
+
+/**
+ * @see https://github.com/elbywan/wretch?tab=readme-ov-file#http-methods-
+ */
+export const getSongs = (songName) => songApi.get(`search?term=${songName}`);
diff --git a/app/translations/en.json b/app/translations/en.json
index 8769e471..062c2b67 100644
--- a/app/translations/en.json
+++ b/app/translations/en.json
@@ -15,5 +15,6 @@
"something_went_wrong": "Sorry. Something went wrong! Please try again in sometime.",
"stories": "Go to Storybook",
"wednesday_solutions": "Wednesday Solutions",
- "default_template": "wednesday-solutions/react-template"
+ "default_template": "wednesday-solutions/react-template",
+ "songs_search_input_placeholder": "Please enter a song name"
}
diff --git a/app/utils/apiUtils.js b/app/utils/apiUtils.js
index 2bdcdce4..bb74cf85 100644
--- a/app/utils/apiUtils.js
+++ b/app/utils/apiUtils.js
@@ -5,10 +5,12 @@ import { mapKeysDeep } from './index';
const API_TYPES = {
GITHUB: 'github',
+ ITUNES: 'itunes',
DEFAULT: 'default'
};
const apiClients = {
[API_TYPES.GITHUB]: null,
+ [API_TYPES.ITUNES]: null,
[API_TYPES.DEFAULT]: null
};
@@ -39,6 +41,12 @@ export const generateApiClient = (type = 'github') => {
apiClients[type] = createApiClientWithTransForm(process.env.GITHUB_URL);
return apiClients[type];
}
+ if (type === API_TYPES.ITUNES) {
+ // store this value for time to come
+ // eslint-disable-next-line immutable/no-mutation
+ apiClients[type] = createApiClientWithTransForm(process.env.ITUNES_URL);
+ return apiClients[type];
+ }
// store this value for time to come
// eslint-disable-next-line immutable/no-mutation
apiClients.default = createApiClientWithTransForm(process.env.GITHUB_URL);
diff --git a/internals/webpack/webpack.config.base.js b/internals/webpack/webpack.config.base.js
index 77edac2c..09ef8fb3 100644
--- a/internals/webpack/webpack.config.base.js
+++ b/internals/webpack/webpack.config.base.js
@@ -142,6 +142,7 @@ module.exports = (options) => ({
// optimizationLevel: 7
},
pngquant: {
+ enabled: false,
quality: [0.65, 0.9],
speed: 4
},