├── README.md
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── styles
│ ├── components
│ │ ├── _links.scss
│ │ ├── _nothing.scss
│ │ ├── _notes.scss
│ │ ├── _auth.scss
│ │ ├── _journal.scss
│ │ └── _buttons.scss
│ ├── base
│ │ ├── _settings.scss
│ │ └── _base.scss
│ └── styles.scss
├── index.js
├── JournalApp.js
├── actions
│ ├── ui.js
│ ├── auth.js
│ └── notes.js
├── components
│ ├── journal
│ │ ├── NothingSelected.js
│ │ ├── JournalEntries.js
│ │ ├── JournalScreen.js
│ │ ├── Sidebar.js
│ │ └── JournalEntry.js
│ ├── notes
│ │ ├── NotesAppBar.js
│ │ └── NoteScreen.js
│ └── auth
│ │ ├── LoginScreen.js
│ │ └── RegisterScreen.js
├── helpers
│ ├── loadNotes.js
│ └── fileUpload.js
├── setupTests.js
├── hooks
│ └── useForm.js
├── reducers
│ ├── authReducer.js
│ ├── uiReducer.js
│ └── notesReducer.js
├── types
│ └── types.js
├── store
│ └── store.js
├── routers
│ ├── PublicRoute.js
│ ├── PrivateRoute.js
│ ├── AuthRouter.js
│ └── AppRouter.js
├── tests
│ ├── components
│ │ ├── journal
│ │ │ ├── __snapshots__
│ │ │ │ ├── Sidebar.test.js.snap
│ │ │ │ └── JournalEntry.test.js.snap
│ │ │ ├── JournalEntry.test.js
│ │ │ └── Sidebar.test.js
│ │ ├── notes
│ │ │ ├── __snapshots__
│ │ │ │ └── NoteScreen.test.js.snap
│ │ │ └── NoteScreen.test.js
│ │ └── auth
│ │ │ ├── __snapshots__
│ │ │ ├── RegisterScreen.test.js.snap
│ │ │ └── LoginScreen.test.js.snap
│ │ │ ├── LoginScreen.test.js
│ │ │ └── RegisterScreen.test.js
│ ├── types
│ │ └── types.test.js
│ ├── actions
│ │ ├── ui.test.js
│ │ ├── auth.test.js
│ │ └── notes.test.js
│ ├── helpers
│ │ └── fileUpload.test.js
│ ├── reducers
│ │ └── authReducer.test.js
│ └── routers
│ │ └── AppRouter.test.js
└── firebase
│ └── firebase-config.js
├── .env.development
├── .env.test
├── .gitignore
└── package.json
/README.md:
--------------------------------------------------------------------------------
1 | # JournalApp
2 |
3 | Una aplicación para llevar mi diario hecha con React y Redux.
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Klerith/react-redux-journal-app/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Klerith/react-redux-journal-app/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Klerith/react-redux-journal-app/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/styles/components/_links.scss:
--------------------------------------------------------------------------------
1 |
2 | .link {
3 | color: $dark-grey;
4 | text-decoration: none;
5 |
6 | &:hover {
7 | text-decoration: underline;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/styles/base/_settings.scss:
--------------------------------------------------------------------------------
1 |
2 | // Colors
3 | $primary: #5C62C5;
4 | $dark-grey: #363636;
5 | $light-grey: #d8d8d8;
6 |
7 | $white: #fff;
8 | $google-blue: #4285f4;
9 | $button-active-blue: #1669f2;
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { JournalApp } from './JournalApp';
5 | import './styles/styles.scss'
6 |
7 |
8 |
9 | ReactDOM.render(
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 |
--------------------------------------------------------------------------------
/src/styles/styles.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | @import './base/settings';
4 | @import './base/base';
5 |
6 | // Components
7 | @import './components/auth';
8 | @import './components/buttons';
9 | @import './components/links';
10 | @import './components/journal';
11 | @import './components/nothing';
12 | @import './components/notes';
--------------------------------------------------------------------------------
/src/styles/components/_nothing.scss:
--------------------------------------------------------------------------------
1 |
2 | .nothing__main-content {
3 | align-items: center;
4 | background-color: $primary;
5 | color: white;
6 | display: flex;
7 | flex-direction: column;
8 | justify-content: center;
9 | height: 100vh;
10 |
11 | p {
12 | text-align: center;
13 | }
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/src/JournalApp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 |
4 | import { store } from './store/store';
5 | import { AppRouter } from './routers/AppRouter';
6 |
7 |
8 |
9 | export const JournalApp = () => {
10 | return (
11 |
12 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | REACT_APP_APIKEY=AIzaSyDFXW2W2SNV0lmj8HJogeu267hdD2HR5WE
2 | REACT_APP_AUTHDOMAIN=react-app-cursos.firebaseapp.com
3 | REACT_APP_DATABASEURL=https://react-app-cursos.firebaseio.com
4 | REACT_APP_PROJECTID=react-app-cursos
5 | REACT_APP_STORAGEBUCKET=react-app-cursos.appspot.com
6 | REACT_APP_MESSAGINGSENDERID=959679322615
7 | REACT_APP_APPID=1:959679322615:web:89d8e604eee1b5fd68f6f2
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | REACT_APP_APIKEY=AIzaSyD5-4gUUrMLCzTWDEJ3QpkmfIboN5PDCq4
2 | REACT_APP_AUTHDOMAIN=push-one-signal-17ede.firebaseapp.com
3 | REACT_APP_DATABASEURL=https://push-one-signal-17ede.firebaseio.com
4 | REACT_APP_PROJECTID=push-one-signal-17ede
5 | REACT_APP_STORAGEBUCKET=push-one-signal-17ede.appspot.com
6 | REACT_APP_MESSAGINGSENDERID=803724161810
7 | REACT_APP_APPID=1:803724161810:web:02f32ebc98a71e376339cb
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/actions/ui.js:
--------------------------------------------------------------------------------
1 | import { types } from '../types/types';
2 |
3 | export const setError = ( err ) => ({
4 | type: types.uiSetError,
5 | payload: err
6 | });
7 |
8 | export const removeError = () => ({
9 | type: types.uiRemoveError
10 | });
11 |
12 | export const startLoading = () => ({
13 | type: types.uiStartLoading
14 | })
15 | export const finishLoading = () => ({
16 | type: types.uiFinishLoading
17 | })
18 |
19 |
--------------------------------------------------------------------------------
/src/components/journal/NothingSelected.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const NothingSelected = () => {
4 | return (
5 |
6 |
7 | Select something
8 |
9 | pr create an entry!
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/helpers/loadNotes.js:
--------------------------------------------------------------------------------
1 | import { db } from '../firebase/firebase-config';
2 |
3 |
4 |
5 | export const loadNotes = async ( uid ) => {
6 |
7 | const notesSnap = await db.collection(`${ uid }/journal/notes`).get();
8 | const notes = [];
9 |
10 | notesSnap.forEach( snapHijo => {
11 | notes.push({
12 | id: snapHijo.id,
13 | ...snapHijo.data()
14 | })
15 | });
16 |
17 | return notes;
18 | }
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/styles/base/_base.scss:
--------------------------------------------------------------------------------
1 | * {
2 | font-family: Helvetica, Arial, sans-serif;
3 | margin: 0px;
4 | }
5 |
6 | html, body {
7 | height: 100vh;
8 | width: 100vw;
9 | }
10 |
11 | main {
12 | width: 100%;
13 | }
14 |
15 | .mt-1 {
16 | margin-top: 5px;
17 | }
18 |
19 | .mt-5 {
20 | margin-top: 20px;
21 | }
22 |
23 | .mb-1 {
24 | margin-bottom: 5px;
25 | }
26 |
27 | .mb-5 {
28 | margin-bottom: 20px;
29 | }
30 |
31 | .pointer {
32 | cursor: pointer;
33 | }
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 | import {createSerializer} from 'enzyme-to-json';
4 | import Swal from 'sweetalert2';
5 |
6 | Enzyme.configure({ adapter: new Adapter() });
7 | expect.addSnapshotSerializer(createSerializer({mode: 'deep'}));
8 |
9 | const noScroll = () => {};
10 | Object.defineProperty( window, 'scrollTo', { value: noScroll, writable: true } );
11 |
12 |
13 |
14 |
15 | jest.mock('sweetalert2', () => ({
16 | fire: jest.fn(),
17 | close: jest.fn(),
18 | }));
--------------------------------------------------------------------------------
/src/hooks/useForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 |
4 | export const useForm = ( initialState = {} ) => {
5 |
6 | const [values, setValues] = useState(initialState);
7 |
8 | const reset = ( newFormState = initialState ) => {
9 | setValues( newFormState );
10 | }
11 |
12 |
13 | const handleInputChange = ({ target }) => {
14 |
15 | setValues({
16 | ...values,
17 | [ target.name ]: target.value
18 | });
19 |
20 | }
21 |
22 | return [ values, handleInputChange, reset ];
23 |
24 | }
--------------------------------------------------------------------------------
/src/reducers/authReducer.js:
--------------------------------------------------------------------------------
1 | import { types } from '../types/types';
2 | /*
3 | {
4 | uid: 'jagdfjahdsf127362718',
5 | name: 'Fernando'
6 | }
7 |
8 | */
9 | export const authReducer = ( state = {}, action ) => {
10 |
11 | switch ( action.type ) {
12 | case types.login:
13 | return {
14 | uid: action.payload.uid,
15 | name: action.payload.displayName
16 | }
17 |
18 | case types.logout:
19 | return { }
20 |
21 | default:
22 | return state;
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/types.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export const types = {
4 |
5 | login: '[Auth] Login',
6 | logout: '[Auth] Logout',
7 |
8 | uiSetError: '[UI] Set Error',
9 | uiRemoveError: '[UI] Remove Error',
10 |
11 | uiStartLoading: '[UI] Start loading',
12 | uiFinishLoading: '[UI] Finish loading',
13 |
14 | notesAddNew: '[Notes] New note',
15 | notesActive: '[Notes] Set active note',
16 | notesLoad: '[Notes] Load notes',
17 | notesUpdated: '[Notes] Updated note',
18 | notesFileUrl: '[Notes] Updated image url',
19 | notesDelete: '[Notes] Delete note',
20 | notesLogoutCleaning: '[Notes] Logout Cleaning',
21 |
22 | }
--------------------------------------------------------------------------------
/src/components/journal/JournalEntries.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSelector } from 'react-redux';
3 | import { JournalEntry } from './JournalEntry';
4 |
5 | export const JournalEntries = () => {
6 |
7 | const { notes } = useSelector( state => state.notes );
8 |
9 | return (
10 |
11 |
12 | {
13 | notes.map( note => (
14 |
18 | ))
19 | }
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 |
4 | import { authReducer } from '../reducers/authReducer';
5 | import { uiReducer } from '../reducers/uiReducer';
6 | import { notesReducer } from '../reducers/notesReducer';
7 |
8 | const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
9 |
10 | const reducers = combineReducers({
11 | auth: authReducer,
12 | ui: uiReducer,
13 | notes: notesReducer
14 | });
15 |
16 |
17 | export const store = createStore(
18 | reducers,
19 | composeEnhancers(
20 | applyMiddleware( thunk )
21 | )
22 | );
--------------------------------------------------------------------------------
/src/routers/PublicRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Route, Redirect } from 'react-router-dom';
5 |
6 |
7 | export const PublicRoute = ({
8 | isAuthenticated,
9 | component: Component,
10 | ...rest
11 | }) => {
12 |
13 | return (
14 | (
16 | ( isAuthenticated )
17 | ? ( )
18 | : ( )
19 | )}
20 |
21 | />
22 | )
23 | }
24 |
25 | PublicRoute.propTypes = {
26 | isAuthenticated: PropTypes.bool.isRequired,
27 | component: PropTypes.func.isRequired
28 | }
29 |
--------------------------------------------------------------------------------
/src/routers/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Route, Redirect } from 'react-router-dom';
5 |
6 |
7 | export const PrivateRoute = ({
8 | isAuthenticated,
9 | component: Component,
10 | ...rest
11 | }) => {
12 |
13 | return (
14 | (
16 | ( isAuthenticated )
17 | ? ( )
18 | : ( )
19 | )}
20 |
21 | />
22 | )
23 | }
24 |
25 | PrivateRoute.propTypes = {
26 | isAuthenticated: PropTypes.bool.isRequired,
27 | component: PropTypes.func.isRequired
28 | }
29 |
--------------------------------------------------------------------------------
/src/helpers/fileUpload.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export const fileUpload = async ( file ) => {
4 |
5 | const cloudUrl = 'https://api.cloudinary.com/v1_1/dx0pryfzn/upload';
6 |
7 | const formData = new FormData();
8 | formData.append('upload_preset','react-journal');
9 | formData.append('file', file );
10 |
11 | try {
12 |
13 | const resp = await fetch( cloudUrl, {
14 | method: 'POST',
15 | body: formData
16 | });
17 |
18 | if ( resp.ok ) {
19 | const cloudResp = await resp.json();
20 | return cloudResp.secure_url;
21 | } else {
22 | return null;
23 | }
24 |
25 | } catch (err) {
26 | throw err;
27 | }
28 |
29 |
30 | // return url de la imagen
31 | }
--------------------------------------------------------------------------------
/src/components/journal/JournalScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Sidebar } from './Sidebar';
3 | import { NoteScreen } from '../notes/NoteScreen';
4 | import { NothingSelected } from './NothingSelected';
5 | import { useSelector } from 'react-redux';
6 |
7 |
8 | export const JournalScreen = () => {
9 |
10 | const { active } = useSelector( state => state.notes );
11 |
12 |
13 | return (
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {
24 | ( active )
25 | ? ( )
26 | : ( )
27 | }
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/tests/components/journal/__snapshots__/Sidebar.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Pruebas en debe de mostrarse correctamente 1`] = `
4 |
45 | `;
46 |
--------------------------------------------------------------------------------
/src/reducers/uiReducer.js:
--------------------------------------------------------------------------------
1 | import { types } from '../types/types';
2 |
3 | const initialState = {
4 | loading: false,
5 | msgError: null
6 | }
7 |
8 |
9 | export const uiReducer = ( state = initialState, action ) => {
10 |
11 | switch ( action.type ) {
12 | case types.uiSetError:
13 | return {
14 | ...state,
15 | msgError: action.payload
16 | }
17 |
18 | case types.uiRemoveError:
19 | return {
20 | ...state,
21 | msgError: null
22 | }
23 |
24 | case types.uiStartLoading:
25 | return {
26 | ...state,
27 | loading: true
28 | }
29 |
30 | case types.uiFinishLoading:
31 | return {
32 | ...state,
33 | loading: false
34 | }
35 |
36 | default:
37 | return state;
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/tests/types/types.test.js:
--------------------------------------------------------------------------------
1 | import { types } from '../../types/types';
2 |
3 |
4 | describe('Pruebas con nuestros tipos', () => {
5 |
6 | test('debe de tener estos tipos', () => {
7 |
8 | expect( types ).toEqual({
9 | login: '[Auth] Login',
10 | logout: '[Auth] Logout',
11 |
12 | uiSetError: '[UI] Set Error',
13 | uiRemoveError: '[UI] Remove Error',
14 |
15 | uiStartLoading: '[UI] Start loading',
16 | uiFinishLoading: '[UI] Finish loading',
17 |
18 | notesAddNew: '[Notes] New note',
19 | notesActive: '[Notes] Set active note',
20 | notesLoad: '[Notes] Load notes',
21 | notesUpdated: '[Notes] Updated note',
22 | notesFileUrl: '[Notes] Updated image url',
23 | notesDelete: '[Notes] Delete note',
24 | notesLogoutCleaning: '[Notes] Logout Cleaning',
25 | })
26 |
27 | })
28 |
29 |
30 |
31 | })
32 |
--------------------------------------------------------------------------------
/src/routers/AuthRouter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route, Redirect } from 'react-router-dom';
3 |
4 | import { LoginScreen } from '../components/auth/LoginScreen';
5 | import { RegisterScreen } from '../components/auth/RegisterScreen';
6 |
7 | export const AuthRouter = () => {
8 | return (
9 |
10 |
11 |
12 |
17 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/components/_notes.scss:
--------------------------------------------------------------------------------
1 | .notes__main-content {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100%;
5 | }
6 |
7 |
8 | .notes__appbar {
9 | align-items: center;
10 | background-color: $primary;
11 | color: white;
12 | display: flex;
13 | justify-content: space-between;
14 | padding: 10px 20px 10px 20px;
15 | }
16 |
17 |
18 | .notes__content {
19 | display: flex;
20 | flex-direction: column;
21 | height: 100%;
22 | padding: 20px;
23 | }
24 |
25 |
26 | .notes__title-input, .notes__textarea {
27 | border: none;
28 |
29 | &:focus{
30 | outline: none;
31 | }
32 | }
33 |
34 | .notes__title-input {
35 | color: $dark-grey;
36 | font-size: 25px;
37 | font-weight: 700;
38 | margin-bottom: 10px;
39 | }
40 |
41 | .notes__textarea {
42 | border: none;
43 | color: $dark-grey;
44 | font-size: 20px;
45 | flex: 1 1 auto;
46 | resize: none;
47 | }
48 |
49 | .notes__image img {
50 | box-shadow: 5px 5px $dark-grey;
51 | height: 150px;
52 | }
--------------------------------------------------------------------------------
/src/tests/actions/ui.test.js:
--------------------------------------------------------------------------------
1 | import { setError, removeError, startLoading, finishLoading } from '../../actions/ui';
2 | import { types } from '../../types/types';
3 |
4 |
5 |
6 | describe('Pruebas en ui-actions', () => {
7 |
8 | test('todas las acciones deben de funcionar', () => {
9 |
10 | const action = setError('HELP!!!!');
11 |
12 | expect( action ).toEqual({
13 | type: types.uiSetError,
14 | payload: 'HELP!!!!'
15 | });
16 |
17 | const removeErrorAction = removeError();
18 | const startLoadingAction = startLoading();
19 | const finishLoadingAction = finishLoading();
20 |
21 |
22 | expect(removeErrorAction).toEqual({
23 | type: types.uiRemoveError
24 | });
25 |
26 | expect(startLoadingAction).toEqual({
27 | type: types.uiStartLoading
28 | });
29 |
30 | expect(finishLoadingAction).toEqual({
31 | type: types.uiFinishLoading
32 | });
33 |
34 |
35 | })
36 |
37 |
38 | })
39 |
--------------------------------------------------------------------------------
/src/tests/components/journal/__snapshots__/JournalEntry.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Pruebas en debe de mostrarse correctamente 1`] = `
4 |
8 |
17 |
20 |
23 | Hola
24 |
25 |
28 | Mundo
29 |
30 |
31 |
34 |
35 |
36 | Wednesday
37 |
38 |
39 |
40 |
41 | 31st
42 |
43 |
44 |
45 |
46 | `;
47 |
--------------------------------------------------------------------------------
/src/tests/helpers/fileUpload.test.js:
--------------------------------------------------------------------------------
1 | import cloudinary from 'cloudinary';
2 |
3 | import { fileUpload } from '../../helpers/fileUpload';
4 |
5 |
6 |
7 | cloudinary.config({
8 | cloud_name: 'dx0pryfzn',
9 | api_key: '422916932349318',
10 | api_secret: 'gM_vs-URpSAyA3xV-PsoTg8xF3M'
11 | });
12 |
13 | describe('Pruebas en fileUpload', () => {
14 |
15 |
16 |
17 | test('debe de cargar un archivo y retornar el URL', async(done) => {
18 |
19 | const resp = await fetch('https://media.sproutsocial.com/uploads/2017/02/10x-featured-social-media-image-size.png');
20 | const blob = await resp.blob();
21 |
22 | const file = new File([blob], 'foto.png');
23 | const url = await fileUpload( file );
24 |
25 | expect( typeof url ).toBe('string');
26 |
27 | // Borrar imagen por ID
28 | const segments = url.split('/');
29 | const imageId = segments[ segments.length - 1 ].replace('.png','');
30 |
31 | cloudinary.v2.api.delete_resources( imageId, {}, ()=> {
32 | done();
33 | });
34 |
35 | })
36 |
37 |
38 | test('debe de retornar un error', async() => {
39 |
40 | const file = new File([], 'foto.png');
41 | const url = await fileUpload( file );
42 |
43 | expect( url ).toBe( null );
44 |
45 |
46 | })
47 |
48 |
49 |
50 |
51 | })
52 |
53 |
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "journal-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "firebase": "^7.14.5",
10 | "moment": "^2.26.0",
11 | "node-sass": "^4.14.1",
12 | "react": "^16.13.1",
13 | "react-dom": "^16.13.1",
14 | "react-redux": "^7.2.0",
15 | "react-router-dom": "^5.2.0",
16 | "react-scripts": "3.4.1",
17 | "redux": "^4.0.5",
18 | "redux-thunk": "^2.3.0",
19 | "sweetalert2": "^9.13.2",
20 | "validator": "^13.0.0"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "cloudinary": "^1.21.0",
45 | "enzyme": "^3.11.0",
46 | "enzyme-adapter-react-16": "^1.15.2",
47 | "enzyme-to-json": "^3.5.0",
48 | "redux-mock-store": "^1.5.4"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/styles/components/_auth.scss:
--------------------------------------------------------------------------------
1 |
2 | .auth__main {
3 | align-items: center;
4 | background-color: $primary;
5 | display: flex;
6 | justify-content: center;
7 | margin: 0px;
8 | height: 100vh;
9 | width: 100vw;
10 | }
11 |
12 | .auth__box-container {
13 | background-color: white;
14 | box-shadow: 0px 3px $dark-grey;
15 | border-radius: 2px;
16 | padding: 20px;
17 | width: 250px;
18 | }
19 |
20 |
21 | .auth__title {
22 | color: darken($color: $primary, $amount: 20);
23 | margin-bottom: 20px;
24 | }
25 |
26 | .auth__input {
27 | color: $dark-grey;
28 | border: 0px;
29 | border-bottom: 1px solid $light-grey;
30 | font-size: 16px;
31 | margin-bottom: 10px;
32 | height: 20px;
33 | width: 100%;
34 |
35 | transition: border-bottom .3s ease;
36 |
37 | &:focus {
38 | border-bottom: 1px solid $primary;
39 | outline: none;
40 | }
41 | }
42 |
43 | .auth__social-networks {
44 | align-items: center;
45 | display: flex;
46 | justify-content: center;
47 | flex-direction: column;
48 | padding-top: 20px;
49 | padding-bottom: 20px;
50 | width: 100%;
51 | }
52 |
53 |
54 | .auth__alert-error {
55 | background-color: red;
56 | border-radius: 5px;
57 | color: white;
58 | display: flex;
59 | justify-content: center;
60 | margin-bottom: 10px;
61 | padding: 5px
62 | }
--------------------------------------------------------------------------------
/src/tests/components/journal/JournalEntry.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Provider } from 'react-redux'
4 |
5 | import configureStore from 'redux-mock-store';
6 | import thunk from 'redux-thunk';
7 |
8 | import '@testing-library/jest-dom';
9 | import { JournalEntry } from '../../../components/journal/JournalEntry';
10 | import { activeNote } from '../../../actions/notes';
11 |
12 | const middlewares = [thunk];
13 | const mockStore = configureStore(middlewares);
14 |
15 | const initState = {};
16 |
17 | let store = mockStore(initState);
18 | store.dispatch = jest.fn();
19 |
20 | const nota = {
21 | id: 10,
22 | date: 0,
23 | title: 'Hola',
24 | body: 'Mundo',
25 | url: 'https://algunlugar.com/foto.jpg'
26 | };
27 |
28 | const wrapper = mount(
29 |
30 |
31 |
32 |
33 | )
34 |
35 |
36 |
37 |
38 |
39 | describe('Pruebas en ', () => {
40 |
41 |
42 | test('debe de mostrarse correctamente', () => {
43 |
44 | expect( wrapper ).toMatchSnapshot();
45 |
46 | });
47 |
48 |
49 |
50 | test('debe de activar la nota', () => {
51 |
52 | wrapper.find('.journal__entry').prop('onClick')();
53 |
54 | expect( store.dispatch ).toHaveBeenCalledWith(
55 | activeNote( nota.id, { ...nota } )
56 | );
57 |
58 |
59 | })
60 |
61 |
62 |
63 |
64 | })
65 |
--------------------------------------------------------------------------------
/src/firebase/firebase-config.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 | import 'firebase/firestore';
3 | import 'firebase/auth';
4 |
5 | const firebaseConfig = {
6 | apiKey: process.env.REACT_APP_APIKEY,
7 | authDomain: process.env.REACT_APP_AUTHDOMAIN,
8 | databaseURL: process.env.REACT_APP_DATABASEURL,
9 | projectId: process.env.REACT_APP_PROJECTID,
10 | storageBucket: process.env.REACT_APP_STORAGEBUCKET,
11 | messagingSenderId: process.env.REACT_APP_MESSAGINGSENDERID,
12 | appId: process.env.REACT_APP_APPID,
13 | };
14 |
15 | // const firebaseConfigTesting = {
16 | // apiKey: "AIzaSyD5-4gUUrMLCzTWDEJ3QpkmfIboN5PDCq4",
17 | // authDomain: "push-one-signal-17ede.firebaseapp.com",
18 | // databaseURL: "https://push-one-signal-17ede.firebaseio.com",
19 | // projectId: "push-one-signal-17ede",
20 | // storageBucket: "push-one-signal-17ede.appspot.com",
21 | // messagingSenderId: "803724161810",
22 | // appId: "1:803724161810:web:02f32ebc98a71e376339cb"
23 | // };
24 |
25 |
26 | // if( process.env.NODE_ENV === 'test' ) {
27 | // // testing
28 | // firebase.initializeApp(firebaseConfigTesting);
29 | // } else {
30 | // dev/prod
31 | // firebase.initializeApp(firebaseConfig);
32 | // }
33 |
34 | firebase.initializeApp(firebaseConfig);
35 |
36 |
37 |
38 | const db = firebase.firestore();
39 | const googleAuthProvider = new firebase.auth.GoogleAuthProvider();
40 |
41 |
42 | export {
43 | db,
44 | googleAuthProvider,
45 | firebase
46 | }
--------------------------------------------------------------------------------
/src/tests/components/notes/__snapshots__/NoteScreen.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Pruebas en debe de mostrarse correctamente 1`] = `
4 |
7 |
10 |
11 | 28 de agosto 2020
12 |
13 |
24 |
25 |
31 |
37 |
38 |
39 |
42 |
51 |
58 |
59 |
65 |
66 | `;
67 |
--------------------------------------------------------------------------------
/src/tests/reducers/authReducer.test.js:
--------------------------------------------------------------------------------
1 | import { authReducer } from '../../reducers/authReducer';
2 | import { types } from '../../types/types';
3 |
4 |
5 | describe('Pruebas en authReducer', () => {
6 |
7 | test('debe de realizar el login', () => {
8 |
9 | const initState = {};
10 |
11 | const action = {
12 | type: types.login,
13 | payload: {
14 | uid: 'abc',
15 | displayName: 'Fernando'
16 | }
17 | };
18 |
19 | const state = authReducer( initState, action );
20 |
21 | expect( state ).toEqual({
22 | uid: 'abc',
23 | name: 'Fernando'
24 | })
25 |
26 |
27 | })
28 |
29 | test('debe de realizar el logout', () => {
30 |
31 | const initState = {
32 | uid: 'jagdfjahdsf127362718',
33 | name: 'Fernando'
34 | };
35 |
36 | const action = {
37 | type: types.logout,
38 | };
39 |
40 | const state = authReducer( initState, action );
41 |
42 | expect( state ).toEqual({});
43 |
44 | })
45 |
46 | test('no debe de hacer cambios en el state', () => {
47 |
48 | const initState = {
49 | uid: 'jagdfjahdsf127362718',
50 | name: 'Fernando'
51 | };
52 |
53 | const action = {
54 | type: 'asdjkasd',
55 | };
56 |
57 | const state = authReducer( initState, action );
58 |
59 | expect( state ).toEqual( initState );
60 |
61 | })
62 |
63 |
64 | })
65 |
--------------------------------------------------------------------------------
/src/components/journal/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 |
4 | import { JournalEntries } from './JournalEntries'
5 | import { startLogout } from '../../actions/auth';
6 | import { startNewNote } from '../../actions/notes';
7 |
8 | export const Sidebar = () => {
9 |
10 | const dispatch = useDispatch();
11 | const { name } = useSelector( state => state.auth );
12 |
13 | const hanleLogout = () => {
14 | dispatch( startLogout() )
15 | }
16 |
17 | const handleAddNew = () => {
18 | dispatch( startNewNote() );
19 | }
20 |
21 | return (
22 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/src/tests/components/auth/__snapshots__/RegisterScreen.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Pruebas en debe de mostrarse correctamente 1`] = `
4 | Array [
5 |
8 | Register
9 |
,
10 | ,
62 | ]
63 | `;
64 |
--------------------------------------------------------------------------------
/src/components/notes/NotesAppBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { startSaveNote, startUploading } from '../../actions/notes';
4 |
5 | export const NotesAppBar = () => {
6 |
7 | const dispatch = useDispatch();
8 | const { active } = useSelector( state => state.notes );
9 |
10 | const handleSave = () => {
11 | dispatch( startSaveNote( active ) );
12 | }
13 |
14 | const handlePictureClick = () => {
15 | document.querySelector('#fileSelector').click();
16 | }
17 |
18 | const handleFileChange = (e) => {
19 | const file = e.target.files[0];
20 | if ( file ) {
21 | dispatch( startUploading( file ) );
22 | }
23 | }
24 |
25 | return (
26 |
27 |
28 de agosto 2020
28 |
29 |
36 |
37 |
38 |
44 |
45 |
51 |
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/journal/JournalEntry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 | import { useDispatch } from 'react-redux';
4 | import { activeNote } from '../../actions/notes';
5 |
6 | export const JournalEntry = ({ id, date, title, body, url }) => {
7 |
8 | const noteDate = moment(date);
9 | const dispatch = useDispatch();
10 |
11 | const handleEntryClick = () => {
12 | dispatch(
13 | activeNote( id, {
14 | date, title, body, url
15 | })
16 | );
17 | }
18 |
19 | return (
20 |
24 |
25 | {
26 | url &&
27 |
34 | }
35 |
36 |
37 |
38 | { title }
39 |
40 |
41 | { body }
42 |
43 |
44 |
45 |
46 | { noteDate.format('dddd') }
47 |
{ noteDate.format('Do') }
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/src/styles/components/_journal.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .journal__main-content {
4 | display: flex;
5 | }
6 |
7 | .journal__sidebar {
8 | background-color: $dark-grey;
9 | color: white;
10 | display: flex;
11 | flex-direction: column;
12 | height: 100vh;
13 | padding-left: 10px;
14 | padding-right: 10px;
15 | width: 450px;
16 | }
17 |
18 | .journal__sidebar-navbar {
19 | display: flex;
20 | justify-content: space-between;
21 | }
22 |
23 | .journal__sidebar-navbar h3 {
24 | font-weight: lighter;
25 | }
26 |
27 | .journal__new-entry {
28 | align-items: center;
29 | cursor: pointer;
30 | display: flex;
31 | flex-direction: column;
32 | justify-content: center;
33 | margin-top: 30px;
34 | width: 100%;
35 |
36 | transition: color .3s ease;
37 |
38 | &:hover {
39 | color: darken($color: white, $amount: 20);
40 | }
41 |
42 | }
43 |
44 | .journal__entries {
45 | flex: 1 1 auto;
46 | margin-top: 30px;
47 | overflow-y: scroll;
48 | }
49 |
50 | .journal__entry {
51 | background-color: white;
52 | border-radius: 4px;
53 | color: $dark-grey;
54 | display: flex;
55 | justify-content: space-between;
56 | margin-bottom: 10px;
57 | overflow: hidden;
58 | }
59 |
60 | .journal__entry-picture {
61 | height: 75px;
62 | width: 75px;
63 | }
64 |
65 |
66 | .journal__entry-body {
67 | padding: 10px;
68 | }
69 |
70 | .journal__entry-title {
71 | font-size: 14px;
72 | font-weight: bold;
73 | }
74 |
75 | .journal__entry-content {
76 | font-size: 10px;
77 | }
78 |
79 | .journal__entry-date-box {
80 | align-items: center;
81 | display: flex;
82 | flex-direction: column;
83 | justify-content: center;
84 | padding: 5px;
85 | }
86 |
87 | .journal__entry-date-box span{
88 | font-size: 12px;
89 | }
90 |
--------------------------------------------------------------------------------
/src/tests/components/auth/__snapshots__/LoginScreen.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Pruebas en debe de mostrarse correctamente 1`] = `
4 | Array [
5 |
8 | Login
9 |
,
10 | ,
74 | ]
75 | `;
76 |
--------------------------------------------------------------------------------
/src/tests/routers/AppRouter.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Provider } from 'react-redux'
4 | import { MemoryRouter } from 'react-router-dom';
5 |
6 | import configureStore from 'redux-mock-store';
7 | import thunk from 'redux-thunk';
8 |
9 | import '@testing-library/jest-dom';
10 |
11 | import { firebase } from '../../firebase/firebase-config';
12 |
13 |
14 | import { login } from '../../actions/auth';
15 | import { AppRouter } from '../../routers/AppRouter';
16 | import { act } from '@testing-library/react';
17 |
18 |
19 | jest.mock('../../actions/auth', () => ({
20 | login: jest.fn(),
21 | }));
22 |
23 |
24 | const middlewares = [thunk];
25 | const mockStore = configureStore(middlewares);
26 |
27 | const initState = {
28 | auth: {},
29 | ui: {
30 | loading: false,
31 | msgError: null
32 | },
33 | notes: {
34 | active: {
35 | id: 'ABC',
36 | },
37 | notes: []
38 | }
39 | };
40 |
41 | let store = mockStore(initState);
42 | store.dispatch = jest.fn();
43 |
44 |
45 |
46 |
47 |
48 | describe('Pruebas en ', () => {
49 |
50 | test('debe de llamar el login si estoy autenticado', async() => {
51 |
52 | let user;
53 |
54 | await act( async () => {
55 |
56 | const userCred = await firebase.auth().signInWithEmailAndPassword('test@testing.com', '123456');
57 | user = userCred.user;
58 |
59 |
60 | const wrapper = mount(
61 |
62 |
63 |
64 |
65 |
66 |
67 | )
68 |
69 | });
70 |
71 |
72 | expect( login ).toHaveBeenCalledWith('fYYrX6ZV7oOD4bJHtciBV0RZWKB3', null);
73 |
74 |
75 |
76 | })
77 |
78 |
79 | })
80 |
--------------------------------------------------------------------------------
/src/reducers/notesReducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | {
3 | notes: [],
4 | active: null,
5 | active: {
6 | id: 'KASKLDJALKSDJ129387123',
7 | title: '',
8 | body: '',
9 | imageUrl: '',
10 | date: 12387612387126
11 | }
12 | }
13 | */
14 |
15 | import { types } from '../types/types';
16 |
17 | const initialState = {
18 | notes: [],
19 | active: null
20 | }
21 |
22 |
23 | export const notesReducer = ( state = initialState, action ) => {
24 |
25 | switch (action.type) {
26 |
27 | case types.notesActive:
28 | return {
29 | ...state,
30 | active: {
31 | ...action.payload
32 | }
33 | }
34 |
35 | case types.notesAddNew:
36 | return {
37 | ...state,
38 | notes: [ action.payload, ...state.notes ]
39 | }
40 |
41 | case types.notesLoad:
42 | return {
43 | ...state,
44 | notes: [ ...action.payload ]
45 | }
46 |
47 | case types.notesUpdated:
48 | return {
49 | ...state,
50 | notes: state.notes.map(
51 | note => note.id === action.payload.id
52 | ? action.payload.note
53 | : note
54 | )
55 | }
56 |
57 | case types.notesDelete:
58 | return {
59 | ...state,
60 | active: null,
61 | notes: state.notes.filter( note => note.id !== action.payload )
62 | }
63 |
64 | case types.notesLogoutCleaning:
65 | return {
66 | ...state,
67 | active: null,
68 | notes: []
69 | }
70 |
71 | default:
72 | return state
73 | }
74 |
75 |
76 | }
--------------------------------------------------------------------------------
/src/tests/components/journal/Sidebar.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Provider } from 'react-redux'
4 |
5 | import configureStore from 'redux-mock-store';
6 | import thunk from 'redux-thunk';
7 |
8 | import '@testing-library/jest-dom';
9 | import { startLogout } from '../../../actions/auth';
10 | import { startNewNote } from '../../../actions/notes';
11 | import { Sidebar } from '../../../components/journal/Sidebar';
12 |
13 |
14 | jest.mock('../../../actions/auth', () => ({
15 | startLogout: jest.fn(),
16 | }));
17 |
18 | jest.mock('../../../actions/notes', () => ({
19 | startNewNote: jest.fn(),
20 | }));
21 |
22 |
23 | const middlewares = [thunk];
24 | const mockStore = configureStore(middlewares);
25 |
26 | const initState = {
27 | auth: {
28 | uid: '1',
29 | name: 'Fernando'
30 | },
31 | ui: {
32 | loading: false,
33 | msgError: null
34 | },
35 | notes: {
36 | active: null,
37 | notes: []
38 | }
39 | };
40 |
41 | let store = mockStore(initState);
42 | store.dispatch = jest.fn();
43 |
44 | const wrapper = mount(
45 |
46 |
47 |
48 |
49 | )
50 |
51 |
52 | describe('Pruebas en ', () => {
53 |
54 |
55 | test('debe de mostrarse correctamente', () => {
56 | // snapshot
57 | expect( wrapper ).toMatchSnapshot();
58 | })
59 |
60 |
61 | test('debe de llamar el startLogout', () => {
62 | // debe de llamar la acción del logout
63 | wrapper.find('button').prop('onClick')();
64 |
65 | expect( startLogout ).toHaveBeenCalled()
66 |
67 | })
68 |
69 | test('debe de llamar el startNewNote', () => {
70 | // debe de llamar la acción startNewNote
71 | wrapper.find('.journal__new-entry').prop('onClick')();
72 | expect( startNewNote ).toHaveBeenCalled();
73 |
74 | })
75 |
76 |
77 | })
78 |
--------------------------------------------------------------------------------
/src/tests/components/auth/LoginScreen.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Provider } from 'react-redux'
4 | import { MemoryRouter } from 'react-router-dom';
5 |
6 | import configureStore from 'redux-mock-store';
7 | import thunk from 'redux-thunk';
8 |
9 | import '@testing-library/jest-dom';
10 |
11 | import { LoginScreen } from '../../../components/auth/LoginScreen';
12 | import { startGoogleLogin, startLoginEmailPassword } from '../../../actions/auth';
13 |
14 | jest.mock('../../../actions/auth', () => ({
15 | startGoogleLogin: jest.fn(),
16 | startLoginEmailPassword: jest.fn(),
17 | }))
18 |
19 |
20 |
21 | const middlewares = [thunk];
22 | const mockStore = configureStore(middlewares);
23 |
24 | const initState = {
25 | auth: {},
26 | ui: {
27 | loading: false,
28 | msgError: null
29 | }
30 | };
31 |
32 | let store = mockStore(initState);
33 | store.dispatch = jest.fn();
34 |
35 | const wrapper = mount(
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 |
44 | describe('Pruebas en ', () => {
45 |
46 | beforeEach(()=> {
47 | store = mockStore(initState);
48 | jest.clearAllMocks();
49 | })
50 |
51 |
52 | test('debe de mostrarse correctamente', () => {
53 |
54 | expect( wrapper ).toMatchSnapshot();
55 |
56 | });
57 |
58 | test('debe de disparar la acción de startGoogleLogin', () => {
59 |
60 | wrapper.find('.google-btn').prop('onClick')();
61 |
62 | expect( startGoogleLogin ).toHaveBeenCalled();
63 | })
64 |
65 |
66 | test('debe de disparar el startLogin con los respectivos argumentos', () => {
67 |
68 |
69 | wrapper.find('form').prop('onSubmit')({
70 | preventDefault(){}
71 | });
72 |
73 | expect( startLoginEmailPassword ).toHaveBeenLastCalledWith('','');
74 |
75 |
76 | })
77 |
78 |
79 | })
80 |
--------------------------------------------------------------------------------
/src/tests/components/notes/NoteScreen.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Provider } from 'react-redux'
4 |
5 | import configureStore from 'redux-mock-store';
6 | import thunk from 'redux-thunk';
7 |
8 | import '@testing-library/jest-dom';
9 | import { activeNote } from '../../../actions/notes';
10 | import { NoteScreen } from '../../../components/notes/NoteScreen';
11 |
12 |
13 | jest.mock('../../../actions/notes', () => ({
14 | activeNote: jest.fn(),
15 | }));
16 |
17 |
18 | const middlewares = [thunk];
19 | const mockStore = configureStore(middlewares);
20 |
21 | const initState = {
22 | auth: {
23 | uid: '1',
24 | name: 'Fernando'
25 | },
26 | ui: {
27 | loading: false,
28 | msgError: null
29 | },
30 | notes: {
31 | active: {
32 | id: 1234,
33 | title: 'Hola',
34 | body: 'mundo',
35 | date: 0
36 | },
37 | notes: []
38 | }
39 | };
40 |
41 | let store = mockStore(initState);
42 | store.dispatch = jest.fn();
43 |
44 | const wrapper = mount(
45 |
46 |
47 |
48 |
49 | )
50 |
51 |
52 |
53 |
54 | describe('Pruebas en ', () => {
55 |
56 | test('debe de mostrarse correctamente', () => {
57 |
58 | expect( wrapper ).toMatchSnapshot();
59 |
60 | })
61 |
62 |
63 | test('debe de disparar el active note', () => {
64 |
65 | wrapper.find('input[name="title"]').simulate('change', {
66 | target: {
67 | name: 'title',
68 | value: 'Hola de nuevo'
69 | }
70 | });
71 |
72 |
73 | expect( activeNote ).toHaveBeenLastCalledWith(
74 | 1234,
75 | {
76 | body: 'mundo',
77 | title: 'Hola de nuevo',
78 | id: 1234,
79 | date: 0
80 | }
81 | );
82 |
83 |
84 | })
85 |
86 |
87 |
88 |
89 |
90 | })
91 |
--------------------------------------------------------------------------------
/src/styles/components/_buttons.scss:
--------------------------------------------------------------------------------
1 |
2 | .btn {
3 | background-color: transparent;
4 | border: none;
5 | color: white;
6 | cursor: pointer;
7 | font-size: 12px;
8 | padding: 7px 10px 7px 10px;
9 |
10 | transition: color .3s ease;
11 |
12 | &:focus {
13 | outline: none;
14 | }
15 |
16 | &:hover {
17 | color: darken($color: white, $amount: 10);
18 | }
19 |
20 | }
21 |
22 |
23 | .btn-primary {
24 | background-color: $primary;
25 | border-radius: 2px;
26 |
27 | &:disabled {
28 | background-color: lighten($color: $primary, $amount: 15);
29 | }
30 |
31 | &:hover {
32 | background-color: darken($color: $primary, $amount: 15);
33 | }
34 | }
35 |
36 | .btn-danger {
37 | background-color: red;
38 |
39 | transition: background-color .3s ease;
40 |
41 | &:hover {
42 | background-color: darken($color: red, $amount: 10);
43 | }
44 | }
45 |
46 | .btn-block {
47 | width: 100%;
48 | }
49 |
50 | .google-btn {
51 | cursor: pointer;
52 | margin-top: 5px;
53 | width: 100%;
54 | height: 42px;
55 | background-color: $google-blue;
56 | border-radius: 2px;
57 | box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.25);
58 |
59 | transition: box-shadow .3s ease;
60 |
61 | .google-icon-wrapper {
62 | position: absolute;
63 | margin-top: 1px;
64 | margin-left: 1px;
65 | width: 40px;
66 | height: 40px;
67 | border-radius: 2px;
68 | background-color: $white;
69 | }
70 | .google-icon {
71 | position: absolute;
72 | margin-top: 11px;
73 | margin-left: 11px;
74 | width: 18px;
75 | height: 18px;
76 | }
77 | .btn-text {
78 | float: right;
79 | margin: 11px 40px 0 0;
80 | color: $white;
81 | font-size: 14px;
82 | letter-spacing: 0.2px;
83 | }
84 | &:hover {
85 | box-shadow: 0 0 6px $google-blue;
86 | }
87 | &:active {
88 | background: $button-active-blue;
89 | }
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 |
33 |
34 | Journal App
35 |
36 |
37 |
38 |
39 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/tests/actions/auth.test.js:
--------------------------------------------------------------------------------
1 | import configureStore from 'redux-mock-store';
2 | import thunk from 'redux-thunk';
3 |
4 | import '@testing-library/jest-dom';
5 |
6 | import { types } from '../../types/types';
7 | import { login, logout, startLogout, startLoginEmailPassword } from '../../actions/auth';
8 |
9 |
10 | const middlewares = [thunk];
11 | const mockStore = configureStore(middlewares);
12 |
13 | const initState = {};
14 |
15 | let store = mockStore(initState);
16 |
17 |
18 | describe('Pruebas con las acciones de Auth', () => {
19 |
20 | beforeEach(()=> {
21 | store = mockStore(initState);
22 | })
23 |
24 |
25 |
26 | test('login y logout deben de crear la acción respectiva', () => {
27 |
28 | const uid = 'ABC123';
29 | const displayName = 'Fernando';
30 |
31 | const loginAction = login( uid, displayName );
32 | const logoutAction = logout();
33 |
34 | expect( loginAction ).toEqual({
35 | type: types.login,
36 | payload: {
37 | uid,
38 | displayName
39 | }
40 | });
41 |
42 | expect( logoutAction ).toEqual({
43 | type: types.logout
44 | });
45 |
46 | })
47 |
48 |
49 | test('debe de realizar el startLogout', async() => {
50 |
51 | await store.dispatch( startLogout() );
52 |
53 | const actions = store.getActions();
54 |
55 | expect( actions[0] ).toEqual({
56 | type: types.logout
57 | });
58 |
59 | expect( actions[1] ).toEqual({
60 | type: types.notesLogoutCleaning
61 | });
62 |
63 |
64 | });
65 |
66 |
67 | test('debe de iniciar el startLoginEmailPassword', async() => {
68 |
69 | await store.dispatch( startLoginEmailPassword('test@testing.com','123456') );
70 |
71 | const actions = store.getActions();
72 |
73 | expect( actions[1] ).toEqual({
74 | type: types.login,
75 | payload: {
76 | uid: 'fYYrX6ZV7oOD4bJHtciBV0RZWKB3',
77 | displayName: null
78 | }
79 | })
80 |
81 | })
82 |
83 |
84 |
85 |
86 | })
87 |
--------------------------------------------------------------------------------
/src/routers/AppRouter.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import {
3 | BrowserRouter as Router,
4 | Switch,
5 | Redirect
6 | } from 'react-router-dom';
7 |
8 | import { useDispatch } from 'react-redux';
9 |
10 | import { firebase } from '../firebase/firebase-config'
11 | import { AuthRouter } from './AuthRouter';
12 | import { PrivateRoute } from './PrivateRoute';
13 |
14 | import { JournalScreen } from '../components/journal/JournalScreen';
15 | import { login } from '../actions/auth';
16 | import { PublicRoute } from './PublicRoute';
17 | import { startLoadingNotes } from '../actions/notes';
18 |
19 | export const AppRouter = () => {
20 |
21 | const dispatch = useDispatch();
22 |
23 | const [ checking, setChecking ] = useState(true);
24 | const [ isLoggedIn, setIsLoggedIn ] = useState(false);
25 |
26 |
27 |
28 | useEffect(() => {
29 |
30 | firebase.auth().onAuthStateChanged( async(user) => {
31 |
32 | if ( user?.uid ) {
33 | dispatch( login( user.uid, user.displayName ) );
34 | setIsLoggedIn( true );
35 | dispatch( startLoadingNotes( user.uid ) );
36 |
37 | } else {
38 | setIsLoggedIn( false );
39 | }
40 |
41 | setChecking(false);
42 |
43 | });
44 |
45 | }, [ dispatch, setChecking, setIsLoggedIn ])
46 |
47 |
48 | if ( checking ) {
49 | return (
50 | Wait...
51 | )
52 | }
53 |
54 |
55 | return (
56 |
57 |
58 |
59 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import Swal from 'sweetalert2';
2 |
3 | import { firebase, googleAuthProvider } from '../firebase/firebase-config';
4 | import { types } from '../types/types';
5 | import { startLoading, finishLoading } from './ui';
6 | import { noteLogout } from './notes';
7 |
8 |
9 | export const startLoginEmailPassword = (email, password) => {
10 | return (dispatch) => {
11 |
12 | dispatch( startLoading() );
13 |
14 |
15 | return firebase.auth().signInWithEmailAndPassword( email, password )
16 | .then( ({ user }) => {
17 | dispatch(login( user.uid, user.displayName ));
18 |
19 | dispatch( finishLoading() );
20 | })
21 | .catch( e => {
22 | console.log(e);
23 | dispatch( finishLoading() );
24 | Swal.fire('Error', e.message, 'error');
25 | })
26 |
27 | }
28 | }
29 |
30 | export const startRegisterWithEmailPasswordName = ( email, password, name ) => {
31 | return ( dispatch ) => {
32 |
33 | firebase.auth().createUserWithEmailAndPassword( email, password )
34 | .then( async({ user }) => {
35 |
36 | await user.updateProfile({ displayName: name });
37 |
38 | dispatch(
39 | login( user.uid, user.displayName )
40 | );
41 | })
42 | .catch( e => {
43 | console.log(e);
44 | Swal.fire('Error', e.message, 'error');
45 | })
46 |
47 | }
48 | }
49 |
50 |
51 |
52 | export const startGoogleLogin = () => {
53 | return ( dispatch ) => {
54 |
55 | firebase.auth().signInWithPopup( googleAuthProvider )
56 | .then( ({ user }) => {
57 | dispatch(
58 | login( user.uid, user.displayName )
59 | )
60 | });
61 |
62 | }
63 | }
64 |
65 |
66 | export const login = (uid, displayName) => ({
67 | type: types.login,
68 | payload: {
69 | uid,
70 | displayName
71 | }
72 | });
73 |
74 |
75 | export const startLogout = () => {
76 | return async( dispatch ) => {
77 | await firebase.auth().signOut();
78 |
79 | dispatch( logout() );
80 | dispatch( noteLogout() );
81 | }
82 | }
83 |
84 |
85 | export const logout = () => ({
86 | type: types.logout
87 | })
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/components/notes/NoteScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 |
4 | import { NotesAppBar } from './NotesAppBar';
5 | import { useForm } from '../../hooks/useForm';
6 | import { activeNote, startDeleting } from '../../actions/notes';
7 |
8 | export const NoteScreen = () => {
9 |
10 | const dispatch = useDispatch();
11 |
12 | const { active:note } = useSelector( state => state.notes );
13 | const [ formValues, handleInputChange, reset ] = useForm( note );
14 | const { body, title, id } = formValues;
15 |
16 | const activeId = useRef( note.id );
17 |
18 | useEffect(() => {
19 |
20 | if ( note.id !== activeId.current ) {
21 | reset( note );
22 | activeId.current = note.id
23 | }
24 |
25 | }, [note, reset])
26 |
27 | useEffect(() => {
28 |
29 | dispatch( activeNote( formValues.id, { ...formValues } ) );
30 |
31 | }, [formValues, dispatch])
32 |
33 |
34 | const handleDelete = () => {
35 | dispatch( startDeleting( id ) );
36 | }
37 |
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
55 |
56 |
63 |
64 | {
65 | (note.url)
66 | && (
67 |
68 |

72 |
73 | )
74 | }
75 |
76 |
77 |
78 |
79 |
80 |
86 |
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/src/tests/components/auth/RegisterScreen.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { Provider } from 'react-redux'
4 | import { MemoryRouter } from 'react-router-dom';
5 |
6 | import configureStore from 'redux-mock-store';
7 | import thunk from 'redux-thunk';
8 |
9 | import '@testing-library/jest-dom';
10 | import { RegisterScreen } from '../../../components/auth/RegisterScreen';
11 | import { types } from '../../../types/types';
12 |
13 | // jest.mock('../../../actions/auth', () => ({
14 | // startGoogleLogin: jest.fn(),
15 | // startLoginEmailPassword: jest.fn(),
16 | // }))
17 |
18 |
19 | const middlewares = [thunk];
20 | const mockStore = configureStore(middlewares);
21 |
22 | const initState = {
23 | auth: {},
24 | ui: {
25 | loading: false,
26 | msgError: null
27 | }
28 | };
29 |
30 | let store = mockStore(initState);
31 | // store.dispatch = jest.fn();
32 |
33 | const wrapper = mount(
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 |
42 |
43 |
44 |
45 | describe('Pruebas en ', () => {
46 |
47 |
48 |
49 |
50 | test('debe de mostrarse correctamente', () => {
51 |
52 | expect( wrapper ).toMatchSnapshot();
53 |
54 | })
55 |
56 |
57 | test('debe de hacer el dispatch de la acción respectiva', () => {
58 |
59 | const emailField = wrapper.find('input[name="email"]');
60 |
61 | emailField.simulate('change', {
62 | target: {
63 | value: '',
64 | name: 'email'
65 | }
66 | });
67 |
68 | wrapper.find('form').simulate('submit', {
69 | preventDefault(){}
70 | });
71 |
72 | const actions = store.getActions();
73 |
74 | expect( actions[0] ).toEqual({
75 | type: types.uiSetError,
76 | payload: 'Email is not valid'
77 | });
78 |
79 | })
80 |
81 |
82 | test('debe de mostrar la caja de alerta con el error', () => {
83 |
84 | const initState = {
85 | auth: {},
86 | ui: {
87 | loading: false,
88 | msgError: 'Email no es correcto'
89 | }
90 | };
91 |
92 | const store = mockStore(initState);
93 |
94 |
95 | const wrapper = mount(
96 |
97 |
98 |
99 |
100 |
101 | );
102 |
103 |
104 | expect( wrapper.find('.auth__alert-error').exists() ).toBe(true);
105 | expect( wrapper.find('.auth__alert-error').text().trim() ).toBe( initState.ui.msgError );
106 |
107 |
108 |
109 | })
110 |
111 |
112 |
113 |
114 | })
115 |
--------------------------------------------------------------------------------
/src/components/auth/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import { Link } from 'react-router-dom';
5 | import { useForm } from '../../hooks/useForm';
6 | import { startLoginEmailPassword, startGoogleLogin } from '../../actions/auth';
7 |
8 | export const LoginScreen = () => {
9 |
10 | const dispatch = useDispatch();
11 | const { loading } = useSelector( state => state.ui );
12 |
13 | const [ formValues, handleInputChange ] = useForm({
14 | email: '',
15 | password: ''
16 | });
17 |
18 | const { email, password } = formValues;
19 |
20 | const handleLogin = (e) => {
21 | e.preventDefault();
22 | dispatch( startLoginEmailPassword( email, password ) );
23 | }
24 |
25 | const handleGoogleLogin = () => {
26 | dispatch( startGoogleLogin() );
27 | }
28 |
29 |
30 | return (
31 | <>
32 | Login
33 |
34 |
92 | >
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/src/actions/notes.js:
--------------------------------------------------------------------------------
1 | import Swal from 'sweetalert2';
2 |
3 | import { db } from '../firebase/firebase-config';
4 | import { types } from '../types/types';
5 | import { loadNotes } from '../helpers/loadNotes';
6 | import { fileUpload } from '../helpers/fileUpload';
7 |
8 |
9 | export const startNewNote = () => {
10 | return async( dispatch, getState ) => {
11 |
12 | const { uid } = getState().auth;
13 |
14 | const newNote = {
15 | title: '',
16 | body: '',
17 | date: new Date().getTime()
18 | }
19 |
20 | try {
21 | const doc = await db.collection(`${ uid }/journal/notes`).add( newNote );
22 |
23 | dispatch( activeNote( doc.id, newNote ) );
24 | dispatch( addNewNote( doc.id, newNote ) );
25 |
26 | } catch (error) {
27 | console.log(error);
28 | }
29 |
30 |
31 | }
32 | }
33 |
34 | export const activeNote = ( id, note ) => ({
35 | type: types.notesActive,
36 | payload: {
37 | id,
38 | ...note
39 | }
40 | });
41 |
42 | export const addNewNote = ( id, note ) => ({
43 | type: types.notesAddNew,
44 | payload: {
45 | id, ...note
46 | }
47 | })
48 |
49 |
50 | export const startLoadingNotes = ( uid ) => {
51 | return async( dispatch ) => {
52 |
53 | const notes = await loadNotes( uid );
54 | dispatch( setNotes( notes ) );
55 |
56 | }
57 | }
58 |
59 |
60 | export const setNotes = ( notes ) => ({
61 | type: types.notesLoad,
62 | payload: notes
63 | });
64 |
65 |
66 | export const startSaveNote = ( note ) => {
67 | return async( dispatch, getState ) => {
68 |
69 | const { uid } = getState().auth;
70 |
71 | if ( !note.url ){
72 | delete note.url;
73 | }
74 |
75 | const noteToFirestore = { ...note };
76 | delete noteToFirestore.id;
77 |
78 | await db.doc(`${ uid }/journal/notes/${ note.id }`).update( noteToFirestore );
79 |
80 | dispatch( refreshNote( note.id, noteToFirestore ) );
81 | Swal.fire('Saved', note.title, 'success');
82 | }
83 | }
84 |
85 | export const refreshNote = ( id, note ) => ({
86 | type: types.notesUpdated,
87 | payload: {
88 | id,
89 | note: {
90 | id,
91 | ...note
92 | }
93 | }
94 | });
95 |
96 |
97 | export const startUploading = ( file ) => {
98 | return async( dispatch, getState ) => {
99 |
100 | const { active:activeNote } = getState().notes;
101 |
102 | Swal.fire({
103 | title: 'Uploading...',
104 | text: 'Please wait...',
105 | allowOutsideClick: false,
106 | onBeforeOpen: () => {
107 | Swal.showLoading();
108 | }
109 | });
110 |
111 | const fileUrl = await fileUpload( file );
112 | activeNote.url = fileUrl;
113 |
114 | dispatch( startSaveNote( activeNote ) )
115 |
116 |
117 | Swal.close();
118 | }
119 | }
120 |
121 |
122 | export const startDeleting = ( id ) => {
123 | return async( dispatch, getState ) => {
124 |
125 | const uid = getState().auth.uid;
126 | await db.doc(`${ uid }/journal/notes/${ id }`).delete();
127 |
128 | dispatch( deleteNote(id) );
129 |
130 | }
131 | }
132 |
133 | export const deleteNote = (id) => ({
134 | type: types.notesDelete,
135 | payload: id
136 | });
137 |
138 |
139 | export const noteLogout = () => ({
140 | type: types.notesLogoutCleaning
141 | });
142 |
--------------------------------------------------------------------------------
/src/tests/actions/notes.test.js:
--------------------------------------------------------------------------------
1 | import configureStore from 'redux-mock-store';
2 | import thunk from 'redux-thunk';
3 |
4 |
5 | import { startNewNote, startLoadingNotes, startSaveNote, startUploading } from '../../actions/notes';
6 | import { types } from '../../types/types';
7 | import { db } from '../../firebase/firebase-config';
8 | import { fileUpload } from '../../helpers/fileUpload';
9 |
10 | jest.mock('../../helpers/fileUpload', () => ({
11 | fileUpload: jest.fn( () => {
12 | return 'https://hola-mundo.com/cosa.jpg';
13 | // return Promise.resolve('https://hola-mundo.com/cosa.jpg');
14 | })
15 | }))
16 |
17 |
18 |
19 |
20 | const middlewares = [thunk];
21 | const mockStore = configureStore(middlewares);
22 |
23 | const initState = {
24 | auth: {
25 | uid: 'TESTING'
26 | },
27 | notes: {
28 | active: {
29 | id: '02L6n2ZPdEgpELw8y7ML',
30 | title: 'Hola',
31 | body: 'Mundo'
32 | }
33 | }
34 | };
35 |
36 | let store = mockStore(initState);
37 |
38 |
39 | describe('Pruebas con las acciones de notes', () => {
40 |
41 | beforeEach( () => {
42 |
43 | store = mockStore(initState);
44 |
45 | });
46 |
47 |
48 | test('debe de crear una nueva nota startNewNote', async() => {
49 |
50 | await store.dispatch( startNewNote() );
51 |
52 | const actions = store.getActions();
53 | // console.log(actions);
54 |
55 | expect( actions[0] ).toEqual({
56 | type: types.notesActive,
57 | payload: {
58 | id: expect.any(String),
59 | title: '',
60 | body: '',
61 | date: expect.any(Number)
62 | }
63 | });
64 |
65 | expect( actions[1] ).toEqual({
66 | type: types.notesAddNew,
67 | payload: {
68 | id: expect.any(String),
69 | title: '',
70 | body: '',
71 | date: expect.any(Number)
72 | }
73 | });
74 |
75 | // const docId .... action.... payload.... id
76 | // await ..... db.... doc(``)..... .delete();
77 | const docId = actions[0].payload.id;
78 | await db.doc(`/TESTING/journal/notes/${ docId }`).delete();
79 |
80 |
81 |
82 | })
83 |
84 |
85 | test('startLoadingNotes debe cargar las notas', async() => {
86 |
87 | await store.dispatch( startLoadingNotes('TESTING') );
88 | const actions = store.getActions();
89 |
90 | expect( actions[0] ).toEqual({
91 | type: types.notesLoad,
92 | payload: expect.any(Array)
93 | });
94 |
95 | const expected = {
96 | id: expect.any(String),
97 | title: expect.any(String),
98 | body: expect.any(String),
99 | date: expect.any(Number),
100 | }
101 |
102 | expect( actions[0].payload[0] ).toMatchObject( expected );
103 |
104 |
105 | })
106 |
107 |
108 | test('startSaveNote debe de actualizar la nota', async() => {
109 |
110 | const note = {
111 | id: '02L6n2ZPdEgpELw8y7ML',
112 | title: 'titulo',
113 | body: 'body'
114 | };
115 |
116 | await store.dispatch( startSaveNote( note ) );
117 |
118 | const actions = store.getActions();
119 | // console.log(actions);
120 | expect( actions[0].type ).toBe( types.notesUpdated );
121 |
122 | const docRef = await db.doc(`/TESTING/journal/notes/${ note.id }`).get();
123 |
124 | expect( docRef.data().title ).toBe( note.title );
125 |
126 | })
127 |
128 |
129 | test('startUploading debe de actualizar el url del entry', async() => {
130 |
131 | const file = new File([], 'foto.jpg');
132 | await store.dispatch( startUploading( file ) );
133 |
134 | const docRef = await db.doc('/TESTING/journal/notes/02L6n2ZPdEgpELw8y7ML').get();
135 | expect( docRef.data().url ).toBe('https://hola-mundo.com/cosa.jpg');
136 |
137 | })
138 |
139 |
140 |
141 | })
142 |
--------------------------------------------------------------------------------
/src/components/auth/RegisterScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useDispatch, useSelector} from 'react-redux';
4 | import validator from 'validator';
5 |
6 | import { useForm } from '../../hooks/useForm';
7 | import { setError, removeError } from '../../actions/ui';
8 | import { startRegisterWithEmailPasswordName } from '../../actions/auth';
9 |
10 | export const RegisterScreen = () => {
11 |
12 | const dispatch = useDispatch();
13 | const { msgError } = useSelector( state => state.ui );
14 |
15 | const [ formValues, handleInputChange ] = useForm({
16 | name: 'Hernando',
17 | email: 'nando@gmail.com',
18 | password: '123456',
19 | password2: '123456',
20 | });
21 |
22 | const { name ,email ,password ,password2 } = formValues;
23 |
24 | const handleRegister = (e) => {
25 | e.preventDefault();
26 |
27 | if ( isFormValid() ) {
28 | dispatch( startRegisterWithEmailPasswordName(email, password, name) );
29 | }
30 |
31 | }
32 |
33 | const isFormValid = () => {
34 |
35 | if ( name.trim().length === 0 ) {
36 | dispatch( setError('Name is required') )
37 | return false;
38 | } else if ( !validator.isEmail( email ) ) {
39 | dispatch( setError('Email is not valid') )
40 | return false;
41 | } else if ( password !== password2 || password.length < 5 ) {
42 | dispatch( setError('Password should be at least 6 characters and match each other') )
43 | return false
44 | }
45 |
46 | dispatch( removeError() );
47 | return true;
48 | }
49 |
50 | return (
51 | <>
52 | Register
53 |
54 |
125 | >
126 | )
127 | }
128 |
--------------------------------------------------------------------------------