├── src ├── components │ ├── Profile │ │ ├── Profile.module.css │ │ ├── MyPosts │ │ │ ├── MyPosts.module.css │ │ │ ├── Post │ │ │ │ ├── Post.module.css │ │ │ │ └── Post.tsx │ │ │ ├── MyPostsContainer.tsx │ │ │ ├── AddPostForm │ │ │ │ └── AddPostForm.tsx │ │ │ └── MyPosts.tsx │ │ ├── ProfileInfo │ │ │ ├── ProfileInfo.module.css │ │ │ ├── ProfileStatusWithHooks.tsx │ │ │ ├── ProfileStatus.tsx │ │ │ ├── ProfileDataForm.tsx │ │ │ ├── ProfileStatus.test.tsx │ │ │ └── ProfileInfo.tsx │ │ ├── Profile.tsx │ │ └── ProfileContainer.tsx │ ├── Users │ │ ├── users.module.css │ │ ├── UsersContainer.tsx │ │ ├── UsersSearchForm.tsx │ │ ├── User.tsx │ │ └── Users.tsx │ ├── Navbar │ │ ├── Navbar.module.css │ │ └── Navbar.tsx │ ├── common │ │ ├── Paginator │ │ │ ├── Paginator.module.css │ │ │ ├── Paginator.test.tsx │ │ │ └── Paginator.tsx │ │ ├── FormsControls │ │ │ ├── FormsControls.module.css │ │ │ └── FormsControls.tsx │ │ └── Preloader │ │ │ └── Preloader.tsx │ ├── Header │ │ ├── Header.module.css │ │ └── Header.tsx │ ├── Dialogs │ │ ├── Message │ │ │ └── Message.tsx │ │ ├── Dialogs.module.css │ │ ├── DialogItem │ │ │ └── DialogItem.tsx │ │ ├── DialogsContainer.tsx │ │ ├── AddMessageForm │ │ │ └── AddMessageForm.tsx │ │ └── Dialogs.tsx │ └── Login │ │ └── LoginPage.tsx ├── react-app-env.d.ts ├── assets │ └── images │ │ ├── user.png │ │ └── preloader.svg ├── redux │ ├── sidebar-reducer.ts │ ├── auth-selectors.ts │ ├── users-selectors.ts │ ├── app-reducer.ts │ ├── dialogs-reducer.ts │ ├── redux-store.ts │ ├── users-reducer.test.ts │ ├── users-reducer.thunks.test.ts │ ├── profile-reducer.test.ts │ ├── auth-reducer.ts │ ├── chat-reducer.ts │ ├── profile-reducer.ts │ └── users-reducer.ts ├── setupTests.ts ├── App.test.tsx ├── api │ ├── security-api.ts │ ├── users-api.ts │ ├── api.ts │ ├── auth-api.ts │ ├── profile-api.ts │ └── chat-api.ts ├── utils │ ├── object-helpers.ts │ └── validators │ │ └── validators.ts ├── hoc │ ├── withSuspense.tsx │ └── withAuthRedirect.tsx ├── index.css ├── App.css ├── index.tsx ├── types │ └── types.ts ├── logo.svg ├── pages │ └── Chat │ │ └── ChatPage.tsx ├── serviceWorker.js ├── serviceWorker.ts └── App.tsx ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /src/components/Profile/Profile.module.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-kamasutra/react-way-of-samurai/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-kamasutra/react-way-of-samurai/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-kamasutra/react-way-of-samurai/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/components/Users/users.module.css: -------------------------------------------------------------------------------- 1 | .userPhoto { 2 | width: 100px; 3 | height: 100px; 4 | } 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/it-kamasutra/react-way-of-samurai/HEAD/src/assets/images/user.png -------------------------------------------------------------------------------- /src/components/Profile/MyPosts/MyPosts.module.css: -------------------------------------------------------------------------------- 1 | .postsBlock { 2 | padding: 10px; 3 | } 4 | 5 | .posts { 6 | margin-top: 10px; 7 | } -------------------------------------------------------------------------------- /src/components/Profile/MyPosts/Post/Post.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | color: greenyellow; 3 | } 4 | 5 | .item img { 6 | width: 50px; 7 | height: 50px; 8 | border-radius: 30px; 9 | } -------------------------------------------------------------------------------- /src/components/Profile/ProfileInfo/ProfileInfo.module.css: -------------------------------------------------------------------------------- 1 | .descriptionBlock { 2 | padding: 10px; 3 | } 4 | 5 | .mainPhoto { 6 | max-width: 200px; 7 | } 8 | 9 | .contact { 10 | padding-left: 10px; 11 | } -------------------------------------------------------------------------------- /src/redux/sidebar-reducer.ts: -------------------------------------------------------------------------------- 1 | let initialState = {} 2 | type InitialStateType = typeof initialState 3 | const sidebarReducer = (state = initialState, action: any) => { 4 | return state; 5 | } 6 | 7 | export default sidebarReducer; 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/components/Navbar/Navbar.module.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | grid-area: n; 3 | background-color: burlywood; 4 | padding: 20px; 5 | } 6 | 7 | .item a { 8 | color: white; 9 | text-decoration: none; 10 | } 11 | 12 | .item a.activeLink { 13 | color: gold; 14 | } -------------------------------------------------------------------------------- /src/components/common/Paginator/Paginator.module.css: -------------------------------------------------------------------------------- 1 | 2 | .paginator { 3 | margin: 10px; 4 | } 5 | .pageNumber { 6 | padding: 2px; 7 | border: 1px solid grey; 8 | } 9 | 10 | .pageNumber.selectedPage { 11 | font-weight: bold; 12 | border-color: black; 13 | } -------------------------------------------------------------------------------- /src/components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | grid-area: h; 3 | background-color: green; 4 | } 5 | 6 | .header img { 7 | width: 20px; 8 | } 9 | 10 | .loginBlock { 11 | float: right; 12 | 13 | } 14 | .loginBlock a { 15 | color: white; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/redux/auth-selectors.ts: -------------------------------------------------------------------------------- 1 | import {AppStateType} from './redux-store' 2 | 3 | export const selectIsAuth = (state: AppStateType) => { 4 | return state.auth.isAuth 5 | } 6 | 7 | export const selectCurrentUserLogin = (state: AppStateType) => { 8 | return state.auth.login 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import SamuraiJSApp from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/api/security-api.ts: -------------------------------------------------------------------------------- 1 | import {instance} from './api'; 2 | 3 | type GetCaptchaUrlResponseType = { 4 | url: string 5 | } 6 | 7 | export const securityAPI = { 8 | getCaptchaUrl() { 9 | return instance.get(`security/get-captcha-url`).then(res => res.data); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/object-helpers.ts: -------------------------------------------------------------------------------- 1 | 2 | export const updateObjectInArray = (items: any, itemId: any, objPropName: any, newObjProps: any) => { 3 | return items.map((u: any) => { 4 | if (u[objPropName] === itemId) { 5 | return {...u, ...newObjProps} 6 | } 7 | return u; 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Dialogs/Message/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './../Dialogs.module.css'; 3 | 4 | type PropsType = { 5 | message: string 6 | } 7 | const Message: React.FC = (props) => { 8 | return
{props.message}
9 | } 10 | 11 | export default Message; 12 | -------------------------------------------------------------------------------- /src/components/common/FormsControls/FormsControls.module.css: -------------------------------------------------------------------------------- 1 | .formControl.error input, 2 | .formControl.error textarea 3 | { 4 | border: solid red 2px; 5 | } 6 | 7 | .formControl.error span 8 | { 9 | color: red; 10 | } 11 | 12 | .formSummaryError { 13 | border: red 1px solid; 14 | padding: 5px; 15 | color: red; 16 | } -------------------------------------------------------------------------------- /src/hoc/withSuspense.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export function withSuspense(WrappedComponent: React.ComponentType) { 4 | return (props: WCP) => { 5 | return loading...} > 6 | 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/common/Preloader/Preloader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import preloader from "../../../assets/images/preloader.svg"; 3 | 4 | type PropsType = { 5 | } 6 | 7 | let Preloader: React.FC = () => { 8 | return
9 | 10 |
11 | } 12 | 13 | export default Preloader; 14 | -------------------------------------------------------------------------------- /src/components/Dialogs/Dialogs.module.css: -------------------------------------------------------------------------------- 1 | .dialogs { 2 | display: grid; 3 | grid-template-columns: 2fr 10fr; 4 | } 5 | 6 | .dialogsItems { 7 | padding: 10px; 8 | color: white; 9 | } 10 | 11 | .dialogsItems .active { 12 | color: gold; 13 | } 14 | 15 | .messages { 16 | padding: 10px; 17 | } 18 | 19 | .messages .message { 20 | color: white; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /.idea 26 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/validators/validators.ts: -------------------------------------------------------------------------------- 1 | export type FieldValidatorType = (value: string) => string | undefined 2 | 3 | export const required: FieldValidatorType = (value) => { 4 | if (value) return undefined; 5 | 6 | return "Field is required"; 7 | } 8 | 9 | export const maxLengthCreator = (maxLength: number): FieldValidatorType => (value) => { 10 | if (value.length > maxLength) return `Max length is ${maxLength} symbols`; 11 | return undefined; 12 | } 13 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .app-wrapper { 2 | margin: 0 auto; 3 | display: grid; 4 | width: 1200px; 5 | grid-template-areas: "h h" "n c"; 6 | 7 | grid-template-rows: 60px 1fr; 8 | grid-template-columns: 2fr 10fr; 9 | /* grid-gap: 10px; */ 10 | } 11 | 12 | .app-wrapper-content { 13 | grid-area: c; 14 | background-color: cornflowerblue; 15 | } 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | /* 27 | 28 | /messages 29 | /dialogs 30 | 31 | /dialogs/3 32 | /dialogs/2 33 | 34 | */ 35 | -------------------------------------------------------------------------------- /src/components/Dialogs/DialogItem/DialogItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './../Dialogs.module.css'; 3 | import {NavLink} from "react-router-dom"; 4 | 5 | 6 | type PropsType = { 7 | id: number 8 | name: string 9 | } 10 | 11 | const DialogItem: React.FC = (props) => { 12 | let path = "/dialogs/" + props.id; 13 | 14 | return
15 | {props.name} 16 |
17 | } 18 | 19 | export default DialogItem; 20 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as serviceWorker from './serviceWorker'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import './index.css'; 6 | import SamuraiJSApp from "./App"; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | 11 | // API 12 | // If you want your app to work offline and load faster, you can change 13 | // unregister() to register() below. Note this comes with some pitfalls. 14 | // Learn more about service workers: http://bit.ly/CRA-PWA 15 | serviceWorker.unregister(); 16 | -------------------------------------------------------------------------------- /src/components/Profile/MyPosts/Post/Post.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Post.module.css'; 3 | 4 | type PropsType = { 5 | message: string 6 | likesCount: number 7 | } 8 | 9 | const Post: React.FC = (props) => { 10 | return ( 11 |
12 | 13 | { props.message } 14 |
15 | like { props.likesCount } 16 |
17 |
18 | ) 19 | } 20 | 21 | export default Post; 22 | -------------------------------------------------------------------------------- /src/components/Users/UsersContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {useSelector} from 'react-redux' 3 | import Preloader from '../common/Preloader/Preloader' 4 | import {getIsFetching} from '../../redux/users-selectors' 5 | import {Users} from './Users' 6 | 7 | type UsersPagePropsType = { 8 | pageTitle: string 9 | } 10 | 11 | export const UsersPage: React.FC = (props) => { 12 | const isFetching = useSelector(getIsFetching) 13 | return <> 14 |

{props.pageTitle}

15 | {isFetching ? : null} 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Dialogs/DialogsContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {actions} from '../../redux/dialogs-reducer'; 3 | import Dialogs from './Dialogs'; 4 | import {connect} from 'react-redux'; 5 | import {withAuthRedirect} from '../../hoc/withAuthRedirect'; 6 | import {compose} from 'redux'; 7 | import {AppStateType} from '../../redux/redux-store'; 8 | 9 | let mapStateToProps = (state: AppStateType) => { 10 | return { 11 | dialogsPage: state.dialogsPage 12 | } 13 | } 14 | 15 | export default compose( 16 | connect(mapStateToProps, {...actions}), 17 | withAuthRedirect 18 | )(Dialogs) 19 | -------------------------------------------------------------------------------- /src/components/Profile/MyPosts/MyPostsContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {actions} from "../../../redux/profile-reducer"; 3 | import MyPosts, {DispatchPropsType, MapPropsType} from "./MyPosts"; 4 | import {connect} from "react-redux"; 5 | import {AppStateType} from '../../../redux/redux-store'; 6 | 7 | const mapStateToProps = (state: AppStateType) => { 8 | return { 9 | posts: state.profilePage.posts 10 | } 11 | } 12 | 13 | const MyPostsContainer = connect(mapStateToProps, { 14 | addPost: actions.addPostActionCreator 15 | })(MyPosts); 16 | 17 | export default MyPostsContainer; 18 | -------------------------------------------------------------------------------- /src/api/users-api.ts: -------------------------------------------------------------------------------- 1 | import {GetItemsType, instance, APIResponseType} from './api'; 2 | 3 | 4 | export const usersAPI = { 5 | getUsers(currentPage = 1, pageSize = 10, term: string = '', friend: null | boolean = null) { 6 | return instance.get(`users?page=${currentPage}&count=${pageSize}&term=${term}` + (friend === null ? '' : `&friend=${friend}`) ) 7 | .then(res => res.data) 8 | }, 9 | follow(userId: number) { 10 | return instance.post(`follow/${userId}`).then(res => res.data) 11 | }, 12 | unfollow(userId: number) { 13 | return instance.delete(`follow/${userId}`).then(res => res.data) as Promise 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import {UserType} from '../types/types'; 3 | 4 | export const instance = axios.create({ 5 | withCredentials: true, 6 | baseURL: 'https://social-network.samuraijs.com/api/1.0/', 7 | headers: { 8 | "API-KEY": "b1775b2f-c3a5-4509-8dc9-90b5629de7c3" 9 | } 10 | }); 11 | 12 | export enum ResultCodesEnum { 13 | Success = 0, 14 | Error = 1 15 | } 16 | 17 | export enum ResultCodeForCapcthaEnum { 18 | CaptchaIsRequired = 10 19 | } 20 | 21 | export type GetItemsType = { 22 | items: Array 23 | totalCount: number 24 | error: string | null 25 | } 26 | export type APIResponseType = { 27 | data: D 28 | messages: Array 29 | resultCode: RC 30 | } 31 | -------------------------------------------------------------------------------- /src/api/auth-api.ts: -------------------------------------------------------------------------------- 1 | import {instance, APIResponseType, ResultCodeForCapcthaEnum, ResultCodesEnum} from "./api"; 2 | 3 | type MeResponseDataType = { 4 | id: number 5 | email: string 6 | login: string 7 | } 8 | type LoginResponseDataType = { 9 | userId: number 10 | } 11 | 12 | export const authAPI = { 13 | me() { 14 | return instance.get>(`auth/me`).then(res => res.data); 15 | }, 16 | login(email: string, password: string, rememberMe = false, captcha: null | string = null) { 17 | return instance.post>(`auth/login`, {email, password, rememberMe, captcha}) 18 | .then(res => res.data); 19 | }, 20 | logout() { 21 | return instance.delete(`auth/login`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | export type PostType = { 2 | id: number 3 | message: string 4 | likesCount: number 5 | } 6 | export type ContactsType = { 7 | github: string 8 | vk: string 9 | facebook: string 10 | instagram: string 11 | twitter: string 12 | website: string 13 | youtube: string 14 | mainLink: string 15 | } 16 | export type PhotosType = { 17 | small: string | null 18 | large: string | null 19 | } 20 | export type ProfileType = { 21 | userId: number 22 | lookingForAJob: boolean 23 | lookingForAJobDescription: string 24 | fullName: string 25 | contacts: ContactsType 26 | photos: PhotosType 27 | aboutMe: string 28 | } 29 | 30 | export type UserType = { 31 | id: number 32 | name: string 33 | status: string 34 | photos: PhotosType 35 | followed: boolean 36 | } 37 | -------------------------------------------------------------------------------- /src/components/common/Paginator/Paginator.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { create } from "react-test-renderer"; 3 | import Paginator from "./Paginator"; 4 | 5 | describe("Paginator component tests", () => { 6 | test("pages count is 11 but should be showed only 10", () => { 7 | const component = create(); 8 | const root = component.root; 9 | let spans = root.findAllByType("span"); 10 | expect(spans.length).toBe(10); 11 | }); 12 | 13 | test("if pages count is more then 10 button NEXT should be present", () => { 14 | const component = create(); 15 | const root = component.root; 16 | let button = root.findAllByType("button"); 17 | expect(button.length).toBe(1); 18 | }); 19 | }); -------------------------------------------------------------------------------- /src/components/Profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProfileInfo from "./ProfileInfo/ProfileInfo"; 3 | import MyPostsContainer from "./MyPosts/MyPostsContainer"; 4 | import {ProfileType} from '../../types/types'; 5 | 6 | type PropsType = { 7 | profile: ProfileType | null 8 | status: string 9 | updateStatus: (status: string) => void 10 | isOwner: boolean 11 | savePhoto: (file: File) => void 12 | saveProfile: (profile: ProfileType) => Promise 13 | } 14 | 15 | const Profile:React.FC = (props) => { 16 | return ( 17 |
18 | 24 | 25 |
26 | ) 27 | } 28 | 29 | export default Profile; 30 | -------------------------------------------------------------------------------- /src/hoc/withAuthRedirect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Redirect} from "react-router-dom"; 3 | import {connect} from "react-redux"; 4 | import {AppStateType} from '../redux/redux-store'; 5 | 6 | let mapStateToPropsForRedirect = (state: AppStateType) => ({ 7 | isAuth: state.auth.isAuth 8 | } as MapPropsType); 9 | 10 | type MapPropsType = { 11 | isAuth: boolean 12 | } 13 | type DispatchPropsType = { 14 | } 15 | 16 | export function withAuthRedirect(WrappedComponent: React.ComponentType) { 17 | 18 | const RedirectComponent: React.FC = (props) => { 19 | let {isAuth, ...restProps} = props 20 | 21 | if (!isAuth) return 22 | 23 | return 24 | } 25 | 26 | let ConnectedAuthRedirectComponent = connect( 27 | mapStateToPropsForRedirect, {}) 28 | (RedirectComponent) 29 | 30 | return ConnectedAuthRedirectComponent; 31 | } 32 | -------------------------------------------------------------------------------- /src/redux/users-selectors.ts: -------------------------------------------------------------------------------- 1 | import {createSelector} from "reselect"; 2 | import { AppStateType } from "./redux-store"; 3 | 4 | const getUsersSelector = (state: AppStateType) => { 5 | return state.usersPage.users; 6 | } 7 | 8 | export const getUsers = createSelector(getUsersSelector, 9 | (users) => { 10 | return users.filter(u => true); 11 | }) 12 | 13 | export const getPageSize = (state: AppStateType) => { 14 | return state.usersPage.pageSize; 15 | } 16 | 17 | export const getTotalUsersCount = (state: AppStateType) => { 18 | return state.usersPage.totalUsersCount; 19 | } 20 | 21 | export const getCurrentPage = (state: AppStateType) => { 22 | return state.usersPage.currentPage; 23 | } 24 | 25 | export const getIsFetching = (state: AppStateType) => { 26 | return state.usersPage.isFetching; 27 | } 28 | 29 | export const getFollowingInProgress = (state: AppStateType) => { 30 | return state.usersPage.followingInProgress; 31 | } 32 | export const getUsersFilter = (state: AppStateType) => { 33 | return state.usersPage.filter; 34 | } 35 | -------------------------------------------------------------------------------- /src/redux/app-reducer.ts: -------------------------------------------------------------------------------- 1 | import {getAuthUserData} from "./auth-reducer" 2 | import {InferActionsTypes} from './redux-store'; 3 | 4 | let initialState = { 5 | initialized: false 6 | }; 7 | 8 | export type InitialStateType = typeof initialState 9 | type ActionsType = InferActionsTypes 10 | 11 | const appReducer = (state = initialState, action: ActionsType): InitialStateType => { 12 | switch (action.type) { 13 | case 'SN/APP/INITIALIZED_SUCCESS': 14 | return { 15 | ...state, 16 | initialized: true 17 | } 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export const actions = { 24 | initializedSuccess: () => ({type: 'SN/APP/INITIALIZED_SUCCESS'} as const) 25 | } 26 | 27 | export const initializeApp = () => (dispatch: any) => { 28 | let promise = dispatch(getAuthUserData()); 29 | 30 | Promise.all([promise]) 31 | .then(() => { 32 | dispatch(actions.initializedSuccess()); 33 | }); 34 | } 35 | 36 | 37 | export default appReducer; 38 | -------------------------------------------------------------------------------- /src/components/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Navbar.module.css'; 3 | import {NavLink} from "react-router-dom"; 4 | 5 | const Navbar: React.FC = () => { 6 | return ( 7 | 28 | ) 29 | } 30 | 31 | export default Navbar; 32 | -------------------------------------------------------------------------------- /src/components/Profile/MyPosts/AddPostForm/AddPostForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {InjectedFormProps, reduxForm} from "redux-form"; 3 | import {createField, GetStringKeys, Input} from '../../../common/FormsControls/FormsControls'; 4 | import {required} from '../../../../utils/validators/validators'; 5 | 6 | type PropsType = { 7 | 8 | } 9 | 10 | export type AddPostFormValuesType = { 11 | newPostText: string 12 | } 13 | 14 | type AddPostFormValuesTypeKeys = GetStringKeys 15 | 16 | const AddPostForm: React.FC & PropsType> = (props) => { 17 | return ( 18 |
19 |
20 | { createField("Your post", 'newPostText', [required], Input) } 21 |
22 |
23 | 24 |
25 |
26 | ) 27 | } 28 | 29 | export default reduxForm({form: 'profile-add-post'})(AddPostForm) 30 | -------------------------------------------------------------------------------- /src/api/profile-api.ts: -------------------------------------------------------------------------------- 1 | import {PhotosType, ProfileType} from '../types/types'; 2 | import {instance, APIResponseType} from './api'; 3 | 4 | type SavePhotoResponseDataType = { 5 | photos: PhotosType 6 | } 7 | 8 | export const profileAPI = { 9 | getProfile(userId: number) { 10 | return instance.get(`profile/` + userId).then(res => res.data) 11 | }, 12 | getStatus(userId: number) { 13 | return instance.get(`profile/status/` + userId).then(res => res.data) 14 | }, 15 | updateStatus(status: string) { 16 | return instance.put(`profile/status`, {status: status}).then(res => res.data); 17 | }, 18 | savePhoto(photoFile: File) { 19 | const formData = new FormData(); 20 | formData.append("image", photoFile); 21 | 22 | return instance.put>(`profile/photo`, formData, { 23 | headers: { 24 | 'Content-Type': 'multipart/form-data' 25 | } 26 | }).then(res => res.data); 27 | }, 28 | saveProfile(profile: ProfileType) { 29 | return instance.put(`profile`, profile).then(res => res.data); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Dialogs/AddMessageForm/AddMessageForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Field, InjectedFormProps, reduxForm} from "redux-form"; 3 | import {createField, Input, Textarea} from '../../common/FormsControls/FormsControls'; 4 | import {maxLengthCreator, required} from "../../../utils/validators/validators"; 5 | import {NewMessageFormValuesType} from '../Dialogs'; 6 | import {LoginFormValuesType} from '../../Login/LoginPage'; 7 | 8 | const maxLength50 = maxLengthCreator(50); 9 | 10 | type NewMessageFormValuesKeysType = Extract 11 | type PropsType = {} 12 | 13 | const AddMessageForm: React.FC & PropsType> 14 | = (props) => { 15 | return ( 16 |
17 |
18 | {createField("Enter your message", 'newMessageBody', [required, maxLength50], Textarea)} 19 |
20 |
21 | 22 |
23 |
24 | ) 25 | } 26 | 27 | export default reduxForm({form: 'dialog-add-message-form'})(AddMessageForm); 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/components/Profile/MyPosts/MyPosts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './MyPosts.module.css'; 3 | import Post from './Post/Post'; 4 | import {Field, reduxForm} from "redux-form"; 5 | import {maxLengthCreator, required} from "../../../utils/validators/validators"; 6 | import {Textarea} from "../../common/FormsControls/FormsControls"; 7 | import AddPostForm, {AddPostFormValuesType} from './AddPostForm/AddPostForm'; 8 | import {PostType} from '../../../types/types'; 9 | 10 | 11 | export type MapPropsType = { 12 | posts: Array 13 | } 14 | export type DispatchPropsType = { 15 | addPost: (newPostText: string) => void 16 | } 17 | 18 | const MyPosts: React.FC = props => { 19 | let postsElements = 20 | [...props.posts] 21 | .reverse() 22 | .map(p => ); 23 | 24 | let onAddPost = (values: AddPostFormValuesType) => { 25 | props.addPost(values.newPostText); 26 | } 27 | 28 | return ( 29 |
30 |

My posts

31 | 32 |
33 | {postsElements} 34 |
35 |
36 | ) 37 | } 38 | 39 | const MyPostsMemorized = React.memo(MyPosts); 40 | 41 | export default MyPostsMemorized; 42 | -------------------------------------------------------------------------------- /src/components/Profile/ProfileInfo/ProfileStatusWithHooks.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, ChangeEvent} from 'react'; 2 | 3 | type PropsType = { 4 | status: string 5 | updateStatus: (status: string) => void 6 | } 7 | 8 | const ProfileStatusWithHooks: React.FC = (props) => { 9 | 10 | let [editMode, setEditMode] = useState(false); 11 | let [status, setStatus] = useState(props.status); 12 | 13 | useEffect(() => { 14 | setStatus(props.status); 15 | }, [props.status]); 16 | 17 | const activateEditMode = () => { 18 | setEditMode(true); 19 | } 20 | 21 | const deactivateEditMode = () => { 22 | setEditMode(false); 23 | props.updateStatus(status); 24 | } 25 | 26 | const onStatusChange = (e: ChangeEvent) => { 27 | setStatus(e.currentTarget.value); 28 | } 29 | 30 | return ( 31 |
32 | {!editMode && 33 |
34 | Status: {props.status || "-------"} 35 |
36 | } 37 | {editMode && 38 |
39 | 41 |
42 | } 43 |
44 | ) 45 | } 46 | 47 | 48 | export default ProfileStatusWithHooks; 49 | -------------------------------------------------------------------------------- /src/redux/dialogs-reducer.ts: -------------------------------------------------------------------------------- 1 | import {InferActionsTypes} from './redux-store'; 2 | 3 | type DialogType = { 4 | id: number 5 | name: string 6 | } 7 | type MessageType = { 8 | id: number 9 | message: string 10 | } 11 | 12 | let initialState = { 13 | dialogs: [ 14 | {id: 1, name: 'Dimych'}, 15 | {id: 2, name: 'Andrew'}, 16 | {id: 3, name: 'Sveta'}, 17 | {id: 4, name: 'Sasha'}, 18 | {id: 5, name: 'Viktor'}, 19 | {id: 6, name: 'Valera'} 20 | ] as Array, 21 | messages: [ 22 | {id: 1, message: 'Hi'}, 23 | {id: 2, message: 'How is your it-kamasutra?'}, 24 | {id: 3, message: 'Yo'}, 25 | {id: 4, message: 'Yo'}, 26 | {id: 5, message: 'Yo'} 27 | ] as Array 28 | } 29 | 30 | const dialogsReducer = (state = initialState, action: ActionsType): InitialStateType => { 31 | switch (action.type) { 32 | case 'SN/DIALOGS/SEND_MESSAGE': 33 | let body = action.newMessageBody; 34 | return { 35 | ...state, 36 | messages: [...state.messages, {id: 6, message: body}] 37 | }; 38 | default: 39 | return state; 40 | } 41 | } 42 | 43 | export const actions = { 44 | sendMessage: (newMessageBody: string) => ({type: 'SN/DIALOGS/SEND_MESSAGE', newMessageBody} as const) 45 | } 46 | 47 | export default dialogsReducer; 48 | 49 | export type InitialStateType = typeof initialState 50 | type ActionsType = InferActionsTypes 51 | -------------------------------------------------------------------------------- /src/redux/redux-store.ts: -------------------------------------------------------------------------------- 1 | import {Action, applyMiddleware, combineReducers, compose, createStore} from "redux"; 2 | import profileReducer from "./profile-reducer"; 3 | import dialogsReducer from "./dialogs-reducer"; 4 | import sidebarReducer from "./sidebar-reducer"; 5 | import usersReducer from "./users-reducer"; 6 | import authReducer from "./auth-reducer"; 7 | import thunkMiddleware, {ThunkAction} from "redux-thunk"; 8 | import {reducer as formReducer} from 'redux-form' 9 | import appReducer from "./app-reducer"; 10 | import chatReducer from './chat-reducer' 11 | 12 | let rootReducer = combineReducers({ 13 | profilePage: profileReducer, 14 | dialogsPage: dialogsReducer, 15 | sidebar: sidebarReducer, 16 | usersPage: usersReducer, 17 | auth: authReducer, 18 | form: formReducer, 19 | app: appReducer, 20 | chat: chatReducer 21 | }) 22 | 23 | type RootReducerType = typeof rootReducer; // (globalstate: AppStateType) => AppStateType 24 | export type AppStateType = ReturnType 25 | 26 | export type InferActionsTypes = T extends { [keys: string]: (...args: any[]) => infer U } ? U : never 27 | 28 | export type BaseThunkType> = ThunkAction 29 | 30 | 31 | // @ts-ignore 32 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 33 | 34 | const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunkMiddleware))) 35 | // @ts-ignore 36 | window.__store__ = store 37 | 38 | export default store 39 | -------------------------------------------------------------------------------- /src/redux/users-reducer.test.ts: -------------------------------------------------------------------------------- 1 | import usersReducer, {actions, InitialState} from './users-reducer' 2 | 3 | let state: InitialState; 4 | 5 | beforeEach(() => { 6 | state = { 7 | users: [ 8 | { 9 | id: 0, name: 'Dimych 0', followed: false, 10 | photos: {small: null, large: null}, status: 'status 0' 11 | }, 12 | { 13 | id: 1, name: 'Dimych 1', followed: false, 14 | photos: {small: null, large: null}, status: 'status 1' 15 | }, 16 | { 17 | id: 2, name: 'Dimych 2', followed: true, 18 | photos: {small: null, large: null}, status: 'status 2' 19 | }, 20 | { 21 | id: 3, name: 'Dimych 3', followed: true, 22 | photos: {small: null, large: null}, status: 'status 3' 23 | }, 24 | ], 25 | pageSize: 10, 26 | totalUsersCount: 0, 27 | currentPage: 1, 28 | isFetching: false, 29 | followingInProgress: [] 30 | } 31 | }) 32 | 33 | test('follow success', () => { 34 | const newState = usersReducer(state, actions.followSuccess(1)) 35 | 36 | expect(newState.users[0].followed).toBeFalsy(); 37 | expect(newState.users[1].followed).toBeTruthy(); 38 | }) 39 | 40 | test('unfollow success', () => { 41 | const newState = usersReducer(state, actions.unfollowSuccess(3)) 42 | 43 | expect(newState.users[2].followed).toBeTruthy(); 44 | expect(newState.users[3].followed).toBeFalsy(); 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/Dialogs/Dialogs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import s from './Dialogs.module.css'; 3 | import DialogItem from "./DialogItem/DialogItem"; 4 | import Message from "./Message/Message"; 5 | import {Redirect} from "react-router-dom"; 6 | import AddMessageForm from "./AddMessageForm/AddMessageForm"; 7 | import {InitialStateType} from '../../redux/dialogs-reducer'; 8 | 9 | type PropsType = { 10 | dialogsPage: InitialStateType 11 | sendMessage: (messageText: string) => void 12 | } 13 | 14 | export type NewMessageFormValuesType = { 15 | newMessageBody: string 16 | } 17 | 18 | const Dialogs: React.FC = (props) => { 19 | let state = props.dialogsPage; 20 | 21 | let dialogsElements = state.dialogs.map( d => ); 22 | let messagesElements = state.messages.map( m => ); 23 | 24 | let addNewMessage = (values: NewMessageFormValuesType) => { 25 | props.sendMessage(values.newMessageBody); 26 | } 27 | 28 | return ( 29 |
30 |
31 | { dialogsElements } 32 |
33 |
34 |
{ messagesElements }
35 |
36 | 37 |
38 | ) 39 | } 40 | 41 | export default Dialogs; 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/redux/users-reducer.thunks.test.ts: -------------------------------------------------------------------------------- 1 | import {actions, follow, unfollow} from './users-reducer' 2 | import {usersAPI} from '../api/users-api' 3 | import {APIResponseType, ResultCodesEnum} from '../api/api' 4 | 5 | jest.mock('../api/users-api') 6 | const userAPIMock = usersAPI as jest.Mocked; 7 | 8 | const dispatchMock = jest.fn(); 9 | const getStateMock = jest.fn(); 10 | 11 | beforeEach(() => { 12 | dispatchMock.mockClear(); 13 | getStateMock.mockClear(); 14 | userAPIMock.follow.mockClear(); 15 | userAPIMock.unfollow.mockClear(); 16 | }) 17 | 18 | 19 | const result: APIResponseType = { 20 | resultCode: ResultCodesEnum.Success, 21 | messages: [], 22 | data: {} 23 | } 24 | 25 | userAPIMock.follow.mockReturnValue(Promise.resolve(result)); 26 | userAPIMock.unfollow.mockReturnValue(Promise.resolve(result)); 27 | 28 | 29 | 30 | test('success follow thunk', async () => { 31 | const thunk = follow(1) 32 | 33 | await thunk(dispatchMock, getStateMock, {}) 34 | 35 | expect(dispatchMock).toBeCalledTimes(3) 36 | expect(dispatchMock).toHaveBeenNthCalledWith(1, actions.toggleFollowingProgress(true, 1)) 37 | expect(dispatchMock).toHaveBeenNthCalledWith(2, actions.followSuccess(1)) 38 | expect(dispatchMock).toHaveBeenNthCalledWith(3, actions.toggleFollowingProgress(false, 1)) 39 | }) 40 | 41 | test('success unfollow thunk', async () => { 42 | const thunk = unfollow(1) 43 | 44 | await thunk(dispatchMock, getStateMock, {}) 45 | 46 | expect(dispatchMock).toBeCalledTimes(3) 47 | expect(dispatchMock).toHaveBeenNthCalledWith(1, actions.toggleFollowingProgress(true, 1)) 48 | expect(dispatchMock).toHaveBeenNthCalledWith(2, actions.unfollowSuccess(1)) 49 | expect(dispatchMock).toHaveBeenNthCalledWith(3, actions.toggleFollowingProgress(false, 1)) 50 | }) 51 | -------------------------------------------------------------------------------- /src/redux/profile-reducer.test.ts: -------------------------------------------------------------------------------- 1 | import profileReducer, {actions} from './profile-reducer'; 2 | import React from 'react'; 3 | import {ProfileType} from '../types/types'; 4 | 5 | let state = { 6 | posts: [ 7 | {id: 1, message: 'Hi, how are you?', likesCount: 12}, 8 | {id: 2, message: 'It\'s my first post', likesCount: 11}, 9 | {id: 3, message: 'Blabla', likesCount: 11}, 10 | {id: 4, message: 'Dada', likesCount: 11} 11 | ], 12 | profile: null, 13 | status: '', 14 | }; 15 | 16 | it('length of posts should be incremented', () => { 17 | // 1. test data 18 | let action = actions.addPostActionCreator("it-kamasutra.com"); 19 | 20 | // 2. action 21 | let newState = profileReducer(state, action); 22 | 23 | // 3. expectation 24 | expect(newState.posts.length).toBe(5); 25 | 26 | }); 27 | 28 | it('message of new post should be correct', () => { 29 | // 1. test data 30 | let action = actions.addPostActionCreator("it-kamasutra.com"); 31 | 32 | // 2. action 33 | let newState = profileReducer(state, action); 34 | 35 | // 3. expectation 36 | expect(newState.posts[4].message).toBe("it-kamasutra.com"); 37 | }); 38 | 39 | it('after deleting length of messages should be decrement', () => { 40 | // 1. test data 41 | let action = actions.deletePost(1); 42 | 43 | // 2. action 44 | let newState = profileReducer(state, action); 45 | 46 | // 3. expectation 47 | expect(newState.posts.length).toBe(3); 48 | }); 49 | 50 | it(`after deleting length shouldn't be decrement if id is incorrect`, () => { 51 | // 1. test data 52 | let action = actions.deletePost(1000); 53 | 54 | // 2. action 55 | let newState = profileReducer(state, action); 56 | 57 | // 3. expectation 58 | expect(newState.posts.length).toBe(4); 59 | }); 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-way-of-samurai", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://it-kamasutra.github.io/react-way-of-samurai", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "@types/classnames": "^2.2.10", 11 | "@types/jest": "^24.0.0", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^16.9.0", 14 | "@types/react-dom": "^16.9.0", 15 | "@types/react-redux": "^7.1.9", 16 | "@types/react-router-dom": "^5.1.5", 17 | "@types/redux-form": "^8.2.6", 18 | "antd": "^4.7.2", 19 | "axios": "^0.19.2", 20 | "classnames": "^2.2.6", 21 | "formik": "^2.1.5", 22 | "react": "^16.13.1", 23 | "react-dom": "^16.13.1", 24 | "react-redux": "^7.2.0", 25 | "react-router-dom": "^5.2.0", 26 | "react-scripts": "3.4.1", 27 | "redux": "^4.0.5", 28 | "redux-form": "^8.3.6", 29 | "redux-thunk": "^2.3.0", 30 | "reselect": "^4.0.0", 31 | "typescript": "~3.7.2", 32 | "uuid": "^8.3.2" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject", 39 | "predeploy": "npm run build", 40 | "deploy": "gh-pages -d build" 41 | }, 42 | "eslintConfig": { 43 | "extends": "react-app" 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | }, 57 | "devDependencies": { 58 | "@types/react-test-renderer": "^16.9.2", 59 | "@types/uuid": "^8.3.0", 60 | "gh-pages": "^3.0.0", 61 | "react-test-renderer": "^16.13.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Profile/ProfileInfo/ProfileStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, {ChangeEvent} from 'react'; 2 | 3 | type PropsType = { 4 | status: string 5 | updateStatus: (newStatus: string) => void 6 | } 7 | type StateType = { 8 | editMode: boolean 9 | status: string 10 | } 11 | 12 | class ProfileStatus extends React.Component { 13 | state = { 14 | editMode: false, 15 | status: this.props.status 16 | } 17 | 18 | activateEditMode = () => { 19 | this.setState({ 20 | editMode: true 21 | }); 22 | } 23 | 24 | deactivateEditMode() { 25 | this.setState({ 26 | editMode: false 27 | }); 28 | this.props.updateStatus(this.state.status); 29 | } 30 | 31 | onStatusChange = (e: ChangeEvent) => { 32 | this.setState({ 33 | status: e.currentTarget.value 34 | }); 35 | } 36 | 37 | componentDidUpdate(prevProps: PropsType, prevState: StateType) { 38 | 39 | if (prevProps.status !== this.props.status) { 40 | this.setState({ 41 | status: this.props.status 42 | }); 43 | } 44 | } 45 | 46 | render() { 47 | 48 | return ( 49 |
50 | {!this.state.editMode && 51 |
52 | {this.props.status || "-------"} 53 |
54 | } 55 | {this.state.editMode && 56 |
57 | 60 |
61 | } 62 |
63 | ) 64 | } 65 | } 66 | 67 | export default ProfileStatus; 68 | -------------------------------------------------------------------------------- /src/components/common/Paginator/Paginator.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import styles from './Paginator.module.css' 3 | import cn from 'classnames' 4 | 5 | type PropsType = { 6 | totalItemsCount: number 7 | pageSize: number 8 | currentPage?: number 9 | onPageChanged?: (pageNumber: number) => void 10 | portionSize?: number 11 | } 12 | 13 | let Paginator: React.FC = ({totalItemsCount, pageSize, 14 | currentPage = 1, 15 | onPageChanged = x => x, 16 | portionSize = 10}) => { 17 | 18 | let pagesCount = Math.ceil(totalItemsCount / pageSize); 19 | 20 | let pages: Array = []; 21 | for (let i = 1; i <= pagesCount; i++) { 22 | pages.push(i); 23 | } 24 | 25 | let portionCount = Math.ceil(pagesCount / portionSize); 26 | let [portionNumber, setPortionNumber] = useState(1); 27 | 28 | let leftPortionPageNumber = (portionNumber - 1) * portionSize + 1; 29 | let rightPortionPageNumber = portionNumber * portionSize; 30 | 31 | 32 | return
33 | { portionNumber > 1 && 34 | } 35 | 36 | {pages 37 | .filter(p => p >= leftPortionPageNumber && p<=rightPortionPageNumber) 38 | .map((p) => { 39 | return { 44 | onPageChanged(p); 45 | }}>{p} 46 | })} 47 | { portionCount > portionNumber && 48 | } 49 | 50 | 51 |
52 | } 53 | 54 | export default Paginator; 55 | -------------------------------------------------------------------------------- /src/components/common/FormsControls/FormsControls.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styles from "./FormsControls.module.css" 3 | import {FieldValidatorType} from "../../../utils/validators/validators" 4 | import {Field, WrappedFieldProps} from "redux-form" 5 | import {WrappedFieldMetaProps} from 'redux-form/lib/Field' 6 | import {LoginFormValuesType} from '../../Login/LoginPage'; 7 | 8 | type FormControlPropsType = { 9 | meta: WrappedFieldMetaProps 10 | } 11 | 12 | const FormControl: React.FC = ({meta: {touched, error}, children}) => { 13 | const hasError = touched && error; 14 | return ( 15 |
16 |
17 | {children} 18 |
19 | {hasError && {error}} 20 |
21 | ) 22 | } 23 | 24 | export const Textarea: React.FC = (props) => { 25 | //const {input, meta, child, ...restProps} = props; 26 | const {input, meta, ...restProps} = props; 27 | return 94 | 95 |
96 | 97 |
98 | 99 | } 100 | 101 | export default ChatPage 102 | -------------------------------------------------------------------------------- /src/assets/images/preloader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/Profile/ProfileInfo/ProfileInfo.tsx: -------------------------------------------------------------------------------- 1 | import React, {ChangeEvent, useState} from 'react'; 2 | import s from './ProfileInfo.module.css'; 3 | import Preloader from "../../common/Preloader/Preloader"; 4 | import ProfileStatusWithHooks from "./ProfileStatusWithHooks"; 5 | import userPhoto from "../../../assets/images/user.png"; 6 | import ProfileDataForm from "./ProfileDataForm"; 7 | import {ContactsType, ProfileType} from '../../../types/types'; 8 | 9 | type PropsType = { 10 | profile: ProfileType | null 11 | status: string 12 | updateStatus: (status: string) => void 13 | isOwner: boolean 14 | savePhoto: (file: File) => void 15 | saveProfile: (profile: ProfileType) => Promise 16 | } 17 | 18 | const ProfileInfo: React.FC = ({profile, status, updateStatus, isOwner, savePhoto, saveProfile}) => { 19 | 20 | let [editMode, setEditMode] = useState(false); 21 | 22 | if (!profile) { 23 | return 24 | } 25 | 26 | const onMainPhotoSelected = (e: ChangeEvent) => { 27 | if (e.target.files && e.target.files.length) { 28 | savePhoto(e.target.files[0]); 29 | } 30 | } 31 | 32 | const onSubmit = (formData: ProfileType) => { 33 | // todo: remove then 34 | saveProfile(formData).then( 35 | () => { 36 | setEditMode(false); 37 | } 38 | ); 39 | } 40 | 41 | return ( 42 |
43 |
44 | 45 | {isOwner && } 46 | 47 | { editMode 48 | ? 49 | : {setEditMode(true)} } profile={profile} isOwner={isOwner}/> } 50 | 51 | 52 |
53 |
54 | ) 55 | } 56 | 57 | type ProfileDataPropsType = { 58 | profile: ProfileType 59 | isOwner: boolean 60 | goToEditMode: () => void 61 | } 62 | const ProfileData: React.FC = ({profile, isOwner, goToEditMode}) => { 63 | return
64 | {isOwner &&
} 65 |
66 | Full name: {profile.fullName} 67 |
68 |
69 | Looking for a job: {profile.lookingForAJob ? "yes" : "no"} 70 |
71 | {profile.lookingForAJob && 72 |
73 | My professional skills: {profile.lookingForAJobDescription} 74 |
75 | } 76 | 77 |
78 | About me: {profile.aboutMe} 79 |
80 |
81 | Contacts: { 82 | Object 83 | .keys(profile.contacts) 84 | .map((key) => { 85 | return 86 | })} 87 |
88 |
89 | } 90 | 91 | 92 | type ContactsPropsType = { 93 | contactTitle: string 94 | contactValue: string 95 | } 96 | const Contact: React.FC = ({contactTitle, contactValue}) => { 97 | return
{contactTitle}: {contactValue}
98 | } 99 | 100 | export default ProfileInfo; 101 | -------------------------------------------------------------------------------- /src/components/Users/Users.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC, useEffect} from 'react' 2 | import Paginator from '../common/Paginator/Paginator' 3 | import User from './User' 4 | import {UsersSearchForm} from './UsersSearchForm' 5 | import {FilterType, requestUsers} from '../../redux/users-reducer' 6 | import {useDispatch, useSelector} from 'react-redux' 7 | import { 8 | getCurrentPage, 9 | getFollowingInProgress, 10 | getPageSize, 11 | getTotalUsersCount, 12 | getUsers, 13 | getUsersFilter 14 | } from '../../redux/users-selectors' 15 | import {useHistory} from 'react-router-dom' 16 | import * as queryString from 'querystring' 17 | 18 | type PropsType = {} 19 | 20 | type QueryParamsType = { term?: string; page?: string; friend?: string } 21 | export const Users: FC = (props) => { 22 | 23 | const users = useSelector(getUsers) 24 | const totalUsersCount = useSelector(getTotalUsersCount) 25 | const currentPage = useSelector(getCurrentPage) 26 | const pageSize = useSelector(getPageSize) 27 | const filter = useSelector(getUsersFilter) 28 | const followingInProgress = useSelector(getFollowingInProgress) 29 | 30 | const dispatch = useDispatch() 31 | const history = useHistory() 32 | 33 | useEffect(() => { 34 | const parsed = queryString.parse(history.location.search.substr(1)) as QueryParamsType 35 | 36 | let actualPage = currentPage 37 | let actualFilter = filter 38 | 39 | if (!!parsed.page) actualPage = Number(parsed.page) 40 | 41 | 42 | if (!!parsed.term) actualFilter = {...actualFilter, term: parsed.term as string} 43 | 44 | switch(parsed.friend) { 45 | case "null": 46 | actualFilter = {...actualFilter, friend: null} 47 | break; 48 | case "true": 49 | actualFilter = {...actualFilter, friend: true} 50 | break; 51 | case "false": 52 | actualFilter = {...actualFilter, friend: false} 53 | break; 54 | } 55 | 56 | dispatch(requestUsers(actualPage, pageSize, actualFilter)) 57 | }, []) 58 | 59 | useEffect(() => { 60 | const query: QueryParamsType = {} 61 | 62 | if (!!filter.term) query.term = filter.term 63 | if (filter.friend !== null) query.friend = String(filter.friend) 64 | if (currentPage !== 1) query.page = String(currentPage) 65 | 66 | history.push({ 67 | pathname: '/developers', 68 | search: queryString.stringify(query) 69 | }) 70 | }, [filter, currentPage]) 71 | 72 | 73 | const onPageChanged = (pageNumber: number) => { 74 | dispatch(requestUsers(pageNumber, pageSize, filter)) 75 | } 76 | const onFilterChanged = (filter: FilterType) => { 77 | dispatch(requestUsers(1, pageSize, filter)) 78 | } 79 | const follow = (userId: number) => { 80 | dispatch(follow(userId)); 81 | } 82 | const unfollow = (userId: number) => { 83 | dispatch(unfollow(userId)); 84 | } 85 | 86 | return
87 | 88 | 89 | 90 | 92 |
93 | { 94 | users.map(u => 100 | ) 101 | } 102 |
103 |
104 | } 105 | -------------------------------------------------------------------------------- /src/redux/profile-reducer.ts: -------------------------------------------------------------------------------- 1 | import {FormAction, stopSubmit} from "redux-form"; 2 | import {PhotosType, PostType, ProfileType} from '../types/types'; 3 | import {usersAPI} from '../api/users-api'; 4 | import {profileAPI} from '../api/profile-api'; 5 | import {BaseThunkType, InferActionsTypes} from './redux-store'; 6 | 7 | let initialState = { 8 | posts: [ 9 | {id: 1, message: 'Hi, how are you?', likesCount: 12}, 10 | {id: 2, message: 'It\'s my first post', likesCount: 11}, 11 | {id: 3, message: 'Blabla', likesCount: 11}, 12 | {id: 4, message: 'Dada', likesCount: 11} 13 | ] as Array, 14 | profile: null as ProfileType | null, 15 | status: '', 16 | } 17 | 18 | const profileReducer = (state = initialState, action: ActionsType): InitialStateType => { 19 | 20 | switch (action.type) { 21 | case 'SN/PROFILE/ADD-POST': { 22 | let newPost = { 23 | id: 5, 24 | message: action.newPostText, 25 | likesCount: 0 26 | }; 27 | return { 28 | ...state, 29 | posts: [...state.posts, newPost], 30 | }; 31 | } 32 | case 'SN/PROFILE/SET_STATUS': { 33 | return { 34 | ...state, 35 | status: action.status 36 | } 37 | } 38 | case 'SN/PROFILE/SET_USER_PROFILE': { 39 | return {...state, profile: action.profile} 40 | } 41 | 42 | case 'SN/PROFILE/DELETE_POST': 43 | return {...state, posts: state.posts.filter(p => p.id != action.postId)} 44 | 45 | case 'SN/PROFILE/SAVE_PHOTO_SUCCESS': 46 | return {...state, profile: {...state.profile, photos: action.photos} as ProfileType} 47 | default: 48 | return state; 49 | } 50 | } 51 | 52 | 53 | export const actions = { 54 | addPostActionCreator: (newPostText: string) => ({type: 'SN/PROFILE/ADD-POST', newPostText} as const), 55 | setUserProfile: (profile: ProfileType) => ({type: 'SN/PROFILE/SET_USER_PROFILE', profile} as const), 56 | setStatus: (status: string) => ({type: 'SN/PROFILE/SET_STATUS', status} as const), 57 | deletePost: (postId: number) => ({type: 'SN/PROFILE/DELETE_POST', postId} as const), 58 | savePhotoSuccess: (photos: PhotosType) => ({type: 'SN/PROFILE/SAVE_PHOTO_SUCCESS', photos} as const) 59 | } 60 | 61 | export const getUserProfile = (userId: number): ThunkType => async (dispatch) => { 62 | const data = await profileAPI.getProfile(userId) 63 | dispatch(actions.setUserProfile(data)) 64 | } 65 | 66 | export const getStatus = (userId: number): ThunkType => async (dispatch) => { 67 | let data = await profileAPI.getStatus(userId) 68 | dispatch(actions.setStatus(data)) 69 | } 70 | 71 | export const updateStatus = (status: string): ThunkType => async (dispatch) => { 72 | try { 73 | let data = await profileAPI.updateStatus(status) 74 | 75 | if (data.resultCode === 0) { 76 | dispatch(actions.setStatus(status)) 77 | } 78 | } catch(error) { 79 | // 80 | } 81 | } 82 | 83 | export const savePhoto = (file: File): ThunkType => async (dispatch) => { 84 | let data = await profileAPI.savePhoto(file) 85 | 86 | if (data.resultCode === 0) { 87 | dispatch(actions.savePhotoSuccess(data.data.photos)) 88 | } 89 | } 90 | 91 | export const saveProfile = (profile: ProfileType): ThunkType => async (dispatch, getState) => { 92 | const userId = getState().auth.userId 93 | const data = await profileAPI.saveProfile(profile) 94 | 95 | if (data.resultCode === 0) { 96 | if (userId != null) { 97 | dispatch(getUserProfile(userId)) 98 | } else { 99 | throw new Error("userId can't be null") 100 | } 101 | } else { 102 | dispatch(stopSubmit("edit-profile", {_error: data.messages[0] })) 103 | return Promise.reject(data.messages[0]) 104 | } 105 | } 106 | 107 | export default profileReducer 108 | 109 | export type InitialStateType = typeof initialState 110 | type ActionsType = InferActionsTypes 111 | type ThunkType = BaseThunkType 112 | -------------------------------------------------------------------------------- /src/redux/users-reducer.ts: -------------------------------------------------------------------------------- 1 | import {updateObjectInArray} from '../utils/object-helpers' 2 | import {UserType} from '../types/types' 3 | import {BaseThunkType, InferActionsTypes} from './redux-store' 4 | import {Dispatch} from 'redux' 5 | import {usersAPI} from '../api/users-api' 6 | import {APIResponseType} from '../api/api' 7 | 8 | let initialState = { 9 | users: [] as Array, 10 | pageSize: 10, 11 | totalUsersCount: 0, 12 | currentPage: 1, 13 | isFetching: true, 14 | followingInProgress: [] as Array, //array of users ids, 15 | filter: { 16 | term: '', 17 | friend: null as null | boolean 18 | } 19 | } 20 | 21 | const usersReducer = (state = initialState, action: ActionsTypes): InitialState => { 22 | switch (action.type) { 23 | case 'SN/USERS/FOLLOW': 24 | return { 25 | ...state, 26 | users: updateObjectInArray(state.users, action.userId, 'id', {followed: true}) 27 | } 28 | case 'SN/USERS/UNFOLLOW': 29 | return { 30 | ...state, 31 | users: updateObjectInArray(state.users, action.userId, 'id', {followed: false}) 32 | } 33 | case 'SN/USERS/SET_USERS': { 34 | return {...state, users: action.users} 35 | } 36 | case 'SN/USERS/SET_CURRENT_PAGE': { 37 | return {...state, currentPage: action.currentPage} 38 | } 39 | case 'SN/USERS/SET_TOTAL_USERS_COUNT': { 40 | return {...state, totalUsersCount: action.count} 41 | } 42 | case 'SN/USERS/TOGGLE_IS_FETCHING': { 43 | return {...state, isFetching: action.isFetching} 44 | } 45 | case 'SN/USERS/SET_FILTER': { 46 | return {...state, filter: action.payload} 47 | } 48 | case 'SN/USERS/TOGGLE_IS_FOLLOWING_PROGRESS': { 49 | return { 50 | ...state, 51 | followingInProgress: action.isFetching 52 | ? [...state.followingInProgress, action.userId] 53 | : state.followingInProgress.filter(id => id != action.userId) 54 | } 55 | } 56 | default: 57 | return state 58 | } 59 | } 60 | 61 | export const actions = { 62 | followSuccess: (userId: number) => ({type: 'SN/USERS/FOLLOW', userId} as const), 63 | unfollowSuccess: (userId: number) => ({type: 'SN/USERS/UNFOLLOW', userId} as const), 64 | setUsers: (users: Array) => ({type: 'SN/USERS/SET_USERS', users} as const), 65 | setCurrentPage: (currentPage: number) => ({type: 'SN/USERS/SET_CURRENT_PAGE', currentPage} as const), 66 | setFilter: (filter: FilterType) => ({type: 'SN/USERS/SET_FILTER', payload: filter} as const), 67 | setTotalUsersCount: (totalUsersCount: number) => ({ 68 | type: 'SN/USERS/SET_TOTAL_USERS_COUNT', 69 | count: totalUsersCount 70 | } as const), 71 | toggleIsFetching: (isFetching: boolean) => ({ 72 | type: 'SN/USERS/TOGGLE_IS_FETCHING', 73 | isFetching 74 | } as const), 75 | toggleFollowingProgress: (isFetching: boolean, userId: number) => ({ 76 | type: 'SN/USERS/TOGGLE_IS_FOLLOWING_PROGRESS', 77 | isFetching, 78 | userId 79 | } as const) 80 | } 81 | 82 | export const requestUsers = (page: number, 83 | pageSize: number, filter: FilterType): ThunkType => { 84 | return async (dispatch, getState) => { 85 | dispatch(actions.toggleIsFetching(true)) 86 | dispatch(actions.setCurrentPage(page)) 87 | dispatch(actions.setFilter(filter)) 88 | 89 | let data = await usersAPI.getUsers(page, pageSize, filter.term, filter.friend) 90 | dispatch(actions.toggleIsFetching(false)) 91 | dispatch(actions.setUsers(data.items)) 92 | dispatch(actions.setTotalUsersCount(data.totalCount)) 93 | } 94 | } 95 | 96 | const _followUnfollowFlow = async (dispatch: Dispatch, 97 | userId: number, 98 | apiMethod: (userId: number) => Promise, 99 | actionCreator: (userId: number) => ActionsTypes) => { 100 | dispatch(actions.toggleFollowingProgress(true, userId)) 101 | let response = await apiMethod(userId) 102 | 103 | if (response.resultCode == 0) { 104 | dispatch(actionCreator(userId)) 105 | } 106 | dispatch(actions.toggleFollowingProgress(false, userId)) 107 | } 108 | 109 | export const follow = (userId: number): ThunkType => { 110 | return async (dispatch) => { 111 | await _followUnfollowFlow(dispatch, userId, usersAPI.follow.bind(usersAPI), actions.followSuccess) 112 | } 113 | } 114 | 115 | export const unfollow = (userId: number): ThunkType => { 116 | return async (dispatch) => { 117 | await _followUnfollowFlow(dispatch, userId, usersAPI.unfollow.bind(usersAPI), actions.unfollowSuccess) 118 | } 119 | } 120 | 121 | export default usersReducer 122 | 123 | export type InitialState = typeof initialState 124 | export type FilterType = typeof initialState.filter 125 | type ActionsTypes = InferActionsTypes 126 | type ThunkType = BaseThunkType 127 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import './App.css' 3 | import 'antd/dist/antd.css' 4 | import {BrowserRouter, Link, Redirect, Route, Switch, withRouter} from 'react-router-dom' 5 | import {LoginPage} from './components/Login/LoginPage' 6 | import {connect, Provider} from 'react-redux' 7 | import {compose} from 'redux' 8 | import {initializeApp} from './redux/app-reducer' 9 | import Preloader from './components/common/Preloader/Preloader' 10 | import store, {AppStateType} from './redux/redux-store' 11 | import {withSuspense} from './hoc/withSuspense' 12 | import {UsersPage} from './components/Users/UsersContainer' 13 | 14 | import {Breadcrumb, Layout, Menu} from 'antd' 15 | import {LaptopOutlined, NotificationOutlined, UserOutlined} from '@ant-design/icons' 16 | import {Header} from './components/Header/Header' 17 | 18 | const {SubMenu} = Menu 19 | const {Content, Footer, Sider} = Layout 20 | 21 | const DialogsContainer = React.lazy(() => import('./components/Dialogs/DialogsContainer')) 22 | const ProfileContainer = React.lazy(() => import('./components/Profile/ProfileContainer')) 23 | const ChatPage = React.lazy(() => import('./pages/Chat/ChatPage')) 24 | 25 | type MapPropsType = ReturnType 26 | type DispatchPropsType = { 27 | initializeApp: () => void 28 | } 29 | 30 | const SuspendedDialogs = withSuspense(DialogsContainer) 31 | const SuspendedProfile = withSuspense(ProfileContainer) 32 | const SuspendedChatPage = withSuspense(ChatPage) 33 | 34 | 35 | class App extends Component { 36 | catchAllUnhandledErrors = (e: PromiseRejectionEvent) => { 37 | alert('Some error occured') 38 | } 39 | 40 | componentDidMount() { 41 | this.props.initializeApp() 42 | window.addEventListener('unhandledrejection', this.catchAllUnhandledErrors) 43 | } 44 | 45 | componentWillUnmount() { 46 | window.removeEventListener('unhandledrejection', this.catchAllUnhandledErrors) 47 | } 48 | 49 | render() { 50 | if (!this.props.initialized) { 51 | return 52 | } 53 | 54 | 55 | return ( 56 | 57 |
58 | 59 | 60 | Home 61 | List 62 | App 63 | 64 | 65 | 66 | 72 | } title="My Profile"> 73 | Profile 74 | Messages 75 | option3 76 | option4 77 | 78 | } title="Developers"> 79 | Developers 80 | option6 81 | option7 82 | option8 83 | 84 | } title="subnav 3"> 85 | Chat 86 | option10 87 | option11 88 | option12 89 | 90 | 91 | 92 | 93 | 94 | 95 | }/> 97 | 98 | }/> 100 | 101 | }/> 103 | 104 | }/> 106 | 107 | }/> 109 | 110 | }/> 112 | 113 |
404 NOT FOUND
}/> 115 |
116 | 117 |
118 |
119 |
120 |
Samurai Social Network ©2020 Created by IT-KAMASUTRA
121 | 122 | 123 | 124 | /*
125 | 126 | 127 |
128 | 129 | }/> 131 | 132 | }/> 134 | 135 | }/> 137 | 138 | }/> 140 | 141 | }/> 143 | 144 |
404 NOT FOUND
}/> 146 |
147 | 148 |
149 |
*/ 150 | ) 151 | } 152 | } 153 | 154 | const mapStateToProps = (state: AppStateType) => ({ 155 | initialized: state.app.initialized 156 | }) 157 | 158 | let AppContainer = compose( 159 | withRouter, 160 | connect(mapStateToProps, {initializeApp}))(App) 161 | 162 | const SamuraiJSApp: React.FC = () => { 163 | return 164 | 165 | 166 | 167 | 168 | } 169 | 170 | export default SamuraiJSApp 171 | --------------------------------------------------------------------------------