├── .editorconfig ├── .env.tmpl ├── .gitignore ├── Dockerfile ├── README.md ├── nginx.conf ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── legacy.html ├── locales │ ├── en │ │ └── translation.json │ └── ru │ │ └── translation.json ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── serviceWorker.js ├── src ├── __tests__ │ └── App.test.tsx ├── components │ ├── elements │ │ ├── CenterMessage.tsx │ │ └── Loading.tsx │ └── pages │ │ ├── Empty.tsx │ │ └── Page404.tsx ├── containers │ ├── App.tsx │ ├── account │ │ └── Account.tsx │ ├── auth │ │ ├── Login.tsx │ │ ├── Password.tsx │ │ ├── Social.tsx │ │ └── Verify.tsx │ ├── utils │ │ ├── LangSwitch.tsx │ │ └── ThemeSwitch.tsx │ └── wrappers │ │ └── ThemeWrapper.tsx ├── images │ └── logo.svg ├── index.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── store │ ├── constants.ts │ ├── modules │ │ ├── auth │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ │ ├── user │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ │ ├── utils │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ │ └── verify │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ ├── reducers.ts │ ├── store.ts │ └── types.ts ├── theme │ ├── styled.d.ts │ ├── theme.ts │ ├── themes │ │ ├── theme-dark.ts │ │ └── theme-light.ts │ └── widgets.tsx └── utils │ ├── api.ts │ └── i18n.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.tmpl: -------------------------------------------------------------------------------- 1 | REACT_APP_PORT= 2 | REACT_APP_API_URL= 3 | REACT_APP_CLIENT_HOST= 4 | -------------------------------------------------------------------------------- /.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 | # firebase files 15 | /.firebase 16 | .firebaserc 17 | firebase.json 18 | 19 | # local env files 20 | .DS_Store 21 | .env.local 22 | .env.*.local 23 | .env.development 24 | .env.production 25 | 26 | # log files 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # Editor directories and files 32 | .idea 33 | .vscode 34 | *.suo 35 | *.ntvs* 36 | *.njsproj 37 | *.sln 38 | *.sw? 39 | *.sublime-workspace 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as build-stage 2 | 3 | WORKDIR /projects/github/react-auth 4 | COPY . . 5 | RUN npm install \ 6 | && npm run build \ 7 | && rm -rf node_modules 8 | 9 | FROM nginx:stable-alpine as production-stage 10 | COPY nginx.conf /etc/nginx/conf.d/default.conf 11 | COPY --from=build-stage /projects/github/react-auth/build /usr/share/nginx/html 12 | 13 | EXPOSE 80 14 | 15 | CMD ["nginx", "-g", "daemon off;"] 16 | # Сборка образа 17 | # sudo docker build -t react-auth . 18 | 19 | # Запуск образа 20 | # sudo docker run -p 3000:80 react-auth 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Create React App based frontend boilerplate 2 | =========================================== 3 | 4 | Описание 5 | -------- 6 | 7 | [Демонстрация](https://react-auth.kafedra.org/) 8 | 9 | Основанная на Create React App реализация Progressive Web App клиента с модулем полноценной аунтефикации через [Backend API](https://github.com/ushliypakostnik/express-auth), локализацией и темезацией. 10 | 11 | Написан на TypeScript со стилизацией с помощью Styled Components. 12 | 13 | 14 | Юзеркейсы: 15 | ---------- 16 | 17 | Пользователь может быть авторизован или не авторизован в системе. 18 | 19 | 20 | ### Вход и регистрация 21 | 22 | Неавторизованному пользователю показывается два поля ввода: адреса электронной почты и пароля, а также три кнопки - входа через Facebook или ВКонтакте, и кнопка входа с помощью валидных данных почты и пароля. 23 | 24 | Пользователь который ввел валидный электронный адрес и пароль будет авторизован в системе. Если пользователя с указанным адресом не существует в базе - он будет добавлен. 25 | 26 | Адрес электронной почты пользователя впервые авторизовавшегося в системе через социальную сеть, также будет добавлен в базу. Он сможет создать пароль при первой же попытке обычного входа по паролю со своей электронной почты. 27 | 28 | 29 | ### Востановление пароля 30 | 31 | Ниже стартовой формы показывается ссылка позволяющая переключиться на форму восстановления пароля, состоящую из только одного контрола ввода электронной почты и кнопки для ее отправки. 32 | 33 | При попытке восстановления пароля на указанный адрес электронной почты отправляется письмо, в том случае, если пользователь с такой почтой уже зарегистрирован в базе. При переходе по ссылке с такого письма пользователь получает аутентификацию и оказывается на специальной форме из двух полей ввода пароля и кнопки, позволяющих создать новый пароль. При отправке двух совпадающих валидных паролей пользователь окажется во внутреннем интерфейсе, аккаунте. Его пароль будет изменен или создан, если ранее он входил только через социальные аккаунты. 34 | 35 | 36 | ### Верификация 37 | 38 | Каждый новый пользователь получает статус неверифицированного и на указанный им почтовый ящик отправляется письмо с предложением подтвердить регистрацию. При переходе по ссылке в письме пользователь получает аутентификацию, верифицируется, и оказывается во внутреннем интерфейсе, аккаунте. 39 | 40 | 41 | ### Аккаунт 42 | 43 | Во внутреннем интерфейсе, аккаунте пользователь видит адрес электронной почты, статус верификации, кнопку выхода из системы. Если пользователь не верифицирован он также видит кнопку повторной отправки письма для подтверждения регистрации с сообщением под ней предлагающим это сделать. 44 | 45 | 46 | ### UI 47 | 48 | Пользователь может выбирать язык и колористическую гамму интерфейса, тему. 49 | 50 | Во всех возможных и необходимых случаях в правильных местах интерфейс показывает информативные сообщения об успехе или провале действий совершаемых пользователем. 51 | 52 | При переходе между состояниями, страницами, во время действий требующих ожидания ответа от сервера пользователю показывается анимированный лоадер. 53 | 54 | 55 | Deploy 56 | ------ 57 | 58 | Установка зависимостей npm packages 59 | 60 | $ npm install 61 | 62 | Запуск сервера для разработки 63 | ----------------------------- 64 | 65 | $ npm start 66 | 67 | http://localhost:3000/ 68 | 69 | Cборка 70 | ------ 71 | 72 | Сборка проекта в продакшен, в папку /build 73 | 74 | $ npm build 75 | 76 | Тесты 77 | ----- 78 | 79 | Запуск тестов 80 | 81 | $ npm test 82 | 83 | 84 | 85 | ## Learn More 86 | 87 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 88 | 89 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 90 | 91 | To learn React, check out the [React documentation](https://reactjs.org/). 92 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | root /usr/share/nginx/html; 6 | index index.html; 7 | 8 | location / { 9 | try_files $uri /index.html; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-auth", 3 | "version": "2.0.0", 4 | "description" : "Create React App based frontend boilerplate", 5 | "private": true, 6 | "author": "Levon Gambaryan", 7 | "license": "ISC", 8 | "repository": { 9 | "type" : "git", 10 | "url" : "https://github.com/ushliypakostnik/react-auth" 11 | }, 12 | "dependencies": { 13 | "@types/jest": "24.0.18", 14 | "@types/js-cookie": "^2.2.2", 15 | "@types/node": "12.7.2", 16 | "@types/react": "16.9.2", 17 | "@types/react-dom": "16.9.0", 18 | "@types/react-redux": "^7.1.2", 19 | "@types/react-router": "^5.0.3", 20 | "@types/react-router-dom": "^4.3.4", 21 | "@types/react-router-redux": "^5.0.18", 22 | "@types/redux-thunk": "^2.1.0", 23 | "@types/styled-components": "^4.1.18", 24 | "axios": "^0.19.0", 25 | "connected-react-router": "^6.5.2", 26 | "i18next": "^17.0.15", 27 | "i18next-browser-languagedetector": "^3.0.3", 28 | "i18next-xhr-backend": "^3.1.2", 29 | "js-cookie": "^2.2.1", 30 | "react": "^16.9.0", 31 | "react-dom": "^16.9.0", 32 | "react-i18next": "^10.12.4", 33 | "react-redux": "^7.1.0", 34 | "react-router": "^5.0.1", 35 | "react-router-dom": "^5.0.1", 36 | "react-router-redux": "^4.0.8", 37 | "react-scripts": "3.1.1", 38 | "redux-thunk": "^2.3.0", 39 | "styled-components": "^4.3.2", 40 | "typescript": "3.5.3" 41 | }, 42 | "scripts": { 43 | "start": "react-scripts start", 44 | "build": "react-scripts build", 45 | "test": "react-scripts test", 46 | "eject": "react-scripts eject" 47 | }, 48 | "eslintConfig": { 49 | "extends": "react-app" 50 | }, 51 | "browserslist": { 52 | "production": [ 53 | ">0.2%", 54 | "not dead", 55 | "not op_mini all" 56 | ], 57 | "development": [ 58 | "last 1 chrome version", 59 | "last 1 firefox version", 60 | "last 1 safari version" 61 | ] 62 | }, 63 | "devDependencies": { 64 | "@types/redux-logger": "^3.0.7", 65 | "redux-logger": "^3.0.6" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bullTo/react-auth-ts/f82e246ad7a53582f8d9e7497edfe892e7955d3c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Create React App: PWA example 7 | 11 | 12 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 73 | 74 | 75 |
76 |
Loading site...
77 |
78 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /public/legacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | У Вас установлена устаревшая версия браузера.
14 | Для полноценной работы в интернете необходимо скачать современный браузер, 15 | например — Google Chrome, 16 | или — Яндекс.Браузер. 17 |
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /public/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "validations": { 3 | "verify_account": "Verify your account! A confirmation email has been sent to your inbox", 4 | "resend_verify_email": "Letter sent successfully", 5 | "is_required": "This field is required", 6 | "password_min_lenght": { 7 | "part1": "Password must be at least ", 8 | "part2": " characters" 9 | }, 10 | "password_contain_digit": "Password must contain lowercase latin letters and at least one digit", 11 | "email_invalid": "Invalid email", 12 | "passwords_do_not_match": "Passwords do not match" 13 | }, 14 | "login": { 15 | "title": "Create React App based frontend boilerplate with authentication", 16 | "input": { 17 | "aria-label": "email input", 18 | "placeholder": "Email" 19 | }, 20 | "password": { 21 | "aria-label": "password input", 22 | "placeholder": "Password" 23 | }, 24 | "submit_button": { 25 | "aria-label1": "Login or registration", 26 | "aria-label2": "Remind password", 27 | "text1": "Login / Registration", 28 | "text2": "Remind password" 29 | }, 30 | "fb_button": { 31 | "aria-label": "login via Facebook", 32 | "text": "Via Facebook" 33 | }, 34 | "vk_button": { 35 | "aria-label": "login via VKontakte", 36 | "text": "Via VKontakte" 37 | }, 38 | "form_link": { 39 | "aria-label1": "Remind password", 40 | "aria-label2": "Back to login", 41 | "text1": "Remind password?", 42 | "text2": "Back to login" 43 | } 44 | }, 45 | "password": { 46 | "title": "Change Password", 47 | "password1": { 48 | "aria-label": "new password input", 49 | "placeholder": "New password" 50 | }, 51 | "password2": { 52 | "aria-label": "new password input again", 53 | "placeholder": "New password again" 54 | }, 55 | "submit_button": { 56 | "aria-label": "set new password", 57 | "text1": "Set new password" 58 | } 59 | }, 60 | "account": { 61 | "title": "Account", 62 | "field1": "Usermail", 63 | "field2": "isVerify", 64 | "logout_button": { 65 | "aria-label": "logout button", 66 | "text": "Sign out" 67 | }, 68 | "resend_button": { 69 | "aria-label": "resend verify email", 70 | "text": "Resend Verify Email" 71 | } 72 | }, 73 | "page404": "Page Not Found!!!", 74 | "boolean": { 75 | "true": "Yes", 76 | "false": "No" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /public/locales/ru/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "validations": { 3 | "verify_account": "Подтвердите ваш аккаунт! Письмо с подтверждением было отправлено на Ваш почтовый ящик", 4 | "resend_verify_email": "Письмо успешно отправлено", 5 | "is_required": "Это поле обязательно к заполнению", 6 | "password_min_lenght": { 7 | "part1": "Пароль должен быть хотя бы ", 8 | "part2": " символов длинной" 9 | }, 10 | "password_contain_digit": "Пароль должен содержать строчные латинские буквы и хотя бы одну цифру", 11 | "email_invalid": "Неверный адрес электронной почты", 12 | "passwords_do_not_match": "Пароли не совпадают" 13 | }, 14 | "login": { 15 | "title": "Основаннный на Create React App стартовый проект фронтенда c аутентификацией", 16 | "input": { 17 | "aria-label": "ввод электронной почты", 18 | "placeholder": "Электронная почта" 19 | }, 20 | "password": { 21 | "aria-label": "ввод пароля", 22 | "placeholder": "Пароль" 23 | }, 24 | "submit_button": { 25 | "aria-label1": "Вход или регистрация", 26 | "aria-label2": "Вспомнить пароль", 27 | "text1": "Вход / Регистрация", 28 | "text2": "Вспомнить пароль" 29 | }, 30 | "fb_button": { 31 | "aria-label": "Вход через Facebook", 32 | "text": "Войти с Facebook" 33 | }, 34 | "vk_button": { 35 | "aria-label": "Вход через ВКонтакте", 36 | "text": "Войти с ВКонтакте" 37 | }, 38 | "form_link": { 39 | "aria-label1": "Вспомнить пароль", 40 | "aria-label2": "Вернуться ко входу", 41 | "text1": "Вспомнить пароль?", 42 | "text2": "Вернуться ко входу" 43 | } 44 | }, 45 | "password": { 46 | "title": "Смена пароля", 47 | "password1": { 48 | "aria-label": "ввод нового пароля", 49 | "placeholder": "Новый пароль" 50 | }, 51 | "password2": { 52 | "aria-label": "ввод нового пароля еще раз", 53 | "placeholder": "Новый пароль еще раз" 54 | }, 55 | "submit_button": { 56 | "aria-label": "установить новый пароль", 57 | "text": "Установить пароль" 58 | } 59 | }, 60 | "account": { 61 | "title": "Аккаунт", 62 | "field1": "Электронная почта", 63 | "field2": "Верифицирован", 64 | "logout_button": { 65 | "aria-label": "кнопка выхода", 66 | "text": "Выйти" 67 | }, 68 | "resend_button": { 69 | "aria-label": "отправить верификационное письмо", 70 | "text": "Отправить еще раз" 71 | } 72 | }, 73 | "page404": "Страница не найдена!!!", 74 | "boolean": { 75 | "true": "Да", 76 | "false": "Нет" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bullTo/react-auth-ts/f82e246ad7a53582f8d9e7497edfe892e7955d3c/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bullTo/react-auth-ts/f82e246ad7a53582f8d9e7497edfe892e7955d3c/public/logo512.png -------------------------------------------------------------------------------- /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": "/?utm_source=homescreen", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This is the service worker with the Cache-first network 2 | 3 | const CACHE = "pwabuilder-precache"; 4 | const precacheFiles = [ 5 | /* Add an array of files to precache for your app */ 6 | ]; 7 | 8 | self.addEventListener("install", function (event) { 9 | console.log("[PWA Builder] Install Event processing"); 10 | 11 | console.log("[PWA Builder] Skip waiting on install"); 12 | self.skipWaiting(); 13 | 14 | event.waitUntil( 15 | caches.open(CACHE).then(function (cache) { 16 | console.log("[PWA Builder] Caching pages during install"); 17 | return cache.addAll(precacheFiles); 18 | }) 19 | ); 20 | }); 21 | 22 | // Allow sw to control of current page 23 | self.addEventListener("activate", function (event) { 24 | console.log("[PWA Builder] Claiming clients for current page"); 25 | event.waitUntil(self.clients.claim()); 26 | }); 27 | 28 | // If any fetch fails, it will look for the request in the cache and serve it from there first 29 | self.addEventListener("fetch", function (event) { 30 | if (event.request.method !== "GET") return; 31 | 32 | event.respondWith( 33 | fromCache(event.request).then( 34 | function (response) { 35 | // The response was found in the cache so we responde with it and update the entry 36 | 37 | // This is where we call the server to get the newest version of the 38 | // file to use the next time we show view 39 | event.waitUntil( 40 | fetch(event.request).then(function (response) { 41 | return updateCache(event.request, response); 42 | }) 43 | ); 44 | 45 | return response; 46 | }, 47 | function () { 48 | // The response was not found in the cache so we look for it on the server 49 | return fetch(event.request) 50 | .then(function (response) { 51 | // If request was success, add or update it in the cache 52 | event.waitUntil(updateCache(event.request, response.clone())); 53 | 54 | return response; 55 | }) 56 | .catch(function (error) { 57 | console.log("[PWA Builder] Network request failed and no cache." + error); 58 | }); 59 | } 60 | ) 61 | ); 62 | }); 63 | 64 | function fromCache(request) { 65 | // Check to see if you have it in the cache 66 | // Return response 67 | // If not in the cache, then return 68 | return caches.open(CACHE).then(function (cache) { 69 | return cache.match(request).then(function (matching) { 70 | if (!matching || matching.status === 404) { 71 | return Promise.reject("no-match"); 72 | } 73 | 74 | return matching; 75 | }); 76 | }); 77 | } 78 | 79 | function updateCache(request, response) { 80 | return caches.open(CACHE).then(function (cache) { 81 | return cache.put(request, response); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App 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/components/elements/CenterMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import logo from '../../images/logo.svg'; 4 | 5 | import { EntryHeaderWpapper, Logo } from '../../theme/widgets'; 6 | 7 | interface CenterMessageProps { 8 | children? : React.ReactNode; 9 | }; 10 | 11 | const CenterMessage : React.SFC = (props) => { 12 | 13 | return ( 14 | 15 | 19 | { props.children } 20 | 21 | ); 22 | }; 23 | 24 | export default CenterMessage; 25 | -------------------------------------------------------------------------------- /src/components/elements/Loading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import styled from 'styled-components'; 4 | 5 | import { Logo } from '../../theme/widgets'; 6 | 7 | import logo from '../../images/logo.svg'; 8 | 9 | const Loading : React.SFC = () => { 10 | return ( 11 | 15 | ); 16 | }; 17 | 18 | export default Loading; 19 | -------------------------------------------------------------------------------- /src/components/pages/Empty.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import CenterMessage from '../elements/CenterMessage'; 4 | import Loading from '../elements/Loading'; 5 | 6 | import { 7 | Page, 8 | CenterWrapper, 9 | EntryHeaderWpapper, 10 | PageProps, 11 | } from '../../theme/widgets'; 12 | 13 | const Empty : React.SFC = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default Empty; 26 | -------------------------------------------------------------------------------- /src/components/pages/Page404.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import CenterMessage from '../elements/CenterMessage'; 4 | 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | import { 8 | Page, 9 | CenterWrapper, 10 | TextLarge, 11 | } from '../../theme/widgets'; 12 | 13 | const Page404 : React.SFC = () => { 14 | const { t, i18n } = useTranslation(); 15 | 16 | return ( 17 | 18 | 19 | 20 | 404 21 | {t('page404')} 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default Page404; 29 | -------------------------------------------------------------------------------- /src/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | Route, 5 | Redirect, 6 | Switch, 7 | RouteComponentProps, 8 | RouteProps, 9 | } from "react-router-dom"; 10 | 11 | import { connect } from 'react-redux'; 12 | import { StoreType } from '../store/types'; 13 | 14 | import Login from './auth/Login'; 15 | import Password from './auth/Password'; 16 | import Verify from './auth/Verify'; 17 | import Social from './auth/Social'; 18 | import Account from './account/Account'; 19 | import Page404 from '../components/pages/Page404'; 20 | 21 | const PrivateRoute : React.SFC = 22 | ({ component: Component, auth, ...rest }) => { 23 | return ( 24 | ) => auth 27 | ? 28 | : } 29 | /> 30 | ); 31 | }; 32 | 33 | const LoginRoute : React.SFC = 34 | ({ component: Component, auth, ...rest }) => { 35 | return ( 36 | ) => auth 39 | ? 40 | : } 41 | /> 42 | ); 43 | }; 44 | 45 | interface Props { 46 | isAuth : boolean; 47 | }; 48 | 49 | const initialState = { 50 | isAuth: false, 51 | }; 52 | 53 | type State = Readonly; 54 | 55 | class App extends React.Component { 56 | 57 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 58 | isAuth: nextProps.isAuth, 59 | }); 60 | 61 | readonly state : State = initialState; 62 | 63 | public render() { 64 | const { isAuth } = this.state; 65 | 66 | return ( 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | }; 78 | 79 | const mapStateToProps = (state : StoreType) : State => ({ 80 | isAuth: state.rootReducer.auth.isAuth, 81 | }); 82 | 83 | export default connect(mapStateToProps, null)(App); 84 | -------------------------------------------------------------------------------- /src/containers/account/Account.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | 7 | import i18n from '../../utils/i18n'; 8 | import { WithTranslation, withTranslation } from 'react-i18next'; 9 | 10 | import { StoreType } from '../../store/types'; 11 | 12 | import { authLogout } from '../../store/modules/auth/actions'; 13 | import { 14 | getUser, 15 | postVerifyEmail, 16 | } from '../../store/modules/user/actions'; 17 | 18 | import Empty from '../../components/pages/Empty'; 19 | import LangSwitch from '../utils/LangSwitch'; 20 | import ThemeSwitch from '../utils/ThemeSwitch'; 21 | 22 | import { 23 | Page, 24 | CenterWrapper, 25 | Button, 26 | ButtonWrapper, 27 | TextHeader, 28 | TextLarge, 29 | TextSmall, 30 | TextString, 31 | Form, 32 | FormGroup, 33 | FormMessage, 34 | Footer, 35 | } from '../../theme/widgets'; 36 | 37 | interface DispatchProps { 38 | authLogout : () => void; 39 | getUser : () => void; 40 | postVerifyEmail: (usermail: string) => void; 41 | }; 42 | 43 | interface StateToProps { 44 | isFetching: boolean; 45 | profile : { 46 | usermail: string; 47 | isVerify: boolean; 48 | }; 49 | success: boolean; 50 | }; 51 | 52 | interface Props extends DispatchProps, StateToProps, WithTranslation {}; 53 | 54 | const initialState = {}; 55 | 56 | type State = Readonly; 57 | 58 | class Account extends React.Component { 59 | 60 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 61 | isFetching: nextProps.isFetching, 62 | profile: nextProps.profile, 63 | success: nextProps.success, 64 | }); 65 | 66 | readonly state : State = initialState; 67 | 68 | public componentDidMount() : void { 69 | this.props.getUser(); 70 | } 71 | 72 | public render() { 73 | const { i18n, isFetching, profile, success } = this.props; 74 | 75 | return ( 76 | 77 | { isFetching ? 78 | : 79 | 80 | 81 | {i18n.t('account.title')} 82 |
83 | 84 | {i18n.t('account.field1')}: 85 | 86 | 87 | { profile.usermail } 88 | 89 | 90 | {i18n.t('account.field2')}: 91 | { profile.isVerify ? i18n.t('boolean.true') : i18n.t('boolean.false') } 92 | 93 | 94 | 101 | {!profile.isVerify && 102 | 103 | 110 | 111 | 112 | { success ? i18n.t('validations.resend_verify_email') : i18n.t('validations.verify_account') } 113 | 114 | 115 | } 116 | 117 |
118 |
119 |
120 |
} 121 |
122 | ); 123 | } 124 | }; 125 | 126 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 127 | isFetching: state.rootReducer.user.isFetching, 128 | profile: state.rootReducer.user.profile, 129 | success: state.rootReducer.user.success, 130 | }); 131 | 132 | const mapDispatchToProps = (dispatch: ThunkDispatch) : DispatchProps => ({ 133 | authLogout: () => dispatch(authLogout()), 134 | getUser: () => dispatch(getUser()), 135 | postVerifyEmail: (usermail: string) => dispatch(postVerifyEmail(usermail)), 136 | }); 137 | 138 | export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Account)); 139 | -------------------------------------------------------------------------------- /src/containers/auth/Login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | 7 | import i18n from '../../utils/i18n'; 8 | import { WithTranslation, withTranslation } from 'react-i18next'; 9 | 10 | import { UTILS } from '../../store/constants'; 11 | 12 | import { 13 | StoreType, 14 | CredentialsType 15 | } from '../../store/types'; 16 | 17 | import { 18 | postAuth, 19 | getFacebookAuth, 20 | getVkontakteAuth, 21 | postRemindPassword, 22 | } from '../../store/modules/auth/actions'; 23 | import { clearMessages } from '../../store/modules/utils/actions'; 24 | 25 | import Empty from '../../components/pages/Empty'; 26 | import CenterMessage from '../../components/elements/CenterMessage'; 27 | import LangSwitch from '../utils/LangSwitch'; 28 | import ThemeSwitch from '../utils/ThemeSwitch'; 29 | 30 | import { 31 | Page, 32 | CenterWrapper, 33 | Form, 34 | FormGroup, 35 | FormMessage, 36 | TextSmall, 37 | TextLarge, 38 | Input, 39 | Button, 40 | A, 41 | Footer, 42 | } from '../../theme/widgets'; 43 | 44 | interface DispatchProps { 45 | postAuth : (credentials: CredentialsType) => void; 46 | getFacebookAuth : () => void; 47 | getVkontakteAuth : () => void; 48 | postRemindPassword : (usermail: string) => void; 49 | clearMessages: () => void; 50 | }; 51 | 52 | interface StateToProps { 53 | isFetching: boolean; 54 | success : string; 55 | error : string; 56 | }; 57 | 58 | interface Props extends DispatchProps, StateToProps, WithTranslation {}; 59 | 60 | const initialState = { 61 | login: true, 62 | mailError: '', 63 | passError: '', 64 | success: '', 65 | error : '', 66 | }; 67 | 68 | type State = Readonly; 69 | 70 | class Login extends React.Component { 71 | private usermailInput: React.RefObject; 72 | private passwordInput: React.RefObject; 73 | 74 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 75 | isFetching: nextProps.isFetching, 76 | success: nextProps.success, 77 | error: nextProps.error, 78 | }); 79 | 80 | constructor(props) { 81 | super(props); 82 | 83 | this.usermailInput = React.createRef(); 84 | this.passwordInput = React.createRef(); 85 | } 86 | 87 | readonly state : State = initialState; 88 | 89 | private submit = (usermail : string, password : string) : void => { 90 | const emailValid = this.validateEmail(usermail); 91 | if (this.state.login) { 92 | const passwordValid = this.validatePassword(password); 93 | if (emailValid && passwordValid) { 94 | const user = { 95 | usermail, 96 | password, 97 | } 98 | this.props.postAuth(user); 99 | } 100 | } else { 101 | if (emailValid) { 102 | this.props.postRemindPassword(usermail); 103 | } 104 | } 105 | }; 106 | 107 | private validateEmail = (email : string) : boolean => { 108 | // eslint-disable-next-line no-useless-escape 109 | const regExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 110 | const validate = regExp.test(email); 111 | let mailError; 112 | if (email === '') { 113 | mailError = i18n.t('validations.is_required'); 114 | } else if (validate) { 115 | mailError = ''; 116 | } else { 117 | mailError = i18n.t('validations.email_invalid'); 118 | } 119 | this.setState({ 120 | mailError: mailError, 121 | }); 122 | return validate; 123 | }; 124 | 125 | private validatePassword = (password : string) : boolean => { 126 | // eslint-disable-next-line no-useless-escape 127 | const regExp = /^(?=.*\d)(?=.*[a-z])(?!.*\s).*$/; 128 | const validate = regExp.test(password); 129 | const minLenght = UTILS.min_password_lenght; 130 | let passError; 131 | if (password === '') { 132 | passError = i18n.t('validations.is_required'); 133 | } else if (password.length < minLenght) { 134 | passError = i18n.t('validations.password_min_lenght.part1') + String(minLenght) + i18n.t('validations.password_min_lenght.part2'); 135 | } else if (!validate) { 136 | passError = i18n.t('password_contain_digit'); 137 | } else { 138 | passError = ''; 139 | } 140 | this.setState({ 141 | passError: passError, 142 | }); 143 | return validate; 144 | } 145 | 146 | public render() { 147 | const { i18n, isFetching } = this.props; 148 | const { login, mailError, passError, success, error } = this.state; 149 | 150 | return ( 151 | 152 | { isFetching && login ? 153 | : 154 | 155 | 156 | 157 | {i18n.t('login.title')} 158 | 159 |
160 | 161 | 167 | {mailError !== '' 168 | && 169 | { mailError } 170 | } 171 | {!login && success !== '' 172 | && 173 | { success } 174 | } 175 | {error !== '' 176 | && 177 | { error } 178 | } 179 | 180 | {login && 181 | 182 | 188 | {passError !== '' 189 | && 190 | { passError } 191 | } 192 | } 193 | 205 | {login && 206 | 207 | 216 | 225 | } 226 | { 230 | e.preventDefault(); 231 | if (success !== '' || error !== '') this.props.clearMessages(); 232 | this.setState({ 233 | login: !login, 234 | }); 235 | }} 236 | >{login ? i18n.t('login.form_link.text1') : i18n.t('login.form_link.text2')} 237 |
238 |
239 |
240 |
} 241 |
242 | ); 243 | } 244 | }; 245 | 246 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 247 | isFetching: state.rootReducer.auth.isFetching, 248 | success: state.rootReducer.auth.success, 249 | error: state.rootReducer.auth.error, 250 | }); 251 | 252 | const mapDispatchToProps = (dispatch: ThunkDispatch) : DispatchProps => ({ 253 | postAuth: (credentials : CredentialsType) => dispatch(postAuth(credentials)), 254 | getFacebookAuth: () => dispatch(getFacebookAuth()), 255 | getVkontakteAuth: () => dispatch(getVkontakteAuth()), 256 | postRemindPassword: (usermail : string) => dispatch(postRemindPassword(usermail)), 257 | clearMessages: () => dispatch(clearMessages()), 258 | }); 259 | 260 | export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Login)); 261 | -------------------------------------------------------------------------------- /src/containers/auth/Password.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | 7 | import i18n from '../../utils/i18n'; 8 | import { WithTranslation, withTranslation } from 'react-i18next'; 9 | 10 | import { UTILS } from '../../store/constants'; 11 | import { history } from '../../store/store'; 12 | 13 | import { 14 | StoreType, 15 | NewPasswordType, 16 | } from '../../store/types'; 17 | 18 | import { 19 | setToken, 20 | postNewPassword 21 | } from '../../store/modules/auth/actions'; 22 | 23 | import Empty from '../../components/pages/Empty'; 24 | import CenterMessage from '../../components/elements/CenterMessage'; 25 | import LangSwitch from '../utils/LangSwitch'; 26 | import ThemeSwitch from '../utils/ThemeSwitch'; 27 | 28 | import { 29 | Page, 30 | CenterWrapper, 31 | Form, 32 | FormGroup, 33 | FormMessage, 34 | TextSmall, 35 | TextLarge, 36 | Input, 37 | Button, 38 | Footer, 39 | } from '../../theme/widgets'; 40 | 41 | interface StateToProps { 42 | hash : string; 43 | isFetching: boolean, 44 | result: string, 45 | }; 46 | 47 | interface DispatchProps { 48 | setToken : (token: string) => void; 49 | postNewPassword : (credentials: NewPasswordType) => void; 50 | } 51 | 52 | interface Props extends StateToProps, DispatchProps, WithTranslation {}; 53 | 54 | const initialState = { 55 | id: '', 56 | pass1Error: '', 57 | pass2Error: '', 58 | hash: '', 59 | match: '', 60 | }; 61 | 62 | type State = Readonly; 63 | 64 | class Login extends React.Component { 65 | private password1Input: React.RefObject; 66 | private password2Input: React.RefObject; 67 | 68 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 69 | hash: nextProps.hash, 70 | isFetching: nextProps.isFetching, 71 | result: nextProps.result, 72 | }); 73 | 74 | constructor(props) { 75 | super(props); 76 | 77 | this.password1Input = React.createRef(); 78 | this.password2Input = React.createRef(); 79 | }; 80 | 81 | readonly state : State = initialState; 82 | 83 | public componentDidMount() : void { 84 | const query = this.state.hash; 85 | const id = query.split('&')[0].slice(4); 86 | const token = query.split('&')[1].slice(6); 87 | this.props.setToken(token); 88 | this.setState({ 89 | id: id, 90 | }); 91 | }; 92 | 93 | public componentDidUpdate(prevProps) { 94 | if (this.props.result !== prevProps.result) history.push('/'); 95 | }; 96 | 97 | private submit = (password1 : string, password2 : string) : void => { 98 | const password1Valid = this.validatePassword(password1, 1); 99 | const password2Valid = this.validatePassword(password2, 2); 100 | 101 | if (password1 !== password2) { 102 | this.setState({ 103 | match: i18n.t('validations.passwords_do_not_match'), 104 | }); 105 | return; 106 | } else { 107 | this.setState({ 108 | match: '', 109 | }); 110 | } 111 | 112 | if (password1Valid && password2Valid) { 113 | this.props.postNewPassword({ id: this.state.id, password: password1 }); 114 | } 115 | }; 116 | 117 | private validatePassword = (password : string, number: number) : boolean => { 118 | // eslint-disable-next-line no-useless-escape 119 | const regExp = /^(?=.*\d)(?=.*[a-z])(?!.*\s).*$/; 120 | const validate = regExp.test(password); 121 | const minLenght = UTILS.min_password_lenght; 122 | let passError; 123 | if (password === '') { 124 | passError = i18n.t('validations.is_required'); 125 | } else if (password.length < minLenght) { 126 | passError = i18n.t('validations.password_min_lenght.part1') + String(minLenght) + i18n.t('validations.password_min_lenght.part2'); 127 | } else if (!validate) { 128 | passError = i18n.t('password_contain_digit'); 129 | } else { 130 | passError = ''; 131 | } 132 | switch (number) { 133 | case 1: 134 | this.setState({ 135 | pass1Error: passError, 136 | }); 137 | break; 138 | case 2: 139 | this.setState({ 140 | pass2Error: passError, 141 | }); 142 | break; 143 | default: 144 | break; 145 | }; 146 | return validate; 147 | } 148 | 149 | render() { 150 | const { i18n, isFetching } = this.props; 151 | const { pass1Error, pass2Error, match } = this.state; 152 | 153 | return ( 154 | 155 | { isFetching ? 156 | : 157 | 158 | 159 | 160 | {i18n.t('password.title')} 161 | 162 |
163 | 164 | 170 | {!(pass1Error === '') 171 | && 172 | { pass1Error } 173 | } 174 | 175 | 176 | 182 | {!(pass2Error === '') 183 | && 184 | { pass2Error } 185 | } 186 | 187 | 188 | 195 | {!(match === '') 196 | && 197 | { match } 198 | } 199 | 200 |
201 |
202 |
203 |
} 204 |
205 | ); 206 | } 207 | }; 208 | 209 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 210 | hash: state.router.location.hash, 211 | isFetching: state.rootReducer.auth.isFetching, 212 | result: state.rootReducer.auth.result, 213 | }); 214 | 215 | const mapDispatchToProps = (dispatch: ThunkDispatch) : DispatchProps => ({ 216 | setToken: (token: string) => dispatch(setToken(token)), 217 | postNewPassword: (credentials: NewPasswordType) => dispatch(postNewPassword(credentials)), 218 | }); 219 | 220 | export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Login)); 221 | -------------------------------------------------------------------------------- /src/containers/auth/Social.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | 7 | import { history } from '../../store/store'; 8 | 9 | import { StoreType } from '../../store/types'; 10 | 11 | import { setToken } from '../../store/modules/auth/actions'; 12 | 13 | import Empty from '../../components/pages/Empty'; 14 | 15 | interface StateToProps { 16 | search : string; 17 | }; 18 | 19 | interface DispatchProps { 20 | setToken: (token: string) => void; 21 | } 22 | 23 | interface Props extends StateToProps, DispatchProps {}; 24 | 25 | const initialState = {}; 26 | 27 | type State = Readonly; 28 | 29 | class Verify extends React.Component { 30 | 31 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 32 | search: nextProps.search, 33 | }); 34 | 35 | public componentDidMount() { 36 | const token = this.props.search.slice(7); 37 | this.props.setToken(token); 38 | history.replace('/'); 39 | } 40 | 41 | readonly state : State = initialState; 42 | 43 | render() { 44 | return ; 45 | } 46 | }; 47 | 48 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 49 | search: state.router.location.search, 50 | }); 51 | 52 | const mapDispatchToProps = (dispatch: ThunkDispatch) : DispatchProps => ({ 53 | setToken: (token: string) => dispatch(setToken(token)), 54 | }); 55 | 56 | export default connect(mapStateToProps, mapDispatchToProps)(Verify); 57 | -------------------------------------------------------------------------------- /src/containers/auth/Verify.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | 7 | import { history } from '../../store/store'; 8 | 9 | import { 10 | StoreType, 11 | } from '../../store/types'; 12 | 13 | import { postVerify } from '../../store/modules/verify/actions'; 14 | 15 | import Empty from '../../components/pages/Empty'; 16 | 17 | interface StateToProps { 18 | search : string; 19 | result : string; 20 | }; 21 | 22 | interface DispatchProps { 23 | postVerify : (id: string) => void; 24 | } 25 | 26 | interface Props extends StateToProps, DispatchProps {}; 27 | 28 | const initialState = {}; 29 | 30 | type State = Readonly; 31 | 32 | class Verify extends React.Component { 33 | 34 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 35 | search: nextProps.search, 36 | result: nextProps.result, 37 | }); 38 | 39 | public componentDidMount() { 40 | const id = this.props.search.slice(4); 41 | this.props.postVerify(id); 42 | } 43 | 44 | public componentDidUpdate(prevProps) { 45 | if (this.props.result !== prevProps.result) history.push('/'); 46 | } 47 | 48 | readonly state : State = initialState; 49 | 50 | render() { 51 | return ; 52 | } 53 | }; 54 | 55 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 56 | search: state.router.location.search, 57 | result: state.rootReducer.verify.result, 58 | }); 59 | 60 | const mapDispatchToProps = (dispatch: ThunkDispatch) : DispatchProps => ({ 61 | postVerify: (id: string) => dispatch(postVerify(id)), 62 | }); 63 | 64 | export default connect(mapStateToProps, mapDispatchToProps)(Verify); 65 | -------------------------------------------------------------------------------- /src/containers/utils/LangSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | 7 | import { StoreType } from '../../store/types'; 8 | 9 | import { LANGUAGES } from '../../store/constants'; 10 | 11 | import { 12 | changeLanguage, 13 | clearMessages 14 | } from '../../store/modules/utils/actions'; 15 | 16 | import { 17 | Card, 18 | TextNormal, 19 | } from '../../theme/widgets'; 20 | 21 | interface StateToProps { 22 | language : string; 23 | }; 24 | 25 | interface DispatchProps { 26 | changeLanguage : (language: string) => void; 27 | clearMessages: () => void; 28 | } 29 | 30 | interface Props extends StateToProps, DispatchProps {}; 31 | 32 | const initialState = {}; 33 | 34 | type State = Readonly; 35 | 36 | class LangSwitch extends React.Component { 37 | 38 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 39 | language: nextProps.language, 40 | }); 41 | 42 | readonly state : State = initialState; 43 | 44 | render() { 45 | const { language } = this.props; 46 | 47 | return ( 48 | 49 | {LANGUAGES.map((lang, index) => { 50 | return ( 51 | 52 | {language !== lang.name ? 53 | { 56 | e.preventDefault(); 57 | this.props.changeLanguage(lang.name); 58 | this.props.clearMessages(); 59 | }} 60 | >{ lang.name } : 61 | { lang.name }} 62 | {index < LANGUAGES.length - 1 && / } 63 | ); 64 | })} 65 | 66 | ); 67 | } 68 | }; 69 | 70 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 71 | language: state.rootReducer.utils.language, 72 | }); 73 | 74 | const mapDispatchToProps = (dispatch: ThunkDispatch) : DispatchProps => ({ 75 | changeLanguage: (language: string) => dispatch(changeLanguage(language)), 76 | clearMessages: () => dispatch(clearMessages()), 77 | }); 78 | 79 | export default connect(mapStateToProps, mapDispatchToProps)(LangSwitch); 80 | -------------------------------------------------------------------------------- /src/containers/utils/ThemeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | import { AnyAction } from 'redux'; 5 | import { ThunkDispatch } from 'redux-thunk'; 6 | 7 | import { StoreType } from '../../store/types'; 8 | 9 | import { THEMES } from '../../store/constants'; 10 | 11 | import { changeTheme } from '../../store/modules/utils/actions'; 12 | 13 | import { 14 | Card, 15 | TextNormal, 16 | } from '../../theme/widgets'; 17 | 18 | interface StateToProps { 19 | theme : string; 20 | }; 21 | 22 | interface DispatchProps { 23 | changeTheme : (theme: string) => void; 24 | } 25 | 26 | interface Props extends StateToProps, DispatchProps {}; 27 | 28 | const initialState = {}; 29 | 30 | type State = Readonly; 31 | 32 | class ThemeSwitch extends React.Component { 33 | 34 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 35 | theme: nextProps.theme, 36 | }); 37 | 38 | readonly state : State = initialState; 39 | 40 | render() { 41 | const { theme } = this.props; 42 | 43 | return ( 44 | 45 | {THEMES.map((t, index) => { 46 | return ( 47 | 48 | {theme !== t.name ? 49 | { 52 | e.preventDefault(); 53 | this.props.changeTheme(t.name); 54 | }} 55 | >{ t.name } : 56 | { t.name }} 57 | {index < THEMES.length - 1 && / } 58 | ); 59 | })} 60 | 61 | ); 62 | } 63 | }; 64 | 65 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 66 | theme: state.rootReducer.utils.theme, 67 | }); 68 | 69 | const mapDispatchToProps = (dispatch: ThunkDispatch) : DispatchProps => ({ 70 | changeTheme: (theme: string) => dispatch(changeTheme(theme)), 71 | }); 72 | 73 | export default connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch); 74 | -------------------------------------------------------------------------------- /src/containers/wrappers/ThemeWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | import styled, { ThemeProvider } from 'styled-components'; 6 | 7 | import { THEMES } from '../../store/constants'; 8 | 9 | import { GlobalStyle } from "../../theme/theme"; 10 | import themeLight from "../../theme/themes/theme-light"; 11 | import themeDark from "../../theme/themes/theme-dark"; 12 | 13 | import App from '../App'; 14 | 15 | import { StoreType } from '../../store/types'; 16 | 17 | interface StateToProps { 18 | t : string; 19 | }; 20 | 21 | interface Props extends StateToProps {}; 22 | 23 | const initialState = {}; 24 | 25 | type State = Readonly; 26 | 27 | class ThemeWrapper extends React.Component { 28 | 29 | public static getDerivedStateFromProps = (nextProps : Props, prevState : State) => ({ 30 | t: nextProps.t, 31 | }); 32 | 33 | readonly state : State = initialState; 34 | 35 | render() { 36 | const { t } = this.props; 37 | 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | }; 48 | 49 | const mapStateToProps = (state : StoreType) : StateToProps => ({ 50 | t: state.rootReducer.utils.theme, 51 | }); 52 | 53 | export default connect(mapStateToProps, null)(ThemeWrapper); 54 | -------------------------------------------------------------------------------- /src/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { Provider } from 'react-redux'; 5 | import { ConnectedRouter } from "connected-react-router"; 6 | 7 | import ThemeWrapper from './containers/wrappers/ThemeWrapper'; 8 | 9 | import './utils/i18n'; 10 | 11 | import * as serviceWorker from './serviceWorker'; 12 | 13 | import store, { history } from './store/store'; 14 | 15 | ReactDOM.render(( 16 | 17 | 18 | 19 | 20 | 21 | ), document.getElementById('root')); 22 | 23 | // If you want your app to work offline and load faster, you can change 24 | // unregister() to register() below. Note this comes with some pitfalls. 25 | // Learn more about service workers: https://bit.ly/CRA-PWA 26 | serviceWorker.register(); 27 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /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.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 | 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 as { env: { [key: string]: string } }).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 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/store/constants.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | 3 | import { 4 | StoreType, 5 | ObjectOfStringsType, 6 | ObjectOfAnyType, 7 | CookiesType, 8 | LocalStorageType, 9 | LanguageObjectType, 10 | ThemeObjectType, 11 | } from './types'; 12 | 13 | export const COOKIES : CookiesType = { 14 | TOKEN: { 15 | name: 'token', 16 | expires: 7, 17 | }, 18 | LANG: { 19 | name: 'language', 20 | expires: 7, 21 | }, 22 | THEME: { 23 | name: 'theme', 24 | expires: 7, 25 | } 26 | }; 27 | 28 | 29 | const isProd : boolean = process.env.NODE_ENV === 'production'; 30 | const apiUrl : string = process.env.REACT_APP_API_URL; 31 | export const API_URL = isProd ? apiUrl || 'https://express-auth.kafedra.org' : apiUrl || 'http://localhost:8082'; 32 | 33 | 34 | // Auto auth 35 | export const AUTO_AUTH : string | null = Cookies.get(COOKIES.TOKEN.name) || null; 36 | const isAuth : boolean = AUTO_AUTH ? true : false; 37 | 38 | export const LANGUAGES : LanguageObjectType[] = [ 39 | { id: 1, name: 'en' }, 40 | { id: 2, name: 'ru' }, 41 | ]; 42 | 43 | // Auto language 44 | const language : string | null = Cookies.get(COOKIES.LANG.name) || null; 45 | export const AUTO_LANG : string = language || LANGUAGES[1].name; 46 | 47 | 48 | export const THEMES : ThemeObjectType[] = [ 49 | { id: 1, name: 'light'}, 50 | { id: 2, name: 'dark' }, 51 | ]; 52 | 53 | // Auto theme 54 | const theme : string | null = Cookies.get(COOKIES.THEME.name) || null; 55 | export const AUTO_THEME : string = theme || THEMES[1].name; 56 | 57 | 58 | export const INITIAL_STATE : StoreType = { 59 | rootReducer: { 60 | auth: { 61 | isFetching: false, 62 | isAuth: isAuth, 63 | error: '', 64 | success: '', 65 | result: '', 66 | }, 67 | verify: { 68 | isFetching: false, 69 | result: '', 70 | }, 71 | user: { 72 | isFetching: false, 73 | profile: { 74 | id: null, 75 | usermail: null, 76 | username: null, 77 | isVerify: false, 78 | userdata: [], 79 | }, 80 | error: '', 81 | success: false, 82 | }, 83 | utils: { 84 | language: AUTO_LANG, 85 | theme: AUTO_THEME, 86 | }, 87 | }, 88 | }; 89 | 90 | 91 | export const LOCALSTORAGE : LocalStorageType = { 92 | PROFILE: 'UserProfile', 93 | } 94 | 95 | export const UTILS : ObjectOfAnyType = { 96 | min_password_lenght: 6, 97 | } 98 | -------------------------------------------------------------------------------- /src/store/modules/auth/actions.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionCreator, Dispatch } from 'redux'; 2 | import { ThunkAction } from 'redux-thunk'; 3 | 4 | import { 5 | CredentialsType, 6 | NewPasswordType, 7 | } from '../../types'; 8 | 9 | import Api, { 10 | setAuth, 11 | deleteAuth, 12 | POST_AUTH_PATH, 13 | GET_AUTH_FACEBOOK_PATH, 14 | GET_AUTH_VKONTAKTE_PATH, 15 | POST_REMIND_PASSWORD_PATH, 16 | POST_NEW_PASSWORD_PATH, 17 | } from '../../../utils/api'; 18 | 19 | // Actions Types 20 | //////////////////////////////////////////////////////////// 21 | 22 | export const AUTH_REQUEST = 'AUTH_REQUEST'; 23 | export const AUTH_FACEBOOK_REQUEST = 'AUTH_FACEBOOK_REQUEST'; 24 | export const AUTH_VKONTAKTE_REQUEST = 'AUTH_VKONTAKTE_REQUEST'; 25 | export const SET_TOKEN = 'SET_TOKEN'; 26 | export const AUTH_SUCCESS = 'AUTH_SUCCESS'; 27 | export const AUTH_ERROR = 'AUTH_ERROR'; 28 | 29 | export const REMIND_PASSWORD_REQUEST = 'REMIND_PASSWORD'; 30 | export const REMIND_PASSWORD_SUCCESS = 'REMIND_PASSWORD_SUCCESS'; 31 | export const REMIND_PASSWORD_ERROR = 'REMIND_PASSWORD_ERROR'; 32 | 33 | export const SET_NEW_PASSWORD = 'SET_NEW_PASSWORD'; 34 | export const SET_NEW_PASSWORD_RESULT = 'SET_NEW_PASSWORD_RESULT'; 35 | 36 | export const AUTH_LOGOUT = 'AUTH_LOGOUT'; 37 | 38 | // Action Creators 39 | //////////////////////////////////////////////////////////// 40 | 41 | export const authRequest : ActionCreator = () => { 42 | return { 43 | type: AUTH_REQUEST, 44 | }; 45 | }; 46 | 47 | export const authFacebookRequest : ActionCreator = () => { 48 | return { 49 | type: AUTH_FACEBOOK_REQUEST, 50 | }; 51 | }; 52 | 53 | export const authVkontakteRequest : ActionCreator = () => { 54 | return { 55 | type: AUTH_VKONTAKTE_REQUEST, 56 | }; 57 | }; 58 | 59 | export const setToken : ActionCreator = (token: string) => { 60 | setAuth(token); 61 | return { 62 | type: SET_TOKEN, 63 | token, 64 | }; 65 | }; 66 | 67 | export const authSuccess : ActionCreator = () => { 68 | return { 69 | type: AUTH_SUCCESS, 70 | }; 71 | }; 72 | 73 | export const authError : ActionCreator = (error: string) => { 74 | return { 75 | type: AUTH_ERROR, 76 | error, 77 | }; 78 | }; 79 | 80 | export const postAuth: ActionCreator, Action, void, any>> 81 | = (credentials : CredentialsType) => { 82 | return async (dispatch : Dispatch) : Promise => { 83 | dispatch(authRequest()); 84 | try { 85 | const response = await Api.post(POST_AUTH_PATH, { user: credentials }); 86 | const { token } = response.data.user; 87 | setAuth(token); 88 | return dispatch(authSuccess()); 89 | } catch (e) { 90 | return dispatch(authError(e.response.data.message)); 91 | }; 92 | }; 93 | }; 94 | 95 | export const getFacebookAuth: ActionCreator, Action, void, any>> 96 | = () => { 97 | return async (dispatch : Dispatch) : Promise => { 98 | dispatch(authFacebookRequest()); 99 | try { 100 | const response = await Api.get(GET_AUTH_FACEBOOK_PATH); 101 | const redirect = response.data._redirect_url; 102 | if (redirect) { 103 | window.location.href = redirect; 104 | return; 105 | } 106 | } catch (e) { 107 | return dispatch(authError(e.response.data.message)); 108 | }; 109 | }; 110 | }; 111 | 112 | export const getVkontakteAuth: ActionCreator, Action, void, any>> 113 | = () => { 114 | return async (dispatch : Dispatch) : Promise => { 115 | dispatch(authVkontakteRequest()); 116 | try { 117 | const response = await Api.get(GET_AUTH_VKONTAKTE_PATH); 118 | const redirect = response.data._redirect_url; 119 | if (redirect) { 120 | window.location.href = redirect; 121 | return; 122 | } 123 | } catch (e) { 124 | return dispatch(authError(e.response.data.message)); 125 | }; 126 | }; 127 | }; 128 | 129 | export const remindPasswordRequest : ActionCreator = () => { 130 | return { 131 | type: REMIND_PASSWORD_REQUEST, 132 | }; 133 | }; 134 | 135 | export const remindPasswordSuccess : ActionCreator = (message: string) => { 136 | return { 137 | type: REMIND_PASSWORD_SUCCESS, 138 | message, 139 | }; 140 | }; 141 | 142 | export const remindPasswordError : ActionCreator = (error: string) => { 143 | return { 144 | type: REMIND_PASSWORD_ERROR, 145 | error, 146 | }; 147 | }; 148 | 149 | export const postRemindPassword: ActionCreator, Action, void, any>> 150 | = (usermail: string) => { 151 | return async (dispatch : Dispatch) : Promise => { 152 | dispatch(remindPasswordRequest()); 153 | try { 154 | const response = await Api.post(POST_REMIND_PASSWORD_PATH, { usermail }); 155 | return dispatch(remindPasswordSuccess(response.data.message)); 156 | } catch (e) { 157 | const error = e.response.data ? e.response.data.message : e.response.statusText; 158 | return dispatch(remindPasswordError(e.response.data.message)); 159 | }; 160 | }; 161 | }; 162 | 163 | export const setNewPassword : ActionCreator = () => { 164 | return { 165 | type: SET_NEW_PASSWORD, 166 | }; 167 | }; 168 | 169 | export const setNewPasswordResult : ActionCreator = (result: string) => { 170 | return { 171 | type: SET_NEW_PASSWORD_RESULT, 172 | result, 173 | }; 174 | }; 175 | 176 | export const postNewPassword: ActionCreator, Action, void, any>> 177 | = (credentials : NewPasswordType) => { 178 | return async (dispatch : Dispatch): Promise => { 179 | const user = { id: credentials.id, password: credentials.password } 180 | dispatch(setNewPassword()); 181 | try { 182 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 183 | const response = await Api.post(POST_NEW_PASSWORD_PATH, { user }); 184 | return dispatch(setNewPasswordResult(response.data.message)); 185 | } catch (e) { 186 | return dispatch(setNewPasswordResult(e.response.data.message)); 187 | }; 188 | }; 189 | }; 190 | 191 | export const authLogout : ActionCreator = () => { 192 | deleteAuth(); 193 | return { 194 | type: AUTH_LOGOUT, 195 | }; 196 | }; 197 | -------------------------------------------------------------------------------- /src/store/modules/auth/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | import { INITIAL_STATE } from '../../constants'; 4 | import { StoreType } from '../../types'; 5 | 6 | import { 7 | AUTH_REQUEST, 8 | AUTH_FACEBOOK_REQUEST, 9 | AUTH_VKONTAKTE_REQUEST, 10 | SET_TOKEN, 11 | AUTH_SUCCESS, 12 | AUTH_ERROR, 13 | REMIND_PASSWORD_REQUEST, 14 | REMIND_PASSWORD_SUCCESS, 15 | REMIND_PASSWORD_ERROR, 16 | SET_NEW_PASSWORD, 17 | SET_NEW_PASSWORD_RESULT, 18 | AUTH_LOGOUT, 19 | } from './actions'; 20 | import { 21 | CLEAR_MESSAGES, 22 | } from '../utils/actions'; 23 | 24 | const auth = (state : StoreType, action: Action & any) => { 25 | if (typeof state === 'undefined') { 26 | return INITIAL_STATE; 27 | } 28 | 29 | switch (action.type) { 30 | case AUTH_REQUEST: 31 | return Object.assign({}, state, { 32 | isFetching: true, 33 | }); 34 | case AUTH_FACEBOOK_REQUEST: 35 | return Object.assign({}, state, { 36 | isFetching: true, 37 | }); 38 | case AUTH_VKONTAKTE_REQUEST: 39 | return Object.assign({}, state, { 40 | isFetching: true, 41 | }); 42 | case SET_TOKEN: 43 | return Object.assign({}, state, { 44 | isFetching: false, 45 | isAuth: true, 46 | }); 47 | case AUTH_SUCCESS: 48 | return Object.assign({}, state, { 49 | isFetching: false, 50 | isAuth: true, 51 | }); 52 | case AUTH_ERROR: 53 | return Object.assign({}, state, { 54 | isFetching: false, 55 | isAuth: false, 56 | error: action.error, 57 | }); 58 | case REMIND_PASSWORD_REQUEST: 59 | return Object.assign({}, state, { 60 | isFetching: true, 61 | }); 62 | case REMIND_PASSWORD_SUCCESS: 63 | return Object.assign({}, state, { 64 | isFetching: false, 65 | success: action.message, 66 | }); 67 | case REMIND_PASSWORD_ERROR: 68 | return Object.assign({}, state, { 69 | isFetching: false, 70 | error: action.error, 71 | }); 72 | case SET_NEW_PASSWORD: 73 | return Object.assign({}, state, { 74 | isFetching: true, 75 | }); 76 | case SET_NEW_PASSWORD_RESULT: 77 | return Object.assign({}, state, { 78 | isFetching: false, 79 | result: action.result, 80 | }); 81 | case CLEAR_MESSAGES: 82 | return Object.assign({}, state, { 83 | success: '', 84 | error: '', 85 | }); 86 | case AUTH_LOGOUT: 87 | return Object.assign({}, state, { 88 | isFetching: false, 89 | isAuth: false, 90 | }); 91 | default: 92 | return state; 93 | }; 94 | }; 95 | 96 | export default auth; 97 | -------------------------------------------------------------------------------- /src/store/modules/user/actions.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionCreator, Dispatch } from 'redux'; 2 | import { ThunkAction } from 'redux-thunk'; 3 | 4 | import Api, { 5 | GET_USER_PATH, 6 | POST_VERIFY_EMAIL_PATH, 7 | } from '../../../utils/api'; 8 | 9 | import { CredentialsType } from '../../types'; 10 | 11 | // Actions Types 12 | //////////////////////////////////////////////////////////// 13 | 14 | export const USER_REQUEST = 'USER_REQUEST'; 15 | export const USER_SUCCESS = 'USER_SUCCESS'; 16 | export const USER_ERROR = 'USER_ERROR'; 17 | 18 | export const SEND_VERIFY_EMAIL = 'SEND_VERIFY_EMAIL'; 19 | export const SEND_VERIFY_EMAIL_SUCCESS = 'SEND_VERIFY_EMAIL_SUCCESS'; 20 | export const SEND_VERIFY_EMAIL_ERROR = 'SEND_VERIFY_EMAIL_ERROR'; 21 | 22 | // Action Creators 23 | //////////////////////////////////////////////////////////// 24 | 25 | export const userRequest : ActionCreator = () => { 26 | return { 27 | type: USER_REQUEST, 28 | }; 29 | }; 30 | 31 | export const userSuccess : ActionCreator = (profile : string) => { 32 | return { 33 | type: USER_SUCCESS, 34 | profile, 35 | }; 36 | }; 37 | 38 | export const userError : ActionCreator = (error: string) => { 39 | return { 40 | type: USER_ERROR, 41 | error, 42 | }; 43 | }; 44 | 45 | export const getUser : ActionCreator, Action, void, any>> 46 | = () => { 47 | return async (dispatch : Dispatch) : Promise => { 48 | dispatch(userRequest()); 49 | try { 50 | const response = await Api.get(GET_USER_PATH); 51 | return dispatch(userSuccess(response.data.user)); 52 | } catch (e) { 53 | console.log(e); 54 | const error = e.response.data ? e.response.data.message : e.response.statusText; 55 | return dispatch(userError(error)); 56 | }; 57 | }; 58 | }; 59 | 60 | export const sendVerifyEmail : ActionCreator = () => { 61 | return { 62 | type: SEND_VERIFY_EMAIL, 63 | }; 64 | }; 65 | 66 | export const sendVerifyEmailSuccess : ActionCreator = () => { 67 | return { 68 | type: SEND_VERIFY_EMAIL_SUCCESS, 69 | }; 70 | }; 71 | 72 | export const sendVerifyEmailError : ActionCreator = (error: string) => { 73 | return { 74 | type: SEND_VERIFY_EMAIL_ERROR, 75 | error, 76 | }; 77 | }; 78 | 79 | export const postVerifyEmail : ActionCreator, Action, void, any>> 80 | = (usermail: string) => { 81 | return async (dispatch : Dispatch) : Promise => { 82 | dispatch(sendVerifyEmail()); 83 | try { 84 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 85 | const response = await Api.post(POST_VERIFY_EMAIL_PATH, { usermail }); 86 | return dispatch(sendVerifyEmailSuccess()); 87 | } catch (e) { 88 | const error = e.response.data ? e.response.data.message : e.response.statusText; 89 | return dispatch(sendVerifyEmailError(error)); 90 | }; 91 | }; 92 | }; 93 | 94 | -------------------------------------------------------------------------------- /src/store/modules/user/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | import { INITIAL_STATE } from '../../constants'; 4 | import { StoreType } from '../../types'; 5 | 6 | import { 7 | USER_REQUEST, 8 | USER_SUCCESS, 9 | USER_ERROR, 10 | SEND_VERIFY_EMAIL, 11 | SEND_VERIFY_EMAIL_SUCCESS, 12 | SEND_VERIFY_EMAIL_ERROR, 13 | } from './actions'; 14 | import { AUTH_LOGOUT } from '../auth/actions'; 15 | 16 | const user = (state : StoreType, action: Action & any) => { 17 | if (typeof state === 'undefined') { 18 | return INITIAL_STATE; 19 | } 20 | 21 | switch (action.type) { 22 | case USER_REQUEST: 23 | return Object.assign({}, state, { 24 | isFetching: true, 25 | }); 26 | case USER_SUCCESS: 27 | return Object.assign({}, state, { 28 | isFetching: false, 29 | profile: action.profile, 30 | success: false, 31 | }); 32 | case USER_ERROR: 33 | return Object.assign({}, state, { 34 | isFetching: false, 35 | error: action.error 36 | }); 37 | case SEND_VERIFY_EMAIL: 38 | return Object.assign({}, state, { 39 | isFetching: true, 40 | }); 41 | case SEND_VERIFY_EMAIL_SUCCESS: 42 | return Object.assign({}, state, { 43 | isFetching: false, 44 | success: true, 45 | }); 46 | case SEND_VERIFY_EMAIL_ERROR: 47 | return Object.assign({}, state, { 48 | isFetching: false, 49 | error: action.error 50 | }); 51 | case AUTH_LOGOUT: 52 | return Object.assign({}, state, { 53 | profile: {}, 54 | success: '', 55 | }); 56 | default: 57 | return state; 58 | }; 59 | }; 60 | 61 | export default user; 62 | -------------------------------------------------------------------------------- /src/store/modules/utils/actions.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionCreator, Dispatch } from 'redux'; 2 | import { ThunkAction } from 'redux-thunk'; 3 | 4 | import i18n from '../../../utils/i18n'; 5 | import { 6 | rememberLanguage, 7 | rememberTheme, 8 | } from '../../../utils/api'; 9 | 10 | // Actions Types 11 | //////////////////////////////////////////////////////////// 12 | 13 | export const CLEAR_MESSAGES = 'CLEAR_MESSAGES'; 14 | export const SET_LANGUAGE = 'SET_LANGUAGE'; 15 | export const SET_THEME = 'SET_THEME'; 16 | 17 | // Action Creators 18 | //////////////////////////////////////////////////////////// 19 | 20 | export const clearMessages : ActionCreator = () => { 21 | return { 22 | type: CLEAR_MESSAGES, 23 | }; 24 | }; 25 | 26 | export const setLanguage : ActionCreator = (language: string) => { 27 | rememberLanguage(language); 28 | return { 29 | type: SET_LANGUAGE, 30 | language, 31 | }; 32 | }; 33 | 34 | export const changeLanguage : ActionCreator> 35 | = (language: string) => { 36 | return dispatch => { 37 | return i18n.changeLanguage(language) 38 | .then((t) => { 39 | dispatch(setLanguage(language)); 40 | }, 41 | (error) => {} 42 | ); 43 | }; 44 | }; 45 | 46 | export const changeTheme : ActionCreator = (theme: string) => { 47 | rememberTheme(theme); 48 | return { 49 | type: SET_THEME, 50 | theme, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/store/modules/utils/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | import { INITIAL_STATE } from '../../constants'; 4 | import { StoreType } from '../../types'; 5 | 6 | import { 7 | SET_LANGUAGE, 8 | SET_THEME, 9 | } from './actions'; 10 | 11 | const utils = (state : StoreType, action: Action & any) => { 12 | if (typeof state === 'undefined') { 13 | return INITIAL_STATE; 14 | } 15 | 16 | switch (action.type) { 17 | case SET_LANGUAGE: 18 | return Object.assign({}, state, { 19 | language: action.language, 20 | }); 21 | case SET_THEME: 22 | return Object.assign({}, state, { 23 | theme: action.theme, 24 | }); 25 | default: 26 | return state; 27 | }; 28 | }; 29 | 30 | export default utils; 31 | -------------------------------------------------------------------------------- /src/store/modules/verify/actions.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionCreator, Dispatch } from 'redux'; 2 | import { ThunkAction } from 'redux-thunk'; 3 | 4 | import Api, { POST_VERIFY } from '../../../utils/api'; 5 | 6 | // Actions Types 7 | //////////////////////////////////////////////////////////// 8 | 9 | export const VERIFY_REQUEST = 'VERIFY_REQUEST'; 10 | export const VERIFY_REQUEST_RESULT = 'VERIFY_REQUEST_RESULT'; 11 | 12 | // Action Creators 13 | //////////////////////////////////////////////////////////// 14 | 15 | export const verifyRequest : ActionCreator = () => { 16 | return { 17 | type: VERIFY_REQUEST, 18 | }; 19 | }; 20 | 21 | export const verifyRequestResult : ActionCreator = (result: string) => { 22 | return { 23 | type: VERIFY_REQUEST_RESULT, 24 | result, 25 | }; 26 | }; 27 | 28 | export const postVerify : ActionCreator, Action, void, any>> 29 | = (id : string) => { 30 | return async (dispatch : Dispatch) : Promise => { 31 | dispatch(verifyRequest()); 32 | try { 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | const response = await Api.post(POST_VERIFY, { id }); 35 | return dispatch(verifyRequestResult(response.data.message)); 36 | } catch (e) { 37 | return dispatch(verifyRequestResult(e.response.data.message)); 38 | }; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/store/modules/verify/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | import { INITIAL_STATE } from '../../constants'; 4 | import { StoreType } from '../../types'; 5 | 6 | import { 7 | VERIFY_REQUEST, 8 | VERIFY_REQUEST_RESULT, 9 | } from './actions'; 10 | 11 | const verify = (state : StoreType, action: Action & any) => { 12 | if (typeof state === 'undefined') { 13 | return INITIAL_STATE; 14 | } 15 | 16 | switch (action.type) { 17 | case VERIFY_REQUEST: 18 | return Object.assign({}, state, { 19 | isFetching: true, 20 | }); 21 | case VERIFY_REQUEST_RESULT: 22 | return Object.assign({}, state, { 23 | isFetching: false, 24 | result: action.result, 25 | }); 26 | default: 27 | return state; 28 | }; 29 | }; 30 | 31 | export default verify; 32 | -------------------------------------------------------------------------------- /src/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import auth from './modules/auth/reducer'; 4 | import verify from './modules/verify/reducer'; 5 | import utils from './modules/utils/reducer'; 6 | import user from './modules/user/reducer'; 7 | 8 | const rootReducer = combineReducers({ 9 | auth, 10 | verify, 11 | utils, 12 | user, 13 | }); 14 | 15 | export default rootReducer; 16 | -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import { connectRouter } from 'connected-react-router'; 4 | import { createBrowserHistory as createHistory } from 'history'; 5 | import thunkMiddleware from 'redux-thunk'; 6 | import { createLogger } from 'redux-logger'; 7 | 8 | import { INITIAL_STATE, LOCALSTORAGE } from './constants'; 9 | import { StoreType } from './types'; 10 | import rootReducer from './reducers'; 11 | 12 | const middlewares : any[] = []; 13 | middlewares.push(thunkMiddleware); 14 | 15 | if (process.env.NODE_ENV !== 'production') { 16 | const loggerMiddleware = createLogger(); 17 | 18 | middlewares.push(loggerMiddleware); 19 | } 20 | 21 | const localStorageMiddleware = ({getState} : any) => { 22 | return (next : any) => (action: any) => { 23 | const result = next(action); 24 | if (getState().rootReducer.auth.isAuth) { 25 | localStorage.setItem(LOCALSTORAGE.PROFILE, JSON.stringify( 26 | getState().rootReducer.user.profile, 27 | )); 28 | } 29 | return result; 30 | }; 31 | }; 32 | middlewares.push(localStorageMiddleware); 33 | 34 | const reHydrateStore = (state: StoreType) => { 35 | if (localStorage.getItem(LOCALSTORAGE.PROFILE) !== null) { 36 | const localData = JSON.parse(localStorage.getItem(LOCALSTORAGE.PROFILE) || '{}'); 37 | const _state = Object.assign({}, state, { 38 | rootReducer: { 39 | ...state.rootReducer, 40 | user: { 41 | ...state.rootReducer.user, 42 | profile: localData, 43 | }, 44 | }, 45 | }); 46 | return _state; 47 | } 48 | return state; 49 | }; 50 | 51 | export const history = createHistory(); 52 | middlewares.push(routerMiddleware(history)); 53 | 54 | function configureStore(state : StoreType) { 55 | return createStore( 56 | combineReducers({ 57 | rootReducer, 58 | router: connectRouter(history), 59 | } as any), 60 | reHydrateStore(state), 61 | applyMiddleware(...middlewares) 62 | ); 63 | } 64 | 65 | const store = configureStore(INITIAL_STATE); 66 | 67 | // console.log('Store: ', store.getState()); 68 | 69 | export default store; 70 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | export interface StoreType { 2 | readonly rootReducer: { 3 | readonly auth : { 4 | readonly isFetching : boolean; 5 | readonly isAuth : boolean; 6 | readonly error : string; 7 | readonly success : string; 8 | readonly result : string; 9 | }; 10 | readonly verify : { 11 | readonly isFetching : boolean; 12 | readonly result : string; 13 | }; 14 | readonly user : { 15 | readonly isFetching : boolean; 16 | readonly profile : { 17 | readonly id : string | null; 18 | readonly usermail : string | null; 19 | readonly username : string | null; 20 | readonly isVerify : boolean; 21 | readonly userdata : []; 22 | }; 23 | readonly error : string; 24 | readonly success : boolean | null; 25 | }; 26 | readonly utils : { 27 | readonly language : string; 28 | readonly theme : string; 29 | }; 30 | }; 31 | readonly router? : any; 32 | }; 33 | 34 | export interface ObjectOfStringsType { 35 | readonly [key: string] : string; 36 | }; 37 | 38 | export interface ObjectOfAnyType { 39 | readonly [key: string] : any; 40 | }; 41 | 42 | export interface LanguageObjectType { 43 | readonly id: number; 44 | readonly name: string; 45 | }; 46 | 47 | export interface ThemeObjectType extends LanguageObjectType {}; 48 | 49 | export interface CredentialsType { 50 | readonly usermail: string; 51 | readonly password: string; 52 | }; 53 | 54 | export interface NewPasswordType { 55 | readonly id: string; 56 | readonly password: string; 57 | }; 58 | 59 | interface CookieType { 60 | readonly name : string; 61 | readonly expires : number; 62 | }; 63 | 64 | export interface CookiesType { 65 | readonly [key: string] : CookieType; 66 | }; 67 | 68 | export interface LocalStorageType { 69 | readonly [key: string] : string; 70 | }; 71 | 72 | -------------------------------------------------------------------------------- /src/theme/styled.d.ts: -------------------------------------------------------------------------------- 1 | import 'styled-components'; 2 | 3 | declare module 'styled-components' { 4 | export interface DefaultTheme { 5 | // Sizes and layouts 6 | sizes? : { 7 | gutter? : number; 8 | input_height? : number; 9 | header_height? : number; 10 | layout_front? : number; 11 | }; 12 | 13 | // Media breackpoints 14 | breackpoints? : { 15 | xs_middle? : string; 16 | xs_max? : string; 17 | }; 18 | 19 | // Colors 20 | colors? : { 21 | // Pallette 22 | color_white? : string; 23 | color_black? : string; 24 | color_mint? : string; 25 | color_red? : string; 26 | color_green? : string; 27 | 28 | // Functional 29 | color_background? : string; 30 | color_text? : string; 31 | color_text_light? : string; 32 | color_link? : string; 33 | color_link_hover? : string, 34 | color_disabled? : string, 35 | color_placeholder? : string, 36 | color_border? : string; 37 | color_shadow? : string; 38 | color_card? : string; 39 | color_header? : string, 40 | 41 | color_primary? : string; 42 | color_success? : string; 43 | color_error? : string; 44 | 45 | // Brands 46 | color_fb? : string; 47 | color_vk? : string; 48 | }; 49 | 50 | // Typography 51 | typography? : { 52 | fontfamily_sans? : string; 53 | fontweight_sans_regular? : number; 54 | fontweight_sans_bold? : number; 55 | letterspacing_normal? : string; 56 | 57 | fontsize_large? : number; 58 | fontsize_normal? : number; 59 | fontsize_small? : number; 60 | 61 | line_height_standart? : number; 62 | // Good line height for all font sizes 63 | lineheight_large? : number; 64 | lineheight_normal? : number; 65 | lineheight_small? : number; 66 | }; 67 | 68 | // Shadow 69 | shadows? : { 70 | shadow_offset_x? : number; 71 | shadow_offset_y? : number; 72 | shadow_size? : number; 73 | shadow_spread: number; 74 | }; 75 | 76 | // Effects 77 | effects? : { 78 | transition_duration: string, 79 | transition_timingfunction: string, 80 | }; 81 | 82 | // Roundings 83 | border_radius? : { 84 | small: string; 85 | large: string; 86 | }; 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /src/theme/theme.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme, createGlobalStyle } from 'styled-components'; 2 | 3 | const theme : DefaultTheme = { 4 | // Sizes and layouts 5 | sizes: { 6 | gutter: 20, 7 | header_height: 70, 8 | input_height: 40, 9 | layout_front: 900, 10 | }, 11 | 12 | // Media breackpoints 13 | breackpoints: { 14 | xs_middle: '360px', 15 | xs_max: '750px', 16 | }, 17 | 18 | // Colors 19 | colors: { 20 | // Pallette 21 | color_white: '#ffffff', 22 | color_black: '#000000', 23 | color_mint: '#00A287', 24 | color_red: '#f76c70', 25 | color_green: '#00C20D', 26 | 27 | // Functional 28 | color_header: '#131920', 29 | color_placeholder: '#a4adb7', 30 | color_shadow: 'rgba(0, 0, 0, 0.15)', 31 | 32 | // Brand 33 | color_fb: '#3b5998', 34 | color_vk: '#45668e', 35 | }, 36 | 37 | // Typography 38 | typography: { 39 | fontfamily_sans: 'sans-serif', 40 | fontweight_sans_regular: 400, 41 | fontweight_sans_bold: 700, 42 | letterspacing_normal: 'normal', 43 | 44 | fontsize_large: 18, 45 | fontsize_normal: 16, 46 | fontsize_small: 13, 47 | 48 | line_height_standart: 1.428571429, 49 | }, 50 | 51 | // Shadows 52 | shadows: { 53 | shadow_offset_x: 0, 54 | shadow_offset_y: 2, 55 | shadow_size: 4, 56 | shadow_spread: -1, 57 | }, 58 | 59 | // Effects 60 | effects: { 61 | transition_duration: '0.2s', 62 | transition_timingfunction: 'linear', 63 | }, 64 | 65 | // Roundings 66 | border_radius : { 67 | small: '2px', 68 | large: '5px', 69 | }, 70 | }; 71 | 72 | // Dependencies 73 | 74 | Object.assign(theme, { 75 | ...theme, 76 | colors: { 77 | ...theme.colors, 78 | color_primary: theme.colors.color_mint, 79 | color_success: theme.colors.color_green, 80 | color_error: theme.colors.color_red, 81 | color_disabled: theme.colors.color_placeholder, 82 | }, 83 | // Good line height for all font sizes 84 | typography: { 85 | ...theme.typography, 86 | lineheight_large: Math.floor(theme.typography.fontsize_large * theme.typography.line_height_standart), 87 | lineheight_normal: Math.floor(theme.typography.fontsize_normal * theme.typography.line_height_standart), 88 | lineheight_small: Math.floor(theme.typography.fontsize_small * theme.typography.line_height_standart), 89 | }, 90 | }); 91 | 92 | Object.assign(theme, { 93 | ...theme, 94 | colors: { 95 | ...theme.colors, 96 | color_link: theme.colors.color_primary, 97 | color_link_hover: theme.colors.color_primary, 98 | }, 99 | }); 100 | 101 | // console.log('Theme variables: ', theme); 102 | 103 | export const GlobalStyle = createGlobalStyle` 104 | #root, 105 | html { 106 | height: 100%; 107 | } 108 | 109 | body { 110 | height: 100%; 111 | margin: 0 112 | padding: 0; 113 | color: ${theme.colors.color_black}; 114 | background-color: ${theme.colors.color_white}; 115 | font-family: ${theme.typography.fontfamily_sans}; 116 | font-size: ${theme.typography.fontsize_small}px; 117 | line-height: ${theme.typography.lineheight_small}px; 118 | font-weight: ${theme.typography.fontweight_sans_regular}; 119 | letter-spacing: ${theme.typography.letterspacing_normal}; 120 | overflow-x: hidden; 121 | backface-visibility: hidden; 122 | -webkit-font-smoothing: antialiased; 123 | -moz-osx-font-smoothing: grayscale; 124 | } 125 | 126 | a { 127 | cursor: pointer; 128 | color: ${theme.colors.color_link}; 129 | text-decoration: none; 130 | &:hover { 131 | color: ${theme.colors.color_link_hover}; 132 | } 133 | } 134 | 135 | ul { 136 | padding-left: 0; 137 | list-style: none; 138 | } 139 | 140 | button { 141 | cursor: pointer; 142 | } 143 | 144 | button, 145 | input, 146 | textarea, 147 | select, 148 | a { 149 | outline: none !important; 150 | &:hover, 151 | &:active, 152 | &:focus { 153 | outline: none !important; 154 | } 155 | } 156 | 157 | // Placeholders 158 | ::-webkit-input-placeholder, 159 | ::-moz-placeholder, 160 | :-moz-placeholder, 161 | :-ms-input-placeholder { 162 | color: ${theme.colors.color_placeholder}; 163 | } 164 | 165 | img { 166 | border-style: none; // Remove the border on images inside links in IE 10. 167 | } 168 | 169 | button, 170 | input { 171 | overflow: visible; // Show the overflow in Edge 172 | } 173 | 174 | button, 175 | select { 176 | text-transform: none; // Remove the inheritance of text transform in Firefox. 177 | } 178 | 179 | textarea { 180 | overflow: auto; // Remove the default vertical scrollbar in IE 10+. 181 | } 182 | 183 | strong { 184 | font-weight: ${theme.typography.fontweight_sans_bold}; 185 | } 186 | `; 187 | 188 | export default theme; 189 | -------------------------------------------------------------------------------- /src/theme/themes/theme-dark.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | 3 | import theme from '../theme'; 4 | 5 | const themeDark : DefaultTheme = {}; 6 | 7 | // Dependencies 8 | 9 | Object.assign(themeDark, { 10 | ...theme, 11 | colors: { 12 | ...theme.colors, 13 | 14 | // Functional 15 | color_background: '#2f4050', 16 | color_text: '#aebfd0', 17 | color_card: '#263340', 18 | color_border: '#1c2630', 19 | }, 20 | }); 21 | 22 | // console.log('Theme dark variables: ', themeDark); 23 | export default themeDark; 24 | -------------------------------------------------------------------------------- /src/theme/themes/theme-light.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | 3 | import theme from '../theme'; 4 | 5 | const themeLight : DefaultTheme = {}; 6 | 7 | // Dependencies 8 | 9 | Object.assign(themeLight, { 10 | ...theme, 11 | colors: { 12 | ...theme.colors, 13 | 14 | // Functional 15 | color_background: '#fafafa', 16 | color_text: '#6c7a89', 17 | color_card: theme.colors.color_white, 18 | color_border: '#dfe5eb', 19 | }, 20 | }); 21 | 22 | // console.log('Theme light variables: ', themeLight); 23 | export default themeLight; 24 | -------------------------------------------------------------------------------- /src/theme/widgets.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css, keyframes } from 'styled-components'; 2 | 3 | // Content 4 | //////////////////////////////////////////////////////////// 5 | 6 | export const A = styled.a` 7 | white-space: nowrap; 8 | text-decoration: underline; 9 | color: ${props => props.theme.colors.color_link}; 10 | 11 | &:hover { 12 | text-decoration: none; 13 | color: ${props => props.theme.colors.color_link_hover}; 14 | } 15 | `; 16 | 17 | export const TextSmall = styled.span` 18 | display: inline; 19 | color: inherit; 20 | font-family: ${props => props.theme.typography.fontfamily_sans}; 21 | font-size: ${props => props.theme.typography.fontsize_small}px; 22 | line-height: ${props => props.theme.typography.lineheight_small}px; 23 | font-weight: ${props => props.theme.typography.fontweight_sans_regular}; 24 | letter-spacing: ${props => props.theme.typography.letterspacing_normal}; 25 | `; 26 | 27 | interface TextNormalProps { 28 | readonly label? : boolean; 29 | readonly bold? : boolean; 30 | readonly uppercase? : boolean; 31 | }; 32 | 33 | export const TextNormal = styled.span` 34 | display: inline; 35 | color: ${props => props.theme.colors.color_text}; 36 | font-family: ${props => props.theme.typography.fontfamily_sans}; 37 | font-size: ${props => props.theme.typography.fontsize_normal}px; 38 | line-height: ${props => props.theme.typography.lineheight_normal}px; 39 | font-weight: ${props => props.theme.typography.fontweight_sans_regular}; 40 | letter-spacing: ${props => props.theme.typography.letterspacing_normal}; 41 | 42 | ${props => props.bold && css` 43 | font-weight: ${props => props.theme.typography.fontweight_sans_bold}; 44 | `} 45 | 46 | ${props => props.uppercase && css` 47 | text-transform: uppercase; 48 | `} 49 | `; 50 | 51 | interface TextLargeProps { 52 | readonly super? : boolean; 53 | readonly light? : boolean; 54 | }; 55 | 56 | export const TextLarge = styled.span` 57 | display: inline; 58 | color: ${props => props.theme.colors.color_text}; 59 | font-family: ${props => props.theme.typography.fontfamily_sans}; 60 | font-size: ${props => props.theme.typography.fontsize_large}px; 61 | line-height: ${props => props.theme.typography.lineheight_large}px; 62 | font-weight: ${props => props.theme.typography.fontweight_sans_bold}; 63 | letter-spacing: ${props => props.theme.typography.letterspacing_normal}; 64 | 65 | ${props => props.super && css` 66 | font-size: calc(${props => props.theme.typography.fontsize_large}px * 4); 67 | line-height: calc(${props => props.theme.typography.lineheight_large}px * 4); 68 | `} 69 | 70 | ${props => props.light && css` 71 | opacity: 0.5; 72 | `} 73 | `; 74 | 75 | export const TextHeader = styled.span` 76 | display: inline; 77 | color: ${props => props.theme.colors.color_header}; 78 | font-family: ${props => props.theme.typography.fontfamily_sans}; 79 | font-size: calc(${props => props.theme.typography.fontsize_large}px * 1.5); 80 | line-height: ${props => props.theme.typography.lineheight_large}px; 81 | font-weight: ${props => props.theme.typography.fontweight_sans_bold}; 82 | letter-spacing: ${props => props.theme.typography.letterspacing_normal}; 83 | 84 | ${props => props.super && css` 85 | font-size: calc(${props => props.theme.typography.fontsize_large}px * 4); 86 | line-height: calc(${props => props.theme.typography.lineheight_large}px * 4); 87 | `} 88 | 89 | ${props => props.light && css` 90 | opacity: 0.5; 91 | `} 92 | 93 | @media screen and (max-width: ${props => props.theme.breackpoints.xs_middle}) { 94 | display: inline-block; 95 | margin-top: 20px; 96 | } 97 | `; 98 | 99 | interface TextStringProps { 100 | readonly top? : boolean; 101 | }; 102 | 103 | export const TextString = styled.div` 104 | text-align: center; 105 | display: block; 106 | margin-bottom: ${props => props.theme.sizes.gutter}px; 107 | 108 | ${props => props.top && css` 109 | margin-bottom: calc(${props => props.theme.sizes.gutter}px / 4); 110 | `} 111 | 112 | > * { 113 | white-space: nowrap; 114 | } 115 | `; 116 | 117 | 118 | // Forms 119 | //////////////////////////////////////////////////////////// 120 | 121 | export const Input = styled.input` 122 | padding: 0 calc(${props => props.theme.sizes.gutter}px / 2); 123 | height: ${props => props.theme.sizes.input_height}px; 124 | background: ${props => props.theme.colors.color_white}; 125 | border: 1px solid ${props => props.theme.colors.color_border}; 126 | border-radius: ${props => props.theme.border_radius.small}; 127 | font-family: ${props => props.theme.typography.fontfamily_sans}; 128 | font-size: ${props => props.theme.typography.fontsize_normal}px; 129 | line-height: ${props => props.theme.typography.lineheight_normal}px; 130 | font-weight: ${props => props.theme.typography.fontweight_sans_regular}; 131 | letter-spacing: ${props => props.theme.typography.letterspacing_normal}; 132 | `; 133 | 134 | interface ButtonProps { 135 | readonly brand? : string; 136 | }; 137 | 138 | export const Button = styled.button` 139 | border: none; 140 | cursor: pointer; 141 | text-align: center; 142 | text-transform: uppercase; 143 | height: ${props => props.theme.sizes.input_height}px; 144 | color: ${props => props.theme.colors.color_white}; 145 | background: ${props => props.theme.colors.color_primary}; 146 | font-family: ${props => props.theme.typography.fontfamily_sans}; 147 | font-size: ${props => props.theme.typography.fontsize_normal}px; 148 | line-height: ${props => props.theme.typography.lineheight_normal}px; 149 | font-weight: ${props => props.theme.typography.fontweight_sans_bold}; 150 | letter-spacing: ${props => props.theme.typography.letterspacing_normal}; 151 | border-radius: ${props => props.theme.border_radius.large}; 152 | margin-bottom: calc(${props => props.theme.sizes.gutter}px / 2); 153 | 154 | ${props => props.brand === "facebook" && css` 155 | background: ${props => props.theme.colors.color_fb}; 156 | `} 157 | 158 | ${props => props.brand === "vkontakte" && css` 159 | background: ${props => props.theme.colors.color_vk}; 160 | `} 161 | `; 162 | 163 | export const FormGroup = styled.div` 164 | display: flex; 165 | position: relative; 166 | padding-bottom: calc(${props => props.theme.sizes.gutter}px * 1.5); 167 | `; 168 | 169 | export const ButtonWrapper = styled.div` 170 | display: flex; 171 | flex-direction: column; 172 | 173 | ${FormGroup} ${Button} { 174 | width: 100%; 175 | } 176 | `; 177 | 178 | interface FormMessageProps { 179 | readonly state? : string; 180 | }; 181 | 182 | export const FormMessage = styled.div` 183 | display: block; 184 | width: 100%; 185 | position: absolute; 186 | left: 0; 187 | right: 0; 188 | text-align: center; 189 | color: ${props => props.theme.colors.color_text}; 190 | top: calc(${props => props.theme.sizes.input_height}px * 1.1); 191 | 192 | ${TextSmall} { 193 | display: inline-block; 194 | line-height: calc(${props => props.theme.sizes.gutter}px / 1.5); 195 | } 196 | 197 | ${props => props.state === "success" && css` 198 | color: ${props => props.theme.colors.color_success}; 199 | `} 200 | 201 | ${props => props.state === "error" && css` 202 | color: ${props => props.theme.colors.color_error}; 203 | `} 204 | `; 205 | 206 | interface FormProps { 207 | readonly bottom? : boolean; 208 | }; 209 | 210 | export const Form = styled.form` 211 | display: flex; 212 | flex-direction: column; 213 | text-align: center; 214 | padding: ${props => props.theme.sizes.gutter}px; 215 | background: ${props => props.theme.colors.color_card}; 216 | border: 1px solid ${props => props.theme.colors.color_border}; 217 | border-radius: ${props => props.theme.border_radius.large}; 218 | 219 | ${Input}, 220 | ${Button} { 221 | display: block; 222 | flex-grow: 1; 223 | } 224 | 225 | ${props => props.bottom && css` 226 | padding-bottom: 0; 227 | `} 228 | `; 229 | 230 | 231 | // Containers 232 | //////////////////////////////////////////////////////////// 233 | 234 | export interface PageProps { 235 | readonly empty? : boolean; 236 | readonly footer? : boolean; 237 | }; 238 | 239 | export const Page = styled.div` 240 | height: calc(100% - 85px); 241 | background: ${props => props.theme.colors.color_background}; 242 | display: flex; 243 | flex-direction: column; 244 | justify-content: center; 245 | align-items: center; 246 | 247 | ${props => props.footer && css` 248 | padding-bottom: calc(${props => props.theme.sizes.gutter}px + 65px); 249 | 250 | @media screen and (max-width: ${props => props.theme.breackpoints.xs_max}) { 251 | padding-bottom: calc((${props => props.theme.sizes.gutter}px / 2 ) + 65px); 252 | } 253 | `} 254 | 255 | ${props => props.empty && css` 256 | height: 100%; 257 | height: 100vh; 258 | `}; 259 | 260 | @media screen and (max-width: ${props => props.theme.breackpoints.xs_middle}) { 261 | height: auto; 262 | } 263 | `; 264 | 265 | export const CenterWrapper = styled.div` 266 | padding-bottom: 13vh; 267 | width: 300px; 268 | 269 | @media screen and (max-width: ${props => props.theme.breackpoints.xs_max}) { 270 | padding-top: 20px; 271 | padding-bottom: 0; 272 | } 273 | 274 | @media screen and (max-width: ${props => props.theme.breackpoints.xs_middle}) { 275 | padding-top: 10px; 276 | padding-bottom: 0; 277 | } 278 | `; 279 | 280 | export const EntryHeaderWpapper = styled.div` 281 | padding-bottom: 20px; 282 | display: flex; 283 | flex-direction: column; 284 | align-items: center; 285 | text-align: center; 286 | 287 | ${TextLarge} { 288 | margin-top: 0; 289 | } 290 | `; 291 | 292 | export const Footer = styled.footer` 293 | text-align: center; 294 | position: fixed; 295 | left: 0; 296 | right: 0; 297 | bottom: 0; 298 | padding-top: calc(${props => props.theme.sizes.gutter}px / 2); 299 | padding-bottom: calc(${props => props.theme.sizes.gutter}px / 2); 300 | background: ${props => props.theme.colors.color_background}; 301 | border-top: 1px solid ${props => props.theme.colors.color_border}; 302 | `; 303 | 304 | 305 | export interface CardProps { 306 | readonly switch? : boolean; 307 | readonly after? : boolean; 308 | }; 309 | 310 | export const Card = styled.div` 311 | display: inline-block; 312 | color: ${props => props.theme.colors.color_text}; 313 | padding: calc(${props => props.theme.sizes.gutter}px / 2); 314 | background: ${props => props.theme.colors.color_card}; 315 | border: 1px solid ${props => props.theme.colors.color_border}; 316 | border-radius: ${props => props.theme.border_radius.large}; 317 | 318 | ${props => props.switch && css` 319 | white-space: nowrap; 320 | `} 321 | 322 | ${props => props.after && css` 323 | margin-left: ${props => props.theme.sizes.gutter}px; 324 | `} 325 | `; 326 | 327 | // Animation and elements 328 | //////////////////////////////////////////////////////////// 329 | 330 | export const rotate = keyframes` 331 | from { 332 | transform: rotate(0deg); 333 | } 334 | to { 335 | transform: rotate(360deg); 336 | } 337 | `; 338 | 339 | export const Logo = styled.img` 340 | height: 60px; 341 | pointer-events: none; 342 | display: inline-block; 343 | animation: ${rotate} infinite calc(${props => props.theme.effects.transition_duration} * 20) ${props => props.theme.effects.transition_timingfunction}; 344 | `; 345 | 346 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Cookies from "js-cookie"; 3 | 4 | import { 5 | COOKIES, 6 | API_URL, 7 | LOCALSTORAGE, 8 | AUTO_AUTH, 9 | AUTO_LANG, 10 | } from '../store/constants'; 11 | 12 | const Api = axios.create({ 13 | baseURL: API_URL, 14 | responseType: 'json', 15 | withCredentials: true, 16 | }); 17 | 18 | // Auto auth 19 | if (AUTO_AUTH) { 20 | // eslint-disable-next-line dot-notation 21 | Api.defaults.headers.common['Authorization'] = `Token ${AUTO_AUTH}`; 22 | } 23 | 24 | // Auto language 25 | Cookies.set(COOKIES.LANG.name, AUTO_LANG, { expires: COOKIES.LANG.expires }); 26 | 27 | export const setAuth = (token: string) : void => { 28 | Cookies.set(COOKIES.TOKEN.name, token, { expires: COOKIES.TOKEN.expires }); 29 | Api.defaults.headers.common['Authorization'] = `Token ${token}`; 30 | }; 31 | 32 | export const deleteAuth = () : void => { 33 | localStorage.removeItem(LOCALSTORAGE.PROFILE); 34 | Cookies.remove(COOKIES.TOKEN.name); 35 | delete Api.defaults.headers.common['Authorization']; 36 | }; 37 | 38 | export const rememberLanguage = (language: string) : void => { 39 | Cookies.set(COOKIES.LANG.name, language, { expires: COOKIES.LANG.expires }); 40 | }; 41 | 42 | export const rememberTheme = (theme: string) : void => { 43 | Cookies.set(COOKIES.THEME.name, theme, { expires: COOKIES.THEME.expires }); 44 | }; 45 | 46 | export const POST_AUTH_PATH = '/api/user/login'; 47 | export const GET_AUTH_FACEBOOK_PATH = '/api/user/facebook'; 48 | export const GET_AUTH_VKONTAKTE_PATH = '/api/user/vkontakte'; 49 | export const GET_USER_PATH = '/api/user/profile'; 50 | export const POST_VERIFY = '/api/user/verify'; 51 | export const POST_REMIND_PASSWORD_PATH = '/api/user/remind'; 52 | export const POST_NEW_PASSWORD_PATH = '/api/user/password'; 53 | export const POST_VERIFY_EMAIL_PATH = '/api/user/send-verify-email'; 54 | 55 | export default Api; 56 | 57 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import Backend from 'i18next-xhr-backend'; 3 | import LanguageDetector from 'i18next-browser-languagedetector'; 4 | import { initReactI18next } from 'react-i18next'; 5 | 6 | import { 7 | LANGUAGES, 8 | AUTO_LANG, 9 | } from '../store/constants'; 10 | 11 | i18n 12 | // load translation using xhr -> see /public/locales 13 | // learn more: https://github.com/i18next/i18next-xhr-backend 14 | .use(Backend) 15 | // detect user language 16 | // learn more: https://github.com/i18next/i18next-browser-languageDetector 17 | .use(LanguageDetector) 18 | // pass the i18n instance to the react-i18next components. 19 | // Alternative use the I18nextProvider: https://react.i18next.com/components/i18nextprov 20 | .use(initReactI18next) 21 | // init i18next 22 | // for all options read: https://www.i18next.com/overview/configuration-options 23 | .init({ 24 | fallbackLng: LANGUAGES[0].name, 25 | lng: AUTO_LANG, 26 | // backend: { 27 | // loadPath: `${process.env.PUBLIC_URL}/locales/{{lng}}/translation.json` 28 | // }, 29 | 30 | debug: false, 31 | 32 | interpolation: { 33 | escapeValue: false, // not needed for react as it escapes by default 34 | }, 35 | 36 | // special options for react-i18next 37 | // learn more: https://react.i18next.com/components/i18next-instance 38 | react: { 39 | wait: true, 40 | useSuspense: false, 41 | }, 42 | }); 43 | 44 | export default i18n; 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "strictNullChecks": false, 10 | "noImplicitAny": false, 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------