├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── actions │ ├── PageActions.js │ └── UserActions.js ├── components │ ├── App.js │ ├── BigPhoto.js │ ├── ListPhoto.js │ ├── Page.js │ ├── PhotoManager.js │ └── User.js ├── containers │ ├── PageContainer.js │ └── UserContainer.js ├── index.css ├── index.js ├── reducers │ ├── index.js │ ├── page.js │ └── user.js ├── registerServiceWorker.js ├── store │ └── configureStore.js └── util │ └── date.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "prettier" 5 | ], 6 | "rules": { 7 | "jsx-quotes": [ 8 | 1, 9 | "prefer-double" 10 | ] 11 | }, 12 | "plugins": [ 13 | "prettier" 14 | ] 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "jsxBracketSameLine": false, 8 | "parser": "flow", 9 | "semi": false 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Код для учебника по Redux (2-е издание) 2 | 3 | - React `^16.4.1` 4 | - Redux `^4.0.0` 5 | 6 | --- 7 | 8 | ### Полезные ссылки 9 | 10 | Мои уроки/вебинары/соц.сети: 11 | 12 | - Полноценный учебник ["Основы React"](https://legacy.gitbook.com/book/maxfarseer/react-course-ru-v2/details) 13 | - Исходный код для учебника ["Основы React"](https://github.com/maxfarseer/react-course-ru-v2) 14 | - [Расписание стримов и вебинаров](http://bit.ly/maxpfrontend-schedule-v2) (на сайте есть текстовые версии вебинаров) 15 | - [Youtube канал](http://bit.ly/youtube-v2) c записями вебинаров и стримов 16 | - Группа [vkontakte](http://bit.ly/vk-v2) 17 | - Канал в [telegram](http://bit.ly/telegram-v2) 18 | - [Twitter](http://bit.ly/twitter-v2) 19 | - [Facebook](http://bit.ly/facebook-v2) 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-course-ru-v2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "prop-types": "^15.6.2", 7 | "react": "^16.4.1", 8 | "react-dom": "^16.4.1", 9 | "react-modal": "^3.5.1", 10 | "react-redux": "^5.0.7", 11 | "react-scripts": "1.1.4", 12 | "redux": "^4.0.0", 13 | "redux-thunk": "^2.3.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject", 20 | "precommit": "lint-staged", 21 | "eslint": "node_modules/.bin/eslint src/" 22 | }, 23 | "devDependencies": { 24 | "eslint-config-prettier": "^2.9.0", 25 | "eslint-plugin-prettier": "^2.6.2", 26 | "husky": "^0.14.3", 27 | "lint-staged": "^7.2.0", 28 | "prettier": "^1.14.0", 29 | "redux-logger": "^3.0.6" 30 | }, 31 | "lint-staged": { 32 | "*.{js, jsx}": [ 33 | "node_modules/.bin/eslint --max-warnings=0", 34 | "prettier --write", 35 | "git add" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxfarseer/redux-course-ru-v2/1f1f896b4ef1c89be7de484f61713936550140eb/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Redux [RU] Tutorial v2 10 | 11 | 12 | 15 |
16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/PageActions.js: -------------------------------------------------------------------------------- 1 | export const GET_PHOTOS_REQUEST = 'GET_PHOTOS_REQUEST' 2 | export const GET_PHOTOS_SUCCESS = 'GET_PHOTOS_SUCCESS' 3 | export const GET_PHOTOS_FAIL = 'GET_PHOTOS_FAIL' 4 | 5 | let photosArr = [] 6 | let cached = false 7 | 8 | function makeYearPhotos(photos, selectedYear) { 9 | let createdYear, 10 | yearPhotos = [] 11 | 12 | photos.forEach(item => { 13 | createdYear = new Date(item.date * 1000).getFullYear() 14 | if (createdYear === selectedYear) { 15 | yearPhotos.push(item) 16 | } 17 | }) 18 | 19 | yearPhotos.sort((a, b) => b.likes.count - a.likes.count) 20 | 21 | return yearPhotos 22 | } 23 | 24 | function getMorePhotos(offset, count, year, dispatch) { 25 | //eslint-disable-next-line no-undef 26 | VK.Api.call( 27 | 'photos.getAll', 28 | { extended: 1, count: count, offset: offset, v: '5.80' }, 29 | r => { 30 | try { 31 | photosArr = photosArr.concat(r.response.items) 32 | if (offset <= r.response.count) { 33 | offset += 200 // максимальное количество фото которое можно получить за 1 запрос 34 | getMorePhotos(offset, count, year, dispatch) 35 | } else { 36 | let photos = makeYearPhotos(photosArr, year) 37 | cached = true 38 | dispatch({ 39 | type: GET_PHOTOS_SUCCESS, 40 | payload: photos, 41 | }) 42 | } 43 | } catch (e) { 44 | dispatch({ 45 | type: GET_PHOTOS_FAIL, 46 | error: true, 47 | payload: new Error(e), 48 | }) 49 | } 50 | } 51 | ) 52 | } 53 | 54 | export function getPhotos(year) { 55 | return dispatch => { 56 | dispatch({ 57 | type: GET_PHOTOS_REQUEST, 58 | payload: year, 59 | }) 60 | 61 | if (cached) { 62 | let photos = makeYearPhotos(photosArr, year) 63 | dispatch({ 64 | type: GET_PHOTOS_SUCCESS, 65 | payload: photos, 66 | }) 67 | } else { 68 | getMorePhotos(0, 200, year, dispatch) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/actions/UserActions.js: -------------------------------------------------------------------------------- 1 | export const LOGIN_REQUEST = 'LOGIN_REQUEST' 2 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' 3 | export const LOGIN_FAIL = 'LOGIN_FAIL' 4 | 5 | export function handleLogin(callback) { 6 | return function(dispatch) { 7 | dispatch({ 8 | type: LOGIN_REQUEST, 9 | }) 10 | 11 | //eslint-disable-next-line no-undef 12 | VK.Auth.login(r => { 13 | if (r.session) { 14 | const username = r.session.user.first_name 15 | 16 | dispatch({ 17 | type: LOGIN_SUCCESS, 18 | payload: username, 19 | }) 20 | callback() 21 | } else { 22 | dispatch({ 23 | type: LOGIN_FAIL, 24 | error: true, 25 | payload: new Error('Ошибка авторизации'), 26 | }) 27 | } 28 | }, 4) // запрос прав на доступ к photo 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import UserContainer from '../containers/UserContainer' // изменили импорт 3 | import PageContainer from '../containers/PageContainer' // изменили импорт 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | } 15 | 16 | export default App 17 | -------------------------------------------------------------------------------- /src/components/BigPhoto.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class BigPhoto extends React.Component { 5 | state = { 6 | isLoading: false, 7 | } 8 | componentDidMount() { 9 | this.loadImage(this.props.url) 10 | } 11 | loadImage = src => { 12 | this.setState({ isLoading: true }) 13 | 14 | let img = new Image() 15 | img.onload = () => { 16 | this.setState({ isLoading: false }) 17 | } 18 | 19 | img.src = src 20 | } 21 | render() { 22 | const { isLoading } = this.state 23 | const { url } = this.props 24 | return isLoading ?

Загружаю...

: big vk 25 | } 26 | } 27 | 28 | export default BigPhoto 29 | 30 | BigPhoto.propTypes = { 31 | url: PropTypes.string.isRequired, 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ListPhoto.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ListPhoto = ({ photos, openModal }) => { 5 | return photos.map(photo => ( 6 |
7 |

8 | thumbnail vk openModal(photo.sizes[4].url)} 12 | /> 13 |

14 |

{photo.likes.count} ❤

15 |
16 | )) 17 | } 18 | 19 | export default ListPhoto 20 | 21 | ListPhoto.propTypes = { 22 | photos: PropTypes.array.isRequired, 23 | openModal: PropTypes.func.isRequired, 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import PhotoManager from './PhotoManager' 4 | 5 | export class Page extends React.Component { 6 | onBtnClick = e => { 7 | const year = +e.currentTarget.innerText 8 | this.props.getPhotos(year) // setYear -> getPhotos 9 | } 10 | renderButtons = () => { 11 | const { years } = this.props 12 | 13 | return years.map((item, index) => { 14 | return ( 15 | 18 | ) 19 | }) 20 | } 21 | renderTemplate = () => { 22 | const { photos, isFetching, error } = this.props 23 | 24 | if (error) { 25 | return

Во время загрузки фото произошла ошибка

26 | } 27 | 28 | if (isFetching) { 29 | return

Загрузка...

30 | } else { 31 | return 32 | } 33 | } 34 | 35 | render() { 36 | const { year, photos } = this.props 37 | return ( 38 |
39 |

{this.renderButtons()}

40 |

41 | {year} год [{photos.length}] 42 |

43 | {this.renderTemplate()} 44 |
45 | ) 46 | } 47 | } 48 | 49 | Page.propTypes = { 50 | year: PropTypes.number.isRequired, 51 | photos: PropTypes.array.isRequired, 52 | getPhotos: PropTypes.func.isRequired, 53 | error: PropTypes.string, 54 | isFetching: PropTypes.bool.isRequired, 55 | years: PropTypes.array.isRequired, 56 | } 57 | -------------------------------------------------------------------------------- /src/components/PhotoManager.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Modal from 'react-modal' 3 | import ListPhoto from './ListPhoto' 4 | import BigPhoto from './BigPhoto' 5 | import PropTypes from 'prop-types' 6 | 7 | const customStyles = { 8 | content: { 9 | top: '50%', 10 | left: '50%', 11 | right: 'auto', 12 | bottom: 'auto', 13 | marginRight: '-50%', 14 | transform: 'translate(-50%, -50%)', 15 | padding: '0px', 16 | }, 17 | overlay: { 18 | backgroundColor: 'rgba(0,0,0,0.7)', 19 | }, 20 | } 21 | 22 | export default class PhotoManager extends React.Component { 23 | constructor(props) { 24 | super(props) 25 | 26 | this.state = { 27 | modalIsOpen: false, 28 | activeUrl: '', 29 | } 30 | } 31 | 32 | openModal = url => { 33 | this.setState({ modalIsOpen: true, activeUrl: url }) 34 | } 35 | 36 | closeModal = () => { 37 | this.setState({ modalIsOpen: false, activeUrl: '' }) 38 | } 39 | 40 | render() { 41 | const { photos } = this.props 42 | const { modalIsOpen, activeUrl } = this.state 43 | return ( 44 | 45 | 46 | 52 | 53 | 54 | 55 | ) 56 | } 57 | } 58 | 59 | PhotoManager.propTypes = { 60 | photos: PropTypes.array.isRequired, 61 | } 62 | -------------------------------------------------------------------------------- /src/components/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export class User extends React.Component { 5 | renderTemplate = () => { 6 | const { name, error, isFetching } = this.props 7 | 8 | if (error) { 9 | return

Во время запроса произошла ошибка, обновите страницу

10 | } 11 | 12 | if (isFetching) { 13 | return

Загружаю...

14 | } 15 | 16 | if (name) { 17 | return

Привет, {name}!

18 | } else { 19 | return ( 20 | 23 | ) 24 | } 25 | } 26 | render() { 27 | return
{this.renderTemplate()}
28 | } 29 | } 30 | 31 | User.propTypes = { 32 | name: PropTypes.string.isRequired, 33 | error: PropTypes.string, 34 | isFetching: PropTypes.bool.isRequired, 35 | handleLogin: PropTypes.func.isRequired, 36 | } 37 | -------------------------------------------------------------------------------- /src/containers/PageContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Page } from '../components/Page' 4 | import { getPhotos } from '../actions/PageActions' 5 | import { getLastYears } from '../util/date' 6 | 7 | const LAST_5_YEARS = 5 8 | 9 | class PageContainer extends React.Component { 10 | constructor(props) { 11 | super(props) 12 | 13 | this.years = getLastYears(LAST_5_YEARS) 14 | } 15 | 16 | render() { 17 | const { page, getPhotos } = this.props 18 | return ( 19 | 27 | ) 28 | } 29 | } 30 | 31 | const mapStateToProps = store => { 32 | return { 33 | page: store.page, 34 | } 35 | } 36 | 37 | const mapDispatchToProps = dispatch => { 38 | return { 39 | getPhotos: year => dispatch(getPhotos(year)), 40 | } 41 | } 42 | 43 | export default connect( 44 | mapStateToProps, 45 | mapDispatchToProps 46 | )(PageContainer) 47 | -------------------------------------------------------------------------------- /src/containers/UserContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { User } from '../components/User' 4 | import { handleLogin } from '../actions/UserActions' 5 | import { getPhotos } from '../actions/PageActions' 6 | import { getCurrentYear } from '../util/date' 7 | 8 | class UserContainer extends React.Component { 9 | handleLogin = () => { 10 | const { handleLogin, getPhotos } = this.props 11 | const successCallback = () => { 12 | const year = getCurrentYear() 13 | getPhotos(year) 14 | } 15 | 16 | handleLogin(successCallback) 17 | } 18 | 19 | render() { 20 | const { user } = this.props 21 | return ( 22 | 28 | ) 29 | } 30 | } 31 | 32 | const mapStateToProps = store => { 33 | return { 34 | user: store.user, 35 | } 36 | } 37 | 38 | const mapDispatchToProps = dispatch => { 39 | return { 40 | handleLogin: successCallback => dispatch(handleLogin(successCallback)), 41 | getPhotos: year => dispatch(getPhotos(year)), 42 | } 43 | } 44 | 45 | export default connect( 46 | mapStateToProps, 47 | mapDispatchToProps 48 | )(UserContainer) 49 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | font-family: sans-serif; 34 | } 35 | ol, ul { 36 | list-style: none; 37 | } 38 | blockquote, q { 39 | quotes: none; 40 | } 41 | blockquote:before, blockquote:after, 42 | q:before, q:after { 43 | content: ''; 44 | content: none; 45 | } 46 | table { 47 | border-collapse: collapse; 48 | border-spacing: 0; 49 | } 50 | 51 | /*end reset*/ 52 | 53 | h3 { 54 | font-size: 22px; 55 | margin: 10px 0 0; 56 | } 57 | .app { 58 | margin: 50px; 59 | font: 14px sans-serif; 60 | } 61 | .ib { 62 | display: inline-block; 63 | } 64 | .page { 65 | width: 80%; 66 | } 67 | .user { 68 | width: 20%; 69 | vertical-align: top; 70 | } 71 | 72 | .btn { 73 | border: none; 74 | border-radius: 2px; 75 | display: inline-block; 76 | height: 36px; 77 | line-height: 36px; 78 | font-size: 16px; 79 | outline: 0; 80 | padding: 0 2rem; 81 | text-transform: uppercase; 82 | vertical-align: middle; 83 | color: #fff; 84 | background-color: #6383a8; 85 | text-align: center; 86 | letter-spacing: .5px; 87 | transition: .2s ease-out; 88 | cursor: pointer; 89 | margin-right: 3px; 90 | } 91 | .btn:hover { 92 | background-color: #6d8cb0 93 | } 94 | .photo { 95 | display: inline-block; 96 | margin: 10px 2px; 97 | width: 130px; 98 | } 99 | .error { 100 | color: #FF0000; 101 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { store } from './store/configureStore' 5 | import App from './components/App' // изменили путь 6 | 7 | import registerServiceWorker from './registerServiceWorker' 8 | 9 | import './index.css' 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ) 17 | registerServiceWorker() 18 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { pageReducer } from './page' 3 | import { userReducer } from './user' 4 | 5 | export const rootReducer = combineReducers({ 6 | page: pageReducer, 7 | user: userReducer, 8 | }) 9 | -------------------------------------------------------------------------------- /src/reducers/page.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_PHOTOS_REQUEST, 3 | GET_PHOTOS_SUCCESS, 4 | GET_PHOTOS_FAIL, 5 | } from '../actions/PageActions' 6 | import { getCurrentYear } from '../util/date' 7 | 8 | const initialState = { 9 | year: getCurrentYear(), 10 | photos: [], 11 | isFetching: false, 12 | error: '', 13 | } 14 | 15 | export function pageReducer(state = initialState, action) { 16 | switch (action.type) { 17 | case GET_PHOTOS_REQUEST: 18 | return { ...state, year: action.payload, isFetching: true, error: '' } 19 | 20 | case GET_PHOTOS_SUCCESS: 21 | return { ...state, photos: action.payload, isFetching: false, error: '' } 22 | 23 | case GET_PHOTOS_FAIL: 24 | return { ...state, error: action.payload.message, isFetching: false } 25 | 26 | default: 27 | return state 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_REQUEST, 3 | LOGIN_SUCCESS, 4 | LOGIN_FAIL, 5 | } from '../actions/UserActions' 6 | 7 | const initialState = { 8 | name: '', 9 | error: '', 10 | isFetching: false, 11 | } 12 | 13 | export function userReducer(state = initialState, action) { 14 | switch (action.type) { 15 | case LOGIN_REQUEST: 16 | return { ...state, isFetching: true, error: '' } 17 | 18 | case LOGIN_SUCCESS: 19 | return { ...state, isFetching: false, name: action.payload } 20 | 21 | case LOGIN_FAIL: 22 | return { ...state, isFetching: false, error: action.payload.message } 23 | 24 | default: 25 | return state 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { rootReducer } from '../reducers' 3 | import logger from 'redux-logger' 4 | import thunk from 'redux-thunk' 5 | 6 | export const store = createStore(rootReducer, applyMiddleware(thunk, logger)) 7 | -------------------------------------------------------------------------------- /src/util/date.js: -------------------------------------------------------------------------------- 1 | export const getCurrentYear = () => new Date().getFullYear() 2 | 3 | export const getLastYears = number => { 4 | const currentYear = getCurrentYear() 5 | 6 | return Array.from({ length: number }, (el, i) => currentYear - i) // массив состоящий из number последних лет 7 | } 8 | --------------------------------------------------------------------------------