diff --git a/README.md b/README.md index f2e0c4a..bc72f51 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,57 @@ # Responsive News Reader -Let's create a responsive news reader website. To get the latest news the app will display we are going to use [News API](https://newsapi.org/). As with the Weather and Unsplash APIs, you will need to register with the service to obtain an API key. +## A responsive news website. Powered by [News API](https://newsapi.org/). +--- +> [View live demo](https://joelamb.github.io/update-live/) -## Objectives +## Features -- [ ] Create a responsive layout that works well and looks good at mobile, tablet and desktop screen sizes. Please create the mobile version first, then tablet and then desktop. +- A responsive mobile-first layout that works well and looks good at mobile, tablet and desktop screen sizes. -- [ ] Fetch news articles from News API and display them including title, image, description, publication, date and link to article. +- News articles fetched from News API and displayed. Each story includes title, image, description, publication source, publication date and a link to the original article. -- [ ] Display images for tablet and desktop screens only +- Publication date is shown as time elapsed since publication of the story. -- [ ] Implement a feature of your choice +- Images only display on tablet and desktop screens. -## Stretch goals +- Category navigation allows the user to see the top stories in Entertainment, Science, Technology, Health, Food & Cycling. -- [ ] Implement pagination which allows you to get the next 20 results and display them +- Pagination links at the foot of the page allow the user to see the next page of results. The current page number and total number of available pages are also displayed. -- [ ] Add search functionality which allows the user to retrieve news about a particular topic +- Search functionality to allows the user to retrieve news about a particular topic. -## Instructions +- Sidebar of most popular stories from the _Daily Mail_ -- Fork and clone this repo -- Commit your changes frequently with short and descriptive commit messages -- Create a pull request when complete -- We want to see great looking webpages -- Your code should have consistent indentation and sensible naming -- Use lots of concise functions with a clear purpose -- Add code comments where it is not immediately obvious what your code does -- Your code should not throw errors and handle edge cases gracefully. For example not break if server fails to return expected results +- Hidden redact feature when the user searches for 'Trump' -## README.md +## Code -When finished, include a README.md in your repo. This should explain what the project is, how to run it and how to use it. Someone who is not familiar with the project should be able to look at it and understand what it is and what to do with it. +- Clean, well-commented code. + +- Concise functions, each with a clear purpose. + +- `redact()` function wraps any 5,6,8 or 9 character word in the headline and story with a `` when the user types 'Trump' in the search query input. + +- `cleanData()` function takes the data returned by the News API and filters out any stories that do not have an image link or content in the 'description' field. + +## Performance + +- Using `window.innerWidth`, images only load in the tablet and desktop views. + +## CSS + +- Clean, well commented CSS. + +- Font used throughout is FF Meta a new Variable Font based on the original typeface designed by Erik Spiekermann. + +- Grid CSS used to layout tablet and desktop views. + +## To Do + +- Error handling to get App to handle edge cases gracefully, alerting the user if there is an error. + +- Sanitise news feed to detect broken image links. + +- Give the user the choice of where they get most popular stories from in the sidebar. + +- Redact 'Trump' search images. Add a class with CSS brightness filter of either 0 or 1 to randomly black out images. diff --git a/index.html b/index.html new file mode 100644 index 0000000..b200bef --- /dev/null +++ b/index.html @@ -0,0 +1,60 @@ + + + + + + + + Update - News Reader + + + + + +
+

Updte Live

+

Saturday

+
+ + +
+ + +
+ + + + + + + + + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..5186248 --- /dev/null +++ b/index.js @@ -0,0 +1,346 @@ +// Initialise url parameters + +const params = { + base: 'https://newsapi.org/v2/', + endpoint: 'top-headlines', + apiKey: 'f29390555fbc483ba17e7ec1cb19af1a', + language: 'en', + country: 'gb', + category: '', + query: '', + sortBy: 'publishedAt', + pageSize: 9, + pageNum: 1, + redacted: false +}; + +//---------------------------// + +const init = () => { + getNews(params); + window.innerWidth > 768 ? getPopular() : false; // Only load sidebar stories on tablet and desktop +}; + +// Fetch News API data + +const setURL = params => { + return `${params.base}${params.endpoint}?apiKey=${params.apiKey}&q=${ + params.query + }&country=${params.country}&category=${params.category}&sortBy=${ + params.sortBy + }&pageSize=${params.pageSize}&page=${params.pageNum}`; +}; + +const getNews = params => { + fetch(setURL(params)) + .then(response => { + return response.json(); + }) + .then(data => { + addArticlesToFeed(data); + }) + .catch(err => { + displayErrorToUser( + 'Server failed to return data. Please refresh your browser to try again.' + ); + }); +}; + +const getPopular = () => { + fetch( + 'https://newsapi.org/v2/everything?language=en&domains=dailymail.co.uk&pageSize=8&sortBy=popularity&apiKey=f29390555fbc483ba17e7ec1cb19af1a' // hard-coded URL + ) + .then(response => { + return response.json(); + }) + .then(data => { + addPopular(data); + }) + .catch(err => { + displayErrorToUser('Server failed to return data'); + }); +}; + +// Error function +const displayErrorToUser = err => { + alert(err); +}; + +// Filter data to prevent stories without description or image loading +const cleanData = data => { + return data.articles.filter( + article => + article.description != null && + article.description != '' && + article.urlToImage != null + ); +}; + +// Create single news article + +const createArticle = articleData => { + if (params.redacted === true) { + articleData.description = redact(articleData.description); + articleData.title = redact(articleData.title); + // articleData.urlToImage = trumped(articleData.urlToImage); + } + const article = document.createElement('article'); + article.classList.add('news__article'); + const elapsedTime = getTimeSinceArticlePublication(articleData.publishedAt); + if (window.innerWidth < 768) { + articleData.urlToImage = ''; + } + article.innerHTML = ` +

${articleData.title}

+

Published ${elapsedTime} ago

+ +

${articleData.description}

+

Read the full story at ${articleData.source.name}

`; + + return article; +}; + +const createPopArticle = articleData => { + const article = document.createElement('article'); + article.classList.add('news__article'); + article.innerHTML = ` + +

${articleData.title}

`; + + return article; +}; + +// Aggregate the individual articles + +const createArticles = data => { + const newsWrapper = document.createElement('div'); + data.forEach(story => { + const newsArticle = createArticle(story); + newsWrapper.appendChild(newsArticle); + }); + + return newsWrapper; +}; + +const createPopArticles = data => { + const popWrapper = document.createElement('div'); + data.forEach(story => { + const newsArticle = createPopArticle(story); + popWrapper.appendChild(newsArticle); + }); + + return popWrapper; +}; + +// Append the aggregated articles to the DOM + +const addArticlesToFeed = data => { + const newsFeed = document.querySelector('section.news'); + const ref = document.querySelector('section.news aside'); + const feed = cleanData(data); + const stories = createArticles(feed); + newsFeed.insertBefore(stories, ref); + document.querySelector('.page-total').textContent = Math.floor( + data.totalResults / params.pageSize // Calculate the total number of pages in the feed + ); +}; + +const addPopular = data => { + const popularFeed = document.querySelector('aside.popular'); + const popFeed = cleanData(data); + const popStories = createPopArticles(popFeed); + popularFeed.appendChild(popStories); +}; + +// Content helper functions + +// Add conditional 's' to min, day and hour when grater than 1 + +const pluralUnits = (val, unit) => { + return val >= 2 ? unit.replace(' ', 's ') : unit; +}; + +// Calculate time since publication of news article + +const getTimeSinceArticlePublication = date => { + let mins = Math.floor((Date.now() - new Date(date).valueOf()) / 60000); + let hours = mins / 60; + let days = hours / 24; + let elapsedTime = ''; + if (days >= 1) { + return (elapsedTime += `${Math.floor(days)} ${pluralUnits(days, 'day ')}`); + } + if (hours >= 1) { + return (elapsedTime += `${Math.floor(hours)} ${pluralUnits( + hours, + 'hour ' + )}`); + } + + return (elapsedTime += `${mins} ${pluralUnits(mins, 'min ')}`); +}; + +// Pagination + +// Delete current news stories from the DOM + +const clearNewsFeed = () => { + document + .querySelector('section.news') + .removeChild(document.querySelector('section.news div')); +}; + +const nextPage = document.querySelector('.page-nav .next'); +nextPage.addEventListener('click', e => { + const currentPageNum = document.querySelector('.page-current'); + const totalPageNum = document.querySelector('.page-total'); + e.preventDefault(); + +currentPageNum.textContent < +totalPageNum.textContent // Prevent next page advancing beyond total no. of pages + ? params.pageNum++ + : false; + clearNewsFeed(); + getNews(params); + currentPageNum.textContent = params.pageNum; +}); + +const prevPage = document.querySelector('.page-nav .prev'); +prevPage.addEventListener('click', e => { + e.preventDefault(); + params.pageNum > 1 ? params.pageNum-- : false; // Prevent previous page returning negative page number + clearNewsFeed(); + getNews(params); + document.querySelector('.page-current').textContent = params.pageNum; +}); + +// Search + +const searchForm = document.querySelector('form.search--form'); +searchForm.addEventListener('submit', e => { + e.preventDefault(); + const query = searchForm.lastElementChild.value; + + // reset redact + params.redacted = false; + + // process input value + if (query === 'Trump' || query === 'trump') { + params.redacted = true; // redact 'Trump' stories + } + if (query !== '') { + params.query = query; + params.endpoint = 'everything'; + params.country = ''; + params.pageNum = 1; // reset page counter + clearNewsFeed(); + getNews(params); + } + + searchForm.lastElementChild.value = ''; + searchForm.lastElementChild.placeholder = `News about ${query}`; // Show search query as topic + + //reset the search input if the user clicks on another part of the page + + searchForm.lastElementChild.addEventListener('blur', e => { + searchInput.placeholder = 'Type your search query...'; + searchInput.value = ''; + }); +}); + +// reset search input when the user makes consequtive searches + +const searchInput = document.querySelector('#search--query'); +searchInput.addEventListener('focus', e => { + searchInput.placeholder = ''; + searchInput.value = ''; +}); + +// Add current day and date to header + +const todayDate = new Date(); +const options = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' +}; + +document.querySelector('.hero__date').textContent = todayDate.toLocaleString( + 'en-UK', + options +); + +// Category select + +const categories = document.querySelectorAll('.category'); + +categories.forEach(category => { + category.addEventListener('click', e => { + const topic = e.target.textContent; + // custom category searches based on query sting + if (topic === 'cycling') { + params.query = 'cycling bicycle cycle'; + params.endpoint = 'everything'; + params.country = ''; + params.category = ''; + } else if (topic === 'food') { + params.query = 'food nutrition cooking'; + params.endpoint = 'everything'; + params.country = ''; + params.category = ''; + // category 'general' renmaed to 'top stories' + } else if (topic === 'top stories') { + params.category = 'general'; + params.query = ''; + params.endpoint = 'top-headlines'; + params.country = 'gb'; + } else { + // category search based on API endpoint + params.category = topic; + params.query = ''; + params.endpoint = 'top-headlines'; + params.country = 'gb'; + } + params.pageNum = 1; // reset page counter + document.querySelector('.page-current').textContent = params.pageNum; + + // reset redact + params.redacted = false; + clearNewsFeed(); + getNews(params); + + Array.from(categories) + .filter(category => category.classList.contains('current')) + .map(category => category.classList.remove('current')); + category.classList.add('current'); + }); +}); + +// redact function wraps 5,6,8 & 9 character words in a span for CSS blackout styling + +const redact = text => { + return text + .split(' ') + .map(word => { + if ( + word.length === 5 || + word.length === 6 || + word.length === 8 || + word.length === 9 + ) { + return `${word}`; + } else { + return word; + } + }) + .join(' '); +}; + +// const trumped = img => { +// return 'http://en.bcdn.biz/Images/2018/6/12/133c8505-d85f-488e-84b3-a0aeb574940d.jpg'; +// }; + +// Start the app and get the news! + +init(); diff --git a/now.json b/now.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/now.json @@ -0,0 +1 @@ +{} diff --git a/style.css b/style.css new file mode 100644 index 0000000..6052b66 --- /dev/null +++ b/style.css @@ -0,0 +1,530 @@ +/* FF Meta Variable Demo Font */ +@font-face { + font-family: 'FF Meta VF'; + src: url('https://variablefonts.monotype.com/MetaVariableDemo-Set.woff2') + format('woff2'); + font-display: swap; + font-style: normal italic; + font-weight: 100 900; +} + +/**********/ +/* global */ + +html { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body { + font-family: 'FF Meta VF', Helvetica, Arial, sans-serif; + padding: 0; + margin: 0; +} + +a { + color: black; + font-weight: 700; + text-decoration: none; +} + +button { + background: white; + border: none; + border-radius: 0.5rem; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4); + font-weight: 600; +} + +em { + font-variation-settings: 'ital' 1; /* set italic for variable font */ +} + +/**********/ +/* mobile */ + +/* header */ + +.hero { + background: rgb(30, 49, 49); + color: white; + position: fixed; + z-index: 10; + padding-top: 1rem; + width: 100%; +} + +.hero__title { + font-size: 2.5rem; + font-weight: 700; + margin: 0 0 0 0.5rem; + text-transform: uppercase; +} + +.hero__title i { + font-size: 2.6rem; + top: -0.3rem; + padding: 0 0.2rem 0 0.1rem; + position: relative; +} + +.hero__title span { + color: yellow; + font-weight: 300; + text-transform: uppercase; +} + +.hero__title span i { + display: inline-block; + margin-left: 0.2rem; + top: -0.1rem; + animation: pulse 0.5s infinite; /* animate logo */ + animation-direction: alternate; + width: 0.5rem; +} + +@keyframes pulse { + 0% { + color: yellow; + } + 100% { + color: rgb(30, 49, 49); + } +} + +.hero__date { + margin: 0 0 0.75rem 1rem; +} + +/* category navigation */ + +.category-nav { + background: white; + padding: 7rem 0.5rem 0.5rem; +} + +.category-nav ul { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + list-style-type: none; + margin: 0; + padding: 0; +} + +.category-nav ul li { + background: rgba(47, 79, 79, 0.5); + border-radius: 1rem; + color: black; + font-size: 0.9rem; + font-weight: 600; + margin: 0.1rem; + padding: 0.25rem 0.75rem; + text-transform: capitalize; +} + +.category-nav ul li.current { + background: darkslategrey; + color: white; +} + +/* search */ + +.search { + background: rgba(47, 79, 79, 0.5); + padding: 0.5rem 0 0.5rem 0.5rem; +} + +.search--form label { + display: none; +} + +#search--query { + border: none; + font-size: 1rem; + padding: 0.5rem; +} + +/* news feed */ + +.news { + background: darkslategrey; + padding-top: 0.5rem; +} + +.news__article { + background: white; + margin: 0 0.5rem 1rem 0.5rem; + padding: 0.25rem 0.5rem; + box-shadow: 0 0.1rem 1rem rgba(0, 0, 0, 0.5); + border-radius: 0.25em; +} + +.news__article:nth-child(even) { + background: gainsboro; +} + +.news__headline { + font-size: 1.2rem; + font-weight: 700; + line-height: 1.2; + margin-top: 0.4rem; +} + +.news__date { + border-width: 1px; + border-color: black; + border-style: solid none dotted none; + font-size: 0.95rem; + padding: 0.25rem 0; +} + +img.news__image { + display: none; + width: 100%; +} + +.news__story span, +.news__headline span { + background: black; +} + +.news__publication { + border-top: 1px dotted black; + padding-top: 0.25rem; +} + +.news__publication a { + font-variation-settings: 'ital' 1; +} + +/* popular stories */ + +.popular { + display: none; +} + +/* page navigation */ + +.page-nav { + display: flex; + justify-content: space-around; + background: darkslategray; + padding: 1rem; +} + +.page-num { + color: white; + font-weight: 600; + margin: 0; +} + +/* footer */ + +.legals { + background: black; + color: white; + display: flex; + flex-direction: column; + padding: 0 1rem; +} + +.legals__copyright { + margin-bottom: 0; +} + +.legals__credit { + margin-top: 0; +} + +.legals__credit a { + color: white; +} + +/**********************/ +/* iPad tablet layout */ + +@media (min-width: 768px) { + button { + height: 3em; + } + + .hero__title { + font-size: 4rem; + } + + .hero__title i { + font-size: 3.6rem; + top: -0.3rem; + padding: 0 0.3rem 0 0.1rem; + position: relative; + } + + .category-nav { + padding-top: 9rem; + } + + .news div { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + align-content: space-between; + } + + /* news articles */ + + .news__article { + display: grid; + grid-template-rows: repeat(3, min-content) min-content 1fr; + } + + .news__article:nth-child(even) { + background: white; + } + + .news__headline { + margin-bottom: 0.25rem; + } + + .news__date { + margin: 0.25rem 0; + } + + img.news__image { + display: block; + margin: 0.25rem 0 0.5rem 0; + object-fit: cover; + } + + .news__publication { + align-self: end; + margin: 0; + } + + /* featured article */ + + .news div .news__article:first-child { + grid-column: 1 / span 2; + grid-row: 1 / span 2; + } + + .news div .news__article:first-child .news__headline { + font-size: 2rem; + } + + .news div .news__article:first-child .news__date { + line-height: 0.5; + padding: 0.5rem 0; + } + + .news div .news__article:first-child img.news__image { + height: 19rem; + } + + .news div .news__article:first-child .news__story { + font-size: 1.2rem; + margin-top: 0; + } + + /* stacked stories (second and third articles) */ + + .news div .news__article:nth-child(2) img.news__image, + .news div .news__article:nth-child(3) img.news__image { + grid-row: 1 / span 2; + } + + .news div .news__article:nth-child(2) .news__story, + .news div .news__article:nth-child(3) .news__story { + display: none; + } + + .news div .news__article:nth-child(2) .news__date, + .news div .news__article:nth-child(3) .news__date { + margin-bottom: 0; + } + + .news div .news__article:nth-child(2) .news__publication, + .news div .news__article:nth-child(3) .news__publication { + border-top: none; + margin-top: 0; + } + + /* page navigation */ + + .page-nav { + justify-content: center; + align-content: center; + } + + .page-num { + margin: 0 1rem; + padding-top: 0.25rem; + } + + /* footer */ + + .legals { + flex-direction: row; + justify-content: space-between; + padding: 0 0.5rem; + } + + .legals__copyright, + .legals__credit { + margin: 0.5rem 0; + } +} + +/******************/ +/* Desktop layout */ + +@media (min-width: 1200px) { + /* category nav */ + + .category-nav { + background: rgb(30, 49, 49); + border-bottom: 1px solid darkslategrey; + padding: 8.25rem 0.5rem 0 0.5rem; + } + + .category-nav ul { + background: white; + } + + .category-nav ul li { + background: rgba(47, 79, 79, 0.5); + border-radius: 0; + border-right: 1px solid lightslategray; + flex-grow: 1; + margin: 0; + text-align: center; + font-size: 1rem; + } + + .category-nav ul li:last-child { + border-right: none; + } + + /* search */ + + .search { + background: darkslategrey; + } + + .search--form { + grid-column: 2 / span 2; + /* justify-self: end; */ + } + + #search--query { + border: none; + font-size: 0.8rem; + padding: 0.5rem; + width: 100%; + } + + /* layout */ + + .news, + .page-nav { + background: white; + } + + .page-num { + color: black; + } + + .news__article { + box-shadow: none; + border-top: 1px solid black; + border-bottom: 0.25rem solid black; + border-radius: 0; + } + + .news, + .hero, + .category-nav, + .search { + display: grid; + grid-template-columns: repeat(12, 1fr); + } + + .news div, + .hero__title, + .hero__date { + grid-column: 2 / span 7; + } + + .news__publication { + border-width: 1px 0 0 0; + border-color: black; + border-style: dotted none solid none; + padding-bottom: 0.25rem; + } + + /* sidebar of shame */ + + .popular { + background: rgba(47, 79, 79, 0.5); + display: block; + grid-column: 10 / span 2; + } + + .popular .news__article { + border: none; + } + + .popular h2 { + background: rgb(30, 49, 49); + color: white; + font-size: 1.3rem; + line-height: 1.2; + padding: 0.5rem 0 0.5rem 1rem; + margin-top: 0; + text-align: center; + } + + .popular h3 { + line-height: 1; + } + + .news aside.popular div { + display: block; + } + + .popular div .news__article:first-child { + display: none; + } + + /* page navigation */ + + .page-nav, + .category-nav ul { + grid-column: 2 / span 10; + justify-content: start; + } + + /* footer */ + + .legals { + justify-content: center; + } + + .legals__copyright { + margin-right: 0.5rem; + } + + .legals__copyright::after { + content: '|'; + display: inline-block; + margin-left: 1rem; + } + + .legals__credit { + margin-left: 0.5rem; + } +}