-
-
-
![{program_title}]({program_image})
{
+ return state.favoriteFeeds.some(el => el.program_link === program_link);
+ }
+
+ return (
+
+ {episodes ? (
+
+
+
+
![{program_title}]({program_image})
+
- {episodes.map((episode, i) => (
-
- ))}
- ) : (
-
- )}
-
- );
- }
+ {episodes.map((episode, i) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
}
export default EpisodeList;
diff --git a/src/components/FavoriteButton.jsx b/src/components/FavoriteButton.jsx
new file mode 100644
index 0000000..76a4574
--- /dev/null
+++ b/src/components/FavoriteButton.jsx
@@ -0,0 +1,43 @@
+import React from "react";
+import SvgIcon from '@material-ui/core/SvgIcon';
+import amber from '@material-ui/core/colors/amber';
+import grey from '@material-ui/core/colors/grey';
+import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
+
+const FavoriteButton = ({selected, onClickAction}) => {
+
+ const theme = createMuiTheme({
+ palette: {
+ primary: {
+ light: amber[100],
+ main: amber[500],
+ dark: amber[800],
+ contrastText: '#fff',
+ },
+ secondary: {
+ light: grey[100],
+ main: grey[400],
+ dark: grey[800],
+ contrastText: '#000',
+ }
+ },
+ });
+
+ const StarIcon = (props) => {
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default FavoriteButton;
\ No newline at end of file
diff --git a/src/components/FavoriteDialog.jsx b/src/components/FavoriteDialog.jsx
new file mode 100644
index 0000000..46dc444
--- /dev/null
+++ b/src/components/FavoriteDialog.jsx
@@ -0,0 +1,68 @@
+import React, { useContext, useState } from 'react';
+import Button from '@material-ui/core/Button';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogTitle from '@material-ui/core/DialogTitle';
+import "../App.css";
+import FavoriteItem from './FavoriteItem';
+import { Context } from '../context/Context';
+const { forwardRef, useImperativeHandle } = React;
+
+const FavoriteDialog = forwardRef((props, ref) => {
+
+ const {state} = useContext(Context);
+
+ const [open, setOpen] = useState(false);
+
+ const handleClose = () => {
+ setOpen(false);
+ }
+ const handleClickOpen = () => {
+ setOpen(true);
+ }
+
+ useImperativeHandle(ref, () => { return {
+ handleClickOpen: handleClickOpen,
+ handleClose: handleClose
+ }});
+
+ return (
+
+
+
+ );
+});
+
+
+export default FavoriteDialog;
\ No newline at end of file
diff --git a/src/components/FavoriteItem.jsx b/src/components/FavoriteItem.jsx
new file mode 100644
index 0000000..a1625ff
--- /dev/null
+++ b/src/components/FavoriteItem.jsx
@@ -0,0 +1,61 @@
+import React, { useContext } from "react";
+import "../App.css";
+import { Context } from "../context/Context";
+import { getFeed } from "./utils/httpRequests";
+import FavoriteButton from "./FavoriteButton";
+
+const FavoriteItem = ({item, index, closePopUp}) => {
+
+ const { state, dispatch } = useContext(Context);
+
+ const removeFavorite = () => {
+ if (isFavoriteSelected) {
+ dispatch({type: 'REMOVE_FAVORITE_FEED', payload: item });
+ }
+ }
+
+ const seeFavorite = (link) => {
+ fetchFeed({target: {elements: {feed_url: {value: link}}}});
+ closePopUp();
+ };
+
+ const fetchFeed = async (event) => {
+ dispatch({ type: 'SET_FETCHING', payload: true });
+ try {
+ const feed = await getFeed(event);
+ dispatch({type: 'SET_CURRENT_FEED', payload: feed})
+ dispatch({ type: 'SET_ERROR', payload: false });
+ return
+ } catch (error) {
+ dispatch({ type: 'SET_ERROR', payload: true });
+ return error;
+ } finally {
+ dispatch({ type: 'SET_FETCHING', payload: false });
+ }
+ }
+
+ const isFavoriteSelected = () => {
+ return state.favoriteFeeds.some(el => el.program_link === item.program_link);
+ }
+
+ return (
+
+
seeFavorite(item.program_link)}>
+
+
![{item.program_title}]({item.program_image})
+
+
+
{item.program_title}
+
{item.program_description}
+
+
+
+
+ )
+}
+
+export default FavoriteItem;
\ No newline at end of file
diff --git a/src/components/SearchHistory.jsx b/src/components/SearchHistory.jsx
index 1856814..73dd42f 100644
--- a/src/components/SearchHistory.jsx
+++ b/src/components/SearchHistory.jsx
@@ -1,7 +1,11 @@
-import React from "react";
+import React, { useContext } from "react";
import { Button, Menu, MenuItem } from "@material-ui/core";
import "../App.css";
-export default function SearchHistory(props) {
+import { Context } from "../context/Context";
+import { getFeed } from "./utils/httpRequests";
+
+export default function SearchHistory() {
+ const { state, dispatch } = useContext(Context);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
@@ -9,8 +13,8 @@ export default function SearchHistory(props) {
};
const handleClose = (event) => {
- if (event.currentTarget.innerText != '')
- props.getFeed({target: {elements: {feed_url: {value: event.currentTarget.innerText}}}});
+ if (event.currentTarget.innerText !== '')
+ fetchFeed({target: {elements: {feed_url: {value: event.currentTarget.innerText}}}});
setAnchorEl(null);
};
@@ -23,9 +27,24 @@ export default function SearchHistory(props) {
};
const renderMenuItems = () => {
- return
{props.history.map(renderItem)}
;
+ return
{state.previousFeeds.map(renderItem)}
;
};
+ const fetchFeed = async (event) => {
+ dispatch({ type: 'SET_FETCHING', payload: true });
+ try {
+ const feed = await getFeed(event);
+ dispatch({type: 'SET_CURRENT_FEED', payload: feed});
+ dispatch({ type: 'SET_ERROR', payload: false });
+ return
+ } catch (error) {
+ dispatch({ type: 'SET_ERROR', payload: true });
+ return error;
+ } finally {
+ dispatch({ type: 'SET_FETCHING', payload: false });
+ }
+ }
+
return (
);
diff --git a/src/components/UserForm.jsx b/src/components/UserForm.jsx
index 79bdbba..b99c21f 100644
--- a/src/components/UserForm.jsx
+++ b/src/components/UserForm.jsx
@@ -1,9 +1,11 @@
-import React, { useState } from "react";
+import React, { useContext, useState } from "react";
import Input from "@material-ui/core/Input";
import Button from "@material-ui/core/Button";
-import SearchHistory from "./SearchHistory";
+import { Context } from "../context/Context";
+import { getFeed } from "./utils/httpRequests";
-const UserForm = ({ getFeed, previous_feeds, past }) => {
+const UserForm = () => {
+ const { dispatch } = useContext(Context);
const [enabled, setEnabled] = useState(true);
const [feeds, setFeeds] = useState([]);
@@ -16,9 +18,24 @@ const UserForm = ({ getFeed, previous_feeds, past }) => {
setFeeds([...feeds, value]);
};
+ const fetchFeed = async (event) => {
+ dispatch({ type: 'SET_FETCHING', payload: true });
+ try {
+ const feed = await getFeed(event);
+ dispatch({type: 'SET_CURRENT_FEED', payload: feed})
+ dispatch({ type: 'SET_ERROR', payload: false });
+ return
+ } catch (error) {
+ dispatch({ type: 'SET_ERROR', payload: true });
+ return error;
+ } finally {
+ dispatch({ type: 'SET_FETCHING', payload: false });
+ }
+ }
+
return (
);
diff --git a/src/components/utils/formatUtils.js b/src/components/utils/formatUtils.js
new file mode 100644
index 0000000..f62a7e0
--- /dev/null
+++ b/src/components/utils/formatUtils.js
@@ -0,0 +1,13 @@
+// Formatting method for URLs to avoid having duplicate favorites in localstorage as the key used is the URL
+export const formatUrl = (url) => {
+ let urlSequence;
+ let finalUrl;
+ if (url.includes('://')) {
+ urlSequence = url.split('://')[1];
+ finalUrl = 'https://' + urlSequence;
+ } else {
+ finalUrl = 'https://' + url;
+ }
+
+ return finalUrl;
+};
\ No newline at end of file
diff --git a/src/components/utils/httpRequests.js b/src/components/utils/httpRequests.js
new file mode 100644
index 0000000..80fa11e
--- /dev/null
+++ b/src/components/utils/httpRequests.js
@@ -0,0 +1,18 @@
+import { formatUrl } from "./formatUtils";
+
+export const getFeed = async (event) => {
+
+ if (event.preventDefault != null)
+ event.preventDefault();
+ const feed_url = formatUrl(event.target.elements.feed_url.value);
+ const Parser = require("rss-parser");
+ const parser = new Parser({
+ customFields: {
+ item: [["enclosure", { keepArray: true }]],
+ },
+ });
+
+ const CORS_PROXY = "https://cors-anywhere.herokuapp.com/";
+
+ return await parser.parseURL(CORS_PROXY + feed_url);
+};
\ No newline at end of file
diff --git a/src/context/Context.js b/src/context/Context.js
new file mode 100644
index 0000000..3d1a975
--- /dev/null
+++ b/src/context/Context.js
@@ -0,0 +1,46 @@
+import React, { useReducer, createContext } from "react";
+import { feedReducer } from "./reducers/FeedReducer";
+
+const initFavorites = () => {
+ let favorites = [];
+ for (let [key, value] of Object.entries(localStorage)) {
+ if (key.startsWith('favorite-')) {
+ favorites.push(JSON.parse(value));
+ }
+ }
+ return favorites;
+}
+
+// Initial state
+const initialState = {
+ currentFeed: {
+ episodes: [],
+ program_title: '',
+ program_image: '',
+ program_description: '',
+ program_link: ''
+ },
+ onFetching: false,
+ previousFeeds: [],
+ error: false,
+ favoriteFeeds: initFavorites()
+};
+
+// Create context
+const Context = createContext({});
+
+// Combine reducer function
+const combineReducers = (...reducers) => (state, action) => {
+ for (let i = 0; i < reducers.length; i++) state = reducers[i](state, action);
+ return state;
+};
+
+// Context provider
+const Provider = ({ children }) => {
+ const [state, dispatch] = useReducer(combineReducers(feedReducer), initialState);
+ const value = { state, dispatch };
+
+ return
{children};
+};
+
+export { Context, Provider };
\ No newline at end of file
diff --git a/src/context/reducers/FeedReducer.js b/src/context/reducers/FeedReducer.js
new file mode 100644
index 0000000..affa462
--- /dev/null
+++ b/src/context/reducers/FeedReducer.js
@@ -0,0 +1,26 @@
+export function feedReducer(state, action) {
+ switch (action.type) {
+ case "SET_CURRENT_FEED":
+ // Update current feed and previous feed at the same time.
+ return { ...state, currentFeed: {
+ episodes: action.payload.items,
+ program_title: action.payload.title,
+ program_image: action.payload.image.url,
+ program_description: action.payload.description,
+ program_link: action.payload.feedUrl
+ }, previousFeeds: [...new Set([...state.previousFeeds, action.payload.feedUrl])]
+ };
+ case "SET_FETCHING":
+ return { ...state, onFetching: action.payload };
+ case "SET_ERROR":
+ return { ...state, error: action.payload };
+ case "ADD_FAVORITE_FEED":
+ localStorage.setItem(`favorite-${action.payload.program_link}`, JSON.stringify(action.payload));
+ return { ...state, favoriteFeeds: [...state.favoriteFeeds, action.payload] };
+ case "REMOVE_FAVORITE_FEED":
+ localStorage.removeItem(`favorite-${action.payload.program_link}`);
+ return { ...state, favoriteFeeds: state.favoriteFeeds.filter(element => element.program_link !== action.payload.program_link) };
+ default:
+ return state;
+ }
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 0c5e75d..2ed2936 100755
--- a/src/index.js
+++ b/src/index.js
@@ -3,8 +3,13 @@ import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
+import { Provider } from './context/Context';
-ReactDOM.render(
, document.getElementById('root'));
+ReactDOM.render(
+
+
+ ,
+document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.