diff --git a/src/App.css b/src/App.css index a64d7ea..89c2d86 100755 --- a/src/App.css +++ b/src/App.css @@ -1,4 +1,6 @@ .App { + display: flex; + flex-direction: column; text-align: center; } @@ -49,6 +51,14 @@ img { border: 1px solid #ccc; } +.favorite{ + cursor: pointer; + margin: 0.5rem auto; +} +.favorite:hover { + fill: rgb(250, 218, 148); +} + @keyframes spin { 0% { transform: rotate(0deg); @@ -65,4 +75,85 @@ img { to { transform: rotate(360deg); } + + +} + +.options-nav { + display: flex; +} + +/* Favorites list section */ +.favorites-list-section { + /* padding: 1rem; */ + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.favorite-item { + background-color: rgb(242, 242, 242); + border-radius: 2px; + margin: 1rem; + padding:1rem; + display: flex; + justify-content: space-between; } + +.favorite-item .action-container { + cursor: pointer; + display: flex; + justify-content: space-between; + /* height: 200px; */ +} + +.favorite-item .action-container > * { + margin: 5px; +} + +.favorite-item .img-container { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 150px; + height: 150px; +} + +.favorite-item .img-container > img { + max-height: 100%; + max-width: 100%; + object-position: center; + object-fit: cover; +} + +.favorite-item .action-container .informations { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; +} + +.favorite-item .informations > .title { + font-weight: bold; + font-size: 21px; +} + +.favorite-item .informations > .description { + text-overflow: ellipsis; + overflow: hidden; + /* CSS properties for ellipsis overflow after 5 lines of text */ + display: block; + display: -webkit-box; + -webkit-line-clamp: 5; + -webkit-box-orient: vertical; + max-height: 5 * 1.6rem; +} + +@media screen and (max-width: 600px) { + .favorite-item { + flex-direction: column; + } + + .favorite-item .action-container { + flex-direction: column; + } +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index cdfec73..0cf425f 100755 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Dialog, @@ -12,6 +12,9 @@ import "./App.css"; import EpisodeList from "./components/EpisodeList"; import UserForm from "./components/UserForm"; import LoadingStatus from "./components/LoadingStatus"; +import FavoriteDialog from "./components/FavoriteDialog"; +import { useRef } from "react"; +import SearchHistory from "./components/SearchHistory"; const App = ({ fetching }) => { const [fetched, setFetched] = useState({}); @@ -19,12 +22,15 @@ const App = ({ fetching }) => { const [previousFeeds, setPreviousFeeds] = useState([]); const [past, setPast] = useState(false); const [error, setError] = useState(false); + const [favoriteFeeds, setFavoriteFeeds] = useState([]); + + const favoritesPopUpRef = useRef(); const getFeed = (event) => { setFetching((prev) => !prev); if (event.preventDefault != null) event.preventDefault(); - const feed_url = event.target.elements.feed_url.value; + const feed_url = formatUrl(event.target.elements.feed_url.value); const Parser = require("rss-parser"); const parser = new Parser({ customFields: { @@ -43,6 +49,7 @@ const App = ({ fetching }) => { program_title: feed.title, program_image: feed.image.url, program_description: feed.description, + program_link: feed_url, }); setFetching((prev) => !prev); setPreviousFeeds([...new Set([...previousFeeds, feed_url])]); @@ -63,6 +70,20 @@ const App = ({ fetching }) => { } }; + // Formatting method for URLs to avoid having duplicate favorites in localstorage as the key used is the URL + const formatUrl = (url) => { + let urlSequence; + let finalUrl; + if (url.includes('://')) { + urlSequence = url.split('://')[1]; + finalUrl = 'https://' + urlSequence; + } else { + finalUrl = 'https://' + url; + } + + return finalUrl; + }; + const handleClose = () => { setFetching(false); setError(false); @@ -90,28 +111,53 @@ const App = ({ fetching }) => { ); + const isFavoriteSelected = (link) => { + return favoriteFeeds.some(el => el.program_link === link); + } + + const updateFavoritesFeeds = () => { + let favorites = []; + for (let [key, value] of Object.entries(localStorage)) { + if (key.startsWith('favorite-')) { + favorites.push(JSON.parse(value)); + } + } + setFavoriteFeeds(favorites); + } + + useEffect(() => { + updateFavoritesFeeds(); + }, []); + return (

quick-feed

setFetching(true)} - past={past} - previous_feeds={[...previousFeeds]} - /> + getFeed={getFeed} /> + + {past ? <> + + + :

Please enter an RSS feed

} + {error ? renderAlert() :
} - {!past ?

Please enter an RSS feed

:
} - + {/* Favorite feeds list dialog component */} +
); }; diff --git a/src/components/Episode.jsx b/src/components/Episode.jsx index aa79690..32bb374 100644 --- a/src/components/Episode.jsx +++ b/src/components/Episode.jsx @@ -32,27 +32,4 @@ const Episode = ({ link, title }) => { ); }; -// class Episode extends Component { -// divStyles = { -// width: "77vw", -// float: "right", -// marginRight: "1vw", -// }; -// render() { -// return ( -//
-// -// {this.props.title} -// -// -//

{this.props.title}

-//
-//
-// ); -// } -// } - export default Episode; diff --git a/src/components/EpisodeList.jsx b/src/components/EpisodeList.jsx index 280da89..3638930 100644 --- a/src/components/EpisodeList.jsx +++ b/src/components/EpisodeList.jsx @@ -1,63 +1,79 @@ -import React, { Component } from "react"; +import React from "react"; +import FavoriteButton from "./FavoriteButton"; import Episode from "./Episode"; +import { useState } from "react"; -class EpisodeList extends Component { - cardStyle = { - width: "20vw", - float: "left", - }; - render() { - const { +const EpisodeList = ({ + program_title, + program_description, + program_image, + episodes, + program_link, + updateFavorites, + isFavoriteSelected + }) => { + + // eslint-disable-next-line no-unused-vars + const [cardStyle, setCardStyle] = useState({width: "20vw", float: "left"}); + + const toggleFavorite = () => { + const feed_data = { program_title, program_description, program_image, - episodes, - } = this.props; + program_link, + }; + + isFavoriteSelected(program_link) ? localStorage.removeItem(`favorite-${program_link}`) : localStorage.setItem(`favorite-${program_link}`, JSON.stringify(feed_data)); + updateFavorites(); + } - return ( -
- {episodes ? ( -
- -
- {program_title} + {episodes ? ( +
+ +
+ {program_title} +
+
{program_title}
+ +
-
-
{program_title}
-
-
- {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..80f20cb --- /dev/null +++ b/src/components/FavoriteDialog.jsx @@ -0,0 +1,68 @@ +import React, { 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'; +const { forwardRef, useImperativeHandle } = React; + +const FavoriteDialog = forwardRef((props, ref) => { + + const [open, setOpen] = useState(false); + + const handleClose = () => { + setOpen(false); + } + const handleClickOpen = () => { + setOpen(true); + } + + useImperativeHandle(ref, () => { return { + handleClickOpen: handleClickOpen, + handleClose: handleClose + }}); + + return ( +
+ handleClose()} + scroll={'paper'} + maxWidth={'md'} + aria-labelledby="scroll-dialog-title" + > + Favorite feeds + +
+ + { + props.favoriteFeeds.length > 0 ? props.favoriteFeeds.map((item, i) => { + return ( + {handleClose()}} + key={i} index={i} />) + }) + : +
No favorites
+ } + +
+
+ + + +
+
+ ); +}); + + +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..de8d1c0 --- /dev/null +++ b/src/components/FavoriteItem.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import "../App.css"; +import FavoriteButton from "./FavoriteButton"; + +const FavoriteItem = ({item, index, closePopUp, updateFavorites, isFavoriteSelected, getFeed}) => { + + const removeFavorite = () => { + localStorage.removeItem(`favorite-${item.program_link}`); + updateFavorites(); + } + + const seeFavorite = (link) => { + getFeed({target: {elements: {feed_url: {value: link}}}}); + closePopUp(); + }; + + return ( +
+
seeFavorite(item.program_link)}> +
+ {item.program_title} +
+
+

{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..48afd04 100644 --- a/src/components/SearchHistory.jsx +++ b/src/components/SearchHistory.jsx @@ -9,7 +9,7 @@ export default function SearchHistory(props) { }; const handleClose = (event) => { - if (event.currentTarget.innerText != '') + if (event.currentTarget.innerText !== '') props.getFeed({target: {elements: {feed_url: {value: event.currentTarget.innerText}}}}); setAnchorEl(null); }; diff --git a/src/components/UserForm.jsx b/src/components/UserForm.jsx index 79bdbba..f0f1972 100644 --- a/src/components/UserForm.jsx +++ b/src/components/UserForm.jsx @@ -1,9 +1,8 @@ import React, { useState } from "react"; import Input from "@material-ui/core/Input"; import Button from "@material-ui/core/Button"; -import SearchHistory from "./SearchHistory"; -const UserForm = ({ getFeed, previous_feeds, past }) => { +const UserForm = ({ getFeed }) => { const [enabled, setEnabled] = useState(true); const [feeds, setFeeds] = useState([]); @@ -39,7 +38,6 @@ const UserForm = ({ getFeed, previous_feeds, past }) => { > Submit - {past ? :
}
);