├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src ├── components │ ├── app │ │ └── app.js │ ├── controller │ │ ├── appLoader.js │ │ ├── controller.js │ │ └── loader.js │ └── view │ │ ├── appView.js │ │ ├── news │ │ ├── news.css │ │ └── news.js │ │ └── sources │ │ ├── sources.css │ │ └── sources.js ├── global.css ├── index.html └── index.js ├── webpack.config.js ├── webpack.dev.config.js └── webpack.prod.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier", "import"], 3 | "extends": [ 4 | "plugin:prettier/recommended", 5 | "prettier" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 2020, 9 | "sourceType": "module" 10 | }, 11 | "env": { 12 | "es6": true, 13 | "browser": true, 14 | "node": true 15 | }, 16 | "rules": { 17 | "no-debugger": "off", 18 | "no-console": 0, 19 | "class-methods-use-this": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # news-JS -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NewsJS", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.base.config.js", 6 | "scripts": { 7 | "start": "webpack serve --open --config ./webpack.config.js --env mode=dev", 8 | "build": "webpack --config ./webpack.config.js --env mode=prod", 9 | "lint": "eslint . --ext .ts" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "" 14 | }, 15 | "keywords": [], 16 | "author": "Aleh Serhiyenia", 17 | "license": "ISC", 18 | "homepage": "", 19 | "devDependencies": { 20 | "clean-webpack-plugin": "^3.0.0", 21 | "copy-webpack-plugin": "^7.0.0", 22 | "css-loader": "^5.1.0", 23 | "eslint": "^7.27.0", 24 | "eslint-config-prettier": "^8.3.0", 25 | "eslint-plugin-import": "^2.23.3", 26 | "eslint-plugin-prettier": "^3.4.0", 27 | "html-webpack-plugin": "^5.2.0", 28 | "prettier": "2.2.1", 29 | "style-loader": "^2.0.0", 30 | "webpack": "^5.37.1", 31 | "webpack-cli": "^4.5.0", 32 | "webpack-dev-server": "^3.11.2", 33 | "webpack-merge": "^5.7.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/app/app.js: -------------------------------------------------------------------------------- 1 | import AppController from '../controller/controller'; 2 | import { AppView } from '../view/appView'; 3 | 4 | class App { 5 | constructor() { 6 | this.controller = new AppController(); 7 | this.view = new AppView(); 8 | } 9 | 10 | start() { 11 | document 12 | .querySelector('.sources') 13 | .addEventListener('click', (e) => this.controller.getNews(e, (data) => this.view.drawNews(data))); 14 | this.controller.getSources((data) => this.view.drawSources(data)); 15 | } 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/components/controller/appLoader.js: -------------------------------------------------------------------------------- 1 | import Loader from './loader'; 2 | 3 | class AppLoader extends Loader { 4 | constructor() { 5 | super('https://newsapi.org/v2/', { 6 | apiKey: '', // получите свой ключ https://newsapi.org/ 7 | }); 8 | } 9 | } 10 | 11 | export default AppLoader; 12 | -------------------------------------------------------------------------------- /src/components/controller/controller.js: -------------------------------------------------------------------------------- 1 | import AppLoader from './appLoader'; 2 | 3 | class AppController extends AppLoader { 4 | getSources(callback) { 5 | super.getResp( 6 | { 7 | endpoint: 'sources', 8 | }, 9 | callback 10 | ); 11 | } 12 | 13 | getNews(e, callback) { 14 | let target = e.target; 15 | const newsContainer = e.currentTarget; 16 | 17 | while (target !== newsContainer) { 18 | if (target.classList.contains('source__item')) { 19 | const sourceId = target.getAttribute('data-source-id'); 20 | if (newsContainer.getAttribute('data-source') !== sourceId) { 21 | newsContainer.setAttribute('data-source', sourceId); 22 | super.getResp( 23 | { 24 | endpoint: 'everything', 25 | options: { 26 | sources: sourceId, 27 | }, 28 | }, 29 | callback 30 | ); 31 | } 32 | return; 33 | } 34 | target = target.parentNode; 35 | } 36 | } 37 | } 38 | 39 | export default AppController; 40 | -------------------------------------------------------------------------------- /src/components/controller/loader.js: -------------------------------------------------------------------------------- 1 | class Loader { 2 | constructor(baseLink, options) { 3 | this.baseLink = baseLink; 4 | this.options = options; 5 | } 6 | 7 | getResp( 8 | { endpoint, options = {} }, 9 | callback = () => { 10 | console.error('No callback for GET response'); 11 | } 12 | ) { 13 | this.load('GET', endpoint, callback, options); 14 | } 15 | 16 | errorHandler(res) { 17 | if (!res.ok) { 18 | if (res.status === 401 || res.status === 404) 19 | console.log(`Sorry, but there is ${res.status} error: ${res.statusText}`); 20 | throw Error(res.statusText); 21 | } 22 | 23 | return res; 24 | } 25 | 26 | makeUrl(options, endpoint) { 27 | const urlOptions = { ...this.options, ...options }; 28 | let url = `${this.baseLink}${endpoint}?`; 29 | 30 | Object.keys(urlOptions).forEach((key) => { 31 | url += `${key}=${urlOptions[key]}&`; 32 | }); 33 | 34 | return url.slice(0, -1); 35 | } 36 | 37 | load(method, endpoint, callback, options = {}) { 38 | fetch(this.makeUrl(options, endpoint), { method }) 39 | .then(this.errorHandler) 40 | .then((res) => res.json()) 41 | .then((data) => callback(data)) 42 | .catch((err) => console.error(err)); 43 | } 44 | } 45 | 46 | export default Loader; 47 | -------------------------------------------------------------------------------- /src/components/view/appView.js: -------------------------------------------------------------------------------- 1 | import News from './news/news'; 2 | import Sources from './sources/sources'; 3 | 4 | export class AppView { 5 | constructor() { 6 | this.news = new News(); 7 | this.sources = new Sources(); 8 | } 9 | 10 | drawNews(data) { 11 | const values = data?.articles ? data?.articles : []; 12 | this.news.draw(values); 13 | } 14 | 15 | drawSources(data) { 16 | const values = data?.sources ? data?.sources : []; 17 | this.sources.draw(values); 18 | } 19 | } 20 | 21 | export default AppView; 22 | -------------------------------------------------------------------------------- /src/components/view/news/news.css: -------------------------------------------------------------------------------- 1 | .news__item { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 1rem auto; 5 | margin-bottom: 1.6%; 6 | background: #fff; 7 | color: #333; 8 | line-height: 1.4; 9 | font-family: Arial, sans-serif; 10 | border-radius: 5px; 11 | overflow: hidden; 12 | } 13 | 14 | .news__item:hover .news__meta-photo { 15 | transform: scale(1.3) rotate(3deg); 16 | } 17 | 18 | .news__item .news__meta { 19 | position: relative; 20 | height: 200px; 21 | } 22 | 23 | .news__item .news__meta-photo { 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | bottom: 0; 28 | left: 0; 29 | background-size: cover; 30 | background-position: center; 31 | transition: transform 0.2s; 32 | } 33 | 34 | .news__item .news__meta-details, 35 | .news__item .news__meta-details ul { 36 | margin: auto; 37 | padding: 0; 38 | list-style: none; 39 | } 40 | 41 | .news__item .news__meta-details { 42 | position: absolute; 43 | top: 0; 44 | bottom: 0; 45 | left: -120%; 46 | margin: auto; 47 | transition: left 0.2s; 48 | background: rgba(0, 0, 0, 0.6); 49 | color: #fff; 50 | padding: 10px; 51 | width: 100%; 52 | font-size: 0.9rem; 53 | } 54 | 55 | .news__item .news__description { 56 | padding: 1rem; 57 | background: #fff; 58 | position: relative; 59 | z-index: 1; 60 | } 61 | 62 | .news__item .news__description h2 { 63 | line-height: 1; 64 | margin: 0; 65 | font-size: 1.7rem; 66 | } 67 | 68 | .news__item .news__description h3 { 69 | font-size: 1rem; 70 | font-weight: 300; 71 | text-transform: uppercase; 72 | color: #a2a2a2; 73 | margin-top: 5px; 74 | } 75 | 76 | .news__item .news__description .news__read-more { 77 | text-align: right; 78 | } 79 | 80 | .news__item .news__description .news__read-more a { 81 | color: #5ad67d; 82 | display: inline-block; 83 | position: relative; 84 | text-decoration: none; 85 | font-weight: 800; 86 | } 87 | 88 | .news__item .news__description .news__read-more a:after { 89 | content: '→'; 90 | margin-left: -10px; 91 | opacity: 0; 92 | vertical-align: middle; 93 | transition: margin 0.3s, opacity 0.3s; 94 | } 95 | 96 | .news__item .news__description .news__read-more a:hover:after { 97 | margin-left: 5px; 98 | opacity: 1; 99 | } 100 | 101 | .news__item p { 102 | margin: 1rem 0 0; 103 | } 104 | 105 | .news__item p:first-of-type { 106 | margin-top: 1.25rem; 107 | position: relative; 108 | } 109 | 110 | .news__item p:first-of-type:before { 111 | content: ''; 112 | position: absolute; 113 | height: 5px; 114 | background: #5ad67d; 115 | width: 35px; 116 | top: -0.75rem; 117 | border-radius: 3px; 118 | } 119 | 120 | .news__item:hover .news__meta-details { 121 | left: 0%; 122 | } 123 | 124 | @media (min-width: 640px) { 125 | .news__item { 126 | flex-direction: row; 127 | max-width: 700px; 128 | } 129 | 130 | .news__item .news__meta { 131 | flex-basis: 40%; 132 | height: auto; 133 | } 134 | 135 | .news__item .news__description { 136 | flex-basis: 60%; 137 | } 138 | 139 | .news__item .news__description:before { 140 | -webkit-transform: skewX(-3deg); 141 | transform: skewX(-3deg); 142 | content: ''; 143 | background: #fff; 144 | width: 30px; 145 | position: absolute; 146 | left: -10px; 147 | top: 0; 148 | bottom: 0; 149 | z-index: -1; 150 | } 151 | 152 | .news__item.alt { 153 | flex-direction: row-reverse; 154 | } 155 | 156 | .news__item.alt .news__description:before { 157 | left: inherit; 158 | right: -10px; 159 | -webkit-transform: skew(3deg); 160 | transform: skew(3deg); 161 | } 162 | 163 | .news__item.alt .news__meta-details { 164 | padding-left: 25px; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/components/view/news/news.js: -------------------------------------------------------------------------------- 1 | import './news.css'; 2 | 3 | class News { 4 | draw(data) { 5 | const news = data.length >= 10 ? data.filter((_item, idx) => idx < 10) : data; 6 | 7 | const fragment = document.createDocumentFragment(); 8 | const newsItemTemp = document.querySelector('#newsItemTemp'); 9 | 10 | news.forEach((item, idx) => { 11 | const newsClone = newsItemTemp.content.cloneNode(true); 12 | 13 | if (idx % 2) newsClone.querySelector('.news__item').classList.add('alt'); 14 | 15 | newsClone.querySelector('.news__meta-photo').style.backgroundImage = `url(${ 16 | item.urlToImage || 'img/news_placeholder.jpg' 17 | })`; 18 | newsClone.querySelector('.news__meta-author').textContent = item.author || item.source.name; 19 | newsClone.querySelector('.news__meta-date').textContent = item.publishedAt 20 | .slice(0, 10) 21 | .split('-') 22 | .reverse() 23 | .join('-'); 24 | 25 | newsClone.querySelector('.news__description-title').textContent = item.title; 26 | newsClone.querySelector('.news__description-source').textContent = item.source.name; 27 | newsClone.querySelector('.news__description-content').textContent = item.description; 28 | newsClone.querySelector('.news__read-more a').setAttribute('href', item.url); 29 | 30 | fragment.append(newsClone); 31 | }); 32 | 33 | document.querySelector('.news').innerHTML = ''; 34 | document.querySelector('.news').appendChild(fragment); 35 | } 36 | } 37 | 38 | export default News; 39 | -------------------------------------------------------------------------------- /src/components/view/sources/sources.css: -------------------------------------------------------------------------------- 1 | .sources { 2 | display: flex; 3 | flex-wrap: nowrap; 4 | width: 100%; 5 | height: 120px; 6 | overflow: auto; 7 | align-items: center; 8 | font: 300 1em 'Fira Sans', sans-serif; 9 | } 10 | 11 | .source__item { 12 | background: none; 13 | border: 2px solid #30c5ff; 14 | font: inherit; 15 | line-height: 1; 16 | margin: 0.5em; 17 | padding: 1em 2em; 18 | color: #70d6ff; 19 | transition: 0.25s; 20 | cursor: pointer; 21 | } 22 | 23 | .source__item:hover, 24 | .source__item:focus { 25 | border-color: #3fcc59; 26 | color: #69db7e; 27 | box-shadow: 0 0.5em 0.5em -0.4em #3fcc59; 28 | transform: translateY(-0.25em); 29 | } 30 | 31 | .source__item-name { 32 | font-weight: 400; 33 | white-space: nowrap; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/view/sources/sources.js: -------------------------------------------------------------------------------- 1 | import './sources.css'; 2 | 3 | class Sources { 4 | draw(data) { 5 | const fragment = document.createDocumentFragment(); 6 | const sourceItemTemp = document.querySelector('#sourceItemTemp'); 7 | 8 | data.forEach((item) => { 9 | const sourceClone = sourceItemTemp.content.cloneNode(true); 10 | 11 | sourceClone.querySelector('.source__item-name').textContent = item.name; 12 | sourceClone.querySelector('.source__item').setAttribute('data-source-id', item.id); 13 | 14 | fragment.append(sourceClone); 15 | }); 16 | 17 | document.querySelector('.sources').append(fragment); 18 | } 19 | } 20 | 21 | export default Sources; 22 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #fff; 3 | background: #17181c; 4 | font-family: sans-serif; 5 | } 6 | 7 | header { 8 | padding: 10px 30px; 9 | } 10 | 11 | header h1 { 12 | font-size: 40px; 13 | font-weight: 800; 14 | } 15 | 16 | footer { 17 | height: 100px; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | } 22 | footer .copyright { 23 | font-size: 14px; 24 | color: #333; 25 | text-align: center; 26 | } 27 | footer .copyright a { 28 | color: #444; 29 | } 30 | footer .copyright a:hover { 31 | color: #555; 32 | } 33 | footer .copyright:before { 34 | content: '©'; 35 | } 36 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |43 | Read More 44 |
45 |