├── .eslintrc ├── .gitignore ├── Client ├── components │ ├── App.js │ ├── Header.js │ └── Notes │ │ ├── AddNoteForm.js │ │ ├── ControlPanel.js │ │ ├── EditNoteForm.js │ │ ├── NoteManager.js │ │ └── NoteTable.js ├── favicon.ico ├── index.html ├── index.js ├── services │ └── note-service.js └── styles │ ├── _base.scss │ ├── _bootstrap.scss │ ├── _react-modal.scss │ ├── _settings.scss │ ├── app.scss │ └── components │ └── _header.scss ├── README.md ├── Server ├── DataAccess │ ├── DbConnection.js │ └── NoteRepository.js ├── Services │ └── NoteManager.js ├── routers │ └── notes-router.js └── server.js ├── package-lock.json ├── package.json └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": [ "eslint:recommended", "plugin:react/recommended" ], 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "react" 19 | ], 20 | "rules": { 21 | "no-extra-boolean-cast": "off", 22 | "no-direct-mutation-state": "off", 23 | "no-console": "off", 24 | "indent": [ 25 | "error", 26 | 4 27 | ], 28 | "quotes": [ 29 | "error", 30 | "single" 31 | ], 32 | "semi": [ 33 | "error", 34 | "always" 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /public 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | /.vscode/**/* 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /Client/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Header from './Header'; 3 | import NoteManager from './Notes/NoteManager'; 4 | 5 | export default class App extends Component { 6 | 7 | constructor(){ 8 | super(); 9 | 10 | this.state = { 11 | title: 'React Starter', 12 | description: 'A basic template that consists of the essential elements that are required to start building a React application' 13 | }; 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 |
20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /Client/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Header = () => ( 4 | 11 | ); 12 | 13 | export default Header; -------------------------------------------------------------------------------- /Client/components/Notes/AddNoteForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import uuidv1 from 'uuid/v1'; 4 | 5 | 6 | class AddNoteForm extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | title: '', 13 | content: '', 14 | tags: [], 15 | validationErrors: [] 16 | }; 17 | 18 | this.onTitleChange = this.onTitleChange.bind(this); 19 | this.onContentChange = this.onContentChange.bind(this); 20 | this.onTagsChange = this.onTagsChange.bind(this); 21 | this.onSave = this.onSave.bind(this); 22 | } 23 | 24 | 25 | onTitleChange(event) { 26 | const title = event.target.value.trim(); 27 | 28 | this.validateTitle(title); 29 | 30 | this.setState({ title: title }); 31 | } 32 | 33 | 34 | onContentChange(event) { 35 | const content = event.target.value.trim(); 36 | 37 | this.validateContent(content); 38 | 39 | this.setState({ content: content }); 40 | } 41 | 42 | 43 | onTagsChange(event) { 44 | const tags = event.target.value.trim(); 45 | 46 | if (this.validateTags(tags)) { 47 | this.setState({ tags: tags.split(',')}); 48 | } 49 | } 50 | 51 | 52 | onSave(event) { 53 | event.preventDefault(); 54 | 55 | if (this.state.validationErrors && this.state.validationErrors.length === 0) { 56 | const { title, content } = this.state; 57 | 58 | if (this.validateTitle(title) && this.validateContent(content)) { 59 | this.props.onSaveNote(this.state); 60 | } 61 | } 62 | } 63 | 64 | 65 | validateTitle(title) { 66 | const message = 'Title is required'; 67 | 68 | if (title === '') { 69 | this.addValidationError(message); 70 | return false; 71 | } else { 72 | this.removeValidationError(message); 73 | return true; 74 | } 75 | } 76 | 77 | 78 | validateContent(content) { 79 | const message = 'Content is required'; 80 | 81 | if (content === '') { 82 | this.addValidationError(message); 83 | return false; 84 | } else { 85 | this.removeValidationError(message); 86 | return true; 87 | } 88 | } 89 | 90 | 91 | validateTags(tags) { 92 | const message = 'Tags must be a comma separated list'; 93 | 94 | if (tags !== '') { 95 | var regex = new RegExp(/^([\w]+[\s]*[,]?[\s]*)+$/); 96 | 97 | if (!regex.test(tags)) { 98 | this.addValidationError(message); 99 | return false; 100 | } else { 101 | this.removeValidationError(message); 102 | return true; 103 | } 104 | } else { 105 | this.removeValidationError(message); 106 | } 107 | } 108 | 109 | 110 | addValidationError(message) { 111 | this.setState((previousState) => { 112 | const validationErrors = [...previousState.validationErrors]; 113 | validationErrors.push({message}); 114 | return { 115 | validationErrors: validationErrors 116 | }; 117 | }); 118 | } 119 | 120 | 121 | removeValidationError(message) { 122 | this.setState((previousState) => { 123 | const validationErrors = previousState 124 | .validationErrors 125 | .filter(error => error.message !== message); 126 | 127 | return { 128 | validationErrors: validationErrors 129 | }; 130 | }); 131 | } 132 | 133 | 134 | render() { 135 | 136 | const validationErrorSummary = this.state.validationErrors.map(error => 137 |
138 | {error.message} 139 | 142 |
143 | ); 144 | 145 | return ( 146 |
147 |
148 | New Note 149 | 150 | 151 | 152 |
153 | {validationErrorSummary} 154 |
155 |
156 | 157 | 158 |
159 |
160 | 161 | 162 |
163 |
164 | 165 | 166 |
167 |
168 |
169 | 172 |
173 |
174 | 179 |
180 |
181 |
182 |
183 | ); 184 | } 185 | } 186 | 187 | AddNoteForm.propTypes = { 188 | onCloseModal: PropTypes.func, 189 | onSaveNote: PropTypes.func 190 | }; 191 | 192 | export default AddNoteForm; -------------------------------------------------------------------------------- /Client/components/Notes/ControlPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class ControlPanel extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | title: '' 11 | }; 12 | 13 | this.onSearchTitleChanged = this.onSearchTitleChanged.bind(this); 14 | } 15 | 16 | onSearchTitleChanged(event) { 17 | const title = event.target.value; 18 | this.setState({title}); 19 | } 20 | 21 | render () { 22 | return ( 23 |
24 |
25 | 26 | 29 | 30 | 31 | 32 | 35 | 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | ControlPanel.propTypes = { 43 | openAddNoteModal: PropTypes.func, 44 | onFindNotes: PropTypes.func 45 | }; 46 | 47 | export default ControlPanel; -------------------------------------------------------------------------------- /Client/components/Notes/EditNoteForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import uuidv1 from 'uuid/v1'; 4 | 5 | 6 | class EditNoteForm extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | id: props.note.id, 13 | title: props.note.title, 14 | content: props.note.content, 15 | tags: props.note.tags, 16 | validationErrors: [] 17 | }; 18 | 19 | this.onTitleChange = this.onTitleChange.bind(this); 20 | this.onContentChange = this.onContentChange.bind(this); 21 | this.onTagsChange = this.onTagsChange.bind(this); 22 | this.onSave = this.onSave.bind(this); 23 | } 24 | 25 | 26 | onTitleChange(event) { 27 | const title = event.target.value; 28 | 29 | this.validateTitle(title); 30 | 31 | this.setState({ title: title }); 32 | } 33 | 34 | 35 | onContentChange(event) { 36 | const content = event.target.value; 37 | 38 | this.validateContent(content); 39 | 40 | this.setState({ content: content }); 41 | } 42 | 43 | 44 | onTagsChange(event) { 45 | const tags = event.target.value; 46 | 47 | if (this.validateTags(tags)) { 48 | this.setState({ tags: tags.split(',')}); 49 | } 50 | } 51 | 52 | 53 | onSave(event) { 54 | event.preventDefault(); 55 | 56 | if (this.state.validationErrors && this.state.validationErrors.length === 0) { 57 | const { title, content } = this.state; 58 | 59 | if (this.validateTitle(title) && this.validateContent(content)) { 60 | this.props.onSaveNote({ 61 | id: this.state.id, 62 | title: this.state.title, 63 | content: this.state.content, 64 | tags: this.state.tags 65 | }); 66 | } 67 | } 68 | } 69 | 70 | 71 | validateTitle(title) { 72 | const message = 'Title is required'; 73 | 74 | if (title === '') { 75 | this.addValidationError(message); 76 | return false; 77 | } else { 78 | this.removeValidationError(message); 79 | return true; 80 | } 81 | } 82 | 83 | 84 | validateContent(content) { 85 | const message = 'Content is required'; 86 | 87 | if (content === '') { 88 | this.addValidationError(message); 89 | return false; 90 | } else { 91 | this.removeValidationError(message); 92 | return true; 93 | } 94 | } 95 | 96 | 97 | validateTags(tags) { 98 | const message = 'Tags must be a comma separated list'; 99 | 100 | if (tags !== '') { 101 | var regex = new RegExp(/^([\w]+[\s]*[,]?[\s]*)+$/); 102 | 103 | if (!regex.test(tags)) { 104 | this.addValidationError(message); 105 | return false; 106 | } else { 107 | this.removeValidationError(message); 108 | return true; 109 | } 110 | } else { 111 | this.removeValidationError(message); 112 | } 113 | } 114 | 115 | 116 | addValidationError(message) { 117 | this.setState((previousState) => { 118 | const validationErrors = [...previousState.validationErrors]; 119 | validationErrors.push({message}); 120 | return { 121 | validationErrors: validationErrors 122 | }; 123 | }); 124 | } 125 | 126 | 127 | removeValidationError(message) { 128 | this.setState((previousState) => { 129 | const validationErrors = previousState 130 | .validationErrors 131 | .filter(error => error.message !== message); 132 | 133 | return { 134 | validationErrors: validationErrors 135 | }; 136 | }); 137 | } 138 | 139 | 140 | render() { 141 | 142 | const validationErrorSummary = this.state.validationErrors.map(error => 143 |
144 | {error.message} 145 | 148 |
149 | ); 150 | 151 | return ( 152 |
153 |
154 | Edit Note 155 | 156 | 157 | 158 |
159 | {validationErrorSummary} 160 |
161 |
162 | 163 | 164 |
165 |
166 | 167 | 168 |
169 |
170 | 171 | 172 |
173 |
174 |
175 | 178 |
179 |
180 | 185 |
186 |
187 |
188 |
189 | ); 190 | } 191 | } 192 | 193 | EditNoteForm.propTypes = { 194 | note: PropTypes.object, 195 | onCloseModal: PropTypes.func, 196 | onSaveNote: PropTypes.func 197 | }; 198 | 199 | export default EditNoteForm; -------------------------------------------------------------------------------- /Client/components/Notes/NoteManager.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Modal from 'react-modal'; 3 | import AddNoteForm from './AddNoteForm'; 4 | import EditNoteForm from './EditNoteForm'; 5 | import NoteTable from './NoteTable'; 6 | import ControlPanel from './ControlPanel'; 7 | const NoteService = require('../../services/note-service'); 8 | 9 | class NoteManager extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | notes: [], 15 | selectedNote: null, 16 | isAddNoteModalOpen: false, 17 | isEditNoteModalOpen: false 18 | }; 19 | 20 | 21 | this.handleOnAddNote = this.handleOnAddNote.bind(this); 22 | this.handleOnEditNote = this.handleOnEditNote.bind(this); 23 | this.handleOnDeleteNote = this.handleOnDeleteNote.bind(this); 24 | this.handleOnFindNotes = this.handleOnFindNotes.bind(this); 25 | 26 | this.handleOpenAddNoteModal = this.handleOpenAddNoteModal.bind(this); 27 | this.handleOnCloseAddNoteModal = this.handleOnCloseAddNoteModal.bind(this); 28 | 29 | this.handleOpenEditNoteModal = this.handleOpenEditNoteModal.bind(this); 30 | this.handleOnCloseEditNoteModal = this.handleOnCloseEditNoteModal.bind(this); 31 | } 32 | 33 | 34 | componentDidMount() { 35 | this.listNotes(); 36 | } 37 | 38 | 39 | listNotes() { 40 | NoteService 41 | .listNotes() 42 | .then(notes => { 43 | this.setState({notes}); 44 | return; 45 | }) 46 | .catch(error => { 47 | console.log(error); 48 | return; 49 | }); 50 | } 51 | 52 | 53 | handleOnDeleteNote(noteId) { 54 | 55 | if (noteId < 1) { 56 | throw Error('Cannot remove note. Invalid note id specified'); 57 | } 58 | 59 | const confirmation = confirm('Are you sure you wish to remove note?'); 60 | 61 | if (confirmation) { 62 | NoteService 63 | .removeNote(noteId) 64 | .then(() => { 65 | NoteService 66 | .listNotes() 67 | .then(notes => { 68 | this.setState({notes}); 69 | return; 70 | }) 71 | .catch(error => { 72 | console.log(error); 73 | return; 74 | }); 75 | }) 76 | .catch(error => { 77 | console.log(error); 78 | return; 79 | }); 80 | } 81 | } 82 | 83 | 84 | handleOnFindNotes(title) { 85 | 86 | if (!title || title === '') { 87 | this.listNotes(); 88 | return; 89 | } 90 | 91 | NoteService 92 | .findNotesByTitle(title) 93 | .then(notes => { 94 | if (!notes) { 95 | notes = []; 96 | } 97 | this.setState({notes}); 98 | return; 99 | }) 100 | .catch(error => { 101 | console.log(error); 102 | return; 103 | }); 104 | } 105 | 106 | 107 | handleOnAddNote(note) { 108 | 109 | this.setState({ isAddNoteModalOpen: false }); 110 | 111 | const { title, content, tags } = note; 112 | 113 | if (!title || title.length === 0) { 114 | throw Error('Title is required'); 115 | } 116 | 117 | if (!content || content.length === 0) { 118 | throw Error('Content is required'); 119 | } 120 | 121 | if (!Array.isArray(tags)) { 122 | throw Error('Tags must be an array'); 123 | } 124 | 125 | NoteService 126 | .addNote(title, content, tags) 127 | .then(newNote => { 128 | NoteService 129 | .listNotes() 130 | .then(notes => { 131 | notes.forEach(n => n.id === newNote.id ? n.isNew = 'true' : n.isNew = undefined); 132 | this.setState({notes}); 133 | }) 134 | .catch(error => console.log(error)); 135 | }) 136 | .catch(error => { 137 | console.log(error); 138 | }); 139 | } 140 | 141 | 142 | handleOnCloseAddNoteModal() { 143 | this.setState({isAddNoteModalOpen: false}); 144 | } 145 | 146 | 147 | handleOpenAddNoteModal() { 148 | this.setState({isAddNoteModalOpen: true}); 149 | } 150 | 151 | 152 | handleOnCloseEditNoteModal() { 153 | this.setState({isEditNoteModalOpen: false}); 154 | } 155 | 156 | 157 | handleOpenEditNoteModal(noteId) { 158 | 159 | if (!noteId || noteId < 1) { 160 | throw Error('Cannot edit note. Invalid note id specified.'); 161 | } 162 | 163 | NoteService 164 | .findNote(noteId) 165 | .then(note => { 166 | this.setState({selectedNote: note}); 167 | this.setState({isEditNoteModalOpen: true}); 168 | return; 169 | }) 170 | .catch(error => { 171 | console.log(error); 172 | return; 173 | }); 174 | } 175 | 176 | 177 | handleOnEditNote(note) { 178 | this.setState({ isEditNoteModalOpen: false }); 179 | 180 | const { title, content, tags } = note; 181 | 182 | if (!title || title.length === 0) { 183 | throw Error('Title is required'); 184 | } 185 | 186 | if (!content || content.length === 0) { 187 | throw Error('Content is required'); 188 | } 189 | 190 | if (!Array.isArray(tags)) { 191 | throw Error('Tags must be an array'); 192 | } 193 | 194 | NoteService 195 | .updateNote(note) 196 | .then(() => { 197 | NoteService 198 | .listNotes() 199 | .then(notes => { 200 | notes.forEach(n => n.id === note.id ? n.isNew = 'true' : n.isNew = undefined); 201 | this.setState({notes}); 202 | }) 203 | .catch(error => console.log(error)); 204 | }) 205 | .catch(error => { 206 | console.log(error); 207 | }); 208 | } 209 | 210 | 211 | render() { 212 | return ( 213 |
214 | 215 | 216 | 217 | 218 | 219 | 220 |
221 | 222 |
223 | 224 |
225 | ); 226 | } 227 | } 228 | 229 | export default NoteManager; -------------------------------------------------------------------------------- /Client/components/Notes/NoteTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const NoteTable = (props) => { 5 | const notes = props.notes; 6 | 7 | const noteRows = notes.map(note => { 8 | 9 | let classes = `small ${!!note.isNew ? 'table-success' : ''}`; 10 | 11 | return ( 12 | 13 | 14 |
15 | props.onOpenEditNoteModal(note.id)}> 16 | 17 | 18 | props.onDeleteNote(note.id)}> 19 | 20 | 21 |
22 | 23 | {note.title} 24 | 25 | 26 | {note.content} 27 | 28 | 29 | {`${new Date(note.updatedDate).toISOString().slice(0, 10)} ${new Date(note.updatedDate).toISOString().slice(11, 16)}`} 30 | 31 | ); 32 | }); 33 | 34 | return ( 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {noteRows} 47 | 48 |
TitleContentUpdated Date
49 |
50 | ); 51 | }; 52 | 53 | NoteTable.propTypes = { 54 | notes: PropTypes.array, 55 | onDeleteNote: PropTypes.func, 56 | onOpenEditNoteModal: PropTypes.func 57 | }; 58 | 59 | export default NoteTable; -------------------------------------------------------------------------------- /Client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drminnaar/noteworx-react-mongodb/df5b9265f53da2630b0fbaab377a407047c96f4b/Client/favicon.ico -------------------------------------------------------------------------------- /Client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Starter 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import AppComponent from './components/App'; 4 | import './styles/app.scss'; 5 | 6 | ReactDOM.render(, document.getElementById('app')); -------------------------------------------------------------------------------- /Client/services/note-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // package references 5 | 6 | 7 | import * as axios from 'axios'; 8 | 9 | 10 | // db options 11 | 12 | 13 | const baseApiUrl = 'http://localhost:8000/api'; 14 | 15 | 16 | // add note 17 | 18 | const addNote = (title, content, tags = []) => { 19 | 20 | return new Promise((resolve, reject) => { 21 | axios 22 | .post(`${baseApiUrl}/notes`, { 23 | 'title': title, 24 | 'content': content, 25 | 'tags': tags.join() }) 26 | .then((result) => { 27 | resolve(result.data); 28 | }) 29 | .catch(error => { 30 | console.log(error); 31 | reject(error.message); 32 | }); 33 | 34 | }); 35 | 36 | }; 37 | 38 | 39 | // find notes 40 | 41 | 42 | const findNote = (id) => { 43 | 44 | return new Promise((resolve, reject) => { 45 | axios 46 | .get(`${baseApiUrl}/notes/${id}`) 47 | .then(response => { 48 | resolve(response.data); 49 | return; 50 | }) 51 | .catch(error => { 52 | reject(error.message); 53 | return; 54 | }); 55 | }); 56 | 57 | }; 58 | 59 | 60 | const findNotesByTitle = (title) => { 61 | 62 | return new Promise((resolve, reject) => { 63 | axios 64 | .get(`${baseApiUrl}/notes?title=${title}`) 65 | .then(response => { 66 | resolve(response.data); 67 | return; 68 | }) 69 | .catch(error => { 70 | reject(error.message); 71 | return; 72 | }); 73 | }); 74 | 75 | }; 76 | 77 | const listNotes = () => { 78 | 79 | return new Promise((resolve, reject) => { 80 | axios 81 | .get(`${baseApiUrl}/notes`) 82 | .then(response => { 83 | resolve(response.data); 84 | return; 85 | }) 86 | .catch(error => { 87 | reject(error.message); 88 | return; 89 | }); 90 | }); 91 | 92 | }; 93 | 94 | 95 | // remove note 96 | 97 | 98 | const removeNote = (id) => { 99 | 100 | return new Promise((resolve, reject) => { 101 | axios 102 | .delete(`${baseApiUrl}/notes/${id}`) 103 | .then(() => { 104 | resolve(); 105 | return; 106 | }) 107 | .catch(error => { 108 | reject(error.message); 109 | return; 110 | }); 111 | }); 112 | 113 | }; 114 | 115 | 116 | // update note 117 | 118 | 119 | const updateNote = (note) => { 120 | return new Promise((resolve, reject) => { 121 | axios 122 | .put(`${baseApiUrl}/notes`, {note}) 123 | .then(() => { 124 | resolve(); 125 | return; 126 | }) 127 | .catch(error => { 128 | reject(error.message); 129 | return; 130 | }); 131 | }); 132 | 133 | }; 134 | 135 | 136 | // exports 137 | 138 | 139 | module.exports = { 140 | 'addNote': addNote, 141 | 'findNote': findNote, 142 | 'findNotesByTitle': findNotesByTitle, 143 | 'listNotes': listNotes, 144 | 'removeNote': removeNote, 145 | 'updateNote': updateNote 146 | }; -------------------------------------------------------------------------------- /Client/styles/_base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #FFF; 3 | font-family: $font-family; 4 | } 5 | 6 | a { 7 | cursor: pointer; 8 | } 9 | 10 | .text-react-blue { 11 | color: $react-blue; 12 | } 13 | 14 | .bg-react-black { 15 | background-color: $react-black; 16 | } -------------------------------------------------------------------------------- /Client/styles/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border-bottom: $react-blue solid 2px; 3 | } 4 | 5 | .btn { 6 | cursor: pointer; 7 | } 8 | 9 | .close { 10 | cursor: pointer; 11 | } -------------------------------------------------------------------------------- /Client/styles/_react-modal.scss: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay { 2 | z-index: 1000; 3 | } -------------------------------------------------------------------------------- /Client/styles/_settings.scss: -------------------------------------------------------------------------------- 1 | $font-family: Open Sans, Helvetica, Arial, sans-serif; 2 | 3 | $white-smoke: #F5F5F5; 4 | $react-black: #2D3037; 5 | $react-blue: #73D8FA; -------------------------------------------------------------------------------- /Client/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import '_settings.scss'; 2 | @import '_base.scss'; 3 | @import '_bootstrap.scss'; 4 | @import '_react-modal.scss'; 5 | @import './components/_header.scss'; -------------------------------------------------------------------------------- /Client/styles/components/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | min-height: 100px; 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NoteWorx README 2 | 3 | A basic note application that uses a [ReactJS] frontend to capture and manage notes, an api written in [ExpressJS], and [MongoDB] to store notes. 4 | 5 | ## Features 6 | 7 | * Add a note 8 | * Edit a note 9 | * Remove a note 10 | * List all notes 11 | * Find note by title 12 | 13 | ## High Level Design 14 | 15 | ![noteworx-react-mongodb](https://user-images.githubusercontent.com/33935506/33653545-0f6d5828-da76-11e7-8a3c-a72bc732ad4f.PNG) 16 | 17 | ## Screenshots 18 | 19 | ![noteworx-react-mongodb-1](https://user-images.githubusercontent.com/33935506/33796539-bed2352e-dcfe-11e7-8392-d1b621af3258.PNG) 20 | 21 | ![noteworx-react-mongodb-2](https://user-images.githubusercontent.com/33935506/33796540-bf33cd7a-dcfe-11e7-9074-38e9e9be28ed.PNG) 22 | 23 | ![noteworx-react-mongodb-3](https://user-images.githubusercontent.com/33935506/33796541-bf65a804-dcfe-11e7-86cb-62948ac5c890.PNG) 24 | 25 | ![noteworx-react-mongodb-4](https://user-images.githubusercontent.com/33935506/33796542-bf9bbcfa-dcfe-11e7-8be5-bb23f3521edb.PNG) 26 | 27 | ![noteworx-react-mongodb-5](https://user-images.githubusercontent.com/33935506/33796543-bfcff36c-dcfe-11e7-94b4-9e3ebcfe49ad.PNG) 28 | 29 | ![noteworx-react-mongodb-6](https://user-images.githubusercontent.com/33935506/33796544-c0036a3a-dcfe-11e7-9775-f9dadfb2b9d1.PNG) 30 | 31 | --- 32 | 33 | ## Developed With 34 | 35 | * [NodeJS] - Javascript runtime 36 | * [MongoDB] - NoSQL database 37 | * [Docker] - Used to host MongoDB instance (Not manadatory. See other options below) 38 | * [ExpressJS] - A web application framework for Node.js 39 | * [ReactJS] - Javascript library for building user interfaces 40 | * [Bootstrap v4.0.0-beta.2] - Build responsive, mobile-first projects 41 | * [Webpack] - Javascript module bundler 42 | 43 | --- 44 | 45 | ## Related Projects 46 | 47 | * [noteworx-cli-fs] 48 | 49 | A basic note application that uses a CLI (Command Line Interface) frontend to capture and manage notes, and a file system to store notes 50 | 51 | * [noteworx-cli-mongodb] 52 | 53 | A basic note application that uses a CLI (Command Line Interface) frontend to capture and manage notes, and mongodb to store notes 54 | 55 | * [noteworx-cli-mongoose] 56 | 57 | A basic note application that uses a CLI (Command Line Interface) frontend to capture and manage notes, Mongoose ODM to manage MongoDB interaction, and mongodb to store notes 58 | 59 | * [noteworx-cli-couchbase] 60 | 61 | A basic note application that uses a CLI (Command Line Interface) frontend to capture and manage notes, and couchbase as a data store 62 | 63 | * [noteworx-cli-express-mongodb] 64 | 65 | A basic note application that uses a CLI (Command Line Interface) frontend to capture and manage notes, an express note management API built using Express, and Mongodb to store notes 66 | 67 | * [noteworx-expressui-mongodb] 68 | 69 | A basic note application that uses an Express frontend to capture and manage notes, and mongodb to store notes 70 | 71 | --- 72 | 73 | ## Getting Started 74 | 75 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 76 | 77 | ### Prerequisites 78 | 79 | The following software is required to be installed on your system: 80 | 81 | * NodeJS 82 | 83 | The following version of Node and Npm are required: 84 | 85 | * Node 8.x 86 | * Npm 3.x 87 | 88 | Type the following commands in the terminal to verify your node and npm versions 89 | 90 | ```bash 91 | node -v 92 | npm -v 93 | ``` 94 | 95 | * MongoDB 96 | 97 | MongoDB 3.x is required 98 | 99 | Type the following command to verify that MongoDB is running on your local machine 100 | 101 | ```bash 102 | mongo -version 103 | ``` 104 | 105 | See alternative MongoDB options below 106 | 107 | ### MongoDB Setup 108 | 109 | A running instance of MongoDB is required. Alternatively use a hosted MongoDB from [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) or [MLab](https://mlab.com/) 110 | 111 | One of the 3 options below is recommended to get up and running with MongoDB: 112 | 113 | * Install and host locally 114 | * Install and host in Docker 115 | * Register for third party MongoDB hosting 116 | * Register for and use MongoDB Atlas (Database As A Service) 117 | * Register for and use MLab (Database As A Service) 118 | 119 | #### Install and Host MongoDB Locally 120 | 121 | Installing MongoDB is relatively straight forward. There are currently 3 platform (Windows, Linux, OSX) releases available and can be found here 122 | 123 | For more specific installation instructions, please see the following links: 124 | 125 | * [Install MongoDB On Linux](https://docs.mongodb.com/v3.0/administration/install-on-linux/) 126 | 127 | * [Install MongoDB On Windows](https://docs.mongodb.com/v3.0/tutorial/install-mongodb-on-windows/) 128 | 129 | * [Install MongoDB On OSX](https://docs.mongodb.com/v3.0/tutorial/install-mongodb-on-os-x/) 130 | 131 | #### Install And Host Using Docker 132 | 133 | ##### Run MongoDB Using Named Volume 134 | 135 | To run a new MongoDB container, execute the following command from the CLI: 136 | 137 | ```docker 138 | docker run --rm --name mongo-dev -p 127.0.0.1:27017:27017 -v mongo-dev-db:/data/db -d mongo 139 | ``` 140 | 141 | CLI Command | Description 142 | --- | --- 143 | --rm | remove container when stopped 144 | --name mongo-dev | give container a custom name 145 | -p | map host port to container port 146 | -v mongo-dev-db/data/db | map the container volume 'data/db' to a custom name 'mongo-dev-db' 147 | -d mongo | run mongo container as a daemon in the background 148 | 149 | ##### Run MongoDB Using Bind Mount 150 | 151 | ```bash 152 | cd 153 | mkdir -p mongodb/data/db 154 | docker run --rm --name mongo-dev -p 127.0.0.1:27017:27017 -v ~/mongodb/data/db:/data/db -d mongo 155 | ``` 156 | 157 | CLI Command | Description 158 | --- | --- 159 | --rm | remove container when stopped 160 | --name mongo-dev | give container a custom name 161 | -p | map host port to container port 162 | -v ~/mongodb/data/db/data/db | map the container volume 'data/db' to a bind mount '~/mongodb/data/db' 163 | -d mongo | run mongo container as a daemon in the background 164 | 165 | #### Third Party Hosting 166 | 167 | ##### MongoDB Atlas 168 | 169 | [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) is basically a database as a service and is hosted in the cloud. That means that you don't need to install or setup anything to start using MongoDB. 170 | 171 | You can get started for free by registering [here](https://www.mongodb.com/cloud/atlas). The free tier entitles you to 512MB storage. 172 | 173 | Please review the documentation [here](https://docs.atlas.mongodb.com/) 174 | 175 | ##### MLab 176 | 177 | [MLab](https://mlab.com/) also provides MongoDB cloud hosting in the form of database as a service. Once again there is no installation or setup required. 178 | 179 | To get started, signup for free account [here](https://mlab.com/signup/). The free tier entitles you to 500MB storage. 180 | 181 | Please review the documentation [here](https://docs.mlab.com/) 182 | 183 | ### Install 184 | 185 | Follow the following steps to get development environment running. 186 | 187 | 1. Clone 'noteworx-react-mongodb' repository from GitHub 188 | 189 | ```bash 190 | git clone https://github.com/drminnaar/noteworx-react-mongodb.git 191 | ``` 192 | 193 | _or using ssh_ 194 | 195 | ```bash 196 | git clone git@github.com:drminnaar/noteworx-react-mongodb.git 197 | ``` 198 | 199 | 1. Install node modules 200 | 201 | ```bash 202 | cd noteworx-react-mongodb 203 | npm install 204 | ``` 205 | 206 | ### Build 207 | 208 | There are 2 build options: 209 | 210 | * Build 211 | 212 | ```javascript 213 | npm run build 214 | ``` 215 | 216 | * Build with watch enabled 217 | 218 | ```javascript 219 | npm run build:watch 220 | ``` 221 | 222 | ### Run ESlint 223 | 224 | * Lint project using ESLint 225 | 226 | ```bash 227 | npm run lint 228 | ``` 229 | 230 | * Lint project using ESLint, and autofix 231 | 232 | ```bash 233 | npm run lint:fix 234 | ``` 235 | 236 | ### Run API Server 237 | 238 | All the API (server) code is found in the *'Server'* folder. 239 | 240 | Before running the React application, the API needs to be started. 241 | 242 | The following command wil start server and host api at http://localhost:8000/api 243 | 244 | ```javascript 245 | npm run serve:api 246 | ``` 247 | 248 | ### Run React App 249 | 250 | * Run Dev Server 251 | 252 | Start React usinf React dev server 253 | 254 | ```javascript 255 | npm run serve:dev 256 | ``` 257 | 258 | --- 259 | 260 | ## Versioning 261 | 262 | I use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/drminnaar/noteworx-react-mongodb/tags). 263 | 264 | ## Authors 265 | 266 | * **Douglas Minnaar** - *Initial work* - [drminnaar](https://github.com/drminnaar) 267 | 268 | [NodeJS]: https://nodejs.org 269 | [MongoDB]: https://www.mongodb.com 270 | [ExpressJS]: http://expressjs.com 271 | [Docker]: https://www.docker.com 272 | [ReactJS]: http://reactjs.org 273 | [Bootstrap v4.0.0-beta.2]: https://getbootstrap.com 274 | [Webpack]: https://webpack.js.org 275 | 276 | [noteworx-cli-fs]: https://github.com/drminnaar/noteworx-cli-fs 277 | [noteworx-cli-mongodb]: https://github.com/drminnaar/noteworx-cli-mongodb 278 | [noteworx-cli-mongoose]: https://github.com/drminnaar/noteworx-cli-mongoose 279 | [noteworx-cli-couchbase]: https://github.com/drminnaar/noteworx-cli-couchbase 280 | [noteworx-cli-express-mongodb]: https://github.com/drminnaar/noteworx-cli-express-mongodb 281 | [noteworx-expressui-mongodb]: https://github.com/drminnaar/noteworx-expressui-mongodb -------------------------------------------------------------------------------- /Server/DataAccess/DbConnection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MongoClient = require('mongodb').MongoClient; 4 | 5 | class DbConnection { 6 | 7 | constructor(connectionUri) { 8 | this.Uri = connectionUri; 9 | } 10 | 11 | open() { 12 | return new Promise((resolve, reject) => { 13 | MongoClient.connect(this.Uri) 14 | .then(db => { 15 | this.Db = db; 16 | resolve(); 17 | }) 18 | .catch(error => { 19 | console.log(error); 20 | reject(); 21 | }); 22 | }); 23 | } 24 | 25 | close() { 26 | if (this.Db) { 27 | this.Db.close().catch(error => console.log(error)); 28 | } 29 | } 30 | } 31 | 32 | module.exports = DbConnection; -------------------------------------------------------------------------------- /Server/DataAccess/NoteRepository.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ObjectID = require('mongodb').ObjectID; 4 | const DbConnection = require('./DbConnection'); 5 | 6 | const collection = 'notes'; 7 | 8 | const connect = () => new DbConnection('mongodb://127.0.0.1:27017/noteworx'); 9 | 10 | const filters = { 11 | id: (id) => { 12 | return { _id: new ObjectID(id) }; 13 | }, 14 | tag: (tag) => { 15 | return { tags: { $regex: new RegExp(tag, 'i') } }; 16 | }, 17 | title: (title) => { 18 | return { 'title': { $regex: new RegExp(title, 'i') } }; 19 | } 20 | }; 21 | 22 | class NoteRepository { 23 | 24 | addNote(note) { 25 | const connection = connect(); 26 | 27 | return new Promise((resolve, reject) => { 28 | connection 29 | .open() 30 | .then(() => { 31 | connection.Db.collection(collection) 32 | .findOne(filters.title(note.title)) 33 | .then(noteData => { 34 | if (noteData) { 35 | connection.close(); 36 | reject(Error('Note already exists')); 37 | } else { 38 | connection.Db 39 | .collection(collection) 40 | .insertOne(note) 41 | .then(result => { 42 | connection.close(); 43 | resolve({ id: result.insertedId }); 44 | }) 45 | .catch(error => { 46 | connection.close(); 47 | reject(error); 48 | }); 49 | } 50 | }) 51 | .catch(error => { 52 | connection.close(); 53 | reject(error); 54 | }); 55 | }) 56 | .catch(error => { 57 | reject(error); 58 | connection.close(); 59 | }); 60 | }); 61 | } 62 | 63 | 64 | findNoteById(id) { 65 | const connection = connect(); 66 | 67 | return new Promise((resolve, reject) => { 68 | connection 69 | .open() 70 | .then(() => { 71 | connection.Db.collection(collection) 72 | .findOne(filters.id(id)) 73 | .then(note => { 74 | resolve(note); 75 | connection.close(); 76 | }) 77 | .catch(error => { 78 | reject(error); 79 | connection.close(); 80 | }); 81 | }) 82 | .catch(error => { 83 | reject(error); 84 | connection.close(); 85 | }); 86 | }); 87 | } 88 | 89 | 90 | findNotesByTag(tag) { 91 | const connection = connect(); 92 | 93 | return new Promise((resolve, reject) => { 94 | connection 95 | .open() 96 | .then(() => { 97 | connection.Db.collection(collection) 98 | .find(filters.tag(tag)) 99 | .sort({ updated_date: -1}) 100 | .toArray() 101 | .then(notes => { 102 | resolve(notes); 103 | connection.close(); 104 | }) 105 | .catch(error => { 106 | reject(error); 107 | connection.close(); 108 | }); 109 | }) 110 | .catch(error => { 111 | reject(error); 112 | connection.close(); 113 | }); 114 | }); 115 | } 116 | 117 | 118 | findNotesByTitle(title) { 119 | const connection = connect(); 120 | 121 | return new Promise((resolve, reject) => { 122 | connection 123 | .open() 124 | .then(() => { 125 | connection.Db.collection(collection) 126 | .find(filters.title(title)) 127 | .sort({ updated_date: -1}) 128 | .toArray() 129 | .then(notes => { 130 | resolve(notes); 131 | connection.close(); 132 | }) 133 | .catch(error => { 134 | reject(error); 135 | connection.close(); 136 | }); 137 | }) 138 | .catch(error => { 139 | reject(error); 140 | connection.close(); 141 | }); 142 | }); 143 | } 144 | 145 | 146 | listNotes() { 147 | const connection = connect(); 148 | 149 | return new Promise((resolve, reject) => { 150 | connection 151 | .open() 152 | .then(() => { 153 | connection.Db.collection(collection) 154 | .find() 155 | .sort({ updated_date: -1}) 156 | .toArray() 157 | .then(notes => { 158 | resolve(notes); 159 | connection.close(); 160 | }) 161 | .catch(error => { 162 | reject(error); 163 | connection.close(); 164 | }); 165 | }) 166 | .catch(error => reject(error)); 167 | }); 168 | } 169 | 170 | 171 | removeNote(id) { 172 | const connection = connect(); 173 | 174 | return new Promise((resolve, reject) => { 175 | connection 176 | .open() 177 | .then(() => { 178 | connection.Db 179 | .collection(collection) 180 | .findOneAndDelete(filters.id(id)) 181 | .then(() => { 182 | resolve(); 183 | connection.close(); 184 | }) 185 | .catch(error => { 186 | reject(error); 187 | connection.close(); 188 | }); 189 | }) 190 | .catch(error => { 191 | resolve(error); 192 | connection.close(); 193 | }); 194 | }); 195 | } 196 | 197 | 198 | tagNote(id, tags) { 199 | const connection = connect(); 200 | 201 | const update = { 202 | $addToSet: { 203 | tags: { 204 | $each: tags 205 | } 206 | } 207 | }; 208 | 209 | return new Promise((resolve, reject) => { 210 | connection 211 | .open() 212 | .then(() => { 213 | connection.Db 214 | .collection(collection) 215 | .findOneAndUpdate( 216 | filters.id(id), 217 | update 218 | ) 219 | .then(() => { 220 | resolve(); 221 | connection.close(); 222 | }) 223 | .catch(error => { 224 | reject(error); 225 | connection.close(); 226 | }); 227 | }) 228 | .catch(error => { 229 | reject(error); 230 | connection.close(); 231 | }); 232 | }); 233 | } 234 | 235 | 236 | updateNote(id, note) { 237 | const connection = connect(); 238 | 239 | return new Promise((resolve, reject) => { 240 | connection 241 | .open() 242 | .then(() => { 243 | connection.Db 244 | .collection(collection) 245 | .update( 246 | filters.id(id), 247 | { 248 | $set: { 249 | title: note.title, 250 | content: note.content, 251 | tags: note.tags, 252 | updated_date: note.updated_date 253 | } 254 | }) 255 | .then(() => { 256 | resolve(); 257 | connection.close(); 258 | }) 259 | .catch(error => { 260 | reject(error); 261 | connection.close(); 262 | }); 263 | }) 264 | .catch(error => { 265 | resolve(error); 266 | connection.close(); 267 | }); 268 | }); 269 | } 270 | } 271 | 272 | module.exports = NoteRepository; -------------------------------------------------------------------------------- /Server/Services/NoteManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const NoteRepository = require('../DataAccess/NoteRepository'); 4 | const assert = require('assert'); 5 | const noteRepository = new NoteRepository(); 6 | 7 | const mapToNoteDto = (note) => { 8 | assert(note, 'Note is required'); 9 | 10 | return { 11 | id: note._id, 12 | title: note.title, 13 | content: note.content, 14 | tags: note.tags, 15 | createdDate: note.created_date, 16 | updatedDate: note.updated_date 17 | }; 18 | }; 19 | 20 | const createUpdatedNote = (title, content, tags) => { 21 | return { 22 | title: title, 23 | content: content, 24 | tags: !Array.isArray(tags) ? convertTagsCsvToArray(tags) : tags, 25 | updated_date: new Date() 26 | }; 27 | }; 28 | 29 | const createNewNote = (title, content, tags) => { 30 | return { 31 | title: title, 32 | content: content, 33 | tags: convertTagsCsvToArray(tags), 34 | created_date: new Date(), 35 | updated_date: new Date() 36 | }; 37 | }; 38 | 39 | const convertTagsCsvToArray = (tags) => { 40 | 41 | var exp = new RegExp(/^((\w+)((,)?|(,\s)))*$/); 42 | assert(exp.test(tags), 'Invalid list of tags specified'); 43 | 44 | return tags 45 | ? Array.from(new Set(tags.split(',').map(tag => tag.toLowerCase()))) 46 | : []; 47 | }; 48 | 49 | class NoteManager { 50 | 51 | addNote(title, content, tags) { 52 | 53 | assert(title, 'Title is required'); 54 | assert(content, 'Content is required'); 55 | 56 | const note = createNewNote(title, content, tags); 57 | 58 | return new Promise((resolve, reject) => { 59 | noteRepository 60 | .addNote(note) 61 | .then(result => resolve(result.id)) 62 | .catch(error => reject(error)); 63 | }); 64 | } 65 | 66 | 67 | findNoteById(id) { 68 | 69 | assert(id, 'Id is required'); 70 | 71 | return new Promise((resolve, reject) => { 72 | noteRepository 73 | .findNoteById(id) 74 | .then(note => resolve(mapToNoteDto(note))) 75 | .catch(error => reject(error)); 76 | }); 77 | } 78 | 79 | 80 | findNotesByTag(tag) { 81 | 82 | assert(tag, 'Tag is required'); 83 | 84 | return new Promise((resolve, reject) => { 85 | noteRepository 86 | .findNotesByTag(tag) 87 | .then(notes => resolve(notes.map(note => mapToNoteDto(note)))) 88 | .catch(error => reject(error)); 89 | }); 90 | } 91 | 92 | 93 | findNotesByTitle(title) { 94 | 95 | assert(title, 'Title is required'); 96 | 97 | return new Promise((resolve, reject) => { 98 | noteRepository 99 | .findNotesByTitle(title) 100 | .then(notes => resolve(notes.map(note => mapToNoteDto(note)))) 101 | .catch(error => reject(error)); 102 | }); 103 | } 104 | 105 | 106 | listNotes() { 107 | return new Promise((resolve, reject) => { 108 | noteRepository 109 | .listNotes() 110 | .then(notes => resolve(notes.map(note => mapToNoteDto(note)))) 111 | .catch(error => reject(error)); 112 | }); 113 | } 114 | 115 | 116 | removeNote(id) { 117 | 118 | assert(id, 'Id is required'); 119 | 120 | return new Promise((resolve, reject) => { 121 | noteRepository 122 | .removeNote(id) 123 | .then(() => resolve()) 124 | .catch(error => reject(error)); 125 | }); 126 | } 127 | 128 | 129 | tagNote(id, tags) { 130 | 131 | assert(id, 'Id is required'); 132 | assert(tags, 'Tags are required'); 133 | 134 | var exp = new RegExp(/^([\w]+[,]?)*$/); 135 | assert(exp.test(tags), 'Invalid list of tags specified'); 136 | 137 | const uniqueTags = tags ? Array.from(new Set(tags.split(',').map(tag => tag.toLowerCase()))) : []; 138 | 139 | return new Promise((resolve, reject) => { 140 | noteRepository 141 | .tagNote(id, uniqueTags) 142 | .then(() => resolve()) 143 | .catch(error => reject(error)); 144 | }); 145 | } 146 | 147 | 148 | updateNote(id, title, content, tags) { 149 | assert(id, 'Id is required'); 150 | assert(title, 'Title is required'); 151 | assert(content, 'Content is required'); 152 | 153 | const note = createUpdatedNote(title, content, tags); 154 | 155 | return new Promise((resolve, reject) => { 156 | noteRepository 157 | .updateNote(id, note) 158 | .then(() => resolve()) 159 | .catch(error => reject(error)); 160 | }); 161 | } 162 | } 163 | 164 | module.exports = NoteManager; -------------------------------------------------------------------------------- /Server/routers/notes-router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // package references 4 | const express = require('express'); 5 | 6 | // app references 7 | const NoteManager = require('../Services/NoteManager'); 8 | 9 | // initialization 10 | const noteManager = new NoteManager(); 11 | 12 | // build router 13 | 14 | const notesRouter = () => { 15 | const router = express.Router(); 16 | 17 | router 18 | .delete('/notes/:id', (request, response) => { 19 | 20 | const { id } = request.params; 21 | 22 | if (!id) { 23 | response.status(400).send('Title is required'); 24 | } else { 25 | noteManager 26 | .removeNote(id) 27 | .then(() => response.status(200).send('Note deleted')) 28 | .catch(error => { 29 | console.log(error.message); 30 | response.status(500).send(); 31 | }); 32 | } 33 | }) 34 | .get('/notes/:id', (request, response) => { 35 | 36 | const { id } = request.params; 37 | 38 | if (!id) { 39 | response.status(400).send('Id is required'); 40 | } else { 41 | noteManager 42 | .findNoteById(id) 43 | .then(note => response.json(note)) 44 | .catch(error => { 45 | console.log(error.message); 46 | response.status(500).send(); 47 | }); 48 | } 49 | }) 50 | .get('/notes', (request, response) => { 51 | 52 | const { title, tag } = request.query; 53 | 54 | if (title) { 55 | noteManager 56 | .findNotesByTitle(title) 57 | .then(notes => response.json(notes)) 58 | .catch(error => { 59 | console.log(error); 60 | response.status(500).send(); 61 | }); 62 | } else if (tag) { 63 | noteManager 64 | .findNotesByTag(tag) 65 | .then(notes => response.json(notes)) 66 | .catch(error => { 67 | console.log(error); 68 | response.status(500).send(); 69 | }); 70 | } else { 71 | noteManager 72 | .listNotes() 73 | .then(notes => response.json(notes)) 74 | .catch(error => { 75 | console.log(error); 76 | response.status(500).send(); 77 | }); 78 | } 79 | }) 80 | .post('/notes', (request, response) => { 81 | console.log(request.body); 82 | const { title, content, tags } = request.body; 83 | 84 | if (!title) { 85 | response.status(400).send('Title is required'); 86 | } else if (!content) { 87 | response.status(400).send('Content is required'); 88 | } else { 89 | noteManager 90 | .addNote(title, content, tags) 91 | .then(id => response.status(201).send({ id: id })) 92 | .catch(error => { 93 | console.log(error.message); 94 | response.status(500).send(error.message); 95 | }); 96 | } 97 | }) 98 | .put('/notes', (request, response) => { 99 | 100 | const { id, title, content, tags } = request.body.note; 101 | 102 | if (!id) { 103 | response.status(400).send('Id is required'); 104 | } else if (!title) { 105 | response.status(400).send('Title is required'); 106 | } else if (!content) { 107 | response.status(400).send('Content is required'); 108 | } else { 109 | noteManager 110 | .updateNote(id, title, content, tags) 111 | .then(() => response.status(200).send()) 112 | .catch(error => { 113 | console.log(error.message); 114 | response.status(500).send(error.message); 115 | }); 116 | } 117 | }) 118 | .patch('/notes/:id', (request, response) => { 119 | 120 | // not an entirely correct use of patch but convenient 121 | // in terms of providing the 'tag' functionality 122 | 123 | const { id } = request.params; 124 | const { tags } = request.body; 125 | 126 | if (!id) { 127 | response.status(400).send('Id is required'); 128 | } else if (!tags) { 129 | response.status(400).send('Tags is required'); 130 | } else { 131 | noteManager 132 | .tagNote(id, tags) 133 | .then(() => response.status(200).send('Tagged note')) 134 | .catch(error => { 135 | console.log(error.message); 136 | response.status(500).send(); 137 | }); 138 | } 139 | }); 140 | 141 | return router; 142 | }; 143 | 144 | module.exports = notesRouter; -------------------------------------------------------------------------------- /Server/server.js: -------------------------------------------------------------------------------- 1 | // package references 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const morgan = require('morgan'); 5 | const cors = require('cors'); 6 | 7 | // app references 8 | const notesRouter = require('./routers/notes-router'); 9 | 10 | // initialization 11 | const PORT = process.env.PORT || 8000; 12 | 13 | // configure server 14 | 15 | const server = express(); 16 | 17 | server.use(bodyParser.urlencoded({ extended: true })); 18 | server.use(bodyParser.json()); 19 | server.use(cors()); 20 | server.use(morgan('combined')); 21 | server.use('/api', notesRouter(PORT)); 22 | 23 | 24 | // start server 25 | 26 | server.listen(PORT, () => { 27 | console.log(`Listening on port ${PORT} ...`); 28 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noteworx-react-mongodb", 3 | "version": "1.0.0", 4 | "description": "A basic note application that uses React frontend to capture and manage notes, an api written in ExpressJS, and mongodb to store notes", 5 | "scripts": { 6 | "build": "webpack -d", 7 | "build:watch": "webpack --watch", 8 | "lint": "eslint .; exit 0", 9 | "lint:fix": "eslint . --fix", 10 | "serve": "webpack -d && live-server ./Client/public", 11 | "serve:dev": "webpack-dev-server --open", 12 | "serve:api": "node ./Server/server.js", 13 | "test": "echo \"No tests available\" && exit 1" 14 | }, 15 | "license": "MIT", 16 | "babel": { 17 | "presets": [ 18 | "env", 19 | "react" 20 | ] 21 | }, 22 | "devDependencies": { 23 | "babel-cli": "^6.26.0", 24 | "babel-core": "^6.26.0", 25 | "babel-eslint": "^8.0.3", 26 | "babel-loader": "^7.1.2", 27 | "babel-preset-env": "^1.6.1", 28 | "babel-preset-react": "^6.24.1", 29 | "clean-webpack-plugin": "^0.1.17", 30 | "css-loader": "^0.28.7", 31 | "eslint": "^4.12.1", 32 | "eslint-loader": "^1.9.0", 33 | "eslint-plugin-react": "^7.5.1", 34 | "html-webpack-plugin": "^2.30.1", 35 | "live-server": "^1.2.0", 36 | "node-sass": "^4.7.2", 37 | "sass-loader": "^6.0.6", 38 | "style-loader": "^0.19.0", 39 | "url-loader": "^0.6.2", 40 | "webpack": "^3.10.0", 41 | "webpack-dev-server": "^2.9.5" 42 | }, 43 | "dependencies": { 44 | "axios": "^0.17.1", 45 | "body-parser": "^1.18.2", 46 | "cors": "^2.8.4", 47 | "express": "^4.16.2", 48 | "moment": "^2.19.3", 49 | "mongodb": "^2.2.33", 50 | "morgan": "^1.9.0", 51 | "react": "^16.2.0", 52 | "react-dom": "^16.2.0", 53 | "react-modal": "^3.1.7", 54 | "uuid": "^3.1.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CleanWebPackPlugin = require('clean-webpack-plugin'); 3 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './Client/index.js', 7 | output: { 8 | filename: 'bundle.js', 9 | path: path.resolve(__dirname, 'public') 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | enforce: 'pre', 15 | test: /\.js$/, 16 | loader: 'eslint-loader', 17 | options: { 18 | failOnWarning: true, 19 | failOnerror: true 20 | }, 21 | exclude: /node_modules/ 22 | }, 23 | { 24 | test: /\.js$/, 25 | loader: 'babel-loader', 26 | exclude: /node_modules/ 27 | }, 28 | { 29 | test: /\.s?css$/, 30 | use: [ 'style-loader', 'css-loader', 'sass-loader' ], 31 | exclude: /node_modules/ 32 | }, 33 | { 34 | test: /\.svg$/, 35 | loader: 'url-loader', 36 | exclude: /node_modules/ 37 | } 38 | ] 39 | }, 40 | plugins: [ 41 | new CleanWebPackPlugin([ 'public' ], { root: path.resolve(__dirname)}), 42 | new HtmlWebPackPlugin({ 43 | template: './Client/index.html', 44 | favicon: './Client/favicon.ico', 45 | inject: false 46 | }) 47 | ], 48 | devtool: 'cheap-module-eval-source-map', 49 | devServer: { 50 | contentBase: path.resolve(__dirname, 'public'), 51 | compress: true, 52 | port: 9000 53 | } 54 | }; --------------------------------------------------------------------------------