├── 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 | 63 | 64 | { 65 | (note.url) 66 | && ( 67 |
68 | imagen 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 |
38 | 39 | 48 | 49 | 57 | 58 | 59 | 66 | 67 | 68 |
69 |

Login with social networks

70 | 71 |
75 |
76 | google button 77 |
78 |

79 | Sign in with google 80 |

81 |
82 |
83 | 84 | 88 | Create new account 89 | 90 | 91 |
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 |
58 | 59 | { 60 | msgError && 61 | ( 62 |
63 | { msgError } 64 |
65 | ) 66 | } 67 | 68 | 69 | 78 | 79 | 88 | 89 | 97 | 98 | 106 | 107 | 108 | 114 | 115 | 116 | 117 | 121 | Already registered? 122 | 123 | 124 |
125 | 126 | ) 127 | } 128 | --------------------------------------------------------------------------------