├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------