diff --git a/frontend/src/components/Pages/Home/index.js b/frontend/src/components/Pages/Home/index.js index 890a0aa..0b198ac 100644 --- a/frontend/src/components/Pages/Home/index.js +++ b/frontend/src/components/Pages/Home/index.js @@ -5,6 +5,7 @@ import { themes, animations, layouts, fonts, colorValues, quoteTypes } from '../ import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import ContributorsCard from '../../ContributorsCard/ContributorCard' +import useQuoteAuthors from '../../../util/authors'; import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles({ @@ -24,6 +25,9 @@ const Home = () => { const [bgColor, setBgColor] = useState(null); const [borderColor, setBorderColor] = useState(null); const [quoteType, setQuoteType] = useState("random"); + const [quoteAuthor, setQuoteAuthor] = useState(null); + + const { quoteAuthors, loadingQuoteAuthors } = useQuoteAuthors(); const classes = useStyles(); @@ -158,11 +162,25 @@ const Home = () => { /> + + { + setQuoteAuthor(newValue) + }} + renderInput={(params) => } + /> + + - + Other layouts @@ -171,7 +189,7 @@ const Home = () => { layouts.filter((item) => item !== layout).map((restLayout) => { return ( - + ) }) diff --git a/frontend/src/components/organisms/TemplateCard/index.js b/frontend/src/components/organisms/TemplateCard/index.js index 6e2fe0a..12c5171 100644 --- a/frontend/src/components/organisms/TemplateCard/index.js +++ b/frontend/src/components/organisms/TemplateCard/index.js @@ -21,11 +21,12 @@ const TemplateCard = (props) => { const [snackbarMessage, setSnackbarMessage] = useState(""); const [isImageLoaded, setImageLoaded] = useState(false); const originUrl = serverUrl; // Note: PORT 3004 since in server is served via that port. Frontend independently served on port 3000 + const author = "Open Source"; const template = new Template(); const data = { quote: "This is going to be the Github quote for your README", - author: "Open Source", + author: props.quoteAuthor ?? author, }; const theme = { ...mainThemes[props.theme] }; @@ -78,6 +79,7 @@ const TemplateCard = (props) => { ...(props.bgColor && { bgColor: props.bgColor }), ...(props.fontColor && { fontColor: props.fontColor }), ...(isLayoutDefault && props.borderColor && { borderColor }), + ...(props.quoteAuthor && { author: props.quoteAuthor }), }); const quoteUrl = `${originUrl}/quote?${params.toString()}`; diff --git a/frontend/src/util/authors/index.js b/frontend/src/util/authors/index.js new file mode 100644 index 0000000..23e472f --- /dev/null +++ b/frontend/src/util/authors/index.js @@ -0,0 +1,39 @@ +import { useState, useCallback, useEffect } from "react"; + +import { serverUrl } from "../../components/Constants/urlConfig"; + +const useQuoteAuthors = () => { + const originUrl = serverUrl; + const [quoteAuthors, setQuoteAuthors] = useState([]); + const [loadingQuoteAuthors, setLoadingQuoteAuthors] = useState(false); + + const fetchQuoteAuthors = useCallback(async () => { + setLoadingQuoteAuthors(true); + try { + const response = await fetch(`${originUrl}/authors`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (data) { + setQuoteAuthors(data); + } + } catch (error) { + console.error("Failed to fetch quote authors:", error); + } finally { + setLoadingQuoteAuthors(false); + } + }, [originUrl]); + + useEffect(() => { + fetchQuoteAuthors(); + }, [fetchQuoteAuthors]); + + return { + quoteAuthors, + loadingQuoteAuthors + }; +} + +export default useQuoteAuthors; \ No newline at end of file diff --git a/src/api/controllers/authorsController.js b/src/api/controllers/authorsController.js new file mode 100644 index 0000000..0798851 --- /dev/null +++ b/src/api/controllers/authorsController.js @@ -0,0 +1,39 @@ +const quoteService = require("../services/quotesService"); + +const authorsController = async (req, res, next) => { + + try { + + let quotesUrl = req.query.quotesUrl || ''; + + let quoteCategory = req.query.quoteCategory || ''; + + let quoteObject = { + quotesUrl, + quoteCategory, + } + + const authors = await quoteService.getAuthors(quoteObject); + + res.setHeader("Content-Type", "application/json"); + + res.header( + "Cache-Control", + "no-cache,max-age=0,no-store,s-maxage=0,proxy-revalidate" + ); + res.header("Pragma", "no-cache"); + res.header("Expires", "-1"); + res.json(authors); + + } catch (error) { + console.error(error); + res.send({ + name: error.name, + message: error.message, + }); + } +}; + +module.exports = { + authorsController +}; diff --git a/src/api/controllers/quotesController.js b/src/api/controllers/quotesController.js index d096e0b..3561e26 100644 --- a/src/api/controllers/quotesController.js +++ b/src/api/controllers/quotesController.js @@ -34,6 +34,8 @@ const quoteController = async (req, res, next) => { let quoteType = req.query.quoteType || ''; + let author = req.query.author || undefined; + let quoteObject = { theme, animation, @@ -42,6 +44,7 @@ const quoteController = async (req, res, next) => { quoteCategory, font, quoteType, + author, borderColor } diff --git a/src/api/routes/quotes-router.js b/src/api/routes/quotes-router.js index 22d6770..b0a35eb 100644 --- a/src/api/routes/quotes-router.js +++ b/src/api/routes/quotes-router.js @@ -116,12 +116,16 @@ const controllers = require('../controllers/quotesController'); +const authorsController = require('../controllers/authorsController'); const defineRoutes = (app) => { // get a quote app.get('/quote', controllers.quoteController); + // get authors + app.get('/authors', authorsController.authorsController); + } module.exports = defineRoutes; diff --git a/src/api/services/quotesService.js b/src/api/services/quotesService.js index 7075555..b27491a 100644 --- a/src/api/services/quotesService.js +++ b/src/api/services/quotesService.js @@ -12,10 +12,16 @@ getQuoteIndex = (apiResponseLength, quoteType) => { return (quoteType === "quote-for-the-day" ? epoch % apiResponseLength : Math.random() * apiResponseLength); } +const filterQuotesByAuthor = (quotes, author) => { + const quotesFiltered = quotes.filter((quote) => quote?.author?.toString().toLowerCase().trim() === author.toString().toLowerCase().trim()); + + return quotesFiltered.length > 0 ? quotesFiltered : quotes; +} + const getQuote = async (quoteObj) => { try { - let { theme, animation, layout, quotesUrl, quoteCategory, font, quoteType, borderColor } = quoteObj; + let { theme, animation, layout, quotesUrl, quoteCategory, font, quoteType, borderColor, author } = quoteObj; let apiResponse; let { customQuotesUrl, isValidUrl } = await getValidUrl(quotesUrl); let isCustomQuote = false; @@ -24,29 +30,43 @@ const getQuote = async (quoteObj) => { //url from params is valid, proceed to verfiy the data apiResponse = await requestApi(customQuotesUrl); + if (author) { + apiResponse = filterQuotesByAuthor(apiResponse, author); + } + if (apiResponse.length > 0) { apiResponse = apiResponse[Math.floor(getQuoteIndex(apiResponse.length, quoteType))]; - if (!apiResponse.quote && !apiResponse.author) { - apiResponse = await requestApi(url); - } else { + + if (apiResponse.quote && apiResponse.author) { isCustomQuote = true; } - } else { - apiResponse = await requestApi(url); } } else if (quoteCategory) { apiResponse = quoteFromCategory[quoteCategory]; + + if (author) { + apiResponse = filterQuotesByAuthor(apiResponse, author); + } + apiResponse = apiResponse[Math.floor(getQuoteIndex(apiResponse.length, quoteType))]; + isCustomQuote = true; } - else { + + if(!isCustomQuote) { apiResponse = await requestApi(url); + + if (author) { + apiResponse = filterQuotesByAuthor(apiResponse, author); + } + + apiResponse = apiResponse[Math.floor(getQuoteIndex(apiResponse.length, quoteType))]; } const template = new Template(); template.setTheme(theme); - template.setData(isCustomQuote ? apiResponse : apiResponse[Math.floor(getQuoteIndex(apiResponse.length, quoteType))]); + template.setData(apiResponse); template.setFont(font); template.setAnimation(animation); template.setBorderColor(borderColor); @@ -59,6 +79,46 @@ const getQuote = async (quoteObj) => { } }; +const getAuthors = async (quoteObj) => { + try { + let {quotesUrl, quoteCategory} = quoteObj; + let apiResponse; + let { customQuotesUrl, isValidUrl } = await getValidUrl(quotesUrl); + let isCustomQuote = false; + + if (isValidUrl) { + //url from params is valid, proceed to verfiy the data + apiResponse = await requestApi(customQuotesUrl); + + if (apiResponse.length > 0) { + if (apiResponse[0].author) { + isCustomQuote = true; + } + } + } + else if (quoteCategory) { + apiResponse = quoteFromCategory[quoteCategory]; + isCustomQuote = true; + } + + if(!isCustomQuote) { + apiResponse = await requestApi(url); + } + + if (!apiResponse || apiResponse.length === 0) { + return []; + } + + // Get array of authors in alphabetical order and without duplicates + const authors = [...new Set(apiResponse.map(quote => quote.author).sort())]; + + return authors; + } catch (error) { + throw error; + } +} + module.exports = { getQuote, + getAuthors };