├── .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 |
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 |
153 | {validationErrorSummary}
154 |
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 |
159 | {validationErrorSummary}
160 |
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 |
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 | Title |
41 | Content |
42 | Updated Date |
43 |
44 |
45 |
46 | {noteRows}
47 |
48 |
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 | 
16 |
17 | ## Screenshots
18 |
19 | 
20 |
21 | 
22 |
23 | 
24 |
25 | 
26 |
27 | 
28 |
29 | 
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 | };
--------------------------------------------------------------------------------