├── .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 |
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 ? Загружаю...
:
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 |
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 |
--------------------------------------------------------------------------------